diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dcc0ca6004..9ec4846117 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -119,8 +119,6 @@ "extensions": [ "elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality "josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide - "ms-python.black-formatter", // Python code formatter using Black - "ms-python.flake8", // Flake8 linter for Python to enforce code quality "ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support "ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers // "Oracle.oracle-java", // Oracle Java extension with additional features for Java development diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 14f6ca750f..a2e3241c65 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,18 +1,28 @@ -# All PRs must be approved by Frooodle or Ludy87 -* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh +# Review ownership is assigned to teams where possible. +# Teams can only contain org members, so outside collaborators are listed by hand. +# +# @Stirling-Tools/maintainers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/backend-reviewers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/frontend-reviewers - Frooodle, jbrunton96, ConnorYoh, reecebrowne, EthanHealy01 +# @Stirling-Tools/devops-reviewers - Frooodle, jbrunton96, ConnorYoh +# @Stirling-Tools/all - all of the above +# +# Outside collaborators (need Write access to count as owners): @Ludy87 @balazs-szucs + +# Default owners for everything +* @Stirling-Tools/maintainers @Ludy87 # Backend -/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh @balazs-szucs +/app/** @Stirling-Tools/backend-reviewers @Ludy87 @balazs-szucs -#V2 frontend -/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @balazs-szucs -/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 @balazs-szucs +# V2 frontend +/frontend/** @Stirling-Tools/frontend-reviewers @balazs-szucs +/app/core/src/main/resources/static/** @Stirling-Tools/frontend-reviewers @Ludy87 @balazs-szucs -#V2 docker -/docker/backend/** @Frooodle @Ludy87 @DarioGii -/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 -/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 +# V2 docker +/docker/backend/** @Stirling-Tools/devops-reviewers @Ludy87 +/docker/frontend/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87 +/docker/compose/** @Stirling-Tools/frontend-reviewers @Stirling-Tools/devops-reviewers @Ludy87 - -#GHA (All users) -/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 @balazs-szucs +# GHA (all users) +/.github/** @Stirling-Tools/all @Ludy87 @balazs-szucs diff --git a/.github/ISSUE_TEMPLATE/1-bug.yml b/.github/ISSUE_TEMPLATE/1-bug.yml index 4a513956d3..fcd0f05613 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yml +++ b/.github/ISSUE_TEMPLATE/1-bug.yml @@ -98,6 +98,13 @@ body: description: | If you have any additional information that might help us understand and resolve the issue, provide it here. + - type: textarea + id: sample-files + attributes: + label: Sample Files + description: | + If possible, attach the PDF or other input files needed to reproduce the issue. Remove any sensitive information before sharing. + - type: markdown attributes: value: | diff --git a/.github/ISSUE_TEMPLATE/2-feature.yml b/.github/ISSUE_TEMPLATE/2-feature.yml index 810f3fcd8c..0463ff7342 100644 --- a/.github/ISSUE_TEMPLATE/2-feature.yml +++ b/.github/ISSUE_TEMPLATE/2-feature.yml @@ -67,6 +67,13 @@ body: description: | If you have any additional information, comments, or resources you think would support or be relevant to your feature request, include them here. + - type: textarea + id: sample-files + attributes: + label: Example Files + description: | + If the feature request depends on specific PDFs or other example files, attach them here when available. Remove any sensitive information before sharing. + - type: checkboxes id: search-confirmation attributes: diff --git a/.github/aur/stirling-pdf-desktop/PKGBUILD b/.github/aur/stirling-pdf-desktop/PKGBUILD index fb6a99cfca..c47c8e3ee7 100644 --- a/.github/aur/stirling-pdf-desktop/PKGBUILD +++ b/.github/aur/stirling-pdf-desktop/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-desktop -pkgver=2.14.2 +pkgver=2.14.3 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)" arch=('x86_64') diff --git a/.github/aur/stirling-pdf-server-bin/PKGBUILD b/.github/aur/stirling-pdf-server-bin/PKGBUILD index 70bcee0423..d6159ddce3 100644 --- a/.github/aur/stirling-pdf-server-bin/PKGBUILD +++ b/.github/aur/stirling-pdf-server-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-server-bin -pkgver=2.14.2 +pkgver=2.14.3 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)" arch=('any') diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 64454c5fe1..7936145677 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -1,13 +1,15 @@ -# CI routing infra. Editing the top-level router (build.yml) or this filter -# config re-runs every area's jobs, so every job-gating filter below includes -# *ci. That makes a change to how jobs are dispatched actually exercise those -# jobs (self-testing), instead of a router edit only matching the project filter. +# CI routing infrastructure. Changes to the top-level router (build.yml) or +# this filter configuration rerun every area's jobs. Every job-gating filter +# therefore includes *ci, so routing changes exercise the jobs they affect +# instead of matching only the project filter. ci: &ci - .github/workflows/build.yml + - .github/workflows/gradle-cache-prime.yml - .github/config/.files.yaml build: &build - *ci + - buildSrc/** - build.gradle - gradle/spotless.gradle - app/(common|core|proprietary|saas)/build.gradle @@ -15,6 +17,22 @@ build: &build - .taskfiles/backend.yml - .github/workflows/check-licence.yml +# Backend build inputs. This is intentionally broader than `build`: Java and +# backend resource changes must exercise the backend matrix even when Gradle +# build scripts themselves are unchanged. +backend: &backend + - *ci + - *build + - gradle/** + - gradle.properties + - gradlew + - gradlew.bat + - settings.gradle + - app/(common|core|proprietary|saas)/src/(main|test)/java/** + - "app/(common|core|proprietary|saas)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*" + - scripts/db-migration/** + - .github/workflows/backend-build.yml + openapi: &openapi - *ci - *build @@ -24,9 +42,9 @@ openapi: &openapi docker-base: &docker-base - docker/base/Dockerfile -# Dockerfiles only (base + embedded + unoserver). Gates the slow multi-arch -# (arm64) leg of the PR docker test build: arm64 is only rebuilt when a -# Dockerfile itself changes, not on every code PR. +# Dockerfiles only (base, embedded, and unoserver). The slow multi-architecture +# (arm64) leg of the PR Docker test build runs only when a Dockerfile changes, +# rather than for every code PR. dockerfiles: &dockerfiles - docker/**/Dockerfile* @@ -68,7 +86,6 @@ project: &project frontend: &frontend - *ci - frontend/** - - .github/workflows/testdriver.yml - testing/** - docker/** - scripts/translations/*.py @@ -84,11 +101,12 @@ frontend: &frontend - .taskfiles/frontend.yml - .taskfiles/e2e.yml - .github/workflows/frontend-validation.yml + - .github/workflows/frontend-a11y.yml - .github/workflows/e2e-stubbed.yml - .github/workflows/e2e-live.yml -# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build -# job on changes to any of these. +# Files that affect the Tauri desktop bundle. Changes to any of these files +# trigger the multi-OS Tauri build job. tauri: &tauri - *ci - frontend/editor/src-tauri/** @@ -101,9 +119,9 @@ tauri: &tauri - Taskfile.yml - .taskfiles/desktop.yml -# Files that affect the AI engine (Python tool models, fixers, tests). Gate -# the engine validation job on changes to engine sources or to the Java -# tool surfaces it generates models from. +# Files that affect the AI engine, including its Python tool models, fixers, +# and tests. The engine validation job also runs when the Java tool surfaces +# used to generate those models change. engine: &engine - *ci - engine/** @@ -113,17 +131,19 @@ engine: &engine - .taskfiles/engine.yml # Files that can make the committed generated API models (frontend tool API -# types + engine tool models) go stale: the Java tool surfaces they derive from, -# the generators, the generated files themselves (to catch a hand-edit), and the -# tasks that drive generation. Deliberately excludes the broad frontend/docker/ -# testing globs, so a CSS-only PR does not boot the backend to rebuild the spec. +# types and engine tool models) stale: their Java sources, generators, +# generated outputs (to catch hand edits), and generation tasks. Broad +# frontend, Docker, and testing globs are intentionally excluded, so a CSS-only +# PR does not start the backend to rebuild the specification. generated-models: &generated-models - *ci - *openapi - frontend/editor/scripts/generate-tool-api-types.mts - frontend/editor/src/core/types/toolApiTypes.ts + - frontend/editor/src/core/types/toolIO.ts - engine/scripts/generate_tool_models.py - engine/src/stirling/models/tool_models.py + - engine/src/stirling/models/tool_io.py - .taskfiles/frontend.yml - .taskfiles/engine.yml - .github/workflows/check-generated-models.yml @@ -138,8 +158,8 @@ licenses-backend: &licenses-backend - ".github/workflows/frontend-backend-licenses-update.yml" - *build -# Files that can affect premium / enterprise behaviour. Gate the enterprise -# Playwright job on changes to any of these on PRs. +# Files that can affect premium or enterprise behaviour. Changes to any of +# these files trigger the enterprise Playwright job for pull requests. proprietary: &proprietary - *ci - app/proprietary/** diff --git a/.github/config/repo_devs.json b/.github/config/repo_devs.json index 8b0bb97a81..597a84dade 100644 --- a/.github/config/repo_devs.json +++ b/.github/config/repo_devs.json @@ -11,7 +11,6 @@ "LaserKaspar", "sbplat", "reecebrowne", - "DarioGii", "ConnorYoh", "EthanHealy01", "jbrunton96", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3ab69765a9..883c4f7f46 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,11 +11,18 @@ updates: - "/app/common" - "/app/core" - "/app/proprietary" + - "/app/saas" + - "/buildSrc" schedule: interval: "weekly" cooldown: default-days: 7 rebase-strategy: "auto" + groups: + simple-java-mail: + patterns: + - "org.simplejavamail:simple-java-mail" + - "org.simplejavamail:outlook-module" - package-ecosystem: "docker" directories: @@ -32,6 +39,19 @@ updates: cooldown: default-days: 7 rebase-strategy: "auto" + groups: + ubuntu: + patterns: + - "ubuntu" + eclipse-temurin: + patterns: + - "eclipse-temurin" + uv: + patterns: + - "ghcr.io/astral-sh/uv" + gradle: + patterns: + - "gradle" - package-ecosystem: github-actions directory: / @@ -45,6 +65,7 @@ updates: directories: - /devTools - /frontend + - /testing/compose/mcp-client-check schedule: interval: "weekly" cooldown: @@ -73,14 +94,13 @@ updates: - "react-dom" - "@types/react" - "@types/react-dom" - typescript-eslint: + tanstack: patterns: - - "@typescript-eslint/*" - - "typescript-eslint" - eslint: + - "@tanstack/*" + typescript: patterns: - - "eslint" - - "@eslint/*" + - "typescript" + - "@typescript/*" vite: patterns: - "vite" @@ -109,6 +129,10 @@ updates: patterns: - "@posthog/*" - "posthog-js" + storybook: + patterns: + - "storybook" + - "@storybook/*" supabase: patterns: - "@supabase/*" @@ -155,8 +179,8 @@ updates: - "tokio" - "tokio-*" - - package-ecosystem: pip - directory: /testing/cucumber + - package-ecosystem: "uv" + directory: "/engine" schedule: interval: "weekly" cooldown: diff --git a/.github/scripts/check_language_toml.py b/.github/scripts/check_language_toml.py index 638951693e..ecd1c648cd 100644 --- a/.github/scripts/check_language_toml.py +++ b/.github/scripts/check_language_toml.py @@ -19,9 +19,9 @@ import argparse import glob import os import re +import tomllib # Python 3.11+ (stdlib) from pathlib import Path -import tomllib # Python 3.11+ (stdlib) import tomli_w # For writing TOML files @@ -133,11 +133,7 @@ def update_missing_keys(reference_file, file_list, branch=""): file_path = Path(file_path) language_dir = file_path.parent.name reference_lang_dir = reference_file.parent.name - if ( - language_dir == reference_lang_dir - or file_path.suffix != ".toml" - or file_path.parents[1].name != "locales" - ): + if language_dir == reference_lang_dir or file_path.suffix != ".toml" or file_path.parents[1].name != "locales": print(f"Skipping file: {file_path}") continue @@ -198,9 +194,7 @@ def check_for_differences(reference_file, file_list, branch, actor): # Verify that file is within the expected directory if not absolute_path.is_relative_to(base_dir): has_differences = True - report.append( - f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n" - ) + report.append(f"\n⚠️ Unsafe file found: `{locale_dir}/{basename_current_file}`\n\n---\n") continue # Verify file size before processing @@ -214,10 +208,7 @@ def check_for_differences(reference_file, file_list, branch, actor): if basename_current_file == basename_reference_file and locale_dir == "en-US": continue - if ( - file_normpath.suffix != ".toml" - or basename_current_file != "translation.toml" - ): + if file_normpath.suffix != ".toml" or basename_current_file != "translation.toml": continue only_reference_file = False @@ -261,9 +252,7 @@ def check_for_differences(reference_file, file_list, branch, actor): ) report.append("") report.append(" Use the following command to remove them:") - report.append( - f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`" - ) + report.append(f" `python scripts/translations/translation_merger.py {locale_dir} remove-unused`") report.append("") if extra_keys_list: report.append( @@ -271,9 +260,7 @@ def check_for_differences(reference_file, file_list, branch, actor): ) report.append("") report.append(" Use the following command to add them:") - report.append( - f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`" - ) + report.append(f" `python scripts/translations/translation_merger.py {locale_dir} add-missing`") report.append("") if missing_keys_list or extra_keys_list: @@ -288,9 +275,7 @@ def check_for_differences(reference_file, file_list, branch, actor): output = "\n".join( [ f" - `{key}`: first at {first}, duplicate at `{duplicate}`" - for key, first, duplicate in find_duplicate_keys( - branch_path / file_normpath - ) + for key, first, duplicate in find_duplicate_keys(branch_path / file_normpath) ] ) report.append("3. **Test Status:** ❌ **_Failed_**") @@ -313,18 +298,14 @@ def check_for_differences(reference_file, file_list, branch, actor): else: report.append("## ✅ Overall Check Status: **_Success_**") report.append("") - report.append( - f"Thanks @{actor} for your help in keeping the translations up to date." - ) + report.append(f"Thanks @{actor} for your help in keeping the translations up to date.") if not only_reference_file: print("\n".join(report)) if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Find missing keys in TOML translation files" - ) + parser = argparse.ArgumentParser(description="Find missing keys in TOML translation files") parser.add_argument( "--actor", required=False, diff --git a/.github/scripts/requirements_dev.in b/.github/scripts/requirements_dev.in deleted file mode 100644 index 1f65a4f4af..0000000000 --- a/.github/scripts/requirements_dev.in +++ /dev/null @@ -1,9 +0,0 @@ -pip -setuptools -WeasyPrint -pdf2image -pillow -unoserver -opencv-python-headless -pre-commit -brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29 diff --git a/.github/scripts/requirements_dev.txt b/.github/scripts/requirements_dev.txt deleted file mode 100644 index d4b9f19132..0000000000 --- a/.github/scripts/requirements_dev.txt +++ /dev/null @@ -1,528 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in' -# -# WARNING: pip install will require the following package to be hashed. -# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip -brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29 - # via - # -r .github/scripts/requirements_dev.in - # fonttools -cffi==2.1.0 \ - --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ - --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ - --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ - --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ - --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ - --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ - --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ - --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ - --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ - --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ - --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ - --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ - --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ - --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ - --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ - --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ - --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ - --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ - --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ - --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ - --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ - --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ - --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ - --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ - --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ - --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ - --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ - --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ - --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ - --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ - --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ - --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ - --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ - --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ - --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ - --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ - --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ - --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ - --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ - --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ - --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ - --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ - --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ - --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ - --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ - --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ - --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ - --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ - --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ - --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ - --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ - --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ - --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ - --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ - --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ - --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ - --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ - --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ - --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ - --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ - --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ - --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ - --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ - --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ - --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ - --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ - --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ - --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ - --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ - --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ - --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ - --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ - --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ - --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ - --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ - --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ - --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ - --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ - --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ - --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ - --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ - --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ - --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ - --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ - --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ - --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ - --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ - --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ - --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ - --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ - --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ - --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ - --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ - --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ - --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ - --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ - --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ - --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ - --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ - --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f - # via weasyprint -cfgv==3.5.0 \ - --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ - --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 - # via pre-commit -cssselect2==0.9.0 \ - --hash=sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563 \ - --hash=sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb - # via weasyprint -distlib==0.4.3 \ - --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ - --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed - # via virtualenv -filelock==3.30.0 \ - --hash=sha256:1774e682dbe443bd60f9609162fc596e2c80dc84ffc2957068953406d0520090 \ - --hash=sha256:40632998f0772e64183bb819f086a1b9def6be1090cf1dcb9d45f46806ef279b - # via - # python-discovery - # virtualenv -fonttools==4.63.0 \ - --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \ - --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \ - --hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \ - --hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \ - --hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \ - --hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \ - --hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \ - --hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \ - --hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \ - --hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \ - --hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \ - --hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \ - --hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \ - --hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \ - --hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \ - --hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \ - --hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \ - --hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \ - --hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \ - --hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \ - --hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \ - --hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \ - --hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \ - --hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \ - --hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \ - --hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \ - --hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \ - --hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \ - --hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \ - --hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \ - --hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \ - --hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \ - --hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \ - --hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \ - --hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \ - --hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \ - --hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \ - --hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \ - --hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \ - --hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \ - --hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \ - --hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \ - --hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \ - --hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \ - --hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \ - --hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \ - --hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \ - --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \ - --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \ - --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 - # via weasyprint -identify==2.6.19 \ - --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ - --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 - # via pre-commit -nodeenv==1.10.0 \ - --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ - --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb - # via pre-commit -numpy==2.4.6 \ - --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ - --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ - --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ - --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ - --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ - --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ - --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ - --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ - --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ - --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ - --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ - --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ - --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ - --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ - --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ - --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ - --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ - --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ - --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ - --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ - --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ - --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ - --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ - --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ - --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ - --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ - --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ - --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ - --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ - --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ - --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ - --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ - --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ - --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ - --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ - --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ - --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ - --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ - --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ - --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ - --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ - --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ - --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ - --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ - --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ - --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ - --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ - --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ - --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ - --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ - --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ - --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ - --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ - --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ - --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ - --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ - --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ - --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ - --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ - --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ - --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ - --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ - --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ - --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ - --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ - --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ - --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ - --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ - --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ - --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ - --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ - --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 - # via opencv-python-headless -opencv-python-headless==5.0.0.93 \ - --hash=sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f \ - --hash=sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4 \ - --hash=sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f \ - --hash=sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00 \ - --hash=sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e \ - --hash=sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4 \ - --hash=sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c \ - --hash=sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9 \ - --hash=sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37 - # via -r .github/scripts/requirements_dev.in -pdf2image==1.17.0 \ - --hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \ - --hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2 - # via -r .github/scripts/requirements_dev.in -pillow==12.3.0 \ - --hash=sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756 \ - --hash=sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a \ - --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ - --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ - --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ - --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ - --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ - --hash=sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b \ - --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ - --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ - --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ - --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ - --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ - --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ - --hash=sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5 \ - --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ - --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd \ - --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ - --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ - --hash=sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed \ - --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ - --hash=sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb \ - --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ - --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ - --hash=sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6 \ - --hash=sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1 \ - --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ - --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ - --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ - --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ - --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ - --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ - --hash=sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c \ - --hash=sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f \ - --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ - --hash=sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f \ - --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ - --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ - --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ - --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ - --hash=sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a \ - --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ - --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ - --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ - --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ - --hash=sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468 \ - --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ - --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ - --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ - --hash=sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a \ - --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ - --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ - --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ - --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ - --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ - --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ - --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ - --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ - --hash=sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94 \ - --hash=sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468 \ - --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ - --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ - --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ - --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd \ - --hash=sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7 \ - --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ - --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ - --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ - --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ - --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ - --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ - --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ - --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ - --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ - --hash=sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3 \ - --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ - --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ - --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ - --hash=sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26 \ - --hash=sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec \ - --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ - --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ - --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ - --hash=sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e \ - --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ - --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ - --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 - # via - # -r .github/scripts/requirements_dev.in - # pdf2image - # weasyprint -platformdirs==4.10.0 \ - --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ - --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a - # via - # python-discovery - # virtualenv -pre-commit==4.6.0 \ - --hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \ - --hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b - # via -r .github/scripts/requirements_dev.in -pycparser==3.0 \ - --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ - --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 - # via cffi -pydyf==0.12.1 \ - --hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \ - --hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095 - # via weasyprint -pyphen==0.17.2 \ - --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ - --hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3 - # via weasyprint -python-discovery==1.4.4 \ - --hash=sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3 \ - --hash=sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe - # via virtualenv -pyyaml==6.0.3 \ - --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ - --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ - --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ - --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ - --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ - --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ - --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ - --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ - --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ - --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ - --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ - --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ - --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ - --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ - --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ - --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ - --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ - --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ - --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ - --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ - --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ - --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ - --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ - --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ - --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ - --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ - --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ - --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ - --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ - --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ - --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ - --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ - --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ - --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ - --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ - --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ - --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ - --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ - --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ - --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ - --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ - --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ - --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ - --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ - --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ - --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ - --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ - --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ - --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ - --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ - --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ - --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ - --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ - --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ - --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ - --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ - --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ - --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ - --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ - --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ - --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ - --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ - --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ - --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ - --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ - --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ - --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ - --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ - --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ - --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ - --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ - --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ - --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 - # via pre-commit -tinycss2==1.5.1 \ - --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ - --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 - # via - # cssselect2 - # weasyprint -tinyhtml5==2.1.0 \ - --hash=sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67 \ - --hash=sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a - # via weasyprint -unoserver==3.7 \ - --hash=sha256:b05f9578506ac7374ae1b314c3a79528636c542ac78220a9ce99110584ca424b \ - --hash=sha256:fc44e6808071c9d2957e705ecf1742cea8a582aa5d5cc23babf36bb332ec6e8e - # via -r .github/scripts/requirements_dev.in -virtualenv==21.6.1 \ - --hash=sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128 \ - --hash=sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b - # via pre-commit -weasyprint==69.0 \ - --hash=sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6 \ - --hash=sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c - # via -r .github/scripts/requirements_dev.in -webencodings==0.5.1 \ - --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ - --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 - # via - # cssselect2 - # tinycss2 - # tinyhtml5 -zopfli==0.4.3 \ - --hash=sha256:0087c9a6f0c8a052be0f6d1a9bb71b6caffdd3e10201d6d6166e28d482cebe6d \ - --hash=sha256:47604eee5c6704bdf0e94d8391fe3b74ddb2abd84128fbcfdc3ee0fc265feaef \ - --hash=sha256:62248dbf8dbcbd588ee194b210e5be9fa80bce29641f55599d6d394bd2a9d8a3 \ - --hash=sha256:628c3e941752880b3491db8d44163d0aedb221944e22a17187ff7fc549b050f6 \ - --hash=sha256:769875152d0625c46707bcca57d4b2233fe653482067acd55fbf6ec525cb9bdc \ - --hash=sha256:7e9703ca6e7ef66c8d05e0826b6f558b680c9db8206f84f05a3ee93430a12e42 \ - --hash=sha256:7fa3c35193475290e3f007bbcdebdbae64ba2f012d75c632da0d727e1da50d5e \ - --hash=sha256:88f4fbe429aad72bc206275d81fab11a097e0f951a5848d1f51083c37ea73073 \ - --hash=sha256:921c2c9907f4364963848da5ad194b46d68865e07fdb975d04fd09bc42d47357 \ - --hash=sha256:d3a50f91a13cea9bafe025de8fd87a005eb26de02a4f0c193127ddbf23ac8ebe \ - --hash=sha256:d4f51dd1ab5312e837e2091284e0d9f1a138188f2e65812f9a5799dc02c45f94 \ - --hash=sha256:eb0c9c1d40a8cb1d58762d7e57290ccb753e0828c4d01be8acb59aae5d0ca206 \ - --hash=sha256:f2e0adcf7d36c6fd0dd36cc771ef7f0c5803a05666feafcd90d7170174a4148e - # via fonttools - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 - # via -r .github/scripts/requirements_dev.in -setuptools==83.0.0 \ - --hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \ - --hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3 - # via -r .github/scripts/requirements_dev.in diff --git a/.github/scripts/requirements_sync_readme.in b/.github/scripts/requirements_sync_readme.in deleted file mode 100644 index f7501c6d0e..0000000000 --- a/.github/scripts/requirements_sync_readme.in +++ /dev/null @@ -1,2 +0,0 @@ -tomlkit -tomli-w diff --git a/.github/scripts/requirements_sync_readme.txt b/.github/scripts/requirements_sync_readme.txt deleted file mode 100644 index d812da2025..0000000000 --- a/.github/scripts/requirements_sync_readme.txt +++ /dev/null @@ -1,14 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in' -# -tomli-w==1.2.0 \ - --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ - --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 - # via -r .github/scripts/requirements_sync_readme.in -tomlkit==0.15.0 \ - --hash=sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 \ - --hash=sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3 - # via -r .github/scripts/requirements_sync_readme.in diff --git a/.github/scripts/verify-updater-signatures.py b/.github/scripts/verify-updater-signatures.py index a1624448e1..1ae46eb84c 100644 --- a/.github/scripts/verify-updater-signatures.py +++ b/.github/scripts/verify-updater-signatures.py @@ -4,19 +4,18 @@ Usage: verify-updater-signatures.py [tauri.conf.json] """ -import binascii -import sys -import json import base64 +import binascii import hashlib +import json +import sys from pathlib import Path -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey ART_ROOT = Path(sys.argv[1]) -CONF = Path( - sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json" -) +CONF = Path(sys.argv[2] if len(sys.argv) > 2 else "frontend/editor/src-tauri/tauri.conf.json") def load_pubkey(): diff --git a/.github/workflows/PR-Auto-Deploy-V2.yml b/.github/workflows/PR-Auto-Deploy-V2.yml index a19a616735..2c9f6e2e26 100644 --- a/.github/workflows/PR-Auto-Deploy-V2.yml +++ b/.github/workflows/PR-Auto-Deploy-V2.yml @@ -26,6 +26,10 @@ jobs: check-pr: if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + # Only reads the PR via pulls.get with the default GITHUB_TOKEN. + permissions: + contents: read + pull-requests: read outputs: should_deploy: ${{ steps.decide.outputs.should_deploy }} is_fork: ${{ steps.resolve.outputs.is_fork }} @@ -35,7 +39,7 @@ jobs: pr_ref: ${{ steps.resolve.outputs.ref }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -86,7 +90,7 @@ jobs: fi fi else - auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs") + auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs") is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done if [ "$is_auth" = true ]; then should=true @@ -97,6 +101,7 @@ jobs: echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT deploy-v2-pr: + environment: pr-preview needs: check-pr runs-on: ubuntu-latest if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true') @@ -107,6 +112,7 @@ jobs: permissions: contents: read issues: write + packages: write pull-requests: write env: # Single source of truth for whether this preview embeds the admin portal: @@ -115,7 +121,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -125,20 +131,11 @@ jobs: repository: ${{ github.repository }} ref: main - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Add deployment started comment id: deployment-started uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; @@ -180,11 +177,12 @@ jobs: with: repository: ${{ needs.check-pr.outputs.pr_repository }} ref: ${{ needs.check-pr.outputs.pr_ref }} - token: ${{ secrets.GITHUB_TOKEN }} + # untrusted tree is built below - never leave credentials in .git/config + persist-credentials: false fetch-depth: 0 # Fetch full history for commit hash detection - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Get version number id: versionNumber @@ -192,11 +190,16 @@ jobs: VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Get commit hash for app id: commit-hash @@ -220,7 +223,7 @@ jobs: - name: Check if image exists id: check-image run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then + if docker manifest inspect ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "Image already exists, skipping build" else @@ -228,6 +231,8 @@ jobs: echo "Image needs to be built" fi + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test - name: Build and push V2 image if: steps.check-image.outputs.exists == 'false' uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -237,7 +242,7 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-latest cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:v2-${{ steps.commit-hash.outputs.app_short }} build-args: | VERSION_TAG=v2-alpha BUILD_PORTAL=${{ env.BUILD_PORTAL }} @@ -246,9 +251,11 @@ jobs: - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy V2 to VPS id: deploy run: | @@ -261,7 +268,7 @@ jobs: services: stirling-pdf-v2: container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }} - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-${{ steps.commit-hash.outputs.app_short }} + image: ${IMAGE_BASE}:v2-${{ steps.commit-hash.outputs.app_short }} ports: - "${V2_PORT}:8080" volumes: @@ -273,8 +280,8 @@ jobs: DISABLE_ADDITIONAL_FEATURES: "false" STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true" SECURITY_ENABLELOGIN: "true" - SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}" - SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}" + SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}" + SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}" SYSTEM_DEFAULTLOCALE: en-US UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}" UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture" @@ -288,9 +295,9 @@ jobs: EOF # Deploy to VPS - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose-v2.yml + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose-v2.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH # Create V2 PR-specific directories mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs,storage} @@ -315,12 +322,19 @@ jobs: # Set port for output echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT + env: + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + TEST_LOGIN_USERNAME: ${{ secrets.TEST_LOGIN_USERNAME }} + TEST_LOGIN_PASSWORD: ${{ secrets.TEST_LOGIN_PASSWORD }} + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} + # ---- Storybook preview (only when this PR touches stories/.storybook) ---- # Runs inside the same approved-contributor-gated deploy job, so it deploys # under the exact same access rules as the app preview. - name: Detect Storybook changes id: sb-changes - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 with: list-files: json filters: | @@ -331,7 +345,7 @@ jobs: - name: Set up Node.js for Storybook if: steps.sb-changes.outputs.storybook == 'true' - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -379,8 +393,9 @@ jobs: env: SB_URL: ${{ steps.storybook.outputs.url }} SB_FILES: ${{ steps.sb-changes.outputs.storybook_files }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ needs.check-pr.outputs.pr_number }}; @@ -401,7 +416,7 @@ jobs: } } - const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${v2Port}`; + const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${v2Port}`; // Only mention the portal when this image actually embeds it. // Use the direct IP URL - the SSL hostname isn't supported yet. @@ -447,6 +462,10 @@ jobs: }); cleanup-v2-deployment: + # Tearing a preview down is not a deployment - no deployment object. + environment: + name: pr-preview + deployment: false if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: @@ -456,26 +475,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Clean up V2 deployment comments uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { owner, repo } = context.repo; const prNumber = ${{ github.event.pull_request.number }}; @@ -504,12 +514,14 @@ jobs: - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Cleanup V2 deployment run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH' + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH' if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then echo "Found V2 PR directory, proceeding with cleanup..." @@ -542,8 +554,11 @@ jobs: # Only remove PR-specific containers and directories ENDSSH + env: + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Cleanup temporary files if: always() run: | - rm -f ../private.key + rm -f ../private.key docker-compose.yml storybook.tgz continue-on-error: true diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 2b81b8c777..410aa82dc9 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -37,7 +37,8 @@ jobs: check-comment: runs-on: ubuntu-latest permissions: - issues: write + contents: read # actions/checkout + issues: write # add reaction to the triggering issue comment if: | vars.CI_PROFILE != 'lite' && ( github.event_name == 'workflow_dispatch' || @@ -54,7 +55,6 @@ jobs: github.event.comment.user.login == 'Ludy87' || github.event.comment.user.login == 'balazs-szucs' || github.event.comment.user.login == 'reecebrowne' || - github.event.comment.user.login == 'DarioGii' || github.event.comment.user.login == 'EthanHealy01' || github.event.comment.user.login == 'jbrunton96' || github.event.comment.user.login == 'ConnorYoh' @@ -70,22 +70,13 @@ jobs: enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Get PR data id: get-pr uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -156,7 +147,7 @@ jobs: id: add-eyes-reaction uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`); try { @@ -175,47 +166,45 @@ jobs: } deploy-pr: + environment: pr-preview needs: check-comment runs-on: ubuntu-latest permissions: - issues: write + contents: read # actions/checkout, incl. the PR merge ref + issues: write # reactions, 'pr-deployed' label, deployment URL comment pull-requests: write + packages: write # push PR image to ghcr.io steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge - token: ${{ steps.setup-bot.outputs.token }} + # untrusted tree gets built below - never leave credentials in .git/config + persist-credentials: false + + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-deploy-pr-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Run Gradle Command @@ -233,13 +222,18 @@ jobs: STIRLING_PDF_DESKTOP_UI: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Build and push PR-specific image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -249,7 +243,7 @@ jobs: push: true cache-from: type=gha,scope=stirling-pdf-latest cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ needs.check-comment.outputs.pr_number }} build-args: | VERSION_TAG=alpha PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }} @@ -259,20 +253,22 @@ jobs: if: needs.check-comment.outputs.enable_prototypes == 'true' uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: - context: ./engine + context: . file: ./engine/Dockerfile push: true cache-from: type=gha,scope=stirling-pdf-engine cache-to: type=gha,mode=max,scope=stirling-pdf-engine - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ needs.check-comment.outputs.pr_number }} + tags: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ needs.check-comment.outputs.pr_number }} platforms: linux/amd64 - name: Set up SSH run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} - name: Deploy to VPS id: deploy run: | @@ -290,11 +286,11 @@ jobs: # Set pro/enterprise settings (enterprise implies pro) if [ "${{ needs.check-comment.outputs.enable_enterprise }}" == "true" ]; then PREMIUM_ENABLED="true" - PREMIUM_KEY="${{ secrets.ENTERPRISE_KEY }}" + PREMIUM_KEY="${ENTERPRISE_KEY}" PREMIUM_PROFEATURES_AUDIT_ENABLED="true" elif [ "${{ needs.check-comment.outputs.enable_pro }}" == "true" ]; then PREMIUM_ENABLED="true" - PREMIUM_KEY="${{ secrets.PREMIUM_KEY }}" + PREMIUM_KEY="${PRO_KEY}" PREMIUM_PROFEATURES_AUDIT_ENABLED="true" else PREMIUM_ENABLED="false" @@ -304,7 +300,6 @@ jobs: ENABLE_PROTOTYPES="${{ needs.check-comment.outputs.enable_prototypes }}" PR_NUMBER="${{ needs.check-comment.outputs.pr_number }}" - DOCKER_USER="${{ secrets.DOCKER_HUB_USERNAME }}" # Build engine env vars for backend (only set when prototypes enabled) if [ "$ENABLE_PROTOTYPES" == "true" ]; then @@ -314,9 +309,9 @@ jobs: ENGINE_SERVICE=" stirling-pdf-engine: container_name: stirling-pdf-engine-pr-${PR_NUMBER} - image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER} + image: ${IMAGE_BASE}:engine-pr-${PR_NUMBER} environment: - ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\" + ANTHROPIC_API_KEY: \"${ANTHROPIC_API_KEY}\" networks: - pr-network restart: on-failure:5" @@ -339,7 +334,7 @@ jobs: services: stirling-pdf: container_name: stirling-pdf-pr-${PR_NUMBER} - image: ${DOCKER_USER}/test:pr-${PR_NUMBER} + image: ${IMAGE_BASE}:pr-${PR_NUMBER} ports: - "${PR_NUMBER}:8080" volumes: @@ -363,9 +358,9 @@ jobs: EOF # Then copy the file and execute commands - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml + scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${NEW_VPS_USERNAME}@${NEW_VPS_HOST}:/tmp/docker-compose.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << ENDSSH # Create PR-specific directories mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs} @@ -381,11 +376,19 @@ jobs: # Set output for use in PR comment echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV + env: + ENTERPRISE_KEY: ${{ secrets.ENTERPRISE_KEY }} + # named PRO_KEY, not PREMIUM_KEY, so the shell var it feeds is not self-referential + PRO_KEY: ${{ secrets.PREMIUM_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + IMAGE_BASE: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Add success reaction to comment if: success() && github.event_name == 'issue_comment' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -420,7 +423,7 @@ jobs: if: failure() && github.event_name == 'issue_comment' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`); try { @@ -439,15 +442,17 @@ jobs: - name: Post deployment URL to PR if: success() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { GITHUB_REPOSITORY } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const prNumber = ${{ needs.check-comment.outputs.pr_number }}; const securityStatus = process.env.security_status || "Security Disabled"; - const deploymentUrl = `http://${{ secrets.NEW_VPS_HOST }}:${prNumber}`; + const deploymentUrl = `http://${process.env.NEW_VPS_HOST}:${prNumber}`; const commentBody = `## 🚀 PR Test Deployment\n\n` + `Your PR has been deployed for testing!\n\n` + `🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` + @@ -472,26 +477,22 @@ jobs: handle-label-commands: if: ${{ github.event.issue.pull_request != null }} runs-on: ubuntu-latest + permissions: + contents: read # actions/checkout, reads repo_devs.json and labels.yml + issues: write # add/remove labels, delete the command comment steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Check out the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Apply label commands uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const fs = require('fs'); const path = require('path'); diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index ab30d29cdb..098f8d7803 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -7,41 +7,33 @@ on: permissions: contents: read -env: - SERVER_IP: ${{ secrets.NEW_VPS_IP }} # Add this to your GitHub secrets - CLEANUP_PERFORMED: "false" # Add flag to track if cleanup occurred - jobs: cleanup: + # Tearing a preview down is not a deployment - no deployment object. + environment: + name: pr-preview + deployment: false if: github.event.action == 'closed' runs-on: ubuntu-latest permissions: + contents: read # actions/checkout pull-requests: write - issues: write + issues: write # list/remove labels, list/delete comments steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout PR uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Remove 'pr-deployed' label if present id: remove-label-comment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const prNumber = ${{ github.event.pull_request.number }}; const owner = context.repo.owner; @@ -100,14 +92,22 @@ jobs: if: steps.remove-label-comment.outputs.present == 'true' run: | mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key + echo "${NEW_VPS_SSH_KEY}" > ../private.key sudo chmod 600 ../private.key + env: + NEW_VPS_SSH_KEY: ${{ secrets.NEW_VPS_SSH_KEY }} + + - name: Convert repository owner to lowercase + id: repoowner + run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT - name: Cleanup PR deployment if: steps.remove-label-comment.outputs.present == 'true' id: cleanup + # ENDSSH heredoc is quoted, so its body is sent literally: secrets inside it + # must stay as GitHub expressions, a shell var would be empty on the remote host. run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << 'ENDSSH' + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${NEW_VPS_USERNAME}@${NEW_VPS_HOST} << 'ENDSSH' if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then echo "Found PR directory, proceeding with cleanup..." @@ -122,8 +122,8 @@ jobs: rm -rf /stirling/PR-${{ github.event.pull_request.number }} # Remove the Docker images - docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true - docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:engine-pr-${{ github.event.pull_request.number }} || true + docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:pr-${{ github.event.pull_request.number }} || true + docker rmi --no-prune ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-test:engine-pr-${{ github.event.pull_request.number }} || true echo "PERFORMED_CLEANUP" else @@ -131,6 +131,9 @@ jobs: echo "NO_CLEANUP_NEEDED" fi ENDSSH + env: + NEW_VPS_USERNAME: ${{ secrets.NEW_VPS_USERNAME }} + NEW_VPS_HOST: ${{ secrets.NEW_VPS_HOST }} - name: Cleanup temporary files if: always() diff --git a/.github/workflows/_runner-pick.yml b/.github/workflows/_runner-pick.yml index 0f32d79b95..d65831c5df 100644 --- a/.github/workflows/_runner-pick.yml +++ b/.github/workflows/_runner-pick.yml @@ -38,7 +38,7 @@ jobs: is_fork: ${{ steps.decide.outputs.is_fork }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml index fbc9848eaf..b8e15466e5 100644 --- a/.github/workflows/ai-engine.yml +++ b/.github/workflows/ai-engine.yml @@ -20,7 +20,7 @@ jobs: pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -28,9 +28,12 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock cache-suffix: ai-engine - name: Install Task @@ -96,6 +99,14 @@ jobs: echo "============================================" exit 1 + - name: Build engine production image + if: always() + run: docker build --file engine/Dockerfile --tag stirling-pdf-engine:ci . + + - name: Build engine development image + if: always() + run: docker build --file engine/Dockerfile.dev --tag stirling-pdf-engine-dev:ci . + - name: Remove engine check comment on success if: steps.engine-check.outcome == 'success' && github.event_name == 'pull_request' continue-on-error: true diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml deleted file mode 100644 index 65e6445637..0000000000 --- a/.github/workflows/ai_pr_title_review.yml +++ /dev/null @@ -1,228 +0,0 @@ -name: AI - PR Title Review - -on: - pull_request: - types: [opened, edited] - branches: [main] - -permissions: # required for secure-repo hardening - contents: read - -jobs: - ai-title-review: - permissions: - contents: read - pull-requests: write - models: read - - runs-on: ubuntu-latest - - steps: - - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Configure Git to suppress detached HEAD warning - run: git config --global advice.detachedHead false - - - name: Setup GitHub App Bot - if: github.actor != 'dependabot[bot]' - id: setup-bot - uses: ./.github/actions/setup-bot - continue-on-error: true - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - - name: Check if actor is repo developer - id: actor - run: | - if [[ "${{ github.actor }}" == *"[bot]" ]]; then - echo "PR opened by a bot – skipping AI title review." - echo "is_repo_dev=false" >> $GITHUB_OUTPUT - exit 0 - fi - if [ ! -f .github/config/repo_devs.json ]; then - echo "Error: .github/config/repo_devs.json not found" >&2 - exit 1 - fi - # Validate JSON and extract repo_devs - REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; }) - # Convert developer list into Bash array - mapfile -t DEVS_ARRAY <<< "$REPO_DEVS" - if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then - echo "is_repo_dev=true" >> $GITHUB_OUTPUT - else - echo "is_repo_dev=false" >> $GITHUB_OUTPUT - fi - - - name: Get PR diff - if: steps.actor.outputs.is_repo_dev == 'true' - id: get_diff - run: | - git fetch origin ${{ github.base_ref }} - git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff - echo "diff<> $GITHUB_OUTPUT - cat pr.diff >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Check and sanitize PR title - if: steps.actor.outputs.is_repo_dev == 'true' - id: sanitize_pr_title - env: - PR_TITLE_RAW: ${{ github.event.pull_request.title }} - run: | - # Sanitize PR title: max 72 characters, only printable characters - PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g') - if [[ ${#PR_TITLE} -lt 5 ]]; then - echo "PR title is too short. Must be at least 5 characters." >&2 - fi - echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT - - - name: AI PR Title Analysis - if: steps.actor.outputs.is_repo_dev == 'true' - id: ai-title-analysis - uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0 - with: - model: openai/gpt-4o - system-prompt-file: ".github/config/system-prompt.txt" - prompt: | - Based on the following input data: - - { - "diff": "${{ steps.get_diff.outputs.diff }}", - "pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}" - } - - Respond ONLY with valid JSON in the format: - { - "improved_rating": <0-10>, - "improved_ai_title_rating": <0-10>, - "improved_title": "" - } - - - name: Validate and set SCRIPT_OUTPUT - if: steps.actor.outputs.is_repo_dev == 'true' - run: | - cat < ai_response.json - ${{ steps.ai-title-analysis.outputs.response }} - EOF - - # Validate JSON structure - jq -e ' - (keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and - (.improved_rating | type == "number" and . >= 0 and . <= 10) and - (.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and - (.improved_title | type == "string") - ' ai_response.json - if [ $? -ne 0 ]; then - echo "Invalid AI response format" >&2 - cat ai_response.json >&2 - exit 1 - fi - # Parse JSON fields - IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json) - IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json) - # Limit comment length to 1000 characters - COMMENT=$(cat < /tmp/ai-title-comment.md - # Log input and output to the GitHub Step Summary - echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY - echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY - echo '```json' >> $GITHUB_STEP_SUMMARY - cat ai_response.json >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: Post comment on PR if needed - if: steps.actor.outputs.is_repo_dev == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - continue-on-error: true - with: - github-token: ${{ steps.setup-bot.outputs.token }} - script: | - const fs = require('fs'); - const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8'); - const { GITHUB_REPOSITORY } = process.env; - const [owner, repo] = GITHUB_REPOSITORY.split('/'); - const issue_number = context.issue.number; - - const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/); - const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null; - - const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]"; - const comments = await github.rest.issues.listComments({ owner, repo, issue_number }); - - const existing = comments.data.find(c => - c.user?.login === expectedActor && - c.body.includes("## 🤖 AI PR Title Suggestion") - ); - - if (rating === null) { - console.log("No rating found in AI response – skipping."); - return; - } - - if (rating <= 5) { - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body - }); - console.log("Updated existing suggestion comment."); - } else { - await github.rest.issues.createComment({ - owner, repo, issue_number, - body - }); - console.log("Created new suggestion comment."); - } - } else { - const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`; - - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, - comment_id: existing.id, - body: praise - }); - console.log("Replaced suggestion with praise."); - } else { - console.log("Rating > 5 and no existing comment – skipping comment."); - } - } - - - name: is not repo dev - if: steps.actor.outputs.is_repo_dev != 'true' - run: | - exit 0 # Skip the AI title review for non-repo developers - - - name: Clean up - if: always() - run: | - rm -f pr.diff ai_response.json /tmp/ai-title-comment.md - echo "Cleaned up temporary files." - continue-on-error: true # Ensure cleanup runs even if previous steps fail diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 8371695856..af0dc85aa6 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -26,7 +26,7 @@ jobs: jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -66,11 +66,12 @@ jobs: echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" publish-aur: + environment: package-publish needs: get-release-info runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -106,7 +107,7 @@ jobs: - name: Publish stirling-pdf-desktop to AUR if: ${{ github.event_name == 'release' || inputs.dry_run == false }} - uses: KSXGitHub/github-actions-deploy-aur@da03e160361ce01bf087e790b6ffd196d7dccff7 # v4.1.3 + uses: KSXGitHub/github-actions-deploy-aur@084b0d9b15415bf9cdb65d44dad1efe37a354050 # v4.2.0 with: pkgname: stirling-pdf-desktop pkgbuild: .github/aur/stirling-pdf-desktop/PKGBUILD diff --git a/.github/workflows/auto-labelerV2.yml b/.github/workflows/auto-labelerV2.yml index 96700ef2a0..8e6d974f3c 100644 --- a/.github/workflows/auto-labelerV2.yml +++ b/.github/workflows/auto-labelerV2.yml @@ -13,26 +13,21 @@ jobs: labeler: runs-on: ubuntu-latest permissions: - pull-requests: write + contents: read # checkout + labeler fetching its config from the repo + pull-requests: write # read changed files, apply labels to the PR + issues: write # labels are applied through the issues API steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - uses: srvaroa/labeler@bf262763a8a8e191f5847873aecc0f29df84f957 # v1.14.0 with: config_path: .github/labeler-config-srvaroa.yml use_local_config: false fail_on_error: true env: - GITHUB_TOKEN: "${{ steps.setup-bot.outputs.token }}" + GITHUB_TOKEN: "${{ github.token }}" diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 28974a5ff6..e9a27a072d 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -20,6 +20,9 @@ permissions: jobs: build: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest strategy: fail-fast: false @@ -28,33 +31,26 @@ jobs: flavor: [core, proprietary, saas] steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK ${{ matrix.jdk-version }} - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: ${{ matrix.jdk-version }} distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-${{ matrix.jdk-version }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Check Java formatting (Spotless) @@ -157,6 +153,9 @@ jobs: MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} STIRLING_FLAVOR: ${{ matrix.flavor }} + # Configure the Gradle daemon explicitly; GRADLE_OPTS alone only + # configures the Gradle client JVM. + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:+UseG1GC" - name: Check Test Reports Exist if: always() @@ -196,12 +195,14 @@ jobs: retention-days: 3 if-no-files-found: warn - - name: Install defusedxml for coverage summary - # coverage-summary.py parses JaCoCo XML through defusedxml to - # silence security scanners that pattern-match on the stdlib - # xml.etree.ElementTree.parse call. + - name: Install uv if: always() && matrix.flavor == 'saas' - run: python -m pip install --quiet defusedxml + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - name: JaCoCo coverage step summary # Only the saas leg posts the JUnit summary - it's a strict @@ -210,7 +211,7 @@ jobs: # near-identical tables crowding out the aggregate report. if: always() && matrix.flavor == 'saas' run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Backend JUnit coverage (JDK ${{ matrix.jdk-version }})" \ --jacoco "common=app/common/build/reports/jacoco/test/jacocoTestReport.xml" \ --jacoco "core=app/core/build/reports/jacoco/test/jacocoTestReport.xml" \ @@ -241,7 +242,7 @@ jobs: # so skip it for merge_group runs and workflow_dispatch. if: github.event_name == 'pull_request' id: jacoco - uses: madrapps/jacoco-report@50d3aff4548aa991e6753342d9ba291084e63848 # v1.7.2 + uses: madrapps/jacoco-report@e51ce1f46f7f8b5331593f935e59cbaf44b84920 # v1.8.0 with: paths: | ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml diff --git a/.github/workflows/build-enterprise.yml b/.github/workflows/build-enterprise.yml index 8c06175a5d..0bd376766e 100644 --- a/.github/workflows/build-enterprise.yml +++ b/.github/workflows/build-enterprise.yml @@ -15,6 +15,11 @@ name: Enterprise E2E (Playwright) on: workflow_call: + inputs: + use_shared_cache: + required: false + type: boolean + default: false push: branches: ["main"] schedule: @@ -37,6 +42,9 @@ jobs: uses: ./.github/workflows/_runner-pick.yml playwright-e2e-enterprise: + environment: + name: ci-unsigned + deployment: false needs: pick # Skip on fork PRs / untrusted authors: they have no PREMIUM_KEY_ENTERPRISE, # so the suite can't boot premium and would fail. See the header comment. @@ -50,18 +58,38 @@ jobs: SYSTEM_ENABLEANALYTICS: "false" steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Restore cache Gradle + if: inputs.use_shared_cache == false + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-playwright-e2e-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" + - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -295,3 +323,80 @@ jobs: name: playwright-report-enterprise-${{ github.run_id }} path: frontend/playwright-report/ retention-days: 7 + + - name: Cleanup temporary files + if: always() + run: | + rm -f /tmp/helpers.sh /tmp/backend.log /tmp/backend.pid + continue-on-error: true + + # Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml) + # and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel). + multinode-e2e: + environment: + name: ci-unsigned + deployment: false + needs: [pick, playwright-e2e-enterprise] + # Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret. + if: >- + always() && needs.pick.outputs.is_fork != 'true' + && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }} + timeout-minutes: 60 + env: + PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }} + PREMIUM_ENABLED: "true" + SYSTEM_ENABLEANALYTICS: "false" + DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }} + MN_COMPOSE: docker-compose-multinode.yml + steps: + - name: Harden Runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock + - name: Install behave test deps + run: | + uv sync --project engine --locked --group cucumber + - name: Build the multi-node image + working-directory: testing/compose + run: docker compose -f "$MN_COMPOSE" build + - name: Bring up the cluster and wait for both nodes healthy + working-directory: testing/compose + run: | + docker compose -f "$MN_COMPOSE" up -d + for i in $(seq 1 90); do + h1=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-1 2>/dev/null || echo starting) + h2=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-2 2>/dev/null || echo starting) + if [ "$h1" = healthy ] && [ "$h2" = healthy ]; then echo "both nodes healthy"; exit 0; fi + sleep 5 + done + echo "::error::nodes did not become healthy" + docker compose -f "$MN_COMPOSE" logs --tail=200 stirling-1 stirling-2 + exit 1 + - name: Seed the cluster (teams, users, S3 connection, policy) + working-directory: testing/compose + run: docker compose -f "$MN_COMPOSE" --profile seed run --rm seed + - name: Run multi-node regression (implemented guarantees) + working-directory: testing/cucumber + # -e overrides behave.ini's exclusion of features/multinode; ~@known_gap skips any tracked-gap scenarios. + run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain + - name: Run multi-node failover (destructive) + working-directory: testing/cucumber + run: uv run --project ../../engine --locked --group cucumber python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain + - name: Dump node logs on failure + if: failure() + working-directory: testing/compose + run: docker compose -f "$MN_COMPOSE" logs --tail=400 stirling-1 stirling-2 + - name: Tear down + if: always() + working-directory: testing/compose + run: docker compose -f "$MN_COMPOSE" --profile seed down -v --remove-orphans diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d4130f214..d35b2ee177 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,7 @@ jobs: timeout-minutes: 3 outputs: build: ${{ steps.changes.outputs.build }} + backend: ${{ steps.changes.outputs.backend }} project: ${{ steps.changes.outputs.project }} openapi: ${{ steps.changes.outputs.openapi }} frontend: ${{ steps.changes.outputs.frontend }} @@ -48,20 +49,26 @@ jobs: proprietary: ${{ steps.changes.outputs.proprietary }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check for file changes - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 id: changes with: filters: .github/config/.files.yaml - build: + gradle-cache-prime: needs: [files-changed] + uses: ./.github/workflows/gradle-cache-prime.yml + secrets: inherit + + build: + if: needs.files-changed.outputs.backend == 'true' + needs: [files-changed, gradle-cache-prime] permissions: actions: read contents: read @@ -76,7 +83,7 @@ jobs: # works after Hibernate's ddl-auto=update migrates the schema. Gated on # the `project` filter so doc-only PRs skip this ~5-minute job. if: needs.files-changed.outputs.project == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/db-migration-test.yml @@ -84,7 +91,7 @@ jobs: check-generateOpenApiDocs: if: needs.files-changed.outputs.openapi == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-openapi.yml @@ -99,6 +106,18 @@ jobs: uses: ./.github/workflows/frontend-validation.yml secrets: inherit + # Required (in all-checks-passed). Scans the stories a branch touches in both + # light and dark; an axe violation in either theme blocks the merge. The + # whole-suite sweep (nightly.yml) still covers stories a change affects without + # touching them directly. + frontend-a11y: + if: needs.files-changed.outputs.frontend == 'true' + needs: [files-changed] + permissions: + contents: read + uses: ./.github/workflows/frontend-a11y.yml + secrets: inherit + playwright-e2e: if: needs.files-changed.outputs.frontend == 'true' needs: [files-changed] @@ -109,7 +128,7 @@ jobs: playwright-e2e-live: if: needs.files-changed.outputs.frontend == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/e2e-live.yml @@ -117,15 +136,17 @@ jobs: playwright-e2e-enterprise: if: needs.files-changed.outputs.proprietary == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/build-enterprise.yml secrets: inherit + with: + use_shared_cache: true check-licence: if: needs.files-changed.outputs.build == 'true' - needs: [files-changed, build] + needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-licence.yml @@ -133,7 +154,7 @@ jobs: docker-compose-tests: if: needs.files-changed.outputs.project == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: actions: write contents: read @@ -144,8 +165,22 @@ jobs: docker-base-changed: ${{ needs.files-changed.outputs.docker-base }} test-build-docker-images: - if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true' - needs: [files-changed, build, check-generateOpenApiDocs, check-licence] + if: | + always() && + github.event_name == 'pull_request' && + needs.files-changed.outputs.project == 'true' && + contains(fromJSON('["success", "skipped"]'), needs.gradle-cache-prime.result) && + contains(fromJSON('["success", "skipped"]'), needs.build.result) && + contains(fromJSON('["success", "skipped"]'), needs.check-generateOpenApiDocs.result) && + contains(fromJSON('["success", "skipped"]'), needs.check-licence.result) + needs: + [ + files-changed, + build, + check-generateOpenApiDocs, + check-licence, + gradle-cache-prime, + ] permissions: contents: read packages: read @@ -157,19 +192,21 @@ jobs: tauri-build: if: needs.files-changed.outputs.tauri == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write uses: ./.github/workflows/tauri-build.yml secrets: inherit - # PR smoke build: Linux only (fastest + cheapest to compile), unsigned, - # deb-only, no AppImage. The full signed multi-OS matrix runs on release; + # PR smoke build: macOS + Windows (the platforms our developers use). + # sign: true only reaches macOS - tauri-build's per-platform gate keeps + # Windows/Linux signing on main, and an unsigned .dmg cannot be opened. + # The full signed multi-OS matrix runs on release; # nightly still warms the Rust cache with all-OS defaults. with: - platform: linux - sign: false - minimal: true + platform: windows-macos + sign: true + use_shared_cache: true ai-engine: if: needs.files-changed.outputs.engine == 'true' @@ -187,12 +224,14 @@ jobs: # frontend filter, so a CSS-only PR does not pay for a backend build. generated-models: if: needs.files-changed.outputs.generated-models == 'true' - needs: [files-changed] + needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write uses: ./.github/workflows/check-generated-models.yml secrets: inherit + with: + use_shared_cache: true pre-commit: needs: [files-changed] @@ -239,10 +278,12 @@ jobs: if: always() needs: - files-changed + - gradle-cache-prime - build - db-migration-test - check-generateOpenApiDocs - frontend-validation + - frontend-a11y - playwright-e2e - playwright-e2e-live - playwright-e2e-enterprise @@ -257,7 +298,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -265,10 +306,12 @@ jobs: env: RESULTS: | files-changed=${{ needs.files-changed.result }} + gradle-cache-prime=${{ needs.gradle-cache-prime.result }} build=${{ needs.build.result }} db-migration-test=${{ needs.db-migration-test.result }} check-generateOpenApiDocs=${{ needs.check-generateOpenApiDocs.result }} frontend-validation=${{ needs.frontend-validation.result }} + frontend-a11y=${{ needs.frontend-a11y.result }} playwright-e2e=${{ needs.playwright-e2e.result }} playwright-e2e-live=${{ needs.playwright-e2e-live.result }} playwright-e2e-enterprise=${{ needs.playwright-e2e-enterprise.result }} diff --git a/.github/workflows/check-generated-models.yml b/.github/workflows/check-generated-models.yml index eef119cd02..4428704b10 100644 --- a/.github/workflows/check-generated-models.yml +++ b/.github/workflows/check-generated-models.yml @@ -1,14 +1,19 @@ name: Check generated models -# Verifies the committed generated API models are still in sync with the Java -# OpenAPI spec: the frontend tool API types -# (frontend/editor/src/core/types/toolApiTypes.ts) and the engine tool -# models (engine/src/stirling/models/tool_models.py). Regenerates both with the -# single top-level `task tool-models` and fails if either committed file is -# out of date. Called from build.yml when the backend Java, frontend, or engine -# changes; also runs on push to main as a post-merge safety net. +# Verifies the committed generated files are still in sync with the Java OpenAPI +# spec: the request models (toolApiTypes.ts, tool_models.py) and the tool I/O +# tables saying what each endpoint accepts and produces (toolIO.ts, tool_io.py). +# Regenerates them all with the single top-level `task tool-models` and fails if +# any committed file is out of date. Called from build.yml when the +# backend Java, frontend, or engine changes; also runs on push to main as a +# post-merge safety net. on: workflow_call: + inputs: + use_shared_cache: + required: false + type: boolean + default: false push: branches: [main] @@ -23,7 +28,7 @@ jobs: pull-requests: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -31,24 +36,40 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock cache-suffix: generated-models + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Restore cache Gradle + if: inputs.use_shared_cache == false + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-generated-models-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.0 - - name: Set up Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -57,18 +78,10 @@ jobs: - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - # Rebuilds the OpenAPI spec from the current Java and regenerates both the - # frontend types and the engine tool models from it. - - name: Regenerate generated models - run: task tool-models - - name: Verify generated models are up to date id: models-check continue-on-error: true - run: | - git diff --exit-code \ - frontend/editor/src/core/types/toolApiTypes.ts \ - engine/src/stirling/models/tool_models.py + run: task tool-models:check - name: Comment on generated models check failure # Only post a comment on PRs. github-script's PR helpers need an @@ -83,9 +96,9 @@ jobs: marker, '### Generated Models Check Failed', '', - 'The generated `frontend/editor/src/core/types/toolApiTypes.ts` and/or `engine/src/stirling/models/tool_models.py` are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.', + 'One or more generated files are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.', '', - 'Run `task tool-models` to regenerate both, then commit the updated files.', + 'Run `task tool-models` to regenerate them, then commit the updated files.', ].join('\n'); const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, @@ -116,11 +129,10 @@ jobs: echo " Generated Models Check Failed" echo "============================================" echo "" - echo "The generated frontend API types and/or engine tool" - echo "models are out of date with the Java OpenAPI spec and" - echo "will need to be regenerated before they can be merged in." + echo "One or more generated files are out of date with the Java" + echo "OpenAPI spec and will need to be regenerated before merging." echo "" - echo "Run 'task tool-models' to regenerate both, then" + echo "Run 'task tool-models' to regenerate them, then" echo "commit the updated files." echo "============================================" exit 1 diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index 9f6cfb39a0..4e04a83656 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -10,37 +10,33 @@ permissions: jobs: check-licence: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Check licenses for compatibility diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index d853afa86f..bc9b302857 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -11,37 +11,33 @@ permissions: jobs: check-generate-openapi-docs: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Generate OpenAPI documentation diff --git a/.github/workflows/check_toml.yml b/.github/workflows/check_toml.yml index b7a277f873..9ccfc4af52 100644 --- a/.github/workflows/check_toml.yml +++ b/.github/workflows/check_toml.yml @@ -23,29 +23,23 @@ jobs: if: github.event_name == 'pull_request_target' runs-on: ubuntu-latest permissions: + contents: read # Checkout, and read translation files via the contents API issues: write # Allow posting comments on issues/PRs pull-requests: write # Allow writing to pull requests steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout main branch first uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup GitHub App Bot - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Get PR data id: get-pr-data uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const prNumber = context.payload.pull_request.number; const repoOwner = context.payload.repository.owner.login; @@ -66,17 +60,18 @@ jobs: - name: Fetch PR changed files id: fetch-pr-changes env: - GH_TOKEN: ${{ steps.setup-bot.outputs.token }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }} run: | echo "Fetching PR changed files..." echo "Getting list of changed files from PR..." # Check if PR number exists - if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then + if [ -z "${PR_NUMBER}" ]; then echo "Error: PR number is empty" exit 1 fi # Get changed files and filter for TOML translation files - gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR" + gh pr view "${PR_NUMBER}" --json files -q ".files[].path" | grep -E '^frontend/editor/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR" # Check if any files were found if [ ! -s changed_files.txt ]; then echo "No TOML translation files changed in this PR" @@ -88,32 +83,36 @@ jobs: - name: Determine reference file id: determine-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + # Untrusted, fork-controlled values are passed via env, never interpolated into the script + PR_NUMBER: ${{ steps.get-pr-data.outputs.pr_number }} + REPO_OWNER: ${{ steps.get-pr-data.outputs.repo_owner }} + REPO_NAME: ${{ steps.get-pr-data.outputs.repo_name }} + PR_REPO_OWNER: ${{ github.event.pull_request.head.repo.owner.login }} + PR_REPO_NAME: ${{ github.event.pull_request.head.repo.name }} + PR_BRANCH: ${{ steps.get-pr-data.outputs.branch }} with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const fs = require("fs"); const path = require("path"); - const prNumber = ${{ steps.get-pr-data.outputs.pr_number }}; - const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}"; - const repoName = "${{ steps.get-pr-data.outputs.repo_name }}"; - - const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}"; - const prRepoName = "${{ github.event.pull_request.head.repo.name }}"; - const branch = "${{ steps.get-pr-data.outputs.branch }}"; - - console.log(`Determining reference file for PR #${prNumber}`); - - // Validate inputs + // Validate inputs before any use const validateInput = (input, regex, name) => { - if (!regex.test(input)) { + if (typeof input !== "string" || !regex.test(input)) { throw new Error(`Invalid ${name}: ${input}`); } + return input; }; - validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner"); - validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name"); - validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name"); + const repoOwner = validateInput(process.env.REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "repository owner"); + const repoName = validateInput(process.env.REPO_NAME, /^[a-zA-Z0-9._-]+$/, "repository name"); + const prRepoOwner = validateInput(process.env.PR_REPO_OWNER, /^[a-zA-Z0-9_-]+$/, "PR repository owner"); + const prRepoName = validateInput(process.env.PR_REPO_NAME, /^[a-zA-Z0-9._-]+$/, "PR repository name"); + const branch = validateInput(process.env.PR_BRANCH, /^[a-zA-Z0-9._/-]+$/, "branch name"); + const prNumber = Number(validateInput(process.env.PR_NUMBER, /^[0-9]+$/, "PR number")); + + console.log(`Determining reference file for PR #${prNumber}`); // Get the list of changed files in the PR const { data: files } = await github.rest.pulls.listFiles({ @@ -126,7 +125,7 @@ jobs: const changedFiles = files .filter(file => file.status !== "removed" && - /^frontend\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename) + /^frontend\/editor\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename) ) .map(file => file.filename); @@ -195,21 +194,26 @@ jobs: console.log(`Reference file path: ${referenceFilePath}`); core.exportVariable("REFERENCE_FILE", referenceFilePath); - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - python-version: "3.12" + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - name: Install Python dependencies run: | - pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt + uv sync --project engine --locked --group tools - name: Run Python script to check files id: run-check + env: + PR_ACTOR: ${{ github.event.pull_request.user.login }} run: | echo "Running Python script to check TOML files..." - python .github/scripts/check_language_toml.py \ - --actor ${{ github.event.pull_request.user.login }} \ + uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py \ + --actor "${PR_ACTOR}" \ --reference-file "${REFERENCE_FILE}" \ --branch "pr-branch" \ --files "${FILES_LIST[@]}" > result.txt @@ -242,7 +246,7 @@ jobs: if: env.SCRIPT_OUTPUT != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); @@ -258,7 +262,7 @@ jobs: const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary")); // Only update or create comments by the action user - const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]"; + const expectedActor = "github-actions[bot]"; if (comment && comment.user.login === expectedActor) { // Update existing comment diff --git a/.github/workflows/coverage-aggregate.yml b/.github/workflows/coverage-aggregate.yml index bbed57db4d..87fbb24eac 100644 --- a/.github/workflows/coverage-aggregate.yml +++ b/.github/workflows/coverage-aggregate.yml @@ -34,42 +34,33 @@ jobs: timeout-minutes: 15 steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.12" - - - name: Install defusedxml for coverage scripts - # Both coverage-summary.py and coverage-matrix.py parse JaCoCo - # XML through defusedxml - see the script headers for context. - run: python -m pip install --quiet defusedxml + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock # Pattern matches every artifact this PR's producers might upload: # jacoco-exec-junit-jdk-25 (uploaded only by the saas @@ -81,7 +72,7 @@ jobs: # Each lands as a sibling dir under coverage-execs/, with the .exec # files preserving their original relative paths. - name: Download all .exec artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: jacoco-exec-* path: coverage-execs/ @@ -162,7 +153,7 @@ jobs: # ("how much of the backend do real user flows cover?"). if: steps.inventory.outputs.found_e2e == 'true' run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Real user-flow backend coverage (e2e:live + cucumber)" \ --jacoco "merged=build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml" \ --github-step-summary @@ -173,7 +164,7 @@ jobs: # is meaningless when one is a strict superset of the other. if: steps.inventory.outputs.found_all == 'true' run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Combined backend coverage (JUnit + e2e:live + cucumber)" \ --jacoco "merged=build/reports/jacoco/aggregate-all/jacocoTestReport.xml" \ --github-step-summary @@ -206,7 +197,7 @@ jobs: # absence on backend-only runs by skipping the download entirely # when the producer job was not part of this workflow run. if: inputs.frontend-validation-result == 'success' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend-coverage path: matrix-inputs/vitest/ @@ -216,7 +207,7 @@ jobs: # e2e-live uploads the artifact with a stable name. Skip the # download entirely when the producer job did not run. if: inputs.playwright-e2e-live-result == 'success' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: playwright-frontend-coverage path: matrix-inputs/playwright/ @@ -228,7 +219,7 @@ jobs: # generated above) plus whichever frontend artifacts landed. # Every input is optional; missing ones render as "-". run: | - python scripts/coverage-matrix.py \ + uv run --project engine --locked --group tools python scripts/coverage-matrix.py \ ${{ steps.inventory.outputs.found_all == 'true' && '--jacoco-all build/reports/jacoco/aggregate-all/jacocoTestReport.xml' || '' }} \ ${{ steps.inventory.outputs.found_e2e == 'true' && '--jacoco-e2e build/reports/jacoco/aggregate-e2e/jacocoTestReport.xml' || '' }} \ --vitest matrix-inputs/vitest/coverage-summary.json \ diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index c6ebc481a7..7176163440 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -13,40 +13,36 @@ permissions: jobs: migration-test: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: 25 distribution: temurin - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - # No `-PnoSpotless` here yet because the upstream cache layer matches the - # backend build's; reuse keeps cold-cache cost identical. + # Keep the normal formatting path here so this smoke test exercises the + # same Gradle configuration as the backend build. - name: Build Stirling-PDF JAR env: MAVEN_USER: ${{ secrets.MAVEN_USER }} @@ -85,3 +81,8 @@ jobs: path: /tmp/stirling-migration-failed-*/app.log retention-days: 7 if-no-files-found: warn + + - name: Cleanup temporary files + if: always() + run: rm -rf /tmp/stirling-migration-failed-* + continue-on-error: true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1449123eff..b27596ddfb 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/deploy-on-v2-commit.yml b/.github/workflows/deploy-on-v2-commit.yml deleted file mode 100644 index 2dcbe2ae65..0000000000 --- a/.github/workflows/deploy-on-v2-commit.yml +++ /dev/null @@ -1,189 +0,0 @@ -name: Auto V2 Deploy on Push - -on: - push: - branches: - - V2 - - deploy-on-v2-commit - -permissions: - contents: read - -jobs: - deploy-v2-on-push: - runs-on: ubuntu-latest - concurrency: - group: deploy-v2-push-V2 - cancel-in-progress: true - - steps: - - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Get commit hashes for frontend and backend - id: commit-hashes - run: | - # Get last commit that touched the frontend folder, docker/frontend, or docker/compose - FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "") - if [ -z "$FRONTEND_HASH" ]; then - FRONTEND_HASH="no-frontend-changes" - fi - - # Get last commit that touched backend code, docker/backend, or docker/compose - BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "") - if [ -z "$BACKEND_HASH" ]; then - BACKEND_HASH="no-backend-changes" - fi - - echo "Frontend hash: $FRONTEND_HASH" - echo "Backend hash: $BACKEND_HASH" - - echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT - echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT - - # Short hashes for tags - if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then - echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT - else - echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT - fi - - if [ "$BACKEND_HASH" = "no-backend-changes" ]; then - echo "backend_short=no-backend" >> $GITHUB_OUTPUT - else - echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT - fi - - - name: Check if frontend image exists - id: check-frontend - run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Frontend image already exists, skipping build" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Frontend image needs to be built" - fi - - - name: Check if backend image exists - id: check-backend - run: | - if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Backend image already exists, skipping build" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Backend image needs to be built" - fi - - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} - - - name: Build and push frontend image - if: steps.check-frontend.outputs.exists == 'false' - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ./docker/frontend/Dockerfile - push: true - cache-from: type=gha,scope=stirling-v2-frontend - cache-to: type=gha,mode=max,scope=stirling-v2-frontend - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest - build-args: VERSION_TAG=v2-alpha - platforms: linux/amd64 - - - name: Build and push backend image - if: steps.check-backend.outputs.exists == 'false' - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ./docker/backend/Dockerfile - push: true - cache-from: type=gha,scope=stirling-v2-backend - cache-to: type=gha,mode=max,scope=stirling-v2-backend - tags: | - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} - ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest - build-args: VERSION_TAG=v2-alpha - platforms: linux/amd64 - - - name: Set up SSH - run: | - mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key - chmod 600 ../private.key - - - name: Deploy to VPS on port 3000 - run: | - export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml - - cat > $UNIQUE_NAME << EOF - version: '3.3' - services: - backend: - container_name: stirling-v2-backend - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} - ports: - - "13000:8080" - volumes: - - /stirling/V2/data:/usr/share/tessdata:rw - - /stirling/V2/config:/configs:rw - - /stirling/V2/logs:/logs:rw - environment: - DISABLE_ADDITIONAL_FEATURES: "true" - SECURITY_ENABLELOGIN: "false" - SYSTEM_DEFAULTLOCALE: en-US - UI_APPNAME: "Stirling-PDF V2" - UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split" - UI_APPNAMENAVBAR: "V2 Deployment" - SYSTEM_MAXFILESIZE: "100" - METRICS_ENABLED: "true" - SYSTEM_GOOGLEVISIBILITY: "false" - SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud" - baseUrl: "https://demo.stirlingpdf.cloud" - restart: on-failure:5 - - frontend: - container_name: stirling-v2-frontend - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} - ports: - - "3000:80" - environment: - VITE_API_BASE_URL: "http://${{ secrets.NEW_VPS_HOST }}:13000" - depends_on: - - backend - restart: on-failure:5 - EOF - - # Copy to remote with unique name - scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/$UNIQUE_NAME - - # SSH and rename/move atomically to avoid interference - ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH - mkdir -p /stirling/V2/{data,config,logs} - mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml - cd /stirling/V2 - docker-compose down || true - docker-compose pull - docker-compose up -d - docker system prune -af --volumes || true - docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true - ENDSSH - - - name: Cleanup temporary files - if: always() - run: | - rm -f ../private.key diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 2db9e5cf72..54f2e6b5f6 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -17,6 +17,9 @@ permissions: jobs: docker-compose-tests: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest permissions: actions: write @@ -25,34 +28,27 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - # When the PR changes the base image, test.sh builds it locally # (stirling-pdf-base:local) into the daemon image store. A buildx # container builder can't see that store, so skip it here and let @@ -61,7 +57,7 @@ jobs: # runtime token isn't exposed) since the docker driver can't use it. - name: Set up Docker Buildx if: inputs.docker-base-changed != 'true' - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 # Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend. - name: Expose GitHub runtime for Buildx cache @@ -70,19 +66,20 @@ jobs: - name: Install Docker Compose run: | - sudo curl -SL "https://github.com/docker/compose/releases/download/v2.39.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo curl -SL "https://github.com/docker/compose/releases/download/v5.4.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - python-version: "3.12" - cache: "pip" # caching pip dependencies - cache-dependency-path: ./testing/cucumber/requirements.txt + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - - name: Pip requirements + - name: Install Cucumber and coverage dependencies run: | - pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt + uv sync --project engine --locked --group cucumber --group tools - name: Extract JaCoCo agent for cucumber coverage # Stages build/jacoco/jacocoagent.jar where the coverage override @@ -125,16 +122,10 @@ jobs: echo "report=false" >> "$GITHUB_OUTPUT" fi - - name: Install defusedxml for coverage summary - # coverage-summary.py parses JaCoCo XML through defusedxml - - # see the script header for context. - if: always() && steps.cucumber-coverage.outputs.report == 'true' - run: python -m pip install --quiet defusedxml - - name: Cucumber coverage step summary if: always() && steps.cucumber-coverage.outputs.report == 'true' run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Cucumber (docker) JaCoCo coverage" \ --jacoco "cucumber=build/reports/jacoco/cucumber/jacocoTestReport.xml" \ --github-step-summary diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 3a1b4e3af1..29668940c4 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -11,22 +11,35 @@ permissions: jobs: playwright-e2e-live: + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" + - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -53,6 +66,11 @@ jobs: # to aggregate. Chromium-only - other engines silently skip. PW_COVERAGE: "1" PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json + # Internal mirror, as in backend-build.yml. Empty on Dependabot and + # fork PRs, where the build falls back to Maven Central. + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} run: task e2e:live - name: Flag flaky tests # Runs regardless of the test outcome: a flaky test (passed on retry) @@ -66,6 +84,10 @@ jobs: - name: Generate JaCoCo report from e2e:live .exec if: always() id: live-coverage + env: + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} # `if: always()` so even a failed test run still produces a # report from whatever flows did exercise the backend before # the failure. The task itself tolerates a missing .exec @@ -82,20 +104,18 @@ jobs: echo "::warning::No e2e:live .exec found at .test-state/playwright/jacoco.exec; skipping report" echo "report=false" >> "$GITHUB_OUTPUT" fi - - name: Set up Python for coverage summary - if: always() && steps.live-coverage.outputs.report == 'true' - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Install uv + if: always() + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - python-version: "3.12" - - name: Install defusedxml for coverage summary - # coverage-summary.py uses defusedxml instead of stdlib xml.etree - # to dodge XXE / billion-laughs scanner findings. - if: always() && steps.live-coverage.outputs.report == 'true' - run: python -m pip install --quiet defusedxml + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - name: e2e:live coverage step summary if: always() && steps.live-coverage.outputs.report == 'true' run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Playwright (live backend) JaCoCo coverage" \ --jacoco "e2e-live=build/reports/jacoco/e2e-live/jacocoTestReport.xml" \ --github-step-summary @@ -118,23 +138,6 @@ jobs: retention-days: 7 if-no-files-found: warn - - name: Set up Python for frontend coverage summary - # Separate from the backend-coverage python step because the - # frontend path doesn't depend on a JaCoCo report - it produces - # a summary even on backend failure, as long as some Playwright - # tests ran far enough to dump V8 coverage. - if: always() - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.12" - - - name: Install defusedxml for frontend coverage summary - # Idempotent re-install: the backend-coverage step may have - # installed it already, but this leg can run on its own when the - # backend report step skips (e.g. .exec missing). - if: always() - run: python -m pip install --quiet defusedxml - - name: Aggregate Playwright frontend (V8) coverage # Rolls per-test V8 dumps from the test-base fixture into one # vitest-shaped coverage-summary.json. Tolerates a missing dump @@ -145,7 +148,7 @@ jobs: run: | if [ -d .test-state/playwright/coverage-pw ] && \ find .test-state/playwright/coverage-pw -name '*.json' -type f | grep -q .; then - python scripts/playwright-coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/playwright-coverage-summary.py \ .test-state/playwright/coverage-pw \ --out .test-state/playwright/coverage-pw-summary/coverage-summary.json echo "summary=true" >> "$GITHUB_OUTPUT" @@ -157,10 +160,10 @@ jobs: - name: Playwright frontend coverage step summary if: always() && steps.pw-frontend-coverage.outputs.summary == 'true' run: | - python scripts/coverage-summary.py \ - --title "Playwright (live) frontend coverage" \ - --vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \ - --github-step-summary + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ + --title "Playwright (live) frontend coverage" \ + --vitest .test-state/playwright/coverage-pw-summary/coverage-summary.json \ + --github-step-summary - name: Upload Playwright frontend coverage # Bundle both the aggregated summary and the raw V8 dumps so diff --git a/.github/workflows/e2e-stubbed.yml b/.github/workflows/e2e-stubbed.yml index 214a7188df..b62c3e72ec 100644 --- a/.github/workflows/e2e-stubbed.yml +++ b/.github/workflows/e2e-stubbed.yml @@ -2,7 +2,8 @@ name: Playwright E2E (stubbed) # Reusable workflow called from build.yml. Backend-free Playwright suite — # fast, no Spring Boot required. Runs against the `stubbed` project which -# mocks API responses in the browser. +# mocks API responses in the browser. Fans out one job per browser +# (chromium/firefox/webkit) so all three run in parallel on their own runner. on: workflow_call: @@ -11,32 +12,57 @@ permissions: jobs: playwright-e2e: + name: playwright-e2e (${{ matrix.browser }}) runs-on: ubuntu-latest + # The image already contains the Playwright browsers and all Linux + # dependencies. This keeps the matrix for per-browser reporting while + # avoiding three concurrent `playwright install --with-deps` runs. + container: + image: mcr.microsoft.com/playwright:v1.58.2-noble@sha256:6446946a1d9fd62d9ae501312a2d76a43ee688542b21622056a372959b65d63d + strategy: + # One browser breaking must not mask a failure in another - report all. + fail-fast: false + matrix: + include: + - browser: chromium + project: stubbed + - browser: firefox + project: stubbed-firefox + - browser: webkit + project: stubbed-webkit steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" cache-dependency-path: frontend/package-lock.json - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - - name: Install Playwright (chromium only) - run: task e2e:install -- chromium - name: Build frontend (production bundle for vite preview) env: VITE_BUILD_FOR_PREVIEW: "1" run: task frontend:build - - name: Run stubbed E2E tests (chromium) + - name: Run stubbed E2E tests (${{ matrix.browser }}) env: + # The official Playwright image expects its browser runtime under + # the root home directory. Keep this scoped to Playwright and use a + # neutral Docker config path so Docker does not read /root/.docker. + HOME: /root + DOCKER_CONFIG: /tmp/playwright-docker-config PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json - run: task e2e:stubbed -- --workers=3 + NPM_CONFIG_PREFER_OFFLINE: "true" + NPM_CONFIG_FETCH_RETRIES: "5" + NPM_CONFIG_FETCH_RETRY_FACTOR: "2" + NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: "1000" + NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: "120000" + run: task e2e:stubbed-project PROJECT=${{ matrix.project }} -- --workers=3 - name: Flag flaky tests # Runs regardless of the test outcome: a flaky test (passed on retry) # leaves the step green, so this is the only place it surfaces. Emits @@ -50,6 +76,6 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-report-stubbed-${{ github.run_id }} + name: playwright-report-stubbed-${{ matrix.browser }}-${{ github.run_id }} path: frontend/playwright-report/ retention-days: 7 diff --git a/.github/workflows/frontend-a11y.yml b/.github/workflows/frontend-a11y.yml new file mode 100644 index 0000000000..8f97120d96 --- /dev/null +++ b/.github/workflows/frontend-a11y.yml @@ -0,0 +1,55 @@ +name: Frontend a11y regression gate + +# Reusable workflow called from build.yml when frontend sources change. +# +# Scans the stories this branch touches in real Chromium and runs axe against +# each; the check fails on any axe violation, or on a story that fails to render +# at all. +# +# Only changed stories, because a full sweep is ~30 minutes: far too slow to sit +# in front of every merge. The whole suite is scanned nightly instead +# (nightly.yml), which catches anything a branch didn't touch. +on: + workflow_call: + +permissions: + contents: read + +jobs: + frontend-a11y: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Harden Runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Need the base branch too, to diff against it. + fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + - name: Install Task + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + - name: a11y gate (changed stories) + run: task frontend:storybook:a11y:changed -- origin/${{ github.base_ref || 'main' }} + - name: Upload scan reports + # The reports carry the offending selector and help text for each + # violation; without them a red run can only be understood by + # reproducing the whole scan locally. + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: a11y-scan-${{ github.run_id }} + path: frontend/.a11y-scan/ + retention-days: 7 + if-no-files-found: ignore + # The reports live in a dot-directory, which upload-artifact treats as + # hidden and silently skips by default. + include-hidden-files: true diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index fcf6e18d0e..9aef2316d2 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -28,7 +28,7 @@ jobs: licenses-backend: ${{ steps.changes.outputs.licenses-backend }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -36,12 +36,16 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check for file changes - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 id: changes with: filters: .github/config/.files.yaml generate-frontend-license-report: + # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. + environment: + name: ci-bot + deployment: false if: needs.files-changed.outputs.licenses-frontend == 'true' name: Generate Frontend License Report needs: files-changed @@ -52,7 +56,7 @@ jobs: repository-projects: write # Required for enabling automerge steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -80,7 +84,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -316,6 +320,10 @@ jobs: GH_TOKEN: ${{ steps.setup-bot.outputs.token }} generate-backend-license-report: + # ci-bot, not bot-identity: this job runs on PRs too, and bot-identity is main-only. + environment: + name: ci-bot + deployment: false if: needs.files-changed.outputs.licenses-backend == 'true' needs: files-changed name: Generate Backend License Report @@ -326,7 +334,7 @@ jobs: repository-projects: write # Required for enabling automerge steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -344,17 +352,20 @@ jobs: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-license-report-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/frontend-validation.yml b/.github/workflows/frontend-validation.yml index a539a1b7b7..0cad599a1c 100644 --- a/.github/workflows/frontend-validation.yml +++ b/.github/workflows/frontend-validation.yml @@ -1,8 +1,8 @@ name: Frontend lint, type-check, and build # Reusable workflow called from build.yml when frontend / testing sources -# change. Runs the consolidated `task frontend:check:all` (lint, types, -# unit tests, build) and uploads the dist artifact for downstream jobs. +# change. Runs `task frontend:check:all` and uploads the +# coverage + dist artifacts for downstream jobs. on: workflow_call: @@ -15,13 +15,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -105,30 +105,18 @@ jobs: comment_id: existing.id, }); } - - name: Vitest coverage - # Separate from `frontend:check:all` so the quality-gate run stays - # uninstrumented (faster signal) and coverage stays an informational - # follow-up. Continue-on-error keeps the workflow green even when - # a handful of test files refuse to import (e.g. missing icon - # specifiers) - the summary still gets posted with whatever - # vitest managed to instrument. - id: frontend-coverage - continue-on-error: true - run: task frontend:test:coverage - - name: Set up Python for coverage summary + - name: Install uv if: always() - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - python-version: "3.12" - - name: Install defusedxml for coverage summary - # See coverage-summary.py header - it parses XML through defusedxml - # to dodge the stdlib parser's exposure to XXE / billion-laughs. - if: always() - run: python -m pip install --quiet defusedxml + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - name: Vitest coverage step summary if: always() run: | - python scripts/coverage-summary.py \ + uv run --project engine --locked --group tools python scripts/coverage-summary.py \ --title "Frontend Vitest coverage" \ --vitest frontend/editor/coverage/coverage-summary.json \ --github-step-summary diff --git a/.github/workflows/gradle-cache-prime.yml b/.github/workflows/gradle-cache-prime.yml new file mode 100644 index 0000000000..b3700d8ac0 --- /dev/null +++ b/.github/workflows/gradle-cache-prime.yml @@ -0,0 +1,66 @@ +name: Prime Gradle Cache + +on: + workflow_call: + push: + branches: ["main"] + +permissions: + contents: read + +jobs: + gradle-cache-prime: + environment: + name: ci-unsigned + deployment: false + name: Prime shared Gradle cache + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Calculate Gradle cache key + id: gradle-cache-key + shell: bash + run: | + echo "key=gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}" >> "$GITHUB_OUTPUT" + + - name: Cache Gradle (lookup-only) + id: cache-gradle-restore + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ steps.gradle-cache-key.outputs.key }} + lookup-only: true + + - name: Set up JDK 25 + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" + + - name: Resolve backend dependencies + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + run: ./gradlew :stirling-pdf:classes --no-daemon + env: + STIRLING_FLAVOR: saas + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + + - name: Save cache Gradle User Home + if: steps.cache-gradle-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ steps.gradle-cache-key.outputs.key }} diff --git a/.github/workflows/manage-label.yml b/.github/workflows/manage-label.yml index c433346711..1a8bdd112d 100644 --- a/.github/workflows/manage-label.yml +++ b/.github/workflows/manage-label.yml @@ -15,7 +15,7 @@ jobs: issues: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 41ddeb7a13..8e1c47b412 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -12,13 +12,14 @@ on: - "true" - "false" platform: - description: "Platform to build (windows, macos, linux, or all)" + description: "Platform to build (windows, windows-arm64, macos, linux, or all)" required: true default: "all" type: choice options: - all - windows + - windows-arm64 - macos - linux sign: @@ -37,6 +38,9 @@ permissions: jobs: determine-matrix: + environment: + name: ci-unsigned + deployment: false if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: @@ -44,32 +48,25 @@ jobs: version: ${{ steps.versionNumber.outputs.versionNumber }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle dependencies + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: - gradle-version: 9.6.1 + java-version: "25" + distribution: "temurin" - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -87,27 +84,43 @@ jobs: - name: Determine build matrix id: set-matrix run: | + # windows-arm64: NSIS only (WiX MSI has no arm64 support in Tauri) and no + # JPDFium natives yet - flip to windows-arm64 once JPDFium ships them. + WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}' + WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}' + MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}' + LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}' + ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - case "${{ github.event.inputs.platform }}" in + case "${INPUT_PLATFORM}" in "windows") - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT + ;; + "windows-arm64") + echo "matrix={\"include\":[$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT ;; "macos") - echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$MACOS]}" >> $GITHUB_OUTPUT ;; "linux") - echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$LINUX]}" >> $GITHUB_OUTPUT ;; *) - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT ;; esac else # For push/release events, build all platforms - echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT fi + env: + INPUT_PLATFORM: ${{ github.event.inputs.platform }} build-jars: + environment: + name: ci-unsigned + deployment: false needs: determine-matrix runs-on: ubuntu-latest strategy: @@ -127,26 +140,29 @@ jobs: file_suffix: "-server" steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - - name: Setup Node.js if: matrix.variant.build_frontend == true - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: "npm" @@ -180,6 +196,7 @@ jobs: retention-days: 1 build: + environment: release-signing needs: determine-matrix strategy: fail-fast: false @@ -187,11 +204,10 @@ jobs: runs-on: ${{ matrix.platform }} env: SM_API_KEY: ${{ secrets.SM_API_KEY }} - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit allowed-endpoints: > @@ -208,7 +224,7 @@ jobs: sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: "npm" @@ -220,13 +236,21 @@ jobs: toolchain: stable targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + # x86_64 JDK is set up first so the aarch64 step below can leave its # JAVA_HOME as the active one. The macOS universal JRE build needs # jmods from both arches; the x64 path is captured into the env # before the second setup-java overwrites JAVA_HOME. - name: Set up x86_64 JDK 25 (macOS universal JRE) if: matrix.platform == 'macos-15' - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" @@ -236,16 +260,12 @@ jobs: if: matrix.platform == 'macos-15' run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does. - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" - distribution: "temurin" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 + distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -271,7 +291,7 @@ jobs: # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -281,22 +301,22 @@ jobs: SM_HOST: ${{ secrets.SM_HOST }} - name: Setup DigiCert KeyLocker Certificate - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: pwsh run: | Write-Host "Setting up DigiCert KeyLocker environment..." # Decode client certificate - $certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}") + $certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64") $certPath = "D:\Certificate_pkcs12.p12" [IO.File]::WriteAllBytes($certPath, $certBytes) # Set environment variables echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV - echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV - echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV + echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV + echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV + echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV + echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV # Get PKCS11 config path from DigiCert action $pkcs11Config = $env:PKCS11_CONFIG @@ -314,40 +334,14 @@ jobs: } } - # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - - name: Import Windows Code Signing Certificate - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} - shell: powershell - run: | - if ($env:WINDOWS_CERTIFICATE) { - Write-Host "Importing Windows Code Signing Certificate..." - - # Decode base64 certificate and save to file - $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) - $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx" - [IO.File]::WriteAllBytes($certPath, $certBytes) - - # Import certificate to CurrentUser\My store - $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force) - - # Extract and set thumbprint as environment variable - $thumbprint = $cert.Thumbprint - Write-Host "Certificate imported with thumbprint: $thumbprint" - echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV - - # Clean up certificate file - Remove-Item $certPath - - Write-Host "Windows certificate import completed." - } else { - Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary" - } - + SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }} + SM_HOST: ${{ secrets.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} - name: Import Apple Developer Certificate - if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -368,7 +362,7 @@ jobs: rm certificate.p12 - name: Verify Certificate - if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'macos-15' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') run: | echo "Verifying Apple Developer Certificate..." KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db @@ -384,7 +378,7 @@ jobs: # Without this, signCommand failures are opaque (Tauri captures but drops # smctl's stderr) - running these loudly surfaces auth/env/keypair issues. - name: Preflight smctl - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: pwsh env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -415,7 +409,7 @@ jobs: # smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD # from env (set by prior DigiCert setup step). No --config-file needed. - name: Configure Windows code signing - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} shell: bash env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -436,7 +430,7 @@ jobs: sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/editor/src-tauri/tauri.windows.conf.json - name: Import release GPG signing key (Linux) - if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') run: | echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import gpg --list-secret-keys --keyid-format=long @@ -454,7 +448,7 @@ jobs: fi - name: Build Tauri app - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2 + uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} @@ -468,8 +462,8 @@ jobs: # APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively # SIGN_KEY appimagetool picks the key matching this fingerprint # Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present. - # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or V2-master, when secret is present. - SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }} + # Mirror the Windows/macOS gate: only sign on a real release/dispatch+sign or the release branch, when secret is present. + SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }} APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} @@ -484,10 +478,57 @@ jobs: projectPath: ./frontend/editor tauriScript: npx tauri args: ${{ matrix.args }} - updaterJsonKeepUniversal: true + + # Bundled libwayland conflicts with the host's on some distros (Fedora + # Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it, + # then regenerate the updater .sig (repack invalidates the original) and + # GPG-sign again when release signing is on. + - name: Strip bundled Wayland libs from AppImage + if: matrix.platform == 'ubuntu-22.04' + continue-on-error: true + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release')) && '1' || '0' }} + SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} + APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} + run: | + set -euo pipefail + AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1) + if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi + chmod +x "$AI" + WORK=$(mktemp -d) + (cd "$WORK" && "$AI" --appimage-extract >/dev/null) + if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then + echo "No bundled libwayland - nothing to strip" + rm -rf "$WORK" + exit 0 + fi + rm -f "$WORK/squashfs-root/usr/lib/"libwayland-* + curl -fsSL -o "$WORK/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + # Pinned checksum: never execute an unverified downloaded binary. On + # mismatch (upstream rebuilt continuous) the step aborts and the + # original AppImage ships unchanged - update the pin deliberately. + echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c - + chmod +x "$WORK/appimagetool" + SIGN_ARGS=() + if [ "$GPG_SIGN" = "1" ] && [ -n "${SIGN_KEY:-}" ]; then + SIGN_ARGS=(--sign --sign-key "$SIGN_KEY") + fi + "$WORK/appimagetool" --appimage-extract-and-run "${SIGN_ARGS[@]}" "$WORK/squashfs-root" "$AI.new" + # Updater payload signature must match the repacked bytes. The CLI + # reads the key/password from env - never pass secrets as argv. + if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then + (cd frontend && npx tauri signer sign "$AI.new") + mv "$AI.new.sig" "$AI.sig" + fi + mv "$AI.new" "$AI" + rm -rf "$WORK" + echo "Stripped bundled libwayland from $(basename "$AI")" - name: Clear release GPG key from runner keyring (Linux) - if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') + if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') env: RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }} run: | @@ -502,10 +543,31 @@ jobs: # artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw # cargo output unsigned, so checking it produces false negatives. - name: Verify Windows Code Signature - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }} + if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/release') }} timeout-minutes: 15 shell: pwsh run: | + # arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the + # inner exe before packing and the setup exe after, so verifying the setup + # exe is the arm64 equivalent of the MSI + inner-exe check below. + if ("${{ matrix.platform }}" -eq "windows-11-arm") { + $setupExes = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*-setup.exe" -Recurse -File + if ($setupExes.Count -eq 0) { + Write-Host "[ERROR] No NSIS installer found under target/" + exit 1 + } + foreach ($exe in $setupExes) { + $sig = Get-AuthenticodeSignature -FilePath $exe.FullName + Write-Host "NSIS installer: $($exe.Name) Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] NSIS installer is not signed" + exit 1 + } + } + Write-Host "[SUCCESS] NSIS installer is properly signed" + exit 0 + } + $allSigned = $true # Check MSI installer (outer wrapper - what users download) @@ -569,7 +631,7 @@ jobs: # but drops stderr when the command exits non-zero, making failures opaque. # The real errors live in smctl's log files - surface them here for debugging. - name: Dump smctl logs on failure - if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }} + if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }} shell: pwsh run: | $logDir = "$env:USERPROFILE\.signingmanager\logs" @@ -605,6 +667,11 @@ jobs: if [ "${{ matrix.platform }}" = "windows-latest" ]; then find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \; find . -name "*.msi.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi.sig" \; + elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then + # arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri). + # The setup exe is also its own updater payload (-> sibling .sig). + find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \; + find . -name "*-setup.exe.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe.sig" \; elif [ "${{ matrix.platform }}" = "macos-15" ]; then # DMG = manual install; .app.tar.gz (+ .sig) = updater payload. # Raw .app is intentionally not shipped (hundreds of MB of uncompressed input). @@ -630,6 +697,17 @@ jobs: path: ./dist/* retention-days: 1 + - name: Cleanup temporary files + if: always() + shell: bash + run: | + rm -f certificate.p12 + rm -rf "$RUNNER_TEMP/msi-verify" + if [ "${{ matrix.platform }}" = "macos-15" ]; then + security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true + fi + continue-on-error: true + collect-and-release: needs: [determine-matrix, build, build-jars] runs-on: ubuntu-latest @@ -637,7 +715,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -719,6 +797,10 @@ jobs: 'bundles': ['Stirling-PDF-windows-x86_64.msi'], 'targets': ['windows-x86_64-msi', 'windows-x86_64'], }, + { + 'bundles': ['Stirling-PDF-windows-arm64-setup.exe'], + 'targets': ['windows-aarch64-nsis', 'windows-aarch64'], + }, { 'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'], 'targets': ['darwin-x86_64', 'darwin-aarch64'], @@ -781,7 +863,7 @@ jobs: PYEOF - name: Upload merged artifacts for review - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-artifacts path: ./artifacts/ @@ -789,21 +871,27 @@ jobs: # Gate publish on valid updater sigs. Runs after the review upload (so # artifacts survive for debugging) and before action-gh-release. + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock - name: Verify updater signatures run: | - python3 -m pip install --quiet 'cryptography==44.0.0' - python3 .github/scripts/verify-updater-signatures.py \ + uv run --project engine --locked --only-group updater-signatures python .github/scripts/verify-updater-signatures.py \ ./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json # workflow_dispatch path requires platform=='all' so a single-platform # dispatch can't overwrite an existing release's full latest.json with a # partial one (action-gh-release defaults overwrite_files:true). - # release / V2-master always build the full matrix so no extra guard needed. + # release event / release branch always build the full matrix so no extra guard needed. # fail_on_unmatched_files makes a missing latest.json or installer fail loudly # instead of silently shipping a broken auto-update. - name: Upload binaries to Release - if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master' - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/release' + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: v${{ needs.determine-matrix.outputs.version }} # Don't regenerate/append notes on re-runs, and don't force this into the @@ -817,6 +905,7 @@ jobs: files: | ./artifacts/**/*.jar ./artifacts/**/*.msi + ./artifacts/**/*-setup.exe ./artifacts/**/*.dmg ./artifacts/**/*.app.tar.gz ./artifacts/**/*.deb diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4047157a45..42898c4b43 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -4,6 +4,11 @@ on: schedule: - cron: "0 2 * * *" # 2 AM UTC every night workflow_dispatch: + pull_request: + paths: + - .github/workflows/nightly.yml + - testing/cucumber/** + - docker/embedded/compose/test_cicd.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -26,7 +31,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" @@ -53,10 +58,63 @@ jobs: path: frontend/playwright-report/ retention-days: 14 + # Whole-suite accessibility sweep. Pull requests only scan the stories they + # touch (frontend-a11y.yml) because a full pass takes ~30 minutes; this covers + # everything else, so a violation introduced by a change somewhere other than + # the story itself — a shared component, a theme token — still surfaces within + # a day. + a11y-all-stories: + name: a11y (every story) + strategy: + fail-fast: false + matrix: + theme: [light, dark] + runs-on: ubuntu-latest + # One full sweep (~30 minutes of browser time). + timeout-minutes: 60 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install Task + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + + - name: a11y gate (every story, ${{ matrix.theme }}) + run: task frontend:storybook:a11y:${{ matrix.theme }} + + - name: Upload scan reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: a11y-scan-nightly-${{ matrix.theme }}-${{ github.run_id }} + path: frontend/.a11y-scan/ + retention-days: 14 + if-no-files-found: ignore + # The reports live in a dot-directory, which upload-artifact treats as + # hidden and silently skips by default. + include-hidden-files: true + # Builds all desktop platforms on a schedule so the Rust dependency cache is # written on main, where PR and merge-queue tauri builds can restore it. + # + # The only job here still pinned to schedule/main: it primes a cache rather than + # testing anything, and Actions scopes a cache written on a PR branch to that PR + # alone, so a PR run costs three platform builds and produces nothing reusable. warm-tauri-cache: name: Warm Tauri Rust cache + if: github.event_name == 'schedule' || github.ref == 'refs/heads/main' permissions: contents: read pull-requests: write @@ -65,3 +123,75 @@ jobs: platform: all sign: false secrets: inherit + + # Runs the @nightly tag (conversion scenarios) plus a 10-shard concurrency run + # of every other feature. + cucumber-nightly: + environment: + name: ci-unsigned + deployment: false + name: Cucumber (nightly scenarios + full concurrency) + runs-on: ubuntu-latest + # Fork pull requests get no MAVEN_* secrets, so the image build cannot work. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: "25" + distribution: "temurin" + + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock + + - name: Install Task + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 + + - name: Start the fat image with login and storage enabled + run: docker compose -f docker/embedded/compose/test_cicd.yml up -d --build + env: + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + + - name: Wait for the server + # Throwaway key from test_cicd.yml; out of the header literal for gitleaks. + env: + TEST_API_KEY: "123456789" + run: | + curl --retry 90 --retry-delay 3 --retry-connrefused --retry-all-errors \ + -sf -H "X-API-KEY: $TEST_API_KEY" http://localhost:8080/api/v1/info/status + + # Heavy LibreOffice/Calibre/Ghostscript conversions, excluded from the PR run. + # Both tasks install the behave deps themselves, so there is no separate uv sync step. + - name: Run @nightly scenarios + run: task cucumber:nightly + + # Genuinely different payloads contending on one backend. + - name: Sharded concurrency validation + run: task cucumber:parallel SHARDS=10 + + - name: Container logs on failure + if: failure() + run: docker compose -f docker/embedded/compose/test_cicd.yml logs --tail 400 + + - name: Tear down + if: always() + run: docker compose -f docker/embedded/compose/test_cicd.yml down -v diff --git a/.github/workflows/package-managers.yml b/.github/workflows/package-managers.yml index 018cc61b67..777c8d6ed4 100644 --- a/.github/workflows/package-managers.yml +++ b/.github/workflows/package-managers.yml @@ -28,7 +28,7 @@ jobs: jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -73,13 +73,14 @@ jobs: echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" update-homebrew-and-scoop: + environment: package-publish needs: get-release-info runs-on: ubuntu-latest permissions: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/pr-conflict-labeler.yml b/.github/workflows/pr-conflict-labeler.yml index f6e1016a4c..c642b6cf3d 100644 --- a/.github/workflows/pr-conflict-labeler.yml +++ b/.github/workflows/pr-conflict-labeler.yml @@ -27,29 +27,22 @@ jobs: name: Label conflicted PRs runs-on: ubuntu-latest permissions: - contents: read - issues: write - pull-requests: read + contents: read # actions/checkout + issues: write # get/create the repo-level conflict label + pull-requests: write # pulls.get/list plus add/remove the label on PRs steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Check out the repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up stirling-bot token - id: setup-bot - uses: ./.github/actions/setup-bot - with: - app-id: ${{ secrets.GH_APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Apply conflict label uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - github-token: ${{ steps.setup-bot.outputs.token }} + github-token: ${{ github.token }} script: | const conflictLabel = process.env.CONFLICT_LABEL; const owner = context.repo.owner; diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 3feca530d1..d4271c6aad 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -25,13 +25,16 @@ jobs: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock cache-suffix: pre-commit - name: Install Task - uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Run pre-commit checks run: task pre-commit diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml index c6fbe3f7ad..97c227f23c 100644 --- a/.github/workflows/push-docker-base.yml +++ b/.github/workflows/push-docker-base.yml @@ -17,6 +17,9 @@ permissions: jobs: push-base: + # Own environment: docker-publish is branch-locked to release/main, + # which excludes the baseDockerImage/accessIssueFix branches this runs on. + environment: docker-base-publish if: ${{ vars.CI_PROFILE != 'lite' && github.actor == 'Frooodle' }} runs-on: ubuntu-24.04-8core permissions: @@ -32,9 +35,11 @@ jobs: - name: Set version id: version + env: + INPUT_VERSION: ${{ github.event.inputs.version }} run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - VERSION="${{ github.event.inputs.version }}" + VERSION="${INPUT_VERSION}" elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then VERSION="1.0.3" else @@ -43,20 +48,20 @@ jobs: echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -64,10 +69,10 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Convert repository owner to lowercase id: repoowner @@ -75,7 +80,7 @@ jobs: - name: Generate tags for base image id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf-base diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index a87f6d68b5..ea379cf7c6 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -20,10 +20,8 @@ on: default: false push: branches: - - master + - release - main - - V2-master - - testMain # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -42,6 +40,7 @@ permissions: jobs: push: + environment: docker-publish if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-24.04-8core permissions: @@ -53,36 +52,29 @@ jobs: RUN_UNOSERVER: ${{ github.event_name != 'workflow_dispatch' || inputs.build_unoserver }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Cache Gradle dependencies + - name: Cache Gradle uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- + key: gradle-push-docker-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - name: Set up JDK 25 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: - gradle-version: 9.6.1 + java-version: "25" + distribution: "temurin" - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -95,32 +87,32 @@ jobs: MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - name: Install cosign - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' + if: github.ref == 'refs/heads/release' uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 with: cosign-release: "v2.4.1" - name: Install cosign - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' + if: github.ref == 'refs/heads/release' uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 with: cosign-release: "v2.4.1" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Convert repository owner to lowercase id: repoowner @@ -129,7 +121,7 @@ jobs: - name: Generate tags for latest id: meta if: env.RUN_MAIN_APP == 'true' - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: | ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf @@ -137,8 +129,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (latest variant) id: build-push-latest @@ -162,7 +154,7 @@ jobs: sbom: true - name: Sign regular images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-latest.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-latest.outputs.digest != '' env: DIGEST: ${{ steps.build-push-latest.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} @@ -177,7 +169,7 @@ jobs: - name: Generate tags for latest-fat id: meta-fat - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' with: images: | @@ -186,8 +178,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (fat variant) id: build-push-fat @@ -208,7 +200,7 @@ jobs: sbom: true - name: Sign fat images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-fat.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-fat.outputs.digest != '' env: DIGEST: ${{ steps.build-push-fat.outputs.digest }} TAGS: ${{ steps.meta-fat.outputs.tags }} @@ -221,7 +213,7 @@ jobs: - name: Generate tags for ultra-lite id: meta-lite - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 if: env.RUN_MAIN_APP == 'true' && github.ref != 'refs/heads/main' && github.ref != 'refs/heads/testMain' with: images: | @@ -230,8 +222,8 @@ jobs: ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf tags: | - type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} - type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master' }} + type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }} + type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/release' }} - name: Build and push Unified Dockerfile (ultra-lite variant) id: build-push-lite @@ -252,7 +244,7 @@ jobs: sbom: true - name: Sign ultra-lite images - if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master') && steps.build-push-lite.outputs.digest != '' + if: env.RUN_MAIN_APP == 'true' && (github.ref == 'refs/heads/release') && steps.build-push-lite.outputs.digest != '' env: DIGEST: ${{ steps.build-push-lite.outputs.digest }} TAGS: ${{ steps.meta-lite.outputs.tags }} @@ -264,7 +256,7 @@ jobs: done # Standalone unoserver image — versioned independently via - # docker/unoserver/VERSION. master/V2-master: publish +latest + # docker/unoserver/VERSION. release: publish +latest # only when the version is new. main/testMain: republish :alpha only # when the source hash differs from the published image's annotation. - name: Read unoserver image version @@ -323,7 +315,7 @@ jobs: fi case "$EFFECTIVE_REF" in - refs/heads/master|refs/heads/V2-master) + refs/heads/release) if [ "${FORCE_REBUILD}" = "true" ]; then echo "force_unoserver_rebuild=true — building stable regardless" mode="stable" diff --git a/.github/workflows/rollback-latest.yml b/.github/workflows/rollback-latest.yml index 7f9f5b0a9a..3e49f727c6 100644 --- a/.github/workflows/rollback-latest.yml +++ b/.github/workflows/rollback-latest.yml @@ -13,12 +13,13 @@ permissions: jobs: rollback: + environment: docker-publish runs-on: ubuntu-latest permissions: packages: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -26,13 +27,13 @@ jobs: uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4 - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index f1e67e6b83..f5b9447720 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -45,7 +45,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif @@ -75,6 +75,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6972a1fc95..af255de3cf 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,12 +17,12 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: 30 days stale issues - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 + uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 90c5984693..1bfc94be5b 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - release # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed @@ -23,27 +23,33 @@ permissions: jobs: push: + # package-publish holds SWAGGERHUB_API_KEY. It requires reviewer approval and + # is limited to main / release / v* tags, so every push to release waits on one. + environment: package-publish if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Cache Gradle + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-swagger-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - - name: Generate Swagger documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs diff --git a/.github/workflows/sync-portal-docs.yml b/.github/workflows/sync-portal-docs.yml index 54ee262239..636cd5f2e1 100644 --- a/.github/workflows/sync-portal-docs.yml +++ b/.github/workflows/sync-portal-docs.yml @@ -24,6 +24,7 @@ permissions: jobs: sync: + environment: bot-identity name: Sync docs manifest runs-on: ubuntu-latest timeout-minutes: 10 @@ -32,7 +33,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -50,7 +51,7 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "22" cache: "npm" diff --git a/.github/workflows/sync_files_v2.yml b/.github/workflows/sync_files_v2.yml index 357f5d5a87..6b436ed2f6 100644 --- a/.github/workflows/sync_files_v2.yml +++ b/.github/workflows/sync_files_v2.yml @@ -33,10 +33,11 @@ permissions: jobs: sync-files: + environment: bot-identity runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -51,28 +52,25 @@ jobs: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - python-version: "3.12" - cache: "pip" # caching pip dependencies + enable-cache: true + cache-dependency-glob: | + engine/pyproject.toml + engine/uv.lock + cache-suffix: sync-files - name: Install Python dependencies run: | - pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt - - - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - cache-suffix: sync-files + uv sync --project engine --locked --group tools - name: Install Task - uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Sync translation TOML files run: | - python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main + uv run --project engine --locked --group tools python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main - name: Sort translation TOML files run: | @@ -85,7 +83,7 @@ jobs: - name: Sync README.md run: | - python scripts/counter_translation_v3.py + uv run --project engine --locked --group tools python scripts/counter_translation_v3.py - name: Run git add run: | diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index c97ae4eaef..4e9eddd614 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -12,7 +12,7 @@ on: workflow_call: inputs: platform: - description: "Platform to build (windows, macos, linux, or all)." + description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)." required: false type: string default: "all" @@ -26,18 +26,24 @@ on: required: false type: boolean default: false + use_shared_cache: + required: false + type: boolean + default: false workflow_dispatch: inputs: platform: - description: "Platform to build (windows, macos, linux, or all)" + description: "Platform to build (windows, windows-arm64, macos, linux, windows-macos, or all)" required: true default: "all" type: choice options: - all - windows + - windows-arm64 - macos - linux + - windows-macos sign: description: "Sign and notarize the bundles." required: false @@ -55,13 +61,18 @@ permissions: jobs: determine-matrix: + # Only probes APPLE_CERTIFICATE for presence, so it stays on the unrestricted + # signing environment - release-signing would block every PR run. + environment: + name: ci-signing + deployment: false if: ${{ vars.CI_PROFILE != 'lite' }} runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -72,14 +83,19 @@ jobs: PLATFORM: ${{ inputs.platform }} run: | WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}' + # ARM64: NSIS only (WiX MSI has no arm64 support in Tauri) and no JPDFium + # natives yet - flip jpdfium_platforms to windows-arm64 once JPDFium ships it. + WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}' MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}' LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}' case "$PLATFORM" in - windows) ENTRIES=("$WINDOWS") ;; - macos) ENTRIES=("$MACOS") ;; - linux) ENTRIES=("$LINUX") ;; - *) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;; + windows) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64") ;; + windows-arm64) ENTRIES=("$WINDOWS_ARM64") ;; + macos) ENTRIES=("$MACOS") ;; + linux) ENTRIES=("$LINUX") ;; + windows-macos) ENTRIES=("$WINDOWS" "$MACOS") ;; + *) ENTRIES=("$WINDOWS" "$WINDOWS_ARM64" "$MACOS" "$LINUX") ;; esac # Drop macOS entries when Apple certificate secret is unavailable @@ -96,6 +112,12 @@ jobs: echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT build: + # Windows/GPG signing only runs on main (see the per-step gates below), so only + # that path needs the reviewer-gated release-signing environment. Everything else + # (PRs, merge queue, nightly) signs macOS only and uses ci-signing, which has no + # approval or branch restriction. + environment: + name: ${{ (inputs.sign && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))) && 'release-signing' || 'ci-signing' }} needs: determine-matrix strategy: fail-fast: false @@ -103,12 +125,16 @@ jobs: runs-on: ${{ matrix.platform }} env: SM_API_KEY: ${{ secrets.SM_API_KEY }} - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} + # Per-platform sign gate. macOS signs on any run with the cert available, + # PRs included: Gatekeeper blocks an unsigned .dmg, so an unsigned macOS + # PR build is not testable. Windows and Linux stay main-only, matching the + # gates on their own signing steps below. + SIGN_BUNDLE: ${{ inputs.sign && (matrix.platform == 'macos-15' && secrets.APPLE_CERTIFICATE != '' || github.ref == 'refs/heads/main') }} steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -122,7 +148,7 @@ jobs: sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 cache: "npm" @@ -138,7 +164,7 @@ jobs: # only recompiles the app crate. Written on main; PRs and the merge queue # restore from it. - name: Cache Rust build - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: frontend/editor/src-tauri # Stable key shared across workflows so the nightly warmer. @@ -148,9 +174,27 @@ jobs: # Save the dependency cache even if a later step fails cache-on-failure: true + - name: Restore cache Gradle User Home + if: inputs.use_shared_cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + + - name: Restore cache Gradle + if: inputs.use_shared_cache == false + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-tauri-build-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up x86_64 JDK 25 (macOS universal JRE) if: matrix.platform == 'macos-15' - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" @@ -160,16 +204,12 @@ jobs: if: matrix.platform == 'macos-15' run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV" + # Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does. - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" - distribution: "temurin" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 + distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }} - name: Setup Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -196,7 +236,7 @@ jobs: # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -206,22 +246,28 @@ jobs: SM_HOST: ${{ secrets.SM_HOST }} - name: Setup DigiCert KeyLocker Certificate - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: pwsh + env: + SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }} + SM_HOST: ${{ secrets.SM_HOST }} + SM_API_KEY: ${{ secrets.SM_API_KEY }} + SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} + SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} run: | Write-Host "Setting up DigiCert KeyLocker environment..." # Decode client certificate - $certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}") + $certBytes = [Convert]::FromBase64String("$env:SM_CLIENT_CERT_FILE_B64") $certPath = "D:\Certificate_pkcs12.p12" [IO.File]::WriteAllBytes($certPath, $certBytes) # Set environment variables echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV - echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV - echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV + echo "SM_HOST=$env:SM_HOST" >> $env:GITHUB_ENV + echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV + echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV + echo "SM_KEYPAIR_ALIAS=$env:SM_KEYPAIR_ALIAS" >> $env:GITHUB_ENV # Get PKCS11 config path from DigiCert action $pkcs11Config = $env:PKCS11_CONFIG @@ -239,40 +285,8 @@ jobs: } } - # Traditional PFX Certificate Import (fallback if KeyLocker not configured) - - name: Import Windows Code Signing Certificate - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }} - env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} - shell: powershell - run: | - if ($env:WINDOWS_CERTIFICATE) { - Write-Host "Importing Windows Code Signing Certificate..." - - # Decode base64 certificate and save to file - $certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) - $certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx" - [IO.File]::WriteAllBytes($certPath, $certBytes) - - # Import certificate to CurrentUser\My store - $cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force) - - # Extract and set thumbprint as environment variable - $thumbprint = $cert.Thumbprint - Write-Host "Certificate imported with thumbprint: $thumbprint" - echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV - - # Clean up certificate file - Remove-Item $certPath - - Write-Host "Windows certificate import completed." - } else { - Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary" - } - - name: Import Apple Developer Certificate - if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != '' + if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15' env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -293,7 +307,7 @@ jobs: rm certificate.p12 - name: Verify Certificate - if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != '' + if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15' run: | echo "Verifying Apple Developer Certificate..." KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db @@ -316,7 +330,7 @@ jobs: ls -la /usr/bin/hd* || echo "No hd* tools found" - name: Preflight smctl - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: pwsh env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -329,7 +343,7 @@ jobs: if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" } - name: Configure Windows code signing - if: ${{ inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + if: ${{ inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} shell: bash env: KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} @@ -366,8 +380,8 @@ jobs: fi - name: Build Tauri app (signed) - if: inputs.sign - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2 + if: env.SIGN_BUNDLE == 'true' + uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} @@ -401,8 +415,8 @@ jobs: args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }} - name: Build Tauri app (unsigned) - if: ${{ !inputs.sign }} - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2 + if: env.SIGN_BUNDLE != 'true' + uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SIGN: "0" @@ -418,7 +432,9 @@ jobs: # Linux: build deb+rpm only here (deb-only on minimal smoke builds). # AppImage runs in its own continue-on-error step below so its # persistent linuxdeploy failure (#6127 onwards) does not tank uploads. - args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }} + args: >- + ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }} + --config '{"bundle":{"createUpdaterArtifacts":false}}' # AppImage is decoupled so its linuxdeploy run gets a fresh process # (rpm scratch state torn down) and its failure can't tank deb/rpm. @@ -426,7 +442,7 @@ jobs: - name: Build Tauri app (Linux AppImage) if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal continue-on-error: true - uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2 + uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SIGN: ${{ (inputs.sign && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }} @@ -443,6 +459,37 @@ jobs: tauriScript: npx tauri args: --bundles appimage + # Bundled libwayland conflicts with the host's on some distros (Fedora + # Wayland: EGL_BAD_PARAMETER, blank window - #6878). The AppImage + # ecosystem excludelist agrees these libs must come from the system. + - name: Strip bundled Wayland libs from AppImage + if: matrix.platform == 'ubuntu-22.04' && !inputs.minimal + continue-on-error: true + run: | + set -euo pipefail + AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1) + if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi + chmod +x "$AI" + WORK=$(mktemp -d) + (cd "$WORK" && "$AI" --appimage-extract >/dev/null) + if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then + echo "No bundled libwayland - nothing to strip" + rm -rf "$WORK" + exit 0 + fi + rm -f "$WORK/squashfs-root/usr/lib/"libwayland-* + curl -fsSL -o "$WORK/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + # Pinned checksum: never execute an unverified downloaded binary. On + # mismatch (upstream rebuilt continuous) the step aborts and the + # original AppImage ships unchanged - update the pin deliberately. + echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c - + chmod +x "$WORK/appimagetool" + "$WORK/appimagetool" --appimage-extract-and-run "$WORK/squashfs-root" "$AI.new" + mv "$AI.new" "$AI" + rm -rf "$WORK" + echo "Stripped bundled libwayland from $(basename "$AI")" + - name: Clear release GPG key from runner keyring (Linux) if: always() && inputs.sign && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main' env: @@ -454,7 +501,7 @@ jobs: fi - name: Verify notarization (macOS only) - if: inputs.sign && matrix.platform == 'macos-15' + if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15' run: | echo "🔍 Verifying notarization status..." cd ./frontend/editor/src-tauri/target @@ -481,6 +528,9 @@ jobs: # Only ship the MSI installer. The loose exe and WiX toolset exes # are not the user-facing installer - the MSI contains the signed inner exe. find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \; + elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then + # arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri). + find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \; elif [ "${{ matrix.platform }}" = "macos-15" ]; then find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \; else @@ -492,9 +542,28 @@ jobs: # Verify the MSI AND the inner exe extracted from it are signed. # The inner exe is what gets installed on users' machines and what AV scans. - name: Verify Windows Code Signature - if: inputs.sign && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' + if: inputs.sign && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' shell: pwsh run: | + # arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the + # inner exe before packing and the setup exe after, so verifying the setup + # exe is the arm64 equivalent of the MSI + inner-exe check below. + if ("${{ matrix.platform }}" -eq "windows-11-arm") { + $exePath = "./dist/Stirling-PDF-${{ matrix.name }}-setup.exe" + if (-not (Test-Path $exePath)) { + Write-Host "[ERROR] NSIS installer not found at $exePath" + exit 1 + } + $sig = Get-AuthenticodeSignature -FilePath $exePath + Write-Host "NSIS installer: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] NSIS installer is not signed" + exit 1 + } + Write-Host "[SUCCESS] NSIS installer is properly signed" + exit 0 + } + $allSigned = $true $msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi" @@ -540,7 +609,7 @@ jobs: Write-Host "[SUCCESS] MSI and inner exe are properly signed" - name: Dump smctl logs on failure - if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }} + if: ${{ failure() && startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' }} shell: pwsh run: | $logDir = "$env:USERPROFILE\.signingmanager\logs" @@ -567,7 +636,7 @@ jobs: cd ./frontend/editor/src-tauri/target # Check for expected artifacts based on platform - if [ "${{ matrix.platform }}" = "windows-latest" ]; then + if [ "${{ matrix.platform }}" = "windows-latest" ] || [ "${{ matrix.platform }}" = "windows-11-arm" ]; then echo "Checking for Windows artifacts..." find . -name "*.exe" -o -name "*.msi" | head -5 if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then @@ -608,15 +677,33 @@ jobs: fi done + - name: Cleanup temporary files + if: always() + shell: bash + run: | + rm -f certificate.p12 + rm -rf "$RUNNER_TEMP/msi-verify" + if [ "${{ matrix.platform }}" = "macos-15" ]; then + security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true + fi + continue-on-error: true + pr-comment: needs: build runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && needs.build.result == 'success' + # Fork and Dependabot pull_request runs receive a read-only GITHUB_TOKEN, + # so the API cannot create or update PR comments there. The artifacts are + # still uploaded and remain available from the Actions run page. + if: >- + github.event_name == 'pull_request' && + needs.build.result == 'success' && + !github.event.pull_request.head.repo.fork && + github.actor != 'dependabot[bot]' permissions: pull-requests: write steps: - name: Harden the runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -639,6 +726,7 @@ jobs: // Map of expected artifact names to display info const artifactMap = { 'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' }, + 'Stirling-PDF-windows-arm64': { icon: '🪟', platform: 'Windows ARM64', files: '-setup.exe (NSIS)' }, 'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' }, 'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' } }; @@ -707,7 +795,7 @@ jobs: if: always() steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index f8344da116..dfdd0e6c62 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -22,21 +22,45 @@ permissions: contents: read jobs: - # TODO: extract a pre-matrix `prepare` job that runs once and produces - # shared artifacts for the three matrix entries below to consume: - # 1. `task backend:build` — currently runs 3× in parallel with - # identical env (DISABLE_ADDITIONAL_FEATURES=true, - # STIRLING_PDF_DESKTOP_UI=false). Build once, upload the JAR as an - # artifact, matrix entries download. - # 2. The base-image `docker build` (gated on docker-base-changed) — - # currently runs 3× in parallel against the same Dockerfile and - # context. Build once, `docker save` to an artifact, matrix entries - # `docker load` before the embedded build. - # Saves ~2 full backend builds + 2 base-image builds per PR that touches - # docker. May also be reusable from backend-build.yml's jdk-25 + - # spring-security=true matrix entry if `task backend:build` and - # `task backend:build:ci` produce equivalent JARs (verify before wiring). + # A changed base image is shared by all three embedded-image builds. Build + # it once and transfer it as an artifact; the matrix jobs use the local + # Docker driver so the loaded image is visible to the build. + prepare-base-image: + if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true' + environment: + name: ci-unsigned + deployment: false + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Harden Runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build base image locally + run: docker build --platform linux/amd64 -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base + + - name: Export base image + run: docker save stirling-pdf-base:pr-test | gzip -1 > stirling-pdf-base-pr-test.tar.gz + + - name: Upload base image + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docker-base-pr-test + path: stirling-pdf-base-pr-test.tar.gz + retention-days: 1 + if-no-files-found: error + test-build-docker-images: + if: always() && (needs.prepare-base-image.result == 'success' || needs.prepare-base-image.result == 'skipped') + needs: [prepare-base-image] + environment: + name: ci-unsigned + deployment: false runs-on: ubuntu-latest strategy: fail-fast: false @@ -53,7 +77,7 @@ jobs: cache-scope: stirling-pdf-fat steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -61,7 +85,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -78,27 +102,30 @@ jobs: docker system prune -af || true echo "Disk space after cleanup:" && df -h + - name: Download prepared base image + if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: docker-base-pr-test + + - name: Load prepared base image + if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true' + run: gzip -dc stirling-pdf-base-pr-test.tar.gz | docker load + + - name: Restore cache Gradle User Home + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }} + - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" - - name: Cache Gradle dependency artifacts - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches/modules-2/files-2.1 - ~/.gradle/caches/modules-2/metadata-2.* - key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }} - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - cache-disabled: true - - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 - name: Build application @@ -111,16 +138,11 @@ jobs: STIRLING_PDF_DESKTOP_UI: false - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Build base image locally (PR base change only) - if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true' - run: | - docker build -t stirling-pdf-base:pr-test -f docker/base/Dockerfile docker/base + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Set base image and platform for this build id: build-params @@ -195,7 +217,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit @@ -203,11 +225,11 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build docker/unoserver/Dockerfile uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml deleted file mode 100644 index f86191656f..0000000000 --- a/.github/workflows/testdriver.yml +++ /dev/null @@ -1,207 +0,0 @@ -name: UI test with TestDriverAI - -on: - push: - branches: ["master", "UITest", "testdriver"] - -# cancel in-progress jobs if a new job is triggered -# This is useful to avoid running multiple builds for the same branch if a new commit is pushed -# or a pull request is updated. -# It helps to save resources and time by ensuring that only the latest commit is built and tested -# This is particularly useful for long-running jobs that may take a while to complete. -# The `group` is set to a combination of the workflow name, event name, and branch name. -# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of -# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened. -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - if: ${{ vars.CI_PROFILE != 'lite' }} - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - java-version: "25" - distribution: "temurin" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - with: - gradle-version: 9.6.1 - - - name: Build with Gradle - run: ./gradlew build - env: - MAVEN_USER: ${{ secrets.MAVEN_USER }} - MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} - MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} - DISABLE_ADDITIONAL_FEATURES: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Get version number - id: versionNumber - run: | - VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}') - echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_API }} - - - name: Build and push test image - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 - with: - context: . - file: ./docker/embedded/Dockerfile - push: true - cache-from: type=gha,scope=stirling-pdf-latest - cache-to: type=gha,mode=max,scope=stirling-pdf-latest - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }} - build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} - platforms: linux/amd64 - - - name: Set up SSH - run: | - mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key - sudo chmod 600 ../private.key - - - name: Deploy to VPS - run: | - cat > docker-compose.yml << EOF - version: '3.3' - services: - stirling-pdf: - container_name: stirling-pdf-test-${{ github.sha }} - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }} - ports: - - "1337:8080" - volumes: - - /stirling/test-${{ github.sha }}/data:/usr/share/tessdata:rw - - /stirling/test-${{ github.sha }}/config:/configs:rw - - /stirling/test-${{ github.sha }}/logs:/logs:rw - environment: - DISABLE_ADDITIONAL_FEATURES: "true" - SECURITY_ENABLELOGIN: "false" - SYSTEM_DEFAULTLOCALE: en-US - UI_APPNAME: "Stirling-PDF Test" - UI_HOMEDESCRIPTION: "Test Deployment" - UI_APPNAMENAVBAR: "Test" - SYSTEM_MAXFILESIZE: "100" - METRICS_ENABLED: "true" - SYSTEM_GOOGLEVISIBILITY: "false" - SYSTEM_ENABLEANALYTICS: "false" - restart: on-failure:5 - EOF - - scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }}:/tmp/docker-compose.yml - - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF - mkdir -p /stirling/test-${{ github.sha }}/{data,config,logs} - mv /tmp/docker-compose.yml /stirling/test-${{ github.sha }}/docker-compose.yml - cd /stirling/test-${{ github.sha }} - docker-compose pull - docker-compose up -d - EOF - - files-changed: - if: always() - name: detect what files changed - runs-on: ubuntu-latest - timeout-minutes: 3 - outputs: - frontend: ${{ steps.changes.outputs.frontend }} - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check for file changes - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - id: changes - with: - filters: ".github/config/.files.yaml" - - test: - if: needs.files-changed.outputs.frontend == 'true' - needs: [deploy, files-changed] - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - cache: "npm" - cache-dependency-path: frontend/package-lock.json - - - name: Run TestDriver.ai - uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3 - with: - key: ${{secrets.TESTDRIVER_API_KEY}} - prerun: | - choco install go-task -y - task frontend:build - cd frontend - npm install dashcam-chrome --save - Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--load-extension=$(pwd)/node_modules/dashcam-chrome/build", "http://${{ secrets.NEW_VPS_HOST }}:1337" - Start-Sleep -Seconds 20 - prompt: | - 1. /run testing/testdriver/test.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FORCE_COLOR: "3" - - cleanup: - needs: [deploy, test] - runs-on: ubuntu-latest - if: always() - - steps: - - name: Harden Runner - uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3 - with: - egress-policy: audit - - - name: Set up SSH - run: | - mkdir -p ~/.ssh/ - echo "${{ secrets.NEW_VPS_SSH_KEY }}" > ../private.key - sudo chmod 600 ../private.key - - - name: Cleanup deployment - if: always() - run: | - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << EOF - cd /stirling/test-${{ github.sha }} - docker-compose down - cd /stirling - rm -rf test-${{ github.sha }} - EOF - continue-on-error: true # Ensure cleanup runs even if previous steps fail diff --git a/.github/workflows/update-gradle.yml b/.github/workflows/update-gradle.yml new file mode 100644 index 0000000000..72b347e62f --- /dev/null +++ b/.github/workflows/update-gradle.yml @@ -0,0 +1,112 @@ +name: Update Gradle + +on: + workflow_dispatch: + schedule: + - cron: "0 3 * * 1" + +concurrency: + group: update-gradle + cancel-in-progress: true + +jobs: + update-gradle: + name: Update Gradle and Docker images + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: "25" + + - name: Find latest Gradle release + id: gradle + shell: bash + run: | + set -euo pipefail + version=$(curl --fail --silent --show-error --retry 3 \ + https://services.gradle.org/versions/current | jq -r '.version') + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "Could not determine a stable Gradle version: $version" >&2 + exit 1 + } + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Find matching Docker image digest + id: docker + env: + GRADLE_VERSION: ${{ steps.gradle.outputs.version }} + shell: bash + run: | + set -euo pipefail + tag="${GRADLE_VERSION}-jdk25" + digest=$(curl --fail --silent --show-error --retry 3 \ + "https://hub.docker.com/v2/repositories/library/gradle/tags/${tag}" \ + | jq -r '.digest // empty') + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { + echo "Docker image gradle:${tag} was not found" >&2 + exit 1 + } + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "digest=$digest" >> "$GITHUB_OUTPUT" + + - name: Update Gradle wrapper + env: + GRADLE_VERSION: ${{ steps.gradle.outputs.version }} + run: ./gradlew wrapper --gradle-version "$GRADLE_VERSION" --distribution-type bin + + - name: Update Gradle Docker images + env: + DOCKER_TAG: ${{ steps.docker.outputs.tag }} + DOCKER_DIGEST: ${{ steps.docker.outputs.digest }} + shell: bash + run: | + set -euo pipefail + find docker -type f -name 'Dockerfile*' -print0 | + xargs -0 sed -E -i \ + "s#gradle:[^@[:space:]]+-jdk25(@sha256:[^[:space:]]+)?#gradle:${DOCKER_TAG}@${DOCKER_DIGEST}#g" + + - name: Verify Gradle update + env: + EXPECTED_VERSION: ${{ steps.gradle.outputs.version }} + shell: bash + run: | + set -euo pipefail + actual=$(./gradlew --version | sed -n 's/^Gradle \([0-9.]*\)$/\1/p') + [[ "$actual" == "$EXPECTED_VERSION" ]] || { + echo "Wrapper resolved Gradle $actual, expected $EXPECTED_VERSION" >&2 + exit 1 + } + if git diff --quiet; then + echo "Gradle is already up to date." + exit 0 + fi + git diff --check + + - name: Create pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: automation/update-gradle + delete-branch: true + commit-message: "chore: update Gradle" + title: "chore: update Gradle to ${{ steps.gradle.outputs.version }}" + body: | + Automated update of the Gradle wrapper and Gradle Docker build images. + + Gradle version: `${{ steps.gradle.outputs.version }}` + Docker image: `gradle:${{ steps.docker.outputs.tag }}` + labels: dependencies diff --git a/.gitignore b/.gitignore index f47b020013..4b34350f67 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,11 @@ pipeline/ customFiles/ configs/ watchedFolders/ +# The rule above targets the app's runtime watched-folders working dir, but it +# also matches this frontend source component dir; keep the source tracked. +!frontend/editor/src/proprietary/components/watchedFolders/ clientWebUI/ +policy-webhook-spool/ # Scratch dir used by local fixture-regeneration runs (see # app/proprietary/src/test/resources/db-migration-fixtures/README.md). # Holds downloaded JARs and disposable workdirs. Never committed. @@ -35,6 +39,7 @@ exampleYmlFiles/stirling/ /testing/file_snapshots /testing/cucumber/junit/ /testing/cucumber/report.html +/testing/cucumber/.parallel/ /testing/.failed_tests /.test-state/ SwaggerDoc.json @@ -60,13 +65,23 @@ app/core/src/main/resources/static/og_images/ app/core/src/main/resources/static/samples/ app/core/src/main/resources/static/manifest-classic.json app/core/src/main/resources/static/og-metadata.json +app/core/src/main/resources/static/og-metadata.saas.json app/core/src/main/resources/static/sw-folder-retry.js app/core/src/main/resources/static/robots.txt +app/core/src/main/resources/static/android-chrome-*.png +app/core/src/main/resources/static/mstile-*.png +app/core/src/main/resources/static/favicon.png +app/core/src/main/resources/static/safari-pinned-tab.svg app/core/src/main/resources/static/pdfium/ app/core/src/main/resources/static/pdfjs/ app/core/src/main/resources/static/vendor/ app/core/src/main/resources/static/**/*.gz app/core/src/main/resources/static/**/*.br +app/core/src/main/resources/static/css/cookieconsent.css +app/core/src/main/resources/static/css/cookieconsentCustomisation.css +app/core/src/main/resources/static/mockServiceWorker.js +app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js +app/core/src/main/resources/static/images/google-drive.svg # Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc. # Gradle diff --git a/.gitleaksignore b/.gitleaksignore index 089de4fedb..c3917e985f 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -22,8 +22,13 @@ frontend/editor/src/portal/components/docs/GettingStartedSection.tsx:generic-api # False positive: generic-api-key matches the Java type name "X509Certificate" # in a method signature (CreateSignatureBase.resolveSignatureAlgorithm) - not a secret. -app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java:generic-api-key:224 +app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.java:generic-api-key:224 # Supabase publishable key (public by design, RLS-protected) used as a CI fallback # default in the tauri-build workflow when the GitHub secret is unset - not a real secret. .github/workflows/tauri-build.yml:generic-api-key:402 + +# Staging Supabase publishable key (public by design). Ignored here rather than with an +# inline gitleaks:allow because a trailing comment in a .properties file is part of the +# value, so the pragma would end up inside the key. +app/saas/src/main/resources/application-staging.properties:generic-api-key:16 diff --git a/.imgbotconfig b/.imgbotconfig index 720b938475..6a1b1bf7a2 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,5 +1,7 @@ { "ignoredFiles": [ - "frontend/editor/src-tauri/icons/icon.png" + "frontend/editor/src-tauri/icons/macos/*", + "frontend/editor/src-tauri/icons/linux/*", + "frontend/editor/src-tauri/icons/windows/*" ] } diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml index dde75433a8..08a12b9535 100644 --- a/.taskfiles/backend.yml +++ b/.taskfiles/backend.yml @@ -57,16 +57,57 @@ tasks: - cmd: ./gradlew clean bootRun -PbuildWithFrontend=true platforms: [linux, darwin] + # SaaS backend. dev:saas -> the PR's preview branch, staging:saas -> shared v3, + # PROFILES=none -> production against your own SAAS_DB_*. Production has no named + # task on purpose. Use `none`, not an empty value: Go template `default` treats "" + # as absent and would resolve back to dev. + dev:saas: - desc: "Start backend in SaaS flavor against Supabase" - # `dotenv:` reads from the root Taskfile's directory (".") because this - # subtaskfile is included with `dir: .`. + desc: "Start SaaS backend against the current PR's Supabase preview branch" + dotenv: ['app/.env.saas.local', 'app/.env.saas'] + vars: + PROFILES: '{{.PROFILES | default "dev"}}' + cmds: + # Don't move this check into a `sh:` var: dotenv is visible in cmds but not + # during var evaluation, so the test would always see an empty value. + - cmd: | + if [ "{{.PROFILES}}" = "dev" ] && [ -z "${SAAS_DEV_PROJECT_REF:-}" ]; then + echo ">> SAAS_DEV_PROJECT_REF is not set." + echo ">> Testing a SaaS PR? Put its ref, DB password and publishable key in app/.env.saas.local." + echo ">> Wanted the shared v3 project? Use 'task backend:staging:saas' instead." + exit 1 + fi + - task: _run:saas + vars: + PORT: '{{.PORT}}' + PROFILES: '{{.PROFILES}}' + AIENGINE_URL: '{{.AIENGINE_URL}}' + AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' + AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' + + staging:saas: + desc: "Start SaaS backend against the shared v3 staging project" + cmds: + - task: _run:saas + vars: + PORT: '{{.PORT}}' + PROFILES: staging + AIENGINE_URL: '{{.AIENGINE_URL}}' + AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' + AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' + + _run:saas: + internal: true dotenv: ['app/.env.saas.local', 'app/.env.saas'] ignore_error: true vars: PORT: '{{.PORT | default "8080"}}' - # Override to "" to run the pure `saas` profile against your own SAAS_DB_*. PROFILES: '{{.PROFILES | default "dev"}}' + # Built here rather than inline in the cmds below: the Windows line is an + # unquoted YAML scalar wrapping a cmd.exe string, so a nested {{if ne .X + # "none"}} needs escaped quotes that reach the Go template as literal + # backslashes and fail with `unexpected "\" in operand`. + PROFILE_ARGS: '{{if ne .PROFILES "none"}}--spring.profiles.include={{.PROFILES}}{{end}}' AIENGINE_URL: '{{.AIENGINE_URL | default ""}}' AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}' AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}' @@ -77,9 +118,11 @@ tasks: AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}' AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}' cmds: - - cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}" + # PROFILE_ARGS is empty when PROFILES=none, i.e. the bare `saas` profile + # against SAAS_DB_* (production). + - cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args=\"{{.PROFILE_ARGS}}\"{{end}}" platforms: [windows] - - cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}} + - cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args='{{.PROFILE_ARGS}}'{{end}} platforms: [linux, darwin] build: @@ -114,6 +157,15 @@ tasks: - cmd: ./gradlew test platforms: [linux, darwin] + test:force: + desc: "Run backend tests, ignoring cached results" + aliases: [test:no-cache] + cmds: + - cmd: cmd /c ".\gradlew.bat cleanTest test --no-build-cache" + platforms: [windows] + - cmd: ./gradlew cleanTest test --no-build-cache + platforms: [linux, darwin] + format: desc: "Auto-fix code formatting" cmds: @@ -137,6 +189,7 @@ tasks: swagger: desc: "Generate OpenAPI docs" + run: once cmds: - cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc" platforms: [windows] diff --git a/.taskfiles/cucumber.yml b/.taskfiles/cucumber.yml new file mode 100644 index 0000000000..cb630abf85 --- /dev/null +++ b/.taskfiles/cucumber.yml @@ -0,0 +1,44 @@ +version: '3' + +tasks: + install: + desc: "Sync the Python environment with the cucumber test dependencies" + run: once + # Deliberately no sources/status fingerprint: the engine venv is shared, so it can + # already exist while synced to a different dependency group. uv no-ops when correct. + cmds: + - uv sync --project ../../engine --locked --group cucumber + + run: + desc: "Run the cucumber suite against a running server (BASE_URL, default localhost:8080)" + deps: [install] + cmds: + - uv run --project ../../engine --locked --group cucumber python -m behave --no-capture -f plain {{.CLI_ARGS}} + + nightly: + desc: "Run the @nightly cucumber scenarios, excluded from the default run" + summary: | + Heavy LibreOffice/Calibre/Ghostscript conversions. behave.ini excludes @nightly, + so this opts back in explicitly. + + Pass extra behave flags via -- : + task cucumber:nightly -- --tags=@convert + deps: [install] + cmds: + - uv run --project ../../engine --locked --group cucumber python -m behave --tags=@nightly --no-capture -f plain {{.CLI_ARGS}} + + parallel: + desc: "Run the cucumber suite as concurrent shards against one server (SHARDS, default 10)" + summary: | + Splits the feature files across SHARDS concurrent behave processes hitting a single + backend, to shake out cross-request interference. Auth-coupled features are pinned + to one shard because they change the admin password mid-scenario. + + task cucumber:parallel + task cucumber:parallel SHARDS=4 + BASE_URL=http://localhost:8081 task cucumber:parallel + deps: [install] + vars: + SHARDS: '{{.SHARDS | default "10"}}' + cmds: + - bash run-parallel.sh {{.SHARDS}} {{if .CLI_ARGS}}-- {{.CLI_ARGS}}{{end}} diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml index c80196305b..555e252a4e 100644 --- a/.taskfiles/desktop.yml +++ b/.taskfiles/desktop.yml @@ -22,6 +22,7 @@ vars: linux-amd64) echo "linux-x64";; linux-arm64) echo "linux-arm64";; windows-amd64) echo "windows-x64";; + windows-arm64) echo "none";; # no JPDFium windows-arm64 natives published yet *) echo "all";; esac fi @@ -194,7 +195,11 @@ tasks: # `desktop:build` run `jlink:clean` first to force a fresh build. - cmd: chmod -R u+w runtime/jre platforms: [linux, darwin] - - cmd: powershell -NoProfile -Command "Get-ChildItem -Recurse runtime/jre | ForEach-Object { $_.IsReadOnly = $false }" + # Single-quoted so Task's shell leaves `$_` and `$false` alone. Double + # quotes let it expand them as its own variables, and since neither is + # set the command PowerShell actually received was + # `ForEach-Object { .IsReadOnly = }`, which fails on every file. + - cmd: powershell -NoProfile -Command 'Get-ChildItem -Recurse -File runtime/jre | ForEach-Object { $_.IsReadOnly = $false }' platforms: [windows] status: - test -f runtime/jre/release diff --git a/.taskfiles/e2e.yml b/.taskfiles/e2e.yml index 618041807f..084906960b 100644 --- a/.taskfiles/e2e.yml +++ b/.taskfiles/e2e.yml @@ -15,6 +15,15 @@ tasks: cmds: - npx playwright test --project=stubbed {{.CLI_ARGS}} + stubbed-project: + desc: "Run the stubbed E2E suite for a single Playwright project" + dir: frontend/editor + deps: [ ':frontend:prepare' ] + vars: + PROJECT: '{{.PROJECT | default "stubbed"}}' + cmds: + - npx playwright test --project={{.PROJECT}} {{.CLI_ARGS}} + live: desc: "Run live E2E tests" summary: | diff --git a/.taskfiles/engine.yml b/.taskfiles/engine.yml index cfe0790241..12d76b08a7 100644 --- a/.taskfiles/engine.yml +++ b/.taskfiles/engine.yml @@ -2,22 +2,49 @@ version: '3' tasks: install: - desc: "Install engine dependencies" + desc: "Install engine runtime and development dependencies" run: once cmds: - uv python install 3.13.8 - - uv sync + - uv sync --locked --group engine --group engine-dev sources: - uv.lock - pyproject.toml status: - test -d .venv + lock: + desc: "Update the engine lockfile from project metadata" + cmds: + - uv lock + + lock:upgrade: + desc: "Upgrade allowed engine dependencies and update the lockfile" + cmds: + - uv lock --upgrade + + lock:check: + desc: "Check whether the engine lockfile is current" + cmds: + - uv lock --check + + update: + desc: "Upgrade engine dependencies and synchronize the environment" + cmds: + - task: lock:upgrade + - uv sync --locked --group engine --group engine-dev + + update:all: + desc: "Upgrade all Python dependency groups and synchronize the environment" + cmds: + - task: lock:upgrade + - uv sync --locked --all-groups + prepare: desc: "Set up engine .env from template" deps: [install] cmds: - - uv run scripts/setup_env.py + - uv run --locked --group engine --group engine-dev scripts/setup_env.py sources: - scripts/setup_env.py generates: @@ -33,7 +60,7 @@ tasks: env: PYTHONUNBUFFERED: "1" cmds: - - uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}" + - uv run --locked --group engine uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --workers "${STIRLING_ENGINE_WORKERS:-4}" dev: desc: "Start engine dev server with hot reload" @@ -45,43 +72,43 @@ tasks: env: PYTHONUNBUFFERED: "1" cmds: - - uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload + - uv run --locked --group engine --group engine-dev uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload lint: desc: "Run linting" deps: [install] cmds: - - uv run ruff check . + - uv run --locked --group engine --group engine-dev ruff check . lint:fix: desc: "Auto-fix lint issues" deps: [install] cmds: - - uv run ruff check . --fix + - uv run --locked --group engine --group engine-dev ruff check . --fix format: desc: "Auto-fix code formatting" deps: [install] cmds: - - uv run ruff format . + - uv run --locked --group engine --group engine-dev ruff format . format:check: desc: "Check code formatting" deps: [install] cmds: - - uv run ruff format . --diff + - uv run --locked --group engine --group engine-dev ruff format . --diff typecheck: desc: "Run type checking" deps: [install] cmds: - - uv run pyright . --warnings + - uv run --locked --group engine --group engine-dev pyright . --warnings test: desc: "Run tests" deps: [prepare] cmds: - - uv run pytest tests + - uv run --locked --group engine --group engine-dev pytest tests fix: desc: "Auto-fix lint + format" @@ -102,12 +129,19 @@ tasks: desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)" deps: [install, ":backend:swagger"] cmds: - - uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py + - uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py sources: - ../SwaggerDoc.json - scripts/generate_tool_models.py generates: - src/stirling/models/tool_models.py + - src/stirling/models/tool_io.py + + tool-models:check: + desc: "Fail if the committed tool models are out of date" + deps: [install, ":backend:swagger"] + cmds: + - uv run --locked --group engine --group engine-dev python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check clean: desc: "Clean build artifacts" diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml index 7ac5300a15..f5325c9ed7 100644 --- a/.taskfiles/frontend.yml +++ b/.taskfiles/frontend.yml @@ -5,6 +5,14 @@ version: '3' # mode flag) or use `--project editor/...` for tsc — so the editor lives # under frontend/editor/ without each task needing a cd. +vars: + # Dev-only browser-tab label so concurrent worktrees are distinguishable. Only + # the worktree folder basename (e.g. "wt1") is exposed — never the full path, + # hostname, or user. Dropped from production builds. + DEV_LABEL: + sh: >- + {{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}} + tasks: install: desc: "Install dependencies" @@ -80,16 +88,52 @@ tasks: OPEN: '{{.OPEN | default ""}}' env: BACKEND_URL: '{{.BACKEND_URL}}' - # Dev-only browser-tab label so concurrent worktrees are distinguishable. - # Only the worktree folder basename (e.g. "wt1") is exposed — never the - # full path, hostname, or user. Consumed at dev-serve time by vite.config - # and dropped from production builds. - STIRLING_DEV_LABEL: - sh: >- - {{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}} + STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' cmds: - npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}} + # Separate from dev:_run rather than a flag on it: Task sets an `env:` key even + # when its value resolves to empty, and Vite treats an empty process.env VITE_* as + # authoritative over the committed editor/.env, so folding these in blanks Supabase + # config for the core, proprietary and desktop dev servers. + dev:_run:saas: + internal: true + ignore_error: true + # The backend's own env files, so both halves target one project. Paths are + # relative to this taskfile's dir, `frontend`. + dotenv: ['../app/.env.saas.local', '../app/.env.saas'] + vars: + PORT: '{{.PORT | default "5173"}}' + BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' + OPEN: '{{.OPEN | default ""}}' + SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' + env: + BACKEND_URL: '{{.BACKEND_URL}}' + STIRLING_DEV_LABEL: '{{.DEV_LABEL}}' + SAAS_ENV: '{{.SAAS_ENV}}' + # A real process.env VITE_* beats a committed .env in Vite (loadEnv applies + # process.env last), which is what lets this override editor/.env. + # + # These must stay `sh:`, not Go templates: dotenv values are visible to Task's + # embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is + # always empty. + VITE_SUPABASE_URL: + sh: | + case "${SAAS_ENV:-dev}" in + staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;; + *) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or run task staging:saas}" ;; + esac + echo "https://${ref}.supabase.co" + VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: + sh: | + case "${SAAS_ENV:-dev}" in + staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;; + *) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;; + esac + cmds: + - 'echo ">> frontend Supabase target: $VITE_SUPABASE_URL"' + - npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}} + dev: desc: "Start frontend dev server" cmds: @@ -111,13 +155,23 @@ tasks: vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } dev:saas: - desc: "Start frontend dev server in SaaS mode" + desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)" deps: - task: prepare vars: { MODE: saas } + vars: + SAAS_ENV: '{{.SAAS_ENV | default "dev"}}' + # prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves + # the committed editor/.env alone. + RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}' cmds: - - task: dev:_run - vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + - task: '{{.RUNNER}}' + vars: + MODE: saas + PORT: '{{.PORT}}' + BACKEND_URL: '{{.BACKEND_URL}}' + OPEN: '{{.OPEN}}' + SAAS_ENV: '{{.SAAS_ENV}}' dev:desktop: desc: "Start frontend dev server in desktop mode" @@ -184,16 +238,90 @@ tasks: storybook: desc: "Start Storybook dev server" - deps: [install] + deps: [prepare] cmds: - npx storybook dev -p 6006 {{.CLI_ARGS}} storybook:build: desc: "Build static Storybook" - deps: [install] + deps: [prepare] cmds: - npx storybook build {{.CLI_ARGS}} + storybook:browser: + internal: true + desc: "Install the Chromium build the story scan runs in" + run: once + deps: [install] + cmds: + - npx playwright install chromium + + storybook:test: + desc: "Scan every story in real Chromium: it must render and pass axe" + deps: [prepare, storybook:browser] + cmds: + # Runs each story as a browser test. Pass a filter through, e.g. + # task frontend:storybook:test -- Button + - npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}} + + storybook:a11y:light: + desc: "a11y gate over every story in light mode" + deps: [prepare, storybook:browser] + cmds: + - node .storybook/a11y-scan.mjs {{.CLI_ARGS}} + - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt + + storybook:a11y:dark: + desc: "a11y gate over every story in dark mode" + deps: [prepare, storybook:browser] + cmds: + - SCAN_THEME=dark node .storybook/a11y-scan.mjs {{.CLI_ARGS}} + - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json + + storybook:a11y: + desc: "a11y gate over every story, light and dark" + cmds: + - task: storybook:a11y:light + - task: storybook:a11y:dark + + storybook:a11y:changed: + desc: "a11y gate over the stories this branch affects (default base origin/main)" + summary: | + Scans the stories a branch affects, which is what pull requests run — a + full scan takes ~30 minutes, far too long to sit in front of every merge. + A story is affected if its file changed, or if a same-named sibling + source file changed (editing Button.tsx or Button.css re-scans + Button.stories.tsx — the story renders the live component, so a component + edit changes what the story shows without touching the story file). + Changes that ripple further than a component's own stories are covered by + the nightly full sweep. + + Pass a base ref through CLI_ARGS, e.g. + task frontend:storybook:a11y:changed -- origin/release + vars: + BASE: '{{.CLI_ARGS | default "origin/main"}}' + CHANGED: + sh: node .storybook/a11y-changed.mjs {{.CLI_ARGS | default "origin/main"}} + cmds: + - cmd: | + if [ -z '{{.CHANGED}}' ]; then + echo "a11y: no story files affected vs {{.BASE}} — nothing to check" + exit 0 + fi + rc=0 + task frontend:storybook:a11y:light -- {{.CHANGED}} || rc=1 + task frontend:storybook:a11y:dark -- {{.CHANGED}} || rc=1 + exit $rc + + storybook:a11y:record: + desc: "Re-record both a11y baselines (run after intentionally fixing/adding violations)" + deps: [prepare, storybook:browser] + cmds: + - node .storybook/a11y-scan.mjs + - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record + - SCAN_THEME=dark node .storybook/a11y-scan.mjs + - node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record --baseline .storybook/a11y-baseline.dark.json + # ============================================================ # Code quality # ============================================================ @@ -202,9 +330,18 @@ tasks: desc: "Run linting" deps: [install] cmds: - - task: lint:eslint - - task: lint:dpdm + - task: lint:oxlint - task: lint:colors + - task: lint:css + + lint:css: + desc: "Lint stylesheets for duplicate selectors" + deps: [install] + cmds: + # Covers the whole editor tree, including the portal/processor layer and + # public/css. Vendored CSS and build output are excluded via ignoreFiles + # in stylelint.config.mjs. + - npx stylelint "editor/**/*.css" lint:colors: desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components" @@ -222,25 +359,17 @@ tasks: cmds: - node editor/scripts/lint/theme-lint.mjs contrast - lint:eslint: - desc: "Run ESLint linting" + lint:oxlint: + desc: "Run oxlint linting" deps: [install] cmds: - - npx eslint --max-warnings=0 - - lint:dpdm: - desc: "Run circular import linting" - deps: [install] - cmds: - # Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is - # shell-agnostic. Covers the whole editor tree, including the portal layer. - - npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1 + - npx oxlint --config oxlint.config.ts --max-warnings=0 lint:fix: desc: "Auto-fix lint issues" deps: [install] cmds: - - npx eslint --fix + - npx oxlint --config oxlint.config.ts --fix format: desc: "Auto-fix code formatting" @@ -330,6 +459,13 @@ tasks: - task: typecheck:_run vars: { PROJECT: editor/src/portal/tsconfig.json } + typecheck:storybook: + desc: "Typecheck Storybook config and stories" + deps: [prepare] + cmds: + - task: typecheck:_run + vars: { PROJECT: .storybook/tsconfig.json } + typecheck:all: desc: "Typecheck all build variants" cmds: @@ -341,6 +477,7 @@ tasks: - task: typecheck:scripts - task: typecheck:prototypes - task: typecheck:portal + - task: typecheck:storybook # ============================================================ # Quality Gate @@ -384,8 +521,17 @@ tasks: test:editor: desc: "Run editor tests" deps: [prepare] + vars: + COVERAGE: '{{.COVERAGE | default .CI | default "false"}}' cmds: - - npx vitest run --root editor + - > + npx vitest run --root editor + {{if eq .COVERAGE "true"}}--coverage + --coverage.provider=v8 + --coverage.reporter=text-summary + --coverage.reporter=json-summary + --coverage.reporter=html + --coverage.reportsDirectory=./coverage{{end}} test:watch: desc: "Run tests in watch mode" @@ -395,24 +541,9 @@ tasks: test:coverage: desc: "Run tests with coverage (one-shot; CI-friendly)." - deps: [prepare] cmds: - # `vitest run` makes this CI-safe (the bare `vitest` form enters watch - # mode). Explicit reporter list because v8 + json-summary is what the - # coverage-summary.py helper consumes; html/text are kept for humans. - # - # reportsDirectory is pinned to ./coverage relative to vitest's root - # (--root editor), so output lands at frontend/editor/coverage/. The - # CI upload step reads from that path. An earlier attempt with - # `./editor/coverage` double-nested into frontend/editor/editor/coverage; - # pinning future-proofs against vitest changing the default. - - > - npx vitest run --root editor --coverage - --coverage.provider=v8 - --coverage.reporter=text-summary - --coverage.reporter=json-summary - --coverage.reporter=html - --coverage.reportsDirectory=./coverage + - task: test:editor + vars: { COVERAGE: "true" } # ============================================================ # Code Generation @@ -422,18 +553,19 @@ tasks: desc: "Generate tool API types from the Java OpenAPI spec" deps: [install, ":backend:swagger"] cmds: - - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts + - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts sources: - editor/scripts/generate-tool-api-types.mts - ../SwaggerDoc.json generates: - editor/src/core/types/toolApiTypes.ts + - editor/src/core/types/toolIO.ts tool-models:check: desc: "Fail if committed tool API types are out of date" deps: [install, ":backend:swagger"] cmds: - - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --check + - npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts --check licenses:generate: desc: "Generate frontend license report" diff --git a/.taskfiles/pre-commit.yml b/.taskfiles/pre-commit.yml index 136f852a5b..444115010f 100644 --- a/.taskfiles/pre-commit.yml +++ b/.taskfiles/pre-commit.yml @@ -45,6 +45,10 @@ vars: # which owns the version and caches the binary here. GITLEAKS_BIN: '.task/bin/gitleaks{{if eq OS "windows"}}.exe{{end}}' +env: + # Keep repository-wide checks isolated from the engine runtime environment. + UV_PROJECT_ENVIRONMENT: '.venv-pre-commit' + tasks: default: desc: "Check formatting, spelling, and secrets across the repo" @@ -73,22 +77,22 @@ tasks: - task: gitleaks install: - desc: "Install the pinned pre-commit Python tools (ruff, codespell, toml-sort)" + desc: "Install the pinned pre-commit Python tools" run: once cmds: - - uv sync --project scripts/pre-commit --locked + - uv sync --project engine --locked --group pre-commit sources: - - scripts/pre-commit/uv.lock - - scripts/pre-commit/pyproject.toml + - engine/uv.lock + - engine/pyproject.toml status: - - test -d scripts/pre-commit/.venv + - test -d engine/.venv-pre-commit clean: - desc: "Remove the cached gitleaks binary and the tool virtualenv" + desc: "Remove the cached gitleaks binary and the pre-commit virtualenv" cmds: - - cmd: rm -rf scripts/pre-commit/.venv .task/bin/gitleaks + - cmd: rm -rf engine/.venv-pre-commit .task/bin/gitleaks platforms: [linux, darwin] - - cmd: cmd /c "rmdir /s /q scripts\pre-commit\.venv & del /q .task\bin\gitleaks.exe" + - cmd: cmd /c "rmdir /s /q engine\.venv-pre-commit & del /q .task\bin\gitleaks.exe" platforms: [windows] ignore_error: true @@ -97,26 +101,26 @@ tasks: ruff: deps: [install] cmds: - - uv run --project scripts/pre-commit --no-sync ruff check --line-length=127 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}}) + - uv run --project engine --locked --group pre-commit ruff check --isolated --line-length=120 {{if .FIX}}--fix {{end}}$(git ls-files {{.PY_FILES}}) ruff-format: deps: [install] cmds: - - uv run --project scripts/pre-commit --no-sync ruff format {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}}) + - uv run --project engine --locked --group pre-commit ruff format --isolated --line-length=120 {{if .FIX}}{{else}}--check {{end}}$(git ls-files {{.PY_FILES}}) codespell: deps: [install] cmds: - - uv run --project scripts/pre-commit --no-sync codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}}) + - uv run --project engine --locked --group pre-commit codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}}) toml-sort: deps: [install] cmds: - - uv run --project scripts/pre-commit --no-sync toml-sort --all --ignore-case {{if .FIX}}--in-place{{else}}--check{{end}} {{.LOCALE_TOML}} + - uv run --project engine --locked --group pre-commit python scripts/pre-commit/sort_locale_toml.py {{if .FIX}}--fix {{end}}{{.LOCALE_TOML}} whitespace: cmds: - - uv run --no-project python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}} + - uv run --project engine --locked --group pre-commit python scripts/pre-commit/whitespace.py {{if .FIX}}--fix {{end}}{{.WS_FILES}} gitleaks: deps: [gitleaks-bin] @@ -130,4 +134,4 @@ tasks: internal: true desc: "Ensure the pinned, checksum-verified gitleaks binary is cached in .task/bin" cmds: - - uv run --no-project python scripts/pre-commit/install_gitleaks.py + - uv run --project engine --locked --group pre-commit python scripts/pre-commit/install_gitleaks.py diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a02437abe7..409d13d2ba 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,8 +2,6 @@ "recommendations": [ "elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality "josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide - "ms-python.black-formatter", // Python code formatter using Black - "ms-python.flake8", // Flake8 linter for Python to enforce code quality "ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support "ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers // "Oracle.oracle-java", // Oracle Java extension with additional features for Java development @@ -19,6 +17,7 @@ "yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing "stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting "redhat.vscode-yaml", // YAML extension for Visual Studio Code - "dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting + "tamasfe.even-better-toml", // TOML language support and formatting + "oxc.oxc-vscode", // Oxc (oxlint) extension for JavaScript/TypeScript linting ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c231b4ef7..ad5c38b976 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,8 +20,9 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" + "editor.defaultFormatter": "charliermarsh.ruff" }, + "ruff.configuration": "${workspaceFolder}/engine/pyproject.toml", "[gradle-kotlin-dsl]": { "editor.defaultFormatter": "vscjava.vscode-gradle" }, @@ -72,40 +73,52 @@ "stirling", ], "java.project.resourceFilters": [ + ".cache/", + ".claude/", ".devcontainer/", ".git/", + ".git-blame-ignore-revs", + ".gitattributes", ".github/", + ".gitignore", ".gradle/", + ".pre-commit-config.yaml", + ".task/", + ".taskfiles/", ".venv/", ".venv*/", ".vscode/", - "bin/", - "app/core/bin/", + "app/.gitignore", + "app/build/", + "app/common/.gitignore", "app/common/bin/", - "app/proprietary/bin/", - "build/", - "app/core/build/", "app/common/build/", - "app/proprietary/build/", - "configs/", + "app/core/.gitignore", + "app/core/bin/", "app/core/configs/", - "customFiles/", "app/core/customFiles/", + "app/core/LOCAL_APPDATA_FONTCONFIG_CACHE/", + "app/core/logs/", + "app/core/pipeline/", + "app/core/storage/", + "app/proprietary/.gitignore", + "app/proprietary/bin/", + "app/proprietary/storage/", + "app/saas/.gitignore", + "app/saas/bin/", + "app/saas/build/", + "bin/", + "build/", + "devGuide/", + "devTools/", + "docker/", "docs/", - "exampleYmlFiles", + "engine/", + "frontend/", "gradle/", "images/", - "logs/", - "pipeline/", "scripts/", "testings/", - ".git-blame-ignore-revs", - ".gitattributes", - ".gitignore", - "app/core/.gitignore", - "app/common/.gitignore", - "app/proprietary/.gitignore", - ".pre-commit-config.yaml", ], // Enables signature help in Java. "java.signatureHelp.enabled": true, @@ -134,13 +147,57 @@ "html.format.indentHandlebars": true, "html.format.preserveNewLines": true, "html.format.maxPreserveNewLines": 2, - "stylelint.configFile": "devTools/.stylelintrc.json", + "stylelint.configFile": "${workspaceFolder}/devTools/.stylelintrc.json", + "css.lint.unknownAtRules": "ignore", + "scss.lint.unknownAtRules": "ignore", + "less.lint.unknownAtRules": "ignore", "java.project.sourcePaths": [ "app/core/src/main/java", "app/common/src/main/java", "app/proprietary/src/main/java" ], + "[javascript]": { + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + } + }, + "[javascriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + } + }, "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + } + }, + "[typescriptreact]": { + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + } + }, + "oxc.enable.oxlint": true, + "oxc.enable.oxfmt": false, + "oxc.configPath": "frontend/oxlint.config.ts", + "oxc.requireConfig": true, + "oxc.lint.run": "onType", + "oxc.fixKind": "safe_fix", + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml", + // Keep TOML formatting compatible with .editorconfig and the pre-commit + // locale sorter. Key ordering itself is handled by task pre-commit:toml-sort. + "editor.insertSpaces": true, + "editor.tabSize": 4, + "editor.rulers": [127], + "evenBetterToml.formatter.alignEntries": false, + "evenBetterToml.formatter.alignComments": false, + "evenBetterToml.formatter.indentString": " ", + "evenBetterToml.formatter.columnWidth": 127, + "evenBetterToml.formatter.reorderKeys": false, + "evenBetterToml.formatter.reorderArrays": false, + "evenBetterToml.formatter.reorderInlineTables": false, + "evenBetterToml.formatter.trailingNewline": true, + "evenBetterToml.formatter.crlf": false } } diff --git a/AGENTS.md b/AGENTS.md index 259729bc04..7383ab3c3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,7 +192,7 @@ What goes where: - **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`. - **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing. -`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (enforced by ESLint). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`. +`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`. Rule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79eb43f645..65fc4bc262 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,13 @@ Thank you for your interest in contributing to Stirling-PDF! There are many ways to contribute other than writing code. For example, reporting bugs, creating suggestions, and adding or modifying translations. +## License + +By contributing to this project, you agree that your contributions will be licensed under the project [license](LICENSE), which follows an open-core model. +The codebase is a mix of MIT and source-available code, so your contribution is licensed according to the directory it is committed to. + +PRs are welcome in any directory by any user, just be aware of which license applies to the code you change. + ## Issue Guidelines Issues can be used to report bugs, request features, or ask questions. If you have a question, you could also ask us in our [Discord](https://discord.gg/FJUSXUSYec). @@ -21,7 +28,7 @@ This project uses [Task](https://taskfile.dev/) as a unified command runner. Aft 1. Install the `task` CLI: https://taskfile.dev/installation/ 2. Run `task install` to install all dependencies -3. Run `task dev` to start backend + frontend +3. Run `task dev` to start backend + frontend or `task desktop:dev` to start the desktop application 4. Run `task check` before submitting a PR Run `task --list` to see all available commands. @@ -63,7 +70,3 @@ For technical guides, setup instructions, and development resources: For configuration and usage guides, see: - [Database Guide](DATABASE.md) - Database setup and configuration - [OCR Guide](HowToUseOCR.md) - OCR setup and configuration - -## License - -By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/DeveloperGuide.md b/DeveloperGuide.md index fbbf6478ac..d87406715b 100644 --- a/DeveloperGuide.md +++ b/DeveloperGuide.md @@ -46,8 +46,8 @@ This guide focuses on developing for Stirling 2.0, including both the React fron - Docker - Git - Java JDK 25 -- Node.js 18+ and npm (required for frontend development) -- Gradle 7.0 or later (Included within the repo) +- Node.js 22+ and npm (required for frontend development) +- Gradle 9.0 or later (Included within the repo) - [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development) - Rust and Cargo (required for Tauri desktop app development) - Tauri CLI (install with `cargo install tauri-cli`) @@ -158,7 +158,7 @@ Stirling-PDF/ │ │ │ └── locales/ # Internationalization files (JSON) │ │ └── vite.config.ts # Vite configuration │ ├── package.json # Shared workspace dependencies -│ └── eslint.config.mjs # Shared lint config +│ └── oxlint.config.ts # Shared lint config ├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files) ├── docs/ # Documentation files ├── exampleYmlFiles/ # Example YAML configuration files @@ -504,7 +504,8 @@ For Stirling 2.0, new features are built as React components: 1. **Create a New Controller:** - Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory. - Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint. - - Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`. + - Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates.")`. + - If the endpoint transforms a document, declare what it accepts and produces with `@ToolIO`, for example `@ToolIO(produces = ToolFormat.PDF)`. This is what lets a pipeline containing the step be checked before it runs, so a chain that cannot work is caught in the builder rather than part-way through a job. Endpoints under the tool namespaces are required to carry it - `ToolIODeclarationCoverageTest` fails the build otherwise. See [Declaring tool inputs and outputs](#declaring-tool-inputs-and-outputs). ```java package stirling.software.SPDF.controller.api; @@ -578,6 +579,38 @@ For Stirling 2.0, new features are built as React components: } ``` +### Declaring tool inputs and outputs + +An endpoint that transforms a document declares what it accepts and produces with `@ToolIO`. This is the single source of truth: it is published into the OpenAPI spec as an `x-stirling-io` extension, and generated from there into the frontend (`toolIO.ts`) and the AI engine (`tool_io.py`). A pipeline can therefore be checked while it is being edited, instead of failing part-way through a job. + +```java +@ToolIO(produces = ToolFormat.PDF) +``` + +`accepts` defaults to `{ ToolFormat.PDF }` and `arity` to `ToolArity.SISO`, so most tools only declare what they produce. + +- **`ToolFormat`** is the kind of file: `PDF`, `PDF_ENCRYPTED`, `IMAGE`, `ZIP`, `WORD`, `PPT`, `EXCEL`, `CSV`, `HTML`, `XML`, `JSON`, `TEXT`, `MARKDOWN`, `JAVASCRIPT`, `EBOOK`, `EMAIL`, `POSTSCRIPT`, `VIDEO`, `CBZ`, `CBR`, plus `ANY` (accepts or produces anything) and `NONE` (returns a report, not a file). Encryption is a format rather than a flag, so the default `accepts = PDF` means an endpoint rejects an encrypted PDF unless it opts in. +- **`ToolArity`** is how many files go in and out: `SISO`, `SIMO`, `MISO`, `MIMO`. This axis carries ZIP-as-transport. A splitter is `produces = PDF, arity = SIMO`, and the caller unpacks the archive; an endpoint whose deliverable really is an archive declares `produces = ZIP` with a single-output arity and stays packed. + +When the output depends on a parameter, declare the exception as a case rather than picking one answer. Add Password produces an encrypted PDF unless both passwords are blank, in which case it has only set permissions: + +```java +@ToolIO( + produces = ToolFormat.PDF_ENCRYPTED, + cases = + @ToolIOCase( + when = { + @ToolIOWhen(param = "password", matches = ""), + @ToolIOWhen(param = "ownerPassword", matches = "") + }, + produces = ToolFormat.PDF, + arity = ToolArity.SISO)) +``` + +Every condition in a `when` must hold for the case to apply, and `matches` is compared as a string, case-insensitively, with an empty string matching an absent or blank value. If a case reads a parameter that is not set yet, the output is reported as uncertain and the chain warns rather than erroring. + +Endpoints under the tool namespaces must carry a declaration; `ToolIODeclarationCoverageTest` fails the build for any that does not, with a short allowlist for endpoints that manage a session, a device or a stored resource rather than transforming a document. The matching rules are implemented three times (Java `ToolChainValidator`, `toolIOCompat.ts`, `tool_io_compat.py`) and pinned to the same answers by the shared fixtures in `testing/tool-io-cases.json`, so a behaviour change belongs in that file first. + ## Adding New Translations to Existing Language Files in Stirling-PDF When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide: diff --git a/Taskfile.yml b/Taskfile.yml index 75183bd4f9..92dcdcc742 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -25,6 +25,9 @@ includes: e2e: taskfile: .taskfiles/e2e.yml dir: . + cucumber: + taskfile: .taskfiles/cucumber.yml + dir: testing/cucumber pre-commit: taskfile: .taskfiles/pre-commit.yml dir: . @@ -96,11 +99,22 @@ tasks: BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' OPEN: "true" + # Set SAAS_DEV_PROJECT_REF in app/.env.saas.local to pick the PR. dev:saas: - desc: "Start SaaS backend + frontend concurrently on free ports" + desc: "Start SaaS backend + frontend + engine against the current PR's preview branch" cmds: - task: dev:_all - vars: { FRONTEND: saas, BACKEND: saas } + vars: { FRONTEND: saas, BACKEND: saas, SAAS_ENV: dev } + + staging:saas: + desc: "Start SaaS backend + frontend + engine against the shared v3 staging project" + cmds: + - task: dev:_all + vars: + FRONTEND: saas + BACKEND: saas + BACKEND_TASK: backend:staging:saas + SAAS_ENV: staging dev:all: desc: "Start backend + frontend + engine concurrently on free ports" @@ -112,6 +126,9 @@ tasks: vars: FRONTEND: '{{.FRONTEND | default "proprietary"}}' BACKEND: '{{.BACKEND | default "proprietary"}}' + BACKEND_TASK: '{{.BACKEND_TASK | default (printf "backend:dev:%s" .BACKEND)}}' + # Only meaningful to the saas frontend; every other flavor ignores it. + SAAS_ENV: '{{.SAAS_ENV | default ""}}' PORTS: sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}' BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}' @@ -121,7 +138,7 @@ tasks: - task: engine:dev vars: PORT: '{{.ENGINE_PORT}}' - - task: 'backend:dev:{{.BACKEND}}' + - task: '{{.BACKEND_TASK}}' vars: PORT: '{{.BACKEND_PORT}}' AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}' @@ -131,6 +148,7 @@ tasks: PORT: '{{.FRONTEND_PORT}}' BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}' OPEN: "true" + SAAS_ENV: '{{.SAAS_ENV}}' # ============================================================ # Build @@ -194,6 +212,12 @@ tasks: - task: frontend:tool-models - task: engine:tool-models + tool-models:check: + desc: "Fail if any committed API model is out of date" + cmds: + - task: frontend:tool-models:check + - task: engine:tool-models:check + # ============================================================ # Quality Gate # ============================================================ diff --git a/WINDOWS_SIGNING.md b/WINDOWS_SIGNING.md index 58ffd6e657..95cbbd24e2 100644 --- a/WINDOWS_SIGNING.md +++ b/WINDOWS_SIGNING.md @@ -4,6 +4,11 @@ This guide explains how to set up Windows code signing for Stirling-PDF desktop ## Overview +Releases are signed with **DigiCert KeyLocker**, a cloud HSM: the private key never +leaves DigiCert, and the runner signs through a PKCS#11 provider. The older approach +of uploading a base64 `.pfx` to a repository secret has been removed from the +workflows - the sections below describe KeyLocker, which is what actually runs. + Windows code signing is essential for: - Preventing Windows SmartScreen warnings - Building trust with users @@ -49,29 +54,19 @@ openssl pkcs12 -export -out certificate.pfx -inkey private-key.key -in certifica ### Required Secrets -Navigate to your GitHub repository → Settings → Secrets and variables → Actions +Navigate to your GitHub repository → Settings → Environments → `release-signing`. -Add the following secrets: +These live in the `release-signing` environment, not at repository scope. That +environment requires reviewer approval and is limited to `main`, `release`, +`hotfix/*` and `v*` tags. All five come from the DigiCert ONE console. -#### 1. `WINDOWS_CERTIFICATE` -- **Description**: Base64-encoded .pfx certificate file -- **How to create**: - -**On macOS/Linux:** -```bash -base64 -i certificate.pfx | pbcopy # Copies to clipboard -``` - -**On Windows (PowerShell):** -```powershell -[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Set-Clipboard -``` - -Paste the entire base64 string into the GitHub secret. - -#### 2. `WINDOWS_CERTIFICATE_PASSWORD` -- **Description**: Password for the .pfx certificate -- **Value**: The password you set when creating/exporting the .pfx file +| Secret | Description | +| --- | --- | +| `SM_API_KEY` | KeyLocker API key. Also acts as the on/off switch: signing steps are gated on it being non-empty. | +| `SM_CLIENT_CERT_FILE_B64` | Base64-encoded PKCS#12 client authentication certificate. | +| `SM_CLIENT_CERT_PASSWORD` | Password for that client certificate. | +| `SM_KEYPAIR_ALIAS` | Alias of the signing keypair to use. | +| `SM_HOST` | DigiCert ONE host, e.g. `https://clientauth.one.digicert.com`. | ### Optional Secrets for Tauri Updater @@ -110,23 +105,23 @@ The Windows signing configuration is already set up: ### 2. GitHub Workflow (.github/workflows/tauri-build.yml) -The workflow includes three Windows signing steps: +The workflow includes four Windows signing steps, all gated on `SM_API_KEY` being +set and the ref being the release branch: -1. **Import Certificate**: Decodes and imports the .pfx certificate into Windows certificate store -2. **Build Tauri App**: Builds and signs the application using the imported certificate -3. **Verify Signature**: Validates that both .exe and .msi files are properly signed +1. **Setup DigiCert KeyLocker**: Installs the DigiCert signing tools via `digicert/ssm-code-signing` +2. **Setup DigiCert KeyLocker Certificate**: Writes the client cert and exports the PKCS#11 config +3. **Configure Windows code signing / Build Tauri app**: Signs through the PKCS#11 provider +4. **Verify Windows Code Signature**: Validates that the .exe and .msi are properly signed ## Testing the Setup ### 1. Local Testing (Windows Only) -Before pushing to GitHub, test locally: +KeyLocker is CI-only. To check signing locally, install your own certificate into +the Windows store and point Tauri at it; the build no longer reads any certificate +from an environment variable. ```powershell -# Set environment variables -$env:WINDOWS_CERTIFICATE = [Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) -$env:WINDOWS_CERTIFICATE_PASSWORD = "your-certificate-password" - # Build the application cd frontend npm run tauri build @@ -191,9 +186,10 @@ Look for: - Consider EV certificate for immediate reputation ### Certificate Not Found During Build -- Verify `WINDOWS_CERTIFICATE` secret is set -- Check base64 encoding is correct (no extra whitespace) -- Ensure password is correct +- Verify `SM_API_KEY` is present in the `release-signing` environment. If it is empty + the signing steps skip silently and the build succeeds unsigned. +- Check `SM_CLIENT_CERT_FILE_B64` base64 encoding is correct (no extra whitespace) +- Ensure `SM_CLIENT_CERT_PASSWORD` and `SM_KEYPAIR_ALIAS` match the DigiCert keypair ## Security Best Practices @@ -220,11 +216,10 @@ Look for: ## Certificate Lifecycle ### Before Expiration -1. Obtain new certificate from CA (typically annual renewal) -2. Convert to .pfx format if needed -3. Update `WINDOWS_CERTIFICATE` secret with new base64-encoded certificate -4. Update `WINDOWS_CERTIFICATE_PASSWORD` if password changed -5. Test build to verify new certificate works +1. Renew the certificate in the DigiCert ONE console (typically annual) +2. If the keypair alias changed, update `SM_KEYPAIR_ALIAS` in the `release-signing` environment +3. If the client authentication certificate was reissued, update `SM_CLIENT_CERT_FILE_B64` and `SM_CLIENT_CERT_PASSWORD` +4. Test build to verify the new certificate works ### Expired Certificates - Signed binaries remain valid (timestamp proves signing time) diff --git a/app/.env.saas b/app/.env.saas index fb5feec559..25eefb84c5 100644 --- a/app/.env.saas +++ b/app/.env.saas @@ -1,15 +1,16 @@ -############################################################################### -# Stirling-PDF SaaS environment defaults. +# Stirling-PDF SaaS environment defaults. Committed, non-secret. Real values for secrets go in +# .env.saas.local, which is loaded first and wins. Do not commit that file. # -# This file is committed and provides non-secret defaults loaded by -# `task backend:dev:saas`. Put real values for secrets (passwords, project -# refs, edge function secrets) in `.env.saas.local` - any variable set there -# takes precedence over what's defined here. +# Three environments, each deriving its Supabase URLs, JWT issuer and JWKS from one project ref: # -# DO NOT commit `.env.saas.local`. Only `.env.saas` is checked in. -############################################################################### +# prod PROFILES=none SAAS_DB_* the live project +# staging PROFILES=staging SAAS_STAGING_* pinned to v3, always there +# dev PROFILES=dev SAAS_DEV_* follows a SaaS PR's preview branch +# +# dev is the default for `task backend:dev:saas`. Use staging for somewhere stable; use dev when +# testing an open SaaS PR, since its preview branch is the only place those migrations are applied. -# ---------- Supabase project ---------- +# ---------- Supabase project (prod / no-profile) ---------- # Project reference (the subdomain part of .supabase.co). Required. # Set in .env.saas.local. SAAS_DB_PROJECT_REF= @@ -17,18 +18,35 @@ SAAS_DB_PROJECT_REF= # Edge function secret used by billing/license rollup calls. Set in .env.saas.local. SUPABASE_EDGE_FUNCTION_SECRET= -# ---------- Database (saas profile) ---------- -# Direct JDBC URL to the Supabase Postgres. Required when running the plain -# `saas` profile (i.e. without `--spring.profiles.include=dev`). +# ---------- Database (no profile) ---------- +# Direct JDBC URL to the Supabase Postgres. Required when running without +# `--spring.profiles.include=...`. # Example: jdbc:postgresql://db..supabase.co:5432/postgres SAAS_DB_URL= SAAS_DB_USERNAME=postgres SAAS_DB_PASSWORD= -# ---------- Database (dev profile overrides) ---------- -# Used when `--spring.profiles.include=dev` is active. The dev profile -# defaults the URL/username to the shared dev Supabase project, but the -# password must still be provided in .env.saas.local. -SAAS_DEV_DB_URL= +# ---------- staging profile ---------- +# The shared long-lived v3 project. application-staging.properties defaults the ref, +# URL, database host and meter endpoint, so staging needs only the password, in +# .env.saas.local. Set SAAS_STAGING_PROJECT_REF to repoint it; everything derives. +# +# The ref and publishable key are duplicated here because the task derives the +# frontend's VITE_SUPABASE_* from them and a shell cannot read a Spring default. +# Neither is secret: the ref is a public subdomain, the key ships in the bundle. +SAAS_STAGING_PROJECT_REF=qacaivhsjtftfwtgjvva +SAAS_STAGING_PUBLISHABLE_KEY=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow +SAAS_STAGING_DB_USERNAME=postgres +SAAS_STAGING_DB_PASSWORD= + +# ---------- dev profile ---------- +# The SaaS PR's Supabase preview branch. Take the ref from that PR's "Supabase +# Preview" check; the profile derives URL, JWT issuer, JWKS, meter endpoint and +# database host from it, so this one value follows a different PR. +# +# A preview branch has its own password and keys; the parent project's will not +# authenticate. Both go in .env.saas.local, along with the ref. +SAAS_DEV_PROJECT_REF= +SAAS_DEV_PUBLISHABLE_KEY= SAAS_DEV_DB_USERNAME=postgres SAAS_DEV_DB_PASSWORD= diff --git a/app/allowed-licenses.json b/app/allowed-licenses.json index 88ed8ba4d5..033661629f 100644 --- a/app/allowed-licenses.json +++ b/app/allowed-licenses.json @@ -32,6 +32,14 @@ "moduleName": ".*", "moduleLicense": "BSD-4 License" }, + { + "moduleName": ".*", + "moduleLicense": "Revised BSD" + }, + { + "moduleName": ".*", + "moduleLicense": "ISC" + }, { "moduleName": ".*", "moduleLicense": "MIT" @@ -48,6 +56,10 @@ "moduleName": ".*", "moduleLicense": "MIT-0" }, + { + "moduleName": ".*", + "moduleLicense": "MIT license" + }, { "moduleName": "com.github.jai-imageio:jai-imageio-core", "moduleLicense": "LICENSE.txt" @@ -144,6 +156,14 @@ "moduleName": ".*", "moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception" }, + { + "moduleName": ".*", + "moduleLicense": "GNU Lesser Public License" + }, + { + "moduleName": ".*", + "moduleLicense": "The GNU Lesser General Public License" + }, { "moduleName": "com.martiansoftware:jsap", "moduleLicense": "LGPL" @@ -212,14 +232,6 @@ "moduleName": "com.google.re2j:re2j", "moduleLicense": "Go License" }, - { - "moduleName": "com.hubspot:algebra", - "moduleLicense": null - }, - { - "moduleName": "com.hubspot.immutables:immutables-exceptions", - "moduleLicense": null - }, { "moduleName": ".*", "moduleLicense": "UnRar License" diff --git a/app/common/build.gradle b/app/common/build.gradle index 71ccbf236f..bb956504cc 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -10,19 +10,19 @@ dependencies { api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' api "org.apache.commons:commons-lang3:${commonsLang3}" - api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor + api 'com.drewnoakes:metadata-extractor:2.21.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' api "org.apache.pdfbox:pdfbox:$pdfboxVersion" api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion" api "org.apache.pdfbox:xmpbox:$pdfboxVersion" api "org.apache.pdfbox:preflight:$pdfboxVersion" - api 'com.github.junrar:junrar:7.6.0' // RAR archive support for CBR files + api 'com.github.junrar:junrar:8.0.0' // RAR archive support for CBR files api 'jakarta.servlet:jakarta.servlet-api:6.1.0' api 'org.snakeyaml:snakeyaml-engine:3.0.1' api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3" // Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage) - api 'org.simplejavamail:simple-java-mail:8.12.6' - api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support + api 'org.simplejavamail:simple-java-mail:9.3.2' + api 'org.simplejavamail:outlook-module:9.3.2' // MSG file support api 'jakarta.mail:jakarta.mail-api:2.1.5' runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5' @@ -36,18 +36,24 @@ dependencies { api "com.stirling:jpdfium:${jpdfiumVersion}" - // -PjpdfiumPlatforms=all| + // -PjpdfiumPlatforms=all|none| + // 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform). def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim() def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64'] - def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all' - ? jpdfiumAllPlatforms - : jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it } + def jpdfiumPlatforms + if (jpdfiumPlatformsProp == 'all') { + jpdfiumPlatforms = jpdfiumAllPlatforms + } else if (jpdfiumPlatformsProp == 'none') { + jpdfiumPlatforms = [] + } else { + jpdfiumPlatforms = jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it } + } def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) } if (jpdfiumInvalid) { throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " + - "Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.") + "Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.") } - logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}") + logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}") jpdfiumPlatforms.each { platform -> runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}" } diff --git a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index a154ed2a7a..5e8f7fe336 100644 --- a/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/common/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @@ -13,6 +14,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.PdfaLevelAServiceInterface; @Service @Slf4j @@ -51,12 +53,16 @@ public class EndpointConfiguration { private Map groupDisableReasons = new ConcurrentHashMap<>(); private Map> endpointAlternatives = new ConcurrentHashMap<>(); private final boolean runningProOrHigher; + private final boolean pdfUaAvailable; public EndpointConfiguration( ApplicationProperties applicationProperties, - @Qualifier("runningProOrHigher") boolean runningProOrHigher) { + @Qualifier("runningProOrHigher") boolean runningProOrHigher, + @Autowired(required = false) PdfaLevelAServiceInterface pdfaLevelAService) { this.applicationProperties = applicationProperties; this.runningProOrHigher = runningProOrHigher; + // The PDF/UA tagger ships in the proprietary module, and so do its endpoints. + this.pdfUaAvailable = pdfaLevelAService != null; init(); processEnvironmentConfigs(); } @@ -338,6 +344,7 @@ public class EndpointConfiguration { addEndpointToGroup("PageOps", "split-pages"); addEndpointToGroup("PageOps", "rearrange-pages"); addEndpointToGroup("PageOps", "rotate-pdf"); + addEndpointToGroup("PageOps", "auto-rotate-pdf"); addEndpointToGroup("PageOps", "multi-page-layout"); addEndpointToGroup("PageOps", "booklet-imposition"); addEndpointToGroup("PageOps", "scale-pages"); @@ -355,6 +362,7 @@ public class EndpointConfiguration { addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "img-to-pdf"); addEndpointToGroup("Convert", "pdf-to-pdfa"); + addEndpointToGroup("Convert", "pdf-to-ua"); addEndpointToGroup("Convert", "file-to-pdf"); addEndpointToGroup("Convert", "pdf-to-word"); addEndpointToGroup("Convert", "pdf-to-presentation"); @@ -394,6 +402,7 @@ public class EndpointConfiguration { // Backend-only endpoints (not in frontend tool registry endpoints) addEndpointToGroup("Security", "redact"); addEndpointToGroup("Security", "verify-pdf"); + addEndpointToGroup("Security", "accessibility-report"); addEndpointToGroup("Security", "sign"); // Adding endpoints to "Other" group @@ -528,6 +537,8 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "json-to-pdf"); addEndpointToGroup("Java", "pdf-to-video"); addEndpointToGroup("Java", "verify-pdf"); + addEndpointToGroup("Java", "pdf-to-ua"); + addEndpointToGroup("Java", "accessibility-report"); addEndpointToGroup("Java", "flatten"); addEndpointToGroup("Java", "unlock-pdf-forms"); addEndpointToGroup("Java", "validate-signature"); @@ -599,6 +610,8 @@ public class EndpointConfiguration { // veraPDF dependent endpoints addEndpointToGroup("veraPDF", "verify-pdf"); + addEndpointToGroup("veraPDF", "pdf-to-ua"); + addEndpointToGroup("veraPDF", "accessibility-report"); // Pdftohtml dependent endpoints addEndpointToGroup("Pdftohtml", "pdf-to-html"); @@ -629,6 +642,11 @@ public class EndpointConfiguration { disableGroup("enterprise"); } + if (!pdfUaAvailable) { + disableEndpoint("pdf-to-ua"); + disableEndpoint("accessibility-report"); + } + if (!applicationProperties.getSystem().isEnableUrlToPDF()) { disableEndpoint("url-to-pdf"); } diff --git a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java index adfad7704b..96293e9c3f 100644 --- a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java +++ b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java @@ -2,7 +2,9 @@ package stirling.software.common.aop; import java.io.IOException; import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -273,6 +275,7 @@ public class AutoJobAspect { // Store the fileId for later reference pdfFile.setFileId(fileId); + recordPendingInputFile(fileId); // Replace the original MultipartFile with our persistent copy MultipartFile persistentFile = fileStorage.retrieveFile(fileId); @@ -290,6 +293,29 @@ public class AutoJobAspect { return originalArgs; } + /** + * Queue an input copy for attribution to the job. The job id does not exist yet at this point, + * so {@link JobExecutorService} drains this list once it mints one. + */ + @SuppressWarnings("unchecked") + private void recordPendingInputFile(String fileId) { + try { + Object existing = request.getAttribute(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR); + List ids; + if (existing instanceof List list) { + ids = (List) list; + } else { + ids = new ArrayList<>(); + request.setAttribute(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR, ids); + } + ids.add(fileId); + } catch (RuntimeException ex) { + // Without a bound request the copy cannot be attributed; the periodic sweep is the + // only backstop, so make the miss visible rather than silently leaking the file. + log.warn("Could not record input copy {} for cleanup: {}", fileId, ex.getMessage()); + } + } + private String getJobIdFromContext() { try { return (String) request.getAttribute("jobId"); diff --git a/app/common/src/main/java/stirling/software/common/config/swagger/ToolIOOperationCustomizer.java b/app/common/src/main/java/stirling/software/common/config/swagger/ToolIOOperationCustomizer.java new file mode 100644 index 0000000000..b5c3a3ef87 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/config/swagger/ToolIOOperationCustomizer.java @@ -0,0 +1,109 @@ +package stirling.software.common.config.swagger; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; + +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; +import stirling.software.common.service.ToolIOParameterDefaults; + +/** + * Publishes each {@link ToolIO} into the spec as {@code x-stirling-io}, which is how the frontend + * and the AI engine get it. + * + *

Also appends the {@code Input:/Output:/Type:} line the docs used to carry by hand, so the + * published text is unchanged without anyone maintaining it. + */ +@Component +public class ToolIOOperationCustomizer + implements GlobalOperationCustomizer, GlobalOpenApiCustomizer { + + public static final String EXTENSION_NAME = "x-stirling-io"; + public static final String VOCABULARY_EXTENSION_NAME = "x-stirling-io-vocabulary"; + + // Published separately from the declarations: generators need the full vocabulary for their + // enums, and deriving it from what is present would shrink it when an endpoint is disabled. + @Override + public void customise(OpenAPI openApi) { + Map vocabulary = new LinkedHashMap<>(); + vocabulary.put("formats", names(ToolFormat.values())); + vocabulary.put("arities", names(ToolArity.values())); + openApi.addExtension(VOCABULARY_EXTENSION_NAME, vocabulary); + } + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + ToolIO declaration = handlerMethod.getMethodAnnotation(ToolIO.class); + if (declaration == null) { + return operation; + } + operation.addExtension(EXTENSION_NAME, toExtension(declaration, handlerMethod.getMethod())); + operation.setDescription(appendSummaryLine(operation.getDescription(), declaration)); + return operation; + } + + private static Map toExtension(ToolIO declaration, Method handler) { + Map extension = new LinkedHashMap<>(); + extension.put("accepts", names(declaration.accepts())); + extension.put("produces", declaration.produces().name()); + extension.put("arity", declaration.arity().name()); + if (declaration.cases().length > 0) { + extension.put("cases", cases(declaration, handler)); + } + return extension; + } + + private static List> cases(ToolIO declaration, Method handler) { + return Arrays.stream(declaration.cases()).map(rule -> toCase(rule, handler)).toList(); + } + + private static Map toCase(ToolIOCase rule, Method handler) { + Map entry = new LinkedHashMap<>(); + entry.put("when", Arrays.stream(rule.when()).map(c -> toCondition(c, handler)).toList()); + entry.put("produces", rule.produces().name()); + entry.put("arity", rule.arity().name()); + return entry; + } + + private static Map toCondition(ToolIOWhen condition, Method handler) { + Map entry = new LinkedHashMap<>(); + entry.put("param", condition.param()); + entry.put("matches", List.of(condition.matches())); + // The default the endpoint uses when this parameter is absent, so a step that never sends + // it still resolves. Omitted when the parameter is required with none. + ToolIOParameterDefaults.resolve(handler, condition.param()) + .ifPresent(value -> entry.put("default", value)); + return entry; + } + + private static List names(Enum[] values) { + return Arrays.stream(values).map(Enum::name).toList(); + } + + private static String appendSummaryLine(String description, ToolIO declaration) { + String summary = + "Input:" + + String.join("/", names(declaration.accepts())) + + " Output:" + + declaration.produces().name() + + " Type:" + + declaration.arity().name(); + return description == null || description.isBlank() + ? summary + : description.trim() + " " + summary; + } +} diff --git a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java index 82921c0ba2..5be836f82a 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java @@ -73,7 +73,7 @@ public class RuntimePathConfig { defaultWatchedFolders, watchedFoldersDirs, pipeline != null ? pipeline.getWatchedFoldersDir() : null); - this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0); + this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.getFirst(); this.pipelineFinishedFoldersPath = resolvePath( defaultFinishedFolders, diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 43f27eec2d..216e0c1e66 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -251,6 +251,21 @@ public class ApplicationProperties { */ private boolean allowPrivateS3Endpoints = false; + /** + * Whether a network source's host (SFTP, FTP, or SMB) may resolve to a loopback, + * link-local, or private address. Off by default so a connection cannot be pointed at + * internal services; enable for an on-network file server (e.g. an internal SFTP drop or a + * Samba share). + */ + private boolean allowPrivateNetworkSources = false; + + /** + * Hostnames (exact, case-insensitive) that a network source may use even when they resolve + * to a private or local address and {@code allowPrivateNetworkSources} is off. Lets shared + * infra allow one named on-prem file server without opening every internal host. + */ + private List allowedPrivateNetworkHosts = new java.util.ArrayList<>(); + /** * Whether an API/Purview/ConsignO integration's base URL may resolve to a loopback, * link-local, or private address. Off by default: unlike S3 connections, any user may @@ -1031,6 +1046,8 @@ public class ApplicationProperties { // 'https://app.example.com'). If not set, falls back to backendUrl. private boolean enableMobileScanner = true; // Enable mobile phone QR code upload feature + private boolean enableMobileSignature = + true; // Enable drawing signatures on a phone via QR code private MobileScannerSettings mobileScannerSettings = new MobileScannerSettings(); private ServerCertificate serverCertificate = new ServerCertificate(); @@ -1076,6 +1093,27 @@ public class ApplicationProperties { private Quotas quotas = new Quotas(); private Sharing sharing = new Sharing(); private Signing signing = new Signing(); + private Encryption encryption = new Encryption(); + + /** + * Encryption at rest for stored files (Pro/Enterprise). Enabling encrypts new writes; + * disabling later only stops encrypting new writes — existing encrypted files keep + * decrypting as long as the key material is present. The master key is resolved like the + * credential key: {@code stirling.security.fileEncryptionKey} property, {@code + * STIRLING_FILE_ENCRYPTION_KEY} env var, or an auto-generated {@code file-encryption.key} + * in the config directory. + */ + @Data + public static class Encryption { + private boolean enabled = false; + + /** + * Emit an audit event for every decrypt of an encrypted blob. Compliance reviewers + * (HIPAA) expect read audit, so it defaults on; busy multi-user installs can disable. + * Denied decrypts and key lifecycle events are always audited regardless. + */ + private boolean auditReads = true; + } @Data public static class Local { @@ -1278,7 +1316,7 @@ public class ApplicationProperties { public static class Ui { private String appNameNavbar; private List languages; - private String logoStyle = "classic"; // Options: "classic" (default) or "modern" + private String logoStyle = "modern"; // Options: "modern" (default) or "classic" private boolean defaultHideUnavailableTools = false; private boolean defaultHideUnavailableConversions = false; private HideDisabledTools hideDisabledTools = new HideDisabledTools(); @@ -1289,10 +1327,10 @@ public class ApplicationProperties { public String getLogoStyle() { // Validate and return either "modern" or "classic" - if ("modern".equalsIgnoreCase(logoStyle)) { - return "modern"; + if ("classic".equalsIgnoreCase(logoStyle)) { + return "classic"; } - return "classic"; // default + return "modern"; // default } @Data diff --git a/app/common/src/main/java/stirling/software/common/model/job/JobResult.java b/app/common/src/main/java/stirling/software/common/model/job/JobResult.java index aa43431a15..b3f1f31472 100644 --- a/app/common/src/main/java/stirling/software/common/model/job/JobResult.java +++ b/app/common/src/main/java/stirling/software/common/model/job/JobResult.java @@ -52,6 +52,13 @@ public class JobResult { /** Key/value metadata that survives the write-through into the shared job store. */ private final Map metadata = new ConcurrentHashMap<>(); + /** + * File ids of the persistent input copies made for this job. An async submit copies the upload + * into FileStorage so the job can still read it after the request returns; without tracking + * them here nothing would ever delete those copies. + */ + @JsonIgnore private final List inputFileIds = new CopyOnWriteArrayList<>(); + /** * Create a new JobResult with the given job ID * @@ -167,6 +174,22 @@ public class JobResult { return Collections.unmodifiableList(notes); } + /** Record a persistent input copy so job cleanup deletes it alongside the results. */ + public void addInputFileId(String fileId) { + if (fileId != null && !fileId.isBlank() && !inputFileIds.contains(fileId)) { + this.inputFileIds.add(fileId); + } + } + + /** + * File ids of this job's persistent input copies. + * + * @return An unmodifiable view of the input file ids + */ + public List getInputFileIds() { + return Collections.unmodifiableList(inputFileIds); + } + /** Attach a metadata value, e.g. a policy id so cluster peers can identify a policy run. */ public void putMetadata(String key, String value) { if (key != null && value != null) { diff --git a/app/common/src/main/java/stirling/software/common/model/oauth2/Provider.java b/app/common/src/main/java/stirling/software/common/model/oauth2/Provider.java index 185956fbb5..7a431baf26 100644 --- a/app/common/src/main/java/stirling/software/common/model/oauth2/Provider.java +++ b/app/common/src/main/java/stirling/software/common/model/oauth2/Provider.java @@ -60,54 +60,40 @@ public class Provider { } private UsernameAttribute validateUsernameAttribute(UsernameAttribute usernameAttribute) { - switch (name) { - case "google" -> { - return validateGoogleUsernameAttribute(usernameAttribute); - } - case "github" -> { - return validateGitHubUsernameAttribute(usernameAttribute); - } - case "keycloak" -> { - return validateKeycloakUsernameAttribute(usernameAttribute); - } - default -> { - return usernameAttribute; - } - } + return switch (name) { + case "google" -> validateGoogleUsernameAttribute(usernameAttribute); + case "github" -> validateGitHubUsernameAttribute(usernameAttribute); + case "keycloak" -> validateKeycloakUsernameAttribute(usernameAttribute); + default -> usernameAttribute; + }; } private UsernameAttribute validateKeycloakUsernameAttribute( UsernameAttribute usernameAttribute) { - switch (usernameAttribute) { - case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> { - return usernameAttribute; - } + return switch (usernameAttribute) { + case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> usernameAttribute; default -> throw new UnsupportedClaimException( String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName)); - } + }; } private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) { - switch (usernameAttribute) { - case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> { - return usernameAttribute; - } + return switch (usernameAttribute) { + case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> usernameAttribute; default -> throw new UnsupportedClaimException( String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName)); - } + }; } private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) { - switch (usernameAttribute) { - case LOGIN, EMAIL, NAME -> { - return usernameAttribute; - } + return switch (usernameAttribute) { + case LOGIN, EMAIL, NAME -> usernameAttribute; default -> throw new UnsupportedClaimException( String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName)); - } + }; } @Override diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolArity.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolArity.java new file mode 100644 index 0000000000..5966d4e181 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolArity.java @@ -0,0 +1,24 @@ +package stirling.software.common.model.tool; + +/** + * How many files an endpoint consumes and produces (Single/Multiple In, Single/Multiple Out). + * + *

This axis carries ZIP-as-transport: a multi-output endpoint returns its results zipped and the + * caller unpacks them, so {@code split-pages} is {@code produces = PDF, arity = SIMO} rather than + * naming a ZIP-of-PDF format. An endpoint whose deliverable really is an archive declares {@link + * ToolFormat#ZIP} with a single-output arity and stays packed. + */ +public enum ToolArity { + SISO, + SIMO, + MISO, + MIMO; + + public boolean isMultiInput() { + return this == MISO || this == MIMO; + } + + public boolean isMultiOutput() { + return this == SIMO || this == MIMO; + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolDiagnostic.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolDiagnostic.java new file mode 100644 index 0000000000..bc3184ba93 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolDiagnostic.java @@ -0,0 +1,47 @@ +package stirling.software.common.model.tool; + +/** + * One problem found while checking a chain, against the step that cannot run. {@code code} is + * stable so the frontend can pick its own wording; {@code message} is an English fallback. + */ +public record ToolDiagnostic(int stepIndex, Severity severity, String code, String message) { + + public enum Severity { + /** The chain cannot run as configured. Only this should block a save. */ + ERROR, + /** May not run, depending on configuration or file content. */ + WARN, + /** Worth knowing but not a problem, such as a step running once per file. */ + INFO + } + + /** The step declares no {@link ToolIO}, so nothing past it can be checked. */ + public static final String UNDECLARED = "undeclared-operation"; + + /** The previous step's output is not a format this step accepts. */ + public static final String FORMAT_MISMATCH = "format-mismatch"; + + /** The previous step's output depends on a parameter that is not set yet. */ + public static final String OUTPUT_UNCERTAIN = "output-uncertain"; + + /** The pipeline's input files are not a format the first step accepts. */ + public static final String SOURCE_MISMATCH = "source-mismatch"; + + /** The previous step emits several files and this one runs once per file. */ + public static final String FAN_OUT = "fan-out"; + + /** The previous step emits several files and this one consumes them in a single call. */ + public static final String FAN_IN = "fan-in"; + + public static ToolDiagnostic error(int stepIndex, String code, String message) { + return new ToolDiagnostic(stepIndex, Severity.ERROR, code, message); + } + + public static ToolDiagnostic warn(int stepIndex, String code, String message) { + return new ToolDiagnostic(stepIndex, Severity.WARN, code, message); + } + + public static ToolDiagnostic info(int stepIndex, String code, String message) { + return new ToolDiagnostic(stepIndex, Severity.INFO, code, message); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolFormat.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolFormat.java new file mode 100644 index 0000000000..3e83c94a39 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolFormat.java @@ -0,0 +1,62 @@ +package stirling.software.common.model.tool; + +import java.util.List; + +import lombok.Getter; + +/** + * The kind of file a tool endpoint consumes or produces. + * + *

Encryption is its own format rather than a separate attribute, so the endpoints accepting only + * {@link #PDF} reject an encrypted one without declaring anything. + * + *

Extensions are a lossy projection used for run-time file checks: {@link #PDF} and {@link + * #PDF_ENCRYPTED} share {@code pdf}, because a filename cannot tell you whether a PDF is encrypted. + */ +@Getter +public enum ToolFormat { + PDF("pdf"), + PDF_ENCRYPTED("pdf"), + + // Vector formats are folded in: the extension set has always included svg/eps, and splitting + // them out would make chains that run fine today report as broken. + IMAGE("png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "psd", "ai", "eps"), + + /** + * An archive that is itself the deliverable. Multiple results use {@link ToolArity} instead. + */ + ZIP("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "lzma", "z"), + + WORD("doc", "docx", "odt", "rtf"), + PPT("ppt", "pptx", "odp"), + EXCEL("xls", "xlsx", "ods"), + CSV("csv"), + HTML("html", "htm", "xhtml"), + XML("xml", "xsd", "xsl"), + JSON("json"), + TEXT("txt", "text", "md", "markdown"), + MARKDOWN("md", "markdown"), + JAVASCRIPT("js", "jsx"), + EBOOK("epub", "mobi", "azw3", "fb2", "txt", "docx"), + EMAIL("eml", "msg"), + POSTSCRIPT("ps", "eps"), + + PCL("pcl", "pxl"), + XPS("xps", "oxps"), + + VIDEO("mp4", "webm", "avi", "mov", "mkv"), + CBZ("cbz"), + CBR("cbr"), + + /** Never reported as incompatible. */ + ANY(), + + /** A report or a status rather than a document. */ + NONE(); + + private final List extensions; + + ToolFormat(String... extensions) { + this.extensions = List.of(extensions); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolIO.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolIO.java new file mode 100644 index 0000000000..30325b4b62 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolIO.java @@ -0,0 +1,30 @@ +package stirling.software.common.model.tool; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * What a tool endpoint consumes and produces, so a chain of steps can be checked before it runs. + * + *

The single source of truth: read off the handler method by {@code ToolIORegistry}, published + * into the OpenAPI spec as {@code x-stirling-io}, and generated from there into the frontend and + * the AI engine. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ToolIO { + + /** Defaulting to a plain PDF is what makes an ordinary endpoint reject an encrypted one. */ + ToolFormat[] accepts() default {ToolFormat.PDF}; + + ToolFormat produces(); + + ToolArity arity() default ToolArity.SISO; + + /** Overrides for an output that depends on a parameter; first match wins. */ + ToolIOCase[] cases() default {}; +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolIOCase.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOCase.java new file mode 100644 index 0000000000..b3af8c2348 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOCase.java @@ -0,0 +1,22 @@ +package stirling.software.common.model.tool; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An output that applies when every condition in {@link #when()} holds. + * + *

Conditions are ANDed because the interesting branches turn on more than one parameter: Add + * Password only leaves the document unencrypted when both passwords are absent. + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ToolIOCase { + + ToolIOWhen[] when(); + + ToolFormat produces(); + + ToolArity arity(); +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSource.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSource.java new file mode 100644 index 0000000000..1e0d896fbd --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSource.java @@ -0,0 +1,19 @@ +package stirling.software.common.model.tool; + +import java.util.Map; +import java.util.Optional; + +/** + * Supplies the {@link ToolIO} declaration for an endpoint path. An interface so a chain can be + * checked against a fixed set of declarations without standing up an application context. + */ +@FunctionalInterface +public interface ToolIOSource { + + Optional find(String operationPath); + + static ToolIOSource of(Map specs) { + Map copy = Map.copyOf(specs); + return path -> Optional.ofNullable(copy.get(path)); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSpec.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSpec.java new file mode 100644 index 0000000000..cf627d97ef --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOSpec.java @@ -0,0 +1,134 @@ +package stirling.software.common.model.tool; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** The runtime form of a {@link ToolIO} declaration, read off a handler method once at startup. */ +public record ToolIOSpec( + Set accepts, ToolFormat produces, ToolArity arity, List cases) { + + /** + * @param paramDefault the value used when the parameter is absent, or null when it has no + * default - an absent parameter then leaves the case unresolved rather than defaulted. + */ + public record When(String param, List matches, String paramDefault) { + + boolean holdsFor(Object value) { + String normalised = normalise(value); + return matches.stream().anyMatch(match -> normalise(match).equals(normalised)); + } + } + + /** Supplies the default value a request parameter takes when a caller omits it. */ + @FunctionalInterface + public interface ParameterDefaults { + Optional defaultFor(String param); + + ParameterDefaults NONE = param -> Optional.empty(); + } + + /** + * Both sides of a condition are normalised at comparison, not at construction: the declaration + * reaches the frontend and the engine as published data, and normalising only one side there + * would silently disagree with this one. + */ + public static String normalise(Object value) { + return value == null ? "" : String.valueOf(value).trim().toLowerCase(Locale.ROOT); + } + + public record Case(List when, ToolFormat produces, ToolArity arity) { + + public Case { + when = List.copyOf(when); + } + } + + /** {@code certain} is false when a {@link Case} keys on a parameter whose value is unknown. */ + public record Output(ToolFormat format, ToolArity arity, boolean certain) {} + + public ToolIOSpec { + accepts = Set.copyOf(accepts); + cases = List.copyOf(cases); + } + + public static ToolIOSpec from(ToolIO annotation) { + return from(annotation, ParameterDefaults.NONE); + } + + public static ToolIOSpec from(ToolIO annotation, ParameterDefaults defaults) { + return new ToolIOSpec( + new LinkedHashSet<>(Arrays.asList(annotation.accepts())), + annotation.produces(), + annotation.arity(), + Arrays.stream(annotation.cases()).map(rule -> toCase(rule, defaults)).toList()); + } + + private static Case toCase(ToolIOCase rule, ParameterDefaults defaults) { + List when = Arrays.stream(rule.when()).map(c -> toWhen(c, defaults)).toList(); + return new Case(when, rule.produces(), rule.arity()); + } + + private static When toWhen(ToolIOWhen condition, ParameterDefaults defaults) { + return new When( + condition.param(), + List.of(condition.matches()), + defaults.defaultFor(condition.param()).orElse(null)); + } + + /** + * First matching {@link Case} wins. A parameter the caller omitted resolves to its declared + * default; only a parameter with no default leaves the output uncertain, since an unseen value + * might then have picked another branch. + * + * @param parameters the step's configured parameters, or null when not known + */ + public Output resolveOutput(Map parameters) { + boolean sawUnknownParam = false; + for (Case rule : cases) { + boolean allHold = true; + for (When condition : rule.when()) { + Object value; + if (parameters != null && parameters.containsKey(condition.param())) { + value = parameters.get(condition.param()); + } else if (condition.paramDefault() != null) { + value = condition.paramDefault(); + } else { + sawUnknownParam = true; + allHold = false; + continue; + } + allHold &= condition.holdsFor(value); + } + if (allHold) { + return new Output(rule.produces(), rule.arity(), true); + } + } + return new Output(produces, arity, !sawUnknownParam); + } + + public Output resolveOutput() { + return resolveOutput(null); + } + + public boolean acceptsFormat(ToolFormat format) { + return format == ToolFormat.ANY + || accepts.contains(ToolFormat.ANY) + || accepts.contains(format); + } + + /** For run-time file checks. Empty means anything is accepted. */ + public List acceptedExtensions() { + if (accepts.contains(ToolFormat.ANY)) { + return List.of(); + } + return accepts.stream() + .flatMap(format -> format.getExtensions().stream()) + .distinct() + .toList(); + } +} diff --git a/app/common/src/main/java/stirling/software/common/model/tool/ToolIOWhen.java b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOWhen.java new file mode 100644 index 0000000000..7202ed1126 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/model/tool/ToolIOWhen.java @@ -0,0 +1,18 @@ +package stirling.software.common.model.tool; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** One condition on a request parameter, guarding a {@link ToolIOCase}. */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ToolIOWhen { + + String param(); + + /** + * Compared as strings, case-insensitively. An empty string matches an absent or blank value. + */ + String[] matches(); +} diff --git a/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java b/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java index c19468b5ed..a9932582f2 100644 --- a/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java +++ b/app/common/src/main/java/stirling/software/common/pdf/PdfMarkdownConverter.java @@ -361,8 +361,8 @@ public class PdfMarkdownConverter { if (xs.isEmpty()) { return List.of(lines); } - float minX = xs.get(0); - float maxX = xs.get(xs.size() - 1); + float minX = xs.getFirst(); + float maxX = xs.getLast(); float splitAt = (minX + maxX) / 2f; float biggestGap = 0; for (int i = 1; i < xs.size(); i++) { @@ -492,7 +492,7 @@ public class PdfMarkdownConverter { List> anchorGroups = new ArrayList<>(); List current = new ArrayList<>(); - current.add(cands.get(0)); + current.add(cands.getFirst()); for (int i = 1; i < cands.size(); i++) { float gap = cands.get(i - 1).y - cands.get(i).y; if (gap > splitThreshold) { @@ -513,8 +513,8 @@ public class PdfMarkdownConverter { if (anchors.size() < 2) { continue; } - float top = anchors.get(0).y; - float bottom = anchors.get(anchors.size() - 1).y; + float top = anchors.getFirst().y; + float bottom = anchors.getLast().y; // Each anchor seeds a row; absorb wrapped continuation lines (non-anchors within the // run's vertical span, with a little slack below the last row) into the anchor above. @@ -674,8 +674,8 @@ public class PdfMarkdownConverter { float minGutter = Math.max(10f, charWidth * 2.5f); List merged = new ArrayList<>(); for (float[] band : columns) { - if (!merged.isEmpty() && band[0] - merged.get(merged.size() - 1)[1] < minGutter) { - merged.get(merged.size() - 1)[1] = band[1]; + if (!merged.isEmpty() && band[0] - merged.getLast()[1] < minGutter) { + merged.getLast()[1] = band[1]; } else { merged.add(new float[] {band[0], band[1]}); } @@ -734,7 +734,7 @@ public class PdfMarkdownConverter { } } StringBuilder sb = new StringBuilder(); - sb.append(buildGfmRow(rows.get(0), widths, cols)).append('\n'); + sb.append(buildGfmRow(rows.getFirst(), widths, cols)).append('\n'); sb.append('|'); for (int c = 0; c < cols; c++) { sb.append('-').append("-".repeat(widths[c])).append('-').append('|'); @@ -910,8 +910,8 @@ public class PdfMarkdownConverter { } // Only merge a sentence continuation between two text paragraphs, never into/out of a // table. - if (!(output.get(output.size() - 1) instanceof String last) - || !(pageItems.get(0) instanceof String first)) { + if (!(output.getLast() instanceof String last) + || !(pageItems.getFirst() instanceof String first)) { return; } if (!first.isEmpty() @@ -932,13 +932,13 @@ public class PdfMarkdownConverter { for (Object e : elements) { if (e instanceof TableBlock tb && !out.isEmpty() - && out.get(out.size() - 1) instanceof TableBlock prev + && out.getLast() instanceof TableBlock prev && columnsMatch(flatten(prev.rows()), flatten(tb.rows()))) { List> merged = new ArrayList<>(prev.rows()); List> tail = tb.rows(); if (!tail.isEmpty() && !prev.rows().isEmpty() - && rowText(tail.get(0)).equals(rowText(prev.rows().get(0)))) { + && rowText(tail.getFirst()).equals(rowText(prev.rows().getFirst()))) { tail = tail.subList(1, tail.size()); } merged.addAll(tail); @@ -971,7 +971,7 @@ public class PdfMarkdownConverter { continue; } if (e instanceof TableBlock tb && !tb.rows().isEmpty()) { - return rowText(tb.rows().get(0)); + return rowText(tb.rows().getFirst()); } return null; } @@ -983,7 +983,7 @@ public class PdfMarkdownConverter { ordered.sort(Comparator.comparingDouble((Line l) -> l.y).reversed()); StringBuilder sb = new StringBuilder(); for (Line l : ordered) { - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } sb.append(l.text); diff --git a/app/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java b/app/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java index 7cc01541e4..9a7caefc7d 100644 --- a/app/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java +++ b/app/common/src/main/java/stirling/software/common/service/CustomPDFDocumentFactory.java @@ -17,7 +17,6 @@ import java.util.concurrent.Semaphore; import java.util.function.Consumer; import org.apache.pdfbox.Loader; -import org.apache.pdfbox.examples.util.DeletingRandomAccessFile; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.io.RandomAccessReadBufferedFile; @@ -31,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.util.DeletingRandomAccessFile; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.TempFileManager; @@ -729,4 +729,32 @@ public class CustomPDFDocumentFactory { p.toFile().deleteOnExit(); return p; } + + /** A custom RandomAccessRead implementation that deletes the file when closed */ + private static class DeletingRandomAccessFile extends RandomAccessReadBufferedFile { + private final Path tempFilePath; + + public DeletingRandomAccessFile(File file) throws IOException { + super(file); + this.tempFilePath = file.toPath(); + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + try { + boolean deleted = Files.deleteIfExists(tempFilePath); + if (deleted) { + log.info("Successfully deleted temp file: {}", tempFilePath); + } else { + log.warn("Failed to delete temp file (may not exist): {}", tempFilePath); + } + } catch (IOException e) { + log.error("Error deleting temp file: {}", tempFilePath, e); + } + } + } + } } diff --git a/app/common/src/main/java/stirling/software/common/service/FileStorage.java b/app/common/src/main/java/stirling/software/common/service/FileStorage.java index c5c1ddb5db..700a6e5a65 100644 --- a/app/common/src/main/java/stirling/software/common/service/FileStorage.java +++ b/app/common/src/main/java/stirling/software/common/service/FileStorage.java @@ -179,6 +179,21 @@ public class FileStorage { return fileStore.delete(fileId); } + /** + * Delete a stored file without the per-file ownership check. + * + *

Job cleanup authorises at the job level and then deletes that job's own files, so the + * deleter is legitimately not their owner - an admin sweeping every user's jobs, or the + * unauthenticated scheduled task. Routing those through {@link #deleteFile(String)} makes the + * ownership check throw and silently orphans the files on disk. + * + *

Only ever pass file ids read back off a job that the caller has already been authorised + * for; never a caller-supplied id. + */ + public boolean deleteFileAsSystem(String fileId) { + return fileStore.delete(fileId); + } + public boolean fileExists(String fileId) { enforceOwnership(fileId); return fileStore.exists(fileId); diff --git a/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java b/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java index dd49ab3f51..aa9bcaa640 100644 --- a/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java +++ b/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java @@ -157,7 +157,7 @@ public class InternalApiClient { boolean hasFilePart = body.values().stream() .flatMap(java.util.List::stream) - .anyMatch(v -> v instanceof Resource); + .anyMatch(Resource.class::isInstance); if (isAiTool && !hasFilePart) { headers.setContentType(MediaType.MULTIPART_FORM_DATA); } diff --git a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java index 23a23e868b..eca4413350 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java +++ b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java @@ -1,6 +1,7 @@ package stirling.software.common.service; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -19,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import jakarta.annotation.PreDestroy; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; @@ -32,6 +34,14 @@ import stirling.software.common.util.RegexPatternUtils; @Slf4j public class JobExecutorService { + /** + * Request attribute holding the FileStorage ids of persistent input copies made for the job + * about to be created. Populated before the job id exists (the aspect copies the upload while + * processing arguments), drained onto the JobResult as soon as the task is created so cleanup + * can delete them. + */ + public static final String PENDING_INPUT_FILE_IDS_ATTR = "autoJobPendingInputFileIds"; + private final TaskManager taskManager; private final FileStorage fileStorage; private final HttpServletRequest request; @@ -63,6 +73,21 @@ public class JobExecutorService { "Job executor configured with effective timeout of {} ms", this.effectiveTimeoutMs); } + /** Stop the service-owned executor when the application context is closed or restarted. */ + @PreDestroy + public void shutdown() { + log.debug("Shutting down job executor"); + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + executor.shutdownNow(); + } + } + public ResponseEntity runJobGeneric(boolean async, Supplier work) { return runJobGeneric(async, work, -1); } @@ -117,6 +142,7 @@ public class JobExecutorService { resourceWeight); taskManager.createTask(jobId); + registerPendingInputFiles(jobId); final String capturedJobIdForQueue = jobId; Supplier wrappedWork = @@ -147,6 +173,7 @@ public class JobExecutorService { return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null)); } else if (async) { taskManager.createTask(jobId); + registerPendingInputFiles(jobId); final String capturedJobId = jobId; @@ -468,4 +495,30 @@ public class JobExecutorService { } return baseJobId; } + + /** + * Hand the input copies made while processing arguments to the freshly created job, so job + * cleanup deletes them. Drains the attribute so a retry cannot attribute the same ids twice. + */ + @SuppressWarnings("unchecked") + private void registerPendingInputFiles(String jobId) { + if (request == null) { + return; + } + Object pending; + try { + pending = request.getAttribute(PENDING_INPUT_FILE_IDS_ATTR); + request.removeAttribute(PENDING_INPUT_FILE_IDS_ATTR); + } catch (RuntimeException ex) { + // No request bound to this thread (e.g. an internally dispatched job). + log.debug("Could not read pending input file ids: {}", ex.getMessage()); + return; + } + if (!(pending instanceof List ids)) { + return; + } + for (String fileId : (List) ids) { + taskManager.registerInputFile(jobId, fileId); + } + } } diff --git a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java index 7c544b6242..18958841b2 100644 --- a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java +++ b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java @@ -225,19 +225,21 @@ public class MobileScannerService { Path sessionDir = getSafeSessionDirectory(sessionId); if (Files.exists(sessionDir)) { // Delete all files in session directory - Files.walk(sessionDir) - .sorted( - (a, b) -> - -a.compareTo(b)) // Reverse order to delete files before - // directory - .forEach( - path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - log.warn("Failed to delete file: {}", path, e); - } - }); + try (var paths = Files.walk(sessionDir)) { + paths.sorted( + (a, b) -> + -a.compareTo( + b)) // Reverse order to delete files before + // directory + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("Failed to delete file: {}", path, e); + } + }); + } } log.info("Deleted session: {}", sessionId); } catch (IllegalArgumentException e) { diff --git a/app/common/src/main/java/stirling/software/common/service/PdfaLevelAServiceInterface.java b/app/common/src/main/java/stirling/software/common/service/PdfaLevelAServiceInterface.java new file mode 100644 index 0000000000..2ee55719b2 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/PdfaLevelAServiceInterface.java @@ -0,0 +1,22 @@ +package stirling.software.common.service; + +import java.util.List; + +/** + * Raises a converted PDF/A file from conformance level B to level A, which needs the tagging the + * PDF/UA tagger does. Implemented only in the proprietary module; core builds convert at level B. + */ +public interface PdfaLevelAServiceInterface { + + /** + * @param levelA true only when the file was tagged and validated, so the claim is never a guess + */ + record Result(byte[] pdfBytes, boolean levelA, List warnings) {} + + /** + * @param part PDF/A part, 1 to 3; part 1 keeps its PDF 1.4 version + * @param alsoDeclareUa additionally claim PDF/UA, but only if it validates + */ + Result upgradeToLevelA( + byte[] pdfBytes, int part, String language, String title, boolean alsoDeclareUa); +} diff --git a/app/common/src/main/java/stirling/software/common/service/TaskManager.java b/app/common/src/main/java/stirling/software/common/service/TaskManager.java index 3fd9ac3fe4..0d0f8c23aa 100644 --- a/app/common/src/main/java/stirling/software/common/service/TaskManager.java +++ b/app/common/src/main/java/stirling/software/common/service/TaskManager.java @@ -17,6 +17,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -48,6 +49,10 @@ public class TaskManager { @Value("${stirling.jobResultExpiryMinutes:30}") private int jobResultExpiryMinutes = 30; + /** Maximum age of a task that never reached a terminal state. */ + @Value("${stirling.job.pendingExpiryMinutes:1440}") + private int pendingJobExpiryMinutes = 1440; + private final FileStorage fileStorage; private final JobStore jobStore; private final ClusterBackplane clusterBackplane; @@ -230,6 +235,24 @@ public class TaskManager { return false; } + /** + * Record a persistent input copy against a job so cleanup deletes it with the results. + * + * @param jobId The job ID + * @param fileId The FileStorage id of the input copy + * @return true if the job exists and the id was recorded + */ + public boolean registerInputFile(String jobId, String fileId) { + JobResult jobResult = jobResults.get(jobId); + if (jobResult == null) { + log.warn("Attempted to register an input file against non-existent job ID: {}", jobId); + return false; + } + jobResult.addInputFileId(fileId); + log.debug("Registered input file {} for job {}", fileId, jobId); + return true; + } + /** Attach metadata to a job and write it through to the shared store for cluster peers. */ public boolean putMetadata(String jobId, String key, String value) { JobResult jobResult = jobResults.get(jobId); @@ -325,26 +348,73 @@ public class TaskManager { return jobResults.computeIfAbsent(jobId, JobResult::createNew); } + /** + * What a cleanup pass removed. Returned by the on-demand cleanup so callers can assert on it. + */ + public record CleanupSummary(int jobsRemoved, int filesDeleted, int jobsRetained) {} + /** Clean up old completed job results. No-op in cluster mode; the backplane TTL owns expiry. */ - public void cleanupOldJobs() { + public CleanupSummary cleanupOldJobs() { if (clusterBackplane != null && !clusterBackplane.shouldRunLocalCleanup()) { - return; + return new CleanupSummary(0, 0, jobResults.size()); } + return cleanupJobs(false, jobId -> true); + } + + /** + * Force-expire this node's finished jobs now, ignoring the age threshold. Jobs still running + * are left alone - deleting their files mid-flight would break them - and are reported as + * retained. + * + *

Unlike {@link #cleanupOldJobs()} this always runs locally: it is an explicit request to + * release this node's storage, not the scheduled sweep the backplane TTL owns. + * + * @param jobIdFilter Only jobs whose id passes this predicate are considered, so a caller can + * restrict the sweep to jobs the requester is allowed to touch + * @return What was removed + */ + public CleanupSummary cleanupFinishedJobsNow(Predicate jobIdFilter) { + return cleanupJobs(true, jobIdFilter); + } + + private CleanupSummary cleanupJobs(boolean force, Predicate filter) { LocalDateTime expiryThreshold = LocalDateTime.now().minus(jobResultExpiryMinutes, ChronoUnit.MINUTES); + LocalDateTime pendingExpiryThreshold = + LocalDateTime.now().minus(pendingJobExpiryMinutes, ChronoUnit.MINUTES); int removedCount = 0; + int filesDeleted = 0; + int retainedCount = 0; try { for (Map.Entry entry : jobResults.entrySet()) { JobResult result = entry.getValue(); - // Remove completed jobs that are older than the expiry threshold - if (result.isComplete() - && result.getCompletedAt() != null - && result.getCompletedAt().isBefore(expiryThreshold)) { + if (!filter.test(entry.getKey())) { + retainedCount++; + continue; + } + + boolean expiredCompletedJob = + result.isComplete() + && (force + || (result.getCompletedAt() != null + && result.getCompletedAt() + .isBefore(expiryThreshold))); + boolean abandonedPendingJob = + !result.isComplete() + && result.getCreatedAt() != null + && result.getCreatedAt().isBefore(pendingExpiryThreshold); + + // Remove old terminal results and abandoned pending jobs. Without the second + // branch, a client that starts a task and never completes it keeps its result in + // memory forever. + if (expiredCompletedJob || abandonedPendingJob) { // Clean up file results - cleanupJobFiles(result, entry.getKey()); + if (expiredCompletedJob) { + filesDeleted += cleanupJobFiles(result, entry.getKey()); + } // Remove the job result jobResults.remove(entry.getKey()); @@ -352,15 +422,22 @@ public class TaskManager { jobStore.delete(entry.getKey()); } removedCount++; + } else { + retainedCount++; } } if (removedCount > 0) { - log.info("Cleaned up {} expired job results", removedCount); + log.info( + "Cleaned up {} {} job results ({} files deleted)", + removedCount, + force ? "finished" : "expired", + filesDeleted); } } catch (Exception e) { log.error("Error during job cleanup: {}", e.getMessage(), e); } + return new CleanupSummary(removedCount, filesDeleted, retainedCount); } /** Mirror the in-memory {@code JobResult} into the cluster-visible {@link JobStore}. */ @@ -508,22 +585,43 @@ public class TaskManager { } } - /** Clean up files associated with a job result */ - private void cleanupJobFiles(JobResult result, String jobId) { + /** + * Clean up files associated with a job result: both the results and the persistent input copy + * an async submit made of the upload. + * + * @return The number of files actually deleted + */ + private int cleanupJobFiles(JobResult result, String jobId) { + int deleted = 0; // Clean up all result files if (result.hasFiles()) { for (ResultFile resultFile : result.getAllResultFiles()) { - try { - fileStorage.deleteFile(resultFile.getFileId()); - } catch (Exception e) { - log.warn( - "Failed to delete file {} for job {}: {}", - resultFile.getFileId(), - jobId, - e.getMessage()); + if (deleteJobFile(resultFile.getFileId(), jobId)) { + deleted++; } } } + for (String inputFileId : result.getInputFileIds()) { + if (deleteJobFile(inputFileId, jobId)) { + deleted++; + } + } + return deleted; + } + + /** + * Deletes as the system, not as the caller: an admin sweeping another user's jobs, or the + * scheduled task running with no security context, is not the file's owner, and the + * ownership-checked delete would throw and leave the file orphaned on disk. The job itself is + * already authorised by the time we get here, and these ids come off that job, not the request. + */ + private boolean deleteJobFile(String fileId, String jobId) { + try { + return fileStorage.deleteFileAsSystem(fileId); + } catch (Exception e) { + log.warn("Failed to delete file {} for job {}: {}", fileId, jobId, e.getMessage()); + return false; + } } /** Find the ResultFile metadata for a given file ID by searching through all job results */ diff --git a/app/common/src/main/java/stirling/software/common/service/ToolChainValidator.java b/app/common/src/main/java/stirling/software/common/service/ToolChainValidator.java new file mode 100644 index 0000000000..074a127925 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/ToolChainValidator.java @@ -0,0 +1,168 @@ +package stirling.software.common.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.model.tool.ToolDiagnostic; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIOSource; +import stirling.software.common.model.tool.ToolIOSpec; + +/** + * Whether a chain of steps can run: what each produces against what the next accepts. + * + *

The frontend and the AI engine implement the same rules against their generated copies, so a + * chain can be checked without a round trip. {@code testing/tool-io-cases.json} pins all three to + * the same answers. + */ +@Service +@RequiredArgsConstructor +public class ToolChainValidator { + + /** {@code parameters} may be null; only used to resolve an output that depends on one. */ + public record Step(String operation, Map parameters) {} + + private final ToolIOSource toolIO; + + public List validate(List steps) { + return validate(steps, null); + } + + /** + * @param sourceFormat the format entering step one, or null when unknown + */ + public List validate(List steps, ToolFormat sourceFormat) { + List diagnostics = new ArrayList<>(); + ToolIOSpec.Output carried = null; + + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + Optional found = toolIO.find(step.operation()); + if (found.isEmpty()) { + diagnostics.add( + ToolDiagnostic.warn( + i, + ToolDiagnostic.UNDECLARED, + "Step " + + step.operation() + + " does not declare what it accepts or produces, so the" + + " rest of the chain cannot be checked.")); + // Nothing is known past an undeclared step. + carried = null; + continue; + } + ToolIOSpec spec = found.get(); + + // Only the first step is handed the pipeline's input. Every later step is handed the + // previous step's output, which is simply unknown once an undeclared step intervened - + // checking it against the input again would judge it on a format it never receives. + if (i == 0) { + checkSource(diagnostics, i, step, spec, sourceFormat); + } else if (carried != null) { + checkTransition(diagnostics, i, step, spec, carried); + } + carried = spec.resolveOutput(step.parameters()); + } + return diagnostics; + } + + public static boolean hasErrors(List diagnostics) { + return diagnostics.stream().anyMatch(d -> d.severity() == ToolDiagnostic.Severity.ERROR); + } + + private static void checkSource( + List diagnostics, + int index, + Step step, + ToolIOSpec spec, + ToolFormat sourceFormat) { + if (sourceFormat == null || spec.acceptsFormat(sourceFormat)) { + return; + } + diagnostics.add( + ToolDiagnostic.error( + index, + ToolDiagnostic.SOURCE_MISMATCH, + "Step " + + step.operation() + + " accepts " + + describe(spec) + + " but the pipeline's input is " + + sourceFormat + + ".")); + } + + private static void checkTransition( + List diagnostics, + int index, + Step step, + ToolIOSpec spec, + ToolIOSpec.Output previous) { + + if (previous.format() == ToolFormat.NONE) { + diagnostics.add( + ToolDiagnostic.error( + index, + ToolDiagnostic.FORMAT_MISMATCH, + "The previous step returns a report rather than a file, so " + + step.operation() + + " has nothing to run on.")); + return; + } + + if (!spec.acceptsFormat(previous.format())) { + String message = + "Step " + + step.operation() + + " accepts " + + describe(spec) + + " but the previous step produces " + + previous.format() + + "."; + diagnostics.add( + previous.certain() + ? ToolDiagnostic.error(index, ToolDiagnostic.FORMAT_MISMATCH, message) + // Unresolved output: may yet be fine once the step is configured. + : ToolDiagnostic.warn(index, ToolDiagnostic.OUTPUT_UNCERTAIN, message)); + return; + } + + if (!previous.certain()) { + diagnostics.add( + ToolDiagnostic.warn( + index, + ToolDiagnostic.OUTPUT_UNCERTAIN, + "The previous step's output depends on how it is configured, so this" + + " step may not be able to run.")); + return; + } + + if (previous.arity().isMultiOutput()) { + diagnostics.add( + spec.arity().isMultiInput() + ? ToolDiagnostic.info( + index, + ToolDiagnostic.FAN_IN, + "This step combines every file the previous step produced.") + : ToolDiagnostic.info( + index, + ToolDiagnostic.FAN_OUT, + "This step runs once for each file the previous step" + + " produced.")); + } + } + + private static String describe(ToolIOSpec spec) { + return spec.accepts().stream() + .map(Enum::name) + .sorted() + .reduce((a, b) -> a + " or " + b) + .orElse("nothing"); + } +} diff --git a/app/common/src/main/java/stirling/software/common/service/ToolIOParameterDefaults.java b/app/common/src/main/java/stirling/software/common/service/ToolIOParameterDefaults.java new file mode 100644 index 0000000000..d77ca63de6 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/ToolIOParameterDefaults.java @@ -0,0 +1,92 @@ +package stirling.software.common.service; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Optional; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.extern.slf4j.Slf4j; + +/** + * The value a request parameter takes when the caller omits it, read from the request model so a + * {@code @ToolIOCase} can be resolved even for a step that never sends the parameter it branches + * on. The default is read from the field so it cannot drift. + */ +@Slf4j +public final class ToolIOParameterDefaults { + + // Swagger's sentinel for an unset @Schema string member; not a real default value. + private static final String SCHEMA_UNSET = "##default"; + + private ToolIOParameterDefaults() {} + + /** + * The default {@code param} resolves to when absent, or empty when the parameter is required + * with no declared default - in which case an unset value leaves the output genuinely unknown + * rather than defaulted, and the chain reports it as uncertain. + * + *

Precedence: an explicit {@code @Schema(defaultValue)}, then the field's own value (a + * primitive's language default, or an initializer), then the empty string for an optional field + * left null, and finally empty for a required field with none of the above. + */ + public static Optional resolve(Method handler, String param) { + for (Parameter parameter : handler.getParameters()) { + Field field = findField(parameter.getType(), param); + if (field != null) { + return fromField(parameter.getType(), field); + } + } + return Optional.empty(); + } + + private static Optional fromField(Class owner, Field field) { + Schema schema = field.getAnnotation(Schema.class); + if (schema != null + && !schema.defaultValue().isEmpty() + && !SCHEMA_UNSET.equals(schema.defaultValue())) { + return Optional.of(schema.defaultValue()); + } + Object value = readField(owner, field); + if (value != null) { + return Optional.of(String.valueOf(value)); + } + return isRequired(field, schema) ? Optional.empty() : Optional.of(""); + } + + private static Object readField(Class owner, Field field) { + try { + Object instance = owner.getDeclaredConstructor().newInstance(); + field.setAccessible(true); + return field.get(instance); + } catch (ReflectiveOperationException | RuntimeException e) { + // A request model we cannot instantiate leaves the default unknown, which the check + // treats conservatively as uncertain. Never break startup over it. + log.warn("Could not read default of {}.{}", owner.getSimpleName(), field.getName(), e); + return null; + } + } + + private static boolean isRequired(Field field, Schema schema) { + if (schema != null && schema.requiredMode() == Schema.RequiredMode.REQUIRED) { + return true; + } + return field.isAnnotationPresent(NotNull.class) + || field.isAnnotationPresent(NotBlank.class); + } + + private static Field findField(Class type, String name) { + for (Class c = type; c != null && c != Object.class; c = c.getSuperclass()) { + try { + return c.getDeclaredField(name); + } catch (NoSuchFieldException ignored) { + // Try the superclass; request models extend a shared file-input base. + } + } + return null; + } +} diff --git a/app/common/src/main/java/stirling/software/common/service/ToolIORegistry.java b/app/common/src/main/java/stirling/software/common/service/ToolIORegistry.java new file mode 100644 index 0000000000..c2219536d1 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/ToolIORegistry.java @@ -0,0 +1,130 @@ +package stirling.software.common.service; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOSource; +import stirling.software.common.model.tool.ToolIOSpec; + +/** + * Reads every {@link ToolIO} declaration off its handler method at startup and serves it by + * endpoint path. Replaces parsing the same information out of the description prose, which meant + * fetching our own {@code /v1/api-docs} over HTTP first. + */ +@Slf4j +@Service +public class ToolIORegistry implements ToolMetadataService, ToolIOSource { + + private final ApplicationContext applicationContext; + + // Written on the startup thread, read on request threads. Spring's lifecycle establishes + // happens-before, so no volatile (same as AiEngineEndpointResolver). + private Map specsByPath = Map.of(); + + // Keep this the only constructor: with two, Spring falls back to a no-arg one that isn't here. + public ToolIORegistry(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** A registry over known declarations rather than ones discovered from the context. */ + static ToolIORegistry forSpecs(Map specs) { + ToolIORegistry registry = new ToolIORegistry(null); + registry.specsByPath = Map.copyOf(specs); + return registry; + } + + @EventListener(ContextRefreshedEvent.class) + public void discoverToolIO() { + Map discovered = new TreeMap<>(); + for (RequestMappingHandlerMapping mapping : + applicationContext.getBeansOfType(RequestMappingHandlerMapping.class).values()) { + mapping.getHandlerMethods() + .forEach((info, handler) -> register(discovered, info, handler)); + } + specsByPath = Map.copyOf(discovered); + log.debug("Discovered {} endpoints declaring @ToolIO", specsByPath.size()); + } + + private static void register( + Map target, RequestMappingInfo info, HandlerMethod handler) { + ToolIO annotation = handler.getMethodAnnotation(ToolIO.class); + if (annotation == null) { + return; + } + Method method = handler.getMethod(); + ToolIOSpec spec = + ToolIOSpec.from( + annotation, param -> ToolIOParameterDefaults.resolve(method, param)); + for (String pattern : extractPatterns(info)) { + target.put(pattern, spec); + } + } + + @Override + public Optional find(String operationPath) { + return Optional.ofNullable(specsByPath.get(operationPath)); + } + + @Override + public boolean isMultiInput(String operationPath) { + return find(operationPath).map(spec -> spec.arity().isMultiInput()).orElse(false); + } + + @Override + public List getExtensionTypes(boolean output, String operationPath) { + Optional spec = find(operationPath); + if (spec.isEmpty()) { + return null; + } + List extensions = + output + ? spec.get().resolveOutput().format().getExtensions() + : spec.get().acceptedExtensions(); + // Callers express "no restriction" as null. + return extensions.isEmpty() ? null : extensions; + } + + @Override + public boolean shouldUnpackZipResponse(String operationPath) { + // Multi-output zips purely as transport. A single-output ZIP is the deliverable + // (extract-attachments) and stays packed. + return find(operationPath) + .map(spec -> spec.resolveOutput().arity().isMultiOutput()) + .orElse(false); + } + + private static Set extractPatterns(RequestMappingInfo info) { + try { + Method getDirectPaths = info.getClass().getMethod("getDirectPaths"); + Object result = getDirectPaths.invoke(info); + if (result instanceof Set set) { + Set patterns = new HashSet<>(); + for (Object value : set) { + if (value instanceof String s) { + patterns.add(s); + } + } + return patterns; + } + } catch (Exception e) { + log.trace("getDirectPaths unavailable on RequestMappingInfo", e); + } + return Set.of(); + } +} diff --git a/app/common/src/main/java/stirling/software/common/service/ToolMetadataService.java b/app/common/src/main/java/stirling/software/common/service/ToolMetadataService.java index fb7a928d76..89fbc89f5a 100644 --- a/app/common/src/main/java/stirling/software/common/service/ToolMetadataService.java +++ b/app/common/src/main/java/stirling/software/common/service/ToolMetadataService.java @@ -17,12 +17,12 @@ public interface ToolMetadataService { List getExtensionTypes(boolean output, String operationPath); /** - * Returns true when the endpoint's ZIP response is a transport for multiple typed results and - * should be unpacked: multi-output endpoints (Type:SIMO / Type:MIMO) and wrapper declarations - * such as {@code Output:ZIP-PDF} or {@code Output:IMAGE/ZIP}. + * Returns true when the endpoint's ZIP response is a transport for several results and should + * be unpacked, which is exactly the multi-output endpoints (a {@code SIMO} or {@code MIMO} + * arity). * - *

Returns false for a bare {@code Output:ZIP} (e.g. {@code get-attachments}), where the - * archive itself is the deliverable and should be kept packed. + *

Returns false for an endpoint whose declared output is an archive in its own right (for + * example {@code extract-attachments}), where unpacking would discard the deliverable. */ boolean shouldUnpackZipResponse(String operationPath); } diff --git a/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java b/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java index ab39ca5da8..16cd26add0 100644 --- a/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java @@ -140,20 +140,25 @@ public class ChecksumUtils { for (String algorithm : algorithms) { String key = algorithm; // keep original key for output - switch (algorithm.toUpperCase(Locale.ROOT)) { - case "CRC32": - checksums.put(key, new CRC32()); - break; - case "ADLER32": - checksums.put(key, new Adler32()); - break; - default: - try { - // For MessageDigest, pass the original name (case-insensitive per JCA) - digests.put(key, MessageDigest.getInstance(algorithm)); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Unsupported algorithm: " + algorithm, e); - } + Object digestOrChecksum = + switch (algorithm.toUpperCase(Locale.ROOT)) { + case "CRC32" -> new CRC32(); + case "ADLER32" -> new Adler32(); + default -> { + try { + // For MessageDigest, pass the original name (case-insensitive + // per JCA) + yield MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException( + "Unsupported algorithm: " + algorithm, e); + } + } + }; + if (digestOrChecksum instanceof Checksum checksum) { + checksums.put(key, checksum); + } else { + digests.put(key, (MessageDigest) digestOrChecksum); } } diff --git a/app/common/src/main/java/org/apache/pdfbox/examples/util/DeletingRandomAccessFile.java b/app/common/src/main/java/stirling/software/common/util/DeletingRandomAccessFile.java similarity index 96% rename from app/common/src/main/java/org/apache/pdfbox/examples/util/DeletingRandomAccessFile.java rename to app/common/src/main/java/stirling/software/common/util/DeletingRandomAccessFile.java index 2c0341e19a..9df98bec9c 100644 --- a/app/common/src/main/java/org/apache/pdfbox/examples/util/DeletingRandomAccessFile.java +++ b/app/common/src/main/java/stirling/software/common/util/DeletingRandomAccessFile.java @@ -1,4 +1,4 @@ -package org.apache.pdfbox.examples.util; +package stirling.software.common.util; import java.io.File; import java.io.IOException; diff --git a/app/common/src/main/java/stirling/software/common/util/EmlParser.java b/app/common/src/main/java/stirling/software/common/util/EmlParser.java index bdc5cc3d07..09d55a02c2 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlParser.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlParser.java @@ -194,7 +194,7 @@ public class EmlParser { } attachment.setFilename(filename); - String contentId = embedded ? stripCid(resourceName) : null; + String contentId = embedded ? stripCid(resource.getContentId()) : null; attachment.setContentId(contentId); String detectedContentType = EmlProcessingUtils.detectMimeType(filename, contentType); diff --git a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java index 0ec94fd51e..4aa49bd26f 100644 --- a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -751,7 +751,7 @@ public class ExceptionUtils { String targetDescription; if (errorInfo.affectedPages() != null && !errorInfo.affectedPages().isEmpty()) { if (errorInfo.affectedPages().size() == 1) { - targetDescription = "page " + errorInfo.affectedPages().get(0); + targetDescription = "page " + errorInfo.affectedPages().getFirst(); } else { targetDescription = "pages " @@ -848,7 +848,7 @@ public class ExceptionUtils { } // Use the first page number, or null if none found - Integer pageNumber = affectedPages.isEmpty() ? null : affectedPages.get(0); + Integer pageNumber = affectedPages.isEmpty() ? null : affectedPages.getFirst(); return new GhostscriptErrorInfo( ErrorCode.GHOSTSCRIPT_PAGE_DRAWING, diff --git a/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java b/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java index 3018a26fd9..1f971d8d9e 100644 --- a/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java +++ b/app/common/src/main/java/stirling/software/common/util/FormFieldTypeSupport.java @@ -114,7 +114,7 @@ public enum FormFieldTypeSupport { return; } - PDAnnotationWidget widget = checkBox.getWidgets().get(0); + PDAnnotationWidget widget = checkBox.getWidgets().getFirst(); PDAppearanceCharacteristicsDictionary appearanceChars = widget.getAppearanceCharacteristics(); diff --git a/app/common/src/main/java/stirling/software/common/util/FormUtils.java b/app/common/src/main/java/stirling/software/common/util/FormUtils.java index 25cb4b5825..a1862d379c 100644 --- a/app/common/src/main/java/stirling/software/common/util/FormUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/FormUtils.java @@ -88,28 +88,16 @@ public class FormUtils { * text) */ public String detectFieldType(PDField field) { - if (field instanceof PDSignatureField) { - return FIELD_TYPE_SIGNATURE; - } - if (field instanceof PDPushButton) { - return FIELD_TYPE_BUTTON; - } - if (field instanceof PDTextField) { - return FIELD_TYPE_TEXT; - } - if (field instanceof PDCheckBox) { - return FIELD_TYPE_CHECKBOX; - } - if (field instanceof PDComboBox) { - return FIELD_TYPE_COMBOBOX; - } - if (field instanceof PDListBox) { - return FIELD_TYPE_LISTBOX; - } - if (field instanceof PDRadioButton) { - return FIELD_TYPE_RADIO; - } - return FIELD_TYPE_TEXT; + return switch (field) { + case PDSignatureField ignored -> FIELD_TYPE_SIGNATURE; + case PDPushButton ignored -> FIELD_TYPE_BUTTON; + case PDTextField ignored -> FIELD_TYPE_TEXT; + case PDCheckBox ignored -> FIELD_TYPE_CHECKBOX; + case PDComboBox ignored -> FIELD_TYPE_COMBOBOX; + case PDListBox ignored -> FIELD_TYPE_LISTBOX; + case PDRadioButton ignored -> FIELD_TYPE_RADIO; + case null, default -> FIELD_TYPE_TEXT; + }; } public List extractFormFields(PDDocument document) { @@ -583,22 +571,17 @@ public class FormUtils { continue; } String type = info.type(); - Object value; - switch (type) { - case FIELD_TYPE_CHECKBOX: - value = isChecked(info.value()) ? Boolean.TRUE : Boolean.FALSE; - break; - case FIELD_TYPE_LISTBOX: - if (info.multiSelect()) { - value = new ArrayList<>(); - } else { - value = safeDefault(info.value()); - } - break; - case FIELD_TYPE_BUTTON, FIELD_TYPE_SIGNATURE: - continue; // skip non-fillable - default: - value = safeDefault(info.value()); + Object value = + switch (type) { + case FIELD_TYPE_CHECKBOX -> + isChecked(info.value()) ? Boolean.TRUE : Boolean.FALSE; + case FIELD_TYPE_LISTBOX -> + info.multiSelect() ? new ArrayList<>() : safeDefault(info.value()); + case FIELD_TYPE_BUTTON, FIELD_TYPE_SIGNATURE -> null; + default -> safeDefault(info.value()); + }; + if (value == null) { + continue; // skip non-fillable } record.put(info.name(), value); } @@ -949,44 +932,44 @@ public class FormUtils { if (selection == null || selection.trim().isEmpty()) return null; List filtered = filterChoiceSelections(List.of(selection), allowedOptions, fieldName); - return filtered.isEmpty() ? null : filtered.get(0); + return filtered.isEmpty() ? null : filtered.getFirst(); } private void applyValueToField(PDField field, String value, boolean strict) throws IOException { try { - if (field instanceof PDTextField textField) { - setTextValue(textField, value); - } else if (field instanceof PDCheckBox checkBox) { - LinkedHashSet candidateStates = collectCheckBoxStates(checkBox); - boolean shouldCheck = shouldCheckBoxBeChecked(value, candidateStates); - try { - if (shouldCheck) { - checkBox.check(); - } else { - checkBox.unCheck(); - } - } catch (IOException checkProblem) { - log.warn( - "Failed to set checkbox state for '{}': {}", - field.getFullyQualifiedName(), - checkProblem.getMessage(), - checkProblem); - if (strict) { - throw checkProblem; + switch (field) { + case PDTextField textField -> setTextValue(textField, value); + case PDCheckBox checkBox -> { + LinkedHashSet candidateStates = collectCheckBoxStates(checkBox); + boolean shouldCheck = shouldCheckBoxBeChecked(value, candidateStates); + try { + if (shouldCheck) { + checkBox.check(); + } else { + checkBox.unCheck(); + } + } catch (IOException checkProblem) { + log.warn( + "Failed to set checkbox state for '{}': {}", + field.getFullyQualifiedName(), + checkProblem.getMessage(), + checkProblem); + if (strict) { + throw checkProblem; + } } } - } else if (field instanceof PDRadioButton radioButton) { - if (value != null && !value.isBlank()) { - radioButton.setValue(value); + case PDRadioButton radioButton -> { + if (value != null && !value.isBlank()) { + radioButton.setValue(value); + } } - } else if (field instanceof PDChoice choiceField) { - applyChoiceValue(choiceField, value); - } else if (field instanceof PDPushButton) { - log.debug("Ignore Push button"); - } else if (field instanceof PDSignatureField) { - log.debug("Skipping signature field '{}'", field.getFullyQualifiedName()); - } else { - field.setValue(value != null ? value : ""); + case PDChoice choiceField -> applyChoiceValue(choiceField, value); + case PDPushButton ignored -> log.debug("Ignore Push button"); + case PDSignatureField ignored -> + log.debug("Skipping signature field '{}'", field.getFullyQualifiedName()); + case null -> log.warn("Attempted to set value on null field"); + default -> field.setValue(value != null ? value : ""); } } catch (Exception e) { log.warn( @@ -1306,37 +1289,42 @@ public class FormUtils { List resolveOptions(PDTerminalField field) { try { - if (field instanceof PDChoice choice) { - LinkedHashSet allowed = new LinkedHashSet<>(); - List exportValues = choice.getOptionsExportValues(); - List displayValues = choice.getOptionsDisplayValues(); + return switch (field) { + case PDChoice choice -> { + LinkedHashSet allowed = new LinkedHashSet<>(); + List exportValues = choice.getOptionsExportValues(); + List displayValues = choice.getOptionsDisplayValues(); - if (exportValues != null) { - exportValues.stream() - .filter(Objects::nonNull) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .forEach(allowed::add); + if (exportValues != null) { + exportValues.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(allowed::add); + } + if (displayValues != null) { + displayValues.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .forEach(allowed::add); + } + yield new ArrayList<>(allowed); } - if (displayValues != null) { - displayValues.stream() - .filter(Objects::nonNull) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .forEach(allowed::add); + case PDRadioButton radio -> { + List exports = radio.getExportValues(); + yield exports != null && !exports.isEmpty() + ? new ArrayList<>(exports) + : Collections.emptyList(); } - return new ArrayList<>(allowed); - } else if (field instanceof PDRadioButton radio) { - List exports = radio.getExportValues(); - if (exports != null && !exports.isEmpty()) { - return new ArrayList<>(exports); + case PDCheckBox checkBox -> { + List exports = checkBox.getExportValues(); + yield exports != null && !exports.isEmpty() + ? new ArrayList<>(exports) + : Collections.emptyList(); } - } else if (field instanceof PDCheckBox checkBox) { - List exports = checkBox.getExportValues(); - if (exports != null && !exports.isEmpty()) { - return new ArrayList<>(exports); - } - } + case null, default -> Collections.emptyList(); + }; } catch (Exception e) { log.debug( "Failed to resolve options for field '{}': {}", @@ -1465,7 +1453,7 @@ public class FormUtils { // Only check options for choice-type fields (combobox, listbox, radio) if (CHOICE_FIELD_TYPES.contains(type) && options != null && !options.isEmpty()) { - String optionCandidate = cleanLabel(options.get(0)); + String optionCandidate = cleanLabel(options.getFirst()); if (optionCandidate != null && !looksGeneric(optionCandidate)) { return optionCandidate; } @@ -1557,7 +1545,7 @@ public class FormUtils { continue; } - PDAnnotationWidget widget = widgets.get(0); + PDAnnotationWidget widget = widgets.getFirst(); PDRectangle originalRectangle = cloneRectangle(widget.getRectangle()); PDPage page = resolveWidgetPage(document, widget, null); if (page == null || originalRectangle == null) { @@ -2446,19 +2434,19 @@ public class FormUtils { private static int firstWidgetPageIndex(FormFieldWithCoordinates f) { return (f.getWidgets() != null && !f.getWidgets().isEmpty()) - ? f.getWidgets().get(0).getPageIndex() + ? f.getWidgets().getFirst().getPageIndex() : -1; } private static float firstWidgetY(FormFieldWithCoordinates f) { return (f.getWidgets() != null && !f.getWidgets().isEmpty()) - ? f.getWidgets().get(0).getY() + ? f.getWidgets().getFirst().getY() : 0; } private static float firstWidgetX(FormFieldWithCoordinates f) { return (f.getWidgets() != null && !f.getWidgets().isEmpty()) - ? f.getWidgets().get(0).getX() + ? f.getWidgets().getFirst().getX() : 0; } diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java index 4a9ef0834b..1d7320ca84 100644 --- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java @@ -12,6 +12,7 @@ import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,6 +55,10 @@ public class GeneralUtils { private final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs"; private final String PYTHON_SCRIPTS_DIR = "python"; + + // Extracted once per run. Rewriting a script while another request is exec-ing it + // races wherever rename is not atomic, such as 9p or NFS bind mounts. + private final Map EXTRACTED_SCRIPTS = new ConcurrentHashMap<>(); private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance(); // Valid size units used for convertSizeToBytes validation and parsing private final Set VALID_SIZE_UNITS = Set.of("B", "KB", "MB", "GB", "TB"); @@ -941,7 +946,7 @@ public class GeneralUtils { } // If no MAC address found, use hostname as fallback - if (sb.length() == 0) { + if (sb.isEmpty()) { String hostname = InetAddress.getLocalHost().getHostName(); sb.append(hostname != null ? hostname : "unknown-host"); log.warn("No MAC address found, using hostname for fingerprint generation"); @@ -1025,17 +1030,30 @@ public class GeneralUtils { } Path scriptsDir = Path.of(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR); - Files.createDirectories(scriptsDir); - Path target = scriptsDir.resolve(scriptName); - ClassPathResource res = - new ClassPathResource("static/" + PYTHON_SCRIPTS_DIR + "/" + scriptName); - if (!res.exists()) { - log.error("Resource not found: {}", res.getPath()); - throw new IOException("Resource not found: " + res.getPath()); + + Path cached = EXTRACTED_SCRIPTS.get(scriptName); + if (cached != null && Files.isRegularFile(cached)) { + return cached; + } + + synchronized (EXTRACTED_SCRIPTS) { + cached = EXTRACTED_SCRIPTS.get(scriptName); + if (cached != null && Files.isRegularFile(cached)) { + return cached; + } + + Files.createDirectories(scriptsDir); + ClassPathResource res = + new ClassPathResource("static/" + PYTHON_SCRIPTS_DIR + "/" + scriptName); + if (!res.exists()) { + log.error("Resource not found: {}", res.getPath()); + throw new IOException("Resource not found: " + res.getPath()); + } + copyResourceToFile(res, target); + EXTRACTED_SCRIPTS.put(scriptName, target); + return target; } - copyResourceToFile(res, target); - return target; } /* diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index 979ad25e99..030751f3e3 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -26,29 +26,27 @@ import lombok.extern.slf4j.Slf4j; public class ImageProcessingUtils { static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { - BufferedImage convertedImage; - switch (colorType) { - case "greyscale": - convertedImage = + return switch (colorType) { + case "greyscale" -> { + BufferedImage convertedImage = new BufferedImage( sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - case "blackwhite": - convertedImage = + yield convertedImage; + } + case "blackwhite" -> { + BufferedImage convertedImage = new BufferedImage( sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - default: // full color - convertedImage = sourceImage; - break; - } - return convertedImage; + yield convertedImage; + } + default -> sourceImage; + }; } public static byte[] getImageData(BufferedImage image) { diff --git a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java index feac968c55..ce164e9db2 100644 --- a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java +++ b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java @@ -330,7 +330,7 @@ public class PDFToFile { if (outputFiles.size() == 1) { // Return single output file - File outputFile = outputFiles.get(0); + File outputFile = outputFiles.getFirst(); if ("txt:Text".equals(outputFormat)) { outputFormat = "txt"; } diff --git a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java index eb36711c0f..1a8786bf1d 100644 --- a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java +++ b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java @@ -307,7 +307,7 @@ public class ProcessExecutor { boolean isQpdf = commandToRun != null && !commandToRun.isEmpty() - && commandToRun.get(0).contains("qpdf"); + && commandToRun.getFirst().contains("qpdf"); if (!outputLines.isEmpty()) { String outputMessage = String.join("\n", outputLines); @@ -370,7 +370,7 @@ public class ProcessExecutor { } // Check if this is a UNO conversion by looking for unoconvert executable - String executable = command.get(0); + String executable = command.getFirst(); if (executable != null) { // Extract basename from path for matching String basename = executable; @@ -504,7 +504,7 @@ public class ProcessExecutor { } // Validate executable (first argument) - String executable = command.get(0); + String executable = command.getFirst(); if (executable == null || executable.isBlank()) { throw new IllegalArgumentException("Command executable must not be empty"); } diff --git a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java index 48e419a0ae..9d2d1b74db 100644 --- a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java @@ -1,17 +1,22 @@ package stirling.software.common.util; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.UncheckedExecutionException; + import lombok.extern.slf4j.Slf4j; @Slf4j public final class RegexPatternUtils { private static final RegexPatternUtils INSTANCE = new RegexPatternUtils(); - private final ConcurrentHashMap patternCache = new ConcurrentHashMap<>(); + private static final long MAX_CACHED_PATTERNS = 512; + private final Cache patternCache = + CacheBuilder.newBuilder().maximumSize(MAX_CACHED_PATTERNS).build(); private static final String WHITESPACE_REGEX = "\\s++"; private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$"; @@ -51,7 +56,7 @@ public final class RegexPatternUtils { throw new IllegalArgumentException("Regex pattern cannot be null"); } - return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern); + return getOrCompile(new PatternKey(regex, 0)); } /** @@ -77,7 +82,7 @@ public final class RegexPatternUtils { throw new IllegalArgumentException("Regex pattern cannot be null"); } - return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern); + return getOrCompile(new PatternKey(regex, flags)); } /** @@ -98,7 +103,7 @@ public final class RegexPatternUtils { * @return true if pattern is cached, false otherwise */ public boolean isCached(String regex, int flags) { - return regex != null && patternCache.containsKey(new PatternKey(regex, flags)); + return regex != null && patternCache.getIfPresent(new PatternKey(regex, flags)) != null; } /** @@ -107,7 +112,7 @@ public final class RegexPatternUtils { * @return number of patterns currently cached */ public int getCacheSize() { - return patternCache.size(); + return (int) patternCache.size(); } /** @@ -115,7 +120,7 @@ public final class RegexPatternUtils { * useful for testing or memory cleanup in long-running applications. */ public void clearCache() { - patternCache.clear(); + patternCache.invalidateAll(); log.debug("Regex pattern cache cleared"); } @@ -141,13 +146,32 @@ public final class RegexPatternUtils { return false; } PatternKey key = new PatternKey(regex, flags); - boolean removed = patternCache.remove(key) != null; + boolean removed = patternCache.getIfPresent(key) != null; + patternCache.invalidate(key); if (removed) { log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags); } return removed; } + private Pattern getOrCompile(PatternKey key) { + try { + return patternCache.get(key, () -> compilePattern(key)); + } catch (UncheckedExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof PatternSyntaxException patternSyntaxException) { + throw patternSyntaxException; + } + throw e; + } catch (java.util.concurrent.ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof PatternSyntaxException patternSyntaxException) { + throw patternSyntaxException; + } + throw new IllegalStateException("Failed to compile regex pattern", cause); + } + } + /** * Internal method to compile a pattern and handle errors consistently. * @@ -537,10 +561,6 @@ public final class RegexPatternUtils { getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters getPattern("[^a-zA-Z0-9 ]"); // Input sanitization getPattern("[^a-zA-Z0-9]"); // Filename sanitization - // API doc patterns - getPattern("Output:\\s*(\\w+)"); - getPattern("Input:\\s*(\\w+)"); - getPattern("Type:\\s*(\\w+)"); log.debug("Pre-compiled {} common regex patterns", patternCache.size()); } @@ -550,23 +570,6 @@ public final class RegexPatternUtils { "^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$"); } - /* Pattern for matching Output: in API descriptions */ - public Pattern getApiDocOutputTypePattern() { - return getPattern("Output:\\s*(\\w+)"); - } - - /* Pattern for matching Input: in API descriptions */ - public Pattern getApiDocInputTypePattern() { - return getPattern("Input:\\s*(\\w+)"); - } - - /** - * Pattern for matching Type: in API descriptions - */ - public Pattern getApiDocTypePattern() { - return getPattern("Type:\\s*(\\w+)"); - } - /* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */ public Pattern getFileExtensionValidationPattern() { return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE); diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index a653ae6c0b..bfd3f53c67 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -4,7 +4,14 @@ import java.util.regex.Pattern; public class RequestUriUtils { - private static final Pattern SHARE_LINK_PATTERN = Pattern.compile("^/share/[^/]+/?$"); + // Share tokens are 36-char lowercase UUIDs (UUID.randomUUID().toString()); match exactly + private static final Pattern SHARE_LINK_PATTERN = + Pattern.compile( + "^/share/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/?$"); + // Invite tokens are 36-char lowercase UUIDs (UUID.randomUUID().toString()); match exactly + private static final Pattern INVITE_LINK_PATTERN = + Pattern.compile( + "^/invite/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/?$"); public static boolean isStaticResource(String requestURI) { return isStaticResource("", requestURI); @@ -56,8 +63,10 @@ public class RequestUriUtils { return true; } - // Mobile scanner page for QR code-based file uploads (peer-to-peer, no backend auth needed) - if (normalizedUri.startsWith("/mobile-scanner")) { + // Mobile pages reached by scanning a QR code (peer-to-peer, no backend auth + // needed): /mobile-scanner uploads photos, /mobile-sign draws a signature. + if (normalizedUri.startsWith("/mobile-scanner") + || normalizedUri.startsWith("/mobile-sign")) { return true; } @@ -67,7 +76,7 @@ public class RequestUriUtils { // cookie, so the server can't authenticate the navigation itself). The // portal gates access via its own auth gate + RequirePortalAccess, and its // data APIs stay protected, so serving the shell pre-auth is safe. - if (normalizedUri.equals("/processor") || normalizedUri.startsWith("/processor/")) { + if ("/processor".equals(normalizedUri) || normalizedUri.startsWith("/processor/")) { return true; } @@ -207,7 +216,9 @@ public class RequestUriUtils { // Workflow participant endpoints - access controlled by share tokens, not login || trimmedUri.startsWith("/api/v1/workflow/participant/") // Share-link SPA bootstrap; data APIs remain protected - || SHARE_LINK_PATTERN.matcher(trimmedUri).matches(); + || SHARE_LINK_PATTERN.matcher(trimmedUri).matches() + // Invite-accept SPA bootstrap; data APIs remain protected + || INVITE_LINK_PATTERN.matcher(trimmedUri).matches(); } private static String stripContextPath(String contextPath, String requestURI) { diff --git a/app/common/src/main/java/stirling/software/common/util/YamlHelper.java b/app/common/src/main/java/stirling/software/common/util/YamlHelper.java index 8fb6896f67..2715f50dfb 100644 --- a/app/common/src/main/java/stirling/software/common/util/YamlHelper.java +++ b/app/common/src/main/java/stirling/software/common/util/YamlHelper.java @@ -114,7 +114,7 @@ public class YamlHelper { for (NodeTuple tuple : mappingNode.getValue()) { ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null; - if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) { + if (keyNode == null || !keyNode.getValue().equals(keys.getFirst())) { updatedTuples.add(tuple); continue; } diff --git a/app/common/src/test/java/stirling/software/SPDF/config/EndpointConfigurationGapTest.java b/app/common/src/test/java/stirling/software/SPDF/config/EndpointConfigurationGapTest.java index 6275b49343..afc6fa7020 100644 --- a/app/common/src/test/java/stirling/software/SPDF/config/EndpointConfigurationGapTest.java +++ b/app/common/src/test/java/stirling/software/SPDF/config/EndpointConfigurationGapTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import stirling.software.SPDF.config.EndpointConfiguration.DisableReason; import stirling.software.SPDF.config.EndpointConfiguration.EndpointAvailability; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.PdfaLevelAServiceInterface; /** * Unit tests for {@link EndpointConfiguration}. The class wires up its endpoint/group registry in @@ -32,7 +33,14 @@ class EndpointConfigurationGapTest { * Construct an EndpointConfiguration with the given pro flag and current applicationProperties. */ private EndpointConfiguration build(boolean runningProOrHigher) { - return new EndpointConfiguration(applicationProperties, runningProOrHigher); + return build(runningProOrHigher, null); + } + + /** The PDF/UA service is only present in proprietary builds, so it is injected separately. */ + private EndpointConfiguration build( + boolean runningProOrHigher, PdfaLevelAServiceInterface pdfaLevelAService) { + return new EndpointConfiguration( + applicationProperties, runningProOrHigher, pdfaLevelAService); } /** Default config: not pro, no removals, url-to-pdf disabled (default System flag is false). */ @@ -177,6 +185,28 @@ class EndpointConfigurationGapTest { } } + @Nested + @DisplayName("PDF/UA availability") + class PdfUaTests { + + @Test + @DisplayName("the PDF/UA endpoints are off when the proprietary tagger is absent") + void disabledWithoutTagger() { + EndpointConfiguration config = build(false, null); + assertFalse(config.isEndpointEnabled("pdf-to-ua")); + assertFalse(config.isEndpointEnabled("accessibility-report")); + } + + @Test + @DisplayName("they are on once the tagger is on the classpath") + void enabledWithTagger() { + EndpointConfiguration config = + build(false, (pdfBytes, part, language, title, alsoDeclareUa) -> null); + assertTrue(config.isEndpointEnabled("pdf-to-ua")); + assertTrue(config.isEndpointEnabled("accessibility-report")); + } + } + @Nested @DisplayName("group enable / disable") class GroupTests { diff --git a/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java b/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java index b3c104da85..7e1d3d2e35 100644 --- a/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java +++ b/app/common/src/test/java/stirling/software/common/pdf/PdfMarkdownConverterTest.java @@ -154,7 +154,7 @@ class PdfMarkdownConverterTest { || isTableSeparatorRow(line)) { continue; } - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append('\n'); } sb.append(line); diff --git a/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java b/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java index 861efa8509..9bf78b6af3 100644 --- a/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java +++ b/app/common/src/test/java/stirling/software/common/service/FileStorageOwnershipTest.java @@ -2,6 +2,7 @@ package stirling.software.common.service; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -65,6 +66,24 @@ class FileStorageOwnershipTest { assertThrows(SecurityException.class, () -> fs.deleteFile(id)); } + @Test + void systemDeleteOfAnotherUsersFile_allowed_soJobCleanupDoesNotOrphanIt(@TempDir Path tempDir) + throws IOException { + // An admin sweeping every user's finished jobs is not the owner of their files. The + // ownership-checked delete throws there, which used to drop the job record and leave the + // files stranded on disk with nothing left able to reference them. + AtomicReference user = new AtomicReference<>("alice"); + FileStorage fs = newStorageWithCurrentUser(tempDir, user); + String id = fs.storeBytes("alice's file".getBytes(), "x.bin"); + user.set("admin"); + + assertThrows(SecurityException.class, () -> fs.deleteFile(id)); + assertTrue(fs.deleteFileAsSystem(id), "System delete must not be blocked by ownership"); + + user.set("alice"); + assertThrows(IOException.class, () -> fs.retrieveBytes(id), "File should really be gone"); + } + @Test void anonymousRetrieveOfOwnedFile_allowed_noCurrentUserMeansNoCompare(@TempDir Path tempDir) throws IOException { diff --git a/app/common/src/test/java/stirling/software/common/service/TaskManagerCleanupTest.java b/app/common/src/test/java/stirling/software/common/service/TaskManagerCleanupTest.java new file mode 100644 index 0000000000..050004dc1f --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/service/TaskManagerCleanupTest.java @@ -0,0 +1,263 @@ +package stirling.software.common.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; + +import stirling.software.common.cluster.ClusterBackplane; +import stirling.software.common.cluster.JobStore; +import stirling.software.common.model.job.JobResult; +import stirling.software.common.model.job.ResultFile; + +/** + * Covers the on-demand cleanup path and the input-copy tracking that makes it complete. An async + * submit persists a copy of the upload as well as its results; before both were tracked, only the + * results were ever deleted and the input copy stayed on disk indefinitely. + */ +class TaskManagerCleanupTest { + + @Mock private FileStorage fileStorage; + @Mock private JobStore jobStore; + @Mock private ClusterBackplane clusterBackplane; + + @InjectMocks private TaskManager taskManager; + + private AutoCloseable closeable; + + @BeforeEach + void setUp() { + closeable = MockitoAnnotations.openMocks(this); + lenient().when(clusterBackplane.localNodeId()).thenReturn("test-node"); + lenient().when(clusterBackplane.shouldRunLocalCleanup()).thenReturn(true); + lenient().when(fileStorage.deleteFileAsSystem(anyString())).thenReturn(true); + ReflectionTestUtils.setField(taskManager, "jobResultExpiryMinutes", 30); + ReflectionTestUtils.setField(taskManager, "pendingJobExpiryMinutes", 1440); + } + + @AfterEach + void tearDown() throws Exception { + closeable.close(); + } + + @SuppressWarnings("unchecked") + private Map jobResults() { + return (Map) ReflectionTestUtils.getField(taskManager, "jobResults"); + } + + /** Complete a job with a single result file, as an async file-producing job would. */ + private void completeWithFile(String jobId, String fileId) { + taskManager.setFileResult(jobId, fileId, "out.pdf", MediaType.APPLICATION_PDF_VALUE); + taskManager.setComplete(jobId); + } + + @Test + void forcedCleanupRemovesFinishedJobsRegardlessOfAge() { + String jobId = "fresh-job"; + taskManager.createTask(jobId); + completeWithFile(jobId, "result-file"); + + // The scheduled sweep leaves it alone: it completed well inside the retention window. + taskManager.cleanupOldJobs(); + assertTrue(jobResults().containsKey(jobId), "Scheduled cleanup should respect the expiry"); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(1, summary.jobsRemoved()); + assertEquals(1, summary.filesDeleted()); + assertEquals(0, summary.jobsRetained()); + assertFalse(jobResults().containsKey(jobId)); + verify(fileStorage).deleteFileAsSystem("result-file"); + verify(jobStore).delete(jobId); + } + + @Test + void forcedCleanupDeletesThePersistedInputCopy() { + String jobId = "job-with-input"; + taskManager.createTask(jobId); + assertTrue(taskManager.registerInputFile(jobId, "input-file")); + completeWithFile(jobId, "result-file"); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(1, summary.jobsRemoved()); + assertEquals(2, summary.filesDeleted(), "Both the result and the input copy must go"); + verify(fileStorage).deleteFileAsSystem("result-file"); + verify(fileStorage).deleteFileAsSystem("input-file"); + } + + @Test + void scheduledCleanupAlsoDeletesThePersistedInputCopy() { + String jobId = "expired-job"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "input-file"); + completeWithFile(jobId, "result-file"); + + JobResult result = taskManager.getJobResult(jobId); + ReflectionTestUtils.setField(result, "completedAt", LocalDateTime.now().minusHours(1)); + + taskManager.cleanupOldJobs(); + + assertFalse(jobResults().containsKey(jobId)); + verify(fileStorage).deleteFileAsSystem("result-file"); + verify(fileStorage).deleteFileAsSystem("input-file"); + } + + @Test + void forcedCleanupLeavesRunningJobsAlone() { + String running = "running-job"; + taskManager.createTask(running); + taskManager.registerInputFile(running, "in-flight-input"); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(0, summary.jobsRemoved()); + assertEquals(0, summary.filesDeleted()); + assertEquals(1, summary.jobsRetained()); + assertTrue(jobResults().containsKey(running)); + // Deleting a running job's input mid-flight would break it. + verify(fileStorage, never()).deleteFileAsSystem(anyString()); + } + + @Test + void forcedCleanupSkipsJobsTheFilterRejects() { + taskManager.createTask("alice:job"); + completeWithFile("alice:job", "alice-file"); + taskManager.createTask("bob:job"); + completeWithFile("bob:job", "bob-file"); + + TaskManager.CleanupSummary summary = + taskManager.cleanupFinishedJobsNow(id -> id.startsWith("alice:")); + + assertEquals(1, summary.jobsRemoved()); + assertEquals(1, summary.jobsRetained()); + assertFalse(jobResults().containsKey("alice:job")); + assertTrue(jobResults().containsKey("bob:job"), "Another user's job must survive"); + verify(fileStorage).deleteFileAsSystem("alice-file"); + verify(fileStorage, never()).deleteFileAsSystem("bob-file"); + } + + @Test + void forcedCleanupIsIdempotent() { + String jobId = "job-to-clean"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "input-file"); + completeWithFile(jobId, "result-file"); + + taskManager.cleanupFinishedJobsNow(id -> true); + TaskManager.CleanupSummary second = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(0, second.jobsRemoved()); + assertEquals(0, second.filesDeleted()); + } + + @Test + void forcedCleanupRunsEvenWhenTheBackplaneOwnsScheduledExpiry() { + // The scheduled sweep defers to the backplane TTL in cluster mode, but an explicit + // request to release this node's storage still has to do something. + when(clusterBackplane.shouldRunLocalCleanup()).thenReturn(false); + String jobId = "clustered-job"; + taskManager.createTask(jobId); + completeWithFile(jobId, "result-file"); + + taskManager.cleanupOldJobs(); + assertTrue(jobResults().containsKey(jobId)); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(1, summary.jobsRemoved()); + assertFalse(jobResults().containsKey(jobId)); + } + + @Test + void cleanupCountsOnlyFilesThatWereActuallyDeleted() { + // A file already gone (a retry deleted it, say) must not be counted as freed. + String jobId = "partially-cleaned"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "already-gone"); + completeWithFile(jobId, "result-file"); + when(fileStorage.deleteFileAsSystem("already-gone")).thenReturn(false); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(1, summary.filesDeleted()); + } + + @Test + void cleanupSurvivesAFileStorageFailure() { + String jobId = "job-with-unhappy-storage"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "input-file"); + completeWithFile(jobId, "result-file"); + when(fileStorage.deleteFileAsSystem("result-file")) + .thenThrow(new RuntimeException("disk on fire")); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + // The job is still released and the remaining file still deleted. + assertEquals(1, summary.jobsRemoved()); + assertEquals(1, summary.filesDeleted()); + assertFalse(jobResults().containsKey(jobId)); + verify(fileStorage).deleteFileAsSystem("input-file"); + } + + @Test + void registerInputFileRejectsAnUnknownJob() { + assertFalse(taskManager.registerInputFile("no-such-job", "input-file")); + } + + @Test + void registerInputFileIgnoresDuplicatesAndBlanks() { + String jobId = "dedupe-job"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "input-file"); + taskManager.registerInputFile(jobId, "input-file"); + taskManager.registerInputFile(jobId, " "); + taskManager.registerInputFile(jobId, null); + + List inputFileIds = taskManager.getJobResult(jobId).getInputFileIds(); + + assertEquals(List.of("input-file"), inputFileIds); + } + + @Test + void multiFileResultsAndTheInputCopyAreAllDeleted() { + String jobId = "split-job"; + taskManager.createTask(jobId); + taskManager.registerInputFile(jobId, "input-file"); + JobResult result = taskManager.getJobResult(jobId); + result.completeWithFiles( + List.of( + ResultFile.builder() + .fileId("page-1") + .fileName("1.pdf") + .contentType(MediaType.APPLICATION_PDF_VALUE) + .fileSize(10L) + .build(), + ResultFile.builder() + .fileId("page-2") + .fileName("2.pdf") + .contentType(MediaType.APPLICATION_PDF_VALUE) + .fileSize(10L) + .build())); + + TaskManager.CleanupSummary summary = taskManager.cleanupFinishedJobsNow(id -> true); + + assertEquals(3, summary.filesDeleted()); + verify(fileStorage).deleteFileAsSystem("page-1"); + verify(fileStorage).deleteFileAsSystem("page-2"); + verify(fileStorage).deleteFileAsSystem("input-file"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/service/TaskManagerMoreTest.java b/app/common/src/test/java/stirling/software/common/service/TaskManagerMoreTest.java index 0a02039ab3..1871e31a3f 100644 --- a/app/common/src/test/java/stirling/software/common/service/TaskManagerMoreTest.java +++ b/app/common/src/test/java/stirling/software/common/service/TaskManagerMoreTest.java @@ -290,7 +290,8 @@ class TaskManagerMoreTest { ReflectionTestUtils.setField(job, "complete", true); ReflectionTestUtils.setField(job, "completedAt", LocalDateTime.now().minusHours(2)); - when(fileStorage.deleteFile("doomed")).thenThrow(new RuntimeException("locked")); + when(fileStorage.deleteFileAsSystem("doomed")) + .thenThrow(new RuntimeException("locked")); // Must not propagate; the job is still removed afterwards. taskManager.cleanupOldJobs(); diff --git a/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java b/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java index 9d880d3451..cb423d977c 100644 --- a/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java +++ b/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java @@ -258,7 +258,7 @@ class TaskManagerTest { .build(); ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile)); - when(fileStorage.deleteFile("file-id")).thenReturn(true); + when(fileStorage.deleteFileAsSystem("file-id")).thenReturn(true); // Obtain access to the private jobResults map Map jobResultsMap = @@ -281,7 +281,7 @@ class TaskManagerTest { assertFalse(jobResultsMap.containsKey(oldJobId)); assertTrue(jobResultsMap.containsKey(recentJobId)); assertTrue(jobResultsMap.containsKey(activeJobId)); - verify(fileStorage).deleteFile("file-id"); + verify(fileStorage).deleteFileAsSystem("file-id"); } @Test @@ -308,7 +308,7 @@ class TaskManagerTest { // Assert: nothing was removed locally, and no jobStore.delete was issued. assertTrue(jobResultsMap.containsKey(oldJobId)); verify(jobStore, never()).delete(anyString()); - verify(fileStorage, never()).deleteFile(anyString()); + verify(fileStorage, never()).deleteFileAsSystem(anyString()); } @Test diff --git a/app/common/src/test/java/stirling/software/common/service/ToolChainValidatorConformanceTest.java b/app/common/src/test/java/stirling/software/common/service/ToolChainValidatorConformanceTest.java new file mode 100644 index 0000000000..78c15d751d --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/service/ToolChainValidatorConformanceTest.java @@ -0,0 +1,164 @@ +package stirling.software.common.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolDiagnostic; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIOSource; +import stirling.software.common.model.tool.ToolIOSpec; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +/** The shared cases in {@code testing/tool-io-cases.json}, which all three implementations run. */ +class ToolChainValidatorConformanceTest { + + private static final JsonMapper MAPPER = JsonMapper.builder().build(); + + @TestFactory + Stream sharedCases() throws IOException { + JsonNode root = MAPPER.readTree(Files.readString(casesFile())); + Map specs = readSpecs(root.get("specs")); + + List tests = new ArrayList<>(); + for (JsonNode testCase : root.get("cases")) { + tests.add( + DynamicTest.dynamicTest( + testCase.get("name").asString(), () -> runCase(testCase, specs))); + } + return tests.stream(); + } + + private static void runCase(JsonNode testCase, Map specs) { + Map registry = new HashMap<>(); + List steps = new ArrayList<>(); + + int index = 0; + for (JsonNode stepNode : testCase.get("steps")) { + // Each step gets its own path so the same spec can appear twice in a chain. + String operation = "/op/" + index++; + JsonNode specName = stepNode.get("spec"); + if (specName != null && !specName.isNull()) { + registry.put(operation, specs.get(specName.asString())); + } + steps.add(new ToolChainValidator.Step(operation, readParameters(stepNode))); + } + + JsonNode sourceNode = testCase.get("sourceFormat"); + ToolFormat sourceFormat = + sourceNode == null || sourceNode.isNull() + ? null + : ToolFormat.valueOf(sourceNode.asString()); + + List actual = + new ToolChainValidator(ToolIOSource.of(registry)).validate(steps, sourceFormat); + + assertEquals(summarise(testCase.get("expected")), summarise(actual), describe(actual)); + } + + private static Map readParameters(JsonNode stepNode) { + JsonNode parameters = stepNode.get("parameters"); + if (parameters == null || parameters.isNull()) { + return null; + } + Map values = new HashMap<>(); + parameters.propertyStream().forEach(e -> values.put(e.getKey(), e.getValue().asString())); + return values; + } + + private static Map readSpecs(JsonNode node) { + Map specs = new HashMap<>(); + node.propertyStream() + .forEach(entry -> specs.put(entry.getKey(), readSpec(entry.getValue()))); + return specs; + } + + private static ToolIOSpec readSpec(JsonNode node) { + Set accepts = new LinkedHashSet<>(); + for (JsonNode format : node.get("accepts")) { + accepts.add(ToolFormat.valueOf(format.asString())); + } + List cases = new ArrayList<>(); + for (JsonNode rule : node.get("cases")) { + List when = new ArrayList<>(); + for (JsonNode condition : rule.get("when")) { + List matches = new ArrayList<>(); + for (JsonNode match : condition.get("matches")) { + matches.add(match.asString()); + } + JsonNode paramDefault = condition.get("default"); + when.add( + new ToolIOSpec.When( + condition.get("param").asString(), + matches, + paramDefault == null ? null : paramDefault.asString())); + } + cases.add( + new ToolIOSpec.Case( + when, + ToolFormat.valueOf(rule.get("produces").asString()), + ToolArity.valueOf(rule.get("arity").asString()))); + } + return new ToolIOSpec( + accepts, + ToolFormat.valueOf(node.get("produces").asString()), + ToolArity.valueOf(node.get("arity").asString()), + cases); + } + + /** Messages are free text, so compare only the contractual parts. */ + private static List summarise(List diagnostics) { + return diagnostics.stream() + .map(d -> d.stepIndex() + ":" + d.severity() + ":" + d.code()) + .toList(); + } + + private static List summarise(JsonNode expected) { + List summary = new ArrayList<>(); + for (JsonNode node : expected) { + summary.add( + node.get("stepIndex").asInt() + + ":" + + node.get("severity").asString() + + ":" + + node.get("code").asString()); + } + return summary; + } + + private static String describe(List actual) { + return actual.stream() + .map(ToolDiagnostic::message) + .reduce((a, b) -> a + " | " + b) + .orElse("no diagnostics"); + } + + /** Shared with the frontend and engine, so it lives at the repo root. */ + private static Path casesFile() { + Path current = Path.of("").toAbsolutePath(); + while (current != null) { + Path candidate = current.resolve("testing/tool-io-cases.json"); + if (Files.exists(candidate)) { + return candidate; + } + current = current.getParent(); + } + throw new IllegalStateException( + "testing/tool-io-cases.json not found above the working directory"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/service/ToolIODiscoveryTest.java b/app/common/src/test/java/stirling/software/common/service/ToolIODiscoveryTest.java new file mode 100644 index 0000000000..4059382a55 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/service/ToolIODiscoveryTest.java @@ -0,0 +1,119 @@ +package stirling.software.common.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOSpec; +import stirling.software.common.model.tool.ToolIOWhen; + +/** + * The registry reads real {@code @ToolIO} annotations off real handler mappings. Everything else + * checks the logic against a hand-built map, which would still pass if discovery silently found + * nothing. + */ +class ToolIODiscoveryTest { + + @RestController + @RequestMapping("/api/v1/fixture") + static class FixtureController { + + @PostMapping("/rotate") + @ToolIO(produces = ToolFormat.PDF) + public String rotate() { + return ""; + } + + @PostMapping("/split") + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) + public String split() { + return ""; + } + + @PostMapping("/add-password") + @ToolIO( + produces = ToolFormat.PDF_ENCRYPTED, + cases = + @ToolIOCase( + when = { + @ToolIOWhen(param = "password", matches = ""), + @ToolIOWhen(param = "ownerPassword", matches = "") + }, + produces = ToolFormat.PDF, + arity = ToolArity.SISO)) + public String addPassword() { + return ""; + } + + @PostMapping("/undeclared") + public String undeclared() { + return ""; + } + } + + private static ToolIORegistry registry; + + @BeforeAll + static void discover() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerSingleton("fixtureController", FixtureController.class); + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setApplicationContext(context); + mapping.afterPropertiesSet(); + context.getBeanFactory().registerSingleton("requestMappingHandlerMapping", mapping); + + registry = new ToolIORegistry(context); + registry.discoverToolIO(); + } + + @Test + void readsDeclarationsOffHandlerMethods() { + ToolIOSpec rotate = registry.find("/api/v1/fixture/rotate").orElseThrow(); + assertEquals(ToolFormat.PDF, rotate.produces()); + assertEquals(ToolArity.SISO, rotate.arity()); + assertTrue(rotate.acceptsFormat(ToolFormat.PDF)); + assertFalse(rotate.acceptsFormat(ToolFormat.PDF_ENCRYPTED)); + } + + @Test + void skipsMethodsWithNoDeclaration() { + assertTrue(registry.find("/api/v1/fixture/undeclared").isEmpty()); + } + + @Test + void carriesArityThroughToTheUnpackDecision() { + assertTrue(registry.shouldUnpackZipResponse("/api/v1/fixture/split")); + assertFalse(registry.shouldUnpackZipResponse("/api/v1/fixture/rotate")); + } + + @Test + void carriesCasesThroughToOutputResolution() { + ToolIOSpec spec = registry.find("/api/v1/fixture/add-password").orElseThrow(); + assertEquals( + ToolFormat.PDF_ENCRYPTED, + spec.resolveOutput(Map.of("password", "x", "ownerPassword", "")).format()); + assertEquals( + ToolFormat.PDF, + spec.resolveOutput(Map.of("password", "", "ownerPassword", "")).format()); + } + + @Test + void exposesInputExtensionsForRunTimeFileChecks() { + assertEquals(List.of("pdf"), registry.getExtensionTypes(false, "/api/v1/fixture/rotate")); + } +} diff --git a/app/common/src/test/java/stirling/software/common/service/ToolIORegistryTest.java b/app/common/src/test/java/stirling/software/common/service/ToolIORegistryTest.java new file mode 100644 index 0000000000..2ff60bfce7 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/service/ToolIORegistryTest.java @@ -0,0 +1,87 @@ +package stirling.software.common.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIOSpec; + +/** The {@link ToolMetadataService} behaviour the pipeline executors depend on. */ +class ToolIORegistryTest { + + private static final String SPLIT = "/api/v1/general/split-pages"; + private static final String MERGE = "/api/v1/general/merge-pdfs"; + private static final String ROTATE = "/api/v1/general/rotate-pdf"; + private static final String ATTACHMENTS = "/api/v1/security/get-attachments"; + private static final String EXTRACT_IMAGES = "/api/v1/misc/extract-images"; + private static final String CONVERT_ANY = "/api/v1/convert/file/pdf"; + private static final String UNKNOWN = "/api/v1/general/does-not-exist"; + + private static ToolIOSpec spec(ToolFormat accepts, ToolFormat produces, ToolArity arity) { + return new ToolIOSpec(Set.of(accepts), produces, arity, List.of()); + } + + private final ToolIORegistry registry = + ToolIORegistry.forSpecs( + Map.of( + SPLIT, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SIMO), + MERGE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.MISO), + ROTATE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SISO), + ATTACHMENTS, spec(ToolFormat.PDF, ToolFormat.ZIP, ToolArity.SISO), + EXTRACT_IMAGES, spec(ToolFormat.PDF, ToolFormat.IMAGE, ToolArity.SIMO), + CONVERT_ANY, spec(ToolFormat.ANY, ToolFormat.PDF, ToolArity.SISO))); + + @Test + void multiInputFollowsArity() { + assertTrue(registry.isMultiInput(MERGE)); + assertFalse(registry.isMultiInput(SPLIT)); + assertFalse(registry.isMultiInput(ROTATE)); + assertFalse(registry.isMultiInput(UNKNOWN)); + } + + @Test + void inputExtensionsComeFromAcceptedFormats() { + assertEquals(List.of("pdf"), registry.getExtensionTypes(false, ROTATE)); + } + + @Test + void outputExtensionsComeFromTheProducedFormat() { + assertEquals(List.of("pdf"), registry.getExtensionTypes(true, ROTATE)); + assertTrue(registry.getExtensionTypes(true, EXTRACT_IMAGES).contains("png")); + } + + @Test + void noRestrictionIsReportedAsNull() { + // Callers treat null as "any type accepted". + assertNull(registry.getExtensionTypes(false, CONVERT_ANY)); + assertNull(registry.getExtensionTypes(false, UNKNOWN)); + } + + @Test + void multiOutputResponsesAreUnpacked() { + assertTrue(registry.shouldUnpackZipResponse(SPLIT)); + assertTrue(registry.shouldUnpackZipResponse(EXTRACT_IMAGES)); + } + + @Test + void anArchiveDeliverableStaysPacked() { + // The archive is the deliverable; unpacking would lose it. + assertFalse(registry.shouldUnpackZipResponse(ATTACHMENTS)); + } + + @Test + void singleOutputResponsesAreNotUnpacked() { + assertFalse(registry.shouldUnpackZipResponse(ROTATE)); + assertFalse(registry.shouldUnpackZipResponse(MERGE)); + assertFalse(registry.shouldUnpackZipResponse(UNKNOWN)); + } +} diff --git a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java index 4ec7164514..b3491f7183 100644 --- a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java +++ b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java @@ -721,7 +721,7 @@ class PDFToFileTest { .thenAnswer( invocation -> { List args = invocation.getArgument(0); - String outputPath = args.get(args.size() - 1); + String outputPath = args.getLast(); Files.write(Path.of(outputPath), "Fake DOCX content".getBytes()); return mockExecutorResult; }); diff --git a/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsMoreTest.java b/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsMoreTest.java index 217f18abf5..6ae84a8e6d 100644 --- a/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsMoreTest.java +++ b/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsMoreTest.java @@ -325,11 +325,8 @@ class RegexPatternUtilsMoreTest { } @Test - void pageModeAndApiDocPatterns() { + void pageModePattern() { assertTrue(utils.getPageModePattern().matcher("a/b").find()); - assertTrue(utils.getApiDocOutputTypePattern().matcher("Output: PDF").find()); - assertTrue(utils.getApiDocInputTypePattern().matcher("Input: PDF").find()); - assertTrue(utils.getApiDocTypePattern().matcher("Type: WEB").find()); } @Test diff --git a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java index 1912f3808b..72e5eae9a2 100644 --- a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java @@ -73,6 +73,13 @@ class RequestUriUtilsTest { assertTrue(RequestUriUtils.isStaticResource("/mobile-scanner")); } + @Test + void testIsStaticResource_mobileSignPath() { + // The phone-side signature drawing page, reached from the Sign tool QR code. + assertTrue(RequestUriUtils.isStaticResource("/mobile-sign")); + assertTrue(RequestUriUtils.isStaticResource("/app", "/app/mobile-sign")); + } + @Test void testIsStaticResource_portalShell() { // The admin portal SPA shell (/processor) is served pre-auth so it's directly navigable. @@ -199,12 +206,24 @@ class RequestUriUtilsTest { @Test void testIsPublicAuthEndpoint_shareLinkTokenTrailingSlash() { - assertTrue(RequestUriUtils.isPublicAuthEndpoint("/share/abc123/", "")); + assertTrue( + RequestUriUtils.isPublicAuthEndpoint( + "/share/00dcac3a-fc7a-4989-9c4f-97745484d62f/", "")); } @Test void testIsPublicAuthEndpoint_shareLinkWithContextPath() { - assertTrue(RequestUriUtils.isPublicAuthEndpoint("/app/share/abc123", "/app")); + assertTrue( + RequestUriUtils.isPublicAuthEndpoint( + "/app/share/00dcac3a-fc7a-4989-9c4f-97745484d62f", "/app")); + } + + @Test + void testIsPublicAuthEndpoint_shareLinkWithInvalidTokenLength() { + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/share/abc123", "")); + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/share/00dcac3a-fc7a-4989-9c4f-97745484d62fa", "")); } @Test @@ -229,4 +248,86 @@ class RequestUriUtilsTest { RequestUriUtils.isPublicAuthEndpoint( "/api/v1/storage/share-links/abc123/metadata", "")); } + + // --- invite-accept SPA bootstrap --- + + private static final String INVITE_TOKEN = "06a20e7e-2e35-4e26-be7d-2dce14f28f12"; + + @Test + void testIsPublicAuthEndpoint_inviteLinkToken() { + assertTrue(RequestUriUtils.isPublicAuthEndpoint("/invite/" + INVITE_TOKEN, "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteLinkTokenTrailingSlash() { + assertTrue(RequestUriUtils.isPublicAuthEndpoint("/invite/" + INVITE_TOKEN + "/", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteLinkWithContextPath() { + assertTrue(RequestUriUtils.isPublicAuthEndpoint("/app/invite/" + INVITE_TOKEN, "/app")); + } + + @Test + void testIsPublicAuthEndpoint_inviteRootNotPublic() { + // Avoid matching bare "/invite" or "/invite/" - must have a token segment + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite", "")); + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteNestedPathNotPublic() { + // Guard against future additions like /invite//foo becoming accidentally public + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/" + INVITE_TOKEN + "/foo", "")); + } + + @Test + void testIsPublicAuthEndpoint_invitePrefixDoesNotOvermatch() { + // "/inviteX" must not match the invite pattern + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/inviteX", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteNonUuidTokenNotPublic() { + // Only exactly-shaped 36-char lowercase UUID tokens are treated as invite links + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/abc123", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteUppercaseUuidNotPublic() { + // Tokens are generated lowercase by UUID.randomUUID().toString() + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/invite/06A20E7E-2E35-4E26-BE7D-2DCE14F28F12", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteWrongLengthNotPublic() { + // 35-char and 37-char UUID-like tokens are not valid UUIDs + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/invite/06a20e7e-2e35-4e26-be7d-2dce14f28f1", "")); + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/invite/06a20e7e-2e35-4e26-be7d-2dce14f28f122", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteWrongGroupingNotPublic() { + // Groups of 8-4-4-4-4 must not be shifted around (e.g. 4-4-4-4-8) + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/invite/06a2-0e7e-2e35-4e26-be7d2dce14f28f12", "")); + } + + @Test + void testIsPublicAuthEndpoint_inviteTokenInvalidCharsNotPublic() { + // Hex-only; anything outside [0-9a-f] or the UUID hyphens is rejected + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/abc$123", "")); + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/abc..123", "")); + assertFalse(RequestUriUtils.isPublicAuthEndpoint("/invite/abc%2F123", "")); + assertFalse( + RequestUriUtils.isPublicAuthEndpoint( + "/invite/06a20e7e-2e35-4e26-be7d-2dce14f28f1g", "")); + } } diff --git a/app/core/build.gradle b/app/core/build.gradle index 578ace75cc..76828445a3 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -50,18 +50,18 @@ dependencies { implementation "org.apache.pdfbox:preflight:$pdfboxVersion" implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion" - implementation 'org.verapdf:validation-model:1.28.2' + implementation 'org.verapdf:validation-model:1.30.2' // CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13 implementation "org.mozilla:rhino:${rhinoVersion}" // veraPDF still uses javax.xml.bind, not the new jakarta namespace implementation 'javax.xml.bind:jaxb-api:2.3.1' implementation 'com.sun.xml.bind:jaxb-impl:2.3.9' - implementation 'com.sun.xml.bind:jaxb-core:4.0.7' + implementation 'com.sun.xml.bind:jaxb-core:4.0.9' // CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7) implementation "com.google.code.gson:gson:${gsonVersion}" - implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4' + implementation 'org.apache.pdfbox:jbig2-imageio:3.0.5' implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv implementation 'org.apache.poi:poi-ooxml:5.5.1' @@ -312,8 +312,9 @@ tasks.register('cleanFrontendAssets', Delete) { delete generatedFrontendPaths.collect { new File(resourcesStaticDir, it) } // Prerendered per-route SPA pages (e.g. compress.html) carry per-tool OG tags and are // copied from the frontend build. Remove stale ones so renamed/removed tools don't linger. - // api-landing.html and mobile-upload.html are real backend source files, not generated artifacts. - delete fileTree(dir: resourcesStaticDir, includes: ['*.html'], excludes: ['api-landing.html', 'mobile-upload.html']) + // api-landing.html, mobile-upload.html and mobile-sign.html are real backend source files, + // not generated artifacts. + delete fileTree(dir: resourcesStaticDir, includes: ['*.html'], excludes: ['api-landing.html', 'mobile-upload.html', 'mobile-sign.html']) // Nested prerendered route pages (e.g. settings/people.html) delete new File(resourcesStaticDir, 'settings') } @@ -330,11 +331,27 @@ tasks.register('copyApiLandingPage', Copy) { } } +tasks.register('copyBackendOnlySamples', Copy) { + group = 'frontend' + description = 'Copy frontend sample files for backend-only mode' + from(new File(frontendEditorDir, 'public/samples')) + into(new File(resourcesStaticDir, 'samples')) + dependsOn cleanFrontendAssets + onlyIf { !buildWithFrontend } +} + // Ensure copyFrontendAssets runs after spotless tasks tasks.named('copyFrontendAssets').configure { mustRunAfter tasks.matching { it.name.startsWith('spotless') } } +// Cleanup removes frontend-generated resources that are also visible to the resource/formatting +// task graph. Keep all Spotless tasks ahead of cleanup so they never snapshot a path that has +// just been removed. +tasks.named('cleanFrontendAssets').configure { + mustRunAfter tasks.matching { it.name.startsWith('spotless') } +} + if (buildWithFrontend) { println "Editor frontend build enabled - JAR will include React frontend (mode=${frontendMode})" processResources.dependsOn copyFrontendAssets @@ -342,6 +359,7 @@ if (buildWithFrontend) { println "Frontend build disabled - JAR will be backend-only with API landing page" // When not building the UI, ensure any stale frontend assets are removed and use API landing page processResources.dependsOn copyApiLandingPage + processResources.dependsOn copyBackendOnlySamples } bootJar.dependsOn ':common:jar' diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 8755dfe2ef..46b1976ca7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -321,7 +321,7 @@ public class ExternalAppDepConfig { new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { - if (sb.length() > 0) sb.append('\n'); + if (!sb.isEmpty()) sb.append('\n'); sb.append(line); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java index 9625f9e671..ff6d1b87bd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/MultipartConfiguration.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,7 +21,11 @@ import stirling.software.SPDF.controller.web.UploadLimitService; @Slf4j public class MultipartConfiguration { - @Autowired private UploadLimitService uploadLimitService; + private final UploadLimitService uploadLimitService; + + public MultipartConfiguration(UploadLimitService uploadLimitService) { + this.uploadLimitService = uploadLimitService; + } /** * Creates MultipartConfigElement that respects fileUploadLimit from settings.yml or environment diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java index 7dc79a3ebc..275bc3f967 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java @@ -40,7 +40,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get PDF page count", - description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO") + description = "Returns total number of pages in PDF.") public ResponseEntity getPageCount(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { return ResponseEntity.ok(Map.of("pageCount", document.getNumberOfPages())); @@ -54,7 +54,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get basic PDF information", - description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO") + description = "Returns page count, version, file size.") public ResponseEntity getBasicInfo(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { Map info = new HashMap<>(); @@ -72,7 +72,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get PDF document properties", - description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO") + description = "Returns title, author, subject, etc.") public ResponseEntity getDocumentProperties(@ModelAttribute PDFFile file) throws IOException { // Load the document in read-only mode to prevent modifications and ensure the integrity of @@ -105,7 +105,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get page dimensions for all pages", - description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO") + description = "Returns width and height of each page.") public ResponseEntity getPageDimensions(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { List> dimensions = new ArrayList<>(); @@ -128,8 +128,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get form field information", - description = - "Returns count and details of form fields. Input:PDF Output:JSON Type:SISO") + description = "Returns count and details of form fields.") public ResponseEntity getFormFields(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { Map formInfo = new HashMap<>(); @@ -155,7 +154,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get annotation information", - description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO") + description = "Returns count and types of annotations.") public ResponseEntity getAnnotationInfo(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { Map annotInfo = new HashMap<>(); @@ -183,8 +182,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get font information", - description = - "Returns list of fonts used in the document. Input:PDF Output:JSON Type:SISO") + description = "Returns list of fonts used in the document.") public ResponseEntity getFontInfo(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { Map fontInfo = new HashMap<>(); @@ -212,8 +210,7 @@ public class AnalysisController { @JsonDataResponse @Operation( summary = "Get security information", - description = - "Returns encryption and permission details. Input:PDF Output:JSON Type:SISO") + description = "Returns encryption and permission details.") public ResponseEntity getSecurityInfo(@ModelAttribute PDFFile file) throws IOException { try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { Map securityInfo = new HashMap<>(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java index d1145fa815..5dbccb649d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java @@ -29,6 +29,8 @@ import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.BookletImpositionRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFileManager; @@ -47,12 +49,13 @@ public class BookletImpositionController { value = "/booklet-imposition", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Create a booklet with proper page imposition", description = - "This operation combines page reordering for booklet printing with multi-page layout. " - + "It rearranges pages in the correct order for booklet printing and places multiple pages " - + "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO") + "This operation combines page reordering for booklet printing with multi-page" + + " layout. It rearranges pages in the correct order for booklet printing and" + + " places multiple pages on each sheet for proper folding and binding.") public ResponseEntity createBookletImposition( @ModelAttribute BookletImpositionRequest request) throws IOException { @@ -294,24 +297,25 @@ public class BookletImpositionController { // Apply rotation if needed (rotate about origin), then translate to keep in cell switch (rot) { - case 90: + case 90 -> { cs.transform(Matrix.getRotateInstance(Math.PI / 2, 0, 0)); // After 90° CCW, the content spans x in [-r.getHeight(), 0] and y in [0, // r.getWidth()] cs.transform(Matrix.getTranslateInstance(0, -r.getWidth())); - break; - case 180: + } + case 180 -> { cs.transform(Matrix.getRotateInstance(Math.PI, 0, 0)); cs.transform(Matrix.getTranslateInstance(-r.getWidth(), -r.getHeight())); - break; - case 270: + } + case 270 -> { cs.transform(Matrix.getRotateInstance(3 * Math.PI / 2, 0, 0)); // After 270° CCW, the content spans x in [0, r.getHeight()] and y in // [-r.getWidth(), 0] cs.transform(Matrix.getTranslateInstance(-r.getHeight(), 0)); - break; - default: + } + default -> { // 0°: no-op + } } // Reuse LayerUtility passed from caller diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java index f5a0cdcba9..571bb42914 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -27,6 +27,8 @@ import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -131,11 +133,12 @@ public class CropController { value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given" - + " coordinates. Input:PDF Output:PDF Type:SISO") + + " coordinates.") public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) throws IOException { if (request.isAutoCrop()) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java index 53abc46ac6..0cd187f2ec 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java @@ -26,6 +26,8 @@ import stirling.software.SPDF.model.api.EditTableOfContentsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFileManager; @@ -47,6 +49,7 @@ public class EditTableOfContentsController { value = "/extract-bookmarks", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.JSON) @Operation( summary = "Extract PDF Bookmarks", description = "Extracts bookmarks/table of contents from a PDF document as JSON.") @@ -151,6 +154,7 @@ public class EditTableOfContentsController { value = "/edit-table-of-contents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Edit Table of Contents", description = "Add or edit bookmarks/table of contents in a PDF document.") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTextController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTextController.java index 3a10b1419b..17d1d7d8a7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTextController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTextController.java @@ -34,6 +34,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.general.EditTextOperation; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -83,19 +85,18 @@ public class EditTextController { value = "/edit-text", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Edit text in a PDF via find and replace", description = "Applies an ordered list of find/replace operations to the text in a PDF and" - + " returns the edited PDF. Useful for find-and-replace, bulk renames" - + " (e.g. updating a company name throughout a document), and copy" - + " editing where the AI agent has identified specific replacements." - + " Matching is performed against the joined text of each page, so" - + " find strings can span multiple visual runs (titles split per word," - + " kerning-broken phrases). Cross-element matches are written as a" - + " single replacement run anchored at the leftmost matched position;" - + " centered or tracked text may shift left when its content changes." - + " Input:PDF Output:PDF Type:SISO") + + " returns the edited PDF. Useful for find-and-replace, bulk renames (e.g." + + " updating a company name throughout a document), and copy editing where the AI" + + " agent has identified specific replacements. Matching is performed against the" + + " joined text of each page, so find strings can span multiple visual runs" + + " (titles split per word, kerning-broken phrases). Cross-element matches are" + + " written as a single replacement run anchored at the leftmost matched position;" + + " centered or tracked text may shift left when its content changes.") public ResponseEntity editText(@ModelAttribute EditTextRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 9408aa821f..77a97b9b05 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -41,6 +40,9 @@ import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -58,8 +60,6 @@ import stirling.software.jpdfium.doc.PdfBookmarkEditor.BookmarkTree; @Slf4j @RequiredArgsConstructor public class MergeController { - - private static final Pattern QUOTE_WRAP_PATTERN = Pattern.compile("^\"|\"$"); private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; @@ -161,30 +161,6 @@ public class MergeController { }; } - private String[] parseClientFileIds(String clientFileIds) { - if (clientFileIds == null || clientFileIds.trim().isEmpty()) { - return new String[0]; - } - try { - String trimmed = clientFileIds.trim(); - if (trimmed.startsWith("[") && trimmed.endsWith("]")) { - String inside = trimmed.substring(1, trimmed.length() - 1).trim(); - if (inside.isEmpty()) { - return new String[0]; - } - String[] parts = inside.split(","); - String[] result = new String[parts.length]; - for (int i = 0; i < parts.length; i++) { - result[i] = QUOTE_WRAP_PATTERN.matcher(parts[i].trim()).replaceAll(""); - } - return result; - } - } catch (Exception e) { - log.warn("Failed to parse client file IDs: {}", clientFileIds, e); - } - return new String[0]; - } - private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) { PDDocumentOutline outline = new PDDocumentOutline(); mergedDocument.getDocumentCatalog().setDocumentOutline(outline); @@ -265,12 +241,13 @@ public class MergeController { value = "/merge-pdfs", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO) @Operation( summary = "Merge multiple PDF files into one", description = "This endpoint merges multiple PDF files into a single PDF file. The merged" + " file will contain all pages from the input files in the order they were" - + " provided. Input:PDF Output:PDF Type:MISO") + + " provided.") public ResponseEntity mergePdfs( @ModelAttribute MergePdfsRequest request, @RequestParam(value = "fileOrder", required = false) String fileOrder) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index a18d0a81c0..e9d49ef262 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -25,6 +25,8 @@ import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralFormCopyUtils; @@ -44,11 +46,12 @@ public class MultiPageLayoutController { value = "/multi-page-layout", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a" - + " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") + + " single sheet in the output PDF file.") public ResponseEntity mergeMultiplePagesIntoOne( @ModelAttribute MergeMultiplePagesRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 4aab03ff55..1d369d282d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -27,6 +27,9 @@ import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -46,17 +49,19 @@ public class PdfOverlayController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO) @Operation( summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential," - + " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") + + " Interleaved, or Fixed Repeat.") public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { MultipartFile baseFile = request.getFileInput(); int overlayPos = request.getOverlayPosition(); MultipartFile[] overlayFiles = request.getOverlayFiles(); + validateOverlayFiles(overlayFiles); File[] overlayPdfFiles = new File[overlayFiles.length]; List tempFiles = new ArrayList<>(); // List to keep track of temporary files @@ -116,10 +121,29 @@ public class PdfOverlayController { } } + // Both fields are declared required, but @ModelAttribute binding leaves them null when the + // caller omits them, which would otherwise surface as a 500 instead of a 400. + private void validateOverlayFiles(MultipartFile[] overlayFiles) { + if (overlayFiles == null || overlayFiles.length == 0) { + throw ExceptionUtils.createIllegalArgumentException( + "error.overlayFilesRequired", "At least one overlay file is required"); + } + for (MultipartFile overlayFile : overlayFiles) { + if (overlayFile == null || overlayFile.isEmpty()) { + throw ExceptionUtils.createIllegalArgumentException( + "error.overlayFileEmpty", "Overlay files must not be empty"); + } + } + } + private Map prepareOverlayGuide( int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) throws IOException { Map overlayGuide = new HashMap<>(); + if (mode == null) { + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidFormat", "Invalid {0} format: {1}", "overlay mode", "null"); + } switch (mode) { case "SequentialOverlay": sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PosterPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PosterPdfController.java index 80f1a159da..24aa4a2c80 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PosterPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PosterPdfController.java @@ -29,6 +29,9 @@ import stirling.software.SPDF.model.api.general.PosterPdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -49,13 +52,13 @@ public class PosterPdfController { consumes = "multipart/form-data", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Split large PDF pages into smaller printable chunks", description = - "This endpoint splits large or oddly-sized PDF pages into smaller chunks " - + "suitable for printing on standard paper sizes (e.g., A4, Letter). " - + "Divides each page into a grid of smaller pages using Apache PDFBox. " - + "Input: PDF Output: ZIP-PDF Type: SISO") + "This endpoint splits large or oddly-sized PDF pages into smaller chunks" + + " suitable for printing on standard paper sizes (e.g., A4, Letter). Divides each" + + " page into a grid of smaller pages using Apache PDFBox.") public ResponseEntity posterPdf(@ModelAttribute PosterPdfRequest request) throws Exception { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 12e3a15b89..4c45951644 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -30,6 +30,8 @@ import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.FormUtils; @@ -50,12 +52,12 @@ public class RearrangePagesPDFController { value = "/remove-pages", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide" - + " a comma-separated list of page numbers or ranges to delete. Input:PDF" - + " Output:PDF Type:SISO") + + " a comma-separated list of page numbers or ranges to delete.") public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request) throws IOException { @@ -234,13 +236,13 @@ public class RearrangePagesPDFController { value = "/rearrange-pages", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page" - + " order or custom mode. Users can provide a page order as a" - + " comma-separated list of page numbers or page ranges, or a custom mode." - + " Input:PDF Output:PDF") + + " order or custom mode. Users can provide a page order as a comma-separated list" + + " of page numbers or page ranges, or a custom mode.") public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index 7dbb891197..31719bad75 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -20,6 +20,8 @@ import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -38,11 +40,12 @@ public class RotationController { value = "/rotate-pdf", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Rotate a PDF file", description = "This endpoint rotates a given PDF file by a specified angle. The angle must be" - + " a multiple of 90. Input:PDF Output:PDF Type:SISO") + + " a multiple of 90.") public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index fb7a55b793..6aa8599eea 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -26,6 +26,8 @@ import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -87,11 +89,12 @@ public class ScalePagesController { value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in" - + " the output PDF file. Input:PDF Output:PDF Type:SISO") + + " the output PDF file.") public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index d4516c0ed6..712a123581 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -30,6 +30,9 @@ import stirling.software.SPDF.model.api.SplitPagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.FormUtils; import stirling.software.common.util.GeneralUtils; @@ -52,13 +55,13 @@ public class SplitPDFController { value = "/split-pages", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Split a PDF file into separate documents", description = "This endpoint splits a given PDF file into separate documents based on the" - + " specified page numbers or ranges. Users can specify pages using" - + " individual numbers, ranges, or 'all' for every page. Input:PDF" - + " Output:PDF Type:SIMO") + + " specified page numbers or ranges. Users can specify pages using individual" + + " numbers, ranges, or 'all' for every page.") public ResponseEntity splitPdf(@ModelAttribute SplitPagesRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index 6ad85d0c92..d4b07c3add 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -34,6 +34,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.PdfMetadata; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.ExceptionUtils; @@ -93,11 +96,10 @@ public class SplitPdfByChaptersController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Split PDFs by Chapters", - description = - "Splits a PDF into chapters and returns a ZIP file. Input:PDF Output:ZIP-PDF" - + " Type:SISO") + description = "Splits a PDF into chapters and returns a ZIP file.") public ResponseEntity splitPdf(@ModelAttribute SplitPdfByChaptersRequest request) throws Exception { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index adaed9ec35..be8ca183a7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -34,6 +34,9 @@ import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -54,13 +57,13 @@ public class SplitPdfBySectionsController { value = "/split-pdf-by-sections", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice" - + " which page to split, and how to split" - + " ( halves, thirds, quarters, etc.), both vertically and horizontally." - + " Input:PDF Output:ZIP-PDF Type:SISO") + + " which page to split, and how to split ( halves, thirds, quarters, etc.), both" + + " vertically and horizontally.") public ResponseEntity splitPdf( @Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index e8ff5d9e23..8721fd3b70 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -29,6 +29,9 @@ import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.FormUtils; @@ -52,14 +55,14 @@ public class SplitPdfBySizeController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages" - + " and split into 5, it does 5 documents each 4 pages\r\n" - + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB" - + " (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF" - + " Output:ZIP-PDF Type:SISO") + + " and split into 5, it does 5 documents each 4 pages\r\n if 10MB and each page" + + " is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts" + + " 1.9MB but not 2.1MB)") public ResponseEntity autoSplitPdf( @ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index d451e98f5e..08d49c579f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -22,6 +22,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFileManager; @@ -39,13 +41,13 @@ public class ToSinglePageController { value = "/pdf-to-single-page", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Convert a multi-page PDF into a single long page PDF", description = "This endpoint converts a multi-page PDF document into a single paged PDF" - + " document. The width of the single page will be same as the input's" - + " width, but the height will be the sum of all the pages' heights." - + " Input:PDF Output:PDF Type:SISO") + + " document. The width of the single page will be same as the input's width, but" + + " the height will be the sum of all the pages' heights.") public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index de391c7c32..7f93fb3d64 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -125,17 +125,14 @@ public class UIDataController { pipelineConfigs.add(content); } - for (String config : pipelineConfigs) { + for (int i = 0; i < jsonFiles.size(); i++) { + String config = pipelineConfigs.get(i); Map jsonContent = objectMapper.readValue( config, new TypeReference>() {}); String name = (String) jsonContent.get("name"); - if (name == null || name.length() < 1) { - String filename = - jsonFiles - .get(pipelineConfigs.indexOf(config)) - .getFileName() - .toString(); + if (name == null || name.isEmpty()) { + String filename = jsonFiles.get(i).getFileName().toString(); name = filename.substring(0, filename.lastIndexOf('.')); } Map configWithName = new HashMap<>(); @@ -301,20 +298,14 @@ public class UIDataController { } private static String getFormatFromExtension(String extension) { - switch (extension) { - case "ttf": - return "truetype"; - case "woff": - return "woff"; - case "woff2": - return "woff2"; - case "eot": - return "embedded-opentype"; - case "svg": - return "svg"; - default: - return ""; - } + return switch (extension) { + case "ttf" -> "truetype"; + case "woff" -> "woff"; + case "woff2" -> "woff2"; + case "eot" -> "embedded-opentype"; + case "svg" -> "svg"; + default -> ""; + }; } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java index a53ae4943c..a835fc4307 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java @@ -29,6 +29,8 @@ import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; @@ -61,11 +63,12 @@ public class ConvertEbookToPDFController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(accepts = ToolFormat.EBOOK, produces = ToolFormat.PDF) @Operation( summary = "Convert an eBook file to PDF", description = "This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)" - + " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO") + + " to PDF using Calibre.") public ResponseEntity convertEbookToPdf( @ModelAttribute ConvertEbookToPdfRequest request) throws Exception { if (!isCalibreEnabled()) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index 7c3978ca00..446892b0fc 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -27,6 +27,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.converters.EmlToPdfRequest; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.EmlToPdf; @@ -49,13 +51,14 @@ public class ConvertEmlToPDF { value = "/eml/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(accepts = ToolFormat.EMAIL, produces = ToolFormat.PDF) @Operation( summary = "Convert EML/MSG to PDF", description = - "This endpoint converts EML (email) and MSG (Outlook) files to PDF format" - + " with extensive customization options. Features include font settings," - + " image constraints, display modes, attachment handling, and HTML debug" - + " output. Input: EML or MSG file, Output: PDF or HTML file. Type: SISO") + "This endpoint converts EML (email) and MSG (Outlook) files to PDF format with" + + " extensive customization options. Features include font settings, image" + + " constraints, display modes, attachment handling, and HTML debug output. or MSG" + + " file, or HTML file.") public ResponseEntity convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) { MultipartFile inputFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index 53a0def649..c7799497a5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -19,6 +19,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.converters.HTMLToPdfRequest; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.*; @@ -39,11 +41,14 @@ public class ConvertHtmlToPDF { value = "/html/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + // A ZIP of HTML plus its CSS is a first-class input here, and is what convert/pdf/html emits. + @ToolIO( + accepts = {ToolFormat.HTML, ToolFormat.ZIP}, + produces = ToolFormat.PDF) @Operation( summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", description = - "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." - + " Input:HTML Output:PDF Type:SISO") + "This endpoint takes an HTML or ZIP file input and converts it to a PDF format.") public ResponseEntity HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index 1255a2a228..b30e8d8c4d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -41,6 +41,11 @@ import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CbrUtils; import stirling.software.common.util.CbzUtils; @@ -78,12 +83,20 @@ public class ConvertImgPDFController { value = "/pdf/img", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO( + produces = ToolFormat.IMAGE, + arity = ToolArity.SIMO, + cases = + @ToolIOCase( + when = @ToolIOWhen(param = "singleOrMultiple", matches = "single"), + produces = ToolFormat.IMAGE, + arity = ToolArity.SISO)) @Operation( summary = "Convert PDF to image(s)", description = "This endpoint converts a PDF file to image(s) with the specified image format," + " color type, and DPI. Users can choose to get a single image or multiple" - + " images. Input:PDF Output:Image Type:SI-Conditional") + + " images.") public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) throws Exception { MultipartFile file = request.getFileInput(); @@ -187,7 +200,7 @@ public class ConvertImgPDFController { } if (webpFiles.size() == 1) { - Path webpFilePath = webpFiles.get(0); + Path webpFilePath = webpFiles.getFirst(); byte[] webpBytes = Files.readAllBytes(webpFilePath); Files.deleteIfExists(tempFile); tempFile = null; @@ -248,12 +261,13 @@ public class ConvertImgPDFController { value = "/img/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(accepts = ToolFormat.IMAGE, produces = ToolFormat.PDF, arity = ToolArity.MISO) @Operation( summary = "Convert images to a PDF file", description = "This endpoint converts one or more images to a PDF file. Users can specify" + " whether to stretch the images to fit the PDF page, and whether to" - + " automatically rotate the images. Input:Image Output:PDF Type:MISO") + + " automatically rotate the images.") public ResponseEntity convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException { MultipartFile[] file = request.getFileInput(); @@ -279,11 +293,10 @@ public class ConvertImgPDFController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbz/pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(accepts = ToolFormat.CBZ, produces = ToolFormat.PDF) @Operation( summary = "Convert CBZ comic book archive to PDF", - description = - "This endpoint converts a CBZ (ZIP) comic book archive to a PDF file. " - + "Input:CBZ Output:PDF Type:SISO") + description = "This endpoint converts a CBZ (ZIP) comic book archive to a PDF file.") public ResponseEntity convertCbzToPdf(@ModelAttribute ConvertCbzToPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -308,11 +321,10 @@ public class ConvertImgPDFController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.CBZ) @Operation( summary = "Convert PDF to CBZ comic book archive", - description = - "This endpoint converts a PDF file to a CBZ (ZIP) comic book archive. " - + "Input:PDF Output:CBZ Type:SISO") + description = "This endpoint converts a PDF file to a CBZ (ZIP) comic book archive.") public ResponseEntity convertPdfToCbz(@ModelAttribute ConvertPdfToCbzRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -334,11 +346,10 @@ public class ConvertImgPDFController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(accepts = ToolFormat.CBR, produces = ToolFormat.PDF) @Operation( summary = "Convert CBR comic book archive to PDF", - description = - "This endpoint converts a CBR (RAR) comic book archive to a PDF file. " - + "Input:CBR Output:PDF Type:SISO") + description = "This endpoint converts a CBR (RAR) comic book archive to a PDF file.") public ResponseEntity convertCbrToPdf(@ModelAttribute ConvertCbrToPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -363,11 +374,12 @@ public class ConvertImgPDFController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbr", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.CBR) @Operation( summary = "Convert PDF to CBR comic book archive", description = - "This endpoint converts a PDF file to a CBR comic book archive using the local RAR CLI. " - + "Input:PDF Output:CBR Type:SISO") + "This endpoint converts a PDF file to a CBR comic book archive using the local" + + " RAR CLI.") public ResponseEntity convertPdfToCbr(@ModelAttribute ConvertPdfToCbrRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index e47f3322ea..c9c95d6ef2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -28,6 +28,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.GeneralFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.*; @@ -47,11 +49,15 @@ public class ConvertMarkdownToPdf { value = "/markdown/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + // A ZIP of Markdown plus its images is a first-class input here, not just a bare .md file. + @ToolIO( + accepts = {ToolFormat.MARKDOWN, ToolFormat.ZIP}, + produces = ToolFormat.PDF) @Operation( summary = "Convert a Markdown file to PDF", description = - "This endpoint takes a Markdown file or ZIP (containing Markdown + images) input, converts it to HTML, and then to" - + " PDF format. Input:MARKDOWN Output:PDF Type:SISO") + "This endpoint takes a Markdown file or ZIP (containing Markdown + images)" + + " input, converts it to HTML, and then to PDF format.") public ResponseEntity markdownToPdf(@ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile fileInput = generalFile.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index 4fc6669bc7..97552000f2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -31,6 +31,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.GeneralFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.ExceptionUtils; @@ -209,11 +211,10 @@ public class ConvertOfficeController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/file/pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(accepts = ToolFormat.ANY, produces = ToolFormat.PDF) @Operation( summary = "Convert a file to a PDF using LibreOffice", - description = - "This endpoint converts a given file to a PDF using LibreOffice API Input:ANY" - + " Output:PDF Type:SISO") + description = "This endpoint converts a given file to a PDF using LibreOffice API") public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile inputFile = generalFile.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java index 7b2de8d11b..c95c2b932f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java @@ -28,6 +28,8 @@ import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.Targe import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; @@ -86,11 +88,10 @@ public class ConvertPDFToEpubController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.EBOOK) @Operation( summary = "Convert PDF to EPUB/AZW3", - description = - "Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre. Input:PDF" - + " Output:EPUB/AZW3 Type:SISO") + description = "Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre.") public ResponseEntity convertPdfToEpub( @ModelAttribute ConvertPdfToEpubRequest request) throws Exception { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java index 59e21138e6..f747ce7447 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java @@ -26,6 +26,8 @@ import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -50,11 +52,12 @@ public class ConvertPDFToExcelController { value = "/pdf/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.EXCEL) @Operation( summary = "Convert a PDF to an Excel spreadsheet (XLSX)", description = "Extracts tabular data from each page of a PDF and writes it into an Excel" - + " workbook, one sheet per table. Input:PDF Output:XLSX Type:SISO") + + " workbook, one sheet per table.") public ResponseEntity pdfToExcel(@ModelAttribute PDFWithPageNums request) throws Exception { String baseName = diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java index 20253a3a00..8f9819c0e4 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java @@ -15,6 +15,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.util.PDFToFile; import stirling.software.common.util.TempFileManager; @@ -29,10 +31,10 @@ public class ConvertPDFToHtml { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.ZIP) @Operation( summary = "Convert PDF to HTML", - description = - "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") + description = "This endpoint converts a PDF file to HTML format.") public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 0fd7dee9a7..fd6218de0d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -24,6 +24,11 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; @@ -43,11 +48,10 @@ public class ConvertPDFToOffice { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PPT) @Operation( summary = "Convert PDF to Presentation format", - description = - "This endpoint converts a given PDF file to a Presentation format. Input:PDF" - + " Output:PPT Type:SISO") + description = "This endpoint converts a given PDF file to a Presentation format.") public ResponseEntity processPdfToPresentation( @ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { @@ -61,11 +65,16 @@ public class ConvertPDFToOffice { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO( + produces = ToolFormat.TEXT, + cases = + @ToolIOCase( + when = @ToolIOWhen(param = "outputFormat", matches = "rtf"), + produces = ToolFormat.WORD, + arity = ToolArity.SISO)) @Operation( summary = "Convert PDF to Text or RTF format", - description = - "This endpoint converts a given PDF file to Text or RTF format. Input:PDF" - + " Output:TXT Type:SISO") + description = "This endpoint converts a given PDF file to Text or RTF format.") public ResponseEntity processPdfToRTForTXT( @ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { @@ -94,11 +103,10 @@ public class ConvertPDFToOffice { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.WORD) @Operation( summary = "Convert PDF to Word document", - description = - "This endpoint converts a given PDF file to a Word document format. Input:PDF" - + " Output:WORD Type:SISO") + description = "This endpoint converts a given PDF file to a Word document format.") public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -111,11 +119,10 @@ public class ConvertPDFToOffice { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.XML) @Operation( summary = "Convert PDF to XML", - description = - "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML" - + " Type:SISO") + description = "This endpoint converts a PDF file to an XML file.") public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index a58d466d7a..0354817315 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -11,6 +11,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; +import java.util.Locale; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -71,6 +72,7 @@ import org.apache.xmpbox.schema.PDFAIdentificationSchema; import org.apache.xmpbox.schema.XMPBasicSchema; import org.apache.xmpbox.xml.DomXmpParser; import org.apache.xmpbox.xml.XmpSerializer; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -83,7 +85,6 @@ import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; @@ -91,6 +92,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.service.PdfaLevelAServiceInterface; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; @@ -100,14 +104,26 @@ import stirling.software.common.util.WebResponseUtils; @ConvertApi @Slf4j -@RequiredArgsConstructor public class ConvertPDFToPDFA { private static final Pattern NON_PRINTABLE_ASCII = Pattern.compile("[^\\x20-\\x7E]"); private final RuntimePathConfig runtimePathConfig; private final stirling.software.SPDF.service.VeraPDFService veraPDFService; + // Level A needs the proprietary tagger; core builds convert at level B instead. + private final PdfaLevelAServiceInterface pdfaLevelAService; private final TempFileManager tempFileManager; + public ConvertPDFToPDFA( + RuntimePathConfig runtimePathConfig, + stirling.software.SPDF.service.VeraPDFService veraPDFService, + @Autowired(required = false) PdfaLevelAServiceInterface pdfaLevelAService, + TempFileManager tempFileManager) { + this.runtimePathConfig = runtimePathConfig; + this.veraPDFService = veraPDFService; + this.pdfaLevelAService = pdfaLevelAService; + this.tempFileManager = tempFileManager; + } + private static final String ICC_RESOURCE_PATH = "/icc/sRGB2014.icc"; private static final int PDFA_COMPATIBILITY_POLICY = 1; @@ -577,10 +593,13 @@ public class ConvertPDFToPDFA { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Convert a PDF to a PDF/A or PDF/X", description = - "This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for long-term archiving, while PDF/X is optimized for print production. Input:PDF Output:PDF Type:SISO") + "This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript" + + " (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for" + + " long-term archiving, while PDF/X is optimized for print production.") public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); @@ -599,7 +618,10 @@ public class ConvertPDFToPDFA { return handlePdfXConversion(inputFile, outputFormat); } else { return handlePdfAConversion( - inputFile, outputFormat, request.getStrict() != null && request.getStrict()); + inputFile, + outputFormat, + request.getStrict() != null && request.getStrict(), + request.getPdfUa() != null && request.getPdfUa()); } } @@ -1810,8 +1832,64 @@ public class ConvertPDFToPDFA { return Files.readAllBytes(outputPdf); } + /** Tags a converted PDF/A for level A; must run after Ghostscript, which discards tags. */ + private PdfaLevelAServiceInterface.Result applyLevelA( + byte[] converted, + Path original, + PdfaProfile profile, + String baseFileName, + boolean declarePdfUa) { + if (!profile.requiresTagging()) { + return new PdfaLevelAServiceInterface.Result(converted, true, List.of()); + } + if (pdfaLevelAService == null) { + return new PdfaLevelAServiceInterface.Result( + converted, + false, + List.of( + "Level A tagging is not available in this build, so the file was left" + + " at conformance level B.")); + } + // Prefer the document's own title/language; hardcoding "en" mislabelled German reports. + // Read the original, not the converted bytes: Ghostscript discards /Lang, so probing its + // output always yields null and every document would be relabelled with the default. + String language = null; + String title = null; + try (PDDocument probe = Loader.loadPDF(original.toFile())) { + language = probe.getDocumentCatalog().getLanguage(); + title = probe.getDocumentInformation().getTitle(); + } catch (IOException e) { + log.debug("Could not read original title/language: {}", e.getMessage()); + } + if (language == null || language.isBlank()) { + try (PDDocument probe = Loader.loadPDF(converted)) { + language = probe.getDocumentCatalog().getLanguage(); + if (title == null || title.isBlank()) { + title = probe.getDocumentInformation().getTitle(); + } + } catch (IOException e) { + log.debug("Could not read converted title/language: {}", e.getMessage()); + } + } + PdfaLevelAServiceInterface.Result result = + pdfaLevelAService.upgradeToLevelA( + converted, + profile.getPart(), + language, + title != null && !title.isBlank() ? title : baseFileName, + declarePdfUa); + result.warnings().forEach(warning -> log.info("PDF/A level A: {}", warning)); + if (!result.levelA()) { + log.warn( + "{} requested but the document could not be tagged; returning level B", + profile.getDisplayName()); + } + return result; + } + private ResponseEntity handlePdfAConversion( - MultipartFile inputFile, String outputFormat, boolean strict) throws Exception { + MultipartFile inputFile, String outputFormat, boolean strict, boolean declarePdfUa) + throws Exception { PdfaProfile profile = PdfaProfile.fromRequest(outputFormat); // Get the original filename without extension @@ -1836,12 +1914,15 @@ public class ConvertPDFToPDFA { log.info("Using Ghostscript for PDF/A conversion to {}", profile.getDisplayName()); try { converted = convertWithGhostscript(inputPath, workingDir, profile); - String outputFilename = baseFileName + profile.outputSuffix(); + var levelA = + applyLevelA(converted, inputPath, profile, baseFileName, declarePdfUa); + converted = levelA.pdfBytes(); + String outputFilename = baseFileName + profile.outputSuffix(levelA.levelA()); validateAndWarnPdfA(converted, profile, "Ghostscript"); if (strict) { - verifyStrictCompliance(converted); + verifyStrictCompliance(converted, profile, levelA.levelA()); } TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); @@ -1862,13 +1943,15 @@ public class ConvertPDFToPDFA { } converted = convertWithPdfBoxMethod(inputPath, profile); - String outputFilename = baseFileName + profile.outputSuffix(); + var levelA = applyLevelA(converted, inputPath, profile, baseFileName, declarePdfUa); + converted = levelA.pdfBytes(); + String outputFilename = baseFileName + profile.outputSuffix(levelA.levelA()); // Validate with PDFBox preflight and warn if issues found validateAndWarnPdfA(converted, profile, "PDFBox/LibreOffice"); if (strict) { - verifyStrictCompliance(converted); + verifyStrictCompliance(converted, profile, levelA.levelA()); } TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); @@ -1884,11 +1967,56 @@ public class ConvertPDFToPDFA { } } - private void verifyStrictCompliance(byte[] pdfBytes) throws IOException { + /** True for a PDF/UA or WCAG result, which says nothing about archival conformance. */ + private static boolean isAccessibilityProfile( + stirling.software.SPDF.model.api.security.PDFVerificationResult result) { + String profile = result.getValidationProfile(); + if (profile == null) { + return false; + } + String normalised = profile.toLowerCase(Locale.ROOT); + return normalised.contains("ua") || normalised.contains("wcag"); + } + + /** + * True when a result speaks for the requested profile. Only archival results count, and a level + * B pass must never satisfy a level A request. + */ + private static boolean answersRequest( + PdfaProfile profile, + stirling.software.SPDF.model.api.security.PDFVerificationResult result) { + if (isAccessibilityProfile(result)) { + return false; + } + String standard = result.getStandard(); + if (standard == null || standard.length() < 2) { + return false; + } + if (standard.charAt(0) != Character.forDigit(profile.getPart(), 10)) { + return false; + } + return !profile.requiresTagging() || Character.toLowerCase(standard.charAt(1)) == 'a'; + } + + private void verifyStrictCompliance(byte[] pdfBytes, PdfaProfile profile, boolean levelAReached) + throws IOException { + // Tagging is the only route to level A, so an untagged file cannot answer a strict request. + if (!levelAReached) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Strict PDF/A mode enabled: the document could not be tagged, so " + + profile.getDisplayName() + + " was not reached. It is valid at level B."); + } try (InputStream is = new ByteArrayInputStream(pdfBytes)) { List results = veraPDFService.validatePDF(is); - boolean isCompliant = results.stream().anyMatch(result -> result.isCompliant()); + boolean isCompliant = + results.stream() + .filter(result -> answersRequest(profile, result)) + .anyMatch( + stirling.software.SPDF.model.api.security.PDFVerificationResult + ::isCompliant); if (!isCompliant) { String details = results.stream() @@ -1896,7 +2024,9 @@ public class ConvertPDFToPDFA { .collect(Collectors.joining("; ")); throw new ResponseStatusException( HttpStatus.BAD_REQUEST, - "Strict PDF/A mode enabled: Conversion is not perfectly compliant. Details: " + "Strict PDF/A mode enabled: the output is not perfectly compliant with " + + profile.getDisplayName() + + ". Details: " + details); } } catch (Exception e) { @@ -2461,11 +2591,16 @@ public class ConvertPDFToPDFA { @Getter private enum PdfaProfile { - PDF_A_1B(1, "PDF/A-1b", "_PDFA-1b.pdf", "1.4", Format.PDF_A1B, "pdfa-1"), - PDF_A_2B(2, "PDF/A-2b", "_PDFA-2b.pdf", "1.7", null, "pdfa", "pdfa-2", "pdfa-2b"), - PDF_A_3B(3, "PDF/A-3b", "_PDFA-3b.pdf", "1.7", null, "pdfa-3", "pdfa-3b"); + PDF_A_1B(1, "B", "PDF/A-1b", "_PDFA-1b.pdf", "1.4", Format.PDF_A1B, "pdfa-1"), + PDF_A_2B(2, "B", "PDF/A-2b", "_PDFA-2b.pdf", "1.7", null, "pdfa", "pdfa-2", "pdfa-2b"), + PDF_A_3B(3, "B", "PDF/A-3b", "_PDFA-3b.pdf", "1.7", null, "pdfa-3", "pdfa-3b"), + // Level A = level B plus tagging, declared language and Unicode text; tagged post-convert. + PDF_A_1A(1, "A", "PDF/A-1a", "_PDFA-1a.pdf", "1.4", Format.PDF_A1B, "pdfa-1a"), + PDF_A_2A(2, "A", "PDF/A-2a", "_PDFA-2a.pdf", "1.7", null, "pdfa-2a"), + PDF_A_3A(3, "A", "PDF/A-3a", "_PDFA-3a.pdf", "1.7", null, "pdfa-3a"); private final int part; + private final String conformanceLevel; private final String displayName; private final String suffix; private final String compatibilityLevel; @@ -2474,12 +2609,14 @@ public class ConvertPDFToPDFA { PdfaProfile( int part, + String conformanceLevel, String displayName, String suffix, String compatibilityLevel, Format preflightFormat, String... requestTokens) { this.part = part; + this.conformanceLevel = conformanceLevel; this.displayName = displayName; this.suffix = suffix; this.compatibilityLevel = compatibilityLevel; @@ -2490,6 +2627,10 @@ public class ConvertPDFToPDFA { .toList(); } + boolean requiresTagging() { + return "A".equals(conformanceLevel); + } + static PdfaProfile fromRequest(String requestToken) { if (requestToken == null) { return PDF_A_2B; @@ -2503,8 +2644,11 @@ public class ConvertPDFToPDFA { return match.orElse(PDF_A_2B); } - String outputSuffix() { - return suffix; + /** + * Names the file at the level actually reached; a level A name over level B content lies. + */ + String outputSuffix(boolean levelAReached) { + return levelAReached ? suffix : "_PDFA-" + part + "b.pdf"; } Optional preflightFormat() { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java index ccac814ef7..6aeb033761 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java @@ -62,7 +62,7 @@ public class ConvertPdfJsonController { @Operation( summary = "Convert PDF to Text Editor Format", description = - "Extracts PDF text, fonts, and metadata into an editable JSON structure for the text editor tool. Input:PDF Output:JSON Type:SISO") + "Extracts PDF text, fonts, and metadata into an editable JSON structure for the text editor tool.") public ResponseEntity convertPdfToJson( @ModelAttribute PDFFile request, @RequestParam(value = "lightweight", defaultValue = "false") boolean lightweight) @@ -104,7 +104,7 @@ public class ConvertPdfJsonController { @Operation( summary = "Convert Text Editor Format to PDF", description = - "Rebuilds a PDF from the editable JSON structure generated by the text editor tool. Input:JSON Output:PDF Type:SISO") + "Rebuilds a PDF from the editable JSON structure generated by the text editor tool.") public ResponseEntity convertJsonToPdf(@ModelAttribute GeneralFile request) throws Exception { MultipartFile jsonFile = request.getFileInput(); @@ -139,7 +139,7 @@ public class ConvertPdfJsonController { description = "Extracts document metadata, fonts, and page dimensions for the text editor tool. Caches the document for" + " subsequent page requests. Returns a server-generated jobId scoped to the" - + " authenticated user. Input:PDF Output:JSON Type:SISO") + + " authenticated user.") public ResponseEntity extractPdfMetadata(@ModelAttribute PDFFile request) throws Exception { MultipartFile inputFile = request.getFileInput(); @@ -226,7 +226,7 @@ public class ConvertPdfJsonController { description = "Retrieves a single page's content from a previously cached PDF document for the text editor tool." + " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the" - + " authenticated user. Output:JSON") + + " authenticated user.") public ResponseEntity extractSinglePage( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { @@ -255,7 +255,7 @@ public class ConvertPdfJsonController { description = "Retrieves the font payloads used by a single page from a previously cached PDF document." + " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the" - + " authenticated user. Output:JSON") + + " authenticated user.") public ResponseEntity extractPageFonts( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoController.java index 8f5d2a4817..df5a91ad67 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoController.java @@ -56,8 +56,7 @@ public class ConvertPdfToVideoController { @Operation( summary = "Convert PDF to Video Slideshow", description = - "This endpoint converts a PDF document into a slideshow-style video." - + " Input:PDF Output:Video Type:SISO") + "This endpoint converts a PDF document into a slideshow-style video.") public ResponseEntity convertPdfToVideo(@ModelAttribute PdfToVideoRequest request) throws Exception { if (!CheckProgramInstall.isFfmpegAvailable()) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java index 314dd28271..0b1bfc8e44 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java @@ -29,6 +29,11 @@ import stirling.software.SPDF.utils.SvgToPdf; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.SvgSanitizer; @@ -50,15 +55,23 @@ public class ConvertSvgToPDF { value = "/svg/pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO( + accepts = ToolFormat.IMAGE, + produces = ToolFormat.PDF, + arity = ToolArity.MIMO, + cases = + @ToolIOCase( + when = @ToolIOWhen(param = "combineIntoSinglePdf", matches = "true"), + produces = ToolFormat.PDF, + arity = ToolArity.MISO)) @Operation( summary = "Convert SVG to PDF", description = - "This endpoint converts one or more SVG (Scalable Vector Graphics) files to PDF format. " - + "Each SVG is converted to a separate PDF file. " - + "The conversion preserves vector graphics for crisp output at any resolution - no rasterization occurs. " - + "SVG dimensions (width/height) determine the PDF page size; defaults to A4 if not specified. " - + "SVG content is sanitized to prevent XSS attacks. " - + "Input: SVG file(s), Output: PDF file(s) or ZIP. Type: MIMO") + "This endpoint converts one or more SVG (Scalable Vector Graphics) files to PDF" + + " format. Each SVG is converted to a separate PDF file. The conversion preserves" + + " vector graphics for crisp output at any resolution - no rasterization occurs." + + " SVG dimensions (width/height) determine the PDF page size; defaults to A4 if" + + " not specified. SVG content is sanitized to prevent XSS attacks.") public ResponseEntity convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) { MultipartFile[] inputFiles = request.getFileInput(); @@ -147,7 +160,7 @@ public class ConvertSvgToPDF { String outputFilename = filenames.isEmpty() ? "combined_svgs.pdf" - : GeneralUtils.generateFilename(filenames.get(0), "_combined.pdf"); + : GeneralUtils.generateFilename(filenames.getFirst(), "_combined.pdf"); log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size()); @@ -203,7 +216,7 @@ public class ConvertSvgToPDF { try { if (convertedPdfs.size() == 1) { - ConvertedPdf pdf = convertedPdfs.get(0); + ConvertedPdf pdf = convertedPdfs.getFirst(); TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); try { Files.write(tempOut.getPath(), pdf.content); @@ -218,7 +231,7 @@ public class ConvertSvgToPDF { filenames.isEmpty() ? "converted_svgs.zip" : GeneralUtils.generateFilename( - filenames.get(0), "_converted_svgs.zip"); + filenames.getFirst(), "_converted_svgs.zip"); TempFile zipFile = createZipFromPdfs(convertedPdfs); return WebResponseUtils.zipFileToWebResponse(zipFile, zipFilename); } catch (IOException e) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index c07525f06c..1902dcf053 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -34,6 +34,8 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -62,11 +64,11 @@ public class ConvertWebsiteToPDF { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(accepts = ToolFormat.NONE, produces = ToolFormat.PDF) @Operation( summary = "Convert a URL to a PDF", description = - "This endpoint fetches content from a URL and converts it to a PDF format." - + " Input:N/A Output:PDF Type:SISO") + "This endpoint fetches content from a URL and converts it to a PDF format.") public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { String URL = request.getUrlInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java index 48ab474d42..22dff8f996 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java @@ -31,6 +31,9 @@ import stirling.software.SPDF.pdf.parser.TabulaTableParser; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @@ -48,11 +51,11 @@ public class ExtractCSVController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.LARGE_WEIGHT) @CsvConversionResponse + @ToolIO(produces = ToolFormat.CSV, arity = ToolArity.SIMO) @Operation( summary = "Extracts a CSV document from a PDF", description = - "This operation takes an input PDF file and returns CSV file of whole page." - + " Input:PDF Output:CSV Type:SISO") + "This operation takes an input PDF file and returns CSV file of whole page.") public ResponseEntity pdfToCsv(@ModelAttribute PDFWithPageNums request) throws Exception { String baseName = getBaseName(request.getFileInput().getOriginalFilename()); List csvEntries = new ArrayList<>(); @@ -82,7 +85,7 @@ public class ExtractCSVController { if (csvEntries.isEmpty()) { return ResponseEntity.noContent().build(); } else if (csvEntries.size() == 1) { - return createCsvResponse(csvEntries.get(0), baseName); + return createCsvResponse(csvEntries.getFirst(), baseName); } else { return createZipResponse(csvEntries, baseName); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java index 0dbddc5e07..6b6a6a0bf0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java @@ -27,6 +27,11 @@ import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; @@ -50,11 +55,11 @@ public class PdfVectorExportController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(accepts = ToolFormat.POSTSCRIPT, produces = ToolFormat.PDF) @Operation( summary = "Convert PostScript formats to PDF", description = - "Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript." - + " Input:PS/EPS Output:PDF Type:SISO") + "Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript.") public ResponseEntity convertGhostscriptInputsToPdf( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { @@ -100,11 +105,26 @@ public class PdfVectorExportController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + // One case per non-default value of outputFormat; the base covers the default, eps. + @ToolIO( + produces = ToolFormat.IMAGE, + cases = { + @ToolIOCase( + when = @ToolIOWhen(param = "outputFormat", matches = "ps"), + produces = ToolFormat.POSTSCRIPT, + arity = ToolArity.SISO), + @ToolIOCase( + when = @ToolIOWhen(param = "outputFormat", matches = "pcl"), + produces = ToolFormat.PCL, + arity = ToolArity.SISO), + @ToolIOCase( + when = @ToolIOWhen(param = "outputFormat", matches = "xps"), + produces = ToolFormat.XPS, + arity = ToolArity.SISO) + }) @Operation( summary = "Convert PDF to vector format", - description = - "Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS)." - + " Input:PDF Output:VECTOR Type:SISO") + description = "Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS).") public ResponseEntity convertPdfToVector( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 7563a09a92..9cd4bbaf1f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -28,6 +28,8 @@ import stirling.software.SPDF.model.api.filter.PageSizeRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.FilterApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; @@ -45,9 +47,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-text", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF contains set text, returns true if does", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF contains set text, returns true if does") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -79,9 +80,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF contains an image", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF contains an image") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -112,9 +112,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF is greater, less or equal to a setPageCount", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -146,9 +145,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF is of a certain size", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF is of a certain size") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -186,9 +184,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF is a set file size", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF is a set file size") @ApiResponses({ @ApiResponse( responseCode = "200", @@ -217,9 +214,8 @@ public class FilterController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation", resourceWeight = ResourceWeight.SMALL_WEIGHT) - @Operation( - summary = "Checks if a PDF is of a certain rotation", - description = "Input:PDF Output:Boolean Type:SISO") + @ToolIO(produces = ToolFormat.PDF) + @Operation(summary = "Checks if a PDF is of a certain rotation") @ApiResponses({ @ApiResponse( responseCode = "200", diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AddCommentsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AddCommentsController.java index 4e322b591d..dc2dd22863 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AddCommentsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AddCommentsController.java @@ -26,6 +26,8 @@ import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.comments.AnnotationLocation; import stirling.software.common.model.api.comments.StickyNoteSpec; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfAnnotationService; import stirling.software.common.util.GeneralUtils; @@ -72,14 +74,14 @@ public class AddCommentsController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add sticky-note comments to a PDF at specified positions or anchored text", description = - "Attaches PDF Text (sticky-note) annotations to the document." - + " Each CommentSpec can either supply absolute coordinates or an" - + " `anchorText` hint; when provided, the tool locates the first matching" - + " line on the target page and anchors the icon there (falling back to" - + " the coordinates if no match). Input:PDF Output:PDF Type:SISO") + "Attaches PDF Text (sticky-note) annotations to the document. Each CommentSpec" + + " can either supply absolute coordinates or an `anchorText` hint; when provided," + + " the tool locates the first matching line on the target page and anchors the" + + " icon there (falling back to the coordinates if no match).") public ResponseEntity addComments(@ModelAttribute AddCommentsRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java index 0e272678c4..a6a3dd8c56 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java @@ -29,6 +29,8 @@ import stirling.software.SPDF.service.AttachmentServiceInterface; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -54,10 +56,10 @@ public class AttachmentController { value = "/add-attachments", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add attachments to PDF", - description = - "This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO") + description = "This endpoint adds attachments to a PDF.") public ResponseEntity addAttachments(@ModelAttribute AddAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); @@ -143,11 +145,11 @@ public class AttachmentController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/extract-attachments", resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.ZIP) @Operation( summary = "Extract attachments from PDF", description = - "This endpoint extracts all embedded attachments from a PDF into a ZIP archive." - + " Input:PDF Output:ZIP Type:SISO") + "This endpoint extracts all embedded attachments from a PDF into a ZIP archive.") public ResponseEntity extractAttachments( @ModelAttribute ExtractAttachmentsRequest request) throws IOException { try (PDDocument document = pdfDocumentFactory.load(request, true)) { @@ -181,10 +183,10 @@ public class AttachmentController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/list-attachments", resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.JSON) @Operation( summary = "List attachments in PDF", - description = - "This endpoint lists all embedded attachments in a PDF. Input:PDF Output:JSON Type:SISO") + description = "This endpoint lists all embedded attachments in a PDF.") public ResponseEntity> listAttachments(@ModelAttribute ListAttachmentsRequest request) throws IOException { try (PDDocument document = pdfDocumentFactory.load(request, true)) { @@ -200,10 +202,10 @@ public class AttachmentController { value = "/rename-attachment", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Rename attachment in PDF", - description = - "This endpoint renames an embedded attachment in a PDF. Input:PDF Output:PDF Type:MISO") + description = "This endpoint renames an embedded attachment in a PDF.") public ResponseEntity renameAttachment( @ModelAttribute RenameAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); @@ -236,10 +238,10 @@ public class AttachmentController { value = "/delete-attachment", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Delete attachment from PDF", - description = - "This endpoint deletes an embedded attachment from a PDF. Input:PDF Output:PDF Type:MISO") + description = "This endpoint deletes an embedded attachment from a PDF.") public ResponseEntity deleteAttachment( @ModelAttribute DeleteAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index 405e45d9f2..34c3da0739 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -24,6 +24,8 @@ import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.TempFileManager; @@ -44,11 +46,12 @@ public class AutoRenameController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename", resourceWeight = ResourceWeight.SMALL_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header" - + " based on heuristics. Input:PDF Output:PDF Type:SISO") + + " based on heuristics.") public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { MultipartFile file = request.getFileInput(); @@ -113,7 +116,9 @@ public class AutoRenameController { mergedLineInfos.sort( Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); String title = - mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; + mergedLineInfos.isEmpty() + ? null + : mergedLineInfos.getFirst().text; return title != null ? title diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRotateController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRotateController.java new file mode 100644 index 0000000000..85d60b02be --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRotateController.java @@ -0,0 +1,396 @@ +package stirling.software.SPDF.controller.api.misc; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.imageio.ImageIO; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; + +import io.swagger.v3.oas.annotations.Operation; + +import jakarta.validation.Valid; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult; +import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult.PageResult; +import stirling.software.SPDF.model.api.misc.AutoRotatePdfRequest; +import stirling.software.SPDF.model.api.misc.PageRotation; +import stirling.software.SPDF.utils.AutoRotateDetection; +import stirling.software.SPDF.utils.AutoRotateDetection.OsdResult; +import stirling.software.SPDF.utils.AutoRotateDetection.TextDirection; +import stirling.software.common.annotations.AutoJobPostMapping; +import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempDirectory; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; + +@MiscApi +@Slf4j +@RequiredArgsConstructor +public class AutoRotateController { + + // OSD decides orientation from script shape, not character identity, so it does not need + // OCR-grade resolution. 150 DPI is ample for that and a quarter of the pixels of 300. + private static final int OSD_RENDER_DPI = 150; + + private static final String METHOD_TEXT = "text"; + private static final String METHOD_OSD = "osd"; + private static final String METHOD_INFERRED = "inferred"; + private static final String METHOD_NONE = "none"; + + private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; + private final EndpointConfiguration endpointConfiguration; + private final RuntimePathConfig runtimePathConfig; + private final ApplicationProperties applicationProperties; + + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/auto-rotate-pdf", + resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO( + produces = ToolFormat.PDF, + cases = + @ToolIOCase( + when = @ToolIOWhen(param = "dryRun", matches = "true"), + produces = ToolFormat.JSON, + arity = ToolArity.SISO)) + @Operation( + summary = "Detect and fix the orientation of every page", + description = + "Detects each page's orientation (embedded-text direction first, Tesseract OSD" + + " for scanned pages) and sets the page rotation so the content" + + " displays upright. With dryRun=true, returns a JSON per-page report" + + " instead of the PDF. With pageRotations set, applies the given" + + " corrections without running detection.") + public ResponseEntity autoRotatePdf(@Valid @ModelAttribute AutoRotatePdfRequest request) + throws IOException, InterruptedException { + String mode = + request.getDetectionMode() == null + ? "auto" + : request.getDetectionMode().toLowerCase(Locale.ROOT); + if (!"auto".equals(mode) && !"text".equals(mode) && !"osd".equals(mode)) { + throw ExceptionUtils.createIllegalArgumentException( + ExceptionUtils.ErrorCode.INVALID_ARGUMENT, + "detectionMode", + "must be one of auto, text, osd"); + } + + try (PDDocument document = pdfDocumentFactory.load(request)) { + if (request.getPageRotations() != null && !request.getPageRotations().isEmpty()) { + applyExplicitRotations(document, request.getPageRotations()); + return pdfResponse(document, request); + } + + AutoRotateAnalysisResult analysis = analyse(document, mode, request); + if (request.isDryRun()) { + return ResponseEntity.ok(analysis); + } + for (PageResult pageResult : analysis.getPages()) { + if (pageResult.isApply()) { + PDPage page = document.getPage(pageResult.getPageNumber() - 1); + page.setRotation( + Math.floorMod(page.getRotation() + pageResult.getCorrection(), 360)); + } + } + return pdfResponse(document, request); + } + } + + private AutoRotateAnalysisResult analyse( + PDDocument document, String mode, AutoRotatePdfRequest request) + throws IOException, InterruptedException { + double threshold = + request.getConfidenceThreshold() == null ? 14.0 : request.getConfidenceThreshold(); + boolean tesseractAvailable = endpointConfiguration.isGroupEnabled("tesseract"); + boolean useText = !"osd".equals(mode); + boolean useOsd = !"text".equals(mode); + + List results = new ArrayList<>(); + List osdCandidates = new ArrayList<>(); + + int pageCount = document.getNumberOfPages(); + // One walk of the document for all pages, rather than one walk per page. + List textDirections = + useText ? AutoRotateDetection.detectTextDirections(document) : List.of(); + + for (int i = 0; i < pageCount; i++) { + int currentRotation = Math.floorMod(document.getPage(i).getRotation(), 360); + PageResult result = + PageResult.builder() + .pageNumber(i + 1) + .currentRotation(currentRotation) + .method(METHOD_NONE) + .build(); + + if (useText) { + TextDirection direction = textDirections.get(i); + if (direction.isConclusive()) { + int correction = + AutoRotateDetection.correctionFromTextDirection( + direction.dominantDirection(), currentRotation); + result.setMethod(METHOD_TEXT); + result.setCorrection(correction); + result.setConfidence(direction.dominance() * 100); + result.setApply(correction != 0); + } else if (!useOsd) { + result.setNote( + direction.glyphCount() < AutoRotateDetection.MIN_GLYPHS + ? "tooFewGlyphs" + : "noDominantDirection"); + } + } + + if (useOsd && METHOD_NONE.equals(result.getMethod())) { + if (tesseractAvailable) { + osdCandidates.add(i); + } else { + result.setNote("tesseractUnavailable"); + } + } + results.add(result); + } + + if (!osdCandidates.isEmpty()) { + runOsdOnPages(document, osdCandidates, results, threshold); + } + + if (request.isInferUndetected()) { + inferUndetectedPages(results); + } + + return summarise(results, pageCount); + } + + /** + * Fill in pages that no signal could decide, using the pages that could. When every decided + * page sharing an undecided page's current rotation agrees on one correction, that correction + * is the document's consensus for that rotation and is applied to the undecided page. This is + * the common "whole document rotated uniformly, but a cover or near-blank page has too little + * text to detect on its own" case. If decided pages disagree, nothing is inferred. + */ + private void inferUndetectedPages(List results) { + // rotation -> the single agreed correction, or null once a conflict is seen + Map consensus = new HashMap<>(); + Set conflicted = new HashSet<>(); + for (PageResult result : results) { + if (METHOD_NONE.equals(result.getMethod())) { + continue; + } + int rotation = result.getCurrentRotation(); + if (conflicted.contains(rotation)) { + continue; + } + Integer existing = consensus.get(rotation); + if (existing == null) { + consensus.put(rotation, result.getCorrection()); + } else if (existing != result.getCorrection()) { + conflicted.add(rotation); + consensus.remove(rotation); + } + } + + for (PageResult result : results) { + if (!METHOD_NONE.equals(result.getMethod())) { + continue; + } + Integer correction = consensus.get(result.getCurrentRotation()); + if (correction == null) { + continue; + } + result.setMethod(METHOD_INFERRED); + result.setCorrection(correction); + result.setConfidence(null); + result.setApply(correction != 0); + result.setNote("inferredFromDocument"); + } + } + + private void runOsdOnPages( + PDDocument document, + List pageIndexes, + List results, + double threshold) + throws IOException, InterruptedException { + String tessDataPath = runtimePathConfig.getTessDataPath(); + boolean haveOsdData = + tessDataPath != null && new File(tessDataPath, "osd.traineddata").exists(); + + int dpi = OSD_RENDER_DPI; + if (applicationProperties != null && applicationProperties.getSystem() != null) { + dpi = Math.min(OSD_RENDER_DPI, applicationProperties.getSystem().getMaxDPI()); + } + final int renderDpi = dpi; + + try (TempDirectory tempDir = new TempDirectory(tempFileManager)) { + PDFRenderer renderer = new PDFRenderer(document); + renderer.setSubsamplingAllowed(true); + // One reused path, deleted after every page: the images are throwaway input for + // Tesseract, so a long document must not accumulate one file per page on disk. + File imageFile = new File(tempDir.getPath().toFile(), "osd-page.bmp"); + + for (int pageIndex : pageIndexes) { + PageResult result = results.get(pageIndex); + try { + // Rendering honours the page's current /Rotate, so OSD sees the page exactly + // as a viewer would and its verdict is always an additive correction. + BufferedImage image = + ExceptionUtils.handleOomRendering( + pageIndex + 1, + renderDpi, + () -> + renderer.renderImageWithDPI( + pageIndex, renderDpi, ImageType.GRAY)); + + if (AutoRotateDetection.isBlankRender(image)) { + // Nothing for OSD to read; skip the process spawn entirely. + result.setNote("blankPage"); + continue; + } + + // BMP, not PNG: the file is deleted straight after Tesseract reads it, so + // paying for compression only to discard the result is wasted work. + ImageIO.write(image, "bmp", imageFile); + + List command = new ArrayList<>(); + command.add("tesseract"); + command.add(imageFile.getAbsolutePath()); + command.add("stdout"); + command.add("--psm"); + command.add("0"); + if (haveOsdData) { + command.add("--tessdata-dir"); + command.add(tessDataPath); + } + + ProcessExecutorResult processResult = + ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT) + .runCommandWithOutputHandling(command); + + Optional osd = + AutoRotateDetection.parseOsd(processResult.getMessages()); + if (osd.isEmpty()) { + result.setNote("osdNoVerdict"); + continue; + } + result.setConfidence(osd.get().confidence()); + result.setCorrection(osd.get().rotate()); + if (osd.get().confidence() >= threshold) { + result.setMethod(METHOD_OSD); + result.setApply(osd.get().rotate() != 0); + } else { + result.setNote("belowThreshold"); + } + } catch (IOException e) { + // Textless pages make Tesseract exit non-zero; skip, never guess. + log.debug("OSD failed for page {}: {}", pageIndex + 1, e.getMessage()); + result.setNote("osdFailed"); + } finally { + Files.deleteIfExists(imageFile.toPath()); + } + } + } + } + + private void applyExplicitRotations(PDDocument document, List rotations) { + int pageCount = document.getNumberOfPages(); + Set seen = new HashSet<>(); + for (PageRotation entry : rotations) { + Integer pageNumber = entry.getPageNumber(); + Integer angle = entry.getRotation(); + if (pageNumber == null + || angle == null + || pageNumber < 1 + || pageNumber > pageCount + || angle % 90 != 0) { + throw ExceptionUtils.createIllegalArgumentException( + ExceptionUtils.ErrorCode.INVALID_ARGUMENT, + "pageRotations", + "page numbers must exist and rotations must be multiples of 90"); + } + // Rotations are additive, so a repeated page would be turned twice; reject rather + // than silently pick a winner. + if (!seen.add(pageNumber)) { + throw ExceptionUtils.createIllegalArgumentException( + ExceptionUtils.ErrorCode.INVALID_ARGUMENT, + "pageRotations", + "page " + pageNumber + " is listed more than once"); + } + PDPage page = document.getPage(pageNumber - 1); + page.setRotation(Math.floorMod(page.getRotation() + angle, 360)); + } + } + + private AutoRotateAnalysisResult summarise(List results, int pageCount) { + int toRotate = 0; + int byText = 0; + int byOsd = 0; + int byInference = 0; + int undetected = 0; + for (PageResult result : results) { + if (result.isApply()) { + toRotate++; + } + switch (result.getMethod()) { + case METHOD_TEXT -> byText++; + case METHOD_OSD -> byOsd++; + case METHOD_INFERRED -> byInference++; + default -> undetected++; + } + } + return AutoRotateAnalysisResult.builder() + .pages(results) + .totalPages(pageCount) + .pagesToRotate(toRotate) + .detectedByText(byText) + .detectedByOsd(byOsd) + .inferred(byInference) + .undetected(undetected) + .build(); + } + + private ResponseEntity pdfResponse(PDDocument document, AutoRotatePdfRequest request) + throws IOException { + String originalName = + request.getFileInput() != null + ? request.getFileInput().getOriginalFilename() + : "document.pdf"; + return WebResponseUtils.pdfDocToWebResponse( + document, + GeneralUtils.generateFilename(originalName, "_auto_rotated.pdf"), + tempFileManager); + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index ad5776b3cd..f513391d7a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -41,6 +41,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -274,13 +277,13 @@ public class AutoSplitPdfController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and" - + " splits the document at the QR code boundaries. The output is a zip" - + " file containing each separate PDF document. Input:PDF Output:ZIP-PDF" - + " Type:SISO") + + " splits the document at the QR code boundaries. The output is a zip file" + + " containing each separate PDF document.") public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -333,7 +336,7 @@ public class AutoSplitPdfController { } if (!splitDocuments.isEmpty() && !isValidQrCode) { - splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page)); + splitDocuments.getLast().addPage(document.getPage(page)); } else if (page == 0) { PDDocument firstDocument = new PDDocument(); firstDocument.addPage(document.getPage(page)); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index 9b702e9c58..8105f309bc 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -33,6 +33,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; @@ -86,12 +89,12 @@ public class BlankPageController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-blanks", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO) @Operation( summary = "Remove blank pages from a PDF file", description = "This endpoint removes blank pages from a given PDF file. Users can specify the" - + " threshold and white percentage to tune the detection of blank pages." - + " Input:PDF Output:PDF Type:SISO") + + " threshold and white percentage to tune the detection of blank pages.") public ResponseEntity removeBlankPages( @ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 8bae8adbf5..cd2b809760 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -50,6 +50,8 @@ import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.LineArtConversionService; import stirling.software.common.util.ExceptionUtils; @@ -267,7 +269,7 @@ public class CompressController { if (references.isEmpty()) continue; // Get the first instance of this image - PDImageXObject originalImage = getOriginalImage(doc, references.get(0)); + PDImageXObject originalImage = getOriginalImage(doc, references.getFirst()); // Track original size int originalSize = (int) originalImage.getCOSObject().getLength(); @@ -927,11 +929,12 @@ public class CompressController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided" - + " parameters. Input:PDF Output:PDF Type:SISO") + + " parameters.") public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); @@ -1167,7 +1170,7 @@ public class CompressController { List references = entry.getValue(); if (references.isEmpty()) continue; - PDImageXObject originalImage = getOriginalImage(doc, references.get(0)); + PDImageXObject originalImage = getOriginalImage(doc, references.getFirst()); int originalSize = (int) originalImage.getCOSObject().getLength(); stats.totalOriginalBytes += originalSize; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index 8044050025..36beb6610c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -194,6 +194,9 @@ public class ConfigController { configData.put( "enableMobileScanner", applicationProperties.getSystem().isEnableMobileScanner()); + configData.put( + "enableMobileSignature", + applicationProperties.getSystem().isEnableMobileSignature()); configData.put( "mobileScannerConvertToPdf", applicationProperties.getSystem().getMobileScannerSettings().isConvertToPdf()); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java index a867937c38..2f475faa9a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java @@ -24,6 +24,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -43,6 +45,7 @@ public class DecompressPdfController { value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Decompress PDF streams", description = "Fully decompresses all PDF streams including text content") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index 2284abfbf6..816b95d604 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -34,6 +34,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; @@ -60,13 +63,13 @@ public class ExtractImageScansController { value = "/extract-image-scans", resourceWeight = ResourceWeight.LARGE_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.IMAGE, arity = ToolArity.SIMO) @Operation( summary = "Extract image scans from an input file", description = "This endpoint extracts image scans from a given file based on certain" + " parameters. Users can specify angle threshold, tolerance, minimum area," - + " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP" - + " Type:SIMO") + + " minimum contour area, and border size.") public ResponseEntity extractImageScans( @ModelAttribute ExtractImageScansRequest request) throws IOException, InterruptedException { @@ -211,7 +214,7 @@ public class ExtractImageScansController { } else { // Return the processed image as a response - byte[] imageBytes = processedImageBytes.get(0); + byte[] imageBytes = processedImageBytes.getFirst(); finalOutput = tempFileManager.createManagedTempFile(".png"); try (OutputStream out = Files.newOutputStream(finalOutput.getPath())) { out.write(imageBytes); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 8faf93ec2d..3dfe1d6ba2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -35,6 +35,9 @@ import stirling.software.SPDF.model.api.PDFExtractImagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -55,12 +58,12 @@ public class ExtractImagesController { value = "/extract-images", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MultiFileResponse + @ToolIO(produces = ToolFormat.IMAGE, arity = ToolArity.SIMO) @Operation( summary = "Extract images from a PDF file", description = "This endpoint extracts images from a given PDF file and returns them in a zip" - + " file. Users can specify the output image format. Input:PDF" - + " Output:IMAGE/ZIP Type:SIMO") + + " file. Users can specify the output image format.") public ResponseEntity extractImages(@ModelAttribute PDFExtractImagesRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java index 21ca7b2d51..9bbbeffccb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java @@ -29,6 +29,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; @@ -48,11 +50,12 @@ public class FlattenController { value = "/flatten", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Flatten PDF form fields or full page", description = "Flattening just PDF form fields or converting each page to images to make text" - + " unselectable. Input:PDF, Output:PDF. Type:SISO") + + " unselectable.") public ResponseEntity flatten(@ModelAttribute FlattenRequest request) throws Exception { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index ca68547003..b51bfdc3fb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -26,6 +26,8 @@ import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.GeneralUtils; @@ -62,12 +64,12 @@ public class MetadataController { value = "/update-metadata", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Update metadata of a PDF file", description = "This endpoint allows you to update the metadata of a given PDF file. You can" - + " add, modify, or delete standard and custom metadata fields. Input:PDF" - + " Output:PDF Type:SISO") + + " add, modify, or delete standard and custom metadata fields.") public ResponseEntity metadata(@ModelAttribute MetadataRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java index c04911ee57..0bfa6c0337 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MobileScannerController.java @@ -62,12 +62,17 @@ public class MobileScannerController { } /** - * Check if mobile scanner feature is enabled + * Check if any feature backed by these transfer sessions is enabled. The mobile scanner and + * mobile signature drawing share this session/upload API, so the endpoints stay available while + * either feature is on; each flag independently controls only its own UI. * * @return Error response if disabled, null if enabled */ private ResponseEntity> checkFeatureEnabled() { - if (!applicationProperties.getSystem().isEnableMobileScanner()) { + boolean anyEnabled = + applicationProperties.getSystem().isEnableMobileScanner() + || applicationProperties.getSystem().isEnableMobileSignature(); + if (!anyEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body( Map.of( @@ -275,7 +280,8 @@ public class MobileScannerController { @Parameter(description = "Filename to download", required = true) @PathVariable String filename) { - if (!applicationProperties.getSystem().isEnableMobileScanner()) { + if (!applicationProperties.getSystem().isEnableMobileScanner() + && !applicationProperties.getSystem().isEnableMobileSignature()) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 4660d5f128..4803184a33 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -41,6 +41,11 @@ import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -88,13 +93,20 @@ public class OCRController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf", resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO( + produces = ToolFormat.PDF, + cases = + @ToolIOCase( + when = @ToolIOWhen(param = "sidecar", matches = "true"), + produces = ToolFormat.ZIP, + arity = ToolArity.SISO)) @Operation( summary = "Process a PDF file with OCR", description = - "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can" - + " specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType," - + " and removeImagesAfter options. Uses OCRmyPDF if available, falls back to" - + " Tesseract. Input:PDF Output:PDF Type:SI-Conditional") + "This endpoint processes a PDF file using OCR (Optical Character Recognition)." + + " Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType," + + " ocrRenderType, and removeImagesAfter options. Uses OCRmyPDF if available," + + " falls back to Tesseract.") public ResponseEntity processPdfWithOCR( @ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index 557c77c76e..4f7c335a23 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -23,6 +23,8 @@ import stirling.software.SPDF.utils.SvgOverlayUtil; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.SvgSanitizer; @@ -43,14 +45,14 @@ public class OverlayImageController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Overlay image onto a PDF file", description = - "This endpoint overlays an image onto a PDF file at the specified coordinates. " - + "Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG). " - + "SVG files are rendered as vector graphics for crisp output at any resolution. " - + "The image can be overlaid on every page of the PDF if specified. " - + "Input:PDF/IMAGE/SVG Output:PDF Type:SISO") + "This endpoint overlays an image onto a PDF file at the specified coordinates." + + " Supports both raster formats (PNG, JPEG, etc.) and vector format (SVG). SVG" + + " files are rendered as vector graphics for crisp output at any resolution. The" + + " image can be overlaid on every page of the PDF if specified.") public ResponseEntity overlayImage(@ModelAttribute OverlayImageRequest request) { MultipartFile pdfFile = request.getFileInput(); MultipartFile imageFile = request.getImageFile(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index e000d9ff3a..bcced01ba9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -27,6 +27,8 @@ import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -45,11 +47,10 @@ public class PageNumbersController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add page numbers to a PDF document", - description = - "This operation takes an input PDF file and adds page numbers to it. Input:PDF" - + " Output:PDF Type:SISO") + description = "This operation takes an input PDF file and adds page numbers to it.") public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java index e369bbf102..82184b68ba 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java @@ -46,8 +46,8 @@ public class PrintFileController { // @Operation( // summary = "Prints PDF/Image file to a set printer", // description = - // "Input of PDF or Image along with a printer name/URL/IP to match against to - // send it to (Fire and forget) Input:Any Output:N/A Type:SISO") + // "Input of PDF or Image along with a printer name/URL/IP to match against + // to send it to (Fire and forget)") public ResponseEntity printFile(@ModelAttribute PrintFileRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java index ce4ca94239..44703af863 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java @@ -27,6 +27,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -46,11 +48,12 @@ public class RemoveImagesController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Remove images from PDF", description = "This endpoint removes all embedded images from a PDF file and returns the" - + " modified document. Input:PDF Output:PDF Type:SISO") + + " modified document.") public ResponseEntity removeImages(@ModelAttribute PDFFile request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index 076e977b4c..7c74c85d09 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -21,6 +21,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -52,12 +54,13 @@ public class RepairController { value = "/repair", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Repair a PDF file", description = - "This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf (fallback), or PDFBox (if no external tools available). The PDF is" - + " first saved to a temporary location, repaired, read back, and then" - + " returned as a response. Input:PDF Output:PDF Type:SISO") + "This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf" + + " (fallback), or PDFBox (if no external tools available). The PDF is first saved" + + " to a temporary location, repaired, read back, and then returned as a response.") public ResponseEntity repairPdf(@ModelAttribute PDFFile file) throws IOException, InterruptedException { MultipartFile inputFile = file.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java index 7c5115e361..3a5f0259f5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java @@ -20,6 +20,8 @@ import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @@ -36,11 +38,13 @@ public class ReplaceAndInvertColorController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/replace-invert-pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Replace-Invert Color PDF", description = - "This endpoint accepts a PDF file and provides options to invert all colors, replace" - + " text and background colors, or convert to CMYK color space for printing. Input:PDF Output:PDF Type:SISO") + "This endpoint accepts a PDF file and provides options to invert all colors," + + " replace text and background colors, or convert to CMYK color space for" + + " printing.") public ResponseEntity replaceAndInvertColor( @ModelAttribute ReplaceAndInvertColorRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java index 445009000f..eb6b74fd58 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java @@ -48,6 +48,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; @@ -564,10 +566,12 @@ public class ScannerEffectController { value = "/scanner-effect", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Apply scanner effect to PDF", description = - "Applies various effects to simulate a scanned document, including rotation, noise, and edge softening. Input:PDF Output:PDF Type:SISO") + "Applies various effects to simulate a scanned document, including rotation," + + " noise, and edge softening.") public ResponseEntity scannerEffect( @Valid @ModelAttribute ScannerEffectRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 9897100a7d..fcba1ea718 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -23,6 +23,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @@ -40,9 +42,10 @@ public class ShowJavascript { value = "/show-javascript", resourceWeight = ResourceWeight.SMALL_WEIGHT) @JavaScriptResponse + @ToolIO(produces = ToolFormat.JAVASCRIPT) @Operation( summary = "Grabs all JS from a PDF and returns a single JS file with all code", - description = "desc. Input:PDF Output:JS Type:SISO") + description = "desc.") public ResponseEntity extractHeader(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); StringBuilder script = new StringBuilder(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 9b9f0213cd..0cd485d7d1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -47,6 +47,8 @@ import stirling.software.SPDF.model.api.misc.AddStampRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -91,12 +93,12 @@ public class StampController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add stamp to a PDF file", description = "This endpoint adds a stamp to a given PDF file. Users can specify the stamp" - + " type (text or image), rotation, opacity, width spacer, and height" - + " spacer. Input:PDF Output:PDF Type:SISO") + + " type (text or image), rotation, opacity, width spacer, and height spacer.") public ResponseEntity addStamp(@ModelAttribute AddStampRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java index 246626b38b..3dcabd43f7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java @@ -25,6 +25,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.RegexPatternUtils; @@ -48,11 +50,10 @@ public class UnlockPDFFormsController { value = "/unlock-pdf-forms", resourceWeight = ResourceWeight.SMALL_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Remove read-only property from form fields", - description = - "Removing read-only property from form fields making them fillable" - + "Input:PDF, Output:PDF. Type:SISO") + description = "Removing read-only property from form fields making them fillable") public ResponseEntity unlockPDFForms(@ModelAttribute PDFFile file) { try (PDDocument document = pdfDocumentFactory.load(file)) { PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index 40102fb742..b624434502 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -59,8 +59,7 @@ public class PipelineController { summary = "Execute automated PDF processing pipeline", description = "This endpoint processes multiple PDF files through a configurable pipeline of operations. " - + "Users provide files and a JSON configuration defining the sequence of operations to perform. " - + "Input:PDF Output:PDF/ZIP Type:MIMO") + + "Users provide files and a JSON configuration defining the sequence of operations to perform.") public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) throws DatabindException, JacksonException { MultipartFile[] files = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index 79223ddd38..627b1461f6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -35,6 +35,7 @@ import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.service.PostHogService; +import stirling.software.common.service.ToolMetadataService; import stirling.software.common.util.FileReadinessChecker; import tools.jackson.databind.ObjectMapper; @@ -48,6 +49,7 @@ public class PipelineDirectoryProcessor { private final ObjectMapper objectMapper; private final ApiDocService apiDocService; + private final ToolMetadataService toolMetadataService; private final PipelineProcessor processor; private final PostHogService postHogService; private final FileReadinessChecker fileReadinessChecker; @@ -61,12 +63,14 @@ public class PipelineDirectoryProcessor { public PipelineDirectoryProcessor( ObjectMapper objectMapper, ApiDocService apiDocService, + ToolMetadataService toolMetadataService, PipelineProcessor processor, PostHogService postHogService, FileReadinessChecker fileReadinessChecker, RuntimePathConfig runtimePathConfig) { this.objectMapper = objectMapper; this.apiDocService = apiDocService; + this.toolMetadataService = toolMetadataService; this.processor = processor; this.postHogService = postHogService; this.fileReadinessChecker = fileReadinessChecker; @@ -229,7 +233,7 @@ public class PipelineDirectoryProcessor { throws IOException { List inputExtensions = - apiDocService.getExtensionTypes(false, operation.getOperation()); + toolMetadataService.getExtensionTypes(false, operation.getOperation()); log.info( "Allowed extensions for operation {}: {}", operation.getOperation(), diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index c4f1c76f5e..08684dd5fe 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -30,6 +30,7 @@ import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; import stirling.software.common.service.AutomationRunContext; import stirling.software.common.service.InternalApiClient; +import stirling.software.common.service.ToolMetadataService; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.ZipExtractionUtils; @@ -39,15 +40,19 @@ public class PipelineProcessor { private final ApiDocService apiDocService; + private final ToolMetadataService toolMetadataService; + private final InternalApiClient internalApiClient; private final TempFileManager tempFileManager; public PipelineProcessor( ApiDocService apiDocService, + ToolMetadataService toolMetadataService, InternalApiClient internalApiClient, TempFileManager tempFileManager) { this.apiDocService = apiDocService; + this.toolMetadataService = toolMetadataService; this.internalApiClient = internalApiClient; this.tempFileManager = tempFileManager; } @@ -92,13 +97,13 @@ public class PipelineProcessor { boolean filtersApplied = false; for (PipelineOperation pipelineOperation : config.getOperations()) { String operation = pipelineOperation.getOperation(); - boolean isMultiInputOperation = apiDocService.isMultiInput(operation); + boolean isMultiInputOperation = toolMetadataService.isMultiInput(operation); log.info( "Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); Map parameters = pipelineOperation.getParameters(); - List inputFileTypes = apiDocService.getExtensionTypes(false, operation); + List inputFileTypes = toolMetadataService.getExtensionTypes(false, operation); if (inputFileTypes == null) { inputFileTypes = new ArrayList<>(List.of("ALL")); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 7ad69ebc5b..58acfda82d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -13,7 +13,6 @@ import java.util.Calendar; import java.util.List; import org.apache.commons.io.FileUtils; -import org.apache.pdfbox.examples.signature.CreateSignatureBase; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -76,9 +75,12 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; +import stirling.software.SPDF.pdf.signature.CreateSignatureBase; import stirling.software.SPDF.service.HardwareKeyStoreService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.util.ExceptionUtils; @@ -170,12 +172,13 @@ public class CertSignController { value = "/cert-sign", resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related" + " information to sign the PDF. It then returns the digitally signed PDF" - + " file. Input:PDF Output:PDF Type:SISO") + + " file.") public ResponseEntity signPDFWithCert( @ModelAttribute SignPDFWithCertRequest request, HttpServletRequest httpRequest) throws Exception { @@ -404,7 +407,7 @@ public class CertSignController { PDAcroForm acroForm = new PDAcroForm(doc); doc.getDocumentCatalog().setAcroForm(acroForm); PDSignatureField signatureField = new PDSignatureField(acroForm); - PDAnnotationWidget widget = signatureField.getWidgets().get(0); + PDAnnotationWidget widget = signatureField.getWidgets().getFirst(); List acroFormFields = acroForm.getFields(); acroForm.setSignaturesExist(true); acroForm.setAppendOnly(true); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index a66b168163..d2775e910b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -58,8 +58,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @@ -268,25 +269,6 @@ public class GetInfoOnPDF { } } - private static void validatePdfFile(MultipartFile file) { - if (file == null || file.isEmpty()) { - throw new IllegalArgumentException("PDF file is required"); - } - - if (file.getSize() > MAX_FILE_SIZE) { - throw ExceptionUtils.createIllegalArgumentException( - "error.fileSizeLimit", - "File size ({0} bytes) exceeds maximum allowed size ({1} bytes)", - file.getSize(), - MAX_FILE_SIZE); - } - - String contentType = file.getContentType(); - if (contentType != null && !"application/pdf".equals(contentType)) { - log.warn("File content type is {}, expected application/pdf", contentType); - } - } - private static ResponseEntity createErrorResponse(String errorMessage) { try { ObjectNode errorNode = objectMapper.createObjectNode(); @@ -1095,21 +1077,13 @@ public class GetInfoOnPDF { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) + @ToolIO(produces = ToolFormat.JSON) @Operation( summary = "Get comprehensive PDF information", - description = - "Extracts all available information from a PDF file. Input:PDF Output:JSON Type:SISO") + description = "Extracts all available information from a PDF file.") public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { MultipartFile inputFile = request.getFileInput(); - // Validate input - try { - validatePdfFile(inputFile); - } catch (IllegalArgumentException e) { - log.error("Invalid PDF file: {}", e.getMessage()); - return createErrorResponse("Invalid PDF file: " + e.getMessage()); - } - List verificationResults = null; try { verificationResults = veraPDFService.validatePDF(inputFile.getInputStream()); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 946eee0359..2ad494fc63 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -21,6 +21,11 @@ import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOCase; +import stirling.software.common.model.tool.ToolIOWhen; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -39,11 +44,14 @@ public class PasswordController { value = "/remove-password", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO( + accepts = {ToolFormat.PDF, ToolFormat.PDF_ENCRYPTED}, + produces = ToolFormat.PDF) @Operation( summary = "Remove password from a PDF file", description = "This endpoint removes the password from a protected PDF file. Users need to" - + " provide the existing password. Input:PDF Output:PDF Type:SISO") + + " provide the existing password.") public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); @@ -71,12 +79,21 @@ public class PasswordController { value = "/add-password", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO( + produces = ToolFormat.PDF_ENCRYPTED, + cases = + @ToolIOCase( + when = { + @ToolIOWhen(param = "password", matches = ""), + @ToolIOWhen(param = "ownerPassword", matches = "") + }, + produces = ToolFormat.PDF, + arity = ToolArity.SISO)) @Operation( summary = "Add password to a PDF file", description = "This endpoint adds password protection to a PDF file. Users can specify a set" - + " of permissions that should be applied to the file. Input:PDF" - + " Output:PDF") + + " of permissions that should be applied to the file.") public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); @@ -107,15 +124,15 @@ public class PasswordController { StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); - if ((ownerPassword != null && ownerPassword.length() > 0) - || (password != null && password.length() > 0)) { + if ((ownerPassword != null && !ownerPassword.isEmpty()) + || (password != null && !password.isEmpty())) { spp.setEncryptionKeyLength(keyLength); } spp.setPermissions(ap); document.protect(spp); - if ((ownerPassword == null || ownerPassword.length() == 0) - && (password == null || password.length() == 0)) + if ((ownerPassword == null || ownerPassword.isEmpty()) + && (password == null || password.isEmpty())) return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index ece149a820..7a186235cd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -1,9 +1,14 @@ package stirling.software.SPDF.controller.api.security; +import java.awt.Color; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageTree; @@ -33,6 +38,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.security.RedactionArea; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; @@ -41,6 +48,10 @@ import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.JsonListPropertyEditor; import stirling.software.common.util.propertyeditor.JsonObjectPropertyEditor; +import stirling.software.jpdfium.PdfDocument; +import stirling.software.jpdfium.redact.PdfRedactor; +import stirling.software.jpdfium.redact.RedactOptions; +import stirling.software.jpdfium.redact.RedactResult; import tools.jackson.core.type.TypeReference; @@ -82,13 +93,14 @@ public class RedactController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( operationId = "redactPdfManual", summary = "Redacts areas and pages in a PDF document", description = - "This endpoint redacts content from a PDF file based on manually specified areas. " - + "Users can specify areas to redact and optionally convert the PDF to an image. " - + "Input:PDF Output:PDF Type:SISO") + "This endpoint redacts content from a PDF file based on manually specified" + + " areas. Users can specify areas to redact and optionally convert the PDF to an" + + " image.") public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) throws IOException { @@ -128,142 +140,147 @@ public class RedactController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Redact PDF automatically", operationId = "redactPdfAuto", description = - "This endpoint automatically redacts text from a PDF file based on specified patterns. " - + "Users can provide text patterns to redact, with options for regex and whole word matching. " - + "Input:PDF Output:PDF Type:SISO") + "This endpoint automatically redacts text from a PDF file based on specified" + + " patterns. Users can provide text patterns to redact, with options for regex" + + " and whole word matching.") public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) { - String rawListOfText = request.getListOfText(); - boolean useRegex = Boolean.TRUE.equals(request.getUseRegex()); - boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch()); + if (request.getFileInput() == null || request.getFileInput().isEmpty()) { + log.error("File input is null or empty"); + throw ExceptionUtils.createFileNullOrEmptyException(); + } + String rawListOfText = request.getListOfText(); if (rawListOfText == null || rawListOfText.trim().isEmpty()) { throw ExceptionUtils.createIllegalArgumentException( "error.redaction.no.patterns", "No text patterns provided for redaction"); } - String[] listOfText = rawListOfText.split("\n"); - if (listOfText.length == 1 && listOfText[0].trim().isEmpty()) { + List terms = + Arrays.stream(rawListOfText.split("\n")) + .map(String::trim) + .filter(s -> !s.isEmpty() && s.length() <= 4096) + .collect(Collectors.toList()); + + if (terms.isEmpty()) { throw ExceptionUtils.createIllegalArgumentException( "error.redaction.no.patterns", "No text patterns provided for redaction"); } - PDDocument document = null; - PDDocument fallbackDocument = null; + boolean useRegex = Boolean.TRUE.equals(request.getUseRegex()); + boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch()); - try { - if (request.getFileInput() == null) { - log.error("File input is null"); - throw ExceptionUtils.createFileNullOrEmptyException(); + if (useRegex) { + for (String term : terms) { + try { + Pattern.compile(term); + } catch (PatternSyntaxException e) { + throw ExceptionUtils.createIllegalArgumentException( + "error.redaction.no.patterns", "Invalid regex pattern: " + term); + } } + } - document = pdfDocumentFactory.load(request.getFileInput()); + String filename = + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName( + request.getFileInput().getOriginalFilename()))) + + "_redacted.pdf"; + Color redactColor = ManualRedactionService.decodeOrDefault(request.getRedactColor()); + int boxColorInt = redactColor.getRGB(); + + try (PDDocument document = pdfDocumentFactory.load(request.getFileInput())) { if (document == null) { log.error("Failed to load PDF document"); throw ExceptionUtils.createPdfCorruptedException( "during redaction", new IOException("Failed to load PDF document")); } - Map> allFoundTextsByPage = - textRedactionService.findTextToRedact( - document, listOfText, useRegex, wholeWordSearchBool); + try (TempFile tempInput = tempFileManager.createManagedTempFile(".pdf")) { + try { + request.getFileInput().transferTo(tempInput.getFile()); + } catch (Exception e) { + document.save(tempInput.getFile()); + } - int totalMatches = allFoundTextsByPage.values().stream().mapToInt(List::size).sum(); - log.info( - "Redaction scan: {} occurrences across {} pages (patterns={}, regex={}, wholeWord={})", - totalMatches, - allFoundTextsByPage.size(), - listOfText.length, - useRegex, - wholeWordSearchBool); + RedactOptions options = + RedactOptions.builder() + .addWords(terms) + .useRegex(useRegex) + .wholeWord(wholeWordSearchBool) + .boxColor(boxColorInt) + .padding(request.getCustomPadding()) + .removeContent(true) + .convertToImage(Boolean.TRUE.equals(request.getConvertPDFToImage())) + .normalizeFonts(false) + .fixToUnicode(false) + .glyphAware(true) + .redactMetadata(true) + .build(); - String filename = - removeFileExtension( - Objects.requireNonNull( - Filenames.toSimpleFileName( - request.getFileInput().getOriginalFilename()))) - + "_redacted.pdf"; + TempFile tempOutput = tempFileManager.createManagedTempFile(".pdf"); + try { + try (PdfDocument checkDoc = PdfDocument.open(tempInput.getFile().toPath())) { + if (checkDoc.pageCount() <= 0) { + throw new IOException("Invalid or empty PDF document"); + } + } - if (allFoundTextsByPage.isEmpty()) { - log.info("No text found matching redaction patterns"); - return WebResponseUtils.pdfDocToWebResponse(document, filename, tempFileManager); + log.debug( + "Calling JPDFium PdfRedactor.redact in RedactController (terms={})", + terms); + RedactResult result = PdfRedactor.redact(tempInput.getFile().toPath(), options); + log.debug( + "JPDFium auto-redact complete (matches={})", + result != null ? result.totalMatches() : -1); + if (result == null) { + throw new IOException("JPDFium auto-redact returned null result"); + } + try { + result.save(tempOutput.getFile().toPath()); + log.info( + "JPDFium auto-redact: {} matches processed into {}", + result.totalMatches(), + filename); + return WebResponseUtils.pdfFileToWebResponse(tempOutput, filename); + } finally { + if (result.document() != null) { + result.document().close(); + } + } + } catch (Exception e) { + tempOutput.close(); + log.warn( + "JPDFium native redaction fell back to manual redaction service: {}", + e.getMessage()); + Map> foundTexts = + textRedactionService.findTextToRedact( + document, + terms.toArray(new String[0]), + useRegex, + wholeWordSearchBool); + TempFile finalized = + manualRedactionService.finalizeRedaction( + document, + foundTexts, + request.getRedactColor(), + request.getCustomPadding(), + request.getConvertPDFToImage(), + false); + return WebResponseUtils.pdfFileToWebResponse(finalized, filename); + } } - - boolean fallbackToBoxOnlyMode; - try { - fallbackToBoxOnlyMode = - textRedactionService.performTextReplacement( - document, - allFoundTextsByPage, - listOfText, - useRegex, - wholeWordSearchBool); - } catch (Exception e) { - log.warn( - "Text replacement redaction failed, falling back to box-only mode: {}", - e.getMessage()); - fallbackToBoxOnlyMode = true; - } - - if (fallbackToBoxOnlyMode) { - log.warn( - "Font compatibility issues detected. Using box-only redaction mode for better reliability."); - - fallbackDocument = pdfDocumentFactory.load(request.getFileInput()); - - allFoundTextsByPage = - textRedactionService.findTextToRedact( - fallbackDocument, listOfText, useRegex, wholeWordSearchBool); - - TempFile finalized = - manualRedactionService.finalizeRedaction( - fallbackDocument, - allFoundTextsByPage, - request.getRedactColor(), - request.getCustomPadding(), - request.getConvertPDFToImage(), - false); - - return WebResponseUtils.pdfFileToWebResponse(finalized, filename); - } - - TempFile finalized = - manualRedactionService.finalizeRedaction( - document, - allFoundTextsByPage, - request.getRedactColor(), - request.getCustomPadding(), - request.getConvertPDFToImage(), - true); - - return WebResponseUtils.pdfFileToWebResponse(finalized, filename); - + } catch (IllegalArgumentException e) { + throw e; } catch (Exception e) { log.error("Redaction operation failed: {}", e.getMessage(), e); throw new RuntimeException("Failed to perform PDF redaction: " + e.getMessage(), e); - - } finally { - if (document != null) { - try { - if (fallbackDocument == null) { - document.close(); - } - } catch (IOException e) { - log.warn("Failed to close main document: {}", e.getMessage()); - } - } - - if (fallbackDocument != null) { - try { - fallbackDocument.close(); - } catch (IOException e) { - log.warn("Failed to close fallback document: {}", e.getMessage()); - } - } } } @@ -272,13 +289,13 @@ public class RedactController { consumes = MediaType.MULTIPART_FORM_DATA_VALUE, resourceWeight = ResourceWeight.LARGE_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( operationId = "redactExecute", summary = "Execute a unified redaction plan on a PDF", description = - "Unified redaction endpoint that accepts exact strings, regex patterns, and " - + "page numbers in a single request. Supports execution strategy hints. " - + "Input:PDF Output:PDF Type:SISO") + "Unified redaction endpoint that accepts exact strings, regex patterns, and" + + " page numbers in a single request. Supports execution strategy hints.") public ResponseEntity executeRedaction(@ModelAttribute RedactExecuteRequest request) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java index 4a53be97b6..10fceee45f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactExecuteService.java @@ -634,7 +634,7 @@ class RedactExecuteService { PageColumnLayout layout = PageColumnLayout.fromLineBoxes(extractor.getLineBoxes(), pageWidth); if (layout.columnCount() > 1) { - float[] g = layout.gutters().get(0); + float[] g = layout.gutters().getFirst(); log.info( "[redact/execute] page {} layout: 2 cols, gutter x=[{}, {}]", pageIdx + 1, @@ -760,12 +760,12 @@ class RedactExecuteService { char ch = raw.charAt(i); if (Character.isLetterOrDigit(ch)) { current.append(ch); - } else if (current.length() > 0) { + } else if (!current.isEmpty()) { tokens.add(current.toString()); current.setLength(0); } } - if (current.length() > 0) tokens.add(current.toString()); + if (!current.isEmpty()) tokens.add(current.toString()); if (tokens.size() < 2) return null; StringBuilder out = new StringBuilder(); for (int i = 0; i < tokens.size(); i++) { @@ -788,25 +788,25 @@ class RedactExecuteService { StringBuilder current = new StringBuilder(); for (String token : tokens) { if (token.isEmpty()) { - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); current.setLength(0); } } else if (token.length() == 1) { current.append(token); } else { - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); current.setLength(0); } - if (result.length() > 0) result.append(' '); + if (!result.isEmpty()) result.append(' '); result.append(token); } } - if (current.length() > 0) { - if (result.length() > 0) result.append(' '); + if (!current.isEmpty()) { + if (!result.isEmpty()) result.append(' '); result.append(current); } return result.toString().trim(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java index 26f4a09e89..4b0cc6e6b0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java @@ -22,6 +22,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFileManager; @@ -39,11 +41,12 @@ public class RemoveCertSignController { value = "/remove-cert-sign", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Remove digital signature from PDF", description = "This endpoint accepts a PDF file and returns the PDF file without the digital" - + " signature. Input:PDF, Output:PDF Type:SISO") + + " signature.") public ResponseEntity removeCertSignPDF(@ModelAttribute PDFFile request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -60,7 +63,7 @@ public class RemoveCertSignController { // Remove signature fields safely List fieldsToRemove = acroForm.getFields().stream() - .filter(field -> field instanceof PDSignatureField) + .filter(PDSignatureField.class::isInstance) .toList(); if (!fieldsToRemove.isEmpty()) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index 15c768349d..4b3fd497a3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -40,6 +40,8 @@ import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFileManager; @@ -58,11 +60,12 @@ public class SanitizeController { value = "/sanitize-pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Sanitize a PDF file", description = "This endpoint processes a PDF file and removes specific elements based on the" - + " provided options. Input:PDF Output:PDF Type:SISO") + + " provided options.") public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException { MultipartFile inputFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TextRedactionService.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TextRedactionService.java index a9633ffa39..9cf4e6c700 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TextRedactionService.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TextRedactionService.java @@ -1,33 +1,20 @@ package stirling.software.SPDF.controller.api.security; +import java.io.File; import java.io.IOException; -import java.util.ArrayList; +import java.nio.file.Files; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.apache.pdfbox.contentstream.operator.Operator; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSFloat; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.cos.COSNumber; -import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdfparser.PDFStreamParser; -import org.apache.pdfbox.pdfwriter.ContentStreamWriter; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDResources; -import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.graphics.PDXObject; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.springframework.stereotype.Service; import lombok.AllArgsConstructor; @@ -35,31 +22,23 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.PDFText; -import stirling.software.SPDF.utils.text.TextEncodingHelper; import stirling.software.SPDF.utils.text.TextFinderUtils; -import stirling.software.SPDF.utils.text.WidthCalculator; +import stirling.software.jpdfium.PdfDocument; +import stirling.software.jpdfium.redact.PdfRedactor; +import stirling.software.jpdfium.redact.RedactOptions; +import stirling.software.jpdfium.redact.RedactResult; @Service @Slf4j class TextRedactionService { - private static final int MAX_XOBJECT_DEPTH = 10; - private static final float PRECISION_THRESHOLD = 1e-3f; - private static final int FONT_SCALE_FACTOR = 1000; - private static final Set TEXT_SHOWING_OPERATORS = Set.of("Tj", "TJ", "'", "\""); - private static final COSString EMPTY_COS_STRING = new COSString(""); - - // ----------------------------------------------------------------------- - // Public API - // ----------------------------------------------------------------------- - Map> findTextToRedact( PDDocument document, String[] listOfText, boolean useRegex, boolean wholeWordSearch) { Set terms = Arrays.stream(listOfText) .map(String::trim) - .filter(s -> !s.isEmpty()) + .filter(s -> !s.isEmpty() && s.length() <= 4096) .collect(Collectors.toSet()); if (terms.isEmpty()) { @@ -98,1086 +77,99 @@ class TextRedactionService { String[] listOfText, boolean useRegex, boolean wholeWordSearchBool) { - if (allFoundTextsByPage.isEmpty()) { + if (allFoundTextsByPage == null || allFoundTextsByPage.isEmpty()) { return false; } - if (detectCustomEncodingFonts(document)) { + List terms = + Arrays.stream(listOfText) + .map(String::trim) + .filter(s -> !s.isEmpty() && s.length() <= 4096) + .toList(); + + if (terms.isEmpty()) { + return false; + } + + File tempIn = null; + File tempOut = null; + try { + tempIn = File.createTempFile("jpdfium_redact_in_", ".pdf"); + tempOut = File.createTempFile("jpdfium_redact_out_", ".pdf"); + + document.save(tempIn); + + RedactOptions options = + RedactOptions.builder() + .addWords(terms) + .useRegex(useRegex) + .wholeWord(wholeWordSearchBool) + .boxColor(0) + .removeContent(true) + .normalizeFonts(false) + .fixToUnicode(false) + .repairWidths(false) + .glyphAware(true) + .build(); + + try (PdfDocument checkDoc = PdfDocument.open(tempIn.toPath())) { + if (checkDoc.pageCount() <= 0) { + return true; + } + } + + log.debug("Calling JPDFium PdfRedactor.redact (terms={})", terms); + RedactResult result = PdfRedactor.redact(tempIn.toPath(), options); + log.debug( + "JPDFium PdfRedactor.redact complete (matches={})", + result != null ? result.totalMatches() : -1); + if (result == null) { + log.warn( + "JPDFium PdfRedactor.redact returned null result, falling back to box-only redaction mode"); + return true; + } + + try { + result.save(tempOut.toPath()); + } finally { + if (result.document() != null) { + result.document().close(); + } + } + + try (PDDocument redactedDoc = Loader.loadPDF(tempOut)) { + while (document.getNumberOfPages() > 0) { + document.removePage(0); + } + PDFMergerUtility merger = new PDFMergerUtility(); + merger.appendDocument(document, redactedDoc); + } + + log.info("JPDFium text replacement complete: {} total matches", result.totalMatches()); + return false; + } catch (Exception e) { log.warn( - "Custom encoded fonts detected (non-standard encodings / DictionaryEncoding / damaged fonts). " - + "Text replacement is unreliable for these fonts. Falling back to box-only redaction mode."); - return true; - } - - try { - Set allSearchTerms = - Arrays.stream(listOfText) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toSet()); - - int pageCount = 0; - for (PDPage page : document.getPages()) { - pageCount++; - List filteredTokens = - createTokensWithoutTargetText( - document, page, allSearchTerms, useRegex, wholeWordSearchBool); - writeFilteredContentStream(document, page, filteredTokens); - } - log.info("Successfully performed text replacement redaction on {} pages.", pageCount); - return false; - } catch (Exception e) { - log.error( - "Text replacement redaction failed due to font or encoding issues. " - + "Will fall back to box-only redaction mode. Error: {}", + "JPDFium native text replacement failed, falling back to box-only redaction mode: {}", e.getMessage()); return true; - } - } - - // ----------------------------------------------------------------------- - // Content stream manipulation - // ----------------------------------------------------------------------- - - List createTokensWithoutTargetText( - PDDocument document, - PDPage page, - Set targetWords, - boolean useRegex, - boolean wholeWordSearch) - throws IOException { - - PDFStreamParser parser = new PDFStreamParser(page); - List tokens = new ArrayList<>(); - Object token; - while ((token = parser.parseNextToken()) != null) { - tokens.add(token); - } - - PDResources resources = page.getResources(); - if (resources != null) { - processPageXObjects(document, resources, targetWords, useRegex, wholeWordSearch); - } - - List textSegments = extractTextSegments(page, tokens); - String completeText = buildCompleteText(textSegments); - List matches = - findAllMatches(completeText, targetWords, useRegex, wholeWordSearch); - - return applyRedactionsToTokens(tokens, textSegments, matches); - } - - void writeFilteredContentStream(PDDocument document, PDPage page, List tokens) - throws IOException { - - PDStream newStream = new PDStream(document); - - try { - try (var out = newStream.createOutputStream()) { - ContentStreamWriter writer = new ContentStreamWriter(out); - writer.writeTokens(tokens); - } - page.setContents(newStream); - } catch (IOException e) { - throw new IOException("Failed to write filtered content stream to page", e); - } - } - - boolean isTextShowingOperator(String opName) { - return TEXT_SHOWING_OPERATORS.contains(opName); - } - - boolean detectCustomEncodingFonts(PDDocument document) { - try { - var documentCatalog = document.getDocumentCatalog(); - if (documentCatalog == null) { - return false; - } - - int totalFonts = 0; - int customEncodedFonts = 0; - int subsetFonts = 0; - int unreliableFonts = 0; - - for (PDPage page : document.getPages()) { - if (TextFinderUtils.hasProblematicFonts(page)) { - log.debug("Page contains fonts flagged as problematic by TextFinderUtils"); - } - - PDResources resources = page.getResources(); - if (resources == null) { - continue; - } - - for (COSName fontName : resources.getFontNames()) { - try { - PDFont font = resources.getFont(fontName); - if (font != null) { - totalFonts++; - - boolean isSubset = TextEncodingHelper.isFontSubset(font.getName()); - boolean hasCustomEncoding = TextEncodingHelper.hasCustomEncoding(font); - boolean isReliable = WidthCalculator.isWidthCalculationReliable(font); - boolean canCalculateWidths = - TextEncodingHelper.canCalculateBasicWidths(font); - - if (isSubset) { - subsetFonts++; - } - if (hasCustomEncoding) { - customEncodedFonts++; - log.debug("Font {} has custom encoding", font.getName()); - } - if (!isReliable || !canCalculateWidths) { - unreliableFonts++; - log.debug( - "Font {} flagged as unreliable: reliable={}, canCalculateWidths={}", - font.getName(), - isReliable, - canCalculateWidths); - } - if (!TextFinderUtils.validateFontReliability(font)) { - log.debug( - "Font {} failed comprehensive reliability check", - font.getName()); - } - } - } catch (Exception e) { - log.debug( - "Font loading/analysis failed for {}: {}", - fontName.getName(), - e.getMessage()); - customEncodedFonts++; - unreliableFonts++; - totalFonts++; - } - } - } - - log.info( - "Enhanced font analysis: {}/{} custom encoding, {}/{} subset, {}/{} unreliable fonts", - customEncodedFonts, - totalFonts, - subsetFonts, - totalFonts, - unreliableFonts, - totalFonts); - - return customEncodedFonts > 0 || unreliableFonts > 0; - - } catch (Exception e) { - log.warn("Enhanced font detection analysis failed: {}", e.getMessage()); - return true; - } - } - - // ----------------------------------------------------------------------- - // Placeholder creation - // ----------------------------------------------------------------------- - - String createPlaceholderWithFont(String originalWord, PDFont font) { - if (originalWord == null || originalWord.isEmpty()) { - return originalWord; - } - - if (font != null && TextEncodingHelper.isFontSubset(font.getName())) { - try { - float originalWidth = safeGetStringWidth(font, originalWord) / FONT_SCALE_FACTOR; - return createAlternativePlaceholder(originalWord, originalWidth, font, 1.0f); - } catch (Exception e) { - log.debug( - "Subset font placeholder creation failed for {}: {}", - font.getName(), - e.getMessage()); - return ""; - } - } - - return " ".repeat(originalWord.length()); - } - - String createPlaceholderWithWidth( - String originalWord, float targetWidth, PDFont font, float fontSize) { - if (originalWord == null || originalWord.isEmpty()) { - return originalWord; - } - - if (font == null || fontSize <= 0) { - return " ".repeat(originalWord.length()); - } - - try { - if (!WidthCalculator.isWidthCalculationReliable(font)) { - log.debug( - "Font {} unreliable for width calculation, using simple placeholder", - font.getName()); - return " ".repeat(originalWord.length()); - } - - if (TextEncodingHelper.isFontSubset(font.getName())) { - return createSubsetFontPlaceholder(originalWord, targetWidth, font, fontSize); - } - - float spaceWidth = WidthCalculator.calculateAccurateWidth(font, " ", fontSize); - - if (spaceWidth <= 0) { - return createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); - } - - int spaceCount = Math.max(1, Math.round(targetWidth / spaceWidth)); - int maxSpaces = - Math.max( - originalWord.length() * 2, Math.round(targetWidth / spaceWidth * 1.5f)); - spaceCount = Math.min(spaceCount, maxSpaces); - - return " ".repeat(spaceCount); - - } catch (Exception e) { - log.debug("Enhanced placeholder creation failed: {}", e.getMessage()); - return createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); - } - } - - private String createSubsetFontPlaceholder( - String originalWord, float targetWidth, PDFont font, float fontSize) { - try { - log.debug("Subset font {} - trying to find replacement characters", font.getName()); - String result = createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); - - if (result.isEmpty()) { - log.debug( - "Subset font {} has no suitable replacement characters, using empty string", - font.getName()); - } - - return result; - - } catch (Exception e) { - log.debug("Subset font placeholder creation failed: {}", e.getMessage()); - return ""; - } - } - - private String createAlternativePlaceholder( - String originalWord, float targetWidth, PDFont font, float fontSize) { - try { - String[] alternatives = {" ", ".", "-", "_", "~", "°", "·"}; - - if (TextEncodingHelper.fontSupportsCharacter(font, " ")) { - float spaceWidth = safeGetStringWidth(font, " ") / FONT_SCALE_FACTOR * fontSize; - if (spaceWidth > 0) { - int spaceCount = Math.max(1, Math.round(targetWidth / spaceWidth)); - int maxSpaces = originalWord.length() * 2; - spaceCount = Math.min(spaceCount, maxSpaces); - log.debug("Using spaces for font {}", font.getName()); - return " ".repeat(spaceCount); - } - } - - for (String altChar : alternatives) { - if (" ".equals(altChar)) continue; - - try { - if (!TextEncodingHelper.fontSupportsCharacter(font, altChar)) { - continue; - } - - float charWidth = - safeGetStringWidth(font, altChar) / FONT_SCALE_FACTOR * fontSize; - if (charWidth > 0) { - int charCount = Math.max(1, Math.round(targetWidth / charWidth)); - int maxChars = originalWord.length() * 2; - charCount = Math.min(charCount, maxChars); - log.debug( - "Using character '{}' for width calculation but spaces for placeholder in font {}", - altChar, - font.getName()); - return " ".repeat(charCount); - } - } catch (Exception e) { - // try next alternative - } - } - - log.debug( - "All placeholder alternatives failed for font {}, using empty string", - font.getName()); - return ""; - - } catch (Exception e) { - log.debug("Alternative placeholder creation failed: {}", e.getMessage()); - return ""; - } - } - - // ----------------------------------------------------------------------- - // Width calculation - // ----------------------------------------------------------------------- - - private float safeGetStringWidth(PDFont font, String text) { - if (font == null || text == null || text.isEmpty()) { - return 0; - } - - if (!WidthCalculator.isWidthCalculationReliable(font)) { - log.debug( - "Font {} flagged as unreliable for width calculation, using fallback", - font.getName()); - return calculateConservativeWidth(font, text); - } - - if (!TextEncodingHelper.canEncodeCharacters(font, text)) { - log.debug( - "Text cannot be encoded by font {}, using character-based fallback", - font.getName()); - return calculateCharacterBasedWidth(font, text); - } - - try { - float width = font.getStringWidth(text); - log.debug("Direct width calculation successful for '{}': {}", text, width); - return width; - - } catch (Exception e) { - log.debug( - "Direct width calculation failed for font {}: {}", - font.getName(), - e.getMessage()); - return calculateFallbackWidth(font, text); - } - } - - private float calculateCharacterBasedWidth(PDFont font, String text) { - try { - float totalWidth = 0; - for (int i = 0; i < text.length(); i++) { - String character = text.substring(i, i + 1); - try { - if (!TextEncodingHelper.fontSupportsCharacter(font, character)) { - totalWidth += font.getAverageFontWidth(); - continue; - } - - byte[] encoded = font.encode(character); - if (encoded.length > 0) { - int glyphCode = encoded[0] & 0xFF; - float glyphWidth = font.getWidth(glyphCode); - - if (glyphWidth == 0) { - try { - glyphWidth = font.getWidthFromFont(glyphCode); - } catch (Exception e2) { - glyphWidth = font.getAverageFontWidth(); - } - } - - totalWidth += glyphWidth; - } else { - totalWidth += font.getAverageFontWidth(); - } - } catch (Exception e2) { - totalWidth += font.getAverageFontWidth(); - } - } - - log.debug("Character-based width calculation: {}", totalWidth); - return totalWidth; - - } catch (Exception e) { - log.debug("Character-based width calculation failed: {}", e.getMessage()); - return calculateConservativeWidth(font, text); - } - } - - private float calculateFallbackWidth(PDFont font, String text) { - try { - if (font.getFontDescriptor() != null - && font.getFontDescriptor().getFontBoundingBox() != null) { - - org.apache.pdfbox.pdmodel.common.PDRectangle bbox = - font.getFontDescriptor().getFontBoundingBox(); - float avgCharWidth = bbox.getWidth() * 0.6f; - float fallbackWidth = text.length() * avgCharWidth; - - log.debug("Bounding box fallback width: {}", fallbackWidth); - return fallbackWidth; - } - - try { - float avgWidth = font.getAverageFontWidth(); - if (avgWidth > 0) { - float fallbackWidth = text.length() * avgWidth; - log.debug("Average width fallback: {}", fallbackWidth); - return fallbackWidth; - } - } catch (Exception e2) { - log.debug("Average font width calculation failed: {}", e2.getMessage()); - } - - return calculateConservativeWidth(font, text); - - } catch (Exception e) { - log.debug("Fallback width calculation failed: {}", e.getMessage()); - return calculateConservativeWidth(font, text); - } - } - - private float calculateConservativeWidth(PDFont font, String text) { - float conservativeWidth = text.length() * 500f; - log.debug( - "Conservative width estimate for font {} text '{}': {}", - font.getName(), - text, - conservativeWidth); - return conservativeWidth; - } - - private float calculateWidthAdjustment(TextSegment segment, List matches) { - try { - if (segment.getFont() == null || segment.getFontSize() <= 0) { - return 0; - } - - String fontName = segment.getFont().getName(); - if (fontName != null - && (fontName.contains("HOEPAP") || TextEncodingHelper.isFontSubset(fontName))) { - log.debug("Skipping width adjustment for problematic/subset font: {}", fontName); - return 0; - } - - float totalOriginal = 0; - float totalPlaceholder = 0; - String text = segment.getText(); - - for (MatchRange match : matches) { - int segStart = Math.max(0, match.getStartPos() - segment.getStartPos()); - int segEnd = Math.min(text.length(), match.getEndPos() - segment.getStartPos()); - - if (segStart < text.length() && segEnd > segStart) { - String originalPart = text.substring(segStart, segEnd); - - float originalWidth = - safeGetStringWidth(segment.getFont(), originalPart) - / FONT_SCALE_FACTOR - * segment.getFontSize(); - - String placeholderPart = - createPlaceholderWithWidth( - originalPart, - originalWidth, - segment.getFont(), - segment.getFontSize()); - - float origUnits = safeGetStringWidth(segment.getFont(), originalPart); - float placeUnits = safeGetStringWidth(segment.getFont(), placeholderPart); - - float orig = (origUnits / FONT_SCALE_FACTOR) * segment.getFontSize(); - float place = (placeUnits / FONT_SCALE_FACTOR) * segment.getFontSize(); - - totalOriginal += orig; - totalPlaceholder += place; - } - } - - float adjustment = totalOriginal - totalPlaceholder; - - float maxReasonableAdjustment = - Math.max( - segment.getText().length() * segment.getFontSize() * 2, - totalOriginal * 1.5f); - - if (Math.abs(adjustment) > maxReasonableAdjustment) { - log.debug( - "Width adjustment {} seems unreasonable for text length {}, capping to 0", - adjustment, - segment.getText().length()); - return 0; - } - - return adjustment; - } catch (Exception ex) { - log.debug("Width adjustment failed: {}", ex.getMessage()); - return 0; - } - } - - // ----------------------------------------------------------------------- - // Token and segment operations - // ----------------------------------------------------------------------- - - private void processPageXObjects( - PDDocument document, - PDResources resources, - Set targetWords, - boolean useRegex, - boolean wholeWordSearch) { - processPageXObjects( - document, resources, targetWords, useRegex, wholeWordSearch, 0, new HashSet<>()); - } - - private void processPageXObjects( - PDDocument document, - PDResources resources, - Set targetWords, - boolean useRegex, - boolean wholeWordSearch, - int depth, - Set visited) { - - if (depth > MAX_XOBJECT_DEPTH) { - log.warn("[redact] XObject nesting depth {} exceeded limit, stopping traversal", depth); - return; - } - - for (COSName xobjName : resources.getXObjectNames()) { - try { - PDXObject xobj = resources.getXObject(xobjName); - if (xobj instanceof PDFormXObject formXObj) { - if (!visited.add(formXObj.getCOSObject())) { - log.debug( - "[redact] Cycle detected in XObject graph, skipping {}", - xobjName.getName()); - continue; - } - processFormXObject( - document, - formXObj, - targetWords, - useRegex, - wholeWordSearch, - depth + 1, - visited); - log.debug("Processed Form XObject: {}", xobjName.getName()); - } - } catch (Exception e) { - log.warn("Failed to process XObject {}: {}", xobjName.getName(), e.getMessage()); - } - } - } - - private void processFormXObject( - PDDocument document, - PDFormXObject formXObject, - Set targetWords, - boolean useRegex, - boolean wholeWordSearch, - int depth, - Set visited) { - - try { - PDResources xobjResources = formXObject.getResources(); - if (xobjResources == null) { - return; - } - - processPageXObjects( - document, - xobjResources, - targetWords, - useRegex, - wholeWordSearch, - depth, - visited); - - PDFStreamParser parser = new PDFStreamParser(formXObject); - List tokens = new ArrayList<>(); - Object token; - while ((token = parser.parseNextToken()) != null) { - tokens.add(token); - } - - List textSegments = extractTextSegmentsFromXObject(xobjResources, tokens); - String completeText = buildCompleteText(textSegments); - List matches = - findAllMatches(completeText, targetWords, useRegex, wholeWordSearch); - - if (!matches.isEmpty()) { - List redactedTokens = - applyRedactionsToTokens(tokens, textSegments, matches); - writeRedactedContentToXObject(document, formXObject, redactedTokens); - log.debug("Processed {} redactions in Form XObject", matches.size()); - } - - } catch (Exception e) { - log.warn("Failed to process Form XObject: {}", e.getMessage()); - } - } - - private void writeRedactedContentToXObject( - PDDocument document, PDFormXObject formXObject, List redactedTokens) - throws IOException { - - PDStream newStream = new PDStream(document); - - try (var out = newStream.createOutputStream()) { - ContentStreamWriter writer = new ContentStreamWriter(out); - writer.writeTokens(redactedTokens); - } - - formXObject.getCOSObject().removeItem(COSName.CONTENTS); - formXObject.getCOSObject().setItem(COSName.CONTENTS, newStream.getCOSObject()); - } - - private List extractTextSegments(PDPage page, List tokens) { - List segments = new ArrayList<>(); - int currentTextPos = 0; - GraphicsState graphicsState = new GraphicsState(); - PDResources resources = page.getResources(); - - for (int i = 0; i < tokens.size(); i++) { - Object currentToken = tokens.get(i); - - if (currentToken instanceof Operator op) { - String opName = op.getName(); - - if ("Tf".equals(opName) && i >= 2) { - try { - COSName fontName = (COSName) tokens.get(i - 2); - COSBase fontSizeBase = (COSBase) tokens.get(i - 1); - if (fontSizeBase instanceof COSNumber cosNumber) { - graphicsState.setFont(resources.getFont(fontName)); - graphicsState.setFontSize(cosNumber.floatValue()); - } - } catch (ClassCastException | IOException e) { - log.debug( - "Failed to extract font and font size from Tf operator: {}", - e.getMessage()); - } - } - - currentTextPos = - getCurrentTextPos( - tokens, segments, currentTextPos, graphicsState, i, opName); - } - } - - return segments; - } - - private List extractTextSegmentsFromXObject( - PDResources resources, List tokens) { - List segments = new ArrayList<>(); - int currentTextPos = 0; - GraphicsState graphicsState = new GraphicsState(); - - for (int i = 0; i < tokens.size(); i++) { - Object currentToken = tokens.get(i); - - if (currentToken instanceof Operator op) { - String opName = op.getName(); - - if ("Tf".equals(opName) && i >= 2) { - try { - COSName fontName = (COSName) tokens.get(i - 2); - COSBase fontSizeBase = (COSBase) tokens.get(i - 1); - if (fontSizeBase instanceof COSNumber cosNumber) { - graphicsState.setFont(resources.getFont(fontName)); - graphicsState.setFontSize(cosNumber.floatValue()); - } - } catch (ClassCastException | IOException e) { - log.debug("Font extraction failed in XObject: {}", e.getMessage()); - } - } - - currentTextPos = - getCurrentTextPos( - tokens, segments, currentTextPos, graphicsState, i, opName); - } - } - - return segments; - } - - private int getCurrentTextPos( - List tokens, - List segments, - int currentTextPos, - GraphicsState graphicsState, - int i, - String opName) { - if (isTextShowingOperator(opName) && i > 0) { - String textContent = extractTextFromToken(tokens.get(i - 1), opName); - if (!textContent.isEmpty()) { - segments.add( - new TextSegment( - i - 1, - opName, - textContent, - currentTextPos, - currentTextPos + textContent.length(), - graphicsState.font, - graphicsState.fontSize)); - currentTextPos += textContent.length(); - } - } - return currentTextPos; - } - - private String buildCompleteText(List segments) { - StringBuilder sb = new StringBuilder(); - for (TextSegment segment : segments) { - sb.append(segment.text); - } - return sb.toString(); - } - - private List findAllMatches( - String completeText, - Set targetWords, - boolean useRegex, - boolean wholeWordSearch) { - - List patterns = - TextFinderUtils.createOptimizedSearchPatterns( - targetWords, useRegex, wholeWordSearch); - - return patterns.stream() - .flatMap( - pattern -> { - try { - return pattern.matcher(completeText).results(); - } catch (Exception e) { - log.debug( - "Pattern matching failed for pattern {}: {}", - pattern.pattern(), - e.getMessage()); - return java.util.stream.Stream.empty(); - } - }) - .map(matchResult -> new MatchRange(matchResult.start(), matchResult.end())) - .sorted(Comparator.comparingInt(MatchRange::getStartPos)) - .collect(Collectors.toList()); - } - - private List applyRedactionsToTokens( - List tokens, List textSegments, List matches) { - - long startTime = System.currentTimeMillis(); - - try { - List newTokens = new ArrayList<>(tokens); - - Map> matchesBySegment = new HashMap<>(); - for (MatchRange match : matches) { - for (int i = 0; i < textSegments.size(); i++) { - TextSegment segment = textSegments.get(i); - int overlapStart = Math.max(match.startPos, segment.startPos); - int overlapEnd = Math.min(match.endPos, segment.endPos); - if (overlapStart < overlapEnd) { - matchesBySegment.computeIfAbsent(i, k -> new ArrayList<>()).add(match); - } - } - } - - List tasks = new ArrayList<>(); - for (Map.Entry> entry : matchesBySegment.entrySet()) { - int segmentIndex = entry.getKey(); - List segmentMatches = entry.getValue(); - TextSegment segment = textSegments.get(segmentIndex); - - if ("Tj".equals(segment.operatorName) || "'".equals(segment.operatorName)) { - String newText = applyRedactionsToSegmentText(segment, segmentMatches); - try { - float adjustment = calculateWidthAdjustment(segment, segmentMatches); - tasks.add(new ModificationTask(segment, newText, adjustment)); - } catch (Exception e) { - log.debug( - "Width adjustment calculation failed for segment: {}", - e.getMessage()); - } - } else if ("TJ".equals(segment.operatorName)) { - tasks.add(new ModificationTask(segment, null, 0)); - } - } - - tasks.sort((a, b) -> Integer.compare(b.segment.tokenIndex, a.segment.tokenIndex)); - - for (ModificationTask task : tasks) { - List segmentMatches = - matchesBySegment.getOrDefault( - textSegments.indexOf(task.segment), - java.util.Collections.emptyList()); - modifyTokenForRedaction( - newTokens, task.segment, task.newText, task.adjustment, segmentMatches); - } - - return newTokens; - } finally { - long processingTime = System.currentTimeMillis() - startTime; - log.debug( - "Token redaction processing completed in {} ms for {} matches", - processingTime, - matches.size()); - } - } - - private String applyRedactionsToSegmentText(TextSegment segment, List matches) { - String text = segment.getText(); - - if (segment.getFont() != null - && !TextEncodingHelper.isTextSegmentRemovable(segment.getFont(), text)) { - log.debug( - "Skipping text segment '{}' - font {} cannot process this text reliably", - text, - segment.getFont().getName()); - return text; - } - - StringBuilder result = new StringBuilder(text); - - for (MatchRange match : matches) { - int segmentStart = Math.max(0, match.getStartPos() - segment.getStartPos()); - int segmentEnd = Math.min(text.length(), match.getEndPos() - segment.getStartPos()); - - if (segmentStart < text.length() && segmentEnd > segmentStart) { - String originalPart = text.substring(segmentStart, segmentEnd); - - if (segment.getFont() != null - && !TextEncodingHelper.isTextSegmentRemovable( - segment.getFont(), originalPart)) { - log.debug( - "Skipping text part '{}' within segment - cannot be processed reliably", - originalPart); - continue; + if (tempIn != null && tempIn.exists()) { + try { + Files.delete(tempIn.toPath()); + } catch (IOException _) { + log.warn("Failed to delete temporary file: {}", tempIn.getAbsolutePath()); } - - float originalWidth = 0; - if (segment.getFont() != null && segment.getFontSize() > 0) { - try { - originalWidth = - safeGetStringWidth(segment.getFont(), originalPart) - / FONT_SCALE_FACTOR - * segment.getFontSize(); - } catch (Exception e) { - log.debug( - "Failed to calculate original width for placeholder: {}", - e.getMessage()); - } - } - - String placeholder = - (originalWidth > 0) - ? createPlaceholderWithWidth( - originalPart, - originalWidth, - segment.getFont(), - segment.getFontSize()) - : createPlaceholderWithFont(originalPart, segment.getFont()); - - result.replace(segmentStart, segmentEnd, placeholder); - } - } - - return result.toString(); - } - - private void modifyTokenForRedaction( - List tokens, - TextSegment segment, - String newText, - float adjustment, - List matches) { - - if (segment.getTokenIndex() < 0 || segment.getTokenIndex() >= tokens.size()) { - return; - } - - Object token = tokens.get(segment.getTokenIndex()); - String operatorName = segment.getOperatorName(); - - try { - if (("Tj".equals(operatorName) || "'".equals(operatorName)) - && token instanceof COSString) { - - if (Math.abs(adjustment) < PRECISION_THRESHOLD) { - if (newText.isEmpty()) { - tokens.set(segment.getTokenIndex(), EMPTY_COS_STRING); - } else { - tokens.set(segment.getTokenIndex(), new COSString(newText)); - } - } else { - COSArray newArray = new COSArray(); - newArray.add(new COSString(newText)); - if (segment.getFontSize() > 0) { - float kerning = (-adjustment / segment.getFontSize()) * FONT_SCALE_FACTOR; - newArray.add(new COSFloat(kerning)); - } - tokens.set(segment.getTokenIndex(), newArray); - - int operatorIndex = segment.getTokenIndex() + 1; - if (operatorIndex < tokens.size() - && tokens.get(operatorIndex) instanceof Operator op - && op.getName().equals(operatorName)) { - tokens.set(operatorIndex, Operator.getOperator("TJ")); - } - } - } else if ("TJ".equals(operatorName) && token instanceof COSArray) { - COSArray newArray = createRedactedTJArray((COSArray) token, segment, matches); - tokens.set(segment.getTokenIndex(), newArray); } - } catch (Exception e) { - log.debug( - "Token modification failed for segment at index {}: {}", - segment.getTokenIndex(), - e.getMessage()); - } - } - - private COSArray createRedactedTJArray( - COSArray originalArray, TextSegment segment, List matches) { - try { - COSArray newArray = new COSArray(); - int textOffsetInSegment = 0; - - for (COSBase element : originalArray) { - if (element instanceof COSString cosString) { - String originalText = cosString.getString(); - - if (segment.getFont() != null - && !TextEncodingHelper.isTextSegmentRemovable( - segment.getFont(), originalText)) { - log.debug( - "Skipping TJ text part '{}' - cannot be processed reliably with font {}", - originalText, - segment.getFont().getName()); - newArray.add(element); - textOffsetInSegment += originalText.length(); - continue; - } - - StringBuilder newText = new StringBuilder(originalText); - boolean modified = false; - - for (MatchRange match : matches) { - int stringStartInPage = segment.getStartPos() + textOffsetInSegment; - int stringEndInPage = stringStartInPage + originalText.length(); - - int overlapStart = Math.max(match.getStartPos(), stringStartInPage); - int overlapEnd = Math.min(match.getEndPos(), stringEndInPage); - - if (overlapStart < overlapEnd) { - int redactionStartInString = overlapStart - stringStartInPage; - int redactionEndInString = overlapEnd - stringStartInPage; - if (redactionStartInString >= 0 - && redactionEndInString <= originalText.length()) { - String originalPart = - originalText.substring( - redactionStartInString, redactionEndInString); - - if (segment.getFont() != null - && !TextEncodingHelper.isTextSegmentRemovable( - segment.getFont(), originalPart)) { - log.debug( - "Skipping TJ text part '{}' - cannot be redacted reliably", - originalPart); - continue; - } - - modified = true; - float originalWidth = 0; - if (segment.getFont() != null && segment.getFontSize() > 0) { - try { - originalWidth = - safeGetStringWidth(segment.getFont(), originalPart) - / FONT_SCALE_FACTOR - * segment.getFontSize(); - } catch (Exception e) { - log.debug( - "Failed to calculate original width for TJ placeholder: {}", - e.getMessage()); - } - } - - String placeholder = - (originalWidth > 0) - ? createPlaceholderWithWidth( - originalPart, - originalWidth, - segment.getFont(), - segment.getFontSize()) - : createPlaceholderWithFont( - originalPart, segment.getFont()); - - newText.replace( - redactionStartInString, redactionEndInString, placeholder); - } - } - } - - String modifiedString = newText.toString(); - newArray.add(new COSString(modifiedString)); - - if (modified && segment.getFont() != null && segment.getFontSize() > 0) { - try { - float originalWidth = - safeGetStringWidth(segment.getFont(), originalText) - / FONT_SCALE_FACTOR - * segment.getFontSize(); - float modifiedWidth = - safeGetStringWidth(segment.getFont(), modifiedString) - / FONT_SCALE_FACTOR - * segment.getFontSize(); - float adjustment = originalWidth - modifiedWidth; - if (Math.abs(adjustment) > PRECISION_THRESHOLD) { - float kerning = - (-adjustment / segment.getFontSize()) - * FONT_SCALE_FACTOR - * 1.10f; - newArray.add(new COSFloat(kerning)); - } - } catch (Exception e) { - log.debug( - "Width adjustment calculation failed for segment: {}", - e.getMessage()); - } - } - - textOffsetInSegment += originalText.length(); - } else { - newArray.add(element); + if (tempOut != null && tempOut.exists()) { + try { + Files.delete(tempOut.toPath()); + } catch (IOException _) { + log.warn("Failed to delete temporary file: {}", tempOut.getAbsolutePath()); } } - return newArray; - } catch (Exception e) { - return originalArray; } } - private String extractTextFromToken(Object token, String operatorName) { - return switch (operatorName) { - case "Tj", "'" -> { - if (token instanceof COSString cosString) { - yield cosString.getString(); - } - yield ""; - } - case "TJ" -> { - if (token instanceof COSArray cosArray) { - StringBuilder sb = new StringBuilder(); - for (COSBase element : cosArray) { - if (element instanceof COSString cosString) { - sb.append(cosString.getString()); - } - } - yield sb.toString(); - } - yield ""; - } - default -> ""; - }; - } - - // ----------------------------------------------------------------------- - // Inner data classes - // ----------------------------------------------------------------------- - - @Data - private static class GraphicsState { - private PDFont font = null; - private float fontSize = 0; - } - @Data @AllArgsConstructor static class TextSegment { @@ -1196,12 +188,4 @@ class TextRedactionService { private int startPos; private int endPos; } - - @Data - @AllArgsConstructor - private static class ModificationTask { - private TextSegment segment; - private String newText; - private float adjustment; - } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java index 9110131f4f..2329ca2484 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java @@ -45,6 +45,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -84,13 +86,13 @@ public class TimestampController { value = "/timestamp-pdf", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add RFC 3161 document timestamp to a PDF", description = "Contacts a trusted Time Stamp Authority (TSA) server and embeds an RFC 3161" - + " document timestamp into the PDF. Only a SHA-256 hash of the" - + " document is sent to the TSA — the PDF itself never leaves the" - + " server. Input:PDF Output:PDF Type:SISO") + + " document timestamp into the PDF. Only a SHA-256 hash of the document is sent" + + " to the TSA - the PDF itself never leaves the server.") public ResponseEntity timestampPdf(@ModelAttribute TimestampPdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java index 47fce44876..c807a68bc8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java @@ -46,6 +46,8 @@ import stirling.software.SPDF.service.CertificateValidationService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; @@ -73,12 +75,12 @@ public class ValidateSignatureController { } @JsonDataResponse + @ToolIO(produces = ToolFormat.JSON) @Operation( summary = "Validate PDF Digital Signature", description = - "Validates the digital signatures in a PDF file using PKIX path building" - + " and time-of-signing semantics. Supports custom trust anchors." - + " Input:PDF Output:JSON Type:SISO") + "Validates the digital signatures in a PDF file using PKIX path building and" + + " time-of-signing semantics. Supports custom trust anchors.") @AutoJobPostMapping( value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -140,11 +142,15 @@ public class ValidateSignatureController { // covers the TSTInfo, which can never match. boolean isDocTimeStamp = SUBFILTER_RFC3161.equals(sig.getSubFilter()); CMSSignedData signedData; + // Parse from a stream: /Contents is zero-padded to its reserved length and the + // byte[] constructors reject those trailing bytes since BC 1.85. if (isDocTimeStamp) { - signedData = new CMSSignedData(signatureBytes); + signedData = new CMSSignedData(new ByteArrayInputStream(signatureBytes)); } else { CMSProcessable content = new CMSProcessableByteArray(signedContent); - signedData = new CMSSignedData(content, signatureBytes); + signedData = + new CMSSignedData( + content, new ByteArrayInputStream(signatureBytes)); } // What actually binds a timestamp to this document: the TSTInfo's message diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java index 505eeb2c62..4226005c8e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/VerifyPDFController.java @@ -22,6 +22,8 @@ import stirling.software.SPDF.service.VeraPDFService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.util.ExceptionUtils; @SecurityApi @@ -31,13 +33,13 @@ public class VerifyPDFController { private final VeraPDFService veraPDFService; + @ToolIO(produces = ToolFormat.JSON) @Operation( summary = "Verify PDF Standards Compliance", description = - "Validates PDF files against the standards declared in their metadata. " - + "Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards " - + "from the document's XMP metadata and validates compliance. " - + "Input:PDF Output:JSON Type:SISO") + "Validates PDF files against the standards declared in their metadata." + + " Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards from the" + + " document's XMP metadata and validates compliance.") @AutoJobPostMapping( value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 38364e541e..0bcc3a6439 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -43,6 +43,8 @@ import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; @@ -74,12 +76,13 @@ public class WatermarkController { value = "/add-watermark", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @StandardPdfResponse + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the" - + " watermark type (text or image), rotation, opacity, width spacer, and" - + " height spacer. Input:PDF Output:PDF Type:SISO") + + " watermark type (text or image), rotation, opacity, width spacer, and height" + + " spacer.") public ResponseEntity addWatermark(@Valid @ModelAttribute AddWatermarkRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index de28d66ca9..4bbf3dfb82 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -251,7 +251,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { @@ -292,7 +292,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { @@ -332,7 +332,7 @@ public class MetricsController { // For GET requests, validate if we have a list of valid endpoints final boolean validateGetEndpoints = - endpointInspector.getValidGetEndpoints().size() != 0; + !endpointInspector.getValidGetEndpoints().isEmpty(); if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 1e05ec17b2..89d4bb9956 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -5,17 +5,26 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.Set; import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.JavaScriptUtils; @@ -32,6 +41,33 @@ public class ReactRoutingController { private static final Pattern BASE_HREF_PATTERN = Pattern.compile(""); + // First path segments owned by the backend or static assets, never SPA routes. + // Mirrors the exclusion regexes on forwardRootPaths/forwardNestedPaths below. + private static final Set NON_SPA_FIRST_SEGMENTS = + Set.of( + "api", + "static", + "pipeline", + "pdfjs", + "pdfjs-legacy", + "pdfium", + "vendor", + "fonts", + "images", + "css", + "js", + "assets", + "locales", + "modern-logo", + "classic-logo", + "Login", + "og_images", + "samples"); + + // After the annotated controllers (order 0), before the resource chain + // (LOWEST_PRECEDENCE - 1). + private static final int SPA_FALLBACK_ORDER = Ordered.LOWEST_PRECEDENCE - 2; + @Value("${server.servlet.context-path:/}") private String contextPath; @@ -44,6 +80,8 @@ public class ReactRoutingController { private boolean saasLandingExists = false; private String cachedMobileUploadHtml; private boolean mobileUploadHtmlExists = false; + private String cachedMobileSignHtml; + private boolean mobileSignHtmlExists = false; @PostConstruct public void init() { @@ -67,10 +105,12 @@ public class ReactRoutingController { } // Desktop (Tauri) serves the SPA from its bundled webview, so a phone scanning the QR can't - // load the React /mobile-scanner route from the local backend. Cache the self-contained - // static upload page to serve at that route in desktop mode instead. + // load the React /mobile-scanner or /mobile-sign routes from the local backend. Cache the + // self-contained static pages to serve at those routes in desktop mode instead. this.cachedMobileUploadHtml = readStaticHtml("mobile-upload.html"); this.mobileUploadHtmlExists = this.cachedMobileUploadHtml != null; + this.cachedMobileSignHtml = readStaticHtml("mobile-sign.html"); + this.mobileSignHtmlExists = this.cachedMobileSignHtml != null; // Check for external index.html first (customFiles/static/) Path externalIndexPath = Path.of(InstallationPathConfig.getStaticPath(), "index.html"); @@ -232,6 +272,17 @@ public class ReactRoutingController { return serveIndexHtml(request); } + @GetMapping(value = "/mobile-sign", produces = MediaType.TEXT_HTML_VALUE) + public ResponseEntity serveMobileSign(HttpServletRequest request) { + if (isDesktopMode() && mobileSignHtmlExists) { + return ResponseEntity.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(cachedMobileSignHtml); + } + return serveIndexHtml(request); + } + @GetMapping(value = "/auth/callback/tauri", produces = MediaType.TEXT_HTML_VALUE) public ResponseEntity serveTauriAuthCallback(HttpServletRequest request) { // cachedCallbackHtml is always initialized in @PostConstruct @@ -256,6 +307,59 @@ public class ReactRoutingController { return serveIndexHtml(request); } + // The regex mappings above only cover 1- and 2-segment paths (Spring path variables cannot + // span '/'), so deep SPA links like /processor/pipelines/new 404d on direct navigation. + // + // Registered as its own mapping rather than exposed as a bare RouterFunction @Bean: + // Spring's own RouterFunctionMapping is ordered -1, ahead of the annotated controllers at + // order 0, so a plain bean would shadow every dot-free backend route the denylist below + // does not name (/v1/api-docs, /error, /actuator, ...). LOWEST_PRECEDENCE - 2 puts it after + // the controllers and before the resource chain (LOWEST_PRECEDENCE - 1), which is the only + // position where a catch-all fallback is safe. + @Bean + public RouterFunctionMapping spaDeepLinkFallbackMapping() { + RouterFunction fallback = + RouterFunctions.route( + request -> { + HttpServletRequest servletRequest = request.servletRequest(); + return "GET".equals(servletRequest.getMethod()) + && isSpaFallbackRoute( + stripContextPath( + servletRequest.getContextPath(), + servletRequest.getRequestURI())); + }, + request -> + ServerResponse.ok() + .cacheControl(CacheControl.noCache().mustRevalidate()) + .contentType(MediaType.TEXT_HTML) + .body(serveIndexHtml(request.servletRequest()).getBody())); + RouterFunctionMapping mapping = new RouterFunctionMapping(fallback); + mapping.setOrder(SPA_FALLBACK_ORDER); + mapping.setMessageConverters( + List.of(new StringHttpMessageConverter(StandardCharsets.UTF_8))); + return mapping; + } + + // Dot-free paths only, so requests for real files still fall through to the resource + // handlers. This is a denylist, so it is only safe because the mapping above runs after + // the annotated controllers - see spaDeepLinkFallbackMapping. + static boolean isSpaFallbackRoute(String path) { + if (path == null || path.isEmpty() || "/".equals(path) || path.indexOf('.') >= 0) { + return false; + } + String[] segments = (path.startsWith("/") ? path.substring(1) : path).split("/"); + return segments.length > 0 + && !segments[0].isEmpty() + && !NON_SPA_FIRST_SEGMENTS.contains(segments[0]); + } + + private static String stripContextPath(String contextPath, String uri) { + if (contextPath != null && !contextPath.isBlank() && uri.startsWith(contextPath)) { + return uri.substring(contextPath.length()); + } + return uri; + } + private String buildFallbackHtml() { String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/"; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java index a15f14af98..7065ac4d79 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.web; import java.util.Locale; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import lombok.extern.slf4j.Slf4j; @@ -13,7 +12,11 @@ import stirling.software.common.model.ApplicationProperties; @Slf4j public class UploadLimitService { - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public UploadLimitService(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } public long getUploadLimit() { String raw = diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java index 42ebd51ab3..45f093148b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java @@ -17,6 +17,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.enumeration.ResourceWeight; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.pdf.PdfMarkdownConverter; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @@ -34,10 +36,10 @@ public class ConvertPDFToMarkdown { value = "/pdf/markdown", resourceWeight = ResourceWeight.MEDIUM_WEIGHT) @MarkdownConversionResponse + @ToolIO(produces = ToolFormat.MARKDOWN) @Operation( summary = "Convert PDF to Markdown", - description = - "This endpoint converts a PDF file to Markdown format. Input:PDF Output:Markdown Type:SISO") + description = "This endpoint converts a PDF file to Markdown format.") public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java index bb0520a4ba..921663912b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPdfARequest.java @@ -14,9 +14,19 @@ public class PdfToPdfARequest extends PDFFile { @Schema( description = "The output format type (PDF/A or PDF/X)", requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = {"pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfx"}) + allowableValues = { + "pdfa", "pdfa-1", "pdfa-2", "pdfa-2b", "pdfa-3", "pdfa-3b", "pdfa-1a", "pdfa-2a", + "pdfa-3a", "pdfx" + }) private String outputFormat; + @Schema( + description = + "Also declare PDF/UA accessibility alongside PDF/A. Only applies to the level A" + + " formats, and the claim is written only if it validates.", + defaultValue = "false") + private Boolean pdfUa; + @Schema( description = "If true, the conversion will fail if the output is not perfectly compliant") diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotateAnalysisResult.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotateAnalysisResult.java new file mode 100644 index 0000000000..375e4eb646 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotateAnalysisResult.java @@ -0,0 +1,73 @@ +package stirling.software.SPDF.model.api.misc; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Per-page orientation report returned by auto-rotate-pdf when dryRun is set. */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AutoRotateAnalysisResult { + + private List pages; + + private int totalPages; + + @Schema(description = "Number of pages a correction would be applied to") + private int pagesToRotate; + + private int detectedByText; + + private int detectedByOsd; + + @Schema(description = "Pages whose correction was inherited from the document consensus") + private int inferred; + + private int undetected; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PageResult { + + @Schema(description = "1-based page number") + private int pageNumber; + + @Schema(description = "The page's current /Rotate value, normalised to 0-270") + private int currentRotation; + + @Schema( + description = + "Detected additional clockwise rotation that would make the page upright." + + " Reported for diagnostics even when it is not used (for example" + + " an OSD verdict below the confidence threshold); 'apply' is the" + + " authority on whether it is actually applied") + private int correction; + + @Schema( + description = + "Detection confidence: percentage of glyphs sharing the dominant direction" + + " for method 'text', Tesseract orientation confidence for method" + + " 'osd', absent when nothing was detected") + private Double confidence; + + @Schema( + description = "How the orientation was determined", + allowableValues = {"text", "osd", "inferred", "none"}) + private String method; + + @Schema(description = "Whether the correction will be (or was) applied") + private boolean apply; + + @Schema(description = "Machine-readable reason when no correction is applied") + private String note; + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotatePdfRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotatePdfRequest.java new file mode 100644 index 0000000000..9994b033e6 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AutoRotatePdfRequest.java @@ -0,0 +1,58 @@ +package stirling.software.SPDF.model.api.misc; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.common.model.api.PDFFile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AutoRotatePdfRequest extends PDFFile { + + @Schema( + description = + "Detection method. 'auto' tries embedded-text direction first and falls back" + + " to Tesseract OSD for pages without usable text; 'text' uses only" + + " embedded-text direction; 'osd' forces Tesseract OSD for every page", + allowableValues = {"auto", "text", "osd"}, + defaultValue = "auto") + private String detectionMode = "auto"; + + @Schema( + description = + "Minimum Tesseract OSD orientation confidence required before a correction is" + + " applied. Matches OCRmyPDF's --rotate-pages-threshold scale", + minimum = "0", + defaultValue = "14.0") + @Min(value = 0, message = "Confidence threshold must be non-negative") + private Double confidenceThreshold = 14.0; + + @Schema( + description = + "If true, no rotation is applied; returns a JSON report of the per-page" + + " detection results instead of a PDF") + private boolean dryRun; + + @Schema( + description = + "When a page cannot be decided on its own but the pages that could be decided" + + " agree on a single correction for that same current rotation, apply" + + " that shared correction to the undecided page. Handles documents" + + " rotated uniformly where some pages are too sparse to detect alone", + defaultValue = "true") + private boolean inferUndetected = true; + + @Schema( + description = + "Optional pre-computed corrections to apply without running detection. Pages" + + " not listed are left unchanged, and a page may only appear once") + @Valid + private List pageRotations; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/PageRotation.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/PageRotation.java new file mode 100644 index 0000000000..dbdbee7de7 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/PageRotation.java @@ -0,0 +1,28 @@ +package stirling.software.SPDF.model.api.misc; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** A pre-computed rotation for one page, used by auto-rotate-pdf's apply-only path. */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PageRotation { + + @Schema( + description = "1-based page number to rotate", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "1") + private Integer pageNumber; + + @Schema( + description = + "Additional clockwise rotation to add to the page's current rotation, in" + + " degrees. Must be a multiple of 90", + requiredMode = Schema.RequiredMode.REQUIRED, + example = "90") + private Integer rotation; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java index 279a41a27f..791b7626c4 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java @@ -45,5 +45,5 @@ public class RedactPdfRequest extends PDFFile { description = "Convert the redacted PDF to an image", defaultValue = "false", requiredMode = Schema.RequiredMode.REQUIRED) - private Boolean convertPDFToImage; + private Boolean convertPDFToImage = Boolean.FALSE; } diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/CMSProcessableInputStream.java similarity index 97% rename from app/core/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java rename to app/core/src/main/java/stirling/software/SPDF/pdf/signature/CMSProcessableInputStream.java index 48d1da9800..ead6a26305 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CMSProcessableInputStream.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/CMSProcessableInputStream.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import java.io.IOException; import java.io.InputStream; diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.java similarity index 99% rename from app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java rename to app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.java index d6fd18f060..77145cb336 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import java.io.IOException; import java.io.InputStream; diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/TSAClient.java similarity index 99% rename from app/core/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java rename to app/core/src/main/java/stirling/software/SPDF/pdf/signature/TSAClient.java index 6f57e205a2..33530e4885 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/TSAClient.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import java.io.IOException; import java.io.InputStream; diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/ValidationTimeStamp.java similarity index 99% rename from app/core/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java rename to app/core/src/main/java/stirling/software/SPDF/pdf/signature/ValidationTimeStamp.java index d01666d769..405678181c 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/signature/ValidationTimeStamp.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/signature/ValidationTimeStamp.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import java.io.ByteArrayInputStream; import java.io.IOException; diff --git a/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java b/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java index 3a8d418aa6..35f194f5d8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java @@ -1,12 +1,7 @@ package stirling.software.SPDF.service; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; @@ -24,28 +19,26 @@ import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.common.model.enumeration.Role; import stirling.software.common.service.UserServiceInterface; -import stirling.software.common.util.RegexPatternUtils; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; +/** + * Validates a request's parameters against the OpenAPI spec. + * + *

Input and output types are no longer read from here: they are declared with {@code @ToolIO} + * and served by {@code ToolIORegistry}, which reads the annotations directly rather than parsing + * them back out of the description prose. + */ @Service @Slf4j -public class ApiDocService implements stirling.software.common.service.ToolMetadataService { - - // Matches a bare "Output:ZIP" declaration (i.e. ZIP is not followed by "-" or "/"). - // Bare ZIP means the archive itself is the deliverable (e.g. get-attachments), so it - // should not be auto-unpacked. Wrapper forms like Output:ZIP-PDF or Output:IMAGE/ZIP - // use ZIP as transport for multiple typed results and are safe to unpack. - private static final Pattern BARE_ZIP_OUTPUT = - Pattern.compile("Output\\s*:\\s*ZIP(?![-/])", Pattern.CASE_INSENSITIVE); +public class ApiDocService { private final Map apiDocumentation = new HashMap<>(); private final ServletContext servletContext; private final UserServiceInterface userService; private final ObjectMapper objectMapper; - Map> outputToFileTypes = new HashMap<>(); JsonNode apiDocsJsonRootNode; public ApiDocService( @@ -63,52 +56,6 @@ public class ApiDocService implements stirling.software.common.service.ToolMetad return "http://localhost:" + port + contextPath + "/v1/api-docs"; } - @Override - public List getExtensionTypes(boolean output, String operationName) { - if (outputToFileTypes.isEmpty()) { - outputToFileTypes.put("PDF", List.of("pdf")); - outputToFileTypes.put( - "IMAGE", - Arrays.asList( - "png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "psd", - "ai", "eps")); - outputToFileTypes.put( - "ZIP", - Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "lzma", "z")); - outputToFileTypes.put("WORD", Arrays.asList("doc", "docx", "odt", "rtf")); - outputToFileTypes.put("CSV", List.of("csv")); - outputToFileTypes.put("JS", Arrays.asList("js", "jsx")); - outputToFileTypes.put("HTML", Arrays.asList("html", "htm", "xhtml")); - outputToFileTypes.put("JSON", List.of("json")); - outputToFileTypes.put("TXT", Arrays.asList("txt", "text", "md", "markdown")); - outputToFileTypes.put("PPT", Arrays.asList("ppt", "pptx", "odp")); - outputToFileTypes.put("XML", Arrays.asList("xml", "xsd", "xsl")); - outputToFileTypes.put( - "BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx")); - // type. - } - if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { - loadApiDocumentation(); - } - if (!apiDocumentation.containsKey(operationName)) { - return null; - } - ApiEndpoint endpoint = apiDocumentation.get(operationName); - String description = endpoint.getDescription(); - Matcher matcher = - (output - ? RegexPatternUtils.getInstance().getApiDocOutputTypePattern() - : RegexPatternUtils.getInstance().getApiDocInputTypePattern()) - .matcher(description); - while (matcher.find()) { - String type = matcher.group(1).toUpperCase(Locale.ROOT); - if (outputToFileTypes.containsKey(type)) { - return outputToFileTypes.get(type); - } - } - return null; - } - private String getApiKeyForUser() { if (userService == null) return ""; return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); @@ -157,55 +104,4 @@ public class ApiDocService implements stirling.software.common.service.ToolMetad ApiEndpoint endpoint = apiDocumentation.get(operationName); return endpoint.areParametersValid(parameters); } - - @Override - public boolean isMultiInput(String operationName) { - if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { - loadApiDocumentation(); - } - if (!apiDocumentation.containsKey(operationName)) { - return false; - } - ApiEndpoint endpoint = apiDocumentation.get(operationName); - String description = endpoint.getDescription(); - Matcher matcher = - RegexPatternUtils.getInstance().getApiDocTypePattern().matcher(description); - if (matcher.find()) { - String type = matcher.group(1); - return type.startsWith("MI"); - } - return false; - } - - @Override - public boolean shouldUnpackZipResponse(String operationName) { - if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { - loadApiDocumentation(); - } - if (!apiDocumentation.containsKey(operationName)) { - return false; - } - ApiEndpoint endpoint = apiDocumentation.get(operationName); - String description = endpoint.getDescription(); - Matcher typeMatcher = - RegexPatternUtils.getInstance().getApiDocTypePattern().matcher(description); - if (typeMatcher.find()) { - String type = typeMatcher.group(1); - // Multi-output endpoints (SIMO/MIMO) return a ZIP of their outputs. - if (type.endsWith("MO")) { - return true; - } - } - Matcher outputMatcher = - RegexPatternUtils.getInstance().getApiDocOutputTypePattern().matcher(description); - if (outputMatcher.find()) { - String output = outputMatcher.group(1).toUpperCase(Locale.ROOT); - if (output.startsWith("ZIP")) { - // Bare "Output:ZIP" is a single-archive deliverable, not a transport. - return !BARE_ZIP_OUTPUT.matcher(description).find(); - } - } - return false; - } } -// Model class for API Endpoint diff --git a/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java b/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java index 988b935f27..93ca36d08e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/HardwareKeyStoreService.java @@ -237,12 +237,12 @@ public class HardwareKeyStoreService { combined.append(env); } if (prop != null && !prop.isBlank()) { - if (combined.length() > 0) { + if (!combined.isEmpty()) { combined.append(java.io.File.pathSeparator); } combined.append(prop); } - if (combined.length() == 0) { + if (combined.isEmpty()) { return List.of(); } return Arrays.stream(combined.toString().split("[,;" + java.io.File.pathSeparator + "]")) diff --git a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java index fea3b52fc5..bc2e49853e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java @@ -2688,7 +2688,7 @@ public class PdfJsonConversionService { // Find which page the field is on PDAnnotationWidget widget = - field.getWidgets().isEmpty() ? null : field.getWidgets().get(0); + field.getWidgets().isEmpty() ? null : field.getWidgets().getFirst(); if (widget != null) { PDPage fieldPage = widget.getPage(); if (fieldPage != null) { @@ -3164,7 +3164,7 @@ public class PdfJsonConversionService { && imageObjectNames != null && !imageObjectNames.isEmpty() && !targetTokens.isEmpty()) { - Object previous = targetTokens.get(targetTokens.size() - 1); + Object previous = targetTokens.getLast(); if (previous instanceof COSName cosName && imageObjectNames.contains(cosName.getName())) { targetTokens.remove(targetTokens.size() - 1); @@ -5246,7 +5246,7 @@ public class PdfJsonConversionService { throws IOException { if (OperatorName.DRAW_OBJECT.equals(operator.getName()) && !operands.isEmpty() - && operands.get(0) instanceof COSName name) { + && operands.getFirst() instanceof COSName name) { currentXObjectName = name; } super.processOperator(operator, operands); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java b/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java index 9e082ba82a..6361157b21 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/VeraPDFService.java @@ -200,8 +200,8 @@ public class VeraPDFService { try (PDFAParser detectionParser = Foundries.defaultInstance().createParser(new ByteArrayInputStream(pdfBytes))) { - declaredFlavour = detectionParser.getFlavour(); - detectedFlavours = detectionParser.getFlavours(); + detectedFlavours = detectedFlavours(detectionParser); + declaredFlavour = firstFlavour(detectedFlavours); } // For PDF/A flavours, we need to validate first to check if PDF/A identification exists in @@ -285,6 +285,8 @@ public class VeraPDFService { } } + // Never force PDF/UA here - it flags every ordinary document as non-compliant and doubles + // verify cost; /accessibility-report checks PDF/UA on demand. if (!hasPdfaDeclaration) { results.add(createNoPdfaDeclarationResult()); } @@ -299,7 +301,7 @@ public class VeraPDFService { Foundries.defaultInstance() .createParser(new ByteArrayInputStream(pdfBytes), flavour)) { - PDFAFlavour parserDeclared = parser.getFlavour(); + PDFAFlavour parserDeclared = firstFlavour(detectedFlavours(parser)); PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false); ValidationResult result = validator.validate(parser); @@ -322,7 +324,19 @@ public class VeraPDFService { } private static boolean isPdfaFlavour(PDFAFlavour flavour) { - return PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A); + return flavour != null + && PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A); + } + + // veraPDF 1.30+ returns an empty flavour list for non-PDF/A files, where getFlavour() throws + private static List detectedFlavours(PDFAParser parser) { + List flavours = parser.getFlavours(); + return flavours != null ? flavours : List.of(); + } + + // null means "no PDF/A flavour detected" rather than an error + private static PDFAFlavour firstFlavour(List flavours) { + return flavours.isEmpty() ? null : flavours.get(0); } private static String formatStandardDisplay( diff --git a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/PdfJsonImageService.java b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/PdfJsonImageService.java index d0a0ad2dae..4ac18596f2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/PdfJsonImageService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/PdfJsonImageService.java @@ -420,7 +420,7 @@ public class PdfJsonImageService { throws IOException { if (OperatorName.DRAW_OBJECT.equals(operator.getName()) && !operands.isEmpty() - && operands.get(0) instanceof COSName name) { + && operands.getFirst() instanceof COSName name) { currentXObjectName = name; } super.processOperator(operator, operands); diff --git a/app/core/src/main/java/stirling/software/SPDF/utils/AutoRotateDetection.java b/app/core/src/main/java/stirling/software/SPDF/utils/AutoRotateDetection.java new file mode 100644 index 0000000000..825705d9bd --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/utils/AutoRotateDetection.java @@ -0,0 +1,189 @@ +package stirling.software.SPDF.utils; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; + +/** + * Page-orientation detection primitives for the auto-rotate tool. + * + *

Two independent signals are supported: the dominant direction of a page's embedded text + * (cheap, digital PDFs only) and Tesseract's orientation-and-script-detection output (works on + * scans, requires the external binary). Both express their result as the additional clockwise + * /Rotate correction that makes the page display upright. + */ +public final class AutoRotateDetection { + + private AutoRotateDetection() {} + + /** Glyphs needed to trust the text signal at the ordinary dominance bar. */ + public static final int MIN_GLYPHS = 30; + + /** Fraction of glyphs that must share one direction at the ordinary bar. */ + public static final double MIN_DOMINANCE = 0.95; + + /** + * Glyphs needed to trust the text signal when the glyphs are near-unanimous. Lets sparse pages + * (a header, a single line, a rotated URL) be decided from their own text instead of falling + * through to OSD, as long as effectively every glyph agrees on the direction. + */ + public static final int MIN_GLYPHS_UNANIMOUS = 8; + + /** Dominance required for the sparse-page path — essentially total agreement. */ + public static final double UNANIMOUS_DOMINANCE = 0.99; + + /** + * Dominant embedded-text direction of one page. + * + * @param dominantDirection glyph direction in page space, degrees CCW (0/90/180/270) + * @param dominance fraction of counted glyphs sharing the dominant direction (0..1) + * @param glyphCount number of non-whitespace glyphs counted + */ + public record TextDirection(int dominantDirection, double dominance, int glyphCount) { + + public boolean isConclusive() { + if (glyphCount >= MIN_GLYPHS && dominance >= MIN_DOMINANCE) { + return true; + } + return glyphCount >= MIN_GLYPHS_UNANIMOUS && dominance >= UNANIMOUS_DOMINANCE; + } + } + + /** + * Parsed Tesseract OSD verdict. + * + * @param rotate clockwise degrees to rotate the rendered page so text is upright + * @param confidence Tesseract's orientation confidence (same scale OCRmyPDF thresholds on) + */ + public record OsdResult(int rotate, double confidence) {} + + private static final Pattern OSD_ROTATE = + Pattern.compile("^Rotate:\\s*(\\d+)", Pattern.MULTILINE); + private static final Pattern OSD_CONFIDENCE = + Pattern.compile("^Orientation confidence:\\s*([0-9.]+)", Pattern.MULTILINE); + + /** + * Counts non-whitespace glyph directions for every page in one pass. A stripper per page would + * re-walk the document once per page, which is quadratic on long documents; this walks it once + * and buckets glyphs by the page being processed. + * + * @return one entry per page, in page order + */ + public static List detectTextDirections(PDDocument document) throws IOException { + int pageCount = document.getNumberOfPages(); + DirectionCountingStripper stripper = new DirectionCountingStripper(pageCount); + stripper.setStartPage(1); + stripper.setEndPage(pageCount); + stripper.getText(document); + + List directions = new ArrayList<>(pageCount); + for (int page = 0; page < pageCount; page++) { + int[] counts = stripper.counts[page]; + int total = 0; + int bestIndex = 0; + for (int i = 0; i < 4; i++) { + total += counts[i]; + if (counts[i] > counts[bestIndex]) { + bestIndex = i; + } + } + double dominance = total == 0 ? 0 : (double) counts[bestIndex] / total; + directions.add(new TextDirection(bestIndex * 90, dominance, total)); + } + return directions; + } + + /** + * True when a rendered page carries no ink worth analysing. Checked after rendering but before + * spawning Tesseract, since the process spawn costs far more than the pixel scan and this + * catches both empty generated pages and scanned blanks (the back of a duplex sheet). + */ + public static boolean isBlankRender(BufferedImage image) { + final int darkThreshold = 200; // 8-bit grey; anything lighter counts as paper + final int step = 4; // subsample: blank pages are uniform, no need for every pixel + long sampled = 0; + long dark = 0; + for (int y = 0; y < image.getHeight(); y += step) { + for (int x = 0; x < image.getWidth(); x += step) { + sampled++; + if ((image.getRGB(x, y) & 0xFF) < darkThreshold) { + dark++; + // A page needs a meaningful amount of ink before OSD can do anything; + // bail out as soon as we know there is enough. + if (dark > sampled / 1000 + 20) { + return false; + } + } + } + } + return true; + } + + /** + * Clockwise /Rotate correction for a page whose dominant glyph direction (page space, CCW) is + * {@code dominantDirection} and whose current /Rotate is {@code pageRotation}. Derivation: the + * on-screen text angle is (direction - rotation) CCW, and adding d to /Rotate turns the display + * a further d clockwise, so the correction that zeroes the screen angle is their difference. + */ + public static int correctionFromTextDirection(int dominantDirection, int pageRotation) { + return Math.floorMod(dominantDirection - pageRotation, 360); + } + + /** Extracts rotation and confidence from `tesseract stdout --psm 0` output. */ + public static Optional parseOsd(String tesseractOutput) { + if (tesseractOutput == null) { + return Optional.empty(); + } + Matcher rotate = OSD_ROTATE.matcher(tesseractOutput); + Matcher confidence = OSD_CONFIDENCE.matcher(tesseractOutput); + if (!rotate.find() || !confidence.find()) { + return Optional.empty(); + } + try { + return Optional.of( + new OsdResult( + Integer.parseInt(rotate.group(1)), + Double.parseDouble(confidence.group(1)))); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + private static class DirectionCountingStripper extends PDFTextStripper { + + // counts[page][i] holds glyphs on that page whose direction is i * 90 degrees + final int[][] counts; + + DirectionCountingStripper(int pageCount) throws IOException { + this.counts = new int[pageCount][4]; + } + + /** + * PDFBox snaps glyph direction to a quadrant, so getDir() only ever yields 0/90/180/270 — + * obliquely drawn text (30, 45, 135 degrees) is reported as 0 rather than as its true + * angle. Skew is therefore invisible to this signal by construction, which is consistent + * with skew being out of scope here: only 90-degree orientation is corrected. + */ + @Override + protected void processTextPosition(TextPosition text) { + String unicode = text.getUnicode(); + if (unicode == null || unicode.isBlank()) { + return; + } + int page = getCurrentPageNo() - 1; + if (page < 0 || page >= counts.length) { + return; + } + counts[page][Math.floorMod(Math.round(text.getDir()), 360) / 90]++; + // super is intentionally not called: we only count, no text assembly needed + } + } +} diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java b/app/core/src/main/java/stirling/software/SPDF/utils/ConnectedInputStream.java similarity index 98% rename from app/core/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java rename to app/core/src/main/java/stirling/software/SPDF/utils/ConnectedInputStream.java index 30434f7165..d8a824ec3b 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/util/ConnectedInputStream.java +++ b/app/core/src/main/java/stirling/software/SPDF/utils/ConnectedInputStream.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.pdfbox.examples.util; +package stirling.software.SPDF.utils; import java.io.IOException; import java.io.InputStream; diff --git a/app/core/src/main/java/stirling/software/common/controller/JobController.java b/app/core/src/main/java/stirling/software/common/controller/JobController.java index bab5dce539..c6fec4b92b 100644 --- a/app/core/src/main/java/stirling/software/common/controller/JobController.java +++ b/app/core/src/main/java/stirling/software/common/controller/JobController.java @@ -12,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -137,7 +138,7 @@ public class JobController { if (result.hasFiles() && !result.hasMultipleFiles()) { try { List files = result.getAllResultFiles(); - ResultFile singleFile = files.get(0); + ResultFile singleFile = files.getFirst(); byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId()); return ResponseEntity.ok() @@ -213,6 +214,35 @@ public class JobController { } } + /** + * Self-service counterpart to the admin-only {@code POST /api/v1/admin/job/cleanup}: that one + * sweeps every user's jobs and needs ROLE_ADMIN, this one releases only the caller's own and so + * is safe for any authenticated user. Both run the same sweep inside {@link TaskManager}. + */ + @PostMapping("/jobs/cleanup") + @Operation( + summary = "Release finished jobs and their stored files now", + description = + "Force-expires this node's finished jobs instead of waiting out the retention" + + " window, deleting their result files and the persistent copies made of" + + " their inputs. Only jobs the caller may access are touched, and jobs" + + " still running are left alone. Admins can sweep every user's jobs" + + " with POST /api/v1/admin/job/cleanup?force=true.") + public ResponseEntity cleanupFinishedJobs() { + TaskManager.CleanupSummary summary = + taskManager.cleanupFinishedJobsNow(this::validateJobAccess); + log.info( + "On-demand job cleanup removed {} job(s) and {} file(s), retained {} job(s)", + summary.jobsRemoved(), + summary.filesDeleted(), + summary.jobsRetained()); + return ResponseEntity.ok( + Map.of( + "jobsRemoved", summary.jobsRemoved(), + "filesDeleted", summary.filesDeleted(), + "jobsRetained", summary.jobsRetained())); + } + @GetMapping("/job/{jobId}/result/files") @Operation(summary = "Get job result files") public ResponseEntity getJobFiles(@PathVariable("jobId") String jobId) { diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index fd9cc13c13..fdfe40b352 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -188,6 +188,7 @@ system: backendUrl: "" # Backend base URL for SAML/OAuth/API callbacks (e.g. 'http://localhost:8080' for dev, 'https://api.example.com' for production). REQUIRED for SSO authentication to work correctly. This is where your IdP will send SAML responses and OAuth callbacks. Leave empty to default to 'http://localhost:8080' in development. frontendUrl: "" # Frontend URL for invite email links (e.g. 'https://app.example.com'). Optional - if not set, will use backendUrl. This is the URL users click in invite emails. enableMobileScanner: true # Enable mobile phone QR code upload feature. Requires frontendUrl to be configured. + enableMobileSignature: true # Enable drawing signatures on a phone via QR code from the Sign tool. Requires frontendUrl to be configured. mobileScannerSettings: convertToPdf: true # Automatically convert uploaded images to PDF format. If false, images are kept as-is. imageResolution: full # Image resolution for mobile uploads: 'full' (original size) or 'reduced' (max 1200px on longest side). Only applies when convertToPdf is true. @@ -292,6 +293,15 @@ storage: linkExpirationDays: 3 # Number of days before share links expire signing: enabled: false # set to 'true' to enable group signing workflow (requires storage.enabled) [ALPHA] + # Encryption at rest for stored files (AES-256, per-team keys). Requires a Pro or + # Enterprise licence. Key setup, cluster requirements, the encrypt-existing migration, + # the revocation kill switch and master-key rotation are documented in + # devGuide/STORAGE_ENCRYPTION_AT_REST.md + # WARNING: back up the master key (configs/file-encryption.key by default) - losing it + # makes every encrypted stored file permanently unrecoverable. + encryption: + enabled: false # set to 'true' to encrypt stored files at rest + auditReads: true # audit every decrypt of an encrypted file (denied decrypts and key lifecycle events are always audited). NOTE: audit events require an Enterprise licence; encryption itself works on Pro. userListScope: org # Signing user-picker scope: 'org' (default) = whole instance, else caller's team only. autoPipeline: outputFolder: "" # Output folder for processed pipeline files (leave empty for default) @@ -303,7 +313,7 @@ autoPipeline: ui: appNameNavbar: "" # custom app/brand name. NOTE: no longer shown in the navbar (the navbar renders the logo). It IS used as the browser tab title and as the TOTP/2FA issuer label in authenticator apps. Empty falls back to "Stirling PDF" - logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo) + logoStyle: modern # Options: 'modern' (default - minimalist logo) or 'classic' (legacy S icon) languages: [] # If empty, all languages are enabled. To restrict to specific languages, use a whitelist like ["de_DE", "pl_PL", "sv_SE"]. Empty list or not restricting any languages will enable all available languages. defaultHideUnavailableTools: false # Default user preference: hide disabled tools instead of greying them out defaultHideUnavailableConversions: false # Default user preference: hide disabled conversion options instead of greying them out diff --git a/app/core/src/main/resources/static/3rdPartyLicenses.json b/app/core/src/main/resources/static/3rdPartyLicenses.json index b13070e8b5..c411735489 100644 --- a/app/core/src/main/resources/static/3rdPartyLicenses.json +++ b/app/core/src/main/resources/static/3rdPartyLicenses.json @@ -1,5 +1,12 @@ { "dependencies": [ + { + "moduleName": "ch.obermuhlner:big-math", + "moduleUrl": "https://github.com/eobermuhlner/big-math", + "moduleVersion": "2.0.0", + "moduleLicense": "MIT License", + "moduleLicenseUrl": "https://raw.githubusercontent.com/eobermuhlner/big-math/master/LICENSE.txt" + }, { "moduleName": "ch.qos.logback:logback-classic", "moduleUrl": "http://www.qos.ch", @@ -7,6 +14,13 @@ "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, + { + "moduleName": "ch.qos.logback:logback-classic", + "moduleUrl": "http://www.qos.ch", + "moduleVersion": "1.6.1", + "moduleLicense": "LGPL-2.1-only", + "moduleLicenseUrl": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" + }, { "moduleName": "ch.qos.logback:logback-core", "moduleUrl": "http://www.qos.ch", @@ -14,6 +28,13 @@ "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, + { + "moduleName": "ch.qos.logback:logback-core", + "moduleUrl": "http://www.qos.ch", + "moduleVersion": "1.6.1", + "moduleLicense": "LGPL-2.1-only", + "moduleLicenseUrl": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" + }, { "moduleName": "com.adobe.xmp:xmpcore", "moduleUrl": "https://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html", @@ -52,7 +73,7 @@ { "moduleName": "com.drewnoakes:metadata-extractor", "moduleUrl": "https://drewnoakes.com/code/exif/", - "moduleVersion": "2.20.0", + "moduleVersion": "2.21.0", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -70,6 +91,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.fasterxml.jackson.core:jackson-core", + "moduleUrl": "https://github.com/FasterXML/jackson-core", + "moduleVersion": "2.22.1", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.fasterxml.jackson.core:jackson-databind", "moduleUrl": "https://github.com/FasterXML/jackson", @@ -77,6 +105,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.fasterxml.jackson.core:jackson-databind", + "moduleUrl": "https://github.com/FasterXML/jackson", + "moduleVersion": "2.22.1", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", "moduleUrl": "https://github.com/FasterXML/jackson-dataformats-text", @@ -84,6 +119,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", + "moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8", + "moduleVersion": "2.21.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", "moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310", @@ -98,6 +140,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.fasterxml.jackson:jackson-bom", + "moduleUrl": "https://github.com/FasterXML/jackson-bom", + "moduleVersion": "2.22.1", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.fasterxml:classmate", "moduleUrl": "https://github.com/FasterXML/java-classmate", @@ -111,17 +160,10 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "com.github.bbottema:jetbrains-runtime-annotations", - "moduleUrl": "https://github.com/bbottema/jetbrains-runtime-nullability-annotations", - "moduleVersion": "1.0.2", - "moduleLicense": "The Apache Software License, Version 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" - }, { "moduleName": "com.github.bbottema:rtf-to-html", "moduleUrl": "http:///github.com/bbottema/rtf-to-html", - "moduleVersion": "1.1.1", + "moduleVersion": "2.0.2", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -147,10 +189,17 @@ { "moduleName": "com.github.junrar:junrar", "moduleUrl": "https://github.com/junrar/junrar", - "moduleVersion": "7.5.10", + "moduleVersion": "8.0.0", "moduleLicense": "UnRar License", "moduleLicenseUrl": "https://github.com/junrar/junrar/blob/master/LICENSE" }, + { + "moduleName": "com.github.mwiede:jsch", + "moduleUrl": "https://github.com/mwiede/jsch", + "moduleVersion": "2.28.6", + "moduleLicense": "Revised BSD", + "moduleLicenseUrl": "https://github.com/mwiede/jsch/blob/master/LICENSE.txt" + }, { "moduleName": "com.github.stephenc.jcip:jcip-annotations", "moduleUrl": "http://stephenc.github.com/jcip-annotations", @@ -186,6 +235,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.google.crypto.tink:tink", + "moduleUrl": "http://github.com/tink-crypto/tink-java", + "moduleVersion": "1.23.0", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.google.errorprone:error_prone_annotations", "moduleUrl": "https://errorprone.info/error_prone_annotations", @@ -227,6 +283,20 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.google.protobuf:protobuf-java", + "moduleUrl": "https://developers.google.com/protocol-buffers/", + "moduleVersion": "4.33.6", + "moduleLicense": "BSD-3-Clause", + "moduleLicenseUrl": "https://opensource.org/licenses/BSD-3-Clause" + }, + { + "moduleName": "com.google.re2j:re2j", + "moduleUrl": "http://github.com/google/re2j", + "moduleVersion": "1.2", + "moduleLicense": "Go License", + "moduleLicenseUrl": "https://golang.org/LICENSE" + }, { "moduleName": "com.google.zxing:core", "moduleUrl": "https://github.com/zxing/zxing/core", @@ -234,6 +304,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "com.googlecode.java-ipv6:java-ipv6", + "moduleUrl": "https://github.com/janvanbesien/java-ipv6/", + "moduleVersion": "0.17", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.googlecode.owasp-java-html-sanitizer:java10-shim", "moduleUrl": "https://github.com/OWASP/java-html-sanitizer", @@ -269,6 +346,41 @@ "moduleLicense": "MPL 2.0", "moduleLicenseUrl": "https://www.mozilla.org/en-US/MPL/2.0/" }, + { + "moduleName": "com.hierynomus:asn-one", + "moduleUrl": "https://github.com/hierynomus/asn-one", + "moduleVersion": "0.6.0", + "moduleLicense": "The Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + { + "moduleName": "com.hierynomus:smbj", + "moduleUrl": "https://github.com/hierynomus/smbj", + "moduleVersion": "0.14.0", + "moduleLicense": "The Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + { + "moduleName": "com.hubspot.immutables:immutables-exceptions", + "moduleUrl": "https://github.com/HubSpot/hubspot-immutables/tree/58628096ac99b286fe4f8bfe12aa3cff0f0589d3", + "moduleVersion": "1.9", + "moduleLicense": "The Apache License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "moduleName": "com.hubspot.jinjava:jinjava", + "moduleUrl": "https://github.com/HubSpot/jinjava", + "moduleVersion": "2.8.4", + "moduleLicense": "The Apache License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "moduleName": "com.hubspot:algebra", + "moduleUrl": "https://github.com/HubSpot/algebra/tree/5d42983fd3a26539df9ba2cbeac32a1bddce0494", + "moduleVersion": "1.5", + "moduleLicense": "The Apache License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "com.nimbusds:content-type", "moduleUrl": "https://connect2id.com", @@ -321,49 +433,49 @@ { "moduleName": "com.sanctionco.jmail:jmail", "moduleUrl": "https://github.com/RohanNagar/jmail", - "moduleVersion": "1.6.3", + "moduleVersion": "2.2.0", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/mit-license.php" }, { "moduleName": "com.stirling:jpdfium", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "com.stirling:jpdfium-natives-darwin-arm64", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "com.stirling:jpdfium-natives-darwin-x64", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "com.stirling:jpdfium-natives-linux-arm64", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "com.stirling:jpdfium-natives-linux-x64", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "com.stirling:jpdfium-natives-windows-x64", "moduleUrl": "https://github.com/Stirling-Tools/JPDFium", - "moduleVersion": "1.0.2", + "moduleVersion": "1.0.4", "moduleLicense": "MIT License", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -391,7 +503,7 @@ { "moduleName": "com.sun.xml.bind:jaxb-core", "moduleUrl": "https://www.eclipse.org", - "moduleVersion": "4.0.7", + "moduleVersion": "4.0.9", "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, @@ -415,27 +527,45 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-image", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.common:common-io", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-io", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.common:common-lang", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-lang", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-batik", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, { "moduleName": "com.twelvemonkeys.imageio:imageio-bmp", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -445,9 +575,15 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-core", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-jpeg", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -457,9 +593,15 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-metadata", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-psd", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -469,12 +611,24 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-tiff", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-webp", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-webp", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.vladsch.flexmark:flexmark", "moduleVersion": "0.64.8", @@ -657,6 +811,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "commons-net:commons-net", + "moduleUrl": "https://commons.apache.org/proper/commons-net/", + "moduleVersion": "3.13.0", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "de.rototor.pdfbox:graphics2d", "moduleVersion": "3.0.5", @@ -1067,6 +1228,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.engio:mbassador", + "moduleUrl": "https://github.com/bennidi/mbassador", + "moduleVersion": "1.3.0", + "moduleLicense": "MIT license", + "moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php" + }, { "moduleName": "net.java.dev.stax-utils:stax-utils", "moduleUrl": "http://java.net/projects/stax-utils/", @@ -1088,30 +1256,61 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.sf.saxon:Saxon-HE", + "moduleUrl": "http://www.saxonica.com/", + "moduleVersion": "12.8", + "moduleLicense": "Mozilla Public License Version 2.0", + "moduleLicenseUrl": "http://www.mozilla.org/MPL/2.0/" + }, { "moduleName": "net.shibboleth:shib-networking", "moduleVersion": "9.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.shibboleth:shib-networking", + "moduleVersion": "9.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "net.shibboleth:shib-security", "moduleVersion": "9.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.shibboleth:shib-security", + "moduleVersion": "9.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "net.shibboleth:shib-support", "moduleVersion": "9.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.shibboleth:shib-support", + "moduleVersion": "9.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "net.shibboleth:shib-velocity", "moduleVersion": "9.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "net.shibboleth:shib-velocity", + "moduleVersion": "9.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.antlr:antlr4-runtime", "moduleUrl": "https://www.antlr.org/", @@ -1206,13 +1405,6 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, - { - "moduleName": "org.apache.httpcomponents:httpclient", - "moduleUrl": "http://hc.apache.org/httpcomponents-client", - "moduleVersion": "4.5.13", - "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" - }, { "moduleName": "org.apache.httpcomponents:httpclient", "moduleUrl": "http://hc.apache.org/httpcomponents-client-ga", @@ -1251,7 +1443,7 @@ { "moduleName": "org.apache.pdfbox:fontbox", "moduleUrl": "https://pdfbox.apache.org", - "moduleVersion": "3.0.7", + "moduleVersion": "3.0.8", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -1263,44 +1455,44 @@ }, { "moduleName": "org.apache.pdfbox:jbig2-imageio", - "moduleVersion": "3.0.4", - "moduleLicense": "Apache License, Version 2.0", + "moduleVersion": "3.0.5", + "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.pdfbox:pdfbox", "moduleUrl": "https://pdfbox.apache.org", - "moduleVersion": "3.0.7", + "moduleVersion": "3.0.8", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.pdfbox:pdfbox-io", "moduleUrl": "https://pdfbox.apache.org", - "moduleVersion": "3.0.7", + "moduleVersion": "3.0.8", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.pdfbox:preflight", "moduleUrl": "https://pdfbox.apache.org", - "moduleVersion": "3.0.7", + "moduleVersion": "3.0.8", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.pdfbox:xmpbox", "moduleUrl": "https://pdfbox.apache.org", - "moduleVersion": "3.0.7", + "moduleVersion": "3.0.8", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.poi:poi", "moduleUrl": "https://poi.apache.org/", - "moduleVersion": "5.2.5", + "moduleVersion": "5.4.1", "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.poi:poi", @@ -1326,9 +1518,9 @@ { "moduleName": "org.apache.poi:poi-scratchpad", "moduleUrl": "https://poi.apache.org/", - "moduleVersion": "5.2.5", + "moduleVersion": "5.4.1", "moduleLicense": "Apache License, Version 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.apache.santuario:xmlsec", @@ -1337,6 +1529,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.apache.santuario:xmlsec", + "moduleUrl": "https://www.apache.org/", + "moduleVersion": "3.0.6", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.apache.tomcat.embed:tomcat-embed-el", "moduleUrl": "https://tomcat.apache.org/", @@ -1351,6 +1550,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.apache.velocity:velocity-engine-core", + "moduleUrl": "https://www.apache.org/", + "moduleVersion": "2.4.1", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.apache.xmlbeans:xmlbeans", "moduleUrl": "https://xmlbeans.apache.org/", @@ -1477,21 +1683,21 @@ { "moduleName": "org.bouncycastle:bcpkix-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", - "moduleVersion": "1.84", + "moduleVersion": "1.85", "moduleLicense": "Bouncy Castle Licence", "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" }, { "moduleName": "org.bouncycastle:bcprov-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", - "moduleVersion": "1.84", + "moduleVersion": "1.85", "moduleLicense": "Bouncy Castle Licence", "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" }, { "moduleName": "org.bouncycastle:bcutil-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", - "moduleVersion": "1.84", + "moduleVersion": "1.85", "moduleLicense": "Bouncy Castle Licence", "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" }, @@ -1502,6 +1708,13 @@ "moduleLicense": "The MIT License", "moduleLicenseUrl": "http://opensource.org/licenses/MIT" }, + { + "moduleName": "org.checkerframework:checker-qual", + "moduleUrl": "https://checkerframework.org/", + "moduleVersion": "3.55.1", + "moduleLicense": "The MIT License", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "org.commonmark:commonmark", "moduleVersion": "0.28.0", @@ -1521,6 +1734,13 @@ "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "http://www.gnu.org/licenses/lgpl-3.0.txt" }, + { + "moduleName": "org.cryptacular:cryptacular", + "moduleUrl": "https://www.cryptacular.org", + "moduleVersion": "1.3.0", + "moduleLicense": "GNU Lesser General Public License", + "moduleLicenseUrl": "https://www.gnu.org/licenses/lgpl-3.0.txt" + }, { "moduleName": "org.eclipse.angus:angus-activation", "moduleUrl": "https://www.eclipse.org", @@ -1829,6 +2049,13 @@ "moduleLicense": "Apache License 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.javassist:javassist", + "moduleUrl": "https://www.javassist.org/", + "moduleVersion": "3.30.2-GA", + "moduleLicense": "MPL 1.1", + "moduleLicenseUrl": "https://www.mozilla.org/en-US/MPL/1.1/" + }, { "moduleName": "org.jboss.logging:jboss-logging", "moduleUrl": "https://www.jboss.org", @@ -1839,7 +2066,7 @@ { "moduleName": "org.jetbrains:annotations", "moduleUrl": "https://github.com/JetBrains/java-annotations", - "moduleVersion": "24.0.1", + "moduleVersion": "26.1.0", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -1883,78 +2110,156 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-core-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-core-impl", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-core-impl", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-messaging-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-messaging-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-profile-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-profile-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-saml-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-saml-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-saml-impl", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-saml-impl", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-security-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-security-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-security-impl", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-security-impl", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-soap-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-soap-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-soap-impl", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-soap-impl", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-storage-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-storage-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-xmlsec-api", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-xmlsec-api", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.opensaml:opensaml-xmlsec-impl", "moduleVersion": "5.1.6", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.opensaml:opensaml-xmlsec-impl", + "moduleVersion": "5.2.2", + "moduleLicense": "The Apache Software License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.ow2.asm:asm", "moduleUrl": "http://asm.ow2.org", @@ -1986,7 +2291,7 @@ { "moduleName": "org.postgresql:postgresql", "moduleUrl": "https://jdbc.postgresql.org/", - "moduleVersion": "42.7.11", + "moduleVersion": "42.7.13", "moduleLicense": "BSD-2-Clause", "moduleLicenseUrl": "https://jdbc.postgresql.org/about/license.html" }, @@ -1999,26 +2304,26 @@ }, { "moduleName": "org.simplejavamail:core-module", - "moduleVersion": "8.12.6", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.simplejavamail:outlook-message-parser", "moduleUrl": "https://github.com/bbottema/outlook-message-parser", - "moduleVersion": "1.14.1", + "moduleVersion": "1.16.2", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.simplejavamail:outlook-module", - "moduleVersion": "8.12.6", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.simplejavamail:simple-java-mail", - "moduleVersion": "8.12.6", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -2431,6 +2736,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "org.springframework.security:spring-security-core", + "moduleUrl": "https://spring.io/projects/spring-security", + "moduleVersion": "7.1.0", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.springframework.security:spring-security-crypto", "moduleUrl": "https://spring.io/projects/spring-security", @@ -2473,6 +2785,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "org.springframework.security:spring-security-saml2-service-provider", + "moduleUrl": "https://spring.io/projects/spring-security", + "moduleVersion": "7.1.0", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.springframework.security:spring-security-web", "moduleUrl": "https://spring.io/projects/spring-security", @@ -2594,44 +2913,44 @@ }, { "moduleName": "org.verapdf:core", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:feature-reporting", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:metadata-fixer", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:parser", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:pdf-model", "moduleUrl": "https://github.com/veraPDF/veraPDF-model/", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:validation-model", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, { "moduleName": "org.verapdf:verapdf-xmp-core", - "moduleVersion": "1.28.2", + "moduleVersion": "1.30.2", "moduleLicense": "Mozilla Public License 2.0 (MPL-2.0)", "moduleLicenseUrl": "https://opensource.org/licenses/MPL-2.0" }, @@ -2649,6 +2968,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://github.com/webjars/webjars-locator-lite/blob/main/LICENSE.md" }, + { + "moduleName": "org.xmlresolver:xmlresolver", + "moduleUrl": "https://github.com/xmlresolver/xmlresolver", + "moduleVersion": "5.3.3", + "moduleLicense": "Apache License version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "org.yaml:snakeyaml", "moduleUrl": "https://bitbucket.org/snakeyaml/snakeyaml", @@ -2665,207 +2991,207 @@ }, { "moduleName": "software.amazon.awssdk:annotations", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { - "moduleName": "software.amazon.awssdk:apache-client", - "moduleVersion": "2.44.12", + "moduleName": "software.amazon.awssdk:apache5-client", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:arns", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:auth", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:aws-core", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:aws-query-protocol", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:aws-xml-protocol", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:checksums", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:checksums-spi", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:crt-core", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:endpoints-spi", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:http-auth", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:http-auth-aws", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:http-auth-aws-eventstream", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:http-auth-spi", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:http-client-spi", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:identity-spi", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:json-utils", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:metrics-spi", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:netty-nio-client", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:profiles", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:protocol-core", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:regions", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:retries", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:retries-spi", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:s3", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:sdk-core", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:third-party-jackson-core", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:url-connection-client", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:utils", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, { "moduleName": "software.amazon.awssdk:utils-lite", "moduleUrl": "https://aws.amazon.com/sdkforjava", - "moduleVersion": "2.44.12", + "moduleVersion": "2.51.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://aws.amazon.com/apache2.0" }, diff --git a/app/core/src/main/resources/static/android-chrome-192x192.png b/app/core/src/main/resources/static/android-chrome-192x192.png deleted file mode 100644 index 4219bb8403..0000000000 Binary files a/app/core/src/main/resources/static/android-chrome-192x192.png and /dev/null differ diff --git a/app/core/src/main/resources/static/android-chrome-512x512.png b/app/core/src/main/resources/static/android-chrome-512x512.png deleted file mode 100644 index 19bc603ec5..0000000000 Binary files a/app/core/src/main/resources/static/android-chrome-512x512.png and /dev/null differ diff --git a/app/core/src/main/resources/static/apple-touch-icon.png b/app/core/src/main/resources/static/apple-touch-icon.png index 6ac076ba54..66a12c54c6 100644 Binary files a/app/core/src/main/resources/static/apple-touch-icon.png and b/app/core/src/main/resources/static/apple-touch-icon.png differ diff --git a/app/core/src/main/resources/static/css/cookieconsent.css b/app/core/src/main/resources/static/css/cookieconsent.css deleted file mode 100644 index fdcc6ba6cb..0000000000 --- a/app/core/src/main/resources/static/css/cookieconsent.css +++ /dev/null @@ -1 +0,0 @@ -#cc-main{background:transparent;color:var(--cc-primary-color);font-family:var(--cc-font-family);font-size:16px;font-weight:400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.15;position:relative;text-rendering:optimizeLegibility;-webkit-text-size-adjust:100%;position:fixed;z-index:var(--cc-z-index)}#cc-main :after,#cc-main :before,#cc-main a,#cc-main button,#cc-main div,#cc-main h2,#cc-main input,#cc-main p,#cc-main span{all:unset;box-sizing:border-box}#cc-main .pm__badge,#cc-main button{all:initial;box-sizing:border-box;color:unset;visibility:unset}#cc-main .pm__badge,#cc-main a,#cc-main button,#cc-main input{-webkit-appearance:none;appearance:none;cursor:pointer;font-family:inherit;font-size:100%;line-height:normal;margin:0;outline:revert;outline-offset:2px;overflow:hidden}#cc-main table,#cc-main tbody,#cc-main td,#cc-main th,#cc-main thead,#cc-main tr{all:revert;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit}:root{--cc-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--cc-modal-border-radius:.5rem;--cc-btn-border-radius:.4rem;--cc-modal-transition-duration:.25s;--cc-link-color:var(--cc-btn-primary-bg);--cc-modal-margin:1rem;--cc-z-index:2147483647;--cc-bg:#fff;--cc-primary-color:#2c2f31;--cc-secondary-color:#5e6266;--cc-btn-primary-bg:#30363c;--cc-btn-primary-color:#fff;--cc-btn-primary-border-color:var(--cc-btn-primary-bg);--cc-btn-primary-hover-bg:#000;--cc-btn-primary-hover-color:#fff;--cc-btn-primary-hover-border-color:var(--cc-btn-primary-hover-bg);--cc-btn-secondary-bg:#eaeff2;--cc-btn-secondary-color:var(--cc-primary-color);--cc-btn-secondary-border-color:var(--cc-btn-secondary-bg);--cc-btn-secondary-hover-bg:#d4dae0;--cc-btn-secondary-hover-color:#000;--cc-btn-secondary-hover-border-color:#d4dae0;--cc-separator-border-color:#f0f4f7;--cc-toggle-on-bg:var(--cc-btn-primary-bg);--cc-toggle-off-bg:#667481;--cc-toggle-on-knob-bg:#fff;--cc-toggle-off-knob-bg:var(--cc-toggle-on-knob-bg);--cc-toggle-enabled-icon-color:var(--cc-bg);--cc-toggle-disabled-icon-color:var(--cc-bg);--cc-toggle-readonly-bg:#d5dee2;--cc-toggle-readonly-knob-bg:#fff;--cc-toggle-readonly-knob-icon-color:var(--cc-toggle-readonly-bg);--cc-section-category-border:var(--cc-cookie-category-block-bg);--cc-cookie-category-block-bg:#f0f4f7;--cc-cookie-category-block-border:#f0f4f7;--cc-cookie-category-block-hover-bg:#e9eff4;--cc-cookie-category-block-hover-border:#e9eff4;--cc-cookie-category-expanded-block-bg:transparent;--cc-cookie-category-expanded-block-hover-bg:#dee4e9;--cc-overlay-bg:rgba(0,0,0,.65);--cc-webkit-scrollbar-bg:var(--cc-section-category-border);--cc-webkit-scrollbar-hover-bg:var(--cc-btn-primary-hover-bg);--cc-footer-bg:var(--cc-btn-secondary-bg);--cc-footer-color:var(--cc-secondary-color);--cc-footer-border-color:#e4eaed;--cc-pm-toggle-border-radius:4em}#cc-main.cc--rtl{direction:rtl}#cc-main .cm__title,#cc-main a,#cc-main b,#cc-main em,#cc-main strong{font-weight:600}#cc-main button>span{pointer-events:none}#cc-main .cc__link,#cc-main a{background-image:linear-gradient(currentColor,currentColor);background-position:0 100%;background-repeat:no-repeat;background-size:0 1px;font-weight:600;position:relative;transition:background-size .25s,color .25s ease}#cc-main .cc__link:hover,#cc-main a:hover{background-size:100% 1px;color:var(--cc-primary-color)}#cc-main .cc__link{color:var(--cc-link-color)}#cc-main .cm__desc,#cc-main .pm__body{overscroll-behavior:auto contain;scrollbar-width:thin}@media screen and (min-width:640px){#cc-main ::-webkit-scrollbar,#cc-main ::-webkit-scrollbar-thumb,#cc-main ::-webkit-scrollbar-track{all:revert}#cc-main ::-webkit-scrollbar-thumb{-webkit-background-clip:padding-box;background-clip:padding-box;background:var(--cc-toggle-readonly-bg);border:.25rem solid var(--cc-bg);border-radius:1rem}#cc-main ::-webkit-scrollbar-thumb:hover{background:var(--cc-toggle-off-bg)}#cc-main ::-webkit-scrollbar{background:transparent;width:12px}}html.disable--interaction.show--consent,html.disable--interaction.show--consent body{height:auto!important;overflow:hidden!important}@media (prefers-reduced-motion){#cc-main{--cc-modal-transition-duration:0s}}.cc--darkmode{--cc-bg:#161a1c;--cc-primary-color:#ebf3f6;--cc-secondary-color:#aebbc5;--cc-btn-primary-bg:#c2d0e0;--cc-btn-primary-color:var(--cc-bg);--cc-btn-primary-border-color:var(--cc-btn-primary-bg);--cc-btn-primary-hover-bg:#98a7b6;--cc-btn-primary-hover-color:#000;--cc-btn-primary-hover-border-color:var(--cc-btn-primary-hover-bg);--cc-btn-secondary-bg:#242c31;--cc-btn-secondary-color:var(--cc-primary-color);--cc-btn-secondary-border-color:var(--cc-btn-secondary-bg);--cc-btn-secondary-hover-bg:#353d43;--cc-btn-secondary-hover-color:#fff;--cc-btn-secondary-hover-border-color:var(--cc-btn-secondary-hover-bg);--cc-separator-border-color:#222a30;--cc-toggle-on-bg:var(--cc-btn-primary-bg);--cc-toggle-off-bg:#525f6b;--cc-toggle-on-knob-bg:var(--cc-btn-primary-color);--cc-toggle-off-knob-bg:var(--cc-btn-primary-color);--cc-toggle-enabled-icon-color:var(--cc-btn-primary-color);--cc-toggle-disabled-icon-color:var(--cc-btn-primary-color);--cc-toggle-readonly-bg:#343e45;--cc-toggle-readonly-knob-bg:#5f6b72;--cc-toggle-readonly-knob-icon-color:var(--cc-toggle-readonly-bg);--cc-section-category-border:#1e2428;--cc-cookie-category-block-bg:#1e2428;--cc-cookie-category-block-border:var(--cc-section-category-border);--cc-cookie-category-block-hover-bg:#242c31;--cc-cookie-category-block-hover-border:#232a2f;--cc-cookie-category-expanded-block-bg:transparent;--cc-cookie-category-expanded-block-hover-bg:var(--cc-toggle-readonly-bg);--cc-overlay-bg:rgba(0,0,0,.65);--cc-webkit-scrollbar-bg:var(--cc-section-category-border);--cc-webkit-scrollbar-hover-bg:var(--cc-btn-primary-hover-bg);--cc-footer-bg:#0c0e0f;--cc-footer-color:var(--cc-secondary-color);--cc-footer-border-color:#060809}.cc--darkmode #cc-main{color-scheme:dark}#cc-main .cm{background:var(--cc-bg);border-radius:var(--cc-modal-border-radius);box-shadow:0 .625em 1.875em rgba(0,0,2,.3);display:flex;flex-direction:column;max-width:24rem;opacity:0;overflow:hidden;position:fixed;transform:translateY(1.6em);visibility:hidden;z-index:1}#cc-main .cm--top{top:var(--cc-modal-margin)}#cc-main .cm--middle{top:50%;transform:translateY(calc(-50% + 1.6em))}#cc-main .cm--bottom{bottom:var(--cc-modal-margin)}#cc-main .cm--center{left:var(--cc-modal-margin);margin:0 auto;right:var(--cc-modal-margin);width:unset}#cc-main .cm--left{left:var(--cc-modal-margin);margin-right:var(--cc-modal-margin)}#cc-main .cm--right{margin-left:var(--cc-modal-margin);right:var(--cc-modal-margin)}#cc-main .cm__body{display:flex;flex-direction:column;justify-content:space-between;position:relative}#cc-main .cm__btns,#cc-main .cm__links{padding:1rem 1.3rem;width:unset}#cc-main .cm__texts{display:flex;flex:1;flex-direction:column;justify-content:center;padding:1rem 0 0}#cc-main .cm__desc,#cc-main .cm__title{padding:0 1.3rem}#cc-main .cm__title{font-size:1.05em}#cc-main .cm__title+.cm__desc{margin-top:1.1em}#cc-main .cm__desc{color:var(--cc-secondary-color);font-size:.9em;line-height:1.5;max-height:40vh;overflow-x:visible;overflow-y:auto;padding-bottom:1em}#cc-main .cm__btns{border-top:1px solid var(--cc-separator-border-color);display:flex;flex-direction:column;justify-content:center}#cc-main .cm__btn-group{display:grid;grid-auto-columns:minmax(0,1fr)}#cc-main .cm__btn+.cm__btn,#cc-main .cm__btn-group+.cm__btn-group{margin-top:.375rem}#cc-main .cm--flip .cm__btn+.cm__btn,#cc-main .cm--flip .cm__btn-group+.cm__btn-group{margin-bottom:.375rem;margin-top:0}#cc-main .cm--inline .cm__btn+.cm__btn{margin-left:.375rem;margin-top:0}#cc-main .cm--inline.cm--flip .cm__btn+.cm__btn{margin-bottom:0;margin-left:0;margin-right:.375rem}#cc-main .cm--inline.cm--flip .cm__btn-group+.cm__btn-group{margin-bottom:.375rem;margin-right:0}#cc-main .cm--wide .cm__btn+.cm__btn,#cc-main .cm--wide .cm__btn-group+.cm__btn-group{margin-left:.375rem;margin-top:0}#cc-main .cm--wide.cm--flip .cm__btn+.cm__btn,#cc-main .cm--wide.cm--flip .cm__btn-group+.cm__btn-group{margin-bottom:0;margin-right:.375rem}#cc-main .cm--bar:not(.cm--inline) .cm__btn-group--uneven,#cc-main .cm--wide .cm__btn-group--uneven{display:flex;flex:1;justify-content:space-between}#cc-main .cm--bar:not(.cm--inline).cm--flip .cm__btn-group--uneven,#cc-main .cm--wide.cm--flip .cm__btn-group--uneven{flex-direction:row-reverse}#cc-main .cm__btn{background:var(--cc-btn-primary-bg);border:1px solid var(--cc-btn-primary-border-color);border-radius:var(--cc-btn-border-radius);color:var(--cc-btn-primary-color);font-size:.82em;font-weight:600;min-height:42px;padding:.5em 1em;text-align:center}#cc-main .cm__btn:hover{background:var(--cc-btn-primary-hover-bg);border-color:var(--cc-btn-primary-hover-border-color);color:var(--cc-btn-primary-hover-color)}#cc-main .cm__btn--secondary{background:var(--cc-btn-secondary-bg);border-color:var(--cc-btn-secondary-border-color);color:var(--cc-btn-secondary-color)}#cc-main .cm__btn--secondary:hover{background:var(--cc-btn-secondary-hover-bg);border-color:var(--cc-btn-secondary-hover-border-color);color:var(--cc-btn-secondary-hover-color)}#cc-main .cm__btn--close{border-radius:0;border-bottom-left-radius:var(--cc-btn-border-radius);border-right:none;border-top:none;display:none;font-size:1em;height:42px;min-width:auto!important;overflow:hidden;padding:0!important;position:absolute;right:0;top:0;width:42px}#cc-main .cm__btn--close svg{stroke:var(--cc-btn-primary-color);transform:scale(.5);transition:stroke .15s ease}#cc-main .cm__btn--close:hover svg{stroke:var(--cc-btn-primary-hover-color)}#cc-main .cm__btn--close.cm__btn--secondary svg{stroke:var(--cc-btn-secondary-color)}#cc-main .cm__btn--close.cm__btn--secondary:hover svg{stroke:var(--cc-btn-secondary-hover-color)}#cc-main .cm__btn--close+.cm__texts .cm__title{padding-right:3rem}#cc-main .cm--inline .cm__btn-group{grid-auto-flow:column}#cc-main .cm__footer{background:var(--cc-footer-bg);border-top:1px solid var(--cc-footer-border-color);color:var(--cc-footer-color);padding:.4em 0 .5em}#cc-main .cm__links{display:flex;flex-direction:row;padding-bottom:0;padding-top:0}#cc-main .cm__link-group{display:flex;flex-direction:row;font-size:.8em;width:100%}#cc-main .cm__link-group>*+*{margin-left:1.3rem}#cc-main .cm--flip .cm__btn:last-child{grid-row:1}#cc-main .cm--inline.cm--flip .cm__btn:last-child{grid-column:1}#cc-main .cm--box .cm__btn--close{display:block}#cc-main .cm--box.cm--flip .cm__btns{flex-direction:column-reverse}#cc-main .cm--box.cm--wide{max-width:36em}#cc-main .cm--box.cm--wide .cm__btns{flex-direction:row;justify-content:space-between}#cc-main .cm--box.cm--wide .cm__btn-group{grid-auto-flow:column}#cc-main .cm--box.cm--wide .cm__btn{min-width:120px;padding-left:1.8em;padding-right:1.8em}#cc-main .cm--box.cm--wide.cm--flip .cm__btns{flex-direction:row-reverse}#cc-main .cm--box.cm--wide.cm--flip .cm__btn:last-child{grid-column:1}#cc-main .cm--cloud{max-width:54em;width:unset}#cc-main .cm--cloud .cm__body{flex-direction:row}#cc-main .cm--cloud .cm__texts{flex:1}#cc-main .cm--cloud .cm__desc{max-height:9.4em}#cc-main .cm--cloud .cm__btns{border-left:1px solid var(--cc-separator-border-color);border-top:none;max-width:23em}#cc-main .cm--cloud .cm__btn-group{flex-direction:column}#cc-main .cm--cloud .cm__btn{min-width:19em}#cc-main .cm--cloud.cm--flip .cm__btn-group,#cc-main .cm--cloud.cm--flip .cm__btns{flex-direction:column-reverse}#cc-main .cm--cloud.cm--inline .cm__btn-group{flex-direction:row}#cc-main .cm--cloud.cm--inline .cm__btn{min-width:10em}#cc-main .cm--cloud.cm--inline.cm--flip .cm__btn-group{flex-direction:row-reverse}#cc-main .cm--bar{border-radius:0;left:0;margin:0;max-width:unset;opacity:1;right:0;transform:translateY(0);width:100vw;--cc-modal-transition-duration:.35s}#cc-main .cm--bar.cm--top{top:0;transform:translateY(-100%)}#cc-main .cm--bar.cm--bottom{bottom:0;transform:translateY(100%)}#cc-main .cm--bar .cm__body,#cc-main .cm--bar .cm__links{margin:0 auto;max-width:55em;width:100%}#cc-main .cm--bar .cm__body{padding:.5em 0 .9em}#cc-main .cm--bar .cm__btns{border-top:none;flex-direction:row;justify-content:space-between}#cc-main .cm--bar .cm__btn-group{grid-auto-flow:column}#cc-main .cm--bar:not(.cm--inline) .cm__btn+.cm__btn,#cc-main .cm--bar:not(.cm--inline) .cm__btn-group+.cm__btn-group{margin-left:.375rem;margin-top:0}#cc-main .cm--bar .cm__btn{min-width:120px;padding-left:2em;padding-right:2em}#cc-main .cm--bar.cm--flip:not(.cm--inline) .cm__btn+.cm__btn,#cc-main .cm--bar.cm--flip:not(.cm--inline) .cm__btn-group+.cm__btn-group{margin-bottom:0;margin-left:0;margin-right:.375rem}#cc-main .cm--bar.cm--flip .cm__btns{flex-direction:row-reverse}#cc-main .cm--bar.cm--flip .cm__btn:last-child{grid-column:1}#cc-main .cm--bar.cm--inline .cm__body,#cc-main .cm--bar.cm--inline .cm__links{max-width:74em}#cc-main .cm--bar.cm--inline .cm__body{flex-direction:row;padding:0}#cc-main .cm--bar.cm--inline .cm__btns{flex-direction:column;justify-content:center;max-width:23em}#cc-main .cm--bar.cm--inline.cm--flip .cm__btns{flex-direction:column-reverse}#cc-main .cc--anim .cm,#cc-main .cc--anim.cm-wrapper:before{transition:opacity var(--cc-modal-transition-duration) ease,visibility var(--cc-modal-transition-duration) ease,transform var(--cc-modal-transition-duration) ease}#cc-main .cc--anim .cm__btn,#cc-main .cc--anim .cm__close{transition:background-color .15s ease,border-color .15s ease,color .15s ease}.disable--interaction #cc-main .cm-wrapper:before{background:var(--cc-overlay-bg);bottom:0;content:"";left:0;opacity:0;position:fixed;right:0;top:0;visibility:hidden;z-index:0}.show--consent #cc-main .cc--anim .cm{opacity:1;transform:translateY(0);visibility:visible!important}.show--consent #cc-main .cc--anim .cm--middle{transform:translateY(-50%)}.show--consent #cc-main .cc--anim .cm--bar{transform:translateY(0)}.show--consent #cc-main .cc--anim.cm-wrapper:before{opacity:1;visibility:visible}#cc-main.cc--rtl .cm__btn--close{border-bottom-left-radius:unset;border-bottom-right-radius:var(--cc-btn-border-radius);left:0;right:unset}#cc-main.cc--rtl .cm__btn--close+.cm__texts .cm__title{padding-left:3rem!important;padding-right:1.3rem}#cc-main.cc--rtl .cm--inline .cm__btn+.cm__btn{margin-left:0;margin-right:.375rem}#cc-main.cc--rtl .cm--inline.cm--flip .cm__btn+.cm__btn{margin-left:.375rem;margin-right:0}#cc-main.cc--rtl .cm:not(.cm--inline).cm--bar .cm__btn+.cm__btn,#cc-main.cc--rtl .cm:not(.cm--inline).cm--bar .cm__btn-group+.cm__btn-group,#cc-main.cc--rtl .cm:not(.cm--inline).cm--wide .cm__btn+.cm__btn,#cc-main.cc--rtl .cm:not(.cm--inline).cm--wide .cm__btn-group+.cm__btn-group{margin-left:0;margin-right:.375rem}#cc-main.cc--rtl .cm:not(.cm--inline).cm--bar.cm--flip .cm__btn+.cm__btn,#cc-main.cc--rtl .cm:not(.cm--inline).cm--wide.cm--flip .cm__btn+.cm__btn{margin-left:.375rem;margin-right:0}#cc-main.cc--rtl .cm__link-group>*+*{margin-left:0;margin-right:1.3rem}@media screen and (max-width:640px){#cc-main{--cc-modal-margin:.5em}#cc-main .cm{max-width:none!important;width:auto!important}#cc-main .cm__body{flex-direction:column!important;padding:0!important}#cc-main .cm__btns,#cc-main .cm__desc,#cc-main .cm__links,#cc-main .cm__title{padding-left:1.1rem!important;padding-right:1.1rem!important}#cc-main .cm__btns{border-left:none!important;border-top:1px solid var(--cc-separator-border-color)!important;flex-direction:column!important;max-width:none!important;min-width:auto!important}#cc-main .cm__btn+.cm__btn,#cc-main .cm__btn-group+.cm__btn-group{margin:.375rem 0 0!important}#cc-main .cm--flip .cm__btn+.cm__btn,#cc-main .cm--flip .cm__btn-group+.cm__btn-group{margin-bottom:.375rem!important;margin-top:0!important}#cc-main .cm__btn-group{display:flex!important;flex-direction:column!important;min-width:auto!important}#cc-main .cm__btn{flex:auto!important}#cc-main .cm__link-group{justify-content:center!important}#cc-main .cm--flip .cm__btn-group,#cc-main .cm--flip .cm__btns{flex-direction:column-reverse!important}}#cc-main .pm-wrapper{position:relative;z-index:2}#cc-main .pm{background:var(--cc-bg);border-radius:var(--cc-modal-border-radius);box-shadow:0 .625em 1.875em rgba(0,0,2,.3);display:flex;flex-direction:column;opacity:0;overflow:hidden;position:fixed;visibility:hidden;width:100%;width:unset;z-index:1}#cc-main svg{fill:none;width:100%}#cc-main .pm__body,#cc-main .pm__footer,#cc-main .pm__header{padding:1em 1.4em}#cc-main .pm__header{align-items:center;border-bottom:1px solid var(--cc-separator-border-color);display:flex;justify-content:space-between}#cc-main .pm__title{align-items:center;display:flex;flex:1;font-weight:600;margin-right:2em}#cc-main .pm__close-btn{background:var(--cc-btn-secondary-bg);border:1px solid var(--cc-btn-secondary-border-color);border-radius:var(--cc-btn-border-radius);height:40px;position:relative;transition:all .15s ease;width:40px}#cc-main .pm__close-btn span{display:flex;height:100%;width:100%}#cc-main .pm__close-btn svg{stroke:var(--cc-btn-secondary-color);transform:scale(.5);transition:stroke .15s ease}#cc-main .pm__close-btn:hover{background:var(--cc-btn-secondary-hover-bg);border-color:var(--cc-btn-secondary-hover-border-color)}#cc-main .pm__close-btn:hover svg{stroke:var(--cc-btn-secondary-hover-color)}#cc-main .pm__body{flex:1;overflow-y:auto;overflow-y:overlay}#cc-main .pm__section,#cc-main .pm__section--toggle{border-radius:var(--cc-btn-border-radius);display:flex;flex-direction:column;margin-bottom:.5em}#cc-main .pm__section--toggle .pm__section-desc-wrapper{border:1px solid var(--cc-cookie-category-block-border);border-radius:var(--cc-btn-border-radius);border-top:none;border-top-left-radius:0;border-top-right-radius:0;display:none;margin-top:0!important;overflow:hidden}#cc-main .pm__section{border:1px solid var(--cc-separator-border-color);padding:1em;transition:background-color .25s ease,border-color .25s ease}#cc-main .pm__section:first-child{border:none;margin-bottom:2em;margin-top:0;padding:0;transition:none}#cc-main .pm__section:not(:first-child):hover{background:var(--cc-cookie-category-block-bg);border-color:var(--cc-cookie-category-block-border)}#cc-main .pm__section-toggles+.pm__section{margin-top:2em}#cc-main .pm__section--toggle{background:var(--cc-cookie-category-block-bg);border-top:none;margin-bottom:.375rem}#cc-main .pm__section--toggle .pm__section-title{align-items:center;background:var(--cc-cookie-category-block-bg);border:1px solid var(--cc-cookie-category-block-border);display:flex;justify-content:space-between;min-height:58px;padding:1.1em 5.4em 1.1em 1.2em;position:relative;transition:background-color .25s ease,border-color .25s ease;width:100%}#cc-main .pm__section--toggle .pm__section-title:hover{background:var(--cc-cookie-category-block-hover-bg);border-color:var(--cc-cookie-category-block-hover-border)}#cc-main .pm__section--toggle .pm__section-desc{margin-top:0;padding:1em}#cc-main .pm__section--toggle.is-expanded{--cc-cookie-category-block-bg:var(--cc-cookie-category-expanded-block-bg);--cc-cookie-category-block-border:var(--cc-cookie-category-expanded-block-hover-bg)}#cc-main .pm__section--toggle.is-expanded .pm__section-title{border-bottom-left-radius:0;border-bottom-right-radius:0}#cc-main .pm__section--toggle.is-expanded .pm__section-arrow svg{transform:scale(.5) rotate(180deg)}#cc-main .pm__section--toggle.is-expanded .pm__section-desc-wrapper{display:flex}#cc-main .pm__section--expandable .pm__section-title{cursor:pointer;padding-left:3.4em}#cc-main .pm__section--expandable .pm__section-arrow{background:var(--cc-toggle-readonly-bg);border-radius:100%;display:flex;height:20px;justify-content:center;left:18px;pointer-events:none;position:absolute;width:20px}#cc-main .pm__section--expandable .pm__section-arrow svg{stroke:var(--cc-btn-secondary-color);transform:scale(.5)}#cc-main .pm__section-title-wrapper{align-items:center;display:flex;position:relative}#cc-main .pm__section-title-wrapper+.pm__section-desc-wrapper{margin-top:.85em}#cc-main .pm__section-title{border-radius:var(--cc-btn-border-radius);font-size:.95em;font-weight:600}#cc-main .pm__badge{align-items:center;background:var(--cc-btn-secondary-bg);border-radius:5em;color:var(--cc-secondary-color);display:flex;flex:none;font-size:.8em;font-weight:600;height:23px;justify-content:center;margin-left:1em;min-width:23px;overflow:hidden;padding:0 .6em 1px;position:relative;text-align:center;white-space:nowrap;width:auto}#cc-main .pm__service-counter{background:var(--cc-btn-primary-bg);color:var(--cc-btn-primary-color);padding:0;width:23px}#cc-main .pm__service-counter[data-counterlabel]{padding:0 .6em 1px;width:auto}#cc-main .section__toggle,#cc-main .section__toggle-wrapper,#cc-main .toggle__icon,#cc-main .toggle__label{border-radius:var(--cc-pm-toggle-border-radius);height:23px;transform:translateZ(0);width:50px}#cc-main .section__toggle-wrapper{cursor:pointer;position:absolute;right:18px;z-index:1}#cc-main .toggle-service{height:19px;position:relative;right:0;width:42px}#cc-main .toggle-service .section__toggle,#cc-main .toggle-service .toggle__icon,#cc-main .toggle-service .toggle__label{height:19px;width:42px}#cc-main .toggle-service .toggle__icon{position:relative}#cc-main .toggle-service .toggle__icon-circle{height:19px;width:19px}#cc-main .toggle-service .section__toggle:checked~.toggle__icon .toggle__icon-circle{transform:translateX(23px)}#cc-main .pm__section--toggle:nth-child(2) .section__toggle-wrapper:after{display:none!important}#cc-main .section__toggle{border:0;cursor:pointer;display:block;left:0;margin:0;position:absolute;top:0}#cc-main .section__toggle:disabled{cursor:not-allowed}#cc-main .toggle__icon{background:var(--cc-toggle-off-bg);box-shadow:0 0 0 1px var(--cc-toggle-off-bg);display:flex;flex-direction:row;pointer-events:none;position:absolute;transition:all .25s ease}#cc-main .toggle__icon-circle{background:var(--cc-toggle-off-knob-bg);border:none;border-radius:var(--cc-pm-toggle-border-radius);box-shadow:0 1px 2px rgba(24,32,3,.36);display:block;height:23px;left:0;position:absolute;top:0;transition:transform .25s ease,background-color .25s ease;width:23px}#cc-main .toggle__icon-off,#cc-main .toggle__icon-on{height:100%;position:absolute;transition:opacity .15s ease;width:100%}#cc-main .toggle__icon-on{opacity:0;transform:rotate(45deg)}#cc-main .toggle__icon-on svg{stroke:var(--cc-toggle-on-bg);transform:scale(.55) rotate(-45deg)}#cc-main .toggle__icon-off{opacity:1}#cc-main .toggle__icon-off svg{stroke:var(--cc-toggle-off-bg);transform:scale(.55)}#cc-main .section__toggle:checked~.toggle__icon{background:var(--cc-toggle-on-bg);box-shadow:0 0 0 1px var(--cc-toggle-on-bg)}#cc-main .section__toggle:checked~.toggle__icon .toggle__icon-circle{background-color:var(--cc-toggle-on-knob-bg);transform:translateX(27px)}#cc-main .section__toggle:checked~.toggle__icon .toggle__icon-on{opacity:1}#cc-main .section__toggle:checked~.toggle__icon .toggle__icon-off{opacity:0}#cc-main .section__toggle:checked:disabled~.toggle__icon{background:var(--cc-toggle-readonly-bg);box-shadow:0 0 0 1px var(--cc-toggle-readonly-bg)}#cc-main .section__toggle:checked:disabled~.toggle__icon .toggle__icon-circle{background:var(--cc-toggle-readonly-knob-bg);box-shadow:none}#cc-main .section__toggle:checked:disabled~.toggle__icon svg{stroke:var(--cc-toggle-readonly-knob-icon-color)}#cc-main .toggle__label{opacity:0;overflow:hidden;pointer-events:none;position:absolute;top:0;z-index:-1}#cc-main .pm__section-desc-wrapper{color:var(--cc-secondary-color);display:flex;flex-direction:column;font-size:.9em}#cc-main .pm__section-desc-wrapper>:not(:last-child){border-bottom:1px solid var(--cc-cookie-category-block-border)}#cc-main .pm__section-services{display:flex;flex-direction:column}#cc-main .pm__service{align-items:center;display:flex;justify-content:space-between;padding:.4em 1.2em;position:relative;transition:background-color .15s ease}#cc-main .pm__service:hover{background-color:var(--cc-cookie-category-block-hover-bg)}#cc-main .pm__service-header{align-items:center;display:flex;margin-right:1em;width:100%}#cc-main .pm__service-icon{border:2px solid;border-radius:100%;height:8px;margin-left:6px;margin-right:20px;margin-top:1px;min-width:8px}#cc-main .pm__service-title{font-size:.95em;width:100%;word-break:break-word}#cc-main .pm__section-desc{line-height:1.5em}#cc-main .pm__section-table{border-collapse:collapse;font-size:.9em;margin:0;overflow:hidden;padding:0;text-align:left;width:100%}#cc-main .pm__table-caption{text-align:left}#cc-main .pm__table-caption,#cc-main .pm__table-head>tr{border-bottom:1px dashed var(--cc-separator-border-color);color:var(--cc-primary-color);font-weight:600}#cc-main .pm__table-tr{transition:background-color .15s ease}#cc-main .pm__table-tr:hover{background:var(--cc-cookie-category-block-hover-bg)}#cc-main .pm__table-caption,#cc-main .pm__table-td,#cc-main .pm__table-th{padding:.625em .625em .625em 1.2em;vertical-align:top}#cc-main .pm__footer{border-top:1px solid var(--cc-separator-border-color);display:flex;justify-content:space-between}#cc-main .pm__btn-group{display:flex}#cc-main .pm__btn+.pm__btn,#cc-main .pm__btn-group+.pm__btn-group{margin-left:.375rem}#cc-main .pm--flip .pm__btn+.pm__btn,#cc-main .pm--flip .pm__btn-group+.pm__btn-group{margin-left:0;margin-right:.375rem}#cc-main .pm__btn{background:var(--cc-btn-primary-bg);border:1px solid var(--cc-btn-primary-border-color);border-radius:var(--cc-btn-border-radius);color:var(--cc-btn-primary-color);flex:auto;font-size:.82em;font-weight:600;min-height:42px;min-width:110px;padding:.5em 1.5em;text-align:center;transition:background-color .15s ease,border-color .15s ease,color .15s ease}#cc-main .pm__btn:hover{background:var(--cc-btn-primary-hover-bg);border-color:var(--cc-btn-primary-hover-border-color);color:var(--cc-btn-primary-hover-color)}#cc-main .pm__btn--secondary{background:var(--cc-btn-secondary-bg);border-color:var(--cc-btn-secondary-border-color);color:var(--cc-btn-secondary-color)}#cc-main .pm__btn--secondary:hover{background:var(--cc-btn-secondary-hover-bg);border-color:var(--cc-btn-secondary-hover-border-color);color:var(--cc-btn-secondary-hover-color)}#cc-main .pm--box{height:calc(100% - 2em);left:var(--cc-modal-margin);margin:0 auto;max-height:37.5em;max-width:43em;right:var(--cc-modal-margin);top:50%;transform:translateY(calc(-50% + 1.6em))}#cc-main .pm--box.pm--flip .pm__btn-group,#cc-main .pm--box.pm--flip .pm__footer{flex-direction:row-reverse}#cc-main .pm--bar{border-radius:0;bottom:0;height:100%;margin:0;max-height:none;max-width:29em;opacity:1;top:0;width:100%;--cc-modal-transition-duration:.35s}#cc-main .pm--bar .pm__section-table,#cc-main .pm--bar .pm__table-body,#cc-main .pm--bar .pm__table-td,#cc-main .pm--bar .pm__table-th,#cc-main .pm--bar .pm__table-tr{display:block}#cc-main .pm--bar .pm__table-head{display:none}#cc-main .pm--bar .pm__table-caption{display:block}#cc-main .pm--bar .pm__table-tr:not(:last-child){border-bottom:1px solid var(--cc-separator-border-color)}#cc-main .pm--bar .pm__table-td{display:flex;justify-content:space-between}#cc-main .pm--bar .pm__table-td:before{color:var(--cc-primary-color);content:attr(data-column);flex:1;font-weight:600;min-width:100px;overflow:hidden;padding-right:2em;text-overflow:ellipsis}#cc-main .pm--bar .pm__table-td>div{flex:3}#cc-main .pm--bar:not(.pm--wide) .pm__body,#cc-main .pm--bar:not(.pm--wide) .pm__footer,#cc-main .pm--bar:not(.pm--wide) .pm__header{padding:1em 1.3em}#cc-main .pm--bar:not(.pm--wide) .pm__btn-group,#cc-main .pm--bar:not(.pm--wide) .pm__footer{flex-direction:column}#cc-main .pm--bar:not(.pm--wide) .pm__btn+.pm__btn,#cc-main .pm--bar:not(.pm--wide) .pm__btn-group+.pm__btn-group{margin:.375rem 0 0}#cc-main .pm--bar:not(.pm--wide).pm--flip .pm__btn-group,#cc-main .pm--bar:not(.pm--wide).pm--flip .pm__footer{flex-direction:column-reverse}#cc-main .pm--bar:not(.pm--wide).pm--flip .pm__btn+.pm__btn,#cc-main .pm--bar:not(.pm--wide).pm--flip .pm__btn-group+.pm__btn-group{margin-bottom:.375rem;margin-top:0}#cc-main .pm--bar:not(.pm--wide) .pm__badge{display:none}#cc-main .pm--bar.pm--left{left:0;transform:translateX(-100%)}#cc-main .pm--bar.pm--right{right:0;transform:translateX(100%)}#cc-main .pm--bar.pm--wide{max-width:35em}#cc-main .pm--bar.pm--wide .pm__body,#cc-main .pm--bar.pm--wide .pm__footer,#cc-main .pm--bar.pm--wide .pm__header{padding:1em 1.4em}#cc-main .pm--bar.pm--wide.pm--flip .pm__btn-group,#cc-main .pm--bar.pm--wide.pm--flip .pm__footer{flex-direction:row-reverse}#cc-main .pm-overlay{background:var(--cc-overlay-bg);bottom:0;content:"";left:0;opacity:0;position:fixed;right:0;top:0;visibility:hidden;z-index:1}#cc-main .cc--anim .pm,#cc-main .cc--anim .pm-overlay{transition:opacity var(--cc-modal-transition-duration) ease,visibility var(--cc-modal-transition-duration) ease,transform var(--cc-modal-transition-duration) ease}.show--preferences #cc-main .cc--anim .pm{opacity:1;visibility:visible!important}.show--preferences #cc-main .cc--anim .pm--box{transform:translateY(-50%)}.show--preferences #cc-main .cc--anim .pm--bar{transform:translateX(0)}.show--preferences #cc-main .cc--anim .pm-overlay{opacity:1;visibility:visible}#cc-main.cc--rtl .pm__service-header{margin-left:1em;margin-right:0}#cc-main.cc--rtl .pm__section-arrow{left:unset;right:18px}#cc-main.cc--rtl .section__toggle-wrapper{left:18px;right:unset;transform-origin:left}#cc-main.cc--rtl .toggle-service{left:0}#cc-main.cc--rtl .pm__service-icon{margin-left:20px;margin-right:5px}#cc-main.cc--rtl .pm__section--toggle .pm__section-title{padding-left:5.4em;padding-right:1.2em}#cc-main.cc--rtl .pm__section--expandable .pm__section-title{padding-right:3.4em}#cc-main.cc--rtl .pm__badge{margin-left:unset;margin-right:1em}#cc-main.cc--rtl .toggle__icon-circle{transform:translateX(27px)}#cc-main.cc--rtl .toggle-service .toggle__icon-circle{transform:translateX(23px)}#cc-main.cc--rtl .section__toggle:checked~.toggle__icon .toggle__icon-circle{transform:translateX(0)}#cc-main.cc--rtl .pm__table-td,#cc-main.cc--rtl .pm__table-th{padding-left:unset;padding-right:1.2em;text-align:right}#cc-main.cc--rtl .pm__table-td{padding-left:unset;padding-right:1.2em}#cc-main.cc--rtl .pm__table-td:before{padding-left:2em;padding-right:unset}#cc-main.cc--rtl .pm__btn+.pm__btn,#cc-main.cc--rtl .pm__btn-group+.pm__btn-group{margin-left:0;margin-right:.375rem}#cc-main.cc--rtl .pm--flip .pm__btn+.pm__btn,#cc-main.cc--rtl .pm--flip .pm__btn-group+.pm__btn-group{margin-left:.375rem;margin-right:0}#cc-main.cc--rtl .pm--flip.pm--bar:not(.pm--wide) .pm__btn+.pm__btn,#cc-main.cc--rtl .pm--flip.pm--bar:not(.pm--wide) .pm__btn-group+.pm__btn-group{margin-left:0}@media screen and (max-width:640px){#cc-main .pm{border-radius:0;bottom:0;height:auto;left:0;max-height:100%;max-width:none!important;right:0;top:0;transform:translateY(1.6em)}#cc-main .pm__body,#cc-main .pm__footer,#cc-main .pm__header{padding:.9em!important}#cc-main .pm__badge{display:none}#cc-main .pm__section-table,#cc-main .pm__table-body,#cc-main .pm__table-caption,#cc-main .pm__table-td,#cc-main .pm__table-th,#cc-main .pm__table-tr{display:block}#cc-main .pm__table-head{display:none}#cc-main .pm__table-tr:not(:last-child){border-bottom:1px solid var(--cc-separator-border-color)}#cc-main .pm__table-td{display:flex;justify-content:space-between}#cc-main .pm__table-td:before{color:var(--cc-primary-color);content:attr(data-column);flex:1;font-weight:600;min-width:100px;overflow:hidden;padding-right:2em;text-overflow:ellipsis}#cc-main .pm__table-td>div{flex:3}#cc-main .pm__btn-group,#cc-main .pm__footer{flex-direction:column!important}#cc-main .pm__btn-group{display:flex!important}#cc-main .pm__btn+.pm__btn,#cc-main .pm__btn-group+.pm__btn-group{margin:.375rem 0 0!important}#cc-main .pm--flip .pm__btn+.pm__btn,#cc-main .pm--flip .pm__btn-group+.pm__btn-group{margin-bottom:.375rem!important;margin-top:0!important}#cc-main .pm--flip .pm__btn-group,#cc-main .pm--flip .pm__footer{flex-direction:column-reverse!important}.show--preferences #cc-main .cc--anim .pm{transform:translateY(0)!important}} \ No newline at end of file diff --git a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css b/app/core/src/main/resources/static/css/cookieconsentCustomisation.css deleted file mode 100644 index fd1a8ff355..0000000000 --- a/app/core/src/main/resources/static/css/cookieconsentCustomisation.css +++ /dev/null @@ -1,205 +0,0 @@ -/* Light theme variables */ -:root { - --cc-bg: #ffffff; - --cc-primary-color: #1c1c1c; - --cc-secondary-color: #666666; - - --cc-btn-primary-bg: #007bff; - --cc-btn-primary-color: #ffffff; - --cc-btn-primary-border-color: #007bff; - --cc-btn-primary-hover-bg: #0056b3; - --cc-btn-primary-hover-color: #ffffff; - --cc-btn-primary-hover-border-color: #0056b3; - - --cc-btn-secondary-bg: #f1f3f4; - --cc-btn-secondary-color: #1c1c1c; - --cc-btn-secondary-border-color: #f1f3f4; - --cc-btn-secondary-hover-bg: #007bff; - --cc-btn-secondary-hover-color: #ffffff; - --cc-btn-secondary-hover-border-color: #007bff; - - --cc-separator-border-color: #e0e0e0; - - --cc-toggle-on-bg: #007bff; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #ffffff; - --cc-toggle-off-knob-bg: #ffffff; - - --cc-toggle-enabled-icon-color: #ffffff; - --cc-toggle-disabled-icon-color: #ffffff; - - --cc-toggle-readonly-bg: #f1f3f4; - --cc-toggle-readonly-knob-bg: #79747e; - --cc-toggle-readonly-knob-icon-color: #f1f3f4; - - --cc-section-category-border: #e0e0e0; - - --cc-cookie-category-block-bg: #f1f3f4; - --cc-cookie-category-block-border: #f1f3f4; - --cc-cookie-category-block-hover-bg: #e9eff4; - --cc-cookie-category-block-hover-border: #e9eff4; - - --cc-cookie-category-expanded-block-bg: #f1f3f4; - --cc-cookie-category-expanded-block-hover-bg: #e9eff4; - - --cc-footer-bg: #ffffff; - --cc-footer-color: #1c1c1c; - --cc-footer-border-color: #ffffff; -} - -/* Dark theme variables */ -.cc--darkmode { - --cc-bg: #2d2d2d; - --cc-primary-color: #e5e5e5; - --cc-secondary-color: #b0b0b0; - - --cc-btn-primary-bg: #4dabf7; - --cc-btn-primary-color: #ffffff; - --cc-btn-primary-border-color: #4dabf7; - --cc-btn-primary-hover-bg: #3d3d3d; - --cc-btn-primary-hover-color: #ffffff; - --cc-btn-primary-hover-border-color: #3d3d3d; - - --cc-btn-secondary-bg: #3d3d3d; - --cc-btn-secondary-color: #ffffff; - --cc-btn-secondary-border-color: #3d3d3d; - --cc-btn-secondary-hover-bg: #4dabf7; - --cc-btn-secondary-hover-color: #ffffff; - --cc-btn-secondary-hover-border-color: #4dabf7; - - --cc-separator-border-color: #555555; - - --cc-toggle-on-bg: #4dabf7; - --cc-toggle-off-bg: #667481; - --cc-toggle-on-knob-bg: #2d2d2d; - --cc-toggle-off-knob-bg: #2d2d2d; - - --cc-toggle-enabled-icon-color: #2d2d2d; - --cc-toggle-disabled-icon-color: #2d2d2d; - - --cc-toggle-readonly-bg: #555555; - --cc-toggle-readonly-knob-bg: #8e8e8e; - --cc-toggle-readonly-knob-icon-color: #555555; - - --cc-section-category-border: #555555; - - --cc-cookie-category-block-bg: #3d3d3d; - --cc-cookie-category-block-border: #3d3d3d; - --cc-cookie-category-block-hover-bg: #4d4d4d; - --cc-cookie-category-block-hover-border: #4d4d4d; - - --cc-cookie-category-expanded-block-bg: #3d3d3d; - --cc-cookie-category-expanded-block-hover-bg: #4d4d4d; - - --cc-footer-bg: #2d2d2d; - --cc-footer-color: #e5e5e5; - --cc-footer-border-color: #2d2d2d; -} -.cm__body { - max-width: 90% !important; - flex-direction: row !important; - align-items: center !important; -} - -.cm__desc { - max-width: 70rem !important; -} - -.cm__btns { - flex-direction: row-reverse !important; - gap: 10px !important; - padding-top: 3.4rem !important; -} - -@media only screen and (max-width: 1400px) { - .cm__body { - max-width: 90% !important; - flex-direction: column !important; - align-items: normal !important; - } - - .cm__btns { - padding-top: 1rem !important; - } -} - -/* Toggle visibility fixes */ -#cc-main .section__toggle { - opacity: 0 !important; /* Keep invisible but functional */ -} - -#cc-main .toggle__icon { - display: flex !important; - align-items: center !important; - justify-content: flex-start !important; -} - -#cc-main .toggle__icon-circle { - display: block !important; - position: absolute !important; - transition: transform 0.25s ease !important; -} - -#cc-main .toggle__icon-on, -#cc-main .toggle__icon-off { - display: flex !important; - align-items: center !important; - justify-content: center !important; - position: absolute !important; - width: 100% !important; - height: 100% !important; -} - -/* Ensure toggles are visible in both themes */ -#cc-main .toggle__icon { - background: var(--cc-toggle-off-bg) !important; - border: 1px solid var(--cc-toggle-off-bg) !important; -} - -#cc-main .section__toggle:checked ~ .toggle__icon { - background: var(--cc-toggle-on-bg) !important; - border: 1px solid var(--cc-toggle-on-bg) !important; -} - -/* Ensure toggle text is visible */ -#cc-main .pm__section-title { - color: var(--cc-primary-color) !important; -} - -#cc-main .pm__section-desc { - color: var(--cc-secondary-color) !important; -} - -/* Make sure the modal has proper contrast */ -#cc-main .pm { - background: var(--cc-bg) !important; - color: var(--cc-primary-color) !important; -} - -/* Lower z-index so cookie banner appears behind onboarding modals */ -#cc-main { - z-index: 100 !important; -} - -/* Ensure consent modal text is visible in both themes */ -#cc-main .cm { - background: var(--cc-bg) !important; - color: var(--cc-primary-color) !important; -} - -#cc-main .cm__title { - color: var(--cc-primary-color) !important; -} - -#cc-main .cm__desc { - color: var(--cc-primary-color) !important; -} - -#cc-main .cm__footer { - color: var(--cc-primary-color) !important; -} - -#cc-main .cm__footer-links a, -#cc-main .cm__link { - color: var(--cc-primary-color) !important; -} diff --git a/app/core/src/main/resources/static/favicon-16x16.png b/app/core/src/main/resources/static/favicon-16x16.png index d88e6615d0..6448ce5424 100644 Binary files a/app/core/src/main/resources/static/favicon-16x16.png and b/app/core/src/main/resources/static/favicon-16x16.png differ diff --git a/app/core/src/main/resources/static/favicon-32x32.png b/app/core/src/main/resources/static/favicon-32x32.png index f44f0c371c..1343e2632d 100644 Binary files a/app/core/src/main/resources/static/favicon-32x32.png and b/app/core/src/main/resources/static/favicon-32x32.png differ diff --git a/app/core/src/main/resources/static/favicon.icns b/app/core/src/main/resources/static/favicon.icns index 7b281937e8..86bad6a2e1 100644 Binary files a/app/core/src/main/resources/static/favicon.icns and b/app/core/src/main/resources/static/favicon.icns differ diff --git a/app/core/src/main/resources/static/favicon.ico b/app/core/src/main/resources/static/favicon.ico index 8ad57cac70..2351219afa 100644 Binary files a/app/core/src/main/resources/static/favicon.ico and b/app/core/src/main/resources/static/favicon.ico differ diff --git a/app/core/src/main/resources/static/favicon.png b/app/core/src/main/resources/static/favicon.png deleted file mode 100644 index 5edc6eae26..0000000000 Binary files a/app/core/src/main/resources/static/favicon.png and /dev/null differ diff --git a/app/core/src/main/resources/static/favicon.svg b/app/core/src/main/resources/static/favicon.svg index 0fef4393aa..b5455291c9 100644 --- a/app/core/src/main/resources/static/favicon.svg +++ b/app/core/src/main/resources/static/favicon.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/app/core/src/main/resources/static/images/Files.svg b/app/core/src/main/resources/static/images/Files.svg deleted file mode 100644 index be11edc054..0000000000 --- a/app/core/src/main/resources/static/images/Files.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/arrow-right-short.svg b/app/core/src/main/resources/static/images/arrow-right-short.svg deleted file mode 100644 index 86978c4ffa..0000000000 --- a/app/core/src/main/resources/static/images/arrow-right-short.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/book.svg b/app/core/src/main/resources/static/images/book.svg deleted file mode 100644 index 4634bfcb91..0000000000 --- a/app/core/src/main/resources/static/images/book.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/clipboard.svg b/app/core/src/main/resources/static/images/clipboard.svg deleted file mode 100644 index f46fc6d28f..0000000000 --- a/app/core/src/main/resources/static/images/clipboard.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/discord.svg b/app/core/src/main/resources/static/images/discord.svg deleted file mode 100644 index 1f1592dbac..0000000000 --- a/app/core/src/main/resources/static/images/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/docker.svg b/app/core/src/main/resources/static/images/docker.svg deleted file mode 100644 index ab6b8b05d7..0000000000 --- a/app/core/src/main/resources/static/images/docker.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/file-earmark-pdf.svg b/app/core/src/main/resources/static/images/file-earmark-pdf.svg deleted file mode 100644 index af24849081..0000000000 --- a/app/core/src/main/resources/static/images/file-earmark-pdf.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/github.svg b/app/core/src/main/resources/static/images/github.svg deleted file mode 100644 index 82a1aeb7d4..0000000000 --- a/app/core/src/main/resources/static/images/github.svg +++ /dev/null @@ -1 +0,0 @@ -Github-colorCreated with Sketch. \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/google-drive.svg b/app/core/src/main/resources/static/images/google-drive.svg deleted file mode 100644 index 03b2f21290..0000000000 --- a/app/core/src/main/resources/static/images/google-drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/redact-auto.svg b/app/core/src/main/resources/static/images/redact-auto.svg deleted file mode 100644 index 7c29b0fc4b..0000000000 --- a/app/core/src/main/resources/static/images/redact-auto.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/redact-manual.svg b/app/core/src/main/resources/static/images/redact-manual.svg deleted file mode 100644 index 62296625e3..0000000000 --- a/app/core/src/main/resources/static/images/redact-manual.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/core/src/main/resources/static/images/rename.svg b/app/core/src/main/resources/static/images/rename.svg deleted file mode 100644 index 97946a086d..0000000000 --- a/app/core/src/main/resources/static/images/rename.svg +++ /dev/null @@ -1 +0,0 @@ -rename \ No newline at end of file diff --git a/app/core/src/main/resources/static/images/signature.png b/app/core/src/main/resources/static/images/signature.png index 1adfcedc3c..7c55bc2928 100644 Binary files a/app/core/src/main/resources/static/images/signature.png and b/app/core/src/main/resources/static/images/signature.png differ diff --git a/app/core/src/main/resources/static/images/split-auto.svg b/app/core/src/main/resources/static/images/split-auto.svg deleted file mode 100644 index 1aa08fcc39..0000000000 --- a/app/core/src/main/resources/static/images/split-auto.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/core/src/main/resources/static/images/split-chapters.svg b/app/core/src/main/resources/static/images/split-chapters.svg deleted file mode 100644 index 4a3ad37d1a..0000000000 --- a/app/core/src/main/resources/static/images/split-chapters.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/core/src/main/resources/static/images/split-size.svg b/app/core/src/main/resources/static/images/split-size.svg deleted file mode 100644 index 9fd1840fce..0000000000 --- a/app/core/src/main/resources/static/images/split-size.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/core/src/main/resources/static/images/update.svg b/app/core/src/main/resources/static/images/update.svg deleted file mode 100644 index b043120221..0000000000 --- a/app/core/src/main/resources/static/images/update.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js b/app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js deleted file mode 100644 index 7f85a316a1..0000000000 --- a/app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! -* CookieConsent 3.1.0 -* https://github.com/orestbida/cookieconsent -* Author Orest Bida -* Released under the MIT License -*/ -var e,t;e=this,t=function(e){'use strict';const t='opt-in',o='opt-out',n='show--consent',s='show--preferences',a='disable--interaction',c='data-category',r='div',i='button',l='aria-hidden',d='btn-group',f='click',_='data-role',u='consentModal',p='preferencesModal';class g{constructor(){this.t={mode:t,revision:0,autoShow:!0,lazyHtmlGeneration:!0,autoClearCookies:!0,manageScriptTags:!0,hideFromBots:!0,cookie:{name:'cc_cookie',expiresAfterDays:182,domain:'',path:'/',secure:!0,sameSite:'Lax'}},this.o={i:{},l:'',_:{},u:{},p:{},m:[],v:!1,h:null,C:null,S:null,M:'',T:!0,D:!1,k:!1,A:!1,N:!1,H:[],V:!1,j:!0,I:[],L:!1,F:'',P:!1,O:[],R:[],B:[],$:[],G:!1,J:!1,U:!1,q:[],K:[],W:[],X:{},Y:{},Z:{},ee:{},te:{},oe:[]},this.ne={se:{},ae:{}},this.ce={},this.re={ie:'cc:onFirstConsent',le:'cc:onConsent',de:'cc:onChange',fe:'cc:onModalShow',_e:'cc:onModalHide',ue:'cc:onModalReady'}}}const m=new g,b=(e,t)=>e.indexOf(t),v=(e,t)=>-1!==b(e,t),y=e=>Array.isArray(e),h=e=>'string'==typeof e,C=e=>!!e&&'object'==typeof e&&!y(e),w=e=>'function'==typeof e,S=e=>Object.keys(e),x=e=>Array.from(new Set(e)),M=()=>document.activeElement,T=e=>e.preventDefault(),D=(e,t)=>e.querySelectorAll(t),k=e=>e.dispatchEvent(new Event('change')),E=e=>{const t=document.createElement(e);return e===i&&(t.type=e),t},A=(e,t,o)=>e.setAttribute(t,o),N=(e,t,o)=>{e.removeAttribute(o?'data-'+t:t)},H=(e,t,o)=>e.getAttribute(o?'data-'+t:t),V=(e,t)=>e.appendChild(t),j=(e,t)=>e.classList.add(t),I=(e,t)=>j(e,'cm__'+t),L=(e,t)=>j(e,'pm__'+t),F=(e,t)=>e.classList.remove(t),P=e=>{if('object'!=typeof e)return e;if(e instanceof Date)return new Date(e.getTime());let t=Array.isArray(e)?[]:{};for(let o in e){let n=e[o];t[o]=P(n)}return t},O=()=>{const e={},{O:t,X:o,Y:n}=m.o;for(const s of t)e[s]=G(n[s],S(o[s]));return e},R=(e,t)=>dispatchEvent(new CustomEvent(e,{detail:t})),B=(e,t,o,n)=>{e.addEventListener(t,o),n&&m.o.m.push({pe:e,ge:t,me:o})},$=()=>{const e=m.t.cookie.expiresAfterDays;return w(e)?e(m.o.F):e},G=(e,t)=>{const o=e||[],n=t||[];return o.filter((e=>!v(n,e))).concat(n.filter((e=>!v(o,e))))},J=e=>{m.o.R=x(e),m.o.F=(()=>{let e='custom';const{R:t,O:o,B:n}=m.o,s=t.length;return s===o.length?e='all':s===n.length&&(e='necessary'),e})()},U=(e,t,o,n)=>{const s='accept-',{show:a,showPreferences:c,hide:r,hidePreferences:i,acceptCategory:l}=t,d=e||document,_=e=>D(d,`[data-cc="${e}"]`),u=(e,t)=>{T(e),l(t),i(),r()},p=_('show-preferencesModal'),g=_('show-consentModal'),b=_(s+'all'),v=_(s+'necessary'),y=_(s+'custom'),h=m.t.lazyHtmlGeneration;for(const e of p)A(e,'aria-haspopup','dialog'),B(e,f,(e=>{T(e),c()})),h&&(B(e,'mouseenter',(e=>{T(e),m.o.N||o(t,n)}),!0),B(e,'focus',(()=>{m.o.N||o(t,n)})));for(let e of g)A(e,'aria-haspopup','dialog'),B(e,f,(e=>{T(e),a(!0)}),!0);for(let e of b)B(e,f,(e=>{u(e,'all')}),!0);for(let e of y)B(e,f,(e=>{u(e)}),!0);for(let e of v)B(e,f,(e=>{u(e,[])}),!0)},z=(e,t)=>{e&&(t&&(e.tabIndex=-1),e.focus(),t&&e.removeAttribute('tabindex'))},q=(e,t)=>{const o=n=>{n.target.removeEventListener('transitionend',o),'opacity'===n.propertyName&&'1'===getComputedStyle(e).opacity&&z((e=>1===e?m.ne.be:m.ne.ve)(t))};B(e,'transitionend',o)};let K;const Q=e=>{clearTimeout(K),e?j(m.ne.ye,a):K=setTimeout((()=>{F(m.ne.ye,a)}),500)},W=['M 19.5 4.5 L 4.5 19.5 M 4.5 4.501 L 19.5 19.5','M 3.572 13.406 L 8.281 18.115 L 20.428 5.885','M 21.999 6.94 L 11.639 17.18 L 2.001 6.82 '],X=(e=0,t=1.5)=>``,Y=e=>{const t=m.ne,o=m.o;(e=>{const n=e===t.he,s=o.i.disablePageInteraction?t.ye:n?t.Ce:t.ye;B(s,'keydown',(t=>{if('Tab'!==t.key||!(n?o.k&&!o.A:o.A))return;const s=M(),a=n?o.q:o.K;0!==a.length&&(t.shiftKey?s!==a[0]&&e.contains(s)||(T(t),z(a[1])):s!==a[1]&&e.contains(s)||(T(t),z(a[0])))}),!0)})(e)},Z=['[href]',i,'input','details','[tabindex]'].map((e=>e+':not([tabindex="-1"])')).join(','),ee=e=>{const{o:t,ne:o}=m,n=(e,t)=>{const o=D(e,Z);t[0]=o[0],t[1]=o[o.length-1]};1===e&&t.D&&n(o.he,t.q),2===e&&t.N&&n(o.we,t.K)},te=(e,t,o)=>{const{de:n,le:s,ie:a,_e:c,ue:r,fe:i}=m.ce,l=m.re;if(t){const n={modalName:t};return e===l.fe?w(i)&&i(n):e===l._e?w(c)&&c(n):(n.modal=o,w(r)&&r(n)),R(e,n)}const d={cookie:m.o.p};e===l.ie?w(a)&&a(P(d)):e===l.le?w(s)&&s(P(d)):(d.changedCategories=m.o.I,d.changedServices=m.o.ee,w(n)&&n(P(d))),R(e,P(d))},oe=(e,t)=>{try{return e()}catch(e){return!t&&console.warn('CookieConsent:',e),!1}},ne=e=>{const{Y:t,ee:o,O:n,X:s,oe:a,p:r,I:i}=m.o;for(const e of n){const n=o[e]||t[e]||[];for(const o of n){const n=s[e][o];if(!n)continue;const{onAccept:a,onReject:c}=n;!n.Se&&v(t[e],o)?(n.Se=!0,w(a)&&a()):n.Se&&!v(t[e],o)&&(n.Se=!1,w(c)&&c())}}if(!m.t.manageScriptTags)return;const l=a,d=e||r.categories||[],f=(e,n)=>{if(n>=e.length)return;const s=a[n];if(s.xe)return f(e,n+1);const r=s.Me,l=s.Te,_=s.De,u=v(d,l),p=!!_&&v(t[l],_);if(!_&&!s.ke&&u||!_&&s.ke&&!u&&v(i,l)||_&&!s.ke&&p||_&&s.ke&&!p&&v(o[l]||[],_)){s.xe=!0;const t=H(r,'type',!0);N(r,'type',!!t),N(r,c);let o=H(r,'src',!0);o&&N(r,'src',!0);const a=E('script');a.textContent=r.innerHTML;for(const{nodeName:e}of r.attributes)A(a,e,r[e]||H(r,e));t&&(a.type=t),o?a.src=o:o=r.src;const i=!!o&&(!t||['text/javascript','module'].includes(t));if(i&&(a.onload=a.onerror=()=>{f(e,++n)}),r.replaceWith(a),i)return}f(e,++n)};f(l,0)},se='bottom',ae='left',ce='center',re='right',ie='inline',le='wide',de='pm--',fe=['middle','top',se],_e=[ae,ce,re],ue={box:{Ee:[le,ie],Ae:fe,Ne:_e,He:se,Ve:re},cloud:{Ee:[ie],Ae:fe,Ne:_e,He:se,Ve:ce},bar:{Ee:[ie],Ae:fe.slice(1),Ne:[],He:se,Ve:''}},pe={box:{Ee:[],Ae:[],Ne:[],He:'',Ve:''},bar:{Ee:[le],Ae:[],Ne:[ae,re],He:'',Ve:ae}},ge=e=>{const t=m.o.i.guiOptions,o=t&&t.consentModal,n=t&&t.preferencesModal;0===e&&me(m.ne.he,ue,o,'cm--','box','cm'),1===e&&me(m.ne.we,pe,n,de,'box','pm')},me=(e,t,o,n,s,a)=>{e.className=a;const c=o&&o.layout,r=o&&o.position,i=o&&o.flipButtons,l=!o||!1!==o.equalWeightButtons,d=c&&c.split(' ')||[],f=d[0],_=d[1],u=f in t?f:s,p=t[u],g=v(p.Ee,_)&&_,b=r&&r.split(' ')||[],y=b[0],h=n===de?b[0]:b[1],C=v(p.Ae,y)?y:p.He,w=v(p.Ne,h)?h:p.Ve,S=t=>{t&&j(e,n+t)};S(u),S(g),S(C),S(w),i&&S('flip');const x=a+'__btn--secondary';if('cm'===a){const{je:e,Ie:t}=m.ne;e&&(l?F(e,x):j(e,x)),t&&(l?F(t,x):j(t,x))}else{const{Le:e}=m.ne;e&&(l?F(e,x):j(e,x))}},be=(e,t)=>{const o=m.o,n=m.ne,{hide:s,hidePreferences:a,acceptCategory:c}=e,u=e=>{c(e),a(),s()},g=o.u&&o.u.preferencesModal;if(!g)return;const b=g.title,v=g.closeIconLabel,y=g.acceptAllBtn,w=g.acceptNecessaryBtn,x=g.savePreferencesBtn,M=g.sections||[],T=y||w||x;if(n.Fe)n.Pe=E(r),L(n.Pe,'body');else{n.Fe=E(r),j(n.Fe,'pm-wrapper');const e=E('div');j(e,'pm-overlay'),V(n.Fe,e),B(e,f,a),n.we=E(r),j(n.we,'pm'),A(n.we,'role','dialog'),A(n.we,l,!0),A(n.we,'aria-modal',!0),A(n.we,'aria-labelledby','pm__title'),B(n.ye,'keydown',(e=>{27===e.keyCode&&a()}),!0),n.Oe=E(r),L(n.Oe,'header'),n.Re=E('h2'),L(n.Re,'title'),n.Re.id='pm__title',n.Be=E(i),L(n.Be,'close-btn'),A(n.Be,'aria-label',g.closeIconLabel||''),B(n.Be,f,a),n.$e=E('span'),n.$e.innerHTML=X(),V(n.Be,n.$e),n.Ge=E(r),L(n.Ge,'body'),n.Je=E(r),L(n.Je,'footer');var D=E(r);j(D,'btns');var k=E(r),N=E(r);L(k,d),L(N,d),V(n.Je,k),V(n.Je,N),V(n.Oe,n.Re),V(n.Oe,n.Be),n.ve=E(r),A(n.ve,'tabIndex',-1),V(n.we,n.ve),V(n.we,n.Oe),V(n.we,n.Ge),T&&V(n.we,n.Je),V(n.Fe,n.we)}let H;b&&(n.Re.innerHTML=b,v&&A(n.Be,'aria-label',v)),M.forEach(((e,t)=>{const s=e.title,a=e.description,c=e.linkedCategory,d=c&&o.P[c],_=e.cookieTable,u=_&&_.body,p=_&&_.caption,m=u&&u.length>0,b=!!d,v=b&&o.X[c],y=C(v)&&S(v)||[],w=b&&(!!a||!!m||S(v).length>0);var x=E(r);if(L(x,'section'),w||a){var M=E(r);L(M,'section-desc-wrapper')}let T=y.length;if(w&&T>0){const e=E(r);L(e,'section-services');for(const t of y){const o=v[t],n=o&&o.label||t,s=E(r),a=E(r),i=E(r),l=E(r);L(s,'service'),L(l,'service-title'),L(a,'service-header'),L(i,'service-icon');const f=ve(n,t,d,!0,c);l.innerHTML=n,V(a,i),V(a,l),V(s,a),V(s,f),V(e,s)}V(M,e)}if(s){var D=E(r),k=E(b?i:r);if(L(D,'section-title-wrapper'),L(k,'section-title'),k.innerHTML=s,V(D,k),b){const e=E('span');e.innerHTML=X(2,3.5),L(e,'section-arrow'),V(D,e),x.className+='--toggle';const t=ve(s,c,d);let o=g.serviceCounterLabel;if(T>0&&h(o)){let e=E('span');L(e,'badge'),L(e,'service-counter'),A(e,l,!0),A(e,'data-servicecounter',T),o&&(o=o.split('|'),o=o.length>1&&T>1?o[1]:o[0],A(e,'data-counterlabel',o)),e.innerHTML=T+(o?' '+o:''),V(k,e)}if(w){L(x,'section--expandable');var N=c+'-desc';A(k,'aria-expanded',!1),A(k,'aria-controls',N)}V(D,t)}else A(k,'role','heading'),A(k,'aria-level','3');V(x,D)}if(a){var I=E('p');L(I,'section-desc'),I.innerHTML=a,V(M,I)}if(w&&(A(M,l,'true'),M.id=N,((e,t,o)=>{B(k,f,(()=>{t.classList.contains('is-expanded')?(F(t,'is-expanded'),A(o,'aria-expanded','false'),A(e,l,'true')):(j(t,'is-expanded'),A(o,'aria-expanded','true'),A(e,l,'false'))}))})(M,x,k),m)){const e=E('table'),o=E('thead'),s=E('tbody');if(p){const t=E('caption');L(t,'table-caption'),t.innerHTML=p,e.appendChild(t)}L(e,'section-table'),L(o,'table-head'),L(s,'table-body');const a=_.headers,c=S(a),i=n.Ue.createDocumentFragment(),l=E('tr');for(const e of c){const o=a[e],n=E('th');n.id='cc__row-'+o+t,A(n,'scope','col'),L(n,'table-th'),n.innerHTML=o,V(i,n)}V(l,i),V(o,l);const d=n.Ue.createDocumentFragment();for(const e of u){const o=E('tr');L(o,'table-tr');for(const n of c){const s=a[n],c=e[n],i=E('td'),l=E(r);L(i,'table-td'),A(i,'data-column',s),A(i,'headers','cc__row-'+s+t),l.insertAdjacentHTML('beforeend',c),V(i,l),V(o,i)}V(d,o)}V(s,d),V(e,o),V(e,s),V(M,e)}(w||a)&&V(x,M);const P=n.Pe||n.Ge;b?(H||(H=E(r),L(H,'section-toggles')),H.appendChild(x)):H=null,V(P,H||x)})),y&&(n.ze||(n.ze=E(i),L(n.ze,'btn'),A(n.ze,_,'all'),V(k,n.ze),B(n.ze,f,(()=>u('all')))),n.ze.innerHTML=y),w&&(n.Le||(n.Le=E(i),L(n.Le,'btn'),A(n.Le,_,'necessary'),V(k,n.Le),B(n.Le,f,(()=>u([])))),n.Le.innerHTML=w),x&&(n.qe||(n.qe=E(i),L(n.qe,'btn'),L(n.qe,'btn--secondary'),A(n.qe,_,'save'),V(N,n.qe),B(n.qe,f,(()=>u()))),n.qe.innerHTML=x),n.Pe&&(n.we.replaceChild(n.Pe,n.Ge),n.Ge=n.Pe),ge(1),o.N||(o.N=!0,te(m.re.ue,p,n.we),t(e),V(n.Ce,n.Fe),Y(n.we),setTimeout((()=>j(n.Fe,'cc--anim')),100)),ee(2)};function ve(e,t,o,n,s){const a=m.o,r=m.ne,i=E('label'),d=E('input'),_=E('span'),u=E('span'),p=E('span'),g=E('span'),b=E('span');if(g.innerHTML=X(1,3),b.innerHTML=X(0,3),d.type='checkbox',j(i,'section__toggle-wrapper'),j(d,'section__toggle'),j(g,'toggle__icon-on'),j(b,'toggle__icon-off'),j(_,'toggle__icon'),j(u,'toggle__icon-circle'),j(p,'toggle__label'),A(_,l,'true'),n?(j(i,'toggle-service'),A(d,c,s),r.ae[s][t]=d):r.se[t]=d,n?(e=>{B(d,'change',(()=>{const t=r.ae[e],o=r.se[e];a.Z[e]=[];for(let o in t){const n=t[o];n.checked&&a.Z[e].push(n.value)}o.checked=a.Z[e].length>0}))})(s):(e=>{B(d,f,(()=>{const t=r.ae[e],o=d.checked;a.Z[e]=[];for(let n in t)t[n].checked=o,o&&a.Z[e].push(n)}))})(t),d.value=t,p.textContent=e.replace(/<.*>.*<\/.*>/gm,''),V(u,b),V(u,g),V(_,u),a.T)(o.readOnly||o.enabled)&&(d.checked=!0);else if(n){const e=a.Y[s];d.checked=o.readOnly||v(e,t)}else v(a.R,t)&&(d.checked=!0);return o.readOnly&&(d.disabled=!0),V(i,d),V(i,_),V(i,p),i}const ye=()=>{const e=E('span');return m.ne.Ke||(m.ne.Ke=e),e},he=(e,t)=>{const o=m.o,n=m.ne,{hide:s,showPreferences:a,acceptCategory:c}=e,p=o.u&&o.u.consentModal;if(!p)return;const g=p.acceptAllBtn,b=p.acceptNecessaryBtn,v=p.showPreferencesBtn,y=p.closeIconLabel,h=p.footer,C=p.label,w=p.title,S=e=>{s(),c(e)};if(!n.Qe){n.Qe=E(r),n.he=E(r),n.We=E(r),n.Xe=E(r),n.Ye=E(r),j(n.Qe,'cm-wrapper'),j(n.he,'cm'),I(n.We,'body'),I(n.Xe,'texts'),I(n.Ye,'btns'),A(n.he,'role','dialog'),A(n.he,'aria-modal','true'),A(n.he,l,'false'),A(n.he,'aria-describedby','cm__desc'),C?A(n.he,'aria-label',C):w&&A(n.he,'aria-labelledby','cm__title');const e='box',t=o.i.guiOptions,s=t&&t.consentModal,a=(s&&s.layout||e).split(' ')[0]===e;w&&y&&a&&(n.Ie||(n.Ie=E(i),n.Ie.innerHTML=X(),I(n.Ie,'btn'),I(n.Ie,'btn--close'),B(n.Ie,f,(()=>{S([])})),V(n.We,n.Ie)),A(n.Ie,'aria-label',y)),V(n.We,n.Xe),(g||b||v)&&V(n.We,n.Ye),n.be=E(r),A(n.be,'tabIndex',-1),V(n.he,n.be),V(n.he,n.We),V(n.Qe,n.he)}w&&(n.Ze||(n.Ze=E('h2'),n.Ze.className=n.Ze.id='cm__title',V(n.Xe,n.Ze)),n.Ze.innerHTML=w);let x=p.description;if(x&&(o.V&&(x=x.replace('{{revisionMessage}}',o.j?'':p.revisionMessage||'')),n.et||(n.et=E('p'),n.et.className=n.et.id='cm__desc',V(n.Xe,n.et)),n.et.innerHTML=x),g&&(n.tt||(n.tt=E(i),V(n.tt,ye()),I(n.tt,'btn'),A(n.tt,_,'all'),B(n.tt,f,(()=>{S('all')}))),n.tt.firstElementChild.innerHTML=g),b&&(n.je||(n.je=E(i),V(n.je,ye()),I(n.je,'btn'),A(n.je,_,'necessary'),B(n.je,f,(()=>{S([])}))),n.je.firstElementChild.innerHTML=b),v&&(n.ot||(n.ot=E(i),V(n.ot,ye()),I(n.ot,'btn'),I(n.ot,'btn--secondary'),A(n.ot,_,'show'),B(n.ot,'mouseenter',(()=>{o.N||be(e,t)})),B(n.ot,f,a)),n.ot.firstElementChild.innerHTML=v),n.nt||(n.nt=E(r),I(n.nt,d),g&&V(n.nt,n.tt),b&&V(n.nt,n.je),(g||b)&&V(n.We,n.nt),V(n.Ye,n.nt)),n.ot&&!n.st&&(n.st=E(r),n.je&&n.tt?(I(n.st,d),V(n.st,n.ot),V(n.Ye,n.st)):(V(n.nt,n.ot),I(n.nt,d+'--uneven'))),h){if(!n.ct){let e=E(r),t=E(r);n.ct=E(r),I(e,'footer'),I(t,'links'),I(n.ct,'link-group'),V(t,n.ct),V(e,t),V(n.he,e)}n.ct.innerHTML=h}ge(0),o.D||(o.D=!0,te(m.re.ue,u,n.he),t(e),V(n.Ce,n.Qe),Y(n.he),setTimeout((()=>j(n.Qe,'cc--anim')),100)),ee(1),U(n.We,e,be,t)},Ce=e=>{if(!h(e))return null;if(e in m.o._)return e;let t=e.slice(0,2);return t in m.o._?t:null},we=()=>m.o.l||m.o.i.language.default,Se=e=>{e&&(m.o.l=e)},xe=async e=>{const t=m.o;let o=Ce(e)?e:we(),n=t._[o];if(h(n)?n=await(async e=>{try{const t=await fetch(e);return await t.json()}catch(e){return console.error(e),!1}})(n):w(n)&&(n=await n()),!n)throw`Could not load translation for the '${o}' language`;return t.u=n,Se(o),!0},Me=()=>{let e=m.o.i.language.rtl,t=m.ne.Ce;e&&t&&(y(e)||(e=[e]),v(e,m.o.l)?j(t,'cc--rtl'):F(t,'cc--rtl'))},Te=()=>{const e=m.ne;if(e.Ce)return;e.Ce=E(r),e.Ce.id='cc-main',e.Ce.setAttribute('data-nosnippet',''),Me();let t=m.o.i.root;t&&h(t)&&(t=document.querySelector(t)),(t||e.Ue.body).appendChild(e.Ce)},De=e=>oe((()=>localStorage.removeItem(e))),ke=(e,t)=>{if(t instanceof RegExp)return e.filter((e=>t.test(e)));{const o=b(e,t);return o>-1?[e[o]]:[]}},Ee=e=>{const{hostname:t,protocol:o}=location,{name:n,path:s,domain:a,sameSite:c,useLocalStorage:r,secure:i}=m.t.cookie,l=e?(()=>{const e=m.o.S,t=e?new Date-e:0;return 864e5*$()-t})():864e5*$(),d=new Date;d.setTime(d.getTime()+l),m.o.p.expirationTime=d.getTime();const f=JSON.stringify(m.o.p);let _=n+'='+encodeURIComponent(f)+(0!==l?'; expires='+d.toUTCString():'')+'; Path='+s+'; SameSite='+c;v(t,'.')&&(_+='; Domain='+a),i&&'https:'===o&&(_+='; Secure'),r?((e,t)=>{oe((()=>localStorage.setItem(e,t)))})(n,f):document.cookie=_,m.o.p},Ae=(e,t,o)=>{if(0===e.length)return;const n=o||m.t.cookie.domain,s=t||m.t.cookie.path,a='www.'===n.slice(0,4),c=a&&n.substring(4),r=(e,t)=>{t&&'.'!==t.slice(0,1)&&(t='.'+t),document.cookie=e+'=; path='+s+(t?'; domain='+t:'')+'; expires=Thu, 01 Jan 1970 00:00:01 GMT;'};for(const t of e)r(t,o),o||r(t,n),a&&r(t,c)},Ne=e=>{const t=e||m.t.cookie.name,o=m.t.cookie.useLocalStorage;return((e,t)=>{let o;return o=oe((()=>JSON.parse(t?e:decodeURIComponent(e))),!0)||{},o})(o?(n=t,oe((()=>localStorage.getItem(n)))||''):He(t,!0),o);var n},He=(e,t)=>{const o=document.cookie.match('(^|;)\\s*'+e+'\\s*=\\s*([^;]+)');return o?t?o.pop():e:''},Ve=e=>{const t=document.cookie.split(/;\s*/),o=[];for(const n of t){let t=n.split('=')[0];e?oe((()=>{e.test(t)&&o.push(t)})):o.push(t)}return o},je=(e,n=[])=>{((e,t)=>{const{O:o,R:n,B:s,N:a,Z:c,$:r,X:i}=m.o;let l=[];if(e){y(e)?l.push(...e):h(e)&&(l='all'===e?o:[e]);for(const e of o)c[e]=v(l,e)?S(i[e]):[]}else l=[...n,...r],a&&(l=(()=>{const e=m.ne.se;if(!e)return[];let t=[];for(let o in e)e[o].checked&&t.push(o);return t})());l=l.filter((e=>!v(o,e)||!v(t,e))),l.push(...s),J(l)})(e,n),(()=>{const e=m.o,{Z:t,B:o,Y:n,X:s,O:a}=e,c=a;e.te=P(n);for(const a of c){const c=s[a],r=S(c),i=t[a]&&t[a].length>0,l=v(o,a);if(0!==r.length){if(n[a]=[],l)n[a].push(...r);else if(i){const e=t[a];n[a].push(...e)}else n[a]=e.Z[a];n[a]=x(n[a])}}})(),(()=>{const e=m.o;e.I=m.t.mode===o&&e.T?G(e.$,e.R):G(e.R,e.p.categories);let n=e.I.length>0,s=!1;for(const t of e.O)e.ee[t]=G(e.Y[t],e.te[t]),e.ee[t].length>0&&(s=!0);const a=m.ne.se;for(const t in a)a[t].checked=v(e.R,t);for(const t of e.O){const o=m.ne.ae[t],n=e.Y[t];for(const e in o)o[e].checked=v(n,e)}e.C||(e.C=new Date),e.M||(e.M=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)))),e.p={categories:P(e.R),revision:m.t.revision,data:e.h,consentTimestamp:e.C.toISOString(),consentId:e.M,services:P(e.Y),languageCode:m.o.l},e.S&&(e.p.lastConsentTimestamp=e.S.toISOString());let c=!1;const r=n||s;(e.T||r)&&(e.T&&(e.T=!1,c=!0),e.S=e.S?new Date:e.C,e.p.lastConsentTimestamp=e.S.toISOString(),Ee(),m.t.autoClearCookies&&(c||r)&&(e=>{const t=m.o,o=Ve(),n=(e=>{const t=m.o;return(e?t.O:t.I).filter((e=>{const o=t.P[e];return!!o&&!o.readOnly&&!!o.autoClear}))})(e);for(const e in t.ee)for(const n of t.ee[e]){const s=t.X[e][n].cookies;if(!v(t.Y[e],n)&&s)for(const e of s){const t=ke(o,e.name);Ae(t,e.path,e.domain)}}for(const s of n){const n=t.P[s].autoClear,a=n&&n.cookies||[],c=v(t.I,s),r=!v(t.R,s),i=c&&r;if(e?r:i){n.reloadPage&&i&&(t.L=!0);for(const e of a){const t=ke(o,e.name);Ae(t,e.path,e.domain)}}}})(c),ne()),c&&(te(m.re.ie),te(m.re.le),m.t.mode===t)||(r&&te(m.re.de),e.L&&(e.L=!1,location.reload()))})()},Ie=e=>{const t=m.o.T?[]:m.o.R;return v(t,e)},Le=(e,t)=>{const o=m.o.T?[]:m.o.Y[t]||[];return v(o,e)},Fe=(e,t,o)=>{let n=[];const s=e=>{if(h(e)){let t=He(e);''!==t&&n.push(t)}else n.push(...Ve(e))};if(y(e))for(let t of e)s(t);else s(e);Ae(n,t,o)},Pe=e=>{const{ne:t,o:o}=m;if(!o.k){if(!o.D){if(!e)return;he($e,Te)}o.k=!0,o.J=M(),o.v&&Q(!0),q(t.he,1),j(t.ye,n),A(t.he,l,'false'),setTimeout((()=>{z(m.ne.be)}),100),te(m.re.fe,u)}},Oe=()=>{const{ne:e,o:t,re:o}=m;t.k&&(t.k=!1,t.v&&Q(),z(e.Ke,!0),F(e.ye,n),A(e.he,l,'true'),z(t.J),t.J=null,te(o._e,u))},Re=()=>{const e=m.o;e.A||(e.N||be($e,Te),e.A=!0,e.k?e.U=M():e.J=M(),q(m.ne.we,2),j(m.ne.ye,s),A(m.ne.we,l,'false'),setTimeout((()=>{z(m.ne.ve)}),100),te(m.re.fe,p))},Be=()=>{const e=m.o;e.A&&(e.A=!1,(()=>{const e=Je(),t=m.o.P,o=m.ne.se,n=m.ne.ae,s=e=>v(m.o.$,e);for(const a in o){const c=!!t[a].readOnly;o[a].checked=c||(e?Ie(a):s(a));for(const t in n[a])n[a][t].checked=c||(e?Le(t,a):s(a))}})(),z(m.ne.$e,!0),F(m.ne.ye,s),A(m.ne.we,l,'true'),e.k?(z(e.U),e.U=null):(z(e.J),e.J=null),te(m.re._e,p))};var $e={show:Pe,hide:Oe,showPreferences:Re,hidePreferences:Be,acceptCategory:je};const Ge=(e,t)=>{const o=Ne(t);return e?o[e]:o},Je=()=>!m.o.T;e.acceptCategory=je,e.acceptService=(e,t)=>{const{O:o,X:n}=m.o;if(!(e&&t&&h(t)&&v(o,t)&&0!==S(n[t]).length))return!1;((e,t)=>{const o=m.o,{X:n,Z:s,N:a}=o,c=m.ne.ae[t]||{},r=m.ne.se[t]||{},i=S(n[t]);if(s[t]=[],h(e)){if('all'===e){if(s[t].push(...i),a)for(let e in c)c[e].checked=!0,k(c[e])}else if(v(i,e)&&s[t].push(e),a)for(let t in c)c[t].checked=e===t,k(c[t])}else if(y(e))for(let o of i){const n=v(e,o);n&&s[t].push(o),a&&(c[o].checked=n,k(c[o]))}const l=0===s[t].length;o.R=l?o.R.filter((e=>e!==t)):x([...o.R,t]),a&&(r.checked=!l,k(r))})(e,t),je()},e.acceptedCategory=Ie,e.acceptedService=Le,e.eraseCookies=Fe,e.getConfig=e=>{const t=m.t,o=m.o.i;return e?t[e]||o[e]:{...t,...o,cookie:{...t.cookie}}},e.getCookie=Ge,e.getUserPreferences=()=>{const{F:e,Y:t}=m.o,{accepted:o,rejected:n}=(()=>{const{T:e,R:t,O:o}=m.o;return{accepted:t,rejected:e?[]:o.filter((e=>!v(t,e)))}})();return P({acceptType:e,acceptedCategories:o,rejectedCategories:n,acceptedServices:t,rejectedServices:O()})},e.hide=Oe,e.hidePreferences=Be,e.loadScript=(e,t)=>{let o=document.querySelector('script[src="'+e+'"]');return new Promise((n=>{if(o)return n(!0);if(o=E('script'),C(t))for(const e in t)A(o,e,t[e]);o.onload=()=>n(!0),o.onerror=()=>{o.remove(),n(!1)},o.src=e,V(document.head,o)}))},e.reset=e=>{const{Ce:t,ye:o}=m.ne,{name:c,path:r,domain:i,useLocalStorage:l}=m.t.cookie;e&&(l?De(c):Fe(c,r,i));for(const{pe:e,ge:t,me:o}of m.o.m)e.removeEventListener(t,o);t&&t.remove(),o&&o.classList.remove(a,s,n);const d=new g;for(const e in m)m[e]=d[e];window._ccRun=!1},e.run=async e=>{const{o:t,t:n,re:s}=m,a=window;if(!a._ccRun){if(a._ccRun=!0,(e=>{const{ne:t,t:n,o:s}=m,a=n,r=s,{cookie:i}=a,l=m.ce,d=e.cookie,f=e.categories,_=S(f)||[],u=navigator,p=document;t.Ue=p,t.ye=p.documentElement,i.domain=location.hostname,r.i=e,r.P=f,r.O=_,r._=e.language.translations,r.v=!!e.disablePageInteraction,l.ie=e.onFirstConsent,l.le=e.onConsent,l.de=e.onChange,l._e=e.onModalHide,l.fe=e.onModalShow,l.ue=e.onModalReady;const{mode:g,autoShow:b,lazyHtmlGeneration:y,autoClearCookies:h,revision:w,manageScriptTags:x,hideFromBots:M}=e;g===o&&(a.mode=g),'boolean'==typeof h&&(a.autoClearCookies=h),'boolean'==typeof x&&(a.manageScriptTags=x),'number'==typeof w&&w>=0&&(a.revision=w,r.V=!0),'boolean'==typeof b&&(a.autoShow=b),'boolean'==typeof y&&(a.lazyHtmlGeneration=y),!1===M&&(a.hideFromBots=!1),!0===a.hideFromBots&&u&&(r.G=u.userAgent&&/bot|crawl|spider|slurp|teoma/i.test(u.userAgent)||u.webdriver),C(d)&&(a.cookie={...i,...d}),a.autoClearCookies,r.V,a.manageScriptTags,(e=>{const{P:t,X:o,Y:n,Z:s,B:a}=m.o;for(let c of e){const e=t[c],r=e.services||{},i=C(r)&&S(r)||[];o[c]={},n[c]=[],s[c]=[],e.readOnly&&(a.push(c),n[c]=i),m.ne.ae[c]={};for(let e of i){const t=r[e];t.Se=!1,o[c][e]=t}}})(_),(()=>{if(!m.t.manageScriptTags)return;const e=m.o,t=D(document,'script['+c+']');for(const o of t){let t=H(o,c),n=o.dataset.service||'',s=!1;if(t&&'!'===t.charAt(0)&&(t=t.slice(1),s=!0),'!'===n.charAt(0)&&(n=n.slice(1),s=!0),v(e.O,t)&&(e.oe.push({Me:o,xe:!1,ke:s,Te:t,De:n}),n)){const o=e.X[t];o[n]||(o[n]={Se:!1})}}})(),Se((()=>{const e=m.o.i.language.autoDetect;if(e){const t={browser:navigator.language,document:document.documentElement.lang},o=Ce(t[e]);if(o)return o}return we()})())})(e),t.G)return;(()=>{const e=m.o,t=m.t,n=Ne(),{categories:s,services:a,consentId:c,consentTimestamp:r,lastConsentTimestamp:i,data:l,revision:d}=n,f=y(s);e.p=n,e.M=c;const _=!!c&&h(c);e.C=r,e.C&&(e.C=new Date(r)),e.S=i,e.S&&(e.S=new Date(i)),e.h=void 0!==l?l:null,e.V&&_&&d!==t.revision&&(e.j=!1),e.T=!(_&&e.j&&e.C&&e.S&&f),t.cookie.useLocalStorage&&!e.T&&(e.T=(new Date).getTime()>(n.expirationTime||0),e.T&&De(t.cookie.name)),e.T,(()=>{const e=m.o;for(const t of e.O){const n=e.P[t];if(n.readOnly||n.enabled){e.$.push(t);const n=e.X[t]||{};for(let s in n)e.Z[t].push(s),e.i.mode===o&&e.Y[t].push(s)}}})(),e.T?t.mode===o&&(e.R=[...e.$]):(e.Y={...e.Y,...a},e.Z={...e.Y},J([...e.B,...s]))})();const i=Je();if(!await xe())return!1;if(U(null,r=$e,be,Te),m.o.T&&he(r,Te),m.t.lazyHtmlGeneration||be(r,Te),n.autoShow&&!i&&Pe(!0),i)return ne(),te(s.le);n.mode===o&&ne(t.$)}var r},e.setCookieData=e=>{let t,o=e.value,n=e.mode,s=!1;const a=m.o;if('update'===n){a.h=t=Ge('data');const e=typeof t==typeof o;if(e&&'object'==typeof t){!t&&(t={});for(let e in o)t[e]!==o[e]&&(t[e]=o[e],s=!0)}else!e&&t||t===o||(t=o,s=!0)}else t=o,s=!0;return s&&(a.h=t,a.p.data=t,Ee(!0)),s},e.setLanguage=async(e,t)=>{if(!Ce(e))return!1;const o=m.o;return!(e===we()&&!0!==t||!await xe(e)||(Se(e),o.D&&he($e,Te),o.N&&be($e,Te),Me(),0))},e.show=Pe,e.showPreferences=Re,e.validConsent=Je,e.validCookie=e=>''!==He(e,!0)},'object'==typeof exports&&'undefined'!=typeof module?t(exports):'function'==typeof define&&define.amd?define(['exports'],t):t((e='undefined'!=typeof globalThis?globalThis:e||self).CookieConsent={}); diff --git a/app/core/src/main/resources/static/mobile-sign.html b/app/core/src/main/resources/static/mobile-sign.html new file mode 100644 index 0000000000..9f0204c4a9 --- /dev/null +++ b/app/core/src/main/resources/static/mobile-sign.html @@ -0,0 +1,642 @@ + + + + + + + + Stirling PDF - Draw Signature + + + + + + + + +

+
+ +
+
Stirling PDF
+
Draw Signature
+
+
+ +
+
Connecting…
+ +
+
+ +
+ +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + + + +

Draw your signature above, then send it. It appears in the Sign tool on your computer automatically.

+
+ + + +
Stirling PDF · signatures transfer directly to your desktop
+
+ + + + diff --git a/app/core/src/main/resources/static/moon.svg b/app/core/src/main/resources/static/moon.svg deleted file mode 100644 index 8f35a2beea..0000000000 --- a/app/core/src/main/resources/static/moon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/mstile-144x144.png b/app/core/src/main/resources/static/mstile-144x144.png deleted file mode 100644 index ff28cf1ac6..0000000000 Binary files a/app/core/src/main/resources/static/mstile-144x144.png and /dev/null differ diff --git a/app/core/src/main/resources/static/mstile-150x150.png b/app/core/src/main/resources/static/mstile-150x150.png deleted file mode 100644 index c900c83ae3..0000000000 Binary files a/app/core/src/main/resources/static/mstile-150x150.png and /dev/null differ diff --git a/app/core/src/main/resources/static/mstile-310x150.png b/app/core/src/main/resources/static/mstile-310x150.png deleted file mode 100644 index 43a095f36d..0000000000 Binary files a/app/core/src/main/resources/static/mstile-310x150.png and /dev/null differ diff --git a/app/core/src/main/resources/static/mstile-310x310.png b/app/core/src/main/resources/static/mstile-310x310.png deleted file mode 100644 index fd52bd61d2..0000000000 Binary files a/app/core/src/main/resources/static/mstile-310x310.png and /dev/null differ diff --git a/app/core/src/main/resources/static/mstile-70x70.png b/app/core/src/main/resources/static/mstile-70x70.png deleted file mode 100644 index 7692923f74..0000000000 Binary files a/app/core/src/main/resources/static/mstile-70x70.png and /dev/null differ diff --git a/app/core/src/main/resources/static/python/png_to_webp.py b/app/core/src/main/resources/static/python/png_to_webp.py index 6f086d5207..094b1b5033 100644 --- a/app/core/src/main/resources/static/python/png_to_webp.py +++ b/app/core/src/main/resources/static/python/png_to_webp.py @@ -16,6 +16,7 @@ To adjust the DPI resolution for rendering PDF pages: import argparse import os + from pdf2image import convert_from_path from PIL import Image @@ -149,9 +150,7 @@ def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.") parser.add_argument("pdf_path", help="The path to the input PDF file.") - parser.add_argument( - "output_dir", help="The directory where the WebP images should be saved." - ) + parser.add_argument("output_dir", help="The directory where the WebP images should be saved.") parser.add_argument( "--dpi", type=int, diff --git a/app/core/src/main/resources/static/rainbow.svg b/app/core/src/main/resources/static/rainbow.svg deleted file mode 100644 index 0ee5e48b8e..0000000000 --- a/app/core/src/main/resources/static/rainbow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/main/resources/static/safari-pinned-tab.svg b/app/core/src/main/resources/static/safari-pinned-tab.svg deleted file mode 100644 index f0a689a75d..0000000000 --- a/app/core/src/main/resources/static/safari-pinned-tab.svg +++ /dev/null @@ -1 +0,0 @@ -Created by potrace 1.14, written by Peter Selinger 2001-2017 \ No newline at end of file diff --git a/app/core/src/main/resources/static/sun.svg b/app/core/src/main/resources/static/sun.svg deleted file mode 100644 index 28dcfe5680..0000000000 --- a/app/core/src/main/resources/static/sun.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/core/src/test/java/stirling/software/SPDF/config/MultipartConfigurationTest.java b/app/core/src/test/java/stirling/software/SPDF/config/MultipartConfigurationTest.java index 60b17a3534..276afbbe99 100644 --- a/app/core/src/test/java/stirling/software/SPDF/config/MultipartConfigurationTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/config/MultipartConfigurationTest.java @@ -4,8 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.lang.reflect.Field; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -24,10 +22,7 @@ class MultipartConfigurationTest { void setUp() throws Exception { // Manually constructed config with a mocked service, so Spring env overrides do not apply. uploadLimitService = mock(UploadLimitService.class); - configuration = new MultipartConfiguration(); - Field field = MultipartConfiguration.class.getDeclaredField("uploadLimitService"); - field.setAccessible(true); - field.set(configuration, uploadLimitService); + configuration = new MultipartConfiguration(uploadLimitService); } @Nested diff --git a/app/core/src/test/java/stirling/software/SPDF/config/ToolIODeclarationCoverageTest.java b/app/core/src/test/java/stirling/software/SPDF/config/ToolIODeclarationCoverageTest.java new file mode 100644 index 0000000000..d7d211a7ce --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/config/ToolIODeclarationCoverageTest.java @@ -0,0 +1,376 @@ +package stirling.software.SPDF.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.model.tool.ToolIOSpec; +import stirling.software.common.service.ToolIOParameterDefaults; + +/** + * Every document-transforming endpoint must declare its I/O, or it becomes a hole in the + * compatibility graph: an undeclared step reads as "cannot tell" and nothing past it is checked. + * + *

The rest pins the declarations that were corrected on the way out of the description prose, + * where several disagreed with what the endpoint actually does. + */ +class ToolIODeclarationCoverageTest { + + /** The namespaces whose endpoints act on documents. */ + private static final List TOOL_NAMESPACES = + List.of( + "/api/v1/general/", + "/api/v1/misc/", + "/api/v1/security/", + "/api/v1/convert/", + "/api/v1/filter/", + "/api/v1/integration/"); + + /** + * Not document transforms, so nothing to declare. A path exempts everything nested under it. + * Keep it short: if you are adding a real tool, annotate it instead. + */ + private static final List EXEMPT = + List.of( + // Interactive editing sessions: page fragments and cache handles, not + // documents. + "/api/v1/convert/pdf/text-editor", + "/api/v1/convert/text-editor/pdf", + // Signing sessions, certificate checks and hardware token enumeration; the + // signing tool itself is /api/v1/security/cert-sign, which is declared. + "/api/v1/security/cert-sign/sessions", + "/api/v1/security/cert-sign/validate-certificate", + "/api/v1/security/cert-sign/hardware", + // Releases finished jobs and their stored files; server maintenance, takes and + // returns no document. + "/api/v1/general/jobs/cleanup"); + + private record Scan(Set required, Map declared) {} + + private static final Scan SCAN = scan(); + private static final Map DECLARED = SCAN.declared(); + + @Test + void everyToolEndpointDeclaresItsIo() { + List missing = + SCAN.required().stream() + .filter(path -> !DECLARED.containsKey(path)) + .sorted() + .toList(); + assertTrue( + missing.isEmpty(), + () -> + "These tool endpoints are missing a @ToolIO declaration. Add one so a" + + " pipeline containing them can be checked before it runs, or" + + " exempt them in EXEMPT with a reason:\n " + + String.join("\n ", missing)); + } + + @ParameterizedTest + @ValueSource( + strings = { + // Splitters emit a set of documents; the archive is only transport. + "/api/v1/general/split-pages", + "/api/v1/general/split-pdf-by-sections", + "/api/v1/general/split-pdf-by-chapters", + "/api/v1/general/split-by-size-or-count", + "/api/v1/general/split-for-poster-print", + "/api/v1/misc/auto-split-pdf", + "/api/v1/misc/extract-images", + "/api/v1/misc/extract-image-scans", + // Corrected: both were declared single-output but return an archive of results. + "/api/v1/misc/remove-blanks", + "/api/v1/convert/pdf/csv" + }) + void multiResultEndpointsAreUnpacked(String path) { + assertTrue(spec(path).resolveOutput().arity().isMultiOutput(), path); + } + + @ParameterizedTest + @ValueSource( + strings = { + // The archive itself is the deliverable. + "/api/v1/misc/extract-attachments", + // A page of HTML plus the assets it references; unpacking scatters them. + "/api/v1/convert/pdf/html", + "/api/v1/general/rotate-pdf", + "/api/v1/general/merge-pdfs", + // Corrected: was declared MIMO, but it returns one overlaid document. + "/api/v1/general/overlay-pdfs", + // Sits under the exempted cert-sign session paths; pinned so it stays declared. + "/api/v1/security/cert-sign" + }) + void singleResultEndpointsAreNotUnpacked(String path) { + assertFalse(spec(path).resolveOutput().arity().isMultiOutput(), path); + } + + @ParameterizedTest + @ValueSource( + strings = { + "/api/v1/general/merge-pdfs", + "/api/v1/general/overlay-pdfs", + "/api/v1/convert/img/pdf" + }) + void multiInputEndpointsTakeEveryFileAtOnce(String path) { + assertTrue(spec(path).arity().isMultiInput(), path); + } + + @ParameterizedTest + @ValueSource( + strings = { + // Corrected: the multiple files are the attachments, a secondary field. The + // primary stream is one document in, one out, so these must not fan in. + "/api/v1/misc/add-attachments", + "/api/v1/misc/delete-attachment", + "/api/v1/misc/rename-attachment" + }) + void attachmentEditsActOnOneDocument(String path) { + assertFalse(spec(path).arity().isMultiInput(), path); + } + + @ParameterizedTest + @ValueSource( + strings = { + // Corrected: these declared Output:Boolean, which would have reported every chain + // after a filter as broken. A filter returns the document, or 204 to drop it. + "/api/v1/filter/filter-contains-text", + "/api/v1/filter/filter-contains-image", + "/api/v1/filter/filter-file-size", + "/api/v1/filter/filter-page-count", + "/api/v1/filter/filter-page-rotation", + "/api/v1/filter/filter-page-size" + }) + void filtersPassTheDocumentThrough(String path) { + ToolIOSpec spec = spec(path); + assertTrue(spec.acceptsFormat(ToolFormat.PDF), path); + assertEquals(ToolFormat.PDF, spec.resolveOutput().format(), path); + } + + @Test + void addPasswordProducesAnEncryptedDocument() { + ToolIOSpec spec = spec("/api/v1/security/add-password"); + assertEquals( + ToolFormat.PDF_ENCRYPTED, + spec.resolveOutput(Map.of("password", "hunter2", "ownerPassword", "")).format()); + } + + @Test + void addPasswordWithNoPasswordsOnlySetsPermissions() { + // The controller returns the document unencrypted when both passwords are absent, so a + // permissions-only call must not close the chain to every downstream tool. + ToolIOSpec spec = spec("/api/v1/security/add-password"); + assertEquals( + ToolFormat.PDF, + spec.resolveOutput(Map.of("password", "", "ownerPassword", "")).format()); + } + + @Test + void pdfToTextProducesTextOnlyForTheTxtBranch() { + // The endpoint serves both formats, and RTF is a word-processor format: declaring the whole + // endpoint as TEXT would pass RTF into text tools and reject it from the Word tools that + // can actually open it. + ToolIOSpec spec = spec("/api/v1/convert/pdf/text"); + assertEquals(ToolFormat.TEXT, spec.resolveOutput(Map.of("outputFormat", "txt")).format()); + assertEquals(ToolFormat.WORD, spec.resolveOutput(Map.of("outputFormat", "rtf")).format()); + } + + @Test + void theHtmlRoundTripIsAValidChain() { + // convert/pdf/html emits a ZIP of the page plus its assets, and convert/html/pdf takes one + // back. Declaring only HTML would reject the round trip the two endpoints exist to support. + assertEquals(ToolFormat.ZIP, spec("/api/v1/convert/pdf/html").resolveOutput().format()); + assertTrue(spec("/api/v1/convert/html/pdf").acceptsFormat(ToolFormat.ZIP)); + assertTrue(spec("/api/v1/convert/html/pdf").acceptsFormat(ToolFormat.HTML)); + } + + @Test + void markdownToPdfTakesAZipOfMarkdownAndImages() { + assertTrue(spec("/api/v1/convert/markdown/pdf").acceptsFormat(ToolFormat.ZIP)); + assertTrue(spec("/api/v1/convert/markdown/pdf").acceptsFormat(ToolFormat.MARKDOWN)); + } + + @Test + void vectorExportDeclaresEveryOutputFormat() { + // Every value the request permits must resolve to what it actually emits. "eps" is the + // default and is an image extension; the other three are not, and would otherwise fall + // through to IMAGE and pass a chain that fails at run time. + ToolIOSpec spec = spec("/api/v1/convert/pdf/vector"); + assertEquals(ToolFormat.IMAGE, spec.resolveOutput(Map.of("outputFormat", "eps")).format()); + assertEquals( + ToolFormat.POSTSCRIPT, spec.resolveOutput(Map.of("outputFormat", "ps")).format()); + assertEquals(ToolFormat.PCL, spec.resolveOutput(Map.of("outputFormat", "pcl")).format()); + assertEquals(ToolFormat.XPS, spec.resolveOutput(Map.of("outputFormat", "xps")).format()); + // The request pattern accepts any casing, so the declaration must too. + assertEquals( + ToolFormat.POSTSCRIPT, spec.resolveOutput(Map.of("outputFormat", "PS")).format()); + } + + @Test + void everyVectorOutputFormatHasACase() { + // Guards the list above against a new allowableValue being added without a declaration. + Set declared = + spec("/api/v1/convert/pdf/vector").cases().stream() + .flatMap(rule -> rule.when().stream()) + .flatMap(when -> when.matches().stream()) + .collect(java.util.stream.Collectors.toSet()); + assertEquals(Set.of("ps", "pcl", "xps"), declared); + } + + @Test + void anAbsentParameterResolvesToItsRequestModelDefault() { + // A pipeline step often omits a parameter a case branches on. The default is read from the + // request model, so the output resolves anyway instead of coming back uncertain. + + // Auto Rotate never sends dryRun; its default (false) means the JSON branch cannot fire. + assertEquals( + ToolFormat.PDF, + spec("/api/v1/misc/auto-rotate-pdf").resolveOutput(Map.of()).format()); + assertTrue(spec("/api/v1/misc/auto-rotate-pdf").resolveOutput(Map.of()).certain()); + + // Change Permissions posts to add-password with no password fields; both default to blank, + // so the unencrypted branch fires and it is not mistaken for producing an encrypted PDF. + assertEquals( + ToolFormat.PDF, + spec("/api/v1/security/add-password").resolveOutput(Map.of()).format()); + assertTrue(spec("/api/v1/security/add-password").resolveOutput(Map.of()).certain()); + } + + @Test + void aRequiredParameterWithNoDefaultStaysUncertainWhenAbsent() { + // pdf/text branches on outputFormat, which is required with no default. Absent, its output + // is genuinely txt-or-rtf-dependent, so it must remain uncertain rather than assume TEXT. + assertFalse(spec("/api/v1/convert/pdf/text").resolveOutput(Map.of()).certain()); + } + + @Test + void onlyRemovePasswordAcceptsAnEncryptedDocument() { + assertTrue( + spec("/api/v1/security/remove-password").acceptsFormat(ToolFormat.PDF_ENCRYPTED)); + List accepting = + DECLARED.entrySet().stream() + .filter(e -> e.getValue().acceptsFormat(ToolFormat.PDF_ENCRYPTED)) + .map(Map.Entry::getKey) + .filter(path -> !path.equals("/api/v1/security/remove-password")) + .filter(path -> !spec(path).accepts().contains(ToolFormat.ANY)) + .sorted() + .toList(); + assertTrue( + accepting.isEmpty(), + () -> + "Only remove-password should accept an encrypted PDF. If one of these" + + " genuinely handles encryption, say so in its @ToolIO:\n " + + String.join("\n ", accepting)); + } + + private static ToolIOSpec spec(String path) { + ToolIOSpec spec = DECLARED.get(path); + if (spec == null) { + throw new AssertionError("No @ToolIO declared for " + path); + } + return spec; + } + + private static Scan scan() { + Set required = new HashSet<>(); + Map declared = new HashMap<>(); + ClassPathScanningCandidateComponentProvider scanner = + new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new AnnotationTypeFilter(RequestMapping.class, true, true)); + + for (BeanDefinition definition : scanner.findCandidateComponents("stirling.software")) { + Class controller = loadClass(definition.getBeanClassName()); + if (controller == null) { + continue; + } + RequestMapping base = + AnnotatedElementUtils.findMergedAnnotation(controller, RequestMapping.class); + if (base == null || base.value().length == 0) { + continue; + } + for (Method method : controller.getDeclaredMethods()) { + ToolIO declaration = + AnnotatedElementUtils.findMergedAnnotation(method, ToolIO.class); + for (String path : mappedPaths(method)) { + String full = join(base.value()[0], path); + if (!requiresDeclaration(full)) { + continue; + } + required.add(full); + if (declaration != null) { + declared.put( + full, + ToolIOSpec.from( + declaration, + param -> ToolIOParameterDefaults.resolve(method, param))); + } + } + } + } + return new Scan(required, declared); + } + + private static boolean requiresDeclaration(String path) { + if (TOOL_NAMESPACES.stream().noneMatch(path::startsWith)) { + return false; + } + // A path variable means it addresses a resource, not a document to transform. + if (path.contains("{")) { + return false; + } + return EXEMPT.stream().noneMatch(e -> path.equals(e) || path.startsWith(e + "/")); + } + + private static List mappedPaths(Method method) { + RequestMapping mapping = + AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (mapping == null) { + return List.of(); + } + boolean writes = + AnnotatedElementUtils.hasAnnotation(method, PostMapping.class) + || AnnotatedElementUtils.hasAnnotation(method, PutMapping.class) + || Set.of(mapping.method()).stream() + .anyMatch(m -> m.name().equals("POST") || m.name().equals("PUT")); + if (!writes) { + return List.of(); + } + return mapping.value().length == 0 ? List.of("") : Arrays.asList(mapping.value()); + } + + private static String join(String base, String path) { + if (path.isEmpty()) { + return base; + } + return base.endsWith("/") || path.startsWith("/") ? base + path : base + "/" + path; + } + + private static Class loadClass(String name) { + try { + return name == null ? null : Class.forName(name); + } catch (Throwable e) { + // Absent optional dependencies in this flavor; not this test's concern. + return null; + } + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/MergeControllerGapTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/MergeControllerGapTest.java index f5543bcdc7..0a6aa0efca 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/MergeControllerGapTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/MergeControllerGapTest.java @@ -84,12 +84,6 @@ class MergeControllerGapTest { return (MultipartFile[]) m.invoke(null, files, fileOrder); } - private String[] parseClientFileIds(String value) throws Exception { - Method m = MergeController.class.getDeclaredMethod("parseClientFileIds", String.class); - m.setAccessible(true); - return (String[]) m.invoke(mergeController, value); - } - private long getPdfDateTimeSafe(MultipartFile file) throws Exception { Method m = MergeController.class.getDeclaredMethod("getPdfDateTimeSafe", MultipartFile.class); @@ -331,58 +325,6 @@ class MergeControllerGapTest { } } - // ---- parseClientFileIds ------------------------------------------------- - - @Nested - @DisplayName("parseClientFileIds") - class ParseClientFileIds { - - @Test - @DisplayName("null input returns empty array") - void nullReturnsEmpty() throws Exception { - assertEquals(0, parseClientFileIds(null).length); - } - - @Test - @DisplayName("blank input returns empty array") - void blankReturnsEmpty() throws Exception { - assertEquals(0, parseClientFileIds(" ").length); - } - - @Test - @DisplayName("empty JSON array returns empty array") - void emptyArrayReturnsEmpty() throws Exception { - assertEquals(0, parseClientFileIds("[]").length); - assertEquals(0, parseClientFileIds("[ ]").length); - } - - @Test - @DisplayName("non-array text returns empty array") - void nonArrayReturnsEmpty() throws Exception { - assertEquals(0, parseClientFileIds("not-an-array").length); - } - - @Test - @DisplayName("parses quoted, comma-separated ids and strips surrounding quotes") - void parsesQuotedIds() throws Exception { - String[] result = parseClientFileIds("[\"id1\", \"id2\",\"id3\"]"); - assertArrayEquals(new String[] {"id1", "id2", "id3"}, result); - } - - @Test - @DisplayName("parses unquoted ids as-is after trimming") - void parsesUnquotedIds() throws Exception { - String[] result = parseClientFileIds("[a, b , c]"); - assertArrayEquals(new String[] {"a", "b", "c"}, result); - } - - @Test - @DisplayName("single element array yields a one-element result") - void singleElement() throws Exception { - assertArrayEquals(new String[] {"only"}, parseClientFileIds("[\"only\"]")); - } - } - // ---- reorderFilesByProvidedOrder ---------------------------------------- @Nested diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/UIDataControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/UIDataControllerTest.java new file mode 100644 index 0000000000..33e2fbf0df --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/UIDataControllerTest.java @@ -0,0 +1,66 @@ +package stirling.software.SPDF.controller.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.http.ResponseEntity; + +import stirling.software.SPDF.service.SharedSignatureService; +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.model.ApplicationProperties; + +import tools.jackson.databind.ObjectMapper; + +class UIDataControllerTest { + + @TempDir Path tempDir; + + @Test + void getPipelineData_usesEachSourceFilenameWhenJsonContentIsIdentical() throws Exception { + Path configDir = tempDir.resolve("defaultWebUIConfigs"); + Files.createDirectories(configDir); + + String configJson = "{\"steps\":[]}"; + Files.writeString(configDir.resolve("first-config.json"), configJson); + Files.writeString(configDir.resolve("second-config.json"), configJson); + + ApplicationProperties applicationProperties = mock(ApplicationProperties.class); + SharedSignatureService signatureService = mock(SharedSignatureService.class); + RuntimePathConfig runtimePathConfig = mock(RuntimePathConfig.class); + + when(runtimePathConfig.getPipelineDefaultWebUiConfigs()).thenReturn(configDir.toString()); + + UIDataController controller = + new UIDataController( + applicationProperties, + signatureService, + null, + new DefaultResourceLoader(), + runtimePathConfig, + new ObjectMapper()); + + ResponseEntity response = controller.getPipelineData(); + + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + UIDataController.PipelineData body = response.getBody(); + assertThat(body).isNotNull(); + + List> configsWithNames = body.getPipelineConfigsWithNames(); + assertThat(configsWithNames).hasSize(2); + assertThat(configsWithNames) + .extracting(entry -> entry.get("name")) + .containsExactlyInAnyOrder("first-config", "second-config"); + assertThat(configsWithNames) + .extracting(entry -> entry.get("json")) + .containsOnly(configJson); + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeControllerTest.java index 90b6a370de..14c49b351a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeControllerTest.java @@ -204,7 +204,7 @@ class ConvertOfficeControllerTest { inv -> { // unoconvert writes directly to the output path (last arg) List command = inv.getArgument(0); - Path out = Path.of(command.get(command.size() - 1)); + Path out = Path.of(command.getLast()); Files.writeString(out, "%PDF-1.4 produced"); return result; }); @@ -239,7 +239,7 @@ class ConvertOfficeControllerTest { inv -> { // soffice writes .pdf into the --outdir (workDir) List command = inv.getArgument(0); - Path inputPath = Path.of(command.get(command.size() - 1)); + Path inputPath = Path.of(command.getLast()); Path out = inputPath.getParent().resolve("report.pdf"); Files.writeString(out, "%PDF soffice"); return result; @@ -311,7 +311,7 @@ class ConvertOfficeControllerTest { .thenAnswer( inv -> { List command = inv.getArgument(0); - Path inputPath = Path.of(command.get(command.size() - 1)); + Path inputPath = Path.of(command.getLast()); Path out = inputPath.getParent().resolve("report.pdf"); Files.write(out, new byte[0]); return result; @@ -344,7 +344,7 @@ class ConvertOfficeControllerTest { .thenAnswer( inv -> { List command = inv.getArgument(0); - Path inputPath = Path.of(command.get(command.size() - 1)); + Path inputPath = Path.of(command.getLast()); Path out = inputPath.getParent().resolve("page.pdf"); Files.writeString(out, "%PDF html"); return result; @@ -398,7 +398,7 @@ class ConvertOfficeControllerTest { .thenAnswer( inv -> { List command = inv.getArgument(0); - Path inputPath = Path.of(command.get(command.size() - 1)); + Path inputPath = Path.of(command.getLast()); Path out = inputPath.getParent().resolve("report.pdf"); Files.writeString(out, "%PDF produced"); return result; diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAGapTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAGapTest.java index b64776665b..ea693ddd27 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAGapTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAGapTest.java @@ -46,6 +46,7 @@ import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; import stirling.software.SPDF.model.api.security.PDFVerificationResult; import stirling.software.SPDF.service.VeraPDFService; import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.service.PdfaLevelAServiceInterface; import stirling.software.common.util.TempFileManager; /** @@ -62,10 +63,12 @@ class ConvertPDFToPDFAGapTest { @Mock private RuntimePathConfig runtimePathConfig; @Mock private VeraPDFService veraPDFService; + @Mock private PdfaLevelAServiceInterface pdfaLevelAService; @Mock private TempFileManager tempFileManager; private ConvertPDFToPDFA newController() { - return new ConvertPDFToPDFA(runtimePathConfig, veraPDFService, tempFileManager); + return new ConvertPDFToPDFA( + runtimePathConfig, veraPDFService, pdfaLevelAService, tempFileManager); } // ---- reflection helpers ---------------------------------------------------------------- @@ -161,9 +164,21 @@ class ConvertPDFToPDFAGapTest { } private String suffixOf(Object profile) throws Exception { - Method m = profile.getClass().getDeclaredMethod("outputSuffix"); + return suffixOf(profile, true); + } + + private String suffixOf(Object profile, boolean levelAReached) throws Exception { + Method m = profile.getClass().getDeclaredMethod("outputSuffix", boolean.class); m.setAccessible(true); - return (String) m.invoke(profile); + return (String) m.invoke(profile, levelAReached); + } + + @Test + @DisplayName("a level A profile falls back to the level B name when tagging failed") + void levelANotReachedIsNamedLevelB() throws Exception { + assertThat(suffixOf(resolveProfile("pdfa-1a"), false)).isEqualTo("_PDFA-1b.pdf"); + assertThat(suffixOf(resolveProfile("pdfa-2a"), false)).isEqualTo("_PDFA-2b.pdf"); + assertThat(suffixOf(resolveProfile("pdfa-3a"), true)).isEqualTo("_PDFA-3a.pdf"); } @Test @@ -717,6 +732,30 @@ class ConvertPDFToPDFAGapTest { @DisplayName("verifyStrictCompliance (VeraPDFService mocked)") class StrictCompliance { + private Object profile(String token) throws Exception { + Class enumClass = null; + for (Class inner : ConvertPDFToPDFA.class.getDeclaredClasses()) { + if (inner.getSimpleName().equals("PdfaProfile")) { + enumClass = inner; + } + } + Method m = enumClass.getDeclaredMethod("fromRequest", String.class); + m.setAccessible(true); + return m.invoke(null, token); + } + + private Throwable verify(String token, boolean levelAReached) throws Exception { + ConvertPDFToPDFA controller = newController(); + return catchThrowable( + () -> + invokeInstance( + controller, + "verifyStrictCompliance", + (Object) "dummy".getBytes(), + profile(token), + levelAReached)); + } + @Test @DisplayName("compliant result passes without throwing") void compliantPasses() throws Exception { @@ -726,14 +765,7 @@ class ConvertPDFToPDFAGapTest { ok.setComplianceSummary("PDF/A-1b compliant"); when(veraPDFService.validatePDF(any())).thenReturn(List.of(ok)); - ConvertPDFToPDFA controller = newController(); - assertThatCode( - () -> - invokeInstance( - controller, - "verifyStrictCompliance", - (Object) "dummy".getBytes())) - .doesNotThrowAnyException(); + assertThat(verify("pdfa-1", true)).isNull(); } @Test @@ -745,34 +777,70 @@ class ConvertPDFToPDFAGapTest { bad.setComplianceSummary("PDF/A-1b with errors"); when(veraPDFService.validatePDF(any())).thenReturn(List.of(bad)); - ConvertPDFToPDFA controller = newController(); - ResponseStatusException ex = - (ResponseStatusException) - catchThrowable( - () -> - invokeInstance( - controller, - "verifyStrictCompliance", - (Object) "dummy".getBytes())); + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-1", true); assertThat(ex).isNotNull(); assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(ex.getReason()).contains("PDF/A-1b with errors"); } + @Test + @DisplayName("a level B pass does not satisfy a level A request") + void levelBDoesNotSatisfyLevelA() throws Exception { + PDFVerificationResult ok = new PDFVerificationResult(); + ok.setCompliant(true); + ok.setStandard("1b"); + ok.setComplianceSummary("PDF/A-1b compliant"); + when(veraPDFService.validatePDF(any())).thenReturn(List.of(ok)); + + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-1a", true); + assertThat(ex).isNotNull(); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(ex.getReason()).contains("PDF/A-1a"); + } + + @Test + @DisplayName("a level A result satisfies a level A request") + void levelASatisfiesLevelA() throws Exception { + PDFVerificationResult ok = new PDFVerificationResult(); + ok.setCompliant(true); + ok.setStandard("2a"); + ok.setComplianceSummary("PDF/A-2a compliant"); + when(veraPDFService.validatePDF(any())).thenReturn(List.of(ok)); + + assertThat(verify("pdfa-2a", true)).isNull(); + } + + @Test + @DisplayName("untagged output fails a level A request before validation runs") + void untaggedLevelARequestFails() throws Exception { + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-2a", false); + assertThat(ex).isNotNull(); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(ex.getReason()).contains("could not be tagged"); + verifyNoInteractions(veraPDFService); + } + + @Test + @DisplayName("a compliant PDF/UA result never satisfies a strict PDF/A request") + void accessibilityResultIsIgnored() throws Exception { + PDFVerificationResult ua = new PDFVerificationResult(); + ua.setCompliant(true); + ua.setStandard("ua1"); + ua.setValidationProfile("ua1"); + ua.setComplianceSummary("PDF/UA-1 compliant"); + when(veraPDFService.validatePDF(any())).thenReturn(List.of(ua)); + + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-2b", true); + assertThat(ex).isNotNull(); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + @Test @DisplayName("empty result list is treated as non-compliant -> 400") void emptyResultsTreatedNonCompliant() throws Exception { when(veraPDFService.validatePDF(any())).thenReturn(Collections.emptyList()); - ConvertPDFToPDFA controller = newController(); - ResponseStatusException ex = - (ResponseStatusException) - catchThrowable( - () -> - invokeInstance( - controller, - "verifyStrictCompliance", - (Object) "dummy".getBytes())); + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-1", true); assertThat(ex).isNotNull(); assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @@ -782,15 +850,7 @@ class ConvertPDFToPDFAGapTest { void serviceErrorWrappedAs500() throws Exception { when(veraPDFService.validatePDF(any())).thenThrow(new IOException("boom")); - ConvertPDFToPDFA controller = newController(); - ResponseStatusException ex = - (ResponseStatusException) - catchThrowable( - () -> - invokeInstance( - controller, - "verifyStrictCompliance", - (Object) "dummy".getBytes())); + ResponseStatusException ex = (ResponseStatusException) verify("pdfa-1", true); assertThat(ex).isNotNull(); assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAMoreTest.java index 31bbc9e1f8..d56a283464 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFAMoreTest.java @@ -42,6 +42,7 @@ import org.springframework.mock.web.MockMultipartFile; import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; import stirling.software.SPDF.service.VeraPDFService; import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.service.PdfaLevelAServiceInterface; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.TempFile; @@ -63,10 +64,12 @@ class ConvertPDFToPDFAMoreTest { @Mock private RuntimePathConfig runtimePathConfig; @Mock private VeraPDFService veraPDFService; + @Mock private PdfaLevelAServiceInterface pdfaLevelAService; @Mock private TempFileManager tempFileManager; private ConvertPDFToPDFA newController() { - return new ConvertPDFToPDFA(runtimePathConfig, veraPDFService, tempFileManager); + return new ConvertPDFToPDFA( + runtimePathConfig, veraPDFService, pdfaLevelAService, tempFileManager); } private static ResponseEntity streamingOk(byte[] bytes) { @@ -197,7 +200,7 @@ class ConvertPDFToPDFAMoreTest { // qpdf normalize/clean writes its (last-arg) output file if (command.contains("--normalize-content=y")) { // qpdf produced file is the last argument - Path out = Path.of(command.get(command.size() - 1)); + Path out = Path.of(command.getLast()); Files.write(out, simplePdfBytes()); } return okResult; diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoControllerTest.java index 7a46611443..52104df48e 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoControllerTest.java @@ -262,7 +262,7 @@ class ConvertPdfToVideoControllerTest { assertTrue(command.contains("+faststart")); assertFalse(command.contains("libvpx-vp9")); // Output path is always the last argument. - assertEquals(backing.getAbsolutePath(), command.get(command.size() - 1)); + assertEquals(backing.getAbsolutePath(), command.getLast()); } @Test @@ -278,7 +278,7 @@ class ConvertPdfToVideoControllerTest { assertTrue(command.contains("30")); assertFalse(command.contains("libx264")); assertFalse(command.contains("+faststart")); - assertEquals(backing.getAbsolutePath(), command.get(command.size() - 1)); + assertEquals(backing.getAbsolutePath(), command.getLast()); } @Test diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRotateControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRotateControllerTest.java new file mode 100644 index 0000000000..0545a17058 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRotateControllerTest.java @@ -0,0 +1,342 @@ +package stirling.software.SPDF.controller.api.misc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.apache.pdfbox.util.Matrix; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; + +import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult; +import stirling.software.SPDF.model.api.misc.AutoRotatePdfRequest; +import stirling.software.SPDF.model.api.misc.PageRotation; +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; + +@ExtendWith(MockitoExtension.class) +class AutoRotateControllerTest { + + private static final String SAMPLE_TEXT = + "The quick brown fox jumps over the lazy dog again and again"; + + @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; + @Mock private EndpointConfiguration endpointConfiguration; + @Mock private RuntimePathConfig runtimePathConfig; + @Mock private ApplicationProperties applicationProperties; + + @InjectMocks private AutoRotateController controller; + + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + lenient().when(endpointConfiguration.isGroupEnabled("tesseract")).thenReturn(false); + } + + private static PDDocument docWithUprightText(int... pageRotations) throws IOException { + PDDocument document = new PDDocument(); + for (int rotation : pageRotations) { + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix(Matrix.getTranslateInstance(72, 400)); + content.showText(SAMPLE_TEXT); + content.endText(); + } + page.setRotation(rotation); + } + return document; + } + + private AutoRotatePdfRequest request(PDDocument document) throws IOException { + AutoRotatePdfRequest request = new AutoRotatePdfRequest(); + request.setFileInput( + new MockMultipartFile( + "fileInput", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + new byte[] {1, 2, 3})); + when(pdfDocumentFactory.load(request)).thenReturn(document); + return request; + } + + private static PDDocument reload(ResponseEntity response) throws IOException { + Resource resource = (Resource) response.getBody(); + return Loader.loadPDF(resource.getContentAsByteArray()); + } + + private static PDDocument docWithTextAt(int textAngleDegrees, int pageRotation) + throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix( + Matrix.getRotateInstance(Math.toRadians(textAngleDegrees), 300, 400)); + content.showText(SAMPLE_TEXT); + content.endText(); + } + page.setRotation(pageRotation); + return document; + } + + /** + * Reads the dominant glyph direction straight from a document, independently of the production + * detection code, so the round-trip assertion below validates the result rather than restating + * the formula under test. + */ + private static int dominantGlyphDirection(PDDocument document) throws IOException { + int[] counts = new int[4]; + PDFTextStripper stripper = + new PDFTextStripper() { + @Override + protected void processTextPosition(TextPosition text) { + if (!text.getUnicode().isBlank()) { + counts[Math.floorMod(Math.round(text.getDir()), 360) / 90]++; + } + } + }; + stripper.setStartPage(1); + stripper.setEndPage(1); + stripper.getText(document); + int best = 0; + for (int i = 1; i < 4; i++) { + if (counts[i] > counts[best]) { + best = i; + } + } + return best * 90; + } + + /** + * End-to-end round trip: build a page whose text is drawn at a known angle under a known + * /Rotate, run the real controller, then assert the output actually displays upright. Upright + * means the glyph direction and the page rotation cancel — computed here in the test, not via + * the production helper. + */ + @ParameterizedTest + @CsvSource({ + "0, 0", + "0, 90", + "0, 180", + "0, 270", + "90, 0", + "90, 90", + "180, 0", + "180, 270", + "270, 90", + }) + void roundTripLeavesPageUpright(int textAngle, int pageRotation) throws Exception { + AutoRotatePdfRequest request = request(docWithTextAt(textAngle, pageRotation)); + request.setDetectionMode("text"); + + ResponseEntity response = controller.autoRotatePdf(request); + + try (PDDocument corrected = reload(response)) { + int glyphDirection = dominantGlyphDirection(corrected); + int finalRotation = Math.floorMod(corrected.getPage(0).getRotation(), 360); + assertThat(Math.floorMod(glyphDirection - finalRotation, 360)) + .as( + "text drawn at %d under /Rotate %d should display upright, got glyph" + + " direction %d with /Rotate %d", + textAngle, pageRotation, glyphDirection, finalRotation) + .isZero(); + } + } + + @Test + void dryRunReportsTextDetection() throws Exception { + AutoRotatePdfRequest request = request(docWithUprightText(90, 0)); + request.setDryRun(true); + request.setDetectionMode("text"); + + ResponseEntity response = controller.autoRotatePdf(request); + + AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody(); + assertThat(result.getTotalPages()).isEqualTo(2); + assertThat(result.getPagesToRotate()).isEqualTo(1); + assertThat(result.getDetectedByText()).isEqualTo(2); + + AutoRotateAnalysisResult.PageResult first = result.getPages().get(0); + assertThat(first.getMethod()).isEqualTo("text"); + assertThat(first.getCorrection()).isEqualTo(270); + assertThat(first.isApply()).isTrue(); + assertThat(first.getConfidence()).isEqualTo(100.0); + + AutoRotateAnalysisResult.PageResult second = result.getPages().get(1); + assertThat(second.getCorrection()).isZero(); + assertThat(second.isApply()).isFalse(); + } + + @Test + void appliesDetectedCorrections() throws Exception { + AutoRotatePdfRequest request = request(docWithUprightText(90, 0)); + request.setDetectionMode("text"); + + ResponseEntity response = controller.autoRotatePdf(request); + + try (PDDocument corrected = reload(response)) { + assertThat(corrected.getPage(0).getRotation()).isZero(); + assertThat(corrected.getPage(1).getRotation()).isZero(); + } + } + + @Test + void appliesExplicitPageRotations() throws Exception { + AutoRotatePdfRequest request = request(docWithUprightText(0, 0)); + request.setPageRotations(List.of(new PageRotation(1, 90))); + + ResponseEntity response = controller.autoRotatePdf(request); + + try (PDDocument corrected = reload(response)) { + assertThat(corrected.getPage(0).getRotation()).isEqualTo(90); + assertThat(corrected.getPage(1).getRotation()).isZero(); + } + } + + @Test + void reportsTesseractUnavailableForTextlessPages() throws Exception { + PDDocument document = new PDDocument(); + document.addPage(new PDPage(PDRectangle.LETTER)); + AutoRotatePdfRequest request = request(document); + request.setDryRun(true); + + ResponseEntity response = controller.autoRotatePdf(request); + + AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody(); + AutoRotateAnalysisResult.PageResult page = result.getPages().get(0); + assertThat(page.getMethod()).isEqualTo("none"); + assertThat(page.getNote()).isEqualTo("tesseractUnavailable"); + assertThat(result.getUndetected()).isEqualTo(1); + } + + @Test + void infersUndetectedPageFromDocumentConsensus() throws Exception { + // Page 1 has body text and is rotated 90 (-> 270 correction); page 2 is blank and shares + // the same rotation. With OSD unavailable, page 2 can't be detected on its own, so it + // should inherit page 1's 270 correction. + PDDocument document = docWithUprightText(90); + PDPage blank = new PDPage(PDRectangle.LETTER); + blank.setRotation(90); + document.addPage(blank); + AutoRotatePdfRequest request = request(document); + request.setDryRun(true); + request.setDetectionMode("text"); + + ResponseEntity response = controller.autoRotatePdf(request); + + AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody(); + AutoRotateAnalysisResult.PageResult page2 = result.getPages().get(1); + assertThat(page2.getMethod()).isEqualTo("inferred"); + assertThat(page2.getCorrection()).isEqualTo(270); + assertThat(page2.isApply()).isTrue(); + assertThat(page2.getNote()).isEqualTo("inferredFromDocument"); + assertThat(result.getInferred()).isEqualTo(1); + assertThat(result.getPagesToRotate()).isEqualTo(2); + } + + @Test + void doesNotInferWhenDisabled() throws Exception { + PDDocument document = docWithUprightText(90); + PDPage blank = new PDPage(PDRectangle.LETTER); + blank.setRotation(90); + document.addPage(blank); + AutoRotatePdfRequest request = request(document); + request.setDryRun(true); + request.setDetectionMode("text"); + request.setInferUndetected(false); + + ResponseEntity response = controller.autoRotatePdf(request); + + AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody(); + AutoRotateAnalysisResult.PageResult page2 = result.getPages().get(1); + assertThat(page2.getMethod()).isEqualTo("none"); + assertThat(page2.isApply()).isFalse(); + assertThat(result.getInferred()).isZero(); + assertThat(result.getUndetected()).isEqualTo(1); + } + + @Test + void rejectsInvalidDetectionMode() { + AutoRotatePdfRequest request = new AutoRotatePdfRequest(); + request.setDetectionMode("magic"); + + assertThatThrownBy(() -> controller.autoRotatePdf(request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void rejectsRotationThatIsNotAMultipleOf90() throws Exception { + AutoRotatePdfRequest request = request(docWithUprightText(0)); + request.setPageRotations(List.of(new PageRotation(1, 45))); + + assertThatThrownBy(() -> controller.autoRotatePdf(request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void rejectsPageRotationOutsideTheDocument() throws Exception { + AutoRotatePdfRequest request = request(docWithUprightText(0)); + request.setPageRotations(List.of(new PageRotation(5, 90))); + + assertThatThrownBy(() -> controller.autoRotatePdf(request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void rejectsDuplicatePageInRotations() throws Exception { + // Rotations are additive, so applying the same page twice would over-rotate it. + AutoRotatePdfRequest request = request(docWithUprightText(0, 0)); + request.setPageRotations(List.of(new PageRotation(1, 90), new PageRotation(1, 90))); + + assertThatThrownBy(() -> controller.autoRotatePdf(request)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/CompressControllerMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/CompressControllerMoreTest.java index b8f4e51b3f..4393c19e3f 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/CompressControllerMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/CompressControllerMoreTest.java @@ -195,7 +195,7 @@ class CompressControllerMoreTest { // The qpdf output path is the last argument of the command. private static Path qpdfOutputPath(List command) { - return Path.of(command.get(command.size() - 1)); + return Path.of(command.getLast()); } /** Stub gs to write a valid PDF to its output file and report success. */ diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java index bfacb4d503..dc9c9538cd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/MobileScannerControllerTest.java @@ -49,6 +49,33 @@ class MobileScannerControllerTest { when(systemProps.isEnableMobileScanner()).thenReturn(false); } + // --- shared-endpoint gating: scanner and mobile signature share this API --- + + @Test + void createSession_whenOnlyMobileSignatureEnabled_returnsOk() { + // The signature feature must work with the scanner switched off. + when(applicationProperties.getSystem()).thenReturn(systemProps); + when(systemProps.isEnableMobileScanner()).thenReturn(false); + when(systemProps.isEnableMobileSignature()).thenReturn(true); + SessionInfo sessionInfo = new SessionInfo("test-session", 1000L, 601000L, 600000L); + when(mobileScannerService.createSession("test-session")).thenReturn(sessionInfo); + + ResponseEntity> response = controller.createSession("test-session"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + void createSession_whenBothFeaturesDisabled_returnsForbidden() { + when(applicationProperties.getSystem()).thenReturn(systemProps); + when(systemProps.isEnableMobileScanner()).thenReturn(false); + when(systemProps.isEnableMobileSignature()).thenReturn(false); + + ResponseEntity> response = controller.createSession("test-session"); + + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + } + // --- createSession tests --- @Test diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RemoveImagesControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RemoveImagesControllerTest.java index 1a97cc1a55..db361b4302 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RemoveImagesControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RemoveImagesControllerTest.java @@ -162,7 +162,7 @@ class RemoveImagesControllerTest { /** Counts every PDImageXObject reachable through page + nested form resources. */ private int countImagesInSavedOutput() throws IOException { assertFalse(savedTempFiles.isEmpty(), "expected the controller to create a temp file"); - File out = savedTempFiles.get(savedTempFiles.size() - 1); + File out = savedTempFiles.getLast(); try (PDDocument doc = Loader.loadPDF(out)) { int count = 0; for (PDPage page : doc.getPages()) { @@ -245,7 +245,7 @@ class RemoveImagesControllerTest { assertEquals(0, countImagesInSavedOutput()); // page count must be preserved - File out = savedTempFiles.get(savedTempFiles.size() - 1); + File out = savedTempFiles.getLast(); try (PDDocument result = Loader.loadPDF(out)) { assertEquals(3, result.getNumberOfPages()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RepairControllerMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RepairControllerMoreTest.java index 6565a84875..f564dafb11 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RepairControllerMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/RepairControllerMoreTest.java @@ -98,12 +98,8 @@ class RepairControllerMoreTest { } } - /** - * Writes a valid PDF to the path at the given command index, mimicking a successful tool run. - */ - private static void writeValidPdfTo(List command, int outputPathIndex) - throws Exception { - Path out = Path.of(command.get(outputPathIndex)); + /** Writes a valid PDF to the given output path, mimicking a successful tool run. */ + private static void writeValidPdfTo(Path out) throws Exception { byte[] pdf = buildPdfBytes(1); Files.write(out, pdf); } @@ -133,7 +129,7 @@ class RepairControllerMoreTest { .thenAnswer( inv -> { List cmd = inv.getArgument(0); - writeValidPdfTo(cmd, 2); + writeValidPdfTo(Path.of(cmd.get(2))); return okResult; }); @@ -176,7 +172,7 @@ class RepairControllerMoreTest { .thenAnswer( inv -> { List cmd = inv.getArgument(0); - writeValidPdfTo(cmd, cmd.size() - 1); + writeValidPdfTo(Path.of(cmd.getLast())); return okResult; }); @@ -216,7 +212,7 @@ class RepairControllerMoreTest { .thenAnswer( inv -> { List cmd = inv.getArgument(0); - writeValidPdfTo(cmd, cmd.size() - 1); + writeValidPdfTo(Path.of(cmd.getLast())); return okResult; }); @@ -256,7 +252,7 @@ class RepairControllerMoreTest { .thenAnswer( inv -> { List cmd = inv.getArgument(0); - writeValidPdfTo(cmd, cmd.size() - 1); + writeValidPdfTo(Path.of(cmd.getLast())); return okResult; }); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java index e87d08a863..aad630448f 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java @@ -25,6 +25,7 @@ import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; import stirling.software.common.service.InternalApiClient; +import stirling.software.common.service.ToolMetadataService; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -32,6 +33,8 @@ class PipelineProcessorTest { @Mock ApiDocService apiDocService; + @Mock ToolMetadataService toolMetadataService; + @Mock InternalApiClient internalApiClient; @Mock TempFileManager tempFileManager; @@ -41,7 +44,8 @@ class PipelineProcessorTest { @BeforeEach void setUp() throws Exception { pipelineProcessor = - new PipelineProcessor(apiDocService, internalApiClient, tempFileManager); + new PipelineProcessor( + apiDocService, toolMetadataService, internalApiClient, tempFileManager); } @Test @@ -55,8 +59,9 @@ class PipelineProcessorTest { Resource file = new MyFileByteArrayResource(); List files = List.of(file); - when(apiDocService.isMultiInput("/api/v1/filter/filter-page-count")).thenReturn(false); - when(apiDocService.getExtensionTypes(false, "/api/v1/filter/filter-page-count")) + when(toolMetadataService.isMultiInput("/api/v1/filter/filter-page-count")) + .thenReturn(false); + when(toolMetadataService.getExtensionTypes(false, "/api/v1/filter/filter-page-count")) .thenReturn(List.of("pdf")); when(apiDocService.isValidOperation(eq("/api/v1/filter/filter-page-count"), anyMap())) .thenReturn(true); @@ -99,8 +104,9 @@ class PipelineProcessorTest { } }; - when(apiDocService.isMultiInput(anyString())).thenReturn(false); - when(apiDocService.getExtensionTypes(anyBoolean(), anyString())).thenReturn(List.of("pdf")); + when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + when(toolMetadataService.getExtensionTypes(anyBoolean(), anyString())) + .thenReturn(List.of("pdf")); when(apiDocService.isValidOperation(anyString(), anyMap())).thenReturn(true); when(internalApiClient.post(anyString(), any())) diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java index 7e2aa388fa..1df66f739e 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFMoreTest.java @@ -264,21 +264,6 @@ class GetInfoOnPDFMoreTest { @DisplayName("error handling") class Errors { - @Test - @DisplayName("empty file input yields an error response") - void emptyFile() throws Exception { - MockMultipartFile mf = - new MockMultipartFile("fileInput", "x.pdf", "application/pdf", new byte[0]); - PDFFile request = new PDFFile(); - request.setFileInput(mf); - ResponseEntity resp = getInfoOnPDF.getPdfInfo(request); - // createErrorResponse returns HTTP 200 with a JSON body carrying an "error" field. - assertThat(resp.getBody()).isNotNull(); - JsonNode body = om.readTree(resp.getBody()); - assertThat(body.has("error")).isTrue(); - assertThat(body.get("error").asText("")).contains("Invalid"); - } - @Test @DisplayName("veraPDF failure is swallowed and a report is still produced") void veraPdfFailureSwallowed() throws Exception { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java index efc09cf190..8d17d729ea 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDFTest.java @@ -556,24 +556,6 @@ class GetInfoOnPDFTest { @DisplayName("Validation and Error Handling Tests") class ValidationErrorTests { - @Test - @DisplayName("Should reject null file") - void testValidation_NullFile() throws IOException { - PDFFile request = new PDFFile(); - request.setFileInput(null); - - ResponseEntity response = getInfoOnPDF.getPdfInfo(request); - - Assertions.assertEquals( - HttpStatus.OK, response.getStatusCode()); // Returns error JSON with 200 - String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8); - JsonNode jsonNode = objectMapper.readTree(jsonResponse); - - Assertions.assertTrue(jsonNode.has("error")); - Assertions.assertTrue( - jsonNode.get("error").asText("").contains("PDF file is required")); - } - @Test @DisplayName("Should reject empty file") void testValidation_EmptyFile() throws IOException { @@ -591,64 +573,6 @@ class GetInfoOnPDFTest { Assertions.assertTrue(jsonNode.has("error")); } - - @Test - @DisplayName("Should reject file that exceeds max size") - void testValidation_TooLargeFile() throws IOException { - MultipartFile largeFile = - new MultipartFile() { - @Override - public String getName() { - return "file"; - } - - @Override - public String getOriginalFilename() { - return "large.pdf"; - } - - @Override - public String getContentType() { - return MediaType.APPLICATION_PDF_VALUE; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public long getSize() { - // Report 101 MB without allocating memory - return 101L * 1024L * 1024L; - } - - @Override - public byte[] getBytes() { - return new byte[0]; - } - - @Override - public java.io.InputStream getInputStream() { - return java.io.InputStream.nullInputStream(); - } - - @Override - public void transferTo(java.io.File dest) throws IllegalStateException {} - }; - - PDFFile request = new PDFFile(); - request.setFileInput(largeFile); - - ResponseEntity response = getInfoOnPDF.getPdfInfo(request); - - String jsonResponse = new String(response.getBody(), StandardCharsets.UTF_8); - JsonNode jsonNode = objectMapper.readTree(jsonResponse); - - Assertions.assertTrue(jsonNode.has("error")); - Assertions.assertTrue( - jsonNode.get("error").asText("").contains("exceeds maximum allowed size")); - } } @Nested diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerMoreTest.java index 150a2355de..0065b02cee 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerMoreTest.java @@ -24,6 +24,8 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.junit.jupiter.api.AfterEach; @@ -120,12 +122,22 @@ class RedactControllerMoreTest { .thenAnswer(inv -> Loader.loadPDF(pdfBytes)); } + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } + return new PDType1Font(Standard14Fonts.FontName.HELVETICA); + } + private byte[] singlePageTextPdf(String... lines) throws IOException { try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); for (int i = 0; i < lines.length; i++) { cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y - i * 16f); @@ -145,7 +157,7 @@ class RedactControllerMoreTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y); cs.showText(line); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java index 15774415a9..2f78ee033b 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java @@ -14,19 +14,25 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; +import java.util.Map; import org.apache.pdfbox.contentstream.operator.Operator; import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSFloat; import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.junit.jupiter.api.AfterEach; @@ -49,6 +55,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.common.model.api.security.RedactionArea; @@ -64,9 +71,9 @@ class RedactControllerTest { return ResponseEntity.ok(new ByteArrayResource(bytes)); } - private static byte[] drainBody(ResponseEntity response) throws java.io.IOException { - java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); - try (java.io.InputStream __in = response.getBody().getInputStream()) { + private static byte[] drainBody(ResponseEntity response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream __in = response.getBody().getInputStream()) { __in.transferTo(baos); } return baos.toByteArray(); @@ -90,13 +97,24 @@ class RedactControllerTest { private PDDocument realDocument; private PDPage realPage; + private static PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + RedactControllerTest.class.getResourceAsStream( + "/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } + return new PDType1Font(Standard14Fonts.FontName.HELVETICA); + } + private static byte[] createSimplePdfContent() throws IOException { try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); doc.addPage(page); try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { contentStream.beginText(); - contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.setFont(helvetica(doc), 12); contentStream.newLineAtOffset(100, 700); contentStream.showText("This is a simple PDF."); contentStream.endText(); @@ -156,8 +174,7 @@ class RedactControllerTest { mockDocument = mock(PDDocument.class); mockPages = mock(PDPageTree.class); mockPage = mock(PDPage.class); - org.apache.pdfbox.pdmodel.PDDocumentCatalog mockCatalog = - mock(org.apache.pdfbox.pdmodel.PDDocumentCatalog.class); + PDDocumentCatalog mockCatalog = mock(PDDocumentCatalog.class); // Setup document structure properly when(pdfDocumentFactory.load(any(MockMultipartFile.class))).thenReturn(mockDocument); @@ -182,9 +199,8 @@ class RedactControllerTest { when(mockPage.hasContents()).thenReturn(true); - org.apache.pdfbox.cos.COSDocument mockCOSDocument = - mock(org.apache.pdfbox.cos.COSDocument.class); - org.apache.pdfbox.cos.COSStream mockCOSStream = mock(org.apache.pdfbox.cos.COSStream.class); + COSDocument mockCOSDocument = mock(COSDocument.class); + COSStream mockCOSStream = mock(COSStream.class); when(mockDocument.getDocument()).thenReturn(mockCOSDocument); when(mockCOSDocument.createCOSStream()).thenReturn(mockCOSStream); @@ -338,17 +354,13 @@ class RedactControllerTest { when(mockPages.get(0)).thenReturn(mockPage); - org.apache.pdfbox.pdmodel.PDDocumentInformation mockInfo = - mock(org.apache.pdfbox.pdmodel.PDDocumentInformation.class); + PDDocumentInformation mockInfo = mock(PDDocumentInformation.class); when(mockDocument.getDocumentInformation()).thenReturn(mockInfo); ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); - - verify(mockDocument).save(any(File.class)); - verify(mockDocument).close(); } } @@ -753,8 +765,6 @@ class RedactControllerTest { assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); assertTrue(drainBody(response).length > 0); - verify(mockDocument, times(1)).save(any(File.class)); - verify(mockDocument, times(1)).close(); } } catch (Exception e) { if (expectSuccess) { @@ -788,11 +798,14 @@ class RedactControllerTest { realPage = new PDPage(PDRectangle.A4); realDocument.addPage(realPage); - // Set up basic page resources - PDResources resources = new PDResources(); - resources.put( - COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); - realPage.setResources(resources); + // Set up basic page resources with embedded font + try { + PDResources resources = new PDResources(); + resources.put(COSName.getPDFName("F1"), helvetica(realDocument)); + realPage.setResources(resources); + } catch (IOException e) { + throw new RuntimeException(e); + } } // Helper methods for real PDF content creation @@ -803,8 +816,7 @@ class RedactControllerTest { } realDocument.addPage(realPage); realPage.setResources(new PDResources()); - realPage.getResources() - .put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + realPage.getResources().put(COSName.getPDFName("F1"), helvetica(realDocument)); try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) { contentStream.beginText(); @@ -822,8 +834,7 @@ class RedactControllerTest { } realDocument.addPage(realPage); realPage.setResources(new PDResources()); - realPage.getResources() - .put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + realPage.getResources().put(COSName.getPDFName("F1"), helvetica(realDocument)); try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) { contentStream.beginText(); @@ -846,8 +857,7 @@ class RedactControllerTest { } realDocument.addPage(realPage); realPage.setResources(new PDResources()); - realPage.getResources() - .put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + realPage.getResources().put(COSName.getPDFName("F1"), helvetica(realDocument)); try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) { contentStream.setLineWidth(2); @@ -874,8 +884,7 @@ class RedactControllerTest { } realDocument.addPage(realPage); realPage.setResources(new PDResources()); - realPage.getResources() - .put(COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + realPage.getResources().put(COSName.getPDFName("F1"), helvetica(realDocument)); try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) { contentStream.beginText(); @@ -1005,22 +1014,6 @@ class RedactControllerTest { } } - private List getOriginalTokens() throws Exception { - // Create a new page to avoid side effects from other tests - PDPage pageForTokenExtraction = new PDPage(PDRectangle.A4); - pageForTokenExtraction.setResources(realPage.getResources()); - try (PDPageContentStream contentStream = - new PDPageContentStream(realDocument, pageForTokenExtraction)) { - contentStream.beginText(); - contentStream.setFont(realPage.getResources().getFont(COSName.getPDFName("F1")), 12); - contentStream.newLineAtOffset(50, 750); - contentStream.showText("Original content"); - contentStream.endText(); - } - return textRedactionService.createTokensWithoutTargetText( - realDocument, pageForTokenExtraction, Collections.emptySet(), false, false); - } - @Nested @DisplayName("Color Decoding Utility Tests") class ColorDecodingTests { @@ -1099,318 +1092,19 @@ class RedactControllerTest { class ContentStreamUnitTests { @Test - @DisplayName("createTokensWithoutTargetText should remove simple text tokens") - void shouldRemoveSimpleTextTokens() throws Exception { - createRealPageWithSimpleText("This document contains confidential information."); + @DisplayName("performTextReplacement should process document text replacement") + void shouldPerformTextReplacement() throws Exception { + createRealPageWithSimpleText("This document contains sensitive information."); + String[] targetWords = new String[] {"sensitive"}; - Set targetWords = Set.of("confidential"); + Map> found = + textRedactionService.findTextToRedact(realDocument, targetWords, false, false); + assertFalse(found.isEmpty(), "Should find target text to redact"); - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - assertNotNull(tokens); - assertFalse(tokens.isEmpty()); - - String reconstructedText = extractTextFromTokens(tokens); - assertFalse( - reconstructedText.contains("confidential"), - "Target text should be replaced with placeholder"); - assertTrue(reconstructedText.contains("document"), "Non-target text should remain"); - } - - @Test - @DisplayName("createTokensWithoutTargetText should handle TJ operator arrays") - void shouldHandleTJOperatorArrays() throws Exception { - createRealPageWithTJArrayText(); - - Set targetWords = Set.of("secret"); - - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - assertNotNull(tokens); - - boolean foundModifiedTJArray = false; - for (Object token : tokens) { - if (token instanceof COSArray array) { - for (int i = 0; i < array.size(); i++) { - if (array.getObject(i) instanceof COSString cosString) { - String text = cosString.getString(); - if (text.contains("secret")) { - fail( - "Target text 'secret' should have been redacted from TJ" - + " array"); - } - foundModifiedTJArray = true; - } - } - } - } - assertTrue(foundModifiedTJArray, "Should find at least one TJ array"); - } - - @Test - @DisplayName("createTokensWithoutTargetText should preserve non-text tokens") - void shouldPreserveNonTextTokens() throws Exception { - createRealPageWithMixedContent(); - - Set targetWords = Set.of("redact"); - - List originalTokens = getOriginalTokens(); - List filteredTokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - long originalNonTextCount = - originalTokens.stream() - .filter( - token -> - token instanceof Operator op - && !textRedactionService.isTextShowingOperator( - op.getName())) - .count(); - - long filteredNonTextCount = - filteredTokens.stream() - .filter( - token -> - token instanceof Operator op - && !textRedactionService.isTextShowingOperator( - op.getName())) - .count(); - - assertTrue(filteredNonTextCount > 0, "Non-text operators should be preserved"); - - assertTrue( - filteredNonTextCount >= originalNonTextCount / 2, - "A reasonable number of non-text operators should be preserved"); - } - - @Test - @DisplayName("createTokensWithoutTargetText should handle regex patterns") - void shouldHandleRegexPatterns() throws Exception { - createRealPageWithSimpleText("Phone: 123-456-7890 and SSN: 111-22-3333"); - - Set targetWords = Set.of("\\d{3}-\\d{2}-\\d{4}"); // SSN pattern - - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, true, false); - - String reconstructedText = extractTextFromTokens(tokens); - assertFalse(reconstructedText.contains("111-22-3333"), "SSN should be redacted"); - assertTrue(reconstructedText.contains("123-456-7890"), "Phone should remain"); - } - - @Test - @DisplayName("createTokensWithoutTargetText should handle whole word search") - void shouldHandleWholeWordSearch() throws Exception { - createRealPageWithSimpleText("This test testing tested document"); - - Set targetWords = Set.of("test"); - - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, true); - - String reconstructedText = extractTextFromTokens(tokens); - assertTrue(reconstructedText.contains("testing"), "Partial matches should remain"); - assertTrue(reconstructedText.contains("tested"), "Partial matches should remain"); - } - - @ParameterizedTest - @ValueSource(strings = {"Tj", "TJ", "'", "\""}) - @DisplayName("createTokensWithoutTargetText should handle all text operators") - void shouldHandleAllTextOperators(String operatorName) throws Exception { - createRealPageWithSpecificOperator(operatorName); - - Set targetWords = Set.of("sensitive"); - - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - String reconstructedText = extractTextFromTokens(tokens); - assertFalse( - reconstructedText.contains("sensitive"), - "Text should be redacted regardless of operator type"); - } - - @Test - @DisplayName("writeFilteredContentStream should write tokens to new stream") - void shouldWriteTokensToNewContentStream() throws Exception { - List tokens = createSampleTokenList(); - - textRedactionService.writeFilteredContentStream(realDocument, realPage, tokens); - - assertNotNull(realPage.getContents(), "Page should have content stream"); - - // Verify the content can be read back - try (InputStream inputStream = realPage.getContents()) { - byte[] content = readAllBytes(inputStream); - assertTrue(content.length > 0, "Content stream should not be empty"); - } - } - - @Test - @DisplayName("writeFilteredContentStream should handle empty token list") - void shouldHandleEmptyTokenList() throws Exception { - List emptyTokens = Collections.emptyList(); - - assertDoesNotThrow( - () -> - textRedactionService.writeFilteredContentStream( - realDocument, realPage, emptyTokens)); - - assertNotNull(realPage.getContents(), "Page should still have content stream"); - } - - @Test - @DisplayName("writeFilteredContentStream should replace existing content") - void shouldReplaceExistingContentStream() throws Exception { - createRealPageWithSimpleText("Original content"); - String originalContent = extractTextFromModifiedPage(realPage); - - List newTokens = createSampleTokenList(); - textRedactionService.writeFilteredContentStream(realDocument, realPage, newTokens); - - String newContent = extractTextFromModifiedPage(realPage); - assertNotEquals(originalContent, newContent, "Content stream should be replaced"); - } - - @Test - @DisplayName("Placeholder creation should maintain text width") - void shouldCreateWidthMatchingPlaceholder() { - String originalText = "confidential"; - String placeholder = - textRedactionService.createPlaceholderWithFont( - originalText, new PDType1Font(Standard14Fonts.FontName.HELVETICA)); - - assertEquals( - originalText.length(), - placeholder.length(), - "Placeholder should maintain character count for width preservation"); - } - - @Test - @DisplayName("Placeholder should handle special characters") - void shouldHandleSpecialCharactersInPlaceholder() { - String originalText = "café naïve"; - String placeholder = - textRedactionService.createPlaceholderWithFont( - originalText, new PDType1Font(Standard14Fonts.FontName.HELVETICA)); - - assertEquals(originalText.length(), placeholder.length()); - assertFalse( - placeholder.contains("café"), "Placeholder should not contain original text"); - } - - @Test - @DisplayName("Integration test: createTokens and writeStream") - void shouldIntegrateTokenCreationAndWriting() throws Exception { - createRealPageWithSimpleText("This document contains secret information."); - - Set targetWords = Set.of("secret"); - - List filteredTokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - textRedactionService.writeFilteredContentStream(realDocument, realPage, filteredTokens); - assertNotNull(realPage.getContents()); - - String finalText = extractTextFromModifiedPage(realPage); - assertFalse(finalText.contains("secret"), "Target text should be completely removed"); - assertTrue(finalText.contains("document"), "Other text should remain"); - } - - @Test - @DisplayName("Should preserve text positioning operators") - void shouldPreserveTextPositioning() throws Exception { - createRealPageWithPositionedText(); - - Set targetWords = Set.of("confidential"); - - List filteredTokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - long filteredPositioning = - filteredTokens.stream() - .filter( - token -> - token instanceof Operator op - && ("Td".equals(op.getName()) - || "TD".equals(op.getName()) - || "Tm".equals(op.getName()))) - .count(); - - assertTrue(filteredPositioning > 0, "Positioning operators should be preserved"); - } - - @Test - @DisplayName("Should handle complex content streams with multiple operators") - void shouldHandleComplexContentStreams() throws Exception { - realPage = new PDPage(PDRectangle.A4); - while (realDocument.getNumberOfPages() > 0) { - realDocument.removePage(0); - } - realDocument.addPage(realPage); - realPage.setResources(new PDResources()); - realPage.getResources() - .put( - COSName.getPDFName("F1"), - new PDType1Font(Standard14Fonts.FontName.HELVETICA)); - - try (PDPageContentStream contentStream = - new PDPageContentStream(realDocument, realPage)) { - contentStream.setLineWidth(2); - contentStream.moveTo(100, 100); - contentStream.lineTo(200, 200); - contentStream.stroke(); - - contentStream.beginText(); - contentStream.setFont( - realPage.getResources().getFont(COSName.getPDFName("F1")), 12); - contentStream.newLineAtOffset(50, 750); - contentStream.showText("This is a complex document with "); - contentStream.setTextRise(5); - contentStream.showText("confidential"); - contentStream.setTextRise(0); - contentStream.showText(" information."); - contentStream.endText(); - - contentStream.addRect(300, 300, 100, 100); - contentStream.fill(); - } - - Set targetWords = Set.of("confidential"); - - List tokens = - textRedactionService.createTokensWithoutTargetText( - realDocument, realPage, targetWords, false, false); - - assertNotNull(tokens); - assertFalse(tokens.isEmpty()); - - String reconstructedText = extractTextFromTokens(tokens); - assertFalse( - reconstructedText.contains("confidential"), "Target text should be redacted"); - - boolean hasGraphicsOperators = - tokens.stream() - .anyMatch( - token -> - token instanceof Operator op - && ("re".equals(op.getName()) - || "f".equals(op.getName()) - || "m".equals(op.getName()) - || "l".equals(op.getName()) - || "S".equals(op.getName()))); - - assertTrue(hasGraphicsOperators, "Graphics operators should be preserved"); + boolean fallback = + textRedactionService.performTextReplacement( + realDocument, found, targetWords, false, false); + assertFalse(fallback, "JPDFium text replacement should complete without fallback"); } @Test @@ -1423,14 +1117,13 @@ class RedactControllerTest { realDocument.addPage(realPage); PDResources resources = new PDResources(); - resources.put( - COSName.getPDFName("F1"), new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + resources.put(COSName.getPDFName("F1"), helvetica(realDocument)); realPage.setResources(resources); try (PDPageContentStream contentStream = new PDPageContentStream(realDocument, realPage)) { contentStream.beginText(); - contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.setFont(helvetica(realDocument), 12); contentStream.newLineAtOffset(50, 750); contentStream.showText("This is the first text block"); contentStream.endText(); @@ -1441,7 +1134,7 @@ class RedactControllerTest { contentStream.stroke(); contentStream.beginText(); - contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.setFont(helvetica(realDocument), 12); contentStream.newLineAtOffset(50, 650); contentStream.showText("This block contains confidential information"); contentStream.endText(); @@ -1450,7 +1143,7 @@ class RedactControllerTest { contentStream.fill(); contentStream.beginText(); - contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.setFont(helvetica(realDocument), 12); contentStream.newLineAtOffset(50, 550); contentStream.showText("This is the third text block"); contentStream.endText(); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceMoreTest.java index 49149e3ca7..246909efba 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceMoreTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.mock; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Method; import java.nio.file.Files; import java.util.ArrayList; @@ -22,6 +23,8 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; @@ -31,6 +34,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; import stirling.software.SPDF.model.PDFText; @@ -118,18 +122,26 @@ class RedactExecuteServiceMoreTest { private RedactExecuteRequest requestFor(byte[] pdfBytes) { RedactExecuteRequest req = new RedactExecuteRequest(); - req.setFileInput( - new org.springframework.mock.web.MockMultipartFile( - "fileInput", "in.pdf", "application/pdf", pdfBytes)); + req.setFileInput(new MockMultipartFile("fileInput", "in.pdf", "application/pdf", pdfBytes)); return req; } + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } + return new PDType1Font(Standard14Fonts.FontName.HELVETICA); + } + private byte[] singlePageTextPdf(String... lines) throws IOException { try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); for (int i = 0; i < lines.length; i++) { cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y - i * LINE_H); @@ -149,7 +161,7 @@ class RedactExecuteServiceMoreTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y); cs.showText("page " + p + " has SECRET content here"); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceTest.java index 8a8b335642..9d2f50a5e4 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactExecuteServiceTest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api.security; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -11,6 +12,8 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.junit.jupiter.api.DisplayName; @@ -219,12 +222,22 @@ class RedactExecuteServiceTest { * anchor) 1: line one 2: line two 3: line three 4: STOP-HERE (end anchor) 5: line five (must * NOT be redacted) */ + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } + return new PDType1Font(Standard14Fonts.FontName.HELVETICA); + } + private PDDocument buildSingleColumnDoc() throws IOException { PDDocument doc = new PDDocument(); PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); String[] lines = { "START-HERE", "line one", "line two", "line three", "STOP-HERE", "line five" }; @@ -247,7 +260,7 @@ class RedactExecuteServiceTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); // Body lines are padded to make each column genuinely wide enough that column // detection (which ignores narrow lines) treats both sides as real columns. String fill = " " + "x".repeat(26); @@ -294,7 +307,7 @@ class RedactExecuteServiceTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); String[] lines = { "#1 Auto layout", "Body about auto layout.", @@ -331,7 +344,7 @@ class RedactExecuteServiceTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); // Header — full width, lines 0..1. for (int i = 0; i < 2; i++) { cs.beginText(); @@ -383,7 +396,7 @@ class RedactExecuteServiceTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); float dateX = PAGE_WIDTH - 144f; // right-aligned dates near the right margin diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceExtraTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceExtraTest.java index 6c94dd3a24..94e4d7cb6c 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceExtraTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceExtraTest.java @@ -3,44 +3,24 @@ package stirling.software.SPDF.controller.api.security; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.io.InputStream; import java.util.List; import java.util.Map; -import java.util.Set; -import org.apache.pdfbox.contentstream.operator.Operator; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSFloat; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import stirling.software.SPDF.model.PDFText; -/** - * Further gap-coverage tests for {@link TextRedactionService}, complementing {@code - * TextRedactionServiceTest} and {@code TextRedactionServiceMoreTest}. These target branches the - * other two suites leave untouched: case-sensitive vs regex find, multi-term and multi-match within - * one segment, the kerning ({@code adjustment != 0}) path that rewrites a {@code Tj} into a {@code - * TJ} array, nested Form XObject traversal, pages with no resources, and the private width helpers - * exercised directly via reflection. - */ @DisplayName("TextRedactionService extra coverage") class TextRedactionServiceExtraTest { @@ -50,43 +30,22 @@ class TextRedactionServiceExtraTest { private final TextRedactionService service = new TextRedactionService(); - private PDFont helvetica() { + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } return new PDType1Font(Standard14Fonts.FontName.HELVETICA); } - private List parseTokens(PDPage page) throws IOException { - PDFStreamParser parser = new PDFStreamParser(page); - List tokens = new ArrayList<>(); - Object t; - while ((t = parser.parseNextToken()) != null) { - tokens.add(t); - } - return tokens; - } - - private String tokensText(List tokens) { - StringBuilder sb = new StringBuilder(); - for (Object token : tokens) { - if (token instanceof COSString cs) { - sb.append(cs.getString()); - } else if (token instanceof COSArray arr) { - for (COSBase el : arr) { - if (el instanceof COSString cs) { - sb.append(cs.getString()); - } - } - } - } - return sb.toString(); - } - - /** Single page, one Tj line per supplied text line, Helvetica 12. */ private PDDocument buildDoc(String... lines) throws IOException { PDDocument doc = new PDDocument(); PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(helvetica(), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); for (int i = 0; i < lines.length; i++) { cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y - i * 16f); @@ -97,25 +56,6 @@ class TextRedactionServiceExtraTest { return doc; } - /** Page whose content stream is exactly {@code rawContent}, font F1=Helvetica. */ - private PDDocument docWithRawContent(String rawContent) throws IOException { - PDDocument doc = new PDDocument(); - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - PDResources resources = new PDResources(); - resources.put(COSName.getPDFName("F1"), helvetica()); - page.setResources(resources); - - PDStream stream = new PDStream(doc); - try (var out = stream.createOutputStream()) { - out.write(rawContent.getBytes(StandardCharsets.ISO_8859_1)); - } - page.setContents(stream); - return doc; - } - - // ── findTextToRedact: matching modes ───────────────────────────────────────────────────────── - @Nested @DisplayName("findTextToRedact matching modes") class FindModes { @@ -126,7 +66,6 @@ class TextRedactionServiceExtraTest { try (PDDocument doc = buildDoc("Secret and secret and SECRET")) { Map> result = service.findTextToRedact(doc, new String[] {"secret"}, false, false); - // Patterns are compiled CASE_INSENSITIVE, so all three occurrences match. assertThat(result.get(0)).hasSize(3); } } @@ -148,7 +87,6 @@ class TextRedactionServiceExtraTest { try (PDDocument doc = buildDoc("abcde")) { Map> result = service.findTextToRedact(doc, new String[] {"[ae]"}, true, false); - // 'a' and 'e' both match -> two single-character hits. assertThat(result.get(0)).hasSize(2); } } @@ -163,436 +101,4 @@ class TextRedactionServiceExtraTest { } } } - - // ── createTokensWithoutTargetText structural branches ──────────────────────────────────────── - - @Nested - @DisplayName("createTokensWithoutTargetText structural branches") - class TokenStructural { - - @Test - @DisplayName("page with null resources still parses and redacts the matched Tj text") - void nullResourcesStillRedacts() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - // No resources set; the content stream references no real font. - String raw = "BT 72 700 Td (SECRET) Tj ET"; - PDStream stream = new PDStream(doc); - try (var out = stream.createOutputStream()) { - out.write(raw.getBytes(StandardCharsets.ISO_8859_1)); - } - page.setContents(stream); - - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - assertThat(tokensText(tokens)).doesNotContain("SECRET"); - } - } - - @Test - @DisplayName("a match inside a Tj segment is redacted and surrounding text survives") - void multipleMatchesOneSegment() throws IOException { - try (PDDocument doc = docWithRawContent("BT /F1 12 Tf 72 700 Td (xAAxAAx) Tj ET")) { - PDPage page = doc.getPage(0); - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("AA"), false, false); - String redacted = tokensText(tokens); - // The segment was rewritten away from the original literal. - assertThat(redacted).isNotEqualTo("xAAxAAx"); - // Redaction replaces matched runs with whitespace, so at least one "AA" is gone - // (the leading occurrence) and the surrounding x characters survive. - assertThat(redacted.split("AA", -1).length - 1).isLessThan(2); - assertThat(redacted).startsWith("x "); - assertThat(redacted).contains("x"); - } - } - - @Test - @DisplayName("a second Tf operator updates the active font for later segments") - void secondTfUpdatesFont() throws IOException { - PDDocument doc = new PDDocument(); - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - PDResources resources = new PDResources(); - resources.put(COSName.getPDFName("F1"), helvetica()); - resources.put( - COSName.getPDFName("F2"), - new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN)); - page.setResources(resources); - String raw = "BT /F1 12 Tf 72 700 Td (first) Tj /F2 18 Tf 0 -20 Td (SECRET) Tj ET"; - PDStream stream = new PDStream(doc); - try (var out = stream.createOutputStream()) { - out.write(raw.getBytes(StandardCharsets.ISO_8859_1)); - } - page.setContents(stream); - try (doc) { - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - assertThat(tokensText(tokens)).doesNotContain("SECRET"); - assertThat(tokensText(tokens)).contains("first"); - } - } - } - - // ── nested Form XObject traversal ──────────────────────────────────────────────────────────── - - @Nested - @DisplayName("nested Form XObject traversal") - class NestedXObjects { - - @Test - @DisplayName("a match in a form nested two levels deep is reached and rewritten") - void nestedTwoLevelsDeep() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - - // Inner form shows SECRET. - PDFormXObject inner = new PDFormXObject(doc); - inner.setResources(new PDResources()); - inner.getResources().put(COSName.getPDFName("F1"), helvetica()); - inner.setBBox(new PDRectangle(0, 0, 200, 50)); - try (var out = inner.getStream().createOutputStream()) { - out.write( - "BT /F1 12 Tf 0 10 Td (SECRET) Tj ET" - .getBytes(StandardCharsets.ISO_8859_1)); - } - - // Outer form references inner via Do. - PDFormXObject outer = new PDFormXObject(doc); - PDResources outerRes = new PDResources(); - COSName innerName = outerRes.add(inner); - outer.setResources(outerRes); - outer.setBBox(new PDRectangle(0, 0, 200, 50)); - try (var out = outer.getStream().createOutputStream()) { - out.write( - ("/" + innerName.getName() + " Do") - .getBytes(StandardCharsets.ISO_8859_1)); - } - - PDResources pageRes = new PDResources(); - COSName outerName = pageRes.add(outer); - page.setResources(pageRes); - PDStream pageStream = new PDStream(doc); - try (var out = pageStream.createOutputStream()) { - out.write( - ("/" + outerName.getName() + " Do") - .getBytes(StandardCharsets.ISO_8859_1)); - } - page.setContents(pageStream); - - service.createTokensWithoutTargetText(doc, page, Set.of("SECRET"), false, false); - - // The deep traversal must have rewritten the inner form's content stream. - assertThat(inner.getCOSObject().containsKey(COSName.CONTENTS)).isTrue(); - } - } - - @Test - @DisplayName("a form XObject with no resources is skipped without error") - void formWithoutResourcesSkipped() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - - PDFormXObject form = new PDFormXObject(doc); - form.setBBox(new PDRectangle(0, 0, 100, 50)); - // Intentionally no resources on the form. - try (var out = form.getStream().createOutputStream()) { - out.write("q Q".getBytes(StandardCharsets.ISO_8859_1)); - } - - PDResources pageRes = new PDResources(); - COSName formName = pageRes.add(form); - page.setResources(pageRes); - PDStream pageStream = new PDStream(doc); - try (var out = pageStream.createOutputStream()) { - out.write( - ("/" + formName.getName() + " Do") - .getBytes(StandardCharsets.ISO_8859_1)); - } - page.setContents(pageStream); - - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - assertThat(tokens).isNotNull(); - } - } - } - - // ── kerning / adjustment path in modifyTokenForRedaction ───────────────────────────────────── - - @Nested - @DisplayName("modifyTokenForRedaction adjustment branches") - class ModifyTokenAdjustment { - - @Test - @DisplayName("a non-zero width adjustment rewrites a Tj into a TJ array with kerning") - void adjustmentRewritesToTjArray() throws Exception { - List tokens = new ArrayList<>(); - tokens.add(new COSString("KEEP")); - tokens.add(Operator.getOperator("Tj")); - - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "Tj", "KEEP", 0, 4, helvetica(), FONT_SIZE); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "modifyTokenForRedaction", - List.class, - TextRedactionService.TextSegment.class, - String.class, - float.class, - List.class); - m.setAccessible(true); - // A clearly non-zero adjustment forces the COSArray + kerning branch. - m.invoke(service, tokens, segment, "AB", 5.0f, List.of()); - - assertThat(tokens.get(0)).isInstanceOf(COSArray.class); - COSArray arr = (COSArray) tokens.get(0); - boolean hasKern = false; - for (COSBase el : arr) { - if (el instanceof COSFloat) { - hasKern = true; - } - } - assertThat(hasKern).as("kerning float should be appended to the TJ array").isTrue(); - // The trailing Tj operator should have been switched to TJ. - assertThat(tokens.get(1)).isInstanceOf(Operator.class); - assertThat(((Operator) tokens.get(1)).getName()).isEqualTo("TJ"); - } - - @Test - @DisplayName("empty replacement text with ~zero adjustment sets the shared empty COSString") - void emptyReplacementZeroAdjustment() throws Exception { - List tokens = new ArrayList<>(); - tokens.add(new COSString("SECRET")); - tokens.add(Operator.getOperator("Tj")); - - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "Tj", "SECRET", 0, 6, helvetica(), FONT_SIZE); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "modifyTokenForRedaction", - List.class, - TextRedactionService.TextSegment.class, - String.class, - float.class, - List.class); - m.setAccessible(true); - m.invoke(service, tokens, segment, "", 0f, List.of()); - - assertThat(tokens.get(0)).isInstanceOf(COSString.class); - assertThat(((COSString) tokens.get(0)).getString()).isEmpty(); - } - - @Test - @DisplayName("the ' operator with a non-zero adjustment is also rewritten to a TJ array") - void apostropheAdjustmentRewrites() throws Exception { - List tokens = new ArrayList<>(); - tokens.add(new COSString("WORD")); - tokens.add(Operator.getOperator("'")); - - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "'", "WORD", 0, 4, helvetica(), FONT_SIZE); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "modifyTokenForRedaction", - List.class, - TextRedactionService.TextSegment.class, - String.class, - float.class, - List.class); - m.setAccessible(true); - m.invoke(service, tokens, segment, "X", 4.0f, List.of()); - - assertThat(tokens.get(0)).isInstanceOf(COSArray.class); - assertThat(((Operator) tokens.get(1)).getName()).isEqualTo("TJ"); - } - } - - // ── createRedactedTJArray edge branches ────────────────────────────────────────────────────── - - @Nested - @DisplayName("createRedactedTJArray edge branches") - class RedactedTjArray { - - @Test - @DisplayName("non-COSString elements (kerning numbers) are preserved in order") - void preservesNumberElements() throws Exception { - COSArray original = new COSArray(); - original.add(new COSString("AA")); - original.add(new COSFloat(-25f)); - original.add(new COSString("BB")); - - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "TJ", "AABB", 0, 4, helvetica(), FONT_SIZE); - List matches = - List.of(new TextRedactionService.MatchRange(0, 2)); // "AA" - - Method m = - TextRedactionService.class.getDeclaredMethod( - "createRedactedTJArray", - COSArray.class, - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - COSArray result = (COSArray) m.invoke(service, original, segment, matches); - - boolean sawFloat = false; - for (COSBase el : result) { - if (el instanceof COSFloat) { - sawFloat = true; - } - } - assertThat(sawFloat).as("original kerning number must be retained").isTrue(); - } - - @Test - @DisplayName("a TJ array with no overlapping match is returned essentially unchanged") - void noMatchLeavesTextIntact() throws Exception { - COSArray original = new COSArray(); - original.add(new COSString("hello")); - original.add(new COSString("world")); - - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "TJ", "helloworld", 0, 10, helvetica(), FONT_SIZE); - List matches = - List.of(new TextRedactionService.MatchRange(50, 60)); // out of range - - Method m = - TextRedactionService.class.getDeclaredMethod( - "createRedactedTJArray", - COSArray.class, - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - COSArray result = (COSArray) m.invoke(service, original, segment, matches); - - StringBuilder sb = new StringBuilder(); - for (COSBase el : result) { - if (el instanceof COSString cs) sb.append(cs.getString()); - } - assertThat(sb.toString()).isEqualTo("helloworld"); - } - } - - // ── private width helpers via reflection ───────────────────────────────────────────────────── - - @Nested - @DisplayName("private width helpers via reflection") - class WidthHelpers { - - private float invokeFloat(String name, Object... args) throws Exception { - Class[] types = new Class[] {PDFont.class, String.class}; - Method m = TextRedactionService.class.getDeclaredMethod(name, types); - m.setAccessible(true); - return (float) m.invoke(service, args); - } - - @Test - @DisplayName("calculateConservativeWidth scales linearly at 500 units per character") - void conservativeWidthLinear() throws Exception { - float w = invokeFloat("calculateConservativeWidth", helvetica(), "abcd"); - assertThat(w).isEqualTo(4 * 500f); - } - - @Test - @DisplayName("calculateCharacterBasedWidth returns a positive width for normal text") - void characterBasedWidthPositive() throws Exception { - float w = invokeFloat("calculateCharacterBasedWidth", helvetica(), "Hello"); - assertThat(w).isGreaterThan(0f); - } - - @Test - @DisplayName("calculateFallbackWidth returns a positive width using font metrics") - void fallbackWidthPositive() throws Exception { - float w = invokeFloat("calculateFallbackWidth", helvetica(), "Hello"); - assertThat(w).isGreaterThan(0f); - } - - @Test - @DisplayName("safeGetStringWidth returns 0 for null/empty inputs") - void safeWidthZeroForEmpty() throws Exception { - assertThat(invokeFloat("safeGetStringWidth", helvetica(), "")).isZero(); - Method m = - TextRedactionService.class.getDeclaredMethod( - "safeGetStringWidth", PDFont.class, String.class); - m.setAccessible(true); - assertThat((float) m.invoke(service, helvetica(), null)).isZero(); - assertThat((float) m.invoke(service, (PDFont) null, "x")).isZero(); - } - - @Test - @DisplayName("safeGetStringWidth returns a positive width for a reliable font") - void safeWidthPositive() throws Exception { - float w = invokeFloat("safeGetStringWidth", helvetica(), "Word"); - assertThat(w).isGreaterThan(0f); - } - } - - // ── createAlternativePlaceholder via reflection ────────────────────────────────────────────── - - @Nested - @DisplayName("createAlternativePlaceholder via reflection") - class AlternativePlaceholder { - - @Test - @DisplayName("Helvetica supports space, so output is a bounded run of spaces") - void boundedSpaces() throws Exception { - Method m = - TextRedactionService.class.getDeclaredMethod( - "createAlternativePlaceholder", - String.class, - float.class, - PDFont.class, - float.class); - m.setAccessible(true); - String result = (String) m.invoke(service, "hidden", 20f, helvetica(), FONT_SIZE); - assertThat(result.chars().allMatch(c -> c == ' ')).isTrue(); - assertThat(result.length()).isLessThanOrEqualTo("hidden".length() * 2); - } - } - - // ── extractTextSegments via reflection ─────────────────────────────────────────────────────── - - @Nested - @DisplayName("extractTextSegments via reflection") - class ExtractSegments { - - @SuppressWarnings("unchecked") - @Test - @DisplayName("a Tf operator sets font and size on the segments that follow it") - void tfSetsFontAndSize() throws Exception { - try (PDDocument doc = docWithRawContent("BT /F1 14 Tf 72 700 Td (hello) Tj ET")) { - PDPage page = doc.getPage(0); - List tokens = parseTokens(page); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "extractTextSegments", PDPage.class, List.class); - m.setAccessible(true); - List segments = - (List) m.invoke(service, page, tokens); - - assertThat(segments).isNotEmpty(); - TextRedactionService.TextSegment first = segments.get(0); - assertThat(first.getText()).isEqualTo("hello"); - assertThat(first.getFontSize()).isEqualTo(14f); - assertThat(first.getFont()).isNotNull(); - } - } - } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceMoreTest.java index ba0377a9e8..e0408bae55 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceMoreTest.java @@ -3,40 +3,24 @@ package stirling.software.SPDF.controller.api.security; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; +import java.io.InputStream; import java.util.List; import java.util.Map; -import java.util.Set; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSFloat; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import stirling.software.SPDF.model.PDFText; -/** - * Gap-coverage tests for {@link TextRedactionService} targeting branches the existing {@code - * TextRedactionServiceTest} does not reach: TJ-array redaction with kerning adjustment, the {@code - * '} and {@code "} text-showing operators, Form XObject content rewriting, multi-page / multi-match - * find+replace, and the private TJ/segment helpers exercised directly via reflection. - */ @DisplayName("TextRedactionService additional coverage") class TextRedactionServiceMoreTest { @@ -50,201 +34,16 @@ class TextRedactionServiceMoreTest { return new PDType1Font(Standard14Fonts.FontName.HELVETICA); } - private List parseTokens(PDPage page) throws IOException { - PDFStreamParser parser = new PDFStreamParser(page); - List tokens = new ArrayList<>(); - Object t; - while ((t = parser.parseNextToken()) != null) { - tokens.add(t); - } - return tokens; - } - - private String tokensText(List tokens) { - StringBuilder sb = new StringBuilder(); - for (Object token : tokens) { - if (token instanceof COSString cs) { - sb.append(cs.getString()); - } else if (token instanceof COSArray arr) { - for (COSBase el : arr) { - if (el instanceof COSString cs) { - sb.append(cs.getString()); - } - } + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); } } - return sb.toString(); + return helvetica(); } - /** - * Builds a single page whose content stream is exactly {@code rawContent}, font F1=Helvetica. - */ - private PDDocument docWithRawContent(String rawContent) throws IOException { - PDDocument doc = new PDDocument(); - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - PDResources resources = new PDResources(); - resources.put(COSName.getPDFName("F1"), helvetica()); - page.setResources(resources); - - PDStream stream = new PDStream(doc); - try (var out = stream.createOutputStream()) { - out.write(rawContent.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)); - } - page.setContents(stream); - return doc; - } - - // ── ' and " operators ──────────────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("apostrophe and quote text-showing operators") - class MoveAndShowOperators { - - @Test - @DisplayName("the ' (move-to-next-line-and-show) operator gets its text redacted") - void apostropheOperatorRedacted() throws IOException { - // ' shows a string on the next line. Content: BT /F1 12 Tf 72 700 Td (PUBLIC) Tj - // (SECRET) ' ET - String raw = "BT /F1 12 Tf 72 700 Td (PUBLIC) Tj (SECRET) ' ET"; - try (PDDocument doc = docWithRawContent(raw)) { - PDPage page = doc.getPage(0); - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - String text = tokensText(tokens); - assertThat(text).doesNotContain("SECRET"); - assertThat(text).contains("PUBLIC"); - } - } - - @Test - @DisplayName("the \" operator is collected as text-showing but its text is not extracted") - void quoteOperatorNotExtracted() throws IOException { - // " is in TEXT_SHOWING_OPERATORS, but extractTextFromToken's switch only handles - // Tj/'/TJ, so a "-shown string yields no segment and survives. This pins that - // behavior: the parse path runs without error and the token list is intact. - String raw = "BT /F1 12 Tf 72 700 Td 1 2 (SECRET) \" ET"; - try (PDDocument doc = docWithRawContent(raw)) { - PDPage page = doc.getPage(0); - List before = parseTokens(page); - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - assertThat(tokens).hasSameSizeAs(before); - assertThat(tokensText(tokens)).contains("SECRET"); - } - } - } - - // ── TJ arrays with kerning ─────────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("TJ positioning arrays") - class TjArrays { - - @Test - @DisplayName("partial match inside a TJ array redacts only the matched run") - void tjArrayPartialRedaction() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(helvetica(), FONT_SIZE); - cs.beginText(); - cs.newLineAtOffset(LEFT_X, TOP_Y); - // showTextWithPositioning emits a single TJ array. - cs.showTextWithPositioning( - new Object[] {"keep ", -50f, "SECRET", 20f, " tail"}); - cs.endText(); - } - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - - boolean sawTj = tokens.stream().anyMatch(t -> t instanceof COSArray); - assertThat(sawTj).as("expected a TJ array token").isTrue(); - assertThat(tokensText(tokens)).doesNotContain("SECRET"); - assertThat(tokensText(tokens)).contains("keep"); - } - } - - @Test - @DisplayName("TJ array with no matching term is left unchanged") - void tjArrayNoMatch() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(helvetica(), FONT_SIZE); - cs.beginText(); - cs.newLineAtOffset(LEFT_X, TOP_Y); - cs.showTextWithPositioning(new Object[] {"alpha ", -30f, "beta"}); - cs.endText(); - } - List before = parseTokens(page); - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("ZZZ"), false, false); - assertThat(tokens).hasSameSizeAs(before); - assertThat(tokensText(tokens)).contains("alpha"); - } - } - } - - // ── Form XObject traversal ─────────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("Form XObject content") - class FormXObjects { - - @Test - @DisplayName("a referenced Form XObject containing a match is traversed and rewritten") - void traversesFormXObject() throws IOException { - try (PDDocument doc = new PDDocument()) { - PDPage page = new PDPage(PDRectangle.LETTER); - doc.addPage(page); - - // Build a form XObject whose own content stream shows "SECRET". - PDFormXObject form = new PDFormXObject(doc); - form.setResources(new PDResources()); - form.getResources().put(COSName.getPDFName("F1"), helvetica()); - form.setBBox(new PDRectangle(0, 0, 200, 50)); - String formContent = "BT /F1 12 Tf 0 10 Td (SECRET) Tj ET"; - try (var out = form.getStream().createOutputStream()) { - out.write(formContent.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)); - } - - PDResources pageResources = new PDResources(); - COSName formName = pageResources.add(form); - page.setResources(pageResources); - - String pageContent = "q 1 0 0 1 100 600 cm /" + formName.getName() + " Do Q"; - PDStream pageStream = new PDStream(doc); - try (var out = pageStream.createOutputStream()) { - out.write(pageContent.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)); - } - page.setContents(pageStream); - - // Processing the page walks into the XObject graph; when a match is found inside - // the - // form, writeRedactedContentToXObject runs and sets a /Contents item on the form's - // COS dictionary. Asserting that item appears proves the XObject redaction path - // executed end-to-end without throwing. - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - - assertThat(tokens).isNotNull(); - assertThat(form.getCOSObject().containsKey(COSName.CONTENTS)) - .as("form XObject redaction path should have written a new content item") - .isTrue(); - } - } - } - - // ── multi-page / multi-match public entry points ───────────────────────────────────────────── - @Nested @DisplayName("findTextToRedact and performTextReplacement across pages") class MultiPage { @@ -256,7 +55,7 @@ class TextRedactionServiceMoreTest { PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(helvetica(), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y); cs.showText(line); @@ -326,225 +125,32 @@ class TextRedactionServiceMoreTest { } } - // ── private TJ / segment helpers via reflection ────────────────────────────────────────────── - @Nested - @DisplayName("private helpers via reflection") - class PrivateHelpers { - - @Test - @DisplayName("createRedactedTJArray replaces the matched substring inside the array") - void createRedactedTjArray() throws Exception { - COSArray original = new COSArray(); - original.add(new COSString("SECRET")); - original.add(new COSFloat(-40f)); - original.add(new COSString(" tail")); - - // Segment text is the concatenation "SECRET tail"; startPos 0. - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "TJ", "SECRET tail", 0, 11, helvetica(), FONT_SIZE); - List matches = - List.of(new TextRedactionService.MatchRange(0, 6)); // "SECRET" - - Method m = - TextRedactionService.class.getDeclaredMethod( - "createRedactedTJArray", - COSArray.class, - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - COSArray result = (COSArray) m.invoke(service, original, segment, matches); - - StringBuilder sb = new StringBuilder(); - for (COSBase el : result) { - if (el instanceof COSString cs) sb.append(cs.getString()); - } - assertThat(sb.toString()).doesNotContain("SECRET"); - assertThat(sb.toString()).contains("tail"); - } - - @Test - @DisplayName("applyRedactionsToSegmentText swaps the matched span for a placeholder") - void applyRedactionsToSegmentText() throws Exception { - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "Tj", "keepSECRETkeep", 0, 14, helvetica(), FONT_SIZE); - List matches = - List.of(new TextRedactionService.MatchRange(4, 10)); // SECRET - - Method m = - TextRedactionService.class.getDeclaredMethod( - "applyRedactionsToSegmentText", - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - String out = (String) m.invoke(service, segment, matches); - assertThat(out).doesNotContain("SECRET"); - assertThat(out).startsWith("keep"); - assertThat(out).endsWith("keep"); - } - - @Test - @DisplayName("calculateWidthAdjustment returns 0 for a null-font segment") - void widthAdjustmentNullFont() throws Exception { - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment(0, "Tj", "abc", 0, 3, null, FONT_SIZE); - Method m = - TextRedactionService.class.getDeclaredMethod( - "calculateWidthAdjustment", - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - float adj = - (float) - m.invoke( - service, - segment, - List.of(new TextRedactionService.MatchRange(0, 3))); - assertThat(adj).isZero(); - } - - @Test - @DisplayName("calculateWidthAdjustment skips subset fonts (returns 0)") - void widthAdjustmentSubsetFontSkipped() throws Exception { - // A subset font name (6 uppercase letters + '+') trips the subset short-circuit. - PDFont subsetNamed = new PDType1Font(Standard14Fonts.FontName.HELVETICA); - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 0, "Tj", "ABCDEF", 0, 6, subsetNamed, FONT_SIZE); - - // The real Helvetica name is not a subset, so this segment goes through the normal - // calculation; assert the call is at least exception-free and finite. - Method m = - TextRedactionService.class.getDeclaredMethod( - "calculateWidthAdjustment", - TextRedactionService.TextSegment.class, - List.class); - m.setAccessible(true); - float adj = - (float) - m.invoke( - service, - segment, - List.of(new TextRedactionService.MatchRange(0, 6))); - assertThat(Float.isFinite(adj)).isTrue(); - } - - @Test - @DisplayName("modifyTokenForRedaction with an out-of-range token index is a no-op") - void modifyTokenOutOfRange() throws Exception { - List tokens = new ArrayList<>(); - tokens.add(new COSString("hello")); - TextRedactionService.TextSegment segment = - new TextRedactionService.TextSegment( - 99, "Tj", "hello", 0, 5, helvetica(), FONT_SIZE); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "modifyTokenForRedaction", - List.class, - TextRedactionService.TextSegment.class, - String.class, - float.class, - List.class); - m.setAccessible(true); - m.invoke(service, tokens, segment, "", 0f, List.of()); - - // Token list is untouched because index 99 is out of bounds. - assertThat(tokens).hasSize(1); - assertThat(((COSString) tokens.get(0)).getString()).isEqualTo("hello"); - } - - @Test - @DisplayName("buildCompleteText concatenates the text of all segments in order") - void buildCompleteText() throws Exception { - List segments = - List.of( - new TextRedactionService.TextSegment( - 0, "Tj", "foo", 0, 3, helvetica(), FONT_SIZE), - new TextRedactionService.TextSegment( - 1, "Tj", "bar", 3, 6, helvetica(), FONT_SIZE)); - Method m = - TextRedactionService.class.getDeclaredMethod("buildCompleteText", List.class); - m.setAccessible(true); - assertThat(m.invoke(service, segments)).isEqualTo("foobar"); - } - - @Test - @DisplayName("extractTextFromToken returns text for the \" operator") - void extractTextFromQuoteOperator() throws Exception { - Method m = - TextRedactionService.class.getDeclaredMethod( - "extractTextFromToken", Object.class, String.class); - m.setAccessible(true); - // The " operator is not in the switch (Tj/'/TJ) -> default branch yields empty string. - assertThat(m.invoke(service, new COSString("x"), "\"")).isEqualTo(""); - } - } - - // ── createPlaceholderWithWidth additional branches ─────────────────────────────────────────── - - @Nested - @DisplayName("createPlaceholderWithWidth reliable-font path") - class PlaceholderWidthBranches { - - @Test - @DisplayName("reliable font with positive width yields a bounded run of spaces") - void reliableFontBoundedSpaces() { - PDFont font = helvetica(); - String original = "Secret"; - float targetWidth; - try { - targetWidth = font.getStringWidth(original) / 1000f * FONT_SIZE; - } catch (IOException e) { - targetWidth = 30f; - } - String placeholder = - service.createPlaceholderWithWidth(original, targetWidth, font, FONT_SIZE); - assertThat(placeholder).isNotEmpty(); - assertThat(placeholder.chars().allMatch(c -> c == ' ')).isTrue(); - // spaceCount is capped at originalLength*2. - assertThat(placeholder.length()).isLessThanOrEqualTo(original.length() * 2); - } - - @Test - @DisplayName("zero target width falls back to alternative placeholder logic") - void zeroTargetWidth() { - PDFont font = helvetica(); - String placeholder = service.createPlaceholderWithWidth("word", 0f, font, FONT_SIZE); - // With a reliable, non-subset font and zero width, output is still all whitespace. - assertThat(placeholder.chars().allMatch(c -> c == ' ')).isTrue(); - } - } - - // ── inner data classes ─────────────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("ModificationTask / GraphicsState data classes") + @DisplayName("TextSegment / MatchRange data classes") class DataClasses { @Test - @DisplayName("GraphicsState defaults are null font and zero size, mutators round-trip") - void graphicsStateRoundTrip() throws Exception { - Class gsClass = - Class.forName( - "stirling.software.SPDF.controller.api.security.TextRedactionService$GraphicsState"); - var ctor = gsClass.getDeclaredConstructor(); - ctor.setAccessible(true); - Object gs = ctor.newInstance(); + @DisplayName("TextSegment exposes its constructor values via accessors") + void textSegmentAccessors() { + PDFont font = helvetica(); + TextRedactionService.TextSegment segment = + new TextRedactionService.TextSegment(3, "Tj", "hello", 10, 15, font, 12f); - Method getFont = gsClass.getDeclaredMethod("getFont"); - Method getSize = gsClass.getDeclaredMethod("getFontSize"); - getFont.setAccessible(true); - getSize.setAccessible(true); - assertThat(getFont.invoke(gs)).isNull(); - assertThat((float) getSize.invoke(gs)).isZero(); + assertThat(segment.getTokenIndex()).isEqualTo(3); + assertThat(segment.getOperatorName()).isEqualTo("Tj"); + assertThat(segment.getText()).isEqualTo("hello"); + assertThat(segment.getStartPos()).isEqualTo(10); + assertThat(segment.getEndPos()).isEqualTo(15); + assertThat(segment.getFont()).isSameAs(font); + assertThat(segment.getFontSize()).isEqualTo(12f); + } - Method setSize = gsClass.getDeclaredMethod("setFontSize", float.class); - setSize.setAccessible(true); - setSize.invoke(gs, 14f); - assertThat((float) getSize.invoke(gs)).isEqualTo(14f); + @Test + @DisplayName("MatchRange exposes start and end positions") + void matchRangeAccessors() { + TextRedactionService.MatchRange range = new TextRedactionService.MatchRange(4, 9); + assertThat(range.getStartPos()).isEqualTo(4); + assertThat(range.getEndPos()).isEqualTo(9); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceTest.java index d487848e09..a47f2c15ab 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/TextRedactionServiceTest.java @@ -2,30 +2,21 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; +import java.io.InputStream; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import org.apache.pdfbox.contentstream.operator.Operator; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdfparser.PDFStreamParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.junit.jupiter.api.DisplayName; @@ -54,13 +45,23 @@ class TextRedactionServiceTest { return new PDType1Font(Standard14Fonts.FontName.HELVETICA); } + private PDFont helvetica(PDDocument doc) throws IOException { + try (InputStream is = + getClass().getResourceAsStream("/type3/library/fonts/dejavu/DejaVuSans.ttf")) { + if (is != null) { + return PDType0Font.load(doc, is); + } + } + return helvetica(); + } + /** Single page, single Tj line per supplied text line, Helvetica 12. */ private PDDocument buildDoc(String... lines) throws IOException { PDDocument doc = new PDDocument(); PDPage page = new PDPage(PDRectangle.LETTER); doc.addPage(page); try (PDPageContentStream cs = new PDPageContentStream(doc, page)) { - cs.setFont(helvetica(), FONT_SIZE); + cs.setFont(helvetica(doc), FONT_SIZE); for (int i = 0; i < lines.length; i++) { cs.beginText(); cs.newLineAtOffset(LEFT_X, TOP_Y - i * 16f); @@ -77,32 +78,6 @@ class TextRedactionServiceTest { return doc; } - // ── isTextShowingOperator ──────────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("isTextShowingOperator") - class IsTextShowingOperator { - - @Test - @DisplayName("recognises the four text-showing operators") - void recognisesTextShowingOperators() { - assertTrue(service.isTextShowingOperator("Tj")); - assertTrue(service.isTextShowingOperator("TJ")); - assertTrue(service.isTextShowingOperator("'")); - assertTrue(service.isTextShowingOperator("\"")); - } - - @Test - @DisplayName("rejects non text-showing operators and junk") - void rejectsOthers() { - assertFalse(service.isTextShowingOperator("BT")); - assertFalse(service.isTextShowingOperator("ET")); - assertFalse(service.isTextShowingOperator("Tf")); - assertFalse(service.isTextShowingOperator("")); - assertFalse(service.isTextShowingOperator("tj")); - } - } - // ── findTextToRedact ───────────────────────────────────────────────────────────────────────── @Nested @@ -253,192 +228,6 @@ class TextRedactionServiceTest { } } - // ── detectCustomEncodingFonts ──────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("detectCustomEncodingFonts") - class DetectCustomEncodingFonts { - - @Test - @DisplayName("standard Helvetica document is not flagged as custom-encoded") - void standardFontNotFlagged() throws IOException { - try (PDDocument doc = buildDoc("plain helvetica text")) { - assertFalse(service.detectCustomEncodingFonts(doc)); - } - } - - @Test - @DisplayName("document with no content / no fonts is not flagged") - void emptyDocumentNotFlagged() throws IOException { - try (PDDocument doc = buildEmptyDoc()) { - assertFalse(service.detectCustomEncodingFonts(doc)); - } - } - } - - // ── createPlaceholderWithFont ──────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("createPlaceholderWithFont") - class CreatePlaceholderWithFont { - - @Test - @DisplayName("returns the input unchanged for null") - void nullReturnsNull() { - assertNull(service.createPlaceholderWithFont(null, helvetica())); - } - - @Test - @DisplayName("returns the input unchanged for empty string") - void emptyReturnsEmpty() { - assertEquals("", service.createPlaceholderWithFont("", helvetica())); - } - - @Test - @DisplayName("non-subset font yields spaces matching the original length") - void nonSubsetFontYieldsMatchingSpaces() { - String placeholder = service.createPlaceholderWithFont("hidden", helvetica()); - assertEquals(" ".repeat("hidden".length()), placeholder); - } - - @Test - @DisplayName("null font is treated as non-subset and yields spaces") - void nullFontYieldsSpaces() { - String placeholder = service.createPlaceholderWithFont("abc", null); - assertEquals(" ", placeholder); - } - } - - // ── createPlaceholderWithWidth ─────────────────────────────────────────────────────────────── - - @Nested - @DisplayName("createPlaceholderWithWidth") - class CreatePlaceholderWithWidth { - - @Test - @DisplayName("returns the input unchanged for null") - void nullReturnsNull() { - assertNull(service.createPlaceholderWithWidth(null, 10f, helvetica(), FONT_SIZE)); - } - - @Test - @DisplayName("returns the input unchanged for empty string") - void emptyReturnsEmpty() { - assertEquals("", service.createPlaceholderWithWidth("", 10f, helvetica(), FONT_SIZE)); - } - - @Test - @DisplayName("null font falls back to one space per original character") - void nullFontFallsBackToSpaces() { - String placeholder = service.createPlaceholderWithWidth("word", 50f, null, FONT_SIZE); - assertEquals(" ".repeat("word".length()), placeholder); - } - - @Test - @DisplayName("non-positive font size falls back to one space per original character") - void nonPositiveFontSizeFallsBackToSpaces() { - String placeholder = service.createPlaceholderWithWidth("word", 50f, helvetica(), 0f); - assertEquals(" ".repeat("word".length()), placeholder); - } - - @Test - @DisplayName("standard font produces a non-null all-whitespace placeholder") - void standardFontProducesWhitespacePlaceholder() { - PDFont font = helvetica(); - float fontSize = FONT_SIZE; - String original = "Secret"; - // Compute a realistic target width the way the service does (text-space / 1000 * size). - float targetWidth; - try { - targetWidth = font.getStringWidth(original) / 1000f * fontSize; - } catch (IOException e) { - targetWidth = 30f; - } - - String placeholder = - service.createPlaceholderWithWidth(original, targetWidth, font, fontSize); - - assertNotNull(placeholder); - assertFalse(placeholder.isEmpty(), "Helvetica supports spaces, so non-empty expected"); - assertTrue( - placeholder.chars().allMatch(c -> c == ' '), - "placeholder should be composed only of spaces"); - } - } - - // ── createTokensWithoutTargetText / writeFilteredContentStream - // ──────────────────────────────── - - @Nested - @DisplayName("createTokensWithoutTargetText") - class CreateTokensWithoutTargetText { - - @Test - @DisplayName( - "returns a non-empty token list and preserves token count when nothing matches") - void noMatchPreservesTokens() throws IOException { - try (PDDocument doc = buildDoc("nothing to hide")) { - PDPage page = doc.getPage(0); - List originalTokens = parseTokens(page); - - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("ABSENT"), false, false); - - assertNotNull(tokens); - assertEquals( - originalTokens.size(), - tokens.size(), - "token count should be unchanged when nothing matched"); - } - } - - @Test - @DisplayName("filtered tokens can be written back and the page re-parses cleanly") - void filteredTokensRoundTrip() throws IOException { - try (PDDocument doc = buildDoc("redact SECRET token roundtrip")) { - PDPage page = doc.getPage(0); - - List tokens = - service.createTokensWithoutTargetText( - doc, page, Set.of("SECRET"), false, false); - assertNotNull(tokens); - - service.writeFilteredContentStream(doc, page, tokens); - - // The page must still hold valid content (at least one operator token). - List reparsed = parseTokens(page); - boolean hasOperator = reparsed.stream().anyMatch(t -> t instanceof Operator); - assertTrue(hasOperator, "rewritten content stream must contain operators"); - } - } - - @Test - @DisplayName("empty target-word set leaves tokens untouched") - void emptyTargetSetLeavesTokens() throws IOException { - try (PDDocument doc = buildDoc("some content")) { - PDPage page = doc.getPage(0); - List originalTokens = parseTokens(page); - - List tokens = - service.createTokensWithoutTargetText( - doc, page, Collections.emptySet(), false, false); - - assertEquals(originalTokens.size(), tokens.size()); - } - } - - private List parseTokens(PDPage page) throws IOException { - PDFStreamParser parser = new PDFStreamParser(page); - List tokens = new ArrayList<>(); - Object token; - while ((token = parser.parseNextToken()) != null) { - tokens.add(token); - } - return tokens; - } - } - // ── inner data classes ─────────────────────────────────────────────────────────────────────── @Nested @@ -484,83 +273,4 @@ class TextRedactionServiceTest { assertFalse(a.equals(b)); } } - - // ── private logic exercised via reflection ─────────────────────────────────────────────────── - - @Nested - @DisplayName("findAllMatches / buildCompleteText (private logic via reflection)") - class PrivateLogic { - - @Test - @DisplayName("findAllMatches returns sorted, non-overlapping match ranges for two terms") - @SuppressWarnings("unchecked") - void findAllMatchesSorted() throws Exception { - String complete = "alpha beta gamma beta"; - Set terms = new LinkedHashSet<>(List.of("beta", "alpha")); - - Method m = - TextRedactionService.class.getDeclaredMethod( - "findAllMatches", - String.class, - Set.class, - boolean.class, - boolean.class); - m.setAccessible(true); - List matches = - (List) - m.invoke(service, complete, terms, false, false); - - assertNotNull(matches); - assertFalse(matches.isEmpty()); - // Results are sorted by start position. - for (int i = 1; i < matches.size(); i++) { - assertTrue( - matches.get(i - 1).getStartPos() <= matches.get(i).getStartPos(), - "matches must be sorted ascending by start position"); - } - // "alpha" at 0, "beta" at 6 and 17 -> three matches total. - assertEquals(3, matches.size()); - assertEquals(0, matches.get(0).getStartPos()); - } - - @Test - @DisplayName("findAllMatches returns nothing when no term occurs") - @SuppressWarnings("unchecked") - void findAllMatchesEmptyWhenAbsent() throws Exception { - Method m = - TextRedactionService.class.getDeclaredMethod( - "findAllMatches", - String.class, - Set.class, - boolean.class, - boolean.class); - m.setAccessible(true); - List matches = - (List) - m.invoke(service, "no terms here", Set.of("XYZ"), false, false); - assertTrue(matches.isEmpty()); - } - - @Test - @DisplayName("extractTextFromToken pulls text from Tj COSString and TJ COSArray") - void extractTextFromToken() throws Exception { - Method m = - TextRedactionService.class.getDeclaredMethod( - "extractTextFromToken", Object.class, String.class); - m.setAccessible(true); - - assertEquals("hi", m.invoke(service, new COSString("hi"), "Tj")); - assertEquals("hi", m.invoke(service, new COSString("hi"), "'")); - - COSArray tjArray = new COSArray(); - tjArray.add(new COSString("foo")); - tjArray.add(new COSString("bar")); - assertEquals("foobar", m.invoke(service, tjArray, "TJ")); - - // Unknown operator yields empty string. - assertEquals("", m.invoke(service, new COSString("x"), "Td")); - // Wrong token type for the operator yields empty string. - assertEquals("", m.invoke(service, new COSArray(), "Tj")); - } - } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/ValidateSignatureControllerMoreTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/ValidateSignatureControllerMoreTest.java index e6de0225dd..65afbefd97 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/ValidateSignatureControllerMoreTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/ValidateSignatureControllerMoreTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -291,6 +292,27 @@ class ValidateSignatureControllerMoreTest { } } + @Nested + @DisplayName("Padded signature contents") + class PaddedContentsTests { + + @Test + @DisplayName("Zero-padded /Contents still parses as CMS") + void zeroPaddedContentsParsesAsCms() throws Exception { + byte[] contents; + try (PDDocument doc = Loader.loadPDF(signedPdfBytes)) { + PDSignature sig = doc.getSignatureDictionaries().get(0); + contents = sig.getContents(new ByteArrayInputStream(signedPdfBytes)); + } + + // /Contents is zero-filled up to its reserved length. Since BC 1.85 the byte[] + // constructors reject that tail ("Extra data detected in stream"), streams do not. + assertThat(contents[contents.length - 1]).isZero(); + CMSSignedData parsed = new CMSSignedData(new ByteArrayInputStream(contents)); + assertThat(parsed.getSignerInfos().size()).isEqualTo(1); + } + } + @Nested @DisplayName("Error and edge handling") class ErrorHandlingTests { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java index 2df3fb5587..41909e3e0a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/web/ReactRoutingControllerTest.java @@ -4,12 +4,24 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.servlet.function.EntityResponse; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; +import org.springframework.web.util.ServletRequestPathUtils; import jakarta.servlet.http.HttpServletRequest; @@ -175,6 +187,83 @@ class ReactRoutingControllerTest { assertNotNull(response.getBody()); } + // --- deep-link SPA fallback (router function) --- + + @Test + void isSpaFallbackRoute_acceptsDeepSpaPaths() { + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/new")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/123/runs/456")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/workflow/sign/some-token")); + assertTrue(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/new/")); + // "pipelines" must not be swallowed by the "pipeline" exclusion + assertTrue(ReactRoutingController.isSpaFallbackRoute("/pipelines")); + } + + @Test + void isSpaFallbackRoute_rejectsBackendStaticAndFilePaths() { + assertFalse(ReactRoutingController.isSpaFallbackRoute("/api/v1/some/endpoint")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/pipeline")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/pipeline/anything")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/assets/deep/path")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/processor/pipelines/file.js")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/branding/sub/logo.png")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("/")); + assertFalse(ReactRoutingController.isSpaFallbackRoute("")); + assertFalse(ReactRoutingController.isSpaFallbackRoute(null)); + } + + @Test + void spaDeepLinkFallback_servesIndexForDeepRoute() throws Exception { + controller.init(); + RouterFunction router = routerOf(controller.spaDeepLinkFallbackMapping()); + + ServerRequest deepRequest = serverRequest("GET", "/processor/pipelines/new"); + Optional> handler = router.route(deepRequest); + assertTrue(handler.isPresent()); + + ServerResponse response = handler.get().handle(deepRequest); + assertEquals(HttpStatus.OK, response.statusCode()); + assertInstanceOf(EntityResponse.class, response); + Object body = ((EntityResponse) response).entity(); + assertTrue(body.toString().contains("Stirling PDF")); + } + + @Test + void spaDeepLinkFallback_ignoresApiFilesAndNonGet() { + controller.init(); + RouterFunction router = routerOf(controller.spaDeepLinkFallbackMapping()); + + assertTrue(router.route(serverRequest("GET", "/api/v1/policies/run")).isEmpty()); + assertTrue(router.route(serverRequest("GET", "/branding/sub/logo.png")).isEmpty()); + assertTrue(router.route(serverRequest("POST", "/processor/pipelines/new")).isEmpty()); + } + + @Test + void spaDeepLinkFallback_runsAfterControllersAndBeforeResources() { + controller.init(); + int order = controller.spaDeepLinkFallbackMapping().getOrder(); + + // A catch-all denylist is only safe below every annotated controller; Spring's own + // RouterFunctionMapping sits at -1, which would shadow /v1/api-docs, /error and friends. + assertTrue(order > 0, "SPA fallback must run after annotated controllers"); + assertTrue( + order < Ordered.LOWEST_PRECEDENCE - 1, + "SPA fallback must run before the static-resource chain"); + } + + private static RouterFunction routerOf(RouterFunctionMapping mapping) { + @SuppressWarnings("unchecked") + RouterFunction router = + (RouterFunction) mapping.getRouterFunction(); + return router; + } + + private static ServerRequest serverRequest(String method, String uri) { + MockHttpServletRequest servletRequest = new MockHttpServletRequest(method, uri); + ServletRequestPathUtils.parseAndCache(servletRequest); + return ServerRequest.create(servletRequest, List.of(new StringHttpMessageConverter())); + } + // --- context path handling --- @Test diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/web/UploadLimitServiceTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/web/UploadLimitServiceTest.java index 219affcd30..173741df87 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/web/UploadLimitServiceTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/web/UploadLimitServiceTest.java @@ -49,15 +49,7 @@ class UploadLimitServiceTest { systemProps = mock(ApplicationProperties.System.class); when(applicationProperties.getSystem()).thenReturn(systemProps); - uploadLimitService = new UploadLimitService(); - // inject mock - try { - var field = UploadLimitService.class.getDeclaredField("applicationProperties"); - field.setAccessible(true); - field.set(uploadLimitService, applicationProperties); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } + uploadLimitService = new UploadLimitService(applicationProperties); } @ParameterizedTest(name = "getReadableUploadLimit case #{index}: rawValue={0}, expected={1}") diff --git a/app/core/src/test/java/org/apache/pdfbox/examples/signature/TSAClientTest.java b/app/core/src/test/java/stirling/software/SPDF/pdf/signature/TSAClientTest.java similarity index 99% rename from app/core/src/test/java/org/apache/pdfbox/examples/signature/TSAClientTest.java rename to app/core/src/test/java/stirling/software/SPDF/pdf/signature/TSAClientTest.java index ebbbc518b7..582f2b332c 100644 --- a/app/core/src/test/java/org/apache/pdfbox/examples/signature/TSAClientTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/pdf/signature/TSAClientTest.java @@ -1,4 +1,4 @@ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/app/core/src/test/java/org/apache/pdfbox/examples/signature/ValidationTimeStampTest.java b/app/core/src/test/java/stirling/software/SPDF/pdf/signature/ValidationTimeStampTest.java similarity index 96% rename from app/core/src/test/java/org/apache/pdfbox/examples/signature/ValidationTimeStampTest.java rename to app/core/src/test/java/stirling/software/SPDF/pdf/signature/ValidationTimeStampTest.java index fdb9eb41e4..3981f1ec9b 100644 --- a/app/core/src/test/java/org/apache/pdfbox/examples/signature/ValidationTimeStampTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/pdf/signature/ValidationTimeStampTest.java @@ -1,4 +1,4 @@ -package org.apache.pdfbox.examples.signature; +package stirling.software.SPDF.pdf.signature; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/app/core/src/test/java/stirling/software/SPDF/service/ApiDocServiceTest.java b/app/core/src/test/java/stirling/software/SPDF/service/ApiDocServiceTest.java index 42b56a9cfa..1fce6a3836 100644 --- a/app/core/src/test/java/stirling/software/SPDF/service/ApiDocServiceTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/service/ApiDocServiceTest.java @@ -3,14 +3,11 @@ package stirling.software.SPDF.service; import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Field; -import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -23,6 +20,11 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; +/** + * Input/output type behaviour used to live here, parsed out of the description prose. It is now + * declared with {@code @ToolIO} and covered by {@code ToolIORegistryTest} and {@code + * ToolIODeclarationCoverageTest}. + */ @ExtendWith(MockitoExtension.class) class ApiDocServiceTest { @@ -52,199 +54,6 @@ class ApiDocServiceTest { field.set(apiDocService, mapper.createObjectNode()); } - @Test - void getExtensionTypesReturnsExpectedList() throws Exception { - String json = "{\"description\": \"Output:PDF\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/test", postNode); - - setApiDocumentation(Map.of("/test", endpoint)); - setApiDocsJsonRootNode(); - - List extensions = apiDocService.getExtensionTypes(true, "/test"); - assertEquals(List.of("pdf"), extensions); - } - - @Test - void getExtensionTypesHandlesUnknownOperation() throws Exception { - setApiDocumentation(Map.of()); - - List extensions = apiDocService.getExtensionTypes(true, "/unknown"); - assertNull(extensions); - } - - @Test - void getExtensionTypesReturnsImageTypes() throws Exception { - String json = "{\"description\": \"Output:IMAGE\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/img", postNode); - setApiDocumentation(Map.of("/img", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/img"); - assertNotNull(extensions); - assertTrue(extensions.contains("png")); - assertTrue(extensions.contains("jpg")); - assertTrue(extensions.contains("gif")); - } - - @Test - void getExtensionTypesReturnsZipTypes() throws Exception { - String json = "{\"description\": \"Output:ZIP\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/zip", postNode); - setApiDocumentation(Map.of("/zip", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/zip"); - assertNotNull(extensions); - assertTrue(extensions.contains("zip")); - assertTrue(extensions.contains("rar")); - } - - @Test - void getExtensionTypesReturnsWordTypes() throws Exception { - String json = "{\"description\": \"Output:WORD\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/word", postNode); - setApiDocumentation(Map.of("/word", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/word"); - assertNotNull(extensions); - assertTrue(extensions.contains("doc")); - assertTrue(extensions.contains("docx")); - } - - @Test - void getExtensionTypesReturnsCsvTypes() throws Exception { - String json = "{\"description\": \"Output:CSV\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/csv", postNode); - setApiDocumentation(Map.of("/csv", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/csv"); - assertEquals(List.of("csv"), extensions); - } - - @Test - void getExtensionTypesReturnsHtmlTypes() throws Exception { - String json = "{\"description\": \"Output:HTML\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/html", postNode); - setApiDocumentation(Map.of("/html", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/html"); - assertNotNull(extensions); - assertTrue(extensions.contains("html")); - assertTrue(extensions.contains("htm")); - } - - @Test - void getExtensionTypesReturnsBookTypes() throws Exception { - String json = "{\"description\": \"Output:BOOK\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/book", postNode); - setApiDocumentation(Map.of("/book", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/book"); - assertNotNull(extensions); - assertTrue(extensions.contains("epub")); - assertTrue(extensions.contains("mobi")); - } - - @Test - void getExtensionTypesReturnsJsonTypes() throws Exception { - String json = "{\"description\": \"Output:JSON\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/json", postNode); - setApiDocumentation(Map.of("/json", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/json"); - assertEquals(List.of("json"), extensions); - } - - @Test - void getExtensionTypesReturnsTxtTypes() throws Exception { - String json = "{\"description\": \"Output:TXT\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/txt", postNode); - setApiDocumentation(Map.of("/txt", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/txt"); - assertNotNull(extensions); - assertTrue(extensions.contains("txt")); - assertTrue(extensions.contains("md")); - } - - @Test - void getExtensionTypesReturnsPptTypes() throws Exception { - String json = "{\"description\": \"Output:PPT\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/ppt", postNode); - setApiDocumentation(Map.of("/ppt", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/ppt"); - assertNotNull(extensions); - assertTrue(extensions.contains("ppt")); - assertTrue(extensions.contains("pptx")); - } - - @Test - void getExtensionTypesReturnsXmlTypes() throws Exception { - String json = "{\"description\": \"Output:XML\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/xml", postNode); - setApiDocumentation(Map.of("/xml", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/xml"); - assertNotNull(extensions); - assertTrue(extensions.contains("xml")); - } - - @Test - void getExtensionTypesReturnsJsTypes() throws Exception { - String json = "{\"description\": \"Output:JS\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/js", postNode); - setApiDocumentation(Map.of("/js", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/js"); - assertNotNull(extensions); - assertTrue(extensions.contains("js")); - assertTrue(extensions.contains("jsx")); - } - - @Test - void getExtensionTypesWithInputMode() throws Exception { - String json = "{\"description\": \"Input:PDF\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/test-input", postNode); - setApiDocumentation(Map.of("/test-input", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(false, "/test-input"); - assertEquals(List.of("pdf"), extensions); - } - - @Test - void getExtensionTypesReturnsNullWhenNoTypeMatch() throws Exception { - String json = "{\"description\": \"No type here\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/notype", postNode); - setApiDocumentation(Map.of("/notype", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/notype"); - assertNull(extensions); - } - - @Test - void getExtensionTypesReturnsNullForUnknownOutputType() throws Exception { - String json = "{\"description\": \"Output:UNKNOWNTYPE\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/unk", postNode); - setApiDocumentation(Map.of("/unk", endpoint)); - setApiDocsJsonRootNode(); - List extensions = apiDocService.getExtensionTypes(true, "/unk"); - assertNull(extensions); - } - @Test void isValidOperationChecksRequiredParameters() throws Exception { String json = @@ -304,184 +113,9 @@ class ApiDocServiceTest { assertFalse(apiDocService.isValidOperation("/mixed", Map.of())); } - @Test - void isMultiInputDetectsTypeMI() throws Exception { - String json = "{\"description\": \"Type:MI\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/multi", postNode); - setApiDocumentation(Map.of("/multi", endpoint)); - setApiDocsJsonRootNode(); - assertTrue(apiDocService.isMultiInput("/multi")); - } - - @Test - void isMultiInputDetectsUnknownOperation() throws Exception { - setApiDocumentation(Map.of()); - assertFalse(apiDocService.isMultiInput("/unknown")); - } - - @Test - void isMultiInputHandlesNoDescription() throws Exception { - String json = "{\"parameters\": [{\"name\":\"param1\"}, {\"name\":\"param2\"}]}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/multi", postNode); - setApiDocumentation(Map.of("/multi", endpoint)); - setApiDocsJsonRootNode(); - assertFalse(apiDocService.isMultiInput("/multi")); - } - - @Test - void isMultiInputReturnsFalseForNonMIType() throws Exception { - String json = "{\"description\": \"Type:SI\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/single", postNode); - setApiDocumentation(Map.of("/single", endpoint)); - setApiDocsJsonRootNode(); - assertFalse(apiDocService.isMultiInput("/single")); - } - - @Test - void isMultiInputReturnsTrueForMISO() throws Exception { - String json = "{\"description\": \"Type:MISO\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/miso", postNode); - setApiDocumentation(Map.of("/miso", endpoint)); - setApiDocsJsonRootNode(); - assertTrue(apiDocService.isMultiInput("/miso")); - } - - @Test - void shouldUnpackZipResponseDetectsMultiOutputType() throws Exception { - String json = "{\"description\": \"Output:PDF Type:SIMO\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/split", postNode); - setApiDocumentation(Map.of("/split", endpoint)); - setApiDocsJsonRootNode(); - assertTrue(apiDocService.shouldUnpackZipResponse("/split")); - } - - @Test - void shouldUnpackZipResponseDetectsMimoType() throws Exception { - String json = "{\"description\": \"Output:PDF Type:MIMO\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/overlay", postNode); - setApiDocumentation(Map.of("/overlay", endpoint)); - setApiDocsJsonRootNode(); - assertTrue(apiDocService.shouldUnpackZipResponse("/overlay")); - } - - @Test - void shouldUnpackZipResponseDetectsZipOutputDeclaration() throws Exception { - String json = "{\"description\": \"Output:ZIP-PDF Type:SISO\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/split-by-sections", postNode); - setApiDocumentation(Map.of("/split-by-sections", endpoint)); - setApiDocsJsonRootNode(); - assertTrue(apiDocService.shouldUnpackZipResponse("/split-by-sections")); - } - - @Test - void shouldUnpackZipResponseReturnsFalseForSisoPdf() throws Exception { - String json = "{\"description\": \"Input:PDF Output:PDF Type:SISO\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/rotate", postNode); - setApiDocumentation(Map.of("/rotate", endpoint)); - setApiDocsJsonRootNode(); - assertFalse(apiDocService.shouldUnpackZipResponse("/rotate")); - } - - @Test - void shouldUnpackZipResponseReturnsFalseForUnknownOperation() throws Exception { - setApiDocumentation(Map.of()); - assertFalse(apiDocService.shouldUnpackZipResponse("/unknown")); - } - - /** - * Coverage test: every Stirling endpoint whose ZIP response is a transport for multiple typed - * results (SIMO/MIMO or Output:ZIP-PDF / Output:IMAGE/ZIP etc.) must be classified as {@code - * shouldUnpackZipResponse = true}. Descriptions below are the real - * {@code @Operation(description=...)} strings from each controller, so if a controller is - * renamed, tweaked or introduced without a {@code Type:} / {@code Output:ZIP-*} tag, this test - * breaks, surfacing the bug before {@code AiWorkflowService} silently registers a multi-result - * ZIP as a single file. - * - *

Add a new row here whenever a new unpack-eligible endpoint is introduced. Descriptions can - * be trimmed to the part containing the relevant tags. - */ - @ParameterizedTest(name = "{0} → shouldUnpackZipResponse") - @CsvSource( - textBlock = - """ - /api/v1/general/split-pages, 'Split pages. Input:PDF Output:PDF Type:SIMO' - /api/v1/general/split-pdf-by-sections, 'Split. Input:PDF Output:ZIP-PDF Type:SISO' - /api/v1/general/split-by-size-or-count, 'Split by size. Input:PDF Output:ZIP-PDF Type:SISO' - /api/v1/general/split-pdf-by-chapters, 'Split by chapters. Input:PDF Output:ZIP-PDF Type:SISO' - /api/v1/general/split-for-poster-print, 'Poster split. Input: PDF Output: ZIP-PDF Type: SISO' - /api/v1/general/overlay-pdfs, 'Overlay PDFs. Input:PDF Output:PDF Type:MIMO' - /api/v1/misc/auto-split-pdf, 'Auto split. Input:PDF Output:ZIP-PDF Type:SISO' - /api/v1/misc/extract-images, 'Extract images. Output:IMAGE/ZIP Type:SIMO' - /api/v1/misc/extract-image-scans, 'Extract image scans. Input:PDF Output:IMAGE/ZIP Type:SIMO' - """) - void shouldUnpackZipResponseClassifiesKnownUnpackableEndpoints( - String endpoint, String description) throws Exception { - String json = mapper.writeValueAsString(Map.of("description", description)); - JsonNode postNode = mapper.readTree(json); - setApiDocumentation(Map.of(endpoint, new ApiEndpoint(endpoint, postNode))); - setApiDocsJsonRootNode(); - assertTrue( - apiDocService.shouldUnpackZipResponse(endpoint), - () -> - "Expected shouldUnpackZipResponse=true for " - + endpoint - + " with description: " - + description); - } - - /** - * Inverse coverage: endpoints whose ZIP response is the deliverable itself (or that return - * single non-ZIP files) must not be flagged for unpacking. Catches regressions where a change - * to the classifier accidentally widens the positive match. - */ - @ParameterizedTest(name = "{0} → !shouldUnpackZipResponse") - @CsvSource( - textBlock = - """ - /api/v1/general/rotate-pdf, 'Rotate. Input:PDF Output:PDF Type:SISO' - /api/v1/general/merge-pdfs, 'Merge. Input:PDF Output:PDF Type:MISO' - /api/v1/misc/compress-pdf, 'Compress. Input:PDF Output:PDF Type:SISO' - /api/v1/misc/flatten, 'Flatten forms. Input:PDF Output:PDF Type:SISO' - /api/v1/security/get-attachments, 'Extract attachments. Input:PDF Output:ZIP Type:SISO' - """) - void shouldUnpackZipResponseRejectsNonUnpackableEndpoints(String endpoint, String description) - throws Exception { - String json = mapper.writeValueAsString(Map.of("description", description)); - JsonNode postNode = mapper.readTree(json); - setApiDocumentation(Map.of(endpoint, new ApiEndpoint(endpoint, postNode))); - setApiDocsJsonRootNode(); - assertFalse( - apiDocService.shouldUnpackZipResponse(endpoint), - () -> - "Expected shouldUnpackZipResponse=false for " - + endpoint - + " with description: " - + description); - } - @Test void constructorAcceptsNullUserService() { ApiDocService service = new ApiDocService(mapper, servletContext, null); assertNotNull(service); } - - @Test - void getExtensionTypesInitializesMapOnFirstCall() throws Exception { - String json = "{\"description\": \"Output:PDF\"}"; - JsonNode postNode = mapper.readTree(json); - ApiEndpoint endpoint = new ApiEndpoint("/test", postNode); - setApiDocumentation(Map.of("/test", endpoint)); - setApiDocsJsonRootNode(); - List first = apiDocService.getExtensionTypes(true, "/test"); - List second = apiDocService.getExtensionTypes(true, "/test"); - assertEquals(first, second); - } } diff --git a/app/core/src/test/java/stirling/software/SPDF/service/VeraPDFServicePdfaFixtureTest.java b/app/core/src/test/java/stirling/software/SPDF/service/VeraPDFServicePdfaFixtureTest.java new file mode 100644 index 0000000000..e071ece3fb --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/service/VeraPDFServicePdfaFixtureTest.java @@ -0,0 +1,208 @@ +package stirling.software.SPDF.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider; +import org.verapdf.pdfa.Foundries; +import org.verapdf.pdfa.PDFAParser; +import org.verapdf.pdfa.PDFAValidator; +import org.verapdf.pdfa.flavours.PDFAFlavour; +import org.verapdf.pdfa.results.ValidationResult; + +import stirling.software.SPDF.model.api.security.PDFVerificationResult; + +/** + * Exercises {@link VeraPDFService} against real PDF/A files. Fixtures were produced by Ghostscript + * with the same flags as ConvertPDFToPDFA and independently confirmed conformant by veraPDF. + */ +class VeraPDFServicePdfaFixtureTest { + + private static final String VALID_1B = "valid-pdfa-1b.pdf"; + private static final String VALID_2B = "valid-pdfa-2b.pdf"; + private static final String DECLARED_BUT_INVALID_1B = "declared-pdfa-1b-no-outputintent.pdf"; + + private VeraPDFService service; + + @BeforeEach + void setUp() { + service = new VeraPDFService(); + service.initialize(); + } + + @Test + void fixtures_areGenuinePdfaAccordingToVeraPdfItself() throws Exception { + assertVeraPdfVerdict(VALID_1B, PDFAFlavour.PDFA_1_B, true); + assertVeraPdfVerdict(VALID_2B, PDFAFlavour.PDFA_2_B, true); + assertVeraPdfVerdict(DECLARED_BUT_INVALID_1B, PDFAFlavour.PDFA_1_B, false); + } + + @Test + void validatePDF_realPdfa1b_reportsCompliantPdfa1b() throws Exception { + PDFVerificationResult result = onlyResult(fixture(VALID_1B)); + + assertEquals("1b", result.getStandard()); + assertEquals("1b", result.getValidationProfile()); + assertTrue(result.isDeclaredPdfa(), "Genuine PDF/A-1b must be reported as declared PDF/A"); + assertTrue(result.isCompliant(), "Genuine PDF/A-1b must validate as compliant"); + assertEquals( + 0, result.getTotalFailures(), () -> "Unexpected failures: " + messages(result)); + assertTrue( + result.getStandardName().startsWith("PDF/A-"), + "Display name should name the PDF/A standard, got: " + result.getStandardName()); + assertTrue( + result.getStandardName().endsWith(" compliant"), + "Display name should read as compliant, got: " + result.getStandardName()); + assertEquals(result.getStandardName(), result.getComplianceSummary()); + } + + @Test + void validatePDF_realPdfa2b_reportsPdfa2bAndNotPdfa1b() throws Exception { + PDFVerificationResult result = onlyResult(fixture(VALID_2B)); + + // Proves firstFlavour() returns the flavour actually declared, not just a non-null one + assertEquals("2b", result.getStandard()); + assertEquals("2b", result.getValidationProfile()); + assertNotEquals("1b", result.getStandard()); + assertTrue(result.isDeclaredPdfa()); + assertTrue(result.isCompliant(), () -> "Unexpected failures: " + messages(result)); + assertEquals(0, result.getTotalFailures()); + } + + @Test + void validatePDF_plainPdf_reportsNotPdfaAndDoesNotThrowIndexOutOfBounds() throws Exception { + byte[] pdfBytes = createSimplePdf(); + + // veraPDF 1.30 returns an empty flavour list here where 1.28 returned [1b]; get(0) threw + List results = + assertDoesNotThrow( + () -> service.validatePDF(new ByteArrayInputStream(pdfBytes)), + "Empty veraPDF flavour list must not surface as IndexOutOfBoundsException"); + + // One result: PDF/UA is checked by the dedicated accessibility-report endpoint, not here. + assertEquals(1, results.size(), () -> "Expected a single PDF/A result, got: " + results); + + PDFVerificationResult result = results.get(0); + assertEquals("not-pdfa", result.getStandard()); + assertFalse(result.isDeclaredPdfa()); + assertFalse(result.isCompliant()); + assertEquals("Not PDF/A (no PDF/A identification metadata)", result.getStandardName()); + } + + @Test + void validatePDF_declaresPdfaButNotConformant_reportsFlavourWithFailures() throws Exception { + PDFVerificationResult result = onlyResult(fixture(DECLARED_BUT_INVALID_1B)); + + // "declares PDF/A but broken" must stay distinct from "not PDF/A at all" + assertEquals("1b", result.getStandard()); + assertNotEquals("not-pdfa", result.getStandard()); + assertTrue(result.isDeclaredPdfa(), "XMP still declares pdfaid:part=1"); + assertFalse(result.isCompliant(), "Stripped OutputIntent must fail conformance"); + assertTrue(result.getTotalFailures() > 0, "Non-conformance must be reported as issues"); + assertTrue( + result.getStandardName().endsWith(" with errors"), + "Display name should flag errors, got: " + result.getStandardName()); + assertTrue( + messages(result).contains("OutputIntent"), + "Expected the missing OutputIntent to be reported, got: " + messages(result)); + } + + @Test + void firstFlavour_withEmptyList_returnsNullInsteadOfThrowing() throws Exception { + Method method = VeraPDFService.class.getDeclaredMethod("firstFlavour", List.class); + method.setAccessible(true); + + assertNull(method.invoke(null, List.of())); + assertEquals(PDFAFlavour.PDFA_2_B, method.invoke(null, List.of(PDFAFlavour.PDFA_2_B))); + } + + @Test + void detectedFlavours_withNullFlavourList_returnsEmptyList() throws Exception { + Method method = + VeraPDFService.class.getDeclaredMethod("detectedFlavours", PDFAParser.class); + method.setAccessible(true); + + PDFAParser parser = mock(PDFAParser.class); + when(parser.getFlavours()).thenReturn(null); + + assertEquals(List.of(), method.invoke(null, parser)); + } + + private static void assertVeraPdfVerdict( + String fixtureName, PDFAFlavour expectedFlavour, boolean expectedCompliant) + throws Exception { + VeraGreenfieldFoundryProvider.initialise(); + byte[] bytes = fixture(fixtureName); + + List flavours; + try (PDFAParser parser = + Foundries.defaultInstance().createParser(new ByteArrayInputStream(bytes))) { + flavours = parser.getFlavours(); + } + assertEquals(List.of(expectedFlavour), flavours, fixtureName + " declared flavours"); + + try (PDFAParser parser = + Foundries.defaultInstance() + .createParser(new ByteArrayInputStream(bytes), expectedFlavour)) { + PDFAValidator validator = + Foundries.defaultInstance().createValidator(expectedFlavour, false); + ValidationResult result = validator.validate(parser); + assertEquals( + expectedCompliant, result.isCompliant(), fixtureName + " veraPDF compliance"); + } + } + + /** The PDF/A result; every document is also checked against PDF/UA, so filter that one out. */ + private PDFVerificationResult onlyResult(byte[] pdfBytes) throws Exception { + List results = + service.validatePDF(new ByteArrayInputStream(pdfBytes)); + + assertNotNull(results); + List pdfaResults = + results.stream().filter(r -> !isUaResult(r)).toList(); + assertEquals( + 1, pdfaResults.size(), () -> "Expected a single PDF/A result, got: " + results); + return pdfaResults.get(0); + } + + private static boolean isUaResult(PDFVerificationResult result) { + String profile = result.getValidationProfile(); + return profile != null && profile.toLowerCase().contains("ua"); + } + + private static String messages(PDFVerificationResult result) { + StringBuilder builder = new StringBuilder(); + for (PDFVerificationResult.ValidationIssue issue : result.getFailures()) { + builder.append(issue.getMessage()).append(" | "); + } + return builder.toString(); + } + + private static byte[] fixture(String name) throws IOException { + try (InputStream in = + VeraPDFServicePdfaFixtureTest.class.getResourceAsStream("/pdfa/" + name)) { + assertNotNull(in, "Missing test fixture /pdfa/" + name); + return in.readAllBytes(); + } + } + + private static byte[] createSimplePdf() throws IOException { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + return baos.toByteArray(); + } + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/util/ExtractScriptConcurrencyTest.java b/app/core/src/test/java/stirling/software/SPDF/util/ExtractScriptConcurrencyTest.java new file mode 100644 index 0000000000..f0c3ff6f95 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/util/ExtractScriptConcurrencyTest.java @@ -0,0 +1,78 @@ +package stirling.software.SPDF.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.common.util.GeneralUtils; + +/** + * Lives in core because the python resources it extracts ship in this module. + * + *

Callers hand the returned path straight to python3, so the file has to stay readable while + * other requests are extracting it. Re-writing it per call used to break that wherever rename is + * not atomic, such as a 9p or NFS bind mount. + */ +class ExtractScriptConcurrencyTest { + + @Test + @DisplayName("concurrent callers always get a readable script") + void concurrentCallersSeeAReadableScript() throws Exception { + int threads = 16; + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threads); + AtomicInteger unreadable = new AtomicInteger(); + AtomicInteger errors = new AtomicInteger(); + + for (int i = 0; i < threads; i++) { + Thread.ofVirtual() + .start( + () -> { + try { + start.await(); + for (int n = 0; n < 25; n++) { + Path script = GeneralUtils.extractScript("png_to_webp.py"); + if (!Files.isReadable(script)) { + unreadable.incrementAndGet(); + } + } + } catch (Exception e) { + errors.incrementAndGet(); + } finally { + done.countDown(); + } + }); + } + + start.countDown(); + assertTrue(done.await(60, TimeUnit.SECONDS), "extractScript threads did not finish"); + assertEquals(0, errors.get(), "extractScript threw under concurrency"); + assertEquals(0, unreadable.get(), "script was missing while another caller held it"); + } + + @Test + @DisplayName("repeated calls return the same path without rewriting the file") + void repeatedCallsAreStable() throws Exception { + Path first = GeneralUtils.extractScript("png_to_webp.py"); + long modified = Files.getLastModifiedTime(first).toMillis(); + long size = Files.size(first); + + // Any rewrite after this pause lands on a later timestamp, so mtime is a + // reliable signal rather than a same-millisecond coin flip. + Thread.sleep(50); + for (int i = 0; i < 5; i++) { + assertEquals(first, GeneralUtils.extractScript("png_to_webp.py")); + } + + assertEquals(modified, Files.getLastModifiedTime(first).toMillis(), "script was rewritten"); + assertEquals(size, Files.size(first)); + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/utils/AutoRotateDetectionTest.java b/app/core/src/test/java/stirling/software/SPDF/utils/AutoRotateDetectionTest.java new file mode 100644 index 0000000000..741d18e652 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/utils/AutoRotateDetectionTest.java @@ -0,0 +1,248 @@ +package stirling.software.SPDF.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.util.Matrix; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import stirling.software.SPDF.utils.AutoRotateDetection.OsdResult; +import stirling.software.SPDF.utils.AutoRotateDetection.TextDirection; + +class AutoRotateDetectionTest { + + private static final String SAMPLE_TEXT = + "The quick brown fox jumps over the lazy dog again and again"; + + private PDDocument docWithText(int textAngleDegrees, int pageRotation) throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix( + Matrix.getRotateInstance(Math.toRadians(textAngleDegrees), 300, 400)); + content.showText(SAMPLE_TEXT); + content.endText(); + } + page.setRotation(pageRotation); + return document; + } + + /** + * Ground truth per the PDF spec: /Rotate R displays the page R degrees clockwise, so upright + * page-space text under /Rotate R needs a further (360 - R) % 360 to display upright again; + * text drawn rotated T degrees CCW in page space needs T clockwise to correct. Combined, the + * expected correction is (T - R) mod 360. + */ + @ParameterizedTest + @CsvSource({ + // textAngle, pageRotation, expectedCorrection + "0, 0, 0", + "0, 90, 270", + "0, 180, 180", + "0, 270, 90", + "90, 0, 90", + "180, 0, 180", + "270, 0, 270", + "90, 90, 0", + "180, 90, 90", + }) + void detectsCorrectionForRotatedTextAndPages( + int textAngle, int pageRotation, int expectedCorrection) throws IOException { + try (PDDocument document = docWithText(textAngle, pageRotation)) { + TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0); + + assertThat(direction.isConclusive()) + .as( + "direction should be conclusive, glyphs=%d dominance=%s", + direction.glyphCount(), direction.dominance()) + .isTrue(); + assertThat( + AutoRotateDetection.correctionFromTextDirection( + direction.dominantDirection(), + Math.floorMod(pageRotation, 360))) + .isEqualTo(expectedCorrection); + } + } + + @Test + void mixedDirectionsAreNotConclusive() throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix(Matrix.getTranslateInstance(100, 400)); + content.showText(SAMPLE_TEXT); + content.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(90), 300, 200)); + content.showText(SAMPLE_TEXT); + content.endText(); + } + try (document) { + TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0); + assertThat(direction.isConclusive()).isFalse(); + } + } + + @Test + void emptyPageIsNotConclusive() throws IOException { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage(PDRectangle.LETTER)); + TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0); + assertThat(direction.glyphCount()).isZero(); + assertThat(direction.isConclusive()).isFalse(); + } + } + + @Test + void shortTextIsNotConclusive() throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix(Matrix.getTranslateInstance(100, 400)); + content.showText("Short"); + content.endText(); + } + try (document) { + TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0); + assertThat(direction.isConclusive()).isFalse(); + } + } + + @Test + void unanimousShortTextIsConclusive() throws IOException { + // Between MIN_GLYPHS_UNANIMOUS (8) and MIN_GLYPHS (30): trusted only because every + // glyph agrees on direction, the sparse-page path (e.g. a lone header or URL line). + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(90), 300, 200)); + content.showText("york.gov.uk/pay"); + content.endText(); + } + try (document) { + TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0); + assertThat(direction.glyphCount()) + .isBetween( + AutoRotateDetection.MIN_GLYPHS_UNANIMOUS, + AutoRotateDetection.MIN_GLYPHS - 1); + assertThat(direction.dominance()).isEqualTo(1.0); + assertThat(direction.isConclusive()).isTrue(); + assertThat(direction.dominantDirection()).isEqualTo(90); + } + } + + @Test + void bucketsGlyphsPerPageInOneWalk() throws IOException { + // Each page carries text at a different angle; the single-pass walk must attribute + // glyphs to the right page rather than pooling them. + int[] angles = {0, 90, 180, 270}; + try (PDDocument document = new PDDocument()) { + for (int angle : angles) { + PDPage page = new PDPage(PDRectangle.LETTER); + document.addPage(page); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + content.beginText(); + content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + content.setTextMatrix( + Matrix.getRotateInstance(Math.toRadians(angle), 300, 400)); + content.showText(SAMPLE_TEXT); + content.endText(); + } + } + + List directions = AutoRotateDetection.detectTextDirections(document); + + assertThat(directions).hasSize(angles.length); + for (int i = 0; i < angles.length; i++) { + assertThat(directions.get(i).isConclusive()).as("page %d", i + 1).isTrue(); + assertThat(directions.get(i).dominantDirection()) + .as("page %d direction", i + 1) + .isEqualTo(angles[i]); + } + } + } + + @Test + void detectsBlankAndInkedRenders() { + BufferedImage blank = new BufferedImage(200, 200, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g = blank.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, 200, 200); + g.dispose(); + assertThat(AutoRotateDetection.isBlankRender(blank)).isTrue(); + + BufferedImage speck = copyOf(blank); + Graphics2D specked = speck.createGraphics(); + specked.setColor(Color.BLACK); + specked.fillRect(0, 0, 2, 2); // a dust speck must not count as content + specked.dispose(); + assertThat(AutoRotateDetection.isBlankRender(speck)).isTrue(); + + BufferedImage inked = copyOf(blank); + Graphics2D inkedG = inked.createGraphics(); + inkedG.setColor(Color.BLACK); + inkedG.fillRect(20, 20, 120, 60); + inkedG.dispose(); + assertThat(AutoRotateDetection.isBlankRender(inked)).isFalse(); + } + + private static BufferedImage copyOf(BufferedImage source) { + BufferedImage copy = + new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); + Graphics2D g = copy.createGraphics(); + g.drawImage(source, 0, 0, null); + g.dispose(); + return copy; + } + + @Test + void parsesTypicalOsdOutput() { + String output = + """ + Estimating resolution as 336 + Page number: 0 + Orientation in degrees: 180 + Rotate: 180 + Orientation confidence: 9.15 + Script: Latin + Script confidence: 4.43 + """; + Optional result = AutoRotateDetection.parseOsd(output); + assertThat(result).isPresent(); + assertThat(result.get().rotate()).isEqualTo(180); + assertThat(result.get().confidence()).isEqualTo(9.15); + } + + @Test + void parseOsdRejectsIncompleteOutput() { + assertThat(AutoRotateDetection.parseOsd("Too few characters. Skipping this page")) + .isEmpty(); + assertThat(AutoRotateDetection.parseOsd("Rotate: 90")).isEmpty(); + assertThat(AutoRotateDetection.parseOsd(null)).isEmpty(); + assertThat(AutoRotateDetection.parseOsd("")).isEmpty(); + } +} diff --git a/app/core/src/test/java/org/apache/pdfbox/examples/util/ConnectedInputStreamTest.java b/app/core/src/test/java/stirling/software/SPDF/utils/ConnectedInputStreamTest.java similarity index 98% rename from app/core/src/test/java/org/apache/pdfbox/examples/util/ConnectedInputStreamTest.java rename to app/core/src/test/java/stirling/software/SPDF/utils/ConnectedInputStreamTest.java index 48d8b11a48..5f02dfb687 100644 --- a/app/core/src/test/java/org/apache/pdfbox/examples/util/ConnectedInputStreamTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/utils/ConnectedInputStreamTest.java @@ -1,4 +1,4 @@ -package org.apache.pdfbox.examples.util; +package stirling.software.SPDF.utils; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java b/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java index 53212a2e65..2083e7339b 100644 --- a/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java +++ b/app/core/src/test/java/stirling/software/common/controller/JobControllerTest.java @@ -5,9 +5,11 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.util.Map; +import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -442,4 +444,52 @@ class JobControllerTest { assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); verify(fileStorage, never()).getFileSize(eq(fileId)); } + + @Test + void testCleanupFinishedJobs_ReportsWhatWasReleased() { + when(taskManager.cleanupFinishedJobsNow(any())) + .thenReturn(new TaskManager.CleanupSummary(2, 5, 1)); + + ResponseEntity response = controller.cleanupFinishedJobs(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = (Map) response.getBody(); + assertEquals(2, body.get("jobsRemoved")); + assertEquals(5, body.get("filesDeleted")); + assertEquals(1, body.get("jobsRetained")); + } + + @Test + void testCleanupFinishedJobs_OnlySweepsJobsTheCallerOwns() { + ReflectionTestUtils.setField(controller, "jobOwnershipService", jobOwnershipService); + when(jobOwnershipService.validateJobAccess("me:job")).thenReturn(true); + when(jobOwnershipService.validateJobAccess("someone-else:job")) + .thenThrow(new SecurityException("not yours")); + when(taskManager.cleanupFinishedJobsNow(any())) + .thenReturn(new TaskManager.CleanupSummary(1, 1, 1)); + + controller.cleanupFinishedJobs(); + + @SuppressWarnings("unchecked") + ArgumentCaptor> filter = ArgumentCaptor.forClass(Predicate.class); + verify(taskManager).cleanupFinishedJobsNow(filter.capture()); + assertTrue(filter.getValue().test("me:job")); + assertFalse( + filter.getValue().test("someone-else:job"), + "A job the caller cannot access must be left in place"); + } + + @Test + void testCleanupFinishedJobs_SweepsEverythingWhenSecurityIsDisabled() { + when(taskManager.cleanupFinishedJobsNow(any())) + .thenReturn(new TaskManager.CleanupSummary(3, 3, 0)); + + controller.cleanupFinishedJobs(); + + @SuppressWarnings("unchecked") + ArgumentCaptor> filter = ArgumentCaptor.forClass(Predicate.class); + verify(taskManager).cleanupFinishedJobsNow(filter.capture()); + assertTrue(filter.getValue().test("any-job-id")); + } } diff --git a/app/core/src/test/resources/pdfa/declared-pdfa-1b-no-outputintent.pdf b/app/core/src/test/resources/pdfa/declared-pdfa-1b-no-outputintent.pdf new file mode 100644 index 0000000000..f3c29fe1aa Binary files /dev/null and b/app/core/src/test/resources/pdfa/declared-pdfa-1b-no-outputintent.pdf differ diff --git a/app/core/src/test/resources/pdfa/valid-pdfa-1b.pdf b/app/core/src/test/resources/pdfa/valid-pdfa-1b.pdf new file mode 100644 index 0000000000..14d8d0c6bd Binary files /dev/null and b/app/core/src/test/resources/pdfa/valid-pdfa-1b.pdf differ diff --git a/app/core/src/test/resources/pdfa/valid-pdfa-2b.pdf b/app/core/src/test/resources/pdfa/valid-pdfa-2b.pdf new file mode 100644 index 0000000000..1b7687fd89 Binary files /dev/null and b/app/core/src/test/resources/pdfa/valid-pdfa-2b.pdf differ diff --git a/app/license-overrides.json b/app/license-overrides.json new file mode 100644 index 0000000000..0ea43fcaf9 --- /dev/null +++ b/app/license-overrides.json @@ -0,0 +1,12 @@ +{ + "com.hubspot.immutables:immutables-exceptions:1.9": { + "name": "The Apache License, Version 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "projectUrl": "https://github.com/HubSpot/hubspot-immutables/tree/58628096ac99b286fe4f8bfe12aa3cff0f0589d3" + }, + "com.hubspot:algebra:1.5": { + "name": "The Apache License, Version 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.txt", + "projectUrl": "https://github.com/HubSpot/algebra/tree/5d42983fd3a26539df9ba2cbeac32a1bddce0494" + } +} diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index d2028f7859..e66288238a 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -27,7 +27,7 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-cache' api 'com.github.ben-manes.caffeine:caffeine' implementation 'org.springframework.boot:spring-boot-starter-data-redis' - api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46' + api 'io.swagger.core.v3:swagger-core-jakarta:2.2.53' implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" // Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide // token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window @@ -37,6 +37,15 @@ dependencies { // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" + // PDF/UA tagging and its validation oracle. + implementation 'org.verapdf:validation-model:1.30.2' + // CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13 + implementation "org.mozilla:rhino:${rhinoVersion}" + // veraPDF still uses javax.xml.bind, not the new jakarta namespace + implementation 'javax.xml.bind:jaxb-api:2.3.1' + runtimeOnly 'com.sun.xml.bind:jaxb-impl:2.3.9' + runtimeOnly 'com.sun.xml.bind:jaxb-core:4.0.9' + implementation "com.google.code.gson:gson:${gsonVersion}" // jinjava/jjwt transitively request older Jackson 2 versions; declare the current @@ -59,7 +68,7 @@ dependencies { runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}" runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}" runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases - runtimeOnly 'org.postgresql:postgresql:42.7.11' + runtimeOnly 'org.postgresql:postgresql:42.7.13' implementation('com.coveo:saml-client:5.0.0') { exclude group: 'org.opensaml', module: 'opensaml-core' } @@ -67,6 +76,15 @@ dependencies { implementation "software.amazon.awssdk:s3:${awsSdkVersion}" implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}" + // Network policy sources (SFTP/FTP/SMB drop folders). All permissively licensed: + // jsch fork BSD, commons-net Apache-2.0, smbj Apache-2.0 (BouncyCastle already on classpath). + implementation "com.github.mwiede:jsch:${jschVersion}" + implementation "commons-net:commons-net:${commonsNetVersion}" + implementation "com.hierynomus:smbj:${smbjVersion}" + + // Streaming AEAD (AES-GCM-HKDF segments) for storage encryption at rest. Apache-2.0. + implementation "com.google.crypto.tink:tink:${tinkVersion}" + // @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the // root) so policy.source repositories can be exercised against embedded H2. testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditEventType.java b/app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditEventType.java index 433244a971..a1220a9fdd 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditEventType.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditEventType.java @@ -19,6 +19,9 @@ public enum AuditEventType { // File operations - STANDARD level FILE_OPERATION("File operation"), + // Storage encryption at rest - STANDARD level + STORAGE_ENCRYPTION("Storage encryption operation"), + // PDF operations - STANDARD level PDF_PROCESS("PDF processing operation"), diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java index ea096a8d23..3ba4adcbee 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java @@ -2,6 +2,7 @@ package stirling.software.proprietary.config; import java.util.Map; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.slf4j.MDC; @@ -12,10 +13,15 @@ import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import jakarta.annotation.PreDestroy; + @Configuration @EnableAsync public class AsyncConfig { + private ExecutorService auditExecutorService; + private ExecutorService aiStreamExecutorService; + /** * MDC context-propagating task decorator. Copies MDC context from the caller thread to the * virtual thread executing the task. @@ -44,8 +50,8 @@ public class AsyncConfig { @Bean(name = "auditExecutor") public Executor auditExecutor() { - TaskExecutorAdapter adapter = - new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); + auditExecutorService = Executors.newVirtualThreadPerTaskExecutor(); + TaskExecutorAdapter adapter = new TaskExecutorAdapter(auditExecutorService); adapter.setTaskDecorator(new MDCContextTaskDecorator()); return adapter; } @@ -53,9 +59,25 @@ public class AsyncConfig { /** Propagates the request's SecurityContext onto background AI-orchestration threads. */ @Bean(name = "aiStreamExecutor") public Executor aiStreamExecutor() { - TaskExecutorAdapter adapter = - new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); + aiStreamExecutorService = Executors.newVirtualThreadPerTaskExecutor(); + TaskExecutorAdapter adapter = new TaskExecutorAdapter(aiStreamExecutorService); adapter.setTaskDecorator(new MDCContextTaskDecorator()); return new DelegatingSecurityContextExecutor(adapter); } + + /** + * Close the underlying executors because the exposed Spring adapters do not own their + * lifecycle. + */ + @PreDestroy + void shutdown() { + shutdownExecutor(auditExecutorService); + shutdownExecutor(aiStreamExecutorService); + } + + private void shutdownExecutor(ExecutorService executor) { + if (executor != null) { + executor.shutdownNow(); + } + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminJobController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminJobController.java index ef941c603f..fa9d6db141 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminJobController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AdminJobController.java @@ -7,6 +7,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; @@ -66,28 +67,40 @@ public class AdminJobController { } /** - * Manually trigger cleanup of old jobs (admin only) + * Manually trigger cleanup of old jobs (admin only). Covers every user's jobs, unlike the + * self-service {@code POST /api/v1/general/jobs/cleanup}, which only releases the caller's own. * - * @return A response indicating how many jobs were cleaned up + * @param force Ignore the retention window and release every finished job now, rather than only + * those already past it + * @return A response indicating how many jobs and files were cleaned up */ @PostMapping("/job/cleanup") - @Operation(summary = "Cleanup old jobs") + @Operation( + summary = "Cleanup old jobs", + description = + "Runs the job retention sweep now across all users. With force=true the" + + " retention window is ignored and every finished job is released" + + " immediately.") @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity cleanupOldJobs() { - int beforeCount = taskManager.getJobStats().getTotalJobs(); - taskManager.cleanupOldJobs(); - int afterCount = taskManager.getJobStats().getTotalJobs(); - int removedCount = beforeCount - afterCount; + public ResponseEntity cleanupOldJobs( + @RequestParam(name = "force", defaultValue = "false") boolean force) { + TaskManager.CleanupSummary summary = + force + ? taskManager.cleanupFinishedJobsNow(jobId -> true) + : taskManager.cleanupOldJobs(); log.info( - "Admin triggered job cleanup: removed {} jobs, {} remaining", - removedCount, - afterCount); + "Admin triggered job cleanup (force={}): removed {} jobs and {} files, {} remaining", + force, + summary.jobsRemoved(), + summary.filesDeleted(), + summary.jobsRetained()); return ResponseEntity.ok( Map.of( "message", "Cleanup complete", - "removedJobs", removedCount, - "remainingJobs", afterCount)); + "removedJobs", summary.jobsRemoved(), + "filesDeleted", summary.filesDeleted(), + "remainingJobs", summary.jobsRetained())); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java index 69c9402926..7376795c66 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java @@ -143,19 +143,12 @@ public class AuditRestController { @RequestParam(value = "period", defaultValue = "week") String period) { // Calculate days based on period - int days; - switch (period.toLowerCase()) { - case "day": - days = 1; - break; - case "month": - days = 30; - break; - case "week": - default: - days = 7; - break; - } + int days = + switch (period.toLowerCase()) { + case "day" -> 1; + case "month" -> 30; + default -> 7; + }; // Get events from the specified period Instant startDate = Instant.now().minus(java.time.Duration.ofDays(days)); @@ -269,19 +262,12 @@ public class AuditRestController { @RequestParam(value = "period", defaultValue = "week") String period) { // Calculate days based on period - int days; - switch (period.toLowerCase()) { - case "day": - days = 1; - break; - case "month": - days = 30; - break; - case "week": - default: - days = 7; - break; - } + int days = + switch (period.toLowerCase()) { + case "day" -> 1; + case "month" -> 30; + default -> 7; + }; // Get events from the specified period and previous period Instant now = Instant.now(); @@ -754,7 +740,7 @@ public class AuditRestController { List> files = (List>) eventData.get("files"); if (files != null && !files.isEmpty()) { - Map firstFile = files.get(0); + Map firstFile = files.getFirst(); data.put("documentname", String.valueOf(firstFile.getOrDefault("name", ""))); data.put("author", String.valueOf(firstFile.getOrDefault("pdfAuthor", ""))); data.put("filehash", String.valueOf(firstFile.getOrDefault("fileHash", ""))); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/MathAuditorAgentController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/MathAuditorAgentController.java index 8beddd5e94..d03c176c23 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/MathAuditorAgentController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/MathAuditorAgentController.java @@ -68,8 +68,6 @@ public class MathAuditorAgentController { Returns a JSON Verdict describing every discrepancy found. How the Verdict is presented to the end user (chat answer, PDF annotations, etc.) is up to the caller. - - Input: PDF Output: JSON Type: SISO """) public ResponseEntity mathAuditorAgent( @Parameter(description = "The PDF document to audit", required = true) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/PdfCommentAgentController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/PdfCommentAgentController.java index fadfacf03a..db9a89a867 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/PdfCommentAgentController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/PdfCommentAgentController.java @@ -67,8 +67,6 @@ public class PdfCommentAgentController { The annotated PDF is streamed back in the response body with Content-Type: application/pdf. - - Input: PDF + prompt Output: PDF Type: SISO """) public ResponseEntity pdfCommentAgent( @Parameter(description = "The PDF document to annotate", required = true) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/converters/ConvertPdfToPdfUa.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/converters/ConvertPdfToPdfUa.java new file mode 100644 index 0000000000..7f5241388f --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/converters/ConvertPdfToPdfUa.java @@ -0,0 +1,176 @@ +package stirling.software.proprietary.controller.api.converters; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.multipart.MultipartFile; + +import io.github.pixee.security.Filenames; +import io.swagger.v3.oas.annotations.Operation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.annotations.AutoJobPostMapping; +import stirling.software.common.annotations.api.ConvertApi; +import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; +import stirling.software.proprietary.model.api.converters.PdfToPdfUaRequest; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; +import stirling.software.proprietary.service.ua.PdfUaConversionService; + +/** Converts a PDF to PDF/UA; response headers say whether the result actually conforms. */ +@ConvertApi +@Slf4j +@RequiredArgsConstructor +public class ConvertPdfToPdfUa { + + private static final String HEADER_DECLARED = "X-Stirling-UA-Declared"; + private static final String HEADER_FAILURES = "X-Stirling-UA-Failures"; + private static final String HEADER_ALT_NEEDED = "X-Stirling-UA-Figures-Needing-Alt"; + private static final String HEADER_WARNINGS = "X-Stirling-UA-Warnings"; + + /** Any line ending, so descriptions pasted from any platform parse the same. */ + private static final Pattern NEWLINE = Pattern.compile("\\R"); + + private final PdfUaConversionService conversionService; + private final TempFileManager tempFileManager; + + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/pdf/ua", + resourceWeight = ResourceWeight.LARGE_WEIGHT) + @ToolIO(produces = ToolFormat.PDF) + @Operation( + summary = "Convert a PDF to PDF/UA-1 or PDF/UA-2", + description = + "Tags the document, marks decorative content as artifacts, embeds fonts and" + + " applies the document-level requirements of PDF/UA, then validates" + + " the result. A conformance declaration is written only if validation" + + " passes, so the returned file never claims more than it delivers.") + public ResponseEntity pdfToPdfUa(@ModelAttribute PdfToPdfUaRequest request) + throws IOException { + + MultipartFile input = request.getFileInput(); + if (input == null || input.isEmpty()) { + throw ExceptionUtils.createPdfFileRequiredException(); + } + + String originalName = Filenames.toSimpleFileName(input.getOriginalFilename()); + String stem = stripExtension(originalName == null ? "document" : originalName); + PdfUaProfile profile = PdfUaProfile.fromRequest(request.getProfile()); + + TaggingOptions options = + TaggingOptions.builder() + .profile(profile) + .title(request.getTitle()) + .fallbackTitle(stem) + // Only used when the document declares no language of its own. + .language( + request.getLanguage() == null || request.getLanguage().isBlank() + ? "en-GB" + : request.getLanguage()) + .overrideLanguage( + request.getOverrideLanguage() != null + && request.getOverrideLanguage()) + .existingTags(existingTags(request.getExistingTags())) + .figurePolicy(figurePolicy(request.getFigurePolicy())) + .embedFonts(request.getEmbedFonts() == null || request.getEmbedFonts()) + .altTextByFigure(parseAltText(request.getAltText())) + .build(); + + PdfUaConversionOutcome outcome = conversionService.convert(input.getBytes(), options); + + log.info( + "Converted '{}' to {}: declared={}, {} remaining failure(s)", + originalName, + profile.displayName(), + outcome.declared(), + outcome.validation().totalFailures()); + + outcome.warnings().forEach(warning -> log.info("PDF/UA warning: {}", warning)); + + // Streamed from a temp file so a large conversion does not hold a second heap copy. + String suffix = outcome.declared() ? "_pdfua" + profile.part() : "_tagged"; + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), outcome.pdfBytes()); + } catch (IOException e) { + tempOut.close(); + throw e; + } + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOut, stem + suffix + ".pdf"); + + return ResponseEntity.status(response.getStatusCode()) + .headers(response.getHeaders()) + .header(HEADER_DECLARED, String.valueOf(outcome.declared())) + .header(HEADER_FAILURES, String.valueOf(outcome.validation().totalFailures())) + .header( + HEADER_ALT_NEEDED, + String.valueOf(outcome.tagging().figuresNeedingAltText())) + // Count only: warning text is multi-line prose, which HTTP headers mangle. + .header(HEADER_WARNINGS, String.valueOf(outcome.warnings().size())) + .body(response.getBody()); + } + + /** + * Parses newline-separated {@code key=description} pairs, keyed as the report hands them out. + * Only the first "=" splits, since a description may contain one. + */ + public static Map parseAltText(String raw) { + if (raw == null || raw.isBlank()) { + return Map.of(); + } + Map parsed = new LinkedHashMap<>(); + for (String line : NEWLINE.split(raw)) { + int split = line.indexOf('='); + if (split <= 0) { + continue; + } + String key = line.substring(0, split).strip(); + String description = line.substring(split + 1).strip(); + if (!key.isEmpty() && !description.isEmpty()) { + parsed.put(key, description); + } + } + return parsed; + } + + private static TaggingOptions.ExistingTags existingTags(String value) { + if (value == null) { + return TaggingOptions.ExistingTags.AUTO; + } + return switch (value.trim().toLowerCase()) { + case "keep" -> TaggingOptions.ExistingTags.KEEP; + case "rebuild" -> TaggingOptions.ExistingTags.REBUILD; + default -> TaggingOptions.ExistingTags.AUTO; + }; + } + + private static TaggingOptions.FigurePolicy figurePolicy(String value) { + if (value != null && value.trim().equalsIgnoreCase("mark-decorative")) { + return TaggingOptions.FigurePolicy.MARK_DECORATIVE; + } + return TaggingOptions.FigurePolicy.REQUIRE_ALT; + } + + private static String stripExtension(String filename) { + int dot = filename.lastIndexOf('.'); + return dot > 0 ? filename.substring(0, dot) : filename; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/security/AccessibilityReportController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/security/AccessibilityReportController.java new file mode 100644 index 0000000000..043734dad8 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/security/AccessibilityReportController.java @@ -0,0 +1,67 @@ +package stirling.software.proprietary.controller.api.security; + +import java.io.IOException; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.annotations.AutoJobPostMapping; +import stirling.software.common.annotations.api.SecurityApi; +import stirling.software.common.enumeration.ResourceWeight; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.proprietary.model.api.ua.AccessibilityReport; +import stirling.software.proprietary.model.api.ua.AccessibilityReportRequest; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.service.ua.AccessibilityAuditService; + +/** Reports how accessible a document is, without modifying it. */ +@SecurityApi +@RequiredArgsConstructor +@Slf4j +public class AccessibilityReportController { + + private final AccessibilityAuditService auditService; + + @ToolIO(produces = ToolFormat.JSON) + @Operation( + summary = "Report a document's accessibility standing", + description = + "Validates the document against PDF/UA and reports what fails, which failures" + + " can be fixed automatically, and which checks still need a person." + + " Does not modify the file.") + // Costs a full veraPDF pass plus the converter's own layout analysis over every page. + @AutoJobPostMapping( + value = "/accessibility-report", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + resourceWeight = ResourceWeight.LARGE_WEIGHT) + public ResponseEntity report( + @ModelAttribute AccessibilityReportRequest request) { + + MultipartFile file = request.getFileInput(); + if (file == null || file.isEmpty()) { + throw ExceptionUtils.createPdfFileRequiredException(); + } + PdfUaProfile profile = PdfUaProfile.fromRequest(request.getProfile()); + try { + AccessibilityReport report = auditService.audit(file.getBytes(), profile); + log.info( + "Accessibility report for '{}': tagged={}, {} issue(s)", + file.getOriginalFilename(), + report.isTagged(), + report.getIssues().size()); + return ResponseEntity.ok(report); + } catch (IOException e) { + throw ExceptionUtils.createRuntimeException( + "error.ioException", "Could not read the PDF: {0}", e, e.getMessage()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/AcknowledgeAction.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/AcknowledgeAction.java new file mode 100644 index 0000000000..7f8741b3d3 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/AcknowledgeAction.java @@ -0,0 +1,39 @@ +package stirling.software.proprietary.failure; + +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +/** + * "Seen, and I own it." Moves {@code NEW} to {@code ACKNOWLEDGED} so the row stops counting as + * unread while staying in the open list. Touches only the status, not the document, ledger or run. + * + *

Re-acknowledging is a no-op that keeps the original actor and timestamp, so the first person + * to pick it up stays credited. + */ +@Component +@RequiredArgsConstructor +public class AcknowledgeAction implements FailureAction { + + private final FileRunEventStore store; + + @Override + public FailureActionId id() { + return FailureActionId.ACKNOWLEDGE; + } + + @Override + public FileRunEvent execute(FileRunEvent event, Map inputs, String actor) { + // Guarded on NEW so a racing acknowledger cannot re-stamp the row, and so re-acknowledging + // returns the first actor's row rather than taking their credit. + return store.applyStatusOnce( + event.id(), + event.teamId(), + FileRunEventStatus.ACKNOWLEDGED, + actor, + List.of(FileRunEventStatus.NEW)); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/DismissAction.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/DismissAction.java new file mode 100644 index 0000000000..3ea22df414 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/DismissAction.java @@ -0,0 +1,32 @@ +package stirling.software.proprietary.failure; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +/** + * "No remediation will happen; close it." Terminal. Named Dismiss rather than Reject because it + * closes the incident, not the document: nothing is deleted or delivered, the ledger row is left + * alone, and a different file failing the same way still opens its own incident. + * + *

Recurrences of this exact failure fold onto the dismissed row, which is what makes "stop + * showing me this" hold for a source that re-lists the same failing file each sweep. + */ +@Component +@RequiredArgsConstructor +public class DismissAction implements FailureAction { + + private final FileRunEventStore store; + + @Override + public FailureActionId id() { + return FailureActionId.DISMISS; + } + + @Override + public FileRunEvent execute(FileRunEvent event, Map inputs, String actor) { + return store.applyStatus(event.id(), event.teamId(), FileRunEventStatus.DISMISSED, actor); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/EditorFailureReport.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/EditorFailureReport.java new file mode 100644 index 0000000000..b200b3d589 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/EditorFailureReport.java @@ -0,0 +1,47 @@ +package stirling.software.proprietary.failure; + +import java.util.List; + +/** + * A failure a user hit in the editor, reported by their own client. The editor calls tools directly + * rather than through the policy engine, so nothing server-side sees these unless the client says + * so. + * + *

Note what the client cannot supply: no team, no actor, no document name. The first two come + * from the authenticated session, and the third is never stored. + * + * @param operation the tool that failed, e.g. {@code remove-password} + * @param errorCode the code from the tool's Problem Details response, or null when there was none + * @param fileIds opaque client-side ids of the documents involved; empty when none is attributable + * @param detail the message the user saw + */ +public record EditorFailureReport( + String operation, String errorCode, List fileIds, String detail) { + + /** + * Cap on the files one report may name. Each one becomes a permanent incident and this endpoint + * is open to any authenticated user, so without a bound a single call can flood a leader's + * queue. 200 is several times the largest batch an editor session plausibly fails on, and the + * most the review queue shows in one page, so a real report never meets it. + * + *

An oversized report is refused whole rather than trimmed: see {@link + * FileRunEventController#report}. + */ + static final int MAX_FILE_IDS = 200; + + public EditorFailureReport { + fileIds = fileIds == null ? List.of() : List.copyOf(fileIds); + } + + boolean hasOperation() { + return operation != null && !operation.isBlank(); + } + + /** + * Counted before the blank ids are dropped, because this bounds the request rather than the + * rows it would produce. + */ + boolean namesTooManyFiles() { + return fileIds.size() > MAX_FILE_IDS; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureAction.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureAction.java new file mode 100644 index 0000000000..98c8ae328c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureAction.java @@ -0,0 +1,23 @@ +package stirling.software.proprietary.failure; + +import java.util.Map; + +/** + * The behaviour behind one {@link FailureActionId}. Implementations are Spring beans injected as a + * {@code List} and resolved by id, the pattern already used for {@code InputSource}, {@code + * PolicyOutputSink} and {@code PolicyTrigger}. + * + *

Keeping behaviour out of the registry keeps that pure data, so a new kind ships by declaring + * an action id that already has a handler. + */ +public interface FailureAction { + + FailureActionId id(); + + /** + * Apply the action and return the updated event. {@code inputs} carries whatever the action + * declared it needs, which is nothing for the two that exist today. Implementations leave the + * document, ledger, run and output destinations alone unless that is the action's purpose. + */ + FileRunEvent execute(FileRunEvent event, Map inputs, String actor); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionException.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionException.java new file mode 100644 index 0000000000..1c899ab529 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionException.java @@ -0,0 +1,49 @@ +package stirling.software.proprietary.failure; + +import lombok.Getter; + +/** + * Why an action could not be dispatched. Carries a {@link Reason} rather than an HTTP status, so + * the service stays web-agnostic and the controller owns the mapping. + */ +@Getter +public class FailureActionException extends RuntimeException { + + public enum Reason { + /** + * No such event, it belongs to another team, or the caller's team did not resolve. One + * reason for all three, so the response does not vary with which it was. Unrelated to + * {@link FailureKind#UNKNOWN}, which is an unclassified failure rather than a refused + * action. + */ + EVENT_NOT_FOUND, + + /** The action id is not in the vocabulary at all, or has no registered handler. */ + ACTION_NOT_RECOGNISED, + + /** + * The action exists but this kind does not declare it, so an incoherent pairing (releasing + * a document whose destination is what failed) cannot be dispatched even by hand. + * + *

Unreachable today: both kinds declare both actions, so no request can trip this guard + * until a kind ships with a restricted action set. Declared now because the guard must + * exist before that kind does, not after. + */ + ACTION_NOT_DECLARED, + + /** The event is already closed, so no further transition is possible. */ + ALREADY_CLOSED + } + + private final Reason reason; + + public FailureActionException(Reason reason, String message) { + this(reason, message, null); + } + + /** For a refusal that follows from a lower-level failure, so its stack is not dropped. */ + public FailureActionException(Reason reason, String message, Throwable cause) { + super(message, cause); + this.reason = reason; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionId.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionId.java new file mode 100644 index 0000000000..dbfcff7fb4 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionId.java @@ -0,0 +1,11 @@ +package stirling.software.proprietary.failure; + +/** + * The actions a {@link FailureKind} may declare. Both are incident dispositions: they change how + * the event is shown and touch nothing else, which is what makes them valid for every kind + * including {@link FailureKind#UNKNOWN}, and why there is no {@code APPROVE} yet. + */ +public enum FailureActionId { + ACKNOWLEDGE, + DISMISS +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionRegistry.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionRegistry.java new file mode 100644 index 0000000000..3928e91845 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureActionRegistry.java @@ -0,0 +1,68 @@ +package stirling.software.proprietary.failure; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +/** + * Resolves a {@link FailureActionId} to the bean that implements it. The startup check is the + * point: because kinds declare action ids as data, one could name an action nobody implements, + * which would otherwise show up as a button that 400s rather than as a failed boot. + */ +@Slf4j +@Service +public class FailureActionRegistry { + + private final Map byId = new EnumMap<>(FailureActionId.class); + + public FailureActionRegistry(List actions) { + for (FailureAction action : actions) { + FailureAction clash = byId.put(action.id(), action); + if (clash != null) { + throw new IllegalStateException( + "Two handlers registered for action " + + action.id() + + ": " + + clash.getClass().getName() + + " and " + + action.getClass().getName()); + } + } + } + + /** + * Fail fast if any kind declares an action with no handler, naming every gap rather than the + * first, so one boot tells you everything that is missing. + */ + @PostConstruct + void verifyEveryDeclaredActionHasAHandler() { + List gaps = + Arrays.stream(FailureKind.values()) + .flatMap( + kind -> + kind.getActions().stream() + .filter(action -> !byId.containsKey(action)) + .map(action -> kind.getId() + " -> " + action)) + .toList(); + if (!gaps.isEmpty()) { + throw new IllegalStateException( + "Failure kinds declare actions with no registered handler: " + gaps); + } + log.debug( + "Failure action registry initialised with {} handler(s) for {} kind(s)", + byId.size(), + FailureKind.values().length); + } + + public Optional find(FailureActionId id) { + return Optional.ofNullable(byId.get(id)); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureClassifier.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureClassifier.java new file mode 100644 index 0000000000..e3350225a5 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureClassifier.java @@ -0,0 +1,109 @@ +package stirling.software.proprietary.failure; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientResponseException; + +import jakarta.annotation.PostConstruct; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.ExceptionUtils; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +/** + * Maps a thrown failure onto a {@link FailureKind}. A tool's 4xx arrives as a {@link + * RestClientResponseException} whose body is the Problem Details document carrying {@code + * errorCode}, so this matches on codes rather than exception messages. + * + *

Anything unrecognised becomes {@link FailureKind#UNKNOWN}, so every failed run still gets a + * record. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FailureClassifier { + + /** Set by {@code GlobalExceptionHandler#createProblemDetailResponse}. */ + private static final String ERROR_CODE_PROPERTY = "errorCode"; + + /** + * Depth bound on the cause chain. The JDK forbids self-causation but not a longer cycle (A + * caused by B caused by A), which an unbounded walk would spin on. Real chains are a handful + * deep. + */ + private static final int MAX_CAUSE_DEPTH = 16; + + private final ObjectMapper objectMapper; + + /** + * Refuse to start on an ambiguous registry: two kinds claiming one code would make {@link + * #classify} depend on declaration order. Checked here because this is what resolves codes to + * kinds, and at boot so the message names the codes rather than arriving as a class-init error. + */ + @PostConstruct + void verifyNoErrorCodeIsClaimedTwice() { + List duplicates = FailureKind.duplicateErrorCodes(); + if (!duplicates.isEmpty()) { + throw new IllegalStateException( + "Error codes claimed by more than one failure kind: " + duplicates); + } + } + + /** Never null, never throws. A classifier that can fail would lose the failure it describes. */ + public FailureKind classify(Throwable throwable) { + Throwable current = throwable; + for (int depth = 0; current != null && depth < MAX_CAUSE_DEPTH; depth++) { + FailureKind resolved = classifyOne(current); + if (resolved != FailureKind.UNKNOWN) { + return resolved; + } + Throwable cause = current.getCause(); + current = cause == current ? null : cause; + } + return FailureKind.UNKNOWN; + } + + private FailureKind classifyOne(Throwable throwable) { + // A tool step's 4xx/5xx: the Problem Details body names the error code. + if (throwable instanceof RestClientResponseException responseException) { + String code = errorCodeFromBody(responseException); + if (code != null) { + return FailureKind.byErrorCode(code).orElse(FailureKind.UNKNOWN); + } + } + // Thrown in-process (not over loopback): the exception carries its own code. + if (throwable instanceof ExceptionUtils.ErrorCodeProvider provider) { + return FailureKind.byErrorCode(provider.getErrorCode()).orElse(FailureKind.UNKNOWN); + } + return FailureKind.UNKNOWN; + } + + /** + * Pull {@code errorCode} from a Problem Details body, or null when the body is absent, not + * JSON, or has no such property (an entitlement sentinel has {@code error} instead). + */ + private String errorCodeFromBody(RestClientResponseException exception) { + String body = exception.getResponseBodyAsString(); + if (body == null || body.isBlank()) { + return null; + } + try { + JsonNode root = objectMapper.readTree(body); + JsonNode code = root.get(ERROR_CODE_PROPERTY); + if (code == null || !code.isTextual()) { + return null; + } + String text = code.asString(); + return text.isBlank() ? null : text; + } catch (JacksonException e) { + log.debug("Downstream error body was not JSON; classifying as UNKNOWN"); + return null; + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKind.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKind.java new file mode 100644 index 0000000000..c9acd0d590 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKind.java @@ -0,0 +1,239 @@ +package stirling.software.proprietary.failure; + +import static stirling.software.proprietary.failure.FailureActionId.ACKNOWLEDGE; +import static stirling.software.proprietary.failure.FailureActionId.DISMISS; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import lombok.AccessLevel; +import lombok.Getter; + +/** + * The registry of failure kinds, described as data: a stable id, i18n keys and an English fallback + * like {@code ExceptionUtils.ErrorCode}, plus the facets a review surface needs. + * + *

Actions are declared here but implemented in {@link FailureAction} beans resolved by id, so a + * new kind ships as a registry entry plus copy. Two members today: {@link #UNKNOWN} gives every + * failed run a record, and kinds get promoted out of it as production shows what occurs. + * + *

A kind offers an acknowledgement only where there is something to acknowledge doing. + * With nothing to fix, "seen it" and "clear it" are the same decision, so the row offers only the + * one that clears it. + */ +@Getter +public enum FailureKind { + INPUT_PASSWORD_PROTECTED( + FailureStage.INPUT, + FailureSeverity.ERROR, + FailureRemedy.NEEDS_USER_INPUT, + FailureScope.FILE, + errorCodes("E004"), + fallback("This document is password-protected, so the pipeline could not read it."), + offer(ACKNOWLEDGE), + offer(DISMISS, "dismissSkipFile")), + + UNKNOWN( + FailureStage.INTERNAL, + FailureSeverity.ERROR, + FailureRemedy.PERMANENT, + FailureScope.RUN, + noErrorCodes(), + fallback("This run failed for a reason Stirling does not yet recognise."), + offer(DISMISS)); + + private static final String KEY_PREFIX = "portal.failures.kind."; + private static final String ACTION_KEY_PREFIX = "portal.failures.action."; + + /** + * Every claimed {@code ErrorCode}, to the kind claiming it. Indexed once rather than scanned + * per lookup, so a duplicate cannot be resolved by declaration order without anyone noticing. A + * duplicate is refused at boot; see {@link #duplicateErrorCodes()}. + */ + private static final Map BY_ERROR_CODE = indexErrorCodes(); + + private final FailureStage stage; + private final FailureSeverity severity; + private final FailureRemedy remedy; + private final FailureScope scope; + + /** English fallback, used when the client has no translation for {@link #getTitleKey()}. */ + private final String defaultTitle; + + /** Codes from {@code ExceptionUtils.ErrorCode} that map onto this kind; may be empty. */ + private final List errorCodes; + + /** + * What the review surface may offer, in display order. Never empty. No generated accessor, + * because {@link Offer} is private; read via {@link #getActions()} or {@link #labelKeyFor}. + */ + @Getter(AccessLevel.NONE) + private final List offers; + + FailureKind( + FailureStage stage, + FailureSeverity severity, + FailureRemedy remedy, + FailureScope scope, + List errorCodes, + String defaultTitle, + Offer... offers) { + this.stage = stage; + this.severity = severity; + this.remedy = remedy; + this.scope = scope; + this.errorCodes = List.copyOf(errorCodes); + this.defaultTitle = defaultTitle; + this.offers = List.of(offers); + } + + /** + * One action this kind offers, with the key to label it by. One ordered list rather than ids + * plus a parallel map of overrides, which could disagree with each other. + * + * @param labelKeySuffix key under {@code portal.failures.action.}, or null for the generic + * label + */ + private record Offer(FailureActionId id, String labelKeySuffix) {} + + /** An action labelled by this kind's own wording, where the generic label reads badly. */ + private static Offer offer(FailureActionId id, String labelKeySuffix) { + return new Offer(id, labelKeySuffix); + } + + /** An action labelled by the shared wording for that action. */ + private static Offer offer(FailureActionId id) { + return new Offer(id, null); + } + + /** + * The {@code ErrorCode}s this kind claims. Java has no named arguments, so these factories + * exist to label the two constructor arguments whose types do not already name themselves. + */ + private static List errorCodes(String... codes) { + return List.of(codes); + } + + /** Claims no {@code ErrorCode}: reached only through the classifier's fallback. */ + private static List noErrorCodes() { + return List.of(); + } + + /** The English text shown when the client has no translation for this kind. */ + private static String fallback(String englishTitle) { + return englishTitle; + } + + /** Stable wire id. Never renamed once shipped: persisted rows reference it. */ + public String getId() { + return name(); + } + + public String getTitleKey() { + return KEY_PREFIX + lowerCamelId() + ".title"; + } + + public String getDescriptionKey() { + return KEY_PREFIX + lowerCamelId() + ".description"; + } + + /** The actions this kind offers, in display order. */ + public List getActions() { + return offers.stream().map(Offer::id).toList(); + } + + /** Whether this kind offers {@code action}. The dispatch guard: see {@code FailureActionId}. */ + public boolean declares(FailureActionId action) { + return offers.stream().anyMatch(offer -> offer.id() == action); + } + + /** The label key for {@code action}: this kind's own wording, else the generic one. */ + public String labelKeyFor(FailureActionId action) { + return offers.stream() + .filter(offer -> offer.id() == action && offer.labelKeySuffix() != null) + .map(offer -> ACTION_KEY_PREFIX + offer.labelKeySuffix()) + .findFirst() + .orElseGet(() -> genericLabelKey(action)); + } + + /** The label key used when a kind supplies no wording of its own. */ + static String genericLabelKey(FailureActionId action) { + return ACTION_KEY_PREFIX + toLowerCamel(action.name()); + } + + /** + * Lookup by wire id. Empty rather than throwing: ids arrive from persisted rows and clients. + */ + public static Optional byId(String id) { + if (id == null || id.isBlank()) { + return Optional.empty(); + } + return Arrays.stream(values()).filter(kind -> kind.name().equals(id)).findFirst(); + } + + private static Map indexErrorCodes() { + Map index = new HashMap<>(); + for (FailureKind kind : values()) { + for (String code : kind.errorCodes) { + index.putIfAbsent(code, kind); + } + } + return Map.copyOf(index); + } + + /** + * Codes claimed by more than one kind, which would make classification depend on declaration + * order. Empty in a well-formed registry. + * + *

Reported for a caller to act on rather than thrown from class init, where it would arrive + * as an {@code ExceptionInInitializerError} blamed on whatever touched the enum first, then as + * {@code NoClassDefFoundError} everywhere after. {@link FailureClassifier} refuses to start. + */ + static List duplicateErrorCodes() { + return duplicatesIn(Arrays.stream(values()).flatMap(kind -> kind.errorCodes.stream())); + } + + /** + * The codes appearing more than once, first-seen order. Split out from the registry because the + * registry is a closed enum: this is the only seam at which the detection itself can be shown + * to find anything. + */ + static List duplicatesIn(Stream codes) { + Set seen = new HashSet<>(); + return codes.filter(code -> !seen.add(code)).distinct().toList(); + } + + /** The kind claiming {@code errorCode}, if any. Empty for a code no kind has adopted yet. */ + public static Optional byErrorCode(String errorCode) { + if (errorCode == null || errorCode.isBlank()) { + return Optional.empty(); + } + return Optional.ofNullable(BY_ERROR_CODE.get(errorCode)); + } + + private String lowerCamelId() { + return toLowerCamel(name()); + } + + /** + * {@code INPUT_PASSWORD_PROTECTED} to {@code inputPasswordProtected}, for i18n key building. + */ + private static String toLowerCamel(String screamingSnake) { + String[] parts = screamingSnake.toLowerCase(Locale.ROOT).split("_"); + StringBuilder out = new StringBuilder(parts[0]); + for (int i = 1; i < parts.length; i++) { + if (parts[i].isEmpty()) { + continue; + } + out.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1)); + } + return out.toString(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKindView.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKindView.java new file mode 100644 index 0000000000..b3e2145ba3 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureKindView.java @@ -0,0 +1,43 @@ +package stirling.software.proprietary.failure; + +import java.util.List; + +/** + * Wire shape of one registry entry, served so a client can describe kinds it was not built with. + * Also a feature probe: a build without the proprietary module has no such route, so a 404 means + * there is no failure tracking here. + */ +public record FailureKindView( + String id, + FailureStage stage, + FailureSeverity severity, + FailureRemedy remedy, + FailureScope scope, + List errorCodes, + String titleKey, + String descriptionKey, + String defaultTitle, + List actions) { + + public static FailureKindView of(FailureKind kind) { + return new FailureKindView( + kind.getId(), + kind.getStage(), + kind.getSeverity(), + kind.getRemedy(), + kind.getScope(), + kind.getErrorCodes(), + kind.getTitleKey(), + kind.getDescriptionKey(), + kind.getDefaultTitle(), + kind.getActions().stream() + .map( + action -> + new ActionDeclaration( + action.name(), kind.labelKeyFor(action))) + .toList()); + } + + /** An action this kind offers, with the label key to render it by. */ + public record ActionDeclaration(String id, String labelKey) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureOrigin.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureOrigin.java new file mode 100644 index 0000000000..78194b610e --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureOrigin.java @@ -0,0 +1,23 @@ +package stirling.software.proprietary.failure; + +/** + * What kind of run failed, not where it was started from. A policy is a policy whether a schedule + * fired it or someone clicked Run in the editor, so the two questions are answered separately: + * {@code actor} names the person for an attended run and {@code sourceId} names the folder, bucket + * or webhook for an unattended one. + */ +public enum FailureOrigin { + + /** One tool called directly, with no policy around it. Today that means the editor. */ + TOOL, + + /** The policy engine ran it, however the run was triggered. */ + POLICY, + + /** + * The watched-folder pipeline that predates policies. Declared ahead of its producer: nothing + * writes this yet, because that pipeline records no failures at all. Instrumenting it is its + * own piece of work. + */ + PIPELINE +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureRemedy.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureRemedy.java new file mode 100644 index 0000000000..966c727065 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureRemedy.java @@ -0,0 +1,14 @@ +package stirling.software.proprietary.failure; + +/** + * What intervention would clear this failure. Advisory metadata for the review surface; nothing + * branches on it server-side yet. + */ +public enum FailureRemedy { + TRANSIENT, + NEEDS_USER_INPUT, + NEEDS_FILE_FIX, + NEEDS_CONFIG_FIX, + NEEDS_SERVER_FIX, + PERMANENT +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureScope.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureScope.java new file mode 100644 index 0000000000..a6b882ce5d --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureScope.java @@ -0,0 +1,13 @@ +package stirling.software.proprietary.failure; + +/** + * What the failure is about, which is what the dedup key groups repeats by: one file, run, policy, + * source, or the whole server. + */ +public enum FailureScope { + FILE, + RUN, + POLICY, + SOURCE, + SERVER +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureSeverity.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureSeverity.java new file mode 100644 index 0000000000..f5d097a13d --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureSeverity.java @@ -0,0 +1,8 @@ +package stirling.software.proprietary.failure; + +/** How loudly a failure kind should be surfaced. */ +public enum FailureSeverity { + ERROR, + WARNING, + INFO +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureStage.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureStage.java new file mode 100644 index 0000000000..f1aca8f9ed --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FailureStage.java @@ -0,0 +1,17 @@ +package stirling.software.proprietary.failure; + +/** + * Where in a run's life the failure happened: reading the document ({@code INPUT}), a tool step or + * the engine ({@code INTERNAL}), delivery ({@code OUTPUT}), a gate refusing it ({@code BLOCKED}), + * or never admitted ({@code NEVER_RAN}). + * + *

All five declared up front so a later kind needs no enum change, which would strand the value + * already snapshotted on existing rows. + */ +public enum FailureStage { + INPUT, + INTERNAL, + OUTPUT, + BLOCKED, + NEVER_RAN +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEvent.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEvent.java new file mode 100644 index 0000000000..10f46cdbe2 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEvent.java @@ -0,0 +1,61 @@ +package stirling.software.proprietary.failure; + +import java.time.Instant; + +/** + * Read model for one recorded incident: the entity's fields with the registry kind already + * resolved. + * + *

The facets are the row's own snapshots and can differ from {@code kind}'s current facets on an + * old row, which is the point of snapshotting, so read them here rather than from {@code kind}. + */ +public record FileRunEvent( + String id, + Long teamId, + String actor, + FailureKind kind, + FailureStage stage, + FailureSeverity severity, + FailureScope scope, + FailureOrigin origin, + String policyId, + String runId, + String sourceId, + String fileId, + String detail, + String dedupKey, + int occurrences, + FileRunEventStatus status, + String statusActor, + Instant statusAt, + Instant createdAt, + Instant lastSeenAt) { + + /** + * Project an entity. An unrecognised {@code kindId} (written by a newer build, or since + * removed) falls back to {@link FailureKind#UNKNOWN}, so the row stays readable and actionable. + */ + public static FileRunEvent of(FileRunEventEntity entity) { + return new FileRunEvent( + entity.getId(), + entity.getTeamId(), + entity.getActor(), + FailureKind.byId(entity.getKindId()).orElse(FailureKind.UNKNOWN), + entity.getStage(), + entity.getSeverity(), + entity.getScope(), + entity.getOrigin(), + entity.getPolicyId(), + entity.getRunId(), + entity.getSourceId(), + entity.getFileId(), + entity.getDetail(), + entity.getDedupKey(), + entity.getOccurrences(), + entity.getStatus(), + entity.getStatusActor(), + entity.getStatusAt(), + entity.getCreatedAt(), + entity.getLastSeenAt()); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventController.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventController.java new file mode 100644 index 0000000000..aac1c3364b --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventController.java @@ -0,0 +1,188 @@ +package stirling.software.proprietary.failure; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Read and triage recorded failures. Note the absence of a team parameter: the team comes from the + * authenticated principal, never the request. + * + *

Every endpoint is open to any authenticated user and scoped in the service instead: a leader + * reads and closes the whole team's failures, everyone else their own. Nothing here decides who may + * do what, so the two cannot drift apart. + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/file-run-events") +@Hidden +@RequiredArgsConstructor +@Tag(name = "File run events", description = "Recorded policy and pipeline failures") +public class FileRunEventController { + + /** Cap on one page, so a client cannot ask for the whole table. */ + private static final int MAX_LIMIT = 200; + + private static final int DEFAULT_LIMIT = 50; + + private final FileRunEventService service; + + @GetMapping + @Operation( + summary = "List recorded failures", + description = + "Failures the caller may see, newest first: their team's for a leader, their own" + + " for everyone else. Each row carries its available actions already" + + " resolved.") + public FileRunEventsResponse list( + // Spring's converter 400s on a value outside the enum, so no hand-rolled parse. + @RequestParam(required = false) FileRunEventStatus status, + @RequestParam(required = false) String kindId, + @RequestParam(required = false) Integer limit) { + // No role gate: the service scopes the read instead, so a member gets their own failures + // and a leader the team's. + int cappedLimit = Math.min(limit == null ? DEFAULT_LIMIT : Math.max(1, limit), MAX_LIMIT); + + List events = + // The kind filter is part of the query, before the limit is applied: filtering an + // already-limited page could return nothing while matching rows exist. + service.list(status, kindId, cappedLimit).stream() + .map(event -> FileRunEventView.of(event, service.availableActions(event))) + .toList(); + return new FileRunEventsResponse(events); + } + + @PostMapping("/{eventId}/actions/{actionId}") + @Operation( + summary = "Apply an action to a recorded failure", + description = + "Rejected with 400 if the failure's kind does not declare the action, so an" + + " action that makes no sense for a given failure cannot be applied.") + public FileRunEventView act( + @PathVariable String eventId, + @PathVariable String actionId, + @RequestBody(required = false) ActionRequest request) { + // No role gate: the service decides, which lets someone close their own failure while + // still keeping a colleague's out of reach. + Map inputs = request == null ? Map.of() : request.safeInputs(); + try { + FileRunEvent updated = service.dispatch(eventId, actionId, inputs); + return FileRunEventView.of(updated, service.availableActions(updated)); + } catch (FailureActionException e) { + throw new ResponseStatusException(statusFor(e.getReason()), e.getMessage(), e); + } + } + + @PostMapping("/reports") + @Operation( + summary = "Report a failure hit in the editor", + description = + "For failures the server never sees, because the editor calls tools directly." + + " Open to any authenticated user: whoever's work failed can say so, and" + + " reads it back scoped to themselves. Rejected with 400 if it names" + + " more files than one report may carry.") + public ResponseEntity report(@RequestBody EditorFailureReport report) { + if (report == null || !report.hasOperation()) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "operation is required to report a failure"); + } + // Refused whole rather than trimmed, and refused before the first write, so an oversized + // report leaves no rows at all. Trimming would hand a reviewer part of a set with nothing + // saying the rest existed, which is what the cap inside the service used to do. The limit + // is stated in the message because the editor reports in the background: a client author + // reading a log is the only person who will ever see this. + if (report.namesTooManyFiles()) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "a report may name at most " + + EditorFailureReport.MAX_FILE_IDS + + " files, and this one named " + + report.fileIds().size()); + } + service.report(report); + // No body: the editor reports and moves on, and has nothing to do with the row. + return ResponseEntity.noContent().build(); + } + + @PostMapping("/removed-files") + @Operation( + summary = "Close the incidents about files deleted from the editor", + description = + "Deleting the document leaves nothing to act on, so its incidents drop out of" + + " the queue while the rows stay for audit. Applies only to the" + + " caller's own editor rows, however senior they are.") + public ResponseEntity filesRemoved(@RequestBody(required = false) RemovedFiles request) { + service.forgetFiles(request == null ? List.of() : request.safeFileIds()); + // No body: the editor is telling the server, not asking it anything. + return ResponseEntity.noContent().build(); + } + + @GetMapping("/kinds") + @Operation( + summary = "List known failure kinds", + description = + "The failure registry. Lets a client describe kinds it was not built with, and" + + " doubles as the probe for whether failure tracking exists at all.") + public List kinds() { + // The registry is copy and metadata, not anyone's data, and a member needs it to render the + // failures they can already see. + return Arrays.stream(FailureKind.values()).map(FailureKindView::of).toList(); + } + + /** + * A closed row is a conflict rather than a bad request: the request was well-formed and would + * have been valid a moment earlier. + */ + private static HttpStatus statusFor(FailureActionException.Reason reason) { + return switch (reason) { + case EVENT_NOT_FOUND -> HttpStatus.NOT_FOUND; + case ACTION_NOT_RECOGNISED, ACTION_NOT_DECLARED -> HttpStatus.BAD_REQUEST; + case ALREADY_CLOSED -> HttpStatus.CONFLICT; + }; + } + + /** Wrapped rather than a bare array so pagination can be added without breaking clients. */ + public record FileRunEventsResponse(List events) {} + + /** + * Files gone from the caller's editor. Opaque ids only, as everywhere else on this API. + * + *

Deliberately uncapped where a report is capped, because this creates nothing: it closes + * rows the caller already owns, so however long the list is, it can only ever touch incidents + * that already exist. Refusing an oversized one would also be the harmful direction here, since + * the editor says this once and never retries: those incidents would sit in the queue asking + * for attention about files that no longer exist. + */ + public record RemovedFiles(List fileIds) { + + List safeFileIds() { + return fileIds == null ? List.of() : fileIds; + } + } + + /** Inputs an action declared it needs. Empty for both actions that exist today. */ + public record ActionRequest(Map inputs) { + + Map safeInputs() { + return inputs == null ? Map.of() : inputs; + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventEntity.java new file mode 100644 index 0000000000..3cfb67a7fc --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventEntity.java @@ -0,0 +1,120 @@ +package stirling.software.proprietary.failure; + +import java.io.Serializable; +import java.time.Instant; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * One recorded failure incident. {@code actor} and {@code teamId} are plain values rather than + * foreign keys, matching {@link stirling.software.proprietary.policy.source.SourceEntity}, and the + * facets are snapshots at write time so re-classifying a kind never rewrites an old row. + * + *

Holds no document name or content: {@code fileId} is opaque, and {@code detail} is stripped of + * names by {@link RecordFailure}. + */ +@Entity +@Table( + name = "file_run_events", + indexes = { + // the review surface: this team's rows, newest first, optionally filtered by status + @Index(name = "idx_file_run_events_team", columnList = "team_id, status, last_seen_at") + }, + // One row per incident, enforced by the database rather than only by the store's read-then- + // insert: two runs failing identically at the same moment would otherwise both find no row + // and both insert, leaving a twin that reappears after the first is dismissed. Doubles as + // the + // rollup's lookup index. Note SQL treats NULLs as distinct, so this does not constrain the + // unteamed rows a login-disabled deployment writes. + uniqueConstraints = + @UniqueConstraint( + name = "uk_file_run_events_dedup", + columnNames = {"team_id", "dedup_key"})) +@NoArgsConstructor +@Getter +@Setter +public class FileRunEventEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "id") + private String id; + + @Column(name = "team_id") + private Long teamId; + + /** The user whose work produced the failure. Null when login is disabled. */ + @Column(name = "actor") + private String actor; + + @Column(name = "kind_id", nullable = false) + private String kindId; + + @Enumerated(EnumType.STRING) + @Column(name = "stage", nullable = false) + private FailureStage stage; + + @Enumerated(EnumType.STRING) + @Column(name = "severity", nullable = false) + private FailureSeverity severity; + + @Enumerated(EnumType.STRING) + @Column(name = "scope", nullable = false) + private FailureScope scope; + + @Enumerated(EnumType.STRING) + @Column(name = "origin", nullable = false) + private FailureOrigin origin; + + @Column(name = "policy_id") + private String policyId; + + @Column(name = "run_id") + private String runId; + + /** Which folder, bucket or webhook fed the run. Null when a user supplied the file. */ + @Column(name = "source_id") + private String sourceId; + + @Column(name = "file_id") + private String fileId; + + @Column(name = "detail", columnDefinition = "text") + private String detail; + + @Column(name = "dedup_key", length = 64, nullable = false) + private String dedupKey; + + /** How many times this same failure has been seen. Starts at 1; the rollup increments it. */ + @Column(name = "occurrences", nullable = false) + private int occurrences; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private FileRunEventStatus status; + + /** Who last changed {@link #status}. Null while the row is untouched. */ + @Column(name = "status_actor") + private String statusActor; + + @Column(name = "status_at") + private Instant statusAt; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + @Column(name = "last_seen_at", nullable = false) + private Instant lastSeenAt; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventRepository.java new file mode 100644 index 0000000000..1930b36e6a --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventRepository.java @@ -0,0 +1,138 @@ +package stirling.software.proprietary.failure; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface FileRunEventRepository extends JpaRepository { + + /** + * As {@link #findByTeamAndStatus} but for a set of statuses, e.g. the open ones. The kind + * filter is in the query, before the limit: filtering an already-limited page could return + * nothing while matching rows exist. + * + *

{@code actor} narrows to one person's own failures. Null means the whole team, which only + * a leader ever asks for: see {@code FileRunEventService#readScope}. + */ + @Query( + "select e from FileRunEventEntity e where ((:teamId is null and e.teamId is null) or" + + " e.teamId = :teamId) and e.status in :statuses" + + " and (:kindId is null or e.kindId = :kindId)" + + " and (:actor is null or e.actor = :actor) order by e.lastSeenAt desc") + List findByTeamAndStatusIn( + @Param("teamId") Long teamId, + @Param("statuses") List statuses, + @Param("kindId") String kindId, + @Param("actor") String actor, + Pageable pageable); + + /** As {@link #findByTeamAndStatusIn} but for exactly one status, for the surface's filters. */ + @Query( + "select e from FileRunEventEntity e where ((:teamId is null and e.teamId is null) or" + + " e.teamId = :teamId) and e.status = :status" + + " and (:kindId is null or e.kindId = :kindId)" + + " and (:actor is null or e.actor = :actor) order by e.lastSeenAt desc") + List findByTeamAndStatus( + @Param("teamId") Long teamId, + @Param("status") FileRunEventStatus status, + @Param("kindId") String kindId, + @Param("actor") String actor, + Pageable pageable); + + /** + * Fold a repeat into an incident in one statement, against the row's current values. A + * read-modify-save here would be a merge of a possibly stale snapshot: concurrent folds would + * lose counts, and an in-flight fold could overwrite a reviewer's dismiss. Same idiom as {@code + * SourceDocCountRepository#increment} and friends. + */ + @Modifying(clearAutomatically = true) + @Transactional + @Query( + "update FileRunEventEntity e set e.occurrences = e.occurrences + 1," + + " e.lastSeenAt = :now, e.detail = coalesce(:detail, e.detail)" + + " where e.id = :id") + int fold(@Param("id") String id, @Param("now") Instant now, @Param("detail") String detail); + + /** + * Reopen a resolved incident whose failure has recurred. Guarded on the current status so only + * {@code RESOLVED} flips; a concurrent dismiss is never overwritten back to {@code NEW}. + */ + @Modifying(clearAutomatically = true) + @Transactional + @Query( + "update FileRunEventEntity e set" + + " e.status = stirling.software.proprietary.failure.FileRunEventStatus.NEW," + + " e.statusActor = null, e.statusAt = null where e.id = :id and e.status =" + + " stirling.software.proprietary.failure.FileRunEventStatus.RESOLVED") + int reopenIfResolved(@Param("id") String id); + + /** + * Apply a status transition only if the row is still in one of {@code allowedFrom}. The guard + * runs in the database, so two racing closes cannot both succeed: the loser updates zero rows. + */ + @Modifying(clearAutomatically = true) + @Transactional + @Query( + "update FileRunEventEntity e set e.status = :target, e.statusActor = :actor," + + " e.statusAt = :now where e.id = :id and ((:teamId is null and e.teamId is" + + " null) or e.teamId = :teamId) and e.status in :allowedFrom") + int applyStatusIf( + @Param("id") String id, + @Param("teamId") Long teamId, + @Param("target") FileRunEventStatus target, + @Param("actor") String actor, + @Param("now") Instant now, + @Param("allowedFrom") Collection allowedFrom); + + /** + * Close the incidents about documents their owner deleted from the editor: the queue is what + * needs attention, and a document that no longer exists needs none. + * + *

Restricted to that owner's own editor rows. File ids are minted by the client, so scoping + * on team alone would let one caller close a colleague's incidents by naming ids. Processor + * rows are excluded outright: nothing was deleted from an editor there. + */ + @Modifying(clearAutomatically = true) + @Transactional + @Query( + "update FileRunEventEntity e set e.status =" + + " stirling.software.proprietary.failure.FileRunEventStatus.FILE_REMOVED," + + " e.statusActor = :actor, e.statusAt = :now where e.origin =" + + " stirling.software.proprietary.failure.FailureOrigin.TOOL and ((:teamId is" + + " null and e.teamId is null) or e.teamId = :teamId) and ((:actor is null and" + + " e.actor is null) or e.actor = :actor) and e.fileId in :fileIds and e.status in" + + " :allowedFrom") + int markFilesRemoved( + @Param("teamId") Long teamId, + @Param("actor") String actor, + @Param("fileIds") Collection fileIds, + @Param("now") Instant now, + @Param("allowedFrom") Collection allowedFrom); + + /** + * The most recent row for this exact failure, so the rollup can increment an existing incident + * instead of opening a new one. Team-scoped, so the same failure in two teams stays two rows. + */ + @Query( + "select e from FileRunEventEntity e where ((:teamId is null and e.teamId is null) or" + + " e.teamId = :teamId) and e.dedupKey = :dedupKey order by e.lastSeenAt desc") + List findByTeamAndDedupKey( + @Param("teamId") Long teamId, @Param("dedupKey") String dedupKey, Pageable pageable); + + /** One row by id, but only if it belongs to {@code teamId}. */ + @Query( + "select e from FileRunEventEntity e where e.id = :id and ((:teamId is null and e.teamId" + + " is null) or e.teamId = :teamId)") + Optional findByIdAndTeam( + @Param("id") String id, @Param("teamId") Long teamId); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventService.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventService.java new file mode 100644 index 0000000000..fa858b6e22 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventService.java @@ -0,0 +1,267 @@ +package stirling.software.proprietary.failure; + +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.UserServiceInterface; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; + +/** + * Reads and acts on the incidents the calling user is allowed to see, which is where that decision + * is made: a leader reads and closes the whole team's failures, everyone else their own. Keeping it + * here rather than on the endpoints means the read and the triage cannot drift apart. + * + *

Team scoping mirrors {@code PolicyAccessGuard}: everyone sees only their own team's rows, the + * team always comes from the authenticated principal, and scoping applies only when login is + * enabled so single-user deployments keep working. When the team cannot be resolved the caller + * reads nothing; see {@link #readScope()}. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileRunEventService { + + private final FileRunEventStore store; + private final FailureActionRegistry actionRegistry; + private final PolicyManagementAuthority policyManagementAuthority; + private final UserServiceInterface userService; + private final ApplicationProperties applicationProperties; + + /** + * Record a failure a user hit in the editor. One incident per named file, so each document + * stays separately actionable; one unattributed incident when the report names none. + * + *

The team and actor come from the session rather than the report, and the kind is + * classified from the reported code, falling back to {@link FailureKind#UNKNOWN} for a code no + * kind claims. + */ + public List report(EditorFailureReport report) { + FailureKind kind = FailureKind.byErrorCode(report.errorCode()).orElse(FailureKind.UNKNOWN); + // The caller's team, not their read scope: recording is open to everyone, and a reader who + // may see nothing still has their failure filed under the team it happened in. + Long teamId = currentTeamId(); + String actor = currentActor(); + String detail = detailFor(report); + + List fileIds = + report.fileIds().stream().filter(id -> id != null && !id.isBlank()).toList(); + if (fileIds.isEmpty()) { + return List.of(recordReported(kind, teamId, actor, null, detail)); + } + // Every named file gets its row. An earlier cap here silently dropped the rest, which lost + // failures a reviewer needed and was inconsistent with the processor path, where a sweep + // records one row per failing file with no limit at all. How many files one report may name + // is bounded at the boundary instead (see EditorFailureReport#MAX_FILE_IDS), where an + // oversized report can be refused whole before anything is written. + return fileIds.stream() + .map(fileId -> recordReported(kind, teamId, actor, fileId, detail)) + .toList(); + } + + private FileRunEvent recordReported( + FailureKind kind, Long teamId, String actor, String fileId, String detail) { + return store.record(RecordFailure.forEditor(kind, teamId, actor, fileId, detail)); + } + + /** + * The operation is the context a reviewer needs, since an editor failure has no policy or run. + */ + private String detailFor(EditorFailureReport report) { + String message = report.detail() == null ? "" : report.detail(); + return message.isBlank() ? report.operation() : report.operation() + ": " + message; + } + + /** + * Close the incidents about documents the caller has deleted from their editor. The queue means + * "needs attention", and a document that no longer exists needs none; the rows stay for audit. + * + *

Best-effort by nature: this only arrives if the browser that owns the file says so, and a + * cleared cache or another device never will. Rows left open that way are retention's problem, + * not this method's. + * + *

Narrowed to the caller's own rows however senior they are, which is why it passes {@link + * #currentActor()} rather than the read scope's actor: file ids are minted by each client, so a + * leader reading with a null actor would match every unattributed row in the team. + * + * @return how many incidents were closed + */ + public int forgetFiles(List fileIds) { + ReadScope scope = readScope(); + if (!scope.permitted()) { + return 0; + } + List named = fileIds.stream().filter(id -> id != null && !id.isBlank()).toList(); + return store.markFilesRemoved(scope.teamId(), currentActor(), named); + } + + /** + * The events the caller may read, newest first: the team's for a leader, their own for everyone + * else. Empty when their team cannot be resolved. + */ + public List list(FileRunEventStatus status, String kindId, int limit) { + ReadScope scope = readScope(); + if (!scope.permitted()) { + return List.of(); + } + return store.list(scope.teamId(), status, kindId, scope.actor(), limit); + } + + /** + * Dispatch an action against one event. + * + * @throws FailureActionException if the event is not the caller's, the action is unknown, the + * event's kind does not declare the action, or the event is already closed + */ + public FileRunEvent dispatch(String eventId, String actionId, Map inputs) { + // Whoever can see it can close it: a leader for the whole team, everyone else for the + // failures they caused. Someone who fixes their own problem should not have to ask a leader + // to clear the row. + // + // Closing the row is all this covers. Acting on the document behind it, such as supplying a + // password for a retry, would need its own permission, and no such action exists yet. + ReadScope scope = readScope(); + if (!scope.permitted()) { + // Reported as "no such event", the same as an id from another team, so the response + // does + // not depend on whether the id happens to exist. + throw new FailureActionException( + FailureActionException.Reason.EVENT_NOT_FOUND, "No such event: " + eventId); + } + FileRunEvent event = + store.find(eventId, scope.teamId()) + // Reported as "no such event" rather than a refusal, so a member cannot + // learn that a colleague's incident exists by trying to close it. + .filter( + found -> + scope.actor() == null + || scope.actor().equals(found.actor())) + .orElseThrow( + () -> + new FailureActionException( + FailureActionException.Reason.EVENT_NOT_FOUND, + "No such event: " + eventId)); + + FailureActionId resolvedId = parseActionId(actionId); + + // A kind that does not offer an action cannot have it applied, so an action that makes no + // sense for a failure is unreachable rather than merely unrendered. + if (!event.kind().declares(resolvedId)) { + throw new FailureActionException( + FailureActionException.Reason.ACTION_NOT_DECLARED, + "Kind " + event.kind().getId() + " does not offer action " + resolvedId); + } + if (event.status().terminal()) { + throw new FailureActionException( + FailureActionException.Reason.ALREADY_CLOSED, + "Event " + eventId + " is already " + event.status()); + } + + FailureAction action = + actionRegistry + .find(resolvedId) + .orElseThrow( + () -> + new FailureActionException( + FailureActionException.Reason.ACTION_NOT_RECOGNISED, + "No handler for action " + resolvedId)); + + return action.execute(event, inputs == null ? Map.of() : inputs, currentActor()); + } + + /** + * Which of an event's declared actions are usable right now. Decided per row, so the client + * never renders a button that would be refused. + */ + public List availableActions(FileRunEvent event) { + boolean closed = event.status().terminal(); + return event.kind().getActions().stream() + .map( + action -> + new AvailableAction( + action, + event.kind().labelKeyFor(action), + !closed, + closed ? "portal.failures.disabled.closed" : null)) + .toList(); + } + + private FailureActionId parseActionId(String actionId) { + for (FailureActionId candidate : FailureActionId.values()) { + if (candidate.name().equals(actionId)) { + return candidate; + } + } + throw new FailureActionException( + FailureActionException.Reason.ACTION_NOT_RECOGNISED, "Unknown action: " + actionId); + } + + /** + * Which rows the caller may read. A leader reviews the whole team's, as before. Everyone else + * reads the failures they caused themselves: a member can already report one, so letting them + * see their own back is what makes telling them about it worth anything, and it exposes nothing + * of a colleague's. + * + *

A null team id means two different things. Login disabled is the self-hosted setup with no + * users or teams, where unteamed rows are everyone's, as {@code PolicyAccessGuard} also treats + * them. Login enabled with no resolvable team reads nothing, because unteamed rows there are + * shared by every team's ad-hoc runs. + */ + private ReadScope readScope() { + if (!enforced()) { + return ReadScope.wholeTeam(null); + } + Long teamId = currentTeamId(); + if (teamId == null) { + return ReadScope.denied(); + } + if (policyManagementAuthority.canEditPolicies()) { + return ReadScope.wholeTeam(teamId); + } + // Narrowing to "mine" needs a name to narrow by. Without one the filter would be dropped + // and a member would read the whole team, so refuse rather than widen. + String actor = currentActor(); + return actor == null ? ReadScope.denied() : ReadScope.mine(teamId, actor); + } + + /** + * What the caller may read. {@code actor} is the person to narrow to, or null for the whole + * team; both are only meaningful when permitted. + */ + private record ReadScope(boolean permitted, Long teamId, String actor) { + + static ReadScope wholeTeam(Long teamId) { + return new ReadScope(true, teamId, null); + } + + static ReadScope mine(Long teamId, String actor) { + return new ReadScope(true, teamId, actor); + } + + static ReadScope denied() { + return new ReadScope(false, null, null); + } + } + + /** The team a row belongs to, which is nobody's when there are no teams to belong to. */ + private Long currentTeamId() { + return enforced() ? policyManagementAuthority.currentUserTeamId() : null; + } + + private String currentActor() { + return enforced() ? userService.getCurrentUsername() : null; + } + + private boolean enforced() { + return applicationProperties.getSecurity().isEnableLogin(); + } + + /** One action as offered for a specific event, with its resolved availability. */ + public record AvailableAction( + FailureActionId id, String labelKey, boolean enabled, String disabledReasonKey) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStatus.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStatus.java new file mode 100644 index 0000000000..9bf0e0b607 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStatus.java @@ -0,0 +1,41 @@ +package stirling.software.proprietary.failure; + +import java.util.Arrays; +import java.util.List; + +/** + * Disposition of one recorded failure. {@code RESOLVED} is declared but not set yet (it becomes + * system-set later); the rollup already defines what a repeat means for it, which is to reopen. + */ +public enum FileRunEventStatus { + NEW(false), + ACKNOWLEDGED(false), + DISMISSED(true), + RESOLVED(true), + + /** + * The document this incident was about was deleted from its owner's editor, so there is nothing + * left to act on. Distinct from {@code DISMISSED}, which is a reviewer's decision, and from + * {@code RESOLVED}, which reopens on recurrence: this one cannot recur, the file is gone. + */ + FILE_REMOVED(true); + + /** The statuses a review queue shows by default: everything still needing a decision. */ + private static final List OPEN = + Arrays.stream(values()).filter(status -> !status.terminal).toList(); + + /** Whether no further transition is possible: the row is closed. */ + private final boolean terminal; + + FileRunEventStatus(boolean terminal) { + this.terminal = terminal; + } + + public boolean terminal() { + return terminal; + } + + public static List open() { + return OPEN; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStore.java new file mode 100644 index 0000000000..2ed86e61b2 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventStore.java @@ -0,0 +1,227 @@ +package stirling.software.proprietary.failure; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Durable store for recorded incidents. Who may act lives in {@link FileRunEventService}. + * + *

{@link #record} folds a repeat into the existing incident rather than inserting again, keyed + * on {@code (teamId, dedupKey)}. That matters for a stateless {@code snapshot} source, which + * re-lists every file on each sweep: the same broken file is one incident, not one per sweep. + * Distinct files keep distinct rows, so a reviewer can still act on any one of them. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FileRunEventStore { + + /** Only ever need the newest match; the index is ordered so this is a single-row read. */ + private static final Pageable NEWEST = PageRequest.of(0, 1); + + private final FileRunEventRepository repository; + + /** + * Record a failure, folding it into an existing incident when one matches. + * + *

Deliberately not {@code @Transactional}: each repository call runs in its own transaction, + * so losing the insert race below leaves no rolled-back transaction to recover from. + */ + public FileRunEvent record(RecordFailure command) { + String dedupKey = command.dedupKey(); + Instant now = Instant.now(); + + Optional folded = + findByDedupKey(command.teamId(), dedupKey) + .flatMap(row -> absorb(row.getId(), command, now)); + if (folded.isPresent()) { + return folded.get(); + } + + try { + return FileRunEvent.of(insert(command, dedupKey, now)); + } catch (DataIntegrityViolationException e) { + // The unique constraint fired, so a concurrent writer inserted this incident between + // our read and our insert. Their row is the incident; fold into it instead of failing. + return findByDedupKey(command.teamId(), dedupKey) + .flatMap(row -> absorb(row.getId(), command, now)) + .orElseThrow(() -> e); + } + } + + private Optional findByDedupKey(Long teamId, String dedupKey) { + return repository.findByTeamAndDedupKey(teamId, dedupKey, NEWEST).stream().findFirst(); + } + + /** + * Fold a repeat into an existing incident. {@code DISMISSED} stays dismissed, so a reviewer's + * "stop showing me this" holds; {@code RESOLVED} reopens, so a recurrence is visible again. + * + *

Both steps are single guarded UPDATE statements against the row's current values, never a + * save of the entity we read: that would merge a possibly stale snapshot over concurrent + * writes, losing counts and reverting a dismiss that landed in between. + * + *

Empty when the row went away between the caller's read and the fold, so the caller inserts + * instead. Reporting that as an error would lose the incident for a log line. + */ + private Optional absorb(String id, RecordFailure command, Instant now) { + if (repository.fold(id, now, command.detail()) == 0) { + return Optional.empty(); + } + repository.reopenIfResolved(id); + return repository.findById(id).map(FileRunEvent::of); + } + + private FileRunEventEntity insert(RecordFailure command, String dedupKey, Instant now) { + FailureKind kind = command.kind(); + FileRunEventEntity entity = new FileRunEventEntity(); + entity.setId(UUID.randomUUID().toString()); + entity.setTeamId(command.teamId()); + entity.setActor(command.actor()); + entity.setKindId(kind.getId()); + // Snapshot the facets so a later registry edit does not rewrite what this row meant. + entity.setStage(kind.getStage()); + entity.setSeverity(kind.getSeverity()); + entity.setScope(kind.getScope()); + entity.setOrigin(command.origin()); + entity.setPolicyId(command.policyId()); + entity.setRunId(command.runId()); + entity.setSourceId(command.sourceId()); + entity.setFileId(command.fileId()); + entity.setDetail(command.detail()); + entity.setDedupKey(dedupKey); + entity.setOccurrences(1); + entity.setStatus(FileRunEventStatus.NEW); + entity.setCreatedAt(now); + entity.setLastSeenAt(now); + // Flushed so a duplicate-key violation surfaces here, inside record()'s catch. A plain + // save() defers the INSERT to commit time once a caller is transactional, and the + // violation would escape the catch as a 500. + return repository.saveAndFlush(entity); + } + + /** + * A page of incidents, newest first, optionally narrowed to one status and one kind. + * + *

With no status asked for this is the open queue rather than every row ever + * recorded: a dismissed failure has been dealt with, and leaving it in the default view means + * the list can never be cleared. Ask for a status to see closed rows. + * + *

Both filters live in the query, before the limit: filtering an already-limited page could + * return nothing while matching rows exist. + * + *

{@code actor} narrows to one person's own failures, or reads the whole team when null. Who + * gets which is the service's decision, not this method's. + */ + @Transactional(readOnly = true) + public List list( + Long teamId, FileRunEventStatus status, String kindId, String actor, int limit) { + Pageable page = PageRequest.of(0, Math.max(1, limit)); + List rows = + status == null + ? repository.findByTeamAndStatusIn( + teamId, FileRunEventStatus.open(), kindId, actor, page) + : repository.findByTeamAndStatus(teamId, status, kindId, actor, page); + return rows.stream().map(FileRunEvent::of).toList(); + } + + @Transactional(readOnly = true) + public Optional find(String id, Long teamId) { + return repository.findByIdAndTeam(id, teamId).map(FileRunEvent::of); + } + + /** + * Transition an open row, refusing a closed one. The service checks {@code terminal()} before + * dispatching, but that check and this write are separate requests under concurrency; the + * guarded UPDATE is what makes two racing closes resolve to one winner. + * + * @throws FailureActionException {@code ALREADY_CLOSED} when the row exists but is terminal, + * {@code EVENT_NOT_FOUND} when it does not exist for this team + */ + @Transactional + public FileRunEvent applyStatus( + String id, Long teamId, FileRunEventStatus target, String actor) { + return applyStatus(id, teamId, target, actor, FileRunEventStatus.open()) + .orElseThrow(() -> refusalFor(id, teamId)); + } + + /** + * As {@link #applyStatus(String, Long, FileRunEventStatus, String)} but with the caller naming + * which current statuses may transition. Empty when the row exists outside {@code allowedFrom}. + * + *

The UPDATE runs first and nothing is read beforehand: a pre-read only to classify the + * refusal would be one more thing to race with, and would report a row deleted in between as + * closed rather than missing. + */ + @Transactional + public Optional applyStatus( + String id, + Long teamId, + FileRunEventStatus target, + String actor, + Collection allowedFrom) { + if (repository.applyStatusIf(id, teamId, target, actor, Instant.now(), allowedFrom) == 0) { + return Optional.empty(); + } + return repository.findByIdAndTeam(id, teamId).map(FileRunEvent::of); + } + + /** + * Transition, or accept that someone else already reached {@code target}. The loser of a race + * reads the winner's row back rather than re-stamping it, so the first actor keeps the credit. + */ + @Transactional + public FileRunEvent applyStatusOnce( + String id, + Long teamId, + FileRunEventStatus target, + String actor, + Collection allowedFrom) { + return applyStatus(id, teamId, target, actor, allowedFrom) + .or(() -> find(id, teamId).filter(current -> current.status() == target)) + .orElseThrow(() -> refusalFor(id, teamId)); + } + + /** + * Close this owner's open incidents about {@code fileIds}, because the documents are gone. The + * rows stay for audit; they just leave the queue. Only open rows move, so a reviewer's dismiss + * keeps its meaning and its actor. + * + * @return how many incidents were closed + */ + @Transactional + public int markFilesRemoved(Long teamId, String actor, Collection fileIds) { + if (fileIds.isEmpty()) { + return 0; + } + return repository.markFilesRemoved( + teamId, actor, fileIds, Instant.now(), FileRunEventStatus.open()); + } + + /** + * Why the guarded UPDATE refused, worked out only once it has. Missing and closed are told + * apart after the fact rather than before, so the answer describes the row the UPDATE saw. + */ + private FailureActionException refusalFor(String id, Long teamId) { + return repository.findByIdAndTeam(id, teamId).isPresent() + ? new FailureActionException( + FailureActionException.Reason.ALREADY_CLOSED, + "Event " + id + " is already closed") + // The same reason the service raises for an id it never found, so losing a delete + // race answers 404 like every other "no such event" rather than 400. + : new FailureActionException( + FailureActionException.Reason.EVENT_NOT_FOUND, "No such event: " + id); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventView.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventView.java new file mode 100644 index 0000000000..3809fad2c6 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/FileRunEventView.java @@ -0,0 +1,75 @@ +package stirling.software.proprietary.failure; + +import java.util.List; + +/** + * Wire shape of one incident. Carries i18n keys plus {@code defaultTitle} rather than rendered + * copy, so the server can ship a new kind without a client release, and {@code actions} arrive + * already resolved so the client needs no rules. No document name: {@code fileId} is an opaque + * reference. + */ +public record FileRunEventView( + String id, + String kindId, + FailureStage stage, + FailureSeverity severity, + FailureScope scope, + FailureOrigin origin, + FailureRemedy remedy, + String titleKey, + String descriptionKey, + String defaultTitle, + String detail, + String policyId, + String runId, + String sourceId, + String fileId, + String actor, + int occurrences, + FileRunEventStatus status, + String statusActor, + List actions, + long createdAt, + long lastSeenAt) { + + public static FileRunEventView of( + FileRunEvent event, List actions) { + FailureKind kind = event.kind(); + return new FileRunEventView( + event.id(), + kind.getId(), + event.stage(), + event.severity(), + event.scope(), + event.origin(), + kind.getRemedy(), + kind.getTitleKey(), + kind.getDescriptionKey(), + kind.getDefaultTitle(), + event.detail(), + event.policyId(), + event.runId(), + event.sourceId(), + event.fileId(), + event.actor(), + event.occurrences(), + event.status(), + event.statusActor(), + actions.stream().map(ActionView::of).toList(), + event.createdAt() == null ? 0L : event.createdAt().toEpochMilli(), + event.lastSeenAt() == null ? 0L : event.lastSeenAt().toEpochMilli()); + } + + /** One button, as offered for this specific row. */ + public record ActionView( + String id, String labelKey, boolean enabled, String disabledReasonKey) { + + static ActionView of(FileRunEventService.AvailableAction action) { + return new ActionView( + action.id().name(), + action.labelKey(), + action.enabled(), + action.disabledReasonKey()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/PolicyFailureRecorder.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/PolicyFailureRecorder.java new file mode 100644 index 0000000000..eaa8e0f5bd --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/PolicyFailureRecorder.java @@ -0,0 +1,94 @@ +package stirling.software.proprietary.failure; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * The seam the policy engine calls when a run fails: classify, then record. Best-effort throughout, + * so failing to write a row cannot change the failure the caller already observed. + * + *

The team comes from the originating policy rather than the calling thread, which carries only + * an audit principal. An ad-hoc run has no stored policy, so its rows land unteamed. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PolicyFailureRecorder { + + private final FailureClassifier classifier; + private final FileRunEventStore store; + private final PolicyStore policyStore; + + /** + * Record a failed run, classifying {@code cause}. {@code detail} is the message the run itself + * reported, kept verbatim so an {@link FailureKind#UNKNOWN} row is still diagnosable. + */ + public void recordRunFailure( + String runId, + String policyId, + String sourceId, + String fileIdentity, + String actor, + String detail, + Throwable cause) { + record(classifier.classify(cause), runId, policyId, sourceId, fileIdentity, actor, detail); + } + + /** + * Record a failure whose kind is already decided, for paths with no exception to classify (a + * run rejected at admission). Named distinctly rather than overloading {@link + * #recordRunFailure}, whose argument list is otherwise near-identical, so a null cause cannot + * pick the wrong one. + */ + public void recordRunFailureAs( + FailureKind kind, + String runId, + String policyId, + String sourceId, + String actor, + String detail) { + // No document reference: a run rejected at admission never got as far as one. + record(kind, runId, policyId, sourceId, null, actor, detail); + } + + private void record( + FailureKind kind, + String runId, + String policyId, + String sourceId, + String fileIdentity, + String actor, + String detail) { + try { + store.record( + RecordFailure.forRun( + kind, + teamFor(policyId), + actor, + policyId, + runId, + sourceId, + fileIdentity, + detail)); + } catch (RuntimeException e) { + // Deliberately swallowed: see the class comment. + log.warn("Could not record failure event for run {} (kind {})", runId, kind.getId(), e); + } + } + + private Long teamFor(String policyId) { + if (policyId == null || policyId.isBlank()) { + return null; + } + try { + return policyStore.get(policyId).map(policy -> policy.teamId()).orElse(null); + } catch (RuntimeException e) { + log.debug("Could not resolve team for policy {}: {}", policyId, e.getMessage()); + return null; + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/failure/RecordFailure.java b/app/proprietary/src/main/java/stirling/software/proprietary/failure/RecordFailure.java new file mode 100644 index 0000000000..8448f26fc3 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/failure/RecordFailure.java @@ -0,0 +1,147 @@ +package stirling.software.proprietary.failure; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +/** + * Everything needed to record one failure. Every reference field is nullable, because a failure can + * happen before a file or policy is known and with no user at all (a trigger-fired run on a + * login-disabled deployment). Only {@code kind} and {@code origin} are required. + */ +public record RecordFailure( + FailureKind kind, + FailureOrigin origin, + Long teamId, + String actor, + String policyId, + String runId, + String sourceId, + String fileId, + String detail) { + + /** Upper bound on a stored message, so one enormous stack trace cannot fill the column. */ + private static final int MAX_DETAIL_LENGTH = 2_000; + + public RecordFailure { + if (kind == null) { + throw new IllegalArgumentException("kind is required"); + } + if (origin == null) { + throw new IllegalArgumentException("origin is required"); + } + // Capped here rather than at each call site, since this record is the only way a row is + // written, and an unclassified failure carries a raw message of unbounded length. Stored + // verbatim otherwise: it is the user's own error about their own file, and hiding parts of + // it makes the row harder to act on without making it meaningfully safer. + detail = truncate(detail); + } + + /** + * A processor-side run failure. {@code sourceId} says which folder, bucket or webhook fed the + * run, and is the only attribution an unattended failure has: there is no user to name. {@code + * fileId} is the source's opaque reference to the document, already hashed upstream. + */ + public static RecordFailure forRun( + FailureKind kind, + Long teamId, + String actor, + String policyId, + String runId, + String sourceId, + String fileId, + String detail) { + return new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + actor, + policyId, + runId, + sourceId, + fileId, + detail); + } + + /** + * A failure a user hit in their own editor. There is no policy, run or source: the user is the + * attribution, and {@code fileId} may be null when the report named no file. + */ + public static RecordFailure forEditor( + FailureKind kind, Long teamId, String actor, String fileId, String detail) { + return new RecordFailure( + kind, FailureOrigin.TOOL, teamId, actor, null, null, null, fileId, detail); + } + + /** + * What this failure is about, per the row's scope. Two failures sharing a kind and a scope + * reference are the same incident; see {@link #dedupKey()}. + */ + public String scopeRef() { + String about = + switch (kind.getScope()) { + case FILE -> nullToEmpty(policyId) + "|" + fileOrRun(); + // An editor report has no run, so a RUN-scoped kind would otherwise put every + // such failure in a team into one incident: fall back to the document. + case RUN -> isBlank(runId) ? fileOrRun() : nullToEmpty(runId); + case POLICY -> nullToEmpty(policyId); + case SOURCE -> nullToEmpty(sourceId); + // One server-wide condition is one incident regardless of which run hit it. + case SERVER -> ""; + }; + // An editor failure belongs to the person who hit it, so two colleagues hitting the same + // thing are two incidents. Folding them would credit one actor for both and offer the + // wrong person the row. Unattended runs have no such owner and are unaffected. + return origin == FailureOrigin.TOOL ? nullToEmpty(actor) + "|" + about : about; + } + + /** + * The file this failure is about, or the run when the producer could not name one. Without the + * fallback, every file failing the same way under one policy shares a scope reference, and the + * second onwards folds into the first as though one document had failed repeatedly. + * + *

Prefixed so a file id and a run id cannot collide. + */ + private String fileOrRun() { + return isBlank(fileId) ? "run:" + nullToEmpty(runId) : "file:" + fileId; + } + + /** + * Decides whether this failure repeats an existing incident: SHA-256 of {@code + * kindId|scope|scopeRef}. Hashed so a long scope reference (an S3 key) fits a fixed-width + * index, and deterministic so two nodes converge on one incident. Every input is an enum name + * or an id. + */ + public String dedupKey() { + String raw = kind.getId() + "|" + kind.getScope().name() + "|" + scopeRef(); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(digest.digest(raw.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is mandated by the JDK; unreachable outside a broken runtime. + throw new IllegalStateException("SHA-256 unavailable", e); + } + } + + private static String truncate(String detail) { + if (detail == null || detail.length() <= MAX_DETAIL_LENGTH) { + return detail; + } + // Leave room for the ellipsis so the cap is the cap, and step back once more rather than + // cutting between the halves of a surrogate pair, which would store invalid UTF-16. + int end = MAX_DETAIL_LENGTH - 1; + if (Character.isHighSurrogate(detail.charAt(end - 1))) { + end--; + } + return detail.substring(0, end) + "…"; + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/integration/api/ExternalApiCallController.java b/app/proprietary/src/main/java/stirling/software/proprietary/integration/api/ExternalApiCallController.java index 499ab90fdb..bb32d85092 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/integration/api/ExternalApiCallController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/integration/api/ExternalApiCallController.java @@ -27,6 +27,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.AutomationRunContext; import stirling.software.common.service.InternalApiClient; import stirling.software.common.util.TempFileManager; @@ -89,7 +91,10 @@ public class ExternalApiCallController { private final TempFileManager tempFileManager; private final ApplicationProperties applicationProperties; + // The document is forwarded as bytes and never parsed, and in 'replace' mode the response + // becomes the document, so neither side can be pinned to a format. @PostMapping(value = "/external-api-call", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ToolIO(accepts = ToolFormat.ANY, produces = ToolFormat.ANY) @Operation( summary = "Send the document to an external API", description = @@ -97,7 +102,7 @@ public class ExternalApiCallController { + " either records the response as a step report or replaces the" + " document with it. Fields, path and headers may reference" + " {{document.*}}, {{classification.*}}, {{sensitivityLabel.*}} and" - + " {{run.*}}. Type:SISO") + + " {{run.*}}.") public ResponseEntity call( @RequestParam("fileInput") MultipartFile fileInput, @RequestParam("connectionId") String connectionId, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/integration/crypto/CredentialEncryption.java b/app/proprietary/src/main/java/stirling/software/proprietary/integration/crypto/CredentialEncryption.java index 8ac0ddda96..5e507ff285 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/integration/crypto/CredentialEncryption.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/integration/crypto/CredentialEncryption.java @@ -92,7 +92,8 @@ public class CredentialEncryption { writeOwnerOnly(path, Base64.getEncoder().encodeToString(generated.getEncoded())); log.warn( "Generated a new credential encryption key at {}. Back this file up: losing it" - + " makes stored integration secrets unrecoverable.", + + " makes stored integration secrets and pipeline supporting files" + + " unrecoverable.", path); return generated; } catch (Exception e) { @@ -119,7 +120,8 @@ public class CredentialEncryption { } } - public static String encrypt(String plaintext) { + /** Raw {@code IV || ciphertext}, for binary columns that can't afford Base64's 33% overhead. */ + public static byte[] encryptBytes(byte[] plaintext) { if (plaintext == null) { return null; } @@ -128,30 +130,46 @@ public class CredentialEncryption { RANDOM.nextBytes(iv); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, requireKey(), new GCMParameterSpec(GCM_TAG_BITS, iv)); - byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + byte[] ciphertext = cipher.doFinal(plaintext); byte[] combined = new byte[iv.length + ciphertext.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); - return Base64.getEncoder().encodeToString(combined); + return combined; } catch (GeneralSecurityException e) { throw new IllegalStateException("Failed to encrypt credential", e); } } + /** Inverse of {@link #encryptBytes}; throws if the blob was tampered with (GCM tag). */ + public static byte[] decryptBytes(byte[] stored) { + if (stored == null) { + return null; + } + try { + byte[] iv = Arrays.copyOfRange(stored, 0, IV_BYTES); + byte[] ciphertext = Arrays.copyOfRange(stored, IV_BYTES, stored.length); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, requireKey(), new GCMParameterSpec(GCM_TAG_BITS, iv)); + return cipher.doFinal(ciphertext); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Failed to decrypt credential", e); + } + } + + public static String encrypt(String plaintext) { + if (plaintext == null) { + return null; + } + return Base64.getEncoder() + .encodeToString(encryptBytes(plaintext.getBytes(StandardCharsets.UTF_8))); + } + public static String decrypt(String stored) { if (stored == null) { return null; } - try { - byte[] combined = Base64.getDecoder().decode(stored); - byte[] iv = Arrays.copyOfRange(combined, 0, IV_BYTES); - byte[] ciphertext = Arrays.copyOfRange(combined, IV_BYTES, combined.length); - Cipher cipher = Cipher.getInstance(TRANSFORMATION); - cipher.init(Cipher.DECRYPT_MODE, requireKey(), new GCMParameterSpec(GCM_TAG_BITS, iv)); - return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); - } catch (GeneralSecurityException e) { - throw new IllegalStateException("Failed to decrypt credential", e); - } + // Base64's IllegalArgumentException stays uncaught: LegacyDecryptStringConverter needs it. + return new String(decryptBytes(Base64.getDecoder().decode(stored)), StandardCharsets.UTF_8); } private static SecretKey requireKey() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/integration/model/IntegrationType.java b/app/proprietary/src/main/java/stirling/software/proprietary/integration/model/IntegrationType.java index 375b7f4604..e372336763 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/integration/model/IntegrationType.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/integration/model/IntegrationType.java @@ -3,6 +3,8 @@ package stirling.software.proprietary.integration.model; /** Kind of external integration a stored config describes. */ public enum IntegrationType { S3, + /** A network file server (SFTP, FTP/FTPS, or SMB/Samba) a policy source polls for documents. */ + NETWORK, MCP, /** A generic outbound HTTP endpoint a pipeline step can post a document to. */ API, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/integration/purview/PurviewLabelController.java b/app/proprietary/src/main/java/stirling/software/proprietary/integration/purview/PurviewLabelController.java index 65555077c3..666e7e5620 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/integration/purview/PurviewLabelController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/integration/purview/PurviewLabelController.java @@ -24,6 +24,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIO; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -59,13 +61,13 @@ public class PurviewLabelController { private final ObjectMapper objectMapper; @PostMapping(value = "/purview-apply-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Apply a Microsoft Purview sensitivity label", description = "Writes the Purview label metadata (MSIP_Label__*) onto the PDF, so" + " Purview-aware tools recognise the label. Applies the label only;" - + " it cannot encrypt, which requires the Microsoft client." - + " Input:PDF Output:PDF Type:SISO") + + " it cannot encrypt, which requires the Microsoft client.") public ResponseEntity applyLabel( @RequestParam("fileInput") MultipartFile fileInput, @RequestParam("connectionId") String connectionId, @@ -95,12 +97,12 @@ public class PurviewLabelController { } @PostMapping(value = "/purview-read-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ToolIO(produces = ToolFormat.PDF) @Operation( summary = "Read the Microsoft Purview sensitivity label on a PDF", description = "Reports the Purview labels a PDF already carries so a policy can act on" - + " them. The document passes through unchanged." - + " Input:PDF Output:PDF Type:SISO") + + " them. The document passes through unchanged.") public ResponseEntity readLabel( @RequestParam("fileInput") MultipartFile fileInput, @RequestParam("connectionId") String connectionId) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/integration/service/IntegrationConfigService.java b/app/proprietary/src/main/java/stirling/software/proprietary/integration/service/IntegrationConfigService.java index a6650569a8..a29ac2c08f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/integration/service/IntegrationConfigService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/integration/service/IntegrationConfigService.java @@ -57,12 +57,15 @@ public class IntegrationConfigService { OwnerScope scope = request.scope() == null ? OwnerScope.USER : request.scope(); IntegrationConfig cfg = new IntegrationConfig(); cfg.setIntegrationType(require(request.integrationType(), "integrationType")); - // S3 is infrastructure, not self-serve: no personal S3 for regular users. TEAM/SERVER - // scopes are already restricted to admins/team owners by assignOwnership. - if (cfg.getIntegrationType() == IntegrationType.S3 + // S3 and network file servers are infrastructure, not self-serve: they hold shared host + // credentials, so no personal connection for regular users. TEAM/SERVER scopes are already + // restricted to admins/team owners by assignOwnership. + if (isInfrastructureType(cfg.getIntegrationType()) && scope == OwnerScope.USER && !ownership.isAdmin(currentUser)) { - throw forbidden("S3 connections can only be created by administrators or team owners"); + throw forbidden( + "S3 and network connections can only be created by administrators or team" + + " owners"); } requireCustomApiAllowed(cfg.getIntegrationType(), currentUser); cfg.setName(require(request.name(), "name")); @@ -147,6 +150,11 @@ public class IntegrationConfigService { } } + /** Types holding shared host credentials, restricted to admins/team owners like S3. */ + private static boolean isInfrastructureType(IntegrationType type) { + return type == IntegrationType.S3 || type == IntegrationType.NETWORK; + } + /** Whether this caller may author custom API integrations, for the UI to offer or hide it. */ public boolean canAuthorCustomApi(User currentUser) { return applicationProperties.getPolicies().isAllowCustomApiIntegrations() diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java index 0009ee386e..661e42c771 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/Team.java @@ -2,13 +2,19 @@ package stirling.software.proprietary.model; import java.io.Serializable; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import org.hibernate.proxy.HibernateProxy; + import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; import stirling.software.proprietary.security.model.User; @@ -18,7 +24,6 @@ import stirling.software.proprietary.security.model.User; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class Team implements Serializable { @@ -29,7 +34,9 @@ public class Team implements Serializable { @Column(name = "team_id") private Long id; - @Column(name = "name", unique = true, nullable = false) + // Not unique: SaaS personal teams all share the name "My Team". TeamController enforces + // uniqueness for admin-created teams. + @Column(name = "name", nullable = false) private String name; @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true) @@ -45,4 +52,31 @@ public class Team implements Serializable { users.remove(user); user.setTeam(null); } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + Team team = (Team) o; + return getId() != null && Objects.equals(getId(), team.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/converters/PdfToPdfUaRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/converters/PdfToPdfUaRequest.java new file mode 100644 index 0000000000..8981398001 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/converters/PdfToPdfUaRequest.java @@ -0,0 +1,73 @@ +package stirling.software.proprietary.model.api.converters; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.common.model.api.PDFFile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PdfToPdfUaRequest extends PDFFile { + + @Schema( + description = "PDF/UA conformance level to target", + defaultValue = "ua1", + allowableValues = {"ua1", "ua2"}) + private String profile; + + @Schema( + description = + "Document title, required by PDF/UA. Falls back to the first heading, then the" + + " filename.") + private String title; + + @Schema( + description = + "Document language as a BCP-47 tag, for example en-GB. Applied only when the" + + " document does not already declare one, unless overrideLanguage is" + + " set.", + defaultValue = "en-GB") + private String language; + + @Schema( + description = + "Replace the language the document already declares. Off by default, so a" + + " document is never relabelled into a language it is not written in.", + defaultValue = "false") + private Boolean overrideLanguage; + + @Schema( + description = + "What to do with an existing structure tree: keep it, rebuild it, or decide" + + " automatically", + defaultValue = "auto", + allowableValues = {"auto", "keep", "rebuild"}) + private String existingTags; + + @Schema( + description = + "How to treat images with no description. require-alt leaves them undescribed so" + + " the report asks for input; mark-decorative treats every image as" + + " decoration.", + defaultValue = "require-alt", + allowableValues = {"require-alt", "mark-decorative"}) + private String figurePolicy; + + @Schema( + description = + "Embed fonts the document references but does not carry. Required for" + + " conformance and needs Ghostscript.", + defaultValue = "true") + private Boolean embedFonts; + + @Schema( + description = + "Alternative descriptions for figures, as key=text pairs separated by newlines." + + " Keys come from the accessibility-report endpoint's" + + " figuresNeedingDescription list, for example \"0:12=Bar chart of" + + " quarterly revenue\". Descriptions are never invented, so without" + + " these an illustrated document cannot claim conformance.") + private String altText; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityIssue.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityIssue.java new file mode 100644 index 0000000000..0c2a86ad99 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityIssue.java @@ -0,0 +1,38 @@ +package stirling.software.proprietary.model.api.ua; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** One accessibility problem, grouped across all of its occurrences. */ +@Data +@Schema(description = "A single accessibility issue found in a document") +public class AccessibilityIssue { + + @Schema(description = "ISO 14289 clause, e.g. 7.3") + private String clause; + + @Schema(description = "Test number within the clause") + private String testNumber; + + @Schema(description = "Plain-English description of the problem") + private String message; + + @Schema(description = "The validator's own wording, for support and debugging") + private String technicalMessage; + + @Schema(description = "error or warning") + private String severity = "error"; + + @Schema(description = "Standard the check came from, e.g. PDF/UA-1") + private String specification; + + @Schema(description = "Where the problem was found, when the validator reports it") + private String location; + + @Schema(description = "How many times this issue occurs") + private int occurrences; + + @Schema(description = "True when the converter can fix this without human input") + private boolean autoFixable; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReport.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReport.java new file mode 100644 index 0000000000..bc810635d1 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReport.java @@ -0,0 +1,63 @@ +package stirling.software.proprietary.model.api.ua; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * A document's accessibility standing. The machine/human split is load-bearing: veraPDF covers only + * about half of the Matterhorn Protocol, so a clean automated pass is not "accessible". + */ +@Data +@Schema(description = "Accessibility standing of a document") +public class AccessibilityReport { + + @Schema(description = "Profile the document was checked against, e.g. PDF/UA-1") + private String profile; + + @Schema(description = "Whether the document has a structure tree at all") + private boolean tagged; + + @Schema(description = "Whether the document declares PDF/UA conformance in its metadata") + private boolean declaresConformance; + + @Schema(description = "Whether every automated check passed") + private boolean passesAutomatedChecks; + + @Schema(description = "Automated checks that failed, grouped by rule") + private List issues = List.of(); + + @Schema(description = "Things a person still has to verify; automation cannot decide these") + private List humanChecks = List.of(); + + @Schema(description = "How many of the failing checks the converter can fix on its own") + private int automaticallyFixable; + + @Schema(description = "How many need information from the user, such as alternative text") + private int needsInput; + + @Schema( + description = + "Figures that need an alternative description. Each carries the key to pass" + + " back in the conversion request's altTextByFigure map, so a caller" + + " can enumerate what is missing and then supply it.") + private List figuresNeedingDescription = List.of(); + + @Schema(description = "Document-level facts that drive most failures") + private Summary summary = new Summary(); + + @Data + @Schema(description = "Quick document-level facts") + public static class Summary { + private int pages; + private boolean hasTitle; + private boolean displaysDocTitle; + private boolean hasLanguage; + private boolean allFontsEmbedded; + private int unembeddedFonts; + private int figures; + private boolean encrypted; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReportRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReportRequest.java new file mode 100644 index 0000000000..178d2637b2 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/AccessibilityReportRequest.java @@ -0,0 +1,19 @@ +package stirling.software.proprietary.model.api.ua; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import stirling.software.common.model.api.PDFFile; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AccessibilityReportRequest extends PDFFile { + + @Schema( + description = "Profile to check against", + defaultValue = "ua1", + allowableValues = {"ua1", "ua2"}) + private String profile; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/FigureDescriptor.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/FigureDescriptor.java new file mode 100644 index 0000000000..1c960d87e7 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/FigureDescriptor.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.model.api.ua; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * One figure needing an alternative description, which is never invented. key is the + * altTextByFigure key "pageIndex:ordinal"; page is 1-based; kind is "figure" or "formula". + */ +@Schema(description = "A figure that needs an alternative description") +public record FigureDescriptor( + String key, + int page, + String kind, + float x, + float y, + float width, + float height, + String existingAlt) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/PdfUaConversionOutcome.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/PdfUaConversionOutcome.java new file mode 100644 index 0000000000..dcef97c916 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/PdfUaConversionOutcome.java @@ -0,0 +1,26 @@ +package stirling.software.proprietary.model.api.ua; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Result of a PDF/UA conversion. + * + * @param declared whether a {@code pdfuaid} conformance claim was written into {@code pdfBytes} + */ +@Schema(description = "Result of converting a document to PDF/UA") +public record PdfUaConversionOutcome( + byte[] pdfBytes, + boolean declared, + UaValidationResult validation, + TaggingSummary tagging, + List warnings) { + + @Schema(description = "What the tagging pass produced") + public record TaggingSummary( + boolean rebuiltStructure, + int taggedElements, + int artifacts, + int figuresNeedingAltText) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/UaValidationResult.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/UaValidationResult.java new file mode 100644 index 0000000000..5e494c35be --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ua/UaValidationResult.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.model.api.ua; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Outcome of validating against one PDF/UA profile. compliant means every automated check passed, + * which is not the same as usable by assistive technology; totalFailures is ungrouped. + */ +@Schema(description = "Result of validating a document against a PDF/UA profile") +public record UaValidationResult( + String profile, boolean compliant, List issues, int totalFailures) { + + public boolean hasIssues() { + return !issues.isEmpty(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java index ccaf337c0b..aeb66b47a8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/security/PersistentAuditEvent.java @@ -1,6 +1,9 @@ package stirling.software.proprietary.model.security; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.*; @@ -28,7 +31,9 @@ import lombok.*; name = "idx_audit_source_timestamp_principal", columnList = "source,timestamp,principal") }) -@Data +@Getter +@Setter +@ToString(onlyExplicitlyIncluded = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -36,14 +41,43 @@ public class PersistentAuditEvent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @ToString.Include private Long id; - private String principal; - private String type; + @ToString.Include private String principal; + + @ToString.Include private String type; private String source; @Column(columnDefinition = "text") private String data; // JSON blob - private Instant timestamp; + @ToString.Include private Instant timestamp; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + PersistentAuditEvent that = (PersistentAuditEvent) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/ArtifactType.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/ArtifactType.java new file mode 100644 index 0000000000..78df1fbc27 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/ArtifactType.java @@ -0,0 +1,23 @@ +package stirling.software.proprietary.pdf.ua; + +/** Artifact subtypes (ISO 32000-1 14.8.2.2). Artifacts are excluded from the structure tree. */ +public enum ArtifactType { + /** Running heads, folios, page numbers. Required by PDF/UA-1 clause 7.8. */ + PAGINATION("Pagination"), + /** Rules, boxes, and other layout ornamentation. */ + LAYOUT("Layout"), + /** Cut marks and colour bars. */ + PAGE("Page"), + /** Background graphics with no informational content. */ + BACKGROUND("Background"); + + private final String subtype; + + ArtifactType(String subtype) { + this.subtype = subtype; + } + + public String subtype() { + return subtype; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/BBox.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/BBox.java new file mode 100644 index 0000000000..f2fbf97438 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/BBox.java @@ -0,0 +1,48 @@ +package stirling.software.proprietary.pdf.ua; + +/** An axis-aligned rectangle in PDF user space, with y increasing upwards. */ +public record BBox(float x0, float y0, float x1, float y1) { + + public static final BBox EMPTY = new BBox(0, 0, 0, 0); + + public static BBox of(float x, float y, float width, float height) { + return new BBox(x, y, x + width, y + height); + } + + public float width() { + return x1 - x0; + } + + public float height() { + return y1 - y0; + } + + public float centreX() { + return (x0 + x1) / 2f; + } + + public BBox union(BBox other) { + if (other == null || other.isEmpty()) { + return this; + } + if (isEmpty()) { + return other; + } + return new BBox( + Math.min(x0, other.x0), + Math.min(y0, other.y0), + Math.max(x1, other.x1), + Math.max(y1, other.y1)); + } + + public boolean isEmpty() { + return x1 <= x0 || y1 <= y0; + } + + /** Horizontal overlap with another box as a fraction of the narrower box's width. */ + public float horizontalOverlap(BBox other) { + float overlap = Math.min(x1, other.x1) - Math.max(x0, other.x0); + float narrower = Math.min(width(), other.width()); + return narrower <= 0 ? 0 : Math.max(0, overlap) / narrower; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/DocumentStructure.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/DocumentStructure.java new file mode 100644 index 0000000000..5922895453 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/DocumentStructure.java @@ -0,0 +1,84 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.Setter; + +/** The derived logical structure of a document, ready for serialisation into a structure tree. */ +@Getter +@Setter +public class DocumentStructure { + + /** Top-level blocks in document reading order. */ + private final List blocks = new ArrayList<>(); + + /** Warnings raised during analysis, surfaced in the conversion report. */ + private final List warnings = new ArrayList<>(); + + private String title; + private String language; + + /** True when real text was wrapped as artifacts, which blocks any conformance claim. */ + private boolean textSuppressed; + + /** Body text size used as the baseline for heading detection, in points. */ + private float bodyFontSize; + + public void add(StructBlock block) { + blocks.add(block); + } + + public void warn(String message) { + if (!warnings.contains(message)) { + warnings.add(message); + } + } + + public void visit(Consumer visitor) { + blocks.forEach(block -> block.visit(visitor)); + } + + public int count(StructType type) { + int[] total = {0}; + visit( + block -> { + if (block.getType() == type) { + total[0]++; + } + }); + return total[0]; + } + + public int artifactCount() { + int[] total = {0}; + visit( + block -> { + if (block.isArtifact()) { + total[0]++; + } + }); + return total[0]; + } + + /** Figures with no alternative description, the most common PDF/UA failure. */ + public List figuresWithoutAlt() { + List missing = new ArrayList<>(); + visit( + block -> { + if ((block.getType() == StructType.FIGURE + || block.getType() == StructType.FORMULA) + && (block.getAlt() == null || block.getAlt().isBlank()) + && (block.getActualText() == null || block.getActualText().isBlank())) { + missing.add(block); + } + }); + return missing; + } + + public boolean isEmpty() { + return blocks.isEmpty(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzer.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzer.java new file mode 100644 index 0000000000..11c510d6ea --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzer.java @@ -0,0 +1,831 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +/** + * Derives a logical structure from extracted lines and graphics, reusing {@code HeadingDetector}'s + * heuristics. Degrades to paragraphs rather than guessing, since a wrong tag misleads readers. + */ +@Slf4j +public class LayoutAnalyzer { + + private static final Pattern BULLET = Pattern.compile("^[•‣◦⁃∙·▪●■o\\-\\*\\+]\\s+.*"); + private static final Pattern ORDERED = + Pattern.compile("^(\\d{1,3}|[a-zA-Z]|[ivxlcIVXLC]{1,5})[\\.\\)]\\s+.*"); + private static final Pattern PAGE_NUMBER = + Pattern.compile( + "^(page\\s+)?\\d{1,4}(\\s*(of|/)\\s*\\d{1,4})?$", Pattern.CASE_INSENSITIVE); + private static final Pattern DIGITS = Pattern.compile("\\d+"); + + /** Fraction of page height treated as the running head / foot band. */ + private static final float MARGIN_BAND = 0.10f; + + /** A line must exceed the body size by this ratio before it can be a heading. */ + private static final float HEADING_RATIO = 1.10f; + + /** Sizes within this many points are treated as the same heading tier. */ + private static final float TIER_TOLERANCE = 0.4f; + + private static final int MAX_HEADING_WORDS = 12; + + /** Word gap beyond this multiple of the font size separates table cells. */ + private static final float CELL_GAP_RATIO = 1.2f; + + /** Images smaller than this in either dimension are decoration, not content. */ + private static final float MIN_FIGURE_SIZE = 12f; + + /** A size used by more than this share of lines is body text, however large the median says. */ + private static final float MAX_HEADING_LINE_SHARE = 0.2f; + + /** Consecutive lines sharing a size are a text block; headings appear alone. */ + private static final int MAX_HEADING_RUN = 3; + + /** A vector thinner than this in either dimension is a rule or border, not a drawing. */ + private static final float MIN_VECTOR_THICKNESS = 3f; + + /** Vector clusters smaller than this are ornament; larger ones are probably a chart. */ + private static final float MIN_VECTOR_FIGURE_SIZE = 40f; + + /** A drawing is built from several strokes; one big rectangle is a panel, not a chart. */ + private static final int MIN_VECTOR_FIGURE_OPS = 4; + + /** More text than this inside the region means shading behind content, not a drawing. */ + private static final int MAX_LINES_INSIDE_FIGURE = 2; + + public DocumentStructure analyse(List pages) { + DocumentStructure structure = new DocumentStructure(); + float bodySize = bodyFontSize(pages); + structure.setBodyFontSize(bodySize); + Map tiers = headingTiers(pages, bodySize); + Map> artifactLines = repeatedMarginLines(pages, bodySize); + + for (PageContent page : pages) { + analysePage( + page, + structure, + bodySize, + tiers, + artifactLines.getOrDefault(page.pageIndex(), List.of())); + } + + List suppressedPages = + pages.stream() + .filter(PageContent::linesDropped) + .map(PageContent::pageIndex) + .toList(); + if (!suppressedPages.isEmpty()) { + structure.setTextSuppressed(true); + structure.warn( + "Text on page(s) " + + suppressedPages.stream() + .map(i -> String.valueOf(i + 1)) + .collect(Collectors.joining(", ")) + + " could not be tagged reliably and was marked as artifacts. The" + + " converter will not claim conformance while real text is hidden" + + " from assistive technology."); + } + + normaliseHeadingLevels(structure); + structure.setTitle(deriveTitle(structure)); + return structure; + } + + // --- Document-wide statistics ----------------------------------------- + + /** Character-weighted median line size, which is far more stable than a plain median. */ + static float bodyFontSize(List pages) { + Map weights = new HashMap<>(); + for (PageContent page : pages) { + for (TextLineInfo line : page.lines()) { + if (line.dominantFontSize() > 0 && !line.isBlank()) { + weights.merge(line.dominantFontSize(), line.charCount(), Integer::sum); + } + } + } + if (weights.isEmpty()) { + return 0f; + } + int total = weights.values().stream().mapToInt(Integer::intValue).sum(); + List> sorted = + weights.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList(); + int seen = 0; + for (Map.Entry entry : sorted) { + seen += entry.getValue(); + if (seen >= total / 2) { + return entry.getKey(); + } + } + return sorted.get(sorted.size() - 1).getKey(); + } + + /** Maps each distinct heading size to a 1-based level, largest size first. */ + static Map headingTiers(List pages, float bodySize) { + if (bodySize <= 0) { + return Map.of(); + } + // A size used by a large share of the lines is body text, whatever the median says. + Map lineCounts = new HashMap<>(); + int totalLines = 0; + for (PageContent page : pages) { + for (TextLineInfo line : page.lines()) { + if (!line.isBlank()) { + lineCounts.merge(line.dominantFontSize(), 1, Integer::sum); + totalLines++; + } + } + } + int headingLineCeiling = Math.max(1, (int) (totalLines * MAX_HEADING_LINE_SHARE)); + + // Headings do not cluster; a run of same-size lines is a text block, not headings. + Map longestRun = new HashMap<>(); + for (PageContent page : pages) { + Float runSize = null; + int runLength = 0; + for (TextLineInfo line : page.lines()) { + if (line.isBlank()) { + continue; + } + float size = line.dominantFontSize(); + if (runSize != null && Float.compare(size, runSize) == 0) { + runLength++; + } else { + runSize = size; + runLength = 1; + } + int seen = longestRun.getOrDefault(size, 0); + if (runLength > seen) { + longestRun.put(size, runLength); + } + } + } + + List sizes = new ArrayList<>(); + for (PageContent page : pages) { + for (TextLineInfo line : page.lines()) { + if (isHeadingCandidate(line) + && line.dominantFontSize() > bodySize * HEADING_RATIO + && lineCounts.getOrDefault(line.dominantFontSize(), 0) <= headingLineCeiling + && longestRun.getOrDefault(line.dominantFontSize(), 0) < MAX_HEADING_RUN) { + sizes.add(line.dominantFontSize()); + } + } + } + List distinct = sizes.stream().distinct().sorted(Comparator.reverseOrder()).toList(); + + Map tiers = new LinkedHashMap<>(); + int level = 0; + Float previous = null; + for (Float size : distinct) { + if (previous == null || previous - size > TIER_TOLERANCE) { + level = Math.min(level + 1, 6); + previous = size; + } + tiers.put(size, level); + } + return tiers; + } + + /** + * Claims a line's operators word run by word run; claiming the whole ordinal interval would + * swallow anything drawn between them, an image included. + */ + private static void claimLine(StructBlock block, TextLineInfo line) { + // Sort by ordinal, not position: merging out-of-order runs silently drops them to + // /Artifact, hiding them from assistive technology while the file still validates. + List words = + line.words().stream() + .filter(w -> !w.isBlank()) + .sorted(Comparator.comparingInt(WordInfo::startOrdinal)) + .toList(); + if (words.isEmpty()) { + block.addRange(line.startOrdinal(), line.endOrdinal()); + return; + } + int start = words.get(0).startOrdinal(); + int end = words.get(0).endOrdinal(); + for (int i = 1; i < words.size(); i++) { + WordInfo word = words.get(i); + if (word.startOrdinal() <= end + 1) { + end = Math.max(end, word.endOrdinal()); + } else { + block.addRange(start, end); + start = word.startOrdinal(); + end = word.endOrdinal(); + } + } + block.addRange(start, end); + } + + static boolean isHeadingCandidate(TextLineInfo line) { + String text = line.text().strip(); + if (text.isEmpty() || line.wordCount() > MAX_HEADING_WORDS) { + return false; + } + char last = text.charAt(text.length() - 1); + return last != '.' && last != '!' && last != '?'; + } + + /** + * Finds lines in the head/foot bands whose text repeats across pages. Digits are masked first + * so that "Page 4" and "Page 5" count as the same running foot. + */ + static Map> repeatedMarginLines(List pages) { + return repeatedMarginLines(pages, bodyFontSize(pages)); + } + + static Map> repeatedMarginLines( + List pages, float bodySize) { + Map> result = new HashMap<>(); + if (pages.isEmpty()) { + return result; + } + Map counts = new HashMap<>(); + Map> candidates = new HashMap<>(); + + for (PageContent page : pages) { + float height = page.mediaBox().height(); + if (height <= 0) { + continue; + } + float topEdge = page.mediaBox().y1() - height * MARGIN_BAND; + float bottomEdge = page.mediaBox().y0() + height * MARGIN_BAND; + List inBand = new ArrayList<>(); + for (TextLineInfo line : page.lines()) { + if (line.bbox().y0() >= topEdge || line.bbox().y1() <= bottomEdge) { + inBand.add(line); + counts.merge(mask(line.text()), 1, Integer::sum); + } + } + candidates.put(page.pageIndex(), inBand); + } + + int threshold = Math.max(2, pages.size() / 2); + for (Map.Entry> entry : candidates.entrySet()) { + List artifacts = new ArrayList<>(); + for (TextLineInfo line : entry.getValue()) { + boolean repeats = + pages.size() >= 3 && counts.getOrDefault(mask(line.text()), 0) >= threshold; + boolean pageNumber = PAGE_NUMBER.matcher(line.text().strip()).matches(); + // Masked digits merge "Section 1" and "Section 2"; size is the tie-break that stops + // a real heading being demoted, as running heads are never larger than body text. + boolean looksLikeChrome = + bodySize <= 0 || line.dominantFontSize() <= bodySize * 1.05f; + if (pageNumber || (repeats && looksLikeChrome)) { + artifacts.add(line); + } + } + result.put(entry.getKey(), artifacts); + } + return result; + } + + private static String mask(String text) { + return DIGITS.matcher(text.strip().toLowerCase()).replaceAll("#").replaceAll("\\s+", " "); + } + + // --- Per-page analysis ------------------------------------------------- + + private void analysePage( + PageContent page, + DocumentStructure structure, + float bodySize, + Map tiers, + List marginArtifacts) { + + for (TextLineInfo line : marginArtifacts) { + StructBlock artifact = StructBlock.artifact(ArtifactType.PAGINATION, page.pageIndex()); + claimLine(artifact, line); + artifact.setBbox(line.bbox()); + artifact.setText(line.text()); + structure.add(artifact); + } + + // Identity set, not List.contains: TextLineInfo is a record whose equals walks its word + // list, so a linear scan per line is quadratic with a deep comparison inside it. + java.util.Set marginSet = Collections.newSetFromMap(new IdentityHashMap<>()); + marginSet.addAll(marginArtifacts); + List body = + page.lines().stream() + .filter(line -> !line.isBlank() && !marginSet.contains(line)) + .sorted(readingOrder(page)) + .toList(); + + List blocks = new ArrayList<>(); + int index = 0; + while (index < body.size()) { + TextLineInfo line = body.get(index); + + int tableEnd = tableRunEnd(body, index); + if (tableEnd > index) { + StructBlock table = buildTable(body.subList(index, tableEnd + 1), page.pageIndex()); + if (table != null) { + blocks.add(table); + index = tableEnd + 1; + continue; + } + } + + int listEnd = listRunEnd(body, index); + if (listEnd > index) { + blocks.add(buildList(body.subList(index, listEnd + 1), page.pageIndex())); + index = listEnd + 1; + continue; + } + + Integer level = headingLevel(line, tiers); + if (level != null) { + StructBlock heading = new StructBlock(StructType.heading(level), page.pageIndex()); + claimLine(heading, line); + heading.setBbox(line.bbox()); + heading.setText(line.text()); + blocks.add(heading); + index++; + continue; + } + + int paragraphEnd = paragraphRunEnd(body, index, tiers, bodySize); + blocks.add(buildParagraph(body.subList(index, paragraphEnd + 1), page.pageIndex())); + index = paragraphEnd + 1; + } + + // Form XObject text is attributed to its Do, so a Figure too would double-claim it. + Set claimed = new HashSet<>(); + for (StructBlock block : blocks) { + block.visit( + node -> + node.getRanges() + .forEach( + range -> { + for (int i = range.start(); i <= range.end(); i++) { + claimed.add(i); + } + })); + } + blocks.addAll(buildGraphics(page, structure, claimed)); + blocks.forEach(structure::add); + } + + /** + * Orders lines top-to-bottom, splitting into columns first when the page is clearly + * multi-column. Without this, a two-column page reads as interleaved half-sentences. + */ + private Comparator readingOrder(PageContent page) { + Float gutter = detectGutter(page); + if (gutter == null) { + return Comparator.comparingDouble((TextLineInfo l) -> -l.bbox().y1()) + .thenComparingDouble(l -> l.bbox().x0()); + } + return Comparator.comparingInt((TextLineInfo l) -> l.bbox().centreX() < gutter ? 0 : 1) + .thenComparingDouble(l -> -l.bbox().y1()) + .thenComparingDouble(l -> l.bbox().x0()); + } + + /** + * Returns the x of a vertical gutter when the page is two-column, else null. A gutter must sit + * near the middle, be crossed by almost no line, and have substantial text on both sides. + */ + static Float detectGutter(PageContent page) { + List lines = page.lines().stream().filter(line -> !line.isBlank()).toList(); + if (lines.size() < 8) { + return null; + } + float pageWidth = page.mediaBox().width(); + if (pageWidth <= 0) { + return null; + } + float centre = page.mediaBox().x0() + pageWidth / 2f; + long crossing = + lines.stream() + .filter( + line -> + line.bbox().x0() < centre - 5 + && line.bbox().x1() > centre + 5) + .count(); + if (crossing > lines.size() * 0.1) { + return null; + } + long left = lines.stream().filter(line -> line.bbox().centreX() < centre).count(); + long right = lines.size() - left; + boolean balanced = left > lines.size() * 0.25 && right > lines.size() * 0.25; + return balanced ? centre : null; + } + + private static Integer headingLevel(TextLineInfo line, Map tiers) { + if (!isHeadingCandidate(line)) { + return null; + } + return tiers.get(line.dominantFontSize()); + } + + // --- Paragraphs -------------------------------------------------------- + + private static int paragraphRunEnd( + List lines, int start, Map tiers, float bodySize) { + int end = start; + for (int i = start + 1; i < lines.size(); i++) { + TextLineInfo previous = lines.get(i - 1); + TextLineInfo current = lines.get(i); + if (headingLevel(current, tiers) != null || startsListItem(current)) { + break; + } + float gap = previous.bbox().y0() - current.bbox().y1(); + float leading = Math.max(bodySize, current.bbox().height()); + boolean sameBlock = gap < leading * 0.8f && gap > -leading; + boolean sentenceEnded = endsSentence(previous.text()); + if (!sameBlock || (sentenceEnded && gap > leading * 0.4f)) { + break; + } + end = i; + } + return end; + } + + private static boolean endsSentence(String text) { + String stripped = text.strip(); + if (stripped.isEmpty()) { + return false; + } + char last = stripped.charAt(stripped.length() - 1); + return last == '.' || last == '!' || last == '?'; + } + + private static StructBlock buildParagraph(List lines, int pageIndex) { + StructBlock paragraph = new StructBlock(StructType.P, pageIndex); + BBox box = BBox.EMPTY; + StringBuilder text = new StringBuilder(); + for (TextLineInfo line : lines) { + claimLine(paragraph, line); + box = box.union(line.bbox()); + if (text.length() > 0) { + text.append(' '); + } + text.append(line.text().strip()); + } + paragraph.setBbox(box); + paragraph.setText(text.toString()); + return paragraph; + } + + // --- Lists ------------------------------------------------------------- + + static boolean startsListItem(TextLineInfo line) { + String text = line.text().strip(); + return BULLET.matcher(text).matches() || ORDERED.matcher(text).matches(); + } + + private static int listRunEnd(List lines, int start) { + if (!startsListItem(lines.get(start))) { + return start; + } + float indent = lines.get(start).bbox().x0(); + int end = start; + for (int i = start + 1; i < lines.size(); i++) { + TextLineInfo line = lines.get(i); + boolean isItem = startsListItem(line) && Math.abs(line.bbox().x0() - indent) < 6f; + boolean isContinuation = !startsListItem(line) && line.bbox().x0() > indent + 2f; + if (!isItem && !isContinuation) { + break; + } + end = i; + } + // A single marker is a stray character, not a list. + long items = + lines.subList(start, end + 1).stream() + .filter(LayoutAnalyzer::startsListItem) + .count(); + return items >= 2 ? end : start; + } + + private static StructBlock buildList(List lines, int pageIndex) { + StructBlock list = new StructBlock(StructType.L, pageIndex); + list.setListNumbering(listNumbering(lines.get(0))); + BBox box = BBox.EMPTY; + StructBlock currentBody = null; + + for (TextLineInfo line : lines) { + box = box.union(line.bbox()); + if (startsListItem(line) || currentBody == null) { + StructBlock item = new StructBlock(StructType.LI, pageIndex); + StructBlock body = new StructBlock(StructType.LBODY, pageIndex); + claimLine(body, line); + body.setBbox(line.bbox()); + body.setText(line.text()); + item.addChild(body); + item.setBbox(line.bbox()); + list.addChild(item); + currentBody = body; + } else { + claimLine(currentBody, line); + currentBody.setBbox(currentBody.getBbox().union(line.bbox())); + currentBody.setText(currentBody.getText() + " " + line.text().strip()); + } + } + list.setBbox(box); + return list; + } + + private static String listNumbering(TextLineInfo first) { + String text = first.text().strip(); + if (BULLET.matcher(text).matches()) { + return "Disc"; + } + char c = text.charAt(0); + if (Character.isDigit(c)) { + return "Decimal"; + } + if ("ivxlc".indexOf(Character.toLowerCase(c)) >= 0 && text.length() > 1) { + return Character.isUpperCase(c) ? "UpperRoman" : "LowerRoman"; + } + return Character.isUpperCase(c) ? "UpperAlpha" : "LowerAlpha"; + } + + // --- Tables ------------------------------------------------------------ + + /** Splits a line into cells wherever the gap between words exceeds the cell threshold. */ + static List> splitCells(TextLineInfo line) { + List words = line.words().stream().filter(w -> !w.isBlank()).toList(); + List> cells = new ArrayList<>(); + if (words.isEmpty()) { + return cells; + } + float threshold = Math.max(line.dominantFontSize(), 1f) * CELL_GAP_RATIO; + List current = new ArrayList<>(); + current.add(words.get(0)); + for (int i = 1; i < words.size(); i++) { + float gap = words.get(i).bbox().x0() - words.get(i - 1).bbox().x1(); + if (gap > threshold) { + cells.add(List.copyOf(current)); + current = new ArrayList<>(); + } + current.add(words.get(i)); + } + cells.add(List.copyOf(current)); + return cells; + } + + /** + * Index of the last line of a table run starting at {@code start}, or {@code start} if none. + */ + private static int tableRunEnd(List lines, int start) { + int end = start; + for (int i = start; i < lines.size(); i++) { + if (splitCells(lines.get(i)).size() < 2) { + break; + } + end = i; + } + return end > start ? end : start; + } + + /** + * Builds a Table when the run really looks tabular and each cell owns its own operators. + * Returns null when it does not, so the caller falls back to paragraphs. + */ + private static StructBlock buildTable(List rows, int pageIndex) { + if (rows.size() < 2) { + return null; + } + List>> grid = new ArrayList<>(); + for (TextLineInfo row : rows) { + if (!row.wordsAreSeparable()) { + log.debug("Table row shares operators between cells; falling back to paragraphs"); + return null; + } + grid.add(splitCells(row)); + } + int columns = grid.get(0).size(); + long consistent = grid.stream().filter(row -> row.size() == columns).count(); + if (columns < 2 || consistent < Math.max(2, grid.size() * 0.6)) { + return null; + } + + boolean headerRow = looksLikeHeader(rows, grid); + StructBlock table = new StructBlock(StructType.TABLE, pageIndex); + BBox box = BBox.EMPTY; + + for (int r = 0; r < grid.size(); r++) { + List> cells = grid.get(r); + if (cells.size() != columns) { + continue; + } + StructBlock tr = new StructBlock(StructType.TR, pageIndex); + boolean isHeader = headerRow && r == 0; + for (List cell : cells) { + StructBlock td = + new StructBlock(isHeader ? StructType.TH : StructType.TD, pageIndex); + if (isHeader) { + td.setScope("Column"); + } + BBox cellBox = BBox.EMPTY; + StringBuilder text = new StringBuilder(); + int from = cell.get(0).startOrdinal(); + int to = cell.get(cell.size() - 1).endOrdinal(); + for (WordInfo word : cell) { + cellBox = cellBox.union(word.bbox()); + if (text.length() > 0) { + text.append(' '); + } + text.append(word.text()); + } + td.addRange(from, to); + td.setBbox(cellBox); + td.setText(text.toString()); + tr.addChild(td); + box = box.union(cellBox); + } + tr.setBbox(box); + table.addChild(tr); + } + table.setBbox(box); + if (table.getChildren().size() < 2) { + return null; + } + // Clause 7.5 needs equal cell counts per row; a ragged table fails validation outright. + long distinctWidths = + table.getChildren().stream() + .map(row -> row.getChildren().size()) + .distinct() + .count(); + if (distinctWidths != 1) { + log.debug("Discarding a table whose rows have different cell counts"); + return null; + } + return table; + } + + /** The first row is a header when it is bold, or when only later rows carry numbers. */ + private static boolean looksLikeHeader( + List rows, List>> grid) { + if (rows.get(0).bold()) { + return true; + } + boolean firstHasDigits = DIGITS.matcher(rows.get(0).text()).find(); + boolean laterHasDigits = + rows.subList(1, rows.size()).stream() + .anyMatch(row -> DIGITS.matcher(row.text()).find()); + return !firstHasDigits && laterHasDigits; + } + + // --- Graphics ---------------------------------------------------------- + + private List buildGraphics( + PageContent page, DocumentStructure structure, java.util.Set claimed) { + List blocks = new ArrayList<>(); + boolean warnedForms = false; + + // Vectors cluster: a chart is many strokes in one region, a rule is a single thin one. + java.util.Set vectorFigureOrdinals = vectorFigureOrdinals(page, claimed); + + for (MarkableOp op : page.ops()) { + if (op.kind() == MarkableOp.Kind.TEXT || claimed.contains(op.ordinal())) { + continue; + } + BBox box = op.bbox(); + + if (op.kind() == MarkableOp.Kind.VECTOR) { + StructBlock block; + if (vectorFigureOrdinals.contains(op.ordinal())) { + block = new StructBlock(StructType.FIGURE, page.pageIndex()); + } else { + block = StructBlock.artifact(ArtifactType.LAYOUT, page.pageIndex()); + } + block.addRange(op.ordinal(), op.ordinal()); + block.setBbox(box); + blocks.add(block); + continue; + } + + boolean decorative = box.width() < MIN_FIGURE_SIZE || box.height() < MIN_FIGURE_SIZE; + if (decorative) { + StructBlock artifact = StructBlock.artifact(ArtifactType.LAYOUT, page.pageIndex()); + artifact.addRange(op.ordinal(), op.ordinal()); + artifact.setBbox(box); + blocks.add(artifact); + continue; + } + + if (op.kind() == MarkableOp.Kind.FORM && !warnedForms) { + structure.warn( + "Content inside form XObjects was tagged as a single region because its" + + " text is not separately addressable; review those areas."); + warnedForms = true; + } + + StructBlock figure = new StructBlock(StructType.FIGURE, page.pageIndex()); + figure.addRange(op.ordinal(), op.ordinal()); + figure.setBbox(box); + blocks.add(figure); + } + return blocks; + } + + /** + * Finds vector operators belonging to a substantial drawing rather than page furniture; thin + * paths are rules and table borders, and a short run is ornament. + */ + private static Set vectorFigureOrdinals( + PageContent page, java.util.Set claimed) { + // A chart's plot area is mostly empty, while shading sits behind the text it decorates. + Set result = new HashSet<>(); + List run = new ArrayList<>(); + BBox extent = BBox.EMPTY; + + for (MarkableOp op : page.ops()) { + boolean substantial = + op.kind() == MarkableOp.Kind.VECTOR + && !claimed.contains(op.ordinal()) + && !op.bbox().isEmpty() + && op.bbox().width() >= MIN_VECTOR_THICKNESS + && op.bbox().height() >= MIN_VECTOR_THICKNESS; + if (substantial) { + run.add(op); + extent = extent.isEmpty() ? op.bbox() : extent.union(op.bbox()); + continue; + } + flushVectorRun(run, extent, page.lines(), result); + run = new ArrayList<>(); + extent = BBox.EMPTY; + } + flushVectorRun(run, extent, page.lines(), result); + return result; + } + + private static void flushVectorRun( + List run, + BBox extent, + List lines, + java.util.Set result) { + if (run.size() < MIN_VECTOR_FIGURE_OPS + || extent.width() < MIN_VECTOR_FIGURE_SIZE + || extent.height() < MIN_VECTOR_FIGURE_SIZE) { + return; + } + if (overlappingLines(extent, lines) > MAX_LINES_INSIDE_FIGURE) { + return; + } + run.forEach(op -> result.add(op.ordinal())); + } + + /** How many text lines sit within the region a vector cluster covers. */ + private static int overlappingLines(BBox extent, List lines) { + int count = 0; + for (TextLineInfo line : lines) { + BBox box = line.bbox(); + boolean inside = + box.x0() >= extent.x0() - 2 + && box.x1() <= extent.x1() + 2 + && box.y0() >= extent.y0() - 2 + && box.y1() <= extent.y1() + 2; + if (inside) { + count++; + } + } + return count; + } + + // --- Post-processing --------------------------------------------------- + + /** + * Rewrites heading levels so no level is skipped, which PDF/UA-1 clause 7.4 requires. A + * document that jumps H1 to H3 is remapped to H1, H2 while preserving relative depth. + */ + static void normaliseHeadingLevels(DocumentStructure structure) { + List headings = new ArrayList<>(); + structure.visit( + block -> { + if (block.getType().isHeading()) { + headings.add(block); + } + }); + int previous = 0; + for (StructBlock heading : headings) { + int level = heading.getType().headingLevel(); + int adjusted = level > previous + 1 ? previous + 1 : level; + heading.setType(StructType.heading(adjusted)); + previous = adjusted; + } + } + + /** Uses the first top-level heading as the title when the document has no metadata title. */ + private static String deriveTitle(DocumentStructure structure) { + for (StructBlock block : structure.getBlocks()) { + if (block.getType().isHeading() && !block.getText().isBlank()) { + return block.getText().strip(); + } + } + return null; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkableOp.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkableOp.java new file mode 100644 index 0000000000..c48319260f --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkableOp.java @@ -0,0 +1,44 @@ +package stirling.software.proprietary.pdf.ua; + +/** + * One operator in a page content stream that may be wrapped in a marked-content sequence. The + * ordinal counts only markable operators, joining text extraction to token rewriting. + */ +public record MarkableOp(int ordinal, Kind kind, BBox bbox, String resourceName) { + + public enum Kind { + /** Tj, TJ, ' or " */ + TEXT, + /** Do referencing an image XObject */ + IMAGE, + /** Do referencing a form XObject */ + FORM, + /** BI ... ID ... EI */ + INLINE_IMAGE, + /** A path-painting or shading operator: rules, borders, fills, logos */ + VECTOR; + + public boolean isGraphic() { + return this == IMAGE || this == INLINE_IMAGE; + } + } + + /** + * Operator names counted as markable; both passes must agree on this set. Path painting is + * included because clause 7.1 needs visible rules and borders tagged or artifacted. + */ + public static boolean isMarkableOperator(String name) { + return switch (name) { + case "Tj", "TJ", "'", "\"", "Do", "BI" -> true; + default -> isPathPainting(name); + }; + } + + /** Painting operators only: {@code n} ends a path without marking the page. */ + public static boolean isPathPainting(String name) { + return switch (name) { + case "S", "s", "f", "F", "f*", "B", "B*", "b", "b*", "sh" -> true; + default -> false; + }; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkedContentInjector.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkedContentInjector.java new file mode 100644 index 0000000000..c9d6330a60 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/MarkedContentInjector.java @@ -0,0 +1,284 @@ +package stirling.software.proprietary.pdf.ua; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSInteger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdfparser.PDFStreamParser; +import org.apache.pdfbox.pdfwriter.ContentStreamWriter; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDStream; + +import lombok.extern.slf4j.Slf4j; + +/** + * Rewrites a page stream so every markable operator sits inside a marked-content sequence: claimed + * content gets an MCID, everything else /Artifact, satisfying PDF/UA-1 clause 7.1 by construction. + */ +@Slf4j +public class MarkedContentInjector { + + private static final COSName ARTIFACT = COSName.getPDFName("Artifact"); + private static final COSName MCID = COSName.getPDFName("MCID"); + private static final COSName ACTUAL_TEXT = COSName.getPDFName("ActualText"); + private static final COSName ALT = COSName.getPDFName("Alt"); + + /** Operators that force an open sequence to close so nesting stays legal. */ + private static boolean isBoundary(String name) { + return "BT".equals(name) || "ET".equals(name) || "q".equals(name) || "Q".equals(name); + } + + private static boolean isMarkedContentOperator(String name) { + return "BDC".equals(name) || "BMC".equals(name) || "EMC".equals(name); + } + + /** + * Path-construction operators; ISO 32000-1 forbids marked content inside a path object, so a + * sequence wrapping a fill or stroke must open before the path starts. + */ + private static boolean isPathConstruction(String name) { + return switch (name) { + case "m", "l", "c", "v", "y", "h", "re" -> true; + default -> false; + }; + } + + private static boolean opensMarkedContent(String name) { + return "BDC".equals(name) || "BMC".equals(name); + } + + /** + * True for an optional-content sequence; stripping an {@code /OC} wrapper would make hidden + * layers such as watermarks or redaction overlays visible. + */ + private static boolean isOptionalContent(String name, List operands) { + return opensMarkedContent(name) + && !operands.isEmpty() + && operands.get(0) instanceof COSName tag + && "OC".equals(tag.getName()); + } + + /** + * True when a sequence supplies replacement text for its glyphs; dropping it leaves a screen + * reader with the font's own mapping, which for a ligature says nothing useful. + */ + private static boolean carriesReplacementText(String name, List operands) { + if (!opensMarkedContent(name)) { + return false; + } + for (COSBase operand : operands) { + if (operand instanceof COSDictionary properties + && (properties.containsKey(ACTUAL_TEXT) + || properties.containsKey(ALT) + || properties.containsKey(COSName.E))) { + return true; + } + } + return false; + } + + /** The source's own ids mean nothing once the tree is rebuilt, so they are dropped. */ + private static void stripStaleMcid(List operands) { + for (COSBase operand : operands) { + if (operand instanceof COSDictionary properties) { + properties.removeItem(MCID); + } + } + } + + /** Wraps every markable operator on the page; returns the next unused marked content id. */ + public int inject( + PDDocument document, + PDPage page, + List blocks, + int nextMcid, + boolean stripExisting) + throws IOException { + + Map owners = ownersByOrdinal(blocks); + List tokens = parse(page); + List output = new ArrayList<>(tokens.size() + owners.size() * 4); + + List operands = new ArrayList<>(); + // Tracks, for each surviving source sequence, whether its closer should be kept. + Deque keptSequences = new ArrayDeque<>(); + StructBlock openBlock = null; + boolean open = false; + int ordinal = -1; + int mcid = nextMcid; + int pathStart = -1; + + for (Object token : tokens) { + if (!(token instanceof Operator operator)) { + operands.add((COSBase) token); + continue; + } + String name = operator.getName(); + + if (stripExisting && isMarkedContentOperator(name)) { + boolean keep; + if (opensMarkedContent(name)) { + keep = + isOptionalContent(name, operands) + || carriesReplacementText(name, operands); + if (keep) { + stripStaleMcid(operands); + } + keptSequences.push(keep); + } else { + // A closer is kept exactly when its matching opener was. + keep = !keptSequences.isEmpty() && keptSequences.pop(); + } + if (!keep) { + operands.clear(); + continue; + } + // Close our own sequence first so the two never interleave illegally. + if (open) { + output.add(Operator.getOperator("EMC")); + open = false; + openBlock = null; + } + output.addAll(operands); + output.add(operator); + operands.clear(); + continue; + } + + if (isBoundary(name) && open) { + output.add(Operator.getOperator("EMC")); + open = false; + openBlock = null; + } + + // Remember where the current path object began so a sequence wrapping its painting + // operator can be opened before it rather than inside it. + if (isPathConstruction(name)) { + if (pathStart < 0) { + pathStart = output.size(); + } + } else if (!MarkableOp.isPathPainting(name) && !"n".equals(name)) { + pathStart = -1; + } + + if (MarkableOp.isMarkableOperator(name)) { + ordinal++; + StructBlock owner = owners.get(ordinal); + if (!open || owner != openBlock) { + boolean insidePath = MarkableOp.isPathPainting(name) && pathStart >= 0; + if (open) { + // Close before the path began, so the EMC also stays outside the path. + output.add( + insidePath ? pathStart : output.size(), + Operator.getOperator("EMC")); + if (insidePath) { + pathStart++; + } + } + int at = insidePath ? pathStart : output.size(); + mcid = openSequenceAt(output, at, owner, mcid); + open = true; + openBlock = owner; + } + } + + output.addAll(operands); + output.add(operator); + operands.clear(); + + if (MarkableOp.isPathPainting(name) || "n".equals(name)) { + pathStart = -1; + } + } + + if (open) { + output.add(Operator.getOperator("EMC")); + } + + write(document, page, output); + return mcid; + } + + /** Emits the opening BDC/BMC at a given position and records the id on the owning block. */ + private int openSequenceAt(List output, int at, StructBlock owner, int mcid) { + List opening = new ArrayList<>(3); + if (owner == null) { + opening.add(ARTIFACT); + opening.add(Operator.getOperator("BMC")); + } else if (owner.isArtifact()) { + COSDictionary properties = new COSDictionary(); + if (owner.getArtifactType() != null) { + properties.setName(COSName.TYPE, owner.getArtifactType().subtype()); + } + opening.add(ARTIFACT); + opening.add(properties); + opening.add(Operator.getOperator("BDC")); + } else { + COSDictionary properties = new COSDictionary(); + properties.setItem(MCID, COSInteger.get(mcid)); + opening.add(COSName.getPDFName(owner.getType().tag())); + opening.add(properties); + opening.add(Operator.getOperator("BDC")); + owner.getMcids().add(mcid); + mcid++; + } + output.addAll(at, opening); + return mcid; + } + + /** + * Maps each claimed ordinal to its block. Overlapping claims are dropped rather than merged: + * two structure elements sharing content would make the reading order ambiguous. + */ + static Map ownersByOrdinal(List blocks) { + Map owners = new HashMap<>(); + for (StructBlock block : blocks) { + block.visit( + node -> { + for (StructBlock.OrdinalRange range : node.getRanges()) { + for (int i = range.start(); i <= range.end(); i++) { + StructBlock existing = owners.putIfAbsent(i, node); + if (existing != null && existing != node) { + log.debug( + "Ordinal {} claimed by both {} and {}; keeping the first", + i, + existing, + node); + } + } + } + }); + } + return owners; + } + + private static List parse(PDPage page) throws IOException { + PDFStreamParser parser = new PDFStreamParser(page); + List tokens = new ArrayList<>(); + Object token; + while ((token = parser.parseNextToken()) != null) { + tokens.add(token); + } + return tokens; + } + + private static void write(PDDocument document, PDPage page, List tokens) + throws IOException { + PDStream stream = new PDStream(document); + try (OutputStream out = stream.createOutputStream(COSName.FLATE_DECODE)) { + new ContentStreamWriter(out).writeTokens(tokens); + } + page.setContents(stream); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PageContent.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PageContent.java new file mode 100644 index 0000000000..6c74212621 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PageContent.java @@ -0,0 +1,33 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.List; + +/** + * Everything the layout analyser needs about one page. carriesTextSemantics: existing marked + * content has ActualText/Alt/expansion a rebuild would discard. linesDropped: text became + * artifacts. + */ +public record PageContent( + int pageIndex, + List lines, + List ops, + int markableCount, + boolean preExistingMarkedContent, + boolean carriesTextSemantics, + boolean linesDropped, + BBox mediaBox) { + + public boolean hasText() { + return lines.stream().anyMatch(line -> !line.isBlank()); + } + + /** Markable operators that draw graphics rather than text. */ + public List graphics() { + return ops.stream().filter(op -> op.kind().isGraphic()).toList(); + } + + /** Form XObject invocations, which are tagged as a unit because their text is opaque here. */ + public List forms() { + return ops.stream().filter(op -> op.kind() == MarkableOp.Kind.FORM).toList(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaIdentificationSchema.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaIdentificationSchema.java new file mode 100644 index 0000000000..d37c2569c9 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaIdentificationSchema.java @@ -0,0 +1,47 @@ +package stirling.software.proprietary.pdf.ua; + +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.XMPSchema; +import org.apache.xmpbox.type.IntegerType; +import org.apache.xmpbox.type.StructuredType; + +/** + * The {@code pdfuaid} XMP conformance schema, which XMPBox does not ship. Only write it once + * validation has passed - it is a compliance claim. + */ +@StructuredType( + preferedPrefix = PdfUaIdentificationSchema.PREFERRED_PREFIX, + namespace = PdfUaIdentificationSchema.NAMESPACE) +public class PdfUaIdentificationSchema extends XMPSchema { + + public static final String PREFERRED_PREFIX = "pdfuaid"; + public static final String NAMESPACE = "http://www.aiim.org/pdfua/ns/id/"; + + public static final String PART = "part"; + public static final String REV = "rev"; + + public PdfUaIdentificationSchema(XMPMetadata metadata) { + super(metadata); + } + + public PdfUaIdentificationSchema(XMPMetadata metadata, String prefix) { + super(metadata, prefix); + } + + /** Sets {@code pdfuaid:part}, the conformance level (1 or 2). */ + public void setPart(int part) { + addProperty(new IntegerType(getMetadata(), getNamespace(), getPrefix(), PART, part)); + } + + /** Sets {@code pdfuaid:rev}, the four-digit revision year used by PDF/UA-2. */ + public void setRevision(int year) { + addProperty(new IntegerType(getMetadata(), getNamespace(), getPrefix(), REV, year)); + } + + public Integer getPart() { + if (getProperty(PART) instanceof IntegerType part) { + return part.getValue(); + } + return null; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriter.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriter.java new file mode 100644 index 0000000000..7f58654c87 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriter.java @@ -0,0 +1,224 @@ +package stirling.software.proprietary.pdf.ua; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.DublinCoreSchema; +import org.apache.xmpbox.schema.XMPSchema; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpSerializer; + +import lombok.extern.slf4j.Slf4j; + +/** Applies the document-level PDF/UA requirements: title, language, tab order, declaration. */ +@Slf4j +public class PdfUaMetadataWriter { + + private static final COSName TABS = COSName.getPDFName("Tabs"); + private static final COSName SUSPECTS = COSName.getPDFName("Suspects"); + + /** + * Applies everything except the conformance declaration. Clause 7.1 requires a title, so a + * blank one falls back to the existing metadata title. + */ + public List applyDocumentRequirements( + PDDocument document, String title, String language, PdfUaProfile profile) + throws IOException { + return applyDocumentRequirements(document, title, language, profile, false); + } + + public List applyDocumentRequirements( + PDDocument document, + String title, + String language, + PdfUaProfile profile, + boolean preserveVersion) + throws IOException { + + List warnings = new ArrayList<>(); + PDDocumentCatalog catalog = document.getDocumentCatalog(); + + if (language != null && !language.isBlank()) { + catalog.setLanguage(language); + } + + String effectiveTitle = resolveTitle(document, title); + if (effectiveTitle != null) { + PDDocumentInformation info = document.getDocumentInformation(); + info.setTitle(effectiveTitle); + document.setDocumentInformation(info); + } + + // Without this a viewer shows the filename instead of the title, which defeats the point. + PDViewerPreferences preferences = catalog.getViewerPreferences(); + if (preferences == null) { + preferences = new PDViewerPreferences(catalog.getCOSObject()); + } + preferences.setDisplayDocTitle(true); + catalog.setViewerPreferences(preferences); + + // Clause 7.18.1: every page needs an explicit tab order. + for (PDPage page : document.getPages()) { + page.getCOSObject().setName(TABS, "S"); + } + + // A structure tree flagged as suspect is not conforming. + if (catalog.getMarkInfo() != null) { + catalog.getMarkInfo().getCOSObject().removeItem(SUSPECTS); + } + + if (!preserveVersion && document.getVersion() < profile.pdfVersion()) { + document.setVersion(profile.pdfVersion()); + } + + warnings.addAll(describeFormFields(document)); + writeXmp(document, effectiveTitle, language, null); + return warnings; + } + + /** + * Gives every form field the {@code /TU} description clause 7.18.1 requires, reusing its + * authored partial name. Unnamed fields are reported, never given a useless placeholder. + */ + private static List describeFormFields(PDDocument document) { + List warnings = new ArrayList<>(); + PDAcroForm form = document.getDocumentCatalog().getAcroForm(); + if (form == null) { + return warnings; + } + int unnamed = 0; + for (PDField field : form.getFieldTree()) { + String existing = field.getAlternateFieldName(); + if (existing != null && !existing.isBlank()) { + continue; + } + String partialName = field.getPartialName(); + if (partialName == null || partialName.isBlank()) { + unnamed++; + continue; + } + field.setAlternateFieldName(partialName); + } + if (unnamed > 0) { + warnings.add( + unnamed + + " form field(s) have neither a description nor a name, so no tooltip" + + " could be derived. Add one for each before claiming conformance."); + } + return warnings; + } + + /** + * Strips the {@code pdfuaid} declaration when validation fails after it was written, so the + * returned file does not assert conformance it lacks. + */ + public void removeConformanceDeclaration(PDDocument document) throws IOException { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + XMPMetadata metadata = loadOrCreate(catalog); + XMPSchema identification = metadata.getSchema(PdfUaIdentificationSchema.NAMESPACE); + if (identification == null) { + return; + } + metadata.removeSchema(identification); + serialiseInto(document, metadata); + } + + /** Writes the {@code pdfuaid:part} declaration. Only call this after validation has passed. */ + public void declareConformance(PDDocument document, PdfUaProfile profile) throws IOException { + writeXmp(document, resolveTitle(document, null), documentLanguage(document), profile); + } + + private String resolveTitle(PDDocument document, String preferred) { + if (preferred != null && !preferred.isBlank()) { + return preferred.strip(); + } + String existing = document.getDocumentInformation().getTitle(); + return existing != null && !existing.isBlank() ? existing.strip() : null; + } + + private static String documentLanguage(PDDocument document) { + return document.getDocumentCatalog().getLanguage(); + } + + /** + * Rewrites the XMP packet, preserving what was there. A malformed packet is replaced, since an + * unparseable one fails validation on its own. + */ + private void writeXmp(PDDocument document, String title, String language, PdfUaProfile profile) + throws IOException { + + PDDocumentCatalog catalog = document.getDocumentCatalog(); + XMPMetadata metadata = loadOrCreate(catalog); + + if (title != null) { + DublinCoreSchema dublinCore = metadata.getDublinCoreSchema(); + if (dublinCore == null) { + dublinCore = metadata.createAndAddDublinCoreSchema(); + } + dublinCore.setTitle(title); + if (language != null + && !language.isBlank() + && (dublinCore.getLanguages() == null + || !dublinCore.getLanguages().contains(language))) { + dublinCore.addLanguage(language); + } + } + + if (profile != null) { + // Re-converting an already-declared file must not leave two pdfuaid schemas. + XMPSchema stale = metadata.getSchema(PdfUaIdentificationSchema.NAMESPACE); + if (stale != null) { + metadata.removeSchema(stale); + } + PdfUaIdentificationSchema identification = new PdfUaIdentificationSchema(metadata); + identification.setPart(profile.part()); + if (profile.revision() > 0) { + identification.setRevision(profile.revision()); + } + metadata.addSchema(identification); + } + + serialiseInto(document, metadata); + } + + private static void serialiseInto(PDDocument document, XMPMetadata metadata) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + new XmpSerializer().serialize(metadata, out, true); + } catch (javax.xml.transform.TransformerException e) { + throw new IOException("Could not serialise XMP metadata", e); + } + PDMetadata pdMetadata = new PDMetadata(document); + pdMetadata.importXMPMetadata(out.toByteArray()); + document.getDocumentCatalog().setMetadata(pdMetadata); + } + + private XMPMetadata loadOrCreate(PDDocumentCatalog catalog) { + PDMetadata existing = catalog.getMetadata(); + if (existing != null) { + try { + DomXmpParser parser = new DomXmpParser(); + // Strict parsing rejects pdfuaid, silently discarding a packet we just wrote. + parser.setStrictParsing(false); + return parser.parse(new ByteArrayInputStream(existing.toByteArray())); + } catch (Exception e) { + log.debug("Replacing unparseable XMP packet: {}", e.getMessage()); + } + } + return XMPMetadata.createXMPMetadata(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaProfile.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaProfile.java new file mode 100644 index 0000000000..9523f5c790 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaProfile.java @@ -0,0 +1,47 @@ +package stirling.software.proprietary.pdf.ua; + +/** The PDF/UA conformance level a conversion targets. */ +public enum PdfUaProfile { + /** ISO 14289-1, layered on PDF 1.7. */ + UA1(1, 1.7f, 0), + /** ISO 14289-2: needs PDF 2.0, namespaced structure types and a revision year. */ + UA2(2, 2.0f, 2024); + + private final int part; + private final float pdfVersion; + private final int revision; + + PdfUaProfile(int part, float pdfVersion, int revision) { + this.part = part; + this.pdfVersion = pdfVersion; + this.revision = revision; + } + + public int part() { + return part; + } + + public float pdfVersion() { + return pdfVersion; + } + + /** The {@code pdfuaid:rev} year, or 0 when the profile does not use one. */ + public int revision() { + return revision; + } + + public String displayName() { + return "PDF/UA-" + part; + } + + public static PdfUaProfile fromRequest(String value) { + if (value == null || value.isBlank()) { + return UA1; + } + String normalised = value.trim().toLowerCase().replace("/", "").replace("-", ""); + return switch (normalised) { + case "ua2", "pdfua2", "2" -> UA2; + default -> UA1; + }; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaTagger.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaTagger.java new file mode 100644 index 0000000000..7f5dcc3739 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/PdfUaTagger.java @@ -0,0 +1,303 @@ +package stirling.software.proprietary.pdf.ua; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; + +import lombok.extern.slf4j.Slf4j; + +/** + * Tags an untagged PDF and applies the document-level PDF/UA requirements. Content must be marked + * before the tree can reference it, and conformance is declared elsewhere, only after validation. + */ +@Slf4j +public class PdfUaTagger { + + private final TaggedContentExtractor extractor = new TaggedContentExtractor(); + private final LayoutAnalyzer analyzer = new LayoutAnalyzer(); + private final MarkedContentInjector injector = new MarkedContentInjector(); + private final PdfUaMetadataWriter metadataWriter = new PdfUaMetadataWriter(); + + public TaggingResult tag(PDDocument document, TaggingOptions options) throws IOException { + boolean alreadyTagged = hasUsableStructureTree(document); + boolean rebuild = + switch (options.getExistingTags()) { + case KEEP -> false; + case REBUILD -> true; + case AUTO -> !alreadyTagged; + }; + + List languageWarnings = new ArrayList<>(); + String language = resolveLanguage(document, options, languageWarnings); + + if (!rebuild) { + log.info("Keeping existing structure tree; applying document requirements only"); + DocumentStructure kept = new DocumentStructure(); + languageWarnings.forEach(kept::warn); + metadataWriter + .applyDocumentRequirements( + document, + options.getTitle(), + language, + options.getProfile(), + options.isPreservePdfVersion()) + .forEach(kept::warn); + return new TaggingResult(kept, false); + } + + // Types the old tree carried, so a rebuild that cannot reproduce them can say so. Font + // embedding may already have deleted the tree, so fall back to what the source had. + Set discardedTypes = + alreadyTagged + ? structureTypes(document) + : options.getSourceFacts().structureTypes(); + + if (alreadyTagged) { + stripStructure(document); + } + + List pages = extractor.extract(document); + DocumentStructure structure = analyzer.analyse(pages); + structure.setLanguage(language); + languageWarnings.forEach(structure::warn); + applyFigurePolicy(structure, options); + + if (structure.isEmpty()) { + structure.warn( + "No taggable content was found; the document may be a scan with no text layer."); + } + + injectMarkedContent(document, structure, pages); + new StructTreeWriter().write(document, structure, options.getProfile()); + // Losing the tree to the embedder is a different problem from a requested rebuild, and + // the advice that helps differs too, so tell them apart. + boolean lostToEmbedder = !alreadyTagged && options.getSourceFacts().hasUsableTree(); + warnAboutFlattenedStructure( + discardedTypes, structureTypes(document), structure, lostToEmbedder); + + String title = resolveTitle(options, structure); + if (title == null) { + structure.warn( + "No document title could be derived. PDF/UA requires one, so supply a title."); + } + metadataWriter + .applyDocumentRequirements( + document, + title, + language, + options.getProfile(), + options.isPreservePdfVersion()) + .forEach(structure::warn); + + return new TaggingResult(structure, true); + } + + /** + * Keeps the language the document already declares. Overwriting it relabels, say, a French file + * as English, and no validator can catch that. + */ + private static String resolveLanguage( + PDDocument document, TaggingOptions options, List warnings) { + String existing = document.getDocumentCatalog().getLanguage(); + if (existing == null || existing.isBlank()) { + // Font embedding discards /Lang, so without this a rewritten French document would + // silently take the caller's default language. + existing = options.getSourceFacts().language(); + } + String requested = options.getLanguage(); + if (existing == null || existing.isBlank() || options.isOverrideLanguage()) { + return requested; + } + if (requested != null && !requested.isBlank() && !requested.equalsIgnoreCase(existing)) { + warnings.add( + "The document already declares its language as '" + + existing + + "', so the requested '" + + requested + + "' was ignored. Ask to override the language to change it."); + } + return existing; + } + + /** Explicit title first, then the first heading, then the caller's fallback. */ + private static String resolveTitle(TaggingOptions options, DocumentStructure structure) { + for (String candidate : + new String[] { + options.getTitle(), structure.getTitle(), options.getFallbackTitle() + }) { + if (candidate != null && !candidate.isBlank()) { + return candidate.strip(); + } + } + return null; + } + + /** Writes the conformance declaration. Separate from tagging so validation can gate it. */ + public void declareConformance(PDDocument document, PdfUaProfile profile) throws IOException { + metadataWriter.declareConformance(document, profile); + } + + /** Withdraws the conformance claim, for a document that turned out not to validate. */ + public void withdrawConformance(PDDocument document) throws IOException { + metadataWriter.removeConformanceDeclaration(document); + } + + /** Wraps content page by page; marked content ids restart on each page. */ + private void injectMarkedContent( + PDDocument document, DocumentStructure structure, List pages) + throws IOException { + Map markableCounts = new LinkedHashMap<>(); + pages.forEach(page -> markableCounts.put(page.pageIndex(), page.markableCount())); + Map> byPage = new LinkedHashMap<>(); + for (StructBlock block : structure.getBlocks()) { + byPage.computeIfAbsent(block.getPageIndex(), k -> new ArrayList<>()).add(block); + } + for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) { + List blocks = byPage.getOrDefault(pageIndex, List.of()); + // Nothing to wrap, and rewriting costs a parse and recompress for an identical stream. + if (blocks.isEmpty() && markableCounts.getOrDefault(pageIndex, 0) == 0) { + continue; + } + injector.inject(document, document.getPage(pageIndex), blocks, 0, true); + } + } + + /** Applies alt text supplied by the caller, or demotes images to artifacts on request. */ + private static void applyFigurePolicy(DocumentStructure structure, TaggingOptions options) { + int[] suppressed = {0}; + structure.visit( + block -> { + if (block.getType() != StructType.FIGURE) { + return; + } + if (options.getFigurePolicy() == TaggingOptions.FigurePolicy.MARK_DECORATIVE) { + block.setType(StructType.ARTIFACT); + block.setArtifactType(ArtifactType.LAYOUT); + suppressed[0]++; + return; + } + int ordinal = + block.getRanges().isEmpty() ? -1 : block.getRanges().get(0).start(); + String alt = options.altTextFor(block.getPageIndex(), ordinal); + if (alt != null && !alt.isBlank()) { + block.setAlt(alt); + } + }); + // Marking images decorative validates by hiding content, so never report it as clean. + if (suppressed[0] > 0) { + structure.warn( + suppressed[0] + + " image(s) were marked as decoration and are now hidden from" + + " assistive technology. Confirm none of them carried meaning."); + } + int missing = structure.figuresWithoutAlt().size(); + if (missing > 0) { + structure.warn( + missing + + " figure(s) have no alternative description. PDF/UA requires one for" + + " every image that carries meaning."); + } + } + + /** + * A tree is only worth keeping when wired up: kids, a parent tree, and a marked catalog. + * Keeping one that fails any of those leaves the document permanently unfixable. + */ + public static boolean hasUsableStructureTree(PDDocument document) { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + PDStructureTreeRoot root = catalog.getStructureTreeRoot(); + if (root == null) { + return false; + } + try { + boolean hasKids = root.getKids() != null && !root.getKids().isEmpty(); + boolean hasParentTree = root.getParentTree() != null; + boolean marked = catalog.getMarkInfo() != null && catalog.getMarkInfo().isMarked(); + return hasKids && hasParentTree && marked; + } catch (RuntimeException e) { + log.debug("Unreadable structure tree, treating as absent: {}", e.getMessage()); + return false; + } + } + + /** + * A rebuild derives structure from layout, so semantics the old tree carried can vanish - a + * table becomes loose paragraphs. Validators cannot see that loss, so it has to be reported. + */ + private static void warnAboutFlattenedStructure( + Set before, + Set after, + DocumentStructure structure, + boolean lostToEmbedder) { + List lost = + MEANINGFUL_TYPES.stream() + .filter(type -> before.contains(type) && !after.contains(type)) + .toList(); + if (lost.isEmpty()) { + return; + } + // Keeping the tags cannot help once the embedder has deleted them, so do not suggest it. + String remedy = + lostToEmbedder + ? " Embedding the missing fonts rewrote the document and deleted its" + + " original tags. Turn off font embedding to keep them." + : " Keep the existing tags instead to preserve it."; + structure.warn( + "Rebuilding the tags could not reproduce " + + String.join(", ", lost) + + " structure, so that content is now plain paragraphs." + + remedy); + } + + /** Structure whose loss changes what a screen reader conveys, not just how it is nested. */ + private static final List MEANINGFUL_TYPES = + List.of("Table", "TH", "Formula", "L", "LI", "TOC", "Note"); + + private static Set structureTypes(PDDocument document) { + Set types = new HashSet<>(); + try { + PDStructureTreeRoot root = document.getDocumentCatalog().getStructureTreeRoot(); + if (root != null) { + collectTypes(root.getKids(), types, 0); + } + } catch (RuntimeException e) { + log.debug("Could not read structure types: {}", e.getMessage()); + } + return types; + } + + private static void collectTypes(Object node, Set types, int depth) { + // Structure trees can be deep or, in damaged files, cyclic; cap rather than overflow. + if (node == null || depth > 64) { + return; + } + if (node instanceof List list) { + list.forEach(child -> collectTypes(child, types, depth + 1)); + } else if (node instanceof PDStructureElement element) { + types.add(element.getStructureType()); + collectTypes(element.getKids(), types, depth + 1); + } + } + + private static void stripStructure(PDDocument document) { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + catalog.getCOSObject().removeItem(COSName.getPDFName("StructTreeRoot")); + catalog.getCOSObject().removeItem(COSName.getPDFName("MarkInfo")); + document.getPages() + .forEach( + page -> + page.getCOSObject() + .removeItem(COSName.getPDFName("StructParents"))); + log.info("Removed existing structure tree before rebuilding"); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/SourceFacts.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/SourceFacts.java new file mode 100644 index 0000000000..4f058d6c48 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/SourceFacts.java @@ -0,0 +1,59 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; + +import lombok.extern.slf4j.Slf4j; + +/** + * What the document said about itself before anything rewrote it. Font embedding shells out to + * Ghostscript, which returns a file with no structure tree, no {@code /Lang} and no XMP, so a + * tagger reading the rewritten document sees an untagged, language-less file and cannot tell that + * anything was lost. These facts are captured from the original and carried past that stage. + * + * @param language the catalog {@code /Lang} the author declared, or null + * @param structureTypes every structure element type the original tree contained + * @param hasUsableTree whether the original had a structure tree worth preserving + */ +@Slf4j +public record SourceFacts(String language, Set structureTypes, boolean hasUsableTree) { + + private static final int MAX_DEPTH = 64; + + /** Facts for a document nothing has rewritten, used when font embedding did not run. */ + public static final SourceFacts NONE = new SourceFacts(null, Set.of(), false); + + public static SourceFacts of(PDDocument document) { + String language = null; + Set types = new HashSet<>(); + boolean usable = false; + try { + language = document.getDocumentCatalog().getLanguage(); + usable = PdfUaTagger.hasUsableStructureTree(document); + PDStructureTreeRoot root = document.getDocumentCatalog().getStructureTreeRoot(); + if (root != null) { + collect(root.getKids(), types, 0); + } + } catch (RuntimeException e) { + log.debug("Could not read source facts: {}", e.getMessage()); + } + return new SourceFacts(language, Set.copyOf(types), usable); + } + + private static void collect(Object node, Set types, int depth) { + // Damaged files can present a cyclic tree; cap rather than overflow the stack. + if (node == null || depth > MAX_DEPTH) { + return; + } + if (node instanceof java.util.List list) { + list.forEach(child -> collect(child, types, depth + 1)); + } else if (node instanceof PDStructureElement element) { + types.add(element.getStructureType()); + collect(element.getKids(), types, depth + 1); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructBlock.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructBlock.java new file mode 100644 index 0000000000..d2f3e452b1 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructBlock.java @@ -0,0 +1,134 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.Setter; + +/** + * One node of the derived logical structure: either page content (ranges of markable operator + * ordinals) or child blocks. Containers with no content are pruned before serialisation. + */ +@Getter +@Setter +public class StructBlock { + + /** A contiguous, inclusive run of markable operator ordinals within one page stream. */ + public record OrdinalRange(int start, int end) { + public boolean contains(int ordinal) { + return ordinal >= start && ordinal <= end; + } + + public int size() { + return end - start + 1; + } + } + + private StructType type; + private ArtifactType artifactType; + private int pageIndex; + private BBox bbox = BBox.EMPTY; + private String text = ""; + + private final List ranges = new ArrayList<>(); + private final List children = new ArrayList<>(); + + /** {@code /Alt} - required on Figure and Formula for PDF/UA. */ + private String alt; + + /** {@code /ActualText} - replacement text for content whose glyphs do not spell the word. */ + private String actualText; + + /** {@code /Lang} - set only where it differs from the document default. */ + private String lang; + + /** {@code /Scope} on a TH: Row, Column or Both. */ + private String scope; + + /** {@code /ListNumbering} on an L. */ + private String listNumbering; + + /** Unique {@code /ID}, required on Note and FENote elements. */ + private String id; + + /** + * Marked content ids assigned during injection; one block yields several when split, since a + * sequence must nest inside BT/ET and q/Q rather than straddle them. + */ + private final List mcids = new ArrayList<>(); + + /** True when the source content was already inside a marked-content sequence. */ + private boolean preMarked; + + public StructBlock(StructType type, int pageIndex) { + this.type = type; + this.pageIndex = pageIndex; + } + + public static StructBlock artifact(ArtifactType artifactType, int pageIndex) { + StructBlock block = new StructBlock(StructType.ARTIFACT, pageIndex); + block.artifactType = artifactType; + return block; + } + + public StructBlock addChild(StructBlock child) { + children.add(child); + return this; + } + + public StructBlock addRange(int start, int end) { + ranges.add(new OrdinalRange(start, end)); + return this; + } + + public boolean isArtifact() { + return type == StructType.ARTIFACT; + } + + /** Depth-first walk over this block and all descendants. */ + public void visit(Consumer visitor) { + visitor.accept(this); + for (StructBlock child : children) { + child.visit(visitor); + } + } + + /** Total number of ordinals owned by this block and its descendants. */ + public int contentCount() { + int total = ranges.stream().mapToInt(OrdinalRange::size).sum(); + for (StructBlock child : children) { + total += child.contentCount(); + } + return total; + } + + /** Concatenated text of this block and its descendants, in tree order. */ + public String collectText() { + StringBuilder sb = new StringBuilder(); + visit( + block -> { + if (!block.text.isBlank()) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append(block.text.strip()); + } + }); + return sb.toString(); + } + + @Override + public String toString() { + return type.tag() + + (artifactType != null ? "[" + artifactType.subtype() + "]" : "") + + "(p" + + pageIndex + + ", " + + ranges.size() + + " ranges, " + + children.size() + + " kids)"; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructTreeWriter.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructTreeWriter.java new file mode 100644 index 0000000000..f6b5d89b4c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructTreeWriter.java @@ -0,0 +1,295 @@ +package stirling.software.proprietary.pdf.ua; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSInteger; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDNumberTreeNode; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDObjectReference; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import org.apache.pdfbox.pdmodel.documentinterchange.taggedpdf.PDListAttributeObject; +import org.apache.pdfbox.pdmodel.documentinterchange.taggedpdf.PDTableAttributeObject; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; + +import lombok.extern.slf4j.Slf4j; + +/** + * Serialises a {@link DocumentStructure} into a PDF structure tree. Must run after {@link + * MarkedContentInjector}, which assigns the marked content ids this writer references. + */ +@Slf4j +public class StructTreeWriter { + + private static final COSName STRUCT_PARENT = COSName.getPDFName("StructParent"); + private static final COSName NUMS = COSName.getPDFName("Nums"); + private static final String PDF2_STANDARD_NAMESPACE = "http://iso.org/pdf2/ssn"; + + /** Per-page marked content id to owning element, built while walking the tree. */ + private final Map> mcidOwners = new LinkedHashMap<>(); + + private COSDictionary standardNamespace; + private int nextParentKey; + + public void write(PDDocument document, DocumentStructure structure, PdfUaProfile profile) + throws IOException { + PDStructureTreeRoot root = new PDStructureTreeRoot(); + PDStructureElement documentElement = + new PDStructureElement(StructType.DOCUMENT.tag(), root); + if (structure.getLanguage() != null) { + documentElement.setLanguage(structure.getLanguage()); + } + if (profile == PdfUaProfile.UA2) { + applyNamespace(documentElement, document); + } + + for (StructBlock block : structure.getBlocks()) { + if (block.isArtifact()) { + continue; + } + PDStructureElement child = buildElement(document, block, documentElement, profile); + if (child != null) { + documentElement.appendKid(child); + } + } + + root.appendKid(documentElement); + buildParentTree(document, root); + registerNamespaces(root); + + PDMarkInfo markInfo = new PDMarkInfo(); + markInfo.setMarked(true); + document.getDocumentCatalog().setMarkInfo(markInfo); + document.getDocumentCatalog().setStructureTreeRoot(root); + } + + /** Recursively builds an element, returning null when the block carries no content at all. */ + private PDStructureElement buildElement( + PDDocument document, + StructBlock block, + PDStructureElement parent, + PdfUaProfile profile) { + + // Prune on assigned MCIDs, not claimed ranges: form-XObject lines all resolve to one Do, + // and emitting the losers would announce empty paragraphs to a screen reader. + if (!carriesContent(block)) { + return null; + } + StructType type = effectiveType(block, profile); + PDStructureElement element = new PDStructureElement(type.tag(), parent); + PDPage page = document.getPage(block.getPageIndex()); + element.setPage(page); + + if (profile == PdfUaProfile.UA2) { + applyNamespace(element, document); + } + applyAttributes(block, element); + + for (int mcid : block.getMcids()) { + element.appendKid(mcid); + mcidOwners + .computeIfAbsent(block.getPageIndex(), k -> new LinkedHashMap<>()) + .put(mcid, element); + } + + for (StructBlock child : block.getChildren()) { + PDStructureElement childElement = buildElement(document, child, element, profile); + if (childElement != null) { + element.appendKid(childElement); + } + } + return element; + } + + /** True when this block, or something beneath it, was actually given marked content. */ + private static boolean carriesContent(StructBlock block) { + if (!block.getMcids().isEmpty()) { + return true; + } + return block.getChildren().stream().anyMatch(StructTreeWriter::carriesContent); + } + + /** PDF/UA-2 replaces Note with FENote for footnotes. */ + private static StructType effectiveType(StructBlock block, PdfUaProfile profile) { + if (profile == PdfUaProfile.UA2 && block.getType() == StructType.NOTE) { + return StructType.FENOTE; + } + return block.getType(); + } + + private static void applyAttributes(StructBlock block, PDStructureElement element) { + if (block.getAlt() != null && !block.getAlt().isBlank()) { + element.setAlternateDescription(block.getAlt()); + } + if (block.getActualText() != null && !block.getActualText().isBlank()) { + element.setActualText(block.getActualText()); + } + if (block.getLang() != null && !block.getLang().isBlank()) { + element.setLanguage(block.getLang()); + } + if (block.getId() != null && !block.getId().isBlank()) { + element.setElementIdentifier(block.getId()); + } + if (block.getScope() != null) { + PDTableAttributeObject table = new PDTableAttributeObject(); + table.setScope(block.getScope()); + element.addAttribute(table); + } + if (block.getListNumbering() != null) { + PDListAttributeObject list = new PDListAttributeObject(); + list.setListNumbering(block.getListNumbering()); + element.addAttribute(list); + } + } + + /** PDF/UA-2 requires every element to declare the standard structure namespace. */ + private void applyNamespace(PDStructureElement element, PDDocument document) { + element.getCOSObject().setItem(COSName.getPDFName("NS"), standardNamespace()); + } + + /** The PDF 2.0 standard structure namespace, created once per document. */ + private COSDictionary standardNamespace() { + if (standardNamespace == null) { + standardNamespace = new COSDictionary(); + standardNamespace.setName(COSName.TYPE, "Namespace"); + standardNamespace.setString(COSName.getPDFName("NS"), PDF2_STANDARD_NAMESPACE); + } + return standardNamespace; + } + + private void registerNamespaces(PDStructureTreeRoot root) { + if (standardNamespace == null) { + return; + } + COSArray namespaces = new COSArray(); + namespaces.add(standardNamespace); + root.getCOSObject().setItem(COSName.getPDFName("Namespaces"), namespaces); + } + + /** + * Builds {@code /ParentTree}: per page, an array indexed by marked content id keyed on {@code + * /StructParents}, plus one entry per annotation keyed on {@code /StructParent}. + */ + private void buildParentTree(PDDocument document, PDStructureTreeRoot root) { + COSArray nums = new COSArray(); + nextParentKey = 0; + + for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) { + Map owners = mcidOwners.get(pageIndex); + if (owners == null || owners.isEmpty()) { + continue; + } + PDPage page = document.getPage(pageIndex); + int key = nextParentKey++; + page.setStructParents(key); + + int maxMcid = owners.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1); + COSArray entries = new COSArray(); + for (int mcid = 0; mcid <= maxMcid; mcid++) { + PDStructureElement owner = owners.get(mcid); + entries.add( + owner != null ? owner.getCOSObject() : org.apache.pdfbox.cos.COSNull.NULL); + } + nums.add(COSInteger.get(key)); + nums.add(entries); + } + + List annotationEntries = tagAnnotations(document, root); + for (int i = 0; i + 1 < annotationEntries.size(); i += 2) { + nums.add(annotationEntries.get(i)); + nums.add(annotationEntries.get(i + 1)); + } + + COSDictionary parentTreeDict = new COSDictionary(); + parentTreeDict.setItem(NUMS, nums); + root.setParentTree(new PDNumberTreeNode(parentTreeDict, PDStructureElement.class)); + root.setParentTreeNextKey(nextParentKey); + } + + /** + * Clause 7.18: every visible annotation needs a structure element so it is reachable from the + * tree. Links become Link elements, anything else an Annot. + */ + private List tagAnnotations(PDDocument document, PDStructureTreeRoot root) { + List entries = new ArrayList<>(); + PDStructureElement documentElement = firstDocumentElement(root); + if (documentElement == null) { + return entries; + } + for (int pageIndex = 0; pageIndex < document.getNumberOfPages(); pageIndex++) { + PDPage page = document.getPage(pageIndex); + List annotations; + try { + annotations = page.getAnnotations(); + } catch (IOException e) { + log.debug("Could not read annotations on page {}: {}", pageIndex, e.getMessage()); + continue; + } + for (PDAnnotation annotation : annotations) { + if (annotation == null + || annotation.isHidden() + || annotation.isNoView() + || "Popup".equals(annotation.getSubtype())) { + continue; + } + PDStructureElement element = + new PDStructureElement(annotationType(annotation), documentElement); + element.setPage(page); + + PDObjectReference reference = new PDObjectReference(); + reference.setReferencedObject(annotation); + element.appendKid(reference); + documentElement.appendKid(element); + + int key = nextParentKey++; + annotation.getCOSObject().setInt(STRUCT_PARENT, key); + entries.add(COSInteger.get(key)); + entries.add(element.getCOSObject()); + + if (annotation.getContents() == null || annotation.getContents().isBlank()) { + annotation.setContents(defaultContents(annotation)); + } + } + } + return entries; + } + + /** Clause 7.18.4: widgets need a Form element, links a Link element, everything else Annot. */ + private static String annotationType(PDAnnotation annotation) { + if (annotation instanceof PDAnnotationWidget) { + return StructType.FORM.tag(); + } + if (annotation instanceof PDAnnotationLink) { + return StructType.LINK.tag(); + } + return "Annot"; + } + + private static String defaultContents(PDAnnotation annotation) { + if (annotation instanceof PDAnnotationLink link && link.getAction() != null) { + return "Link"; + } + return annotation.getSubtype() == null ? "Annotation" : annotation.getSubtype(); + } + + private static PDStructureElement firstDocumentElement(PDStructureTreeRoot root) { + for (Object kid : root.getKids()) { + if (kid instanceof PDStructureElement element) { + return element; + } + } + return null; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructType.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructType.java new file mode 100644 index 0000000000..b4634a58c6 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/StructType.java @@ -0,0 +1,62 @@ +package stirling.software.proprietary.pdf.ua; + +/** + * PDF standard structure types emitted by the tagger (ISO 32000-1 14.8.4), limited to the PDF/UA + * subset. {@link #ARTIFACT} is not one: it marks content in the stream and stays out of the tree. + */ +public enum StructType { + DOCUMENT("Document"), + PART("Part"), + SECT("Sect"), + H1("H1"), + H2("H2"), + H3("H3"), + H4("H4"), + H5("H5"), + H6("H6"), + P("P"), + L("L"), + LI("LI"), + LBL("Lbl"), + LBODY("LBody"), + TABLE("Table"), + TR("TR"), + TH("TH"), + TD("TD"), + FIGURE("Figure"), + CAPTION("Caption"), + FORMULA("Formula"), + NOTE("Note"), + FENOTE("FENote"), + LINK("Link"), + /** Wraps a widget annotation; PDF/UA-1 clause 7.18.4 requires widgets to sit inside one. */ + FORM("Form"), + SPAN("Span"), + ARTIFACT("Artifact"); + + private final String tag; + + StructType(String tag) { + this.tag = tag; + } + + /** The name written into the PDF {@code /S} entry. */ + public String tag() { + return tag; + } + + public boolean isHeading() { + return this == H1 || this == H2 || this == H3 || this == H4 || this == H5 || this == H6; + } + + /** Heading level 1-6, or 0 when this is not a heading. */ + public int headingLevel() { + return isHeading() ? ordinal() - H1.ordinal() + 1 : 0; + } + + /** The heading type for a 1-based level, clamped to the H1-H6 range. */ + public static StructType heading(int level) { + int clamped = Math.max(1, Math.min(6, level)); + return values()[H1.ordinal() + clamped - 1]; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggedContentExtractor.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggedContentExtractor.java new file mode 100644 index 0000000000..01c86215d4 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggedContentExtractor.java @@ -0,0 +1,630 @@ +package stirling.software.proprietary.pdf.ua; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdfparser.PDFStreamParser; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.font.PDType3Font; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.Vector; + +import lombok.extern.slf4j.Slf4j; + +/** + * Extracts text lines and graphic ops from page streams, tagging each with its operator ordinal. + * Both passes count the same operators in the same order, so ordinals cross-reference. + */ +@Slf4j +public class TaggedContentExtractor { + + /** Glyph size below which a run is treated as noise rather than a line. */ + private static final float MIN_FONT_SIZE = 0.5f; + + public List extract(PDDocument document) throws IOException { + LineCollector collector = new LineCollector(); + collector.setSortByPosition(true); + collector.setStartPage(1); + collector.setEndPage(document.getNumberOfPages()); + collector.writeText(document, Writer.nullWriter()); + + List pages = new ArrayList<>(document.getNumberOfPages()); + for (int i = 0; i < document.getNumberOfPages(); i++) { + PDPage page = document.getPage(i); + List ops = collector.opsFor(i); + List lines = collector.linesFor(i); + boolean dropped = false; + if (ops.size() < maxOrdinal(lines) + 1) { + // Untrusted ordinals: drop the lines so the page is untaggable rather than + // mis-tagged, and flag it so the caller refuses to declare conformance. + log.warn( + "Ordinal mismatch on page {} (ops={}, text={}); skipping page", + i, + ops.size(), + maxOrdinal(lines) + 1); + dropped = !lines.isEmpty(); + lines = List.of(); + } + pages.add( + new PageContent( + i, + lines, + ops, + ops.size(), + collector.preMarkedOn(i), + collector.textSemanticsOn(i), + dropped, + normalisedBox(page))); + } + return pages; + } + + /** + * The page box in the space of extracted line coordinates: origin-zero, width and height + * swapped for 90/270 rotations, because the text engine reports in the rotated frame. + */ + static BBox normalisedBox(PDPage page) { + PDRectangle mediaBox = page.getMediaBox(); + boolean sideways = page.getRotation() % 180 != 0; + float width = sideways ? mediaBox.getHeight() : mediaBox.getWidth(); + float height = sideways ? mediaBox.getWidth() : mediaBox.getHeight(); + return new BBox(0, 0, width, height); + } + + /** Counts images with the token scan alone, skipping the expensive text pass. */ + public int countGraphics(PDDocument document) { + int total = 0; + for (int i = 0; i < document.getNumberOfPages(); i++) { + try { + PDResources resources = document.getPage(i).getResources(); + PDFStreamParser parser = new PDFStreamParser(document.getPage(i)); + List operands = new ArrayList<>(); + Object token; + while ((token = parser.parseNextToken()) != null) { + if (!(token instanceof Operator operator)) { + operands.add((COSBase) token); + continue; + } + if (isGraphicOperator(operator.getName(), operands, resources)) { + total++; + } + operands.clear(); + } + } catch (IOException e) { + log.debug("Could not scan page {} for graphics: {}", i, e.getMessage()); + } + } + return total; + } + + /** True for an inline image, or a Do that resolves to an image XObject. */ + private static boolean isGraphicOperator( + String name, List operands, PDResources resources) { + if ("BI".equals(name)) { + return true; + } + if (!"Do".equals(name) || resources == null || operands.size() != 1) { + return false; + } + if (!(operands.get(0) instanceof COSName resourceName)) { + return false; + } + try { + return resources.getXObject(resourceName) instanceof PDImageXObject; + } catch (IOException e) { + return false; + } + } + + private static int maxOrdinal(List lines) { + return lines.stream().mapToInt(TextLineInfo::endOrdinal).max().orElse(-1); + } + + static BBox toBBox(PDRectangle rect) { + return new BBox( + rect.getLowerLeftX(), + rect.getLowerLeftY(), + rect.getUpperRightX(), + rect.getUpperRightY()); + } + + // --- Operator classification ------------------------------------------- + + private static boolean isPathConstruction(String name) { + return switch (name) { + case "m", "l", "c", "v", "y", "re" -> true; + default -> false; + }; + } + + /** True when a sequence carries replacement or alternative text, which a rebuild would drop. */ + private static boolean carriesTextSemantics(List operands) { + for (COSBase operand : operands) { + if (operand instanceof COSDictionary dictionary + && (dictionary.containsKey(COSName.getPDFName("ActualText")) + || dictionary.containsKey(COSName.getPDFName("Alt")) + || dictionary.containsKey(COSName.E))) { + return true; + } + } + return false; + } + + /** + * Describes one markable operator, placed with the engine's own matrix rather than a + * hand-rolled q/Q/cm stack that would get nesting and form matrices wrong. + */ + private static MarkableOp classify( + String name, + List operands, + PDResources resources, + Matrix ctm, + BBox pathBox, + int ordinal) { + + if ("BI".equals(name)) { + return new MarkableOp(ordinal, MarkableOp.Kind.INLINE_IMAGE, unitSquare(ctm), null); + } + if (MarkableOp.isPathPainting(name)) { + return new MarkableOp(ordinal, MarkableOp.Kind.VECTOR, pathBox, null); + } + if (!"Do".equals(name)) { + return new MarkableOp(ordinal, MarkableOp.Kind.TEXT, BBox.EMPTY, null); + } + COSName resourceName = + operands.size() == 1 && operands.get(0) instanceof COSName n ? n : null; + if (resourceName == null || resources == null) { + return new MarkableOp(ordinal, MarkableOp.Kind.FORM, unitSquare(ctm), null); + } + try { + PDXObject xobject = resources.getXObject(resourceName); + if (xobject instanceof PDImageXObject) { + return new MarkableOp( + ordinal, MarkableOp.Kind.IMAGE, unitSquare(ctm), resourceName.getName()); + } + if (xobject instanceof PDFormXObject form) { + return new MarkableOp( + ordinal, MarkableOp.Kind.FORM, formBox(form, ctm), resourceName.getName()); + } + } catch (IOException e) { + log.debug("Could not resolve XObject {}: {}", resourceName.getName(), e.getMessage()); + } + return new MarkableOp( + ordinal, MarkableOp.Kind.FORM, unitSquare(ctm), resourceName.getName()); + } + + /** + * Extends the running path box with one path-construction operator's points; without it every + * vector had an empty box and charts and vector logos vanished from the structure tree. + */ + private static BBox extendPath(BBox current, String name, List operands, Matrix ctm) { + int pairs = + switch (name) { + case "m", "l" -> 1; + case "re" -> 2; + case "v", "y" -> 2; + case "c" -> 3; + default -> 0; + }; + if (pairs == 0 || operands.size() < pairs * 2) { + return current; + } + + // Deliberately allocation-free; the obvious version cost a third of the extraction budget. + float minX = current.isEmpty() ? Float.MAX_VALUE : current.x0(); + float minY = current.isEmpty() ? Float.MAX_VALUE : current.y0(); + float maxX = current.isEmpty() ? -Float.MAX_VALUE : current.x1(); + float maxY = current.isEmpty() ? -Float.MAX_VALUE : current.y1(); + + for (int pair = 0; pair < pairs; pair++) { + Float x = numberAt(operands, pair * 2); + Float y = numberAt(operands, pair * 2 + 1); + if (x == null || y == null) { + continue; + } + float px = x; + float py = y; + // "re" gives origin plus size, so the second pair is a corner offset from the first. + if ("re".equals(name) && pair == 1) { + Float ox = numberAt(operands, 0); + Float oy = numberAt(operands, 1); + if (ox == null || oy == null) { + continue; + } + px = ox + x; + py = oy + y; + } + float tx = ctm.getScaleX() * px + ctm.getShearX() * py + ctm.getTranslateX(); + float ty = ctm.getShearY() * px + ctm.getScaleY() * py + ctm.getTranslateY(); + minX = Math.min(minX, tx); + minY = Math.min(minY, ty); + maxX = Math.max(maxX, tx); + maxY = Math.max(maxY, ty); + } + return maxX < minX ? current : new BBox(minX, minY, maxX, maxY); + } + + private static Float numberAt(List operands, int index) { + return index < operands.size() + && operands.get(index) instanceof org.apache.pdfbox.cos.COSNumber number + ? number.floatValue() + : null; + } + + /** The unit square mapped through the CTM, which is how images are placed. */ + private static BBox unitSquare(Matrix ctm) { + return transformBox(new BBox(0, 0, 1, 1), ctm); + } + + private static BBox formBox(PDFormXObject form, Matrix ctm) { + PDRectangle box = form.getBBox(); + if (box == null) { + return unitSquare(ctm); + } + Matrix combined = form.getMatrix() != null ? form.getMatrix().multiply(ctm) : ctm; + return transformBox(toBBox(box), combined); + } + + private static BBox transformBox(BBox box, Matrix m) { + float[] xs = new float[4]; + float[] ys = new float[4]; + float[][] corners = { + {box.x0(), box.y0()}, {box.x1(), box.y0()}, + {box.x0(), box.y1()}, {box.x1(), box.y1()} + }; + for (int i = 0; i < 4; i++) { + Vector v = m.transform(new Vector(corners[i][0], corners[i][1])); + xs[i] = v.getX(); + ys[i] = v.getY(); + } + float minX = Math.min(Math.min(xs[0], xs[1]), Math.min(xs[2], xs[3])); + float maxX = Math.max(Math.max(xs[0], xs[1]), Math.max(xs[2], xs[3])); + float minY = Math.min(Math.min(ys[0], ys[1]), Math.min(ys[2], ys[3])); + float maxY = Math.max(Math.max(ys[0], ys[1]), Math.max(ys[2], ys[3])); + return new BBox(minX, minY, maxX, maxY); + } + + // --- Text pass --------------------------------------------------------- + + /** Marker recorded for each glyph so a finished line knows where it came from. */ + private record GlyphOrigin(int ordinal, boolean marked) {} + + private static final class LineCollector extends PDFTextStripper { + + private final Map> byPage = new HashMap<>(); + private final Map> opsByPage = new HashMap<>(); + private final Map preMarkedByPage = new HashMap<>(); + private final Map textSemanticsByPage = new HashMap<>(); + private final Map origins = new IdentityHashMap<>(); + private final List lineBuffer = new ArrayList<>(); + private final List lineWords = new ArrayList<>(); + private final StringBuilder lineText = new StringBuilder(); + + private int ordinal = -1; + private int markedDepth; + private int nestedDepth; + private BBox pathBox = BBox.EMPTY; + private int syntheticDepth; + private float pageHeight; + private int pageIndex; + + LineCollector() throws IOException { + super(); + } + + List linesFor(int index) { + return byPage.getOrDefault(index, List.of()); + } + + List opsFor(int index) { + return opsByPage.getOrDefault(index, List.of()); + } + + boolean preMarkedOn(int index) { + return preMarkedByPage.getOrDefault(index, false); + } + + boolean textSemanticsOn(int index) { + return textSemanticsByPage.getOrDefault(index, false); + } + + @Override + protected void startPage(PDPage page) throws IOException { + ordinal = -1; + markedDepth = 0; + nestedDepth = 0; + syntheticDepth = 0; + pathBox = BBox.EMPTY; + origins.clear(); + lineBuffer.clear(); + lineWords.clear(); + lineText.setLength(0); + // Dir-adjusted glyph coordinates live in the rotated frame, so the flip must too. + pageHeight = normalisedBox(page).height(); + pageIndex = getCurrentPageNo() - 1; + super.startPage(page); + } + + @Override + protected void endPage(PDPage page) throws IOException { + flushLine(); + super.endPage(page); + } + + /** + * Counts only operators physically present in the page's own stream: PDFBox re-enters here + * with synthetic calls for {@code '} and {@code "}, and descends into form XObjects. + */ + @Override + protected void processOperator(Operator operator, List operands) + throws IOException { + String name = operator.getName(); + if (nestedDepth == 0 && syntheticDepth == 0) { + if (isPathConstruction(name)) { + pathBox = + extendPath( + pathBox, + name, + operands, + getGraphicsState().getCurrentTransformationMatrix()); + } + if (MarkableOp.isMarkableOperator(name)) { + ordinal++; + // Classified here rather than in a second parse of the same stream: the engine + // already has the operands and the live transformation matrix. + opsByPage + .computeIfAbsent(pageIndex, k -> new ArrayList<>()) + .add( + classify( + name, + operands, + getResources(), + getGraphicsState().getCurrentTransformationMatrix(), + pathBox, + ordinal)); + if (MarkableOp.isPathPainting(name)) { + pathBox = BBox.EMPTY; + } + } else if ("n".equals(name)) { + pathBox = BBox.EMPTY; + } else if ("BDC".equals(name) || "BMC".equals(name)) { + markedDepth++; + preMarkedByPage.put(pageIndex, true); + if (carriesTextSemantics(operands)) { + textSemanticsByPage.put(pageIndex, true); + } + } else if ("EMC".equals(name) && markedDepth > 0) { + markedDepth--; + } + } + boolean synthesises = "'".equals(name) || "\"".equals(name); + if (synthesises) { + syntheticDepth++; + } + try { + super.processOperator(operator, operands); + } finally { + if (synthesises) { + syntheticDepth--; + } + } + } + + @Override + public void showForm(PDFormXObject form) throws IOException { + nestedDepth++; + try { + super.showForm(form); + } finally { + nestedDepth--; + } + } + + @Override + public void showTransparencyGroup(PDTransparencyGroup group) throws IOException { + nestedDepth++; + try { + super.showTransparencyGroup(group); + } finally { + nestedDepth--; + } + } + + @Override + protected void showType3Glyph( + Matrix textRenderingMatrix, + PDType3Font font, + int code, + org.apache.pdfbox.util.Vector displacement) + throws IOException { + nestedDepth++; + try { + super.showType3Glyph(textRenderingMatrix, font, code, displacement); + } finally { + nestedDepth--; + } + } + + @Override + protected void processChildStream( + org.apache.pdfbox.contentstream.PDContentStream contentStream, PDPage page) + throws IOException { + nestedDepth++; + try { + super.processChildStream(contentStream, page); + } finally { + nestedDepth--; + } + } + + @Override + protected void processTextPosition(TextPosition text) { + origins.put(text, new GlyphOrigin(ordinal, markedDepth > 0)); + super.processTextPosition(text); + } + + @Override + protected void writeString(String text, List positions) { + lineText.append(text); + lineBuffer.addAll(positions); + WordInfo word = buildWord(text, positions); + if (word != null) { + lineWords.add(word); + } + } + + private WordInfo buildWord(String text, List positions) { + if (text == null || text.isBlank() || positions.isEmpty()) { + return null; + } + Bounds bounds = new Bounds(); + for (TextPosition tp : positions) { + bounds.accept(tp, pageHeight, origins.get(tp)); + } + if (bounds.end < 0) { + return null; + } + return new WordInfo( + text, + bounds.box(), + bounds.start, + bounds.end, + bounds.dominantSize(), + bounds.bold); + } + + @Override + protected void writeWordSeparator() { + lineText.append(' '); + } + + @Override + protected void writeLineSeparator() { + flushLine(); + } + + @Override + protected void writeParagraphSeparator() { + flushLine(); + } + + private void flushLine() { + if (lineBuffer.isEmpty()) { + lineText.setLength(0); + lineWords.clear(); + return; + } + TextLineInfo line = buildLine(); + lineBuffer.clear(); + lineWords.clear(); + lineText.setLength(0); + if (line != null) { + byPage.computeIfAbsent(pageIndex, k -> new ArrayList<>()).add(line); + } + } + + private TextLineInfo buildLine() { + String text = lineText.toString(); + if (text.isBlank()) { + return null; + } + Bounds bounds = new Bounds(); + for (TextPosition tp : lineBuffer) { + bounds.accept(tp, pageHeight, origins.get(tp)); + } + if (bounds.end < 0) { + return null; + } + return new TextLineInfo( + pageIndex, + text, + bounds.box(), + bounds.dominantSize(), + bounds.bold, + bounds.start, + bounds.end, + bounds.marked, + List.copyOf(lineWords)); + } + } + + /** Accumulates glyph geometry, ordinals and font signals for a word or a line. */ + private static final class Bounds { + private float minX = Float.MAX_VALUE; + private float maxX = -Float.MAX_VALUE; + private float minY = Float.MAX_VALUE; + private float maxY = -Float.MAX_VALUE; + private int start = Integer.MAX_VALUE; + private int end = -1; + private boolean marked; + private boolean bold; + private final Map sizeCounts = new HashMap<>(); + + void accept(TextPosition tp, float pageHeight, GlyphOrigin origin) { + float top = pageHeight - tp.getYDirAdj(); + float bottom = top - Math.max(tp.getHeightDir(), 0); + minX = Math.min(minX, tp.getXDirAdj()); + maxX = Math.max(maxX, tp.getXDirAdj() + tp.getWidthDirAdj()); + minY = Math.min(minY, bottom); + maxY = Math.max(maxY, top); + + if (origin != null) { + start = Math.min(start, origin.ordinal()); + end = Math.max(end, origin.ordinal()); + marked |= origin.marked(); + } + float size = tp.getFontSizeInPt(); + if (size > MIN_FONT_SIZE) { + sizeCounts.merge(round(size), 1, Integer::sum); + } + bold |= isBold(tp); + } + + BBox box() { + return new BBox(minX, minY, maxX, maxY); + } + + float dominantSize() { + return sizeCounts.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(0f); + } + + private static float round(float value) { + return Math.round(value * 10f) / 10f; + } + + private static boolean isBold(TextPosition tp) { + if (tp.getFont() == null) { + return false; + } + String name = tp.getFont().getName(); + if (name != null && name.toLowerCase().contains("bold")) { + return true; + } + PDFontDescriptor descriptor = tp.getFont().getFontDescriptor(); + return descriptor != null + && (descriptor.getFontWeight() >= 600 || descriptor.isForceBold()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingOptions.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingOptions.java new file mode 100644 index 0000000000..bba15ed09f --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingOptions.java @@ -0,0 +1,63 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.Map; + +import lombok.Builder; +import lombok.Getter; + +/** Inputs that change how a document is tagged. */ +@Getter +@Builder(toBuilder = true) +public class TaggingOptions { + + /** What to do when the source already has a structure tree. */ + public enum ExistingTags { + /** Leave the tree alone and fix only document-level requirements. */ + KEEP, + /** Discard the tree and derive a new one. */ + REBUILD, + /** Keep a usable tree, rebuild an empty or trivially broken one. */ + AUTO + } + + /** How images with no alternative description are handled. */ + public enum FigurePolicy { + /** Leave undescribed so validation fails honestly; a faked {@code /Alt} helps nobody. */ + REQUIRE_ALT, + /** Treat every image as decoration and mark it as an artifact. */ + MARK_DECORATIVE + } + + @Builder.Default private PdfUaProfile profile = PdfUaProfile.UA1; + + /** BCP-47 language tag for the document, for example {@code en-GB}. */ + private String language; + + /** Replace a language the document already declares. Off, so a French file stays French. */ + @Builder.Default private boolean overrideLanguage = false; + + private String title; + + /** Last resort when no title is given and none can be derived; pass the uploaded filename. */ + private String fallbackTitle; + + /** Embed any font the document references but does not carry, which clause 7.21 requires. */ + @Builder.Default private boolean embedFonts = true; + + /** Leave the PDF version alone; raising it would break PDF/A-1, defined on PDF 1.4. */ + @Builder.Default private boolean preservePdfVersion = false; + + @Builder.Default private ExistingTags existingTags = ExistingTags.AUTO; + + @Builder.Default private FigurePolicy figurePolicy = FigurePolicy.REQUIRE_ALT; + + /** Alternative descriptions supplied by the caller, keyed by "pageIndex:ordinal". */ + @Builder.Default private Map altTextByFigure = Map.of(); + + /** What the document said before font embedding rewrote it; see {@link SourceFacts}. */ + @Builder.Default private SourceFacts sourceFacts = SourceFacts.NONE; + + public String altTextFor(int pageIndex, int ordinal) { + return altTextByFigure.get(pageIndex + ":" + ordinal); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingResult.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingResult.java new file mode 100644 index 0000000000..d54fad5de8 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TaggingResult.java @@ -0,0 +1,42 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; + +/** What a tagging run produced, for the conversion report. */ +@Getter +public class TaggingResult { + + private final List warnings = new ArrayList<>(); + private final DocumentStructure structure; + private final boolean rebuilt; + private final int taggedElements; + private final int artifacts; + private final int figuresNeedingAlt; + + /** True when text was hidden as artifacts; the caller must not declare conformance. */ + private final boolean contentSuppressed; + + public TaggingResult(DocumentStructure structure, boolean rebuilt) { + this.structure = structure; + this.rebuilt = rebuilt; + this.warnings.addAll(structure.getWarnings()); + int[] elements = {0}; + structure.visit( + block -> { + if (!block.isArtifact()) { + elements[0]++; + } + }); + this.taggedElements = elements[0]; + this.artifacts = structure.artifactCount(); + this.figuresNeedingAlt = structure.figuresWithoutAlt().size(); + this.contentSuppressed = structure.isTextSuppressed(); + } + + public boolean needsHumanReview() { + return figuresNeedingAlt > 0 || !warnings.isEmpty(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TextLineInfo.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TextLineInfo.java new file mode 100644 index 0000000000..67f37d1c8d --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/TextLineInfo.java @@ -0,0 +1,42 @@ +package stirling.software.proprietary.pdf.ua; + +import java.util.List; + +/** + * A run of text on one baseline, with the operator ordinals that produced it. {@code preMarked} + * means the source stream already wrapped this text in BDC/EMC. + */ +public record TextLineInfo( + int pageIndex, + String text, + BBox bbox, + float dominantFontSize, + boolean bold, + int startOrdinal, + int endOrdinal, + boolean preMarked, + List words) { + + public boolean isBlank() { + return text == null || text.isBlank(); + } + + public int charCount() { + return text == null ? 0 : text.strip().length(); + } + + public int wordCount() { + return (int) words.stream().filter(w -> !w.isBlank()).count(); + } + + /** True when every word occupies its own operator run, so cells can be tagged separately. */ + public boolean wordsAreSeparable() { + List real = words.stream().filter(w -> !w.isBlank()).toList(); + for (int i = 1; i < real.size(); i++) { + if (!real.get(i - 1).isSeparableFrom(real.get(i))) { + return false; + } + } + return true; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/WordInfo.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/WordInfo.java new file mode 100644 index 0000000000..515775b6f8 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/ua/WordInfo.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.pdf.ua; + +/** + * A whitespace-delimited run of glyphs, with the operator ordinals that produced it. Cell detection + * needs both: geometry to find cells, ordinals to tell whether they can be tagged separately. + */ +public record WordInfo( + String text, BBox bbox, int startOrdinal, int endOrdinal, float fontSize, boolean bold) { + + public boolean isBlank() { + return text == null || text.isBlank(); + } + + /** True when this word shares no operator with the other, so both can carry their own MCID. */ + public boolean isSeparableFrom(WordInfo other) { + return endOrdinal < other.startOrdinal || other.endOrdinal < startOrdinal; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/InProcessPolicyAssetStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/InProcessPolicyAssetStore.java new file mode 100644 index 0000000000..35f20aa15a --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/InProcessPolicyAssetStore.java @@ -0,0 +1,82 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory {@link PolicyAssetStore} for tests and any future no-database mode. {@link + * JpaPolicyAssetStore} is the runtime bean. + */ +public class InProcessPolicyAssetStore implements PolicyAssetStore { + + private final Map assets = new ConcurrentHashMap<>(); + private final Map contents = new ConcurrentHashMap<>(); + + @Override + public PolicyAsset save(PolicyAsset asset, byte[] content) { + String id = + asset.id() == null || asset.id().isBlank() + ? UUID.randomUUID().toString() + : asset.id(); + PolicyAsset stored = + new PolicyAsset( + id, + asset.fileName(), + asset.contentType(), + content.length, + asset.owner(), + asset.teamId(), + asset.createdAt()); + assets.put(id, stored); + contents.put(id, content); + return stored; + } + + @Override + public Optional get(String id) { + return Optional.ofNullable(assets.get(id)); + } + + @Override + public Optional content(String id) { + return Optional.ofNullable(contents.get(id)); + } + + @Override + public List findByTeam(Long teamId) { + return assets.values().stream() + .filter(asset -> Objects.equals(asset.teamId(), teamId)) + .sorted(newestFirst()) + .toList(); + } + + @Override + public List all() { + return assets.values().stream().sorted(newestFirst()).toList(); + } + + @Override + public List idsCreatedBefore(long cutoff) { + return assets.values().stream() + .filter(asset -> asset.createdAt() < cutoff) + .map(PolicyAsset::id) + .toList(); + } + + @Override + public boolean delete(String id) { + contents.remove(id); + return assets.remove(id) != null; + } + + private static Comparator newestFirst() { + return Comparator.comparingLong(PolicyAsset::createdAt) + .reversed() + .thenComparing(PolicyAsset::id); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStore.java new file mode 100644 index 0000000000..65c6b5ed25 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStore.java @@ -0,0 +1,95 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +import stirling.software.proprietary.integration.crypto.CredentialEncryption; + +/** + * Durable {@link PolicyAssetStore} backed by JPA; the runtime store. Bytes are encrypted at rest + * here rather than by an attribute converter, so the metadata reads (list, validate, clean up) + * never decrypt - only {@link #content} does. + */ +@Service +@RequiredArgsConstructor +public class JpaPolicyAssetStore implements PolicyAssetStore { + + private final PolicyAssetRepository repository; + + @Override + @Transactional + public PolicyAsset save(PolicyAsset asset, byte[] content) { + String id = + asset.id() == null || asset.id().isBlank() + ? UUID.randomUUID().toString() + : asset.id(); + PolicyAssetEntity entity = new PolicyAssetEntity(); + entity.setId(id); + entity.setFileName(asset.fileName()); + entity.setContentType(asset.contentType()); + // Plaintext length: it is the size the UI shows, not the stored ciphertext's. + entity.setFileSize(content.length); + entity.setOwner(asset.owner()); + entity.setTeamId(asset.teamId()); + entity.setCreatedAt(asset.createdAt()); + entity.setData(CredentialEncryption.encryptBytes(content)); + repository.save(entity); + return toAsset(entity); + } + + @Override + public Optional get(String id) { + return repository.findMetaById(id); + } + + @Override + @Transactional(readOnly = true) + public Optional content(String id) { + // The only read of the data column, and so the only decrypt. + return repository + .findById(id) + .map(entity -> CredentialEncryption.decryptBytes(entity.getData())); + } + + @Override + public List findByTeam(Long teamId) { + return repository.findMetaByTeam(teamId); + } + + @Override + public List all() { + return repository.findAllMeta(); + } + + @Override + public List idsCreatedBefore(long cutoff) { + return repository.findIdsCreatedBefore(cutoff); + } + + @Override + @Transactional + public boolean delete(String id) { + if (!repository.existsById(id)) { + return false; + } + repository.deleteById(id); + return true; + } + + private static PolicyAsset toAsset(PolicyAssetEntity entity) { + return new PolicyAsset( + entity.getId(), + entity.getFileName(), + entity.getContentType(), + entity.getFileSize(), + entity.getOwner(), + entity.getTeamId(), + entity.getCreatedAt()); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAsset.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAsset.java new file mode 100644 index 0000000000..697319a182 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAsset.java @@ -0,0 +1,17 @@ +package stirling.software.proprietary.policy.asset; + +/** + * Metadata for a stored supporting file (e.g. a watermark image, signing certificate, or overlay + * PDF) that pipeline steps reference from their {@code fileParameters}. The bytes live in the + * {@link PolicyAssetStore}; this record is what lists and API responses carry. Team-scoped like + * policies: {@code owner}/{@code teamId} are stamped server-side at upload ({@code null} when login + * is disabled). + */ +public record PolicyAsset( + String id, + String fileName, + String contentType, + long size, + String owner, + Long teamId, + long createdAt) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetCleaner.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetCleaner.java new file mode 100644 index 0000000000..582465e874 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetCleaner.java @@ -0,0 +1,124 @@ +package stirling.software.proprietary.policy.asset; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * Removes stored assets that no policy references any more, so replaced certificates/images don't + * accumulate. Runs after a policy save (assets its old version referenced but its new one dropped) + * and after a policy delete (everything the deleted policy referenced). An asset still referenced + * by any other policy in the team survives; assets belonging to a different team are never touched, + * whatever a policy claims. + * + *

Those two hooks only ever see ids a policy once referenced, so an upload abandoned before any + * policy bound it is reclaimed by {@link #sweepAbandonedUploads()} instead. Cleanup is best-effort + * throughout: the policy write has already committed, so a failure is logged, not propagated. + */ +@Slf4j +@Service +public class PolicyAssetCleaner { + + // An upload sits unreferenced until the save that binds it, so the window has to outlast a + // builder session comfortably. + private static final Duration ABANDONED_UPLOAD_AGE = Duration.ofDays(1); + + private final PolicyAssetStore assetStore; + private final PolicyStore policyStore; + private final Supplier clock; + + @Autowired + public PolicyAssetCleaner(PolicyAssetStore assetStore, PolicyStore policyStore) { + this(assetStore, policyStore, Instant::now); + } + + // Clock seam so tests can pin "now"; the runtime bean uses the wall clock above. + PolicyAssetCleaner( + PolicyAssetStore assetStore, PolicyStore policyStore, Supplier clock) { + this.assetStore = assetStore; + this.policyStore = policyStore; + this.clock = clock; + } + + /** + * After an update: drop assets the previous version referenced and the new one no longer does. + */ + public void cleanupAfterSave(Policy previous, Policy saved) { + if (previous == null) { + return; + } + Set dropped = new HashSet<>(PolicyAssetRefs.referencedAssetIds(previous.steps())); + dropped.removeAll(PolicyAssetRefs.referencedAssetIds(saved.steps())); + deleteUnreferenced(saved.teamId(), dropped); + } + + /** After a delete: drop everything the deleted policy referenced, if now unreferenced. */ + public void cleanupAfterDelete(Policy deleted) { + deleteUnreferenced(deleted.teamId(), PolicyAssetRefs.referencedAssetIds(deleted.steps())); + } + + /** + * Uploads no policy ever bound: the save/delete hooks never see these ids, so without this they + * would keep their (up to 50 MB) bytes forever. Deliberately not team-scoped - an abandoned + * upload is unreferenced everywhere or nowhere. Runs at startup too, so an instance restarted + * more often than daily still reclaims; the age cutoff is what keeps fresh uploads safe. + */ + @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS) + public void sweepAbandonedUploads() { + long cutoff = clock.get().minus(ABANDONED_UPLOAD_AGE).toEpochMilli(); + try { + for (String id : assetStore.idsCreatedBefore(cutoff)) { + if (!policyStore.anyPolicyReferences(id) && assetStore.delete(id)) { + log.debug("Deleted abandoned policy asset {}", id); + } + } + } catch (RuntimeException e) { + log.warn("Abandoned policy asset sweep failed: {}", e.getMessage(), e); + } + } + + private void deleteUnreferenced(Long teamId, Set candidates) { + if (candidates.isEmpty()) { + return; + } + try { + Set stillReferenced = new HashSet<>(); + for (Policy policy : policyStore.findByTeam(teamId)) { + stillReferenced.addAll(PolicyAssetRefs.referencedAssetIds(policy.steps())); + } + for (String id : candidates) { + if (stillReferenced.contains(id)) { + continue; + } + assetStore + .get(id) + .filter(asset -> Objects.equals(asset.teamId(), teamId)) + .ifPresent( + asset -> { + assetStore.delete(id); + log.debug( + "Deleted unreferenced policy asset {} ({})", + id, + asset.fileName()); + }); + } + } catch (RuntimeException e) { + // The save/delete already committed: failing here must not fail the request or skip + // the caller's trigger re-sync. + log.warn("Policy asset cleanup failed for team {}: {}", teamId, e.getMessage(), e); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetController.java new file mode 100644 index 0000000000..14349fd64c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetController.java @@ -0,0 +1,187 @@ +package stirling.software.proprietary.policy.asset; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import io.github.pixee.security.Filenames; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.policy.config.PolicyAccessGuard; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * Stored supporting files for pipelines: the certificate/image/overlay a step needs beyond its + * document stream. Uploaded when a pipeline is built, persisted server-side, and referenced from a + * step's {@code fileParameters} as {@code asset:} - so triggered and scheduled runs have the + * file without anyone re-supplying it. Team-scoped exactly like the policies that reference them. + */ +@RestController +@RequestMapping("/api/v1/policies/assets") +@Hidden +@RequiredArgsConstructor +@Tag(name = "Policies", description = "Run tool pipelines on the backend") +public class PolicyAssetController { + + /** Defensive cap; supporting files (certs, images, overlay PDFs) are far smaller. */ + private static final long MAX_ASSET_BYTES = 50L * 1024 * 1024; + + private final PolicyAssetStore assetStore; + private final PolicyStore policyStore; + private final PolicyAccessGuard policyAccessGuard; + private final PolicyManagementAuthority policyManagementAuthority; + private final ApplicationProperties applicationProperties; + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Upload a pipeline supporting file", + description = + "Stores a supporting file (multipart field 'file') for pipeline steps to" + + " reference from their fileParameters as 'asset:', and returns" + + " its metadata including the assigned id.") + public ResponseEntity upload(@RequestPart("file") MultipartFile file) + throws IOException { + requirePolicyEditingAllowed(); + if (file == null || file.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Uploaded file is empty"); + } + if (file.getSize() > MAX_ASSET_BYTES) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Supporting files may be at most " + (MAX_ASSET_BYTES / (1024 * 1024)) + " MB"); + } + String fileName = Filenames.toSimpleFileName(file.getOriginalFilename()); + if (fileName == null || fileName.isBlank()) { + fileName = "asset"; + } + PolicyAsset meta = + new PolicyAsset( + null, + fileName, + file.getContentType(), + file.getSize(), + policyAccessGuard.ownerForNewPolicy(), + policyAccessGuard.teamForNewPolicy(), + System.currentTimeMillis()); + return ResponseEntity.ok(assetStore.save(meta, file.getBytes())); + } + + @GetMapping + @Operation( + summary = "List stored supporting files", + description = + "Lists the supporting files visible to the caller's team (metadata only), so" + + " the builder can show which file a step's binding points at.") + public List list() { + return policyAccessGuard.visibleFrom(assetStore); + } + + @GetMapping("/{assetId}/content") + @Operation( + summary = "Download a stored supporting file", + description = + "Returns the asset's bytes with its stored content type and filename. Gated" + + " like upload and delete: supporting files include signing" + + " certificates, so reading the bytes back needs the same authority" + + " that put them there, not merely team membership.") + public ResponseEntity content(@PathVariable String assetId) { + requirePolicyEditingAllowed(); + PolicyAsset asset = accessibleAsset(assetId); + byte[] bytes = + assetStore + .content(assetId) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, "No asset: " + assetId)); + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + try { + if (asset.contentType() != null) { + mediaType = MediaType.parseMediaType(asset.contentType()); + } + } catch (RuntimeException ignored) { + // Stored content type unparsable: serve as a generic binary. + } + return ResponseEntity.ok() + .contentType(mediaType) + .header( + HttpHeaders.CONTENT_DISPOSITION, + // UTF-8 so a non-ASCII filename encodes per RFC 5987 instead of mangling. + ContentDisposition.attachment() + .filename(asset.fileName(), StandardCharsets.UTF_8) + .build() + .toString()) + .body(new ByteArrayResource(bytes)); + } + + @DeleteMapping("/{assetId}") + @Operation( + summary = "Delete a stored supporting file", + description = + "Removes an asset no pipeline references. An asset still referenced by a" + + " pipeline's step returns 409 - remove or replace the binding first." + + " (Assets are also cleaned up automatically when the pipelines" + + " referencing them are saved without them or deleted.)") + public ResponseEntity delete(@PathVariable String assetId) { + requirePolicyEditingAllowed(); + accessibleAsset(assetId); + boolean referenced = + policyAccessGuard.visibleFrom(policyStore).stream() + .anyMatch( + policy -> + PolicyAssetRefs.referencedAssetIds(policy.steps()) + .contains(assetId)); + if (referenced) { + throw new ResponseStatusException( + HttpStatus.CONFLICT, "Asset is still referenced by a pipeline step"); + } + assetStore.delete(assetId); + return ResponseEntity.noContent().build(); + } + + /** The asset, scoped to the caller's team — another team's asset reads as not-found. */ + private PolicyAsset accessibleAsset(String assetId) { + return assetStore + .get(assetId) + .filter(policyAccessGuard::canAccess) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, "No asset: " + assetId)); + } + + /** Same gate as policy edits (see {@code PolicyController#requirePolicyEditingAllowed}). */ + private void requirePolicyEditingAllowed() { + if (!applicationProperties.getSecurity().isEnableLogin()) { + return; + } + if (!policyManagementAuthority.canEditPolicies()) { + throw new ResponseStatusException( + HttpStatus.FORBIDDEN, + "Policies may only be created or modified by a team leader"); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetEntity.java new file mode 100644 index 0000000000..b535cc1971 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetEntity.java @@ -0,0 +1,58 @@ +package stirling.software.proprietary.policy.asset; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * JPA row for a {@link PolicyAsset} plus its bytes. Stored in the database (not on disk) so + * multi-node deployments see the same assets regardless of which node stored or runs a policy. + * {@code owner} and {@code teamId} are plain values, not foreign keys, matching {@code + * PolicyEntity}. + */ +@Entity +@Table(name = "policy_assets") +@NoArgsConstructor +@Getter +@Setter +public class PolicyAssetEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "id") + private String id; + + @Column(name = "file_name") + private String fileName; + + @Column(name = "content_type") + private String contentType; + + // Not "size": a reserved word in Oracle, and an HQL keyword the metadata projections would + // read as the collection-size function. + @Column(name = "file_size") + private long fileSize; + + @Column(name = "owner") + private String owner; + + @Column(name = "team_id") + private Long teamId; + + @Column(name = "created_at") + private long createdAt; + + // Ciphertext; JpaPolicyAssetStore is the only place these bytes are encrypted or decrypted. + @Lob + @Column(name = "data", nullable = false, columnDefinition = "bytea") + private byte[] data; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRefs.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRefs.java new file mode 100644 index 0000000000..1acf3d7c76 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRefs.java @@ -0,0 +1,63 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import stirling.software.proprietary.policy.model.PipelineStep; + +/** + * Parses stored-asset references out of pipeline steps. A step's {@code fileParameters} value takes + * one of two forms: + * + *

    + *
  • {@code asset:[,...]} - stored assets, loaded from the asset store at run time. + * Several ids appear when one tool field carries multiple files (e.g. attachments). + *
  • anything else - the name of a file supplied with the run itself (the multipart {@code + * assets[i].key} form), which is all a binding could mean before assets could be stored. + *
+ * + *

The prefix is what keeps the two apart: without it a run-supplied key reads as a missing asset + * id, and a stored binding can be satisfied by whatever file the run supplies. The executor looks + * supporting files up by the whole value, so resolution keeps it as the map key and only splits it + * to load each asset. + */ +public final class PolicyAssetRefs { + + /** Marks a {@code fileParameters} value as stored asset ids rather than a run-supplied key. */ + public static final String PREFIX = "asset:"; + + private PolicyAssetRefs() {} + + /** Whether a {@code fileParameters} value names stored assets. */ + public static boolean isAssetRef(String fileParameterValue) { + return fileParameterValue != null && fileParameterValue.startsWith(PREFIX); + } + + /** The individual asset ids inside one {@code fileParameters} value; none if it isn't a ref. */ + public static List assetIds(String fileParameterValue) { + List ids = new ArrayList<>(); + if (!isAssetRef(fileParameterValue)) { + return ids; + } + for (String token : fileParameterValue.substring(PREFIX.length()).split(",")) { + String id = token.trim(); + if (!id.isEmpty()) { + ids.add(id); + } + } + return ids; + } + + /** Every asset id referenced by any step's {@code fileParameters}, in encounter order. */ + public static Set referencedAssetIds(List steps) { + Set ids = new LinkedHashSet<>(); + for (PipelineStep step : steps) { + for (String value : step.fileParameters().values()) { + ids.addAll(assetIds(value)); + } + } + return ids; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRepository.java new file mode 100644 index 0000000000..e423ad01ef --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetRepository.java @@ -0,0 +1,42 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PolicyAssetRepository extends JpaRepository { + + /** + * Metadata projection. Lists, validation and cleanup need only the fields, and selecting whole + * entities would drag every asset's LOB (up to 50 MB each) along with them. + */ + String META = + "select new stirling.software.proprietary.policy.asset.PolicyAsset(a.id, a.fileName," + + " a.contentType, a.fileSize, a.owner, a.teamId, a.createdAt) from" + + " PolicyAssetEntity a"; + + @Query(META + " where a.id = :id") + Optional findMetaById(@Param("id") String id); + + /** + * Assets belonging to a team, newest first. A {@code null} teamId matches the rows with no team + * (login-disabled data), mirroring {@code PolicyRepository#findByTeam}. + */ + @Query( + META + + " where ((:teamId is null and a.teamId is null) or a.teamId = :teamId) order" + + " by a.createdAt desc, a.id asc") + List findMetaByTeam(@Param("teamId") Long teamId); + + @Query(META + " order by a.createdAt desc, a.id asc") + List findAllMeta(); + + /** Ids of assets uploaded before {@code cutoff}, for the abandoned-upload sweep. */ + @Query("select a.id from PolicyAssetEntity a where a.createdAt < :cutoff") + List findIdsCreatedBefore(@Param("cutoff") long cutoff); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetResolver.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetResolver.java new file mode 100644 index 0000000000..862d3511a7 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetResolver.java @@ -0,0 +1,112 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyInputs; + +/** + * Loads a stored policy's referenced supporting files into a run's inputs, so a triggered or + * scheduled run has the certificate/image/overlay its steps need without anyone uploading it at run + * time. Assets are matched to the policy's own team (both stamped server-side), so a forged asset + * id in a policy JSON can't pull another team's file - run-time has no principal to check instead. + * + *

Stored assets win: a binding the policy stores is overwritten with the policy's own asset, so + * a member who can run the policy (runs are not leader-gated) can't swap the pinned certificate by + * posting {@code assets[i].key=}. A binding that names no stored asset keeps whatever the run + * supplied, which is what a binding meant before assets could be stored. + * + *

Resolution is all or nothing per binding: a partly resolvable one is dropped so it surfaces as + * the executor's existing missing-supporting-file error, rather than running the step short a file. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PolicyAssetResolver { + + private final PolicyAssetStore assetStore; + + /** Inputs with the policy's stored assets merged in under each step's asset key. */ + public PolicyInputs resolve(Policy policy, PolicyInputs inputs) { + Map> supporting = null; + // Track loaded keys separately: `supporting` starts as a copy of the run's own files, so + // it can't tell "already loaded" from "the run supplied this one" - which is what we + // deliberately overwrite. + Set loaded = new HashSet<>(); + for (PipelineStep step : policy.steps()) { + for (String assetKey : step.fileParameters().values()) { + if (assetKey == null || assetKey.isBlank() || !loaded.add(assetKey)) { + continue; + } + List resources = load(policy, assetKey); + if (resources.isEmpty()) { + // A stored binding that has gone dead must not fall back to whatever the run + // posted under its key: that is the override this class exists to prevent. + if (PolicyAssetRefs.isAssetRef(assetKey) + && inputs.supportingFiles().containsKey(assetKey)) { + if (supporting == null) { + supporting = new LinkedHashMap<>(inputs.supportingFiles()); + } + supporting.remove(assetKey); + } + continue; + } + if (supporting == null) { + supporting = new LinkedHashMap<>(inputs.supportingFiles()); + } + supporting.put(assetKey, resources); + } + } + return supporting == null ? inputs : new PolicyInputs(inputs.primary(), supporting); + } + + /** Every id or none: a part-loaded binding would run the step with fewer files than it asks. */ + private List load(Policy policy, String assetKey) { + List resources = new ArrayList<>(); + for (String id : PolicyAssetRefs.assetIds(assetKey)) { + PolicyAsset asset = assetStore.get(id).orElse(null); + if (asset == null || !Objects.equals(asset.teamId(), policy.teamId())) { + log.warn( + "Policy {} references stored asset {} which is missing or inaccessible;" + + " dropping its binding", + policy.id(), + id); + return List.of(); + } + byte[] content = assetStore.content(id).orElse(null); + if (content == null) { + log.warn( + "Policy {} stored asset {} has no content; dropping its binding", + policy.id(), + id); + return List.of(); + } + resources.add(named(content, asset.fileName())); + } + return resources; + } + + /** The asset bytes as a Resource carrying its original filename (tools read the extension). */ + private static Resource named(byte[] content, String fileName) { + return new ByteArrayResource(content) { + @Override + public String getFilename() { + return fileName; + } + }; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetStore.java new file mode 100644 index 0000000000..f5406e8980 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/asset/PolicyAssetStore.java @@ -0,0 +1,32 @@ +package stirling.software.proprietary.policy.asset; + +import java.util.List; +import java.util.Optional; + +/** + * Persistence for pipeline supporting files. Metadata and bytes are stored together but read + * separately: lists and validation only need {@link PolicyAsset}, while a run loads {@link + * #content} for just the assets its steps reference. + */ +public interface PolicyAssetStore { + + /** Persist an asset (a blank id is assigned) and return the stored metadata. */ + PolicyAsset save(PolicyAsset asset, byte[] content); + + Optional get(String id); + + /** The asset's bytes, or empty if the id is unknown. */ + Optional content(String id); + + /** Assets belonging to a team, newest first. {@code null} matches no-team (login-disabled). */ + List findByTeam(Long teamId); + + /** All assets, for team-scoping-off (login-disabled) reads. */ + List all(); + + /** Ids of assets uploaded before {@code cutoff} (epoch millis), for orphan reclamation. */ + List idsCreatedBefore(long cutoff); + + /** Remove an asset. Returns false if the id was unknown. */ + boolean delete(String id); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java index a49c8e5aa9..6d8229aa26 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthority.java @@ -26,6 +26,11 @@ public class AdminPolicyManagementAuthority implements PolicyManagementAuthority return userService.isCurrentUserAdmin(); } + @Override + public boolean canTriggerPolicies() { + return userService.isCurrentUserAdmin(); + } + @Override public Long currentUserTeamId() { String username = userService.getCurrentUsername(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java index 4fa90cc04b..2d7061c966 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyAccessGuard.java @@ -9,6 +9,8 @@ import lombok.RequiredArgsConstructor; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.UserServiceInterface; +import stirling.software.proprietary.policy.asset.PolicyAsset; +import stirling.software.proprietary.policy.asset.PolicyAssetStore; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.store.PolicyStore; @@ -58,6 +60,22 @@ public class PolicyAccessGuard { return store.findByTeam(policyManagementAuthority.currentUserTeamId()); } + /** Whether the stored asset belongs to the current user's team (same rule as policies). */ + public boolean canAccess(PolicyAsset asset) { + if (!enforced()) { + return true; + } + return Objects.equals(asset.teamId(), policyManagementAuthority.currentUserTeamId()); + } + + /** The stored assets visible to the caller, scoped exactly like {@link #visibleFrom}. */ + public List visibleFrom(PolicyAssetStore store) { + if (!enforced()) { + return store.all(); + } + return store.findByTeam(policyManagementAuthority.currentUserTeamId()); + } + private boolean enforced() { return applicationProperties.getSecurity().isEnableLogin(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java index 0ea3c298ad..d7e4f50ad1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/config/PolicyManagementAuthority.java @@ -12,6 +12,17 @@ public interface PolicyManagementAuthority { /** Whether the current user may create, edit, or delete policies (for their own team). */ boolean canEditPolicies(); + /** + * Whether the current user may run a policy against its configured sources (the manual + * "run now" sweep). Kept separate from {@link #canEditPolicies()} because the two are distinct + * capabilities, even where a deployment grants both to the same people: a sweep operates on the + * team's configured sources using the server's stored connection credentials, which makes it a + * policy-management capability rather than ordinary use. Running a policy over the caller's + * own uploaded files is not covered by this and stays open to every team member — that + * is ordinary editor enforcement. + */ + boolean canTriggerPolicies(); + /** * The team that scopes the current user's policies — the team a new policy is stamped with and * the only team whose policies the user may see/run/edit. {@code null} when it can't be diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java index 06172b8cbc..9b9fca4133 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/controller/PolicyController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.RequestContextHolder; @@ -44,10 +45,14 @@ import stirling.software.common.cluster.JobStore; import stirling.software.common.cluster.JobStoreEntry; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.job.JobResponse; +import stirling.software.common.model.tool.ToolDiagnostic; import stirling.software.common.service.JobOwnershipService; +import stirling.software.common.service.ToolChainValidator; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.proprietary.audit.AuditContext; +import stirling.software.proprietary.policy.asset.PolicyAssetCleaner; +import stirling.software.proprietary.policy.asset.PolicyAssetResolver; import stirling.software.proprietary.policy.config.PolicyAccessGuard; import stirling.software.proprietary.policy.config.PolicyManagementAuthority; import stirling.software.proprietary.policy.engine.PolicyRunHandle; @@ -59,6 +64,7 @@ import stirling.software.proprietary.policy.ledger.ProcessedLedger; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.PipelineDefinition; import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.PipelineValidation; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.PolicyInputs; import stirling.software.proprietary.policy.model.PolicyRun; @@ -68,6 +74,7 @@ import stirling.software.proprietary.policy.overview.PoliciesOverviewResponse; import stirling.software.proprietary.policy.overview.PolicyOverviewService; import stirling.software.proprietary.policy.progress.PolicyProgressListener; import stirling.software.proprietary.policy.source.EditorSource; +import stirling.software.proprietary.policy.source.Source; import stirling.software.proprietary.policy.source.SourceAccessGuard; import stirling.software.proprietary.policy.source.SourceDocCounter; import stirling.software.proprietary.policy.source.SourceStore; @@ -100,6 +107,8 @@ public class PolicyController { private final PolicyManagementAuthority policyManagementAuthority; private final PolicyTriggerManager policyTriggerManager; private final PolicyOverviewService policyOverviewService; + private final PolicyAssetCleaner assetCleaner; + private final PolicyAssetResolver assetResolver; private final ProcessedLedger processedLedger; private final List policyTriggers; private final ApplicationProperties applicationProperties; @@ -119,12 +128,13 @@ public class PolicyController { + " endpoint and download outputs via /api/v1/general/files/{id}.") public ResponseEntity> run( @RequestPart("json") PipelineDefinition definition, + @RequestParam(value = "policyId", required = false) String policyId, @Valid @ModelAttribute PolicyRunFiles files) throws IOException { stampPolicyAudit(definition); requireRunnable(definition); validateAdHocRun(definition); - PolicyInputs inputs = toInputs(files); + PolicyInputs inputs = resolveStoredAssets(policyId, toInputs(files)); PolicyRunHandle handle = policyRunner.runAdHoc(definition, inputs, PolicyProgressListener.NOOP); recordEditorDocs(inputs); @@ -140,12 +150,13 @@ public class PolicyController { + " 'cancelled', or 'waiting' event carrying the final run view.") public SseEmitter runStream( @RequestPart("json") PipelineDefinition definition, + @RequestParam(value = "policyId", required = false) String policyId, @Valid @ModelAttribute PolicyRunFiles files) throws IOException { stampPolicyAudit(definition); requireRunnable(definition); validateAdHocRun(definition); - PolicyInputs inputs = toInputs(files); + PolicyInputs inputs = resolveStoredAssets(policyId, toInputs(files)); SseEmitter emitter = new SseEmitter(applicationProperties.getPolicies().getStreamTimeoutMs()); @@ -238,6 +249,23 @@ public class PolicyController { // --- Policy management --- + @PostMapping(value = "/validate", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Check whether a chain of steps can run", + description = + "Reports which steps cannot accept what the step before them produces, without" + + " running anything or storing anything. Saving already rejects a" + + " chain whose steps cannot run; this answers the same question up" + + " front, and also returns the warnings and fan-out notes that saving" + + " does not.") + public PipelineValidation.Response validateChain( + @RequestBody PipelineValidation.Request request) { + List diagnostics = + policyValidator.diagnoseChain(request.steps(), request.sourceFormat()); + return new PipelineValidation.Response( + !ToolChainValidator.hasErrors(diagnostics), diagnostics); + } + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @Operation( summary = "Create or update a policy", @@ -248,12 +276,20 @@ public class PolicyController { requirePolicyEditingAllowed(); Policy owned = withStoredOutputSecrets(resolveOwnership(policy)); requireAccessibleSources(owned); + requireAccessibleOutput(owned); try { policyValidator.validate(owned); } catch (IllegalArgumentException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } + // Snapshot the previous version before saving so supporting files this edit dropped can + // be cleaned up once nothing references them. + Policy previous = + owned.id() == null || owned.id().isBlank() + ? null + : policyStore.get(owned.id()).orElse(null); Policy saved = policyStore.save(owned); + assetCleaner.cleanupAfterSave(previous, saved); // Re-sync trigger registrations now so a new/changed folder-watch policy starts being // watched immediately instead of after the next reconcile sweep. policyTriggerManager.notifyPoliciesChanged(); @@ -290,6 +326,40 @@ public class PolicyController { } } + /** + * A policy's output destination is a {@link Source} used as a write target: it must resolve to + * a source in the caller's team, so a client can neither reference a non-existent location nor + * reach across teams to write to another team's. The editor is virtual and has no writable + * location, so it can't be a destination. The config is then validated on this (request) thread + * so an S3 destination's connection is authorization-checked against the caller - the async + * delivery worker has no principal. A policy with no reference (inline / editor / one-off) has + * nothing to check. + */ + private void requireAccessibleOutput(Policy policy) { + for (String outputId : policy.outputIds()) { + Source destination = + sourceStore + .get(outputId) + .filter(sourceAccessGuard::canAccess) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Unknown or inaccessible output source: " + + outputId)); + if (EditorSource.TYPE.equals(destination.type())) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "The editor can't be used as an output destination"); + } + try { + policyValidator.validateOutput(destination.toOutputSpec()); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } + } + /** * Assign owner + owning team server-side. Create stamps the current user and their team; update * preserves the existing owner and team after verifying the policy belongs to the caller's team @@ -319,10 +389,10 @@ public class PolicyController { policy.name(), owner, policy.enabled(), - policy.trigger(), - policy.sourceIds(), + policy.inputs(), policy.steps(), policy.output(), + policy.outputIds(), teamId); } @@ -358,16 +428,7 @@ public class PolicyController { } private static Policy withOutput(Policy policy, OutputSpec output) { - return new Policy( - policy.id(), - policy.name(), - policy.owner(), - policy.enabled(), - policy.trigger(), - policy.sourceIds(), - policy.steps(), - output, - policy.teamId()); + return policy.withOutput(output); } /** @@ -376,21 +437,47 @@ public class PolicyController { * admin gets no say on SaaS. Team scoping (which team's policies) is enforced separately by * {@link PolicyAccessGuard}. Every mutation routes through {@link #savePolicy} (pause/resume * re-save with a flipped {@code enabled} flag) or {@link #deletePolicy}, so gating those two - * covers them all; runs ({@code /run}) stay open to the team. Single-user deployments (login - * disabled) have no such role, so they trust the local operator. The path allowlist for folder - * sources/outputs is enforced separately by {@link PolicyValidator} at validation time. + * covers them all; runs over the caller's own files ({@code /{id}/run}) stay open to the team, + * while source sweeps are gated by {@link #requirePolicySweepAllowed}. Single-user deployments + * (login disabled) have no such role, so they trust the local operator. The path allowlist for + * folder sources/outputs is enforced separately by {@link PolicyValidator} at validation time. */ private void requirePolicyEditingAllowed() { - if (!applicationProperties.getSecurity().isEnableLogin()) { - return; - } - if (!policyManagementAuthority.canEditPolicies()) { + if (!policyEditingAllowed()) { throw new ResponseStatusException( HttpStatus.FORBIDDEN, "Policies may only be created or modified by a team leader"); } } + /** + * Sweeping a policy's configured sources requires the same role as managing policies: the sweep + * operates on the team's configured sources using the server's stored connection credentials, + * which makes it a policy-management capability rather than ordinary use, and team scoping on + * its own does not express that. Deliberately narrower than it looks: it gates only the sweep, + * not {@link #runStoredPolicy}, because running a policy over documents the caller supplied is + * ordinary editor enforcement that every member performs on upload and export. + */ + private void requirePolicySweepAllowed() { + if (!applicationProperties.getSecurity().isEnableLogin()) { + return; + } + if (!policyManagementAuthority.canTriggerPolicies()) { + throw new ResponseStatusException( + HttpStatus.FORBIDDEN, + "Not permitted to run this policy against its configured sources"); + } + } + + /** + * Whether the caller may create/modify policies (a team leader, or any operator when login is + * off). + */ + private boolean policyEditingAllowed() { + return !applicationProperties.getSecurity().isEnableLogin() + || policyManagementAuthority.canEditPolicies(); + } + @GetMapping @Operation( summary = "List policies", @@ -450,10 +537,10 @@ public class PolicyController { public ResponseEntity deletePolicy(@PathVariable String policyId) { requirePolicyEditingAllowed(); // Scope to the caller's team: a policy in another team reads as not-found. - boolean accessible = - policyStore.get(policyId).filter(policyAccessGuard::canAccess).isPresent(); - if (accessible && policyStore.delete(policyId)) { + Policy policy = policyStore.get(policyId).filter(policyAccessGuard::canAccess).orElse(null); + if (policy != null && policyStore.delete(policyId)) { processedLedger.clearPolicy(policyId); + assetCleaner.cleanupAfterDelete(policy); // Cancel any now-orphaned folder watch promptly rather than leaving the WatchKey open // until the next reconcile sweep. policyTriggerManager.notifyPoliciesChanged(); @@ -487,8 +574,9 @@ public class PolicyController { description = "Runs the stored policy's pipeline on the supplied files (primary documents" + " under 'fileInput', supporting files under 'assets[i].key' /" - + " 'assets[i].file'). Runs regardless of the policy's enabled flag," - + " which only gates automatic triggering. Returns a run id.") + + " 'assets[i].file' - only for bindings the policy does not already" + + " store). Runs regardless of the policy's enabled flag, which only" + + " gates automatic triggering. Returns a run id.") public ResponseEntity> runStoredPolicy( @PathVariable String policyId, @Valid @ModelAttribute PolicyRunFiles files) throws IOException { @@ -514,8 +602,10 @@ public class PolicyController { + " the enabled flag (which only gates automatic triggering). Returns" + " the ids of the runs started (poll the run-status endpoint for each)" + " plus what the sweep skipped - already-processed, parked-by-failure," - + " and in-flight counts - so an empty result explains itself.") + + " and in-flight counts - so an empty result explains itself. Requires" + + " the policy-management role.") public ResponseEntity trigger(@PathVariable String policyId) { + requirePolicySweepAllowed(); Policy policy = policyStore .get(policyId) @@ -573,8 +663,9 @@ public class PolicyController { // step dereferences its connection by id on a principal-less worker thread, so this // request thread is the only place that reference can be checked against the caller. policyValidator.validateSteps(definition.steps()); - if (definition.output() != null) { - policyValidator.validateOutput(definition.output()); + // Every destination is checked; an ad-hoc run with no destinations validates nothing. + for (OutputSpec output : definition.outputs()) { + policyValidator.validateOutput(output); } } catch (IllegalArgumentException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); @@ -592,6 +683,25 @@ public class PolicyController { inputs.primary().size()); } + /** + * Resolve a test run's stored {@code asset:} bindings from the saved policy the builder is + * editing, so their bytes need not be re-uploaded. Scoped to that policy (the resolver loads + * only the assets it references, in its own team) and gated to policy editors - the same + * authority that can read asset bytes - so a member can't rebind a policy's stored asset into + * an ad-hoc step to read it back. A blank id (an unsaved pipeline has no stored bindings) or an + * inaccessible policy leaves the run-supplied inputs untouched. + */ + private PolicyInputs resolveStoredAssets(String policyId, PolicyInputs inputs) { + if (policyId == null || policyId.isBlank() || !policyEditingAllowed()) { + return inputs; + } + return policyStore + .get(policyId) + .filter(policyAccessGuard::canAccess) + .map(policy -> assetResolver.resolve(policy, inputs)) + .orElse(inputs); + } + /** * Turn the typed run files into engine {@link PolicyInputs}: the primary documents plus the * named supporting-file store, where each asset's {@code key} is the name a step references diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java index abdb69cb07..0d75866a6f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyEngine.java @@ -16,6 +16,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientResponseException; +import jakarta.annotation.PreDestroy; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,6 +32,9 @@ import stirling.software.common.service.ResourceMonitor; import stirling.software.common.service.TaskManager; import stirling.software.common.util.ExecutorFactory; import stirling.software.common.util.JobContext; +import stirling.software.proprietary.failure.FailureKind; +import stirling.software.proprietary.failure.PolicyFailureRecorder; +import stirling.software.proprietary.policy.asset.PolicyAssetResolver; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.PipelineDefinition; import stirling.software.proprietary.policy.model.Policy; @@ -37,6 +42,7 @@ import stirling.software.proprietary.policy.model.PolicyInputs; import stirling.software.proprietary.policy.model.PolicyRun; import stirling.software.proprietary.policy.model.WaitState; import stirling.software.proprietary.policy.output.OutputDelivery; +import stirling.software.proprietary.policy.output.PolicyOutputResolver; import stirling.software.proprietary.policy.output.PolicyOutputSink; import stirling.software.proprietary.policy.progress.PolicyProgressListener; import stirling.software.proprietary.service.DownstreamEntitlementError; @@ -69,14 +75,33 @@ public class PolicyEngine { private final PolicyExecutor stepExecutor; private final TaskManager taskManager; private final PolicyRunRegistry registry; + // Durable record of why a run failed. Best-effort by contract: see PolicyFailureRecorder. + private final PolicyFailureRecorder failureRecorder; private final FileStorage fileStorage; private final JobOwnershipService jobOwnershipService; private final List outputSinks; + private final PolicyOutputResolver outputResolver; private final ResourceMonitor resourceMonitor; private final JobQueue jobQueue; + private final PolicyAssetResolver assetResolver; private final ExecutorService asyncExecutor = ExecutorFactory.newVirtualThreadExecutor(); + /** Stop the service-owned executor when the application context is closed or restarted. */ + @PreDestroy + void shutdown() { + log.debug("Shutting down policy engine executor"); + asyncExecutor.shutdown(); + try { + if (!asyncExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { + asyncExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + asyncExecutor.shutdownNow(); + } + } + /** * Submit a pipeline to run asynchronously. The handle's run id scopes a {@link TaskManager} job * (status/notes/results observable via the job endpoints); its future resolves when the run @@ -104,12 +129,36 @@ public class PolicyEngine { // async // worker. String principal = currentActingPrincipal(); - return submitForPrincipal(principal, principal, policyId, definition, inputs, listener); + return submitForPrincipal( + principal, + principal, + principal, + policyId, + definition, + inputs, + listener, + null, + null); } /** Run a stored policy on demand. {@code enabled} gates triggers, not explicit runs. */ public PolicyRunHandle runPolicy( Policy policy, PolicyInputs inputs, PolicyProgressListener listener) { + return runPolicy(policy, inputs, listener, null, null); + } + + /** + * As {@link #runPolicy(Policy, PolicyInputs, PolicyProgressListener)}, recording which source + * fed the run and its opaque reference to the document. The first says where an unattended + * failure came from; the second says which document, and is what lets the same document failing + * again fold into one incident. Both null for a user's upload. + */ + public PolicyRunHandle runPolicy( + Policy policy, + PolicyInputs inputs, + PolicyProgressListener listener, + String sourceId, + String fileIdentity) { // Bill the policy owner: trigger-fired runs have no security context, and the async worker // doesn't inherit the caller's, so the owner (stamped at policy creation) is the reliable // billing identity — and for org-wide policies the org/owner is meant to pay. But own the @@ -117,19 +166,45 @@ public class PolicyEngine { // they can download their enforced file; otherwise an org-wide policy's output is owned by // the admin and the triggering user is denied it. Trigger-fired runs have no such user, so // the owner owns those outputs. + // + // The triggering user is also carried on the run, as the actor of any failure it records: + // null for a trigger-fired run, which is what makes an unattended incident ownerless rather + // than the owner's problem. Three identities, deliberately not interchangeable. String triggeringUser = currentActingPrincipal(); String fileOwner = triggeringUser != null ? triggeringUser : policy.owner(); + // Stored supporting files (certificates, watermark images, ...) load here, before the + // async hop: worker threads have no principal, so assets bind by the policy's own team. + PolicyInputs resolved = assetResolver.resolve(policy, inputs); + // Resolve the referenced output destinations live (like sourceIds), so a stored policy + // delivers to each of its saved Source destinations. Unreferenced policies fall back to + // their inline output. + PipelineDefinition definition = + new PipelineDefinition( + policy.name(), policy.steps(), outputResolver.resolve(policy)); return submitForPrincipal( - policy.owner(), fileOwner, policy.id(), policy.toDefinition(), inputs, listener); + policy.owner(), + fileOwner, + triggeringUser, + policy.id(), + definition, + // main's asset-resolved inputs, not the raw ones: stored certificates and watermark + // images bind here, before the async hop, because worker threads have no principal. + resolved, + listener, + sourceId, + fileIdentity); } private PolicyRunHandle submitForPrincipal( String billingPrincipal, String fileOwner, + String triggeringUser, String policyId, PipelineDefinition definition, PolicyInputs inputs, - PolicyProgressListener listener) { + PolicyProgressListener listener, + String sourceId, + String fileIdentity) { // Scope the run id to the current user (this request thread) so the file-download // ownership check passes. No-op when security is off. String runId = jobOwnershipService.createScopedJobKey(UUID.randomUUID().toString()); @@ -138,7 +213,8 @@ public class PolicyEngine { if (policyId != null) { taskManager.putMetadata(runId, "policyId", policyId); } - PolicyRun run = new PolicyRun(runId, policyId, definition); + PolicyRun run = + new PolicyRun(runId, policyId, definition, sourceId, fileIdentity, triggeringUser); registry.register(run); CompletableFuture completion = new CompletableFuture<>(); PolicyProgressListener tracking = trackingListener(runId, run, listener); @@ -212,13 +288,21 @@ public class PolicyEngine { run.markRunning(); PolicyExecutionResult result = stepExecutor.execute(run.getDefinition(), inputs, listener); - OutputSpec output = run.getDefinition().output(); - List outputs = - sinkFor(output) - .deliver( - new OutputDelivery(runId, run.getPolicyId()), - result.files(), - output); + // Deliver the run's files to every destination; no destinations means inline + // delivery (results stored/returned to the caller), preserving ad-hoc/AI behaviour. + List destinations = run.getDefinition().outputs(); + if (destinations.isEmpty()) { + destinations = List.of(OutputSpec.inline()); + } + List outputs = new ArrayList<>(); + for (OutputSpec destination : destinations) { + outputs.addAll( + sinkFor(destination) + .deliver( + new OutputDelivery(runId, run.getPolicyId()), + result.files(), + destination)); + } taskManager.setMultipleFileResults(runId, outputs); taskManager.setComplete(runId); run.complete(outputs); @@ -238,6 +322,7 @@ public class PolicyEngine { e.getMessage()); run.fail(message); taskManager.setError(runId, message); + recordFailure(run, message, e); } catch (RestClientResponseException e) { // A downstream tool call returned an error status. When it's a structured // entitlement @@ -261,17 +346,20 @@ public class PolicyEngine { run.failWithCode( message, code, DownstreamEntitlementError.extractSubscribed(e)); taskManager.setError(runId, message); + recordFailure(run, message, e); } else { String message = "Policy run failed: " + e.getMessage(); log.error("Policy run {} failed (downstream HTTP error)", runId, e); run.fail(message); taskManager.setError(runId, message); + recordFailure(run, message, e); } } catch (Exception e) { String message = "Policy run failed: " + e.getMessage(); log.error("Policy run {} failed", runId, e); run.fail(message); taskManager.setError(runId, message); + recordFailure(run, message, e); } finally { // Always resolve so stream/await callers unblock. completion.complete(run); @@ -289,11 +377,42 @@ public class PolicyEngine { // Transient admission rejection, not a processing failure (see QUEUE_FULL_CODE). run.failWithCode(message, QUEUE_FULL_CODE, null); taskManager.setError(run.getRunId(), message); + // No exception to classify here: nothing was thrown by a tool, the run simply was not + // admitted. Record it explicitly so a run lost to load pressure is still accounted for. + // Attributed like any other failure: a user whose run was refused is still the person + // holding that document, and an unattended sweep's run carries no triggering user. + failureRecorder.recordRunFailureAs( + FailureKind.UNKNOWN, + run.getRunId(), + run.getPolicyId(), + run.getSourceId(), + run.getTriggeringUser(), + message); completion.complete(run); } return null; } + /** + * Record why a run failed. Called after the run's own state transition and task-manager update, + * so a recording problem cannot change the outcome the caller observes. + * + *

The actor is the run's triggering user, not the MDC audit principal: that carries the + * BILLING identity, which for a stored policy is always its owner. Reading it here filed every + * failure under the owner — hiding an attended failure from the member who caused it and holds + * the document, and leaving an unattended sweep's failure looking attended. + */ + private void recordFailure(PolicyRun run, String message, Throwable cause) { + failureRecorder.recordRunFailure( + run.getRunId(), + run.getPolicyId(), + run.getSourceId(), + run.getFileIdentity(), + run.getTriggeringUser(), + message, + cause); + } + private WaitState suspend(PolicyInputRequiredException e) { List fileIds = new ArrayList<>(); for (Resource resource : e.getPendingFiles()) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java index d6202b520e..8a36700460 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyExecutor.java @@ -131,7 +131,7 @@ public class PolicyExecutor { // One call over all inputs. The outputs derive from a single input only when exactly // one entered; otherwise (a genuine merge) there is no single source. ToolResult r = callEndpoint(step, inputFiles, supportingFiles); - Integer origin = inputOrigins.size() == 1 ? inputOrigins.get(0) : null; + Integer origin = inputOrigins.size() == 1 ? inputOrigins.getFirst() : null; for (Resource file : r.files()) { files.add(file); origins.add(origin); @@ -281,18 +281,33 @@ public class PolicyExecutor { } for (Resource file : files) { if (!matchesType(file, accepted)) { + // Reports the extension rather than the filename, since this message becomes the + // run's error and is persisted on the failure record. throw new IOException( "Step " + operation + " accepts " + accepted - + " but received '" - + file.getFilename() - + "'"); + + " but received a '" + + extensionOf(file) + + "' file"); } } } + /** The file's extension, or {@code unknown} when it has no usable name. */ + private static String extensionOf(Resource file) { + String filename = file.getFilename(); + if (filename == null) { + return "unknown"; + } + int dot = filename.lastIndexOf('.'); + if (dot < 0 || dot == filename.length() - 1) { + return "unknown"; + } + return filename.substring(dot + 1).toLowerCase(Locale.ROOT); + } + private static boolean matchesType(Resource file, List acceptedExtensions) { String filename = file.getFilename(); if (filename == null) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java index 866fe0910d..b4609f94ea 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyRunner.java @@ -15,6 +15,7 @@ import stirling.software.proprietary.policy.input.ResolvedInput; import stirling.software.proprietary.policy.ledger.ProcessedLedger; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.PipelineDefinition; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.PolicyInputs; import stirling.software.proprietary.policy.model.PolicyRun; @@ -42,30 +43,47 @@ public class PolicyRunner { private final SourceDocCounter docCounter; private final ProcessedLedger processedLedger; - /** Full-listing sweep: resolve every source, then reconcile the ledger. */ + /** Full-listing sweep over every input: resolve each source, then reconcile the ledger. */ public SweepOutcome run(Policy policy) { return run(policy, SweepKind.FULL); } - /** - * Trigger entry point. Pulls every referenced source; each yielded unit becomes its own run so - * one failure does not affect the others. No sources means one run with no input (generator - * pipeline). Missing or disabled sources are skipped so one broken reference does not stop the - * rest. Returns the ids of the runs it started plus what the sweep skipped, so a manual trigger - * can report which runs to follow or why nothing ran. - */ + /** Sweep every input of the policy at the given listing depth. */ public SweepOutcome run(Policy policy, SweepKind sweep) { + return run(policy, policy.inputs(), sweep); + } + + /** + * Fire one input binding: a background trigger pulling its own source without touching the + * policy's other inputs. Never reconciles the ledger (it sees a single source, so pruning would + * wrongly forget the rest); a full-policy sweep handles that. + */ + public SweepOutcome runInput(Policy policy, PipelineInput input, SweepKind sweep) { + return run(policy, List.of(input), sweep); + } + + /** + * Core sweep: pulls each of the given inputs' sources; each yielded unit becomes its own run so + * one failure does not affect the others. No inputs means one run with no input (generator + * pipeline). Missing or disabled sources are skipped so one broken reference does not stop the + * rest. Presence cleanup only runs when the sweep covered every input of the policy - a + * single-binding fire cannot reconcile the whole policy's ledger. Returns the ids of the runs + * it started plus what the sweep skipped, so a manual trigger can report which runs to follow + * or why nothing ran. + */ + public SweepOutcome run(Policy policy, List inputs, SweepKind sweep) { long sweepStart = System.currentTimeMillis(); PolicySweep context = new PolicySweep(policy.id(), sweep, processedLedger); List runIds = new ArrayList<>(); - List sourceIds = policy.sourceIds(); - if (sourceIds.isEmpty()) { + if (inputs.isEmpty()) { // Generator pipeline: one run with no input. Still fall through to the cleanup // below so rows recorded for its folder outputs are pruned like anything else, // instead of accumulating until the policy is deleted. - runIds.add(startRun(policy, PolicyInputs.of(List.of()), unused -> {})); + // Generator pipeline: no input, so neither a source nor a document to attribute to. + runIds.add(startRun(policy, null, null, PolicyInputs.of(List.of()), unused -> {})); } - for (String sourceId : sourceIds) { + for (PipelineInput input : inputs) { + String sourceId = input.sourceId(); Source source = sourceStore.get(sourceId).orElse(null); if (source == null) { // No veto: a deleted source's rows should age out via the cleanup below. @@ -84,7 +102,8 @@ public class PolicyRunner { } runIds.addAll(pullAndRun(policy, sourceId, source.toInputSpec(), context)); } - if (context.cleanupAllowed()) { + boolean fullPolicy = inputs.size() == policy.inputs().size(); + if (fullPolicy && context.cleanupAllowed()) { processedLedger.markSeen(policy.id(), context.presentIdentities()); int removed = processedLedger.deleteUnseen(policy.id(), sweepStart); if (removed > 0) { @@ -149,17 +168,29 @@ public class PolicyRunner { List runIds = new ArrayList<>(); long docsFed = 0; for (ResolvedInput unit : work) { - runIds.add(startRun(policy, unit.inputs(), unit.onComplete())); + runIds.add( + startRun( + policy, + sourceId, + unit.fileIdentity(), + unit.inputs(), + unit.onComplete())); docsFed += unit.inputs().primary().size(); } docCounter.record(sourceId, docsFed); return runIds; } - private String startRun(Policy policy, PolicyInputs inputs, Consumer onComplete) { + private String startRun( + Policy policy, + String sourceId, + String fileIdentity, + PolicyInputs inputs, + Consumer onComplete) { log.info("Running policy {} ({})", policy.id(), policy.name()); PolicyRunHandle handle = - policyEngine.runPolicy(policy, inputs, PolicyProgressListener.NOOP); + policyEngine.runPolicy( + policy, inputs, PolicyProgressListener.NOOP, sourceId, fileIdentity); handle.completion() .whenComplete((run, throwable) -> onComplete.accept(succeeded(run, throwable))); return handle.runId(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java index c2d1357889..1faa016c42 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/engine/PolicyValidator.java @@ -1,14 +1,22 @@ package stirling.software.proprietary.policy.engine; import java.util.List; +import java.util.Map; +import java.util.Objects; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import stirling.software.common.model.tool.ToolDiagnostic; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.service.ToolChainValidator; +import stirling.software.proprietary.policy.asset.PolicyAssetRefs; +import stirling.software.proprietary.policy.asset.PolicyAssetStore; import stirling.software.proprietary.policy.input.InputSource; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.TriggerConfig; @@ -18,10 +26,11 @@ import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.trigger.PolicyTrigger; /** - * Validates a policy at save time by delegating each facet (trigger, sources, steps, output) to the - * bean that handles its type, so a misconfiguration fails fast rather than at run time. A null - * trigger is a manual-only policy and skips trigger validation. Each referenced {@code sourceId} - * must resolve to a persisted {@link Source} whose config its {@link InputSource} bean accepts. + * Validates a policy at save time by delegating each facet (inputs, their triggers, output) to the + * bean that handles its type, so a misconfiguration fails fast rather than at run time. Each + * input's {@code sourceId} must resolve to a persisted {@link Source} whose config its {@link + * InputSource} bean accepts; its optional trigger must be a known type compatible with that source. + * A null trigger is a manual-only input and skips trigger validation. */ @Service @RequiredArgsConstructor @@ -32,30 +41,118 @@ public class PolicyValidator { private final List outputSinks; private final List stepValidators; private final SourceStore sourceStore; + private final PolicyAssetStore assetStore; + private final ToolChainValidator toolChainValidator; /** - * @throws IllegalArgumentException if any facet's type is unknown, a referenced source does not - * exist, or any config is invalid + * @throws IllegalArgumentException if the policy has more than one input or output, any facet's + * type is unknown, a referenced source does not exist, a trigger is incompatible with its + * input's source, or any config is invalid */ public void validate(Policy policy) { - if (policy.trigger() != null) { - triggerFor(policy.trigger()).validate(policy); + // Deliberate product cap, not a model limit: the lists stay lists so multiple + // inputs/outputs can be supported later, but today a policy carries at most one of + // each (zero of either remains fine - run on demand / inline output). + if (policy.inputs().size() > 1) { + throw new IllegalArgumentException("a policy supports at most one input"); } - for (String sourceId : policy.sourceIds()) { + if (policy.outputIds().size() > 1) { + throw new IllegalArgumentException("a policy supports at most one output"); + } + for (PipelineInput input : policy.inputs()) { Source source = sourceStore - .get(sourceId) + .get(input.sourceId()) .orElseThrow( () -> new IllegalArgumentException( - "unknown source: " + sourceId)); + "unknown source: " + input.sourceId())); + if (input.trigger() != null) { + validateTrigger(policy, input, source); + } InputSpec spec = source.toInputSpec(); inputSourceFor(spec).validate(spec); } validateSteps(policy.steps()); + validateAssetReferences(policy); + validateChain(policy.steps()); validateOutput(policy.output()); } + /** + * A step binding that names stored assets ({@code asset:}) must resolve in the policy's own + * team, so a saved pipeline can't fail its later (principal-less) runs on a missing file, and a + * client can't bind another team's asset by id. A binding without that prefix names a file + * supplied with the run instead, and is only checked when the run arrives. + */ + private void validateAssetReferences(Policy policy) { + for (PipelineStep step : policy.steps()) { + for (Map.Entry binding : step.fileParameters().entrySet()) { + if (!PolicyAssetRefs.isAssetRef(binding.getValue())) { + continue; + } + List ids = PolicyAssetRefs.assetIds(binding.getValue()); + if (ids.isEmpty()) { + throw new IllegalArgumentException( + "step " + + step.operation() + + " has an empty file binding for field '" + + binding.getKey() + + "'"); + } + for (String id : ids) { + // One message for absent and other-team: existence must not leak across teams. + assetStore + .get(id) + .filter(asset -> Objects.equals(asset.teamId(), policy.teamId())) + .orElseThrow( + () -> + new IllegalArgumentException( + "unknown stored file: " + id)); + } + } + } + } + + /** + * Reject a chain whose steps cannot run on each other. Such a policy saves fine today and only + * fails part-way through its first run, which for a scheduled one may be much later. + * + *

Only errors block; warnings depend on configuration or file content. + * + * @throws IllegalArgumentException if any step cannot accept what the one before it produces + */ + public void validateChain(List steps) { + List diagnostics = diagnoseChain(steps, null); + if (!ToolChainValidator.hasErrors(diagnostics)) { + return; + } + String reasons = + diagnostics.stream() + .filter(d -> d.severity() == ToolDiagnostic.Severity.ERROR) + .map(d -> "step " + (d.stepIndex() + 1) + ": " + d.message()) + .reduce((a, b) -> a + "; " + b) + .orElse(""); + throw new IllegalArgumentException("pipeline steps cannot run in this order - " + reasons); + } + + /** + * Everything wrong with a chain, without rejecting it, so a caller can show warnings and + * fan-out notes too. + * + * @param sourceFormat the format entering the first step, or null when unknown + */ + public List diagnoseChain(List steps, ToolFormat sourceFormat) { + List chain = + steps.stream() + .map( + step -> + new ToolChainValidator.Step( + step.operation(), step.parameters())) + .toList(); + return toolChainValidator.validate(chain, sourceFormat); + } + /** * Validate each step against every registered {@link PipelineStepValidator}. Must be called on * a request thread (caller's principal present) for the same reason as {@link @@ -73,6 +170,24 @@ public class PolicyValidator { } } + /** + * Check an input's trigger is a known type whose source constraints its source satisfies (e.g. + * folder-watch only on a folder source), then let the trigger validate its own options. + */ + private void validateTrigger(Policy policy, PipelineInput input, Source source) { + PolicyTrigger trigger = triggerFor(input.trigger()); + if (!trigger.supportedSourceTypes().isEmpty() + && !trigger.supportedSourceTypes().contains(source.type())) { + throw new IllegalArgumentException( + "trigger '" + + trigger.type() + + "' is not compatible with source type '" + + source.type() + + "'"); + } + trigger.validate(policy, input); + } + /** * Validate an output spec against its sink. Must be called on a request thread (caller's * principal present) so an S3 output's connection is authorization-checked against the caller - diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java index 4f02d13c82..1e07185ab5 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/FolderInputSource.java @@ -118,8 +118,9 @@ public class FolderInputSource implements InputSource { continue; } work.add( - new ResolvedInput( + ResolvedInput.forFile( PolicyInputs.of(List.of(fileResource(file))), + identity, success -> completeConsumed( ctx, identity, file, gate, contentHash, success))); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/ResolvedInput.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/ResolvedInput.java index b5d7de94f8..1b10ff3a06 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/ResolvedInput.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/ResolvedInput.java @@ -2,21 +2,40 @@ package stirling.software.proprietary.policy.input; import java.util.function.Consumer; +import stirling.software.proprietary.policy.ledger.IdentityHasher; import stirling.software.proprietary.policy.model.PolicyInputs; /** * One unit of work from an {@link InputSource}: the files to run plus a completion callback invoked * with the run's success (e.g. a folder source routes the input to done/error). A source may return * several of these, one per file. + * + * @param fileIdentity stable opaque reference to the document, or null when the source has none. + * Always hashed, never the source's own identity: a folder identity is a path, and a path is a + * filename. See {@link #forFile}. */ -public record ResolvedInput(PolicyInputs inputs, Consumer onComplete) { +public record ResolvedInput( + PolicyInputs inputs, String fileIdentity, Consumer onComplete) { public ResolvedInput { onComplete = onComplete == null ? success -> {} : onComplete; } - /** No completion side effect. */ + /** + * One document, referenced by the hash of the source's identity for it. Stable across sweeps, + * which is what lets the same broken file fold into one incident instead of opening a fresh one + * every time the source re-lists it. + */ + public static ResolvedInput forFile( + PolicyInputs inputs, String identity, Consumer onComplete) { + return new ResolvedInput( + inputs, + identity == null ? null : IdentityHasher.identityHash(identity), + onComplete); + } + + /** No document reference and no completion side effect. */ public static ResolvedInput of(PolicyInputs inputs) { - return new ResolvedInput(inputs, success -> {}); + return new ResolvedInput(inputs, null, success -> {}); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/S3InputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/S3InputSource.java index 34576b47b9..326ccef147 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/S3InputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/S3InputSource.java @@ -119,8 +119,9 @@ public class S3InputSource implements InputSource { continue; } work.add( - new ResolvedInput( + ResolvedInput.forFile( PolicyInputs.of(List.of(objectResource(client, config, object))), + identity, success -> completeConsumed( ctx, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java index 49cb3c70c4..ff79b5be20 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/input/WebhookInputSource.java @@ -100,8 +100,9 @@ public class WebhookInputSource implements InputSource { continue; } work.add( - new ResolvedInput( + ResolvedInput.forFile( PolicyInputs.of(List.of(fileResource(file))), + identity, success -> completeConsumed(ctx, identity, file, gate, success))); } return work; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigration.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigration.java new file mode 100644 index 0000000000..9837cd4f45 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigration.java @@ -0,0 +1,40 @@ +package stirling.software.proprietary.policy.migration; + +import java.io.Serializable; +import java.time.Instant; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * A one-time policy-subsystem migration that has finished, keyed by a stable migration id. Its + * presence lets a migration skip its (otherwise every-boot) scan once it has run, instead of + * re-scanning and finding nothing to do forever. + */ +@Entity +@Table(name = "policy_completed_migrations") +@NoArgsConstructor +@Getter +@Setter +public class CompletedMigration implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "id") + private String id; + + @Column(name = "applied_at") + private Instant appliedAt; + + public CompletedMigration(String id, Instant appliedAt) { + this.id = id; + this.appliedAt = appliedAt; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrationRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrationRepository.java new file mode 100644 index 0000000000..23dec902c5 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrationRepository.java @@ -0,0 +1,7 @@ +package stirling.software.proprietary.policy.migration; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CompletedMigrationRepository extends JpaRepository {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrations.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrations.java new file mode 100644 index 0000000000..f03defa92b --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/CompletedMigrations.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.policy.migration; + +/** + * Tracks which one-time policy-subsystem migrations have finished, so a migration can skip its + * every-boot scan once done. {@link JpaCompletedMigrations} is the runtime bean; {@link + * InProcessCompletedMigrations} backs tests. + */ +public interface CompletedMigrations { + + /** Whether the migration with this id has already been recorded as complete. */ + boolean isDone(String id); + + /** + * Record the migration as complete. Safe to call concurrently: a race on first boot leaves the + * marker recorded exactly once and never propagates a failure to the caller. + */ + void markDone(String id); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/InProcessCompletedMigrations.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/InProcessCompletedMigrations.java new file mode 100644 index 0000000000..0744c85965 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/InProcessCompletedMigrations.java @@ -0,0 +1,23 @@ +package stirling.software.proprietary.policy.migration; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory {@link CompletedMigrations} for tests and any future no-database mode. {@link + * JpaCompletedMigrations} is the runtime bean. + */ +public class InProcessCompletedMigrations implements CompletedMigrations { + + private final Set done = ConcurrentHashMap.newKeySet(); + + @Override + public boolean isDone(String id) { + return done.contains(id); + } + + @Override + public void markDone(String id) { + done.add(id); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/JpaCompletedMigrations.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/JpaCompletedMigrations.java new file mode 100644 index 0000000000..853e18be51 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/migration/JpaCompletedMigrations.java @@ -0,0 +1,38 @@ +package stirling.software.proprietary.policy.migration; + +import java.time.Instant; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Durable {@link CompletedMigrations} backed by JPA; the runtime bean. {@code markDone} relies on + * the primary-key uniqueness of {@link CompletedMigration#getId()} to stay safe under a concurrent + * first boot: whichever node inserts first wins, and the loser's duplicate insert is swallowed + * rather than propagated, so it never disturbs the migration that called it. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class JpaCompletedMigrations implements CompletedMigrations { + + private final CompletedMigrationRepository repository; + + @Override + public boolean isDone(String id) { + return repository.existsById(id); + } + + @Override + public void markDone(String id) { + try { + repository.save(new CompletedMigration(id, Instant.now())); + } catch (DataIntegrityViolationException alreadyRecorded) { + // A concurrent boot recorded the same marker first; the row exists, so we are done. + log.debug("Completion marker '{}' was already recorded concurrently", id); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineDefinition.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineDefinition.java index 146424b0fb..209756c67f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineDefinition.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineDefinition.java @@ -3,13 +3,20 @@ package stirling.software.proprietary.policy.model; import java.util.List; /** - * An ordered chain of tool steps plus an output destination; the unit the engine executes. + * An ordered chain of tool steps plus its output destinations; the unit the engine executes. * - *

{@code output} may be null for callers that handle result files themselves (e.g. the AI - * workflow, which builds its own response payload). + *

{@code outputs} may be empty for callers that handle result files themselves (e.g. the AI + * workflow, which builds its own response payload) - the engine then falls back to inline delivery. + * A run's files are delivered to every destination in the list. */ -public record PipelineDefinition(String name, List steps, OutputSpec output) { +public record PipelineDefinition(String name, List steps, List outputs) { public PipelineDefinition { steps = steps == null ? List.of() : steps; + outputs = outputs == null ? List.of() : List.copyOf(outputs); + } + + /** Convenience for the common single-destination (or inline) case. A null output is empty. */ + public PipelineDefinition(String name, List steps, OutputSpec output) { + this(name, steps, output == null ? List.of() : List.of(output)); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineInput.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineInput.java new file mode 100644 index 0000000000..15242a9916 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineInput.java @@ -0,0 +1,16 @@ +package stirling.software.proprietary.policy.model; + +/** + * One input of a policy: a reference to a persisted {@code Source} paired with the {@link + * TriggerConfig} that decides when this source is pulled. The trigger lives on the + * binding, not on the source (so one connection can feed many policies on different schedules) and + * not on the policy (so a folder input can be watched while an S3 input on the same policy polls). + * A {@code null} trigger means this input is pulled only when the policy is run on demand. + */ +public record PipelineInput(String sourceId, TriggerConfig trigger) { + + /** An input with no automatic trigger: pulled only on a manual run. */ + public static PipelineInput manual(String sourceId) { + return new PipelineInput(sourceId, null); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineStep.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineStep.java index 99d8884996..00a072e895 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineStep.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineStep.java @@ -9,7 +9,9 @@ import java.util.Map; * *

{@code fileParameters} maps a tool's named file field (e.g. {@code stampImage}, beyond the * primary {@code fileInput} stream) to an asset key in the run's supporting-file store, keeping - * supporting inputs out of the document stream that flows step to step. + * supporting inputs out of the document stream that flows step to step. The key is either {@code + * asset:} for a stored supporting file or a plain name supplied with the run itself; see {@code + * PolicyAssetRefs}. */ public record PipelineStep( String operation, Map parameters, Map fileParameters) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineValidation.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineValidation.java new file mode 100644 index 0000000000..53eba97aa6 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PipelineValidation.java @@ -0,0 +1,39 @@ +package stirling.software.proprietary.policy.model; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import stirling.software.common.model.tool.ToolDiagnostic; +import stirling.software.common.model.tool.ToolFormat; + +/** + * Checking whether a policy's steps can run on each other, without the files they would run on. + * + *

The same check runs at save time in {@code PolicyValidator}. This exposes it on its own so a + * chain can be checked before it is saved, and so callers that are not the editor can ask. + */ +public final class PipelineValidation { + + private PipelineValidation() {} + + /** + * @param steps the steps to check, in the order they would run + * @param sourceFormat the format of the files entering the first step, or null to check only + * that the steps fit together + */ + @Schema(description = "A chain of steps to check") + public record Request(List steps, ToolFormat sourceFormat) { + + public List steps() { + return steps == null ? List.of() : steps; + } + } + + /** + * @param valid false when at least one diagnostic is an error, meaning the chain cannot run + * @param diagnostics every problem found, each against the step that cannot run + */ + @Schema(description = "Whether a chain of steps can run, and what is wrong with it if not") + public record Response(boolean valid, List diagnostics) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/Policy.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/Policy.java index 51dcc4ac0c..a3d43b712d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/Policy.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/Policy.java @@ -3,28 +3,51 @@ package stirling.software.proprietary.policy.model; import java.util.List; /** - * A stored automation: ordered tool steps, input sources, and an output destination. + * A stored automation: ordered tool steps, input bindings, and output destinations. * - *

Always runnable on demand. An optional {@link TriggerConfig} fires it automatically; a {@code - * null} trigger means manual-only. Trigger decides when; {@code sourceIds} reference the persisted - * {@code Source} connections (resolved live at run time) that decide where files come from; a run - * pulls from every referenced source. + *

Always runnable on demand. Each {@link PipelineInput} references a persisted {@code Source} + * connection (resolved live at run time) and carries its own optional {@link TriggerConfig}: the + * trigger decides when that source is pulled, so one input can be watched while another polls, and + * a {@code null} trigger makes that input manual-only. An input with no trigger, or a policy with + * no triggered inputs, still runs when the policy is run on demand; a manual run pulls every input. + * + *

{@code outputIds} reference the {@code Source} locations (resolved live) a run's files are + * delivered to - a run is delivered to every one; when empty the inline {@link #output} is used + * (results returned to the caller), the case for editor and one-off policies. */ public record Policy( String id, String name, String owner, boolean enabled, - TriggerConfig trigger, - List sourceIds, + List inputs, List steps, OutputSpec output, + List outputIds, Long teamId) { public Policy { - sourceIds = sourceIds == null ? List.of() : List.copyOf(sourceIds); + inputs = inputs == null ? List.of() : List.copyOf(inputs); steps = steps == null ? List.of() : steps; output = output == null ? OutputSpec.inline() : output; + outputIds = outputIds == null ? List.of() : List.copyOf(outputIds); + } + + /** + * Without output references: the inline output is used as-is. Kept for the engine, migrations, + * and tests, and for editor/one-off policies that return results to the caller rather than a + * stored destination. + */ + public Policy( + String id, + String name, + String owner, + boolean enabled, + List inputs, + List steps, + OutputSpec output, + Long teamId) { + this(id, name, owner, enabled, inputs, steps, output, List.of(), teamId); } /** @@ -36,26 +59,41 @@ public record Policy( String name, String owner, boolean enabled, - TriggerConfig trigger, - List sourceIds, + List inputs, List steps, OutputSpec output) { - this(id, name, owner, enabled, trigger, sourceIds, steps, output, null); + this(id, name, owner, enabled, inputs, steps, output, List.of(), null); } - /** A policy with no configured sources (a generator, or files supplied directly to a run). */ - public Policy( - String id, - String name, - String owner, - boolean enabled, - TriggerConfig trigger, - List steps, - OutputSpec output) { - this(id, name, owner, enabled, trigger, List.of(), steps, output, null); + /** The source ids this policy pulls from, in input order; a derived view for reads. */ + public List sourceIds() { + return inputs.stream().map(PipelineInput::sourceId).toList(); } - /** This policy's pipeline as the engine sees it. */ + /** The distinct trigger types configured across this policy's inputs (manual inputs aside). */ + public List triggerTypes() { + return inputs.stream() + .map(PipelineInput::trigger) + .filter(trigger -> trigger != null) + .map(TriggerConfig::type) + .distinct() + .toList(); + } + + /** A copy with the inline output replaced (e.g. resolved for the engine, or migrated). */ + public Policy withOutput(OutputSpec resolved) { + return new Policy(id, name, owner, enabled, inputs, steps, resolved, outputIds, teamId); + } + + /** A copy referencing the given saved output destinations. */ + public Policy withOutputIds(List newOutputIds) { + return new Policy(id, name, owner, enabled, inputs, steps, output, newOutputIds, teamId); + } + + /** + * This policy's pipeline as the engine sees it (inline output; destinations resolved + * elsewhere). + */ public PipelineDefinition toDefinition() { return new PipelineDefinition(name, steps, output); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyBinding.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyBinding.java new file mode 100644 index 0000000000..18e93ce1bc --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyBinding.java @@ -0,0 +1,31 @@ +package stirling.software.proprietary.policy.model; + +import java.util.List; + +/** + * A policy paired with one of its {@link PipelineInput}s: the unit a background trigger fires. A + * policy with two triggered inputs yields two bindings, so each fires independently on its own + * trigger and pulls only its own source. + */ +public record PolicyBinding(Policy policy, PipelineInput input) { + + /** + * The bindings across these policies whose input carries a trigger of the given type. Shared by + * the {@code PolicyStore} implementations so every backend derives a trigger's bindings the + * same way. Callers pass the policies a background trigger should consider (i.e. the enabled + * ones). + */ + public static List matching(List policies, String triggerType) { + return policies.stream() + .flatMap( + policy -> + policy.inputs().stream() + .filter( + input -> + input.trigger() != null + && triggerType.equals( + input.trigger().type())) + .map(input -> new PolicyBinding(policy, input))) + .toList(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyRun.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyRun.java index bba84813ce..8eb0cfcc73 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyRun.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/model/PolicyRun.java @@ -21,7 +21,28 @@ public class PolicyRun { /** ID of the stored policy that produced this run; null for ad-hoc pipelines. */ private final String policyId; + /** + * ID of the source the input came from (folder, S3, webhook), or null when a user supplied the + * files. Recorded on a failure so a reviewer can see where an unattended file came from. + */ + private final String sourceId; + private final PipelineDefinition definition; + + /** + * The source's opaque reference to the document this run is about; null for an ad-hoc run or a + * source that names no document. Hashed upstream, so never a path or a filename. + */ + private final String fileIdentity; + + /** + * The user who triggered this run, or null when nothing attended it (a trigger-fired sweep). + * Recorded as a failure's actor, so an attended failure is handed to the person holding the + * document. Deliberately not the billing principal: a shared policy is billed to its owner, who + * may never have touched the file. + */ + private final String triggeringUser; + private final Instant createdAt = Instant.now(); private volatile PolicyRunStatus status = PolicyRunStatus.PENDING; @@ -49,10 +70,25 @@ public class PolicyRun { private volatile List outputs = List.of(); private volatile Instant updatedAt = Instant.now(); - public PolicyRun(String runId, String policyId, PipelineDefinition definition) { + /** + * All three attribution references are required rather than defaulted: a run with none is a + * real case (an unattended sweep of a generator pipeline), but it should be stated at the call + * site. Overloads that omitted them would make losing the attribution the frictionless option, + * which is how {@code sourceId} and {@code fileIdentity} went unpopulated in the first place. + */ + public PolicyRun( + String runId, + String policyId, + PipelineDefinition definition, + String sourceId, + String fileIdentity, + String triggeringUser) { this.runId = runId; this.policyId = policyId; + this.sourceId = sourceId; this.definition = definition; + this.fileIdentity = fileIdentity; + this.triggeringUser = triggeringUser; } public int stepCount() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/FtpFileClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/FtpFileClient.java new file mode 100644 index 0000000000..d493831437 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/FtpFileClient.java @@ -0,0 +1,181 @@ +package stirling.software.proprietary.policy.network; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.net.ftp.FTP; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; +import org.apache.commons.net.ftp.FTPReply; +import org.apache.commons.net.ftp.FTPSClient; + +/** + * FTP client over Apache Commons Net, with optional TLS ({@code EXPLICIT} = AUTH TLS, {@code + * IMPLICIT} = TLS on connect). Passive mode is the default since it works through NAT. A blank + * directory lists the login working directory. One transfer is in flight at a time, so the read + * stream calls {@code completePendingCommand} on close before the session is reused or shut down. + */ +final class FtpFileClient implements RemoteFileClient { + + private static final int CONNECT_TIMEOUT_MS = 15_000; + private static final int READ_TIMEOUT_MS = 60_000; + private static final int MAX_DEPTH = 64; + + private final FTPClient ftp; + + private FtpFileClient(FTPClient ftp) { + this.ftp = ftp; + } + + static FtpFileClient connect(NetworkConfig config) throws IOException { + FTPClient ftp = clientFor(config); + ftp.setConnectTimeout(CONNECT_TIMEOUT_MS); + // Read timeouts so a server that connects then stalls cannot hang a poller: default + // becomes the control socket's SO timeout at connect, data covers transfer sockets. + ftp.setDefaultTimeout(READ_TIMEOUT_MS); + ftp.setDataTimeout(Duration.ofMillis(READ_TIMEOUT_MS)); + try { + ftp.connect(config.host(), config.port()); + int reply = ftp.getReplyCode(); + if (!FTPReply.isPositiveCompletion(reply)) { + ftp.disconnect(); + throw new IOException( + "FTP server " + config.host() + " refused connection: " + reply); + } + if (ftp instanceof FTPSClient ftps) { + // Encrypt the data channel too, not only the control channel. + ftps.execPBSZ(0); + ftps.execPROT("P"); + } + if (!ftp.login(config.username(), config.password())) { + int code = ftp.getReplyCode(); + ftp.disconnect(); + throw new IOException("FTP login to " + config.host() + " failed: " + code); + } + if (config.passive()) { + ftp.enterLocalPassiveMode(); + } + ftp.setFileType(FTP.BINARY_FILE_TYPE); + return new FtpFileClient(ftp); + } catch (IOException e) { + quietlyDisconnect(ftp); + throw e; + } + } + + private static FTPClient clientFor(NetworkConfig config) { + return switch (config.security()) { + case NONE -> new FTPClient(); + case IMPLICIT -> new FTPSClient(true); + case EXPLICIT -> new FTPSClient(false); + }; + } + + @Override + public List list(String directory, boolean recursive) throws IOException { + List files = new ArrayList<>(); + collect(dir(directory), recursive, 0, files); + return files; + } + + private void collect(String directory, boolean recursive, int depth, List out) + throws IOException { + FTPFile[] entries = ftp.listFiles(directory); + for (FTPFile entry : entries) { + if (out.size() >= MAX_FILES) { + return; + } + if (entry == null) { + continue; + } + String name = entry.getName(); + if (name.equals(".") || name.equals("..") || name.startsWith(".")) { + continue; + } + String path = join(directory, name); + if (entry.isDirectory()) { + if (recursive && !entry.isSymbolicLink() && depth < MAX_DEPTH) { + collect(path, true, depth + 1, out); + } + continue; + } + if (entry.isFile()) { + out.add(new RemoteFile(path, name, entry.getSize(), lastModified(entry))); + } + } + } + + @Override + public RemoteFile stat(String path) throws IOException { + // LIST, not MLST: listing uses listFiles, so the re-stat that guards the consume delete + // must read the timestamp the same way or the version gate never matches (and many servers + // do not implement MLST at all, returning null and skipping the delete entirely). + FTPFile[] listed = ftp.listFiles(path); + if (listed.length == 0 || listed[0] == null) { + return null; + } + FTPFile entry = listed[0]; + String name = path.substring(path.lastIndexOf('/') + 1); + return new RemoteFile(path, name, entry.getSize(), lastModified(entry)); + } + + @Override + public InputStream open(String path) throws IOException { + InputStream stream = ftp.retrieveFileStream(path); + if (stream == null) { + throw new IOException("cannot read " + path + ": " + ftp.getReplyString()); + } + // The data stream must be closed and the transfer acknowledged before the session is + // reused. + return new FilterInputStream(stream) { + @Override + public void close() throws IOException { + super.close(); + ftp.completePendingCommand(); + } + }; + } + + @Override + public void delete(String path) throws IOException { + ftp.deleteFile(path); + } + + @Override + public void close() throws IOException { + try { + ftp.logout(); + } finally { + quietlyDisconnect(ftp); + } + } + + private static long lastModified(FTPFile entry) { + return entry.getTimestamp() == null ? 0L : entry.getTimestamp().getTimeInMillis(); + } + + private static void quietlyDisconnect(FTPClient ftp) { + if (ftp.isConnected()) { + try { + ftp.disconnect(); + } catch (IOException ignored) { + // Best-effort teardown; the session is being abandoned anyway. + } + } + } + + private static String dir(String directory) { + return directory == null || directory.isBlank() ? "." : directory; + } + + private static String join(String directory, String name) { + if (directory.equals(".")) { + return name; + } + return directory.endsWith("/") ? directory + name : directory + "/" + name; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConfig.java new file mode 100644 index 0000000000..403799a1bf --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConfig.java @@ -0,0 +1,222 @@ +package stirling.software.proprietary.policy.network; + +import java.util.Map; + +/** + * The fully resolved settings a network source runs with - produced by {@link + * NetworkConnectionResolver} merging a stored connection (protocol, host, credentials, and, for + * SMB, the share) with per-use source options (directory, mode, recursive). Credentials are + * required: there is no anonymous fallback, so a source can never reach a server without + * config-supplied identity. {@code snapshot} and {@code recursive} are input-only. + */ +public record NetworkConfig( + NetworkProtocol protocol, + String host, + int port, + String username, + String password, + String privateKey, + String privateKeyPassphrase, + String hostKeyFingerprint, + String domain, + String share, + FtpSecurity security, + boolean passive, + String directory, + boolean snapshot, + boolean recursive) { + + /** FTP transport security: plaintext, or TLS negotiated up-front (implicit) or via AUTH TLS. */ + public enum FtpSecurity { + NONE, + EXPLICIT, + IMPLICIT; + + static FtpSecurity fromOption(String value) { + if (value == null || value.isBlank()) { + return NONE; + } + String normalized = value.trim().toUpperCase(); + for (FtpSecurity security : values()) { + if (security.name().equals(normalized)) { + return security; + } + } + throw new IllegalArgumentException( + "network config 'security' must be NONE, EXPLICIT or IMPLICIT"); + } + } + + private static final String PROTOCOL_OPTION = "protocol"; + private static final String HOST_OPTION = "host"; + private static final String PORT_OPTION = "port"; + private static final String USERNAME_OPTION = "username"; + private static final String PASSWORD_OPTION = "password"; + private static final String PRIVATE_KEY_OPTION = "privateKey"; + private static final String PRIVATE_KEY_PASSPHRASE_OPTION = "privateKeyPassphrase"; + private static final String HOST_KEY_FINGERPRINT_OPTION = "hostKeyFingerprint"; + private static final String DOMAIN_OPTION = "domain"; + private static final String SHARE_OPTION = "share"; + private static final String SECURITY_OPTION = "security"; + private static final String PASSIVE_OPTION = "passive"; + private static final String DIRECTORY_OPTION = "directory"; + private static final String MODE_OPTION = "mode"; + private static final String MODE_CONSUME = "consume"; + private static final String MODE_SNAPSHOT = "snapshot"; + private static final String RECURSIVE_OPTION = "recursive"; + + public static NetworkConfig from(Map options) { + NetworkProtocol protocol = NetworkProtocol.fromOption(str(options.get(PROTOCOL_OPTION))); + if (protocol == null) { + throw new IllegalArgumentException( + "network config requires a 'protocol' of sftp, ftp or smb"); + } + String host = trimmed(options.get(HOST_OPTION)); + if (host == null) { + throw new IllegalArgumentException("network config requires a 'host'"); + } + FtpSecurity security = FtpSecurity.fromOption(str(options.get(SECURITY_OPTION))); + int port = port(options.get(PORT_OPTION), defaultPort(protocol, security)); + String username = trimmed(options.get(USERNAME_OPTION)); + if (username == null) { + throw new IllegalArgumentException("network config requires a 'username'"); + } + String password = trimmed(options.get(PASSWORD_OPTION)); + String privateKey = trimmed(options.get(PRIVATE_KEY_OPTION)); + String passphrase = trimmed(options.get(PRIVATE_KEY_PASSPHRASE_OPTION)); + String fingerprint = trimmed(options.get(HOST_KEY_FINGERPRINT_OPTION)); + String domain = trimmed(options.get(DOMAIN_OPTION)); + String share = trimmed(options.get(SHARE_OPTION)); + boolean passive = parseBoolean(options.get(PASSIVE_OPTION), true); + String directory = directory(options.get(DIRECTORY_OPTION)); + boolean recursive = parseBoolean(options.get(RECURSIVE_OPTION), false); + boolean snapshot = snapshot(str(options.get(MODE_OPTION))); + + if (protocol == NetworkProtocol.SFTP && password == null && privateKey == null) { + throw new IllegalArgumentException( + "sftp connection requires a 'password' or a 'privateKey'"); + } + if ((protocol == NetworkProtocol.FTP || protocol == NetworkProtocol.SMB) + && password == null) { + throw new IllegalArgumentException( + protocol.name() + " connection requires a 'password'"); + } + if (protocol == NetworkProtocol.SMB && share == null) { + throw new IllegalArgumentException( + "smb connection requires a 'share' (e.g. documents)"); + } + return new NetworkConfig( + protocol, + host, + port, + username, + password, + privateKey, + passphrase, + fingerprint, + domain, + share, + security, + passive, + directory, + snapshot, + recursive); + } + + /** Implicit FTPS listens on 990, not the plain-FTP 21. */ + private static int defaultPort(NetworkProtocol protocol, FtpSecurity security) { + if (protocol == NetworkProtocol.FTP && security == FtpSecurity.IMPLICIT) { + return 990; + } + return protocol.defaultPort(); + } + + private static int port(Object value, int fallback) { + String text = trimmed(value); + if (text == null) { + return fallback; + } + int port; + try { + port = Integer.parseInt(text); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("network config 'port' must be a number"); + } + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("network config 'port' must be between 1 and 65535"); + } + return port; + } + + /** + * A poll directory relative to the login/home dir (SFTP/FTP) or the share root (SMB). Traversal + * segments are rejected so a connection cannot be steered above where the operator scoped it. + */ + private static String directory(Object value) { + String text = trimmed(value); + if (text == null) { + return ""; + } + String normalized = text.replace('\\', '/'); + if (normalized.indexOf('\0') >= 0) { + throw new IllegalArgumentException("network config 'directory' contains a null byte"); + } + for (String segment : normalized.split("/")) { + if (segment.equals("..")) { + throw new IllegalArgumentException( + "network config 'directory' must not contain '..'"); + } + } + return normalized; + } + + private static boolean snapshot(String mode) { + if (mode == null || mode.isBlank()) { + return false; + } + if (!MODE_CONSUME.equals(mode) && !MODE_SNAPSHOT.equals(mode)) { + throw new IllegalArgumentException( + "network config 'mode' must be 'consume' or 'snapshot'"); + } + return MODE_SNAPSHOT.equals(mode); + } + + private static boolean parseBoolean(Object value, boolean fallback) { + String text = trimmed(value); + return text == null ? fallback : Boolean.parseBoolean(text); + } + + private static String str(Object value) { + return value == null ? null : value.toString(); + } + + private static String trimmed(Object value) { + if (value == null) { + return null; + } + String text = value.toString().trim(); + return text.isEmpty() ? null : text; + } + + /** Never prints the credentials, so an accidental log line cannot leak them. */ + @Override + public String toString() { + return "NetworkConfig[protocol=" + + protocol + + ", host=" + + host + + ", port=" + + port + + ", username=" + + username + + ", share=" + + share + + ", directory=" + + directory + + ", snapshot=" + + snapshot + + ", recursive=" + + recursive + + "]"; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConnectionResolver.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConnectionResolver.java new file mode 100644 index 0000000000..098b31b561 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkConnectionResolver.java @@ -0,0 +1,136 @@ +package stirling.software.proprietary.policy.network; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +import stirling.software.proprietary.access.model.ResourceType; +import stirling.software.proprietary.access.service.OwnershipService; +import stirling.software.proprietary.integration.model.IntegrationConfig; +import stirling.software.proprietary.integration.model.IntegrationType; +import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.security.service.UserService; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +/** + * Turns a network source's options into a full {@link NetworkConfig} by dereferencing its {@code + * connectionId} to a stored NETWORK {@link IntegrationConfig} (the connection owns protocol, host, + * credentials and, for SMB, the share; the options own the per-use directory and mode). Options + * with no {@code connectionId} are parsed directly, so a programmatic caller can embed config. + * + *

Mirrors {@code S3ConnectionResolver}: an authenticated caller (save-time validation) must be + * allowed to use the connection; a background sweep with no caller skips that check, since the + * referencing source was access-checked when it was saved. + */ +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NetworkConnectionResolver { + + static final String CONNECTION_ID_OPTION = "connectionId"; + private static final String DIRECTORY_OPTION = "directory"; + private static final String MODE_OPTION = "mode"; + private static final String RECURSIVE_OPTION = "recursive"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final IntegrationConfigRepository connections; + private final OwnershipService ownership; + private final UserService userService; + + public NetworkConfig resolve(Map options) { + Long connectionId = connectionId(options); + if (connectionId == null) { + return NetworkConfig.from(options); + } + IntegrationConfig connection = + connections + .findById(connectionId) + .filter(cfg -> cfg.getIntegrationType() == IntegrationType.NETWORK) + .filter(this::usableByCurrentUser) + // Existence and access collapse into one error so a caller cannot tell + // "no such connection" from "someone else's" and enumerate ids. + .orElseThrow( + () -> + new IllegalArgumentException( + "unknown or inaccessible network connection")); + if (!connection.isEnabled()) { + throw new IllegalArgumentException("network connection is disabled"); + } + Map merged = new LinkedHashMap<>(connectionConfig(connection)); + copyPerUseOption(options, merged, DIRECTORY_OPTION); + copyPerUseOption(options, merged, MODE_OPTION); + copyPerUseOption(options, merged, RECURSIVE_OPTION); + return NetworkConfig.from(merged); + } + + static Long connectionId(Map options) { + Object reference = options.get(CONNECTION_ID_OPTION); + if (reference == null || (reference instanceof String s && s.isBlank())) { + return null; + } + if (reference instanceof Number number) { + return number.longValue(); + } + try { + return Long.valueOf(reference.toString().trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "network 'connectionId' is not a valid connection reference: " + reference); + } + } + + private boolean usableByCurrentUser(IntegrationConfig connection) { + User user = currentUser(); + return user == null || ownership.canUse(ResourceType.INTEGRATION_CONFIG, connection, user); + } + + private User currentUser() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + return null; + } + Object principal = auth.getPrincipal(); + if (principal instanceof User user) { + return user; + } + if (principal instanceof UserDetails userDetails) { + return userService.findByUsername(userDetails.getUsername()).orElse(null); + } + if (principal instanceof String username && !"anonymousUser".equals(username)) { + return userService.findByUsername(username).orElse(null); + } + return null; + } + + private static Map connectionConfig(IntegrationConfig connection) { + String json = connection.getConfig(); + if (json == null || json.isBlank()) { + return Map.of(); + } + try { + return OBJECT_MAPPER.readValue( + json, new TypeReference>() {}); + } catch (Exception e) { + throw new IllegalArgumentException( + "network connection '" + connection.getName() + "' has unreadable config", e); + } + } + + private static void copyPerUseOption( + Map options, Map merged, String key) { + Object value = options.get(key); + if (value != null && !value.toString().isBlank()) { + merged.put(key, value); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkHostGuard.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkHostGuard.java new file mode 100644 index 0000000000..44a174f989 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkHostGuard.java @@ -0,0 +1,68 @@ +package stirling.software.proprietary.policy.network; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.model.ApplicationProperties; + +/** + * Refuses a network source whose host resolves to a loopback, link-local, or private address, + * unless the operator opts in via {@code policies.allowPrivateNetworkSources} (every host) or names + * the host in {@code policies.allowedPrivateNetworkHosts} (that host only, for shared infra). The + * host comes from a portal user, so without this a connection could be aimed at internal services + * (the cloud metadata address, an admin panel). Mirrors the S3 endpoint guard; enforced both at + * save time and before every connect. + */ +@Component +@RequiredArgsConstructor +public class NetworkHostGuard { + + private final ApplicationProperties applicationProperties; + + public void requirePermitted(String host) { + if (applicationProperties.getPolicies().isAllowPrivateNetworkSources()) { + return; + } + if (host == null || host.isBlank()) { + throw new IllegalArgumentException("network source requires a host"); + } + if (isAllowlisted(host)) { + return; + } + InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(host); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("cannot resolve network host '" + host + "'", e); + } + for (InetAddress address : addresses) { + if (isPrivateOrLocal(address)) { + throw new IllegalArgumentException( + "network host '" + + host + + "' resolves to a private or local address (" + + address.getHostAddress() + + "); add it to policies.allowedPrivateNetworkHosts or set" + + " policies.allowPrivateNetworkSources=true to allow an" + + " on-network server"); + } + } + } + + private boolean isAllowlisted(String host) { + return applicationProperties.getPolicies().getAllowedPrivateNetworkHosts().stream() + .anyMatch(allowed -> allowed != null && allowed.trim().equalsIgnoreCase(host)); + } + + private static boolean isPrivateOrLocal(InetAddress address) { + return address.isLoopbackAddress() + || address.isLinkLocalAddress() + || address.isSiteLocalAddress() + || address.isAnyLocalAddress() + || address.isMulticastAddress(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIdentities.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIdentities.java new file mode 100644 index 0000000000..fb8d344abd --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIdentities.java @@ -0,0 +1,52 @@ +package stirling.software.proprietary.policy.network; + +/** + * The network backend's identity and version scheme, shared by {@link NetworkInputSource} and its + * completion hook. Identity keys a file's ledger row and stays stable across sweeps; the gate is + * size + last-modified, since SFTP/FTP/SMB expose no ETag - the same "stat" strength as a folder + * source, so a touch that changes mtime counts as a new version. + */ +public final class NetworkIdentities { + + private NetworkIdentities() {} + + /** A stable, unique key for a remote file: {@code protocol://host:port[/share]/path}. */ + public static String identity(NetworkConfig config, String path) { + StringBuilder id = + new StringBuilder(config.protocol().name().toLowerCase()) + .append("://") + .append(config.host()) + .append(':') + .append(config.port()); + if (config.share() != null && !config.share().isBlank()) { + id.append('/').append(trimSlashes(config.share())); + } + id.append('/').append(trimLeadingSlash(path)); + return id.toString(); + } + + /** The version gate: size and last-modified, mirroring the folder source's stat identity. */ + public static String gate(long size, long lastModifiedMs) { + return size + ":" + lastModifiedMs; + } + + private static String trimSlashes(String value) { + int start = 0; + int end = value.length(); + while (start < end && (value.charAt(start) == '/' || value.charAt(start) == '\\')) { + start++; + } + while (end > start && (value.charAt(end - 1) == '/' || value.charAt(end - 1) == '\\')) { + end--; + } + return value.substring(start, end); + } + + private static String trimLeadingSlash(String value) { + int start = 0; + while (start < value.length() && value.charAt(start) == '/') { + start++; + } + return value.substring(start); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkInputSource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkInputSource.java new file mode 100644 index 0000000000..0fdf8d116f --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkInputSource.java @@ -0,0 +1,149 @@ +package stirling.software.proprietary.policy.network; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.input.InputSource; +import stirling.software.proprietary.policy.input.ResolveContext; +import stirling.software.proprietary.policy.input.ResolvedInput; +import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.policy.model.PolicyInputs; + +/** + * Reads input files from a network file server (SFTP, FTP/FTPS, or SMB), one bean serving all three + * source types ({@code sftp}, {@code ftp}, {@code network}); the protocol comes from the referenced + * connection. Each listed file is its own unit of work, claimed through the {@link ResolveContext} + * ledger and tracked in place, mirroring the S3 source. The version gate is size + last-modified + * (there is no ETag), so this has folder-source "stat" strength. Options: "connectionId" references + * the stored NETWORK connection; "directory" is the poll folder (relative to the login home or + * share root); "recursive" descends into subdirectories; "mode" is "consume" (default: a processed + * file is deleted once every policy that claimed it has settled successfully and it is still the + * version that ran) or "snapshot" (stateless, every run sees the full set). + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class NetworkInputSource implements InputSource { + + private final NetworkConnectionResolver connectionResolver; + private final RemoteFileClientFactory clientFactory; + + @Override + public String type() { + return NetworkProtocol.SMB.sourceType(); + } + + @Override + public boolean supports(InputSpec spec) { + return spec != null && NetworkProtocol.forSourceType(spec.type()) != null; + } + + /** + * Fails fast at save time: an unknown/disabled/inaccessible connection, a protocol that does + * not match the source type, a blocked host, or a server the connection cannot list. + */ + @Override + public void validate(InputSpec spec) { + NetworkConfig config = connectionResolver.resolve(spec.options()); + NetworkProtocol expected = NetworkProtocol.forSourceType(spec.type()); + if (expected != null && config.protocol() != expected) { + throw new IllegalArgumentException( + "source type '" + + spec.type() + + "' does not match the connection protocol " + + config.protocol()); + } + try (RemoteFileClient client = clientFactory.connect(config)) { + client.list(config.directory(), false); + } catch (IOException e) { + throw new IllegalArgumentException( + "cannot access " + config.protocol() + " source: " + e.getMessage(), e); + } + } + + @Override + public List resolve(InputSpec spec, ResolveContext ctx) throws IOException { + NetworkConfig config = connectionResolver.resolve(spec.options()); + // A listing failure propagates so the sweep reads it as "could not list" (which vetoes + // presence cleanup), never as "verifiably no files". + List files; + try (RemoteFileClient client = clientFactory.connect(config)) { + files = client.list(config.directory(), config.recursive()); + } + if (files.size() >= RemoteFileClient.MAX_FILES) { + log.warn( + "Network source listing hit the {}-file cap; remaining files are picked up" + + " on later sweeps", + RemoteFileClient.MAX_FILES); + } + + if (config.snapshot()) { + return files.stream() + .map(file -> ResolvedInput.of(PolicyInputs.of(List.of(resource(config, file))))) + .toList(); + } + + ctx.reportPresent( + files.stream() + .map(file -> NetworkIdentities.identity(config, file.path())) + .toList()); + + List work = new ArrayList<>(); + for (RemoteFile file : files) { + String identity = NetworkIdentities.identity(config, file.path()); + String gate = NetworkIdentities.gate(file.size(), file.lastModifiedMs()); + if (!ctx.claim(identity, gate, null)) { + continue; + } + work.add( + ResolvedInput.forFile( + PolicyInputs.of(List.of(resource(config, file))), + identity, + success -> + completeConsumed(ctx, config, file, identity, gate, success))); + } + return work; + } + + /** + * Settle at the version this run claimed, then remove the file only when it still carries that + * version and every policy that claimed it has settled DONE, mirroring the S3 source's + * consensus delete. A failed run settles ERROR and never deletes; the DONE row of a file that + * could not be deleted still stops reprocessing. + */ + private void completeConsumed( + ResolveContext ctx, + NetworkConfig config, + RemoteFile file, + String identity, + String claimGate, + boolean success) { + ctx.settle(identity, claimGate, null, success); + if (!success) { + return; + } + try (RemoteFileClient client = clientFactory.connect(config)) { + RemoteFile current = client.stat(file.path()); + if (current == null) { + return; // removed by the user or a co-watching policy's consensus delete + } + String currentGate = NetworkIdentities.gate(current.size(), current.lastModifiedMs()); + if (currentGate.equals(claimGate) && ctx.allSettledDone(identity)) { + client.delete(file.path()); + } + } catch (IOException e) { + log.warn("Could not remove consumed network file {}: {}", identity, e.getMessage()); + } + } + + private Resource resource(NetworkConfig config, RemoteFile file) { + return new RemoteFileResource(clientFactory, config, file); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidator.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidator.java new file mode 100644 index 0000000000..a9ec9cc260 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidator.java @@ -0,0 +1,35 @@ +package stirling.software.proprietary.policy.network; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +import stirling.software.proprietary.integration.model.IntegrationType; +import stirling.software.proprietary.integration.service.IntegrationConfigValidator; + +/** + * The network connection schema, enforced when a NETWORK {@link IntegrationType} config is saved: + * protocol, host, credentials (and, for SMB, a share) required, with the same private-address guard + * {@link RemoteFileClientFactory} applies before connecting, moved to save time so a bad connection + * fails in the form rather than in a sweep. A live connectivity check runs later, when the source + * that uses the connection is saved ({@link NetworkInputSource#validate}). + */ +@Component +@RequiredArgsConstructor +public class NetworkIntegrationValidator implements IntegrationConfigValidator { + + private final NetworkHostGuard hostGuard; + + @Override + public IntegrationType type() { + return IntegrationType.NETWORK; + } + + @Override + public void validate(Map config) { + NetworkConfig parsed = NetworkConfig.from(config); + hostGuard.requirePermitted(parsed.host()); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkProtocol.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkProtocol.java new file mode 100644 index 0000000000..bbd6dc5d7e --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/NetworkProtocol.java @@ -0,0 +1,57 @@ +package stirling.software.proprietary.policy.network; + +/** + * The file-transfer protocols a network source speaks. Each maps to a {@link RemoteFileClient} + * implementation and to a portal source type: {@code sftp} -> SFTP, {@code ftp} -> FTP/FTPS, {@code + * network} -> SMB (a Windows/Samba share). The stored connection carries the protocol; the source + * type only routes the UI. + */ +public enum NetworkProtocol { + SFTP(22, "sftp"), + FTP(21, "ftp"), + SMB(445, "network"); + + private final int defaultPort; + private final String sourceType; + + NetworkProtocol(int defaultPort, String sourceType) { + this.defaultPort = defaultPort; + this.sourceType = sourceType; + } + + public int defaultPort() { + return defaultPort; + } + + /** The portal source {@code type} string this protocol backs. */ + public String sourceType() { + return sourceType; + } + + /** Parse a stored {@code protocol} option (case-insensitive), or null when absent/unknown. */ + public static NetworkProtocol fromOption(String value) { + if (value == null) { + return null; + } + String normalized = value.trim().toUpperCase(); + for (NetworkProtocol protocol : values()) { + if (protocol.name().equals(normalized)) { + return protocol; + } + } + return null; + } + + /** The protocol a source {@code type} string implies, or null when it is not a network type. */ + public static NetworkProtocol forSourceType(String type) { + if (type == null) { + return null; + } + for (NetworkProtocol protocol : values()) { + if (protocol.sourceType.equals(type)) { + return protocol; + } + } + return null; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFile.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFile.java new file mode 100644 index 0000000000..8c72ddab64 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFile.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.policy.network; + +/** + * One remote file discovered by a {@link RemoteFileClient} listing: its full path on the server, + * its display name, size in bytes, and last-modified epoch milliseconds. Size and mtime form the + * version gate ({@link NetworkIdentities#gate}) since network protocols expose no ETag. + */ +public record RemoteFile(String path, String name, long size, long lastModifiedMs) { + + public RemoteFile { + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("remote file requires a path"); + } + if (name == null || name.isBlank()) { + name = path.substring(path.lastIndexOf('/') + 1); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClient.java new file mode 100644 index 0000000000..217c590031 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClient.java @@ -0,0 +1,38 @@ +package stirling.software.proprietary.policy.network; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * A connected session to one network file server (SFTP, FTP, or SMB), created by {@link + * RemoteFileClientFactory}. Sessions are short-lived and single-use: the caller opens one, performs + * a listing, a read, or a delete, then closes it - there is no pooling, because network sessions + * time out and go stale far more readily than an S3 HTTP client. Callers must {@link #close()} + * every client (try-with-resources), except when streaming, where ownership is handed to the + * returned stream so it can be read after {@code resolve} returns. + */ +public interface RemoteFileClient extends Closeable { + + /** + * Listing cap: a listing stops once this many files are collected, bounding memory against a + * huge (or maliciously deep) tree. Consume mode drains the rest on later sweeps. + */ + int MAX_FILES = 10_000; + + /** + * Every regular, non-hidden file under {@code directory}, optionally descending into it, + * truncated at {@link #MAX_FILES}. + */ + List list(String directory, boolean recursive) throws IOException; + + /** The current metadata for one path, or null when it no longer exists. */ + RemoteFile stat(String path) throws IOException; + + /** Opens the file for reading; the stream stays valid until this client is closed. */ + InputStream open(String path) throws IOException; + + /** Removes the file; a no-op if it is already gone. */ + void delete(String path) throws IOException; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClientFactory.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClientFactory.java new file mode 100644 index 0000000000..fe3f43b1bb --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileClientFactory.java @@ -0,0 +1,28 @@ +package stirling.software.proprietary.policy.network; + +import java.io.IOException; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +/** + * Opens a fresh {@link RemoteFileClient} for one {@link NetworkConfig}, dispatched by protocol. The + * host is guarded against private addresses before every connect, since it comes from a portal user + * and each operation opens its own short-lived session (there is no long-lived pool to guard once). + */ +@Component +@RequiredArgsConstructor +public class RemoteFileClientFactory { + + private final NetworkHostGuard hostGuard; + + public RemoteFileClient connect(NetworkConfig config) throws IOException { + hostGuard.requirePermitted(config.host()); + return switch (config.protocol()) { + case SFTP -> SftpFileClient.connect(config); + case FTP -> FtpFileClient.connect(config); + case SMB -> SmbFileClient.connect(config); + }; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileResource.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileResource.java new file mode 100644 index 0000000000..e9429e0ea7 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/RemoteFileResource.java @@ -0,0 +1,68 @@ +package stirling.software.proprietary.policy.network; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.core.io.AbstractResource; + +/** + * A policy input backed by a remote file, streamed on demand. Each read opens its own short-lived + * client (the listing client is long gone by the time the run reads the file) and hands it to the + * stream, so closing the stream tears the session down. Size and name come from the listing, so the + * pipeline can size the input without a second round trip. + */ +final class RemoteFileResource extends AbstractResource { + + private final RemoteFileClientFactory factory; + private final NetworkConfig config; + private final RemoteFile file; + + RemoteFileResource(RemoteFileClientFactory factory, NetworkConfig config, RemoteFile file) { + this.factory = factory; + this.config = config; + this.file = file; + } + + @Override + public InputStream getInputStream() throws IOException { + RemoteFileClient client = factory.connect(config); + try { + InputStream stream = client.open(file.path()); + return new FilterInputStream(stream) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + client.close(); + } + } + }; + } catch (IOException e) { + client.close(); + throw e; + } + } + + /** Listed just now; a reader gets a precise error from {@link #getInputStream} instead. */ + @Override + public boolean exists() { + return true; + } + + @Override + public long contentLength() { + return file.size(); + } + + @Override + public String getFilename() { + return file.name(); + } + + @Override + public String getDescription() { + return "network file " + NetworkIdentities.identity(config, file.path()); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java new file mode 100644 index 0000000000..d222e3be40 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SftpFileClient.java @@ -0,0 +1,312 @@ +package stirling.software.proprietary.policy.network; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Vector; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.HostKey; +import com.jcraft.jsch.HostKeyRepository; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpATTRS; +import com.jcraft.jsch.SftpException; +import com.jcraft.jsch.UserInfo; + +import stirling.software.common.configuration.InstallationPathConfig; + +/** + * SFTP client over jsch. Host keys are verified: a connection with a configured {@code + * hostKeyFingerprint} only accepts the server presenting that key; otherwise the key seen on the + * first connect is pinned in a known-hosts file under the config directory and any later change is + * refused (trust-on-first-use). Directories are listed, read and deleted through one channel; a + * blank directory means the login home. Symlinked directories are not followed and hidden entries + * are skipped, mirroring the folder source. + */ +final class SftpFileClient implements RemoteFileClient { + + private static final int CONNECT_TIMEOUT_MS = 15_000; + private static final int READ_TIMEOUT_MS = 60_000; + private static final int MAX_DEPTH = 64; + static final String KNOWN_HOSTS_FILE = "sftp_known_hosts"; + + private final Session session; + private final ChannelSftp channel; + + private SftpFileClient(Session session, ChannelSftp channel) { + this.session = session; + this.channel = channel; + } + + static SftpFileClient connect(NetworkConfig config) throws IOException { + JSch jsch = new JSch(); + try { + if (config.privateKey() != null) { + byte[] passphrase = + config.privateKeyPassphrase() == null + ? null + : config.privateKeyPassphrase().getBytes(StandardCharsets.UTF_8); + jsch.addIdentity( + "network-source", + config.privateKey().getBytes(StandardCharsets.UTF_8), + null, + passphrase); + } + Session session = jsch.getSession(config.username(), config.host(), config.port()); + if (config.password() != null) { + session.setPassword(config.password().getBytes(StandardCharsets.UTF_8)); + } + if (config.hostKeyFingerprint() != null) { + // Pinned key: only the configured fingerprint is ever accepted. + session.setHostKeyRepository( + new PinnedHostKeyRepository(config.hostKeyFingerprint())); + } else { + // Trust-on-first-use: the first key seen is recorded, a changed key is refused. + jsch.setKnownHosts(knownHostsFile().toString()); + session.setHostKeyRepository( + new TofuHostKeyRepository(jsch.getHostKeyRepository())); + } + session.setConfig("StrictHostKeyChecking", "yes"); + session.connect(CONNECT_TIMEOUT_MS); + session.setTimeout(READ_TIMEOUT_MS); + ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); + channel.connect(CONNECT_TIMEOUT_MS); + return new SftpFileClient(session, channel); + } catch (JSchException e) { + throw new IOException( + "SFTP connection to " + config.host() + " failed: " + e.getMessage(), e); + } + } + + /** jsch only persists newly pinned keys into a file that already exists, so create it. */ + private static Path knownHostsFile() throws IOException { + Path file = Path.of(InstallationPathConfig.getConfigPath(), KNOWN_HOSTS_FILE); + Files.createDirectories(file.getParent()); + try { + Files.createFile(file); + } catch (FileAlreadyExistsException ignored) { + // Another connect already created it; the content is managed by jsch. + } + return file; + } + + /** + * Trust-on-first-use over the shared known-hosts file: an unknown host's key is pinned on first + * contact, and a key that later differs is refused ({@code CHANGED}). Rejection happens before + * authentication, so credentials are never sent to an impersonating server. + */ + private static final class TofuHostKeyRepository implements HostKeyRepository { + + private final HostKeyRepository knownHosts; + + private TofuHostKeyRepository(HostKeyRepository knownHosts) { + this.knownHosts = knownHosts; + } + + @Override + public int check(String host, byte[] key) { + int result = knownHosts.check(host, key); + if (result != NOT_INCLUDED) { + return result; + } + try { + knownHosts.add(new HostKey(host, key), null); + } catch (JSchException e) { + return CHANGED; // a key we cannot even parse is refused, not trusted + } + return OK; + } + + @Override + public void add(HostKey hostkey, UserInfo ui) { + knownHosts.add(hostkey, ui); + } + + @Override + public void remove(String host, String type) { + knownHosts.remove(host, type); + } + + @Override + public void remove(String host, String type, byte[] key) { + knownHosts.remove(host, type, key); + } + + @Override + public String getKnownHostsRepositoryID() { + return knownHosts.getKnownHostsRepositoryID(); + } + + @Override + public HostKey[] getHostKey() { + return knownHosts.getHostKey(); + } + + @Override + public HostKey[] getHostKey(String host, String type) { + return knownHosts.getHostKey(host, type); + } + } + + /** + * Accepts only the server key whose SHA-256 fingerprint matches the configured value (OpenSSH + * {@code SHA256:base64} form, prefix optional). Rejection happens before authentication, so + * credentials are never sent to an impersonating server. + */ + private static final class PinnedHostKeyRepository implements HostKeyRepository { + + private final String expected; + + private PinnedHostKeyRepository(String fingerprint) { + this.expected = normalize(fingerprint); + } + + @Override + public int check(String host, byte[] key) { + return expected.equals(sha256Fingerprint(key)) ? OK : CHANGED; + } + + private static String normalize(String fingerprint) { + String value = fingerprint.trim(); + if (value.toUpperCase(Locale.ROOT).startsWith("SHA256:")) { + value = value.substring("SHA256:".length()); + } + // OpenSSH prints the digest base64 without padding; accept it padded too. + return value.replace("=", ""); + } + + private static String sha256Fingerprint(byte[] key) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return Base64.getEncoder().withoutPadding().encodeToString(digest.digest(key)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 unavailable", e); + } + } + + @Override + public void add(HostKey hostkey, UserInfo ui) {} + + @Override + public void remove(String host, String type) {} + + @Override + public void remove(String host, String type, byte[] key) {} + + @Override + public String getKnownHostsRepositoryID() { + return "pinned-fingerprint"; + } + + @Override + public HostKey[] getHostKey() { + return new HostKey[0]; + } + + @Override + public HostKey[] getHostKey(String host, String type) { + return new HostKey[0]; + } + } + + @Override + public List list(String directory, boolean recursive) throws IOException { + List files = new ArrayList<>(); + collect(dir(directory), recursive, 0, files); + return files; + } + + @SuppressWarnings("unchecked") + private void collect(String directory, boolean recursive, int depth, List out) + throws IOException { + Vector entries; + try { + entries = channel.ls(directory); + } catch (SftpException e) { + throw new IOException("cannot list " + directory + ": " + e.getMessage(), e); + } + for (ChannelSftp.LsEntry entry : entries) { + if (out.size() >= MAX_FILES) { + return; + } + String name = entry.getFilename(); + if (name.equals(".") || name.equals("..") || name.startsWith(".")) { + continue; + } + SftpATTRS attrs = entry.getAttrs(); + String path = join(directory, name); + if (attrs.isDir()) { + if (recursive && !attrs.isLink() && depth < MAX_DEPTH) { + collect(path, true, depth + 1, out); + } + continue; + } + if (attrs.isReg()) { + out.add(new RemoteFile(path, name, attrs.getSize(), attrs.getMTime() * 1000L)); + } + } + } + + @Override + public RemoteFile stat(String path) throws IOException { + try { + SftpATTRS attrs = channel.stat(path); + String name = path.substring(path.lastIndexOf('/') + 1); + return new RemoteFile(path, name, attrs.getSize(), attrs.getMTime() * 1000L); + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + return null; + } + throw new IOException("cannot stat " + path + ": " + e.getMessage(), e); + } + } + + @Override + public InputStream open(String path) throws IOException { + try { + return channel.get(path); + } catch (SftpException e) { + throw new IOException("cannot read " + path + ": " + e.getMessage(), e); + } + } + + @Override + public void delete(String path) throws IOException { + try { + channel.rm(path); + } catch (SftpException e) { + if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + return; + } + throw new IOException("cannot delete " + path + ": " + e.getMessage(), e); + } + } + + @Override + public void close() { + channel.disconnect(); + session.disconnect(); + } + + private static String dir(String directory) { + return directory == null || directory.isBlank() ? "." : directory; + } + + private static String join(String directory, String name) { + if (directory.equals(".")) { + return name; + } + return directory.endsWith("/") ? directory + name : directory + "/" + name; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SmbFileClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SmbFileClient.java new file mode 100644 index 0000000000..b49d6dad53 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/network/SmbFileClient.java @@ -0,0 +1,236 @@ +package stirling.software.proprietary.policy.network; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.hierynomus.msdtyp.AccessMask; +import com.hierynomus.mserref.NtStatus; +import com.hierynomus.msfscc.FileAttributes; +import com.hierynomus.msfscc.fileinformation.FileAllInformation; +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation; +import com.hierynomus.mssmb2.SMB2CreateDisposition; +import com.hierynomus.mssmb2.SMB2ShareAccess; +import com.hierynomus.mssmb2.SMBApiException; +import com.hierynomus.protocol.commons.socket.ProxySocketFactory; +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.common.SMBRuntimeException; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; + +/** + * SMB2/3 client over smbj against a Windows or Samba share. The share is named on the connection; + * directories are relative to its root, using backslashes as SMB expects. A blank directory lists + * the share root. Hidden and system entries are skipped, and symlink/reparse dirs are not + * descended, mirroring the folder source. + */ +final class SmbFileClient implements RemoteFileClient { + + private static final int CONNECT_TIMEOUT_MS = 15_000; + private static final int READ_TIMEOUT_MS = 60_000; + private static final int MAX_DEPTH = 64; + + private final SMBClient smbClient; + private final Connection connection; + private final Session session; + private final DiskShare share; + + private SmbFileClient( + SMBClient smbClient, Connection connection, Session session, DiskShare share) { + this.smbClient = smbClient; + this.connection = connection; + this.session = session; + this.share = share; + } + + static SmbFileClient connect(NetworkConfig config) throws IOException { + // Bounded connect and read timeouts (both default to unlimited) so a host that accepts + // the TCP connection then stalls cannot wedge a poller. + SmbConfig smbConfig = + SmbConfig.builder() + .withSocketFactory(new ProxySocketFactory(CONNECT_TIMEOUT_MS)) + .withSoTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .withTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build(); + SMBClient smbClient = new SMBClient(smbConfig); + Connection connection = smbClient.connect(config.host(), config.port()); + try { + char[] password = + config.password() == null ? new char[0] : config.password().toCharArray(); + AuthenticationContext auth = + new AuthenticationContext(config.username(), password, config.domain()); + Session session = connection.authenticate(auth); + DiskShare share = (DiskShare) session.connectShare(config.share()); + return new SmbFileClient(smbClient, connection, session, share); + } catch (RuntimeException e) { + closeQuietly(connection); + closeQuietly(smbClient); + throw new IOException( + "SMB connection to \\\\" + + config.host() + + "\\" + + config.share() + + " failed: " + + e.getMessage(), + e); + } + } + + @Override + public List list(String directory, boolean recursive) throws IOException { + List files = new ArrayList<>(); + collect(smbDir(directory), recursive, 0, files); + return files; + } + + private void collect(String directory, boolean recursive, int depth, List out) + throws IOException { + List entries; + try { + entries = share.list(directory); + } catch (SMBRuntimeException e) { + // smbj throws unchecked; surface as IOException like the SFTP/FTP clients. + throw new IOException("cannot list " + slashed(directory) + ": " + e.getMessage(), e); + } + for (FileIdBothDirectoryInformation info : entries) { + if (out.size() >= MAX_FILES) { + return; + } + String name = info.getFileName(); + if (name.equals(".") || name.equals("..") || name.startsWith(".")) { + continue; + } + long attributes = info.getFileAttributes(); + if (isSet(attributes, FileAttributes.FILE_ATTRIBUTE_HIDDEN) + || isSet(attributes, FileAttributes.FILE_ATTRIBUTE_SYSTEM)) { + continue; + } + String path = join(directory, name); + if (isSet(attributes, FileAttributes.FILE_ATTRIBUTE_DIRECTORY)) { + if (recursive + && !isSet(attributes, FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) + && depth < MAX_DEPTH) { + collect(path, true, depth + 1, out); + } + continue; + } + out.add( + new RemoteFile( + slashed(path), + name, + info.getEndOfFile(), + info.getLastWriteTime().toEpochMillis())); + } + } + + @Override + public RemoteFile stat(String path) throws IOException { + String smbPath = smbPath(path); + try { + FileAllInformation info = share.getFileInformation(smbPath); + String name = path.substring(path.lastIndexOf('/') + 1); + return new RemoteFile( + slashed(smbPath), + name, + info.getStandardInformation().getEndOfFile(), + info.getBasicInformation().getLastWriteTime().toEpochMillis()); + } catch (SMBApiException e) { + if (isNotFound(e)) { + return null; + } + throw new IOException("cannot stat " + path + ": " + e.getMessage(), e); + } + } + + @Override + public InputStream open(String path) throws IOException { + try { + com.hierynomus.smbj.share.File file = + share.openFile( + smbPath(path), + EnumSet.of(AccessMask.GENERIC_READ), + null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, + null); + // Close the SMB file handle when the stream is closed, before the session is torn down. + return new FilterInputStream(file.getInputStream()) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + file.close(); + } + } + }; + } catch (SMBApiException e) { + throw new IOException("cannot read " + path + ": " + e.getMessage(), e); + } + } + + @Override + public void delete(String path) throws IOException { + try { + share.rm(smbPath(path)); + } catch (SMBApiException e) { + if (isNotFound(e)) { + return; + } + throw new IOException("cannot delete " + path + ": " + e.getMessage(), e); + } + } + + @Override + public void close() { + closeQuietly(share); + closeQuietly(session); + closeQuietly(connection); + closeQuietly(smbClient); + } + + private static boolean isNotFound(SMBApiException e) { + return e.getStatus() == NtStatus.STATUS_OBJECT_NAME_NOT_FOUND + || e.getStatus() == NtStatus.STATUS_OBJECT_PATH_NOT_FOUND; + } + + private static boolean isSet(long attributes, FileAttributes attribute) { + return (attributes & attribute.getValue()) != 0; + } + + private static void closeQuietly(AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception ignored) { + // Best-effort teardown; the session is being abandoned anyway. + } + } + + /** The share-relative listing path: blank means the share root, which smbj lists as "". */ + private static String smbDir(String directory) { + return directory == null || directory.isBlank() ? "" : smbPath(directory); + } + + /** SMB paths use backslashes; internal identities keep forward slashes. */ + private static String smbPath(String path) { + return path.replace('/', '\\'); + } + + private static String slashed(String path) { + return path.replace('\\', '/'); + } + + private static String join(String directory, String name) { + if (directory.isEmpty()) { + return name; + } + return directory.endsWith("\\") ? directory + name : directory + "\\" + name; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigration.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigration.java new file mode 100644 index 0000000000..a93b60d000 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigration.java @@ -0,0 +1,144 @@ +package stirling.software.proprietary.policy.output; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.migration.CompletedMigrations; +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.source.Source; +import stirling.software.proprietary.policy.source.SourceStore; +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * One-time, idempotent migration of policies' inline output destinations onto stored {@link Source} + * references: policies written before a destination was a saved location carry their folder/S3 + * destination inline; this points each at a {@link Source} (reusing one at the same location, or + * creating it) so the destination becomes a managed location like any other source. Policies with + * an inline "return to caller" output have no location to store and are left as-is. + * + *

Idempotent by construction: a policy that already carries an {@code outputId} is skipped, so a + * sequential re-run finds nothing to do. Matches are keyed by the write-relevant config within a + * team, so an output to a folder/prefix an input source already covers links to that same source - + * unifying the "output of A is the input of B" case onto one location. A concurrent multi-node boot + * can at worst create a redundant (unreferenced) source row, never corrupt a policy. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PolicyInlineOutputMigration { + + /** Completion-marker id: once recorded, later boots skip the scan entirely. */ + private static final String MIGRATION_ID = "policy-inline-output"; + + // Destination types worth persisting as a location; "inline" has nothing to store. + private static final List DESTINATION_TYPES = List.of("folder", "s3"); + // The options that actually address a write destination, per type. Read-only options (e.g. a + // folder's consume mode) are excluded so an output matches an existing input source at the same + // place regardless of how that source reads. + private static final Map> ADDRESS_OPTIONS = + Map.of("folder", List.of("directory"), "s3", List.of("connectionId", "prefix")); + // Field separator for the dedup key: a unit-separator control char that cannot appear in a + // directory/prefix/connection id, so distinct field sets can never collide. + private static final char DELIMITER = '\u001f'; + + private final PolicyStore policyStore; + private final SourceStore sourceStore; + private final CompletedMigrations completedMigrations; + + // Runs after EmbeddedS3CredentialMigration (@Order(1)) so any legacy S3 output has already had + // its embedded credentials extracted into a connection; the Source created here then references + // that connection rather than copying credentials into source_json. Not wrapped in a single + // transaction: each store write is its own (idempotent) commit, so a crash mid-run just re-runs + // next boot, and the marker below is written only once the whole pass succeeds. + @Order(2) + @EventListener(ApplicationReadyEvent.class) + public void migrate() { + if (completedMigrations.isDone(MIGRATION_ID)) { + return; + } + Map byAddress = indexExistingSources(); + int migrated = 0; + for (Policy policy : policyStore.all()) { + if (!policy.outputIds().isEmpty()) { + continue; // already references one or more locations + } + OutputSpec output = policy.output(); + if (output == null || !DESTINATION_TYPES.contains(output.type())) { + continue; // inline / editor / no location to migrate + } + Source destination = destinationFor(policy, output, byAddress); + policyStore.save(policy.withOutputIds(List.of(destination.id()))); + migrated++; + } + if (migrated > 0) { + log.info("Linked {} policy output(s) to stored source locations", migrated); + } + completedMigrations.markDone(MIGRATION_ID); + } + + /** Reuses an existing team source at the same address, else creates a minimal one. */ + private Source destinationFor(Policy policy, OutputSpec spec, Map byAddress) { + String key = addressKey(policy.teamId(), spec.type(), spec.options()); + Source existing = byAddress.get(key); + if (existing != null) { + return existing; + } + Source created = + sourceStore.save( + new Source( + null, + destinationName(spec), + spec.type(), + spec.options(), + true, + policy.owner(), + policy.teamId())); + byAddress.put(key, created); + return created; + } + + private Map indexExistingSources() { + Map byKey = new LinkedHashMap<>(); + for (Source source : sourceStore.all()) { + if (!DESTINATION_TYPES.contains(source.type())) { + continue; + } + byKey.putIfAbsent(addressKey(source.teamId(), source.type(), source.options()), source); + } + return byKey; + } + + private static String addressKey(Long teamId, String type, Map options) { + StringBuilder key = new StringBuilder(); + key.append(teamId == null ? "" : teamId).append(DELIMITER); + key.append(type == null ? "" : type).append(DELIMITER); + for (String option : ADDRESS_OPTIONS.getOrDefault(type, List.of())) { + Object value = options.get(option); + key.append(value == null ? "" : value.toString()).append(DELIMITER); + } + return key.toString(); + } + + /** A readable default name derived from the destination; the user can rename it later. */ + private static String destinationName(OutputSpec spec) { + if ("folder".equals(spec.type())) { + Object directory = spec.options().get("directory"); + return directory == null ? "Folder" : "Folder: " + directory; + } + if ("s3".equals(spec.type())) { + Object prefix = spec.options().get("prefix"); + return prefix == null || prefix.toString().isBlank() ? "S3 bucket" : "S3: " + prefix; + } + return spec.type(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyOutputResolver.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyOutputResolver.java new file mode 100644 index 0000000000..3e9d804683 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/PolicyOutputResolver.java @@ -0,0 +1,53 @@ +package stirling.software.proprietary.policy.output; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.source.Source; +import stirling.software.proprietary.policy.source.SourceStore; + +/** + * Resolves a policy's effective output destinations at run time: each {@code outputId} references a + * {@link Source} used as a destination, looked up live (so editing a location updates every policy + * that writes to it), exactly as input {@code sourceIds} are resolved. A run is delivered to every + * resolved destination. A policy with no references keeps its inline output (results returned to + * the caller) - the case for editor and one-off policies. A reference that no longer resolves + * (location deleted out from under a live policy - normally blocked by the source delete guard) is + * skipped; if none resolve, delivery falls back to inline so the run still completes. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PolicyOutputResolver { + + private final SourceStore sourceStore; + + public List resolve(Policy policy) { + List outputIds = policy.outputIds(); + if (outputIds.isEmpty()) { + return List.of(policy.output()); + } + List resolved = new ArrayList<>(); + for (String outputId : outputIds) { + sourceStore + .get(outputId) + .map(Source::toOutputSpec) + .ifPresentOrElse( + resolved::add, + () -> + log.warn( + "Policy {} references missing output source {}; skipping" + + " that destination", + policy.id(), + outputId)); + } + return resolved.isEmpty() ? List.of(policy.output()) : resolved; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/S3OutputSink.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/S3OutputSink.java index b0bd210e10..10bf80e278 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/S3OutputSink.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/output/S3OutputSink.java @@ -34,6 +34,7 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.ObjectLockMode; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; @@ -176,6 +177,7 @@ public class S3OutputSink implements PolicyOutputSink { if (conditionalPuts) { put.ifNoneMatch("*"); } + applyObjectLock(put, config); try { PutObjectResponse response = client.putObject(put.build(), RequestBody.fromFile(staged)); @@ -267,4 +269,24 @@ public class S3OutputSink implements PolicyOutputSink { throw new IllegalStateException("MD5 unavailable", e); } } + + /** + * Write the object under Object Lock retention when the connection asks for it. + * + *

The retain-until date is computed per object from "now", so a policy that runs daily gives + * each document its own full retention window rather than a shared deadline. + * + *

Requires the bucket to have Object Lock enabled; S3 rejects the PUT otherwise, which is + * the correct outcome - silently storing a deletable object while an operator believes it is + * locked would be worse than failing. + */ + private static void applyObjectLock(PutObjectRequest.Builder put, S3Config config) { + if (config.objectLockMode() == null || config.retentionDays() == null) { + return; + } + put.objectLockMode(ObjectLockMode.fromValue(config.objectLockMode())) + .objectLockRetainUntilDate( + java.time.Instant.now() + .plus(config.retentionDays(), java.time.temporal.ChronoUnit.DAYS)); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java index 117d78de40..b2be7b668e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java @@ -4,6 +4,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -13,30 +14,34 @@ import stirling.software.proprietary.policy.config.PolicyAccessGuard; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; -import stirling.software.proprietary.policy.model.TriggerConfig; import stirling.software.proprietary.policy.source.Source; import stirling.software.proprietary.policy.source.SourceAccessGuard; import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; /** - * Builds the Pipelines overview: every policy the caller's team owns, each annotated with its - * referenced sources (resolved to display names), its pipeline steps, and a trigger/output summary. - * Source names are resolved from the team's sources in memory rather than persisted on the policy, - * so the view always reflects the live source set. This is the "all pipelines" admin surface; the - * user-facing Policies page builds only a friendly subset of the same backend policies. + * Builds the Pipelines overview: one row per policy the caller's team built on the Pipelines page, + * with its sources resolved to live display names, its steps, and a trigger/output summary. + * Frontend/catalogue policies (marked by a {@code categoryId} in their output options) belong to + * the user-facing Policies page and are excluded; a folder-watch trigger is not a signal. */ @Service @RequiredArgsConstructor public class PolicyOverviewService { + // Output-options key marking a frontend/catalogue policy (set by the Policies page and seeder). + private static final String CATEGORY_OPTION = "categoryId"; + private final PolicyStore policyStore; private final SourceStore sourceStore; private final PolicyAccessGuard policyAccessGuard; private final SourceAccessGuard sourceAccessGuard; public PoliciesOverviewResponse overview() { - List policies = policyAccessGuard.visibleFrom(policyStore); + List policies = + policyAccessGuard.visibleFrom(policyStore).stream() + .filter(PolicyOverviewService::isPipeline) + .toList(); Map sourceNames = sourceNames(); List views = @@ -50,6 +55,18 @@ public class PolicyOverviewService { return new PoliciesOverviewResponse(buildKpis(policies), views); } + private static boolean isPipeline(Policy policy) { + return !isCataloguePolicy(policy); + } + + /** A frontend/catalogue policy, marked by a {@code categoryId} in its output options. */ + private static boolean isCataloguePolicy(Policy policy) { + OutputSpec output = policy.output(); + return output != null + && output.options().get(CATEGORY_OPTION) instanceof String category + && !category.isBlank(); + } + /** Display names for every source the caller's team can see, keyed by source id. */ private Map sourceNames() { Map names = new HashMap<>(); @@ -72,16 +89,35 @@ public class PolicyOverviewService { policy.name(), policy.enabled(), policy.enabled() ? "active" : "paused", - triggerSummary(policy.trigger()), + triggerSummary(policy), sources, steps, - outputSummary(policy.output()), + outputSummary(policy, sourceNames), policy.owner()); } - /** A null trigger is a manual-only policy; otherwise the trigger's type keys the summary. */ - private static String triggerSummary(TriggerConfig trigger) { - return trigger == null ? "manual" : trigger.type(); + /** + * A policy that delivers to sources shows those locations' display names, comma-joined (each + * falling back to its id if it's since been deleted or isn't visible); otherwise the inline + * output's type. + */ + private static String outputSummary(Policy policy, Map sourceNames) { + List outputIds = policy.outputIds(); + if (!outputIds.isEmpty()) { + return outputIds.stream() + .map(id -> sourceNames.getOrDefault(id, id)) + .collect(Collectors.joining(", ")); + } + return outputSummary(policy.output()); + } + + /** + * Summarise a policy's triggers for the overview row: "manual" when no input is triggered, + * otherwise the distinct trigger types across its inputs (e.g. "folder-watch, schedule"). + */ + private static String triggerSummary(Policy policy) { + List types = policy.triggerTypes(); + return types.isEmpty() ? "manual" : String.join(", ", types); } private static String outputSummary(OutputSpec output) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigration.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigration.java index e324cda525..bbbb9cb8b1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigration.java @@ -6,8 +6,8 @@ import java.util.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,6 +18,7 @@ import stirling.software.proprietary.integration.model.IntegrationConfig; import stirling.software.proprietary.integration.model.IntegrationType; import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.policy.migration.CompletedMigrations; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.source.Source; @@ -45,6 +46,9 @@ import tools.jackson.databind.ObjectMapper; @RequiredArgsConstructor public class EmbeddedS3CredentialMigration { + /** Completion-marker id: once recorded, later boots skip the scan entirely. */ + private static final String MIGRATION_ID = "embedded-s3-credentials"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final List CONNECTION_OPTIONS = List.of("bucket", "region", "endpoint", "accessKeyId", "secretAccessKey"); @@ -56,10 +60,19 @@ public class EmbeddedS3CredentialMigration { private final PolicyStore policyStore; private final IntegrationConfigRepository connections; private final TeamRepository teamRepository; + private final CompletedMigrations completedMigrations; + // Must run before PolicyInlineOutputMigration: that migration copies a policy's inline output + // options into a Source, so embedded S3 credentials have to be extracted into a connection here + // first, or they would be copied verbatim (plaintext) into the new source row. Each rewrite is + // its own idempotent commit (dedup by credential key), so a crash mid-run just re-runs next + // boot; the completion marker below is written only once the whole pass succeeds. + @Order(1) @EventListener(ApplicationReadyEvent.class) - @Transactional public void migrate() { + if (completedMigrations.isDone(MIGRATION_ID)) { + return; + } Map byCredentialKey = indexExistingConnections(); int migrated = 0; for (Source source : sourceStore.all()) { @@ -89,6 +102,7 @@ public class EmbeddedS3CredentialMigration { if (migrated > 0) { log.info("Extracted embedded S3 credentials from {} row(s) into connections", migrated); } + completedMigrations.markDone(MIGRATION_ID); } private static boolean embedsCredentials(Map options) { @@ -196,15 +210,6 @@ public class EmbeddedS3CredentialMigration { } private static Policy withOutput(Policy policy, OutputSpec output) { - return new Policy( - policy.id(), - policy.name(), - policy.owner(), - policy.enabled(), - policy.trigger(), - policy.sourceIds(), - policy.steps(), - output, - policy.teamId()); + return policy.withOutput(output); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/S3Config.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/S3Config.java index c9d3eabd83..faaa8fe76d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/S3Config.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/s3/S3Config.java @@ -19,7 +19,9 @@ public record S3Config( String endpoint, String accessKeyId, String secretAccessKey, - boolean snapshot) { + boolean snapshot, + String objectLockMode, + Integer retentionDays) { private static final String BUCKET_OPTION = "bucket"; private static final String REGION_OPTION = "region"; @@ -28,6 +30,12 @@ public record S3Config( private static final String ACCESS_KEY_ID_OPTION = "accessKeyId"; private static final String SECRET_ACCESS_KEY_OPTION = "secretAccessKey"; private static final String MODE_OPTION = "mode"; + private static final String OBJECT_LOCK_MODE_OPTION = "objectLockMode"; + private static final String RETENTION_DAYS_OPTION = "retentionDays"; + + private static final String LOCK_GOVERNANCE = "GOVERNANCE"; + private static final String LOCK_COMPLIANCE = "COMPLIANCE"; + private static final int MAX_RETENTION_DAYS = 36525; private static final String MODE_CONSUME = "consume"; private static final String MODE_SNAPSHOT = "snapshot"; @@ -52,6 +60,41 @@ public record S3Config( if (mode != null && !MODE_CONSUME.equals(mode) && !MODE_SNAPSHOT.equals(mode)) { throw new IllegalArgumentException("s3 config 'mode' must be 'consume' or 'snapshot'"); } + // Object Lock: write-once retention, for records that must survive an administrator. + // COMPLIANCE cannot be shortened or deleted by anyone (not even the account root) before + // the retain-until date; GOVERNANCE can be bypassed with a specific IAM permission, so + // only COMPLIANCE is the answer to SEC 17a-4(f) / FINRA. The bucket must already have + // Object Lock enabled - it cannot be turned on per-object - and that in turn requires + // versioning, which can then never be suspended. + String objectLockMode = trimmed(options.get(OBJECT_LOCK_MODE_OPTION)); + if (objectLockMode != null) { + objectLockMode = objectLockMode.toUpperCase(java.util.Locale.ROOT); + if (!LOCK_GOVERNANCE.equals(objectLockMode) + && !LOCK_COMPLIANCE.equals(objectLockMode)) { + throw new IllegalArgumentException( + "s3 config 'objectLockMode' must be 'GOVERNANCE' or 'COMPLIANCE'"); + } + } + Integer retentionDays = null; + Object rawRetention = options.get(RETENTION_DAYS_OPTION); + if (rawRetention != null && !rawRetention.toString().isBlank()) { + try { + retentionDays = Integer.valueOf(rawRetention.toString().trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("s3 config 'retentionDays' must be a number"); + } + if (retentionDays < 1 || retentionDays > MAX_RETENTION_DAYS) { + throw new IllegalArgumentException( + "s3 config 'retentionDays' must be between 1 and " + MAX_RETENTION_DAYS); + } + } + // S3 rejects one without the other, so catch it here where the operator can still fix it + // rather than at upload time on a worker thread. + if ((objectLockMode == null) != (retentionDays == null)) { + throw new IllegalArgumentException( + "s3 config 'objectLockMode' and 'retentionDays' must be set together"); + } + return new S3Config( bucket, region == null ? "us-east-1" : region, @@ -59,7 +102,9 @@ public record S3Config( endpoint, accessKeyId, secretAccessKey, - MODE_SNAPSHOT.equals(mode)); + MODE_SNAPSHOT.equals(mode), + objectLockMode, + retentionDays); } private static String validEndpoint(String endpoint) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeeder.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeeder.java index aca6ae0e84..63ebae833a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeeder.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeeder.java @@ -46,9 +46,10 @@ public class DefaultClassificationPolicySeeder { .ifPresent(team -> seedIfMissing(team.getId(), team.getName())); } - // Any team created at runtime (admin-created, SaaS sign-ups); after the team's commit so a - // rolled-back team never leaves a policy behind. - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + // Any team created at runtime (admin-created, SaaS sign-ups). Seeds inside the team's own + // transaction: rollback still leaves no policy behind, and the store's pessimistic lock needs a + // live transaction, which AFTER_COMMIT cannot offer. + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) public void onTeamCreated(TeamCreatedEvent event) { seedIfMissing(event.teamId(), event.teamName()); } @@ -86,7 +87,6 @@ public class DefaultClassificationPolicySeeder { POLICY_NAME, "system", true, - null, List.of(), List.of(new PipelineStep(CLASSIFY_ENDPOINT, Map.of())), new OutputSpec("inline", options), diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/Source.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/Source.java index ca80c1b46f..c064a33d9a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/Source.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/Source.java @@ -3,14 +3,18 @@ package stirling.software.proprietary.policy.source; import java.util.Map; import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.policy.model.OutputSpec; /** - * A persisted, reusable input connection: the instantiation of a source definition. Policies - * reference sources by {@code id} rather than embedding their config, so one connection is - * configured once and can feed many policies. + * A persisted, reusable storage location: the instantiation of a source definition. Policies + * reference sources by {@code id} rather than embedding their config, so one location is configured + * once and can be used by many policies - as an input (files come from it) and/or as an output (a + * run's files are delivered to it), which is how a folder or bucket can be both the output of one + * pipeline and the input of the next. * - *

{@code type} keys an {@link stirling.software.proprietary.policy.input.InputSource} bean, - * matching {@link InputSpec#type()}; {@code options} is that source's config. {@code owner} and + *

{@code type} keys an {@link stirling.software.proprietary.policy.input.InputSource} bean (and, + * for writable types, a {@link stirling.software.proprietary.policy.output.PolicyOutputSink}), + * matching {@link InputSpec#type()}; {@code options} is that location's config. {@code owner} and * {@code teamId} scope the source to a team, mirroring {@link * stirling.software.proprietary.policy.model.Policy}. */ @@ -27,8 +31,17 @@ public record Source( options = options == null ? Map.of() : options; } - /** The runtime form the policy engine resolves and runs against. */ + /** The runtime form the policy engine resolves and reads inputs from. */ public InputSpec toInputSpec() { return new InputSpec(type, options); } + + /** + * The runtime form the policy engine delivers a run's outputs to, when this source is used as a + * policy's destination. Read-only options (e.g. a folder's consume mode) are simply ignored by + * the output sink. + */ + public OutputSpec toOutputSpec() { + return new OutputSpec(type, options); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java index 6539d9a34d..73622738f0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceController.java @@ -307,10 +307,17 @@ public class SourceController { } } - /** Names of the caller's visible policies that reference the given source. */ + /** + * Names of the caller's visible policies that reference the given source - as an input ({@code + * sourceIds}) or as their output destination ({@code outputId}), so a location in use either + * way is protected from deletion. + */ private List referencingPolicyNames(String sourceId) { return policyAccessGuard.visibleFrom(policyStore).stream() - .filter(policy -> policy.sourceIds().contains(sourceId)) + .filter( + policy -> + policy.sourceIds().contains(sourceId) + || policy.outputIds().contains(sourceId)) .map(Policy::name) .toList(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceOverviewService.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceOverviewService.java index 2c02b9f152..0f9df21440 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceOverviewService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/SourceOverviewService.java @@ -3,8 +3,10 @@ package stirling.software.proprietary.policy.source; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.stereotype.Service; @@ -115,11 +117,17 @@ public class SourceOverviewService { return sources instanceof List list && list.contains(EditorSource.ID); } - /** Policies referencing each source id, across the caller's visible policies. */ + /** + * Policies referencing each source id, across the caller's visible policies. A source counts + * whether a policy reads from it ({@code sourceIds}) or writes to it ({@code outputId}); a + * policy that does both counts once. + */ private static Map> referencesBySource(List policies) { Map> bySource = new HashMap<>(); for (Policy policy : policies) { - for (String sourceId : policy.sourceIds()) { + Set referenced = new LinkedHashSet<>(policy.sourceIds()); + referenced.addAll(policy.outputIds()); + for (String sourceId : referenced) { bySource.computeIfAbsent(sourceId, key -> new ArrayList<>()).add(policy); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/InProcessPolicyStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/InProcessPolicyStore.java index 685ddb21db..70d67bba0f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/InProcessPolicyStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/InProcessPolicyStore.java @@ -8,7 +8,9 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import stirling.software.proprietary.policy.asset.PolicyAssetRefs; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; /** * In-memory {@link PolicyStore} for tests and any future no-database mode. {@link JpaPolicyStore} @@ -32,10 +34,10 @@ public class InProcessPolicyStore implements PolicyStore { policy.name(), policy.owner(), policy.enabled(), - policy.trigger(), - policy.sourceIds(), + policy.inputs(), policy.steps(), policy.output(), + policy.outputIds(), policy.teamId()); policies.put(id, stored); // Existing policy keeps its position; a new one appends to the end of its team's queue. @@ -76,12 +78,18 @@ public class InProcessPolicyStore implements PolicyStore { } @Override - public List findByTriggerType(String triggerType) { + public boolean anyPolicyReferences(String assetId) { return policies.values().stream() - .filter(Policy::enabled) - .filter(policy -> policy.trigger() != null) - .filter(policy -> triggerType.equals(policy.trigger().type())) - .toList(); + .anyMatch( + policy -> + PolicyAssetRefs.referencedAssetIds(policy.steps()) + .contains(assetId)); + } + + @Override + public List findBindingsByTriggerType(String triggerType) { + List enabled = policies.values().stream().filter(Policy::enabled).toList(); + return PolicyBinding.matching(enabled, triggerType); } @Override diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java index 41da23c734..6edaa76c78 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java @@ -12,8 +12,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; +import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; /** * Durable {@link PolicyStore} backed by JPA; the runtime store. Policies are persisted as JSON via @@ -40,10 +44,10 @@ public class JpaPolicyStore implements PolicyStore { policy.name(), policy.owner(), policy.enabled(), - policy.trigger(), - policy.sourceIds(), + policy.inputs(), policy.steps(), policy.output(), + policy.outputIds(), policy.teamId()); PolicyEntity entity = new PolicyEntity(); @@ -51,7 +55,6 @@ public class JpaPolicyStore implements PolicyStore { entity.setName(stored.name()); entity.setOwner(stored.owner()); entity.setEnabled(stored.enabled()); - entity.setTriggerType(stored.trigger() == null ? null : stored.trigger().type()); entity.setTeamId(stored.teamId()); // Preserve an existing policy's run-order position; append a new one to the end of its // team's queue (max + 1), so setting up a policy adds it last by default. @@ -118,11 +121,18 @@ public class JpaPolicyStore implements PolicyStore { } @Override - public List findByTriggerType(String triggerType) { - return repository.findByTriggerTypeAndEnabledTrue(triggerType).stream() - .map(this::toPolicy) - .flatMap(Optional::stream) - .toList(); + public boolean anyPolicyReferences(String assetId) { + return assetId != null && !assetId.isBlank() && repository.anyMentioning(assetId); + } + + @Override + public List findBindingsByTriggerType(String triggerType) { + List enabled = + repository.findByEnabledTrue().stream() + .map(this::toPolicy) + .flatMap(Optional::stream) + .toList(); + return PolicyBinding.matching(enabled, triggerType); } @Override @@ -138,7 +148,8 @@ public class JpaPolicyStore implements PolicyStore { // One unreadable row must never abort a bulk read or crash startup. private Optional toPolicy(PolicyEntity entity) { try { - return Optional.of(objectMapper.readValue(entity.getPolicyJson(), Policy.class)); + JsonNode node = upgradeLegacyShape(objectMapper.readTree(entity.getPolicyJson())); + return Optional.of(objectMapper.treeToValue(node, Policy.class)); } catch (Exception e) { log.error( "Skipping unreadable policy id={} name={}: stored JSON could not be parsed" @@ -149,4 +160,35 @@ public class JpaPolicyStore implements PolicyStore { return Optional.empty(); } } + + /** + * Migrate a policy JSON blob written before triggers moved onto inputs. The old shape carried a + * single policy-level {@code trigger} and a {@code sourceIds} list; pair each source with that + * trigger so an upgraded policy keeps firing. A trigger incompatible with a source + * (folder-watch on an S3 source) is simply inert at run time, matching the old behaviour where + * such a source was never watched. New-shape blobs (already carrying {@code inputs}) are + * returned untouched. + */ + private JsonNode upgradeLegacyShape(JsonNode root) { + if (!(root instanceof ObjectNode obj) || obj.has("inputs")) { + return root; + } + JsonNode trigger = obj.get("trigger"); + JsonNode sourceIds = obj.get("sourceIds"); + ArrayNode inputs = objectMapper.createArrayNode(); + if (sourceIds != null && sourceIds.isArray()) { + for (JsonNode sourceId : sourceIds) { + ObjectNode input = objectMapper.createObjectNode(); + input.set("sourceId", sourceId); + if (trigger != null && !trigger.isNull()) { + input.set("trigger", trigger); + } + inputs.add(input); + } + } + obj.set("inputs", inputs); + obj.remove("trigger"); + obj.remove("sourceIds"); + return obj; + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyEntity.java index c871267bc4..fa639b5c19 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyEntity.java @@ -17,10 +17,10 @@ import stirling.software.proprietary.integration.crypto.LegacyDecryptStringConve /** * JPA row for a {@link stirling.software.proprietary.policy.model.Policy}. The whole policy lives * as JSON in {@code policyJson} (authoritative on read); the scalar columns are denormalized copies - * for querying, notably {@code triggerType} + {@code enabled} so background triggers can fetch - * their policies, and {@code teamId} so the caller's team can be loaded without scanning every - * team's rows. {@code owner} and {@code teamId} are plain values, not foreign keys, to stay - * decoupled from the security entities. + * for querying, notably {@code enabled} so background triggers can scan the active policies, and + * {@code teamId} so the caller's team can be loaded without scanning every team's rows. {@code + * owner} and {@code teamId} are plain values, not foreign keys, to stay decoupled from the security + * entities. */ @Entity @Table(name = "policies") @@ -44,9 +44,6 @@ public class PolicyEntity implements Serializable { @Column(name = "enabled") private boolean enabled; - @Column(name = "trigger_type") - private String triggerType; - @Column(name = "team_id") private Long teamId; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyRepository.java index 82ae4355dd..07f31b4a9c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyRepository.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyRepository.java @@ -13,8 +13,12 @@ import jakarta.persistence.LockModeType; @Repository public interface PolicyRepository extends JpaRepository { - /** Enabled policies of a given trigger type, for background triggers to activate. */ - List findByTriggerTypeAndEnabledTrue(String triggerType); + /** + * Enabled policies, for background triggers to scan for inputs of their trigger type. Which + * inputs (and their trigger types) a policy carries lives in the JSON blob, so the type filter + * is applied after parsing rather than in SQL. + */ + List findByEnabledTrue(); /** * Policies belonging to a team, in run order (ascending {@code sortOrder}; a null order sorts @@ -31,6 +35,14 @@ public interface PolicyRepository extends JpaRepository { @Query("select p from PolicyEntity p order by coalesce(p.sortOrder, 0) asc, p.id asc") List findAllOrdered(); + /** + * Whether any policy's stored JSON mentions this id. Matched against the raw column rather than + * parsed steps so a row we can't deserialize still protects the assets it references; asset ids + * are UUIDs, so a substring false positive only ever means "keep". + */ + @Query("select count(p) > 0 from PolicyEntity p where p.policyJson like concat('%', :id, '%')") + boolean anyMentioning(@Param("id") String id); + /** * The team's policy rows, locked for the transaction (SELECT … FOR UPDATE). Appending a new * policy reads the max {@code sortOrder} from these under the lock, so two concurrent creates diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyStore.java index ac9e210439..d86663d596 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/PolicyStore.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; /** Stores {@link Policy} definitions. */ public interface PolicyStore { @@ -18,8 +19,18 @@ public interface PolicyStore { /** Policies owned by the given team, loaded scoped rather than fetched globally. */ List findByTeam(Long teamId); - /** Enabled policies with the given trigger type, for background triggers. */ - List findByTriggerType(String triggerType); + /** + * Whether any policy mentions this stored-asset id, across every team. Asked before reclaiming + * an asset, so it must answer from the raw stored form: a row {@link #all()} skips as + * unreadable still holds its certificate hostage, and deleting that would be unrecoverable. + */ + boolean anyPolicyReferences(String assetId); + + /** + * Enabled inputs with the given trigger type, as {@code (policy, input)} bindings, so a + * background trigger fires each input independently and pulls only its own source. + */ + List findBindingsByTriggerType(String triggerType); /** * Set the team's run order from {@code orderedIds} (position → sortOrder). Only policies that diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java index a4470a418b..5b859a4786 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/FolderWatchTrigger.java @@ -31,7 +31,9 @@ import stirling.software.proprietary.policy.engine.PolicyRunner; import stirling.software.proprietary.policy.engine.SweepKind; import stirling.software.proprietary.policy.input.InputSource; import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.source.Source; import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; @@ -85,10 +87,10 @@ public class FolderWatchTrigger implements PolicyTrigger { } @Override - public void validate(Policy policy) { - if (watchDirsOf(policy).isEmpty()) { + public void validate(Policy policy, PipelineInput input) { + if (watchDirsOf(input).isEmpty()) { throw new IllegalArgumentException( - "folder-watch trigger requires at least one watchable (folder) input source"); + "folder-watch trigger requires a watchable (folder) input source"); } } @@ -185,24 +187,30 @@ public class FolderWatchTrigger implements PolicyTrigger { return changed; } - /** Run every folder-watch policy that draws from one of the changed directories. */ + /** Fire every folder-watch input that draws from one of the changed directories. */ void runForChangedDirs(Set changedDirs) { if (changedDirs.isEmpty()) { return; } - for (Policy policy : policyStore.findByTriggerType(TYPE)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { List dirs; try { - dirs = watchDirsOf(policy); + dirs = watchDirsOf(binding.input()); } catch (RuntimeException e) { log.warn( - "Folder-watch policy {} is misconfigured: {}", policy.id(), e.getMessage()); + "Folder-watch input {}/{} is misconfigured: {}", + binding.policy().id(), + binding.input().sourceId(), + e.getMessage()); continue; } if (dirs.stream().anyMatch(changedDirs::contains)) { - log.debug("Folder-watch policy {} ({}) saw activity", policy.id(), policy.name()); + log.debug( + "Folder-watch input {}/{} saw activity", + binding.policy().id(), + binding.input().sourceId()); // Light: the periodic reconcile does the full sweep. - policyRunner.run(policy, SweepKind.LIGHT); + policyRunner.runInput(binding.policy(), binding.input(), SweepKind.LIGHT); } } } @@ -216,15 +224,16 @@ public class FolderWatchTrigger implements PolicyTrigger { } } - /** Reconcile safety net: run every folder-watch policy regardless of watch events. */ + /** Reconcile safety net: run every folder-watch input regardless of watch events. */ void runAll() { - for (Policy policy : policyStore.findByTriggerType(TYPE)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { try { - policyRunner.run(policy); + policyRunner.runInput(binding.policy(), binding.input(), SweepKind.FULL); } catch (RuntimeException e) { log.warn( - "Folder-watch reconcile run failed for policy {}: {}", - policy.id(), + "Folder-watch reconcile run failed for input {}/{}: {}", + binding.policy().id(), + binding.input().sourceId(), e.getMessage()); } } @@ -269,41 +278,43 @@ public class FolderWatchTrigger implements PolicyTrigger { return Set.copyOf(keysByDir.keySet()); } - /** Every existing directory any current folder-watch policy wants watched. */ + /** Every existing directory any current folder-watch input wants watched. */ private Set desiredDirs() { Set dirs = new HashSet<>(); - for (Policy policy : policyStore.findByTriggerType(TYPE)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { try { - for (Path dir : watchDirsOf(policy)) { + for (Path dir : watchDirsOf(binding.input())) { if (Files.isDirectory(dir)) { dirs.add(dir); } } } catch (RuntimeException e) { log.warn( - "Folder-watch policy {} is misconfigured: {}", policy.id(), e.getMessage()); + "Folder-watch input {}/{} is misconfigured: {}", + binding.policy().id(), + binding.input().sourceId(), + e.getMessage()); } } return dirs; } // Absolute + normalised so registration keys and event-time matching compare regardless of how - // the path was configured. - private List watchDirsOf(Policy policy) { + // the path was configured. Empty for a non-folder or missing source (that input is never + // watched), so a folder-watch trigger paired with an S3 input is simply inert. + private List watchDirsOf(PipelineInput input) { List dirs = new ArrayList<>(); - for (String sourceId : policy.sourceIds()) { - Source source = sourceStore.get(sourceId).orElse(null); - if (source == null) { - continue; - } - InputSpec spec = source.toInputSpec(); - InputSource inputSource = sourceFor(spec); - if (inputSource == null) { - continue; - } - for (Path dir : inputSource.watchTargets(spec)) { - dirs.add(dir.toAbsolutePath().normalize()); - } + Source source = sourceStore.get(input.sourceId()).orElse(null); + if (source == null) { + return dirs; + } + InputSpec spec = source.toInputSpec(); + InputSource inputSource = sourceFor(spec); + if (inputSource == null) { + return dirs; + } + for (Path dir : inputSource.watchTargets(spec)) { + dirs.add(dir.toAbsolutePath().normalize()); } return dirs; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTrigger.java index ade5357162..e1e0cc7873 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/PolicyTrigger.java @@ -2,11 +2,13 @@ package stirling.software.proprietary.policy.trigger; import java.util.Set; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; /** - * Decides when a policy runs. On firing it hands the policy to {@code PolicyRunner}; it - * never resolves sources itself. New trigger kinds are just new beans of this type. + * Decides when a policy input runs. On firing it hands the binding to {@code + * PolicyRunner}, which pulls only that input's source; it never resolves sources itself. New + * trigger kinds are just new beans of this type. */ public interface PolicyTrigger { @@ -32,10 +34,11 @@ public interface PolicyTrigger { } /** - * Validate at save time so misconfiguration fails fast, not at fire time. Receives the whole - * {@link Policy} so triggers that depend on the policy's sources (folder-watch) can check that. + * Validate one input's use of this trigger at save time so misconfiguration fails fast, not at + * fire time. Receives the owning {@link Policy} and the specific {@link PipelineInput} so a + * trigger that depends on the input's source (folder-watch) can check it. */ - default void validate(Policy policy) {} + default void validate(Policy policy, PipelineInput input) {} default void start() {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java index 2018ffd126..f4ffb30146 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/ScheduleTrigger.java @@ -17,14 +17,18 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.policy.engine.PolicyRunner; +import stirling.software.proprietary.policy.engine.SweepKind; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.Schedule; import stirling.software.proprietary.policy.store.PolicyStore; import tools.jackson.databind.ObjectMapper; /** - * Fires policies on a {@link Schedule}: a fixed-interval sweep runs each due "schedule" policy. + * Fires policy inputs on a {@link Schedule}: a fixed-interval sweep pulls each due "schedule" + * input, independently of the policy's other inputs. * *

Last-fire times are in memory, so this assumes a single node and resets on restart. */ @@ -40,17 +44,20 @@ public class ScheduleTrigger implements PolicyTrigger { private final ObjectMapper objectMapper; private final ApplicationProperties applicationProperties; - private final Map lastFiredByPolicy = new ConcurrentHashMap<>(); + private final Map lastFiredByBinding = new ConcurrentHashMap<>(); private volatile ScheduledExecutorService scheduler; + /** Identifies a schedule binding: one input (by source) of one policy. */ + private record BindingKey(String policyId, String sourceId) {} + @Override public String type() { return TYPE; } @Override - public void validate(Policy policy) { - ScheduleConfig.from(objectMapper, policy.trigger().options()); + public void validate(Policy policy, PipelineInput input) { + ScheduleConfig.from(objectMapper, input.trigger().options()); } @Override @@ -83,19 +90,26 @@ public class ScheduleTrigger implements PolicyTrigger { } } - /** Fire every scheduled policy that is due as of {@code now}. Package-visible for testing. */ + /** Fire every scheduled input that is due as of {@code now}. Package-visible for testing. */ void sweep(Instant now) { - for (Policy policy : policyStore.findByTriggerType(TYPE)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { + Policy policy = binding.policy(); + PipelineInput input = binding.input(); ScheduleConfig config; try { - config = ScheduleConfig.from(objectMapper, policy.trigger().options()); + config = ScheduleConfig.from(objectMapper, input.trigger().options()); } catch (IllegalArgumentException e) { - log.warn("Scheduled policy {} is misconfigured: {}", policy.id(), e.getMessage()); + log.warn( + "Scheduled input {}/{} is misconfigured: {}", + policy.id(), + input.sourceId(), + e.getMessage()); continue; } - // Baseline a newly-seen policy to now so it does not fire immediately. - Instant last = lastFiredByPolicy.computeIfAbsent(policy.id(), id -> now); + // Baseline a newly-seen binding to now so it does not fire immediately. + BindingKey key = new BindingKey(policy.id(), input.sourceId()); + Instant last = lastFiredByBinding.computeIfAbsent(key, id -> now); ZonedDateTime next = config.schedule().nextAfter(last.atZone(config.zone())); if (next.toInstant().isAfter(now)) { continue; @@ -105,9 +119,13 @@ public class ScheduleTrigger implements PolicyTrigger { next = later; later = config.schedule().nextAfter(later); } - lastFiredByPolicy.put(policy.id(), next.toInstant()); - log.info("Scheduled policy {} ({}) is due", policy.id(), policy.name()); - policyRunner.run(policy); + lastFiredByBinding.put(key, next.toInstant()); + log.info( + "Scheduled input {}/{} ({}) is due", + policy.id(), + input.sourceId(), + policy.name()); + policyRunner.runInput(policy, input, SweepKind.FULL); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java index 816f510bba..d5bb412a7c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/trigger/WebhookTrigger.java @@ -13,7 +13,9 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.policy.engine.PolicyRunner; import stirling.software.proprietary.policy.engine.SweepKind; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.source.Source; import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; @@ -50,15 +52,14 @@ public class WebhookTrigger implements PolicyTrigger { } @Override - public void validate(Policy policy) { - boolean hasWebhookSource = - policy.sourceIds().stream() - .map(sourceStore::get) - .flatMap(java.util.Optional::stream) - .anyMatch(source -> WEBHOOK_SOURCE_TYPE.equals(source.type())); - if (!hasWebhookSource) { - throw new IllegalArgumentException( - "webhook trigger requires at least one webhook input source"); + public void validate(Policy policy, PipelineInput input) { + boolean isWebhookSource = + sourceStore + .get(input.sourceId()) + .filter(source -> WEBHOOK_SOURCE_TYPE.equals(source.type())) + .isPresent(); + if (!isWebhookSource) { + throw new IllegalArgumentException("webhook trigger requires a webhook input source"); } } @@ -83,29 +84,38 @@ public class WebhookTrigger implements PolicyTrigger { } } + /** Fire every webhook input fed by this webhook, pulling only that input's source. */ public void fireForWebhook(String webhookId) { - for (Policy policy : policyStore.findByTriggerType(TYPE)) { - if (!referencesWebhook(policy, webhookId)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { + if (!referencesWebhook(binding.input(), webhookId)) { continue; } try { - log.debug("Webhook policy {} ({}) saw a delivery", policy.id(), policy.name()); - policyRunner.run(policy, SweepKind.LIGHT); + log.debug( + "Webhook input {}/{} saw a delivery", + binding.policy().id(), + binding.input().sourceId()); + policyRunner.runInput(binding.policy(), binding.input(), SweepKind.LIGHT); } catch (RuntimeException e) { - log.warn("Webhook run failed for policy {}: {}", policy.id(), e.getMessage()); + log.warn( + "Webhook run failed for input {}/{}: {}", + binding.policy().id(), + binding.input().sourceId(), + e.getMessage()); } } } private void safeReconcile() { try { - for (Policy policy : policyStore.findByTriggerType(TYPE)) { + for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) { try { - policyRunner.run(policy); + policyRunner.runInput(binding.policy(), binding.input(), SweepKind.FULL); } catch (RuntimeException e) { log.warn( - "Webhook reconcile run failed for policy {}: {}", - policy.id(), + "Webhook reconcile run failed for input {}/{}: {}", + binding.policy().id(), + binding.input().sourceId(), e.getMessage()); } } @@ -114,17 +124,13 @@ public class WebhookTrigger implements PolicyTrigger { } } - private boolean referencesWebhook(Policy policy, String webhookId) { - for (String sourceId : policy.sourceIds()) { - Source source = sourceStore.get(sourceId).orElse(null); - if (source == null || !WEBHOOK_SOURCE_TYPE.equals(source.type())) { - continue; - } - Object configured = source.options().get(WebhookConfig.WEBHOOK_ID_OPTION); - if (configured != null && configured.toString().equals(webhookId)) { - return true; - } + /** Whether this input draws from the webhook source the delivery arrived on. */ + private boolean referencesWebhook(PipelineInput input, String webhookId) { + Source source = sourceStore.get(input.sourceId()).orElse(null); + if (source == null || !WEBHOOK_SOURCE_TYPE.equals(source.type())) { + return false; } - return false; + Object configured = source.options().get(WebhookConfig.WEBHOOK_ID_OPTION); + return configured != null && configured.toString().equals(webhookId); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java index eef27703f2..36dcaecfd2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java @@ -2,7 +2,6 @@ package stirling.software.proprietary.security.configuration; import java.time.Duration; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCacheManager; @@ -19,7 +18,6 @@ public class CacheConfig { private final ApplicationProperties applicationProperties; - @Autowired public CacheConfig(ApplicationProperties applicationProperties) { this.applicationProperties = applicationProperties; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java index a9c567885f..389ffa535e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java @@ -32,24 +32,30 @@ import stirling.software.common.model.exception.UnsupportedProviderException; "stirling.software.proprietary.repository", "stirling.software.proprietary.storage.repository", "stirling.software.proprietary.workflow.repository", + "stirling.software.proprietary.policy.asset", "stirling.software.proprietary.policy.store", "stirling.software.proprietary.policy.source", + "stirling.software.proprietary.policy.migration", "stirling.software.proprietary.policy.ledger", "stirling.software.proprietary.accountlink", "stirling.software.proprietary.access.repository", - "stirling.software.proprietary.integration.repository" + "stirling.software.proprietary.integration.repository", + "stirling.software.proprietary.failure" }) @EntityScan({ "stirling.software.proprietary.security.model", "stirling.software.proprietary.model", "stirling.software.proprietary.storage.model", "stirling.software.proprietary.workflow.model", + "stirling.software.proprietary.policy.asset", "stirling.software.proprietary.policy.store", "stirling.software.proprietary.policy.source", + "stirling.software.proprietary.policy.migration", "stirling.software.proprietary.policy.ledger", "stirling.software.proprietary.accountlink", "stirling.software.proprietary.access.model", - "stirling.software.proprietary.integration.model" + "stirling.software.proprietary.integration.model", + "stirling.software.proprietary.failure" }) public class DatabaseConfig { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java index 213e08ea0c..8145531dc0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java @@ -46,9 +46,7 @@ public class EmailController { resourceWeight = ResourceWeight.SMALL_WEIGHT) @Operation( summary = "Send an email with an attachment", - description = - "This endpoint sends an email with an attachment. Input:PDF" - + " Output:Success/Failure Type:MISO") + description = "This endpoint sends an email with an attachment.") public ResponseEntity sendEmailWithAttachment(@Valid @ModelAttribute Email email) { log.info("Sending email to: {}", email.toString()); try { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java index b19c052ff1..fdacda72b2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java @@ -199,7 +199,7 @@ public class UserController { return ResponseEntity.status(HttpStatus.CONFLICT) .body(Map.of("error", "usernameExists", "message", "Username already exists")); } - if (newUsername != null && newUsername.length() > 0) { + if (newUsername != null && !newUsername.isEmpty()) { try { userService.changeUsername(user, newUsername); } catch (IllegalArgumentException e) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java index fe9c9f4209..312cddb662 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/PersistentLogin.java @@ -1,17 +1,23 @@ package stirling.software.proprietary.security.model; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.Data; +import lombok.*; @Entity @Table(name = "persistent_logins") -@Data +@Getter +@Setter +@ToString(onlyExplicitlyIncluded = true) +@NoArgsConstructor public class PersistentLogin { @Id @@ -19,11 +25,40 @@ public class PersistentLogin { private String series; @Column(name = "username", length = 64, nullable = false) + @ToString.Include private String username; @Column(name = "token", length = 64, nullable = false) private String token; @Column(name = "last_used", nullable = false) + @ToString.Include private Instant lastUsed; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + PersistentLogin that = (PersistentLogin) o; + return getSeries() != null && Objects.equals(getSeries(), that.getSeries()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java index 552d97d022..44b2153500 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/SessionEntity.java @@ -2,16 +2,22 @@ package stirling.software.proprietary.security.model; import java.io.Serializable; import java.time.Instant; +import java.util.Objects; + +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.Table; -import lombok.Data; +import lombok.*; @Entity -@Data +@Getter +@Setter +@ToString +@NoArgsConstructor @Table( name = "sessions", indexes = { @@ -23,11 +29,42 @@ import lombok.Data; @Index(name = "idx_sessions_expired", columnList = "expired") }) public class SessionEntity implements Serializable { - @Id private String sessionId; + @Id + @Setter(AccessLevel.NONE) + private String sessionId; private String principalName; private Instant lastRequest; private boolean expired; + + public void setSessionId(String sessionId) { + if (this.sessionId != null && !this.sessionId.equals(sessionId)) { + throw new IllegalStateException("sessionId is immutable once set"); + } + this.sessionId = sessionId; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + SessionEntity that = (SessionEntity) o; + return getSessionId() != null && Objects.equals(getSessionId(), that.getSessionId()); + } + + @Override + public final int hashCode() { + return getSessionId() != null ? getSessionId().hashCode() : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java index 32733f5fc5..9455ed6e4b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java @@ -2,27 +2,19 @@ package stirling.software.proprietary.security.model; import java.io.Serializable; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.proxy.HibernateProxy; import org.springframework.security.core.userdetails.UserDetails; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; import stirling.software.common.model.enumeration.Role; import stirling.software.proprietary.model.Team; @@ -35,7 +27,6 @@ import stirling.software.proprietary.model.Team; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class User implements UserDetails, Serializable { @@ -44,7 +35,6 @@ public class User implements UserDetails, Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") - @EqualsAndHashCode.Include private Long id; @Column(name = "username", unique = true) @@ -181,4 +171,31 @@ public class User implements UserDetails, Serializable { public void setOauthGrandfathered(boolean oauthGrandfathered) { this.oauthGrandfathered = oauthGrandfathered; } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + User user = (User) o; + return getId() != null && Objects.equals(getId(), user.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java index 0acc98ee55..b2ce4adb68 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java @@ -61,7 +61,7 @@ public class CustomSaml2ResponseAuthenticationConverter @Override public Saml2Authentication convert(ResponseToken responseToken) { - Assertion assertion = responseToken.getResponse().getAssertions().get(0); + Assertion assertion = responseToken.getResponse().getAssertions().getFirst(); Map> attributes = extractAttributes(assertion); // Debug log with actual values @@ -117,6 +117,6 @@ public class CustomSaml2ResponseAuthenticationConverter private String getFirstAttributeValue(Map> attributes, String name) { List values = attributes.get(name); - return values != null && !values.isEmpty() ? values.get(0).toString() : null; + return values != null && !values.isEmpty() ? values.getFirst().toString() : null; } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java index f18f6bd160..2c074c0a6b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java @@ -232,7 +232,7 @@ public class DatabaseService implements DatabaseServiceInterface { List backupList = this.getBackupList(); backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed()); - Path latestExport = Path.of(backupList.get(0).getFilePath()); + Path latestExport = Path.of(backupList.getFirst().getFilePath()); executeDatabaseScript(latestExport); } @@ -376,7 +376,7 @@ public class DatabaseService implements DatabaseServiceInterface { List backupList = this.getBackupList(); List> deletedFiles = new ArrayList<>(); if (!backupList.isEmpty()) { - FileInfo lastBackup = backupList.get(backupList.size() - 1); + FileInfo lastBackup = backupList.getLast(); try { Files.deleteIfExists(Path.of(lastBackup.getFilePath())); deletedFiles.add(Pair.of(lastBackup, true)); @@ -399,7 +399,7 @@ public class DatabaseService implements DatabaseServiceInterface { Comparator.comparing( p -> p.getFileName().substring(7, p.getFileName().length() - 4))); - FileInfo oldestFile = filteredBackupList.get(0); + FileInfo oldestFile = filteredBackupList.getFirst(); Files.deleteIfExists(Path.of(oldestFile.getFilePath())); log.info("Deleted oldest backup: {}", oldestFile.getFileName()); } catch (IOException e) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index 0cb4653ef1..e08dd52d07 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -205,7 +205,7 @@ public class UserService implements UserServiceInterface { User user = findByUsernameIgnoreCase(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); - if (user.getApiKey() == null || user.getApiKey().length() == 0) { + if (user.getApiKey() == null || user.getApiKey().isEmpty()) { user = addApiKeyToUser(username); } return user.getApiKey(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java index 19adb9a970..e615416e59 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java @@ -193,6 +193,6 @@ public class SessionPersistentRegistry implements SessionRegistry { allSessions.sort((s1, s2) -> s2.getLastRequest().compareTo(s1.getLastRequest())); // The first session in the list is the latest session for the given principal name - return Optional.of(allSessions.get(0)); + return Optional.of(allSessions.getFirst()); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java index c47df3649f..9a76cdd25b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/PortalInfraAuditService.java @@ -224,7 +224,7 @@ public class PortalInfraAuditService { if (word.isEmpty()) { continue; } - if (sb.length() > 0) { + if (!sb.isEmpty()) { sb.append(' '); } String lower = word.toLowerCase(Locale.ROOT); @@ -299,7 +299,7 @@ public class PortalInfraAuditService { Object files = data.get("files"); if (files instanceof List list && !list.isEmpty() - && list.get(0) instanceof Map f) { + && list.getFirst() instanceof Map f) { Object name = ((Map) f).get("name"); return name != null ? String.valueOf(name) : null; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java index 54660a1ccb..085a9ffcf1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/UserLicenseSettingsService.java @@ -515,7 +515,7 @@ public class UserLicenseSettingsService { appendIfPresent(builder, applicationProperties.getAutomaticallyGenerated().getUUID()); appendIfPresent(builder, applicationProperties.getPremium().getKey()); - if (builder.length() == 0) { + if (builder.isEmpty()) { builder.append(DEFAULT_INTEGRITY_SECRET); } @@ -524,7 +524,7 @@ public class UserLicenseSettingsService { private void appendIfPresent(StringBuilder builder, String value) { if (value != null && !value.isBlank()) { - if (builder.length() > 0) { + if (!builder.isEmpty()) { builder.append(SIGNATURE_SEPARATOR); } builder.append(value); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/AccessibilityAuditService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/AccessibilityAuditService.java new file mode 100644 index 0000000000..869d489a04 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/AccessibilityAuditService.java @@ -0,0 +1,187 @@ +package stirling.software.proprietary.service.ua; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.ExceptionUtils; +import stirling.software.proprietary.model.api.ua.AccessibilityIssue; +import stirling.software.proprietary.model.api.ua.AccessibilityReport; +import stirling.software.proprietary.model.api.ua.FigureDescriptor; +import stirling.software.proprietary.model.api.ua.UaValidationResult; +import stirling.software.proprietary.pdf.ua.BBox; +import stirling.software.proprietary.pdf.ua.DocumentStructure; +import stirling.software.proprietary.pdf.ua.LayoutAnalyzer; +import stirling.software.proprietary.pdf.ua.PageContent; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.StructBlock; +import stirling.software.proprietary.pdf.ua.StructType; +import stirling.software.proprietary.pdf.ua.TaggedContentExtractor; + +/** Produces an accessibility report without changing the document. */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AccessibilityAuditService { + + /** Checks no validator can make; omitting them implies the work does not exist. */ + private static final List HUMAN_CHECKS = + List.of( + "Is the reading order correct for someone who cannot see the layout?", + "Does each alternative description convey what the image is for, not just what" + + " it looks like?", + "Are headings used for structure rather than for visual emphasis?", + "Is any information conveyed by colour alone also available another way?", + "Do tables have headers that identify the right rows and columns?", + "Is the document language correct, including for quoted passages?", + "Do links describe their destination rather than saying 'click here'?"); + + /** The report walks every page and validates, so it carries the conversion's own caps. */ + private static final long MAX_INPUT_BYTES = 100L * 1024 * 1024; + + private static final int MAX_PAGES = 2000; + + private final PdfUaValidationService validationService; + + public AccessibilityReport audit(byte[] pdfBytes, PdfUaProfile profile) throws IOException { + enforceLimits(pdfBytes); + AccessibilityReport report = new AccessibilityReport(); + report.setProfile(profile.displayName()); + + UaValidationResult validation = validationService.validate(pdfBytes, profile); + report.setIssues(validation.issues()); + report.setPassesAutomatedChecks(validation.compliant()); + report.setHumanChecks(HUMAN_CHECKS); + + int fixable = 0; + int needsInput = 0; + for (AccessibilityIssue issue : validation.issues()) { + if (issue.isAutoFixable()) { + fixable++; + } else { + needsInput++; + } + } + report.setAutomaticallyFixable(fixable); + report.setNeedsInput(needsInput); + + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + populateSummary(document, report); + report.setFiguresNeedingDescription(figuresNeedingDescription(document)); + } catch (IOException e) { + log.debug("Could not inspect document for the summary: {}", e.getMessage()); + } + return report; + } + + /** + * Rejects before the expensive pass. An unreadable file is left to the report itself to say. + */ + private static void enforceLimits(byte[] pdfBytes) { + if (pdfBytes.length > MAX_INPUT_BYTES) { + throw ExceptionUtils.createIllegalArgumentException( + "error.fileTooLarge", + "This PDF is {0} MB. The accessibility report is limited to {1} MB.", + pdfBytes.length / (1024 * 1024), + MAX_INPUT_BYTES / (1024 * 1024)); + } + int pages; + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + pages = document.getNumberOfPages(); + } catch (IOException e) { + return; + } + if (pages > MAX_PAGES) { + throw ExceptionUtils.createIllegalArgumentException( + "error.tooManyPages", + "This PDF has {0} pages. The accessibility report is limited to {1} pages;" + + " split it first.", + pages, + MAX_PAGES); + } + } + + private void populateSummary(PDDocument document, AccessibilityReport report) + throws IOException { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + AccessibilityReport.Summary summary = report.getSummary(); + + report.setTagged(catalog.getStructureTreeRoot() != null); + report.setDeclaresConformance(declaresUa(document)); + + summary.setPages(document.getNumberOfPages()); + summary.setEncrypted(document.isEncrypted()); + summary.setHasLanguage(catalog.getLanguage() != null && !catalog.getLanguage().isBlank()); + + String title = document.getDocumentInformation().getTitle(); + summary.setHasTitle(title != null && !title.isBlank()); + + PDViewerPreferences preferences = catalog.getViewerPreferences(); + summary.setDisplaysDocTitle(preferences != null && preferences.displayDocTitle()); + + Set unembedded = FontEmbeddingService.findUnembeddedFonts(document); + summary.setUnembeddedFonts(unembedded.size()); + summary.setAllFontsEmbedded(unembedded.isEmpty()); + + try { + summary.setFigures(new TaggedContentExtractor().countGraphics(document)); + } catch (Exception e) { + log.debug("Could not count figures: {}", e.getMessage()); + } + } + + /** + * Lists the figures a conversion would leave undescribed, running the converter's own analysis + * because counting raster images would miss vector charts and existing descriptions. + */ + private List figuresNeedingDescription(PDDocument document) { + try { + List pages = new TaggedContentExtractor().extract(document); + DocumentStructure structure = new LayoutAnalyzer().analyse(pages); + List figures = new ArrayList<>(); + for (StructBlock block : structure.figuresWithoutAlt()) { + int ordinal = block.getRanges().isEmpty() ? -1 : block.getRanges().get(0).start(); + BBox box = block.getBbox(); + figures.add( + new FigureDescriptor( + block.getPageIndex() + ":" + ordinal, + block.getPageIndex() + 1, + block.getType() == StructType.FORMULA ? "formula" : "figure", + box.x0(), + box.y0(), + box.width(), + box.height(), + block.getAlt())); + } + return figures; + } catch (Exception e) { + log.debug("Could not enumerate figures: {}", e.getMessage()); + return List.of(); + } + } + + /** True when the XMP packet carries a pdfuaid identifier. */ + private static boolean declaresUa(PDDocument document) { + try { + var metadata = document.getDocumentCatalog().getMetadata(); + if (metadata == null) { + return false; + } + String xmp = + new String(metadata.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); + return xmp.contains("pdfuaid"); + } catch (IOException e) { + return false; + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/FontEmbeddingService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/FontEmbeddingService.java new file mode 100644 index 0000000000..9c38b54f6b --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/FontEmbeddingService.java @@ -0,0 +1,254 @@ +package stirling.software.proprietary.service.ua; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; + +/** + * Embeds any font the document references but does not carry, as PDF/UA-1 clause 7.21 requires. + * Ghostscript does the embedding and discards the structure tree, so this must run before tagging. + */ +@Service +@Slf4j +public class FontEmbeddingService { + + public boolean hasUnembeddedFonts(byte[] pdfBytes) { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + return !findUnembeddedFonts(document).isEmpty(); + } catch (IOException e) { + log.debug("Could not inspect fonts: {}", e.getMessage()); + return false; + } + } + + public static Set findUnembeddedFonts(PDDocument document) { + Set missing = new HashSet<>(); + for (PDPage page : document.getPages()) { + PDResources resources = page.getResources(); + if (resources == null) { + continue; + } + for (COSName name : resources.getFontNames()) { + try { + PDFont font = resources.getFont(name); + if (font != null && !font.isEmbedded()) { + missing.add(font.getName()); + } + } catch (IOException e) { + log.debug("Could not read font {}: {}", name.getName(), e.getMessage()); + } + } + } + return missing; + } + + /** + * Returns the document with all fonts embedded, or the input unchanged. Never throws: failing + * to embed is a reportable shortfall, not a reason to abandon the conversion. + */ + public Result embedFonts(byte[] pdfBytes) { + Set missing; + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + missing = findUnembeddedFonts(document); + } catch (IOException e) { + return new Result( + pdfBytes, false, Set.of(), "Could not inspect fonts: " + e.getMessage()); + } + if (missing.isEmpty()) { + return new Result(pdfBytes, false, Set.of(), null); + } + if (!isGhostscriptAvailable()) { + return new Result( + pdfBytes, + false, + missing, + "Ghostscript is not installed, so " + + missing.size() + + " unembedded font(s) could not be embedded. PDF/UA requires every font" + + " to be embedded."); + } + + Path workingDir = null; + try { + workingDir = Files.createTempDirectory("pdfua_fonts_"); + Path input = workingDir.resolve("input.pdf"); + Path output = workingDir.resolve("output.pdf"); + Files.write(input, pdfBytes); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command(input, output, workingDir)); + + if (result.getRc() != 0 || !Files.exists(output)) { + return new Result( + pdfBytes, + false, + missing, + "Font embedding failed with code " + result.getRc()); + } + byte[] embedded = Files.readAllBytes(output); + + // Ghostscript can exit 0 having written a blank page, so keep the original rather than + // return an empty document. + if (!survived(pdfBytes, embedded)) { + log.warn("Ghostscript produced a degenerate document; keeping the original"); + return new Result( + pdfBytes, + false, + missing, + "Font embedding was skipped because the embedder returned a document that" + + " had lost content. " + + missing.size() + + " font(s) remain unembedded."); + } + + // It can also exit 0 while simply leaving fonts unembedded. + Set remaining; + try (PDDocument check = Loader.loadPDF(embedded)) { + remaining = findUnembeddedFonts(check); + } + if (!remaining.isEmpty()) { + return new Result( + embedded, + true, + remaining, + remaining.size() + + " font(s) could not be embedded (" + + String.join(", ", remaining) + + "). PDF/UA requires every font to be embedded."); + } + log.info("Embedded {} previously unembedded font(s)", missing.size()); + return new Result(embedded, true, missing, null); + + } catch (Exception e) { + log.warn("Font embedding failed: {}", e.getMessage()); + return new Result(pdfBytes, false, missing, "Font embedding failed: " + e.getMessage()); + } finally { + deleteQuietly(workingDir); + } + } + + /** + * True when the rewritten document still holds the original's content. A collapse in page count + * or content-stream size is the only signature of a failed rewrite the exit code hides. + */ + private static boolean survived(byte[] original, byte[] rewritten) { + try (PDDocument before = Loader.loadPDF(original); + PDDocument after = Loader.loadPDF(rewritten)) { + if (after.getNumberOfPages() != before.getNumberOfPages()) { + return false; + } + long beforeBytes = contentBytes(before); + long afterBytes = contentBytes(after); + if (beforeBytes == 0) { + return true; + } + return afterBytes * 20L >= beforeBytes; + } catch (IOException e) { + log.debug("Could not compare documents after embedding: {}", e.getMessage()); + return false; + } + } + + private static long contentBytes(PDDocument document) { + long total = 0; + for (PDPage page : document.getPages()) { + try (InputStream in = page.getContents()) { + if (in != null) { + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) > 0) { + total += read; + } + } + } catch (IOException e) { + log.debug("Could not measure page content: {}", e.getMessage()); + } + } + return total; + } + + private static List command(Path input, Path output, Path workingDir) { + List command = new ArrayList<>(); + command.add("gs"); + command.add("--permit-file-read=" + workingDir.toAbsolutePath()); + command.add("--permit-file-write=" + workingDir.toAbsolutePath()); + command.add("-sDEVICE=pdfwrite"); + command.add("-dEmbedAllFonts=true"); + command.add("-dSubsetFonts=true"); + command.add("-dCompressFonts=true"); + command.add("-dNOSUBSTFONTS=false"); + command.add("-dPDFSETTINGS=/prepress"); + command.add("-dNOPAUSE"); + command.add("-dBATCH"); + command.add("-sOutputFile=" + output.toAbsolutePath()); + command.add(input.toAbsolutePath().toString()); + return command; + } + + /** Cached after the first probe: availability does not change mid-process. */ + private volatile Boolean ghostscriptAvailable; + + private boolean isGhostscriptAvailable() { + Boolean cached = ghostscriptAvailable; + if (cached != null) { + return cached; + } + boolean available; + try { + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(List.of("gs", "--version")); + available = result.getRc() == 0; + } catch (Exception e) { + log.debug("Ghostscript availability check failed: {}", e.getMessage()); + available = false; + } + ghostscriptAvailable = available; + return available; + } + + private static void deleteQuietly(Path directory) { + if (directory == null) { + return; + } + try (Stream stream = Files.walk(directory)) { + stream.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.debug("Could not delete {}", path); + } + }); + } catch (IOException e) { + log.debug("Could not clean {}", directory); + } + } + + /** + * @param warning non-null when fonts remain unembedded, for the conversion report + */ + public record Result(byte[] pdfBytes, boolean changed, Set fonts, String warning) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaConversionService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaConversionService.java new file mode 100644 index 0000000000..1d0920c109 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaConversionService.java @@ -0,0 +1,251 @@ +package stirling.software.proprietary.service.ua; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.model.api.ua.UaValidationResult; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.PdfUaTagger; +import stirling.software.proprietary.pdf.ua.SourceFacts; +import stirling.software.proprietary.pdf.ua.TaggingOptions; +import stirling.software.proprietary.pdf.ua.TaggingResult; + +/** + * Converts a PDF to PDF/UA. The declaration is written first and withdrawn unless validation + * passes, so a returned file either conforms or does not claim to. + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class PdfUaConversionService { + + private final PdfUaValidationService validationService; + private final FontEmbeddingService fontEmbeddingService; + private final stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory; + + /** Matches the cap GetInfoOnPDF already applies to comparable whole-document work. */ + private static final long MAX_INPUT_BYTES = 100L * 1024 * 1024; + + /** Beyond this the structure model alone runs to hundreds of megabytes. */ + private static final int MAX_PAGES = 2000; + + public PdfUaConversionOutcome convert(byte[] input, TaggingOptions options) throws IOException { + if (input.length > MAX_INPUT_BYTES) { + throw new IOException( + "This PDF is " + + (input.length / (1024 * 1024)) + + " MB. PDF/UA conversion is limited to " + + (MAX_INPUT_BYTES / (1024 * 1024)) + + " MB."); + } + PdfUaProfile profile = options.getProfile(); + List warnings = new ArrayList<>(); + + // Read the document's own facts before anything rewrites it. Font embedding runs + // Ghostscript over the whole file, which discards the structure tree, /Lang and XFA, so + // every guard and every "what did the source say" question must be answered from here. + SourceFacts facts; + try (PDDocument original = load(input)) { + rejectUnsupportedSource(original); + warnSignatures(original, warnings); + facts = SourceFacts.of(original); + } + + byte[] source = input; + if (options.isEmbedFonts()) { + // Must precede tagging: the embedder rewrites the file and drops any structure tree. + FontEmbeddingService.Result fonts = fontEmbeddingService.embedFonts(input); + source = fonts.pdfBytes(); + if (fonts.warning() != null) { + warnings.add(fonts.warning()); + } + source = keepTagsOverFonts(input, source, facts, options, warnings); + } + + TaggingOptions effective = options.toBuilder().sourceFacts(facts).build(); + + // Tag and declare in one pass; the claim is withdrawn below if validation disagrees. + byte[] declared; + TaggingResult taggingResult; + PdfUaTagger tagger = new PdfUaTagger(); + try (PDDocument document = load(source)) { + rejectEncrypted(document); + taggingResult = tagger.tag(document, effective); + warnings.addAll(taggingResult.getWarnings()); + tagger.declareConformance(document, profile); + declared = save(document); + } + + UaValidationResult validation = validationService.validate(declared, profile); + + // A validator cannot see text hidden behind artifact markers, so a clean verdict over + // suppressed content would be a false claim. + boolean honest = !taggingResult.isContentSuppressed(); + + if (validation.compliant() && honest) { + log.info("{} conversion passed validation", profile.displayName()); + return new PdfUaConversionOutcome( + declared, true, validation, summary(taggingResult), warnings); + } + + byte[] undeclared; + try (PDDocument document = load(declared)) { + tagger.withdrawConformance(document); + undeclared = save(document); + } + + if (!validation.compliant()) { + warnings.add( + "The document could not be made " + + profile.displayName() + + " conformant, so no conformance claim was written. " + + validation.totalFailures() + + " automated check(s) still fail."); + } + log.info( + "{} conversion left undeclared: {} failures, suppressedText={}", + profile.displayName(), + validation.totalFailures(), + !honest); + return new PdfUaConversionOutcome( + undeclared, false, validation, summary(taggingResult), warnings); + } + + private static PdfUaConversionOutcome.TaggingSummary summary(TaggingResult result) { + return new PdfUaConversionOutcome.TaggingSummary( + result.isRebuilt(), + result.getTaggedElements(), + result.getArtifacts(), + result.getFiguresNeedingAlt()); + } + + /** + * Tagging rewrites the content streams a signature covers, so the conversion still runs but the + * caller has to know the signature will no longer verify. + */ + private static void warnSignatures(PDDocument document, List warnings) { + int signatures = document.getSignatureDictionaries().size(); + if (signatures > 0) { + warnings.add( + signatures + + " digital signature(s) will stop verifying: tagging rewrites the" + + " content streams they cover. Convert first, then re-sign."); + } + } + + /** Replaces PDFBox's "incorrect password" wording, baffling when the caller supplied none. */ + private PDDocument load(byte[] bytes) throws IOException { + try { + // The factory spills large documents to a temp-file cache instead of the heap. + return pdfDocumentFactory.load(bytes); + } catch (IOException | RuntimeException e) { + // The factory wraps the parse failure, so check the cause chain rather than the type. + if (mentionsPassword(e)) { + throw new IOException( + "This PDF is encrypted. Remove the password before converting it to" + + " PDF/UA.", + e); + } + throw e; + } + } + + private static boolean mentionsPassword(Throwable error) { + for (Throwable cause = error; cause != null; cause = cause.getCause()) { + if (cause instanceof InvalidPasswordException) { + return true; + } + String message = cause.getMessage(); + if (message != null) { + String lower = message.toLowerCase(Locale.ROOT); + if (lower.contains("password") || lower.contains("decrypt")) { + return true; + } + } + } + return false; + } + + /** XFA is forbidden by PDF/UA-1 clause 7.15; encrypted or huge files cannot be restructured. */ + /** + * Under KEEP nothing rebuilds a tree, so if the embedder deleted one we would hand back an + * untagged document. Fonts are not worth the whole structure; give the tags back instead. + */ + private byte[] keepTagsOverFonts( + byte[] input, + byte[] embedded, + SourceFacts facts, + TaggingOptions options, + List warnings) + throws IOException { + if (options.getExistingTags() != TaggingOptions.ExistingTags.KEEP + || !facts.hasUsableTree() + || embedded == input) { + return embedded; + } + boolean survived; + try (PDDocument rewritten = load(embedded)) { + survived = PdfUaTagger.hasUsableStructureTree(rewritten); + } + if (survived) { + return embedded; + } + warnings.add( + "Embedding the missing fonts would have deleted the document's existing tags, so" + + " the tags were kept and the fonts left unembedded. Turn off font" + + " embedding to silence this, or rebuild the tags to embed them."); + return input; + } + + /** + * Checks that must see the document as the author wrote it. Font embedding strips XFA, so + * running this afterwards would let a dynamic form through unnoticed, and it would push a + * document we are about to reject through the whole embedder first. + */ + private static void rejectUnsupportedSource(PDDocument document) throws IOException { + if (document.getNumberOfPages() > MAX_PAGES) { + throw new IOException( + "This PDF has " + + document.getNumberOfPages() + + " pages. PDF/UA conversion is limited to " + + MAX_PAGES + + " pages; split it first."); + } + PDAcroForm form = document.getDocumentCatalog().getAcroForm(); + if (form != null && form.xfaIsDynamic()) { + throw new IOException( + "Dynamic XFA forms are not permitted by PDF/UA. Flatten the form first."); + } + } + + /** + * Deliberately checked on the working document rather than the source. Permissions-only + * encryption with an empty user password is common in published documents, the embedder + * resolves it, and those files convert usefully; rejecting them up front would fail a document + * for a password its author never set. + */ + private static void rejectEncrypted(PDDocument document) throws IOException { + if (document.isEncrypted()) { + throw new IOException( + "Encrypted PDFs cannot be converted to PDF/UA. Remove the password first."); + } + } + + private static byte[] save(PDDocument document) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaValidationService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaValidationService.java new file mode 100644 index 0000000000..18d9fcaee7 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfUaValidationService.java @@ -0,0 +1,245 @@ +package stirling.software.proprietary.service.ua; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider; +import org.verapdf.pdfa.Foundries; +import org.verapdf.pdfa.PDFAParser; +import org.verapdf.pdfa.PDFAValidator; +import org.verapdf.pdfa.flavours.PDFAFlavour; +import org.verapdf.pdfa.results.TestAssertion; +import org.verapdf.pdfa.results.ValidationResult; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.api.ua.AccessibilityIssue; +import stirling.software.proprietary.model.api.ua.UaValidationResult; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; + +/** + * Validates a document against a PDF/UA profile using veraPDF, the oracle a conversion is declared + * against. It checks only the machine-verifiable subset, so a clean result is not "accessible". + */ +@Service +@Slf4j +public class PdfUaValidationService { + + /** Plain-English text and remediability for the clauses users actually hit. */ + private static final Map CLAUSES = buildClauseTable(); + + record ClauseInfo(String message, boolean autoFixable) {} + + @PostConstruct + public void initialise() { + try { + VeraGreenfieldFoundryProvider.initialise(); + } catch (Exception e) { + log.error("Failed to initialise veraPDF for PDF/UA validation", e); + } + } + + public UaValidationResult validate(byte[] pdfBytes, PdfUaProfile profile) { + PDFAFlavour flavour = flavourFor(profile); + try (PDFAParser parser = + Foundries.defaultInstance() + .createParser(new ByteArrayInputStream(pdfBytes), flavour)) { + + PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false); + ValidationResult result = validator.validate(parser); + return toResult(profile, result); + + } catch (Exception e) { + log.warn("PDF/UA validation failed for {}: {}", profile.displayName(), e.getMessage()); + AccessibilityIssue issue = new AccessibilityIssue(); + issue.setMessage("Validation could not run: " + e.getMessage()); + issue.setSeverity("error"); + issue.setClause("n/a"); + return new UaValidationResult(profile.displayName(), false, List.of(issue), 0); + } + } + + public static PDFAFlavour flavourFor(PdfUaProfile profile) { + return profile == PdfUaProfile.UA2 ? PDFAFlavour.PDFUA_2 : PDFAFlavour.PDFUA_1; + } + + /** + * Whether the bytes really validate as PDF/A level A for the given part. Tagging is necessary + * for level A but not sufficient, so the claim is only written once veraPDF agrees. + */ + public boolean validatesAsPdfaLevelA(byte[] pdfBytes, int part) { + PDFAFlavour flavour = + switch (part) { + case 1 -> PDFAFlavour.PDFA_1_A; + case 2 -> PDFAFlavour.PDFA_2_A; + case 3 -> PDFAFlavour.PDFA_3_A; + default -> null; + }; + if (flavour == null) { + log.warn("No PDF/A level A flavour for part {}", part); + return false; + } + try (PDFAParser parser = + Foundries.defaultInstance() + .createParser(new ByteArrayInputStream(pdfBytes), flavour)) { + PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false); + return validator.validate(parser).isCompliant(); + } catch (Exception e) { + log.warn("Level A validation could not run: {}", e.getMessage()); + return false; + } + } + + /** + * Groups repeated failures of the same rule so a report lists issues, not thousands of lines. + */ + private static UaValidationResult toResult(PdfUaProfile profile, ValidationResult result) { + Map grouped = new LinkedHashMap<>(); + int total = 0; + + for (TestAssertion assertion : result.getTestAssertions()) { + if (assertion.getStatus() != TestAssertion.Status.FAILED) { + continue; + } + total++; + String clause = + assertion.getRuleId() != null ? assertion.getRuleId().getClause() : "unknown"; + int test = assertion.getRuleId() != null ? assertion.getRuleId().getTestNumber() : 0; + String key = clause + "-" + test; + + AccessibilityIssue issue = + grouped.computeIfAbsent( + key, + k -> { + AccessibilityIssue created = new AccessibilityIssue(); + created.setClause(clause); + created.setTestNumber(String.valueOf(test)); + created.setSeverity("error"); + ClauseInfo info = lookupClause(clause); + created.setMessage( + info != null ? info.message() : assertion.getMessage()); + created.setTechnicalMessage(assertion.getMessage()); + created.setAutoFixable(info != null && info.autoFixable()); + created.setSpecification(profile.displayName()); + return created; + }); + issue.setOccurrences(issue.getOccurrences() + 1); + if (issue.getLocation() == null && assertion.getLocation() != null) { + issue.setLocation(assertion.getLocation().toString()); + } + } + + List issues = new ArrayList<>(grouped.values()); + return new UaValidationResult( + profile.displayName(), result.isCompliant() && total == 0, issues, total); + } + + /** + * Finds the most specific entry covering a clause by walking up the dotted hierarchy. String + * prefixes would be wrong: {@code 7.1} prefixes {@code 7.18.1} without being its ancestor. + */ + static ClauseInfo lookupClause(String clause) { + if (clause == null) { + return null; + } + String current = clause; + while (!current.isEmpty()) { + ClauseInfo info = CLAUSES.get(current); + if (info != null) { + return info; + } + int dot = current.lastIndexOf('.'); + if (dot < 0) { + return null; + } + current = current.substring(0, dot); + } + return null; + } + + private static Map buildClauseTable() { + Map table = new LinkedHashMap<>(); + table.put( + "7.1", + new ClauseInfo( + "Document is not tagged, or some content is neither tagged nor marked as an artifact.", + true)); + table.put( + "7.2", + new ClauseInfo( + "Text cannot be mapped to Unicode, or the document language is not declared.", + true)); + table.put( + "7.3", + new ClauseInfo("An image or graphic has no alternative description.", false)); + table.put( + "7.4", + new ClauseInfo( + "Heading levels skip a level, or headings are nested incorrectly.", true)); + table.put( + "7.5", + new ClauseInfo("A table is missing header cells or header associations.", false)); + table.put( + "7.6", new ClauseInfo("A list is not structured as list items with bodies.", true)); + table.put( + "7.7", + new ClauseInfo("A mathematical expression has no alternative description.", false)); + table.put( + "7.8", + new ClauseInfo("Running heads or page numbers are not marked as artifacts.", true)); + table.put("7.9", new ClauseInfo("A note is missing a unique identifier.", true)); + // Tagging does not touch optional content groups, so this needs the authoring tool. + table.put("7.10", new ClauseInfo("An optional content group has no name.", false)); + // The attachment's own /AFRelationship and /Desc are not something tagging can supply. + table.put( + "7.11", + new ClauseInfo( + "An embedded file is missing its relationship or description.", false)); + table.put( + "7.15", + new ClauseInfo( + "The document uses a dynamic XFA form, which PDF/UA does not allow.", + false)); + table.put( + "7.16", + new ClauseInfo( + "Security settings prevent assistive technology from reading the content.", + true)); + table.put("7.17", new ClauseInfo("Navigation aids such as page labels are missing.", true)); + table.put( + "7.18", + new ClauseInfo( + "An annotation is missing a description, tab order, or structure entry.", + true)); + table.put( + "7.20", + new ClauseInfo( + "A form or group XObject is not marked as content or as an artifact.", + false)); + // Most font defects (CIDFont, CMap, metrics, encoding) need the font itself repaired. + table.put( + "7.21", + new ClauseInfo("A font in the document does not meet PDF/UA rules.", false)); + // The one font defect embedding does fix. + table.put("7.21.4.1", new ClauseInfo("A font used in the document is not embedded.", true)); + // ToUnicode gaps need the font itself repaired, which embedding does not do. + table.put( + "7.21.7", + new ClauseInfo( + "A font does not map every character it uses to Unicode, so extracted text" + + " may be wrong.", + false)); + table.put( + "5", + new ClauseInfo( + "The document does not declare PDF/UA conformance in its XMP metadata.", + true)); + return table; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfaAccessibilityService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfaAccessibilityService.java new file mode 100644 index 0000000000..5a13b4ab65 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ua/PdfaAccessibilityService.java @@ -0,0 +1,297 @@ +package stirling.software.proprietary.service.ua; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdfwriter.compress.CompressParameters; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.PDFAExtensionSchema; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; +import org.apache.xmpbox.type.AbstractStructuredType; +import org.apache.xmpbox.type.ArrayProperty; +import org.apache.xmpbox.type.Cardinality; +import org.apache.xmpbox.type.PDFAPropertyType; +import org.apache.xmpbox.type.PDFASchemaType; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpSerializer; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.PdfaLevelAServiceInterface; +import stirling.software.proprietary.pdf.ua.PdfUaIdentificationSchema; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.PdfUaTagger; +import stirling.software.proprietary.pdf.ua.TaggingOptions; +import stirling.software.proprietary.pdf.ua.TaggingResult; + +/** + * Raises a PDF/A file from conformance level B to level A, which adds the tagging the PDF/UA tagger + * already does. Must run after Ghostscript, which discards any structure tree it is given. + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class PdfaAccessibilityService implements PdfaLevelAServiceInterface { + + /** + * Matches the PDF/UA converter's own cap; beyond this the structure model exhausts the heap. + */ + private static final int MAX_TAGGABLE_PAGES = 2000; + + private final PdfUaValidationService validationService; + + /** + * Tags a converted PDF/A and marks it conformance A, or returns it unchanged rather than + * claiming level A over untagged content. part is 1 to 3; part 1 keeps its PDF 1.4 version. + */ + public Result upgradeToLevelA(byte[] pdfBytes, int part, String language, String title) { + return upgradeToLevelA(pdfBytes, part, language, title, false); + } + + /** + * @param alsoDeclareUa additionally claim PDF/UA, but only if it validates + */ + @Override + public Result upgradeToLevelA( + byte[] pdfBytes, int part, String language, String title, boolean alsoDeclareUa) { + List warnings = new ArrayList<>(); + try { + byte[] tagged; + TaggingResult taggingResult; + + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + // Tagging holds a model of the whole document; without a cap a large file exhausts + // the heap, and OutOfMemoryError is an Error, so the catch below never sees it. + if (document.getNumberOfPages() > MAX_TAGGABLE_PAGES) { + warnings.add( + "This document has " + + document.getNumberOfPages() + + " pages, more than the " + + MAX_TAGGABLE_PAGES + + " that can be tagged, so it was left at conformance level B."); + return new Result(pdfBytes, false, warnings); + } + TaggingOptions options = + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language(language) + .title(title) + .fallbackTitle(title) + // Fonts were embedded on the PDF/A pass; a rewrite would undo it. + .embedFonts(false) + // PDF/A-1 is defined on PDF 1.4; raising it breaks conformance. + .preservePdfVersion(part == 1) + .existingTags(TaggingOptions.ExistingTags.AUTO) + .build(); + + taggingResult = new PdfUaTagger().tag(document, options); + warnings.addAll(taggingResult.getWarnings()); + tagged = save(document, part); + } + + if (taggingResult.getTaggedElements() == 0 && taggingResult.isRebuilt()) { + warnings.add( + "No taggable content was found, so the file cannot claim PDF/A level A." + + " It remains valid at level B."); + return new Result(pdfBytes, false, warnings); + } + if (taggingResult.isContentSuppressed()) { + warnings.add( + "Some text could not be tagged reliably and was marked as an artifact, so" + + " no level A claim was written. The file remains valid at level B."); + return new Result(tagged, false, warnings); + } + + byte[] declared = setConformance(tagged, part, "A"); + + // Tagging is necessary for level A but not sufficient: Unicode mappings are too. + if (!validationService.validatesAsPdfaLevelA(declared, part)) { + warnings.add( + "The document was tagged but does not validate as PDF/A-" + + part + + "a, so it was left at conformance level B."); + return new Result(setConformance(tagged, part, "B"), false, warnings); + } + + if (alsoDeclareUa) { + byte[] withUa = declarePdfUaAlongsidePdfa(declared, part); + var uaResult = validationService.validate(withUa, PdfUaProfile.UA1); + if (uaResult.compliant()) { + log.info("Upgraded PDF/A-{} to level A and declared PDF/UA", part); + return new Result(withUa, true, warnings); + } + // The archival upgrade stands on its own; only the accessibility claim is dropped. + warnings.add( + "PDF/UA was requested alongside PDF/A but " + + uaResult.totalFailures() + + " accessibility check(s) still fail, so no PDF/UA claim was" + + " written. The file is valid PDF/A-" + + part + + "a."); + } + + log.info("Upgraded PDF/A-{} to conformance level A", part); + return new Result(declared, true, warnings); + + } catch (Exception e) { + log.warn("Could not upgrade to PDF/A level A: {}", e.getMessage()); + warnings.add( + "Level A upgrade failed (" + + e.getMessage() + + "), so the file was left at conformance level B."); + return new Result(pdfBytes, false, warnings); + } + } + + /** + * Declares PDF/UA alongside PDF/A in one file. The extension schema is required: PDF/A forbids + * XMP properties no schema describes, and XMPBox has none for {@code pdfuaid}. + */ + static byte[] declarePdfUaAlongsidePdfa(byte[] pdfBytes, int part) throws Exception { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + XMPMetadata xmp = parseOrCreate(document); + + PdfUaIdentificationSchema identification = new PdfUaIdentificationSchema(xmp); + identification.setPart(1); + xmp.addSchema(identification); + + addPdfUaExtensionSchema(xmp); + writeMetadata(document, xmp); + return save(document, part); + } + } + + /** + * Describes the pdfuaid namespace so a PDF/A validator accepts it. Fields are set individually, + * not by subclassing: XMPBox reads the namespace from an annotation, which is not inherited. + */ + private static void addPdfUaExtensionSchema(XMPMetadata xmp) { + PDFAExtensionSchema extension = + (PDFAExtensionSchema) xmp.getSchema(PDFAExtensionSchema.class); + if (extension == null) { + extension = xmp.createAndAddPDFAExtensionSchemaWithDefaultNS(); + } + + PDFAPropertyType partProperty = new PDFAPropertyType(xmp); + addField(xmp, partProperty, PDFAPropertyType.NAME, "part"); + addField(xmp, partProperty, PDFAPropertyType.VALUETYPE, "Integer"); + addField(xmp, partProperty, PDFAPropertyType.CATEGORY, "internal"); + addField( + xmp, + partProperty, + PDFAPropertyType.DESCRIPTION, + "Indicates which part of ISO 14289 the document conforms to"); + + PDFASchemaType schema = new PDFASchemaType(xmp); + addField(xmp, schema, PDFASchemaType.SCHEMA, "PDF/UA Universal Accessibility Schema"); + addField(xmp, schema, PDFASchemaType.NAMESPACE_URI, PdfUaIdentificationSchema.NAMESPACE); + addField(xmp, schema, PDFASchemaType.PREFIX, PdfUaIdentificationSchema.PREFERRED_PREFIX); + + ArrayProperty properties = + xmp.getTypeMapping() + .createArrayProperty( + schema.getNamespace(), + schema.getPrefix(), + PDFASchemaType.PROPERTY, + Cardinality.Seq); + properties.getContainer().addProperty(partProperty); + schema.getContainer().addProperty(properties); + + // A freshly created extension schema has no schemas bag yet, so make one. + ArrayProperty schemas = extension.getSchemasProperty(); + if (schemas == null) { + schemas = + xmp.getTypeMapping() + .createArrayProperty( + extension.getNamespace(), + extension.getPrefix(), + PDFAExtensionSchema.SCHEMAS, + Cardinality.Bag); + extension.addProperty(schemas); + } + schemas.getContainer().addProperty(schema); + } + + /** Adds one text field to a structured type, in that type's own namespace. */ + private static void addField( + XMPMetadata xmp, AbstractStructuredType target, String name, String value) { + target.getContainer() + .addProperty( + xmp.getTypeMapping() + .createText( + target.getNamespace(), target.getPrefix(), name, value)); + } + + private static XMPMetadata parseOrCreate(PDDocument document) throws Exception { + PDMetadata existing = document.getDocumentCatalog().getMetadata(); + if (existing == null) { + return XMPMetadata.createXMPMetadata(); + } + try (InputStream in = new ByteArrayInputStream(existing.toByteArray())) { + DomXmpParser parser = new DomXmpParser(); + parser.setStrictParsing(false); + return parser.parse(in); + } + } + + private static void writeMetadata(PDDocument document, XMPMetadata xmp) throws Exception { + ByteArrayOutputStream serialised = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmp, serialised, true); + PDMetadata metadata = new PDMetadata(document); + metadata.importXMPMetadata(serialised.toByteArray()); + document.getDocumentCatalog().setMetadata(metadata); + } + + /** Rewrites {@code pdfaid:conformance} without disturbing the rest of the packet. */ + static byte[] setConformance(byte[] pdfBytes, int part, String conformance) throws Exception { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + PDMetadata existing = document.getDocumentCatalog().getMetadata(); + XMPMetadata xmp; + if (existing != null) { + try (InputStream in = new ByteArrayInputStream(existing.toByteArray())) { + DomXmpParser parser = new DomXmpParser(); + parser.setStrictParsing(false); + xmp = parser.parse(in); + } + } else { + xmp = XMPMetadata.createXMPMetadata(); + } + + PDFAIdentificationSchema identification = + (PDFAIdentificationSchema) xmp.getSchema(PDFAIdentificationSchema.class); + if (identification == null) { + identification = xmp.createAndAddPDFAIdentificationSchema(); + } + identification.setPart(part); + identification.setConformance(conformance); + + ByteArrayOutputStream serialised = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmp, serialised, true); + PDMetadata metadata = new PDMetadata(document); + metadata.importXMPMetadata(serialised.toByteArray()); + document.getDocumentCatalog().setMetadata(metadata); + + return save(document, part); + } + } + + /** + * Part 1 is saved uncompressed: PDFBox's default object streams need PDF 1.5, which would push + * a PDF/A-1 file off its required 1.4 version. + */ + private static byte[] save(PDDocument document, int part) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save( + out, part == 1 ? CompressParameters.NO_COMPRESSION : new CompressParameters()); + return out.toByteArray(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java index db063cda80..1217d81cc8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java @@ -6,20 +6,35 @@ import java.nio.file.Path; import java.util.Locale; import java.util.Optional; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionOperations; +import org.springframework.transaction.support.TransactionTemplate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.util.TempFileManager; import stirling.software.proprietary.cluster.s3.S3Clients; +import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.AuditingStorageEncryptionListener; +import stirling.software.proprietary.storage.crypto.EncryptingStorageProvider; +import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService; +import stirling.software.proprietary.storage.crypto.FileEncryptionMasterKey; +import stirling.software.proprietary.storage.crypto.StorageEncryptionAuditListener; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; import stirling.software.proprietary.storage.provider.DatabaseStorageProvider; import stirling.software.proprietary.storage.provider.LocalStorageProvider; import stirling.software.proprietary.storage.provider.S3StorageProvider; import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; import stirling.software.proprietary.storage.repository.StoredFileBlobRepository; @Configuration @@ -29,10 +44,104 @@ public class StorageProviderConfig { private final ApplicationProperties applicationProperties; private final StoredFileBlobRepository storedFileBlobRepository; + private final FileEncryptionKeyRepository fileEncryptionKeyRepository; private final LicenseKeyChecker licenseKeyChecker; + private final AuditService auditService; + + /** + * The encryption state behind the always-installed decorator, shared with the admin API and + * migration job. Key machinery is created eagerly when the write flag is on (licence-gated) or + * key rows already exist — so a wrong master key fails startup, not the first download — and + * lazily if encrypted content shows up later (config drift on one cluster node must fail + * loudly, never stream ciphertext). Turning the flag off or losing the licence only stops + * encrypting new writes; decryption stays available. + */ + @Bean + public StorageEncryptionState storageEncryptionState( + @Value("${stirling.security.fileEncryptionKey:}") String configuredFileEncryptionKey, + @Value("${stirling.security.fileEncryptionKeyPrevious:}") + String previousFileEncryptionKey, + @Value("${stirling.security.fileEncryptionKeyVersion:1}") int fileEncryptionKeyVersion, + @Value("${cluster.enabled:false}") boolean clusterEnabled, + PlatformTransactionManager transactionManager) { + boolean writeEnabled = applicationProperties.getStorage().getEncryption().isEnabled(); + if (writeEnabled) { + licenseKeyChecker.requireProOrEnterprise("storage.encryption"); + warnIfAuditUnavailable(); + } + StorageEncryptionAuditListener listener = + new AuditingStorageEncryptionListener( + auditService, + applicationProperties.getStorage().getEncryption().isAuditReads()); + // Key creation must commit independently of any caller transaction (see + // FileEncryptionKeyService#createActive). + TransactionTemplate requiresNew = new TransactionTemplate(transactionManager); + requiresNew.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + StorageEncryptionState state = + new StorageEncryptionState( + writeEnabled, + () -> + createKeyService( + configuredFileEncryptionKey, + previousFileEncryptionKey, + fileEncryptionKeyVersion, + clusterEnabled, + listener, + requiresNew), + fileEncryptionKeyRepository, + listener); + // The registry table may not exist when storage is unused, so only probe if it is on. + boolean probeForExistingKeys = + !writeEnabled && applicationProperties.getStorage().isEnabled(); + if (writeEnabled || (probeForExistingKeys && state.encryptedContentMayExist())) { + state.initialiseEagerly(); + log.info( + "Storage encryption at rest active (writes {})", + writeEnabled ? "encrypted" : "plaintext; decrypt-only mode"); + } + return state; + } + + /** + * Encryption at rest is available on Pro, but {@code AuditService} only records events on an + * Enterprise licence. Without this warning a Pro operator would enable encryption, be told it + * is audited, and silently get no encrypt/decrypt/revocation trail at all. + */ + private void warnIfAuditUnavailable() { + if (licenseKeyChecker.getPremiumLicenseEnabledResult() != License.ENTERPRISE) { + log.warn( + "Storage encryption at rest is enabled, but audit events require an Enterprise" + + " licence: encrypt/decrypt, revocation and plaintext-export events" + + " will NOT be recorded on this licence tier. Encryption itself is" + + " unaffected. See devGuide/STORAGE_ENCRYPTION_AT_REST.md"); + } + } + + private FileEncryptionKeyService createKeyService( + String configuredKey, + String previousKey, + int keyVersion, + boolean clusterEnabled, + StorageEncryptionAuditListener listener, + TransactionOperations keyCreationTx) { + FileEncryptionMasterKey masterKey = + new FileEncryptionMasterKey(configuredKey, previousKey, keyVersion, clusterEnabled); + FileEncryptionKeyService keyService = + new FileEncryptionKeyService( + fileEncryptionKeyRepository, masterKey, listener, keyCreationTx); + // Wrong key must fail fast, not silently start a second key hierarchy. + keyService.verifyMasterKey(); + return keyService; + } @Bean(destroyMethod = "close") - public StorageProvider storageProvider() { + public StorageProvider storageProvider( + StorageEncryptionState encryptionState, Optional tempFileManager) { + return new EncryptingStorageProvider( + innerStorageProvider(), encryptionState, tempFileManager.orElse(null)); + } + + private StorageProvider innerStorageProvider() { boolean storageEnabled = applicationProperties.getStorage().isEnabled(); String providerName = Optional.ofNullable(applicationProperties.getStorage().getProvider()) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java index 4eb299cd24..3e5589b31d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java @@ -5,6 +5,7 @@ import java.net.URI; import java.time.Duration; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import org.springframework.http.ContentDisposition; @@ -31,7 +32,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.storage.model.FileShare; import stirling.software.proprietary.storage.model.StoredFile; import stirling.software.proprietary.storage.model.api.CreateShareLinkRequest; @@ -56,6 +59,7 @@ public class FileStorageController { private final FileStorageService fileStorageService; private final StorageProvider storageProvider; + private final AuditService auditService; @PostMapping( value = "/files", @@ -262,6 +266,21 @@ public class FileStorageController { private ResponseEntity buildFileResponse( StoredFile file, boolean inline) { org.springframework.core.io.Resource resource = fileStorageService.loadFile(file); + if (file.getEncryptionKeyId() != null) { + // Compliance marker: a plaintext copy of encrypted-at-rest content left the platform + // (inline=true is an in-app view; false is a saved download). + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of( + "action", + "plaintextExport", + "fileId", + file.getId(), + "inline", + inline, + "keyId", + file.getEncryptionKeyId())); + } String contentType = file.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminController.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminController.java new file mode 100644 index 0000000000..d0845678ef --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminController.java @@ -0,0 +1,275 @@ +package stirling.software.proprietary.storage.controller; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.audit.AuditEventType; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService; +import stirling.software.proprietary.storage.crypto.StorageEncryptionException; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; +import stirling.software.proprietary.storage.model.FileEncryptionKey; +import stirling.software.proprietary.storage.model.api.StorageEncryptionStatusResponse; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; +import stirling.software.proprietary.storage.repository.StoredFileRepository; +import stirling.software.proprietary.storage.service.StorageEncryptionMigrationService; + +/** + * Admin surface for storage encryption at rest: status/backup verification, the per-scope kill + * switch, the encrypt-existing migration, and master-key rotation. Deliberately no delete endpoint + * — key material can be disabled but never destroyed through the API. + */ +@RestController +@RequestMapping("/api/v1/admin/storage-encryption") +@PreAuthorize("hasRole('ADMIN')") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Admin: Storage Encryption", description = "Encryption-at-rest administration") +public class StorageEncryptionAdminController { + + private final ApplicationProperties applicationProperties; + private final StorageEncryptionState encryptionState; + private final FileEncryptionKeyRepository keyRepository; + private final StoredFileRepository storedFileRepository; + private final StorageEncryptionMigrationService migrationService; + private final AuditService auditService; + + /** + * Reports write state, master-key fingerprint and encrypted/plaintext counts. Refuses rather + * than reading the registry when storage is off, because a deployment that never stores files + * may not have the table at all — the same reason the decorator's boot probe is gated. + */ + @GetMapping(value = "/status", produces = MediaType.APPLICATION_JSON_VALUE) + public StorageEncryptionStatusResponse status() { + if (!applicationProperties.getStorage().isEnabled()) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Storage is disabled"); + } + try { + return buildStatus(); + } catch (DataAccessException e) { + log.warn("Could not read storage encryption status: {}", e.getMessage()); + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, + "The storage encryption key registry could not be read", + e); + } + } + + private StorageEncryptionStatusResponse buildStatus() { + List keys = + keyRepository.findAll(Sort.by("createdAt")).stream() + .map( + k -> + new StorageEncryptionStatusResponse.KeyInfo( + k.getKeyId(), + k.getScopeType().name(), + k.getScopeId(), + k.getKeyVersion(), + k.getMasterKeyVersion(), + k.getStatus().name(), + k.getCreatedAt(), + k.getStatusChangedAt(), + k.getStatusChangedBy())) + .toList(); + String fingerprint = null; + Integer masterKeyVersion = null; + if (encryptionState.isMaterialised()) { + try { + FileEncryptionKeyService keyService = encryptionState.keyService(); + fingerprint = keyService.masterKey().fingerprint(); + masterKeyVersion = keyService.masterKey().currentVersion(); + } catch (StorageEncryptionException ignored) { + // Materialisation failed; status still reports counts and key rows. + } + } + return new StorageEncryptionStatusResponse( + encryptionState.isWriteEnabled(), + encryptionState.isMaterialised(), + fingerprint, + masterKeyVersion, + storedFileRepository.countByEncryptionKeyIdIsNotNull(), + storedFileRepository.countByEncryptionKeyIdIsNull(), + keys); + } + + /** Kill switch: content under this key fails closed (403) until re-enabled. Reversible. */ + @PostMapping("/keys/{keyId}/disable") + public StorageEncryptionStatusResponse.KeyInfo disableKey(@PathVariable UUID keyId) { + FileEncryptionKey row = setStatus(keyId, FileEncryptionKey.Status.DISABLED); + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of("action", "key.disabled", "keyId", keyId.toString())); + return toKeyInfo(row); + } + + /** + * Reverses the kill switch: only DISABLED keys can be enabled, and they come back ACTIVE unless + * the scope acquired another active key while revoked, in which case they come back RETIRED — + * readable, but not a second key wrapping new writes. The response carries the resulting + * status. + */ + @PostMapping("/keys/{keyId}/enable") + public StorageEncryptionStatusResponse.KeyInfo enableKey(@PathVariable UUID keyId) { + FileEncryptionKey existing = + keyRepository + .findById(keyId) + .orElseThrow( + () -> + new ResponseStatusException( + HttpStatus.NOT_FOUND, "No such encryption key")); + if (existing.getStatus() != FileEncryptionKey.Status.DISABLED) { + throw new ResponseStatusException( + HttpStatus.CONFLICT, + "Key is not disabled (status: " + existing.getStatus() + ")"); + } + FileEncryptionKeyService keyService = requireKeyService(); + FileEncryptionKey row; + try { + row = keyService.enable(keyId, currentUsername()); + } catch (StorageEncryptionException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of( + "action", + "key.enabled", + "keyId", + keyId.toString(), + "status", + row.getStatus().name())); + return toKeyInfo(row); + } + + @PostMapping("/migrate") + public MigrationStatusResponse startMigration() { + try { + return MigrationStatusResponse.from(migrationService.start()); + } catch (IllegalStateException e) { + throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage()); + } + } + + @GetMapping(value = "/migrate/status", produces = MediaType.APPLICATION_JSON_VALUE) + public MigrationStatusResponse migrationStatus() { + return migrationService + .status() + .map(MigrationStatusResponse::from) + .orElse(MigrationStatusResponse.IDLE); + } + + /** + * Re-wraps KEK rows below the configured master-key version under the primary master key. Key + * material is never accepted over HTTP — new keys arrive via config/env and a restart; this + * endpoint only performs the re-wrap step of the rotation runbook. + */ + @PostMapping("/master/rotate") + public Map rotateMasterKey() { + FileEncryptionKeyService keyService = requireKeyService(); + int rewrapped; + try { + rewrapped = keyService.rotateMasterKey(); + } catch (StorageEncryptionException e) { + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, "Rotation failed: " + e.getMessage(), e); + } + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of( + "action", + "master.rotated", + "rowsRewrapped", + rewrapped, + "masterKeyVersion", + keyService.masterKey().currentVersion())); + return Map.of( + "rewrapped", + rewrapped, + "masterKeyVersion", + keyService.masterKey().currentVersion()); + } + + private FileEncryptionKey setStatus(UUID keyId, FileEncryptionKey.Status status) { + FileEncryptionKeyService keyService = requireKeyService(); + try { + return keyService.setKeyStatus(keyId, status, currentUsername()); + } catch (StorageEncryptionException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } + } + + private FileEncryptionKeyService requireKeyService() { + try { + return encryptionState.keyService(); + } catch (StorageEncryptionException e) { + throw new ResponseStatusException( + HttpStatus.CONFLICT, + "Storage encryption is not configured: " + e.getMessage(), + e); + } + } + + private static String currentUsername() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth != null ? auth.getName() : "system"; + } + + private static StorageEncryptionStatusResponse.KeyInfo toKeyInfo(FileEncryptionKey k) { + return new StorageEncryptionStatusResponse.KeyInfo( + k.getKeyId(), + k.getScopeType().name(), + k.getScopeId(), + k.getKeyVersion(), + k.getMasterKeyVersion(), + k.getStatus().name(), + k.getCreatedAt(), + k.getStatusChangedAt(), + k.getStatusChangedBy()); + } + + public record MigrationStatusResponse( + String state, + Long total, + Long processed, + Long skipped, + Long failed, + Instant startedAt, + Instant finishedAt) { + + static final MigrationStatusResponse IDLE = + new MigrationStatusResponse("IDLE", null, null, null, null, null, null); + + static MigrationStatusResponse from(StorageEncryptionMigrationService.MigrationStatus s) { + return new MigrationStatusResponse( + s.state().name(), + s.total(), + s.processed(), + s.skipped(), + s.failed(), + s.startedAt(), + s.finishedAt()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListener.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListener.java new file mode 100644 index 0000000000..2b46c9dc62 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListener.java @@ -0,0 +1,62 @@ +package stirling.software.proprietary.storage.crypto; + +import java.util.Map; +import java.util.UUID; + +import stirling.software.proprietary.audit.AuditEventType; +import stirling.software.proprietary.service.AuditService; + +/** + * Bridges storage-encryption events into the audit trail. Per-read decrypt events can be noisy on + * busy installs, so they honour {@code storage.encryption.auditReads}; denials and key lifecycle + * events are always recorded. + */ +public class AuditingStorageEncryptionListener implements StorageEncryptionAuditListener { + + private final AuditService auditService; + private final boolean auditReads; + + public AuditingStorageEncryptionListener(AuditService auditService, boolean auditReads) { + this.auditService = auditService; + this.auditReads = auditReads; + } + + @Override + public void encrypted(String storageKey, UUID keyId) { + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of("action", "encrypt", "storageKey", storageKey, "keyId", keyId.toString())); + } + + @Override + public void decrypted(String storageKey, UUID keyId) { + if (!auditReads) { + return; + } + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of("action", "decrypt", "storageKey", storageKey, "keyId", keyId.toString())); + } + + @Override + public void decryptDenied(UUID keyId, String reason) { + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of("action", "decrypt.denied", "keyId", keyId.toString(), "reason", reason)); + } + + @Override + public void keyCreated(UUID keyId, String scope, int version) { + auditService.audit( + AuditEventType.STORAGE_ENCRYPTION, + Map.of( + "action", + "key.created", + "keyId", + keyId.toString(), + "scope", + scope, + "keyVersion", + version)); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormat.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormat.java new file mode 100644 index 0000000000..389caa1e3c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormat.java @@ -0,0 +1,105 @@ +package stirling.software.proprietary.storage.crypto; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.UUID; + +/** + * On-disk format for storage-encrypted blobs. The object is self-describing so every {@code + * StorageProvider} backend stores identical bytes and legacy plaintext blobs are recognised by the + * absence of the magic: + * + *
+ * offset size field
+ * 0      8    magic "SPDFEAR1"
+ * 8      1    format version (1)
+ * 9      1    cipher suite (1 = AES-256-GCM-HKDF streaming, 1 MiB segments)
+ * 10     16   key id (file_encryption_keys row that wraps the DEK)
+ * 26     8    plaintext length
+ * 34     2    wrapped-DEK length (60 for suite 1)
+ * 36     60   wrapped DEK = IV(12) || AES-256-GCM(scope KEK, DEK) || tag(16)
+ * 96     ..   payload: streaming-AEAD ciphertext
+ * 
+ * + *

Bytes 0–33 are the associated data for both the DEK wrap and the payload AEAD, so neither a + * header transplanted onto another payload nor an altered key id / plaintext length authenticates. + */ +public final class EncryptedFileFormat { + + public static final byte[] MAGIC = {'S', 'P', 'D', 'F', 'E', 'A', 'R', '1'}; + public static final byte FORMAT_VERSION = 1; + public static final byte SUITE_AES_GCM_HKDF_1MIB = 1; + public static final int SEGMENT_SIZE_BYTES = 1 << 20; + public static final int DEK_LENGTH_BYTES = 32; + public static final int WRAPPED_DEK_LENGTH = 12 + DEK_LENGTH_BYTES + 16; + public static final int HEADER_LENGTH = 8 + 1 + 1 + 16 + 8 + 2 + WRAPPED_DEK_LENGTH; + + private static final int AAD_LENGTH = 34; + + private EncryptedFileFormat() {} + + public record Header( + byte formatVersion, + byte cipherSuite, + UUID keyId, + long plaintextLength, + byte[] wrappedDek) { + + public byte[] serialize() { + ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH); + buffer.put(MAGIC); + buffer.put(formatVersion); + buffer.put(cipherSuite); + buffer.putLong(keyId.getMostSignificantBits()); + buffer.putLong(keyId.getLeastSignificantBits()); + buffer.putLong(plaintextLength); + buffer.putShort((short) wrappedDek.length); + buffer.put(wrappedDek); + return buffer.array(); + } + + /** The header prefix (everything before the wrapped DEK) used as AEAD associated data. */ + public byte[] associatedData() { + return Arrays.copyOfRange(serialize(), 0, AAD_LENGTH); + } + } + + /** + * Parses a header from the first {@link #HEADER_LENGTH} bytes of a blob. Returns {@code null} + * when the bytes are not a storage-encrypted object (legacy plaintext passthrough). + * + * @throws StorageEncryptionException when the magic matches but the version/suite is unknown — + * checked so it flows through the callers' IOException error mapping instead of surfacing + * as a bare 500. + */ + public static Header parse(byte[] prefix) throws StorageEncryptionException { + if (prefix == null || prefix.length < HEADER_LENGTH) { + return null; + } + for (int i = 0; i < MAGIC.length; i++) { + if (prefix[i] != MAGIC[i]) { + return null; + } + } + ByteBuffer buffer = ByteBuffer.wrap(prefix, MAGIC.length, HEADER_LENGTH - MAGIC.length); + byte version = buffer.get(); + byte suite = buffer.get(); + UUID keyId = new UUID(buffer.getLong(), buffer.getLong()); + long plaintextLength = buffer.getLong(); + int wrappedDekLength = Short.toUnsignedInt(buffer.getShort()); + if (version != FORMAT_VERSION + || suite != SUITE_AES_GCM_HKDF_1MIB + || plaintextLength < 0 + || wrappedDekLength != WRAPPED_DEK_LENGTH) { + throw new StorageEncryptionException( + "Unsupported storage-encryption header (version=" + + version + + ", suite=" + + suite + + "). This build cannot read the file."); + } + byte[] wrappedDek = new byte[wrappedDekLength]; + buffer.get(wrappedDek); + return new Header(version, suite, keyId, plaintextLength, wrappedDek); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProvider.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProvider.java new file mode 100644 index 0000000000..9d1def1935 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProvider.java @@ -0,0 +1,482 @@ +package stirling.software.proprietary.storage.crypto; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.SequenceInputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.Arrays; +import java.util.Optional; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import com.google.crypto.tink.subtle.AesGcmHkdfStreaming; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.TempFileManager; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.provider.StoredObject; + +/** + * Envelope-encryption decorator over any {@link StorageProvider}: encrypts on {@code store} (when + * the write flag is on), decrypts on {@code load}, and passes legacy plaintext blobs through + * untouched (detected by the {@link EncryptedFileFormat} magic). Backends store opaque ciphertext + * and need no changes. + * + *

The decorator is installed unconditionally (see {@link StorageEncryptionState}), so a node + * whose config lags the cluster decrypts or fails loudly instead of streaming raw ciphertext. + * Decryption keeps working after the feature is switched off; only new writes revert to plaintext. + * + *

Presigned download URLs delegate to the backend only while no encrypted content can exist; + * otherwise they are suppressed, because an S3 presigned GET would hand ciphertext straight to the + * browser. Callers already fall back to app-streamed {@link #load} when no URL is offered. + */ +@Slf4j +public class EncryptingStorageProvider implements StorageProvider { + + private static final SecureRandom RANDOM = new SecureRandom(); + + private final StorageProvider delegate; + private final StorageEncryptionState state; + private final TempFileManager tempFileManager; + + public EncryptingStorageProvider(StorageProvider delegate, StorageEncryptionState state) { + this(delegate, state, null); + } + + public EncryptingStorageProvider( + StorageProvider delegate, + StorageEncryptionState state, + TempFileManager tempFileManager) { + this.delegate = delegate; + this.state = state; + this.tempFileManager = tempFileManager; + } + + /** Test convenience mirroring the pre-state constructor shape. */ + public EncryptingStorageProvider( + StorageProvider delegate, FileEncryptionKeyService keys, boolean writeEnabled) { + this(delegate, StorageEncryptionState.of(writeEnabled, keys), null); + } + + /** Test convenience with an explicit audit listener. */ + public EncryptingStorageProvider( + StorageProvider delegate, + FileEncryptionKeyService keys, + boolean writeEnabled, + StorageEncryptionAuditListener auditListener) { + this(delegate, StorageEncryptionState.of(writeEnabled, keys, auditListener), null); + } + + @Override + public StoredObject store(User owner, MultipartFile file) throws IOException { + if (!state.isWriteEnabled()) { + return delegate.store(owner, file); + } + FileEncryptionKeyService.ScopeKek kek = state.keyService().activeKekForOwner(owner); + byte[] dek = new byte[EncryptedFileFormat.DEK_LENGTH_BYTES]; + RANDOM.nextBytes(dek); + + // Spool ciphertext to a temp file: DatabaseStorageProvider needs getBytes() and + // S3StorageProvider needs an exact Content-Length, so the ciphertext size must be known + // before the delegate reads the upload. TempFileManager-registered so a crash mid-upload + // doesn't orphan the spool forever. + Path spool = createSpoolFile(); + try { + EncryptedFileFormat.Header header = buildHeader(kek, dek, file.getSize()); + byte[] aad = header.associatedData(); + long plaintextBytes; + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(spool))) { + out.write(header.serialize()); + OutputStream encrypting = streamingAead(dek).newEncryptingStream(out, aad); + try (InputStream in = file.getInputStream()) { + plaintextBytes = in.transferTo(encrypting); + } + encrypting.close(); + } catch (GeneralSecurityException e) { + throw new StorageEncryptionException("Failed to encrypt upload", e); + } + // The header (and AAD) already carry file.getSize(); a MultipartFile that mis-reports + // would otherwise surface as a Content-Length mismatch, i.e. a truncated or hanging + // download instead of an error. + if (plaintextBytes != file.getSize()) { + throw new StorageEncryptionException( + "Upload reported " + + file.getSize() + + " bytes but streamed " + + plaintextBytes); + } + StoredObject stored = delegate.store(owner, new SpooledUpload(file, spool)); + log.debug( + "Encrypted {} under key {} ({} plaintext bytes)", + stored.getStorageKey(), + kek.keyId(), + file.getSize()); + state.auditListener().encrypted(stored.getStorageKey(), kek.keyId()); + return stored.toBuilder() + .sizeBytes(file.getSize()) + .encryptionKeyId(kek.keyId().toString()) + .build(); + } finally { + Arrays.fill(dek, (byte) 0); + Files.deleteIfExists(spool); + } + } + + private Path createSpoolFile() throws IOException { + if (tempFileManager != null) { + return tempFileManager.createTempFile(".enc").toPath(); + } + return Files.createTempFile("stirling-enc-", ".bin"); + } + + @Override + public Resource load(String storageKey) throws IOException { + Resource raw = delegate.load(storageKey); + if (raw.isOpen()) { + return wrapOneShot(storageKey, raw); + } + return wrapReopenable(storageKey, raw); + } + + @Override + public void delete(String storageKey) throws IOException { + delegate.delete(storageKey); + } + + @Override + public void close() { + try { + delegate.close(); + } catch (Exception e) { + log.warn("Error closing delegate storage provider", e); + } + } + + @Override + public Optional signedDownloadUrl(String storageKey, Duration ttl) throws IOException { + if (state.suppressDirectDownloads()) { + return Optional.empty(); + } + return delegate.signedDownloadUrl(storageKey, ttl); + } + + @Override + public Optional signedDownloadUrl( + String storageKey, Duration ttl, boolean inline, String originalFilename) + throws IOException { + if (state.suppressDirectDownloads()) { + return Optional.empty(); + } + return delegate.signedDownloadUrl(storageKey, ttl, inline, originalFilename); + } + + // ---- store helpers ------------------------------------------------------------------- + + private EncryptedFileFormat.Header buildHeader( + FileEncryptionKeyService.ScopeKek kek, byte[] dek, long plaintextLength) + throws StorageEncryptionException { + // AAD covers the header prefix, so build a header with a placeholder wrap first to get + // the prefix bytes, then wrap the DEK bound to that prefix. + EncryptedFileFormat.Header prototype = + new EncryptedFileFormat.Header( + EncryptedFileFormat.FORMAT_VERSION, + EncryptedFileFormat.SUITE_AES_GCM_HKDF_1MIB, + kek.keyId(), + plaintextLength, + new byte[EncryptedFileFormat.WRAPPED_DEK_LENGTH]); + byte[] aad = prototype.associatedData(); + byte[] wrappedDek = wrapDek(dek, kek.key(), aad); + return new EncryptedFileFormat.Header( + EncryptedFileFormat.FORMAT_VERSION, + EncryptedFileFormat.SUITE_AES_GCM_HKDF_1MIB, + kek.keyId(), + plaintextLength, + wrappedDek); + } + + private static byte[] wrapDek(byte[] dek, byte[] kek, byte[] aad) + throws StorageEncryptionException { + try { + byte[] iv = new byte[12]; + RANDOM.nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init( + Cipher.ENCRYPT_MODE, + new SecretKeySpec(kek, "AES"), + new GCMParameterSpec(128, iv)); + cipher.updateAAD(aad); + byte[] ct = cipher.doFinal(dek); + byte[] out = new byte[iv.length + ct.length]; + System.arraycopy(iv, 0, out, 0, iv.length); + System.arraycopy(ct, 0, out, iv.length, ct.length); + return out; + } catch (GeneralSecurityException e) { + throw new StorageEncryptionException("Failed to wrap file key", e); + } + } + + private byte[] unwrapDek(EncryptedFileFormat.Header header) throws IOException { + byte[] kek = state.keyService().kekForDecrypt(header.keyId()); + try { + byte[] wrapped = header.wrappedDek(); + byte[] iv = Arrays.copyOfRange(wrapped, 0, 12); + byte[] ct = Arrays.copyOfRange(wrapped, 12, wrapped.length); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init( + Cipher.DECRYPT_MODE, + new SecretKeySpec(kek, "AES"), + new GCMParameterSpec(128, iv)); + cipher.updateAAD(header.associatedData()); + return cipher.doFinal(ct); + } catch (GeneralSecurityException e) { + throw new StorageEncryptionException( + "Failed to unwrap file key for key " + header.keyId() + " — tampered header?", + e); + } + } + + /** + * Uses Tink's {@code subtle} API directly rather than the keyset/StreamingAead registry: + * keysets would store DEKs in Tink's own serialisation and key-management model, while this + * format keeps the (already-wrapped) DEK in our header. {@code subtle} is outside Tink's + * stability guarantee — the pinned version plus the round-trip tests are what make upgrades + * safe; re-run them on any Tink bump. + */ + private static AesGcmHkdfStreaming streamingAead(byte[] dek) throws GeneralSecurityException { + return new AesGcmHkdfStreaming( + dek, + "HMACSHA256", + EncryptedFileFormat.DEK_LENGTH_BYTES, + EncryptedFileFormat.SEGMENT_SIZE_BYTES, + 0); + } + + // ---- load helpers -------------------------------------------------------------------- + + /** Re-openable delegate (local file, DB byte array): sniff via a throwaway stream. */ + private Resource wrapReopenable(String storageKey, Resource raw) throws IOException { + byte[] prefix; + try (InputStream in = raw.getInputStream()) { + prefix = in.readNBytes(EncryptedFileFormat.HEADER_LENGTH); + } + EncryptedFileFormat.Header header = EncryptedFileFormat.parse(prefix); + if (header == null) { + return raw; + } + byte[] dek = unwrapDek(header); + state.auditListener().decrypted(storageKey, header.keyId()); + return new ReopenableDecryptedResource(raw, header, dek); + } + + /** + * One-shot delegate (S3 stream): the sniffed prefix must be replayed or decrypted inline. The + * stream is a live HTTP connection, so every failure path (unknown header version, revoked key, + * tampered wrap) must close it — leaking here would starve the S3 connection pool precisely + * when the kill switch is being exercised. + */ + private Resource wrapOneShot(String storageKey, Resource raw) throws IOException { + InputStream in = raw.getInputStream(); + try { + byte[] prefix = in.readNBytes(EncryptedFileFormat.HEADER_LENGTH); + EncryptedFileFormat.Header header = EncryptedFileFormat.parse(prefix); + if (header == null) { + long length; + try { + length = raw.contentLength(); + } catch (IOException | RuntimeException e) { + // Stock InputStreamResource refuses contentLength() once the stream is + // partially read; S3's resource reports it from the response header instead. + length = -1; + } + return new OneShotResource( + new SequenceInputStream(new ByteArrayInputStream(prefix), in), + length, + raw.getDescription()); + } + byte[] dek = unwrapDek(header); + InputStream decrypting; + try { + decrypting = streamingAead(dek).newDecryptingStream(in, header.associatedData()); + } catch (GeneralSecurityException e) { + throw new StorageEncryptionException("Failed to open decrypting stream", e); + } + state.auditListener().decrypted(storageKey, header.keyId()); + return new OneShotResource(decrypting, header.plaintextLength(), raw.getDescription()); + } catch (IOException | RuntimeException e) { + try { + in.close(); + } catch (IOException closeFailure) { + e.addSuppressed(closeFailure); + } + throw e; + } + } + + /** + * Fresh decrypting stream per read; supports repeated reads and range-skip consumers. Note a + * range request still decrypts from byte 0 and discards up to the offset — inherent to + * streaming AEAD without a seekable-channel implementation. + */ + private static final class ReopenableDecryptedResource extends AbstractResource { + private final Resource ciphertext; + private final EncryptedFileFormat.Header header; + private final byte[] dek; + + private ReopenableDecryptedResource( + Resource ciphertext, EncryptedFileFormat.Header header, byte[] dek) { + this.ciphertext = ciphertext; + this.header = header; + this.dek = dek; + } + + @Override + public InputStream getInputStream() throws IOException { + InputStream in = ciphertext.getInputStream(); + try { + in.skipNBytes(EncryptedFileFormat.HEADER_LENGTH); + return streamingAead(dek).newDecryptingStream(in, header.associatedData()); + } catch (GeneralSecurityException | IOException e) { + in.close(); + throw e instanceof IOException io + ? io + : new StorageEncryptionException("Failed to open decrypting stream", e); + } + } + + @Override + public long contentLength() { + return header.plaintextLength(); + } + + @Override + public boolean exists() { + return ciphertext.exists(); + } + + @Override + public String getFilename() { + return ciphertext.getFilename(); + } + + @Override + public String getDescription() { + return "decrypted " + ciphertext.getDescription(); + } + } + + /** Single-use resource over an already-open stream (mirrors InputStreamResource semantics). */ + private static final class OneShotResource extends AbstractResource { + private final InputStream stream; + private final long contentLength; + private final String description; + private boolean consumed; + + private OneShotResource(InputStream stream, long contentLength, String description) { + this.stream = stream; + this.contentLength = contentLength; + this.description = description; + } + + @Override + public synchronized InputStream getInputStream() { + if (consumed) { + throw new IllegalStateException( + "InputStream has already been read - do not use OneShotResource twice"); + } + consumed = true; + return stream; + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public String getDescription() { + return "decrypted " + description; + } + } + + /** Presents the spooled ciphertext file as the upload the delegate should persist. */ + private static final class SpooledUpload implements MultipartFile { + private final MultipartFile original; + private final Path spool; + + private SpooledUpload(MultipartFile original, Path spool) { + this.original = original; + this.spool = spool; + } + + @Override + public String getName() { + return original.getName(); + } + + @Override + public String getOriginalFilename() { + return original.getOriginalFilename(); + } + + @Override + public String getContentType() { + return original.getContentType(); + } + + @Override + public boolean isEmpty() { + return getSize() == 0; + } + + @Override + public long getSize() { + try { + return Files.size(spool); + } catch (IOException e) { + throw new IllegalStateException("Spooled ciphertext unavailable", e); + } + } + + @Override + public byte[] getBytes() throws IOException { + return Files.readAllBytes(spool); + } + + @Override + public InputStream getInputStream() throws IOException { + return Files.newInputStream(spool); + } + + @Override + public void transferTo(File dest) throws IOException { + Files.copy(spool, dest.toPath()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyService.java new file mode 100644 index 0000000000..c81f03249c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyService.java @@ -0,0 +1,366 @@ +package stirling.software.proprietary.storage.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.support.TransactionOperations; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.model.FileEncryptionKey; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; + +/** + * Resolves and unwraps scope-level KEKs for storage encryption. Per-team scoping by default (files + * keep decrypting after team changes because each blob header pins its exact key id); GLOBAL is the + * fallback for owners without a team; SOURCE is reserved for pipeline encryption (P2). + * + *

Unwrapped KEKs are cached briefly so the kill switch (DISABLED status) propagates across + * cluster nodes within {@link #CACHE_TTL} without a per-read DB round-trip. Note the TTL bounds the + * write side too: a key disabled on another node can wrap new blobs for up to the TTL there. + */ +@Slf4j +public class FileEncryptionKeyService { + + private static final Duration CACHE_TTL = Duration.ofSeconds(60); + private static final SecureRandom RANDOM = new SecureRandom(); + + private final FileEncryptionKeyRepository repository; + private final FileEncryptionMasterKey masterKey; + private final StorageEncryptionAuditListener auditListener; + + /** + * Runs key-row creation in its own committed transaction (REQUIRES_NEW in production). Callers + * like WorkflowSessionService are {@code @Transactional}, and with an assigned-UUID id the + * INSERT would otherwise defer to the outer commit — the duplicate-key exception would surface + * far from {@link #createActive}'s recovery catch, and the outer transaction would already be + * rollback-only. + */ + private final TransactionOperations keyCreationTx; + + private final Cache unwrapCache = + Caffeine.newBuilder().expireAfterWrite(CACHE_TTL).maximumSize(10_000).build(); + private final Cache activeScopeCache = + Caffeine.newBuilder().expireAfterWrite(CACHE_TTL).maximumSize(10_000).build(); + + public FileEncryptionKeyService( + FileEncryptionKeyRepository repository, FileEncryptionMasterKey masterKey) { + this( + repository, + masterKey, + StorageEncryptionAuditListener.NOOP, + TransactionOperations.withoutTransaction()); + } + + public FileEncryptionKeyService( + FileEncryptionKeyRepository repository, + FileEncryptionMasterKey masterKey, + StorageEncryptionAuditListener auditListener) { + this(repository, masterKey, auditListener, TransactionOperations.withoutTransaction()); + } + + public FileEncryptionKeyService( + FileEncryptionKeyRepository repository, + FileEncryptionMasterKey masterKey, + TransactionOperations keyCreationTx) { + this(repository, masterKey, StorageEncryptionAuditListener.NOOP, keyCreationTx); + } + + public FileEncryptionKeyService( + FileEncryptionKeyRepository repository, + FileEncryptionMasterKey masterKey, + StorageEncryptionAuditListener auditListener, + TransactionOperations keyCreationTx) { + this.repository = repository; + this.masterKey = masterKey; + this.auditListener = auditListener; + this.keyCreationTx = keyCreationTx; + } + + public FileEncryptionMasterKey masterKey() { + return masterKey; + } + + public record ScopeKek(UUID keyId, byte[] key) {} + + /** The ACTIVE KEK for the owner's scope, created on first use. */ + public ScopeKek activeKekForOwner(User owner) throws StorageEncryptionException { + FileEncryptionKey.ScopeType scopeType = FileEncryptionKey.ScopeType.GLOBAL; + long scopeId = 0; + Team team = owner != null ? owner.getTeam() : null; + if (team != null && team.getId() != null) { + scopeType = FileEncryptionKey.ScopeType.TEAM; + scopeId = team.getId(); + } + UUID cachedId = activeScopeCache.getIfPresent(scopeType + ":" + scopeId); + if (cachedId != null) { + byte[] cachedKey = unwrapCache.getIfPresent(cachedId); + if (cachedKey != null) { + return new ScopeKek(cachedId, cachedKey); + } + } + FileEncryptionKey row = findOrCreateActive(scopeType, scopeId); + byte[] kek = unwrapRow(row); + activeScopeCache.put(scopeType + ":" + scopeId, row.getKeyId()); + return new ScopeKek(row.getKeyId(), kek); + } + + /** Unwraps the KEK for decrypting an existing blob. Fails closed on DISABLED or missing. */ + public byte[] kekForDecrypt(UUID keyId) throws StorageEncryptionException { + byte[] cached = unwrapCache.getIfPresent(keyId); + if (cached != null) { + return cached; + } + FileEncryptionKey row = + repository + .findById(keyId) + .orElseThrow( + () -> { + auditListener.decryptDenied(keyId, "key not found"); + return new StorageEncryptionException( + "No encryption key " + + keyId + + " — the key registry does not match the" + + " stored data (restored from an older" + + " database backup?)"); + }); + if (row.getStatus() == FileEncryptionKey.Status.DISABLED) { + auditListener.decryptDenied(keyId, "key disabled"); + throw new StorageKeyRevokedException( + "Encryption key " + keyId + " is disabled; access to this content is revoked"); + } + return unwrapRow(row); + } + + /** + * Flips a key's status (the kill switch) and invalidates the local caches so the change is + * immediate on this node; other cluster nodes converge within the cache TTL. + */ + public FileEncryptionKey setKeyStatus(UUID keyId, FileEncryptionKey.Status status, String actor) + throws StorageEncryptionException { + FileEncryptionKey row = + repository + .findById(keyId) + .orElseThrow( + () -> new StorageEncryptionException("No encryption key " + keyId)); + row.setStatus(status); + row.setStatusChangedAt(LocalDateTime.now()); + row.setStatusChangedBy(actor); + FileEncryptionKey saved = repository.save(row); + invalidate(keyId); + return saved; + } + + /** + * Reverses the kill switch. Returns the key to ACTIVE only while its scope has no other ACTIVE + * key, otherwise to RETIRED — which unwraps existing content just the same, and keeps exactly + * one key wrapping new writes per scope. + * + *

The second case is reached whenever the scope uploaded anything while revoked: those + * writes minted a fresh ACTIVE key, since revoking a key blocks reads of existing content + * rather than stopping the scope from storing new files. + */ + public FileEncryptionKey enable(UUID keyId, String actor) throws StorageEncryptionException { + FileEncryptionKey row = + repository + .findById(keyId) + .orElseThrow( + () -> new StorageEncryptionException("No encryption key " + keyId)); + boolean scopeHasAnotherActiveKey = + activeForScope(row.getScopeType(), row.getScopeId()) + .filter(other -> !other.getKeyId().equals(keyId)) + .isPresent(); + FileEncryptionKey.Status target = + scopeHasAnotherActiveKey + ? FileEncryptionKey.Status.RETIRED + : FileEncryptionKey.Status.ACTIVE; + if (scopeHasAnotherActiveKey) { + log.info( + "Enabling key {} as RETIRED: {}:{} already has an active key wrapping new" + + " writes. Existing content under {} is readable again.", + keyId, + row.getScopeType(), + row.getScopeId(), + keyId); + } + return setKeyStatus(keyId, target, actor); + } + + /** + * Drops cached material for a key so status changes take effect without waiting out the TTL. + */ + public void invalidate(UUID keyId) { + unwrapCache.invalidate(keyId); + activeScopeCache.asMap().values().removeIf(keyId::equals); + } + + /** + * Re-wraps every KEK row below the configured master-key version under the primary master key. + * Cheap by design: touches only this small table, never file contents. Returns the number of + * rows re-wrapped. + */ + public int rotateMasterKey() throws StorageEncryptionException { + int rewrapped = 0; + for (FileEncryptionKey row : + repository.findByMasterKeyVersionLessThan(masterKey.currentVersion())) { + byte[] kek = unwrapRow(row); + row.setWrappedKey( + Base64.getEncoder() + .encodeToString(masterKey.wrap(kek, aadFor(row.getKeyId())))); + row.setMasterKeyVersion(masterKey.currentVersion()); + repository.save(row); + invalidate(row.getKeyId()); + rewrapped++; + } + return rewrapped; + } + + /** + * Startup self-check: proves the configured master key can unwrap every KEK row, so a + * wrong or half-rotated key fails fast instead of silently writing new files under a second key + * hierarchy — or leaving some scopes' files unreadable while the rest of the app looks healthy. + * Checking all rows rather than a sample matters because rotation can stop part-way; the table + * holds one row per scope per rotation, so this stays cheap. + */ + public void verifyMasterKey() { + long pending = repository.countByMasterKeyVersionLessThan(masterKey.currentVersion()); + if (pending > 0) { + log.warn( + "{} encryption key row(s) are still wrapped by the previous master key. Run" + + " POST /api/v1/admin/storage-encryption/master/rotate, then remove" + + " stirling.security.fileEncryptionKeyPrevious.", + pending); + } + long stranded = repository.countByMasterKeyVersionGreaterThan(masterKey.currentVersion()); + if (stranded > 0) { + log.warn( + "{} encryption key row(s) are wrapped by a master-key version newer than the" + + " configured stirling.security.fileEncryptionKeyVersion={}. Rotation" + + " only re-wraps rows below the configured version, so these rows can" + + " never be re-wrapped while it stays this low.", + stranded, + masterKey.currentVersion()); + } + List unreadable = new ArrayList<>(); + StorageEncryptionException firstFailure = null; + // DISABLED rows are included: revocation is meant to be reversible, and a row that cannot + // be unwrapped would not come back on enable. + for (FileEncryptionKey row : repository.findAll()) { + try { + unwrapRow(row); + } catch (StorageEncryptionException e) { + unreadable.add(row); + if (firstFailure == null) { + firstFailure = e; + } + } + } + if (!unreadable.isEmpty()) { + throw new IllegalStateException( + "The configured file encryption key (fingerprint " + + masterKey.fingerprint() + + ") cannot unwrap " + + unreadable.size() + + " of " + + repository.count() + + " encryption key row(s), starting with " + + unreadable.get(0).getKeyId() + + " (" + + unreadable.get(0).getScopeType() + + ":" + + unreadable.get(0).getScopeId() + + ", master key version " + + unreadable.get(0).getMasterKeyVersion() + + "). Files under those keys would be unreadable. Refusing to start —" + + " restore the original key as" + + " stirling.security.fileEncryptionKeyPrevious (or as" + + " STIRLING_FILE_ENCRYPTION_KEY) and re-run the rotation.", + firstFailure); + } + } + + private FileEncryptionKey findOrCreateActive( + FileEncryptionKey.ScopeType scopeType, long scopeId) throws StorageEncryptionException { + return activeForScope(scopeType, scopeId).orElseGet(() -> createActive(scopeType, scopeId)); + } + + private Optional activeForScope( + FileEncryptionKey.ScopeType scopeType, long scopeId) { + return repository.findFirstByScopeTypeAndScopeIdAndStatusOrderByKeyVersionDesc( + scopeType, scopeId, FileEncryptionKey.Status.ACTIVE); + } + + // Package-private so the @DataJpaTest can drive duplicate-insert recovery deterministically. + FileEncryptionKey createActive(FileEncryptionKey.ScopeType scopeType, long scopeId) { + byte[] kek = new byte[EncryptedFileFormat.DEK_LENGTH_BYTES]; + RANDOM.nextBytes(kek); + FileEncryptionKey row = new FileEncryptionKey(); + row.setKeyId(UUID.randomUUID()); + row.setScopeType(scopeType); + row.setScopeId(scopeId); + int version = + repository + .findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc( + scopeType, scopeId) + .map(FileEncryptionKey::getKeyVersion) + .orElse(0) + + 1; + row.setKeyVersion(version); + row.setWrappedKey( + Base64.getEncoder().encodeToString(masterKey.wrap(kek, aadFor(row.getKeyId())))); + row.setMasterKeyVersion(masterKey.currentVersion()); + row.setStatus(FileEncryptionKey.Status.ACTIVE); + try { + // saveAndFlush inside a fresh transaction so a unique-constraint violation surfaces + // right here (not at some outer commit) and the caller's transaction stays healthy. + FileEncryptionKey saved = keyCreationTx.execute(status -> repository.saveAndFlush(row)); + log.info( + "Created storage encryption key {} for {}:{}", + saved.getKeyId(), + scopeType, + scopeId); + unwrapCache.put(saved.getKeyId(), kek); + auditListener.keyCreated(saved.getKeyId(), scopeType + ":" + scopeId, version); + return saved; + } catch (DataIntegrityViolationException raced) { + // Another node created the scope key concurrently; use theirs. + return activeForScope(scopeType, scopeId).orElseThrow(() -> raced); + } + } + + private byte[] unwrapRow(FileEncryptionKey row) throws StorageEncryptionException { + try { + byte[] kek = + masterKey.unwrap( + Base64.getDecoder().decode(row.getWrappedKey()), + aadFor(row.getKeyId())); + unwrapCache.put(row.getKeyId(), kek); + return kek; + } catch (GeneralSecurityException e) { + throw new StorageEncryptionException( + "Failed to unwrap encryption key " + + row.getKeyId() + + " — master key mismatch or corrupted key row", + e); + } + } + + // Binds each wrapped KEK to its row identity so ciphertexts can't be swapped between rows. + private static byte[] aadFor(UUID keyId) { + return keyId.toString().getBytes(StandardCharsets.US_ASCII); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionMasterKey.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionMasterKey.java new file mode 100644 index 0000000000..5b78b00135 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/FileEncryptionMasterKey.java @@ -0,0 +1,230 @@ +package stirling.software.proprietary.storage.crypto; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import java.util.EnumSet; +import java.util.HexFormat; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.configuration.InstallationPathConfig; + +/** + * Master key-encryption key for storage encryption at rest. Deliberately a separate key from {@code + * credential-encryption.key}: file data and integration secrets have different blast radii and + * rotate independently. + * + *

Resolution order mirrors {@code CredentialEncryption}: {@code + * stirling.security.fileEncryptionKey} property, {@code STIRLING_FILE_ENCRYPTION_KEY} env var, then + * an auto-generated owner-only {@code file-encryption.key} in the config dir. Cluster mode requires + * an explicitly shared key. + */ +@Slf4j +public class FileEncryptionMasterKey { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_TAG_BITS = 128; + private static final int IV_BYTES = 12; + private static final String KEY_FILE = "file-encryption.key"; + private static final SecureRandom RANDOM = new SecureRandom(); + + /** Default master-key version when rotation has never been configured. */ + public static final int CURRENT_VERSION = 1; + + private final SecretKey key; + private final SecretKey previousKey; + private final int currentVersion; + + public FileEncryptionMasterKey(String configuredKey, boolean clusterEnabled) { + this(configuredKey, null, CURRENT_VERSION, clusterEnabled); + } + + /** + * @param previousKeyBase64 optional outgoing master key kept only during rotation: {@link + * #unwrap} falls back to it so existing KEK rows stay readable until {@code rotate} + * re-wraps them under the primary key. + * @param currentVersion admin-bumped version stamped on newly wrapped KEK rows ({@code + * stirling.security.fileEncryptionKeyVersion}); rotation re-wraps rows below it. + */ + public FileEncryptionMasterKey( + String configuredKey, + String previousKeyBase64, + int currentVersion, + boolean clusterEnabled) { + this.key = resolveKey(configuredKey, clusterEnabled); + this.previousKey = + previousKeyBase64 == null || previousKeyBase64.isBlank() + ? null + : decodeKey( + previousKeyBase64, "stirling.security.fileEncryptionKeyPrevious"); + if (currentVersion < 1) { + log.warn( + "stirling.security.fileEncryptionKeyVersion={} is not a valid version; using 1", + currentVersion); + } + this.currentVersion = Math.max(1, currentVersion); + log.info( + "Storage encryption master key initialised (AES-256-GCM, fingerprint {}, version" + + " {}{})", + fingerprint(), + this.currentVersion, + previousKey != null ? ", previous key configured for rotation" : ""); + } + + public int currentVersion() { + return currentVersion; + } + + public boolean hasPreviousKey() { + return previousKey != null; + } + + private static SecretKey resolveKey(String configuredKey, boolean clusterEnabled) { + String configured = configuredKey; + String source = "stirling.security.fileEncryptionKey"; + if (configured == null || configured.isBlank()) { + configured = System.getenv("STIRLING_FILE_ENCRYPTION_KEY"); + source = "STIRLING_FILE_ENCRYPTION_KEY"; + } + if (configured != null && !configured.isBlank()) { + return decodeKey(configured, source); + } + if (clusterEnabled) { + throw new IllegalStateException( + "cluster.enabled=true requires a shared file encryption key. Set" + + " STIRLING_FILE_ENCRYPTION_KEY (or" + + " stirling.security.fileEncryptionKey) to the same value on every" + + " node."); + } + return loadOrCreateKeyFile(); + } + + /** + * Decodes and validates key material: must be valid base64 for exactly 32 bytes. Without the + * length check a short key would silently downgrade to AES-128/192 while the startup log claims + * AES-256. + */ + private static SecretKey decodeKey(String base64, String source) { + byte[] bytes; + try { + bytes = Base64.getDecoder().decode(base64.trim()); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(source + " is not valid base64", e); + } + if (bytes.length != 32) { + throw new IllegalStateException( + source + + " must decode to exactly 32 bytes (a 256-bit AES key), got " + + bytes.length + + " bytes. Generate one with: openssl rand -base64 32"); + } + return new SecretKeySpec(bytes, ALGORITHM); + } + + private static SecretKey loadOrCreateKeyFile() { + Path path = Path.of(InstallationPathConfig.getConfigPath(), KEY_FILE); + try { + if (Files.exists(path)) { + return decodeKey(Files.readString(path), path.toString()); + } + KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM); + generator.init(256); + SecretKey generated = generator.generateKey(); + Files.createDirectories(path.getParent()); + writeOwnerOnly(path, Base64.getEncoder().encodeToString(generated.getEncoded())); + log.warn( + "Generated a new file encryption key at {}. Back this file up: losing it makes" + + " every encrypted stored file unrecoverable.", + path); + return generated; + } catch (Exception e) { + throw new IllegalStateException("Unable to initialise file encryption key", e); + } + } + + private static void writeOwnerOnly(Path path, String content) throws IOException { + EnumSet ownerOnly = + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + try { + Files.createFile(path, PosixFilePermissions.asFileAttribute(ownerOnly)); + } catch (UnsupportedOperationException e) { + // Non-POSIX filesystem (Windows): the config-dir ACL is the protection. + Files.createFile(path); + } + Files.writeString(path, content); + try { + Files.setPosixFilePermissions(path, ownerOnly); + } catch (UnsupportedOperationException ignored) { + } + } + + public byte[] wrap(byte[] kek, byte[] associatedData) { + try { + byte[] iv = new byte[IV_BYTES]; + RANDOM.nextBytes(iv); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv)); + cipher.updateAAD(associatedData); + byte[] ciphertext = cipher.doFinal(kek); + byte[] combined = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length); + return combined; + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Failed to wrap scope key", e); + } + } + + public byte[] unwrap(byte[] wrapped, byte[] associatedData) throws GeneralSecurityException { + try { + return unwrapWith(key, wrapped, associatedData); + } catch (GeneralSecurityException primaryFailure) { + if (previousKey == null) { + throw primaryFailure; + } + try { + return unwrapWith(previousKey, wrapped, associatedData); + } catch (GeneralSecurityException previousFailure) { + // Report the primary key's failure, or a corrupt row gets diagnosed through the + // outgoing key's error message. + primaryFailure.addSuppressed(previousFailure); + throw primaryFailure; + } + } + } + + private static byte[] unwrapWith(SecretKey unwrapKey, byte[] wrapped, byte[] associatedData) + throws GeneralSecurityException { + byte[] iv = Arrays.copyOfRange(wrapped, 0, IV_BYTES); + byte[] ciphertext = Arrays.copyOfRange(wrapped, IV_BYTES, wrapped.length); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, unwrapKey, new GCMParameterSpec(GCM_TAG_BITS, iv)); + cipher.updateAAD(associatedData); + return cipher.doFinal(ciphertext); + } + + /** SHA-256 prefix of the key material so admins can verify backups without exposing it. */ + public String fingerprint() { + try { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(key.getEncoded()); + return HexFormat.of().formatHex(digest, 0, 8); + } catch (GeneralSecurityException e) { + return "unavailable"; + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/ResourceUpload.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/ResourceUpload.java new file mode 100644 index 0000000000..fa48d3bb39 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/ResourceUpload.java @@ -0,0 +1,74 @@ +package stirling.software.proprietary.storage.crypto; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +/** + * Presents a loaded {@link Resource} as a {@link MultipartFile} so existing blobs can be re-stored + * through {@code StorageProvider.store(...)} — the encrypt-existing migration re-uploads plaintext + * blobs through the encrypting decorator this way. The size must be accurate: the decorator records + * it as the plaintext length in the blob header. + */ +public class ResourceUpload implements MultipartFile { + + private final Resource resource; + private final String filename; + private final String contentType; + private final long sizeBytes; + + public ResourceUpload(Resource resource, String filename, String contentType, long sizeBytes) { + this.resource = resource; + this.filename = filename; + this.contentType = contentType; + this.sizeBytes = sizeBytes; + } + + @Override + public String getName() { + return "file"; + } + + @Override + public String getOriginalFilename() { + return filename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return sizeBytes == 0; + } + + @Override + public long getSize() { + return sizeBytes; + } + + @Override + public byte[] getBytes() throws IOException { + try (InputStream in = resource.getInputStream()) { + return in.readAllBytes(); + } + } + + @Override + public InputStream getInputStream() throws IOException { + return resource.getInputStream(); + } + + @Override + public void transferTo(File dest) throws IOException { + try (InputStream in = resource.getInputStream()) { + Files.copy(in, dest.toPath()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionAuditListener.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionAuditListener.java new file mode 100644 index 0000000000..978bf3af0d --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionAuditListener.java @@ -0,0 +1,21 @@ +package stirling.software.proprietary.storage.crypto; + +import java.util.UUID; + +/** + * Security-event hook for the storage-encryption layer. The crypto classes stay plain (non-Spring) + * objects, so audit emission is injected through this interface; production wires {@link + * AuditingStorageEncryptionListener}, tests default to {@link #NOOP}. + */ +public interface StorageEncryptionAuditListener { + + StorageEncryptionAuditListener NOOP = new StorageEncryptionAuditListener() {}; + + default void encrypted(String storageKey, UUID keyId) {} + + default void decrypted(String storageKey, UUID keyId) {} + + default void decryptDenied(UUID keyId, String reason) {} + + default void keyCreated(UUID keyId, String scope, int version) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionErrors.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionErrors.java new file mode 100644 index 0000000000..f04e5a5dcc --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionErrors.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.storage.crypto; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +/** + * Shared HTTP translation for encryption failures, so every path that serves stored bytes answers a + * revoked key the same way. {@link StorageKeyRevokedException} extends {@code IOException}, so a + * caller that only catches {@code IOException} reports a deliberate, reversible policy state as a + * server fault. + */ +public final class StorageEncryptionErrors { + + private StorageEncryptionErrors() {} + + public static ResponseStatusException revoked(StorageKeyRevokedException cause) { + return new ResponseStatusException( + HttpStatus.FORBIDDEN, + "Access to this file has been revoked (its encryption key is disabled)", + cause); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionException.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionException.java new file mode 100644 index 0000000000..8db303dd42 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionException.java @@ -0,0 +1,15 @@ +package stirling.software.proprietary.storage.crypto; + +import java.io.IOException; + +/** Storage encrypt/decrypt failure (missing/disabled key, master-key mismatch, tampered blob). */ +public class StorageEncryptionException extends IOException { + + public StorageEncryptionException(String message) { + super(message); + } + + public StorageEncryptionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionState.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionState.java new file mode 100644 index 0000000000..0d2bd112fc --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageEncryptionState.java @@ -0,0 +1,153 @@ +package stirling.software.proprietary.storage.crypto; + +import java.time.Duration; +import java.util.Optional; +import java.util.function.Supplier; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; + +/** + * Holds the storage-encryption machinery for the always-installed {@link EncryptingStorageProvider} + * decorator, shared with the admin API and migration job so kill-switch cache invalidation hits the + * caches the decorator reads. + * + *

The decorator is unconditional so a node whose config lags the cluster (flag off, rolling + * deploy, config drift) can never stream raw ciphertext: it always sniffs the header and decrypts + * or fails loudly. The expensive parts - resolving the master key (which may generate a key file) + * and the key service - are created lazily: eagerly at startup only when the write flag is on or + * key rows already exist (preserving the fail-fast master-key verification), otherwise on first + * encounter with an encrypted blob. + */ +@Slf4j +public class StorageEncryptionState { + + private static final Duration KEYS_EXIST_CACHE_TTL = Duration.ofSeconds(60); + + private final boolean writeEnabled; + private final Supplier keyServiceFactory; + private final FileEncryptionKeyRepository keyRepository; + private final StorageEncryptionAuditListener auditListener; + + private volatile FileEncryptionKeyService keyService; + private volatile boolean keysExistEverChecked; + private volatile long keysExistCheckedAtNanos; + private volatile boolean keysExistCached; + + public StorageEncryptionState( + boolean writeEnabled, + Supplier keyServiceFactory, + FileEncryptionKeyRepository keyRepository, + StorageEncryptionAuditListener auditListener) { + this.writeEnabled = writeEnabled; + this.keyServiceFactory = keyServiceFactory; + this.keyRepository = keyRepository; + this.auditListener = auditListener; + } + + /** Test convenience: a pre-materialised state around an existing service. */ + public static StorageEncryptionState of( + boolean writeEnabled, FileEncryptionKeyService keyService) { + return of(writeEnabled, keyService, StorageEncryptionAuditListener.NOOP); + } + + /** Test convenience: a pre-materialised state with an explicit audit listener. */ + public static StorageEncryptionState of( + boolean writeEnabled, + FileEncryptionKeyService keyService, + StorageEncryptionAuditListener auditListener) { + StorageEncryptionState state = + new StorageEncryptionState(writeEnabled, () -> keyService, null, auditListener); + state.keyService = keyService; + return state; + } + + /** True when new writes are encrypted (flag on + licence passed). */ + public boolean isWriteEnabled() { + return writeEnabled; + } + + /** True once the key machinery has been materialised (eagerly at boot or on first use). */ + public boolean isMaterialised() { + return keyService != null; + } + + public StorageEncryptionAuditListener auditListener() { + return auditListener; + } + + /** + * The key service, created on first use. A failure here (no key material, wrong key) is a loud, + * actionable error - never silently-served ciphertext. + */ + public FileEncryptionKeyService keyService() throws StorageEncryptionException { + FileEncryptionKeyService current = keyService; + if (current != null) { + return current; + } + synchronized (this) { + if (keyService == null) { + try { + keyService = keyServiceFactory.get(); + } catch (RuntimeException e) { + throw new StorageEncryptionException( + "Encrypted content was encountered but the storage encryption key" + + " machinery could not be initialised: " + + e.getMessage(), + e); + } + } + return keyService; + } + } + + /** Forces eager initialisation (startup fail-fast path). */ + public void initialiseEagerly() { + keyService = keyServiceFactory.get(); + } + + /** + * Whether presigned/direct download URLs must be suppressed: any time this node encrypts new + * writes or encrypted content may exist, a presigned URL could hand ciphertext to the client. + * The registry check is cached briefly, so installs that never enable encryption keep the S3 + * fast path. Per-file precision (redirecting plaintext blobs via {@code + * StoredFile.encryptionKeyId}) is a deliberate follow-up. + */ + public boolean suppressDirectDownloads() { + if (writeEnabled || keyService != null) { + return true; + } + if (keyRepository == null) { + return false; + } + long now = System.nanoTime(); + if (keysExistEverChecked + && now - keysExistCheckedAtNanos < KEYS_EXIST_CACHE_TTL.toNanos()) { + return keysExistCached; + } + keysExistCached = countKeys().orElse(true /* unreadable registry: fail safe */); + keysExistCheckedAtNanos = now; + keysExistEverChecked = true; + return keysExistCached; + } + + /** True when key rows exist; empty when the registry could not be read. */ + private Optional countKeys() { + try { + return Optional.of(keyRepository.count() > 0); + } catch (RuntimeException e) { + log.warn( + "Could not read the storage encryption key registry ({}). Treating direct" + + " downloads as unsafe; encrypted content is still decrypted on" + + " demand.", + e.getMessage()); + return Optional.empty(); + } + } + + /** Whether key rows exist, for the decrypt-only startup path; false if unreadable. */ + public boolean encryptedContentMayExist() { + return countKeys().orElse(false); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageKeyRevokedException.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageKeyRevokedException.java new file mode 100644 index 0000000000..a773a14860 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/crypto/StorageKeyRevokedException.java @@ -0,0 +1,14 @@ +package stirling.software.proprietary.storage.crypto; + +/** + * The encryption key protecting the requested content is disabled (the per-scope kill switch). + * Distinct from a generic {@link StorageEncryptionException} so callers can surface this as an + * access-denied (403) rather than an internal error (500): it is a deliberate, reversible policy + * state, not a failure. Re-enabling the key restores access. + */ +public class StorageKeyRevokedException extends StorageEncryptionException { + + public StorageKeyRevokedException(String message) { + super(message); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/FileEncryptionKey.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/FileEncryptionKey.java new file mode 100644 index 0000000000..b58e8922c6 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/FileEncryptionKey.java @@ -0,0 +1,95 @@ +package stirling.software.proprietary.storage.model; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * A scope-level key-encryption key (KEK) for storage encryption at rest. The 256-bit KEK is stored + * AES-GCM-wrapped by the master file-encryption key; per-file data keys are wrapped by the KEK + * inside each encrypted blob's header, so this table stays tiny (one row per scope per rotation) + * and master-key rotation only re-wraps these rows, never file contents. + * + *

Status semantics: ACTIVE wraps new files and unwraps existing ones; RETIRED (post-rotation) + * unwraps only; DISABLED unwraps nothing — the per-scope kill switch. + */ +@Entity +@Table( + name = "file_encryption_keys", + indexes = {@Index(name = "idx_file_enc_keys_scope", columnList = "scope_type,scope_id")}, + uniqueConstraints = + @UniqueConstraint( + name = "uk_file_enc_keys_scope_version", + columnNames = {"scope_type", "scope_id", "key_version"})) +@NoArgsConstructor +@Getter +@Setter +public class FileEncryptionKey implements Serializable { + + private static final long serialVersionUID = 1L; + + public enum ScopeType { + GLOBAL, + TEAM, + SOURCE + } + + public enum Status { + ACTIVE, + RETIRED, + DISABLED + } + + @Id + @Column(name = "key_id", nullable = false) + private UUID keyId; + + @Enumerated(EnumType.STRING) + @Column(name = "scope_type", nullable = false, length = 16) + private ScopeType scopeType; + + /** Scope discriminator (team id, source id). 0 for GLOBAL so the unique constraint holds. */ + @Column(name = "scope_id", nullable = false) + private long scopeId; + + /** Increments when the scope KEK is rotated; the unique constraint tolerates retired rows. */ + @Column(name = "key_version", nullable = false) + private int keyVersion; + + /** Base64 of IV(12) || AES-256-GCM(masterKey, kek) || tag(16), AAD-bound to keyId. */ + @Column(name = "wrapped_key", nullable = false, length = 512) + private String wrappedKey; + + /** Which master-key version wrapped this row; supports master rotation re-wraps. */ + @Column(name = "master_key_version", nullable = false) + private int masterKeyVersion; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 16) + private Status status; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "status_changed_at") + private LocalDateTime statusChangedAt; + + @Column(name = "status_changed_by") + private String statusChangedBy; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/StoredFile.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/StoredFile.java index cbf5316db6..db80bd1e91 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/StoredFile.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/StoredFile.java @@ -102,6 +102,14 @@ public class StoredFile implements Serializable { @Enumerated(EnumType.STRING) private FilePurpose purpose; + /** + * file_encryption_keys id under which this file's blobs (main/history/audit) were encrypted; + * null = stored plaintext. The per-blob truth is each blob's own header — this column exists + * for reporting and the encrypt-existing migration. Nullable so ddl-auto upgrades cleanly. + */ + @Column(name = "encryption_key_id", length = 36) + private String encryptionKeyId; + @OneToMany( mappedBy = "file", fetch = FetchType.LAZY, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/api/StorageEncryptionStatusResponse.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/api/StorageEncryptionStatusResponse.java new file mode 100644 index 0000000000..0699a1aecd --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/model/api/StorageEncryptionStatusResponse.java @@ -0,0 +1,30 @@ +package stirling.software.proprietary.storage.model.api; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * Admin view of storage encryption at rest. The master-key fingerprint (SHA-256 prefix, never key + * material) lets admins verify their key backup matches the live key. + */ +public record StorageEncryptionStatusResponse( + boolean writeEnabled, + boolean active, + String masterKeyFingerprint, + Integer masterKeyVersion, + long encryptedFiles, + long plaintextFiles, + List keys) { + + public record KeyInfo( + UUID keyId, + String scopeType, + long scopeId, + int keyVersion, + int masterKeyVersion, + String status, + LocalDateTime createdAt, + LocalDateTime statusChangedAt, + String statusChangedBy) {} +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/StoredObject.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/StoredObject.java index d58f223c75..d455d3f15c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/StoredObject.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/StoredObject.java @@ -4,10 +4,15 @@ import lombok.Builder; import lombok.Getter; @Getter -@Builder +@Builder(toBuilder = true) public class StoredObject { private final String storageKey; private final String originalFilename; private final String contentType; + + /** Plaintext size. When encryption at rest is active the stored blob is larger. */ private final long sizeBytes; + + /** file_encryption_keys id that wraps this blob's data key; null when stored plaintext. */ + private final String encryptionKeyId; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/FileEncryptionKeyRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/FileEncryptionKeyRepository.java new file mode 100644 index 0000000000..90450ee4ad --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/FileEncryptionKeyRepository.java @@ -0,0 +1,40 @@ +package stirling.software.proprietary.storage.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import stirling.software.proprietary.storage.model.FileEncryptionKey; + +@Repository +public interface FileEncryptionKeyRepository extends JpaRepository { + + /** + * Ordered so that if a scope ever holds more than one row in the given status, every node picks + * the same one instead of following database row order. + */ + Optional findFirstByScopeTypeAndScopeIdAndStatusOrderByKeyVersionDesc( + FileEncryptionKey.ScopeType scopeType, long scopeId, FileEncryptionKey.Status status); + + Optional findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc( + FileEncryptionKey.ScopeType scopeType, long scopeId); + + Optional findFirstByStatus(FileEncryptionKey.Status status); + + /** + * Rows still wrapped by an older master key. Counting and fetching are separate so the startup + * check can ask the database for a number instead of materialising every row. + */ + long countByMasterKeyVersionLessThan(int masterKeyVersion); + + List findByMasterKeyVersionLessThan(int masterKeyVersion); + + /** + * Rows wrapped by a version the configuration has since gone below. Rotation only re-wraps rows + * under the configured version, so these can never be re-wrapped. + */ + long countByMasterKeyVersionGreaterThan(int masterKeyVersion); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/StoredFileRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/StoredFileRepository.java index 47545ab58a..b7ff77c987 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/StoredFileRepository.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/repository/StoredFileRepository.java @@ -3,6 +3,7 @@ package stirling.software.proprietary.storage.repository; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -73,4 +74,53 @@ public interface StoredFileRepository extends JpaRepository { + "WHERE sf.workflowSession IN " + "(SELECT ws FROM WorkflowSession ws WHERE ws.owner = :user)") void clearWorkflowSessionReferencesByOwner(@Param("user") User user); + + // ---- storage encryption at rest ---------------------------------------------------- + + long countByEncryptionKeyIdIsNull(); + + long countByEncryptionKeyIdIsNotNull(); + + /** + * Next batch of plaintext files for the encrypt-existing migration. Cursor-based ({@code id > + * lastId}) so per-file failures don't wedge the loop, and owner is fetched eagerly because the + * job re-stores blobs under the owner's scope key outside a web transaction. + */ + @Query( + "SELECT f FROM StoredFile f JOIN FETCH f.owner " + + "WHERE f.encryptionKeyId IS NULL AND f.id > :lastId ORDER BY f.id ASC") + List findMigratableAfter(@Param("lastId") long lastId, Pageable pageable); + + /** + * Compare-and-swap updates for the migration: each blob's storage key only flips if it still + * holds the value the job read, so a user replacing the file mid-migration wins and the job + * discards its own copy. The main-blob swap also stamps the key id, which is what removes the + * row from the migration's selection. + */ + @Modifying + @Transactional + @Query( + "UPDATE StoredFile f SET f.storageKey = :newKey, f.encryptionKeyId = :keyId " + + "WHERE f.id = :id AND f.storageKey = :oldKey") + int swapMainBlob( + @Param("id") Long id, + @Param("oldKey") String oldKey, + @Param("newKey") String newKey, + @Param("keyId") String keyId); + + @Modifying + @Transactional + @Query( + "UPDATE StoredFile f SET f.historyStorageKey = :newKey " + + "WHERE f.id = :id AND f.historyStorageKey = :oldKey") + int swapHistoryBlob( + @Param("id") Long id, @Param("oldKey") String oldKey, @Param("newKey") String newKey); + + @Modifying + @Transactional + @Query( + "UPDATE StoredFile f SET f.auditLogStorageKey = :newKey " + + "WHERE f.id = :id AND f.auditLogStorageKey = :oldKey") + int swapAuditLogBlob( + @Param("id") Long id, @Param("oldKey") String oldKey, @Param("newKey") String newKey); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java index 32371222f6..7b7d9b68a6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java @@ -32,6 +32,8 @@ import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.service.EmailService; +import stirling.software.proprietary.storage.crypto.StorageEncryptionErrors; +import stirling.software.proprietary.storage.crypto.StorageKeyRevokedException; import stirling.software.proprietary.storage.model.FileShare; import stirling.software.proprietary.storage.model.FileShareAccess; import stirling.software.proprietary.storage.model.FileShareAccessType; @@ -143,6 +145,7 @@ public class FileStorageService { storedFile.setContentType(mainObject.getContentType()); storedFile.setSizeBytes(mainObject.getSizeBytes()); storedFile.setStorageKey(mainObject.getStorageKey()); + storedFile.setEncryptionKeyId(mainObject.getEncryptionKeyId()); applyHistoryMetadata(storedFile, historyObject); applyAuditMetadata(storedFile, auditObject); try { @@ -207,6 +210,7 @@ public class FileStorageService { existing.setContentType(mainObject.getContentType()); existing.setSizeBytes(mainObject.getSizeBytes()); existing.setStorageKey(mainObject.getStorageKey()); + existing.setEncryptionKeyId(mainObject.getEncryptionKeyId()); if (historyObject != null) { applyHistoryMetadata(existing, historyObject); } @@ -492,6 +496,12 @@ public class FileStorageService { ensureStorageEnabled(); try { return storageProvider.load(file.getStorageKey()); + } catch (StorageKeyRevokedException e) { + log.warn( + "Access to stored file {} denied: {}", + file != null ? file.getId() : null, + e.getMessage()); + throw StorageEncryptionErrors.revoked(e); } catch (IOException e) { log.error( "Failed to load stored file {} (key: {})", diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java index 5ea5f28def..32c54c6298 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageCleanupService.java @@ -7,12 +7,14 @@ import java.util.concurrent.TimeUnit; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.storage.model.StorageCleanupEntry; import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.repository.FileShareAccessRepository; import stirling.software.proprietary.storage.repository.FileShareRepository; import stirling.software.proprietary.storage.repository.StorageCleanupEntryRepository; @@ -25,6 +27,7 @@ public class StorageCleanupService { private final StorageProvider storageProvider; private final StorageCleanupEntryRepository cleanupEntryRepository; + private final FileShareAccessRepository fileShareAccessRepository; private final FileShareRepository fileShareRepository; @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS) @@ -62,12 +65,14 @@ public class StorageCleanupService { } @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS) + @Transactional public void cleanupExpiredShareLinks() { List expired = fileShareRepository.findByExpiresAtBeforeAndShareTokenNotNull(LocalDateTime.now()); if (expired.isEmpty()) { return; } + expired.forEach(fileShareAccessRepository::deleteByFileShare); fileShareRepository.deleteAll(expired); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationService.java new file mode 100644 index 0000000000..6df4d1aaea --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationService.java @@ -0,0 +1,316 @@ +package stirling.software.proprietary.storage.service; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.core.io.Resource; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.audit.AuditEventType; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.ResourceUpload; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; +import stirling.software.proprietary.storage.model.StoredFile; +import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.provider.StoredObject; +import stirling.software.proprietary.storage.repository.StoredFileRepository; + +/** + * Encrypts the pre-existing plaintext backlog after storage encryption is enabled (new writes are + * encrypted from the moment the flag is on; this job converts what was stored before). + * + *

Crash-safe per file: store the encrypted copy under a NEW storage key, compare-and-swap the + * row, and only then delete the old blob. A CAS miss means a user replaced the file mid-migration — + * the job discards its own copy and moves on. The worst crash outcome is an orphaned new blob, + * never a lost file, and re-runs are idempotent because selection is {@code encryptionKeyId IS + * NULL} (only stamped by the final main-blob swap). + */ +@Service +@Slf4j +public class StorageEncryptionMigrationService { + + private static final int PAGE_SIZE = 25; + private static final long PAUSE_BETWEEN_PAGES_MS = 200; + + private final StoredFileRepository storedFileRepository; + private final StorageProvider storageProvider; + private final StorageEncryptionState encryptionState; + private final AuditService auditService; + + private final AtomicReference currentRun = new AtomicReference<>(); + + public StorageEncryptionMigrationService( + StoredFileRepository storedFileRepository, + StorageProvider storageProvider, + StorageEncryptionState encryptionState, + AuditService auditService) { + this.storedFileRepository = storedFileRepository; + this.storageProvider = storageProvider; + this.encryptionState = encryptionState; + this.auditService = auditService; + } + + public enum State { + RUNNING, + COMPLETED, + FAILED + } + + public record MigrationStatus( + State state, + long total, + long processed, + long skipped, + long failed, + Instant startedAt, + Instant finishedAt) {} + + /** Starts the migration; throws {@link IllegalStateException} if one is already running. */ + public MigrationStatus start() { + if (!encryptionState.isWriteEnabled()) { + throw new IllegalStateException( + "storage.encryption.enabled must be on before migrating existing files"); + } + // Captured here because the run itself executes on a virtual thread with no security + // context: without this, re-encrypting every stored file is attributed to "system". + String principal = auditService.captureCurrentPrincipal(); + Run run = new Run(storedFileRepository.countByEncryptionKeyIdIsNull(), principal); + Run previous = currentRun.get(); + if (previous != null && previous.state == State.RUNNING) { + throw new IllegalStateException("A migration is already running"); + } + if (!currentRun.compareAndSet(previous, run)) { + throw new IllegalStateException("A migration is already running"); + } + auditService.audit( + principal, + AuditEventType.STORAGE_ENCRYPTION, + Map.of("action", "migration.started", "plaintextFiles", run.total)); + Thread.ofVirtual().name("storage-encryption-migration").start(() -> execute(run)); + return run.snapshot(); + } + + /** Latest run's progress, or empty if none has started this uptime. */ + public Optional status() { + Run run = currentRun.get(); + return Optional.ofNullable(run).map(Run::snapshot); + } + + private void execute(Run run) { + log.info("Storage encryption migration started ({} plaintext files)", run.total); + State terminal; + try { + terminal = migratePages(run); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Storage encryption migration interrupted"); + terminal = State.FAILED; + } catch (Exception e) { + log.error("Storage encryption migration aborted", e); + terminal = State.FAILED; + } + run.finish(terminal); + MigrationStatus done = run.snapshot(); + log.info( + "Storage encryption migration finished: {} encrypted, {} skipped, {} failed", + done.processed(), + done.skipped(), + done.failed()); + auditService.audit( + run.principal, + AuditEventType.STORAGE_ENCRYPTION, + Map.of( + "action", "migration.completed", + "state", done.state().name(), + "encrypted", done.processed(), + "skipped", done.skipped(), + "failed", done.failed())); + } + + /** Walks the plaintext backlog a page at a time; returns the state the run ends in. */ + private State migratePages(Run run) throws InterruptedException { + long lastId = 0; + while (true) { + List page = + storedFileRepository.findMigratableAfter(lastId, PageRequest.of(0, PAGE_SIZE)); + if (page.isEmpty()) { + return State.COMPLETED; + } + for (StoredFile file : page) { + if (!encryptionState.isWriteEnabled()) { + // Run-level condition, not a per-file failure: carrying on would copy every + // remaining file as plaintext and delete it again. + log.warn( + "Encryption write path turned off mid-run; stopping with {} of {} files" + + " encrypted. Re-enable storage.encryption.enabled and start" + + " the migration again.", + run.processed.get(), + run.total); + return State.FAILED; + } + lastId = file.getId(); + try { + migrateFile(file, run); + } catch (Exception e) { + run.failed.incrementAndGet(); + log.error( + "Failed to encrypt stored file {} (key {})", + file.getId(), + file.getStorageKey(), + e); + } + } + Thread.sleep(PAUSE_BETWEEN_PAGES_MS); + } + } + + /** + * The three blobs are swapped independently, so a compare-and-swap miss on a secondary one + * abandons the whole file for this run: the row keeps {@code encryptionKeyId} null and the next + * run picks it up from the top. + */ + private void migrateFile(StoredFile file, Run run) throws IOException { + User owner = file.getOwner(); + + // Secondary blobs first; the main-blob swap stamps encryptionKeyId and thereby removes + // the row from the migration's selection, so it must come last. + if (file.getHistoryStorageKey() != null) { + String oldKey = file.getHistoryStorageKey(); + String newKey = + reencrypt( + owner, + oldKey, + file.getHistoryFilename(), + file.getHistoryContentType(), + file.getHistorySizeBytes()) + .getStorageKey(); + if (storedFileRepository.swapHistoryBlob(file.getId(), oldKey, newKey) == 1) { + deleteQuietly(oldKey); + } else { + deleteQuietly(newKey); + run.skipped.incrementAndGet(); + return; + } + } + if (file.getAuditLogStorageKey() != null) { + String oldKey = file.getAuditLogStorageKey(); + String newKey = + reencrypt( + owner, + oldKey, + file.getAuditLogFilename(), + file.getAuditLogContentType(), + file.getAuditLogSizeBytes()) + .getStorageKey(); + if (storedFileRepository.swapAuditLogBlob(file.getId(), oldKey, newKey) == 1) { + deleteQuietly(oldKey); + } else { + deleteQuietly(newKey); + run.skipped.incrementAndGet(); + return; + } + } + + String oldKey = file.getStorageKey(); + StoredObject encrypted = + reencrypt( + owner, + oldKey, + file.getOriginalFilename(), + file.getContentType(), + file.getSizeBytes()); + if (encrypted.getEncryptionKeyId() == null) { + // The flag flipped between this page's check and the store; the loop stops on the next + // file, so this only ever discards one plaintext copy. + deleteQuietly(encrypted.getStorageKey()); + throw new IllegalStateException("Encryption write path is no longer active"); + } + if (storedFileRepository.swapMainBlob( + file.getId(), + oldKey, + encrypted.getStorageKey(), + encrypted.getEncryptionKeyId()) + == 1) { + deleteQuietly(oldKey); + run.processed.incrementAndGet(); + } else { + deleteQuietly(encrypted.getStorageKey()); + run.skipped.incrementAndGet(); + } + } + + private StoredObject reencrypt( + User owner, String storageKey, String filename, String contentType, Long sizeBytes) + throws IOException { + Resource plaintext = storageProvider.load(storageKey); + long size = -1; + try { + size = plaintext.contentLength(); + } catch (IOException | RuntimeException e) { + // Some resources refuse contentLength() once partially read. + size = -1; + } + if (size < 0) { + // A resource may also simply report -1; the row's recorded plaintext size is + // authoritative, so prefer it over failing the file. + size = sizeBytes != null ? sizeBytes : -1; + } + if (size < 0) { + throw new IOException("Cannot determine plaintext size for " + storageKey); + } + return storageProvider.store( + owner, new ResourceUpload(plaintext, filename, contentType, size)); + } + + private void deleteQuietly(String storageKey) { + try { + storageProvider.delete(storageKey); + } catch (IOException e) { + log.warn("Could not delete blob {} after migration step; orphaned", storageKey, e); + } + } + + private static final class Run { + private final long total; + + /** The admin who started the run, so the completion event is attributed to them. */ + private final String principal; + + private final Instant startedAt = Instant.now(); + private final AtomicLong processed = new AtomicLong(); + private final AtomicLong skipped = new AtomicLong(); + private final AtomicLong failed = new AtomicLong(); + private volatile State state = State.RUNNING; + private volatile Instant finishedAt; + + private Run(long total, String principal) { + this.total = total; + this.principal = principal; + } + + private void finish(State terminal) { + this.state = terminal; + this.finishedAt = Instant.now(); + } + + private MigrationStatus snapshot() { + return new MigrationStatus( + state, + total, + processed.get(), + skipped.get(), + failed.get(), + startedAt, + finishedAt); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java index 2464b6ed62..4e95707217 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java @@ -93,7 +93,7 @@ public class SigningSessionController { summary = "Create a shared signing session", description = "Starts a collaboration session, distributes share links, and optionally notifies" - + " participants. Input:PDF Output:JSON Type:SISO") + + " participants.") public ResponseEntity createSession( @org.springframework.web.bind.annotation.RequestParam("file") org.springframework.web.multipart.MultipartFile file, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java index 0ad30a3bf7..aef781dd9b 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/model/UserServerCertificateEntity.java @@ -2,9 +2,11 @@ package stirling.software.proprietary.workflow.model; import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Objects; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.proxy.HibernateProxy; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -19,7 +21,6 @@ import stirling.software.proprietary.security.model.User; @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) public class UserServerCertificateEntity implements Serializable { @@ -28,7 +29,6 @@ public class UserServerCertificateEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") - @EqualsAndHashCode.Include @ToString.Include private Long id; @@ -70,4 +70,31 @@ public class UserServerCertificateEntity implements Serializable { @UpdateTimestamp @Column(name = "updated_at") private LocalDateTime updatedAt; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = + o instanceof HibernateProxy + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); + Class thisEffectiveClass = + this instanceof HibernateProxy + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + UserServerCertificateEntity that = (UserServerCertificateEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy + ? ((HibernateProxy) this) + .getHibernateLazyInitializer() + .getPersistentClass() + .hashCode() + : getClass().hashCode(); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/UnifiedAccessControlService.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/UnifiedAccessControlService.java index 57ad1c97e7..dd3fcb9cc3 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/UnifiedAccessControlService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/UnifiedAccessControlService.java @@ -119,20 +119,18 @@ public class UnifiedAccessControlService { public ShareAccessRole getEffectiveRole(WorkflowParticipant participant) { ParticipantStatus status = participant.getStatus(); - switch (status) { - case SIGNED: - case DECLINED: - // After action completed, downgrade to read-only - return ShareAccessRole.VIEWER; - case PENDING: - case NOTIFIED: - case VIEWED: - // Active participants retain their assigned role - return participant.getAccessRole(); - default: + return switch (status) { + case SIGNED, DECLINED -> + // After action completed, downgrade to read-only + ShareAccessRole.VIEWER; + case PENDING, NOTIFIED, VIEWED -> + // Active participants retain their assigned role + participant.getAccessRole(); + default -> { log.warn("Unknown participant status: {}", status); - return ShareAccessRole.VIEWER; - } + yield ShareAccessRole.VIEWER; + } + }; } /** Checks if a user can access a specific file */ diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java index 364dbf39ce..4c60c60df2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java @@ -37,6 +37,8 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.crypto.StorageEncryptionErrors; +import stirling.software.proprietary.storage.crypto.StorageKeyRevokedException; import stirling.software.proprietary.storage.model.FilePurpose; import stirling.software.proprietary.storage.model.ShareAccessRole; import stirling.software.proprietary.storage.model.StoredFile; @@ -263,6 +265,7 @@ public class WorkflowSessionService { storedFile.setContentType(storedObject.getContentType()); storedFile.setSizeBytes(storedObject.getSizeBytes()); storedFile.setStorageKey(storedObject.getStorageKey()); + storedFile.setEncryptionKeyId(storedObject.getEncryptionKeyId()); storedFile.setPurpose(purpose); return storedFileRepository.save(storedFile); @@ -443,9 +446,21 @@ public class WorkflowSessionService { HttpStatus.NOT_FOUND, "No processed file available for this session"); } - String storageKey = session.getProcessedFile().getStorageKey(); - org.springframework.core.io.Resource resource = storageProvider.load(storageKey); - return resource.getContentAsByteArray(); + return readBlob(session.getProcessedFile().getStorageKey()); + } + + /** + * Reads a stored blob, translating a revoked encryption key into 403 the same way {@code + * FileStorageService} does. Without this the callers' {@code IOException} handling would report + * an administrator's deliberate revocation as a server error. + */ + private byte[] readBlob(String storageKey) throws IOException { + try { + return storageProvider.load(storageKey).getContentAsByteArray(); + } catch (StorageKeyRevokedException e) { + log.warn("Access to workflow blob {} denied: {}", storageKey, e.getMessage()); + throw StorageEncryptionErrors.revoked(e); + } } /** Retrieves the original file data for a workflow session. */ @@ -457,9 +472,7 @@ public class WorkflowSessionService { HttpStatus.NOT_FOUND, "Original file no longer available (session may be finalized)"); } - String storageKey = session.getOriginalFile().getStorageKey(); - org.springframework.core.io.Resource resource = storageProvider.load(storageKey); - return resource.getContentAsByteArray(); + return readBlob(session.getOriginalFile().getStorageKey()); } /** Deletes a workflow session and associated files. */ @@ -657,9 +670,7 @@ public class WorkflowSessionService { } try { - org.springframework.core.io.Resource resource = - storageProvider.load(fileToServe.getStorageKey()); - return resource.getContentAsByteArray(); + return readBlob(fileToServe.getStorageKey()); } catch (IOException e) { log.error("Failed to retrieve document for session {}", sessionId, e); throw new ResponseStatusException( diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java index c26528a715..0e6cb2f4de 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java @@ -298,24 +298,44 @@ class LiveValkeyIntegrationTest { ValkeyRateLimitStore store = newRateLimitStore(factoryA); String key = "boundary-" + java.util.UUID.randomUUID(); long capacity = 5; - Duration window = Duration.ofMillis(500); + // refillGreedy tops the bucket up continuously, one token every window/capacity. A 500ms + // window left the drain loop only 100ms before a 6th token appeared, so a slow Valkey + // round-trip broke the count; 4s spaces refills 800ms apart, clear of any burst. + Duration window = Duration.ofSeconds(4); + long refillIntervalMs = window.toMillis() / capacity; + long drainStart = System.nanoTime(); int firstAllowed = 0; for (int i = 0; i < 10; i++) { if (store.tryConsume(key, capacity, window).allowed()) firstAllowed++; } - assertEquals(capacity, firstAllowed, "must allow exactly capacity tokens initially"); + long drainMs = (System.nanoTime() - drainStart) / 1_000_000; + // Refill never pauses, so a slow drain earns extra tokens honestly - allow exactly the + // number the elapsed time can have produced and no more. + long earned = drainMs / refillIntervalMs; + assertTrue( + firstAllowed >= capacity && firstAllowed <= capacity + earned, + "initial burst must be capacity (" + + capacity + + ") plus at most the " + + earned + + " token(s) refilled during a " + + drainMs + + "ms drain, got " + + firstAllowed); - Thread.sleep(window.toMillis() + 50); + // A fixed-window limiter would hand back a whole fresh capacity at the boundary; a token + // bucket hands back one token per refill interval. + Thread.sleep(refillIntervalMs + 200); int secondAllowed = 0; - long start = System.nanoTime(); - for (int i = 0; i < 20 && (System.nanoTime() - start) < 20_000_000L; i++) { + for (int i = 0; i < 10; i++) { if (store.tryConsume(key, capacity, window).allowed()) secondAllowed++; } assertTrue( - secondAllowed <= capacity, - "token-bucket must not let a fresh full capacity be consumed instantly across" - + " the boundary; got " + secondAllowed >= 1 && secondAllowed < capacity, + "one refill interval must yield about one token, not a fresh full window of " + + capacity + + "; got " + secondAllowed); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/controller/api/AdminJobControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/controller/api/AdminJobControllerTest.java new file mode 100644 index 0000000000..95d2e097cf --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/controller/api/AdminJobControllerTest.java @@ -0,0 +1,89 @@ +package stirling.software.proprietary.controller.api; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import stirling.software.common.service.JobQueue; +import stirling.software.common.service.TaskManager; + +/** + * The admin sweep covers every user's jobs, so it must stay distinct from the self-service endpoint + * on JobController - and both must run the same TaskManager sweep rather than each growing their + * own cleanup logic. + */ +class AdminJobControllerTest { + + @Mock private TaskManager taskManager; + @Mock private JobQueue jobQueue; + + @InjectMocks private AdminJobController controller; + + private AutoCloseable closeable; + + @BeforeEach + void setUp() { + closeable = MockitoAnnotations.openMocks(this); + } + + @Test + void cleanupWithoutForceRunsTheRetentionSweep() throws Exception { + when(taskManager.cleanupOldJobs()).thenReturn(new TaskManager.CleanupSummary(2, 4, 3)); + + ResponseEntity response = controller.cleanupOldJobs(false); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = (Map) response.getBody(); + assertEquals(2, body.get("removedJobs")); + assertEquals(4, body.get("filesDeleted")); + assertEquals(3, body.get("remainingJobs")); + verify(taskManager).cleanupOldJobs(); + verify(taskManager, never()).cleanupFinishedJobsNow(any()); + closeable.close(); + } + + @Test + void cleanupWithForceIgnoresTheRetentionWindow() throws Exception { + when(taskManager.cleanupFinishedJobsNow(any())) + .thenReturn(new TaskManager.CleanupSummary(5, 9, 0)); + + ResponseEntity response = controller.cleanupOldJobs(true); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = (Map) response.getBody(); + assertEquals(5, body.get("removedJobs")); + assertEquals(9, body.get("filesDeleted")); + verify(taskManager).cleanupFinishedJobsNow(any()); + verify(taskManager, never()).cleanupOldJobs(); + closeable.close(); + } + + @Test + void forcedAdminCleanupSweepsEveryUsersJobs() throws Exception { + when(taskManager.cleanupFinishedJobsNow(any())) + .thenReturn(new TaskManager.CleanupSummary(1, 1, 0)); + + controller.cleanupOldJobs(true); + + @SuppressWarnings("unchecked") + org.mockito.ArgumentCaptor> filter = + org.mockito.ArgumentCaptor.forClass(java.util.function.Predicate.class); + verify(taskManager).cleanupFinishedJobsNow(filter.capture()); + // Unlike the self-service endpoint, the admin sweep is not scoped to one caller. + assertTrue(filter.getValue().test("alice:job")); + assertTrue(filter.getValue().test("bob:job")); + closeable.close(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureClassifierTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureClassifierTest.java new file mode 100644 index 0000000000..951b11c056 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureClassifierTest.java @@ -0,0 +1,216 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpClientErrorException; + +import jakarta.annotation.PostConstruct; + +import stirling.software.common.service.InternalApiTimeoutException; +import stirling.software.common.util.ExceptionUtils; + +import tools.jackson.databind.json.JsonMapper; + +/** + * Tests for {@link FailureClassifier}. The fixtures matter as much as the assertions: a tool's 400 + * arrives as a {@link org.springframework.web.client.RestClientResponseException} carrying the + * Problem Details body, so these build that exact shape rather than a convenient stand-in. + */ +class FailureClassifierTest { + + private final FailureClassifier classifier = + new FailureClassifier(JsonMapper.builder().build()); + + /** The Problem Details body {@code GlobalExceptionHandler} produces for a coded exception. */ + private static HttpClientErrorException problemDetail(HttpStatus status, String errorCode) { + String body = + """ + {"type":"/errors/pdf-password","title":"PDF password required",\ + "status":%d,"detail":"The PDF Document is passworded",\ + "errorCode":"%s","timestamp":"2026-01-01T00:00:00Z"}""" + .formatted(status.value(), errorCode); + return HttpClientErrorException.create( + status, + status.getReasonPhrase(), + new HttpHeaders(), + body.getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8); + } + + private static HttpClientErrorException rawBody(String body) { + return HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, + "Bad Request", + new HttpHeaders(), + body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8); + } + + @Nested + @DisplayName("a downstream tool error") + class DownstreamErrors { + + @Test + void withAClaimedErrorCodeClassifiesToThatKind() { + assertThat(classifier.classify(problemDetail(HttpStatus.BAD_REQUEST, "E004"))) + .isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + } + + @Test + void withAnUnclaimedErrorCodeFallsBackToUnknown() { + // E001 (corrupted PDF) is a real code that no kind has adopted yet. It must land in + // UNKNOWN rather than being force-fitted to the nearest kind. + assertThat(classifier.classify(problemDetail(HttpStatus.BAD_REQUEST, "E001"))) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void withANonJsonBodyFallsBackToUnknownWithoutThrowing() { + assertThat(classifier.classify(rawBody("Internal Server Error"))) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void withAnEmptyBodyFallsBackToUnknown() { + assertThat(classifier.classify(rawBody(""))).isEqualTo(FailureKind.UNKNOWN); + assertThat(classifier.classify(rawBody(null))).isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void withJsonButNoErrorCodePropertyFallsBackToUnknown() { + assertThat(classifier.classify(rawBody("{\"detail\":\"nope\"}"))) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void entitlementSentinelIsNotMistakenForACodedFailure() { + // An entitlement 402 carries `error`, not `errorCode`. It must not be misclassified, + // and specifically must not be attributed to a file problem. + FailureKind kind = + classifier.classify( + rawBody("{\"error\":\"PAYG_LIMIT_REACHED\",\"subscribed\":false}")); + assertThat(kind).isEqualTo(FailureKind.UNKNOWN); + assertThat(kind).isNotEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + } + } + + @Nested + @DisplayName("an exception thrown in process") + class InProcess { + + @Test + void carryingItsOwnErrorCodeIsClassifiedByThatCode() { + // Not every path goes over loopback; a coded exception thrown directly must work too. + ExceptionUtils.PdfPasswordException e = + ExceptionUtils.createPdfPasswordException(new IOException("password")); + assertThat(classifier.classify(e)).isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + } + + @Test + void isClassifiedWhenWrappedInsideAnotherException() { + // The engine wraps and rethrows, so the code is often not on the outermost throwable. + Throwable wrapped = + new IllegalStateException( + "step failed", + ExceptionUtils.createPdfPasswordException(new IOException("pw"))); + assertThat(classifier.classify(wrapped)) + .isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + } + } + + @Nested + @DisplayName("anything unrecognised") + class Fallback { + + @Test + void toolTimeoutIsUnknownUntilAKindClaimsIt() { + // Deliberate: a timeout has its own catch site in the engine but no kind yet. This + // proves the fallback holds even where specific handling already exists. + InternalApiTimeoutException e = + new InternalApiTimeoutException( + "/api/v1/misc/ocr-pdf", Duration.ofSeconds(30), new IOException("t")); + assertThat(classifier.classify(e)).isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void bareRuntimeExceptionIsUnknown() { + assertThat(classifier.classify(new RuntimeException("boom"))) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void nullMessageDoesNotBreakClassification() { + assertThat(classifier.classify(new RuntimeException((String) null))) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void nullThrowableIsUnknown() { + assertThat(classifier.classify(null)).isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void cyclicCauseChainTerminates() { + // The JDK blocks self-causation but not a two-node cycle, and an unbounded walk would + // spin on it forever, taking the run's error handling down with it. + RuntimeException outer = new RuntimeException("outer"); + RuntimeException inner = new RuntimeException("inner", outer); + outer.initCause(inner); + + assertThat(classifier.classify(outer)).isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void aCodeBuriedBeyondTheDepthBoundIsNotFound() { + // Documents the trade: the bound is what guarantees termination, so a pathologically + // deep chain classifies as UNKNOWN rather than hanging. UNKNOWN is always recoverable. + Throwable deepest = + ExceptionUtils.createPdfPasswordException(new IOException("password")); + Throwable chain = deepest; + for (int i = 0; i < 20; i++) { + chain = new IllegalStateException("wrap " + i, chain); + } + assertThat(classifier.classify(chain)).isEqualTo(FailureKind.UNKNOWN); + } + } + + @Nested + @DisplayName("the boot guard on an ambiguous registry") + class BootGuard { + + @Test + void isWiredToRunAtStartupRatherThanOnlyBeingCallable() throws Exception { + // The check moved out of FailureKind's class initialiser to get a readable message. + // That only buys anything if Spring actually invokes it. + assertThat( + FailureClassifier.class + .getDeclaredMethod("verifyNoErrorCodeIsClaimedTwice") + .isAnnotationPresent(PostConstruct.class)) + .isTrue(); + } + + @Test + void acceptsTheRealRegistry() { + assertThatCode(classifier::verifyNoErrorCodeIsClaimedTwice).doesNotThrowAnyException(); + } + + @Test + void wouldRejectADuplicate() { + // FailureKind is a closed enum, so the guard cannot be handed a bad registry. What can + // be shown is that the detection it depends on finds a duplicate when there is one. + assertThat(FailureKind.duplicatesIn(Stream.of("E004", "E001", "E004"))) + .containsExactly("E004"); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureKindTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureKindTest.java new file mode 100644 index 0000000000..a9baf5288f --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FailureKindTest.java @@ -0,0 +1,269 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import stirling.software.common.util.ExceptionUtils; + +/** + * Tests for {@link FailureKind}. Mostly invariants over the whole enum rather than assertions about + * individual members, so a kind added later cannot be malformed in a way that only shows up as a + * button that fails at runtime. + */ +class FailureKindTest { + + @Nested + @DisplayName("every kind is well formed") + class Invariants { + + @ParameterizedTest + @EnumSource(FailureKind.class) + void hasAllFacetsAndAtLeastOneAction(FailureKind kind) { + assertThat(kind.getStage()).isNotNull(); + assertThat(kind.getSeverity()).isNotNull(); + assertThat(kind.getRemedy()).isNotNull(); + assertThat(kind.getScope()).isNotNull(); + assertThat(kind.getActions()) + .as("a kind with no actions cannot be triaged at all") + .isNotEmpty(); + } + + @ParameterizedTest + @EnumSource(FailureKind.class) + void hasCopyKeysAndAnEnglishFallback(FailureKind kind) { + assertThat(kind.getTitleKey()).isNotBlank(); + assertThat(kind.getDescriptionKey()).isNotBlank(); + // The fallback is what lets a client render a kind it was never built with. + assertThat(kind.getDefaultTitle()).isNotBlank(); + } + + @ParameterizedTest + @EnumSource(FailureKind.class) + void idIsScreamingSnakeCase(FailureKind kind) { + assertThat(kind.getId()).matches("^[A-Z][A-Z0-9_]*$"); + } + + @Test + void idsAreUnique() { + Set ids = new HashSet<>(); + for (FailureKind kind : FailureKind.values()) { + assertThat(ids.add(kind.getId())).as("duplicate id %s", kind.getId()).isTrue(); + } + } + + @ParameterizedTest + @EnumSource(FailureKind.class) + void everyDeclaredErrorCodeIsARealErrorCode(FailureKind kind) { + Set known = + Arrays.stream(ExceptionUtils.ErrorCode.values()) + .map(ExceptionUtils.ErrorCode::getCode) + .collect(Collectors.toSet()); + assertThat(known).containsAll(kind.getErrorCodes()); + } + + @ParameterizedTest + @EnumSource(FailureKind.class) + void everyDeclaredActionResolvesToALabelKey(FailureKind kind) { + // Offers are one ordered list, so a label with no matching action is unrepresentable; + // all that is left to assert is that each action gets a key. + for (FailureActionId action : kind.getActions()) { + assertThat(kind.labelKeyFor(action)).startsWith("portal.failures.action."); + } + } + + @Test + void noTwoKindsClaimTheSameErrorCode() { + // Computed independently of duplicateErrorCodes(), then checked against it: the boot + // guard reads that method, so a version of it that always returned empty would leave + // the guard decorative and every other test still passing. + assertThat(FailureKind.duplicateErrorCodes()).isEmpty(); + + Set claimed = new HashSet<>(); + Stream.of(FailureKind.values()) + .flatMap(kind -> kind.getErrorCodes().stream()) + .forEach( + code -> + assertThat(claimed.add(code)) + .as("error code %s claimed twice", code) + .isTrue()); + } + } + + @Nested + @DisplayName("every derived key resolves to English copy") + class Copy { + + /** + * The enum builds its i18n keys from the constant name, so renaming a kind or shipping a + * new one sends keys the client has no copy for, and the UI renders the raw key. Nothing + * else checks that: the portal reads these keys at runtime, and the unused-translation + * audit only looks the other way, for copy no source file mentions. + */ + private static final Set KEYS = englishKeys(); + + @ParameterizedTest + @EnumSource(FailureKind.class) + void titleAndDescriptionAreTranslated(FailureKind kind) { + assertThat(KEYS).contains(kind.getTitleKey(), kind.getDescriptionKey()); + } + + @ParameterizedTest + @EnumSource(FailureKind.class) + void everyOfferedActionIsLabelled(FailureKind kind) { + for (FailureActionId action : kind.getActions()) { + assertThat(KEYS) + .as("%s offers %s, but nothing labels it", kind.getId(), action) + .contains(kind.labelKeyFor(action)); + } + } + + /** + * Every dotted key in the English file, as {@code [section]} plus the name before {@code + * =}. + */ + private static Set englishKeys() { + Set keys = new HashSet<>(); + String section = ""; + for (String raw : readTranslations()) { + String line = raw.strip(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + if (line.startsWith("[") && line.endsWith("]")) { + section = line.substring(1, line.length() - 1).strip() + "."; + continue; + } + int equals = line.indexOf('='); + if (equals > 0) { + keys.add(section + line.substring(0, equals).strip()); + } + } + return keys; + } + + private static List readTranslations() { + // Located by walking up, so the test does not depend on the directory Gradle runs it + // in. + Path relative = Path.of("frontend/editor/public/locales/en-US/translation.toml"); + for (Path dir = Path.of("").toAbsolutePath(); dir != null; dir = dir.getParent()) { + Path candidate = dir.resolve(relative); + if (Files.isRegularFile(candidate)) { + try { + return Files.readAllLines(candidate, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + throw new IllegalStateException( + "No " + relative + " above " + Path.of("").toAbsolutePath()); + } + } + + @Nested + @DisplayName("UNKNOWN is the catch-all") + class Unknown { + + @Test + void offersOnlyTheActionThatClearsIt() { + // Nothing here can be fixed, so "seen it" and "clear it" would be the same decision. + // Offering both just asks the reviewer to press two buttons to reach one outcome. + assertThat(FailureKind.UNKNOWN.getActions()).containsExactly(FailureActionId.DISMISS); + } + + @Test + void claimsNoErrorCodeSoItNeverWinsALookup() { + assertThat(FailureKind.UNKNOWN.getErrorCodes()).isEmpty(); + } + } + + @Nested + @DisplayName("lookup") + class Lookup { + + @Test + void byIdRoundTripsEveryKind() { + for (FailureKind kind : FailureKind.values()) { + assertThat(FailureKind.byId(kind.getId())).contains(kind); + } + } + + @Test + void byIdIsEmptyForAnUnknownId() { + // Ids arrive from persisted rows and from clients, so this must not throw. + assertThat(FailureKind.byId("NO_SUCH_KIND")).isEmpty(); + assertThat(FailureKind.byId(null)).isEmpty(); + assertThat(FailureKind.byId(" ")).isEmpty(); + } + + @Test + void byErrorCodeResolvesTheClaimingKind() { + assertThat(FailureKind.byErrorCode("E004")) + .contains(FailureKind.INPUT_PASSWORD_PROTECTED); + } + + @Test + void byErrorCodeIsEmptyForACodeNoKindHasAdoptedYet() { + // E001 is PDF_CORRUPTED: a real error code, deliberately not yet a kind. + assertThat(FailureKind.byErrorCode("E001")).isEmpty(); + assertThat(FailureKind.byErrorCode(null)).isEmpty(); + } + } + + @Nested + @DisplayName("action declaration and labels") + class Actions { + + @Test + void declaresOnlyWhatItLists() { + assertThat(FailureKind.UNKNOWN.declares(FailureActionId.DISMISS)).isTrue(); + assertThat(FailureKind.UNKNOWN.declares(FailureActionId.ACKNOWLEDGE)).isFalse(); + } + + @Test + void aKindWithSomethingToFixOffersTheFixAndAWayToSkipIt() { + assertThat(FailureKind.INPUT_PASSWORD_PROTECTED.getActions()) + .containsExactly(FailureActionId.ACKNOWLEDGE, FailureActionId.DISMISS); + } + + @Test + void overriddenLabelWinsOverTheGenericOne() { + String label = + FailureKind.INPUT_PASSWORD_PROTECTED.labelKeyFor(FailureActionId.DISMISS); + assertThat(label).isEqualTo("portal.failures.action.dismissSkipFile"); + assertThat(label).isNotEqualTo(FailureKind.genericLabelKey(FailureActionId.DISMISS)); + } + + @Test + void genericLabelIsUsedWhenAKindDeclaresNoOverride() { + assertThat(FailureKind.UNKNOWN.labelKeyFor(FailureActionId.DISMISS)) + .isEqualTo(FailureKind.genericLabelKey(FailureActionId.DISMISS)) + .isEqualTo("portal.failures.action.dismiss"); + } + + @Test + void copyKeysAreDerivedFromTheIdInLowerCamel() { + assertThat(FailureKind.INPUT_PASSWORD_PROTECTED.getTitleKey()) + .isEqualTo("portal.failures.kind.inputPasswordProtected.title"); + assertThat(FailureKind.INPUT_PASSWORD_PROTECTED.getDescriptionKey()) + .isEqualTo("portal.failures.kind.inputPasswordProtected.description"); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventControllerTest.java new file mode 100644 index 0000000000..58b8d1b408 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventControllerTest.java @@ -0,0 +1,540 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.UserServiceInterface; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; + +/** + * Tests for {@link FileRunEventController}: the wire shape, the status mapping for each refusal + * reason, and that the team is taken from the principal rather than the request. + */ +@ExtendWith(MockitoExtension.class) +class FileRunEventControllerTest { + + private static final Long TEAM = 4L; + + @Mock private PolicyManagementAuthority authority; + @Mock private UserServiceInterface userService; + + private FileRunEventStore store; + private FileRunEventController controller; + + @BeforeEach + void setUp() { + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(true); + store = new FileRunEventStore(new InMemoryFileRunEventRepository()); + FailureActionRegistry registry = + new FailureActionRegistry( + List.of(new AcknowledgeAction(store), new DismissAction(store))); + controller = + new FileRunEventController( + new FileRunEventService(store, registry, authority, userService, props)); + + lenient().when(authority.canEditPolicies()).thenReturn(true); + lenient().when(authority.currentUserTeamId()).thenReturn(TEAM); + lenient().when(userService.getCurrentUsername()).thenReturn("reviewer@example.com"); + } + + private FileRunEvent given(FailureKind kind, Long teamId, String fileId) { + return recorded("author@example.com", kind, teamId, fileId, "run-1"); + } + + /** + * As {@link #given} but naming who hit it, in its own run. A RUN-scoped kind keys on the run, + * so two rows sharing one run id are one incident, however they differ otherwise. + */ + private FileRunEvent givenHitBy(String actor, FailureKind kind, Long teamId, String fileId) { + return recorded(actor, kind, teamId, fileId, "run-" + fileId); + } + + private FileRunEvent recorded( + String actor, FailureKind kind, Long teamId, String fileId, String runId) { + return store.record( + new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + actor, + "policy-1", + runId, + null, + fileId, + "the raw failure message")); + } + + private static List fileIds(int count) { + return java.util.stream.IntStream.range(0, count).mapToObj(i -> "f-" + i).toList(); + } + + /** The status a refused call came back with. Fails the test if the call was allowed. */ + private HttpStatus statusOf(Runnable call) { + try { + call.run(); + } catch (ResponseStatusException e) { + return HttpStatus.valueOf(e.getStatusCode().value()); + } + throw new AssertionError("expected the call to be refused"); + } + + @Nested + @DisplayName("listing") + class Listing { + + @Test + void returnsOnlyTheCallersTeamsRows() { + given(FailureKind.UNKNOWN, TEAM, "mine"); + given(FailureKind.UNKNOWN, 99L, "theirs"); + + assertThat(controller.list(null, null, null).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("mine"); + } + + @Test + void carriesTheCopyKeysAndTheEnglishFallback() { + given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + FileRunEventView view = controller.list(null, null, null).events().getFirst(); + + assertThat(view.titleKey()) + .isEqualTo("portal.failures.kind.inputPasswordProtected.title"); + assertThat(view.descriptionKey()) + .isEqualTo("portal.failures.kind.inputPasswordProtected.description"); + assertThat(view.defaultTitle()).isNotBlank(); + assertThat(view.detail()).isEqualTo("the raw failure message"); + } + + @Test + void carriesActionsAlreadyResolvedForTheRow() { + given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + List actions = + controller.list(null, null, null).events().getFirst().actions(); + + assertThat(actions) + .extracting(FileRunEventView.ActionView::id) + .containsExactlyInAnyOrder("ACKNOWLEDGE", "DISMISS"); + assertThat(actions).allMatch(FileRunEventView.ActionView::enabled); + } + + @Test + void showsAClosedRowsActionsDisabledWithAReasonRatherThanHidingThem() { + // Only visible by asking for the closed status: the default queue drops it. + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + controller.act(event.id(), "DISMISS", null); + + List actions = + controller + .list(FileRunEventStatus.DISMISSED, null, null) + .events() + .getFirst() + .actions(); + + assertThat(actions).noneMatch(FileRunEventView.ActionView::enabled); + assertThat(actions) + .allMatch(a -> "portal.failures.disabled.closed".equals(a.disabledReasonKey())); + } + + @Test + void filtersByStatusAndByKind() { + FileRunEvent locked = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "locked"); + given(FailureKind.UNKNOWN, TEAM, "open"); + controller.act(locked.id(), "ACKNOWLEDGE", null); + + assertThat(controller.list(FileRunEventStatus.ACKNOWLEDGED, null, null).events()) + .hasSize(1); + assertThat(controller.list(null, "INPUT_PASSWORD_PROTECTED", null).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("locked"); + // Acknowledged is still open work, so it stays in the default queue. + assertThat(controller.list(null, "NO_SUCH_KIND", null).events()).isEmpty(); + } + + @Test + void capsTheRequestedLimitSoAClientCannotAskForTheWholeTable() { + // Distinct files, so five genuinely distinct rows exist. The earlier version of this + // test used a RUN-scoped kind with one hardcoded run id, so all five folded into one + // row and the assertion measured the rollup rather than the cap. + for (int i = 0; i < 5; i++) { + given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f" + i); + } + + // Over-large and non-positive limits are both coerced rather than rejected. + assertThat(controller.list(null, null, 100_000).events()).hasSize(5); + assertThat(controller.list(null, null, 2).events()).hasSize(2); + assertThat(controller.list(null, null, 0).events()).hasSize(1); + } + + @Test + void kindFilterAppliesBeforeTheLimitNotAfter() { + // A post-limit filter would take the newest N rows and then discard non-matches, + // returning nothing while matching rows exist. The filter lives in the query. + given(FailureKind.UNKNOWN, TEAM, "old-unknown"); + for (int i = 0; i < 3; i++) { + given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "newer-" + i); + } + + assertThat(controller.list(null, "UNKNOWN", 1).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("old-unknown"); + } + } + + @Nested + @DisplayName("acting") + class Acting { + + @Test + void appliesADeclaredActionAndReturnsTheUpdatedRow() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + FileRunEventView updated = controller.act(event.id(), "ACKNOWLEDGE", null); + + assertThat(updated.status()).isEqualTo(FileRunEventStatus.ACKNOWLEDGED); + assertThat(updated.statusActor()).isEqualTo("reviewer@example.com"); + } + + @Test + void acceptsAnAbsentBodyBecauseTheseActionsNeedNoInput() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + + assertThat(controller.act(event.id(), "DISMISS", null).status()) + .isEqualTo(FileRunEventStatus.DISMISSED); + } + + @Test + void anUnknownActionIsABadRequest() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + + assertThat(statusOf(() -> controller.act(event.id(), "APPROVE", null))) + .isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void anotherTeamsRowIsNotFoundRatherThanForbidden() { + // 404 rather than 403, so the response does not confirm the row exists. + FileRunEvent theirs = given(FailureKind.UNKNOWN, 99L, "f1"); + + assertThat(statusOf(() -> controller.act(theirs.id(), "ACKNOWLEDGE", null))) + .isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void anAlreadyClosedRowIsAConflict() { + // The request was well formed and would have been valid a moment earlier. + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + controller.act(event.id(), "DISMISS", null); + + assertThat(statusOf(() -> controller.act(event.id(), "ACKNOWLEDGE", null))) + .isEqualTo(HttpStatus.CONFLICT); + } + } + + @Nested + @DisplayName("the kinds catalogue") + class Kinds { + + @Test + void servesEveryRegisteredKindWithItsFacetsAndDeclaredActions() { + List kinds = controller.kinds(); + + assertThat(kinds).hasSameSizeAs(FailureKind.values()); + assertThat(kinds).extracting(FailureKindView::id).contains("UNKNOWN"); + assertThat(kinds).allMatch(k -> k.stage() != null && !k.actions().isEmpty()); + } + + @Test + void carriesTheLabelKeyForEachDeclaredAction() { + FailureKindView locked = + controller.kinds().stream() + .filter(k -> "INPUT_PASSWORD_PROTECTED".equals(k.id())) + .findFirst() + .orElseThrow(); + + assertThat(locked.actions()) + .extracting(FailureKindView.ActionDeclaration::labelKey) + .contains("portal.failures.action.dismissSkipFile"); + } + + @Test + void reportsTheErrorCodesEachKindClaims() { + FailureKindView locked = + controller.kinds().stream() + .filter(k -> "INPUT_PASSWORD_PROTECTED".equals(k.id())) + .findFirst() + .orElseThrow(); + + assertThat(locked.errorCodes()).containsExactly("E004"); + } + } + + @Nested + @DisplayName("a leader reviews the team's failures, everyone else their own") + class Authorization { + + @Test + void aMemberSeesTheirOwnFailuresAndNobodyElses() { + // A member can report a failure, so they get to see it back. It must not widen to a + // colleague's. + givenHitBy("reviewer@example.com", FailureKind.UNKNOWN, TEAM, "mine"); + givenHitBy("colleague@example.com", FailureKind.UNKNOWN, TEAM, "theirs"); + when(authority.canEditPolicies()).thenReturn(false); + + assertThat(controller.list(null, null, null).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("mine"); + } + + @Test + void aLeaderSeesTheWholeTeams() { + givenHitBy("reviewer@example.com", FailureKind.UNKNOWN, TEAM, "mine"); + givenHitBy("colleague@example.com", FailureKind.UNKNOWN, TEAM, "theirs"); + when(authority.canEditPolicies()).thenReturn(true); + + assertThat(controller.list(null, null, null).events()) + .extracting(FileRunEventView::fileId) + .containsExactlyInAnyOrder("mine", "theirs"); + } + + @Test + void aMemberMayCloseTheirOwn() { + // Someone who fixes their own problem should not have to ask a leader to clear the row. + FileRunEvent mine = + givenHitBy("reviewer@example.com", FailureKind.UNKNOWN, TEAM, "mine"); + when(authority.canEditPolicies()).thenReturn(false); + + assertThat(controller.act(mine.id(), "DISMISS", null).status()) + .isEqualTo(FileRunEventStatus.DISMISSED); + } + + @Test + void aMemberCannotCloseAColleaguesEvenKnowingTheId() { + // Refusing the read is not enough on its own: an id learned any other way must not work + // either. Answered as not-found rather than forbidden, so trying does not confirm the + // row exists. + FileRunEvent theirs = + givenHitBy("colleague@example.com", FailureKind.UNKNOWN, TEAM, "theirs"); + when(authority.canEditPolicies()).thenReturn(false); + + assertThat(statusOf(() -> controller.act(theirs.id(), "DISMISS", null))) + .isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void theRegistryIsOpenBecauseItIsCopyNotData() { + // A member renders the failures they can see, so they need the labels for them. No role + // stub: the point is that kinds() never asks. + assertThat(controller.kinds()).isNotEmpty(); + } + + @Test + void loginDisabledTrustsTheLocalOperator() { + // A single-user deployment has no roles to distinguish, so the narrowing must not leave + // the only user reading nothing. + ApplicationProperties unsecured = new ApplicationProperties(); + unsecured.getSecurity().setEnableLogin(false); + FileRunEventController noLogin = + new FileRunEventController( + new FileRunEventService( + store, + new FailureActionRegistry( + List.of( + new AcknowledgeAction(store), + new DismissAction(store))), + authority, + userService, + unsecured)); + + assertThatCode(() -> noLogin.list(null, null, null)).doesNotThrowAnyException(); + // Not merely permitted: the role is never consulted at all, which is what makes the + // carve-out independent of however the authority answers with no users configured. + verify(authority, never()).canEditPolicies(); + } + } + + @Nested + @DisplayName("team scoping cannot be influenced by the caller") + class Scoping { + + @Test + void thereIsNoWayToAskForAnotherTeam() { + // Stated as a test because the absence of the parameter is the property. Adding a + // teamId parameter to list() breaks this at compile time. + given(FailureKind.UNKNOWN, 99L, "theirs"); + + FileRunEventController.FileRunEventsResponse response = + controller.list(null, null, null); + + assertThat(response.events()).isEmpty(); + assertThat( + Arrays.stream(FileRunEventController.class.getDeclaredMethods()) + .filter(m -> "list".equals(m.getName())) + .flatMap(m -> Arrays.stream(m.getParameterTypes())) + .toList()) + .doesNotContain(Long.class); + } + + @Test + void loginDisabledReadsTheUnteamedRows() { + ApplicationProperties unsecured = new ApplicationProperties(); + unsecured.getSecurity().setEnableLogin(false); + FileRunEventController noLogin = + new FileRunEventController( + new FileRunEventService( + store, + new FailureActionRegistry( + List.of( + new AcknowledgeAction(store), + new DismissAction(store))), + authority, + userService, + unsecured)); + given(FailureKind.UNKNOWN, null, "unteamed"); + given(FailureKind.UNKNOWN, TEAM, "teamed"); + + assertThat(noLogin.list(null, null, null).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("unteamed"); + } + } + + @Nested + @DisplayName("reporting from the editor") + class Reporting { + + @Test + void aMemberMayReportAndThenSeeTheirOwnReport() { + // Reporting was always open to a member; reading their own back is the round trip that + // makes the report worth anything to them. + when(authority.canEditPolicies()).thenReturn(false); + + assertThatCode( + () -> + controller.report( + new EditorFailureReport( + "compress", "E004", List.of("f-1"), "boom"))) + .doesNotThrowAnyException(); + assertThat(controller.list(null, null, null).events()) + .extracting(FileRunEventView::fileId) + .containsExactly("f-1"); + } + + @Test + void answersWithNoContentSoTheEditorNeverWaitsOnABody() { + EditorFailureReport report = + new EditorFailureReport("compress", "E004", List.of("f-1"), "boom"); + + assertThat(controller.report(report).getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + void rejectsAReportWithNoOperation() { + assertThat( + statusOf( + () -> + controller.report( + new EditorFailureReport( + " ", "E004", List.of("f-1"), "boom")))) + .isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void acceptsAReportAtTheFileLimitAndRecordsEveryRow() { + List atLimit = fileIds(EditorFailureReport.MAX_FILE_IDS); + + EditorFailureReport report = + new EditorFailureReport("compress", "E004", atLimit, "boom"); + + assertThat(controller.report(report).getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + assertThat(store.list(TEAM, null, null, null, EditorFailureReport.MAX_FILE_IDS + 10)) + .hasSize(EditorFailureReport.MAX_FILE_IDS); + } + + @Test + void refusesAReportOverTheFileLimitAndRecordsNothing() { + // One call used to be able to mint an unbounded number of permanent incidents, since + // each named file gets its own row and TOOL dedup keys never fold across ids. Refused + // rather than trimmed so nothing is lost silently, and refused before the first write + // so a rejected report cannot leave a partial set behind either. + List overLimit = fileIds(EditorFailureReport.MAX_FILE_IDS + 1); + + assertThat( + statusOf( + () -> + controller.report( + new EditorFailureReport( + "compress", + "E004", + overLimit, + "boom")))) + .isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(store.list(TEAM, null, null, null, EditorFailureReport.MAX_FILE_IDS + 10)) + .isEmpty(); + } + + @Test + void saysWhatTheLimitIsSoAClientAuthorCanSeeWhatHappened() { + // The editor reports in the background, so the message is the only place this surfaces. + assertThatThrownBy( + () -> + controller.report( + new EditorFailureReport( + "compress", + "E004", + fileIds(EditorFailureReport.MAX_FILE_IDS + 1), + "boom"))) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining(String.valueOf(EditorFailureReport.MAX_FILE_IDS)); + } + + @Test + void theReportHasNoTeamOrFileNameToSupply() { + // Stated as a test because the absence of those fields is the property. Adding either + // to + // EditorFailureReport breaks this at compile time. + List components = + java.util.Arrays.stream(EditorFailureReport.class.getRecordComponents()) + .map(java.lang.reflect.RecordComponent::getName) + .toList(); + + assertThat(components).containsExactly("operation", "errorCode", "fileIds", "detail"); + } + } + + @Nested + @DisplayName("action request body") + class RequestBody { + + @Test + void nullInputsAreTreatedAsEmpty() { + assertThat(new FileRunEventController.ActionRequest(null).safeInputs()).isEmpty(); + assertThat( + new FileRunEventController.ActionRequest(Map.of("password", "x")) + .safeInputs()) + .containsEntry("password", "x"); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventHttpIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventHttpIntegrationTest.java new file mode 100644 index 0000000000..375e46f4ad --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventHttpIntegrationTest.java @@ -0,0 +1,445 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration; +import org.springframework.boot.security.autoconfigure.actuate.web.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterAutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.UserServiceInterface; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +/** + * End-to-end test of the read path and action dispatch over real HTTP, against live Jetty on a + * random port. + * + *

The other tests here call controller methods directly, which skips response serialisation, + * body binding, query coercion and status mapping. A double-encoded request body already slipped + * through that gap once, so these use a real {@link HttpClient} against a real socket. + */ +@SpringBootTest( + classes = FileRunEventHttpIntegrationTest.TestApp.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class FileRunEventHttpIntegrationTest { + + private static final Long TEAM = 42L; + private static final String ACTOR = "reviewer@example.com"; + + @LocalServerPort private int port; + + private final HttpClient http = HttpClient.newHttpClient(); + private final JsonMapper mapper = JsonMapper.builder().build(); + + /** Shared so a test can seed a row and then read it back over the wire. */ + private static InMemoryFileRunEventRepository repository; + + @BeforeEach + void resetRows() { + repository.rows.clear(); + } + + private String seed(FailureKind kind, Long teamId, String fileId, String detail) { + FileRunEventStore store = new FileRunEventStore(repository); + return store.record( + new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + "author@example.com", + "policy-1", + "run-1", + null, + fileId, + detail)) + .id(); + } + + private HttpResponse get(String path) throws Exception { + return http.send( + HttpRequest.newBuilder(URI.create("http://localhost:" + port + path)).GET().build(), + HttpResponse.BodyHandlers.ofString()); + } + + private HttpResponse post(String path, String jsonBody) throws Exception { + HttpRequest.Builder builder = + HttpRequest.newBuilder(URI.create("http://localhost:" + port + path)) + .header("Content-Type", "application/json"); + builder = + jsonBody == null + ? builder.POST(HttpRequest.BodyPublishers.noBody()) + : builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody)); + return http.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + } + + @Nested + @DisplayName("the read path") + class ReadPath { + + @Test + void serialisesAnEventWithItsFacetsCopyKeysAndResolvedActions() throws Exception { + seed(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "acme-msa", "locked tight"); + + HttpResponse response = get("/api/v1/file-run-events"); + + assertThat(response.statusCode()).isEqualTo(200); + JsonNode row = mapper.readTree(response.body()).get("events").get(0); + + assertThat(row.get("kindId").asString()).isEqualTo("INPUT_PASSWORD_PROTECTED"); + assertThat(row.get("stage").asString()).isEqualTo("INPUT"); + assertThat(row.get("severity").asString()).isEqualTo("ERROR"); + assertThat(row.get("scope").asString()).isEqualTo("FILE"); + assertThat(row.get("origin").asString()).isEqualTo("POLICY"); + assertThat(row.get("remedy").asString()).isEqualTo("NEEDS_USER_INPUT"); + assertThat(row.get("titleKey").asString()) + .isEqualTo("portal.failures.kind.inputPasswordProtected.title"); + assertThat(row.get("defaultTitle").asString()).isNotBlank(); + assertThat(row.get("detail").asString()).isEqualTo("locked tight"); + assertThat(row.get("status").asString()).isEqualTo("NEW"); + assertThat(row.get("occurrences").asInt()).isEqualTo(1); + // Epoch millis, not an ISO string: the client renders relative times from a number. + assertThat(row.get("lastSeenAt").isNumber()).isTrue(); + + JsonNode actions = row.get("actions"); + assertThat(actions).hasSize(2); + assertThat(actions.get(0).get("id").asString()).isEqualTo("ACKNOWLEDGE"); + assertThat(actions.get(0).get("labelKey").asString()) + .isEqualTo("portal.failures.action.acknowledge"); + assertThat(actions.get(0).get("enabled").asBoolean()).isTrue(); + assertThat(actions.get(0).get("disabledReasonKey").isNull()).isTrue(); + } + + @Test + void doesNotReturnAnotherTeamsRows() throws Exception { + seed(FailureKind.UNKNOWN, 999L, "theirs", "not yours"); + + JsonNode events = mapper.readTree(get("/api/v1/file-run-events").body()).get("events"); + + assertThat(events).isEmpty(); + } + + @Test + void ignoresAClientSuppliedTeamId() throws Exception { + // The team comes from the principal, so a teamId query param is ignored rather than + // honoured. + seed(FailureKind.UNKNOWN, 999L, "theirs", "not yours"); + + JsonNode events = + mapper.readTree(get("/api/v1/file-run-events?teamId=999").body()).get("events"); + + assertThat(events).isEmpty(); + } + + @Test + void coercesQueryParametersAndFiltersOnThem() throws Exception { + String locked = seed(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "locked", "b"); + seed(FailureKind.UNKNOWN, TEAM, "open", "a"); + post("/api/v1/file-run-events/" + locked + "/actions/ACKNOWLEDGE", "{\"inputs\":{}}"); + + JsonNode acknowledged = + mapper.readTree(get("/api/v1/file-run-events?status=ACKNOWLEDGED").body()) + .get("events"); + assertThat(acknowledged).hasSize(1); + + JsonNode byKind = + mapper.readTree( + get("/api/v1/file-run-events?kindId=INPUT_PASSWORD_PROTECTED") + .body()) + .get("events"); + assertThat(byKind).hasSize(1); + assertThat(byKind.get(0).get("fileId").asString()).isEqualTo("locked"); + + // `limit` binds as an Integer, so a non-numeric value is a bad request, not a 500. + assertThat(get("/api/v1/file-run-events?limit=abc").statusCode()).isEqualTo(400); + } + + @Test + void rejectsAnUnknownStatusFilter() throws Exception { + assertThat(get("/api/v1/file-run-events?status=BANANA").statusCode()).isEqualTo(400); + } + + @Test + void servesTheKindsCatalogue() throws Exception { + HttpResponse response = get("/api/v1/file-run-events/kinds"); + + assertThat(response.statusCode()).isEqualTo(200); + JsonNode kinds = mapper.readTree(response.body()); + assertThat(kinds).hasSize(FailureKind.values().length); + assertThat(kinds.get(0).get("actions").isArray()).isTrue(); + } + } + + @Nested + @DisplayName("reporting from the editor") + class Reporting { + + @Test + void bindsAReportBodyAndAnswersNoContent() throws Exception { + HttpResponse response = + post( + "/api/v1/file-run-events/reports", + "{\"operation\":\"remove-password\",\"errorCode\":\"E004\"," + + "\"fileIds\":[\"f-1\",\"f-2\"],\"detail\":\"locked\"}"); + + assertThat(response.statusCode()).isEqualTo(204); + assertThat(response.body()).isEmpty(); + + JsonNode events = mapper.readTree(get("/api/v1/file-run-events").body()).get("events"); + assertThat(events).hasSize(2); + assertThat(events.get(0).get("origin").asString()).isEqualTo("TOOL"); + assertThat(events.get(0).get("kindId").asString()) + .isEqualTo("INPUT_PASSWORD_PROTECTED"); + } + + @Test + void acceptsAReportWithNoCodeOrFiles() throws Exception { + HttpResponse response = + post( + "/api/v1/file-run-events/reports", + "{\"operation\":\"compress\",\"detail\":\"network died\"}"); + + assertThat(response.statusCode()).isEqualTo(204); + JsonNode events = mapper.readTree(get("/api/v1/file-run-events").body()).get("events"); + assertThat(events).hasSize(1); + assertThat(events.get(0).get("kindId").asString()).isEqualTo("UNKNOWN"); + assertThat(events.get(0).get("fileId").isNull()).isTrue(); + } + + @Test + void rejectsAReportWithNoOperation() throws Exception { + assertThat( + post( + "/api/v1/file-run-events/reports", + "{\"errorCode\":\"E004\",\"detail\":\"boom\"}") + .statusCode()) + .isEqualTo(400); + } + + @Test + void rejectsAnOversizedReportWithoutRecordingAnyOfIt() throws Exception { + // Over the wire because that is where the flood would arrive: one request, an + // arbitrarily long fileIds array, a permanent row per entry. The read-back is the point + // of the test, since a partial write would be worse than either accepting or refusing. + String ids = + java.util.stream.IntStream.range(0, EditorFailureReport.MAX_FILE_IDS + 1) + .mapToObj(i -> "\"f-" + i + "\"") + .collect(java.util.stream.Collectors.joining(",")); + + HttpResponse response = + post( + "/api/v1/file-run-events/reports", + "{\"operation\":\"compress\",\"errorCode\":\"E004\",\"fileIds\":[" + + ids + + "],\"detail\":\"boom\"}"); + + assertThat(response.statusCode()).isEqualTo(400); + assertThat(mapper.readTree(get("/api/v1/file-run-events").body()).get("events")) + .isEmpty(); + } + } + + @Nested + @DisplayName("action dispatch") + class Dispatch { + + @Test + void bindsTheRequestBodyAndReturnsTheUpdatedRow() throws Exception { + // The regression guard: an object body, sent as real JSON over the wire, binding into + // ActionRequest. A double-encoded string would fail here. + String id = seed(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1", "boom"); + + HttpResponse response = + post( + "/api/v1/file-run-events/" + id + "/actions/ACKNOWLEDGE", + "{\"inputs\":{}}"); + + assertThat(response.statusCode()).isEqualTo(200); + JsonNode row = mapper.readTree(response.body()); + assertThat(row.get("status").asString()).isEqualTo("ACKNOWLEDGED"); + assertThat(row.get("statusActor").asString()).isEqualTo(ACTOR); + } + + @Test + void acceptsAPopulatedInputsMap() throws Exception { + // Nothing consumes inputs yet, but the shape must bind so the first action that needs + // one (a password) does not discover a broken contract. + String id = seed(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1", "locked"); + + HttpResponse response = + post( + "/api/v1/file-run-events/" + id + "/actions/ACKNOWLEDGE", + "{\"inputs\":{\"password\":\"hunter2\"}}"); + + assertThat(response.statusCode()).isEqualTo(200); + } + + @Test + void acceptsAnAbsentBody() throws Exception { + String id = seed(FailureKind.UNKNOWN, TEAM, "f1", "boom"); + + assertThat( + post("/api/v1/file-run-events/" + id + "/actions/DISMISS", null) + .statusCode()) + .isEqualTo(200); + } + + @Test + void mapsAnUnknownActionToBadRequest() throws Exception { + String id = seed(FailureKind.UNKNOWN, TEAM, "f1", "boom"); + + assertThat( + post( + "/api/v1/file-run-events/" + id + "/actions/APPROVE", + "{\"inputs\":{}}") + .statusCode()) + .isEqualTo(400); + } + + @Test + void mapsAnotherTeamsRowToNotFound() throws Exception { + String id = seed(FailureKind.UNKNOWN, 999L, "theirs", "boom"); + + assertThat( + post( + "/api/v1/file-run-events/" + + id + + "/actions/ACKNOWLEDGE", + "{\"inputs\":{}}") + .statusCode()) + .isEqualTo(404); + } + + @Test + void mapsAnAlreadyClosedRowToConflict() throws Exception { + String id = seed(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1", "boom"); + post("/api/v1/file-run-events/" + id + "/actions/DISMISS", "{\"inputs\":{}}"); + + assertThat( + post( + "/api/v1/file-run-events/" + + id + + "/actions/ACKNOWLEDGE", + "{\"inputs\":{}}") + .statusCode()) + .isEqualTo(409); + } + + @Test + void aDismissedRowComesBackWithItsActionsDisabled() throws Exception { + String id = seed(FailureKind.UNKNOWN, TEAM, "f1", "boom"); + + JsonNode row = + mapper.readTree( + post("/api/v1/file-run-events/" + id + "/actions/DISMISS", null) + .body()); + + assertThat(row.get("status").asString()).isEqualTo("DISMISSED"); + for (JsonNode action : row.get("actions")) { + assertThat(action.get("enabled").asBoolean()).isFalse(); + assertThat(action.get("disabledReasonKey").asString()) + .isEqualTo("portal.failures.disabled.closed"); + } + } + } + + /** + * Spring Security is excluded rather than configured to permit everything: the auto-configured + * chain would answer 401 before the handler runs, and this test is about routing, body binding, + * serialisation and status mapping. Authorisation and team scoping have their own tests. + */ + @SpringBootConfiguration + @EnableAutoConfiguration( + exclude = { + SecurityAutoConfiguration.class, + ServletWebSecurityAutoConfiguration.class, + ManagementWebSecurityAutoConfiguration.class, + SecurityFilterAutoConfiguration.class, + UserDetailsServiceAutoConfiguration.class + }) + static class TestApp { + + /** + * Backed by the shared in-memory repository, since this test is about the HTTP layer and + * persistence has its own tests. + */ + @Bean + FileRunEventStore fileRunEventStore() { + repository = new InMemoryFileRunEventRepository(); + return new FileRunEventStore(repository); + } + + @Bean + FailureActionRegistry failureActionRegistry(List actions) { + return new FailureActionRegistry(actions); + } + + @Bean + AcknowledgeAction acknowledgeAction(FileRunEventStore store) { + return new AcknowledgeAction(store); + } + + @Bean + DismissAction dismissAction(FileRunEventStore store) { + return new DismissAction(store); + } + + @Bean + ApplicationProperties applicationProperties() { + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(true); + props.getAutomaticallyGenerated().setAppVersion("test"); + return props; + } + + @Bean + PolicyManagementAuthority policyManagementAuthority() { + PolicyManagementAuthority authority = + org.mockito.Mockito.mock(PolicyManagementAuthority.class); + org.mockito.Mockito.when(authority.currentUserTeamId()).thenReturn(TEAM); + org.mockito.Mockito.when(authority.canEditPolicies()).thenReturn(true); + return authority; + } + + @Bean + UserServiceInterface userService() { + UserServiceInterface users = org.mockito.Mockito.mock(UserServiceInterface.class); + org.mockito.Mockito.when(users.getCurrentUsername()).thenReturn(ACTOR); + return users; + } + + @Bean + FileRunEventService fileRunEventService( + FileRunEventStore store, + FailureActionRegistry registry, + PolicyManagementAuthority authority, + UserServiceInterface users, + ApplicationProperties props) { + return new FileRunEventService(store, registry, authority, users, props); + } + + @Bean + FileRunEventController fileRunEventController(FileRunEventService service) { + return new FileRunEventController(service); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventServiceTest.java new file mode 100644 index 0000000000..4e6508bed9 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventServiceTest.java @@ -0,0 +1,725 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.UserServiceInterface; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; + +/** + * Tests for {@link FileRunEventService}: team scoping, the declaration guard, transition legality, + * and that triaging an incident leaves the document alone. + */ +@ExtendWith(MockitoExtension.class) +class FileRunEventServiceTest { + + private static final Long TEAM = 3L; + private static final String ACTOR = "reviewer@example.com"; + + @Mock private PolicyManagementAuthority authority; + @Mock private UserServiceInterface userService; + + private FileRunEventStore store; + private FileRunEventService service; + + @BeforeEach + void setUp() { + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(true); + + store = new FileRunEventStore(new InMemoryFileRunEventRepository()); + FailureActionRegistry registry = + new FailureActionRegistry( + List.of(new AcknowledgeAction(store), new DismissAction(store))); + registry.verifyEveryDeclaredActionHasAHandler(); + + service = new FileRunEventService(store, registry, authority, userService, props); + + lenient().when(authority.currentUserTeamId()).thenReturn(TEAM); + lenient().when(userService.getCurrentUsername()).thenReturn(ACTOR); + // A leader unless a test says otherwise: most of these are about team scoping, which is + // what a leader sees. The member narrowing has its own tests. + lenient().when(authority.canEditPolicies()).thenReturn(true); + } + + private FileRunEvent given(FailureKind kind, Long teamId, String fileId) { + return store.record( + new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + "author@example.com", + "policy-1", + "run-1", + null, + fileId, + "detail")); + } + + @Nested + @DisplayName("acknowledge") + class Acknowledge { + + @Test + void movesANewEventToAcknowledgedAndStampsTheActor() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + FileRunEvent updated = service.dispatch(event.id(), "ACKNOWLEDGE", Map.of()); + + assertThat(updated.status()).isEqualTo(FileRunEventStatus.ACKNOWLEDGED); + assertThat(updated.statusActor()).isEqualTo(ACTOR); + assertThat(updated.statusAt()).isNotNull(); + } + + @Test + void isANoOpWhenAlreadyAcknowledgedSoOwnershipIsNotStolen() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + FileRunEvent first = service.dispatch(event.id(), "ACKNOWLEDGE", Map.of()); + Instant originalAt = first.statusAt(); + + when(userService.getCurrentUsername()).thenReturn("someone-else@example.com"); + FileRunEvent second = service.dispatch(event.id(), "ACKNOWLEDGE", Map.of()); + + assertThat(second.status()).isEqualTo(FileRunEventStatus.ACKNOWLEDGED); + assertThat(second.statusActor()).isEqualTo(ACTOR); + assertThat(second.statusAt()).isEqualTo(originalAt); + } + } + + @Nested + @DisplayName("dismiss") + class Dismiss { + + @Test + void closesANewEvent() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + + assertThat(service.dispatch(event.id(), "DISMISS", Map.of()).status()) + .isEqualTo(FileRunEventStatus.DISMISSED); + } + + @Test + void closesAnAcknowledgedEvent() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + service.dispatch(event.id(), "ACKNOWLEDGE", Map.of()); + + assertThat(service.dispatch(event.id(), "DISMISS", Map.of()).status()) + .isEqualTo(FileRunEventStatus.DISMISSED); + } + } + + @Nested + @DisplayName("triage never touches the document") + class NeverTouchesTheDocument { + + /** + * What makes both actions valid for every kind, UNKNOWN included: they are incident + * dispositions, not document ones. Asserted structurally — an action whose only dependency + * is the event store cannot reach the ledger, file storage, or an output sink. A + * mocked-collaborator version of this test passed vacuously, because nothing ever handed + * the mocks to the actions. + */ + @Test + void actionsDependOnNothingButTheEventStore() { + for (FailureAction action : + List.of(new AcknowledgeAction(store), new DismissAction(store))) { + String name = action.getClass().getSimpleName(); + List> dependencies = + Arrays.stream(action.getClass().getDeclaredFields()) + // Coverage instrumentation adds its own field; only ours count. + .filter(field -> !field.isSynthetic()) + .map(Field::getType) + .toList(); + + // Asserted first, so an action that lost its fields fails here rather than + // satisfying the check below by holding nothing at all. + assertThat(dependencies).as("%s declares no dependencies", name).isNotEmpty(); + assertThat(dependencies) + .as( + "%s: an incident disposition reaches the event store and nothing" + + " else. Adding to this is how one starts touching" + + " documents.", + name) + .containsOnly(FileRunEventStore.class); + } + } + + @Test + void dismissLeavesTheFileReferenceIntactRatherThanClearingIt() { + // Nothing is deleted, so the row must still say which file it was about. + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + FileRunEvent dismissed = service.dispatch(event.id(), "DISMISS", Map.of()); + + assertThat(dismissed.fileId()).isEqualTo("f1"); + } + } + + @Nested + @DisplayName("guards") + class Guards { + + @Test + void anotherTeamsEventIsNotFound() { + FileRunEvent theirs = given(FailureKind.UNKNOWN, 99L, "f1"); + + assertThatThrownBy(() -> service.dispatch(theirs.id(), "ACKNOWLEDGE", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.EVENT_NOT_FOUND); + } + + @Test + void anUnknownEventIdIsNotFound() { + assertThatThrownBy(() -> service.dispatch("nope", "ACKNOWLEDGE", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.EVENT_NOT_FOUND); + } + + @Test + void anUnknownActionIdIsRejected() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + + assertThatThrownBy(() -> service.dispatch(event.id(), "APPROVE", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.ACTION_NOT_RECOGNISED); + } + + @Test + void aDeclaredActionWithNoRegisteredHandlerIsRejectedWithoutAnyStateChange() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(true); + FileRunEventService missingHandler = + new FileRunEventService( + store, + new FailureActionRegistry(List.of(new AcknowledgeAction(store))), + authority, + userService, + props); + + assertThatThrownBy(() -> missingHandler.dispatch(event.id(), "DISMISS", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.ACTION_NOT_RECOGNISED); + + assertThat(store.find(event.id(), TEAM).orElseThrow().status()) + .isEqualTo(FileRunEventStatus.NEW); + } + + @Test + void aClosedEventCannotBeActedOnAgain() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + service.dispatch(event.id(), "DISMISS", Map.of()); + + assertThatThrownBy(() -> service.dispatch(event.id(), "ACKNOWLEDGE", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.ALREADY_CLOSED); + + assertThatThrownBy(() -> service.dispatch(event.id(), "DISMISS", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.ALREADY_CLOSED); + } + } + + @Nested + @DisplayName("available actions are resolved per row") + class Availability { + + @Test + void openRowOffersEveryDeclaredActionEnabled() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + List actions = service.availableActions(event); + + assertThat(actions).hasSize(2); + assertThat(actions).allMatch(FileRunEventService.AvailableAction::enabled); + assertThat(actions).allMatch(action -> action.disabledReasonKey() == null); + } + + @Test + void closedRowOffersThemDisabledWithAReasonRatherThanHidingThem() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + FileRunEvent dismissed = service.dispatch(event.id(), "DISMISS", Map.of()); + + List actions = service.availableActions(dismissed); + + assertThat(actions).isNotEmpty(); + assertThat(actions).noneMatch(FileRunEventService.AvailableAction::enabled); + assertThat(actions) + .allMatch( + action -> + "portal.failures.disabled.closed" + .equals(action.disabledReasonKey())); + } + + @Test + void carriesTheKindsOverriddenLabelWhereItHasOne() { + FileRunEvent event = given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1"); + + assertThat(service.availableActions(event)) + .extracting(FileRunEventService.AvailableAction::labelKey) + .contains("portal.failures.action.dismissSkipFile"); + } + + @Test + void fallsBackToTheGenericLabelWhereTheKindDeclaresNoOverride() { + FileRunEvent event = given(FailureKind.UNKNOWN, TEAM, "f1"); + + assertThat(service.availableActions(event)) + .extracting(FileRunEventService.AvailableAction::labelKey) + .containsExactly("portal.failures.action.dismiss"); + } + } + + @Nested + @DisplayName("read scoping") + class Scoping { + + @Test + void listReturnsOnlyTheCallersTeam() { + given(FailureKind.UNKNOWN, TEAM, "mine"); + given(FailureKind.UNKNOWN, 99L, "theirs"); + + assertThat(service.list(null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("mine"); + } + + @Test + void aMemberReadsOnlyTheFailuresTheyCaused() { + // Reporting is open to a member, so reading their own back is what lets us tell them + // anything at all. A colleague's must not come with it. + store.record(RecordFailure.forEditor(FailureKind.UNKNOWN, TEAM, ACTOR, "mine", "boom")); + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, "colleague@example.com", "theirs", "boom")); + when(authority.canEditPolicies()).thenReturn(false); + + assertThat(service.list(null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("mine"); + } + + @Test + void aMemberWithNoResolvableNameReadsNothingRatherThanEverything() { + // Narrowing to "mine" needs a name to narrow by. Dropping the filter would hand the + // whole team to someone who may not have it. + given(FailureKind.UNKNOWN, TEAM, "mine"); + when(authority.canEditPolicies()).thenReturn(false); + when(userService.getCurrentUsername()).thenReturn(null); + + assertThat(service.list(null, null, 50)).isEmpty(); + } + + @Test + void aMemberCannotActOnAColleaguesRowEvenKnowingItsId() { + // Refusing the read is not enough on its own: an id learned any other way must not work + // either. Reported as not-found, so trying does not confirm the row exists. + FileRunEvent theirs = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, + TEAM, + "colleague@example.com", + "theirs", + "boom")); + when(authority.canEditPolicies()).thenReturn(false); + + assertThatThrownBy(() -> service.dispatch(theirs.id(), "DISMISS", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.EVENT_NOT_FOUND); + + assertThat(store.find(theirs.id(), TEAM).orElseThrow().status()) + .isEqualTo(FileRunEventStatus.NEW); + } + + @Test + void aMemberMayCloseTheirOwn() { + // Someone who fixes their own problem should not have to ask a leader to clear the row. + FileRunEvent mine = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, ACTOR, "mine", "boom")); + when(authority.canEditPolicies()).thenReturn(false); + + assertThat(service.dispatch(mine.id(), "DISMISS", Map.of()).status()) + .isEqualTo(FileRunEventStatus.DISMISSED); + } + + @Test + void aCallerWhoseTeamCannotBeResolvedReadsNothing() { + // A run with no stored policy is recorded unteamed, and those rows are shared by every + // team, so a caller whose team does not resolve reads nothing instead. + given(FailureKind.UNKNOWN, null, "someone-elses-adhoc-run"); + given(FailureKind.UNKNOWN, TEAM, "mine"); + when(authority.currentUserTeamId()).thenReturn(null); + + assertThat(service.list(null, null, 50)).isEmpty(); + } + + @Test + void aCallerWhoseTeamCannotBeResolvedCannotActOnAnUnteamedRow() { + FileRunEvent unteamed = given(FailureKind.UNKNOWN, null, "someone-elses-adhoc-run"); + when(authority.currentUserTeamId()).thenReturn(null); + + assertThatThrownBy(() -> service.dispatch(unteamed.id(), "ACKNOWLEDGE", Map.of())) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + // Not a distinct "no team" reason: that would confirm the id exists. + .isEqualTo(FailureActionException.Reason.EVENT_NOT_FOUND); + + assertThat(store.find(unteamed.id(), null).orElseThrow().status()) + .isEqualTo(FileRunEventStatus.NEW); + } + + @Test + void loginDisabledFallsBackToTheUnteamedRowsWithoutConsultingTheAuthority() { + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(false); + FailureActionRegistry registry = + new FailureActionRegistry( + List.of(new AcknowledgeAction(store), new DismissAction(store))); + FileRunEventService unsecured = + new FileRunEventService(store, registry, authority, userService, props); + + given(FailureKind.UNKNOWN, null, "unteamed"); + given(FailureKind.UNKNOWN, TEAM, "teamed"); + + assertThat(unsecured.list(null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("unteamed"); + } + } + + @Nested + @DisplayName("the action registry") + class Registry { + + @Test + void refusesToStartWhenAKindDeclaresAnActionWithNoHandler() { + // A missing handler would otherwise surface as a button that 400s in production. + FailureActionRegistry incomplete = + new FailureActionRegistry(List.of(new AcknowledgeAction(store))); + + assertThatThrownBy(incomplete::verifyEveryDeclaredActionHasAHandler) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("DISMISS"); + } + + @Test + void refusesTwoHandlersForTheSameAction() { + assertThatThrownBy( + () -> + new FailureActionRegistry( + List.of( + new AcknowledgeAction(store), + new AcknowledgeAction(store)))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("ACKNOWLEDGE"); + } + + @Test + void acceptsACompleteSetOfHandlers() { + FailureActionRegistry complete = + new FailureActionRegistry( + List.of(new AcknowledgeAction(store), new DismissAction(store))); + + complete.verifyEveryDeclaredActionHasAHandler(); + + for (FailureActionId id : FailureActionId.values()) { + assertThat(complete.find(id)).isPresent(); + } + } + } + + @Nested + @DisplayName("reporting a failure the user hit in the editor") + class Reporting { + + @Test + void classifiesTheReportedCodeAndStampsItAsEditorOrigin() { + service.report( + new EditorFailureReport("remove-password", "E004", List.of("f-1"), "boom")); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.kind()).isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + assertThat(event.origin()).isEqualTo(FailureOrigin.TOOL); + assertThat(event.fileId()).isEqualTo("f-1"); + assertThat(event.detail()).contains("boom"); + } + + @Test + void takesTheTeamAndActorFromThePrincipalNotTheReport() { + // The report carries no team or actor field at all; both come from the caller's + // session. + service.report(new EditorFailureReport("compress", "E004", List.of("f-1"), "boom")); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.teamId()).isEqualTo(TEAM); + assertThat(event.actor()).isEqualTo(ACTOR); + } + + @Test + void stillFilesTheRowUnderTheTeamWhenTheReporterCannotBeNamed() { + // Recording is open to everyone and takes the caller's team, not their read scope: a + // reporter who cannot be named reads nothing back, but the row is still the team's + // rather than dropping into the unteamed bucket every team shares. No role stub either, + // since recording never asks. + when(userService.getCurrentUsername()).thenReturn(null); + + service.report(new EditorFailureReport("compress", "E004", List.of("f-1"), "boom")); + + assertThat(store.list(TEAM, null, null, null, 10)) + .singleElement() + .extracting(FileRunEvent::teamId) + .isEqualTo(TEAM); + } + + @Test + void recordsAnUnrecognisedCodeAsUnknownRatherThanDroppingIt() { + service.report(new EditorFailureReport("ocr", "E999", List.of("f-1"), "no idea")); + + assertThat(store.list(TEAM, null, null, null, 10).getFirst().kind()) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void recordsAnAbsentCodeAsUnknown() { + service.report(new EditorFailureReport("ocr", null, List.of("f-1"), "network died")); + + assertThat(store.list(TEAM, null, null, null, 10).getFirst().kind()) + .isEqualTo(FailureKind.UNKNOWN); + } + + @Test + void recordsOneIncidentPerFileSoEachDocumentStaysActionable() { + service.report( + new EditorFailureReport( + "compress", "E004", List.of("f-1", "f-2", "f-3"), "boom")); + + assertThat(store.list(TEAM, null, null, null, 10)) + .hasSize(3) + .extracting(FileRunEvent::fileId) + .containsExactlyInAnyOrder("f-1", "f-2", "f-3"); + } + + @Test + void foldsARepeatOfTheSameFileIntoTheExistingIncident() { + service.report(new EditorFailureReport("compress", "E004", List.of("f-1"), "boom")); + service.report( + new EditorFailureReport("compress", "E004", List.of("f-1"), "boom again")); + + assertThat(store.list(TEAM, null, null, null, 10)) + .singleElement() + .extracting(FileRunEvent::occurrences) + .isEqualTo(2); + } + + @Test + void recordsOneUnattributedIncidentWhenNoFileWasNamed() { + service.report(new EditorFailureReport("compress", "E004", List.of(), "boom")); + + assertThat(store.list(TEAM, null, null, null, 10)) + .singleElement() + .extracting(FileRunEvent::fileId) + .isNull(); + } + + @Test + void recordsEveryFileInALargeBatchRatherThanTrimmingIt() { + // A cap here used to drop the overflow silently, so a reviewer saw 25 of 60 failures + // with nothing indicating the rest existed. The processor path has never had one. + List many = + java.util.stream.IntStream.range(0, 60).mapToObj(i -> "f-" + i).toList(); + + service.report(new EditorFailureReport("compress", "E004", many, "boom")); + + assertThat(store.list(TEAM, null, null, null, 200)).hasSize(60); + } + + @Test + void keepsTheOperationNameOutOfTheStoredDocumentReferences() { + // The operation is context for the reviewer, not a document reference: it belongs in + // detail, never in fileId. + service.report( + new EditorFailureReport("remove-password", "E004", List.of("f-1"), "boom")); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.detail()).contains("remove-password"); + assertThat(event.fileId()).isEqualTo("f-1"); + } + + @Test + void storesTheReportedMessageVerbatimAlongsideTheOperation() { + // The user's own error about their own file. The operation is prefixed because an + // editor failure has no run to give a reviewer context. + service.report( + new EditorFailureReport( + "compress", "E004", List.of("f-1"), "Failed on Q4 report.pdf")); + + assertThat(store.list(TEAM, null, null, null, 10).getFirst().detail()) + .isEqualTo("compress: Failed on Q4 report.pdf"); + } + + @Test + void theServiceHoldsNothingThatCouldReachADocument() { + // Asserted structurally rather than with verifyNoInteractions on unwired mocks, which + // is how the version of this test on the other branch passed without proving anything. + List> forbidden = + List.of( + stirling.software.proprietary.policy.ledger.ProcessedLedger.class, + stirling.software.common.service.FileStorage.class, + stirling.software.proprietary.policy.output.PolicyOutputSink.class); + + List> held = + Arrays.stream(FileRunEventService.class.getDeclaredFields()) + .filter(field -> !field.isSynthetic()) + .map(Field::getType) + .toList(); + + assertThat(held).isNotEmpty().doesNotContainAnyElementsOf(forbidden); + } + } + + @Nested + @DisplayName("editor incidents stay separate") + class EditorIncidentIdentity { + + private void reportedBy(String actor, String fileId) { + when(userService.getCurrentUsername()).thenReturn(actor); + service.report(new EditorFailureReport("compress", null, List.of(fileId), "boom")); + } + + @Test + void twoPeopleHittingTheSameUnclassifiedFailureAreTwoIncidents() { + // UNKNOWN is RUN scoped and an editor report has no run, so without the fallback every + // unclassified editor failure in a team collapsed into one row: one actor credited for + // everyone's, and the wrong person offered the row. + reportedBy("alice@example.com", "a-1"); + reportedBy("bob@example.com", "b-1"); + + assertThat(store.list(TEAM, null, null, null, 10)) + .extracting(FileRunEvent::actor) + .containsExactlyInAnyOrder("alice@example.com", "bob@example.com"); + } + + @Test + void onePersonsTwoBrokenFilesAreTwoIncidents() { + reportedBy("alice@example.com", "a-1"); + reportedBy("alice@example.com", "a-2"); + + assertThat(store.list(TEAM, null, null, null, 10)) + .extracting(FileRunEvent::fileId) + .containsExactlyInAnyOrder("a-1", "a-2"); + } + + @Test + void theSamePersonHittingTheSameFileTwiceIsOneIncident() { + reportedBy("alice@example.com", "a-1"); + reportedBy("alice@example.com", "a-1"); + + assertThat(store.list(TEAM, null, null, null, 10)) + .singleElement() + .extracting(FileRunEvent::occurrences) + .isEqualTo(2); + } + } + + @Nested + @DisplayName("files deleted from the editor") + class RemovedFiles { + + private void reported(String fileId) { + service.report(new EditorFailureReport("compress", "E004", List.of(fileId), "boom")); + } + + @Test + void closeTheirIncidentsSoTheQueueStopsAskingAboutThem() { + reported("f-1"); + + assertThat(service.forgetFiles(List.of("f-1"))).isEqualTo(1); + assertThat(service.list(null, null, 10)) + .as("the open queue is what the reviewer works from") + .isEmpty(); + } + + @Test + void theRowSurvivesForAudit() { + reported("f-1"); + service.forgetFiles(List.of("f-1")); + + assertThat(service.list(FileRunEventStatus.FILE_REMOVED, null, 10)) + .singleElement() + .satisfies( + event -> { + assertThat(event.fileId()).isEqualTo("f-1"); + assertThat(event.detail()).contains("boom"); + }); + } + + @Test + void aReviewersDismissKeepsItsMeaningAndItsActor() { + reported("f-1"); + FileRunEvent event = service.list(null, null, 10).getFirst(); + service.dispatch(event.id(), "DISMISS", Map.of()); + + assertThat(service.forgetFiles(List.of("f-1"))) + .as("only open rows move; a closed one has already been decided") + .isZero(); + assertThat(service.list(FileRunEventStatus.DISMISSED, null, 10)).hasSize(1); + } + + @Test + void aColleaguesIncidentIsUntouchedEvenForALeader() { + // File ids come from the client, so naming one must not close someone else's row. The + // caller here is a leader, who reads the whole team: this path narrows to their own + // rows + // regardless, since a null actor would otherwise match every unattributed row. + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, "employee@example.com", "f-1", "theirs")); + + assertThat(service.forgetFiles(List.of("f-1"))).isZero(); + } + + @Test + void aProcessorIncidentIsUntouchedEvenOnTheSameFileId() { + // Nothing was deleted from an editor there, and the file may still be in the bucket. + given(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f-1"); + + assertThat(service.forgetFiles(List.of("f-1"))).isZero(); + } + + @Test + void namingNoFilesClosesNothing() { + reported("f-1"); + + assertThat(service.forgetFiles(List.of())).isZero(); + assertThat(service.forgetFiles(java.util.Arrays.asList(null, " "))).isZero(); + assertThat(service.list(null, null, 10)).hasSize(1); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreDbTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreDbTest.java new file mode 100644 index 0000000000..982d804354 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreDbTest.java @@ -0,0 +1,384 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * {@link FileRunEventStore} against the real JPQL on a real (H2) database. Every other test in this + * package substitutes {@link InMemoryFileRunEventRepository}, whose Java filters would keep passing + * if the queries themselves regressed — the team-isolation clause could be deleted from the JPQL + * and no in-memory test would notice. + * + *

{@code NOT_SUPPORTED} suspends {@code @DataJpaTest}'s per-test transaction, so every store + * call commits in its own transaction as at runtime. That is what makes the stale-snapshot test + * meaningful: inside one shared persistence context, both reads would hand back the same managed + * instance and the bug could not show. + */ +@DataJpaTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class FileRunEventStoreDbTest { + + private static final Long TEAM = 7L; + private static final Long OTHER_TEAM = 9L; + + @Autowired private FileRunEventRepository repository; + + private FileRunEventStore store; + + @BeforeEach + void setUp() { + store = new FileRunEventStore(repository); + } + + @AfterEach + void wipe() { + repository.deleteAllInBatch(); + } + + private RecordFailure failure(FailureKind kind, Long teamId, String fileId) { + return failure(kind, teamId, "author@example.com", fileId); + } + + /** As {@link #failure} but naming the actor, which is what the read scope narrows by. */ + private RecordFailure failure(FailureKind kind, Long teamId, String actor, String fileId) { + return new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + actor, + "policy-1", + "run-1", + null, + fileId, + "detail"); + } + + @Test + @DisplayName("team isolation is enforced by the query, including the unteamed rows") + void teamIsolationIsEnforcedBySql() { + store.record(failure(FailureKind.UNKNOWN, TEAM, "ours")); + store.record(failure(FailureKind.UNKNOWN, OTHER_TEAM, "theirs")); + store.record(failure(FailureKind.UNKNOWN, null, "unteamed")); + + assertThat(store.list(TEAM, null, null, null, 10)) + .extracting(FileRunEvent::fileId) + .containsExactly("ours"); + // A plain `e.teamId = :teamId` would return nothing here: SQL equality against NULL is + // never true, which is what the explicit null branch in the JPQL exists for. + assertThat(store.list(null, null, null, null, 10)) + .extracting(FileRunEvent::fileId) + .containsExactly("unteamed"); + } + + @Test + @DisplayName("actor narrowing is enforced by the query, within the team") + void actorNarrowingIsEnforcedBySql() { + // The clause that makes a member read only their own rows. Exercised here rather than only + // against the in-memory repository, which reimplements the filter in Java and would agree + // with a query that had lost it. + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "mine@example.com", "f1")); + store.record( + failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "theirs@example.com", "f2")); + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, null, "f3")); + + assertThat(store.list(TEAM, null, null, "mine@example.com", 10)) + .extracting(FileRunEvent::fileId) + .containsExactly("f1"); + // A null actor is "no filter", which is what a leader reads with: the whole team, including + // the rows nobody is named on. + assertThat(store.list(TEAM, null, null, null, 10)) + .extracting(FileRunEvent::fileId) + .containsExactlyInAnyOrder("f1", "f2", "f3"); + } + + @Test + @DisplayName("a fold lands on the row's current state, not the caller's snapshot") + void foldTargetsTheCurrentRowNotACallersSnapshot() { + // The lost-update shape: record reads the row, a reviewer dismisses it, then the fold + // applies. A save() of the stale snapshot would merge status = NEW back over the dismiss. + FileRunEvent event = store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f")); + store.applyStatus(event.id(), TEAM, FileRunEventStatus.DISMISSED, "reviewer@example.com"); + + FileRunEvent folded = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f")); + + assertThat(folded.status()).isEqualTo(FileRunEventStatus.DISMISSED); + assertThat(folded.statusActor()).isEqualTo("reviewer@example.com"); + assertThat(folded.occurrences()).isEqualTo(2); + } + + @Test + @DisplayName("a dismiss landing mid-fold is not reverted by the folder's stale snapshot") + void aDismissLandingMidFoldIsNotReverted() { + // The reviewer's repro, made deterministic: the fold's read happens, then a dismiss lands, + // then the fold applies. The old save()-based absorb merged the pre-dismiss snapshot back + // over the row, reverting status to NEW and erasing the reviewer's action. + FileRunEvent event = store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f")); + AtomicBoolean interleaved = new AtomicBoolean(); + FileRunEventRepository interleaving = + (FileRunEventRepository) + Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {FileRunEventRepository.class}, + (proxy, method, args) -> { + Object result; + try { + result = method.invoke(repository, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + if ("findByTeamAndDedupKey".equals(method.getName()) + && interleaved.compareAndSet(false, true)) { + store.applyStatus( + event.id(), + TEAM, + FileRunEventStatus.DISMISSED, + "reviewer@example.com"); + } + return result; + }); + + FileRunEvent folded = + new FileRunEventStore(interleaving) + .record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f")); + + assertThat(folded.status()).isEqualTo(FileRunEventStatus.DISMISSED); + assertThat(folded.statusActor()).isEqualTo("reviewer@example.com"); + assertThat(folded.occurrences()).isEqualTo(2); + } + + @Test + @DisplayName("a row deleted between the dedup read and the fold is re-inserted, not lost") + void aVanishedRowIsReinsertedRatherThanLost() { + // Retention or a manual purge can delete the row a fold was about to land on. The fold + // then matches nothing, and treating that as an error would drop the incident entirely: + // the recorder swallows what record() throws, leaving the failure as only a log line. + FileRunEvent first = store.record(failure(FailureKind.UNKNOWN, TEAM, "f")); + AtomicBoolean deleted = new AtomicBoolean(); + FileRunEventRepository vanishing = + (FileRunEventRepository) + Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {FileRunEventRepository.class}, + (proxy, method, args) -> { + if ("fold".equals(method.getName()) + && deleted.compareAndSet(false, true)) { + repository.deleteById(first.id()); + } + try { + return method.invoke(repository, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + + FileRunEvent replacement = + new FileRunEventStore(vanishing).record(failure(FailureKind.UNKNOWN, TEAM, "f")); + + assertThat(replacement.id()).isNotEqualTo(first.id()); + assertThat(replacement.occurrences()).isEqualTo(1); + assertThat(store.list(TEAM, null, null, null, 10)).hasSize(1); + } + + @Test + @DisplayName("the same broken file folds across runs, and distinct files do not") + void repeatsFoldOnTheDocumentNotTheRun() { + // The point of the rollup. Each sweep starts a new run, so keying on the run id alone + // meant a file that failed every sweep opened a new incident every sweep, and several + // files failing in one run collapsed into one. Both are wrong; the document is the key. + RecordFailure firstSweep = locked("run-1", "file-hash-a"); + RecordFailure secondSweep = locked("run-2", "file-hash-a"); + RecordFailure otherFile = locked("run-1", "file-hash-b"); + + store.record(firstSweep); + store.record(otherFile); + FileRunEvent folded = store.record(secondSweep); + + assertThat(folded.occurrences()).isEqualTo(2); + assertThat(store.list(TEAM, null, null, null, 10)) + .as("one incident per document, however many runs it failed in") + .extracting(FileRunEvent::fileId) + .containsExactlyInAnyOrder("file-hash-a", "file-hash-b"); + } + + private RecordFailure locked(String runId, String fileId) { + return RecordFailure.forRun( + FailureKind.INPUT_PASSWORD_PROTECTED, + TEAM, + null, + null, + "policy-1", + runId, + fileId, + "locked"); + } + + @Test + @DisplayName("closing deleted files touches only that owner's own open editor rows") + void markFilesRemovedIsScopedBySqlNotByTheCaller() { + // The scoping is entirely in the JPQL, so the in-memory fake proves nothing about it: + // it implements the same rules by hand and would agree with a wrong query. + FileRunEvent mine = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, "owner@example.com", "f-1", "boom")); + FileRunEvent theirs = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, "colleague@example.com", "f-1", "boom")); + FileRunEvent otherTeam = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, + OTHER_TEAM, + "owner@example.com", + "f-1", + "boom")); + FileRunEvent fromProcessor = store.record(failure(FailureKind.UNKNOWN, TEAM, "f-1")); + + int closed = store.markFilesRemoved(TEAM, "owner@example.com", List.of("f-1")); + + assertThat(closed).isEqualTo(1); + assertThat(store.find(mine.id(), TEAM).orElseThrow().status()) + .isEqualTo(FileRunEventStatus.FILE_REMOVED); + assertThat(store.find(theirs.id(), TEAM).orElseThrow().status()) + .as("another person's incident about their own file") + .isEqualTo(FileRunEventStatus.NEW); + assertThat(store.find(otherTeam.id(), OTHER_TEAM).orElseThrow().status()) + .as("another team entirely") + .isEqualTo(FileRunEventStatus.NEW); + assertThat(store.find(fromProcessor.id(), TEAM).orElseThrow().status()) + .as("nothing was deleted from an editor here") + .isEqualTo(FileRunEventStatus.NEW); + } + + @Test + @DisplayName("a row already closed by a reviewer is left as they left it") + void markFilesRemovedLeavesClosedRowsAlone() { + FileRunEvent event = + store.record( + RecordFailure.forEditor( + FailureKind.UNKNOWN, TEAM, "owner@example.com", "f-1", "boom")); + store.applyStatus(event.id(), TEAM, FileRunEventStatus.DISMISSED, "reviewer@example.com"); + + assertThat(store.markFilesRemoved(TEAM, "owner@example.com", List.of("f-1"))).isZero(); + assertThat(store.find(event.id(), TEAM).orElseThrow().statusActor()) + .isEqualTo("reviewer@example.com"); + } + + @Test + @DisplayName("the kind filter applies before the limit, not after") + void kindFilterAppliesBeforeTheLimit() { + store.record(failure(FailureKind.UNKNOWN, TEAM, "old-unknown")); + for (int i = 0; i < 3; i++) { + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "newer-" + i)); + } + + assertThat(store.list(TEAM, null, "UNKNOWN", null, 1)) + .extracting(FileRunEvent::fileId) + .containsExactly("old-unknown"); + } + + @Test + @DisplayName("two racing closes resolve to one winner") + void twoClosesResolveToOneWinner() { + FileRunEvent event = store.record(failure(FailureKind.UNKNOWN, TEAM, "f")); + store.applyStatus(event.id(), TEAM, FileRunEventStatus.DISMISSED, "first@example.com"); + + // The second close passed the service's terminal() pre-check in its own request; the + // guarded UPDATE is what refuses it here. + assertThatThrownBy( + () -> + store.applyStatus( + event.id(), + TEAM, + FileRunEventStatus.DISMISSED, + "second@example.com")) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.ALREADY_CLOSED); + + assertThat(store.find(event.id(), TEAM).orElseThrow().statusActor()) + .isEqualTo("first@example.com"); + } + + @Test + @DisplayName("losing a delete race reports the row missing, not closed") + void aDeletedRowIsReportedMissingRatherThanClosed() { + // Both refusals come from the same guarded UPDATE returning zero rows, so the store has to + // tell them apart afterwards. Getting it wrong answers 400 for a row that is simply gone, + // where every other "no such event" answers 404. + FileRunEvent event = store.record(failure(FailureKind.UNKNOWN, TEAM, "f")); + repository.deleteById(event.id()); + + assertThatThrownBy( + () -> + store.applyStatus( + event.id(), + TEAM, + FileRunEventStatus.DISMISSED, + "reviewer@example.com")) + .isInstanceOf(FailureActionException.class) + .extracting(e -> ((FailureActionException) e).getReason()) + .isEqualTo(FailureActionException.Reason.EVENT_NOT_FOUND); + } + + @Test + @DisplayName("a reopened incident only flips from RESOLVED, never from DISMISSED") + void reopenFlipsOnlyResolved() { + FileRunEvent resolved = store.record(failure(FailureKind.UNKNOWN, TEAM, "resolved-file")); + store.applyStatus(resolved.id(), TEAM, FileRunEventStatus.RESOLVED, "system"); + + FileRunEvent reopened = store.record(failure(FailureKind.UNKNOWN, TEAM, "resolved-file")); + + assertThat(reopened.status()).isEqualTo(FileRunEventStatus.NEW); + assertThat(reopened.statusActor()).isNull(); + } + + @Test + @DisplayName("the unique constraint exists in the DDL, not only in the entity annotation") + void duplicateIncidentInsertTripsTheUniqueConstraint() { + store.record(failure(FailureKind.UNKNOWN, TEAM, "f")); + String dedupKey = failure(FailureKind.UNKNOWN, TEAM, "f").dedupKey(); + + FileRunEventEntity duplicate = new FileRunEventEntity(); + duplicate.setId(UUID.randomUUID().toString()); + duplicate.setTeamId(TEAM); + duplicate.setKindId(FailureKind.UNKNOWN.getId()); + duplicate.setStage(FailureStage.INTERNAL); + duplicate.setSeverity(FailureSeverity.ERROR); + duplicate.setScope(FailureScope.RUN); + duplicate.setOrigin(FailureOrigin.POLICY); + duplicate.setDedupKey(dedupKey); + duplicate.setOccurrences(1); + duplicate.setStatus(FileRunEventStatus.NEW); + duplicate.setCreatedAt(Instant.now()); + duplicate.setLastSeenAt(Instant.now()); + + assertThatThrownBy(() -> repository.saveAndFlush(duplicate)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @SpringBootConfiguration + @AutoConfigurationPackage + static class TestApp {} +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreTest.java new file mode 100644 index 0000000000..97de1952e9 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/FileRunEventStoreTest.java @@ -0,0 +1,370 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link FileRunEventStore}, driven through a hand-written in-memory repository so the + * rollup rules are tested directly and fast. The rollup is the load-bearing part: without it, + * Dismiss means nothing for a source that re-lists the same failing file on every sweep. + */ +class FileRunEventStoreTest { + + private static final Long TEAM = 7L; + private static final Long OTHER_TEAM = 9L; + + private InMemoryFileRunEventRepository repository; + private FileRunEventStore store; + + @BeforeEach + void setUp() { + repository = new InMemoryFileRunEventRepository(); + store = new FileRunEventStore(repository); + } + + private RecordFailure failure(FailureKind kind, Long teamId, String fileId, String detail) { + return new RecordFailure( + kind, + FailureOrigin.POLICY, + teamId, + "ethan@example.com", + "policy-1", + "run-1", + null, + fileId, + detail); + } + + @Nested + @DisplayName("recording a new failure") + class Insert { + + @Test + void persistsEveryFieldAndSnapshotsTheRegistryFacets() { + FileRunEvent event = + store.record( + failure( + FailureKind.INPUT_PASSWORD_PROTECTED, + TEAM, + "file-1", + "locked")); + + assertThat(event.id()).isNotBlank(); + assertThat(event.teamId()).isEqualTo(TEAM); + assertThat(event.actor()).isEqualTo("ethan@example.com"); + assertThat(event.kind()).isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + assertThat(event.policyId()).isEqualTo("policy-1"); + assertThat(event.runId()).isEqualTo("run-1"); + assertThat(event.fileId()).isEqualTo("file-1"); + assertThat(event.origin()).isEqualTo(FailureOrigin.POLICY); + assertThat(event.status()).isEqualTo(FileRunEventStatus.NEW); + assertThat(event.occurrences()).isEqualTo(1); + assertThat(event.createdAt()).isNotNull(); + assertThat(event.lastSeenAt()).isNotNull(); + + // Snapshotted, not derived: re-classifying a kind later must not rewrite this row. + assertThat(event.stage()).isEqualTo(FailureStage.INPUT); + assertThat(event.severity()).isEqualTo(FailureSeverity.ERROR); + assertThat(event.scope()).isEqualTo(FailureScope.FILE); + } + + @Test + void keepsTheRawDetailVerbatimForAnUnknownKind() { + // For UNKNOWN this string is the only diagnostic there is, so it has to reach the row + // unaltered. Redaction itself has its own tests. + String raw = "Policy run failed: java.lang.IllegalStateException: pool exhausted"; + FileRunEvent event = store.record(failure(FailureKind.UNKNOWN, TEAM, null, raw)); + + assertThat(event.detail()).isEqualTo(raw); + } + } + + @Nested + @DisplayName("the rollup") + class Rollup { + + @Test + void foldsARepeatIntoTheOpenIncidentInsteadOfInsertingAgain() { + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-1", "first")); + FileRunEvent second = + store.record( + failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-1", "again")); + + assertThat(second.occurrences()).isEqualTo(2); + assertThat(repository.rows).hasSize(1); + } + + @Test + void refreshesTheDetailSoTheNewestDiagnosticIsTheOneOnShow() { + store.record(failure(FailureKind.UNKNOWN, TEAM, "file-1", "old message")); + FileRunEvent second = + store.record(failure(FailureKind.UNKNOWN, TEAM, "file-1", "new message")); + + assertThat(second.detail()).isEqualTo("new message"); + } + + @Test + void treatsADifferentFileAsItsOwnIncident() { + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-1", "a")); + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-2", "b")); + + assertThat(repository.rows).hasSize(2); + } + + @Test + void treatsADifferentKindAsItsOwnIncident() { + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-1", "a")); + store.record(failure(FailureKind.UNKNOWN, TEAM, "file-1", "b")); + + assertThat(repository.rows).hasSize(2); + } + + @Test + void neverFoldsAcrossTeams() { + // Two tenants hitting the same problem are two incidents, each owned by its own team. + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "file-1", "a")); + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, OTHER_TEAM, "file-1", "b")); + + assertThat(repository.rows).hasSize(2); + } + + @Test + void absorbsARepeatOntoADismissedRowWithoutReopeningIt() { + // A snapshot-mode source re-lists every file each sweep, so without absorption a + // dismissed failure would reappear as a new row every time. + FileRunEvent first = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "a")); + store.applyStatus( + first.id(), TEAM, FileRunEventStatus.DISMISSED, "reviewer@example.com"); + + FileRunEvent repeat = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "b")); + + assertThat(repository.rows).hasSize(1); + assertThat(repeat.status()).isEqualTo(FileRunEventStatus.DISMISSED); + assertThat(repeat.occurrences()).isEqualTo(2); + assertThat(repeat.statusActor()).isEqualTo("reviewer@example.com"); + } + + @Test + void reopensAResolvedRowBecauseTheProblemHasComeBack() { + // The counterweight to absorption, so a recurrence still surfaces. + FileRunEvent first = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "a")); + store.applyStatus(first.id(), TEAM, FileRunEventStatus.RESOLVED, "system"); + + FileRunEvent repeat = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "b")); + + assertThat(repository.rows).hasSize(1); + assertThat(repeat.status()).isEqualTo(FileRunEventStatus.NEW); + assertThat(repeat.occurrences()).isEqualTo(2); + // The previous closer no longer owns a reopened incident. + assertThat(repeat.statusActor()).isNull(); + assertThat(repeat.statusAt()).isNull(); + } + + @Test + void keepsAnAcknowledgedRowAcknowledgedOnRepeat() { + FileRunEvent first = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "a")); + store.applyStatus(first.id(), TEAM, FileRunEventStatus.ACKNOWLEDGED, "owner"); + + FileRunEvent repeat = + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "b")); + + assertThat(repeat.status()).isEqualTo(FileRunEventStatus.ACKNOWLEDGED); + assertThat(repeat.statusActor()).isEqualTo("owner"); + } + + @Test + void aSweepWhereEveryFileFailsProducesARowPerFile() { + // Each resolved input gets its own run, so a 400-file sweep is 400 incidents rather + // than one with a counter of 400. Grouping for display is a read-time concern. + for (int i = 0; i < 50; i++) { + store.record( + new RecordFailure( + FailureKind.UNKNOWN, + FailureOrigin.POLICY, + TEAM, + "ethan@example.com", + "policy-1", + "run-" + i, + null, + "file-" + i, + "OCR unavailable")); + } + + assertThat(repository.rows).hasSize(50); + assertThat(repository.rows.values()).allMatch(row -> row.getOccurrences() == 1); + } + + @Test + void treatsEachFileAsItsOwnIncidentEvenWhenNoFileIdWasCaptured() { + // The producer path cannot always name a file, and a FILE-scoped kind then falls back + // to + // the run. Without that, every locked file under one policy would fold into one row. + store.record(withoutFileId("run-a")); + store.record(withoutFileId("run-b")); + + assertThat(repository.rows).hasSize(2); + assertThat(repository.rows.values()).allMatch(row -> row.getOccurrences() == 1); + } + + @Test + void aRetryOfTheSameRunStillFoldsWhenNoFileIdWasCaptured() { + // The flip side: the fallback must not turn a genuine repeat into a second row. + store.record(withoutFileId("run-a")); + store.record(withoutFileId("run-a")); + + assertThat(repository.rows).hasSize(1); + assertThat(repository.rows.values().iterator().next().getOccurrences()).isEqualTo(2); + } + + @Test + void foldsIntoTheWinnersRowWhenAConcurrentWriterInsertedFirst() { + // Two runs failing identically at once: both read, find nothing, and try to insert. The + // loser's insert trips the unique constraint and has to fold rather than fail. + RecordFailure command = failure(FailureKind.UNKNOWN, TEAM, "file-1", "OCR unavailable"); + FileRunEventEntity winner = rowFor(command); + repository.beforeNextSave = () -> repository.rows.put(winner.getId(), winner); + + FileRunEvent result = store.record(command); + + assertThat(repository.rows).hasSize(1); + assertThat(result.id()).isEqualTo(winner.getId()); + assertThat(result.occurrences()).isEqualTo(2); + } + + /** A row as the winning writer would have inserted it, for the race above. */ + private FileRunEventEntity rowFor(RecordFailure command) { + FileRunEventEntity entity = new FileRunEventEntity(); + entity.setId("winner"); + entity.setTeamId(command.teamId()); + entity.setKindId(command.kind().getId()); + entity.setStage(command.kind().getStage()); + entity.setSeverity(command.kind().getSeverity()); + entity.setScope(command.kind().getScope()); + entity.setOrigin(command.origin()); + entity.setDedupKey(command.dedupKey()); + entity.setOccurrences(1); + entity.setStatus(FileRunEventStatus.NEW); + entity.setCreatedAt(java.time.Instant.now()); + entity.setLastSeenAt(java.time.Instant.now()); + return entity; + } + + private RecordFailure withoutFileId(String runId) { + return new RecordFailure( + FailureKind.INPUT_PASSWORD_PROTECTED, + FailureOrigin.POLICY, + TEAM, + "ethan@example.com", + "policy-1", + runId, + null, + null, + "The PDF Document is passworded"); + } + } + + @Nested + @DisplayName("reading") + class Reading { + + @Test + void neverReturnsAnotherTeamsRows() { + store.record(failure(FailureKind.UNKNOWN, TEAM, "mine", "a")); + store.record(failure(FailureKind.UNKNOWN, OTHER_TEAM, "theirs", "b")); + + assertThat(store.list(TEAM, null, null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("mine"); + } + + @Test + void treatsANullTeamAsTheUnteamedRows() { + // Login-disabled deployments have no team; a plain `= null` comparison would return + // nothing, which is why the query special-cases it. + store.record(failure(FailureKind.UNKNOWN, null, "unteamed", "a")); + store.record(failure(FailureKind.UNKNOWN, TEAM, "teamed", "b")); + + assertThat(store.list(null, null, null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("unteamed"); + } + + @Test + void dismissingARowClearsItFromTheDefaultQueue() { + // The reviewer's whole complaint: without this, dismissing changes the buttons and + // leaves the row sitting there, so the list can only ever grow. + FileRunEvent event = store.record(failure(FailureKind.UNKNOWN, TEAM, "f1", "boom")); + store.applyStatus(event.id(), TEAM, FileRunEventStatus.DISMISSED, "reviewer"); + + assertThat(store.list(TEAM, null, null, null, 10)).isEmpty(); + assertThat(store.list(TEAM, FileRunEventStatus.DISMISSED, null, null, 10)).hasSize(1); + } + + @Test + void anAcknowledgedRowIsStillOpenWorkSoItStays() { + FileRunEvent event = store.record(failure(FailureKind.UNKNOWN, TEAM, "f1", "boom")); + store.applyStatus(event.id(), TEAM, FileRunEventStatus.ACKNOWLEDGED, "reviewer"); + + assertThat(store.list(TEAM, null, null, null, 10)).hasSize(1); + } + + @Test + void filtersByStatus() { + FileRunEvent open = store.record(failure(FailureKind.UNKNOWN, TEAM, "open", "a")); + store.record(failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "closed", "b")); + store.applyStatus(open.id(), TEAM, FileRunEventStatus.ACKNOWLEDGED, "me"); + + assertThat(store.list(TEAM, FileRunEventStatus.ACKNOWLEDGED, null, null, 50)) + .extracting(FileRunEvent::fileId) + .containsExactly("open"); + } + + @Test + void findScopesByTeamSoAnIdFromAnotherTeamIsInvisible() { + FileRunEvent theirs = store.record(failure(FailureKind.UNKNOWN, OTHER_TEAM, "x", "a")); + + assertThat(store.find(theirs.id(), TEAM)).isEmpty(); + assertThat(store.find(theirs.id(), OTHER_TEAM)).isPresent(); + } + } + + @Nested + @DisplayName("dedup keys") + class Keys { + + @Test + void areDeterministicForTheSameFailure() { + RecordFailure command = failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f", "a"); + + assertThat(command.dedupKey()).isEqualTo(command.dedupKey()); + } + + @Test + void differByKindAndByScopeReference() { + RecordFailure a = failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f1", "x"); + RecordFailure b = failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, "f2", "x"); + RecordFailure c = failure(FailureKind.UNKNOWN, TEAM, "f1", "x"); + + assertThat(a.dedupKey()).isNotEqualTo(b.dedupKey()); + assertThat(a.dedupKey()).isNotEqualTo(c.dedupKey()); + } + + @Test + void areFixedWidthSoAnyLengthOfScopeReferenceFitsTheIndex() { + String longKey = "s3://bucket/" + "nested/".repeat(500) + "file.pdf"; + RecordFailure command = + failure(FailureKind.INPUT_PASSWORD_PROTECTED, TEAM, longKey, "x"); + + assertThat(command.dedupKey()).hasSize(64); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/InMemoryFileRunEventRepository.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/InMemoryFileRunEventRepository.java new file mode 100644 index 0000000000..6dbb16322b --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/InMemoryFileRunEventRepository.java @@ -0,0 +1,362 @@ +package stirling.software.proprietary.failure; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.springframework.data.domain.Pageable; + +/** + * Minimal stand-in for the JPA repository: enough to exercise the store's branching without a + * database. Ordering mirrors the real queries (newest first) since the rollup relies on it, and + * {@link #save} enforces the {@code (team_id, dedup_key)} unique constraint the real table + * declares. + */ +class InMemoryFileRunEventRepository implements FileRunEventRepository { + + final Map rows = new HashMap<>(); + + /** + * Runs once at the start of the next {@link #save}, so a test can interleave a competing writer + * between the store's read and its insert. + */ + Runnable beforeNextSave; + + private static boolean sameTeam(FileRunEventEntity entity, Long teamId) { + return teamId == null ? entity.getTeamId() == null : teamId.equals(entity.getTeamId()); + } + + private List newestFirst(List matches) { + List sorted = new ArrayList<>(matches); + sorted.sort( + (a, b) -> { + int byTime = b.getLastSeenAt().compareTo(a.getLastSeenAt()); + // Stable tiebreak: identical timestamps are common in fast tests. + return byTime != 0 ? byTime : a.getId().compareTo(b.getId()); + }); + return sorted; + } + + /** The real queries page in SQL, so the fake must page too or limit tests test nothing. */ + private static List page(List rows, Pageable pageable) { + return pageable == null || pageable.isUnpaged() || rows.size() <= pageable.getPageSize() + ? rows + : rows.subList(0, pageable.getPageSize()); + } + + private static boolean sameKind(FileRunEventEntity entity, String kindId) { + return kindId == null || kindId.equals(entity.getKindId()); + } + + /** Null means the whole team, matching the JPQL's {@code :actor is null} branch. */ + private static boolean sameActor(FileRunEventEntity entity, String actor) { + return actor == null || actor.equals(entity.getActor()); + } + + @Override + public List findByTeamAndStatus( + Long teamId, + FileRunEventStatus status, + String kindId, + String actor, + Pageable pageable) { + return page( + newestFirst( + rows.values().stream() + .filter( + e -> + sameTeam(e, teamId) + && e.getStatus() == status + && sameKind(e, kindId) + && sameActor(e, actor)) + .toList()), + pageable); + } + + @Override + public int fold(String id, Instant now, String detail) { + FileRunEventEntity entity = rows.get(id); + if (entity == null) { + return 0; + } + entity.setOccurrences(entity.getOccurrences() + 1); + entity.setLastSeenAt(now); + if (detail != null) { + entity.setDetail(detail); + } + return 1; + } + + @Override + public int reopenIfResolved(String id) { + FileRunEventEntity entity = rows.get(id); + if (entity == null || entity.getStatus() != FileRunEventStatus.RESOLVED) { + return 0; + } + entity.setStatus(FileRunEventStatus.NEW); + entity.setStatusActor(null); + entity.setStatusAt(null); + return 1; + } + + @Override + public int applyStatusIf( + String id, + Long teamId, + FileRunEventStatus target, + String actor, + Instant now, + Collection allowedFrom) { + FileRunEventEntity entity = rows.get(id); + if (entity == null + || !sameTeam(entity, teamId) + || !allowedFrom.contains(entity.getStatus())) { + return 0; + } + entity.setStatus(target); + entity.setStatusActor(actor); + entity.setStatusAt(now); + return 1; + } + + @Override + public int markFilesRemoved( + Long teamId, + String actor, + Collection fileIds, + Instant now, + Collection allowedFrom) { + int closed = 0; + for (FileRunEventEntity entity : rows.values()) { + if (entity.getOrigin() != FailureOrigin.TOOL + || !sameTeam(entity, teamId) + || !Objects.equals(entity.getActor(), actor) + || entity.getFileId() == null + || !fileIds.contains(entity.getFileId()) + || !allowedFrom.contains(entity.getStatus())) { + continue; + } + entity.setStatus(FileRunEventStatus.FILE_REMOVED); + entity.setStatusActor(actor); + entity.setStatusAt(now); + closed++; + } + return closed; + } + + @Override + public List findByTeamAndStatusIn( + Long teamId, + List statuses, + String kindId, + String actor, + Pageable pageable) { + return page( + newestFirst( + rows.values().stream() + .filter( + e -> + sameTeam(e, teamId) + && statuses.contains(e.getStatus()) + && sameKind(e, kindId) + && sameActor(e, actor)) + .toList()), + pageable); + } + + @Override + public List findByTeamAndDedupKey( + Long teamId, String dedupKey, Pageable pageable) { + return newestFirst( + rows.values().stream() + .filter(e -> sameTeam(e, teamId) && dedupKey.equals(e.getDedupKey())) + .toList()); + } + + @Override + public Optional findByIdAndTeam(String id, Long teamId) { + return Optional.ofNullable(rows.get(id)).filter(e -> sameTeam(e, teamId)); + } + + @Override + public S save(S entity) { + if (beforeNextSave != null) { + Runnable hook = beforeNextSave; + beforeNextSave = null; + hook.run(); + } + boolean isInsert = !rows.containsKey(entity.getId()); + if (isInsert && clashesOnDedupKey(entity)) { + throw new org.springframework.dao.DataIntegrityViolationException( + "uk_file_run_events_dedup violated for " + entity.getDedupKey()); + } + rows.put(entity.getId(), entity); + return entity; + } + + /** Mirrors the unique constraint. SQL treats NULL teams as distinct, and so does this. */ + private boolean clashesOnDedupKey(FileRunEventEntity candidate) { + if (candidate.getTeamId() == null) { + return false; + } + return rows.values().stream() + .anyMatch( + row -> + candidate.getTeamId().equals(row.getTeamId()) + && candidate.getDedupKey().equals(row.getDedupKey())); + } + + // ── unused JpaRepository surface ──────────────────────────────────────── + @Override + public void flush() {} + + @Override + public S saveAndFlush(S entity) { + return save(entity); + } + + @Override + public List saveAllAndFlush(Iterable entities) { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteAllInBatch(Iterable entities) {} + + @Override + public void deleteAllByIdInBatch(Iterable ids) {} + + @Override + public void deleteAllInBatch() {} + + @Override + public FileRunEventEntity getOne(String id) { + throw new UnsupportedOperationException(); + } + + @Override + public FileRunEventEntity getById(String id) { + throw new UnsupportedOperationException(); + } + + @Override + public FileRunEventEntity getReferenceById(String id) { + throw new UnsupportedOperationException(); + } + + @Override + public List findAll( + org.springframework.data.domain.Example example) { + throw new UnsupportedOperationException(); + } + + @Override + public List findAll( + org.springframework.data.domain.Example example, + org.springframework.data.domain.Sort sort) { + throw new UnsupportedOperationException(); + } + + @Override + public List saveAll(Iterable entities) { + throw new UnsupportedOperationException(); + } + + @Override + public List findAll() { + return List.copyOf(rows.values()); + } + + @Override + public List findAllById(Iterable ids) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(rows.get(id)); + } + + @Override + public boolean existsById(String id) { + return rows.containsKey(id); + } + + @Override + public long count() { + return rows.size(); + } + + @Override + public void deleteById(String id) { + rows.remove(id); + } + + @Override + public void delete(FileRunEventEntity entity) { + rows.remove(entity.getId()); + } + + @Override + public void deleteAllById(Iterable ids) {} + + @Override + public void deleteAll(Iterable entities) {} + + @Override + public void deleteAll() { + rows.clear(); + } + + @Override + public List findAll(org.springframework.data.domain.Sort sort) { + throw new UnsupportedOperationException(); + } + + @Override + public org.springframework.data.domain.Page findAll(Pageable pageable) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional findOne( + org.springframework.data.domain.Example example) { + throw new UnsupportedOperationException(); + } + + @Override + public org.springframework.data.domain.Page findAll( + org.springframework.data.domain.Example example, Pageable pageable) { + throw new UnsupportedOperationException(); + } + + @Override + public long count( + org.springframework.data.domain.Example example) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean exists( + org.springframework.data.domain.Example example) { + throw new UnsupportedOperationException(); + } + + @Override + public R findBy( + org.springframework.data.domain.Example example, + Function< + org.springframework.data.repository.query.FluentQuery + .FetchableFluentQuery< + S>, + R> + queryFunction) { + throw new UnsupportedOperationException(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureAttributionTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureAttributionTest.java new file mode 100644 index 0000000000..2a71c236a7 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureAttributionTest.java @@ -0,0 +1,264 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.MDC; +import org.springframework.core.io.ByteArrayResource; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.FileStorage; +import stirling.software.common.service.InternalApiClient; +import stirling.software.common.service.JobOwnershipService; +import stirling.software.common.service.JobQueue; +import stirling.software.common.service.ResourceMonitor; +import stirling.software.common.service.TaskManager; +import stirling.software.common.service.ToolMetadataService; +import stirling.software.common.service.UserServiceInterface; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.TempFileRegistry; +import stirling.software.proprietary.policy.asset.InProcessPolicyAssetStore; +import stirling.software.proprietary.policy.asset.PolicyAssetResolver; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; +import stirling.software.proprietary.policy.engine.PolicyEngine; +import stirling.software.proprietary.policy.engine.PolicyExecutor; +import stirling.software.proprietary.policy.engine.PolicyRunRegistry; +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyInputs; +import stirling.software.proprietary.policy.output.InlineOutputSink; +import stirling.software.proprietary.policy.output.PolicyOutputResolver; +import stirling.software.proprietary.policy.progress.PolicyProgressListener; +import stirling.software.proprietary.policy.source.InProcessSourceStore; +import stirling.software.proprietary.policy.store.PolicyStore; + +import tools.jackson.databind.json.JsonMapper; + +/** + * Pins what the engine records as a failure's actor against what a reader gets back, because the + * two sides used to assert independently: the engine's test passed {@code any()} for the actor, and + * the service's fixtures assumed an actor the engine never actually produced. Every collaborator + * between the failing tool call and the read is real here, so a regression in either one fails. + * + *

This is what makes this PR's promise hold. Reads are narrowed to the rows the caller is the + * actor on, so if the engine names the wrong person, a member reads nothing at all. + * + *

The bug it exists for: the engine recorded the BILLING principal as the actor, which for a + * stored policy is always its owner. So an attended failure was filed under someone who never + * touched the document, and the member who did could not see it. + */ +@ExtendWith(MockitoExtension.class) +class PolicyFailureAttributionTest { + + private static final String ROTATE = "/api/v1/general/rotate-pdf"; + private static final Long TEAM = 3L; + + @Mock private InternalApiClient internalApiClient; + @Mock private ToolMetadataService toolMetadataService; + @Mock private TaskManager taskManager; + @Mock private FileStorage fileStorage; + @Mock private JobOwnershipService jobOwnershipService; + @Mock private ResourceMonitor resourceMonitor; + @Mock private JobQueue jobQueue; + @Mock private PolicyStore policyStore; + @Mock private PolicyManagementAuthority authority; + @Mock private UserServiceInterface userService; + + @TempDir Path tempDir; + + private PolicyEngine engine; + private FileRunEventService service; + + @BeforeEach + void setUp() { + ApplicationProperties props = new ApplicationProperties(); + props.getSecurity().setEnableLogin(true); + props.getSystem().getTempFileManagement().setBaseTmpDir(tempDir.toString()); + props.getSystem().getTempFileManagement().setPrefix("failure-attribution-test-"); + + FileRunEventStore store = new FileRunEventStore(new InMemoryFileRunEventRepository()); + service = + new FileRunEventService( + store, + new FailureActionRegistry( + List.of(new AcknowledgeAction(store), new DismissAction(store))), + authority, + userService, + props); + + PolicyFailureRecorder recorder = + new PolicyFailureRecorder( + new FailureClassifier(JsonMapper.builder().build()), store, policyStore); + PolicyExecutor executor = + new PolicyExecutor( + internalApiClient, + toolMetadataService, + new TempFileManager(new TempFileRegistry(), props), + JsonMapper.builder().build()); + engine = + new PolicyEngine( + executor, + taskManager, + new PolicyRunRegistry(new ApplicationProperties()), + recorder, + fileStorage, + jobOwnershipService, + List.of(new InlineOutputSink(fileStorage)), + new PolicyOutputResolver(new InProcessSourceStore()), + resourceMonitor, + jobQueue, + new PolicyAssetResolver(new InProcessPolicyAssetStore())); + + lenient() + .when(jobOwnershipService.createScopedJobKey(anyString())) + .thenAnswer(invocation -> invocation.getArgument(0)); + lenient().when(resourceMonitor.shouldQueueJob(anyInt())).thenReturn(false); + lenient().when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + // The team is resolved from the policy, so the recorded row lands in the reader's team. + lenient().when(policyStore.get(anyString())).thenReturn(Optional.of(sharedPolicy())); + lenient().when(authority.currentUserTeamId()).thenReturn(TEAM); + } + + /** Alice's policy, shared with her team. Bob is a member of it and does not own it. */ + private static Policy sharedPolicy() { + return new Policy( + "p1", + "rotate", + "alice", + true, + List.of(), + List.of(new PipelineStep(ROTATE, Map.of())), + OutputSpec.inline(), + TEAM); + } + + /** + * Run the shared policy so its single tool step fails, as {@code triggeredBy} (null = sweep). + */ + private void runAndFail(String triggeredBy, String sourceId, String fileIdentity) + throws Exception { + when(internalApiClient.post(eq(ROTATE), any())).thenThrow(new RuntimeException("boom")); + if (triggeredBy != null) { + MDC.put("auditPrincipal", triggeredBy); + } + try { + engine.runPolicy( + sharedPolicy(), + PolicyInputs.of(List.of(pdf())), + PolicyProgressListener.NOOP, + sourceId, + fileIdentity) + .completion() + .get(10, TimeUnit.SECONDS); + } finally { + MDC.remove("auditPrincipal"); + } + } + + private static ByteArrayResource pdf() { + return new ByteArrayResource("input".getBytes()) { + @Override + public String getFilename() { + return "input.pdf"; + } + }; + } + + /** Read as a plain member, who is narrowed to the rows they are the actor on. */ + private FileRunEvent asMember(String reader) { + lenient().when(userService.getCurrentUsername()).thenReturn(reader); + lenient().when(authority.canEditPolicies()).thenReturn(false); + List visible = service.list(null, null, 10); + return visible.isEmpty() ? null : visible.getFirst(); + } + + /** Read as a team leader, who reviews the whole team's incidents. */ + private FileRunEvent asReviewer(String reader) { + lenient().when(userService.getCurrentUsername()).thenReturn(reader); + lenient().when(authority.canEditPolicies()).thenReturn(true); + return service.list(null, null, 10).getFirst(); + } + + @Nested + @DisplayName("a non-owner runs a shared policy on their own upload") + class AttendedByANonOwner { + + @Test + void theTriggeringUserCanReadTheFailureTheyCaused() throws Exception { + runAndFail("bob", null, "bob-doc-1"); + + // The whole point: Bob's read scope narrows to his own rows, so the row only reaches + // him if the engine named him. Before the fix this list was empty. + FileRunEvent mine = asMember("bob"); + assertThat(mine).as("bob must be able to see the failure he caused").isNotNull(); + assertThat(mine.actor()).isEqualTo("bob"); + } + + @Test + void thePolicyOwnerIsNotNamedAsTheActorMerelyForBeingBilled() throws Exception { + runAndFail("bob", null, "bob-doc-1"); + + // Alice owns the policy and pays for the run, but she never touched the document. + assertThat(asReviewer("alice").actor()).isEqualTo("bob"); + } + + @Test + void aColleagueWhoDidNotTriggerItCannotSeeItAtAll() throws Exception { + runAndFail("bob", null, "bob-doc-1"); + + assertThat(asMember("carol")).isNull(); + } + } + + @Nested + @DisplayName("an unattended sweep pulls a file from a source") + class UnattendedSweep { + + @Test + void theRowIsRecordedWithNoActorWhileStillBillingTheOwner() throws Exception { + runAndFail(null, "src-watched-folder", "file-hash-1"); + + assertThat(asReviewer("alice").actor()) + .as("a trigger-fired run has no user to name") + .isNull(); + } + + @Test + void theSourceThatFedItIsStillRecorded() throws Exception { + runAndFail(null, "src-watched-folder", "file-hash-1"); + + FileRunEvent unattended = asReviewer("alice"); + assertThat(unattended.sourceId()).isEqualTo("src-watched-folder"); + assertThat(unattended.fileId()).isEqualTo("file-hash-1"); + } + + @Test + void aMemberDoesNotInheritAnUnattendedFailureAsTheirOwn() throws Exception { + // An unowned row must not fall to whoever happens to be reading: with no actor there is + // nothing for a member's narrowed read to match. + runAndFail(null, "src-watched-folder", "file-hash-1"); + + assertThat(asMember("bob")).isNull(); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureRecorderTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureRecorderTest.java new file mode 100644 index 0000000000..73c5add6ad --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/PolicyFailureRecorderTest.java @@ -0,0 +1,383 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.HttpClientErrorException; + +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.store.PolicyStore; + +import tools.jackson.databind.json.JsonMapper; + +/** + * Tests for {@link PolicyFailureRecorder}. Two behaviours beyond "it writes a row": the team comes + * from the originating policy, and a store problem is swallowed rather than replacing the failure + * being recorded. + */ +@ExtendWith(MockitoExtension.class) +class PolicyFailureRecorderTest { + + private static final Long TEAM = 11L; + + @Mock private PolicyStore policyStore; + + private InMemoryFileRunEventRepository repository; + private FileRunEventStore store; + private PolicyFailureRecorder recorder; + + @BeforeEach + void setUp() { + repository = new InMemoryFileRunEventRepository(); + store = new FileRunEventStore(repository); + recorder = + new PolicyFailureRecorder( + new FailureClassifier(JsonMapper.builder().build()), store, policyStore); + } + + private static Policy policy(String id, Long teamId) { + return new Policy( + id, + "Contract redaction", + "owner@example.com", + true, + List.of(), + List.of(), + OutputSpec.inline(), + List.of(), + teamId); + } + + private static HttpClientErrorException passwordFailure() { + String body = "{\"type\":\"/errors/pdf-password\",\"errorCode\":\"E004\"}"; + return HttpClientErrorException.create( + HttpStatus.BAD_REQUEST, + "Bad Request", + new HttpHeaders(), + body.getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8); + } + + @Nested + @DisplayName("classifying and recording") + class Recording { + + @Test + void classifiesTheCauseAndStoresTheRunsOwnMessage() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + "dana@example.com", + "Policy run failed: locked", + passwordFailure()); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.kind()).isEqualTo(FailureKind.INPUT_PASSWORD_PROTECTED); + assertThat(event.runId()).isEqualTo("run-1"); + assertThat(event.policyId()).isEqualTo("policy-1"); + assertThat(event.actor()).isEqualTo("dana@example.com"); + assertThat(event.origin()).isEqualTo(FailureOrigin.POLICY); + // The run's message, not the exception's: that is what the operator saw. Stored + // verbatim, including for a kind we recognise, so the row says what actually happened. + assertThat(event.detail()).isEqualTo("Policy run failed: locked"); + } + + @Test + void keepsTheMessageForAFailureItCouldNotClassify() { + // The opposite case, and the reason the message is stored at all: with no kind to + // describe it, this text is the only thing telling a reviewer what went wrong. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + "dana@example.com", + "Policy run failed: something we do not recognise", + new RuntimeException("boom")); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.kind()).isEqualTo(FailureKind.UNKNOWN); + assertThat(event.detail()).contains("something we do not recognise"); + } + + @Test + void recordsAnUnclassifiableFailureAsUnknownRatherThanDroppingIt() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-2", + "policy-1", + null, + null, + null, + "Policy run failed: java.lang.NullPointerException", + new RuntimeException("npe")); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.kind()).isEqualTo(FailureKind.UNKNOWN); + assertThat(event.detail()).contains("NullPointerException"); + } + + @Test + void acceptsAPreDecidedKindForPathsWithNothingToClassify() { + // A run rejected at admission threw nothing; the outcome is still known. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailureAs( + FailureKind.UNKNOWN, "run-3", "policy-1", null, null, "could not be queued"); + + assertThat(store.list(TEAM, null, null, null, 10)).hasSize(1); + } + + @Test + void twoFilesFailingTheSameWayUnderOnePolicyAreTwoIncidents() { + // The producer records per run and cannot name a file, so a FILE-scoped kind has only + // the + // run to tell two documents apart. Tested here rather than against a hand-built + // RecordFailure, because that is what let three locked files collapse into one row. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + "dana@example.com", + "locked", + passwordFailure()); + recorder.recordRunFailure( + "run-2", + "policy-1", + null, + null, + "dana@example.com", + "locked", + passwordFailure()); + + List events = store.list(TEAM, null, null, null, 10); + assertThat(events).hasSize(2); + assertThat(events).allMatch(event -> event.occurrences() == 1); + assertThat(events) + .extracting(FileRunEvent::runId) + .containsExactlyInAnyOrder("run-1", "run-2"); + } + + @Test + void namesTheSourceWhenNoUserWasInvolved() { + // An unattended file has no actor, so the source is the only attribution a reviewer + // gets: which bucket, folder or webhook fed the run. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", + "policy-1", + "src-s3-invoices", + null, + null, + "locked", + passwordFailure()); + + FileRunEvent event = store.list(TEAM, null, null, null, 10).getFirst(); + assertThat(event.sourceId()).isEqualTo("src-s3-invoices"); + assertThat(event.actor()).isNull(); + } + + @Test + void keepsTwoSourcesApartEvenWhenTheyFailIdentically() { + // Same kind, same policy, no user on either: without the source they would be one row. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailureAs( + FailureKind.UNKNOWN, "run-1", "policy-1", "src-a", null, "unreachable"); + recorder.recordRunFailureAs( + FailureKind.UNKNOWN, "run-2", "policy-1", "src-b", null, "unreachable"); + + assertThat(store.list(TEAM, null, null, null, 10)) + .hasSize(2) + .extracting(FileRunEvent::sourceId) + .containsExactlyInAnyOrder("src-a", "src-b"); + } + + @Test + void thatSameRunFailingTwiceStaysOneIncident() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + "dana@example.com", + "locked", + passwordFailure()); + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + "dana@example.com", + "locked", + passwordFailure()); + + assertThat(store.list(TEAM, null, null, null, 10)) + .singleElement() + .extracting(FileRunEvent::occurrences) + .isEqualTo(2); + } + } + + @Nested + @DisplayName("team resolution") + class TeamResolution { + + @Test + void takesTheTeamFromTheOriginatingPolicy() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", "policy-1", null, null, null, "boom", new RuntimeException()); + + assertThat(store.list(TEAM, null, null, null, 10)).hasSize(1); + } + + @Test + void leavesAnAdHocRunUnteamedRatherThanGuessing() { + // No stored policy means no team to attribute it to. Recorded unteamed rather than + // attributed to whichever team happened to be nearby. + recorder.recordRunFailure( + "run-1", null, null, null, null, "boom", new RuntimeException()); + + assertThat(store.list(null, null, null, null, 10)).hasSize(1); + assertThat(store.list(TEAM, null, null, null, 10)).isEmpty(); + } + + @Test + void survivesAPolicyLookupFailure() { + when(policyStore.get(anyString())).thenThrow(new RuntimeException("db down")); + + assertThatCode( + () -> + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + null, + "boom", + new RuntimeException())) + .doesNotThrowAnyException(); + // Still recorded, just unteamed: a lookup problem must not lose the incident. + assertThat(store.list(null, null, null, null, 10)).hasSize(1); + } + } + + @Nested + @DisplayName("recording is best effort") + class BestEffort { + + @Test + void aStoreFailureIsSwallowedSoItCannotReplaceTheRealFailure() { + // If this threw, the engine's catch block would report a store error instead of the + // password problem that actually stopped the run. + FileRunEventStore broken = mock(FileRunEventStore.class); + when(broken.record(any())).thenThrow(new RuntimeException("event store unavailable")); + lenient() + .when(policyStore.get("policy-1")) + .thenReturn(Optional.of(policy("policy-1", TEAM))); + PolicyFailureRecorder fragile = + new PolicyFailureRecorder( + new FailureClassifier(JsonMapper.builder().build()), + broken, + policyStore); + + assertThatCode( + () -> + fragile.recordRunFailure( + "run-1", + "policy-1", + null, + null, + null, + "Policy run failed: locked", + passwordFailure())) + .doesNotThrowAnyException(); + } + + @Test + void aNullCauseDoesNotBreakRecording() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + assertThatCode( + () -> + recorder.recordRunFailure( + "run-1", + "policy-1", + null, + null, + null, + "no cause", + null)) + .doesNotThrowAnyException(); + assertThat(store.list(TEAM, null, null, null, 10).getFirst().kind()) + .isEqualTo(FailureKind.UNKNOWN); + } + } + + @Nested + @DisplayName("repeats") + class Repeats { + + @Test + void twoIdenticalRunFailuresFoldIntoOneIncident() { + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", "policy-1", null, null, null, "boom", new IOException("x")); + recorder.recordRunFailure( + "run-1", "policy-1", null, null, null, "boom", new IOException("x")); + + List events = store.list(TEAM, null, null, null, 10); + assertThat(events).hasSize(1); + assertThat(events.getFirst().occurrences()).isEqualTo(2); + } + + @Test + void twoDifferentRunsAreSeparateIncidents() { + // UNKNOWN is RUN-scoped, so the run id is what distinguishes them. + when(policyStore.get("policy-1")).thenReturn(Optional.of(policy("policy-1", TEAM))); + + recorder.recordRunFailure( + "run-1", "policy-1", null, null, null, "boom", new IOException("x")); + recorder.recordRunFailure( + "run-2", "policy-1", null, null, null, "boom", new IOException("x")); + + assertThat(store.list(TEAM, null, null, null, 10)).hasSize(2); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/failure/RecordFailurePrivacyTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/failure/RecordFailurePrivacyTest.java new file mode 100644 index 0000000000..86c52c4007 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/failure/RecordFailurePrivacyTest.java @@ -0,0 +1,117 @@ +package stirling.software.proprietary.failure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Field; +import java.lang.reflect.RecordComponent; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * The privacy contract: a recorded failure carries no document identity of its own. There is no + * name column and the dedup key is built only from opaque ids, so nothing here derives from what a + * document is called. + * + *

The message itself is stored verbatim. It is the user's own error about their own file, and + * scrubbing it made rows harder to act on without making them meaningfully safer. + */ +@DisplayName("a recorded failure carries no document identity of its own") +class RecordFailurePrivacyTest { + + private static RecordFailure withDetail(String detail) { + return RecordFailure.forRun( + FailureKind.UNKNOWN, + 1L, + "dana@example.com", + "policy-1", + "run-1", + null, + null, + detail); + } + + @Test + void toleratesAnAbsentMessage() { + assertThat(withDetail(null).detail()).isNull(); + } + + @Test + void hasNoFileNameFieldAtAll() { + // Structural, not behavioural: if a fileName component is ever added back, this fails. + assertThat(List.of(RecordFailure.class.getRecordComponents())) + .extracting(RecordComponent::getName) + .doesNotContain("fileName"); + assertThat(List.of(FileRunEventEntity.class.getDeclaredFields())) + .extracting(Field::getName) + .doesNotContain("fileName"); + assertThat(List.of(FileRunEventView.class.getRecordComponents())) + .extracting(RecordComponent::getName) + .doesNotContain("fileName"); + } + + @Test + void dedupKeyIsBuiltOnlyFromOpaqueIdentifiers() { + // Two files under the same policy hash differently (so they stay separate incidents), but + // the inputs are ids, so the hash is not reversible to a document name. + RecordFailure a = + new RecordFailure( + FailureKind.INPUT_PASSWORD_PROTECTED, + FailureOrigin.POLICY, + 1L, + null, + "policy-1", + "run-1", + null, + "file-aaa", + "x"); + RecordFailure b = + new RecordFailure( + FailureKind.INPUT_PASSWORD_PROTECTED, + FailureOrigin.POLICY, + 1L, + null, + "policy-1", + "run-1", + null, + "file-bbb", + "x"); + + assertThat(a.dedupKey()).isNotEqualTo(b.dedupKey()); + assertThat(a.dedupKey()).hasSize(64).isEqualTo(a.dedupKey()); + assertThat(a.scopeRef()).doesNotContain(".pdf"); + } + + @Nested + @DisplayName("the detail cap") + class DetailCap { + + @Test + void capsAnOversizedMessageAtTheLimitIncludingTheEllipsis() { + String stored = withDetail("x".repeat(5_000)).detail(); + + assertThat(stored).hasSize(2_000).endsWith("…"); + } + + @Test + void leavesAMessageAtTheLimitAlone() { + String atLimit = "x".repeat(2_000); + + assertThat(withDetail(atLimit).detail()).isEqualTo(atLimit); + } + + @Test + void neverCutsBetweenTheHalvesOfASurrogatePair() { + // A string of astral-plane characters: every char is half of a surrogate pair, so a + // blind substring at the cap has a 50% chance of storing invalid UTF-16. + String stored = withDetail("\uD835\uDC9C".repeat(3_000)).detail(); + + assertThat(new String(stored.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8)) + .as("a round-trip through UTF-8 mangles an unpaired surrogate") + .isEqualTo(stored); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/integration/crypto/CredentialEncryptionTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/integration/crypto/CredentialEncryptionTest.java index e6e5d356bf..9f424e6367 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/integration/crypto/CredentialEncryptionTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/integration/crypto/CredentialEncryptionTest.java @@ -61,4 +61,47 @@ class CredentialEncryptionTest { assertThatThrownBy(() -> CredentialEncryption.decrypt(tampered)) .isInstanceOf(IllegalStateException.class); } + + @Test + void encryptStaysBase64SoStringColumnsKeepTheirWireFormat() { + assertThat(Base64.getDecoder().decode(CredentialEncryption.encrypt("secret"))).isNotEmpty(); + } + + @Test + void byteRoundTripRecoversPlaintext() { + // Not valid UTF-8: the byte path must not go via a charset. + byte[] plaintext = new byte[] {0, -1, 0x7F, -128}; + + byte[] encrypted = CredentialEncryption.encryptBytes(plaintext); + + assertThat(encrypted).isNotEqualTo(plaintext); + assertThat(CredentialEncryption.decryptBytes(encrypted)).isEqualTo(plaintext); + } + + @Test + void sameBytesProduceDifferentCiphertext() { + byte[] plaintext = "repeated-secret".getBytes(); + + byte[] first = CredentialEncryption.encryptBytes(plaintext); + byte[] second = CredentialEncryption.encryptBytes(plaintext); + + assertThat(first).isNotEqualTo(second); + assertThat(CredentialEncryption.decryptBytes(first)).isEqualTo(plaintext); + assertThat(CredentialEncryption.decryptBytes(second)).isEqualTo(plaintext); + } + + @Test + void tamperedBlobIsRejected() { + byte[] encrypted = CredentialEncryption.encryptBytes("top-secret".getBytes()); + encrypted[encrypted.length - 1] ^= 0x01; + + assertThatThrownBy(() -> CredentialEncryption.decryptBytes(encrypted)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void nullBytesPassThrough() { + assertThat(CredentialEncryption.encryptBytes(null)).isNull(); + assertThat(CredentialEncryption.decryptBytes(null)).isNull(); + } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzerTest.java new file mode 100644 index 0000000000..e9aaab0a1a --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/LayoutAnalyzerTest.java @@ -0,0 +1,286 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** Unit tests for the heuristics that decide what a run of text means. */ +class LayoutAnalyzerTest { + + private static final BBox A4 = new BBox(0, 0, 595, 842); + + private static TextLineInfo line(String text, float size, float x, float y) { + return line(text, size, x, y, false, 0, 0); + } + + private static TextLineInfo line( + String text, float size, float x, float y, boolean bold, int start, int end) { + List words = new ArrayList<>(); + float cursor = x; + for (String token : text.strip().split("\\s+")) { + float width = token.length() * size * 0.5f; + words.add( + new WordInfo( + token, + new BBox(cursor, y, cursor + width, y + size), + start, + end, + size, + bold)); + cursor += width + size * 0.3f; + } + return new TextLineInfo( + 0, text, new BBox(x, y, cursor, y + size), size, bold, start, end, false, words); + } + + private static PageContent page(List lines) { + return new PageContent(0, lines, List.of(), lines.size(), false, false, false, A4); + } + + @Nested + @DisplayName("body font size") + class BodyFontSize { + + @Test + @DisplayName("weights by characters so one huge title does not skew the baseline") + void weightsByCharacterCount() { + List lines = + List.of( + line("A Very Large Title", 32, 50, 700), + line("Body text line one which is long", 11, 50, 650), + line("Body text line two which is long", 11, 50, 630), + line("Body text line three also long", 11, 50, 610)); + assertEquals(11f, LayoutAnalyzer.bodyFontSize(List.of(page(lines)))); + } + + @Test + @DisplayName("returns zero when there is no text") + void handlesEmptyDocument() { + assertEquals(0f, LayoutAnalyzer.bodyFontSize(List.of(page(List.of())))); + } + } + + @Nested + @DisplayName("heading detection") + class Headings { + + @Test + @DisplayName("assigns distinct sizes to descending levels") + void assignsTiers() { + List lines = + List.of( + line("Title", 24, 50, 800), + line("Chapter", 18, 50, 750), + line("Section", 14, 50, 700), + line("Body text that is long enough to set a baseline", 11, 50, 650)); + Map tiers = LayoutAnalyzer.headingTiers(List.of(page(lines)), 11f); + assertEquals(1, tiers.get(24f)); + assertEquals(2, tiers.get(18f)); + assertEquals(3, tiers.get(14f)); + assertNull(tiers.get(11f), "body size must not be a heading tier"); + } + + @Test + @DisplayName("rejects long lines and full sentences whatever their size") + void rejectsProse() { + assertFalse( + LayoutAnalyzer.isHeadingCandidate( + line("This line ends like a sentence does.", 20, 50, 700)), + "a line ending in a full stop reads as prose"); + assertFalse( + LayoutAnalyzer.isHeadingCandidate( + line( + "one two three four five six seven eight nine ten eleven twelve" + + " thirteen", + 20, + 50, + 700)), + "a long line is body text however large"); + assertTrue(LayoutAnalyzer.isHeadingCandidate(line("Financial Results", 20, 50, 700))); + } + + @Test + @DisplayName("boldness alone never promotes a line to a heading") + void boldIsNotAHeadingSignal() { + List lines = + List.of( + line("Bold Label", 11, 50, 700, true, 0, 0), + line("Body text long enough to set the baseline here", 11, 50, 650)); + assertTrue( + LayoutAnalyzer.headingTiers(List.of(page(lines)), 11f).isEmpty(), + "a bold line at body size is emphasis, not a heading"); + } + + @Test + @DisplayName("rewrites skipped levels so H1 is never followed by H3") + void normalisesSkippedLevels() { + DocumentStructure structure = new DocumentStructure(); + structure.add(new StructBlock(StructType.H1, 0)); + structure.add(new StructBlock(StructType.H3, 0)); + structure.add(new StructBlock(StructType.H4, 0)); + LayoutAnalyzer.normaliseHeadingLevels(structure); + + assertEquals(StructType.H1, structure.getBlocks().get(0).getType()); + assertEquals(StructType.H2, structure.getBlocks().get(1).getType()); + assertEquals(StructType.H3, structure.getBlocks().get(2).getType()); + } + } + + @Nested + @DisplayName("lists") + class Lists { + + @Test + @DisplayName("recognises bullet and ordered markers") + void recognisesMarkers() { + assertTrue(LayoutAnalyzer.startsListItem(line("• First item", 11, 50, 700))); + assertTrue(LayoutAnalyzer.startsListItem(line("- First item", 11, 50, 700))); + assertTrue(LayoutAnalyzer.startsListItem(line("1. First item", 11, 50, 700))); + assertTrue(LayoutAnalyzer.startsListItem(line("a) First item", 11, 50, 700))); + assertFalse(LayoutAnalyzer.startsListItem(line("Ordinary prose here", 11, 50, 700))); + } + } + + @Nested + @DisplayName("table cells") + class Tables { + + @Test + @DisplayName("splits a row at wide gaps but not at ordinary word spacing") + void splitsOnWideGaps() { + List words = + List.of( + new WordInfo("Region", new BBox(50, 700, 90, 711), 0, 0, 11, false), + new WordInfo("name", new BBox(93, 700, 125, 711), 0, 0, 11, false), + new WordInfo("Units", new BBox(250, 700, 285, 711), 1, 1, 11, false)); + TextLineInfo row = + new TextLineInfo( + 0, + "Region name Units", + new BBox(50, 700, 285, 711), + 11, + false, + 0, + 1, + false, + words); + List> cells = LayoutAnalyzer.splitCells(row); + assertEquals(2, cells.size(), "the small gap is a word space, the large one is a cell"); + assertEquals(2, cells.get(0).size()); + assertEquals("Units", cells.get(1).get(0).text()); + } + + @Test + @DisplayName("words sharing an operator cannot become separate cells") + void detectsInseparableWords() { + List shared = + List.of( + new WordInfo("A", new BBox(50, 700, 60, 711), 3, 3, 11, false), + new WordInfo("B", new BBox(250, 700, 260, 711), 3, 3, 11, false)); + TextLineInfo row = + new TextLineInfo( + 0, "A B", new BBox(50, 700, 260, 711), 11, false, 3, 3, false, shared); + assertFalse( + row.wordsAreSeparable(), + "cells drawn by one operator cannot carry separate marked content ids"); + } + } + + @Nested + @DisplayName("running heads") + class RunningHeads { + + @Test + @DisplayName("treats text repeating in the margin band across pages as an artifact") + void findsRepeatedMarginText() { + List pages = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + List lines = + List.of( + line("Confidential Report", 9, 50, 800), + line("Body content for the page", 11, 50, 400), + line("Page " + (i + 1), 9, 300, 20)); + pages.add(new PageContent(i, lines, List.of(), 3, false, false, false, A4)); + } + Map> artifacts = LayoutAnalyzer.repeatedMarginLines(pages); + assertEquals( + 2, artifacts.get(0).size(), "the running head and the folio are artifacts"); + assertTrue( + artifacts.get(0).stream().noneMatch(l -> l.text().contains("Body content")), + "body text must never be demoted to an artifact"); + } + + @Test + @DisplayName("does not treat a one-off margin line as a running head") + void ignoresUniqueMarginText() { + List titles = List.of("Alpha", "Beta", "Gamma", "Delta"); + List pages = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + List lines = + List.of( + line(titles.get(i) + " overview", 9, 50, 800), + line("Body content", 11, 50, 400)); + pages.add(new PageContent(i, lines, List.of(), 2, false, false, false, A4)); + } + assertTrue(LayoutAnalyzer.repeatedMarginLines(pages).get(0).isEmpty()); + } + + @Test + @DisplayName("a large heading high on the page stays a heading, not chrome") + void doesNotDemoteHeadingsNearTheTop() { + List pages = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + List lines = + List.of( + // Masking digits makes these look identical across pages. + line("Section " + (i + 1), 20, 50, 800), + line("Body text long enough to set the baseline", 11, 50, 400)); + pages.add(new PageContent(i, lines, List.of(), 2, false, false, false, A4)); + } + assertTrue( + LayoutAnalyzer.repeatedMarginLines(pages, 11f).get(0).isEmpty(), + "a heading larger than body text is content, wherever it sits"); + } + } + + @Nested + @DisplayName("columns") + class Columns { + + @Test + @DisplayName("detects a gutter when text sits in two balanced blocks") + void detectsTwoColumns() { + List lines = new ArrayList<>(); + for (int i = 0; i < 6; i++) { + lines.add(line("Left column text", 10, 50, 700 - i * 14)); + lines.add(line("Right column text", 10, 320, 700 - i * 14)); + } + assertNotNull(LayoutAnalyzer.detectGutter(page(lines))); + } + + @Test + @DisplayName("does not split a page whose lines span the full width") + void ignoresSingleColumn() { + List lines = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + lines.add( + line( + "A full width line of prose that crosses the centre of the page", + 10, + 50, + 700 - i * 14)); + } + assertNull(LayoutAnalyzer.detectGutter(page(lines))); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentInjectorTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentInjectorTest.java new file mode 100644 index 0000000000..73d4c64817 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentInjectorTest.java @@ -0,0 +1,164 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests the content-stream rewriting that makes tagging possible. */ +class MarkedContentInjectorTest { + + private static byte[] threeLinePdf() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + for (int i = 0; i < 3; i++) { + cs.beginText(); + cs.setFont(font, 12); + cs.newLineAtOffset(50, 700 - i * 20); + cs.showText("Line " + i); + cs.endText(); + } + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } + } + + private static String contentOf(PDDocument document) throws IOException { + try (InputStream in = document.getPage(0).getContents()) { + return new String(in.readAllBytes(), StandardCharsets.ISO_8859_1); + } + } + + @Test + @DisplayName("wraps claimed content in BDC/EMC with a marked content id") + void wrapsClaimedContent() throws Exception { + try (PDDocument document = Loader.loadPDF(threeLinePdf())) { + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.addRange(0, 1); + + int next = + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(paragraph), 0, true); + + String content = contentOf(document); + assertTrue(content.contains("/P"), "the structure type was not written"); + assertTrue(content.contains("/MCID"), "no marked content id was written"); + assertTrue(content.contains("BDC"), "no marked content sequence was opened"); + assertTrue(content.contains("EMC"), "no marked content sequence was closed"); + assertFalse(paragraph.getMcids().isEmpty(), "the block was given no marked content id"); + assertTrue(next > 0, "the id counter did not advance"); + } + } + + @Test + @DisplayName("marks unclaimed content as an artifact so nothing is left untagged") + void unclaimedContentBecomesArtifact() throws Exception { + try (PDDocument document = Loader.loadPDF(threeLinePdf())) { + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.addRange(0, 0); + + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(paragraph), 0, true); + + String content = contentOf(document); + assertTrue( + content.contains("/Artifact"), + "content nobody claimed must be marked as an artifact, or PDF/UA clause 7.1" + + " fails"); + } + } + + @Test + @DisplayName("opens and closes sequences in balanced pairs") + void sequencesAreBalanced() throws Exception { + try (PDDocument document = Loader.loadPDF(threeLinePdf())) { + StructBlock first = new StructBlock(StructType.P, 0); + first.addRange(0, 0); + StructBlock second = new StructBlock(StructType.H1, 0); + second.addRange(2, 2); + + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(first, second), 0, true); + + String content = contentOf(document); + int opens = count(content, "BDC") + count(content, "BMC"); + int closes = count(content, "EMC"); + assertEquals(opens, closes, "every opened sequence must be closed"); + } + } + + @Test + @DisplayName("rewriting does not change what a reader extracts") + void textIsUnchanged() throws Exception { + byte[] original = threeLinePdf(); + String before = extract(original); + + byte[] rewritten; + try (PDDocument document = Loader.loadPDF(original)) { + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.addRange(0, 2); + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(paragraph), 0, true); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + rewritten = out.toByteArray(); + } + assertEquals(before, extract(rewritten), "marked content operators must not render"); + } + + @Test + @DisplayName("two blocks claiming the same content keep the first, not both") + void overlappingClaimsAreResolved() { + StructBlock first = new StructBlock(StructType.P, 0); + first.addRange(0, 2); + StructBlock second = new StructBlock(StructType.H1, 0); + second.addRange(1, 1); + + Map owners = + MarkedContentInjector.ownersByOrdinal(List.of(first, second)); + assertSame(first, owners.get(1), "the first claim wins so reading order stays unambiguous"); + assertEquals(3, owners.size()); + } + + private static int count(String haystack, String needle) { + int total = 0; + int index = 0; + while ((index = haystack.indexOf(needle, index)) >= 0) { + total++; + index += needle.length(); + } + return total; + } + + private static String extract(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + return new org.apache.pdfbox.text.PDFTextStripper() + .getText(document) + .replaceAll("\\s+", " ") + .strip(); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentSafetyTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentSafetyTest.java new file mode 100644 index 0000000000..29cc18c525 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/MarkedContentSafetyTest.java @@ -0,0 +1,193 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Regression tests for rewriter damage a validator cannot see, so it still passes validation. */ +class MarkedContentSafetyTest { + + private static String contentOf(PDDocument document) throws IOException { + try (InputStream in = document.getPage(0).getContents()) { + return new String(in.readAllBytes(), StandardCharsets.ISO_8859_1); + } + } + + private static void setContent(PDDocument document, String content) throws IOException { + PDStream stream = new PDStream(document); + try (var out = stream.createOutputStream()) { + out.write(content.getBytes(StandardCharsets.ISO_8859_1)); + } + document.getPage(0).setContents(stream); + } + + private static PDDocument onePage() throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + cs.beginText(); + cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + cs.newLineAtOffset(50, 700); + cs.showText("visible"); + cs.endText(); + } + return document; + } + + @Test + @DisplayName("an optional-content layer survives the rebuild, so hidden content stays hidden") + void optionalContentIsPreserved() throws Exception { + try (PDDocument document = onePage()) { + String original = contentOf(document); + setContent(document, "/OC /MC0 BDC\n" + original + "\nEMC\n"); + + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.addRange(0, 0); + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(paragraph), 0, true); + + String rewritten = contentOf(document); + assertTrue( + rewritten.contains("/OC"), + "the optional-content wrapper was stripped, which would make a hidden" + + " DRAFT/CONFIDENTIAL or redaction layer permanently visible:\n" + + rewritten); + assertEquals( + countOf(rewritten, "BDC") + countOf(rewritten, "BMC"), + countOf(rewritten, "EMC"), + "marked content is unbalanced after preserving the layer"); + } + } + + @Test + @DisplayName("replacement text survives the rebuild so ligatures still read correctly") + void actualTextIsPreserved() throws Exception { + try (PDDocument document = onePage()) { + String original = contentOf(document); + // A generator marks an ffi ligature with what it really spells. + setContent( + document, "/Span <> BDC\n" + original + "\nEMC\n"); + + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.addRange(0, 0); + new MarkedContentInjector() + .inject(document, document.getPage(0), List.of(paragraph), 0, true); + + String rewritten = contentOf(document); + assertTrue( + rewritten.contains("ActualText"), + "dropping ActualText leaves a screen reader announcing the raw glyph:\n" + + rewritten); + assertFalse( + rewritten.contains("/MCID 7"), + "the source's own marked content id is meaningless after a rebuild"); + assertEquals( + countOf(rewritten, "BDC") + countOf(rewritten, "BMC"), + countOf(rewritten, "EMC"), + "marked content is unbalanced after preserving replacement text"); + } + } + + @Test + @DisplayName("a sequence wrapping a fill opens before the path, not inside it") + void markedContentNeverOpensInsideAPathObject() throws Exception { + try (PDDocument document = onePage()) { + setContent(document, "0 0 0 rg\n10 10 50 5 re\nf\n"); + + new MarkedContentInjector().inject(document, document.getPage(0), List.of(), 0, true); + + String rewritten = contentOf(document); + int reAt = rewritten.indexOf(" re"); + int openAt = Math.max(rewritten.indexOf("BMC"), rewritten.indexOf("BDC")); + assertTrue(openAt >= 0, "no sequence was opened at all: " + rewritten); + assertTrue( + openAt < reAt, + "ISO 32000-1 does not permit a marked-content operator inside a path object;" + + " the sequence must open before the path construction:\n" + + rewritten); + } + } + + @Test + @DisplayName("words drawn out of stream order are still claimed, not silently artifacted") + void outOfOrderWordsAreClaimed() { + // A line whose second word on the page was painted first: ordinals 1 then 0. + WordInfo right = new WordInfo("label", new BBox(50, 700, 90, 712), 1, 1, 11, false); + WordInfo left = new WordInfo("value", new BBox(200, 700, 240, 712), 0, 0, 11, false); + TextLineInfo line = + new TextLineInfo( + 0, + "label value", + new BBox(50, 700, 240, 712), + 11, + false, + 0, + 1, + false, + List.of(right, left)); + + PageContent page = + new PageContent( + 0, + List.of(line), + List.of(), + 2, + false, + false, + false, + new BBox(0, 0, 595, 842)); + DocumentStructure structure = new LayoutAnalyzer().analyse(List.of(page)); + + boolean[] claimed = new boolean[2]; + structure.visit( + block -> { + if (block.isArtifact()) { + return; + } + block.getRanges() + .forEach( + r -> { + for (int i = r.start(); i <= r.end() && i < 2; i++) { + claimed[i] = true; + } + }); + }); + assertTrue( + claimed[0] && claimed[1], + "an out-of-order word was left unclaimed and would be hidden from assistive" + + " technology while the file still validated"); + } + + private static int countOf(String haystack, String needle) { + int total = 0; + int index = 0; + while ((index = haystack.indexOf(needle, index)) >= 0) { + total++; + index += needle.length(); + } + return total; + } + + private static byte[] bytes(PDDocument document) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaFormAndDeclarationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaFormAndDeclarationTest.java new file mode 100644 index 0000000000..bae4ebed9a --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaFormAndDeclarationTest.java @@ -0,0 +1,171 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDTextField; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Covers form-field descriptions, widget nesting and withdrawing a conformance claim. */ +class PdfUaFormAndDeclarationTest { + + /** A document with one named text field and one unnamed one. */ + private static PDDocument formDocument(boolean nameTheSecondField) throws IOException { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + PDAcroForm form = new PDAcroForm(document); + document.getDocumentCatalog().setAcroForm(form); + + PDTextField named = new PDTextField(form); + named.setPartialName("EmailAddress"); + addWidget(named, page, 700); + form.getFields().add(named); + + PDTextField second = new PDTextField(form); + if (nameTheSecondField) { + second.setPartialName("PostCode"); + } + addWidget(second, page, 650); + form.getFields().add(second); + + return document; + } + + private static void addWidget(PDTextField field, PDPage page, float y) throws IOException { + PDAnnotationWidget widget = field.getWidgets().get(0); + PDRectangle rectangle = new PDRectangle(); + rectangle.setLowerLeftX(50); + rectangle.setLowerLeftY(y); + rectangle.setUpperRightX(250); + rectangle.setUpperRightY(y + 18); + widget.setRectangle(rectangle); + widget.setPage(page); + page.getAnnotations().add(widget); + } + + private static String xmpOf(PDDocument document) throws IOException { + var metadata = document.getDocumentCatalog().getMetadata(); + assertNotNull(metadata, "no XMP packet"); + return new String(metadata.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + @DisplayName("a form field gets its tooltip from its own name, not an invented one") + void derivesTooltipFromFieldName() throws Exception { + try (PDDocument document = formDocument(true)) { + List warnings = + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Form", "en", PdfUaProfile.UA1); + + PDAcroForm form = document.getDocumentCatalog().getAcroForm(); + assertEquals("EmailAddress", form.getField("EmailAddress").getAlternateFieldName()); + assertEquals("PostCode", form.getField("PostCode").getAlternateFieldName()); + assertTrue(warnings.isEmpty(), "nothing needed reporting: " + warnings); + } + } + + @Test + @DisplayName("a field with no name is reported rather than given a placeholder tooltip") + void reportsUnnameableField() throws Exception { + try (PDDocument document = formDocument(false)) { + List warnings = + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Form", "en", PdfUaProfile.UA1); + assertEquals(1, warnings.size()); + assertTrue(warnings.get(0).contains("form field"), warnings.get(0)); + } + } + + @Test + @DisplayName("an existing description is never overwritten") + void keepsExistingDescription() throws Exception { + try (PDDocument document = formDocument(true)) { + PDAcroForm form = document.getDocumentCatalog().getAcroForm(); + form.getField("EmailAddress").setAlternateFieldName("Your email address"); + + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Form", "en", PdfUaProfile.UA1); + assertEquals( + "Your email address", form.getField("EmailAddress").getAlternateFieldName()); + } + } + + @Test + @DisplayName("widget annotations are nested inside a Form structure element") + void widgetsAreNestedInFormElements() throws Exception { + try (PDDocument document = formDocument(true)) { + DocumentStructure structure = new DocumentStructure(); + StructBlock paragraph = new StructBlock(StructType.P, 0); + paragraph.getMcids().add(0); + structure.add(paragraph); + + new StructTreeWriter().write(document, structure, PdfUaProfile.UA1); + + var root = document.getDocumentCatalog().getStructureTreeRoot(); + assertTrue( + typesUnder(root).contains("Form"), + "clause 7.18.4 requires a widget to sit inside a Form element, found: " + + typesUnder(root)); + } + } + + private static List typesUnder(PDStructureNode node) { + List types = new java.util.ArrayList<>(); + for (Object kid : node.getKids()) { + if (kid instanceof PDStructureElement element) { + types.add(element.getStructureType()); + types.addAll(typesUnder(element)); + } + } + return types; + } + + @Test + @DisplayName("withdrawing conformance removes the claim but keeps the other metadata") + void withdrawingConformanceRemovesOnlyTheClaim() throws Exception { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage()); + PdfUaTagger tagger = new PdfUaTagger(); + PdfUaMetadataWriter writer = new PdfUaMetadataWriter(); + + writer.applyDocumentRequirements(document, "Kept Title", "en-GB", PdfUaProfile.UA1); + writer.declareConformance(document, PdfUaProfile.UA1); + assertTrue(xmpOf(document).contains("pdfuaid")); + + tagger.withdrawConformance(document); + + String xmp = xmpOf(document); + assertFalse(xmp.contains("pdfuaid"), "the conformance claim should be gone"); + assertTrue(xmp.contains("Kept Title"), "the title should survive"); + assertEquals("en-GB", document.getDocumentCatalog().getLanguage()); + } + } + + @Test + @DisplayName("withdrawing conformance on a document that never claimed it is harmless") + void withdrawingIsIdempotent() throws Exception { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage()); + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Title", "en", PdfUaProfile.UA1); + new PdfUaTagger().withdrawConformance(document); + assertFalse(xmpOf(document).contains("pdfuaid")); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaLanguageTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaLanguageTest.java new file mode 100644 index 0000000000..ed05e8de0b --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaLanguageTest.java @@ -0,0 +1,79 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * A relabelled language is invisible to every validator, so the tagger must not guess over one the + * document already declares. + */ +class PdfUaLanguageTest { + + private static PDDocument documentWithLanguage(String language) { + PDDocument document = new PDDocument(); + document.addPage(new PDPage()); + if (language != null) { + document.getDocumentCatalog().setLanguage(language); + } + return document; + } + + private static TaggingOptions.TaggingOptionsBuilder options() { + return TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Rapport") + .embedFonts(false); + } + + @Test + @DisplayName("keeps the language the document already declares") + void keepsExistingLanguage() throws Exception { + try (PDDocument document = documentWithLanguage("fr-FR")) { + TaggingResult result = new PdfUaTagger().tag(document, options().build()); + + assertEquals("fr-FR", document.getDocumentCatalog().getLanguage()); + assertTrue( + result.getWarnings().stream().anyMatch(w -> w.contains("fr-FR")), + "ignoring the requested language must be reported: " + result.getWarnings()); + } + } + + @Test + @DisplayName("applies the requested language when the document declares none") + void fillsInMissingLanguage() throws Exception { + try (PDDocument document = documentWithLanguage(null)) { + new PdfUaTagger().tag(document, options().build()); + + assertEquals("en-GB", document.getDocumentCatalog().getLanguage()); + } + } + + @Test + @DisplayName("replaces the declared language only when the caller asks") + void overridesOnRequest() throws Exception { + try (PDDocument document = documentWithLanguage("fr-FR")) { + new PdfUaTagger().tag(document, options().overrideLanguage(true).build()); + + assertEquals("en-GB", document.getDocumentCatalog().getLanguage()); + } + } + + @Test + @DisplayName("keeps the existing language when an existing structure tree is left alone") + void keepsExistingLanguageWithoutRebuilding() throws Exception { + try (PDDocument document = documentWithLanguage("de-DE")) { + new PdfUaTagger() + .tag( + document, + options().existingTags(TaggingOptions.ExistingTags.KEEP).build()); + + assertEquals("de-DE", document.getDocumentCatalog().getLanguage()); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriterTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriterTest.java new file mode 100644 index 0000000000..f953488b03 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaMetadataWriterTest.java @@ -0,0 +1,137 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests the document-level requirements that have nothing to do with tagging. */ +class PdfUaMetadataWriterTest { + + private static PDDocument twoPageDocument() { + PDDocument document = new PDDocument(); + document.addPage(new PDPage()); + document.addPage(new PDPage()); + return document; + } + + private static String xmpOf(PDDocument document) throws IOException { + var metadata = document.getDocumentCatalog().getMetadata(); + assertNotNull(metadata, "no XMP packet was written"); + return new String(metadata.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + @DisplayName("sets title, language, tab order and the display-title flag") + void appliesDocumentRequirements() throws Exception { + try (PDDocument document = twoPageDocument()) { + new PdfUaMetadataWriter() + .applyDocumentRequirements( + document, "Annual Report", "en-GB", PdfUaProfile.UA1); + + assertEquals("en-GB", document.getDocumentCatalog().getLanguage()); + assertEquals("Annual Report", document.getDocumentInformation().getTitle()); + assertTrue( + document.getDocumentCatalog().getViewerPreferences().displayDocTitle(), + "without DisplayDocTitle a viewer shows the filename instead of the title"); + + for (PDPage page : document.getPages()) { + assertEquals( + "S", + page.getCOSObject().getNameAsString(COSName.getPDFName("Tabs")), + "clause 7.18.1 requires an explicit tab order on every page"); + } + } + } + + @Test + @DisplayName("writes dc:title into the XMP packet, not just the info dictionary") + void writesDublinCoreTitle() throws Exception { + try (PDDocument document = twoPageDocument()) { + new PdfUaMetadataWriter() + .applyDocumentRequirements( + document, "Annual Report", "en-GB", PdfUaProfile.UA1); + assertTrue(xmpOf(document).contains("Annual Report")); + } + } + + @Test + @DisplayName("does not declare conformance as part of applying requirements") + void doesNotDeclareEarly() throws Exception { + try (PDDocument document = twoPageDocument()) { + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Report", "en", PdfUaProfile.UA1); + assertFalse( + xmpOf(document).contains("pdfuaid"), + "the conformance claim must wait until validation has passed"); + } + } + + @Test + @DisplayName("declaring conformance writes pdfuaid with the right part") + void declaresConformance() throws Exception { + try (PDDocument document = twoPageDocument()) { + PdfUaMetadataWriter writer = new PdfUaMetadataWriter(); + writer.applyDocumentRequirements(document, "Report", "en", PdfUaProfile.UA1); + writer.declareConformance(document, PdfUaProfile.UA1); + + String xmp = xmpOf(document); + assertTrue(xmp.contains("pdfuaid"), "no PDF/UA identifier was written"); + assertTrue(xmp.contains("part"), "no conformance part was written"); + } + } + + @Test + @DisplayName("UA-2 raises the PDF version to 2.0") + void ua2RaisesVersion() throws Exception { + try (PDDocument document = twoPageDocument()) { + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, "Report", "en", PdfUaProfile.UA2); + assertEquals(2.0f, document.getVersion()); + } + } + + @Test + @DisplayName("keeps an existing title when none is supplied") + void keepsExistingTitle() throws Exception { + try (PDDocument document = twoPageDocument()) { + var info = document.getDocumentInformation(); + info.setTitle("Original Title"); + document.setDocumentInformation(info); + + new PdfUaMetadataWriter() + .applyDocumentRequirements(document, null, "en", PdfUaProfile.UA1); + assertEquals("Original Title", document.getDocumentInformation().getTitle()); + } + } + + @Test + @DisplayName("survives a round trip through save and reload") + void survivesRoundTrip() throws Exception { + byte[] saved; + try (PDDocument document = twoPageDocument()) { + PdfUaMetadataWriter writer = new PdfUaMetadataWriter(); + writer.applyDocumentRequirements(document, "Round Trip", "fr-FR", PdfUaProfile.UA1); + writer.declareConformance(document, PdfUaProfile.UA1); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + saved = out.toByteArray(); + } + try (PDDocument reloaded = Loader.loadPDF(saved)) { + assertEquals("fr-FR", reloaded.getDocumentCatalog().getLanguage()); + assertEquals("Round Trip", reloaded.getDocumentInformation().getTitle()); + assertTrue(xmpOf(reloaded).contains("pdfuaid")); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaModelTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaModelTest.java new file mode 100644 index 0000000000..de189f2f74 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/PdfUaModelTest.java @@ -0,0 +1,160 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** Tests for the small types the tagger is built from. */ +class PdfUaModelTest { + + @Nested + @DisplayName("structure types") + class Types { + + @Test + @DisplayName("maps levels to heading tags and back") + void headingLevelsRoundTrip() { + for (int level = 1; level <= 6; level++) { + assertEquals(level, StructType.heading(level).headingLevel()); + assertEquals("H" + level, StructType.heading(level).tag()); + } + } + + @Test + @DisplayName("clamps out-of-range levels rather than throwing") + void clampsLevels() { + assertEquals(StructType.H1, StructType.heading(0)); + assertEquals(StructType.H1, StructType.heading(-3)); + assertEquals(StructType.H6, StructType.heading(9)); + } + + @Test + @DisplayName("reports zero for types that are not headings") + void nonHeadingsHaveNoLevel() { + assertEquals(0, StructType.P.headingLevel()); + assertFalse(StructType.TABLE.isHeading()); + } + } + + @Nested + @DisplayName("markable operators") + class Markable { + + @ParameterizedTest + @ValueSource(strings = {"Tj", "TJ", "'", "\"", "Do", "BI", "S", "f", "f*", "B", "sh"}) + @DisplayName("counts text, XObjects and path painting") + void counted(String operator) { + assertTrue(MarkableOp.isMarkableOperator(operator), operator + " should be markable"); + } + + @ParameterizedTest + @ValueSource(strings = {"q", "Q", "cm", "BT", "ET", "Tf", "Td", "n", "W", "gs", "re"}) + @DisplayName("ignores operators that paint nothing") + void notCounted(String operator) { + assertFalse( + MarkableOp.isMarkableOperator(operator), operator + " should not be markable"); + } + + @Test + @DisplayName("n ends a path without painting, so it is not content") + void pathEndIsNotPainting() { + assertFalse(MarkableOp.isPathPainting("n")); + assertTrue(MarkableOp.isPathPainting("f")); + } + } + + @Nested + @DisplayName("profiles") + class Profiles { + + @Test + @DisplayName("parses the shapes a caller might send") + void parsesRequestValues() { + assertEquals(PdfUaProfile.UA1, PdfUaProfile.fromRequest("ua1")); + assertEquals(PdfUaProfile.UA1, PdfUaProfile.fromRequest(null)); + assertEquals(PdfUaProfile.UA1, PdfUaProfile.fromRequest("")); + assertEquals(PdfUaProfile.UA1, PdfUaProfile.fromRequest("nonsense")); + assertEquals(PdfUaProfile.UA2, PdfUaProfile.fromRequest("ua2")); + assertEquals(PdfUaProfile.UA2, PdfUaProfile.fromRequest("PDF/UA-2")); + } + + @Test + @DisplayName("UA-2 requires PDF 2.0") + void ua2NeedsPdf2() { + assertEquals(2.0f, PdfUaProfile.UA2.pdfVersion()); + assertEquals(1.7f, PdfUaProfile.UA1.pdfVersion()); + } + } + + @Nested + @DisplayName("bounding boxes") + class Boxes { + + @Test + @DisplayName("union of an empty box is the other box") + void unionWithEmpty() { + BBox box = new BBox(10, 10, 20, 20); + assertEquals(box, box.union(BBox.EMPTY)); + assertEquals(box, BBox.EMPTY.union(box)); + } + + @Test + @DisplayName("union covers both boxes") + void unionCoversBoth() { + BBox union = new BBox(0, 0, 10, 10).union(new BBox(20, 5, 30, 25)); + assertEquals(new BBox(0, 0, 30, 25), union); + } + + @Test + @DisplayName("reports horizontal overlap as a fraction of the narrower box") + void overlapIsRelative() { + BBox wide = new BBox(0, 0, 100, 10); + BBox narrow = new BBox(40, 0, 60, 10); + assertEquals(1.0f, wide.horizontalOverlap(narrow)); + assertEquals(0f, wide.horizontalOverlap(new BBox(200, 0, 220, 10))); + } + } + + @Nested + @DisplayName("structure blocks") + class Blocks { + + @Test + @DisplayName("counts content across the whole subtree") + void countsDescendantContent() { + StructBlock table = new StructBlock(StructType.TABLE, 0); + StructBlock row = new StructBlock(StructType.TR, 0); + StructBlock cell = new StructBlock(StructType.TD, 0); + cell.addRange(3, 5); + row.addChild(cell); + table.addChild(row); + assertEquals(3, table.contentCount()); + } + + @Test + @DisplayName("collects text in tree order") + void collectsText() { + StructBlock list = new StructBlock(StructType.L, 0); + StructBlock first = new StructBlock(StructType.LI, 0); + first.setText("one"); + StructBlock second = new StructBlock(StructType.LI, 0); + second.setText("two"); + list.addChild(first).addChild(second); + assertEquals("one two", list.collectText()); + } + + @Test + @DisplayName("an artifact is not a structure element") + void artifactsAreDistinct() { + StructBlock artifact = StructBlock.artifact(ArtifactType.PAGINATION, 0); + assertTrue(artifact.isArtifact()); + assertEquals("Pagination", artifact.getArtifactType().subtype()); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/VectorAndHeadingTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/VectorAndHeadingTest.java new file mode 100644 index 0000000000..aa1b13e3b9 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/pdf/ua/VectorAndHeadingTest.java @@ -0,0 +1,155 @@ +package stirling.software.proprietary.pdf.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests the heuristics that decide what counts as a drawing and what counts as a heading. */ +class VectorAndHeadingTest { + + private static final BBox A4 = new BBox(0, 0, 595, 842); + + private static TextLineInfo line(String text, float size, float x, float y) { + List words = new ArrayList<>(); + float cursor = x; + for (String token : text.strip().split("\\s+")) { + float width = token.length() * size * 0.5f; + words.add( + new WordInfo( + token, + new BBox(cursor, y, cursor + width, y + size), + 0, + 0, + size, + false)); + cursor += width + size * 0.3f; + } + return new TextLineInfo( + 0, text, new BBox(x, y, cursor, y + size), size, false, 0, 0, false, words); + } + + private static MarkableOp vector(int ordinal, BBox box) { + return new MarkableOp(ordinal, MarkableOp.Kind.VECTOR, box, null); + } + + private static DocumentStructure analyse(List lines, List ops) { + PageContent page = new PageContent(0, lines, ops, ops.size(), false, false, false, A4); + return new LayoutAnalyzer().analyse(List.of(page)); + } + + private static long countOf(DocumentStructure structure, StructType type) { + long[] total = {0}; + structure.visit( + block -> { + if (block.getType() == type) { + total[0]++; + } + }); + return total[0]; + } + + @Test + @DisplayName("a cluster of substantial strokes becomes a figure, not silent decoration") + void chartBecomesAFigure() { + List bars = new ArrayList<>(); + for (int i = 0; i < 6; i++) { + bars.add(vector(i, new BBox(100 + i * 20, 400, 115 + i * 20, 400 + 30 + i * 10))); + } + DocumentStructure structure = analyse(List.of(), bars); + + assertTrue( + countOf(structure, StructType.FIGURE) > 0, + "a bar chart drawn with path operators must not vanish as decoration"); + assertTrue( + structure.figuresWithoutAlt().size() > 0, + "the report must say the chart needs a description"); + } + + @Test + @DisplayName("thin rules and table borders stay artifacts") + void tableRulesStayDecoration() { + List rules = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + rules.add(vector(i, new BBox(60, 700 - i * 20, 540, 701 - i * 20))); + } + DocumentStructure structure = analyse(List.of(), rules); + + assertEquals( + 0, + countOf(structure, StructType.FIGURE), + "horizontal rules are page furniture and must not demand alt text"); + assertEquals(0, structure.figuresWithoutAlt().size()); + } + + @Test + @DisplayName("a lone box is ornament, not a chart") + void singleBoxIsNotAFigure() { + DocumentStructure structure = + analyse(List.of(), List.of(vector(0, new BBox(60, 400, 500, 700)))); + assertEquals(0, countOf(structure, StructType.FIGURE)); + } + + @Test + @DisplayName("shaded table rows behind text are not mistaken for a chart") + void shadedTableRowsAreNotFigures() { + List shading = new ArrayList<>(); + List rows = new ArrayList<>(); + for (int i = 0; i < 6; i++) { + float y = 600 - i * 20; + // A filled row background, tall enough to pass the thinness test. + shading.add(vector(i, new BBox(60, y, 540, y + 16))); + rows.add(line("Expense line item " + i + " amount", 10, 64, y + 3)); + } + DocumentStructure structure = analyse(rows, shading); + + assertEquals( + 0, + countOf(structure, StructType.FIGURE), + "row shading sits behind the text it decorates and is not a drawing"); + } + + @Test + @DisplayName("small print dominating an invoice does not promote addresses to headings") + void smallPrintDoesNotCreateHeadings() { + List lines = new ArrayList<>(); + // Address block at ordinary 11pt. + lines.add(line("Acme Industries Limited", 11, 60, 780)); + lines.add(line("14 Example Street", 11, 60, 765)); + lines.add(line("Manchester M1 2AB", 11, 60, 750)); + // 40 lines of 9pt line-item small print, which dominates the character count. + for (int i = 0; i < 40; i++) { + lines.add(line("Item " + i + " widget assembly part number " + i, 9, 60, 700 - i * 12)); + } + + Map tiers = + LayoutAnalyzer.headingTiers( + List.of(new PageContent(0, lines, List.of(), 0, false, false, false, A4)), + 9f); + assertNull( + tiers.get(11f), + "11pt address lines are body text on an invoice, not headings: " + tiers); + } + + @Test + @DisplayName("a genuinely rare large size is still a heading") + void realHeadingsSurvive() { + List lines = new ArrayList<>(); + lines.add(line("Annual Report", 24, 60, 780)); + for (int i = 0; i < 40; i++) { + lines.add( + line("Body prose line number " + i + " continues here", 11, 60, 700 - i * 12)); + } + Map tiers = + LayoutAnalyzer.headingTiers( + List.of(new PageContent(0, lines, List.of(), 0, false, false, false, A4)), + 11f); + assertEquals(1, tiers.get(24f), "a rare large size is exactly what a heading looks like"); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStoreDbTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStoreDbTest.java new file mode 100644 index 0000000000..4677166aeb --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStoreDbTest.java @@ -0,0 +1,127 @@ +package stirling.software.proprietary.policy.asset; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; + +import stirling.software.proprietary.integration.crypto.CredentialEncryption; + +/** + * {@link JpaPolicyAssetStore} on a real (H2) database: proves the entity (including its LOB column) + * creates via ddl-auto, that bytes round-trip while sitting encrypted in the column, and that the + * metadata projections select every field in the right order. + */ +@DataJpaTest +@Import(CredentialEncryption.class) +@TestPropertySource( + properties = + "stirling.security.credentialEncryptionKey=" + + "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") +class JpaPolicyAssetStoreDbTest { + + @Autowired private PolicyAssetRepository repository; + + @Test + void savesAndReadsBackMetadataAndContent() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + byte[] content = new byte[] {1, 2, 3, 4}; + + PolicyAsset saved = + store.save( + new PolicyAsset(null, "logo.png", "image/png", 0, "owner", 7L, 42L), + content); + + assertFalse(saved.id().isBlank()); + assertEquals(content.length, saved.size()); + PolicyAsset read = store.get(saved.id()).orElseThrow(); + // Every field: a transposed projection argument is invisible to the compiler. + assertEquals(saved, read); + assertEquals("logo.png", read.fileName()); + assertEquals("image/png", read.contentType()); + assertEquals("owner", read.owner()); + assertEquals(content.length, read.size()); + assertEquals(7L, read.teamId()); + assertEquals(42L, read.createdAt()); + assertArrayEquals(content, store.content(saved.id()).orElseThrow()); + } + + @Test + void storesTheBytesEncrypted() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + byte[] content = "-----BEGIN PRIVATE KEY-----".getBytes(); + + PolicyAsset saved = + store.save(new PolicyAsset(null, "cert.p12", null, 0, null, null, 1L), content); + + byte[] atRest = repository.findById(saved.id()).orElseThrow().getData(); + assertFalse(Arrays.equals(content, atRest)); + assertArrayEquals(content, CredentialEncryption.decryptBytes(atRest)); + // Plaintext length, not the ciphertext's: it is the size the UI shows. + assertEquals(content.length, saved.size()); + } + + @Test + void findByTeamScopesRowsAndMatchesNullTeam() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + PolicyAsset teamAsset = + store.save(new PolicyAsset(null, "a.pdf", null, 0, null, 7L, 1L), new byte[] {1}); + PolicyAsset noTeamAsset = + store.save(new PolicyAsset(null, "b.pdf", null, 0, null, null, 2L), new byte[] {2}); + + List team = store.findByTeam(7L); + List noTeam = store.findByTeam(null); + + assertEquals(List.of(teamAsset), team); + assertEquals(List.of(noTeamAsset.id()), noTeam.stream().map(PolicyAsset::id).toList()); + } + + @Test + void allReturnsEveryAssetNewestFirst() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + PolicyAsset oldest = + store.save(new PolicyAsset(null, "a.pdf", null, 0, null, 7L, 1L), new byte[] {1}); + PolicyAsset newest = + store.save(new PolicyAsset(null, "b.pdf", null, 0, null, 99L, 3L), new byte[] {2}); + + assertEquals( + List.of(newest.id(), oldest.id()), + store.all().stream().map(PolicyAsset::id).toList()); + } + + @Test + void idsCreatedBeforeSelectsOnlyOlderRows() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + PolicyAsset old = + store.save(new PolicyAsset(null, "old.p12", null, 0, null, 7L, 1L), new byte[] {1}); + store.save(new PolicyAsset(null, "new.p12", null, 0, null, 7L, 1000L), new byte[] {2}); + + assertEquals(List.of(old.id()), store.idsCreatedBefore(500L)); + } + + @Test + void deleteRemovesTheRow() { + JpaPolicyAssetStore store = new JpaPolicyAssetStore(repository); + PolicyAsset saved = + store.save(new PolicyAsset(null, "x.p12", null, 0, null, null, 1L), new byte[] {1}); + + assertTrue(store.delete(saved.id())); + assertFalse(store.get(saved.id()).isPresent()); + assertFalse(store.delete(saved.id())); + } + + @SpringBootConfiguration + @AutoConfigurationPackage + static class TestApp {} +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetCleanerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetCleanerTest.java new file mode 100644 index 0000000000..7d7fbb5d97 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetCleanerTest.java @@ -0,0 +1,195 @@ +package stirling.software.proprietary.policy.asset; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.store.InProcessPolicyStore; +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * Tests for {@link PolicyAssetCleaner}: assets a policy stops referencing are deleted once no other + * policy in the team references them, never across teams, and uploads no policy ever bound are + * reclaimed by the sweep once they are old enough. + */ +class PolicyAssetCleanerTest { + + private static final Instant NOW = Instant.ofEpochMilli(10_000_000_000L); + + private final InProcessPolicyAssetStore assetStore = new InProcessPolicyAssetStore(); + private final PolicyStore policyStore = new InProcessPolicyStore(); + private final PolicyAssetCleaner cleaner = + new PolicyAssetCleaner(assetStore, policyStore, () -> NOW); + + @Test + void deletesAssetsDroppedByASave() { + PolicyAsset dropped = asset("old.png", 7L); + PolicyAsset kept = asset("kept.png", 7L); + Policy previous = + savedPolicy( + "p1", + 7L, + step("watermarkImage", dropped.id()), + step("stampImage", kept.id())); + Policy saved = savedPolicy("p1", 7L, step("stampImage", kept.id())); + + cleaner.cleanupAfterSave(previous, saved); + + assertFalse(assetStore.get(dropped.id()).isPresent()); + assertTrue(assetStore.get(kept.id()).isPresent()); + } + + @Test + void keepsAnAssetAnotherPolicyStillReferences() { + PolicyAsset shared = asset("shared.p12", 7L); + savedPolicy("other", 7L, step("p12File", shared.id())); + Policy deleted = policy("gone", 7L, true, step("p12File", shared.id())); + + cleaner.cleanupAfterDelete(deleted); + + assertTrue(assetStore.get(shared.id()).isPresent()); + } + + @Test + void keepsAnAssetOnlyAPausedPolicyReferences() { + // Pausing is a re-save with enabled=false, so reference counting must span every policy in + // the team, not just the enabled ones. + PolicyAsset shared = asset("paused.p12", 7L); + policyStore.save(policy("paused", 7L, false, step("p12File", shared.id()))); + Policy deleted = policy("gone", 7L, true, step("p12File", shared.id())); + + cleaner.cleanupAfterDelete(deleted); + + assertTrue(assetStore.get(shared.id()).isPresent()); + } + + @Test + void deletesAssetsAfterTheirLastReferencingPolicyIsDeleted() { + PolicyAsset orphaned = asset("orphan.png", 7L); + Policy deleted = policy("gone", 7L, true, step("watermarkImage", orphaned.id())); + + cleaner.cleanupAfterDelete(deleted); + + assertFalse(assetStore.get(orphaned.id()).isPresent()); + } + + @Test + void neverDeletesAnotherTeamsAsset() { + PolicyAsset foreign = asset("foreign.png", 99L); + Policy deleted = policy("gone", 7L, true, step("watermarkImage", foreign.id())); + + cleaner.cleanupAfterDelete(deleted); + + assertTrue(assetStore.get(foreign.id()).isPresent()); + } + + @Test + void ignoresARunSuppliedFileKey() { + // No asset: prefix, so the binding names a run-supplied file and nothing is a candidate. + PolicyAsset unrelated = asset("unrelated.png", 7L); + Policy deleted = + policy( + "gone", + 7L, + true, + new PipelineStep("/api/v1/x", Map.of(), Map.of("watermarkImage", "logo"))); + + cleaner.cleanupAfterDelete(deleted); + + assertTrue(assetStore.get(unrelated.id()).isPresent()); + } + + @Test + void swallowsAStoreFailureSoTheCallersOwnWorkIsNotLost() { + // The policy write has already committed by the time cleanup runs. + PolicyAsset orphaned = asset("orphan.png", 7L); + PolicyStore failing = mock(PolicyStore.class); + when(failing.findByTeam(any())).thenThrow(new RuntimeException("db down")); + PolicyAssetCleaner guarded = new PolicyAssetCleaner(assetStore, failing, () -> NOW); + Policy deleted = policy("gone", 7L, true, step("watermarkImage", orphaned.id())); + + assertDoesNotThrow(() -> guarded.cleanupAfterDelete(deleted)); + assertTrue(assetStore.get(orphaned.id()).isPresent()); + } + + @Test + void sweepDeletesAnUploadNoPolicyEverReferenced() { + PolicyAsset abandoned = agedAsset("abandoned.p12", NOW.toEpochMilli() - dayMillis(2), 7L); + + cleaner.sweepAbandonedUploads(); + + assertFalse(assetStore.get(abandoned.id()).isPresent()); + } + + @Test + void sweepKeepsARecentUpload() { + // The grace window is what stops the sweep racing a builder session that is mid-save. + PolicyAsset justUploaded = agedAsset("fresh.p12", NOW.toEpochMilli() - 60_000L, 7L); + + cleaner.sweepAbandonedUploads(); + + assertTrue(assetStore.get(justUploaded.id()).isPresent()); + } + + @Test + void sweepKeepsAnOldAssetAPolicyStillReferences() { + PolicyAsset bound = agedAsset("bound.p12", NOW.toEpochMilli() - dayMillis(30), 7L); + savedPolicy("p1", 7L, step("p12File", bound.id())); + + cleaner.sweepAbandonedUploads(); + + assertTrue(assetStore.get(bound.id()).isPresent()); + } + + @Test + void sweepReclaimsAcrossTeams() { + // An abandoned upload is unreferenced everywhere or nowhere, so the sweep is not scoped. + PolicyAsset teamSeven = agedAsset("seven.png", NOW.toEpochMilli() - dayMillis(2), 7L); + PolicyAsset teamNinetyNine = agedAsset("99.png", NOW.toEpochMilli() - dayMillis(2), 99L); + + cleaner.sweepAbandonedUploads(); + + assertFalse(assetStore.get(teamSeven.id()).isPresent()); + assertFalse(assetStore.get(teamNinetyNine.id()).isPresent()); + } + + private static long dayMillis(int days) { + return days * 24L * 60L * 60L * 1000L; + } + + private PolicyAsset asset(String name, Long teamId) { + return assetStore.save( + new PolicyAsset(null, name, null, 0, "owner", teamId, 1L), new byte[] {1}); + } + + private PolicyAsset agedAsset(String name, long createdAt, Long teamId) { + return assetStore.save( + new PolicyAsset(null, name, null, 0, "owner", teamId, createdAt), new byte[] {1}); + } + + private static PipelineStep step(String field, String assetId) { + return new PipelineStep( + "/api/v1/x", Map.of(), Map.of(field, PolicyAssetRefs.PREFIX + assetId)); + } + + private static Policy policy(String id, Long teamId, boolean enabled, PipelineStep... steps) { + return new Policy( + id, id, "owner", enabled, List.of(), List.of(steps), OutputSpec.inline(), teamId); + } + + private Policy savedPolicy(String id, Long teamId, PipelineStep... steps) { + return policyStore.save(policy(id, teamId, true, steps)); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetControllerTest.java new file mode 100644 index 0000000000..f2e3ea147b --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetControllerTest.java @@ -0,0 +1,243 @@ +package stirling.software.proprietary.policy.asset; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.policy.config.PolicyAccessGuard; +import stirling.software.proprietary.policy.config.PolicyManagementAuthority; +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.store.InProcessPolicyStore; +import stirling.software.proprietary.policy.store.PolicyStore; + +@ExtendWith(MockitoExtension.class) +@DisplayName("PolicyAssetController") +class PolicyAssetControllerTest { + + @Mock private PolicyAccessGuard policyAccessGuard; + @Mock private PolicyManagementAuthority policyManagementAuthority; + + private final PolicyAssetStore assetStore = new InProcessPolicyAssetStore(); + private final PolicyStore policyStore = new InProcessPolicyStore(); + private ApplicationProperties applicationProperties; + private PolicyAssetController controller; + + @BeforeEach + void setUp() { + applicationProperties = new ApplicationProperties(); + controller = + new PolicyAssetController( + assetStore, + policyStore, + policyAccessGuard, + policyManagementAuthority, + applicationProperties); + } + + @Nested + @DisplayName("upload") + class Upload { + + @Test + void stampsTheCallersOwnerAndTeamAndTheActualByteLength() throws Exception { + // Owner/team come from the guard, never the request: a client cannot forge either. + applicationProperties.getSecurity().setEnableLogin(false); + when(policyAccessGuard.ownerForNewPolicy()).thenReturn("lead@example.com"); + when(policyAccessGuard.teamForNewPolicy()).thenReturn(7L); + + PolicyAsset saved = controller.upload(file("logo.png", "image/png", "abcd")).getBody(); + + assertThat(saved).isNotNull(); + assertThat(saved.id()).isNotBlank(); + assertThat(saved.owner()).isEqualTo("lead@example.com"); + assertThat(saved.teamId()).isEqualTo(7L); + assertThat(saved.size()).isEqualTo(4); + } + + @Test + void stripsAnyPathFromTheSuppliedFilename() throws Exception { + applicationProperties.getSecurity().setEnableLogin(false); + + PolicyAsset saved = + controller.upload(file("../../etc/passwd", "text/plain", "x")).getBody(); + + assertThat(saved).isNotNull(); + assertThat(saved.fileName()).isEqualTo("passwd"); + } + + @Test + void rejectsAnEmptyUpload() { + applicationProperties.getSecurity().setEnableLogin(false); + + assertThatThrownBy(() -> controller.upload(file("empty.png", "image/png", ""))) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void rejectsANonLeaderWhenLoginIsEnabled() { + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canEditPolicies()).thenReturn(false); + + assertThatThrownBy(() -> controller.upload(file("c.p12", null, "x"))) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN); + } + } + + @Nested + @DisplayName("content") + class Content { + + @Test + void needsTheSameAuthorityAsUploading() { + // Supporting files include signing certificates: reading the bytes back is gated on + // who may manage policies, not merely on team membership. + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canEditPolicies()).thenReturn(false); + PolicyAsset asset = store("cert.p12", 7L, "secret"); + + assertThatThrownBy(() -> controller.content(asset.id())) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void readsAnotherTeamsAssetAsNotFound() { + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canEditPolicies()).thenReturn(true); + PolicyAsset asset = store("cert.p12", 9L, "secret"); + when(policyAccessGuard.canAccess(asset)).thenReturn(false); + + assertThatThrownBy(() -> controller.content(asset.id())) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void encodesANonAsciiFilenameInTheContentDispositionHeader() { + applicationProperties.getSecurity().setEnableLogin(false); + PolicyAsset asset = store("café.png", null, "bytes"); + when(policyAccessGuard.canAccess(asset)).thenReturn(true); + + String disposition = + controller + .content(asset.id()) + .getHeaders() + .getFirst(HttpHeaders.CONTENT_DISPOSITION); + + // RFC 5987 form, not the raw non-ASCII byte that would mangle in the header. + assertThat(disposition).contains("filename*=UTF-8''caf%C3%A9.png"); + } + + @Test + void fallsBackToOctetStreamForAnUnparsableStoredContentType() { + applicationProperties.getSecurity().setEnableLogin(false); + PolicyAsset asset = + assetStore.save( + new PolicyAsset(null, "x.bin", "not/a/media/type", 0, null, null, 1L), + "bytes".getBytes()); + when(policyAccessGuard.canAccess(asset)).thenReturn(true); + + assertThat(controller.content(asset.id()).getHeaders().getContentType()) + .hasToString("application/octet-stream"); + } + } + + @Nested + @DisplayName("delete") + class Delete { + + @Test + void refusesWhileAPipelineStepStillReferencesTheAsset() { + applicationProperties.getSecurity().setEnableLogin(false); + PolicyAsset asset = store("stamp.png", null, "bytes"); + when(policyAccessGuard.canAccess(asset)).thenReturn(true); + policyStore.save(policyBinding(asset.id())); + when(policyAccessGuard.visibleFrom(policyStore)) + .thenReturn(policyStore.findByTeam(null)); + + assertThatThrownBy(() -> controller.delete(asset.id())) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.CONFLICT); + assertThat(assetStore.get(asset.id())).isPresent(); + } + + @Test + void removesAnAssetNoStepReferences() { + applicationProperties.getSecurity().setEnableLogin(false); + PolicyAsset asset = store("old.png", null, "bytes"); + when(policyAccessGuard.canAccess(asset)).thenReturn(true); + when(policyAccessGuard.visibleFrom(policyStore)).thenReturn(List.of()); + + assertThat(controller.delete(asset.id()).getStatusCode()) + .isEqualTo(HttpStatus.NO_CONTENT); + assertThat(assetStore.get(asset.id())).isEmpty(); + } + + @Test + void seesOneIdInsideAMultiFileBindingAsStillReferenced() { + // A field carrying several files stores its ids comma-joined; each one still counts. + applicationProperties.getSecurity().setEnableLogin(false); + PolicyAsset first = store("a.pdf", null, "a"); + PolicyAsset second = store("b.pdf", null, "b"); + when(policyAccessGuard.canAccess(second)).thenReturn(true); + policyStore.save(policyBinding(first.id() + "," + second.id())); + when(policyAccessGuard.visibleFrom(policyStore)) + .thenReturn(policyStore.findByTeam(null)); + + assertThatThrownBy(() -> controller.delete(second.id())) + .isInstanceOf(ResponseStatusException.class) + .extracting(e -> ((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.CONFLICT); + } + } + + private PolicyAsset store(String fileName, Long teamId, String content) { + return assetStore.save( + new PolicyAsset(null, fileName, null, 0, null, teamId, 1L), content.getBytes()); + } + + private static Policy policyBinding(String assetIds) { + return new Policy( + "p1", + "p", + "owner", + true, + List.of(PipelineInput.manual("s1")), + List.of( + new PipelineStep( + "/api/v1/security/cert-sign", + Map.of(), + Map.of("certFile", PolicyAssetRefs.PREFIX + assetIds))), + OutputSpec.inline()); + } + + private static MockMultipartFile file(String name, String contentType, String content) { + return new MockMultipartFile("file", name, contentType, content.getBytes()); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetResolverTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetResolverTest.java new file mode 100644 index 0000000000..b71536ffa6 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/PolicyAssetResolverTest.java @@ -0,0 +1,243 @@ +package stirling.software.proprietary.policy.asset; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; + +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyInputs; + +/** + * Tests for {@link PolicyAssetResolver}: stored assets referenced by a policy's steps load into the + * run's supporting files, keyed by the step's binding value, team-checked, and winning over + * anything the run supplied under the same key. + */ +class PolicyAssetResolverTest { + + private final InProcessPolicyAssetStore store = new InProcessPolicyAssetStore(); + private final PolicyAssetResolver resolver = new PolicyAssetResolver(store); + + @Test + void loadsReferencedAssetsUnderTheStepBindingKey() throws IOException { + PolicyAsset image = save("logo.png", 7L, new byte[] {1, 2}); + String key = ref(image.id()); + Policy policy = + policy( + 7L, + new PipelineStep( + "/api/v1/security/add-watermark", + Map.of(), + Map.of("watermarkImage", key))); + + PolicyInputs resolved = resolver.resolve(policy, PolicyInputs.of(List.of())); + + List bound = resolved.supportingFiles().get(key); + assertEquals(1, bound.size()); + assertEquals("logo.png", bound.get(0).getFilename()); + assertArrayEquals(new byte[] {1, 2}, bound.get(0).getContentAsByteArray()); + } + + @Test + void aCommaSeparatedBindingLoadsEveryAssetUnderTheFullKey() { + PolicyAsset first = save("a.pdf", null, new byte[] {1}); + PolicyAsset second = save("b.pdf", null, new byte[] {2}); + String key = ref(first.id() + "," + second.id()); + Policy policy = + policy( + null, + new PipelineStep( + "/api/v1/general/overlay-pdfs", + Map.of(), + Map.of("overlayFiles", key))); + + PolicyInputs resolved = resolver.resolve(policy, PolicyInputs.of(List.of())); + + assertEquals(2, resolved.supportingFiles().get(key).size()); + } + + @Test + void storedAssetsWinOverRunSuppliedOnes() throws IOException { + // Runs are open to the whole team, so a member must not be able to swap a leader-pinned + // certificate by posting their own file under the binding's key. + PolicyAsset stored = save("stored.png", null, new byte[] {9}); + String key = ref(stored.id()); + Policy policy = + policy( + null, + new PipelineStep( + "/api/v1/security/add-watermark", + Map.of(), + Map.of("watermarkImage", key))); + Resource supplied = new ByteArrayResource(new byte[] {5}); + PolicyInputs inputs = new PolicyInputs(List.of(), Map.of(key, List.of(supplied))); + + PolicyInputs resolved = resolver.resolve(policy, inputs); + + assertArrayEquals( + new byte[] {9}, resolved.supportingFiles().get(key).get(0).getContentAsByteArray()); + } + + @Test + void aLaterStepsStoredBindingStillWinsOverARunSuppliedFile() { + // The first resolved key seeds the merged map from the run's own files; the second must + // still overwrite its run-supplied entry rather than read as already resolved. + PolicyAsset first = save("first.png", null, new byte[] {1}); + PolicyAsset second = save("second.p12", null, new byte[] {2}); + String firstKey = ref(first.id()); + String secondKey = ref(second.id()); + Policy policy = + new Policy( + "p1", + "p", + "owner", + true, + List.of(), + List.of( + new PipelineStep( + "/api/v1/security/add-watermark", + Map.of(), + Map.of("watermarkImage", firstKey)), + new PipelineStep( + "/api/v1/security/cert-sign", + Map.of(), + Map.of("p12File", secondKey))), + OutputSpec.inline(), + null); + Resource supplied = new ByteArrayResource(new byte[] {5}); + PolicyInputs inputs = new PolicyInputs(List.of(), Map.of(secondKey, List.of(supplied))); + + PolicyInputs resolved = resolver.resolve(policy, inputs); + + assertEquals("second.p12", resolved.supportingFiles().get(secondKey).get(0).getFilename()); + } + + @Test + void aRunSuppliedKeyIsLeftAlone() { + // Bindings without the asset: prefix name a file uploaded with the run, as they did before + // supporting files could be stored. + Policy policy = + policy( + null, + new PipelineStep( + "/api/v1/security/add-watermark", + Map.of(), + Map.of("watermarkImage", "company-logo"))); + Resource supplied = new ByteArrayResource(new byte[] {5}); + PolicyInputs inputs = + new PolicyInputs(List.of(), Map.of("company-logo", List.of(supplied))); + + PolicyInputs resolved = resolver.resolve(policy, inputs); + + assertSame(inputs, resolved); + assertSame(supplied, resolved.supportingFiles().get("company-logo").get(0)); + } + + @Test + void anotherTeamsAssetDoesNotResolve() { + PolicyAsset foreign = save("secret.p12", 99L, new byte[] {1}); + String key = ref(foreign.id()); + Policy policy = + policy( + 7L, + new PipelineStep( + "/api/v1/security/cert-sign", Map.of(), Map.of("p12File", key))); + + PolicyInputs resolved = resolver.resolve(policy, PolicyInputs.of(List.of())); + + assertFalse(resolved.supportingFiles().containsKey(key)); + } + + @Test + void aBindingWithOneUnresolvableIdResolvesToNothing() { + // Half a binding would run the step short a file; drop it so the executor fails the run. + PolicyAsset present = save("a.pdf", null, new byte[] {1}); + String key = ref(present.id() + ",missing-id"); + Policy policy = + policy( + null, + new PipelineStep( + "/api/v1/general/overlay-pdfs", + Map.of(), + Map.of("overlayFiles", key))); + PolicyInputs inputs = PolicyInputs.of(List.of()); + + PolicyInputs resolved = resolver.resolve(policy, inputs); + + assertSame(inputs, resolved); + assertFalse(resolved.supportingFiles().containsKey(key)); + } + + @Test + void aDeadStoredBindingDoesNotFallBackToTheRunSuppliedFile() { + // Otherwise losing the pinned asset would hand the binding straight back to the member + // running the policy - the very override stored assets are meant to prevent. + String key = ref("missing-id"); + Policy policy = + policy( + 7L, + new PipelineStep( + "/api/v1/security/cert-sign", Map.of(), Map.of("p12File", key))); + PolicyInputs inputs = + new PolicyInputs( + List.of(), Map.of(key, List.of(new ByteArrayResource(new byte[] {5})))); + + PolicyInputs resolved = resolver.resolve(policy, inputs); + + // Key absent, so the executor raises its missing-supporting-file error and the run fails. + assertFalse(resolved.supportingFiles().containsKey(key)); + } + + @Test + void aBindingWhoseSecondIdIsAnotherTeamsResolvesToNothing() { + PolicyAsset own = save("own.pdf", 7L, new byte[] {1}); + PolicyAsset foreign = save("foreign.pdf", 99L, new byte[] {2}); + String key = ref(own.id() + "," + foreign.id()); + Policy policy = + policy( + 7L, + new PipelineStep( + "/api/v1/general/overlay-pdfs", + Map.of(), + Map.of("overlayFiles", key))); + + PolicyInputs resolved = resolver.resolve(policy, PolicyInputs.of(List.of())); + + assertFalse(resolved.supportingFiles().containsKey(key)); + } + + @Test + void stepsWithoutBindingsLeaveInputsUntouched() { + Policy policy = policy(null, new PipelineStep("/api/v1/misc/compress-pdf", Map.of())); + PolicyInputs inputs = PolicyInputs.of(List.of()); + + assertSame(inputs, resolver.resolve(policy, inputs)); + assertTrue(inputs.supportingFiles().isEmpty()); + } + + private static String ref(String ids) { + return PolicyAssetRefs.PREFIX + ids; + } + + private PolicyAsset save(String name, Long teamId, byte[] content) { + return store.save( + new PolicyAsset(null, name, "application/octet-stream", 0, "owner", teamId, 1L), + content); + } + + private static Policy policy(Long teamId, PipelineStep step) { + return new Policy( + "p1", "p", "owner", true, List.of(), List.of(step), OutputSpec.inline(), teamId); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java index 811aae6b4f..0f97fcaf62 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/AdminPolicyManagementAuthorityTest.java @@ -39,6 +39,18 @@ class AdminPolicyManagementAuthorityTest { assertFalse(authority().canEditPolicies()); } + @Test + void adminMayTriggerPolicies() { + when(userService.isCurrentUserAdmin()).thenReturn(true); + assertTrue(authority().canTriggerPolicies()); + } + + @Test + void nonAdminMayNotTriggerPolicies() { + when(userService.isCurrentUserAdmin()).thenReturn(false); + assertFalse(authority().canTriggerPolicies()); + } + @Test void currentUserTeamIdResolvesFromTheCurrentUsersTeam() { Team team = new Team(); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/FolderAccessGuardTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/FolderAccessGuardTest.java index 351dcd1d2f..431d74d8b8 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/FolderAccessGuardTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/FolderAccessGuardTest.java @@ -17,6 +17,7 @@ import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.source.InProcessSourceStore; import stirling.software.proprietary.policy.source.Source; @@ -189,6 +190,13 @@ class FolderAccessGuardTest { null)) .id()) .toList(); - return new Policy("p1", "p", "owner", true, null, sourceIds, List.of(), output); + return new Policy( + "p1", + "p", + "owner", + true, + sourceIds.stream().map(PipelineInput::manual).toList(), + List.of(), + output); } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/PolicyAccessGuardTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/PolicyAccessGuardTest.java index 35c4d2631e..c3f6552dc5 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/PolicyAccessGuardTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/config/PolicyAccessGuardTest.java @@ -90,6 +90,6 @@ class PolicyAccessGuardTest { private static Policy inTeam(Long teamId) { return new Policy( - null, "p", "owner", true, null, List.of(), List.of(), OutputSpec.inline(), teamId); + null, "p", "owner", true, List.of(), List.of(), OutputSpec.inline(), teamId); } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java index cf33de8656..36f5cc221b 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyControllerTest.java @@ -2,7 +2,12 @@ package stirling.software.proprietary.policy.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -31,6 +36,7 @@ import stirling.software.common.cluster.JobStore; import stirling.software.common.cluster.inprocess.InProcessJobStore; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.job.JobResponse; +import stirling.software.common.model.tool.ToolDiagnostic; import stirling.software.common.service.JobOwnershipService; import stirling.software.common.util.TempFileManager; import stirling.software.proprietary.policy.config.PolicyAccessGuard; @@ -44,6 +50,7 @@ import stirling.software.proprietary.policy.ledger.ProcessedLedger; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.PipelineDefinition; import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.PipelineValidation; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.PolicyRun; import stirling.software.proprietary.policy.model.PolicyRunView; @@ -74,6 +81,10 @@ class PolicyControllerTest { private stirling.software.proprietary.policy.overview.PolicyOverviewService policyOverviewService; + @Mock private stirling.software.proprietary.policy.asset.PolicyAssetCleaner assetCleaner; + + @Mock private stirling.software.proprietary.policy.asset.PolicyAssetResolver assetResolver; + @Mock private ProcessedLedger processedLedger; @Mock private TempFileManager tempFileManager; @@ -105,6 +116,8 @@ class PolicyControllerTest { policyManagementAuthority, policyTriggerManager, policyOverviewService, + assetCleaner, + assetResolver, processedLedger, policyTriggers, applicationProperties, @@ -113,6 +126,59 @@ class PolicyControllerTest { jobStore); } + @Test + void validateReportsAChainThatCannotRun() { + // Delegates to PolicyValidator so the endpoint and the save-time gate cannot disagree. + ToolDiagnostic mismatch = + ToolDiagnostic.error( + 1, ToolDiagnostic.FORMAT_MISMATCH, "rotate cannot take an image"); + when(policyValidator.diagnoseChain(anyList(), any())).thenReturn(List.of(mismatch)); + + PipelineValidation.Response response = + controller.validateChain( + new PipelineValidation.Request( + List.of( + new PipelineStep( + "/api/v1/misc/extract-images", Map.of(), Map.of()), + new PipelineStep( + "/api/v1/general/rotate-pdf", Map.of(), Map.of())), + null)); + + assertFalse(response.valid()); + assertEquals(List.of(mismatch), response.diagnostics()); + } + + @Test + void validateReportsAWorkableChainAsValid() { + // Warnings and fan-out notes come back without making the chain invalid. + ToolDiagnostic fanOut = + ToolDiagnostic.info(1, ToolDiagnostic.FAN_OUT, "runs once per file"); + when(policyValidator.diagnoseChain(anyList(), any())).thenReturn(List.of(fanOut)); + + PipelineValidation.Response response = + controller.validateChain( + new PipelineValidation.Request( + List.of( + new PipelineStep( + "/api/v1/general/split-pages", Map.of(), Map.of()), + new PipelineStep( + "/api/v1/general/rotate-pdf", Map.of(), Map.of())), + null)); + + assertTrue(response.valid()); + assertEquals(List.of(fanOut), response.diagnostics()); + } + + @Test + void validateToleratesNoSteps() { + when(policyValidator.diagnoseChain(anyList(), any())).thenReturn(List.of()); + + PipelineValidation.Response response = + controller.validateChain(new PipelineValidation.Request(null, null)); + + assertTrue(response.valid()); + } + private static stirling.software.proprietary.policy.trigger.PolicyTrigger trigger( String type, boolean requiresSource, java.util.Set sourceTypes) { return new stirling.software.proprietary.policy.trigger.PolicyTrigger() { @@ -135,11 +201,11 @@ class PolicyControllerTest { private static PipelineDefinition definitionWithStep() { return new PipelineDefinition( - "pipe", List.of(new PipelineStep("/api/v1/misc/compress-pdf", null)), null); + "pipe", List.of(new PipelineStep("/api/v1/misc/compress-pdf", null)), List.of()); } private static Policy policy(String id, Long teamId) { - return new Policy(id, "name", "owner", true, null, List.of(), List.of(), null, teamId); + return new Policy(id, "name", "owner", true, List.of(), List.of(), null, teamId); } private static Policy s3OutputPolicy(String id, String secret) { @@ -150,11 +216,11 @@ class PolicyControllerTest { "bucket", "outbox", "accessKeyId", "AKIAEXAMPLE", "secretAccessKey", secret)); - return new Policy(id, "name", "owner", true, null, List.of(), List.of(), output, 1L); + return new Policy(id, "name", "owner", true, List.of(), List.of(), output, 1L); } private static PolicyRunHandle handle(String runId) { - PolicyRun run = new PolicyRun(runId, null, definitionWithStep()); + PolicyRun run = new PolicyRun(runId, null, definitionWithStep(), null, null, null); return new PolicyRunHandle(runId, CompletableFuture.completedFuture(run)); } @@ -169,7 +235,7 @@ class PolicyControllerTest { .thenReturn(handle("run-1")); ResponseEntity> response = - controller.run(definitionWithStep(), new PolicyRunFiles()); + controller.run(definitionWithStep(), null, new PolicyRunFiles()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); assertThat(response.getBody().getJobId()).isEqualTo("run-1"); @@ -182,7 +248,7 @@ class PolicyControllerTest { .thenReturn(handle("run-1")); when(sourceAccessGuard.currentTeamId()).thenReturn(3L); - controller.run(definitionWithStep(), new PolicyRunFiles()); + controller.run(definitionWithStep(), null, new PolicyRunFiles()); verify(docCounter).record(EditorSource.counterKey(3L), 0L); } @@ -190,9 +256,9 @@ class PolicyControllerTest { @Test @DisplayName("rejects a pipeline with no steps") void rejectsEmptyPipeline() { - PipelineDefinition empty = new PipelineDefinition("pipe", List.of(), null); + PipelineDefinition empty = new PipelineDefinition("pipe", List.of(), List.of()); - assertThatThrownBy(() -> controller.run(empty, new PolicyRunFiles())) + assertThatThrownBy(() -> controller.run(empty, null, new PolicyRunFiles())) .isInstanceOf(ResponseStatusException.class) .satisfies( e -> @@ -214,7 +280,7 @@ class PolicyControllerTest { .when(policyValidator) .validateOutput(any()); - assertThatThrownBy(() -> controller.run(definition, new PolicyRunFiles())) + assertThatThrownBy(() -> controller.run(definition, null, new PolicyRunFiles())) .isInstanceOf(ResponseStatusException.class) .satisfies( e -> @@ -222,6 +288,38 @@ class PolicyControllerTest { .isEqualTo(HttpStatus.BAD_REQUEST)); verify(policyRunner, never()).runAdHoc(any(), any(), any()); } + + @Test + @DisplayName("resolves stored assets from the supplied policy when the caller may edit it") + void resolvesStoredAssetsForEditor() throws Exception { + applicationProperties.getSecurity().setEnableLogin(false); // editing allowed + Policy p = policy("pol-1", 1L); + when(policyStore.get("pol-1")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + when(assetResolver.resolve(eq(p), any())).thenAnswer(inv -> inv.getArgument(1)); + when(policyRunner.runAdHoc(any(), any(), eq(PolicyProgressListener.NOOP))) + .thenReturn(handle("run-1")); + + controller.run(definitionWithStep(), "pol-1", new PolicyRunFiles()); + + verify(assetResolver).resolve(eq(p), any()); + } + + @Test + @DisplayName("does not resolve a policy's stored assets for a caller who cannot edit it") + void skipsStoredAssetsForNonEditor() throws Exception { + // Gating asset resolution to editors keeps a member from rebinding a policy's stored + // asset into an ad-hoc step to read it back. + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canEditPolicies()).thenReturn(false); + when(policyRunner.runAdHoc(any(), any(), eq(PolicyProgressListener.NOOP))) + .thenReturn(handle("run-1")); + + controller.run(definitionWithStep(), "pol-1", new PolicyRunFiles()); + + verify(assetResolver, never()).resolve(any(), any()); + verify(policyStore, never()).get(any()); + } } @Nested @@ -233,7 +331,8 @@ class PolicyControllerTest { void returnsEmitter() throws Exception { when(policyRunner.runAdHoc(any(), any(), any())).thenReturn(handle("run-2")); - SseEmitter emitter = controller.runStream(definitionWithStep(), new PolicyRunFiles()); + SseEmitter emitter = + controller.runStream(definitionWithStep(), null, new PolicyRunFiles()); assertThat(emitter).isNotNull(); } @@ -241,9 +340,9 @@ class PolicyControllerTest { @Test @DisplayName("rejects a pipeline with no steps") void rejectsEmpty() { - PipelineDefinition empty = new PipelineDefinition("pipe", List.of(), null); + PipelineDefinition empty = new PipelineDefinition("pipe", List.of(), List.of()); - assertThatThrownBy(() -> controller.runStream(empty, new PolicyRunFiles())) + assertThatThrownBy(() -> controller.runStream(empty, null, new PolicyRunFiles())) .isInstanceOf(ResponseStatusException.class); } } @@ -255,7 +354,7 @@ class PolicyControllerTest { @Test @DisplayName("returns the run view when present") void found() { - PolicyRun run = new PolicyRun("run-3", null, definitionWithStep()); + PolicyRun run = new PolicyRun("run-3", null, definitionWithStep(), null, null, null); when(runRegistry.get("run-3")).thenReturn(run); ResponseEntity response = controller.status("run-3"); @@ -284,9 +383,11 @@ class PolicyControllerTest { @Test @DisplayName("excludes ad-hoc runs and runs owned by others") void filtersRuns() { - PolicyRun adHoc = new PolicyRun("adhoc", null, definitionWithStep()); - PolicyRun ownedStored = new PolicyRun("owned", "policy-A", definitionWithStep()); - PolicyRun otherStored = new PolicyRun("other", "policy-B", definitionWithStep()); + PolicyRun adHoc = new PolicyRun("adhoc", null, definitionWithStep(), null, null, null); + PolicyRun ownedStored = + new PolicyRun("owned", "policy-A", definitionWithStep(), null, null, null); + PolicyRun otherStored = + new PolicyRun("other", "policy-B", definitionWithStep(), null, null, null); when(runRegistry.all()).thenReturn(List.of(adHoc, ownedStored, otherStored)); // ownedByCurrentUser: strip then re-apply scope reproduces the key only for the owned @@ -401,8 +502,7 @@ class PolicyControllerTest { void updatePreservesOwnership() { applicationProperties.getSecurity().setEnableLogin(false); Policy existing = - new Policy( - "p2", "name", "origOwner", true, null, List.of(), List.of(), null, 3L); + new Policy("p2", "name", "origOwner", true, List.of(), List.of(), null, 3L); when(policyStore.get("p2")).thenReturn(Optional.of(existing)); when(policyAccessGuard.canAccess(existing)).thenReturn(true); when(policyStore.save(any())).thenAnswer(i -> i.getArgument(0)); @@ -410,12 +510,27 @@ class PolicyControllerTest { ResponseEntity response = controller.savePolicy( new Policy( - "p2", "name", "forged", true, null, List.of(), List.of(), null, - 77L)); + "p2", "name", "forged", true, List.of(), List.of(), null, 77L)); assertThat(response.getBody().owner()).isEqualTo("origOwner"); assertThat(response.getBody().teamId()).isEqualTo(3L); } + + @Test + @DisplayName("hands the pre-save version to the asset cleaner") + void cleansUpAssetsTheEditDropped() { + applicationProperties.getSecurity().setEnableLogin(false); + Policy existing = + new Policy("p2", "name", "owner", true, List.of(), List.of(), null, 3L); + when(policyStore.get("p2")).thenReturn(Optional.of(existing)); + when(policyAccessGuard.canAccess(existing)).thenReturn(true); + when(policyStore.save(any())).thenAnswer(i -> i.getArgument(0)); + + controller.savePolicy( + new Policy("p2", "name", "owner", true, List.of(), List.of(), null, 3L)); + + verify(assetCleaner).cleanupAfterSave(eq(existing), any()); + } } @Nested @@ -500,6 +615,7 @@ class PolicyControllerTest { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); verify(processedLedger).clearPolicy("a"); + verify(assetCleaner).cleanupAfterDelete(p); verify(policyTriggerManager).notifyPoliciesChanged(); } @@ -659,5 +775,78 @@ class PolicyControllerTest { assertThat(((ResponseStatusException) e).getStatusCode()) .isEqualTo(HttpStatus.NOT_FOUND)); } + + @Test + @DisplayName("trigger is forbidden for a team member who cannot manage policies") + void triggerForbiddenForMember() { + // Sweeping a policy's configured sources is a policy-management capability, so being + // in the policy's team is not on its own enough to perform it. + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canTriggerPolicies()).thenReturn(false); + + assertThatThrownBy(() -> controller.trigger("a")) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + // Rejected before the policy is looked up, so no run starts. + verify(policyRunner, never()).run(any()); + verify(policyStore, never()).get(any()); + } + + @Test + @DisplayName("trigger runs for a caller who may manage policies") + void triggerAllowedForLeader() { + applicationProperties.getSecurity().setEnableLogin(true); + when(policyManagementAuthority.canTriggerPolicies()).thenReturn(true); + Policy p = policy("a", 1L); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + SweepOutcome outcome = new SweepOutcome(List.of("run-a"), 1, 0, 0, 0); + when(policyRunner.run(p)).thenReturn(outcome); + + ResponseEntity response = controller.trigger("a"); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + assertThat(response.getBody()).isEqualTo(outcome); + } + + @Test + @DisplayName("trigger skips the role check when login is disabled") + void triggerTrustsTheLocalOperator() { + // Single-user deployments have no roles at all; the gate must not lock them out of + // their + // own sweeps. + applicationProperties.getSecurity().setEnableLogin(false); + Policy p = policy("a", null); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + SweepOutcome outcome = new SweepOutcome(List.of("run-a"), 1, 0, 0, 0); + when(policyRunner.run(p)).thenReturn(outcome); + + assertThat(controller.trigger("a").getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + verify(policyManagementAuthority, never()).canTriggerPolicies(); + } + + @Test + @DisplayName("running a policy over the caller's own files stays open to any member") + void storedRunIsNotGatedByRole() { + // Editor enforcement: every member's upload/export runs the team's stored policies on + // their own documents. Gating this the way the sweep is gated would break the editor. + applicationProperties.getSecurity().setEnableLogin(true); + Policy p = policy("a", 1L); + when(policyStore.get("a")).thenReturn(Optional.of(p)); + when(policyAccessGuard.canAccess(p)).thenReturn(true); + when(policyRunner.runWith(eq(p), any(), eq(PolicyProgressListener.NOOP))) + .thenReturn(handle("run-9")); + + ResponseEntity> response = + assertDoesNotThrow(() -> controller.runStoredPolicy("a", new PolicyRunFiles())); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + verify(policyManagementAuthority, never()).canTriggerPolicies(); + verify(policyManagementAuthority, never()).canEditPolicies(); + } } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyRunRoutesTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyRunRoutesTest.java index f1be12ae1d..e7081ec8ca 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyRunRoutesTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/controller/PolicyRunRoutesTest.java @@ -72,7 +72,9 @@ class PolicyRunRoutesTest { private static final Map EXPECTED_ID_EXECUTES = Map.of( "/api/v1/policies/{policyId}/run", true, - "/api/v1/policies/{policyId}/trigger", true); + "/api/v1/policies/{policyId}/trigger", true, + // Checks whether a chain could run; runs and stores nothing, so not billable. + "/api/v1/policies/validate", false); /** * Fail-safe: this matcher is the sole billing gate, so an unmatched execute route would run diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyEngineTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyEngineTest.java index 78348fa5c4..1efe094fe4 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyEngineTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyEngineTest.java @@ -3,23 +3,30 @@ package stirling.software.proprietary.policy.engine; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; @@ -37,6 +44,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpClientErrorException; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.job.ResultFile; import stirling.software.common.service.FileStorage; import stirling.software.common.service.FileStorage.StoredFile; import stirling.software.common.service.InternalApiClient; @@ -47,6 +55,9 @@ import stirling.software.common.service.TaskManager; import stirling.software.common.service.ToolMetadataService; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileRegistry; +import stirling.software.proprietary.failure.PolicyFailureRecorder; +import stirling.software.proprietary.policy.asset.InProcessPolicyAssetStore; +import stirling.software.proprietary.policy.asset.PolicyAssetResolver; import stirling.software.proprietary.policy.model.OutputSpec; import stirling.software.proprietary.policy.model.PipelineDefinition; import stirling.software.proprietary.policy.model.PipelineStep; @@ -55,7 +66,11 @@ import stirling.software.proprietary.policy.model.PolicyInputs; import stirling.software.proprietary.policy.model.PolicyRun; import stirling.software.proprietary.policy.model.PolicyRunStatus; import stirling.software.proprietary.policy.output.InlineOutputSink; +import stirling.software.proprietary.policy.output.OutputDelivery; +import stirling.software.proprietary.policy.output.PolicyOutputResolver; +import stirling.software.proprietary.policy.output.PolicyOutputSink; import stirling.software.proprietary.policy.progress.PolicyProgressListener; +import stirling.software.proprietary.policy.source.InProcessSourceStore; import tools.jackson.databind.json.JsonMapper; @@ -78,9 +93,11 @@ class PolicyEngineTest { @Mock private JobOwnershipService jobOwnershipService; @Mock private ResourceMonitor resourceMonitor; @Mock private JobQueue jobQueue; + @Mock private PolicyFailureRecorder failureRecorder; @TempDir Path tempDir; + private final RecordingSink recordingSink = new RecordingSink(); private PolicyRunRegistry registry; private PolicyEngine engine; @@ -98,16 +115,20 @@ class PolicyEngineTest { JsonMapper.builder().build()); registry = new PolicyRunRegistry(new ApplicationProperties()); InlineOutputSink sink = new InlineOutputSink(fileStorage); + PolicyOutputResolver outputResolver = new PolicyOutputResolver(new InProcessSourceStore()); engine = new PolicyEngine( executor, taskManager, registry, + failureRecorder, fileStorage, jobOwnershipService, - List.of(sink), + List.of(sink, recordingSink), + outputResolver, resourceMonitor, - jobQueue); + jobQueue, + new PolicyAssetResolver(new InProcessPolicyAssetStore())); // Identity scoping: the run id is the generated UUID unchanged. Lenient because the // resume/cancel tests do not submit a run. @@ -156,6 +177,36 @@ class PolicyEngineTest { verify(taskManager, atLeastOnce()).addNote(eq(runId), anyString()); } + @Test + void deliversTheRunsFilesToEveryDestination() throws Exception { + when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(anyString())).thenReturn(false); + stubEndpoint(COMPRESS, pdf("compressed", "out.pdf")); + + // Two destinations of a recording sink; each fully reads the (shared) result file, so this + // also proves the result Resources are re-readable across more than one delivery. + PipelineDefinition definition = + new PipelineDefinition( + "multi", + List.of(new PipelineStep(COMPRESS, Map.of())), + List.of( + new OutputSpec("record", Map.of("dest", "a")), + new OutputSpec("record", Map.of("dest", "b")))); + + PolicyRun run = + engine.submit( + definition, + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP) + .completion() + .get(10, TimeUnit.SECONDS); + + assertEquals(PolicyRunStatus.COMPLETED, run.getStatus()); + // One result file per destination, and each destination read the same output content. + assertEquals(2, run.getOutputs().size()); + assertEquals(List.of("a:compressed", "b:compressed"), recordingSink.deliveries()); + } + @Test void submitFailsRunWhenAToolErrors() throws Exception { when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); @@ -172,6 +223,206 @@ class PolicyEngineTest { assertEquals(PolicyRunStatus.FAILED, run.getStatus()); verify(taskManager).setError(eq(runId), anyString()); verify(taskManager, never()).setComplete(runId); + // A failed run is recorded durably, so an admin can see it after the in-memory run expires. + verify(failureRecorder) + .recordRunFailure( + eq(runId), any(), any(), any(), any(), anyString(), any(Throwable.class)); + } + + @Test + void recordsWhichSourceFedAFailedRun() throws Exception { + // The source is threaded onto the run so an unattended failure is attributable: there is no + // user to name for a file that arrived from a bucket. The actor is asserted null rather + // than + // any(): a loose matcher here is what let the owner be recorded as the actor unnoticed. + when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); + when(internalApiClient.post(eq(ROTATE), any())).thenThrow(new RuntimeException("boom")); + + PolicyRunHandle handle = + engine.runPolicy( + policyOwnedBy("owner"), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP, + "src-s3-invoices", + "file-hash-1"); + handle.completion().get(10, TimeUnit.SECONDS); + + verify(failureRecorder) + .recordRunFailure( + anyString(), + any(), + eq("src-s3-invoices"), + eq("file-hash-1"), + isNull(), + anyString(), + any(Throwable.class)); + } + + @Test + void anAttendedFailureIsRecordedAgainstWhoTriggeredItNotThePolicysOwner() throws Exception { + // Bob runs Alice's shared policy on his own upload and it fails. The row must name Bob: he + // is the one whose browser holds the document, and a member's read scope narrows to their + // own rows, so filing it under Alice hides it from the only person who can act on it. + when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); + when(internalApiClient.post(eq(ROTATE), any())).thenThrow(new RuntimeException("boom")); + + MDC.put("auditPrincipal", "bob"); // the request thread's acting user + try { + engine.runPolicy( + policyOwnedBy("alice"), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP, + null, + "bob-doc-1") + .completion() + .get(10, TimeUnit.SECONDS); + } finally { + MDC.remove("auditPrincipal"); + } + + verify(failureRecorder) + .recordRunFailure( + anyString(), + any(), + isNull(), + eq("bob-doc-1"), + eq("bob"), + anyString(), + any(Throwable.class)); + } + + @Test + void anUnattendedFailureIsRecordedWithNoActorWhileStillBillingTheOwner() throws Exception { + // The two identities are deliberately different, and this pins both at once: usage is + // charged to the owner (MDC audit principal on the worker), but the failure has no actor, + // which is what makes it UNOWNED and hands the owner actions to the team's reviewer. + when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); + String[] principalAtDispatch = {""}; + when(internalApiClient.post(eq(ROTATE), any())) + .thenAnswer( + invocation -> { + principalAtDispatch[0] = MDC.get("auditPrincipal"); + throw new RuntimeException("boom"); + }); + + // No MDC and no security context: exactly a trigger-fired sweep. + engine.runPolicy( + policyOwnedBy("alice"), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP, + "src-watched-folder", + "file-hash-1") + .completion() + .get(10, TimeUnit.SECONDS); + + assertEquals("alice", principalAtDispatch[0], "billing must still be the policy owner"); + verify(failureRecorder) + .recordRunFailure( + anyString(), + any(), + eq("src-watched-folder"), + eq("file-hash-1"), + isNull(), + anyString(), + any(Throwable.class)); + } + + @Test + void anAdHocFailureIsRecordedAgainstTheSubmittingUser() throws Exception { + // An ad-hoc run has no stored policy, so the submitter is both payer and actor. Asserted so + // the two entry points cannot drift apart. + when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); + when(internalApiClient.post(eq(ROTATE), any())).thenThrow(new RuntimeException("boom")); + + MDC.put("auditPrincipal", "bob"); + try { + engine.submit( + definition(new PipelineStep(ROTATE, Map.of())), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP) + .completion() + .get(10, TimeUnit.SECONDS); + } finally { + MDC.remove("auditPrincipal"); + } + + verify(failureRecorder) + .recordRunFailure( + anyString(), + any(), + any(), + any(), + eq("bob"), + anyString(), + any(Throwable.class)); + } + + @Test + void aRunRefusedAtAdmissionIsRecordedAgainstWhoeverTriggeredIt() throws Exception { + // The queue-full path records its own row, and it is attended: the user is still holding + // the + // document, so it must reach them rather than landing as an ownerless incident. + when(resourceMonitor.shouldQueueJob(anyInt())).thenReturn(true); + CompletableFuture rejected = new CompletableFuture<>(); + rejected.completeExceptionally(new RuntimeException("Job queue full")); + doReturn(rejected).when(jobQueue).queueJob(anyString(), anyInt(), any(), anyLong()); + + MDC.put("auditPrincipal", "bob"); + try { + engine.runPolicy( + policyOwnedBy("alice"), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP, + null, + "bob-doc-1"); + } finally { + MDC.remove("auditPrincipal"); + } + + verify(failureRecorder) + .recordRunFailureAs(any(), anyString(), any(), isNull(), eq("bob"), anyString()); + } + + @Test + void recordingAFailureNeverChangesTheRunsOutcome() throws Exception { + // Recording is best-effort: losing the incident row is bad, but turning a classified + // failure + // into a different, confusing failure is worse. + when(toolMetadataService.isMultiInput(ROTATE)).thenReturn(false); + when(internalApiClient.post(eq(ROTATE), any())).thenThrow(new RuntimeException("boom")); + doThrow(new RuntimeException("event store unavailable")) + .when(failureRecorder) + .recordRunFailure( + anyString(), any(), any(), any(), any(), anyString(), any(Throwable.class)); + + PolicyRunHandle handle = + engine.submit( + definition(new PipelineStep(ROTATE, Map.of())), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP); + PolicyRun run = handle.completion().get(10, TimeUnit.SECONDS); + + assertEquals(PolicyRunStatus.FAILED, run.getStatus()); + // The original failure message survives, rather than being replaced by the store's. + assertTrue(run.getError().contains("boom")); + } + + @Test + void successfulRunRecordsNoFailureEvent() throws Exception { + when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(anyString())).thenReturn(false); + stubEndpoint(ROTATE, pdf("rotated", "rotated.pdf")); + when(fileStorage.storeInputStream(any(InputStream.class), anyString())) + .thenReturn(new StoredFile("file-1", 7L)); + + engine.submit( + definition(new PipelineStep(ROTATE, Map.of())), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP) + .completion() + .get(10, TimeUnit.SECONDS); + + verifyNoInteractions(failureRecorder); } @Test @@ -222,7 +473,7 @@ class PolicyEngineTest { "rotate", "owner", true, - null, + List.of(), List.of(new PipelineStep(ROTATE, Map.of())), OutputSpec.inline()); @@ -266,7 +517,7 @@ class PolicyEngineTest { "rotate", "alice", true, - null, + List.of(), List.of(new PipelineStep(ROTATE, Map.of())), OutputSpec.inline()); @@ -367,12 +618,60 @@ class PolicyEngineTest { assertFalse(engine.cancel("does-not-exist")); } + @Test + void cancellingARunRecordsNoFailureEvent() throws Exception { + // A cancellation is an intended outcome, not an incident. Recording one would put a row in + // front of an admin describing something a user deliberately did. + // + // The cancel happens while the tool call is in flight, held on a latch: an earlier version + // of this test awaited completion first, and cancel() on a finished run is a documented + // no-op, so it asserted nothing. + when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(anyString())).thenReturn(false); + CountDownLatch toolEntered = new CountDownLatch(1); + CountDownLatch releaseTool = new CountDownLatch(1); + when(internalApiClient.post(eq(ROTATE), any())) + .thenAnswer( + invocation -> { + toolEntered.countDown(); + assertTrue( + releaseTool.await(10, TimeUnit.SECONDS), + "test never released the tool call"); + return ResponseEntity.ok(pdf("rotated", "rotated.pdf")); + }); + when(fileStorage.storeInputStream(any(InputStream.class), anyString())) + .thenReturn(new StoredFile("file-1", 7L)); + + PolicyRunHandle handle = + engine.submit( + definition(new PipelineStep(ROTATE, Map.of())), + PolicyInputs.of(List.of(pdf("input", "input.pdf"))), + PolicyProgressListener.NOOP); + assertTrue(toolEntered.await(10, TimeUnit.SECONDS), "run never reached the tool call"); + assertTrue(engine.cancel(handle.runId()), "cancel was a no-op, so this asserts nothing"); + releaseTool.countDown(); + handle.completion().get(10, TimeUnit.SECONDS); + + verifyNoInteractions(failureRecorder); + } + // --- helpers --- private static PipelineDefinition definition(PipelineStep... steps) { return new PipelineDefinition("test", List.of(steps), OutputSpec.inline()); } + private static Policy policyOwnedBy(String owner) { + return new Policy( + "p1", + "rotate", + owner, + true, + List.of(), + List.of(new PipelineStep(ROTATE, Map.of())), + OutputSpec.inline()); + } + private void stubEndpoint(String endpoint, Resource body) { when(internalApiClient.post(eq(endpoint), any())).thenReturn(ResponseEntity.ok(body)); } @@ -385,4 +684,50 @@ class PolicyEngineTest { } }; } + + /** + * A test output sink (type "record") that fully reads each delivered file and records + * "{dest}:{content}" per file, so a test can assert the run was delivered to every destination. + */ + private static final class RecordingSink implements PolicyOutputSink { + + private final List deliveries = new ArrayList<>(); + + List deliveries() { + return deliveries; + } + + @Override + public String type() { + return "record"; + } + + @Override + public boolean supports(OutputSpec spec) { + return spec != null && "record".equals(spec.type()); + } + + @Override + public List deliver( + OutputDelivery delivery, List outputs, OutputSpec spec) + throws IOException { + String dest = String.valueOf(spec.options().get("dest")); + List results = new ArrayList<>(); + for (Resource file : outputs) { + byte[] bytes; + try (InputStream is = file.getInputStream()) { + bytes = is.readAllBytes(); + } + deliveries.add(dest + ":" + new String(bytes)); + results.add( + ResultFile.builder() + .fileId("rec-" + dest) + .fileName(dest + "/" + file.getFilename()) + .contentType("application/pdf") + .fileSize(bytes.length) + .build()); + } + return results; + } + } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java index 4d9ea1deb7..6e2ab66df7 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyExecutorTest.java @@ -1,6 +1,7 @@ package stirling.software.proprietary.policy.engine; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -299,7 +300,11 @@ class PolicyExecutorTest { definition(new PipelineStep(compress, Map.of())), PolicyInputs.of(List.of(pdf("img", "image.png"))), PolicyProgressListener.NOOP)); - assertTrue(ex.getMessage().contains("image.png")); + // Names the rejected extension, never the document. This message becomes the run's error + // and + // is persisted on the failure record, which holds no document name. + assertTrue(ex.getMessage().contains("png")); + assertFalse(ex.getMessage().contains("image.png")); // Type check happens before any dispatch. verify(internalApiClient, never()).post(anyString(), any()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunRegistryTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunRegistryTest.java index 1549ba62ac..692fe84c37 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunRegistryTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunRegistryTest.java @@ -95,7 +95,14 @@ class PolicyRunRegistryTest { } private PolicyRun register(String runId) { - PolicyRun run = new PolicyRun(runId, null, new PipelineDefinition(runId, List.of(), null)); + PolicyRun run = + new PolicyRun( + runId, + null, + new PipelineDefinition(runId, List.of(), List.of()), + null, + null, + null); registry.register(run); return run; } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunnerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunnerTest.java index e21f371305..d6f5951e3a 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunnerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyRunnerTest.java @@ -36,6 +36,7 @@ import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; import stirling.software.proprietary.policy.ledger.ProcessedLedger; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.PolicyInputs; @@ -77,13 +78,13 @@ class PolicyRunnerTest { @Test void runsOnceWithNoFilesWhenThePolicyHasNoSources() { Policy policy = policy(List.of()); - when(policyEngine.runPolicy(eq(policy), any(), any())) + when(policyEngine.runPolicy(eq(policy), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", new CompletableFuture<>())); runner.run(policy); ArgumentCaptor inputs = ArgumentCaptor.forClass(PolicyInputs.class); - verify(policyEngine).runPolicy(eq(policy), inputs.capture(), any()); + verify(policyEngine).runPolicy(eq(policy), inputs.capture(), any(), any(), any()); assertTrue(inputs.getValue().primary().isEmpty()); // Ledger hygiene still runs: rows recorded for a generator policy's folder outputs // are pruned by its own sweeps rather than accumulating until the policy is deleted. @@ -136,12 +137,12 @@ class PolicyRunnerTest { List.of( ResolvedInput.of(PolicyInputs.of(List.of())), ResolvedInput.of(PolicyInputs.of(List.of())))); - when(policyEngine.runPolicy(any(), any(), any())) + when(policyEngine.runPolicy(any(), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", new CompletableFuture<>())); runner.run(policy); - verify(policyEngine, times(2)).runPolicy(eq(policy), any(), any()); + verify(policyEngine, times(2)).runPolicy(eq(policy), any(), any(), any(), any()); } @Test @@ -149,11 +150,11 @@ class PolicyRunnerTest { InputSpec spec = InputSpec.folder("/in"); Policy policy = policy(List.of(spec)); AtomicBoolean outcome = new AtomicBoolean(false); - ResolvedInput unit = new ResolvedInput(PolicyInputs.of(List.of()), outcome::set); + ResolvedInput unit = new ResolvedInput(PolicyInputs.of(List.of()), null, outcome::set); when(folderSource.supports(spec)).thenReturn(true); when(folderSource.resolve(eq(spec), any())).thenReturn(List.of(unit)); CompletableFuture completion = new CompletableFuture<>(); - when(policyEngine.runPolicy(any(), any(), any())) + when(policyEngine.runPolicy(any(), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", completion)); runner.run(policy); @@ -170,11 +171,11 @@ class PolicyRunnerTest { InputSpec spec = InputSpec.folder("/in"); Policy policy = policy(List.of(spec)); AtomicBoolean outcome = new AtomicBoolean(true); - ResolvedInput unit = new ResolvedInput(PolicyInputs.of(List.of()), outcome::set); + ResolvedInput unit = new ResolvedInput(PolicyInputs.of(List.of()), null, outcome::set); when(folderSource.supports(spec)).thenReturn(true); when(folderSource.resolve(eq(spec), any())).thenReturn(List.of(unit)); CompletableFuture completion = new CompletableFuture<>(); - when(policyEngine.runPolicy(any(), any(), any())) + when(policyEngine.runPolicy(any(), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", completion)); runner.run(policy); @@ -223,12 +224,12 @@ class PolicyRunnerTest { when(folderSource.supports(spec)).thenReturn(true); when(folderSource.resolve(eq(spec), any())) .thenReturn(List.of(ResolvedInput.of(PolicyInputs.of(List.of())))); - when(policyEngine.runPolicy(any(), any(), any())) + when(policyEngine.runPolicy(any(), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", new CompletableFuture<>())); runner.run(policy, SweepKind.LIGHT); - verify(policyEngine).runPolicy(eq(policy), any(), any()); + verify(policyEngine).runPolicy(eq(policy), any(), any(), any(), any()); verify(processedLedger, never()).markSeen(any(), any()); verify(processedLedger, never()).deleteUnseen(any(), anyLong()); } @@ -243,12 +244,13 @@ class PolicyRunnerTest { when(folderSource.resolve(eq(broken), any())).thenThrow(new IOException("mount gone")); when(folderSource.resolve(eq(healthy), any())) .thenReturn(List.of(ResolvedInput.of(PolicyInputs.of(List.of())))); - when(policyEngine.runPolicy(any(), any(), any())) + when(policyEngine.runPolicy(any(), any(), any(), any(), any())) .thenReturn(new PolicyRunHandle("r", new CompletableFuture<>())); runner.run(policy); - verify(policyEngine).runPolicy(eq(policy), any(), any()); // healthy source still ran + verify(policyEngine) + .runPolicy(eq(policy), any(), any(), any(), any()); // healthy source still ran verify(processedLedger, never()).deleteUnseen(any(), anyLong()); // history preserved } @@ -306,7 +308,6 @@ class PolicyRunnerTest { "p", "owner", true, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline(), @@ -338,8 +339,7 @@ class PolicyRunnerTest { "p", "owner", true, - null, - sourceIds, + sourceIds.stream().map(PipelineInput::manual).toList(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyValidatorTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyValidatorTest.java index 2e9613a024..2928dd3b65 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyValidatorTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/engine/PolicyValidatorTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.when; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,9 +18,20 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import stirling.software.common.model.tool.ToolArity; +import stirling.software.common.model.tool.ToolFormat; +import stirling.software.common.model.tool.ToolIOSource; +import stirling.software.common.model.tool.ToolIOSpec; +import stirling.software.common.service.ToolChainValidator; +import stirling.software.proprietary.policy.asset.InProcessPolicyAssetStore; +import stirling.software.proprietary.policy.asset.PolicyAsset; +import stirling.software.proprietary.policy.asset.PolicyAssetRefs; +import stirling.software.proprietary.policy.asset.PolicyAssetStore; import stirling.software.proprietary.policy.input.InputSource; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; +import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.TriggerConfig; import stirling.software.proprietary.policy.output.PolicyOutputSink; @@ -38,6 +50,7 @@ class PolicyValidatorTest { @Mock private PipelineStepValidator stepValidator; private final SourceStore sourceStore = new InProcessSourceStore(); + private final PolicyAssetStore assetStore = new InProcessPolicyAssetStore(); private PolicyValidator validator; @BeforeEach @@ -48,7 +61,9 @@ class PolicyValidatorTest { List.of(inputSource), List.of(outputSink), List.of(stepValidator), - sourceStore); + sourceStore, + assetStore, + new ToolChainValidator(path -> java.util.Optional.empty())); } @Test @@ -60,25 +75,27 @@ class PolicyValidatorTest { validator.validate(policy); - verify(trigger).validate(policy); + verify(trigger).validate(policy, policy.inputs().get(0)); verify(inputSource).validate(InputSpec.folder("/in")); verify(outputSink).validate(policy.output()); } @Test - void skipsTriggerValidationForAManualOnlyPolicy() { + void skipsTriggerValidationForAManualOnlyInput() { when(inputSource.supports(any())).thenReturn(true); when(outputSink.supports(any())).thenReturn(true); validator.validate(manualOnly()); - verify(trigger, never()).validate(any()); + verify(trigger, never()).validate(any(), any()); } @Test void surfacesAnInvalidConfigFromAHandler() { when(trigger.type()).thenReturn("schedule"); - doThrow(new IllegalArgumentException("invalid schedule")).when(trigger).validate(any()); + doThrow(new IllegalArgumentException("invalid schedule")) + .when(trigger) + .validate(any(), any()); IllegalArgumentException ex = assertThrows( @@ -109,6 +126,90 @@ class PolicyValidatorTest { () -> validator.validateOutput(new OutputSpec("s3", Map.of("connectionId", 1)))); } + @Test + void acceptsAStepBindingThatReferencesATeamAsset() { + when(inputSource.supports(any())).thenReturn(true); + when(outputSink.supports(any())).thenReturn(true); + PolicyAsset asset = + assetStore.save( + new PolicyAsset(null, "logo.png", null, 0, "owner", null, 1L), + new byte[] {1}); + + validator.validate(withFileBinding(PolicyAssetRefs.PREFIX + asset.id(), null)); + } + + @Test + void acceptsARunSuppliedFileKey() { + // No asset: prefix, so the binding names a file uploaded with the run - it existed before + // stored assets did, and pausing such a policy must not start failing. + when(inputSource.supports(any())).thenReturn(true); + when(outputSink.supports(any())).thenReturn(true); + + validator.validate(withFileBinding("company-logo", null)); + } + + @Test + void rejectsAStepBindingToAnUnknownAsset() { + when(inputSource.supports(any())).thenReturn(true); + + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> + validator.validate( + withFileBinding( + PolicyAssetRefs.PREFIX + "missing-asset", null))); + assertTrue(ex.getMessage().contains("unknown stored file")); + } + + @Test + void rejectsAStepBindingToAnotherTeamsAsset() { + when(inputSource.supports(any())).thenReturn(true); + PolicyAsset foreign = + assetStore.save( + new PolicyAsset(null, "secret.p12", null, 0, "owner", 99L, 1L), + new byte[] {1}); + + // Policy has no team; the asset belongs to team 99 - must read as unknown, not leak. + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> + validator.validate( + withFileBinding( + PolicyAssetRefs.PREFIX + foreign.id(), null))); + assertTrue(ex.getMessage().contains("unknown stored file")); + } + + @Test + void rejectsAnAssetBindingWithNoIds() { + when(inputSource.supports(any())).thenReturn(true); + + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> validator.validate(withFileBinding(PolicyAssetRefs.PREFIX, null))); + assertTrue(ex.getMessage().contains("empty file binding")); + } + + /** A manual-only policy whose single step binds a file field to the given asset key. */ + private Policy withFileBinding(String assetKey, Long teamId) { + PipelineStep step = + new PipelineStep( + "/api/v1/security/add-watermark", + Map.of(), + Map.of("watermarkImage", assetKey)); + return new Policy( + "p1", + "p", + "owner", + true, + List.of(PipelineInput.manual(folderSourceId())), + List.of(step), + OutputSpec.inline(), + teamId); + } + @Test void rejectsAnUnknownTriggerType() { when(trigger.type()).thenReturn("schedule"); @@ -120,14 +221,125 @@ class PolicyValidatorTest { assertTrue(ex.getMessage().contains("unknown trigger type")); } + @Test + void rejectsAChainWhoseStepsCannotRunOnEachOther() { + // Extracting images then rotating them saves fine today and fails part-way through the + // first run, which for a scheduled policy can be long after the mistake was made. + // The chain is checked before the output, so the sink is never reached here. + when(inputSource.supports(any())).thenReturn(true); + PolicyValidator strict = validatorWith(IMAGE_THEN_PDF); + + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> strict.validate(withSteps(EXTRACT_IMAGES, ROTATE))); + + assertTrue(ex.getMessage().contains("cannot run in this order"), ex.getMessage()); + } + + @Test + void acceptsAChainWhoseStepsLineUp() { + when(inputSource.supports(any())).thenReturn(true); + when(outputSink.supports(any())).thenReturn(true); + PolicyValidator strict = validatorWith(IMAGE_THEN_PDF); + + strict.validate(withSteps(ROTATE, ROTATE)); + + verify(outputSink).validate(any()); + } + + private static final String EXTRACT_IMAGES = "/api/v1/misc/extract-images"; + private static final String ROTATE = "/api/v1/general/rotate-pdf"; + + private static final ToolIOSource IMAGE_THEN_PDF = + ToolIOSource.of( + Map.of( + EXTRACT_IMAGES, + new ToolIOSpec( + Set.of(ToolFormat.PDF), + ToolFormat.IMAGE, + ToolArity.SIMO, + List.of()), + ROTATE, + new ToolIOSpec( + Set.of(ToolFormat.PDF), + ToolFormat.PDF, + ToolArity.SISO, + List.of()))); + + private PolicyValidator validatorWith(ToolIOSource toolIO) { + return new PolicyValidator( + List.of(trigger), + List.of(inputSource), + List.of(outputSink), + List.of(stepValidator), + sourceStore, + assetStore, + new ToolChainValidator(toolIO)); + } + + private Policy withSteps(String... operations) { + return new Policy( + "p1", + "p", + "owner", + true, + List.of(PipelineInput.manual(folderSourceId())), + java.util.Arrays.stream(operations) + .map(op -> new PipelineStep(op, Map.of(), Map.of())) + .toList(), + OutputSpec.inline()); + } + + // The one-input/one-output caps are a product decision, not a model limit: the lists stay so + // multiple can be supported later, but saving more than one of either is rejected today. + + @Test + void rejectsMoreThanOneInput() { + Policy twoInputs = + new Policy( + "p1", + "p", + "owner", + true, + List.of( + PipelineInput.manual(folderSourceId()), + PipelineInput.manual(folderSourceId())), + List.of(), + OutputSpec.inline()); + + IllegalArgumentException ex = + assertThrows(IllegalArgumentException.class, () -> validator.validate(twoInputs)); + assertTrue(ex.getMessage().contains("at most one input")); + } + + @Test + void rejectsMoreThanOneOutput() { + Policy twoOutputs = manualOnly().withOutputIds(List.of("out-a", "out-b")); + + IllegalArgumentException ex = + assertThrows(IllegalArgumentException.class, () -> validator.validate(twoOutputs)); + assertTrue(ex.getMessage().contains("at most one output")); + } + + @Test + void allowsZeroInputsAndZeroOutputs() { + when(outputSink.supports(any())).thenReturn(true); + Policy bare = + new Policy("p1", "p", "owner", true, List.of(), List.of(), OutputSpec.inline()); + + validator.validate(bare); + } + private Policy policy(String triggerType) { return new Policy( "p1", "p", "owner", true, - new TriggerConfig(triggerType, Map.of()), - List.of(folderSourceId()), + List.of( + new PipelineInput( + folderSourceId(), new TriggerConfig(triggerType, Map.of()))), List.of(), OutputSpec.inline()); } @@ -138,8 +350,7 @@ class PolicyValidatorTest { "p", "owner", true, - null, - List.of(folderSourceId()), + List.of(PipelineInput.manual(folderSourceId())), List.of(), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/FtpNetworkSourceIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/FtpNetworkSourceIntegrationTest.java new file mode 100644 index 0000000000..567ed4e7cf --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/FtpNetworkSourceIntegrationTest.java @@ -0,0 +1,199 @@ +package stirling.software.proprietary.policy.network; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.FixedHostPortGenericContainer; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.access.service.OwnershipService; +import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; +import stirling.software.proprietary.policy.input.ResolveContext; +import stirling.software.proprietary.policy.input.ResolvedInput; +import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; +import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.security.service.UserService; + +/** + * End-to-end {@link NetworkInputSource} test over FTP against a real vsftpd server, through the + * production {@link FtpFileClient}: listing, claiming, streaming the actual bytes (exercising the + * passive data channel and {@code completePendingCommand}), consume delete, and save-time + * validation. Passive FTP hands the client a data port, so the control and one passive port are + * pinned to fixed host ports and the server advertises 127.0.0.1 - the only way this works through + * Testcontainers' per-run port mapping. + */ +@Testcontainers(disabledWithoutDocker = true) +@EnabledIfEnvironmentVariable( + named = "RUN_NETWORK_INTEGRATION_TESTS", + matches = "true", + disabledReason = + "Spins up SFTP/FTP/SMB containers; opt-in to keep several heavy containers off the" + + " standard CI runner. Run with RUN_NETWORK_INTEGRATION_TESTS=true.") +class FtpNetworkSourceIntegrationTest { + + private static final String POLICY = "p1"; + private static final String USER = "stirling"; + private static final String PASS = "secret"; + private static final String HOME = "/ftp/stirling"; + // Passive FTP needs the data port pinned to a known host port, so pick two free ones up front + // (rather than hard-coding) to avoid clashing with anything else on a CI runner. + private static final int CONTROL_PORT; + private static final int PASSIVE_PORT; + + static { + try (java.net.ServerSocket control = new java.net.ServerSocket(0); + java.net.ServerSocket passive = new java.net.ServerSocket(0)) { + CONTROL_PORT = control.getLocalPort(); + PASSIVE_PORT = passive.getLocalPort(); + } catch (java.io.IOException e) { + throw new java.io.UncheckedIOException(e); + } + } + + @org.testcontainers.junit.jupiter.Container + static GenericContainer ftp = + new FixedHostPortGenericContainer<>("delfer/alpine-ftp-server:latest") + .withFixedExposedPort(CONTROL_PORT, 21) + .withFixedExposedPort(PASSIVE_PORT, PASSIVE_PORT) + .withEnv("USERS", USER + "|" + PASS + "|" + HOME + "|1000") + .withEnv("ADDRESS", "127.0.0.1") + .withEnv("MIN_PORT", String.valueOf(PASSIVE_PORT)) + .withEnv("MAX_PORT", String.valueOf(PASSIVE_PORT)) + .waitingFor(Wait.forListeningPort()); + + private NetworkInputSource source; + private InProcessProcessedLedger ledger; + private RecordingContext ctx; + + @BeforeEach + void setUp() { + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(true); + RemoteFileClientFactory factory = + new RemoteFileClientFactory(new NetworkHostGuard(properties)); + NetworkConnectionResolver resolver = + new NetworkConnectionResolver( + mock(IntegrationConfigRepository.class), + mock(OwnershipService.class), + mock(UserService.class)); + source = new NetworkInputSource(resolver, factory); + ledger = new InProcessProcessedLedger(); + ctx = new RecordingContext(); + } + + @Test + void consumeListsStreamsAndDeletes() throws Exception { + put("doc.pdf", "hello ftp"); + + List work = source.resolve(spec(Map.of()), ctx); + + assertThat(work).hasSize(1); + assertThat(read(work.get(0))).isEqualTo("hello ftp"); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + + work.get(0).onComplete().accept(true); + assertThat(exists("doc.pdf")).isFalse(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void aFailedFileStaysOnTheServer() throws Exception { + put("doc.pdf", "data"); + + source.resolve(spec(Map.of()), ctx).get(0).onComplete().accept(false); + + assertThat(exists("doc.pdf")).isTrue(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void validateConnectsAndRejectsBadCredentials() { + source.validate(spec(Map.of())); + + Map wrong = new HashMap<>(baseOptions()); + wrong.put("password", "not-the-password"); + assertThatThrownBy(() -> source.validate(new InputSpec("ftp", wrong))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot access"); + } + + private Map baseOptions() { + Map options = new HashMap<>(); + options.put("protocol", "ftp"); + options.put("host", "127.0.0.1"); + options.put("port", String.valueOf(CONTROL_PORT)); + options.put("username", USER); + options.put("password", PASS); + return options; + } + + private InputSpec spec(Map extra) { + Map options = new HashMap<>(baseOptions()); + options.putAll(extra); + return new InputSpec("ftp", options); + } + + private void put(String name, String content) throws Exception { + String path = HOME + "/" + name; + exec("printf '%s' '" + content + "' > " + path + " && chown 1000 " + path); + } + + private boolean exists(String name) throws Exception { + return exec("test -f " + HOME + "/" + name + " && echo yes || echo no").contains("yes"); + } + + private String exec(String script) throws Exception { + Container.ExecResult result = ftp.execInContainer("sh", "-c", script); + return result.getStdout() + result.getStderr(); + } + + private static String read(ResolvedInput unit) throws IOException { + try (InputStream stream = unit.inputs().primary().get(0).getInputStream()) { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private class RecordingContext implements ResolveContext { + + private final List present = new ArrayList<>(); + + @Override + public boolean claim(String identity, String gate, Supplier contentHash) { + return ledger.claim(POLICY, identity, gate, contentHash); + } + + @Override + public void settle( + String identity, String finalGate, String finalContentHash, boolean success) { + ledger.settle(POLICY, identity, finalGate, finalContentHash, success); + } + + @Override + public boolean allSettledDone(String identity) { + return ledger.allSettledDone(identity); + } + + @Override + public void reportPresent(Collection identities) { + present.addAll(identities); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkConfigTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkConfigTest.java new file mode 100644 index 0000000000..3fb48501f5 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkConfigTest.java @@ -0,0 +1,136 @@ +package stirling.software.proprietary.policy.network; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class NetworkConfigTest { + + private static Map base(String protocol) { + Map options = new LinkedHashMap<>(); + options.put("protocol", protocol); + options.put("host", "files.example.com"); + options.put("username", "u"); + options.put("password", "p"); + return options; + } + + @Test + void parsesAnSftpConnectionWithDefaultPort() { + NetworkConfig config = NetworkConfig.from(base("sftp")); + assertEquals(NetworkProtocol.SFTP, config.protocol()); + assertEquals(22, config.port()); + assertEquals("files.example.com", config.host()); + } + + @Test + void defaultsPortPerProtocol() { + assertEquals(21, NetworkConfig.from(base("ftp")).port()); + Map smb = base("smb"); + smb.put("share", "docs"); + assertEquals(445, NetworkConfig.from(smb).port()); + } + + @Test + void implicitFtpsDefaultsToPort990() { + Map implicit = base("ftp"); + implicit.put("security", "implicit"); + assertEquals(990, NetworkConfig.from(implicit).port()); + + implicit.put("port", "2121"); + assertEquals(2121, NetworkConfig.from(implicit).port()); + } + + @Test + void sftpAcceptsAPrivateKeyInsteadOfAPassword() { + Map options = base("sftp"); + options.remove("password"); + options.put("privateKey", "dummy-test-key-material"); + NetworkConfig config = NetworkConfig.from(options); + assertEquals(NetworkProtocol.SFTP, config.protocol()); + } + + @Test + void sftpWithoutPasswordOrKeyIsRejected() { + Map options = base("sftp"); + options.remove("password"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(options)); + } + + @Test + void ftpRequiresAPassword() { + Map options = base("ftp"); + options.remove("password"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(options)); + } + + @Test + void smbRequiresAShare() { + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(base("smb"))); + } + + @Test + void requiresProtocolHostAndUsername() { + assertThrows( + IllegalArgumentException.class, + () -> NetworkConfig.from(Map.of("host", "h", "username", "u", "password", "p"))); + Map noHost = base("sftp"); + noHost.remove("host"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(noHost)); + Map noUser = base("sftp"); + noUser.remove("username"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(noUser)); + } + + @Test + void rejectsAnOutOfRangePort() { + Map options = base("sftp"); + options.put("port", "70000"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(options)); + } + + @Test + void rejectsATraversalDirectory() { + Map options = base("sftp"); + options.put("directory", "in/../../etc"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(options)); + } + + @Test + void mapsModeToSnapshotFlag() { + Map consume = base("sftp"); + consume.put("mode", "consume"); + assertFalse(NetworkConfig.from(consume).snapshot()); + + Map snapshot = base("sftp"); + snapshot.put("mode", "snapshot"); + assertTrue(NetworkConfig.from(snapshot).snapshot()); + + Map bad = base("sftp"); + bad.put("mode", "sometimes"); + assertThrows(IllegalArgumentException.class, () -> NetworkConfig.from(bad)); + } + + @Test + void carriesTheHostKeyFingerprint() { + Map options = base("sftp"); + options.put("hostKeyFingerprint", " SHA256:abc123 "); + assertEquals("SHA256:abc123", NetworkConfig.from(options).hostKeyFingerprint()); + assertNull(NetworkConfig.from(base("sftp")).hostKeyFingerprint()); + } + + @Test + void toStringHidesSecrets() { + Map options = base("sftp"); + options.put("password", "hunter2"); + String text = NetworkConfig.from(options).toString(); + assertFalse(text.contains("hunter2"), "password must not appear in toString"); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkHostGuardTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkHostGuardTest.java new file mode 100644 index 0000000000..fffbf77305 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkHostGuardTest.java @@ -0,0 +1,64 @@ +package stirling.software.proprietary.policy.network; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.ApplicationProperties; + +class NetworkHostGuardTest { + + private static NetworkHostGuard guard(boolean allowPrivate) { + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(allowPrivate); + return new NetworkHostGuard(properties); + } + + private static NetworkHostGuard guardWithAllowlist(String... hosts) { + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowedPrivateNetworkHosts(List.of(hosts)); + return new NetworkHostGuard(properties); + } + + @Test + void rejectsLoopbackByDefault() { + assertThrows( + IllegalArgumentException.class, () -> guard(false).requirePermitted("127.0.0.1")); + } + + @Test + void rejectsPrivateRangesByDefault() { + assertThrows( + IllegalArgumentException.class, () -> guard(false).requirePermitted("10.0.0.1")); + assertThrows( + IllegalArgumentException.class, + () -> guard(false).requirePermitted("192.168.1.10")); + } + + @Test + void rejectsABlankHost() { + assertThrows(IllegalArgumentException.class, () -> guard(false).requirePermitted("")); + } + + @Test + void optInAllowsPrivateAddresses() { + assertDoesNotThrow(() -> guard(true).requirePermitted("127.0.0.1")); + assertDoesNotThrow(() -> guard(true).requirePermitted("10.0.0.1")); + } + + @Test + void allowlistedHostIsAllowedWithoutTheGlobalFlag() { + assertDoesNotThrow(() -> guardWithAllowlist("10.0.0.1").requirePermitted("10.0.0.1")); + assertDoesNotThrow(() -> guardWithAllowlist("FILES.local").requirePermitted("files.LOCAL")); + } + + @Test + void allowlistDoesNotOpenOtherPrivateHosts() { + assertThrows( + IllegalArgumentException.class, + () -> guardWithAllowlist("10.0.0.1").requirePermitted("10.0.0.2")); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIdentitiesTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIdentitiesTest.java new file mode 100644 index 0000000000..00f3637bdf --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIdentitiesTest.java @@ -0,0 +1,49 @@ +package stirling.software.proprietary.policy.network; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class NetworkIdentitiesTest { + + private static NetworkConfig config(String protocol, String share) { + Map options = new LinkedHashMap<>(); + options.put("protocol", protocol); + options.put("host", "files.example.com"); + options.put("port", "2222"); + options.put("username", "u"); + options.put("password", "p"); + if (share != null) { + options.put("share", share); + } + return NetworkConfig.from(options); + } + + @Test + void identityIsProtocolHostPortAndPath() { + assertEquals( + "sftp://files.example.com:2222/in/doc.pdf", + NetworkIdentities.identity(config("sftp", null), "in/doc.pdf")); + } + + @Test + void smbIdentityIncludesTheShare() { + Map options = new LinkedHashMap<>(); + options.put("protocol", "smb"); + options.put("host", "files.example.com"); + options.put("username", "u"); + options.put("password", "p"); + options.put("share", "documents"); + assertEquals( + "smb://files.example.com:445/documents/reports/q1.pdf", + NetworkIdentities.identity(NetworkConfig.from(options), "reports/q1.pdf")); + } + + @Test + void gateIsSizeAndModifiedTime() { + assertEquals("42:1700000000000", NetworkIdentities.gate(42, 1700000000000L)); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkInputSourceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkInputSourceTest.java new file mode 100644 index 0000000000..c125dd4919 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkInputSourceTest.java @@ -0,0 +1,280 @@ +package stirling.software.proprietary.policy.network; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.access.service.OwnershipService; +import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; +import stirling.software.proprietary.policy.input.ResolveContext; +import stirling.software.proprietary.policy.input.ResolvedInput; +import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; +import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.security.service.UserService; + +/** + * Tests for {@link NetworkInputSource}: consume mode tracks remote files through the ledger, the + * version-guarded delete survives a mid-run replacement, a shared file is removed only once every + * policy is done, snapshot stays stateless, and the source type must match the connection protocol. + * The remote server is faked in memory, so no real SFTP/FTP/SMB endpoint is needed. + */ +class NetworkInputSourceTest { + + private static final String POLICY = "p1"; + + private FakeServer server; + private NetworkInputSource source; + private InProcessProcessedLedger ledger; + private RecordingContext ctx; + + @BeforeEach + void setUp() { + server = new FakeServer(); + // allowPrivate=true so the host guard never resolves DNS; connect() is overridden anyway. + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(true); + RemoteFileClientFactory factory = + new RemoteFileClientFactory(new NetworkHostGuard(properties)) { + @Override + public RemoteFileClient connect(NetworkConfig config) { + return new FakeClient(server); + } + }; + NetworkConnectionResolver resolver = + new NetworkConnectionResolver( + mock(IntegrationConfigRepository.class), + mock(OwnershipService.class), + mock(UserService.class)); + source = new NetworkInputSource(resolver, factory); + ledger = new InProcessProcessedLedger(); + ctx = new RecordingContext(); + } + + private static InputSpec sftp(Map extra) { + Map options = new LinkedHashMap<>(); + options.put("protocol", "sftp"); + options.put("host", "files.example.com"); + options.put("username", "u"); + options.put("password", "p"); + options.put("directory", "in"); + options.putAll(extra); + return new InputSpec("sftp", options); + } + + @Test + void supportsTheThreeNetworkSourceTypes() { + assertTrue(source.supports(new InputSpec("sftp", Map.of()))); + assertTrue(source.supports(new InputSpec("ftp", Map.of()))); + assertTrue(source.supports(new InputSpec("network", Map.of()))); + assertFalse(source.supports(new InputSpec("folder", Map.of()))); + } + + @Test + void consumeRemovesTheFileOnceProcessed() throws Exception { + server.put("in/doc.pdf", "data", 1000); + + List work = source.resolve(sftp(Map.of()), ctx); + + assertEquals(1, work.size()); + assertEquals(1, work.get(0).inputs().primary().size()); + // In flight: still on the server, but a second sweep does not pick it up again. + assertTrue(server.has("in/doc.pdf")); + assertTrue(source.resolve(sftp(Map.of()), ctx).isEmpty()); + + work.get(0).onComplete().accept(true); + assertFalse(server.has("in/doc.pdf")); + assertTrue(source.resolve(sftp(Map.of()), ctx).isEmpty()); + } + + @Test + void aFileReplacedMidRunSurvivesTheDeleteAndRunsAgain() throws Exception { + server.put("in/doc.pdf", "data", 1000); + + List work = source.resolve(sftp(Map.of()), ctx); + // A new version lands while the run is executing (different size/mtime = new gate). + server.put("in/doc.pdf", "new data, different size", 2000); + work.get(0).onComplete().accept(true); + + // The delete is version-guarded: the replacement is not the file that ran, so it stays and + // is claimed as fresh work next sweep. + assertTrue(server.has("in/doc.pdf")); + assertEquals(1, source.resolve(sftp(Map.of()), ctx).size()); + } + + @Test + void aSharedFileIsRemovedOnlyOnceEveryPolicyHasProcessedIt() throws Exception { + server.put("in/doc.pdf", "data", 1000); + RecordingContext other = new RecordingContext("p2"); + + List mine = source.resolve(sftp(Map.of()), ctx); + List theirs = source.resolve(sftp(Map.of()), other); + assertEquals(1, mine.size()); + assertEquals(1, theirs.size()); + + mine.get(0).onComplete().accept(true); + assertTrue(server.has("in/doc.pdf")); // the other claim is still in flight + + theirs.get(0).onComplete().accept(true); + assertFalse(server.has("in/doc.pdf")); + } + + @Test + void aFailedFileStaysInPlaceAndIsNotRetriedUntilItChanges() throws Exception { + server.put("in/doc.pdf", "data", 1000); + + source.resolve(sftp(Map.of()), ctx).get(0).onComplete().accept(false); + + assertTrue(server.has("in/doc.pdf")); + assertTrue(source.resolve(sftp(Map.of()), ctx).isEmpty()); + + server.put("in/doc.pdf", "data", 2000); // touched: new mtime is a new version + assertEquals(1, source.resolve(sftp(Map.of()), ctx).size()); + } + + @Test + void snapshotReadsStatelesslyEverySweep() throws Exception { + server.put("in/doc.pdf", "data", 1000); + InputSpec spec = sftp(Map.of("mode", "snapshot")); + + List first = source.resolve(spec, ctx); + first.get(0).onComplete().accept(true); + List second = source.resolve(spec, ctx); + + assertEquals(1, first.size()); + assertEquals(1, second.size()); // no ledger involvement + assertTrue(ctx.present.isEmpty()); + assertTrue(server.has("in/doc.pdf")); + } + + @Test + void streamsTheFileContent() throws Exception { + server.put("in/doc.pdf", "hello", 1000); + + List work = source.resolve(sftp(Map.of()), ctx); + try (InputStream in = work.get(0).inputs().primary().get(0).getInputStream()) { + assertEquals("hello", new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } + + @Test + void validateRejectsASourceTypeThatDoesNotMatchTheConnectionProtocol() { + // A "network" (SMB) source pointed at an SFTP connection is a misconfiguration. + Map options = new LinkedHashMap<>(); + options.put("protocol", "sftp"); + options.put("host", "files.example.com"); + options.put("username", "u"); + options.put("password", "p"); + assertThrows( + IllegalArgumentException.class, + () -> source.validate(new InputSpec("network", options))); + } + + /** In-memory remote server shared between a test's fake clients. */ + private static final class FakeServer { + private final Map files = new LinkedHashMap<>(); + + void put(String path, String content, long mtime) { + files.put(path, new FakeFile(content.getBytes(StandardCharsets.UTF_8), mtime)); + } + + boolean has(String path) { + return files.containsKey(path); + } + } + + private record FakeFile(byte[] content, long mtime) {} + + /** A RemoteFileClient over the in-memory server; every op reads/writes the shared map. */ + private static final class FakeClient implements RemoteFileClient { + private final FakeServer server; + + private FakeClient(FakeServer server) { + this.server = server; + } + + @Override + public List list(String directory, boolean recursive) { + List out = new ArrayList<>(); + server.files.forEach( + (path, file) -> { + String name = path.substring(path.lastIndexOf('/') + 1); + out.add(new RemoteFile(path, name, file.content().length, file.mtime())); + }); + return out; + } + + @Override + public RemoteFile stat(String path) { + FakeFile file = server.files.get(path); + if (file == null) { + return null; + } + String name = path.substring(path.lastIndexOf('/') + 1); + return new RemoteFile(path, name, file.content().length, file.mtime()); + } + + @Override + public InputStream open(String path) { + return new ByteArrayInputStream(server.files.get(path).content()); + } + + @Override + public void delete(String path) { + server.files.remove(path); + } + + @Override + public void close() {} + } + + /** Policy-scoped context backed by the in-process ledger, recording presence reports. */ + private final class RecordingContext implements ResolveContext { + private final String policyId; + private final List present = new ArrayList<>(); + + private RecordingContext() { + this(POLICY); + } + + private RecordingContext(String policyId) { + this.policyId = policyId; + } + + @Override + public boolean claim(String identity, String gate, Supplier contentHash) { + return ledger.claim(policyId, identity, gate, contentHash); + } + + @Override + public void settle( + String identity, String finalGate, String finalContentHash, boolean success) { + ledger.settle(policyId, identity, finalGate, finalContentHash, success); + } + + @Override + public boolean allSettledDone(String identity) { + return ledger.allSettledDone(identity); + } + + @Override + public void reportPresent(Collection identities) { + present.addAll(identities); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidatorTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidatorTest.java new file mode 100644 index 0000000000..40a3fb70a8 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/NetworkIntegrationValidatorTest.java @@ -0,0 +1,55 @@ +package stirling.software.proprietary.policy.network; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.integration.model.IntegrationType; + +class NetworkIntegrationValidatorTest { + + private static NetworkIntegrationValidator validator(boolean allowPrivate) { + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(allowPrivate); + return new NetworkIntegrationValidator(new NetworkHostGuard(properties)); + } + + private static Map config(String host) { + Map options = new LinkedHashMap<>(); + options.put("protocol", "sftp"); + options.put("host", host); + options.put("username", "u"); + options.put("password", "p"); + return options; + } + + @Test + void reportsTheNetworkType() { + assertEquals(IntegrationType.NETWORK, validator(true).type()); + } + + @Test + void acceptsAValidConfig() { + assertDoesNotThrow(() -> validator(true).validate(config("10.0.0.5"))); + } + + @Test + void rejectsAMalformedConfig() { + assertThrows( + IllegalArgumentException.class, + () -> validator(true).validate(Map.of("host", "h"))); + } + + @Test + void rejectsAPrivateHostUnlessOptedIn() { + assertThrows( + IllegalArgumentException.class, + () -> validator(false).validate(config("10.0.0.5"))); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SftpNetworkSourceIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SftpNetworkSourceIntegrationTest.java new file mode 100644 index 0000000000..3df4f49cb3 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SftpNetworkSourceIntegrationTest.java @@ -0,0 +1,241 @@ +package stirling.software.proprietary.policy.network; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +import stirling.software.common.configuration.InstallationPathConfig; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.access.service.OwnershipService; +import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; +import stirling.software.proprietary.policy.input.ResolveContext; +import stirling.software.proprietary.policy.input.ResolvedInput; +import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; +import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.security.service.UserService; + +/** + * End-to-end {@link NetworkInputSource} test over SFTP against a real atmoz/sftp server, through + * the production {@link SftpFileClient}: listing, claiming, streaming the actual bytes, consume + * delete, and save-time validation. The server chroots the user to its home; {@code upload} is the + * writable drop folder. + */ +@Testcontainers(disabledWithoutDocker = true) +@EnabledIfEnvironmentVariable( + named = "RUN_NETWORK_INTEGRATION_TESTS", + matches = "true", + disabledReason = + "Spins up SFTP/FTP/SMB containers; opt-in to keep several heavy containers off the" + + " standard CI runner. Run with RUN_NETWORK_INTEGRATION_TESTS=true.") +class SftpNetworkSourceIntegrationTest { + + private static final String POLICY = "p1"; + private static final String USER = "stirling"; + private static final String PASS = "secret"; + private static final String DIR = "upload"; + private static final int SFTP_PORT = 22; + + @org.testcontainers.junit.jupiter.Container + static GenericContainer sftp = + new GenericContainer<>("atmoz/sftp:alpine") + .withExposedPorts(SFTP_PORT) + .withCommand(USER + ":" + PASS + ":::" + DIR) + .waitingFor(Wait.forListeningPort()); + + private NetworkInputSource source; + private InProcessProcessedLedger ledger; + private RecordingContext ctx; + + @BeforeEach + void setUp() throws IOException { + // Each run's container has a fresh host key, so drop pins from earlier runs: a reused + // mapped port would otherwise trip the trust-on-first-use "key changed" refusal. + Files.deleteIfExists( + Path.of(InstallationPathConfig.getConfigPath(), SftpFileClient.KNOWN_HOSTS_FILE)); + // The container resolves to loopback, so the private-host opt-in must be on. + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(true); + RemoteFileClientFactory factory = + new RemoteFileClientFactory(new NetworkHostGuard(properties)); + NetworkConnectionResolver resolver = + new NetworkConnectionResolver( + mock(IntegrationConfigRepository.class), + mock(OwnershipService.class), + mock(UserService.class)); + source = new NetworkInputSource(resolver, factory); + ledger = new InProcessProcessedLedger(); + ctx = new RecordingContext(); + } + + @Test + void consumeListsStreamsAndDeletes() throws Exception { + put("doc.pdf", "hello sftp"); + + List work = source.resolve(spec(Map.of()), ctx); + + assertThat(work).hasSize(1); + String identity = + "sftp://" + + sftp.getHost() + + ":" + + sftp.getMappedPort(SFTP_PORT) + + "/upload/doc.pdf"; + assertThat(ctx.present).containsExactly(identity); + assertThat(read(work.get(0))).isEqualTo("hello sftp"); + // In flight: a second sweep does not re-claim it. + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + + work.get(0).onComplete().accept(true); + assertThat(exists("doc.pdf")).isFalse(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void aFailedFileStaysOnTheServer() throws Exception { + put("doc.pdf", "data"); + + source.resolve(spec(Map.of()), ctx).get(0).onComplete().accept(false); + + assertThat(exists("doc.pdf")).isTrue(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void validateConnectsAndRejectsBadCredentials() { + source.validate(spec(Map.of())); + + Map wrong = new HashMap<>(baseOptions()); + wrong.put("password", "not-the-password"); + assertThatThrownBy(() -> source.validate(new InputSpec("sftp", wrong))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot access"); + } + + @Test + void acceptsTheServersRealHostKeyFingerprint() throws Exception { + String fingerprint = + exec("ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | awk '{print $2}'").trim(); + Map pinned = new HashMap<>(baseOptions()); + pinned.put("hostKeyFingerprint", fingerprint); + source.validate(new InputSpec("sftp", pinned)); + } + + @Test + void rejectsAWrongHostKeyFingerprint() { + Map wrong = new HashMap<>(baseOptions()); + wrong.put("hostKeyFingerprint", "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + // Refused during the handshake, before any credentials are sent. + assertThatThrownBy(() -> source.validate(new InputSpec("sftp", wrong))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot access"); + } + + @Test + void refusesAChangedHostKey() throws Exception { + source.validate(spec(Map.of())); // pins the current key (trust-on-first-use) + + Path knownHosts = + Path.of(InstallationPathConfig.getConfigPath(), SftpFileClient.KNOWN_HOSTS_FILE); + String pinned = Files.readString(knownHosts); + // Swap the pinned key for a valid-but-different one so the server's real key reads as + // changed rather than unparseable. + Files.writeString(knownHosts, pinned.replaceFirst("(?m) [^ ]+$", " " + bogusEd25519Key())); + + assertThatThrownBy(() -> source.validate(spec(Map.of()))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot access"); + } + + /** A structurally valid ssh-ed25519 public key blob (all-zero key material), base64. */ + private static String bogusEd25519Key() { + byte[] type = "ssh-ed25519".getBytes(StandardCharsets.US_ASCII); + ByteBuffer blob = ByteBuffer.allocate(4 + type.length + 4 + 32); + blob.putInt(type.length).put(type).putInt(32); + return java.util.Base64.getEncoder().encodeToString(blob.array()); + } + + private Map baseOptions() { + Map options = new HashMap<>(); + options.put("protocol", "sftp"); + options.put("host", sftp.getHost()); + options.put("port", String.valueOf(sftp.getMappedPort(SFTP_PORT))); + options.put("username", USER); + options.put("password", PASS); + options.put("directory", DIR); + return options; + } + + private InputSpec spec(Map extra) { + Map options = new HashMap<>(baseOptions()); + options.putAll(extra); + return new InputSpec("sftp", options); + } + + private void put(String name, String content) throws Exception { + String path = "/home/" + USER + "/" + DIR + "/" + name; + exec("printf '%s' '" + content + "' > " + path + " && chmod 644 " + path); + } + + private boolean exists(String name) throws Exception { + return exec("test -f /home/" + USER + "/" + DIR + "/" + name + " && echo yes || echo no") + .contains("yes"); + } + + private String exec(String script) throws Exception { + Container.ExecResult result = sftp.execInContainer("sh", "-c", script); + return result.getStdout() + result.getStderr(); + } + + private static String read(ResolvedInput unit) throws IOException { + try (InputStream stream = unit.inputs().primary().get(0).getInputStream()) { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private class RecordingContext implements ResolveContext { + + private final List present = new ArrayList<>(); + + @Override + public boolean claim(String identity, String gate, Supplier contentHash) { + return ledger.claim(POLICY, identity, gate, contentHash); + } + + @Override + public void settle( + String identity, String finalGate, String finalContentHash, boolean success) { + ledger.settle(POLICY, identity, finalGate, finalContentHash, success); + } + + @Override + public boolean allSettledDone(String identity) { + return ledger.allSettledDone(identity); + } + + @Override + public void reportPresent(Collection identities) { + present.addAll(identities); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SmbNetworkSourceIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SmbNetworkSourceIntegrationTest.java new file mode 100644 index 0000000000..9d07100bc4 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/network/SmbNetworkSourceIntegrationTest.java @@ -0,0 +1,194 @@ +package stirling.software.proprietary.policy.network; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.access.service.OwnershipService; +import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; +import stirling.software.proprietary.policy.input.ResolveContext; +import stirling.software.proprietary.policy.input.ResolvedInput; +import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; +import stirling.software.proprietary.policy.model.InputSpec; +import stirling.software.proprietary.security.service.UserService; + +/** + * End-to-end {@link NetworkInputSource} test over SMB against a real Samba server, through the + * production {@link SmbFileClient}: listing a share, claiming, streaming the actual bytes, consume + * delete, and save-time validation. The share is served world-writable so the SMB user can remove a + * consumed file. + */ +@Testcontainers(disabledWithoutDocker = true) +@EnabledIfEnvironmentVariable( + named = "RUN_NETWORK_INTEGRATION_TESTS", + matches = "true", + disabledReason = + "Spins up SFTP/FTP/SMB containers; opt-in to keep several heavy containers off the" + + " standard CI runner. Run with RUN_NETWORK_INTEGRATION_TESTS=true.") +class SmbNetworkSourceIntegrationTest { + + private static final String POLICY = "p1"; + private static final String USER = "stirling"; + private static final String PASS = "secret"; + private static final String SHARE = "documents"; + private static final int SMB_PORT = 445; + + @org.testcontainers.junit.jupiter.Container + static GenericContainer samba = + new GenericContainer<>("dperson/samba") + .withExposedPorts(SMB_PORT) + .withCommand( + "-p", + "-u", + USER + ";" + PASS, + "-s", + SHARE + ";/share;yes;no;no;" + USER) + .waitingFor(Wait.forListeningPort()); + + private NetworkInputSource source; + private InProcessProcessedLedger ledger; + private RecordingContext ctx; + + @BeforeEach + void setUp() throws Exception { + // The share dir must be writable by the SMB user so a consumed file can be deleted. + samba.execInContainer("sh", "-c", "mkdir -p /share && chmod -R 0777 /share"); + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateNetworkSources(true); + RemoteFileClientFactory factory = + new RemoteFileClientFactory(new NetworkHostGuard(properties)); + NetworkConnectionResolver resolver = + new NetworkConnectionResolver( + mock(IntegrationConfigRepository.class), + mock(OwnershipService.class), + mock(UserService.class)); + source = new NetworkInputSource(resolver, factory); + ledger = new InProcessProcessedLedger(); + ctx = new RecordingContext(); + } + + @Test + void consumeListsStreamsAndDeletes() throws Exception { + put("doc.pdf", "hello smb"); + + List work = source.resolve(spec(Map.of()), ctx); + + assertThat(work).hasSize(1); + String identity = + "smb://" + + samba.getHost() + + ":" + + samba.getMappedPort(SMB_PORT) + + "/documents/doc.pdf"; + assertThat(ctx.present).containsExactly(identity); + assertThat(read(work.get(0))).isEqualTo("hello smb"); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + + work.get(0).onComplete().accept(true); + assertThat(exists("doc.pdf")).isFalse(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void aFailedFileStaysOnTheShare() throws Exception { + put("doc.pdf", "data"); + + source.resolve(spec(Map.of()), ctx).get(0).onComplete().accept(false); + + assertThat(exists("doc.pdf")).isTrue(); + assertThat(source.resolve(spec(Map.of()), ctx)).isEmpty(); + } + + @Test + void validateConnectsAndRejectsBadCredentials() { + source.validate(spec(Map.of())); + + Map wrong = new HashMap<>(baseOptions()); + wrong.put("password", "not-the-password"); + assertThatThrownBy(() -> source.validate(new InputSpec("network", wrong))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot access"); + } + + private Map baseOptions() { + Map options = new HashMap<>(); + options.put("protocol", "smb"); + options.put("host", samba.getHost()); + options.put("port", String.valueOf(samba.getMappedPort(SMB_PORT))); + options.put("username", USER); + options.put("password", PASS); + options.put("share", SHARE); + return options; + } + + private InputSpec spec(Map extra) { + Map options = new HashMap<>(baseOptions()); + options.putAll(extra); + return new InputSpec("network", options); + } + + private void put(String name, String content) throws Exception { + String path = "/share/" + name; + exec("printf '%s' '" + content + "' > " + path + " && chmod 0666 " + path); + } + + private boolean exists(String name) throws Exception { + return exec("test -f /share/" + name + " && echo yes || echo no").contains("yes"); + } + + private String exec(String script) throws Exception { + Container.ExecResult result = samba.execInContainer("sh", "-c", script); + return result.getStdout() + result.getStderr(); + } + + private static String read(ResolvedInput unit) throws IOException { + try (InputStream stream = unit.inputs().primary().get(0).getInputStream()) { + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private class RecordingContext implements ResolveContext { + + private final List present = new ArrayList<>(); + + @Override + public boolean claim(String identity, String gate, Supplier contentHash) { + return ledger.claim(POLICY, identity, gate, contentHash); + } + + @Override + public void settle( + String identity, String finalGate, String finalContentHash, boolean success) { + ledger.settle(POLICY, identity, finalGate, finalContentHash, success); + } + + @Override + public boolean allSettledDone(String identity) { + return ledger.allSettledDone(identity); + } + + @Override + public void reportPresent(Collection identities) { + present.addAll(identities); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigrationTest.java new file mode 100644 index 0000000000..8cd91c56e9 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyInlineOutputMigrationTest.java @@ -0,0 +1,146 @@ +package stirling.software.proprietary.policy.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import stirling.software.proprietary.policy.migration.InProcessCompletedMigrations; +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.source.InProcessSourceStore; +import stirling.software.proprietary.policy.source.Source; +import stirling.software.proprietary.policy.source.SourceStore; +import stirling.software.proprietary.policy.store.InProcessPolicyStore; +import stirling.software.proprietary.policy.store.PolicyStore; + +/** + * Tests for {@link PolicyInlineOutputMigration}: policies carrying a folder/S3 destination inline + * are rewritten to reference a {@link Source} location by id; inline (return-to-caller) policies + * are left untouched; the pass is idempotent; two policies sharing a destination in one team share + * one source; and an output at a location an input source already covers reuses that source. + */ +class PolicyInlineOutputMigrationTest { + + private final PolicyStore policyStore = new InProcessPolicyStore(); + private final SourceStore sourceStore = new InProcessSourceStore(); + private final PolicyInlineOutputMigration migration = + new PolicyInlineOutputMigration( + policyStore, sourceStore, new InProcessCompletedMigrations()); + + @Test + void migratesAFolderPolicyToAStoredSource() { + Policy saved = policyStore.save(folderPolicy("Archive", "/out")); + + migration.migrate(); + + Policy migrated = policyStore.get(saved.id()).orElseThrow(); + assertEquals(1, migrated.outputIds().size()); + Source destination = sourceStore.get(migrated.outputIds().get(0)).orElseThrow(); + assertEquals("folder", destination.type()); + assertEquals("/out", destination.options().get("directory")); + } + + @Test + void leavesInlinePoliciesUntouched() { + Policy saved = + policyStore.save( + new Policy( + null, + "Editor run", + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline())); + + migration.migrate(); + + assertTrue(policyStore.get(saved.id()).orElseThrow().outputIds().isEmpty()); + assertTrue(sourceStore.all().isEmpty()); + } + + @Test + void isIdempotent() { + policyStore.save(folderPolicy("Archive", "/out")); + + migration.migrate(); + int afterFirst = sourceStore.all().size(); + migration.migrate(); + + assertEquals(afterFirst, sourceStore.all().size()); + } + + @Test + void skipsTheScanOnceComplete() { + // First pass records the completion marker (even with nothing to migrate). + migration.migrate(); + + // A migratable folder policy created afterwards is left untouched: the marker means the + // migration never scans again, rather than re-scanning and finding it every boot. + Policy later = policyStore.save(folderPolicy("Late", "/late")); + migration.migrate(); + + assertTrue(policyStore.get(later.id()).orElseThrow().outputIds().isEmpty()); + assertTrue(sourceStore.all().isEmpty()); + } + + @Test + void dedupesASharedDestinationWithinATeam() { + policyStore.save(teamFolderPolicy("A", "/shared", 7L)); + policyStore.save(teamFolderPolicy("B", "/shared", 7L)); + + migration.migrate(); + + assertEquals(1, sourceStore.all().size()); + } + + @Test + void reusesAnExistingInputSourceAtTheSameLocation() { + // An input source already reads /shared (with consume mode); a policy that outputs there + // should link to that same source, not mint a duplicate. + Source existing = + sourceStore.save( + new Source( + null, + "Shared", + "folder", + Map.of("directory", "/shared", "mode", "consume"), + true, + "owner", + 7L)); + Policy saved = policyStore.save(teamFolderPolicy("Writer", "/shared", 7L)); + + migration.migrate(); + + assertEquals(1, sourceStore.all().size()); + assertEquals(List.of(existing.id()), policyStore.get(saved.id()).orElseThrow().outputIds()); + } + + private static Policy folderPolicy(String name, String directory) { + return new Policy( + null, + name, + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.folder(directory)); + } + + private static Policy teamFolderPolicy(String name, String directory, Long teamId) { + return new Policy( + null, + name, + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.folder(directory), + teamId); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyOutputResolverTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyOutputResolverTest.java new file mode 100644 index 0000000000..acaa68dae1 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/PolicyOutputResolverTest.java @@ -0,0 +1,72 @@ +package stirling.software.proprietary.policy.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineStep; +import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.source.InProcessSourceStore; +import stirling.software.proprietary.policy.source.Source; +import stirling.software.proprietary.policy.source.SourceStore; + +/** + * Tests for {@link PolicyOutputResolver}: a policy's {@code outputIds} resolve live to the stored + * sources used as destinations (one spec each), an unreferenced policy keeps its inline output, and + * a dangling reference falls back to inline delivery rather than failing the run. + */ +class PolicyOutputResolverTest { + + private final SourceStore sourceStore = new InProcessSourceStore(); + private final PolicyOutputResolver resolver = new PolicyOutputResolver(sourceStore); + + @Test + void resolvesEachOutputIdToItsStoredSource() { + Source archive = sourceStore.save(folder("Archive", "/out")); + Source backup = sourceStore.save(folder("Backup", "/backup")); + + List specs = + resolver.resolve(policy().withOutputIds(List.of(archive.id(), backup.id()))); + + assertEquals(2, specs.size()); + assertEquals("/out", specs.get(0).options().get("directory")); + assertEquals("/backup", specs.get(1).options().get("directory")); + } + + @Test + void anUnreferencedPolicyKeepsItsInlineOutput() { + List specs = resolver.resolve(policy()); + + assertEquals(1, specs.size()); + assertEquals("inline", specs.get(0).type()); + } + + @Test + void whenNoReferencesResolveItFallsBackToInline() { + List specs = + resolver.resolve(policy().withOutputIds(List.of("does-not-exist"))); + + assertEquals(1, specs.size()); + assertEquals("inline", specs.get(0).type()); + } + + private static Source folder(String name, String directory) { + return new Source( + null, name, "folder", Map.of("directory", directory), true, "owner", null); + } + + private static Policy policy() { + return new Policy( + "p1", + "Pipeline", + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline()); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/S3OutputSinkObjectLockMinioTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/S3OutputSinkObjectLockMinioTest.java new file mode 100644 index 0000000000..89d2fe845d --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/output/S3OutputSinkObjectLockMinioTest.java @@ -0,0 +1,183 @@ +package stirling.software.proprietary.policy.output; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ByteArrayResource; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.model.job.ResultFile; +import stirling.software.proprietary.policy.ledger.InProcessProcessedLedger; +import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.s3.S3ConnectionPool; +import stirling.software.proprietary.policy.s3.S3TestConnections; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.ObjectLockMode; +import software.amazon.awssdk.services.s3.model.S3Exception; + +/** + * Proves the Object Lock (WORM) retention path against a real S3 API. + * + *

The claim being tested is a compliance one - SEC 17a-4(f) and FINRA require records on + * non-rewritable, non-erasable storage - so asserting that we merely send the retention + * headers would be worthless. What matters is that the store then genuinely refuses to delete the + * object, which is what {@link #anObjectWrittenUnderComplianceRetentionCannotBeDeleted} checks. + */ +@Testcontainers(disabledWithoutDocker = true) +class S3OutputSinkObjectLockMinioTest { + + private static final String POLICY = "p1"; + private static final String ACCESS_KEY = "minioadmin"; + private static final String SECRET_KEY = "minioadmin"; + + @Container + static MinIOContainer minio = + new MinIOContainer("minio/minio:latest") + .withUserName(ACCESS_KEY) + .withPassword(SECRET_KEY); + + private static S3Client adminClient; + private static int bucketCounter; + + private String bucket; + private S3OutputSink sink; + + @BeforeEach + void setUp() { + if (adminClient == null) { + adminClient = + S3Client.builder() + .endpointOverride(URI.create(minio.getS3URL())) + .httpClient(UrlConnectionHttpClient.create()) + .region(Region.US_EAST_1) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(ACCESS_KEY, SECRET_KEY))) + .serviceConfiguration( + S3Configuration.builder().pathStyleAccessEnabled(true).build()) + .build(); + } + bucket = "worm-archive-" + ++bucketCounter; + // Object Lock can only be enabled at bucket creation here, and implies versioning. + adminClient.createBucket( + CreateBucketRequest.builder() + .bucket(bucket) + .objectLockEnabledForBucket(true) + .build()); + + ApplicationProperties properties = new ApplicationProperties(); + properties.getPolicies().setAllowPrivateS3Endpoints(true); + sink = + new S3OutputSink( + new S3ConnectionPool(properties), + S3TestConnections.legacyResolver(), + new InProcessProcessedLedger()); + } + + @Test + void anObjectWrittenUnderComplianceRetentionCannotBeDeleted() throws IOException { + List results = + sink.deliver( + new OutputDelivery("run-1", POLICY), + List.of(output("statement.pdf", "regulated record")), + lockedOutputSpec("COMPLIANCE", 7)); + + assertThat(results).hasSize(1); + + HeadObjectResponse head = + adminClient.headObject( + HeadObjectRequest.builder().bucket(bucket).key("statement.pdf").build()); + assertThat(head.objectLockMode()).isEqualTo(ObjectLockMode.COMPLIANCE); + // Retention is computed per object from "now", so a daily policy gives each document its + // own full window rather than a shared deadline. + assertThat(head.objectLockRetainUntilDate()) + .isBetween( + Instant.now().plus(6, ChronoUnit.DAYS), + Instant.now().plus(8, ChronoUnit.DAYS)); + + // The point of the feature: the store itself refuses, not us. + assertThatThrownBy( + () -> + adminClient.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucket) + .key("statement.pdf") + .versionId(head.versionId()) + .build())) + .isInstanceOf(S3Exception.class); + + // And it is still readable - locked, not quarantined. + assertThat( + adminClient + .getObject( + GetObjectRequest.builder() + .bucket(bucket) + .key("statement.pdf") + .build()) + .response() + .contentLength()) + .isEqualTo("regulated record".length()); + } + + @Test + void withoutRetentionConfiguredObjectsAreWrittenUnlockedAsBefore() throws IOException { + sink.deliver( + new OutputDelivery("run-2", POLICY), + List.of(output("scratch.pdf", "ordinary output")), + lockedOutputSpec(null, null)); + + HeadObjectResponse head = + adminClient.headObject( + HeadObjectRequest.builder().bucket(bucket).key("scratch.pdf").build()); + // No accidental retention: an Object-Lock-enabled bucket must not silently lock everything. + assertThat(head.objectLockMode()).isNull(); + assertThat(head.objectLockRetainUntilDate()).isNull(); + } + + private OutputSpec lockedOutputSpec(String lockMode, Integer retentionDays) { + Map options = new java.util.LinkedHashMap<>(); + options.put("bucket", bucket); + options.put("prefix", ""); + options.put("endpoint", minio.getS3URL()); + options.put("accessKeyId", ACCESS_KEY); + options.put("secretAccessKey", SECRET_KEY); + if (lockMode != null) { + options.put("objectLockMode", lockMode); + options.put("retentionDays", String.valueOf(retentionDays)); + } + return new OutputSpec("s3", options); + } + + private static org.springframework.core.io.Resource output(String name, String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)) { + @Override + public String getFilename() { + return name; + } + }; + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java index 221cbc4482..373b596136 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java @@ -16,6 +16,7 @@ import stirling.software.common.service.UserServiceInterface; import stirling.software.proprietary.policy.config.PolicyAccessGuard; import stirling.software.proprietary.policy.config.PolicyManagementAuthority; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.model.TriggerConfig; @@ -27,9 +28,11 @@ import stirling.software.proprietary.policy.store.InProcessPolicyStore; import stirling.software.proprietary.policy.store.PolicyStore; /** - * Tests for {@link PolicyOverviewService}: every policy appears once with its sources resolved to - * names, its steps and trigger/output summarised, and the KPI strip counting active vs paused. - * Login is disabled so the team guards pass everything through. + * Tests for {@link PolicyOverviewService}: every Pipelines-page policy appears once with its + * sources resolved to names, its steps and trigger/output summarised, and the KPI strip counting + * active vs paused. Frontend/catalogue policies (owned by the Policies page) are excluded, while a + * pipeline that uses a folder-watch trigger stays. Login is disabled so the team guards pass + * everything through. */ class PolicyOverviewServiceTest { @@ -57,8 +60,9 @@ class PolicyOverviewServiceTest { "Redaction", "owner", true, - new TriggerConfig("schedule", Map.of()), - List.of(claims.id()), + List.of( + new PipelineInput( + claims.id(), new TriggerConfig("schedule", Map.of()))), List.of(new PipelineStep("/api/v1/security/auto-redact", Map.of())), OutputSpec.inline())); policyStore.save( @@ -67,7 +71,6 @@ class PolicyOverviewServiceTest { "Archive (paused)", "owner", false, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline())); @@ -94,6 +97,51 @@ class PolicyOverviewServiceTest { assertEquals(List.of(2L, 1L, 1L), response.kpis().stream().map(PolicyKpi::value).toList()); } + @Test + void excludesCataloguePoliciesButKeepsFolderWatchPipelines() { + Source inbox = source("Inbox", "/inbox"); + // A hand-built pipeline: shows. + policyStore.save( + new Policy( + null, + "Compress pipeline", + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline())); + // A folder-watch pipeline is still a pipeline: shows. + policyStore.save( + new Policy( + null, + "Inbox watcher", + "owner", + true, + List.of( + new PipelineInput( + inbox.id(), new TriggerConfig("folder-watch", Map.of()))), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline())); + // A frontend/catalogue policy (categoryId in output options): hidden. + policyStore.save( + new Policy( + null, + "Classification Policy", + "system", + true, + List.of(), + List.of(new PipelineStep("/api/v1/ai/tools/classify-and-label", Map.of())), + new OutputSpec("inline", Map.of("categoryId", "classification")))); + + PoliciesOverviewResponse response = service.overview(); + + assertEquals( + List.of("Compress pipeline", "Inbox watcher"), + response.pipelines().stream().map(PolicyView::name).toList()); + // KPIs count both visible pipelines, not the hidden catalogue policy. + assertEquals(List.of(2L, 2L, 0L), response.kpis().stream().map(PolicyKpi::value).toList()); + } + @Test void anUnresolvedSourceFallsBackToItsId() { policyStore.save( @@ -102,8 +150,7 @@ class PolicyOverviewServiceTest { "Orphan", "owner", true, - null, - List.of("src-missing"), + List.of(PipelineInput.manual("src-missing")), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline())); @@ -170,8 +217,7 @@ class PolicyOverviewServiceTest { name, "owner", true, - null, - List.of(sourceIds), + List.of(sourceIds).stream().map(PipelineInput::manual).toList(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline(), teamId)); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigrationTest.java index ff14d8cf81..41f80461cd 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigrationTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/EmbeddedS3CredentialMigrationTest.java @@ -23,7 +23,9 @@ import stirling.software.proprietary.access.model.OwnerScope; import stirling.software.proprietary.integration.model.IntegrationConfig; import stirling.software.proprietary.integration.repository.IntegrationConfigRepository; import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.policy.migration.InProcessCompletedMigrations; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.source.InProcessSourceStore; @@ -49,7 +51,11 @@ class EmbeddedS3CredentialMigrationTest { void setUp() { migration = new EmbeddedS3CredentialMigration( - sourceStore, policyStore, connections, teamRepository); + sourceStore, + policyStore, + connections, + teamRepository, + new InProcessCompletedMigrations()); AtomicLong ids = new AtomicLong(100); // Lenient: the nothing-to-migrate cases never create a connection. lenient().when(connections.findAll()).thenReturn(List.of()); @@ -92,8 +98,7 @@ class EmbeddedS3CredentialMigrationTest { "Rotate", "alice", true, - null, - List.of(source.id()), + List.of(PipelineInput.manual(source.id())), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), new OutputSpec( "s3", diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/PolicyS3ConnectionUsageCheckTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/PolicyS3ConnectionUsageCheckTest.java index 847a553ba6..4af725428d 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/PolicyS3ConnectionUsageCheckTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/s3/PolicyS3ConnectionUsageCheckTest.java @@ -39,7 +39,6 @@ class PolicyS3ConnectionUsageCheckTest { "Rotate", "alice", true, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), new OutputSpec("s3", Map.of("connectionId", "5")), diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeederTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeederTest.java index 2f270afddb..49159e7d6e 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeederTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/seed/DefaultClassificationPolicySeederTest.java @@ -41,7 +41,6 @@ class DefaultClassificationPolicySeederTest { "Classification Policy", "system", true, - null, List.of(), List.of(), new OutputSpec("inline", Map.of("categoryId", "classification")), diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java index 545ea8e50f..20f783f75c 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceControllerTest.java @@ -30,6 +30,7 @@ import stirling.software.proprietary.policy.config.PolicyManagementAuthority; import stirling.software.proprietary.policy.input.InputSource; import stirling.software.proprietary.policy.input.WebhookInputSource; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.store.InProcessPolicyStore; @@ -274,8 +275,7 @@ class SourceControllerTest { name, "owner", true, - null, - List.of(sourceId), + List.of(PipelineInput.manual(sourceId)), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceOverviewServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceOverviewServiceTest.java index 3f63e18407..a8c295acc8 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceOverviewServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/source/SourceOverviewServiceTest.java @@ -16,6 +16,7 @@ import stirling.software.common.service.UserServiceInterface; import stirling.software.proprietary.policy.config.PolicyAccessGuard; import stirling.software.proprietary.policy.config.PolicyManagementAuthority; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; import stirling.software.proprietary.policy.store.InProcessPolicyStore; @@ -216,8 +217,7 @@ class SourceOverviewServiceTest { name, "owner", true, - null, - List.of(sourceIds), + List.of(sourceIds).stream().map(PipelineInput::manual).toList(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline())); } @@ -232,7 +232,6 @@ class SourceOverviewServiceTest { name, "owner", true, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), new OutputSpec("inline", Map.of("sources", List.of("editor"))))); @@ -245,8 +244,7 @@ class SourceOverviewServiceTest { name, "owner", true, - null, - List.of(sourceIds), + List.of(sourceIds).stream().map(PipelineInput::manual).toList(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline(), teamId)); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/InProcessPolicyStoreTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/InProcessPolicyStoreTest.java index 60f14be20d..f74a4cfb5b 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/InProcessPolicyStoreTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/InProcessPolicyStoreTest.java @@ -12,11 +12,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.TriggerConfig; -/** Tests for {@link InProcessPolicyStore}: id assignment, upsert, trigger-type lookup, delete. */ +/** Tests for {@link InProcessPolicyStore}: id assignment, upsert, binding lookup, delete. */ class InProcessPolicyStoreTest { private PolicyStore store; @@ -45,7 +47,7 @@ class InProcessPolicyStoreTest { "after", "owner", true, - null, + List.of(), List.of(), OutputSpec.inline())); @@ -54,16 +56,16 @@ class InProcessPolicyStoreTest { } @Test - void findByTriggerTypeReturnsOnlyEnabledMatches() { + void findBindingsByTriggerTypeReturnsOnlyEnabledMatches() { store.save(policy(null, "nightly", "schedule", true)); store.save(policy(null, "nightly-disabled", "schedule", false)); store.save(policy(null, "hooked", "webhook", true)); store.save(policy(null, "on-demand", null, true)); // manual-only: no trigger - List scheduled = store.findByTriggerType("schedule"); + List scheduled = store.findBindingsByTriggerType("schedule"); assertEquals(1, scheduled.size()); - assertEquals("nightly", scheduled.get(0).name()); + assertEquals("nightly", scheduled.get(0).policy().name()); } @Test @@ -76,14 +78,16 @@ class InProcessPolicyStoreTest { } private static Policy policy(String id, String name, String triggerType, boolean enabled) { - TriggerConfig trigger = - triggerType == null ? null : new TriggerConfig(triggerType, Map.of()); + PipelineInput input = + triggerType == null + ? PipelineInput.manual("src") + : new PipelineInput("src", new TriggerConfig(triggerType, Map.of())); return new Policy( id, name, "owner", enabled, - trigger, + List.of(input), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreDbTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreDbTest.java new file mode 100644 index 0000000000..e690c72827 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreDbTest.java @@ -0,0 +1,65 @@ +package stirling.software.proprietary.policy.store; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; + +import tools.jackson.databind.json.JsonMapper; + +/** + * {@link JpaPolicyStore#anyPolicyReferences} on a real (H2) database. It guards a scheduled DELETE + * over stored certificates, so it has to be proven against the raw column rather than the parsed + * policies {@code all()} would hand back. + */ +@DataJpaTest +class JpaPolicyStoreDbTest { + + @Autowired private PolicyRepository repository; + + private JpaPolicyStore store() { + return new JpaPolicyStore(repository, JsonMapper.builder().build()); + } + + @Test + void seesAnAssetIdMentionedByAStoredPolicy() { + save("p1", "{\"steps\":[{\"fileParameters\":{\"p12File\":\"asset:abc-123\"}}]}"); + + assertTrue(store().anyPolicyReferences("abc-123")); + assertFalse(store().anyPolicyReferences("def-456")); + } + + @Test + void seesAnAssetIdInsideAPolicyRowItCannotParse() { + // The whole point of matching raw JSON: a row all() skips as unreadable still holds its + // certificate, and reclaiming that would be unrecoverable. + save("broken", "{ this is not valid json, asset:abc-123"); + + assertTrue(store().all().isEmpty()); + assertTrue(store().anyPolicyReferences("abc-123")); + } + + @Test + void ignoresBlankIds() { + save("p1", "{\"steps\":[]}"); + + assertFalse(store().anyPolicyReferences("")); + assertFalse(store().anyPolicyReferences(null)); + } + + private void save(String id, String policyJson) { + PolicyEntity entity = new PolicyEntity(); + entity.setId(id); + entity.setName(id); + entity.setPolicyJson(policyJson); + repository.save(entity); + } + + @SpringBootConfiguration + @AutoConfigurationPackage + static class TestApp {} +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreTest.java index e9667f2e8d..2a1d2b4f11 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/store/JpaPolicyStoreTest.java @@ -19,8 +19,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.TriggerConfig; import tools.jackson.databind.ObjectMapper; @@ -53,8 +55,9 @@ class JpaPolicyStoreTest { "compress incoming", "alice", true, - new TriggerConfig("schedule", Map.of()), - List.of("src-in"), + List.of( + new PipelineInput( + "src-in", new TriggerConfig("schedule", Map.of()))), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline())); @@ -63,7 +66,6 @@ class JpaPolicyStoreTest { verify(repository).save(captor.capture()); PolicyEntity entity = captor.getValue(); assertEquals(saved.id(), entity.getId()); - assertEquals("schedule", entity.getTriggerType()); assertTrue(entity.isEnabled()); // The stored JSON round-trips back to an equal policy. assertEquals(saved, objectMapper.readValue(entity.getPolicyJson(), Policy.class)); @@ -77,7 +79,7 @@ class JpaPolicyStoreTest { "rotate", "alice", true, - null, // manual-only: no automatic trigger + List.of(), // no inputs: run on demand only List.of( new PipelineStep( "/api/v1/general/rotate-pdf", Map.of("angle", 90))), @@ -87,6 +89,30 @@ class JpaPolicyStoreTest { assertEquals(policy, store.get("p1").orElseThrow()); } + @Test + void getUpgradesLegacyTriggerAndSourceIdsToPerInputTriggers() { + // A blob written before triggers moved onto inputs: one policy-level trigger + sourceIds. + String legacyJson = + "{\"id\":\"p1\",\"name\":\"legacy\",\"owner\":\"alice\",\"enabled\":true," + + "\"trigger\":{\"type\":\"schedule\",\"options\":{}}," + + "\"sourceIds\":[\"s1\",\"s2\"],\"steps\":[]," + + "\"output\":{\"type\":\"inline\",\"options\":{}}}"; + PolicyEntity entity = new PolicyEntity(); + entity.setId("p1"); + entity.setName("legacy"); + entity.setEnabled(true); + entity.setPolicyJson(legacyJson); + when(repository.findById("p1")).thenReturn(Optional.of(entity)); + + Policy upgraded = store.get("p1").orElseThrow(); + + assertEquals( + List.of( + new PipelineInput("s1", new TriggerConfig("schedule", Map.of())), + new PipelineInput("s2", new TriggerConfig("schedule", Map.of()))), + upgraded.inputs()); + } + @Test void saveDenormalizesTeamIdForScopedQueries() { store.save( @@ -95,7 +121,6 @@ class JpaPolicyStoreTest { "scoped", "alice", true, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline(), @@ -114,7 +139,6 @@ class JpaPolicyStoreTest { "ours", "alice", true, - null, List.of(), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline(), @@ -128,23 +152,28 @@ class JpaPolicyStoreTest { } @Test - void findByTriggerTypeUsesTheEnabledQuery() { + void findBindingsByTriggerTypeScansEnabledPoliciesForMatchingInputs() { Policy policy = new Policy( "p1", "watch", "alice", true, - new TriggerConfig("schedule", Map.of()), + List.of( + new PipelineInput( + "src-in", new TriggerConfig("schedule", Map.of())), + PipelineInput.manual("src-manual")), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); - when(repository.findByTriggerTypeAndEnabledTrue("schedule")) - .thenReturn(List.of(entityFor(policy))); + when(repository.findByEnabledTrue()).thenReturn(List.of(entityFor(policy))); - List scheduled = store.findByTriggerType("schedule"); + List scheduled = store.findBindingsByTriggerType("schedule"); + // Only the scheduled input yields a binding; the manual input on the same policy does not. assertEquals(1, scheduled.size()); - assertEquals("p1", scheduled.get(0).id()); + assertEquals("p1", scheduled.get(0).policy().id()); + assertEquals("src-in", scheduled.get(0).input().sourceId()); + assertEquals("schedule", scheduled.get(0).input().trigger().type()); } @Test @@ -163,7 +192,6 @@ class JpaPolicyStoreTest { entity.setName(policy.name()); entity.setOwner(policy.owner()); entity.setEnabled(policy.enabled()); - entity.setTriggerType(policy.trigger() == null ? null : policy.trigger().type()); entity.setTeamId(policy.teamId()); entity.setPolicyJson(objectMapper.writeValueAsString(policy)); return entity; diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/FolderWatchTriggerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/FolderWatchTriggerTest.java index 2c3adf691e..c955e9ed8d 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/FolderWatchTriggerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/FolderWatchTriggerTest.java @@ -14,6 +14,7 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.WatchService; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,8 +32,10 @@ import stirling.software.proprietary.policy.engine.SweepKind; import stirling.software.proprietary.policy.input.InputSource; import stirling.software.proprietary.policy.model.InputSpec; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.TriggerConfig; import stirling.software.proprietary.policy.source.InProcessSourceStore; import stirling.software.proprietary.policy.source.Source; @@ -41,10 +44,10 @@ import stirling.software.proprietary.policy.store.PolicyStore; /** * Tests for {@link FolderWatchTrigger}'s dispatch logic via the package-visible {@code - * runForChangedDirs}/{@code runAll}, plus its cross-facet validation. The OS watch loop and - * scheduled reconcile are thin glue around these and are not exercised here (a real {@code - * WatchService} is timing-dependent), mirroring how {@link ScheduleTriggerTest} drives {@code - * sweep} directly. The folder source is stubbed to mirror {@code FolderInputSource.watchTargets}. + * runForChangedDirs}/{@code runAll}, plus its per-input validation. The OS watch loop and scheduled + * reconcile are thin glue around these and are not exercised here (a real {@code WatchService} is + * timing-dependent), mirroring how {@link ScheduleTriggerTest} drives {@code sweep} directly. The + * folder source is stubbed to mirror {@code FolderInputSource.watchTargets}. */ @ExtendWith(MockitoExtension.class) class FolderWatchTriggerTest { @@ -83,39 +86,43 @@ class FolderWatchTriggerTest { } @Test - void validateRejectsPolicyWithNoWatchableSource() { + void validateRejectsInputWithNoWatchableSource() { + PolicyBinding binding = + bindings(folderWatch("p1", List.of(new InputSpec("folder", Map.of())))).get(0); assertThrows( IllegalArgumentException.class, - () -> trigger.validate(folderWatch("p1", List.of()))); + () -> trigger.validate(binding.policy(), binding.input())); } @Test - void validateAcceptsPolicyWithAFolderSource() { - trigger.validate(folderWatch("p1", List.of(InputSpec.folder("/in")))); + void validateAcceptsAFolderSource() { + PolicyBinding binding = + bindings(folderWatch("p1", List.of(InputSpec.folder("/in")))).get(0); + trigger.validate(binding.policy(), binding.input()); } @Test - void runsOnlyPoliciesDrawingFromTheChangedDirectory() { + void runsOnlyInputsDrawingFromTheChangedDirectory() { Policy a = folderWatch("a", List.of(InputSpec.folder("/in/a"))); Policy b = folderWatch("b", List.of(InputSpec.folder("/in/b"))); - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(a, b)); + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(bindings(a, b)); trigger.runForChangedDirs(Set.of(normalized("/in/a"))); - verify(policyRunner).run(a, SweepKind.LIGHT); - verify(policyRunner, never()).run(eq(b), any()); + verify(policyRunner).runInput(a, a.inputs().get(0), SweepKind.LIGHT); + verify(policyRunner, never()).runInput(eq(b), any(), any()); } @Test - void skipsAMisconfiguredPolicyButStillRunsTheOthers() { + void skipsAMisconfiguredInputButStillRunsTheOthers() { Policy bad = folderWatch("bad", List.of(new InputSpec("folder", Map.of()))); Policy good = folderWatch("good", List.of(InputSpec.folder("/in/a"))); - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(bad, good)); + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(bindings(bad, good)); trigger.runForChangedDirs(Set.of(normalized("/in/a"))); - verify(policyRunner).run(good, SweepKind.LIGHT); - verify(policyRunner, never()).run(eq(bad), any()); + verify(policyRunner).runInput(good, good.inputs().get(0), SweepKind.LIGHT); + verify(policyRunner, never()).runInput(eq(bad), any(), any()); } @Test @@ -126,15 +133,15 @@ class FolderWatchTriggerTest { } @Test - void reconcileRunsEveryFolderWatchPolicyAsASafetyNet() { + void reconcileRunsEveryFolderWatchInputAsASafetyNet() { Policy a = folderWatch("a", List.of(InputSpec.folder("/in/a"))); Policy b = folderWatch("b", List.of(InputSpec.folder("/in/b"))); - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(a, b)); + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(bindings(a, b)); trigger.runAll(); - verify(policyRunner).run(a); - verify(policyRunner).run(b); + verify(policyRunner).runInput(a, a.inputs().get(0), SweepKind.FULL); + verify(policyRunner).runInput(b, b.inputs().get(0), SweepKind.FULL); } @Test @@ -151,15 +158,16 @@ class FolderWatchTriggerTest { try { trigger.watchService = service; - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(a, b, m)); + when(policyStore.findBindingsByTriggerType("folder-watch")) + .thenReturn(bindings(a, b, m)); trigger.syncRegistrations(); // Existing dirs are watched; the non-existent one is skipped. assertEquals( Set.of(normalized(dirA.toString()), normalized(dirB.toString())), trigger.watchedDirs()); - // b's policy is removed: its registration is cancelled, a remains. - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(a)); + // b's input is removed: its registration is cancelled, a remains. + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(bindings(a)); trigger.syncRegistrations(); assertEquals(Set.of(normalized(dirA.toString())), trigger.watchedDirs()); } finally { @@ -175,15 +183,15 @@ class FolderWatchTriggerTest { WatchService service = FileSystems.getDefault().newWatchService(); try { trigger.watchService = service; - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of(p)); + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(bindings(p)); - // The mutation hook registers the new policy's directory without waiting for a + // The mutation hook registers the new input's directory without waiting for a // reconcile. trigger.onPoliciesChanged(); assertEquals(Set.of(normalized(dir.toString())), trigger.watchedDirs()); - // Once the policy is gone, the same hook cancels its registration. - when(policyStore.findByTriggerType("folder-watch")).thenReturn(List.of()); + // Once the input is gone, the same hook cancels its registration. + when(policyStore.findBindingsByTriggerType("folder-watch")).thenReturn(List.of()); trigger.onPoliciesChanged(); assertEquals(Set.of(), trigger.watchedDirs()); } finally { @@ -195,31 +203,40 @@ class FolderWatchTriggerTest { return Path.of(dir).toAbsolutePath().normalize(); } + /** Every (policy, input) binding across the given policies, as the store would return them. */ + private static List bindings(Policy... policies) { + return Arrays.stream(policies) + .flatMap( + policy -> policy.inputs().stream().map(in -> new PolicyBinding(policy, in))) + .toList(); + } + /** Persists each spec as a source and returns a folder-watch policy referencing them by id. */ private Policy folderWatch(String id, List sources) { - List sourceIds = + List inputs = sources.stream() .map( spec -> - sourceStore - .save( - new Source( - null, - "src", - spec.type(), - spec.options(), - true, - "owner", - null)) - .id()) + new PipelineInput( + sourceStore + .save( + new Source( + null, + "src", + spec.type(), + spec.options(), + true, + "owner", + null)) + .id(), + new TriggerConfig("folder-watch", Map.of()))) .toList(); return new Policy( id, "watcher", "owner", true, - new TriggerConfig("folder-watch", Map.of()), - sourceIds, + inputs, List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/ScheduleTriggerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/ScheduleTriggerTest.java index 3f81392285..b7b7dccc9a 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/ScheduleTriggerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/ScheduleTriggerTest.java @@ -25,8 +25,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.policy.engine.PolicyRunner; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.Schedule; import stirling.software.proprietary.policy.model.TriggerConfig; import stirling.software.proprietary.policy.store.PolicyStore; @@ -35,9 +37,9 @@ import tools.jackson.databind.json.JsonMapper; /** * Tests for {@link ScheduleTrigger}'s due-firing logic via the package-visible {@code - * sweep(Instant)}. The trigger only decides when a policy is due; pulling sources and starting runs - * is the {@link PolicyRunner}'s job, so these assert it delegates to the runner. Schedules default - * to UTC, so explicit UTC instants make these deterministic. + * sweep(Instant)}. The trigger only decides when an input is due; pulling the source and starting + * runs is the {@link PolicyRunner}'s job, so these assert it delegates to the runner per binding. + * Schedules default to UTC, so explicit UTC instants make these deterministic. */ @ExtendWith(MockitoExtension.class) class ScheduleTriggerTest { @@ -59,102 +61,105 @@ class ScheduleTriggerTest { @Test void firesOncePerScheduleWhenItComesDue() { - Policy policy = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + PolicyBinding binding = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); Instant t0 = Instant.parse("2026-06-05T10:00:30Z"); trigger.sweep(t0); // first sight: baseline, must not fire immediately - verify(policyRunner, never()).run(any()); + verify(policyRunner, never()).runInput(any(), any(), any()); trigger.sweep(t0.plusSeconds(120)); // the one-minute mark has passed - verify(policyRunner, times(1)).run(eq(policy)); + verify(policyRunner, times(1)).runInput(eq(binding.policy()), eq(binding.input()), any()); } @Test void anIntervalMatchingTheSweepPeriodFiresEverySweepDespiteJitter() { - Policy policy = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + PolicyBinding binding = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); Instant t0 = Instant.parse("2026-06-05T10:00:00Z"); trigger.sweep(t0); // baseline // The sweep that fires runs a few ms late (scheduler jitter)... trigger.sweep(t0.plusSeconds(60).plusMillis(5)); - verify(policyRunner, times(1)).run(eq(policy)); + verify(policyRunner, times(1)).runInput(eq(binding.policy()), eq(binding.input()), any()); // ...and the next sweep lands exactly on the 60s grid. Anchoring lastFired to the due // time (not the jittered observation) means this must still fire, not alias to skip. trigger.sweep(t0.plusSeconds(120)); - verify(policyRunner, times(2)).run(eq(policy)); + verify(policyRunner, times(2)).runInput(eq(binding.policy()), eq(binding.input()), any()); } @Test void aGapFiresOnceNotOncePerMissedInterval() { - Policy policy = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + PolicyBinding binding = scheduled("p1", new Schedule.Every(1, Schedule.Unit.MINUTES)); + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); Instant t0 = Instant.parse("2026-06-05T10:00:00Z"); trigger.sweep(t0); // baseline // Ten minutes of downtime: nine missed due points collapse into one firing. trigger.sweep(t0.plusSeconds(600)); - verify(policyRunner, times(1)).run(eq(policy)); + verify(policyRunner, times(1)).runInput(eq(binding.policy()), eq(binding.input()), any()); // Not due again until a full interval after the latest due point. trigger.sweep(t0.plusSeconds(630)); - verify(policyRunner, times(1)).run(eq(policy)); + verify(policyRunner, times(1)).runInput(eq(binding.policy()), eq(binding.input()), any()); trigger.sweep(t0.plusSeconds(660)); - verify(policyRunner, times(2)).run(eq(policy)); + verify(policyRunner, times(2)).runInput(eq(binding.policy()), eq(binding.input()), any()); } @Test void doesNotFireBeforeTheNextScheduledTime() { - Policy policy = scheduled("p1", new Schedule.Daily(LocalTime.of(3, 0))); // 03:00 UTC daily - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + PolicyBinding binding = + scheduled("p1", new Schedule.Daily(LocalTime.of(3, 0))); // 03:00 UTC daily + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); Instant t0 = Instant.parse("2026-06-05T10:00:00Z"); trigger.sweep(t0); trigger.sweep(t0.plusSeconds(60)); // next 03:00 is far away - verify(policyRunner, never()).run(any()); + verify(policyRunner, never()).runInput(any(), any(), any()); } @Test void firesWeeklyOnAChosenDay() { // 2026-06-05 is a Friday; the next Monday 09:00 is the soonest firing. - Policy policy = + PolicyBinding binding = scheduled("p1", new Schedule.Weekly(Set.of(DayOfWeek.MONDAY), LocalTime.of(9, 0))); - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); Instant friday = Instant.parse("2026-06-05T10:00:00Z"); trigger.sweep(friday); // baseline trigger.sweep(Instant.parse("2026-06-08T09:00:00Z")); // Monday 09:00 - verify(policyRunner, times(1)).run(eq(policy)); + verify(policyRunner, times(1)).runInput(eq(binding.policy()), eq(binding.input()), any()); } @Test - void skipsPoliciesWithAnInvalidSchedule() { - Policy policy = scheduledWithRawOptions("p1", Map.of()); // no schedule - when(policyStore.findByTriggerType("schedule")).thenReturn(List.of(policy)); + void skipsInputsWithAnInvalidSchedule() { + PolicyBinding binding = scheduledWithRawOptions("p1", Map.of()); // no schedule + when(policyStore.findBindingsByTriggerType("schedule")).thenReturn(List.of(binding)); trigger.sweep(Instant.parse("2026-06-05T10:00:00Z")); - verify(policyRunner, never()).run(any()); + verify(policyRunner, never()).runInput(any(), any(), any()); } @Test void validateRejectsMissingSchedule() { + PolicyBinding binding = scheduledWithRawOptions("p1", Map.of()); assertThrows( IllegalArgumentException.class, - () -> trigger.validate(scheduledWithRawOptions("p1", Map.of()))); + () -> trigger.validate(binding.policy(), binding.input())); } @Test void validateRejectsAnInvalidSchedule() { Map options = Map.of("schedule", Map.of("type", "every", "count", -5, "unit", "MINUTES")); + PolicyBinding binding = scheduledWithRawOptions("p1", options); assertThrows( IllegalArgumentException.class, - () -> trigger.validate(scheduledWithRawOptions("p1", options))); + () -> trigger.validate(binding.policy(), binding.input())); } @Test @@ -162,21 +167,25 @@ class ScheduleTriggerTest { Map options = new LinkedHashMap<>(); options.put("schedule", new Schedule.Daily(LocalTime.of(2, 0))); options.put("zone", "Europe/London"); - trigger.validate(scheduledWithRawOptions("p1", options)); + PolicyBinding binding = scheduledWithRawOptions("p1", options); + trigger.validate(binding.policy(), binding.input()); } - private static Policy scheduled(String id, Schedule schedule) { + private static PolicyBinding scheduled(String id, Schedule schedule) { return scheduledWithRawOptions(id, Map.of("schedule", schedule)); } - private static Policy scheduledWithRawOptions(String id, Map options) { - return new Policy( - id, - "nightly", - "owner", - true, - new TriggerConfig("schedule", options), - List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), - OutputSpec.inline()); + private static PolicyBinding scheduledWithRawOptions(String id, Map options) { + PipelineInput input = new PipelineInput("s1", new TriggerConfig("schedule", options)); + Policy policy = + new Policy( + id, + "nightly", + "owner", + true, + List.of(input), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline()); + return new PolicyBinding(policy, input); } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java index 63b721dd6d..a5b06bde01 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/trigger/WebhookTriggerTest.java @@ -2,10 +2,12 @@ package stirling.software.proprietary.policy.trigger; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -19,8 +21,10 @@ import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.policy.engine.PolicyRunner; import stirling.software.proprietary.policy.engine.SweepKind; import stirling.software.proprietary.policy.model.OutputSpec; +import stirling.software.proprietary.policy.model.PipelineInput; import stirling.software.proprietary.policy.model.PipelineStep; import stirling.software.proprietary.policy.model.Policy; +import stirling.software.proprietary.policy.model.PolicyBinding; import stirling.software.proprietary.policy.model.TriggerConfig; import stirling.software.proprietary.policy.source.InProcessSourceStore; import stirling.software.proprietary.policy.source.Source; @@ -46,37 +50,45 @@ class WebhookTriggerTest { } @Test - void firesOnlyPoliciesReferencingTheDeliveredWebhook() { + void firesOnlyInputsReferencingTheDeliveredWebhook() { Policy matching = webhookPolicy("a", "whkA"); Policy other = webhookPolicy("b", "whkB"); - when(policyStore.findByTriggerType(TYPE)).thenReturn(List.of(matching, other)); + when(policyStore.findBindingsByTriggerType(TYPE)).thenReturn(bindings(matching, other)); trigger.fireForWebhook("whkA"); - verify(policyRunner).run(matching, SweepKind.LIGHT); - verify(policyRunner, never()).run(other, SweepKind.LIGHT); + verify(policyRunner).runInput(matching, matching.inputs().get(0), SweepKind.LIGHT); + verify(policyRunner, never()).runInput(eq(other), any(), any()); } @Test void ignoresADeliveryForAnUnknownWebhookId() { Policy policy = webhookPolicy("a", "whkA"); - when(policyStore.findByTriggerType(TYPE)).thenReturn(List.of(policy)); + when(policyStore.findBindingsByTriggerType(TYPE)).thenReturn(bindings(policy)); trigger.fireForWebhook("whkZ"); - verify(policyRunner, never()).run(any(), any(SweepKind.class)); + verify(policyRunner, never()).runInput(any(), any(), any()); } @Test void validateRequiresAWebhookSource() { + // A non-webhook source (here: an id that resolves to nothing) is rejected. + Policy notWebhook = policy("p", PipelineInput.manual("missing-source")); assertThrows( IllegalArgumentException.class, - () -> trigger.validate(policy("p", webhookTriggerConfig(), List.of()))); - trigger.validate(webhookPolicy("p", "whkA")); + () -> trigger.validate(notWebhook, notWebhook.inputs().get(0))); + + Policy hooked = webhookPolicy("p", "whkA"); + trigger.validate(hooked, hooked.inputs().get(0)); } - private static TriggerConfig webhookTriggerConfig() { - return new TriggerConfig(TYPE, Map.of()); + /** Every (policy, input) binding across the given policies, as the store would return them. */ + private static List bindings(Policy... policies) { + return Arrays.stream(policies) + .flatMap( + policy -> policy.inputs().stream().map(in -> new PolicyBinding(policy, in))) + .toList(); } private Policy webhookPolicy(String id, String webhookId) { @@ -98,17 +110,16 @@ class WebhookTriggerTest { "owner", null)) .id(); - return policy(id, webhookTriggerConfig(), List.of(sourceId)); + return policy(id, new PipelineInput(sourceId, new TriggerConfig(TYPE, Map.of()))); } - private static Policy policy(String id, TriggerConfig trigger, List sourceIds) { + private static Policy policy(String id, PipelineInput input) { return new Policy( id, "hook", "owner", true, - trigger, - sourceIds, + List.of(input), List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), OutputSpec.inline()); } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/AltTextRoundTripTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/AltTextRoundTripTest.java new file mode 100644 index 0000000000..0f8e52d9ec --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/AltTextRoundTripTest.java @@ -0,0 +1,115 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.controller.api.converters.ConvertPdfToPdfUa; +import stirling.software.proprietary.model.api.ua.AccessibilityReport; +import stirling.software.proprietary.model.api.ua.FigureDescriptor; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** + * The alt-text loop end to end: the report hands out keys the conversion accepts. The converter + * never invents descriptions, so a caller must be able to supply them. + */ +class AltTextRoundTripTest { + + private static PdfUaConversionService conversion; + private static AccessibilityAuditService audit; + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + conversion = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + audit = new AccessibilityAuditService(validation); + } + + private static TaggingOptions.TaggingOptionsBuilder options() { + return TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Illustrated") + .embedFonts(false); + } + + @Test + @DisplayName("the report names the figures that need describing, with usable keys") + void reportEnumeratesFigures() throws Exception { + byte[] input = PdfUaTestDocuments.imageDocument(); + AccessibilityReport report = audit.audit(input, PdfUaProfile.UA1); + + assertFalse( + report.getFiguresNeedingDescription().isEmpty(), + "a document with an undescribed image must say which figure needs text"); + + FigureDescriptor figure = report.getFiguresNeedingDescription().get(0); + assertTrue(figure.key().matches("\\d+:\\d+"), "key should be pageIndex:ordinal: " + figure); + assertEquals(1, figure.page(), "pages are reported 1-based for humans"); + assertTrue(figure.width() > 0 && figure.height() > 0, "figure should carry its box"); + } + + @Test + @DisplayName("feeding the report's key back makes the document conform") + void suppliedDescriptionClosesTheLoop() throws Exception { + byte[] input = PdfUaTestDocuments.imageDocument(); + + PdfUaConversionOutcome before = conversion.convert(input, options().build()); + assertFalse(before.declared(), "an undescribed image must block the claim"); + + String key = + audit.audit(input, PdfUaProfile.UA1).getFiguresNeedingDescription().get(0).key(); + PdfUaConversionOutcome after = + conversion.convert( + input, options().altTextByFigure(Map.of(key, "A blue rectangle")).build()); + + assertEquals( + 0, + after.tagging().figuresNeedingAltText(), + "the description supplied against the report's own key was not applied"); + assertTrue(after.declared(), "with every figure described the document should conform"); + } + + @Test + @DisplayName("the request's key=text form parses the way the report emits keys") + void parsesTheWireFormat() { + Map parsed = + ConvertPdfToPdfUa.parseAltText( + "0:12=Bar chart of quarterly revenue\r\n" + + "1:3=Company logo\n" + + " \n" + + "malformed-line\n" + + "2:7=Diagram showing the approval flow = end to end"); + + assertEquals(3, parsed.size(), "blank and malformed lines are skipped: " + parsed); + assertEquals("Bar chart of quarterly revenue", parsed.get("0:12")); + assertEquals("Company logo", parsed.get("1:3")); + assertEquals( + "Diagram showing the approval flow = end to end", + parsed.get("2:7"), + "only the first equals splits, so descriptions may contain one"); + } + + @Test + @DisplayName("no descriptions supplied means none invented") + void emptyInputInventsNothing() { + assertTrue(ConvertPdfToPdfUa.parseAltText(null).isEmpty()); + assertTrue(ConvertPdfToPdfUa.parseAltText(" ").isEmpty()); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUa2ProfileTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUa2ProfileTest.java new file mode 100644 index 0000000000..9cf342824f --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUa2ProfileTest.java @@ -0,0 +1,92 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** PDF/UA-2 is not just a metadata number: it needs PDF 2.0 and namespaced structure types. */ +class PdfUa2ProfileTest { + + private static PdfUaConversionService conversion; + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + conversion = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + } + + private static PdfUaConversionOutcome convertUa2(byte[] input) throws Exception { + return conversion.convert( + input, + TaggingOptions.builder() + .profile(PdfUaProfile.UA2) + .language("en-GB") + .title("UA-2 Document") + .embedFonts(false) + .build()); + } + + @Test + @DisplayName("raises the file to PDF 2.0 and namespaces the structure tree") + void producesPdf2WithNamespaces() throws Exception { + PdfUaConversionOutcome outcome = convertUa2(PdfUaTestDocuments.headingHierarchy()); + + try (PDDocument document = Loader.loadPDF(outcome.pdfBytes())) { + assertEquals(2.0f, document.getVersion(), "UA-2 is defined on PDF 2.0"); + + var root = document.getDocumentCatalog().getStructureTreeRoot(); + assertNotNull(root, "no structure tree was written"); + assertNotNull( + root.getCOSObject().getDictionaryObject(COSName.getPDFName("Namespaces")), + "UA-2 requires the standard structure namespace to be declared"); + } + } + + @Test + @DisplayName("validates against the PDF/UA-2 profile, not the UA-1 one") + void validatesAgainstUa2() throws Exception { + PdfUaConversionOutcome outcome = convertUa2(PdfUaTestDocuments.simpleDocument()); + assertEquals("PDF/UA-2", outcome.validation().profile()); + } + + @Test + @DisplayName("reaches UA-2 conformance and declares it") + void reachesUa2Conformance() throws Exception { + PdfUaConversionOutcome outcome = convertUa2(PdfUaTestDocuments.simpleDocument()); + + String failures = + outcome.validation().issues().stream() + .map(issue -> issue.getClause() + ": " + issue.getTechnicalMessage()) + .collect(java.util.stream.Collectors.joining("; ")); + assertEquals(0, outcome.validation().totalFailures(), "UA-2 checks failed: " + failures); + assertTrue(outcome.declared(), "a conforming UA-2 file must carry the declaration"); + assertTrue(outcome.pdfBytes().length > 0); + } + + @Test + @DisplayName("an illustrated document still cannot claim UA-2 without descriptions") + void undescribedImageBlocksTheUa2Claim() throws Exception { + PdfUaConversionOutcome outcome = convertUa2(PdfUaTestDocuments.imageDocument()); + assertFalse(outcome.declared(), "an undescribed image must block the claim"); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaConversionIntegrationTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaConversionIntegrationTest.java new file mode 100644 index 0000000000..e785957b7b --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaConversionIntegrationTest.java @@ -0,0 +1,231 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.AccessibilityIssue; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.model.api.ua.UaValidationResult; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** + * End-to-end conversion over the fixture corpus, validated with veraPDF. The corpus is deliberately + * varied: what breaks a tagger is rarely the simple case. + */ +class PdfUaConversionIntegrationTest { + + private static PdfUaConversionService service; + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + service = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + } + + /** Fixtures already embed fonts, so the Ghostscript pass is off to keep tests hermetic. */ + private static TaggingOptions options() { + return TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Test Document") + .embedFonts(false) + .build(); + } + + private static PdfUaConversionOutcome convert(byte[] input) throws IOException { + return service.convert(input, options()); + } + + private static String extractText(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setSortByPosition(true); + return PdfUaRealCorpusTest.normalise(stripper.getText(document)); + } + } + + @Test + @DisplayName("every fixture converts without error and gains a structure tree") + void corpusConverts() throws Exception { + Map> corpus = corpus(); + StringBuilder report = new StringBuilder("\nPDF/UA conversion over the fixture corpus\n"); + + for (Map.Entry> entry : corpus.entrySet()) { + byte[] input = + entry.getKey().equals("empty") + ? entry.getValue().call() + : entry.getValue().call(); + PdfUaConversionOutcome outcome = convert(input); + + assertNotNull(outcome.pdfBytes(), entry.getKey() + " produced no output"); + report.append( + String.format( + " %-18s declared=%-5s failures=%-3d elements=%-3d artifacts=%-3d altNeeded=%d%n", + entry.getKey(), + outcome.declared(), + outcome.validation().totalFailures(), + outcome.tagging().taggedElements(), + outcome.tagging().artifacts(), + outcome.tagging().figuresNeedingAltText())); + for (AccessibilityIssue issue : outcome.validation().issues()) { + report.append( + String.format( + " clause %-6s x%-4d %s%n", + issue.getClause(), + issue.getOccurrences(), + issue.getTechnicalMessage())); + } + } + System.out.println(report); + } + + @Test + @DisplayName("tagging never changes the text content of a page") + void textIsPreserved() throws Exception { + for (Map.Entry> entry : corpus().entrySet()) { + byte[] input = entry.getValue().call(); + String before = extractText(input); + String after = extractText(convert(input).pdfBytes()); + assertEquals(before, after, "text changed for fixture " + entry.getKey()); + } + } + + @Test + @DisplayName("a simple document gains a structure tree with headings and paragraphs") + void simpleDocumentIsTagged() throws Exception { + PdfUaConversionOutcome outcome = convert(PdfUaTestDocuments.simpleDocument()); + try (PDDocument document = Loader.loadPDF(outcome.pdfBytes())) { + assertNotNull( + document.getDocumentCatalog().getStructureTreeRoot(), + "no structure tree was written"); + assertTrue( + document.getDocumentCatalog().getMarkInfo() != null + && document.getDocumentCatalog().getMarkInfo().isMarked(), + "MarkInfo/Marked was not set"); + assertEquals("en-GB", document.getDocumentCatalog().getLanguage()); + assertEquals("Test Document", document.getDocumentInformation().getTitle()); + } + assertTrue(outcome.tagging().taggedElements() > 0, "nothing was tagged"); + } + + @Test + @DisplayName("running heads and page numbers become artifacts, not content") + void runningHeadersBecomeArtifacts() throws Exception { + PdfUaConversionOutcome outcome = convert(PdfUaTestDocuments.runningHeadersDocument()); + assertTrue( + outcome.tagging().artifacts() >= 4, + "expected the repeated header on each page to become an artifact, got " + + outcome.tagging().artifacts()); + } + + @Test + @DisplayName("an image is tagged as a figure and reported as needing alt text") + void imagesNeedAltText() throws Exception { + PdfUaConversionOutcome outcome = convert(PdfUaTestDocuments.imageDocument()); + assertEquals(1, outcome.tagging().figuresNeedingAltText()); + assertFalse( + outcome.declared(), + "a document with an undescribed image must not claim conformance"); + } + + @Test + @DisplayName("supplying alt text lets an illustrated document conform") + void suppliedAltTextIsApplied() throws Exception { + byte[] input = PdfUaTestDocuments.imageDocument(); + PdfUaConversionOutcome probe = convert(input); + assertEquals(1, probe.tagging().figuresNeedingAltText()); + + TaggingOptions withAlt = + options().toBuilder() + .altTextByFigure(Map.of(figureKey(input), "A blue rectangle")) + .build(); + PdfUaConversionOutcome outcome = service.convert(input, withAlt); + assertEquals( + 0, + outcome.tagging().figuresNeedingAltText(), + "alt text supplied by the caller was not applied"); + } + + @Test + @DisplayName("an empty document does not crash the converter") + void emptyDocumentIsHandled() throws Exception { + PdfUaConversionOutcome outcome = convert(PdfUaTestDocuments.emptyDocument()); + assertNotNull(outcome.pdfBytes()); + assertFalse(outcome.warnings().isEmpty(), "an empty document should warn"); + } + + @Test + @DisplayName("an un-OCRed scan is reported rather than silently declared conformant") + void scannedDocumentIsNotDeclared() throws Exception { + PdfUaConversionOutcome outcome = convert(PdfUaTestDocuments.scannedDocument()); + assertFalse(outcome.declared(), "a scan with no text layer must not claim conformance"); + } + + @Test + @DisplayName("validation of an untagged document reports the missing structure") + void untaggedDocumentFailsValidation() throws Exception { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + UaValidationResult result = + validation.validate(PdfUaTestDocuments.simpleDocument(), PdfUaProfile.UA1); + assertFalse(result.compliant(), "an untagged document cannot be PDF/UA compliant"); + assertTrue(result.hasIssues()); + } + + /** The key the tagger uses for figure alt text is "pageIndex:firstOrdinal". */ + private static String figureKey(byte[] input) throws IOException { + try (PDDocument document = Loader.loadPDF(input)) { + var pages = + new stirling.software.proprietary.pdf.ua.TaggedContentExtractor() + .extract(document); + for (var page : pages) { + for (var op : page.graphics()) { + return page.pageIndex() + ":" + op.ordinal(); + } + } + } + return "0:0"; + } + + private static Map> corpus() { + Map> corpus = new LinkedHashMap<>(); + corpus.put("simple", PdfUaTestDocuments::simpleDocument); + corpus.put("headings", PdfUaTestDocuments::headingHierarchy); + corpus.put("lists", PdfUaTestDocuments::listDocument); + corpus.put("table", PdfUaTestDocuments::tableDocument); + corpus.put("image", PdfUaTestDocuments::imageDocument); + corpus.put("runningHeads", PdfUaTestDocuments::runningHeadersDocument); + corpus.put("twoColumn", PdfUaTestDocuments::twoColumnDocument); + corpus.put("link", PdfUaTestDocuments::linkDocument); + corpus.put("empty", PdfUaTestDocuments::emptyDocument); + corpus.put("scanned", PdfUaTestDocuments::scannedDocument); + corpus.put("formXObject", PdfUaTestDocuments::formXObjectDocument); + corpus.put("multiStream", PdfUaTestDocuments::multiStreamDocument); + corpus.put("rotated", PdfUaTestDocuments::rotatedDocument); + corpus.put("offsetMediaBox", PdfUaTestDocuments::offsetMediaBoxDocument); + return corpus; + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHardeningTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHardeningTest.java new file mode 100644 index 0000000000..cf72013a13 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHardeningTest.java @@ -0,0 +1,322 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.pdfparser.PDFStreamParser; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PageContent; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.StructBlock; +import stirling.software.proprietary.pdf.ua.StructType; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** Covers the document shapes and failure modes the first round of tests missed. */ +class PdfUaHardeningTest { + + private static PdfUaConversionService service; + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + service = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + } + + private static PdfUaConversionOutcome convert(byte[] input) throws IOException { + return service.convert( + input, + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Hardening") + .embedFonts(false) + .build()); + } + + private static String extract(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setSortByPosition(true); + return PdfUaRealCorpusTest.normalise(stripper.getText(document)); + } + } + + @Nested + @DisplayName("document shapes") + class Shapes { + + @Test + @DisplayName("text inside a form XObject is attributed to the Do and tagged as prose") + void formXObjectTextIsTagged() throws Exception { + byte[] input = PdfUaTestDocuments.formXObjectDocument(); + assertTrue( + extract(input).contains("inside the form XObject"), + "fixture must actually draw text inside a form"); + + PdfUaConversionOutcome outcome = convert(input); + assertTrue(outcome.declared(), warnings(outcome)); + assertEquals(extract(input), extract(outcome.pdfBytes())); + assertEquals( + 0, + outcome.tagging().figuresNeedingAltText(), + "a form whose text is reachable must not degrade to an undescribed figure"); + } + + @Test + @DisplayName("a page built from multiple content streams converts as one sequence") + void multiStreamPageConverts() throws Exception { + byte[] input = PdfUaTestDocuments.multiStreamDocument(); + PdfUaConversionOutcome outcome = convert(input); + assertTrue(outcome.declared(), warnings(outcome)); + assertEquals(extract(input), extract(outcome.pdfBytes())); + } + + @Test + @DisplayName("a rotated page keeps its text and converts") + void rotatedPageConverts() throws Exception { + byte[] input = PdfUaTestDocuments.rotatedDocument(); + PdfUaConversionOutcome outcome = convert(input); + assertEquals(extract(input), extract(outcome.pdfBytes())); + assertTrue(outcome.tagging().taggedElements() > 0, "rotated text was not tagged"); + } + + @Test + @DisplayName("a MediaBox that does not start at the origin does not break analysis") + void offsetMediaBoxConverts() throws Exception { + byte[] input = PdfUaTestDocuments.offsetMediaBoxDocument(); + PdfUaConversionOutcome outcome = convert(input); + assertTrue(outcome.declared(), warnings(outcome)); + assertEquals(extract(input), extract(outcome.pdfBytes())); + } + + private static String warnings(PdfUaConversionOutcome outcome) { + return "warnings: " + + String.join(" | ", outcome.warnings()) + + " issues: " + + outcome.validation().issues(); + } + } + + @Nested + @DisplayName("pre-marked content") + class PreMarked { + + @Test + @DisplayName("existing BDC/EMC operators are stripped before new ones are written") + void stripsExistingMarkedContent() throws Exception { + byte[] premarked = premarkedDocument(); + PdfUaConversionOutcome outcome = convert(premarked); + + try (PDDocument document = Loader.loadPDF(outcome.pdfBytes())) { + Counts counts = countMarkedContent(document.getPage(0)); + assertEquals( + counts.opens(), + counts.closes(), + "unbalanced marked content after stripping and re-injection"); + assertFalse( + contentOf(document).contains("/OldTag"), + "the source's own marked content survived the rebuild"); + } + assertEquals(extract(premarked), extract(outcome.pdfBytes())); + } + + /** A document whose stream already contains a BDC sequence under a custom tag. */ + private static byte[] premarkedDocument() throws Exception { + byte[] plain = PdfUaTestDocuments.simpleDocument(); + try (PDDocument document = Loader.loadPDF(plain)) { + PDPage page = document.getPage(0); + String content; + try (InputStream in = page.getContents()) { + content = new String(in.readAllBytes(), StandardCharsets.ISO_8859_1); + } + String wrapped = "/OldTag <> BDC\n" + content + "\nEMC\n"; + var stream = new org.apache.pdfbox.pdmodel.common.PDStream(document); + try (var out = stream.createOutputStream()) { + out.write(wrapped.getBytes(StandardCharsets.ISO_8859_1)); + } + page.setContents(stream); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } + } + + private record Counts(int opens, int closes) {} + + private static Counts countMarkedContent(PDPage page) throws IOException { + int opens = 0; + int closes = 0; + PDFStreamParser parser = new PDFStreamParser(page); + Object token; + while ((token = parser.parseNextToken()) != null) { + if (token instanceof Operator operator) { + switch (operator.getName()) { + case "BDC", "BMC" -> opens++; + case "EMC" -> closes++; + default -> {} + } + } + } + return new Counts(opens, closes); + } + + private static String contentOf(PDDocument document) throws IOException { + try (InputStream in = document.getPage(0).getContents()) { + return new String(in.readAllBytes(), StandardCharsets.ISO_8859_1); + } + } + } + + @Nested + @DisplayName("honesty rules") + class Honesty { + + @Test + @DisplayName("suppressed text blocks the conformance claim even when validation passes") + void suppressedTextBlocksDeclaration() { + var structure = new stirling.software.proprietary.pdf.ua.DocumentStructure(); + structure.setTextSuppressed(true); + var result = new stirling.software.proprietary.pdf.ua.TaggingResult(structure, true); + assertTrue( + result.isContentSuppressed(), + "the suppression flag must survive into the tagging result"); + } + + @Test + @DisplayName("dropped lines are reported per page by the analyser") + void analyserWarnsOnDroppedLines() { + PageContent dropped = + new PageContent( + 0, + List.of(), + List.of(), + 5, + false, + false, + true, + new stirling.software.proprietary.pdf.ua.BBox(0, 0, 595, 842)); + var structure = + new stirling.software.proprietary.pdf.ua.LayoutAnalyzer() + .analyse(List.of(dropped)); + assertTrue(structure.isTextSuppressed()); + assertTrue( + structure.getWarnings().stream().anyMatch(w -> w.contains("page(s) 1")), + "warning should name the affected page: " + structure.getWarnings()); + } + } + + @Nested + @DisplayName("clause table") + class Clauses { + + @Test + @DisplayName("subclauses resolve to their parent entry, not to a string prefix") + void subclauseLookupWalksSegments() { + assertNotNull(PdfUaValidationService.lookupClause("7.21.4.1"), "7.21.4.1 -> 7.21"); + assertNotNull(PdfUaValidationService.lookupClause("7.18.1"), "7.18.1 -> 7.18"); + var toUnicode = PdfUaValidationService.lookupClause("7.21.7"); + assertNotNull(toUnicode); + assertFalse( + toUnicode.autoFixable(), + "a missing ToUnicode map is not fixable by embedding fonts"); + assertNull(PdfUaValidationService.lookupClause("9.9.9")); + assertNull(PdfUaValidationService.lookupClause(null)); + } + } + + @Nested + @DisplayName("range claiming") + class Claiming { + + @Test + @DisplayName("a figure drawn between two text runs on one line stays a figure") + void interleavedFigureIsNotSwallowed() throws Exception { + // Words at ordinals 0 and 2 with an image at ordinal 1 between them. + var line = + new stirling.software.proprietary.pdf.ua.TextLineInfo( + 0, + "left right", + new stirling.software.proprietary.pdf.ua.BBox(50, 700, 400, 712), + 11, + false, + 0, + 2, + false, + List.of( + new stirling.software.proprietary.pdf.ua.WordInfo( + "left", + new stirling.software.proprietary.pdf.ua.BBox( + 50, 700, 90, 712), + 0, + 0, + 11, + false), + new stirling.software.proprietary.pdf.ua.WordInfo( + "right", + new stirling.software.proprietary.pdf.ua.BBox( + 360, 700, 400, 712), + 2, + 2, + 11, + false))); + var image = + new stirling.software.proprietary.pdf.ua.MarkableOp( + 1, + stirling.software.proprietary.pdf.ua.MarkableOp.Kind.IMAGE, + new stirling.software.proprietary.pdf.ua.BBox(150, 650, 350, 760), + "Im0"); + PageContent page = + new PageContent( + 0, + List.of(line), + List.of(image), + 3, + false, + false, + false, + new stirling.software.proprietary.pdf.ua.BBox(0, 0, 595, 842)); + + var structure = + new stirling.software.proprietary.pdf.ua.LayoutAnalyzer() + .analyse(List.of(page)); + List figures = new java.util.ArrayList<>(); + structure.visit( + block -> { + if (block.getType() == StructType.FIGURE) { + figures.add(block); + } + }); + assertEquals( + 1, + figures.size(), + "the image between the words must survive as its own figure"); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHttpEndpointTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHttpEndpointTest.java new file mode 100644 index 0000000000..90c40af82b --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaHttpEndpointTest.java @@ -0,0 +1,183 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.common.util.TempFileManager; +import stirling.software.proprietary.controller.api.converters.ConvertPdfToPdfUa; +import stirling.software.proprietary.controller.api.security.AccessibilityReportController; + +/** + * Exercises the endpoints over HTTP, not through the service layer. Covers route mapping, multipart + * binding, and the headers and JSON a client depends on. + */ +class PdfUaHttpEndpointTest { + + private static MockMvc convertMvc; + private static MockMvc reportMvc; + private static final ObjectMapper JSON = new ObjectMapper(); + + @BeforeAll + static void setUp() throws Exception { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + PdfUaConversionService conversion = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + + // A real TempFileManager, so the streamed response path is exercised rather than mocked. + TempFileManager tempFiles = + new TempFileManager( + new stirling.software.common.util.TempFileRegistry(), + new stirling.software.common.model.ApplicationProperties()); + + // Stand in for the app's advice, which lives in core; without one every rejection is a 500. + var advice = new BadRequestAdvice(); + convertMvc = + MockMvcBuilders.standaloneSetup(new ConvertPdfToPdfUa(conversion, tempFiles)) + .setControllerAdvice(advice) + .build(); + reportMvc = + MockMvcBuilders.standaloneSetup( + new AccessibilityReportController( + new AccessibilityAuditService(validation))) + .setControllerAdvice(advice) + .build(); + } + + private static MockMultipartFile upload(byte[] pdf, String name) { + return new MockMultipartFile("fileInput", name, "application/pdf", pdf); + } + + /** Mirrors the one rule these endpoints rely on: a rejected input is a 400, not a 500. */ + @org.springframework.web.bind.annotation.RestControllerAdvice + static class BadRequestAdvice { + @org.springframework.web.bind.annotation.ExceptionHandler(IllegalArgumentException.class) + org.springframework.http.ResponseEntity badRequest(IllegalArgumentException ex) { + return org.springframework.http.ResponseEntity.badRequest().body(ex.getMessage()); + } + } + + @Test + @DisplayName("POST /api/v1/convert/pdf/ua returns a PDF and reports what it did in headers") + void conversionEndpointResponds() throws Exception { + MvcResult result = + convertMvc + .perform( + multipart("/api/v1/convert/pdf/ua") + .file(upload(PdfUaTestDocuments.simpleDocument(), "in.pdf")) + .param("language", "en-GB") + .param("title", "Over The Wire") + .param("embedFonts", "false")) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Stirling-UA-Declared")) + .andExpect(header().exists("X-Stirling-UA-Failures")) + .andReturn(); + + byte[] body = result.getResponse().getContentAsByteArray(); + assertTrue(body.length > 0, "no document came back"); + assertEquals( + "%PDF", + new String(body, 0, 4, java.nio.charset.StandardCharsets.ISO_8859_1), + "the response body is not a PDF"); + assertEquals( + "true", + result.getResponse().getHeader("X-Stirling-UA-Declared"), + "a simple embedded-font document should convert and be declared conformant"); + } + + @Test + @DisplayName("POST /api/v1/security/accessibility-report returns the figure inventory as JSON") + void reportEndpointResponds() throws Exception { + MvcResult result = + reportMvc + .perform( + multipart("/api/v1/security/accessibility-report") + .file(upload(PdfUaTestDocuments.imageDocument(), "in.pdf")) + .param("profile", "ua1")) + .andExpect(status().isOk()) + .andReturn(); + + JsonNode json = JSON.readTree(result.getResponse().getContentAsString()); + assertEquals("PDF/UA-1", json.get("profile").asText()); + assertNotNull(json.get("summary"), "the report should carry a summary"); + + JsonNode figures = json.get("figuresNeedingDescription"); + assertNotNull(figures, "the field a caller needs to supply alt text is missing"); + assertTrue(figures.isArray() && figures.size() > 0, "the image should be listed: " + json); + assertTrue( + figures.get(0).get("key").asText().matches("\\d+:\\d+"), + "the key must be usable in a follow-up conversion request"); + } + + @Test + @DisplayName("alt text supplied as form data reaches the converter over HTTP") + void altTextBindsFromFormData() throws Exception { + byte[] input = PdfUaTestDocuments.imageDocument(); + + // Discover the key the way a client would, through the report endpoint. + MvcResult reported = + reportMvc + .perform( + multipart("/api/v1/security/accessibility-report") + .file(upload(input, "in.pdf"))) + .andExpect(status().isOk()) + .andReturn(); + String key = + JSON.readTree(reported.getResponse().getContentAsString()) + .get("figuresNeedingDescription") + .get(0) + .get("key") + .asText(); + + MvcResult converted = + convertMvc + .perform( + multipart("/api/v1/convert/pdf/ua") + .file(upload(input, "in.pdf")) + .param("embedFonts", "false") + .param("altText", key + "=A blue rectangle")) + .andExpect(status().isOk()) + .andReturn(); + + assertEquals( + "0", + converted.getResponse().getHeader("X-Stirling-UA-Figures-Needing-Alt"), + "the description posted as form data was not applied"); + } + + @Test + @DisplayName("a request with no file is rejected rather than processed") + void missingFileIsRejected() throws Exception { + convertMvc + .perform( + multipart("/api/v1/convert/pdf/ua") + .file( + new MockMultipartFile( + "fileInput", + "e.pdf", + "application/pdf", + new byte[0]))) + .andExpect(status().is4xxClientError()); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaRealCorpusTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaRealCorpusTest.java new file mode 100644 index 0000000000..9ebda08736 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaRealCorpusTest.java @@ -0,0 +1,249 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** + * Runs the converter over every PDF in the repository, where real-tool output breaks assumptions. A + * clean refusal counts as a pass; nothing may crash or make a false conformance claim. + */ +class PdfUaRealCorpusTest { + + private static PdfUaConversionService service; + private static Path repoRoot; + + /** Files the converter is expected to refuse rather than process. */ + private static final List EXPECTED_REJECTS = List.of("encrypted.pdf", "corrupted.pdf"); + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + service = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + repoRoot = findRepoRoot(); + } + + private static Path findRepoRoot() { + Path current = Path.of("").toAbsolutePath(); + while (current != null && !Files.exists(current.resolve("settings.gradle"))) { + current = current.getParent(); + } + return current; + } + + private record Outcome(String name, String status, int failures, int elements, int artifacts) {} + + private static TaggingOptions.TaggingOptionsBuilder options(String fallbackTitle) { + return TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .fallbackTitle(fallbackTitle) + .existingTags(TaggingOptions.ExistingTags.REBUILD); + } + + @Test + @DisplayName("converts every PDF in the repository without crashing or lying about conformance") + void realCorpusConverts() throws Exception { + assertNotNull(repoRoot, "could not locate the repository root"); + List pdfs = findPdfs(); + assertTrue(pdfs.size() >= 20, "expected a substantial corpus, found " + pdfs.size()); + + List outcomes = new ArrayList<>(); + List crashes = new ArrayList<>(); + java.util.Map clauseFiles = new java.util.TreeMap<>(); + java.util.Map clauseText = new java.util.HashMap<>(); + java.util.Map clauseExamples = new java.util.HashMap<>(); + + for (Path pdf : pdfs) { + String name = repoRoot.relativize(pdf).toString().replace('\\', '/'); + byte[] input; + try { + input = Files.readAllBytes(pdf); + } catch (IOException e) { + continue; + } + try { + String stem = pdf.getFileName().toString().replaceFirst("\\.pdf$", ""); + + // Fidelity is a tagging property, so measure it with font embedding off. + PdfUaConversionOutcome taggedOnly = + service.convert(input, options(stem).embedFonts(false).build()); + assertTextPreserved(name, input, taggedOnly.pdfBytes()); + + PdfUaConversionOutcome outcome = service.convert(input, options(stem).build()); + // Full pipeline too: Ghostscript can exit 0 having blanked the document. + assertTextPreserved(name + " (with font embedding)", input, outcome.pdfBytes()); + outcomes.add( + new Outcome( + name, + outcome.declared() ? "CONFORMS" : "improved", + outcome.validation().totalFailures(), + outcome.tagging().taggedElements(), + outcome.tagging().artifacts())); + outcome.validation() + .issues() + .forEach( + issue -> { + clauseFiles.merge(issue.getClause(), 1, Integer::sum); + clauseText.putIfAbsent( + issue.getClause(), issue.getTechnicalMessage()); + clauseExamples.putIfAbsent(issue.getClause(), name); + }); + + } catch (IOException e) { + // A refusal with an explanation is an acceptable outcome. + outcomes.add(new Outcome(name, "refused: " + e.getMessage(), 0, 0, 0)); + } catch (RuntimeException e) { + crashes.add(name + " -> " + e); + outcomes.add(new Outcome(name, "CRASH: " + e, 0, 0, 0)); + } + } + + System.out.println(render(outcomes)); + + StringBuilder clauses = + new StringBuilder("\nBlocking clauses, by number of files affected\n"); + clauseFiles.entrySet().stream() + .sorted(java.util.Map.Entry.comparingByValue().reversed()) + .forEach( + e -> + clauses.append( + String.format( + " clause %-9s %-3d files e.g. %s%n %s%n", + e.getKey(), + e.getValue(), + clauseExamples.get(e.getKey()), + abbreviate(clauseText.get(e.getKey()))))); + System.out.println(clauses); + + List unexpectedCrashes = + crashes.stream() + .filter(c -> EXPECTED_REJECTS.stream().noneMatch(c::contains)) + .toList(); + assertTrue( + unexpectedCrashes.isEmpty(), + "converter crashed on: " + String.join("; ", unexpectedCrashes)); + } + + /** Tagging must not change extracted text; a diff means the rewrite corrupted the page. */ + private static void assertTextPreserved(String name, byte[] before, byte[] after) { + String textBefore = safeExtract(before); + if (textBefore == null) { + // The source itself is unreadable, so there is nothing to compare against. + return; + } + String textAfter = safeExtract(after); + assertTrue( + textAfter != null, "the converted file could not be read back at all for " + name); + assertTrue( + textBefore.equals(textAfter), + "tagging changed extracted text for " + + name + + "\n before: " + + preview(textBefore) + + "\n after: " + + preview(textAfter)); + } + + private static String abbreviate(String text) { + if (text == null) { + return ""; + } + return text.length() <= 110 ? text : text.substring(0, 110) + "..."; + } + + private static String preview(String text) { + return text.length() <= 160 ? text : text.substring(0, 160) + "..."; + } + + private static String safeExtract(byte[] pdf) { + try (PDDocument document = Loader.loadPDF(pdf)) { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setSortByPosition(true); + return normalise(stripper.getText(document)); + } catch (Exception e) { + return null; + } + } + + /** Drops invisible formatting characters: a rebuild legitimately loses soft hyphens. */ + static String normalise(String text) { + StringBuilder sb = new StringBuilder(text.length()); + text.codePoints() + .forEach( + cp -> { + if (Character.getType(cp) != Character.FORMAT && cp != 0x00AD) { + sb.appendCodePoint(cp); + } + }); + return sb.toString().replaceAll("\\s+", " ").strip(); + } + + private List findPdfs() throws IOException { + try (Stream stream = Files.walk(repoRoot)) { + return stream.filter(Files::isRegularFile) + .filter(p -> p.toString().toLowerCase().endsWith(".pdf")) + .filter(p -> !p.toString().contains("node_modules")) + .filter(p -> !p.toString().contains(File_BUILD)) + .filter(p -> !p.toString().contains(".git")) + .sorted(Comparator.comparing(Path::toString)) + .toList(); + } + } + + private static final String File_BUILD = "build" + java.io.File.separator; + + private static String render(List outcomes) { + StringBuilder sb = new StringBuilder("\nPDF/UA conversion over the repository corpus\n"); + long conforming = outcomes.stream().filter(o -> "CONFORMS".equals(o.status())).count(); + long refused = outcomes.stream().filter(o -> o.status().startsWith("refused")).count(); + sb.append( + String.format( + " %d files: %d conform, %d improved but not conformant, %d refused%n%n", + outcomes.size(), + conforming, + outcomes.size() - conforming - refused, + refused)); + for (Outcome outcome : outcomes) { + sb.append( + String.format( + " %-62s %-10s fail=%-4d el=%-5d art=%d%n", + outcome.name().length() > 60 + ? "..." + outcome.name().substring(outcome.name().length() - 57) + : outcome.name(), + outcome.status().length() > 10 + ? outcome.status().substring(0, 10) + : outcome.status(), + outcome.failures(), + outcome.elements(), + outcome.artifacts())); + } + return sb.toString(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaSampleDumpTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaSampleDumpTest.java new file mode 100644 index 0000000000..46cdb37c76 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaSampleDumpTest.java @@ -0,0 +1,103 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** + * Dumps converted output for independent checkers; validating PDFBox with PDFBox is circular. Off + * by default as it writes outside the build directory: run with {@code DUMP_UA_SAMPLES=

}. + */ +@EnabledIfEnvironmentVariable(named = "DUMP_UA_SAMPLES", matches = ".+") +class PdfUaSampleDumpTest { + + private static PdfUaConversionService service; + private static Path repoRoot; + + @BeforeAll + static void setUp() { + PdfUaValidationService validation = new PdfUaValidationService(); + validation.initialise(); + service = + new PdfUaConversionService( + validation, + new FontEmbeddingService(), + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + repoRoot = Path.of("").toAbsolutePath(); + while (repoRoot != null && !Files.exists(repoRoot.resolve("settings.gradle"))) { + repoRoot = repoRoot.getParent(); + } + } + + @Test + @DisplayName("writes original and converted pairs for external validation") + void dumpSamples() throws Exception { + Path out = Path.of(System.getenv("DUMP_UA_SAMPLES")); + Files.createDirectories(out); + + List pdfs; + try (Stream stream = Files.walk(repoRoot)) { + pdfs = + stream.filter(Files::isRegularFile) + .filter(p -> p.toString().toLowerCase().endsWith(".pdf")) + .filter(p -> !p.toString().contains("node_modules")) + .filter(p -> !p.toString().contains(java.io.File.separator + "build")) + .filter(p -> !p.toString().contains(".git")) + .sorted() + .toList(); + } + + List manifest = new ArrayList<>(); + int written = 0; + for (Path pdf : pdfs) { + String stem = pdf.getFileName().toString().replaceFirst("\\.pdf$", ""); + byte[] input; + try { + input = Files.readAllBytes(pdf); + } catch (Exception e) { + continue; + } + try { + PdfUaConversionOutcome outcome = + service.convert( + input, + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .fallbackTitle(stem) + .existingTags(TaggingOptions.ExistingTags.REBUILD) + .build()); + Files.write(out.resolve(stem + "__before.pdf"), input); + Files.write(out.resolve(stem + "__after.pdf"), outcome.pdfBytes()); + manifest.add( + stem + + "\tdeclared=" + + outcome.declared() + + "\tfailures=" + + outcome.validation().totalFailures()); + written++; + } catch (Exception e) { + manifest.add(stem + "\tREFUSED\t" + e.getMessage()); + } + } + Files.write(out.resolve("manifest.tsv"), manifest); + System.out.println("Wrote " + written + " before/after pairs to " + out); + assertTrue(written > 10, "expected a usable sample set, wrote " + written); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaServicesTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaServicesTest.java new file mode 100644 index 0000000000..9d2e14ee0d --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaServicesTest.java @@ -0,0 +1,319 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; +import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.proprietary.model.api.ua.AccessibilityReport; +import stirling.software.proprietary.model.api.ua.PdfUaConversionOutcome; +import stirling.software.proprietary.model.api.ua.UaValidationResult; +import stirling.software.proprietary.pdf.ua.PdfUaProfile; +import stirling.software.proprietary.pdf.ua.TaggingOptions; + +/** Tests for the services that validate, audit and convert. */ +class PdfUaServicesTest { + + private static PdfUaValidationService validation; + private static PdfUaConversionService conversion; + private static AccessibilityAuditService audit; + private static FontEmbeddingService fonts; + + @BeforeAll + static void setUp() { + validation = new PdfUaValidationService(); + validation.initialise(); + fonts = new FontEmbeddingService(); + conversion = + new PdfUaConversionService( + validation, + fonts, + new CustomPDFDocumentFactory( + org.mockito.Mockito.mock(PdfMetadataService.class))); + audit = new AccessibilityAuditService(validation); + } + + /** Uses a standard 14 font deliberately: never embedded, which clause 7.21 forbids. */ + private static byte[] unembeddedFontPdf() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + cs.beginText(); + cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + cs.newLineAtOffset(50, 700); + cs.showText("Hello accessibility"); + cs.endText(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } + } + + private static byte[] manyPages(int pages) throws IOException { + try (PDDocument document = new PDDocument()) { + for (int i = 0; i < pages; i++) { + document.addPage(new PDPage()); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } + } + + private static byte[] encryptedPdf() throws IOException { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage()); + AccessPermission permissions = new AccessPermission(); + document.protect(new StandardProtectionPolicy("owner", "user", permissions)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } + } + + @Nested + @DisplayName("validation") + class Validation { + + @Test + @DisplayName("an untagged document fails and the failures are grouped by rule") + void untaggedFails() throws Exception { + UaValidationResult result = validation.validate(unembeddedFontPdf(), PdfUaProfile.UA1); + assertFalse(result.compliant()); + assertTrue(result.hasIssues()); + assertTrue( + result.totalFailures() >= result.issues().size(), + "grouping must not invent failures"); + assertTrue( + result.issues().stream().allMatch(i -> i.getOccurrences() > 0), + "every grouped issue should count its occurrences"); + } + + @Test + @DisplayName("malformed input reports a failure instead of throwing") + void malformedInputIsReported() { + UaValidationResult result = + validation.validate("not a pdf".getBytes(), PdfUaProfile.UA1); + assertFalse(result.compliant()); + assertFalse(result.issues().isEmpty()); + } + + @Test + @DisplayName("issues carry plain-English text as well as the validator's own wording") + void issuesAreReadable() throws Exception { + UaValidationResult result = validation.validate(unembeddedFontPdf(), PdfUaProfile.UA1); + assertTrue( + result.issues().stream() + .allMatch(i -> i.getMessage() != null && !i.getMessage().isBlank())); + } + } + + @Nested + @DisplayName("auditing") + class Auditing { + + @Test + @DisplayName("reports the document facts that drive most failures") + void reportsSummary() throws Exception { + AccessibilityReport report = audit.audit(unembeddedFontPdf(), PdfUaProfile.UA1); + + assertFalse(report.isTagged(), "the fixture has no structure tree"); + assertFalse(report.isDeclaresConformance()); + assertFalse(report.isPassesAutomatedChecks()); + assertEquals(1, report.getSummary().getPages()); + assertFalse(report.getSummary().isAllFontsEmbedded()); + assertTrue(report.getSummary().getUnembeddedFonts() > 0); + assertFalse(report.getSummary().isHasLanguage()); + assertFalse(report.getSummary().isHasTitle()); + } + + @Test + @DisplayName("always lists the checks a person still has to make") + void listsHumanChecks() throws Exception { + AccessibilityReport report = audit.audit(unembeddedFontPdf(), PdfUaProfile.UA1); + assertFalse( + report.getHumanChecks().isEmpty(), + "a report showing only automated results implies the rest does not exist"); + } + + @Test + @DisplayName("splits failures into automatically fixable and needs-input") + void splitsRemediability() throws Exception { + AccessibilityReport report = audit.audit(unembeddedFontPdf(), PdfUaProfile.UA1); + assertEquals( + report.getIssues().size(), + report.getAutomaticallyFixable() + report.getNeedsInput()); + } + + @Test + @DisplayName("refuses a document past the page cap the conversion also applies") + void refusesTooManyPages() throws Exception { + byte[] oversized = manyPages(2001); + assertThrows( + IllegalArgumentException.class, + () -> audit.audit(oversized, PdfUaProfile.UA1), + "an uncapped report walks every page of any document a caller uploads"); + } + + @Test + @DisplayName("a converted document reports as tagged and conformant") + void reportsAfterConversion() throws Exception { + PdfUaConversionOutcome outcome = + conversion.convert( + unembeddedFontPdf(), + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Converted") + .build()); + AccessibilityReport report = audit.audit(outcome.pdfBytes(), PdfUaProfile.UA1); + assertTrue(report.isTagged()); + assertTrue(report.getSummary().isHasTitle()); + assertTrue(report.getSummary().isHasLanguage()); + assertTrue(report.getSummary().isDisplaysDocTitle()); + } + } + + @Nested + @DisplayName("font embedding") + class Fonts { + + @Test + @DisplayName("detects a standard 14 font as unembedded") + void detectsUnembedded() throws Exception { + assertTrue(fonts.hasUnembeddedFonts(unembeddedFontPdf())); + } + + @Test + @DisplayName("embeds fonts, or explains why it could not") + void embedsOrExplains() throws Exception { + FontEmbeddingService.Result result = fonts.embedFonts(unembeddedFontPdf()); + assertNotNull(result.pdfBytes()); + if (result.changed()) { + assertFalse( + fonts.hasUnembeddedFonts(result.pdfBytes()), + "embedding reported success but fonts are still missing"); + } else { + assertNotNull( + result.warning(), "failing to embed must be explained, not passed over"); + } + } + + @Test + @DisplayName("leaves a document alone when every font is already embedded") + void skipsWhenNothingToDo() throws Exception { + byte[] embedded = PdfUaTestDocuments.simpleDocument(); + FontEmbeddingService.Result result = fonts.embedFonts(embedded); + assertFalse(result.changed()); + assertEquals(embedded.length, result.pdfBytes().length); + } + } + + @Nested + @DisplayName("conversion") + class Conversion { + + @Test + @DisplayName("refuses an encrypted document with an explanation") + void refusesEncrypted() throws Exception { + byte[] encrypted = encryptedPdf(); + IOException error = + assertThrows( + IOException.class, + () -> + conversion.convert( + encrypted, + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .build())); + assertTrue(error.getMessage().toLowerCase().contains("encrypted")); + } + + @Test + @DisplayName("keeping existing tags does not rebuild the tree") + void keepRespectsExistingTags() throws Exception { + byte[] tagged = + conversion + .convert( + PdfUaTestDocuments.simpleDocument(), + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("First pass") + .embedFonts(false) + .build()) + .pdfBytes(); + + PdfUaConversionOutcome second = + conversion.convert( + tagged, + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Second pass") + .embedFonts(false) + .existingTags(TaggingOptions.ExistingTags.KEEP) + .build()); + + assertFalse(second.tagging().rebuiltStructure(), "KEEP must not rebuild"); + try (PDDocument document = Loader.loadPDF(second.pdfBytes())) { + assertEquals("Second pass", document.getDocumentInformation().getTitle()); + } + } + + @Test + @DisplayName("marking images decorative removes the alt-text blocker") + void decorativePolicyClearsFigures() throws Exception { + PdfUaConversionOutcome outcome = + conversion.convert( + PdfUaTestDocuments.imageDocument(), + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Decorative") + .embedFonts(false) + .figurePolicy(TaggingOptions.FigurePolicy.MARK_DECORATIVE) + .build()); + assertEquals(0, outcome.tagging().figuresNeedingAltText()); + assertTrue(outcome.declared(), "with no undescribed figures the file should conform"); + } + + @Test + @DisplayName("converting twice produces the same conformance verdict") + void conversionIsStable() throws Exception { + byte[] input = PdfUaTestDocuments.headingHierarchy(); + TaggingOptions options = + TaggingOptions.builder() + .profile(PdfUaProfile.UA1) + .language("en-GB") + .title("Stable") + .embedFonts(false) + .build(); + PdfUaConversionOutcome first = conversion.convert(input, options); + PdfUaConversionOutcome second = conversion.convert(first.pdfBytes(), options); + assertEquals(first.declared(), second.declared()); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaTestDocuments.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaTestDocuments.java new file mode 100644 index 0000000000..589edbc1a2 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaTestDocuments.java @@ -0,0 +1,390 @@ +package stirling.software.proprietary.service.ua; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDFormContentStream; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.util.Matrix; + +/** + * Builds the fixture corpus used by the PDF/UA tests. Fonts are embedded deliberately: the standard + * 14 fail clause 7.21 and would mask every result. + */ +final class PdfUaTestDocuments { + + private static final String FONT_RESOURCE = "/static/fonts/DejaVuSans.ttf"; + // The font ships with core's resources, which are not on this module's classpath. + private static final String FONT_REPO_PATH = + "app/core/src/main/resources/static/fonts/DejaVuSans.ttf"; + private static final float MARGIN = 60f; + + private PdfUaTestDocuments() {} + + static PDFont font(PDDocument document) throws IOException { + try (InputStream in = PdfUaTestDocuments.class.getResourceAsStream(FONT_RESOURCE)) { + if (in != null) { + return PDType0Font.load(document, in, true); + } + } + Path repoRoot = Path.of("").toAbsolutePath(); + while (repoRoot != null && !Files.exists(repoRoot.resolve("settings.gradle"))) { + repoRoot = repoRoot.getParent(); + } + Path font = repoRoot == null ? null : repoRoot.resolve(FONT_REPO_PATH); + if (font == null || !Files.exists(font)) { + throw new IOException( + "Test font not found: " + FONT_RESOURCE + " or " + FONT_REPO_PATH); + } + try (InputStream in = Files.newInputStream(font)) { + return PDType0Font.load(document, in, true); + } + } + + /** A heading followed by two paragraphs: the simplest thing that should convert cleanly. */ + static byte[] simpleDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 20, MARGIN, y, "Quarterly Report"); + y -= 14; + y = text(cs, font, 11, MARGIN, y, "This document summarises the results for the"); + y = text(cs, font, 11, MARGIN, y, "period and outlines the outlook for next year."); + y -= 14; + text(cs, font, 11, MARGIN, y, "A second paragraph follows the first one here."); + } + return bytes(document); + } + } + + /** Three heading tiers, to exercise level assignment and the no-skipped-levels rule. */ + static byte[] headingHierarchy() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 24, MARGIN, y, "Annual Review"); + y -= 12; + y = text(cs, font, 11, MARGIN, y, "Introductory prose sits under the title here."); + y -= 16; + y = text(cs, font, 17, MARGIN, y, "Financial Results"); + y -= 10; + y = text(cs, font, 11, MARGIN, y, "Revenue grew steadily across every region."); + y -= 16; + y = text(cs, font, 13, MARGIN, y, "Europe"); + y -= 10; + text(cs, font, 11, MARGIN, y, "European revenue rose by eleven per cent."); + } + return bytes(document); + } + } + + /** A bulleted and a numbered list. */ + static byte[] listDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 18, MARGIN, y, "Checklist"); + y -= 14; + y = text(cs, font, 11, MARGIN, y, "• Review the source document"); + y = text(cs, font, 11, MARGIN, y, "• Check every heading level"); + y = text(cs, font, 11, MARGIN, y, "• Describe each image"); + y -= 16; + y = text(cs, font, 11, MARGIN, y, "1. Open the file"); + y = text(cs, font, 11, MARGIN, y, "2. Run the converter"); + text(cs, font, 11, MARGIN, y, "3. Validate the result"); + } + return bytes(document); + } + } + + /** A three-column table whose cells each occupy their own text-showing operator. */ + static byte[] tableDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 18, MARGIN, y, "Regional Totals"); + y -= 20; + String[][] rows = { + {"Region", "Units", "Revenue"}, + {"North", "1200", "48000"}, + {"South", "980", "39200"}, + {"East", "1430", "57200"} + }; + float[] columns = {MARGIN, MARGIN + 160, MARGIN + 300}; + for (String[] row : rows) { + tableRow(cs, font, 11, columns, y, row); + y -= 20; + } + } + return bytes(document); + } + } + + /** A page with a real image, which must end up as a Figure needing alternative text. */ + static byte[] imageDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + BufferedImage bitmap = new BufferedImage(120, 90, BufferedImage.TYPE_INT_RGB); + java.awt.Graphics2D graphics = bitmap.createGraphics(); + graphics.setColor(Color.BLUE); + graphics.fillRect(0, 0, 120, 90); + graphics.dispose(); + PDImageXObject image = LosslessFactory.createFromImage(document, bitmap); + + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 18, MARGIN, y, "Illustrated Page"); + y -= 20; + y = text(cs, font, 11, MARGIN, y, "The chart below shows the trend."); + cs.drawImage(image, MARGIN, y - 120, 180, 100); + } + return bytes(document); + } + } + + /** Four pages sharing a running head and a page number, which must become artifacts. */ + static byte[] runningHeadersDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDFont font = null; + for (int i = 1; i <= 4; i++) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + if (font == null) { + font = font(document); + } + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + text(cs, font, 9, MARGIN, 810, "Confidential Internal Report"); + float y = 750; + y = text(cs, font, 16, MARGIN, y, "Section " + i); + y -= 12; + text(cs, font, 11, MARGIN, y, "Body text for section number " + i + " here."); + text(cs, font, 9, 300, 30, "Page " + i); + } + } + return bytes(document); + } + } + + /** Two columns of prose, to exercise reading order. */ + static byte[] twoColumnDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float left = MARGIN; + float right = 320; + float y = 740; + for (int i = 1; i <= 8; i++) { + text(cs, font, 10, left, y - i * 16, "Left column line number " + i); + text(cs, font, 10, right, y - i * 16, "Right column line number " + i); + } + } + return bytes(document); + } + } + + /** A page carrying a link annotation, which must be reachable from the structure tree. */ + static byte[] linkDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + float y = 760; + y = text(cs, font, 18, MARGIN, y, "Useful Links"); + text(cs, font, 11, MARGIN, y - 20, "Visit the project home page for details."); + } + PDAnnotationLink link = new PDAnnotationLink(); + PDRectangle rectangle = new PDRectangle(); + rectangle.setLowerLeftX(MARGIN); + rectangle.setLowerLeftY(725); + rectangle.setUpperRightX(MARGIN + 200); + rectangle.setUpperRightY(740); + link.setRectangle(rectangle); + PDActionURI action = new PDActionURI(); + action.setURI("https://example.org"); + link.setAction(action); + page.getAnnotations().add(link); + return bytes(document); + } + } + + /** A page with no content at all. */ + static byte[] emptyDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + document.addPage(new PDPage(PDRectangle.A4)); + return bytes(document); + } + } + + /** A page whose only content is a full-page image, standing in for an un-OCRed scan. */ + static byte[] scannedDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + BufferedImage bitmap = new BufferedImage(600, 850, BufferedImage.TYPE_INT_RGB); + java.awt.Graphics2D graphics = bitmap.createGraphics(); + graphics.setColor(Color.WHITE); + graphics.fillRect(0, 0, 600, 850); + graphics.setColor(Color.BLACK); + graphics.drawString("scanned page", 40, 60); + graphics.dispose(); + PDImageXObject image = LosslessFactory.createFromImage(document, bitmap); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + cs.drawImage(image, 0, 0, PDRectangle.A4.getWidth(), PDRectangle.A4.getHeight()); + } + return bytes(document); + } + } + + /** Text drawn inside a form XObject, attributed to the Do operator that invoked it. */ + static byte[] formXObjectDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + + PDFormXObject form = new PDFormXObject(document); + form.setBBox(new PDRectangle(220, 40)); + form.setResources(new PDResources()); + try (PDFormContentStream fcs = new PDFormContentStream(form)) { + fcs.beginText(); + fcs.setFont(font, 11); + fcs.newLineAtOffset(4, 14); + fcs.showText("Text living inside the form XObject"); + fcs.endText(); + } + + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + text(cs, font, 18, MARGIN, 760, "Page With Embedded Form"); + text(cs, font, 11, MARGIN, 730, "Ordinary page text sits above the form."); + cs.saveGraphicsState(); + cs.transform(Matrix.getTranslateInstance(MARGIN, 650)); + cs.drawForm(form); + cs.restoreGraphicsState(); + } + return bytes(document); + } + } + + /** Content split across two streams (a PDF array) - the parser must see one sequence. */ + static byte[] multiStreamDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + text(cs, font, 18, MARGIN, 760, "First Stream Heading"); + } + try (PDPageContentStream cs = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true)) { + text(cs, font, 11, MARGIN, 720, "Second stream paragraph appended later."); + } + return bytes(document); + } + } + + /** A landscape page via /Rotate 90, which flips the frame the text engine reports in. */ + static byte[] rotatedDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(PDRectangle.A4); + page.setRotation(90); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + // Drawn rotated so the text reads upright on the rotated page. + cs.transform(Matrix.getRotateInstance(Math.toRadians(90), 595, 0)); + text(cs, font, 18, MARGIN, 500, "Rotated Page Title"); + text(cs, font, 11, MARGIN, 470, "Body text on a landscape page."); + } + return bytes(document); + } + } + + /** A MediaBox whose origin is not (0,0), which some scanners produce. */ + static byte[] offsetMediaBoxDocument() throws IOException { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(new PDRectangle(100, 200, 595, 842)); + document.addPage(page); + PDFont font = font(document); + try (PDPageContentStream cs = new PDPageContentStream(document, page)) { + text(cs, font, 18, 160, 960, "Offset Origin Title"); + text(cs, font, 11, 160, 930, "Text on a page whose MediaBox starts at 100,200."); + } + return bytes(document); + } + } + + // --- helpers ----------------------------------------------------------- + + private static float text( + PDPageContentStream cs, PDFont font, float size, float x, float y, String value) + throws IOException { + cs.beginText(); + cs.setFont(font, size); + cs.newLineAtOffset(x, y); + cs.showText(value); + cs.endText(); + return y - size * 1.35f; + } + + /** + * Emits one row with a separate show-text operator per cell, so each cell gets its own MCID. + */ + private static void tableRow( + PDPageContentStream cs, + PDFont font, + float size, + float[] columns, + float y, + String[] values) + throws IOException { + cs.beginText(); + cs.setFont(font, size); + cs.newLineAtOffset(columns[0], y); + cs.showText(values[0]); + for (int i = 1; i < values.length; i++) { + cs.newLineAtOffset(columns[i] - columns[i - 1], 0); + cs.showText(values[i]); + } + cs.endText(); + } + + private static byte[] bytes(PDDocument document) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + document.save(out); + return out.toByteArray(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfaLevelATest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfaLevelATest.java new file mode 100644 index 0000000000..367b9d1171 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfaLevelATest.java @@ -0,0 +1,202 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.verapdf.pdfa.Foundries; +import org.verapdf.pdfa.PDFAParser; +import org.verapdf.pdfa.flavours.PDFAFlavour; +import org.verapdf.pdfa.results.TestAssertion; +import org.verapdf.pdfa.results.ValidationResult; + +/** + * Proves tagging raises a PDF/A file from level B to the accessible level A. veraPDF is the + * arbiter: the claim only counts if the validator agrees. + */ +class PdfaLevelATest { + + private static PdfaAccessibilityService service; + private static Path repoRoot; + + @BeforeAll + static void setUp() { + PdfUaValidationService uaValidation = new PdfUaValidationService(); + uaValidation.initialise(); + service = new PdfaAccessibilityService(uaValidation); + repoRoot = Path.of("").toAbsolutePath(); + while (repoRoot != null && !Files.exists(repoRoot.resolve("settings.gradle"))) { + repoRoot = repoRoot.getParent(); + } + } + + private static byte[] fixture(String name) throws Exception { + return Files.readAllBytes( + repoRoot.resolve("app/core/src/test/resources/pdfa").resolve(name)); + } + + private static String xmpOf(byte[] pdf) throws Exception { + try (PDDocument document = Loader.loadPDF(pdf)) { + var metadata = document.getDocumentCatalog().getMetadata(); + assertNotNull(metadata, "no XMP packet"); + return new String(metadata.toByteArray(), StandardCharsets.UTF_8); + } + } + + /** The flavour the file declares in its XMP, which is what a validator picks up by itself. */ + private static String declaredStandard(byte[] pdf) throws Exception { + try (PDFAParser parser = + Foundries.defaultInstance().createParser(new ByteArrayInputStream(pdf))) { + List flavours = parser.getFlavours(); + return flavours == null || flavours.isEmpty() ? null : flavours.get(0).getId(); + } + } + + private static ValidationResult validate(byte[] pdf, PDFAFlavour flavour) throws Exception { + try (PDFAParser parser = + Foundries.defaultInstance().createParser(new ByteArrayInputStream(pdf), flavour)) { + return Foundries.defaultInstance().createValidator(flavour, false).validate(parser); + } + } + + private static List failures(ValidationResult result) { + return result.getTestAssertions().stream() + .filter(assertion -> assertion.getStatus() == TestAssertion.Status.FAILED) + .map(TestAssertion::getMessage) + .toList(); + } + + @Test + @DisplayName("a level B file gains a structure tree and a conformance A claim") + void upgradesLevelBToLevelA() throws Exception { + byte[] levelB = fixture("valid-pdfa-2b.pdf"); + + try (PDDocument before = Loader.loadPDF(levelB)) { + assertEquals( + null, + before.getDocumentCatalog().getStructureTreeRoot(), + "the fixture should start untagged, or the test proves nothing"); + } + + PdfaAccessibilityService.Result result = + service.upgradeToLevelA(levelB, 2, "en-GB", "Archived Report"); + assertTrue(result.levelA(), "upgrade failed: " + result.warnings()); + + try (PDDocument after = Loader.loadPDF(result.pdfBytes())) { + assertNotNull( + after.getDocumentCatalog().getStructureTreeRoot(), "no structure tree written"); + assertTrue(after.getDocumentCatalog().getMarkInfo().isMarked()); + assertEquals("en-GB", after.getDocumentCatalog().getLanguage()); + } + + String xmp = xmpOf(result.pdfBytes()); + assertTrue(xmp.contains("part"), "pdfaid:part missing"); + assertTrue( + xmp.contains(">A<") || xmp.contains("conformance=\"A\""), + "conformance was not raised to A: " + xmp); + } + + @Test + @DisplayName("the upgraded file still validates as PDF/A, now at level A") + void upgradedFileStillValidates() throws Exception { + byte[] levelB = fixture("valid-pdfa-2b.pdf"); + PdfaAccessibilityService.Result result = + service.upgradeToLevelA(levelB, 2, "en-GB", "Archived Report"); + assertTrue(result.levelA(), "upgrade failed: " + result.warnings()); + + assertEquals( + "2a", declaredStandard(result.pdfBytes()), "the file should now declare PDF/A-2a"); + + ValidationResult pdfa = validate(result.pdfBytes(), PDFAFlavour.PDFA_2_A); + assertTrue(pdfa.isCompliant(), () -> "PDF/A-2a validation failed: " + failures(pdfa)); + } + + @Test + @DisplayName("PDF/A-1 keeps its 1.4 version, since level A must not change the part") + void partOneKeepsItsVersion() throws Exception { + byte[] levelB = fixture("valid-pdfa-1b.pdf"); + float versionBefore; + try (PDDocument document = Loader.loadPDF(levelB)) { + versionBefore = document.getVersion(); + } + + PdfaAccessibilityService.Result result = + service.upgradeToLevelA(levelB, 1, "en", "Archived"); + try (PDDocument document = Loader.loadPDF(result.pdfBytes())) { + assertEquals( + versionBefore, + document.getVersion(), + "raising the PDF version would break PDF/A-1 conformance"); + } + } + + @Test + @DisplayName("a document with nothing to tag is left at level B rather than mislabelled") + void refusesToClaimLevelAWithoutTags() throws Exception { + byte[] blank; + try (PDDocument document = new PDDocument()) { + document.addPage(new org.apache.pdfbox.pdmodel.PDPage()); + var out = new java.io.ByteArrayOutputStream(); + document.save(out); + blank = out.toByteArray(); + } + + PdfaAccessibilityService.Result result = service.upgradeToLevelA(blank, 2, "en", "Empty"); + assertFalse(result.levelA(), "an untaggable document must not claim level A"); + assertFalse(result.warnings().isEmpty(), "the refusal should be explained"); + } + + @Test + @DisplayName("setting conformance leaves the rest of the XMP packet intact") + void conformanceRewritePreservesPacket() throws Exception { + byte[] levelB = fixture("valid-pdfa-2b.pdf"); + byte[] rewritten = PdfaAccessibilityService.setConformance(levelB, 2, "A"); + + assertEquals("2a", declaredStandard(rewritten), "the rewritten packet should declare 2a"); + } + + @Test + @DisplayName("a file can declare PDF/A and PDF/UA at once without breaking either") + void combinedPdfaAndPdfUa() throws Exception { + byte[] levelB = fixture("valid-pdfa-2b.pdf"); + PdfaAccessibilityService.Result upgraded = + service.upgradeToLevelA(levelB, 2, "en-GB", "Archived and Accessible"); + assertTrue(upgraded.levelA(), "upgrade failed: " + upgraded.warnings()); + + byte[] both = PdfaAccessibilityService.declarePdfUaAlongsidePdfa(upgraded.pdfBytes(), 2); + + String xmp = xmpOf(both); + assertTrue(xmp.contains("pdfuaid"), "no PDF/UA identifier"); + assertTrue( + xmp.contains("pdfaSchema") || xmp.contains("schemas"), + "PDF/A requires an extension schema describing pdfuaid, none found: " + xmp); + + assertEquals( + "2a", declaredStandard(both), "the combined file should still declare PDF/A-2a"); + + ValidationResult pdfa = validate(both, PDFAFlavour.PDFA_2_A); + assertTrue( + pdfa.isCompliant(), + () -> "adding the PDF/UA identifier broke PDF/A: " + failures(pdfa)); + } + + @Test + @DisplayName("PDFAFlavour exposes the level A profiles the converter now targets") + void flavoursExistForLevelA() { + assertNotNull(PDFAFlavour.PDFA_1_A); + assertNotNull(PDFAFlavour.PDFA_2_A); + assertNotNull(PDFAFlavour.PDFA_3_A); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/TaggedContentExtractorRealFilesTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/TaggedContentExtractorRealFilesTest.java new file mode 100644 index 0000000000..b42a429007 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/TaggedContentExtractorRealFilesTest.java @@ -0,0 +1,99 @@ +package stirling.software.proprietary.service.ua; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.proprietary.pdf.ua.PageContent; +import stirling.software.proprietary.pdf.ua.TaggedContentExtractor; + +/** + * Guards the invariant the tagger rests on: the token pass and text pass must agree on ordinals. + * They can silently disagree, and the extractor then drops the page rather than mis-tag it. + */ +class TaggedContentExtractorRealFilesTest { + + private static Path repoRoot; + + @BeforeAll + static void setUp() { + repoRoot = Path.of("").toAbsolutePath(); + while (repoRoot != null && !Files.exists(repoRoot.resolve("settings.gradle"))) { + repoRoot = repoRoot.getParent(); + } + } + + @Test + @DisplayName("pages with extractable text always yield lines across the repository corpus") + void ordinalsAgreeOnRealFiles() throws Exception { + assertNotNull(repoRoot, "could not locate the repository root"); + List dropped = new ArrayList<>(); + int inspected = 0; + + for (Path pdf : findPdfs()) { + byte[] bytes; + try { + bytes = Files.readAllBytes(pdf); + } catch (Exception e) { + continue; + } + try (PDDocument document = Loader.loadPDF(bytes)) { + if (document.getNumberOfPages() > 30) { + continue; + } + inspected++; + List pages = new TaggedContentExtractor().extract(document); + + for (PageContent page : pages) { + if (page.markableCount() == 0 || !hasText(document, page.pageIndex())) { + continue; + } + if (page.lines().isEmpty() && page.forms().isEmpty()) { + dropped.add(repoRoot.relativize(pdf) + " page " + page.pageIndex()); + } + } + } catch (Exception e) { + // Unreadable files are covered by the conversion tests. + } + } + + assertTrue(inspected > 15, "expected to inspect a real corpus, saw " + inspected); + assertTrue( + dropped.isEmpty(), + "the two extraction passes disagreed, so these pages were skipped: " + dropped); + } + + private static boolean hasText(PDDocument document, int pageIndex) { + try { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setStartPage(pageIndex + 1); + stripper.setEndPage(pageIndex + 1); + return !stripper.getText(document).isBlank(); + } catch (Exception e) { + return false; + } + } + + private List findPdfs() throws Exception { + try (Stream stream = Files.walk(repoRoot)) { + return stream.filter(Files::isRegularFile) + .filter(p -> p.toString().toLowerCase().endsWith(".pdf")) + .filter(p -> !p.toString().contains("node_modules")) + .filter(p -> !p.toString().contains(java.io.File.separator + "build")) + .filter(p -> !p.toString().contains(".git")) + .toList(); + } + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/config/StorageProviderConfigTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/config/StorageProviderConfigTest.java index ddd67db020..4098d6ab19 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/storage/config/StorageProviderConfigTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/config/StorageProviderConfigTest.java @@ -4,99 +4,228 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Base64; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.transaction.PlatformTransactionManager; import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.model.Team; import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker; -import stirling.software.proprietary.storage.provider.LocalStorageProvider; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.EncryptingStorageProvider; +import stirling.software.proprietary.storage.crypto.InMemoryKeyRepo; +import stirling.software.proprietary.storage.crypto.StorageEncryptionAuditListener; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; import stirling.software.proprietary.storage.repository.StoredFileBlobRepository; /** - * Verifies the Pro/Enterprise license gate on the S3 storage backend without touching real S3 - * clients (and without needing Docker). Provider-specific construction is delegated to the existing - * provider tests. + * Verifies the Pro/Enterprise license gates and, critically, that the encryption decorator is + * installed in EVERY configuration — a node whose config lags the cluster (flag off, key rows + * already existing elsewhere) must never serve raw ciphertext. */ class StorageProviderConfigTest { - @Test - void provider_local_normalLicense_buildsLocalProviderWithoutLicenseCheck() { - StorageProviderConfig cfg = newConfig("local", License.NORMAL); + private static final String MASTER = + Base64.getEncoder().encodeToString("0123456789abcdef0123456789abcdef".getBytes()); - StorageProvider provider = cfg.storageProvider(); - assertThat(provider).isInstanceOf(LocalStorageProvider.class); + private final InMemoryKeyRepo keyRepo = new InMemoryKeyRepo(); + private final PlatformTransactionManager txManager = mock(PlatformTransactionManager.class); + + private StorageEncryptionState newState(StorageProviderConfig cfg) { + return cfg.storageEncryptionState(MASTER, "", 1, false, txManager); + } + + // ---- decorator installation matrix ------------------------------------------------- + + @Test + void decorator_alwaysInstalled_evenWhenEncryptionOffAndNoKeys() { + StorageProviderConfig cfg = newConfig("local", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); + + StorageProvider provider = cfg.storageProvider(state, Optional.empty()); + assertThat(provider).isInstanceOf(EncryptingStorageProvider.class); + assertThat(state.isWriteEnabled()).isFalse(); + // Nothing encrypted can exist -> the S3 fast path stays available. + assertThat(state.suppressDirectDownloads()).isFalse(); } + @Test + void decorator_writeEnabled_requiresLicenceAndSuppressesDirectDownloads() { + StorageProviderConfig cfg = newConfig("local", License.SERVER, true); + StorageEncryptionState state = newState(cfg); + + StorageProvider provider = cfg.storageProvider(state, Optional.empty()); + assertThat(provider).isInstanceOf(EncryptingStorageProvider.class); + assertThat(state.isWriteEnabled()).isTrue(); + assertThat(state.suppressDirectDownloads()).isTrue(); + } + + @Test + void decorator_flagOffButKeysExist_decryptOnlyModeStillMaterialises() throws Exception { + // Drifted node: keys created elsewhere; storage on, as it must be to serve files. + StorageProviderConfig seedCfg = newConfig("local", License.SERVER, true, true); + StorageEncryptionState seedState = newState(seedCfg); + Team team = new Team(); + team.setId(1L); + User owner = new User(); + owner.setTeam(team); + seedState.keyService().activeKekForOwner(owner); + + StorageProviderConfig cfg = newConfig("local", License.NORMAL, false, true); + StorageEncryptionState state = newState(cfg); + + assertThat(cfg.storageProvider(state, Optional.empty())) + .isInstanceOf(EncryptingStorageProvider.class); + assertThat(state.isWriteEnabled()).isFalse(); + // Encrypted content may exist -> presigned URLs must be suppressed on this node too. + assertThat(state.suppressDirectDownloads()).isTrue(); + // Eager init ran (keys existed at boot), so decryption works without a licence. + assertThat(state.isMaterialised()).isTrue(); + } + + @Test + void encryption_enabled_normalLicense_failsStartup() { + StorageProviderConfig cfg = newConfig("local", License.NORMAL, true); + assertThatThrownBy(() -> newState(cfg)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("storage.encryption requires a Pro or Enterprise license"); + } + + @Test + void encryption_enabled_wrongLengthKey_failsStartup() { + StorageProviderConfig cfg = newConfig("local", License.SERVER, true); + String shortKey = Base64.getEncoder().encodeToString("only16bytes-yes!".getBytes()); + assertThatThrownBy(() -> cfg.storageEncryptionState(shortKey, "", 1, false, txManager)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("32 bytes"); + } + + // ---- deployments that do not use storage (the SaaS shape) --------------------------- + + @Test + void storageDisabled_neverQueriesTheKeyRegistry() { + StorageProviderConfig cfg = newConfig("local", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); + + assertThat(state.isWriteEnabled()).isFalse(); + // No probe means no eager init, so no master key is resolved on a node that never stores. + assertThat(state.isMaterialised()).isFalse(); + verify(keyRepo.mock, never()).count(); + } + + @Test + void storageDisabled_decoratorStillInstalledSoCiphertextIsNeverServedRaw() { + StorageProviderConfig cfg = newConfig("local", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); + + assertThat(cfg.storageProvider(state, Optional.empty())) + .isInstanceOf(EncryptingStorageProvider.class); + } + + @Test + void unreadableKeyRegistry_stillStartsAndFailsSafeOnDirectDownloads() { + FileEncryptionKeyRepository broken = mock(FileEncryptionKeyRepository.class); + when(broken.count()) + .thenThrow(new InvalidDataAccessResourceUsageException("no such table")); + StorageEncryptionState state = + new StorageEncryptionState( + false, () -> null, broken, StorageEncryptionAuditListener.NOOP); + + assertThat(state.encryptedContentMayExist()).isFalse(); + assertThat(state.suppressDirectDownloads()).isTrue(); + } + + @Test + void storageEnabledWithoutEncryption_probesRegistry() { + StorageProviderConfig cfg = newConfig("local", License.NORMAL, false, true); + newState(cfg); + verify(keyRepo.mock, atLeastOnce()).count(); + } + + // ---- backend licence gates (unchanged behaviour) ------------------------------------ + @Test void provider_s3_normalLicense_throwsBeforeBuildingClient() { - StorageProviderConfig cfg = newConfig("s3", License.NORMAL); + StorageProviderConfig cfg = newConfig("s3", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); // License check must throw BEFORE S3Clients.build tries to validate endpoint / bucket. - // Otherwise an empty config would surface as a confusing "bucket must be set" error. - assertThatThrownBy(cfg::storageProvider) + assertThatThrownBy(() -> cfg.storageProvider(state, Optional.empty())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("storage.provider=s3 requires a Pro or Enterprise license"); } @Test void provider_database_normalLicense_throws() { - StorageProviderConfig cfg = newConfig("database", License.NORMAL); + StorageProviderConfig cfg = newConfig("database", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); - assertThatThrownBy(cfg::storageProvider) + assertThatThrownBy(() -> cfg.storageProvider(state, Optional.empty())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining( "storage.provider=database requires a Pro or Enterprise license"); } @Test - void provider_database_serverLicense_buildsDatabaseProvider() { - StorageProviderConfig cfg = newConfig("database", License.SERVER); - assertThatCode(cfg::storageProvider).doesNotThrowAnyException(); + void provider_database_serverLicense_builds() { + StorageProviderConfig cfg = newConfig("database", License.SERVER, false); + StorageEncryptionState state = newState(cfg); + assertThatCode(() -> cfg.storageProvider(state, Optional.empty())) + .doesNotThrowAnyException(); } @Test void provider_s3_serverLicense_passesLicenseCheck_thenFailsOnEmptyConfig() { - StorageProviderConfig cfg = newConfig("s3", License.SERVER); + StorageProviderConfig cfg = newConfig("s3", License.SERVER, false); + StorageEncryptionState state = newState(cfg); // Valid license, but no bucket/endpoint configured - so we expect a CONFIG error, - // not a license error. The error message must not mention the license. - assertThatThrownBy(cfg::storageProvider) + // not a license error. + assertThatThrownBy(() -> cfg.storageProvider(state, Optional.empty())) .isInstanceOf(IllegalStateException.class) .hasMessageNotContaining("Pro or Enterprise license"); } @Test - void provider_s3_enterpriseLicense_passesLicenseCheck_thenFailsOnEmptyConfig() { - StorageProviderConfig cfg = newConfig("s3", License.ENTERPRISE); + void provider_unknown_throwsUnsupportedProvider_notLicense() { + StorageProviderConfig cfg = newConfig("magic", License.NORMAL, false); + StorageEncryptionState state = newState(cfg); - assertThatThrownBy(cfg::storageProvider) - .isInstanceOf(IllegalStateException.class) - .hasMessageNotContaining("Pro or Enterprise license"); - } - - @Test - void provider_unknown_normalLicense_throwsUnsupportedProvider_notLicense() { - StorageProviderConfig cfg = newConfig("magic", License.NORMAL); - - assertThatThrownBy(cfg::storageProvider) + assertThatThrownBy(() -> cfg.storageProvider(state, Optional.empty())) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Storage provider not supported: magic") .hasMessageNotContaining("license"); } - private static StorageProviderConfig newConfig(String provider, License license) { + private StorageProviderConfig newConfig( + String provider, License license, boolean encryptionEnabled) { + return newConfig(provider, license, encryptionEnabled, false); + } + + private StorageProviderConfig newConfig( + String provider, License license, boolean encryptionEnabled, boolean storageEnabled) { ApplicationProperties props = new ApplicationProperties(); props.getStorage().setProvider(provider); - props.getStorage() - .setEnabled(false); // local-fallback path skips dir creation when disabled + props.getStorage().setEnabled(storageEnabled); + props.getStorage().getEncryption().setEnabled(encryptionEnabled); StoredFileBlobRepository repo = mock(StoredFileBlobRepository.class); LicenseKeyChecker checker = mock(LicenseKeyChecker.class); + AuditService audit = mock(AuditService.class); when(checker.getPremiumLicenseEnabledResult()).thenReturn(license); if (license == License.SERVER || license == License.ENTERPRISE) { doNothing().when(checker).requireProOrEnterprise(anyString()); @@ -111,6 +240,6 @@ class StorageProviderConfigTest { .when(checker) .requireProOrEnterprise(anyString()); } - return new StorageProviderConfig(props, repo, checker); + return new StorageProviderConfig(props, repo, keyRepo.mock, checker, audit); } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerMoreTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerMoreTest.java index 1575dfbff5..f423ecba22 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerMoreTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerMoreTest.java @@ -31,6 +31,7 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.storage.model.FileShare; import stirling.software.proprietary.storage.model.ShareAccessRole; import stirling.software.proprietary.storage.model.StoredFile; @@ -48,12 +49,13 @@ class FileStorageControllerMoreTest { @Mock private FileStorageService fileStorageService; @Mock private StorageProvider storageProvider; + @Mock private AuditService auditService; private FileStorageController controller; @BeforeEach void setUp() { - controller = new FileStorageController(fileStorageService, storageProvider); + controller = new FileStorageController(fileStorageService, storageProvider, auditService); } private User user() { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerTest.java index 79033abc2d..df6cad881d 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/FileStorageControllerTest.java @@ -1,11 +1,13 @@ package stirling.software.proprietary.storage.controller; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -14,20 +16,26 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.net.URI; import java.time.Duration; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; import stirling.software.proprietary.storage.model.StoredFile; import stirling.software.proprietary.storage.provider.StorageProvider; import stirling.software.proprietary.storage.service.FileStorageService; @@ -40,13 +48,14 @@ class FileStorageControllerTest { @Mock private FileStorageService fileStorageService; @Mock private StorageProvider storageProvider; + @Mock private AuditService auditService; private MockMvc mockMvc; @BeforeEach void setUp() { FileStorageController controller = - new FileStorageController(fileStorageService, storageProvider); + new FileStorageController(fileStorageService, storageProvider, auditService); mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } @@ -113,6 +122,75 @@ class FileStorageControllerTest { eq("11/abc-doc.pdf"), any(Duration.class), eq(true), eq("doc.pdf")); } + @Test + void downloadFile_encryptedContent_auditsPlaintextExportAsAttachment() throws Exception { + StoredFile file = newStoredFile(); + file.setEncryptionKeyId("cafe1234-0000-0000-0000-000000000001"); + streamedDownload(file); + + mockMvc.perform(get("/api/v1/storage/files/{fileId}/download", 77L)) + .andExpect(status().isOk()); + + // The marker is the compliance evidence that a decrypted copy left the platform, so it + // must carry the key it came from and whether it was viewed in-app or saved. + assertThat(exportEvents()) + .singleElement() + .satisfies( + event -> { + assertThat(event).containsEntry("action", "plaintextExport"); + assertThat(event).containsEntry("fileId", 77L); + assertThat(event).containsEntry("inline", false); + assertThat(event) + .containsEntry("keyId", "cafe1234-0000-0000-0000-000000000001"); + }); + } + + @Test + void downloadFile_inlineView_marksTheExportInline() throws Exception { + StoredFile file = newStoredFile(); + file.setEncryptionKeyId("cafe1234-0000-0000-0000-000000000001"); + streamedDownload(file); + + mockMvc.perform(get("/api/v1/storage/files/{fileId}/download", 77L).param("inline", "true")) + .andExpect(status().isOk()); + + // An in-app view and a saved copy are both exports, but a reviewer needs to tell them + // apart. + assertThat(exportEvents()) + .singleElement() + .satisfies(e -> assertThat(e).containsEntry("inline", true)); + } + + @Test + void downloadFile_plaintextContent_recordsNoExportEvent() throws Exception { + StoredFile file = newStoredFile(); // encryptionKeyId stays null + streamedDownload(file); + + mockMvc.perform(get("/api/v1/storage/files/{fileId}/download", 77L)) + .andExpect(status().isOk()); + + // Nothing was encrypted at rest, so there is no decryption to attest to. + verifyNoInteractions(auditService); + } + + /** Stubs an app-streamed (non-presigned) download of {@code file}. */ + private void streamedDownload(StoredFile file) throws Exception { + when(fileStorageService.requireAuthenticatedUser()).thenReturn(file.getOwner()); + when(fileStorageService.getAccessibleFile(file.getOwner(), 77L)).thenReturn(file); + when(storageProvider.signedDownloadUrl( + anyString(), any(Duration.class), anyBoolean(), anyString())) + .thenReturn(Optional.empty()); + when(fileStorageService.loadFile(file)) + .thenReturn(new ByteArrayResource("decrypted bytes".getBytes(UTF_8))); + } + + private List> exportEvents() { + @SuppressWarnings("unchecked") + ArgumentCaptor> data = ArgumentCaptor.forClass(Map.class); + verify(auditService).audit(eq(AuditEventType.STORAGE_ENCRYPTION), data.capture()); + return data.getAllValues(); + } + private static StoredFile newStoredFile() { User user = new User(); user.setId(11L); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerHttpTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerHttpTest.java new file mode 100644 index 0000000000..27b5a893c7 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerHttpTest.java @@ -0,0 +1,209 @@ +package stirling.software.proprietary.storage.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.Instant; +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService; +import stirling.software.proprietary.storage.crypto.FileEncryptionMasterKey; +import stirling.software.proprietary.storage.crypto.InMemoryKeyRepo; +import stirling.software.proprietary.storage.crypto.StorageEncryptionAuditListener; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; +import stirling.software.proprietary.storage.repository.StoredFileRepository; +import stirling.software.proprietary.storage.service.StorageEncryptionMigrationService; + +/** + * Drives the admin API over HTTP rather than as Java calls: path mappings, status codes and — the + * part unit tests can't see — that the response records actually serialise. A UUID/Instant/enum + * that Jackson cannot render would pass every direct-invocation test and fail the first request. + * + *

Note {@code standaloneSetup} deliberately does not install the security filter chain, so this + * cannot prove {@code @PreAuthorize} enforcement; {@link #controller_isAdminOnly()} pins the + * annotation instead (method security is enabled globally in {@code SecurityConfiguration}). + */ +class StorageEncryptionAdminControllerHttpTest { + + private static final String MASTER = + Base64.getEncoder().encodeToString("0123456789abcdef0123456789abcdef".getBytes()); + + private InMemoryKeyRepo keyRepo; + private FileEncryptionKeyService keyService; + private StorageEncryptionMigrationService migrationService; + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + keyRepo = new InMemoryKeyRepo(); + keyService = + new FileEncryptionKeyService( + keyRepo.mock, new FileEncryptionMasterKey(MASTER, false)); + StoredFileRepository storedFileRepository = mock(StoredFileRepository.class); + when(storedFileRepository.countByEncryptionKeyIdIsNotNull()).thenReturn(7L); + when(storedFileRepository.countByEncryptionKeyIdIsNull()).thenReturn(3L); + migrationService = mock(StorageEncryptionMigrationService.class); + + ApplicationProperties props = new ApplicationProperties(); + props.getStorage().setEnabled(true); + StorageEncryptionAdminController controller = + new StorageEncryptionAdminController( + props, + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + keyRepo.mock, + storedFileRepository, + migrationService, + mock(AuditService.class)); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + private UUID seedKey() throws Exception { + Team team = new Team(); + team.setId(1L); + User owner = new User(); + owner.setTeam(team); + return keyService.activeKekForOwner(owner).keyId(); + } + + @Test + void status_serialisesCountsFingerprintAndKeyRows() throws Exception { + UUID keyId = seedKey(); + + mockMvc.perform(get("/api/v1/admin/storage-encryption/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.writeEnabled").value(true)) + .andExpect(jsonPath("$.active").value(true)) + .andExpect(jsonPath("$.masterKeyFingerprint").isString()) + .andExpect(jsonPath("$.masterKeyVersion").value(1)) + .andExpect(jsonPath("$.encryptedFiles").value(7)) + .andExpect(jsonPath("$.plaintextFiles").value(3)) + .andExpect(jsonPath("$.keys[0].keyId").value(keyId.toString())) + .andExpect(jsonPath("$.keys[0].scopeType").value("TEAM")) + .andExpect(jsonPath("$.keys[0].scopeId").value(1)) + .andExpect(jsonPath("$.keys[0].status").value("ACTIVE")); + } + + @Test + void disableThenEnable_serialiseKeyInfoAndReportStatusTransitions() throws Exception { + UUID keyId = seedKey(); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/keys/" + keyId + "/disable")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.keyId").value(keyId.toString())) + .andExpect(jsonPath("$.status").value("DISABLED")) + .andExpect(jsonPath("$.statusChangedAt").isNotEmpty()); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/keys/" + keyId + "/enable")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("ACTIVE")); + } + + @Test + void enable_onActiveKey_returns409() throws Exception { + UUID keyId = seedKey(); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/keys/" + keyId + "/enable")) + .andExpect(status().isConflict()); + } + + @Test + void disable_unknownKey_returns404() throws Exception { + mockMvc.perform( + post( + "/api/v1/admin/storage-encryption/keys/" + + UUID.randomUUID() + + "/disable")) + .andExpect(status().isNotFound()); + } + + @Test + void migrateStatus_neverStarted_serialisesIdle() throws Exception { + when(migrationService.status()).thenReturn(Optional.empty()); + + mockMvc.perform(get("/api/v1/admin/storage-encryption/migrate/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.state").value("IDLE")); + } + + @Test + void migrate_serialisesRunningSnapshotIncludingInstants() throws Exception { + when(migrationService.start()) + .thenReturn( + new StorageEncryptionMigrationService.MigrationStatus( + StorageEncryptionMigrationService.State.RUNNING, + 10, + 2, + 1, + 0, + Instant.parse("2026-01-01T00:00:00Z"), + null)); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/migrate")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.state").value("RUNNING")) + .andExpect(jsonPath("$.total").value(10)) + .andExpect(jsonPath("$.processed").value(2)) + .andExpect(jsonPath("$.skipped").value(1)) + .andExpect(jsonPath("$.startedAt").isNotEmpty()) + .andExpect(jsonPath("$.finishedAt").doesNotExist()); + } + + @Test + void migrate_alreadyRunning_returns409() throws Exception { + when(migrationService.start()).thenThrow(new IllegalStateException("already running")); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/migrate")) + .andExpect(status().isConflict()); + } + + @Test + void rotate_serialisesRewrapCount() throws Exception { + UUID keyId = seedKey(); + keyRepo.rows.get(keyId).setMasterKeyVersion(0); + + mockMvc.perform(post("/api/v1/admin/storage-encryption/master/rotate")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rewrapped").value(1)) + .andExpect(jsonPath("$.masterKeyVersion").value(1)); + } + + @Test + void controller_isAdminOnly() { + PreAuthorize preAuthorize = + StorageEncryptionAdminController.class.getAnnotation(PreAuthorize.class); + assertThat(preAuthorize).as("admin API must be role-gated").isNotNull(); + assertThat(preAuthorize.value()).isEqualTo("hasRole('ADMIN')"); + } + + @Test + void noDeleteEndpointExists() { + // The "key material can be disabled but never destroyed" guarantee is structural: assert no + // handler method maps a DELETE. + boolean anyDelete = + java.util.Arrays.stream(StorageEncryptionAdminController.class.getMethods()) + .anyMatch( + m -> + m.isAnnotationPresent( + org.springframework.web.bind.annotation + .DeleteMapping.class)); + assertThat(anyDelete).as("no endpoint may delete key material").isFalse(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerTest.java new file mode 100644 index 0000000000..b976985d45 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/controller/StorageEncryptionAdminControllerTest.java @@ -0,0 +1,228 @@ +package stirling.software.proprietary.storage.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService; +import stirling.software.proprietary.storage.crypto.FileEncryptionMasterKey; +import stirling.software.proprietary.storage.crypto.InMemoryKeyRepo; +import stirling.software.proprietary.storage.crypto.StorageEncryptionAuditListener; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; +import stirling.software.proprietary.storage.model.api.StorageEncryptionStatusResponse; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; +import stirling.software.proprietary.storage.repository.StoredFileRepository; +import stirling.software.proprietary.storage.service.StorageEncryptionMigrationService; + +class StorageEncryptionAdminControllerTest { + + private static final String MASTER = + Base64.getEncoder().encodeToString("0123456789abcdef0123456789abcdef".getBytes()); + + private InMemoryKeyRepo keyRepo; + private FileEncryptionKeyService keyService; + private StoredFileRepository storedFileRepository; + private StorageEncryptionMigrationService migrationService; + private StorageEncryptionAdminController controller; + + @BeforeEach + void setUp() { + keyRepo = new InMemoryKeyRepo(); + keyService = + new FileEncryptionKeyService( + keyRepo.mock, new FileEncryptionMasterKey(MASTER, false)); + storedFileRepository = mock(StoredFileRepository.class); + when(storedFileRepository.countByEncryptionKeyIdIsNotNull()).thenReturn(4L); + when(storedFileRepository.countByEncryptionKeyIdIsNull()).thenReturn(2L); + migrationService = mock(StorageEncryptionMigrationService.class); + controller = + new StorageEncryptionAdminController( + storageEnabled(true), + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + keyRepo.mock, + storedFileRepository, + migrationService, + mock(AuditService.class)); + } + + private static ApplicationProperties storageEnabled(boolean enabled) { + ApplicationProperties props = new ApplicationProperties(); + props.getStorage().setEnabled(enabled); + return props; + } + + private UUID createKey() throws Exception { + Team team = new Team(); + team.setId(1L); + User owner = new User(); + owner.setTeam(team); + return keyService.activeKekForOwner(owner).keyId(); + } + + @Test + void status_reportsCountsFingerprintAndKeys() throws Exception { + UUID keyId = createKey(); + StorageEncryptionStatusResponse status = controller.status(); + + assertThat(status.writeEnabled()).isTrue(); + assertThat(status.active()).isTrue(); + assertThat(status.masterKeyFingerprint()).hasSize(16); + assertThat(status.encryptedFiles()).isEqualTo(4); + assertThat(status.plaintextFiles()).isEqualTo(2); + assertThat(status.keys()).hasSize(1); + assertThat(status.keys().get(0).keyId()).isEqualTo(keyId); + assertThat(status.keys().get(0).status()).isEqualTo("ACTIVE"); + } + + @Test + void disableThenEnable_roundTripsAndFailsClosedInBetween() throws Exception { + UUID keyId = createKey(); + + StorageEncryptionStatusResponse.KeyInfo disabled = controller.disableKey(keyId); + assertThat(disabled.status()).isEqualTo("DISABLED"); + assertThatThrownBy(() -> keyService.kekForDecrypt(keyId)).hasMessageContaining("disabled"); + + StorageEncryptionStatusResponse.KeyInfo enabled = controller.enableKey(keyId); + assertThat(enabled.status()).isEqualTo("ACTIVE"); + assertThat(keyService.kekForDecrypt(keyId)).isNotNull(); + } + + @Test + void enable_keyNotDisabled_conflicts() throws Exception { + UUID keyId = createKey(); + assertThatThrownBy(() -> controller.enableKey(keyId)) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.CONFLICT)); + } + + @Test + void disable_unknownKey_notFound() { + assertThatThrownBy(() -> controller.disableKey(UUID.randomUUID())) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND)); + } + + @Test + void migrate_alreadyRunning_conflicts() { + when(migrationService.start()).thenThrow(new IllegalStateException("already running")); + assertThatThrownBy(() -> controller.startMigration()) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.CONFLICT)); + } + + @Test + void migrationStatus_neverStarted_reportsIdle() { + when(migrationService.status()).thenReturn(Optional.empty()); + assertThat(controller.migrationStatus().state()).isEqualTo("IDLE"); + } + + @Test + void rotate_whenInactive_conflicts() { + StorageEncryptionAdminController inactive = + new StorageEncryptionAdminController( + storageEnabled(true), + new StorageEncryptionState( + false, + () -> { + throw new IllegalStateException("no key material configured"); + }, + keyRepo.mock, + StorageEncryptionAuditListener.NOOP), + keyRepo.mock, + storedFileRepository, + migrationService, + mock(AuditService.class)); + assertThatThrownBy(inactive::rotateMasterKey) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.CONFLICT)); + } + + @Test + void status_storageDisabled_refusesWithoutTouchingTheRegistry() { + StorageEncryptionAdminController storageOff = + new StorageEncryptionAdminController( + storageEnabled(false), + StorageEncryptionState.of( + false, keyService, StorageEncryptionAuditListener.NOOP), + keyRepo.mock, + storedFileRepository, + migrationService, + mock(AuditService.class)); + + assertThatThrownBy(storageOff::status) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + // The registry table need not exist on a deployment that never stores files. + verifyNoInteractions(storedFileRepository); + verify(keyRepo.mock, never()).findAll(any(Sort.class)); + } + + @Test + void status_unreadableRegistry_reports503RatherThanRaw500() { + FileEncryptionKeyRepository broken = mock(FileEncryptionKeyRepository.class); + when(broken.findAll(any(Sort.class))) + .thenThrow(new InvalidDataAccessResourceUsageException("no such table")); + StorageEncryptionAdminController brokenRegistry = + new StorageEncryptionAdminController( + storageEnabled(true), + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + broken, + storedFileRepository, + migrationService, + mock(AuditService.class)); + + assertThatThrownBy(brokenRegistry::status) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)); + } + + @Test + void rotate_rewrapsPendingRows() throws Exception { + UUID keyId = createKey(); + keyRepo.rows.get(keyId).setMasterKeyVersion(0); // pretend wrapped by an older master + + var result = controller.rotateMasterKey(); + assertThat(result.get("rewrapped")).isEqualTo(1); + assertThat(keyRepo.rows.get(keyId).getMasterKeyVersion()).isEqualTo(1); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListenerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListenerTest.java new file mode 100644 index 0000000000..0c5fa53bc7 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/AuditingStorageEncryptionListenerTest.java @@ -0,0 +1,99 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import stirling.software.proprietary.audit.AuditEventType; +import stirling.software.proprietary.service.AuditService; + +/** + * Covers the one knob a compliance reviewer actually turns: {@code storage.encryption.auditReads} + * must silence per-read decrypt events without silencing denials or key lifecycle events. + */ +class AuditingStorageEncryptionListenerTest { + + private static final UUID KEY_ID = UUID.fromString("11111111-2222-3333-4444-555555555555"); + + private final AuditService auditService = mock(AuditService.class); + + private List> auditedEvents() { + @SuppressWarnings("unchecked") + ArgumentCaptor> data = ArgumentCaptor.forClass(Map.class); + verify(auditService, atLeastOnce()) + .audit(eq(AuditEventType.STORAGE_ENCRYPTION), data.capture()); + return data.getAllValues(); + } + + @Test + void auditReadsOn_recordsDecryptWithKeyAndStorageKey() { + new AuditingStorageEncryptionListener(auditService, true).decrypted("team/blob", KEY_ID); + + assertThat(auditedEvents()) + .singleElement() + .satisfies( + event -> { + assertThat(event).containsEntry("action", "decrypt"); + assertThat(event).containsEntry("storageKey", "team/blob"); + assertThat(event).containsEntry("keyId", KEY_ID.toString()); + }); + } + + @Test + void auditReadsOff_dropsDecryptButKeepsEverythingElse() { + AuditingStorageEncryptionListener listener = + new AuditingStorageEncryptionListener(auditService, false); + + listener.decrypted("team/blob", KEY_ID); + verify(auditService, never()).audit(eq(AuditEventType.STORAGE_ENCRYPTION), anyMap()); + + listener.encrypted("team/blob", KEY_ID); + listener.decryptDenied(KEY_ID, "key disabled"); + listener.keyCreated(KEY_ID, "TEAM:7", 2); + + // Denials and lifecycle events are the compliance-relevant ones; they are never optional. + assertThat(auditedEvents()) + .extracting(event -> event.get("action")) + .containsExactly("encrypt", "decrypt.denied", "key.created"); + } + + @Test + void decryptDenied_carriesTheReason() { + new AuditingStorageEncryptionListener(auditService, false) + .decryptDenied(KEY_ID, "key not found"); + + assertThat(auditedEvents()) + .singleElement() + .satisfies( + event -> { + assertThat(event).containsEntry("action", "decrypt.denied"); + assertThat(event).containsEntry("reason", "key not found"); + assertThat(event).containsEntry("keyId", KEY_ID.toString()); + }); + } + + @Test + void keyCreated_carriesScopeAndVersion() { + new AuditingStorageEncryptionListener(auditService, true).keyCreated(KEY_ID, "TEAM:7", 2); + + assertThat(auditedEvents()) + .singleElement() + .satisfies( + event -> { + assertThat(event).containsEntry("action", "key.created"); + assertThat(event).containsEntry("scope", "TEAM:7"); + assertThat(event).containsEntry("keyVersion", 2); + }); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormatTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormatTest.java new file mode 100644 index 0000000000..89aad24587 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptedFileFormatTest.java @@ -0,0 +1,78 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class EncryptedFileFormatTest { + + private static EncryptedFileFormat.Header sampleHeader() { + return new EncryptedFileFormat.Header( + EncryptedFileFormat.FORMAT_VERSION, + EncryptedFileFormat.SUITE_AES_GCM_HKDF_1MIB, + UUID.randomUUID(), + 123_456_789L, + new byte[EncryptedFileFormat.WRAPPED_DEK_LENGTH]); + } + + @Test + void serialize_parse_roundtrips() throws Exception { + EncryptedFileFormat.Header header = sampleHeader(); + byte[] bytes = header.serialize(); + assertThat(bytes).hasSize(EncryptedFileFormat.HEADER_LENGTH); + + EncryptedFileFormat.Header parsed = EncryptedFileFormat.parse(bytes); + assertThat(parsed).isNotNull(); + assertThat(parsed.keyId()).isEqualTo(header.keyId()); + assertThat(parsed.plaintextLength()).isEqualTo(header.plaintextLength()); + assertThat(parsed.wrappedDek()).isEqualTo(header.wrappedDek()); + } + + @Test + void parse_returnsNullForPlaintext() throws Exception { + byte[] pdf = new byte[EncryptedFileFormat.HEADER_LENGTH]; + pdf[0] = '%'; + pdf[1] = 'P'; + pdf[2] = 'D'; + pdf[3] = 'F'; + assertThat(EncryptedFileFormat.parse(pdf)).isNull(); + } + + @Test + void parse_returnsNullForShortPrefix() throws Exception { + assertThat(EncryptedFileFormat.parse(new byte[10])).isNull(); + assertThat(EncryptedFileFormat.parse(null)).isNull(); + } + + @Test + void parse_throwsCheckedOnUnknownVersion() { + byte[] bytes = sampleHeader().serialize(); + bytes[8] = 99; // format version + // Checked StorageEncryptionException so callers' IOException mapping catches it — + // a downgrade-read must not surface as a bare 500. + assertThatThrownBy(() -> EncryptedFileFormat.parse(bytes)) + .isInstanceOf(StorageEncryptionException.class) + .hasMessageContaining("Unsupported"); + } + + @Test + void associatedData_excludesWrappedDek() { + EncryptedFileFormat.Header header = sampleHeader(); + byte[] aad = header.associatedData(); + assertThat(aad).hasSize(34); + // Changing the wrapped DEK must not change the AAD (it is authenticated separately). + byte[] otherDek = new byte[EncryptedFileFormat.WRAPPED_DEK_LENGTH]; + otherDek[0] = 1; + EncryptedFileFormat.Header other = + new EncryptedFileFormat.Header( + header.formatVersion(), + header.cipherSuite(), + header.keyId(), + header.plaintextLength(), + otherDek); + assertThat(other.associatedData()).isEqualTo(aad); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProviderTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProviderTest.java new file mode 100644 index 0000000000..ce592f8bb0 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/EncryptingStorageProviderTest.java @@ -0,0 +1,448 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.model.FileEncryptionKey; +import stirling.software.proprietary.storage.provider.LocalStorageProvider; +import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.provider.StoredObject; + +class EncryptingStorageProviderTest { + + private static final byte[] PLAINTEXT = + "This is the secret PDF payload used to prove round-trips work." + .getBytes(StandardCharsets.UTF_8); + private static final String MASTER = + Base64.getEncoder() + .encodeToString( + "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8)); + + @TempDir Path tempDir; + + private InMemoryKeyRepo repo; + private LocalStorageProvider inner; + private EncryptingStorageProvider provider; + private User owner; + + @BeforeEach + void setUp() { + repo = new InMemoryKeyRepo(); + inner = new LocalStorageProvider(tempDir); + provider = new EncryptingStorageProvider(inner, newKeyService(), true); + Team team = new Team(); + team.setId(7L); + owner = new User(); + owner.setId(1L); + owner.setTeam(team); + } + + private FileEncryptionKeyService newKeyService() { + return new FileEncryptionKeyService(repo.mock, new FileEncryptionMasterKey(MASTER, false)); + } + + private static MultipartFile upload() { + return new MockMultipartFile("file", "test.pdf", "application/pdf", PLAINTEXT); + } + + @Test + void store_load_roundTripsAndKeepsPlaintextMetadata() throws IOException { + StoredObject stored = provider.store(owner, upload()); + + assertThat(stored.getSizeBytes()).isEqualTo(PLAINTEXT.length); + assertThat(stored.getEncryptionKeyId()).isNotNull(); + assertThat(stored.getOriginalFilename()).isEqualTo("test.pdf"); + + Resource loaded = provider.load(stored.getStorageKey()); + assertThat(loaded.contentLength()).isEqualTo(PLAINTEXT.length); + try (InputStream in = loaded.getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + // Re-openable resources must support a second full read (e.g. retry after range abort). + try (InputStream in = loaded.getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + } + + @Test + void store_writesOnlyCiphertextToDisk() throws IOException { + StoredObject stored = provider.store(owner, upload()); + byte[] onDisk = Files.readAllBytes(tempDir.resolve(stored.getStorageKey())); + + assertThat(new String(onDisk, 0, 8, StandardCharsets.US_ASCII)).isEqualTo("SPDFEAR1"); + assertThat(new String(onDisk, StandardCharsets.ISO_8859_1)).doesNotContain("secret"); + assertThat(onDisk.length).isGreaterThan(PLAINTEXT.length); + } + + @Test + void load_legacyPlaintextBlob_passesThroughUntouched() throws IOException { + StoredObject legacy = inner.store(owner, upload()); + Resource loaded = provider.load(legacy.getStorageKey()); + try (InputStream in = loaded.getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + } + + @Test + void store_writeDisabled_staysPlaintextButLoadStillDecrypts() throws IOException { + StoredObject encrypted = provider.store(owner, upload()); + + EncryptingStorageProvider decryptOnly = + new EncryptingStorageProvider(inner, newKeyService(), false); + StoredObject plain = decryptOnly.store(owner, upload()); + + assertThat(plain.getEncryptionKeyId()).isNull(); + byte[] onDisk = Files.readAllBytes(tempDir.resolve(plain.getStorageKey())); + assertThat(onDisk).isEqualTo(PLAINTEXT); + + // Decrypt-only mode still reads previously encrypted content. + try (InputStream in = decryptOnly.load(encrypted.getStorageKey()).getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + } + + @Test + void load_disabledKey_failsClosed() throws IOException { + StoredObject stored = provider.store(owner, upload()); + repo.rows + .get(UUID.fromString(stored.getEncryptionKeyId())) + .setStatus(FileEncryptionKey.Status.DISABLED); + + // Fresh service = fresh unwrap cache, as another node would see it. + EncryptingStorageProvider fresh = + new EncryptingStorageProvider(inner, newKeyService(), true); + assertThatThrownBy(() -> fresh.load(stored.getStorageKey())) + .isInstanceOf(StorageKeyRevokedException.class) + .hasMessageContaining("disabled"); + } + + @Test + void load_tamperedPayload_failsAuthentication() throws IOException { + StoredObject stored = provider.store(owner, upload()); + Path blob = tempDir.resolve(stored.getStorageKey()); + byte[] bytes = Files.readAllBytes(blob); + bytes[EncryptedFileFormat.HEADER_LENGTH + 20] ^= 0x42; // flip a payload bit + Files.write(blob, bytes); + + Resource loaded = provider.load(stored.getStorageKey()); + assertThatThrownBy( + () -> { + try (InputStream in = loaded.getInputStream()) { + in.readAllBytes(); + } + }) + .isInstanceOf(IOException.class); + } + + @Test + void load_tamperedHeaderKeyId_failsAuthentication() throws IOException { + StoredObject stored = provider.store(owner, upload()); + // Second key so the swapped-in id exists and is ACTIVE. + Team otherTeam = new Team(); + otherTeam.setId(99L); + User otherOwner = new User(); + otherOwner.setId(2L); + otherOwner.setTeam(otherTeam); + StoredObject other = provider.store(otherOwner, upload()); + + Path blob = tempDir.resolve(stored.getStorageKey()); + byte[] bytes = Files.readAllBytes(blob); + byte[] otherBytes = Files.readAllBytes(tempDir.resolve(other.getStorageKey())); + System.arraycopy(otherBytes, 10, bytes, 10, 16); // transplant the other key id + Files.write(blob, bytes); + + assertThatThrownBy(() -> provider.load(stored.getStorageKey())) + .isInstanceOf(StorageEncryptionException.class); + } + + @Test + void signedDownloadUrl_isSuppressed() throws IOException { + StoredObject stored = provider.store(owner, upload()); + assertThat(provider.signedDownloadUrl(stored.getStorageKey(), Duration.ofMinutes(5))) + .isEmpty(); + assertThat( + provider.signedDownloadUrl( + stored.getStorageKey(), Duration.ofMinutes(5), true, "a.pdf")) + .isEmpty(); + } + + @Test + void load_oneShotStreamBackend_roundTrips() throws IOException { + // Simulates S3: load() hands back a single-use InputStreamResource. + StorageProvider oneShotInner = + new StorageProvider() { + @Override + public StoredObject store(User owner, MultipartFile file) throws IOException { + return inner.store(owner, file); + } + + @Override + public Resource load(String storageKey) throws IOException { + InputStream in = inner.load(storageKey).getInputStream(); + return new InputStreamResource(in); + } + + @Override + public void delete(String storageKey) throws IOException { + inner.delete(storageKey); + } + }; + EncryptingStorageProvider oneShot = + new EncryptingStorageProvider(oneShotInner, newKeyService(), true); + + StoredObject stored = oneShot.store(owner, upload()); + Resource loaded = oneShot.load(stored.getStorageKey()); + assertThat(loaded.isOpen()).isTrue(); + assertThat(loaded.contentLength()).isEqualTo(PLAINTEXT.length); + try (InputStream in = loaded.getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + + // Legacy plaintext through the one-shot path replays the sniffed prefix. + StoredObject legacy = inner.store(owner, upload()); + try (InputStream in = oneShot.load(legacy.getStorageKey()).getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(PLAINTEXT); + } + } + + @Test + void store_largeFile_roundTripsAcrossSegmentBoundaries() throws IOException { + byte[] big = new byte[EncryptedFileFormat.SEGMENT_SIZE_BYTES * 2 + 12345]; + for (int i = 0; i < big.length; i++) { + big[i] = (byte) (i * 31); + } + StoredObject stored = + provider.store( + owner, new MockMultipartFile("file", "big.bin", "application/pdf", big)); + assertThat(stored.getSizeBytes()).isEqualTo(big.length); + try (InputStream in = provider.load(stored.getStorageKey()).getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(big); + } + } + + @Test + void auditListener_receivesEncryptDecryptAndDeniedEvents() throws IOException { + List events = new java.util.ArrayList<>(); + StorageEncryptionAuditListener recording = + new StorageEncryptionAuditListener() { + @Override + public void encrypted(String storageKey, UUID keyId) { + events.add("encrypt:" + storageKey); + } + + @Override + public void decrypted(String storageKey, UUID keyId) { + events.add("decrypt:" + storageKey); + } + + @Override + public void decryptDenied(UUID keyId, String reason) { + events.add("denied:" + reason); + } + }; + FileEncryptionKeyService keyService = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER, false), recording); + EncryptingStorageProvider audited = + new EncryptingStorageProvider(inner, keyService, true, recording); + + StoredObject stored = audited.store(owner, upload()); + try (InputStream in = audited.load(stored.getStorageKey()).getInputStream()) { + in.readAllBytes(); + } + // Legacy plaintext must NOT produce a decrypt event. + StoredObject legacy = inner.store(owner, upload()); + audited.load(legacy.getStorageKey()); + + assertThat(events) + .containsExactly( + "encrypt:" + stored.getStorageKey(), "decrypt:" + stored.getStorageKey()); + + repo.rows + .get(UUID.fromString(stored.getEncryptionKeyId())) + .setStatus(FileEncryptionKey.Status.DISABLED); + keyService.invalidate(UUID.fromString(stored.getEncryptionKeyId())); + assertThatThrownBy(() -> audited.load(stored.getStorageKey())) + .isInstanceOf(StorageKeyRevokedException.class); + assertThat(events).hasSize(3); + assertThat(events.get(2)).isEqualTo("denied:key disabled"); + } + + @Test + void load_tinyLegacyBlob_shorterThanHeader_passesThrough() throws IOException { + byte[] tiny = "hi".getBytes(StandardCharsets.UTF_8); + StoredObject legacy = + inner.store(owner, new MockMultipartFile("file", "tiny.txt", "text/plain", tiny)); + try (InputStream in = provider.load(legacy.getStorageKey()).getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(tiny); + } + } + + /** Tracks close() so leak regressions on the one-shot (S3-style) path are caught. */ + private final class TrackingOneShotProvider implements StorageProvider { + final java.util.concurrent.atomic.AtomicInteger openStreams = + new java.util.concurrent.atomic.AtomicInteger(); + + @Override + public StoredObject store(User owner, MultipartFile file) throws IOException { + return inner.store(owner, file); + } + + @Override + public Resource load(String storageKey) throws IOException { + openStreams.incrementAndGet(); + InputStream tracked = + new java.io.FilterInputStream(inner.load(storageKey).getInputStream()) { + @Override + public void close() throws IOException { + openStreams.decrementAndGet(); + super.close(); + } + }; + return new InputStreamResource(tracked); + } + + @Override + public void delete(String storageKey) throws IOException { + inner.delete(storageKey); + } + } + + @Test + void load_oneShot_disabledKey_closesUnderlyingStream() throws IOException { + StoredObject stored = provider.store(owner, upload()); + repo.rows + .get(UUID.fromString(stored.getEncryptionKeyId())) + .setStatus(FileEncryptionKey.Status.DISABLED); + + TrackingOneShotProvider tracking = new TrackingOneShotProvider(); + EncryptingStorageProvider oneShot = + new EncryptingStorageProvider(tracking, newKeyService(), true); + + // Exercising the kill switch repeatedly must not leak connections. + for (int i = 0; i < 3; i++) { + assertThatThrownBy(() -> oneShot.load(stored.getStorageKey())) + .isInstanceOf(StorageKeyRevokedException.class); + } + assertThat(tracking.openStreams.get()).isZero(); + } + + @Test + void load_oneShot_unknownHeaderVersion_closesUnderlyingStream() throws IOException { + StoredObject stored = provider.store(owner, upload()); + Path blob = tempDir.resolve(stored.getStorageKey()); + byte[] bytes = Files.readAllBytes(blob); + bytes[8] = 99; // future format version + Files.write(blob, bytes); + + TrackingOneShotProvider tracking = new TrackingOneShotProvider(); + EncryptingStorageProvider oneShot = + new EncryptingStorageProvider(tracking, newKeyService(), true); + + assertThatThrownBy(() -> oneShot.load(stored.getStorageKey())) + .isInstanceOf(StorageEncryptionException.class) + .hasMessageContaining("Unsupported"); + assertThat(tracking.openStreams.get()).isZero(); + } + + @Test + void store_uploadMisreportingSize_failsInsteadOfCorruptingContentLength() { + MultipartFile lying = + new MockMultipartFile("file", "test.pdf", "application/pdf", PLAINTEXT) { + @Override + public long getSize() { + return PLAINTEXT.length + 5; + } + }; + assertThatThrownBy(() -> provider.store(owner, lying)) + .isInstanceOf(StorageEncryptionException.class) + .hasMessageContaining("streamed"); + } + + @Test + void load_tamperedPlaintextLength_failsAuthentication() throws IOException { + StoredObject stored = provider.store(owner, upload()); + Path blob = tempDir.resolve(stored.getStorageKey()); + byte[] bytes = Files.readAllBytes(blob); + bytes[33] ^= 0x01; // low byte of the header's plaintextLength field + Files.write(blob, bytes); + + // plaintextLength is part of the AAD, so the DEK unwrap must reject the header. + assertThatThrownBy(() -> provider.load(stored.getStorageKey())) + .isInstanceOf(StorageEncryptionException.class); + } + + @Test + void signedDownloadUrl_delegatesWhenNoEncryptedContentPossible() throws IOException { + // Vanilla install: flag off, no key rows anywhere -> keep the backend's fast path. + StorageEncryptionState vanilla = + new StorageEncryptionState( + false, + () -> newKeyService(), + repo.mock, + StorageEncryptionAuditListener.NOOP); + StorageProvider withUrls = + new StorageProvider() { + @Override + public StoredObject store(User owner, MultipartFile file) throws IOException { + return inner.store(owner, file); + } + + @Override + public Resource load(String storageKey) throws IOException { + return inner.load(storageKey); + } + + @Override + public void delete(String storageKey) throws IOException { + inner.delete(storageKey); + } + + @Override + public java.util.Optional signedDownloadUrl( + String storageKey, + Duration ttl, + boolean inline, + String originalFilename) { + return java.util.Optional.of(java.net.URI.create("https://signed.example")); + } + }; + EncryptingStorageProvider decorated = new EncryptingStorageProvider(withUrls, vanilla); + + assertThat(decorated.signedDownloadUrl("k", Duration.ofMinutes(5), false, "a.pdf")) + .contains(java.net.URI.create("https://signed.example")); + + // As soon as key rows exist, the same node must stop handing out direct URLs. + StoredObject encrypted = provider.store(owner, upload()); + assertThat(encrypted.getEncryptionKeyId()).isNotNull(); + StorageEncryptionState drifted = + new StorageEncryptionState( + false, + () -> newKeyService(), + repo.mock, + StorageEncryptionAuditListener.NOOP); + EncryptingStorageProvider driftedNode = new EncryptingStorageProvider(withUrls, drifted); + assertThat(driftedNode.signedDownloadUrl("k", Duration.ofMinutes(5), false, "a.pdf")) + .isEmpty(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceDbTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceDbTest.java new file mode 100644 index 0000000000..e2b5be0186 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceDbTest.java @@ -0,0 +1,113 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Base64; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; + +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.model.FileEncryptionKey; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; + +/** + * Key-creation against a real database and real transaction boundaries. The Mockito race test + * throws synchronously from save(), which hides the production failure mode: under a caller's + * transaction (e.g. WorkflowSessionService is class-level {@code @Transactional}) an assigned-UUID + * INSERT defers to the outer flush, so without REQUIRES_NEW the duplicate-key error would surface + * at commit — far from the recovery catch — with the outer transaction already rollback-only. + */ +@DataJpaTest +class FileEncryptionKeyServiceDbTest { + + private static final String MASTER = + Base64.getEncoder().encodeToString("0123456789abcdef0123456789abcdef".getBytes()); + + @Autowired private FileEncryptionKeyRepository repository; + @Autowired private PlatformTransactionManager transactionManager; + + @AfterEach + void wipe() { + repository.deleteAllInBatch(); + } + + private FileEncryptionKeyService newService() { + TransactionTemplate requiresNew = new TransactionTemplate(transactionManager); + requiresNew.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return new FileEncryptionKeyService( + repository, new FileEncryptionMasterKey(MASTER, false), requiresNew); + } + + private static User teamUser(long teamId) { + Team team = new Team(); + team.setId(teamId); + User user = new User(); + user.setTeam(team); + return user; + } + + @Test + void createActive_duplicateInsertInsideCallerTransaction_recoversAndKeepsCallerHealthy() { + // "Node A" creates the scope key first (committed independently). + FileEncryptionKey winner = newService().createActive(FileEncryptionKey.ScopeType.TEAM, 42L); + assertThat(repository.count()).isEqualTo(1); + + // "Node B" raced: it computed its key version BEFORE A's commit was visible, so it + // attempts the same (scope, version) row. Only that read is faked; the flush, the + // unique-constraint violation, and the recovery all run against the real database. + FileEncryptionKeyRepository raceTimedRepo = + org.mockito.Mockito.mock( + FileEncryptionKeyRepository.class, + org.mockito.AdditionalAnswers.delegatesTo(repository)); + org.mockito.Mockito.doReturn(java.util.Optional.empty()) + .when(raceTimedRepo) + .findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc( + FileEncryptionKey.ScopeType.TEAM, 42L); + + TransactionTemplate requiresNew = new TransactionTemplate(transactionManager); + requiresNew.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + FileEncryptionKeyService raced = + new FileEncryptionKeyService( + raceTimedRepo, new FileEncryptionMasterKey(MASTER, false), requiresNew); + + // Inside a caller transaction (the WorkflowSessionService shape): the duplicate insert + // must be absorbed by createActive's own REQUIRES_NEW transaction, recover to the + // winner's row, and leave the caller transaction committable. + TransactionTemplate callerTx = new TransactionTemplate(transactionManager); + FileEncryptionKey resolved = + callerTx.execute( + status -> { + FileEncryptionKey row = + raced.createActive(FileEncryptionKey.ScopeType.TEAM, 42L); + assertThat(status.isRollbackOnly()).isFalse(); + return row; + }); + + assertThat(resolved.getKeyId()).isEqualTo(winner.getKeyId()); + assertThat(repository.count()).isEqualTo(1); + } + + @Test + void activeKekForOwner_roundTripsThroughRealDatabase() throws Exception { + FileEncryptionKeyService service = newService(); + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(7)); + + // Fresh service (cold caches) unwraps the same key material from the committed row. + assertThat(newService().kekForDecrypt(created.keyId())).isEqualTo(created.key()); + } + + // The key entity/repository live in sibling packages, so point the JPA slice at the whole + // proprietary tree (the entity graph pulls in User/Team either way). + @SpringBootConfiguration + @AutoConfigurationPackage(basePackages = "stirling.software.proprietary") + static class TestApp {} +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceTest.java new file mode 100644 index 0000000000..a4dd9720d3 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/FileEncryptionKeyServiceTest.java @@ -0,0 +1,354 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; + +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.model.FileEncryptionKey; + +class FileEncryptionKeyServiceTest { + + private static final String MASTER_A = + Base64.getEncoder() + .encodeToString( + new byte[] { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 + }); + private static final String MASTER_B = + Base64.getEncoder() + .encodeToString( + new byte[] { + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, + 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 + }); + + private InMemoryKeyRepo repo; + private FileEncryptionKeyService service; + + @BeforeEach + void setUp() { + repo = new InMemoryKeyRepo(); + service = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_A, false)); + } + + private static User teamUser(long teamId) { + Team team = new Team(); + team.setId(teamId); + User user = new User(); + user.setTeam(team); + return user; + } + + @Test + void activeKekForOwner_createsPerTeamKeyOnDemand_thenReuses() throws Exception { + FileEncryptionKeyService.ScopeKek first = service.activeKekForOwner(teamUser(5)); + FileEncryptionKeyService.ScopeKek second = service.activeKekForOwner(teamUser(5)); + assertThat(second.keyId()).isEqualTo(first.keyId()); + assertThat(repo.rows).hasSize(1); + FileEncryptionKey row = repo.rows.values().iterator().next(); + assertThat(row.getScopeType()).isEqualTo(FileEncryptionKey.ScopeType.TEAM); + assertThat(row.getScopeId()).isEqualTo(5); + assertThat(row.getStatus()).isEqualTo(FileEncryptionKey.Status.ACTIVE); + + FileEncryptionKeyService.ScopeKek otherTeam = service.activeKekForOwner(teamUser(6)); + assertThat(otherTeam.keyId()).isNotEqualTo(first.keyId()); + assertThat(repo.rows).hasSize(2); + } + + @Test + void activeKekForOwner_withoutTeam_usesGlobalScope() throws Exception { + service.activeKekForOwner(new User()); + FileEncryptionKey row = repo.rows.values().iterator().next(); + assertThat(row.getScopeType()).isEqualTo(FileEncryptionKey.ScopeType.GLOBAL); + assertThat(row.getScopeId()).isZero(); + } + + @Test + void kekForDecrypt_unwrapsWhatWasWrapped() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + // A fresh service (empty cache) must unwrap the same key material from the DB row. + FileEncryptionKeyService fresh = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_A, false)); + assertThat(fresh.kekForDecrypt(created.keyId())).isEqualTo(created.key()); + } + + @Test + void kekForDecrypt_disabledKey_failsClosed() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + repo.rows.get(created.keyId()).setStatus(FileEncryptionKey.Status.DISABLED); + FileEncryptionKeyService fresh = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_A, false)); + assertThatThrownBy(() -> fresh.kekForDecrypt(created.keyId())) + .isInstanceOf(StorageKeyRevokedException.class) + .hasMessageContaining("disabled"); + } + + @Test + void kekForDecrypt_unknownKey_failsClosed() { + assertThatThrownBy(() -> service.kekForDecrypt(UUID.randomUUID())) + .isInstanceOf(StorageEncryptionException.class) + .hasMessageContaining("No encryption key"); + } + + @Test + void verifyMasterKey_wrongKey_refusesStartup() throws Exception { + service.activeKekForOwner(teamUser(1)); + FileEncryptionKeyService wrongKey = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_B, false)); + assertThatThrownBy(wrongKey::verifyMasterKey) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot unwrap"); + } + + @Test + void verifyMasterKey_correctKeyOrEmptyRegistry_passes() throws Exception { + service.verifyMasterKey(); // empty registry: nothing to prove + service.activeKekForOwner(teamUser(1)); + service.verifyMasterKey(); + } + + @Test + void verifyMasterKey_retiredOnlyRegistry_stillVerifies() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + repo.rows.get(created.keyId()).setStatus(FileEncryptionKey.Status.RETIRED); + + // A retired-only registry must still prove the key (RETIRED rows decrypt old blobs)... + new FileEncryptionKeyService(repo.mock, new FileEncryptionMasterKey(MASTER_A, false)) + .verifyMasterKey(); + // ...and still refuse a mismatched master key. + assertThatThrownBy( + () -> + new FileEncryptionKeyService( + repo.mock, + new FileEncryptionMasterKey(MASTER_B, false)) + .verifyMasterKey()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot unwrap"); + } + + @Test + void verifyMasterKey_halfRotatedRegistryWithPreviousKeyRemoved_refusesStartup() + throws Exception { + // Team 1 gets re-wrapped under the new master key; team 2 does not, because the rotation + // stopped part-way (or was never run). + FileEncryptionKeyService.ScopeKek rewrapped = service.activeKekForOwner(teamUser(1)); + FileEncryptionKeyService.ScopeKek leftBehind = service.activeKekForOwner(teamUser(2)); + FileEncryptionMasterKey rotating = + new FileEncryptionMasterKey(MASTER_B, MASTER_A, 2, false); + FileEncryptionKey row = repo.rows.get(rewrapped.keyId()); + row.setWrappedKey( + Base64.getEncoder() + .encodeToString( + rotating.wrap( + rewrapped.key(), + rewrapped + .keyId() + .toString() + .getBytes(StandardCharsets.US_ASCII)))); + row.setMasterKeyVersion(2); + + // The operator now removes the previous key, believing rotation finished. Team 2's files + // are only recoverable while MASTER_A still exists, so startup must not look healthy. + FileEncryptionKeyService afterRemoval = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_B, null, 2, false)); + + assertThatThrownBy(afterRemoval::verifyMasterKey) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot unwrap 1 of 2") + .hasMessageContaining(leftBehind.keyId().toString()) + .hasMessageContaining("Refusing to start"); + } + + @Test + void verifyMasterKey_disabledRowThatCannotUnwrap_refusesStartup() throws Exception { + // Revocation is advertised as reversible, so an unreadable DISABLED row is just as much a + // loss of access as an unreadable ACTIVE one - it would not come back on enable. + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + service.setKeyStatus(created.keyId(), FileEncryptionKey.Status.DISABLED, "admin"); + + FileEncryptionKeyService wrongKey = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_B, false)); + + assertThatThrownBy(wrongKey::verifyMasterKey) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot unwrap 1 of 1"); + } + + @Test + void createActive_concurrentInsertRace_fallsBackToWinnersRow() throws Exception { + // First save call hits the unique constraint; the service must re-read the winner's row. + FileEncryptionKey winner = new FileEncryptionKey(); + winner.setKeyId(UUID.randomUUID()); + winner.setScopeType(FileEncryptionKey.ScopeType.TEAM); + winner.setScopeId(9); + winner.setKeyVersion(1); + winner.setStatus(FileEncryptionKey.Status.ACTIVE); + winner.setMasterKeyVersion(1); + FileEncryptionMasterKey master = new FileEncryptionMasterKey(MASTER_A, false); + byte[] winnerKek = new byte[32]; + winner.setWrappedKey( + Base64.getEncoder() + .encodeToString( + master.wrap( + winnerKek, + winner.getKeyId() + .toString() + .getBytes( + java.nio.charset.StandardCharsets + .US_ASCII)))); + + doAnswer( + inv -> { + repo.rows.put(winner.getKeyId(), winner); + throw new DataIntegrityViolationException("duplicate key"); + }) + .doAnswer( + inv -> { + FileEncryptionKey row = inv.getArgument(0); + repo.rows.put(row.getKeyId(), row); + return row; + }) + .when(repo.mock) + .saveAndFlush(any(FileEncryptionKey.class)); + + FileEncryptionKeyService racedService = new FileEncryptionKeyService(repo.mock, master); + FileEncryptionKeyService.ScopeKek resolved = racedService.activeKekForOwner(teamUser(9)); + assertThat(resolved.keyId()).isEqualTo(winner.getKeyId()); + assertThat(resolved.key()).isEqualTo(winnerKek); + } + + @Test + void setKeyStatus_disable_takesEffectImmediatelyOnSameService() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + // The unwrap cache is warm from creation; disabling must invalidate it, not wait for TTL. + service.setKeyStatus(created.keyId(), FileEncryptionKey.Status.DISABLED, "admin"); + assertThatThrownBy(() -> service.kekForDecrypt(created.keyId())) + .isInstanceOf(StorageKeyRevokedException.class); + assertThat(repo.rows.get(created.keyId()).getStatusChangedBy()).isEqualTo("admin"); + + service.setKeyStatus(created.keyId(), FileEncryptionKey.Status.ACTIVE, "admin"); + assertThat(service.kekForDecrypt(created.keyId())).isEqualTo(created.key()); + } + + @Test + void unwrap_fallsBackToPreviousMasterKeyDuringRotation() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + + // New primary key B, old key A kept as previous, version bumped to 2. + FileEncryptionMasterKey rotated = new FileEncryptionMasterKey(MASTER_B, MASTER_A, 2, false); + FileEncryptionKeyService rotatedService = new FileEncryptionKeyService(repo.mock, rotated); + assertThat(rotatedService.kekForDecrypt(created.keyId())).isEqualTo(created.key()); + rotatedService.verifyMasterKey(); // must pass via the previous-key fallback + } + + @Test + void rotateMasterKey_rewrapsRowsBelowCurrentVersion() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + String wrappedBefore = repo.rows.get(created.keyId()).getWrappedKey(); + + FileEncryptionMasterKey rotated = new FileEncryptionMasterKey(MASTER_B, MASTER_A, 2, false); + FileEncryptionKeyService rotatedService = new FileEncryptionKeyService(repo.mock, rotated); + assertThat(rotatedService.rotateMasterKey()).isEqualTo(1); + + FileEncryptionKey row = repo.rows.get(created.keyId()); + assertThat(row.getMasterKeyVersion()).isEqualTo(2); + assertThat(row.getWrappedKey()).isNotEqualTo(wrappedBefore); + + // Same key material now unwraps WITHOUT the previous key configured. + FileEncryptionKeyService afterCleanup = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_B, null, 2, false)); + assertThat(afterCleanup.kekForDecrypt(created.keyId())).isEqualTo(created.key()); + + // Second rotation call is a no-op. + assertThat(rotatedService.rotateMasterKey()).isZero(); + } + + @Test + void enable_afterScopeMintedAnotherKey_comesBackRetiredNotSecondActive() throws Exception { + FileEncryptionKeyService.ScopeKek revoked = service.activeKekForOwner(teamUser(1)); + service.setKeyStatus(revoked.keyId(), FileEncryptionKey.Status.DISABLED, "admin"); + + // Revoking does not stop the team uploading: this mints a second key for the scope. + FileEncryptionKeyService.ScopeKek minted = service.activeKekForOwner(teamUser(1)); + assertThat(minted.keyId()).isNotEqualTo(revoked.keyId()); + + FileEncryptionKey reEnabled = service.enable(revoked.keyId(), "admin"); + + assertThat(reEnabled.getStatus()).isEqualTo(FileEncryptionKey.Status.RETIRED); + assertThat(activeKeysForTeam(1)).containsExactly(minted.keyId()); + // RETIRED still unwraps, so the revoked content is readable again. + assertThat(service.kekForDecrypt(revoked.keyId())).isEqualTo(revoked.key()); + } + + @Test + void enable_scopeHasNoOtherActiveKey_comesBackActive() throws Exception { + FileEncryptionKeyService.ScopeKek created = service.activeKekForOwner(teamUser(1)); + service.setKeyStatus(created.keyId(), FileEncryptionKey.Status.DISABLED, "admin"); + + FileEncryptionKey reEnabled = service.enable(created.keyId(), "admin"); + + assertThat(reEnabled.getStatus()).isEqualTo(FileEncryptionKey.Status.ACTIVE); + assertThat(activeKeysForTeam(1)).containsExactly(created.keyId()); + assertThat(service.kekForDecrypt(created.keyId())).isEqualTo(created.key()); + } + + @Test + void activeKekForOwner_twoActiveRowsInScope_picksTheHighestVersionOnEveryNode() + throws Exception { + // A registry left in the pre-fix shape (an older build re-enabled a key straight to + // ACTIVE). + FileEncryptionKeyService.ScopeKek older = service.activeKekForOwner(teamUser(1)); + service.setKeyStatus(older.keyId(), FileEncryptionKey.Status.DISABLED, "admin"); + FileEncryptionKeyService.ScopeKek newer = service.activeKekForOwner(teamUser(1)); + service.setKeyStatus(older.keyId(), FileEncryptionKey.Status.ACTIVE, "old-build"); + assertThat(activeKeysForTeam(1)).hasSize(2); + + // Two independent nodes (fresh caches) must agree, rather than follow DB row order. + for (int node = 0; node < 2; node++) { + FileEncryptionKeyService fresh = + new FileEncryptionKeyService( + repo.mock, new FileEncryptionMasterKey(MASTER_A, false)); + assertThat(fresh.activeKekForOwner(teamUser(1)).keyId()).isEqualTo(newer.keyId()); + } + } + + private List activeKeysForTeam(long teamId) { + return repo.rows.values().stream() + .filter(r -> r.getScopeType() == FileEncryptionKey.ScopeType.TEAM) + .filter(r -> r.getScopeId() == teamId) + .filter(r -> r.getStatus() == FileEncryptionKey.Status.ACTIVE) + .map(FileEncryptionKey::getKeyId) + .toList(); + } + + @Test + void createActive_raceWithoutWinner_rethrows() { + doThrow(new DataIntegrityViolationException("duplicate key")) + .when(repo.mock) + .saveAndFlush(any(FileEncryptionKey.class)); + assertThatThrownBy(() -> service.activeKekForOwner(teamUser(9))) + .isInstanceOf(DataIntegrityViolationException.class); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/InMemoryKeyRepo.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/InMemoryKeyRepo.java new file mode 100644 index 0000000000..3d5e7ede3d --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/crypto/InMemoryKeyRepo.java @@ -0,0 +1,102 @@ +package stirling.software.proprietary.storage.crypto; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.data.domain.Sort; + +import stirling.software.proprietary.storage.model.FileEncryptionKey; +import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository; + +/** Map-backed Mockito stub of the key repository for crypto tests (no JPA slice needed). */ +public final class InMemoryKeyRepo { + + public final Map rows = new ConcurrentHashMap<>(); + public final FileEncryptionKeyRepository mock; + + public InMemoryKeyRepo() { + mock = mock(FileEncryptionKeyRepository.class); + when(mock.saveAndFlush(any(FileEncryptionKey.class))) + .thenAnswer( + inv -> { + FileEncryptionKey row = inv.getArgument(0); + rows.put(row.getKeyId(), row); + return row; + }); + when(mock.save(any(FileEncryptionKey.class))) + .thenAnswer( + inv -> { + FileEncryptionKey row = inv.getArgument(0); + rows.put(row.getKeyId(), row); + return row; + }); + when(mock.findById(any(UUID.class))) + .thenAnswer(inv -> Optional.ofNullable(rows.get(inv.getArgument(0)))); + when(mock.findFirstByScopeTypeAndScopeIdAndStatusOrderByKeyVersionDesc( + any(), anyLong(), any())) + .thenAnswer( + inv -> + rows.values().stream() + .filter(r -> r.getScopeType() == inv.getArgument(0)) + .filter(r -> r.getScopeId() == inv.getArgument(1)) + .filter(r -> r.getStatus() == inv.getArgument(2)) + .max( + Comparator.comparingInt( + FileEncryptionKey::getKeyVersion))); + when(mock.findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc(any(), anyLong())) + .thenAnswer( + inv -> + rows.values().stream() + .filter(r -> r.getScopeType() == inv.getArgument(0)) + .filter(r -> r.getScopeId() == inv.getArgument(1)) + .max( + Comparator.comparingInt( + FileEncryptionKey::getKeyVersion))); + when(mock.findFirstByStatus(any())) + .thenAnswer( + inv -> + rows.values().stream() + .filter(r -> r.getStatus() == inv.getArgument(0)) + .findFirst()); + when(mock.count()).thenAnswer(inv -> (long) rows.size()); + when(mock.findAll()).thenAnswer(inv -> List.copyOf(rows.values())); + when(mock.countByMasterKeyVersionLessThan(anyInt())) + .thenAnswer( + inv -> + rows.values().stream() + .filter( + r -> + r.getMasterKeyVersion() + < inv.getArgument(0)) + .count()); + when(mock.findByMasterKeyVersionLessThan(anyInt())) + .thenAnswer( + inv -> + rows.values().stream() + .filter( + r -> + r.getMasterKeyVersion() + < inv.getArgument(0)) + .toList()); + when(mock.countByMasterKeyVersionGreaterThan(anyInt())) + .thenAnswer( + inv -> + rows.values().stream() + .filter( + r -> + r.getMasterKeyVersion() + > inv.getArgument(0)) + .count()); + when(mock.findAll(any(Sort.class))).thenAnswer(inv -> List.copyOf(rows.values())); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/repository/StoredFileMigrationQueriesDbTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/repository/StoredFileMigrationQueriesDbTest.java new file mode 100644 index 0000000000..430b0e1522 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/repository/StoredFileMigrationQueriesDbTest.java @@ -0,0 +1,148 @@ +package stirling.software.proprietary.storage.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.annotation.DirtiesContext; + +import jakarta.persistence.EntityManager; + +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.model.StoredFile; + +/** + * The encrypt-existing migration's queries against a real database. The service-level tests mock + * {@link StoredFileRepository} wholesale, so without this the JOIN FETCH + Pageable selection and + * the three compare-and-swap updates would never actually execute — a wrong CAS predicate would + * pass every mocked test and then silently skip every file in production. + */ +@DataJpaTest +@DirtiesContext +class StoredFileMigrationQueriesDbTest { + + @Autowired private StoredFileRepository repository; + @Autowired private EntityManager entityManager; + + private User owner; + + @BeforeEach + void setUp() { + Team team = new Team(); + team.setName("team-" + UUID.randomUUID()); + entityManager.persist(team); + + owner = new User(); + owner.setUsername("owner-" + UUID.randomUUID()); + owner.setPassword("x"); + owner.setTeam(team); + entityManager.persist(owner); + entityManager.flush(); + } + + private StoredFile persistFile(String storageKey, String encryptionKeyId) { + StoredFile file = new StoredFile(); + file.setOwner(owner); + file.setOriginalFilename("doc.pdf"); + file.setContentType("application/pdf"); + file.setSizeBytes(123); + file.setStorageKey(storageKey); + file.setEncryptionKeyId(encryptionKeyId); + entityManager.persist(file); + entityManager.flush(); + return file; + } + + @Test + void findMigratableAfter_selectsOnlyPlaintextRows_inIdOrder_withOwnerFetched() { + StoredFile plain1 = persistFile("k-plain-1", null); + StoredFile plain2 = persistFile("k-plain-2", null); + persistFile("k-encrypted", UUID.randomUUID().toString()); + entityManager.clear(); + + List page = repository.findMigratableAfter(0L, PageRequest.of(0, 10)); + + assertThat(page) + .extracting(StoredFile::getId) + .containsExactly(plain1.getId(), plain2.getId()); + // JOIN FETCH must populate owner (+ its EAGER team) for use outside this session. + assertThat(page.get(0).getOwner().getUsername()).isEqualTo(owner.getUsername()); + assertThat(page.get(0).getOwner().getTeam()).isNotNull(); + } + + @Test + void findMigratableAfter_cursorAndPageSizeAreHonoured() { + StoredFile first = persistFile("k-1", null); + StoredFile second = persistFile("k-2", null); + entityManager.clear(); + + assertThat(repository.findMigratableAfter(0L, PageRequest.of(0, 1))) + .extracting(StoredFile::getId) + .containsExactly(first.getId()); + assertThat(repository.findMigratableAfter(first.getId(), PageRequest.of(0, 10))) + .extracting(StoredFile::getId) + .containsExactly(second.getId()); + assertThat(repository.findMigratableAfter(second.getId(), PageRequest.of(0, 10))).isEmpty(); + } + + @Test + void swapMainBlob_appliesOnlyWhenStorageKeyStillMatches() { + StoredFile file = persistFile("k-old", null); + String keyId = UUID.randomUUID().toString(); + entityManager.clear(); + + // Stale expectation (someone else replaced the file) -> no row updated. + assertThat(repository.swapMainBlob(file.getId(), "k-stale", "k-new", keyId)).isZero(); + + assertThat(repository.swapMainBlob(file.getId(), "k-old", "k-new", keyId)).isEqualTo(1); + entityManager.clear(); + + StoredFile reloaded = repository.findById(file.getId()).orElseThrow(); + assertThat(reloaded.getStorageKey()).isEqualTo("k-new"); + assertThat(reloaded.getEncryptionKeyId()).isEqualTo(keyId); + // Stamping the key id must remove the row from the migration's selection. + assertThat(repository.findMigratableAfter(0L, PageRequest.of(0, 10))).isEmpty(); + assertThat(repository.countByEncryptionKeyIdIsNotNull()).isEqualTo(1); + assertThat(repository.countByEncryptionKeyIdIsNull()).isZero(); + } + + @Test + void swapSecondaryBlobs_applyIndependentlyAndLeaveKeyIdAlone() { + StoredFile file = persistFile("k-main", null); + file.setHistoryStorageKey("k-hist-old"); + file.setAuditLogStorageKey("k-audit-old"); + entityManager.merge(file); + entityManager.flush(); + entityManager.clear(); + + assertThat(repository.swapHistoryBlob(file.getId(), "k-wrong", "k-hist-new")).isZero(); + assertThat(repository.swapHistoryBlob(file.getId(), "k-hist-old", "k-hist-new")) + .isEqualTo(1); + assertThat(repository.swapAuditLogBlob(file.getId(), "k-audit-old", "k-audit-new")) + .isEqualTo(1); + entityManager.clear(); + + StoredFile reloaded = repository.findById(file.getId()).orElseThrow(); + assertThat(reloaded.getHistoryStorageKey()).isEqualTo("k-hist-new"); + assertThat(reloaded.getAuditLogStorageKey()).isEqualTo("k-audit-new"); + // Secondary swaps must not stamp encryptionKeyId, so the row stays selectable until the + // main blob is done. + assertThat(reloaded.getEncryptionKeyId()).isNull(); + assertThat(repository.findMigratableAfter(0L, PageRequest.of(0, 10))) + .extracting(StoredFile::getId) + .containsExactly(file.getId()); + } + + @SpringBootConfiguration + @AutoConfigurationPackage(basePackages = "stirling.software.proprietary") + static class TestApp {} +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/FileStorageServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/FileStorageServiceTest.java index 83d9111981..d02b04ae2b 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/FileStorageServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/FileStorageServiceTest.java @@ -19,12 +19,15 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.server.ResponseStatusException; import stirling.software.common.model.ApplicationProperties; import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.crypto.StorageEncryptionException; +import stirling.software.proprietary.storage.crypto.StorageKeyRevokedException; import stirling.software.proprietary.storage.model.FileShare; import stirling.software.proprietary.storage.model.ShareAccessRole; import stirling.software.proprietary.storage.model.StoredFile; @@ -570,4 +573,37 @@ class FileStorageServiceTest { verify(storedFileRepository).delete(f); } + + // ------------------------------------------------------------------------- + // loadFile — encryption error mapping + // ------------------------------------------------------------------------- + + @Test + void loadFile_revokedKey_throwsForbidden() throws IOException { + StoredFile f = ownedFile(user(1L)); + f.setStorageKey("k"); + when(storageProvider.load("k")) + .thenThrow(new StorageKeyRevokedException("Encryption key X is disabled")); + + assertThatThrownBy(() -> service.loadFile(f)) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + } + + @Test + void loadFile_genericIoError_throwsInternalServerError() throws IOException { + StoredFile f = ownedFile(user(1L)); + f.setStorageKey("k"); + when(storageProvider.load("k")).thenThrow(new StorageEncryptionException("corrupt blob")); + + assertThatThrownBy(() -> service.loadFile(f)) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)); + } } diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationServiceTest.java new file mode 100644 index 0000000000..e71f8ef714 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/storage/service/StorageEncryptionMigrationServiceTest.java @@ -0,0 +1,467 @@ +package stirling.software.proprietary.storage.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.core.io.InputStreamResource; +import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import stirling.software.proprietary.audit.AuditEventType; +import stirling.software.proprietary.model.Team; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.service.AuditService; +import stirling.software.proprietary.storage.crypto.EncryptingStorageProvider; +import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService; +import stirling.software.proprietary.storage.crypto.FileEncryptionMasterKey; +import stirling.software.proprietary.storage.crypto.InMemoryKeyRepo; +import stirling.software.proprietary.storage.crypto.StorageEncryptionAuditListener; +import stirling.software.proprietary.storage.crypto.StorageEncryptionState; +import stirling.software.proprietary.storage.model.StoredFile; +import stirling.software.proprietary.storage.provider.LocalStorageProvider; +import stirling.software.proprietary.storage.provider.StorageProvider; +import stirling.software.proprietary.storage.provider.StoredObject; +import stirling.software.proprietary.storage.repository.StoredFileRepository; + +class StorageEncryptionMigrationServiceTest { + + private static final byte[] CONTENT = + "legacy plaintext content that must end up encrypted".getBytes(StandardCharsets.UTF_8); + private static final String MASTER = + Base64.getEncoder() + .encodeToString( + "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8)); + + @TempDir Path tempDir; + + private final Map rows = new ConcurrentHashMap<>(); + private LocalStorageProvider inner; + private StorageProvider provider; + private StoredFileRepository fileRepo; + private StorageEncryptionMigrationService service; + private FileEncryptionKeyService keyService; + private User owner; + + @BeforeEach + void setUp() { + inner = new LocalStorageProvider(tempDir); + keyService = + new FileEncryptionKeyService( + new InMemoryKeyRepo().mock, new FileEncryptionMasterKey(MASTER, false)); + provider = + new EncryptingStorageProvider( + inner, keyService, true, StorageEncryptionAuditListener.NOOP); + + Team team = new Team(); + team.setId(3L); + owner = new User(); + owner.setId(1L); + owner.setTeam(team); + + fileRepo = mock(StoredFileRepository.class); + when(fileRepo.countByEncryptionKeyIdIsNull()) + .thenAnswer( + inv -> + rows.values().stream() + .filter(f -> f.getEncryptionKeyId() == null) + .count()); + when(fileRepo.findMigratableAfter(anyLong(), any(Pageable.class))) + .thenAnswer( + inv -> { + long lastId = inv.getArgument(0); + Pageable p = inv.getArgument(1); + return rows.values().stream() + .filter(f -> f.getEncryptionKeyId() == null) + .filter(f -> f.getId() > lastId) + .sorted(Comparator.comparing(StoredFile::getId)) + .limit(p.getPageSize()) + .toList(); + }); + when(fileRepo.swapMainBlob(anyLong(), anyString(), anyString(), anyString())) + .thenAnswer( + inv -> { + StoredFile f = rows.get(inv.getArgument(0)); + if (f == null || !f.getStorageKey().equals(inv.getArgument(1))) { + return 0; + } + f.setStorageKey(inv.getArgument(2)); + f.setEncryptionKeyId(inv.getArgument(3)); + return 1; + }); + when(fileRepo.swapHistoryBlob(anyLong(), anyString(), anyString())) + .thenAnswer( + inv -> { + StoredFile f = rows.get(inv.getArgument(0)); + if (f == null || !inv.getArgument(1).equals(f.getHistoryStorageKey())) { + return 0; + } + f.setHistoryStorageKey(inv.getArgument(2)); + return 1; + }); + + service = + new StorageEncryptionMigrationService( + fileRepo, + provider, + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + mock(AuditService.class)); + } + + private StoredFile plaintextFile(long id, boolean withHistory) throws Exception { + StoredObject main = + inner.store( + owner, + new MockMultipartFile( + "file", "doc" + id + ".pdf", "application/pdf", CONTENT)); + StoredFile f = new StoredFile(); + f.setId(id); + f.setOwner(owner); + f.setOriginalFilename("doc" + id + ".pdf"); + f.setContentType("application/pdf"); + f.setSizeBytes(CONTENT.length); + f.setStorageKey(main.getStorageKey()); + if (withHistory) { + StoredObject hist = + inner.store( + owner, + new MockMultipartFile( + "file", "hist" + id + ".zip", "application/zip", CONTENT)); + f.setHistoryStorageKey(hist.getStorageKey()); + f.setHistoryFilename("hist" + id + ".zip"); + f.setHistoryContentType("application/zip"); + f.setHistorySizeBytes((long) CONTENT.length); + } + rows.put(id, f); + return f; + } + + private StorageEncryptionMigrationService.MigrationStatus awaitCompletion() throws Exception { + for (int i = 0; i < 300; i++) { + Optional s = service.status(); + if (s.isPresent() + && s.get().state() != StorageEncryptionMigrationService.State.RUNNING) { + return s.get(); + } + Thread.sleep(100); + } + throw new AssertionError("migration did not finish in time"); + } + + @Test + void migrate_encryptsBacklogAndPreservesContent() throws Exception { + plaintextFile(1, false); + plaintextFile(2, true); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.state()).isEqualTo(StorageEncryptionMigrationService.State.COMPLETED); + assertThat(done.processed()).isEqualTo(2); + assertThat(done.failed()).isZero(); + + for (StoredFile f : rows.values()) { + assertThat(f.getEncryptionKeyId()).isNotNull(); + byte[] onDisk = Files.readAllBytes(tempDir.resolve(f.getStorageKey())); + assertThat(new String(onDisk, 0, 8, StandardCharsets.US_ASCII)).isEqualTo("SPDFEAR1"); + try (var in = provider.load(f.getStorageKey()).getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(CONTENT); + } + if (f.getHistoryStorageKey() != null) { + byte[] hist = Files.readAllBytes(tempDir.resolve(f.getHistoryStorageKey())); + assertThat(new String(hist, 0, 8, StandardCharsets.US_ASCII)).isEqualTo("SPDFEAR1"); + } + } + // Exactly one blob per storage key remains (old plaintext blobs were deleted). + long blobCount; + try (var stream = Files.walk(tempDir)) { + blobCount = stream.filter(Files::isRegularFile).count(); + } + assertThat(blobCount).isEqualTo(3); + } + + @Test + void migrate_casMiss_discardsOwnCopyAndSkips() throws Exception { + plaintextFile(1, false); + // Simulate a user replacing the file mid-migration: main-blob CAS always misses. + when(fileRepo.swapMainBlob(anyLong(), anyString(), anyString(), anyString())).thenReturn(0); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.skipped()).isEqualTo(1); + assertThat(done.processed()).isZero(); + StoredFile f = rows.get(1L); + assertThat(f.getEncryptionKeyId()).isNull(); + // The user's blob is untouched and still loadable; the migration's copy is gone. + byte[] onDisk = Files.readAllBytes(tempDir.resolve(f.getStorageKey())); + assertThat(onDisk).isEqualTo(CONTENT); + long blobCount; + try (var stream = Files.walk(tempDir)) { + blobCount = stream.filter(Files::isRegularFile).count(); + } + assertThat(blobCount).isEqualTo(1); + } + + @Test + void start_secondConcurrentRun_rejected() throws Exception { + for (long i = 1; i <= 30; i++) { + plaintextFile(i, false); + } + service.start(); + assertThatThrownBy(service::start) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("already running"); + awaitCompletion(); + // After completion a new run is allowed again. + service.start(); + awaitCompletion(); + } + + @Test + void start_writeDisabled_rejected() { + StorageEncryptionMigrationService disabled = + new StorageEncryptionMigrationService( + fileRepo, + provider, + new StorageEncryptionState( + false, () -> null, null, StorageEncryptionAuditListener.NOOP), + mock(AuditService.class)); + assertThatThrownBy(disabled::start) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("storage.encryption.enabled"); + } + + @Test + void migrate_writeFlagOffMidRun_stopsFailedWithoutChurningTheRestOfTheBacklog() + throws Exception { + for (long i = 1; i <= 30; i++) { // more than one page, so the run has somewhere to churn + plaintextFile(i, false); + } + AtomicBoolean writeEnabled = new AtomicBoolean(true); + AtomicInteger stores = new AtomicInteger(); + // Flip the flag the instant the first file has been stored, exactly as an admin toggling + // storage.encryption.enabled mid-run would. + StorageProvider countingInner = + new StorageProvider() { + @Override + public StoredObject store(User o, MultipartFile f) throws java.io.IOException { + StoredObject stored = inner.store(o, f); + if (stores.incrementAndGet() == 1) { + writeEnabled.set(false); + } + return stored; + } + + @Override + public org.springframework.core.io.Resource load(String key) + throws java.io.IOException { + return inner.load(key); + } + + @Override + public void delete(String key) throws java.io.IOException { + inner.delete(key); + } + }; + StorageEncryptionState flippable = flippableState(writeEnabled); + service = + new StorageEncryptionMigrationService( + fileRepo, + new EncryptingStorageProvider(countingInner, flippable), + flippable, + mock(AuditService.class)); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.state()).isEqualTo(StorageEncryptionMigrationService.State.FAILED); + assertThat(done.processed()).isEqualTo(1); + // The remaining 29 files were never re-stored, so nothing was written and deleted for + // nothing, and none of them is counted as a failure. + assertThat(stores.get()).isEqualTo(1); + assertThat(done.failed()).isZero(); + assertThat(rows.values().stream().filter(f -> f.getEncryptionKeyId() == null).count()) + .isEqualTo(29); + } + + @Test + void start_auditsTheAdminWhoTriggeredIt_onStartAndCompletion() throws Exception { + plaintextFile(1, false); + AuditService audit = mock(AuditService.class); + when(audit.captureCurrentPrincipal()).thenReturn("admin-alice"); + service = + new StorageEncryptionMigrationService( + fileRepo, + provider, + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + audit); + + service.start(); + awaitCompletion(); + + // Both ends of the run must name the admin: the completion event runs on a virtual thread + // with no security context, where the principal would otherwise resolve to "system". + verify(audit) + .audit( + eq("admin-alice"), + eq(AuditEventType.STORAGE_ENCRYPTION), + argThat(data -> "migration.started".equals(data.get("action")))); + verify(audit) + .audit( + eq("admin-alice"), + eq(AuditEventType.STORAGE_ENCRYPTION), + argThat(data -> "migration.completed".equals(data.get("action")))); + } + + // ---- one-shot backends (S3, database) ------------------------------------------------ + + /** + * Wraps the local provider so {@code load} hands back a stock {@link InputStreamResource}: a + * one-shot stream that refuses {@code contentLength()} once partially read, the way an S3 or + * database resource behaves. Local files are re-openable and never exercise that branch. + */ + private StorageProvider oneShotBacked() { + return new StorageProvider() { + @Override + public StoredObject store(User o, MultipartFile f) throws java.io.IOException { + return inner.store(o, f); + } + + @Override + public org.springframework.core.io.Resource load(String key) + throws java.io.IOException { + return new InputStreamResource(inner.load(key).getInputStream()); + } + + @Override + public void delete(String key) throws java.io.IOException { + inner.delete(key); + } + }; + } + + private void useProvider(StorageProvider backing) { + service = + new StorageEncryptionMigrationService( + fileRepo, + new EncryptingStorageProvider( + backing, + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP)), + StorageEncryptionState.of( + true, keyService, StorageEncryptionAuditListener.NOOP), + mock(AuditService.class)); + } + + @Test + void migrate_oneShotResource_fallsBackToTheRecordedSizeAndStillEncrypts() throws Exception { + StoredFile file = plaintextFile(1, false); + useProvider(oneShotBacked()); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.state()).isEqualTo(StorageEncryptionMigrationService.State.COMPLETED); + assertThat(done.processed()).isEqualTo(1); + assertThat(file.getEncryptionKeyId()).isNotNull(); + byte[] onDisk = Files.readAllBytes(tempDir.resolve(file.getStorageKey())); + assertThat(new String(onDisk, 0, 8, StandardCharsets.US_ASCII)).isEqualTo("SPDFEAR1"); + // Decrypting through a fresh load must return the original bytes, not a truncated stream. + try (var in = + new EncryptingStorageProvider(inner, keyService, true) + .load(file.getStorageKey()) + .getInputStream()) { + assertThat(in.readAllBytes()).isEqualTo(CONTENT); + } + } + + @Test + void migrate_oneShotResourceWithWrongRecordedSize_failsTheFileAndLeavesTheRowAlone() + throws Exception { + StoredFile file = plaintextFile(1, false); + String originalKey = file.getStorageKey(); + file.setSizeBytes(CONTENT.length + 7L); // stale/incorrect row size + useProvider(oneShotBacked()); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + // The size mismatch is caught inside store(), before the compare-and-swap, so the file is + // counted as failed and the row still points at its untouched plaintext blob. + assertThat(done.failed()).isEqualTo(1); + assertThat(done.processed()).isZero(); + assertThat(file.getEncryptionKeyId()).isNull(); + assertThat(file.getStorageKey()).isEqualTo(originalKey); + assertThat(Files.readAllBytes(tempDir.resolve(originalKey))).isEqualTo(CONTENT); + } + + @Test + void migrate_oneShotResourceWithNoRecordedSize_failsTheFileRatherThanGuessing() + throws Exception { + // Only the secondary blobs have a nullable size, so that is where "no size available at + // all" is reachable: neither the resource nor the row can say how long the plaintext is. + StoredFile file = plaintextFile(1, true); + String historyKey = file.getHistoryStorageKey(); + file.setHistorySizeBytes(null); + useProvider(oneShotBacked()); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.failed()).isEqualTo(1); + assertThat(done.processed()).isZero(); + assertThat(file.getEncryptionKeyId()).isNull(); + assertThat(file.getHistoryStorageKey()).isEqualTo(historyKey); + } + + private StorageEncryptionState flippableState(AtomicBoolean writeEnabled) { + return new StorageEncryptionState( + true, () -> keyService, null, StorageEncryptionAuditListener.NOOP) { + @Override + public boolean isWriteEnabled() { + return writeEnabled.get(); + } + }; + } + + @Test + void migrate_perFileFailure_countsAndContinues() throws Exception { + StoredFile broken = plaintextFile(1, false); + plaintextFile(2, false); + // Point file 1 at a missing blob so its migration throws. + broken.setStorageKey("1/does-not-exist"); + + service.start(); + StorageEncryptionMigrationService.MigrationStatus done = awaitCompletion(); + + assertThat(done.state()).isEqualTo(StorageEncryptionMigrationService.State.COMPLETED); + assertThat(done.failed()).isEqualTo(1); + assertThat(done.processed()).isEqualTo(1); + assertThat(rows.get(2L).getEncryptionKeyId()).isNotNull(); + } +} diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/workflow/service/WorkflowSessionServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/workflow/service/WorkflowSessionServiceTest.java index cf985d568e..184926574e 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/workflow/service/WorkflowSessionServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/workflow/service/WorkflowSessionServiceTest.java @@ -28,6 +28,7 @@ import stirling.software.common.model.ApplicationProperties.Storage; import stirling.software.common.model.ApplicationProperties.Storage.Signing; import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.storage.crypto.StorageKeyRevokedException; import stirling.software.proprietary.storage.model.StoredFile; import stirling.software.proprietary.storage.provider.StorageProvider; import stirling.software.proprietary.storage.provider.StoredObject; @@ -639,4 +640,46 @@ class WorkflowSessionServiceTest { assertThat(service.listUserSessions(owner)).isEmpty(); } + + // ------------------------------------------------------------------------- + // revoked encryption keys (storage encryption kill switch) + // ------------------------------------------------------------------------- + + @Test + void getOriginalFile_revokedEncryptionKey_isForbiddenNotServerError() throws Exception { + WorkflowSession session = new WorkflowSession(); + StoredFile original = new StoredFile(); + original.setStorageKey("1/original"); + session.setOriginalFile(original); + when(workflowSessionRepository.findBySessionId("s1")).thenReturn(Optional.of(session)); + when(storageProvider.load("1/original")) + .thenThrow(new StorageKeyRevokedException("key disabled")); + + assertThatThrownBy(() -> service.getOriginalFile("s1")) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + } + + @Test + void getSignRequestDocument_revokedEncryptionKey_isForbiddenNotServerError() throws Exception { + User participantUser = user("bob"); + WorkflowSession session = sessionWithParticipant("s2", pendingParticipant(participantUser)); + StoredFile original = new StoredFile(); + original.setStorageKey("1/sign-me"); + session.setOriginalFile(original); + when(storageProvider.load("1/sign-me")) + .thenThrow(new StorageKeyRevokedException("key disabled")); + + // This path wraps the read in catch(IOException) -> 500; the revocation must not be + // swallowed by it, because StorageKeyRevokedException IS an IOException. + assertThatThrownBy(() -> service.getSignRequestDocument("s2", participantUser)) + .isInstanceOf(ResponseStatusException.class) + .satisfies( + e -> + assertThat(((ResponseStatusException) e).getStatusCode()) + .isEqualTo(HttpStatus.FORBIDDEN)); + } } diff --git a/app/saas/build.gradle b/app/saas/build.gradle index 495f583a74..a954e6d05a 100644 --- a/app/saas/build.gradle +++ b/app/saas/build.gradle @@ -6,6 +6,11 @@ dependencies { implementation project(':common') implementation project(':proprietary') + // Markdown -> HTML for rendering versioned legal documents (agreement) to PDF via the + // shared FileToPdf/WeasyPrint path in :common. Same library the core Markdown-to-PDF tool uses. + implementation "org.commonmark:commonmark:$commonmarkVersion" + implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion" + api 'org.springframework.boot:spring-boot-starter-security' api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' diff --git a/app/saas/src/main/java/stirling/software/saas/accountlink/AccountLinkController.java b/app/saas/src/main/java/stirling/software/saas/accountlink/AccountLinkController.java index 490b3d5db1..646791f76e 100644 --- a/app/saas/src/main/java/stirling/software/saas/accountlink/AccountLinkController.java +++ b/app/saas/src/main/java/stirling/software/saas/accountlink/AccountLinkController.java @@ -152,7 +152,7 @@ public class AccountLinkController { if (rows.isEmpty()) { return new LeaderTeam(null, null, HttpStatus.FORBIDDEN); } - TeamMembership m = rows.get(0); + TeamMembership m = rows.getFirst(); if (m.getRole() != TeamRole.LEADER) { return new LeaderTeam(null, null, HttpStatus.FORBIDDEN); } diff --git a/app/saas/src/main/java/stirling/software/saas/config/MigrationOwnedSchemaFilter.java b/app/saas/src/main/java/stirling/software/saas/config/MigrationOwnedSchemaFilter.java new file mode 100644 index 0000000000..4b2496bf15 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/config/MigrationOwnedSchemaFilter.java @@ -0,0 +1,108 @@ +package stirling.software.saas.config; + +import org.hibernate.boot.model.relational.Namespace; +import org.hibernate.boot.model.relational.Sequence; +import org.hibernate.mapping.Table; +import org.hibernate.tool.schema.spi.SchemaFilter; +import org.hibernate.tool.schema.spi.SchemaFilterProvider; + +/** + * Hides the migration-owned tables from Hibernate's schema management. + * + *

Wired on the SaaS profile only, via {@code hibernate.hbm2ddl.schema_filter_provider}. + * Self-hosted is untouched: there Hibernate rightly owns everything. + * + *

Why a filter rather than simply turning {@code ddl-auto} off: the SaaS database has two + * writers. The Supabase migrations own the SaaS tables, and Hibernate owns roughly thirty tables + * inherited from the self-hosted app that no migration has ever created. Turn {@code ddl-auto} off + * and a fresh preview branch is missing that second half; leave it on and Hibernate is free to + * reconcile migration-owned tables, which is how {@code team_memberships.role} ended up widened to + * varchar(255) and needed a migration to put back. A filter keeps the first half working and makes + * the second impossible. + * + *

Note that this is a per-table filter, not a per-schema one. Hibernate's schema management runs + * over every mapped entity regardless of namespace, so moving SaaS tables to their own schema would + * not by itself keep Hibernate out of them. {@link SaasSchemaOwnership} is the register; this class + * only applies it. + * + *

Foreign keys still cross the line, on purpose. Several inherited tables reference + * migration-owned ones — {@code folders}, {@code stored_files} and {@code file_shares} all point at + * {@code users}/{@code teams}. Hibernate's {@code SchemaCreatorImpl.createForeignKeys} and {@code + * AbstractSchemaMigrator.applyForeignKeys} check {@code includeTable} against the *owning* table + * only and then emit every foreign key on it, without consulting the referenced table. So excluding + * {@code users} does not cost the branch its referential integrity, and a branch ends up matching + * staging. It does mean the referenced tables have to exist by the time Hibernate runs, which holds + * because a Supabase branch applies its migrations at build time and the app connects afterwards. + * + *

Known gap: this cannot detect drift. Filtering means Hibernate never inspects these + * tables, and {@link #getValidateFilter()} extends that to {@code validate}, so nothing here + * compares a migration-owned table against its entity. Combined with the register being a + * hand-maintained list of another repo's contents (see {@link SaasSchemaOwnership}), there is + * currently no automated signal when the register, the entities and the database disagree. That is + * a deliberate trade for a boot that does not fail on differences we accept, not a claim that drift + * cannot happen; a non-fatal drift report is the missing piece and belongs outside this class. + */ +public class MigrationOwnedSchemaFilter implements SchemaFilterProvider, SchemaFilter { + + /** + * The one decision this class makes. Everything Hibernate might do to a table it does not own — + * create, alter, drop, truncate — is refused. + */ + @Override + public boolean includeTable(Table table) { + return !SaasSchemaOwnership.isMigrationOwned(table.getName()); + } + + /** + * Namespaces are never filtered. The inherited tables and the migration-owned ones share {@code + * stirling_pdf}, so excluding the namespace would take both with it. + */ + @Override + public boolean includeNamespace(Namespace namespace) { + return true; + } + + /** + * Sequences are left alone. Every id here is an identity column rather than a mapped generator, + * so there is nothing for Hibernate to create; filtering them would be dead code pretending to + * be a safeguard. + */ + @Override + public boolean includeSequence(Sequence sequence) { + return true; + } + + @Override + public SchemaFilter getCreateFilter() { + return this; + } + + @Override + public SchemaFilter getMigrateFilter() { + return this; + } + + @Override + public SchemaFilter getDropFilter() { + return this; + } + + @Override + public SchemaFilter getTruncatorFilter() { + return this; + } + + /** + * Validation is filtered too, which is the one debatable call here. + * + *

Letting it through would give a useful signal when a migration-owned table drifts from its + * entity. But {@code ddl-auto=validate} fails startup, and it would fail on differences we have + * deliberately accepted — {@code ai_create_sessions} carries columns from a reverted feature + * that nothing maps, for instance. A boot failure over a table we have chosen not to manage is + * noise, so the rule stays uniform: Hibernate does not concern itself with these tables at all. + */ + @Override + public SchemaFilter getValidateFilter() { + return this; + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java index cd991cc44e..612c6e3171 100644 --- a/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java @@ -20,7 +20,8 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; "stirling.software.saas.ai.repository", "stirling.software.saas.payg.repository", "stirling.software.saas.payg.bundle", - "stirling.software.saas.procurement.repository" + "stirling.software.saas.procurement.repository", + "stirling.software.saas.legal" }) @EntityScan({ "stirling.software.saas.accountlink", @@ -28,6 +29,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories; "stirling.software.saas.billing.model", "stirling.software.saas.ai.model", "stirling.software.saas.payg", - "stirling.software.saas.procurement.model" + "stirling.software.saas.procurement.model", + "stirling.software.saas.legal" }) public class SaasJpaConfig {} diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java b/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java new file mode 100644 index 0000000000..39305fcbcd --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasProjectNotice.java @@ -0,0 +1,53 @@ +package stirling.software.saas.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +/** Logs which Supabase project this backend is talking to, and its schema policy. */ +@Slf4j +@Component +@Profile({"dev", "staging"}) +public class SaasProjectNotice { + + private final Environment environment; + private final String projectRef; + private final String ddlAuto; + + public SaasProjectNotice( + Environment environment, + @Value("${app.supabase.project-ref:unknown}") String projectRef, + @Value("${spring.jpa.hibernate.ddl-auto:none}") String ddlAuto) { + this.environment = environment; + this.projectRef = projectRef; + this.ddlAuto = ddlAuto; + } + + @EventListener(ApplicationReadyEvent.class) + public void announceProject() { + boolean staging = environment.matchesProfiles("staging"); + if (staging) { + log.info( + """ + SaaS staging profile: Supabase project {}, ddl-auto={}. This is the SHARED \ + long-lived environment, so its data and schema are not yours alone. Testing an \ + open SaaS PR? Use that PR's preview branch instead \ + (SAAS_DEV_PROJECT_REF in app/.env.saas.local); staging will not have its \ + migrations.\ + """, + projectRef, + ddlAuto); + return; + } + log.info( + "SaaS dev profile: Supabase preview branch {}, ddl-auto={}. Disposable, so Hibernate" + + " is allowed to add the inherited tables the migrations do not create.", + projectRef, + ddlAuto); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/config/SaasSchemaOwnership.java b/app/saas/src/main/java/stirling/software/saas/config/SaasSchemaOwnership.java new file mode 100644 index 0000000000..dffd1eccf1 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/config/SaasSchemaOwnership.java @@ -0,0 +1,117 @@ +package stirling.software.saas.config; + +import java.util.Set; + +/** + * Which side owns each table in the SaaS database. + * + *

The SaaS schema has two writers and always has: the Supabase migrations in the + * Stirling-PDF-SaaS repo, and Hibernate's {@code ddl-auto}. That was a convention rather than a + * rule, and it leaked twice. An older {@code ddl-auto} run widened {@code team_memberships.role} to + * varchar(255), which needed a dedicated migration to repair because RLS policies depended on the + * column. Separately {@code payg_instance_usage} went months with an entity and no migration, so it + * simply did not exist on a fresh preview branch. + * + *

This class makes the boundary explicit and {@code SaasSchemaOwnershipTest} makes it binding: + * every {@code @Entity} the SaaS app maps must appear in exactly one of these two sets. A new + * entity fails the build until someone states who owns its table, which is the decision that was + * previously made by accident. + * + *

{@link MigrationOwnedSchemaFilter} enforces it at runtime: Hibernate is never shown the + * migration-owned tables, so it cannot create, alter or drop them whatever {@code ddl-auto} says. + * Inherited tables stay under Hibernate, so a preview branch built from migrations alone still + * heals itself on first boot. + * + *

What this does not catch. The register is a hand-maintained copy of what lives in + * another repository, and only one direction is enforced. The test fails when a *new* entity + * appears with no owner. It cannot notice a table changing sides: write a migration for {@code + * folders} in Stirling-PDF-SaaS and nothing here changes, the test still passes, and Hibernate + * carries on managing a table the migrations now own — which is precisely how {@code + * team_memberships.role} got widened. Adding a migration for anything in {@link #HIBERNATE_MANAGED} + * therefore means moving it to {@link #MIGRATION_OWNED} in the same change; nothing will remind + * you. Making that structural rather than remembered is what moving the SaaS tables into their own + * schema would buy, and is the reason this class is a stepping stone rather than the answer. + */ +public final class SaasSchemaOwnership { + + /** + * Created and altered by the Supabase migrations. Hibernate must not touch these: the + * migrations carry constraints, defaults and RLS policies it knows nothing about and would + * reconcile away. + */ + public static final Set MIGRATION_OWNED = + Set.of( + "ai_create_sessions", + "audit_events", + "authorities", + "billing_subscriptions", + "job_artifact_hash", + "legal_consent", + "linked_instance", + "payg_instance_usage", + "payg_meter_event_log", + "payg_prepaid_bundle", + "payg_shadow_charge", + "payg_team_extensions", + "persistent_logins", + "pricing_policy", + "processing_job", + "processing_job_step", + "procurement_agreement_signature", + "procurement_deal", + "procurement_quote", + "saas_team_extensions", + "saas_user_extensions", + "sessions", + "team_invitations", + "team_memberships", + "teams", + "users", + "wallet_entitlement_snapshot", + "wallet_ledger", + "wallet_policy"); + + /** + * Inherited from the self-hosted app, where {@code ddl-auto} owns the schema and no Supabase + * migration exists. Deliberately left under Hibernate so a fresh branch gets them on first + * boot. + */ + public static final Set HIBERNATE_MANAGED = + Set.of( + "account_link_device_credential", + "account_link_metered_signature", + "account_link_sync_state", + "account_link_usage_counter", + "api_key_daily_usage", + "api_keys", + "file_encryption_keys", + "file_run_events", + "file_share_accesses", + "file_shares", + "folders", + "integration_configs", + "invite_tokens", + "jwt_signing_keys", + "policies", + "policy_assets", + "policy_completed_migrations", + "policy_processed_files", + "policy_source_doc_counts", + "policy_source_doc_totals", + "policy_sources", + "resource_grants", + "storage_cleanup_entries", + "stored_file_blobs", + "stored_files", + "user_license_settings", + "user_server_certificates", + "workflow_participants", + "workflow_sessions"); + + private SaasSchemaOwnership() {} + + /** Case-insensitive: Hibernate hands us whatever casing the mapping used. */ + public static boolean isMigrationOwned(String tableName) { + return tableName != null && MIGRATION_OWNED.contains(tableName.toLowerCase()); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java new file mode 100644 index 0000000000..5698133fe8 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsent.java @@ -0,0 +1,62 @@ +package stirling.software.saas.legal; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * An append-only record that a user accepted a versioned legal document at a particular moment in + * the product. Distinct from a signed agreement (which is a negotiated, signature-bearing artifact, + * see {@code ProcurementAgreementSignature}); this captures the lighter clickwrap consents — the + * EULA accepted at trial start and at quote generation — with the exact document version, so what + * was agreed is auditable even after the document versions up. + */ +@Entity +@Table(name = "legal_consent") +@NoArgsConstructor +@Getter +@Setter +public class LegalConsent implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "consent_id") + private Long consentId; + + @Column(name = "team_id") + private Long teamId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "document_id", nullable = false, length = 64) + private String documentId; + + @Column(name = "document_version", nullable = false, length = 32) + private String documentVersion; + + // Where in the product the consent was given: "trial", "quote", etc. + @Column(name = "context", nullable = false, length = 32) + private String context; + + @Column(name = "signer_ip", length = 64) + private String signerIp; + + @CreationTimestamp + @Column(name = "consented_at", nullable = false, updatable = false) + private LocalDateTime consentedAt; +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java new file mode 100644 index 0000000000..9d79640cd0 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentRepository.java @@ -0,0 +1,5 @@ +package stirling.software.saas.legal; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LegalConsentRepository extends JpaRepository {} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java new file mode 100644 index 0000000000..c6377f35b7 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalConsentService.java @@ -0,0 +1,47 @@ +package stirling.software.saas.legal; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** Records clickwrap consents to versioned legal documents (see {@link LegalConsent}). */ +@Slf4j +@Service +@Profile("saas") +@RequiredArgsConstructor +public class LegalConsentService { + + private final LegalDocumentRegistry registry; + private final LegalConsentRepository consents; + + /** + * Record that the given user accepted the current version of {@code documentId} in {@code + * context} (e.g. "trial", "quote"). No-op for an unknown document. Best-effort: callers treat a + * failure as non-fatal so it never blocks the flow the consent accompanies. + */ + @Transactional + public void record(Long teamId, Long userId, String documentId, String context, String ip) { + LegalDocumentMeta meta = registry.meta(documentId).orElse(null); + if (meta == null) { + log.warn("[legal] consent for unknown document '{}' ignored", documentId); + return; + } + LegalConsent consent = new LegalConsent(); + consent.setTeamId(teamId); + consent.setUserId(userId); + consent.setDocumentId(meta.id()); + consent.setDocumentVersion(meta.version()); + consent.setContext(context); + consent.setSignerIp(ip); + consents.save(consent); + log.info( + "[legal] consent recorded team={} doc={} v{} context={}", + teamId, + meta.id(), + meta.version(), + context); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java new file mode 100644 index 0000000000..308b3e99bd --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalController.java @@ -0,0 +1,133 @@ +package stirling.software.saas.legal; + +import java.util.Optional; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Hidden; + +import jakarta.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.TeamMembership; +import stirling.software.proprietary.security.database.repository.UserRepository; +import stirling.software.proprietary.security.model.User; +import stirling.software.proprietary.security.repository.TeamMembershipRepository; +import stirling.software.saas.util.AuthenticationUtils; + +/** + * Serves the versioned legal documents (EULA, SLA exhibit, subprocessors) for in-product viewing, + * and records the lighter clickwrap consents. The enterprise agreement itself is served + signed + * through the procurement controller, since it needs a quote to fill its Order Form. + */ +@Slf4j +@Hidden +@RestController +@RequestMapping("/api/v1/legal") +@Profile("saas") +@RequiredArgsConstructor +public class LegalController { + + private final LegalDocumentRegistry registry; + private final LegalConsentService consents; + private final TeamMembershipRepository memberRepo; + private final UserRepository userRepository; + + /** A legal document rendered for viewing: registry metadata + the static markdown body. */ + public record LegalDocumentResponse( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown) {} + + public record ConsentRequest(String documentId, String context) {} + + /** Fetch a legal document's current version as markdown. 404 for an unknown document. */ + @GetMapping("/{docId}") + @PreAuthorize("isAuthenticated()") + public ResponseEntity document(@PathVariable String docId) { + return registry.meta(docId) + .>map( + meta -> + ResponseEntity.ok( + new LegalDocumentResponse( + meta.id(), + meta.version(), + meta.versionLabel(), + meta.displayName(), + meta.effectiveDate(), + meta.status(), + registry.staticMarkdown(docId)))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Record a clickwrap consent (e.g. the EULA accepted at trial start or quote generation). + * Best-effort — a teamless caller still returns 200 so the accompanying flow is never blocked. + */ + @PostMapping("/consent") + @PreAuthorize("isAuthenticated()") + public ResponseEntity consent( + @RequestBody ConsentRequest request, Authentication auth, HttpServletRequest http) { + if (request == null || request.documentId() == null || request.context() == null) { + return ResponseEntity.badRequest().build(); + } + Optional membership = primaryMembership(auth); + Long teamId = membership.map(m -> m.getTeam().getId()).orElse(null); + Long userId = membership.map(m -> m.getUser().getId()).orElse(null); + // Best-effort for real: consent is audit metadata, not an authorisation gate, so a failed + // write must not fail the trial start or quote generation this call accompanies. Previously + // that only held because the caller happened to swallow the 500. + try { + consents.record( + teamId, userId, request.documentId(), request.context(), clientIp(http)); + } catch (RuntimeException e) { + log.warn( + "[legal] consent not recorded doc={} context={}: {}", + request.documentId(), + request.context(), + e.getMessage()); + } + return ResponseEntity.ok().build(); + } + + private Optional primaryMembership(Authentication auth) { + User user; + try { + user = AuthenticationUtils.getCurrentUser(auth, userRepository); + } catch (SecurityException e) { + return Optional.empty(); + } + return memberRepo.findPrimaryMembership(user.getId()).stream().findFirst(); + } + + /** + * Best guess at the caller's address, for the audit record. + * + *

Informational only, and must stay that way: the first {@code X-Forwarded-For} hop is set + * by the client, so a stored address is trivially spoofable and is not evidence of where a + * consent or signature came from. Treat it as a hint when reconstructing events, never as + * proof. + */ + private static String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + return forwarded.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java new file mode 100644 index 0000000000..9f35e42053 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentMeta.java @@ -0,0 +1,27 @@ +package stirling.software.saas.legal; + +import java.util.List; + +/** + * One legal document's registry entry, as declared in {@code legal/manifest.json}. Immutable + * snapshot loaded at startup by {@link LegalDocumentRegistry}. + * + *

{@code parts} lists the pieces, in render order, that make up the document. A plain entry + * (e.g. {@code "msa.md"}) is a static markdown file under {@code legal///}; an entry + * prefixed with {@code "@"} (e.g. {@code "@order-form"}) is a dynamic section that a document + * assembler generates at render time. + */ +public record LegalDocumentMeta( + String id, + String label, + String displayName, + String version, + String effectiveDate, + String status, + List parts) { + + /** Fully-qualified version label shown to users and stored on signatures, e.g. "SEA v0.9.1". */ + public String versionLabel() { + return label + " v" + version; + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java new file mode 100644 index 0000000000..ffbe030863 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/legal/LegalDocumentRegistry.java @@ -0,0 +1,151 @@ +package stirling.software.saas.legal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.annotation.PostConstruct; + +import lombok.extern.slf4j.Slf4j; + +/** + * Loads the versioned legal-document registry from {@code legal/manifest.json} on startup and + * serves document metadata + rendered markdown from the classpath. + * + *

Publishing a new version of any document is a content-only change: drop the markdown under + * {@code legal///} and bump that document's {@code version} in the manifest — no + * code change. Signatures pin the exact {@code {id, version, contentHash}} they were signed against + * (see the procurement agreement flow), so historical documents stay reproducible. + * + *

Token slots of the form {{name}} in the markdown are filled at render time. This + * registry fills the document-level common tokens ({@code version}, {@code version_date}, {@code + * subprocessor_url}, {@code eula_url}); callers that need per-quote tokens (the enterprise + * agreement's Order Form) fill the rest. + */ +@Slf4j +@Service +public class LegalDocumentRegistry { + + private static final String MANIFEST = "legal/manifest.json"; + private static final Pattern TOKEN = Pattern.compile("\\{\\{\\s*([a-zA-Z0-9_]+)\\s*}}"); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final Map documents = new LinkedHashMap<>(); + private String subprocessorUrl = ""; + private String eulaUrl = ""; + + @PostConstruct + void load() throws IOException { + JsonNode root; + try (InputStream in = new ClassPathResource(MANIFEST).getInputStream()) { + root = objectMapper.readTree(in); + } + subprocessorUrl = root.path("subprocessorUrl").asText(""); + eulaUrl = root.path("eulaUrl").asText(""); + JsonNode docs = root.path("documents"); + docs.fieldNames() + .forEachRemaining( + id -> { + JsonNode d = docs.get(id); + List parts = + objectMapper.convertValue( + d.path("parts"), + objectMapper + .getTypeFactory() + .constructCollectionType( + List.class, String.class)); + documents.put( + id, + new LegalDocumentMeta( + id, + d.path("label").asText(id), + d.path("displayName").asText(id), + d.path("version").asText("0"), + d.path("effectiveDate").asText(""), + d.path("status").asText("draft"), + parts == null ? List.of() : parts)); + }); + log.info("[legal] loaded {} document(s) from {}", documents.size(), MANIFEST); + } + + public Optional meta(String docId) { + return Optional.ofNullable(documents.get(docId)); + } + + public String subprocessorUrl() { + return subprocessorUrl; + } + + public String eulaUrl() { + return eulaUrl; + } + + /** Document-level tokens available to every document (before any per-quote tokens). */ + public Map commonTokens(LegalDocumentMeta meta) { + Map t = new LinkedHashMap<>(); + t.put("version", meta.version()); + t.put("version_date", meta.effectiveDate()); + t.put("subprocessor_url", subprocessorUrl); + t.put("eula_url", eulaUrl); + return t; + } + + /** Read one static markdown part of a document from the classpath. */ + public String readPart(LegalDocumentMeta meta, String partFile) { + String path = "legal/" + meta.id() + "/" + meta.version() + "/" + partFile; + try (InputStream in = new ClassPathResource(path).getInputStream()) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Missing legal document part: " + path, e); + } + } + + /** + * The concatenated static parts of a document (dynamic {@code @}-parts skipped), with only the + * common tokens filled. Use for fully-static documents (EULA, SLA, subprocessors). + */ + public String staticMarkdown(String docId) { + LegalDocumentMeta meta = + meta(docId) + .orElseThrow( + () -> new IllegalArgumentException("Unknown document: " + docId)); + Map tokens = commonTokens(meta); + StringBuilder sb = new StringBuilder(); + for (String part : meta.parts()) { + if (part.startsWith("@")) continue; // dynamic section — not part of the static body + if (sb.length() > 0) sb.append("\n\n"); + sb.append(fill(readPart(meta, part), tokens)); + } + return sb.toString(); + } + + /** Replace {@code {{token}}} slots; unknown tokens are left intact so gaps are visible. */ + public static String fill(String markdown, Map tokens) { + Matcher m = TOKEN.matcher(markdown); + StringBuilder out = new StringBuilder(); + while (m.find()) { + String key = m.group(1); + String value = tokens.get(key); + m.appendReplacement( + out, + value == null + ? Matcher.quoteReplacement(m.group(0)) + : Matcher.quoteReplacement(value)); + } + m.appendTail(out); + return out.toString(); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/model/SaasUserExtensions.java b/app/saas/src/main/java/stirling/software/saas/model/SaasUserExtensions.java index 036fdd27be..8d7dda1726 100644 --- a/app/saas/src/main/java/stirling/software/saas/model/SaasUserExtensions.java +++ b/app/saas/src/main/java/stirling/software/saas/model/SaasUserExtensions.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.UpdateTimestamp; +import org.springframework.data.domain.Persistable; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -16,6 +17,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.MapsId; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import jakarta.persistence.Transient; import lombok.Getter; import lombok.NoArgsConstructor; @@ -37,7 +39,7 @@ import stirling.software.proprietary.security.model.User; @NoArgsConstructor @Getter @Setter -public class SaasUserExtensions implements Serializable { +public class SaasUserExtensions implements Serializable, Persistable { private static final long serialVersionUID = 1L; @@ -80,4 +82,17 @@ public class SaasUserExtensions implements Serializable { public boolean isMeteredBillingEnabled() { return Boolean.TRUE.equals(hasMeteredBillingEnabled); } + + @Override + public Long getId() { + return userId; + } + + // Decided on the timestamp, not the id: the constructor pre-sets the @MapsId id, so an + // id-based check would route a new row to merge() and fail with "null identifier". + @Override + @Transient + public boolean isNew() { + return createdAt == null; + } } diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygInvoicesController.java b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygInvoicesController.java index 80c20902ce..ad48a03743 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygInvoicesController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygInvoicesController.java @@ -105,7 +105,7 @@ public class PaygInvoicesController { if (rows.isEmpty()) { return ResponseEntity.ok(List.of()); } - Long teamId = rows.get(0).getTeam().getId(); + Long teamId = rows.getFirst().getTeam().getId(); // No PAYG extension row OR no Stripe customer id → team has never subscribed → no // invoices. Empty list, not 404 — the UI distinguishes "no invoices yet" from a diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygPaymentMethodController.java b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygPaymentMethodController.java index 89ab18230f..22a84eb489 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygPaymentMethodController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygPaymentMethodController.java @@ -85,7 +85,7 @@ public class PaygPaymentMethodController { if (rows.isEmpty()) { return ResponseEntity.ok(PaymentMethodResponse.absent()); } - Long teamId = rows.get(0).getTeam().getId(); + Long teamId = rows.getFirst().getTeam().getId(); Optional ext = extRepo.findById(teamId); if (ext.isEmpty() || ext.get().getStripeCustomerId() == null) { diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java index 2ab19ad4cb..ffc4a056fc 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java @@ -1,5 +1,6 @@ package stirling.software.saas.payg.api; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -206,6 +207,12 @@ public class PaygWalletController { // Prepaid while pools still have units to draw; once exhausted the meter is live again. String billingMode = prepaidRemaining > 0 ? BILLING_MODE_PREPAID : BILLING_MODE_PAYG; + // Per-credit rate for the bundle calculator — the bundle:processor price, NOT the metered + // per-doc rate. Resolved in the team's currency (USD fallback), null when the price is + // unsynced. + BigDecimal bundleRatePerCreditMinor = + billingService.resolveBundleRatePerCreditMinor(billing.currency()); + WalletSnapshotResponse body = new WalletSnapshotResponse( teamId, @@ -234,7 +241,8 @@ public class PaygWalletController { prepaidRemaining, prepaidTotal, prepaidExpiresAt, - billingMode); + billingMode, + bundleRatePerCreditMinor); return ResponseEntity.ok(body); } @@ -419,7 +427,7 @@ public class PaygWalletController { private Optional primaryMembership(Long userId) { List rows = memberRepo.findPrimaryMembership(userId); - return rows.isEmpty() ? Optional.empty() : Optional.of(rows.get(0)); + return rows.isEmpty() ? Optional.empty() : Optional.of(rows.getFirst()); } private List buildMemberRows( @@ -512,6 +520,7 @@ public class PaygWalletController { 0L, 0L, null, - BILLING_MODE_PAYG); + BILLING_MODE_PAYG, + null); } } diff --git a/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java b/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java index 3d381ef165..8bf887a0ea 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/api/WalletSnapshotResponse.java @@ -55,6 +55,11 @@ import java.util.List; * @param members leader-only roster of team members + their per-member sub-caps. Empty for member * callers. * @param recent latest wallet-ledger entries (newest first) for the activity feed. + * @param bundleRatePerCreditMinor per-credit rate of the prepaid-bundle Stripe Price (lookup key + * {@code bundle:processor}) in minor units of {@code currency} (may be fractional); {@code + * null} when unresolved. The in-app bundle calculator multiplies its pool by this so its + * estimate matches the checkout edge fn's charge. Distinct from {@code pricePerDocMinor} (the + * metered per-document rate) — the two must not be conflated. */ public record WalletSnapshotResponse( Long teamId, @@ -83,7 +88,8 @@ public record WalletSnapshotResponse( long prepaidUnitsRemaining, long prepaidUnitsTotal, String prepaidExpiresAt, - String billingMode) { + String billingMode, + BigDecimal bundleRatePerCreditMinor) { // Prepaid usage bundles, aggregated across the team's in-term pools (drawn ahead of the meter, // outside the spend cap): diff --git a/app/saas/src/main/java/stirling/software/saas/payg/billing/TeamBillingService.java b/app/saas/src/main/java/stirling/software/saas/payg/billing/TeamBillingService.java index 260efbd9ab..8bca25758a 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/billing/TeamBillingService.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/billing/TeamBillingService.java @@ -68,6 +68,14 @@ public class TeamBillingService { */ private static final String PAYG_LOOKUP_KEY = "plan:processor"; + /** + * Stripe Price {@code lookup_key} for the prepaid-bundle price — the per-credit rate the bundle + * calculator prices its pool at. A DIFFERENT price from {@link #PAYG_LOOKUP_KEY} (the metered + * per-document rate); the two must not be conflated, or the in-app estimate diverges from the + * amount the checkout edge fn actually charges (which bills against this same price). + */ + private static final String BUNDLE_LOOKUP_KEY = "bundle:processor"; + private final PaygTeamExtensionsRepository extensionsRepository; private final WalletPolicyRepository walletPolicyRepository; private final PricingPolicyService pricingPolicyService; @@ -254,6 +262,23 @@ public class TeamBillingService { .longValue()); } + /** + * Per-credit rate of the prepaid-bundle Stripe Price (lookup key {@code bundle:processor}) in + * {@code currency} (USD fallback) — the rate the in-app bundle calculator multiplies its pool + * by, so its estimate matches the amount the checkout edge fn charges (which bills the pool + * against this same price). Distinct from the metered {@code perDocMinor}; a bundle credit is + * one size-scaled run, priced per {@code unit_amount} of the bundle price. {@code null} when + * the rate can't be resolved (stripe schema absent, price unsynced) — the calculator then hides + * the figure and defers to the server total. + */ + public BigDecimal resolveBundleRatePerCreditMinor(String currency) { + return subscriptionDao + .findRateByLookupKey( + BUNDLE_LOOKUP_KEY, currency != null ? currency : DISPLAY_CURRENCY) + .map(StripeSubscriptionDao.PriceRate::perDocMinor) + .orElse(null); + } + /** * Inclusive-start / exclusive-end window for the calendar month — the monthly billing window * used when there's no Stripe subscription period to anchor on. diff --git a/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java b/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java index 2fc175f224..1310d2ea97 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/charge/JobChargeService.java @@ -324,7 +324,7 @@ public class JobChargeService { List paths = inputs.stream().map(JobInput::path).toList(); DocumentMetrics metrics = multiparts.size() == 1 - ? classifier.classify(multiparts.get(0), paths.get(0), policy) + ? classifier.classify(multiparts.getFirst(), paths.getFirst(), policy) : classifier.classify(multiparts, paths, policy); // Apply the policy-level minChargeUnits floor per design § 3.4. The classifier returns // raw docUnits with a "non-empty input → ≥1" floor; the charge formula's diff --git a/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java b/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java index 09df6ed12f..2a92d941bf 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/lineage/JpaJobLineageStore.java @@ -75,7 +75,7 @@ public class JpaJobLineageStore implements JobLineageStore { List matches = hashRepository.findOpenJobsForSignatures( userId, JobStatus.OPEN, since, storageKeys, Limit.of(1)); - return matches.isEmpty() ? Optional.empty() : Optional.of(matches.get(0)); + return matches.isEmpty() ? Optional.empty() : Optional.of(matches.getFirst()); } @Override diff --git a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java index d28b0fd09d..c9cda11831 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java @@ -123,9 +123,11 @@ public class PricingPolicy implements Serializable { @Column(name = "bundle_stripe_price_id", length = 128) private String bundleStripePriceId; - /** Stripe coupon id applying the 12-for-10 prepaid discount. Null = bundles not offered. */ - @Column(name = "bundle_coupon_id", length = 128) - private String bundleCouponId; + // No bundle_coupon_id field: the 12-for-10 discount is minted per-quote as an inline amount_off + // coupon by the create-payg-bundle-quote edge fn (computed from the bundle Price), so the + // pre-made percent coupon this policy used to carry is no longer consulted by anything. The + // column still exists (payg_get_bundle_pricing returns it) and is dropped in a later cleanup; + // ddl-auto=update never drops columns, so removing the mapping here is safe. /** * Exactly one row in the table has {@code is_default = true}; enforced by partial unique idx. diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java b/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java index d67a0fddbf..1c495b3afd 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/api/ProcurementController.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletRequest; + import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.enumeration.TeamRole; @@ -30,6 +32,8 @@ import stirling.software.proprietary.security.database.repository.UserRepository import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamMembershipRepository; import stirling.software.saas.procurement.config.ProcurementConfigurationProperties; +import stirling.software.saas.procurement.legal.AgreementSigning; +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; import stirling.software.saas.procurement.model.ProcurementDeal; import stirling.software.saas.procurement.model.ProcurementQuote; import stirling.software.saas.procurement.model.QuoteDetails; @@ -178,7 +182,17 @@ public class ProcurementController { String taxId) {} /** Trial setup captured before the trial starts: deployment target + seat count. */ - public record StartTrialRequest(String deployment, int users) {} + /** + * Setup step 2 collects the buying entity; all of it is optional so an older client still + * starts. + */ + public record StartTrialRequest( + String deployment, + int users, + String businessName, + String contactName, + String contactEmail, + String inviteEmails) {} public record SnapshotResponse( Long dealId, @@ -190,8 +204,33 @@ public class ProcurementController { int trialExtensionsUsed, boolean licensed, String licenseKey, + // Version label of the signed agreement PDF available for download, else null. + String agreementSignedVersion, + // Buying entity captured at trial setup; null on deals started before that step. + String businessName, + String contactName, + String contactEmail, QuoteResponse latestQuote) {} + /** The filled agreement for review: registry metadata + the rendered markdown body. */ + public record AgreementDocumentResponse( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown) {} + + /** Buyer-supplied signing inputs from the agreement stage. */ + public record SignAgreementRequest( + String customerLegalName, + String signatoryName, + String signatoryTitle, + boolean authorityConfirmed) {} + + public record SignAgreementResponse(Long signatureId, String versionLabel, boolean pdfStored) {} + // ---- endpoints ---------------------------------------------------------- /** @@ -213,7 +252,8 @@ public class ProcurementController { } private static final SnapshotResponse EMPTY_SNAPSHOT = - new SnapshotResponse(null, null, null, 0, null, null, 0, false, null, null); + new SnapshotResponse( + null, null, null, 0, null, null, 0, false, null, null, null, null, null, null); /** * Download the offline / air-gapped licence file (.lic) for the team — available for an @@ -239,6 +279,15 @@ public class ProcurementController { .orElseGet(() -> ResponseEntity.notFound().build()); } + /** Mark the account as looking at enterprise. Idempotent; never disturbs an existing deal. */ + @PostMapping("/interest") + @PreAuthorize("isAuthenticated()") + public ResponseEntity recordInterest(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return ResponseEntity.ok(toSnapshot(procurement.recordInterest(teamId), true)); + } + @PostMapping("/trial/start") @PreAuthorize("isAuthenticated()") public ResponseEntity startTrial( @@ -248,8 +297,30 @@ public class ProcurementController { // Body is optional so an older client (no setup step) still starts a cloud trial. String deployment = request != null ? request.deployment() : null; int seats = request != null ? request.users() : 0; - return ResponseEntity.ok( - toSnapshot(procurement.startTrial(teamId, deployment, seats), true)); + ProcurementDeal deal; + try { + deal = + procurement.startTrial( + teamId, + deployment, + seats, + request != null ? request.businessName() : null, + request != null ? request.contactName() : null, + request != null ? request.contactEmail() : null, + request != null ? request.inviteEmails() : null); + } catch (IllegalStateException e) { + // Past the trial the deal holds a committed licence; restarting would replace it. + log.warn("[procurement] trial start rejected team={}: {}", teamId, e.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + // After the trial exists, so a rejected invite can never stop it starting. + if (request != null) { + procurement.sendTrialInvites( + teamId, + primaryMembership(auth).map(TeamMembership::getUser).orElse(null), + request.inviteEmails()); + } + return ResponseEntity.ok(toSnapshot(deal, true)); } @PostMapping("/trial/extend") @@ -270,8 +341,17 @@ public class ProcurementController { @RequestBody QuoteRequest request, Authentication auth) { Long teamId = requireLeader(auth); if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - return ResponseEntity.ok( - toQuote(procurement.buildQuote(teamId, request.toConfig(), request.toDetails()))); + try { + return ResponseEntity.ok( + toQuote( + procurement.buildQuote( + teamId, request.toConfig(), request.toDetails()))); + } catch (IllegalStateException e) { + // Below the minimum deal size, or the deal is already live. A client error, not a + // fault. + log.warn("[procurement] quote rejected team={}: {}", teamId, e.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } } // Issue + accept are Supabase edge functions (they own Stripe): issue-procurement-quote turns a @@ -293,6 +373,111 @@ public class ProcurementController { } } + /** + * The filled Stirling Enterprise Agreement (MSA + Order Form + DPA) for the team's current + * quote, as markdown, for the buyer to review before signing. 404 when there's no quote yet. + */ + @GetMapping("/agreement/document") + @PreAuthorize("isAuthenticated()") + public ResponseEntity agreementDocument(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .agreementDocument(teamId) + .>map( + a -> + ResponseEntity.ok( + new AgreementDocumentResponse( + a.docId(), + a.version(), + a.versionLabel(), + a.displayName(), + a.effectiveDate(), + a.status(), + a.markdown()))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Record a signed agreement: capture the typed legal name / signatory / title / authority, pin + * the exact document version + content hash + variable snapshot, and store the rendered PDF + * (best-effort). The caller then proceeds to accept the quote as before. + */ + @PostMapping("/agreement/sign") + @PreAuthorize("isAuthenticated()") + public ResponseEntity signAgreement( + @RequestBody SignAgreementRequest request, + Authentication auth, + HttpServletRequest http) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + if (request == null + || request.signatoryName() == null + || request.signatoryName().isBlank() + || !request.authorityConfirmed()) { + return ResponseEntity.badRequest().build(); + } + try { + ProcurementAgreementSignature sig = + procurement.signAgreement( + teamId, + new AgreementSigning( + request.customerLegalName(), + request.signatoryName(), + request.signatoryTitle(), + request.authorityConfirmed()), + clientIp(http)); + return ResponseEntity.ok( + new SignAgreementResponse( + sig.getSignatureId(), sig.getDocumentLabel(), sig.getPdf() != null)); + } catch (IllegalStateException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + } + + /** Download the stored signed-agreement PDF for the team. 404 if none was rendered/stored. */ + @GetMapping("/agreement/signature/pdf") + @PreAuthorize("isAuthenticated()") + public ResponseEntity signaturePdf(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .signedAgreementPdf(teamId) + .>map( + pdf -> + ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment;" + + " filename=\"stirling-enterprise-agreement.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdf)) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + /** + * Download the current (unsigned) agreement as a PDF — the document shown at the sign step. 404 + * when there's no quote yet or the render runtime is unavailable. + */ + @GetMapping("/agreement/document/pdf") + @PreAuthorize("isAuthenticated()") + public ResponseEntity agreementDocumentPdf(Authentication auth) { + Long teamId = requireLeader(auth); + if (teamId == null) return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + return procurement + .agreementDocumentPdf(teamId) + .>map( + pdf -> + ResponseEntity.ok() + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment;" + + " filename=\"stirling-enterprise-agreement.pdf\"") + .contentType(MediaType.APPLICATION_PDF) + .body(pdf)) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + /** * Provision on accept: upgrade the team's licence to the committed annual term, valid * immediately. Called server-side by the accept edge function (ROLE_ADMIN via X-API-Key) once @@ -311,9 +496,38 @@ public class ProcurementController { } } + /** + * Go live once payment settles: advance the deal to active and re-affirm the annual licence. + * Called server-side by the {@code invoice.paid} webhook (ROLE_ADMIN via X-API-Key), alongside + * {@code /provision}, which runs earlier at accept and deliberately leaves the stage alone. + * + *

Answers 200 when the team has no deal at all, rather than erroring: a committed + * subscription can be closed directly in Stripe by sales with no portal deal behind it, and a + * non-2xx would have Stripe retry a webhook that can never succeed. + * + *

{@code invoiceId} is what makes this idempotent without swallowing renewals: the same + * invoice twice is a redelivery, a different one is next year's payment and has to re-issue the + * licence. Optional so an older caller still works, at the cost of that distinction. + */ + @PostMapping("/activate") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity activate( + @RequestParam("teamId") long teamId, + @RequestParam(value = "invoiceId", required = false) String invoiceId) { + try { + procurement.markLive(teamId, invoiceId); + } catch (IllegalStateException e) { + log.info( + "[procurement] activate skipped, no deal for team={}: {}", + teamId, + e.getMessage()); + } + return ResponseEntity.ok().build(); + } + /** * Demo/manual stand-in for the {@code invoice.paid} webhook: mark the deal live (issue the - * annual licence, advance to active). The real go-live is webhook-driven once payment settles. + * annual licence, advance to active). Production go-live runs through {@code /activate}. */ @PostMapping("/go-live") @PreAuthorize("isAuthenticated()") @@ -360,6 +574,21 @@ public class ProcurementController { .orElse(null); } + /** + * Best-effort client IP for the signature record: first X-Forwarded-For hop, else the peer. + * + *

Informational only. That header is client-set, so {@code signer_ip} is spoofable and is + * not evidence of where a signature came from — the document hash and version are what make the + * record trustworthy. Treat the address as a hint, never as proof. + */ + private static String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + return forwarded.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } + /** * Build the snapshot for a deal. {@code includeLicenseKey} is true only for the team leader; a * member sees {@code licensed} but not the key itself (see {@link #snapshot}). Mutation @@ -381,6 +610,10 @@ public class ProcurementController { deal.getTrialExtensionsUsed(), deal.getLicenseRef() != null, includeLicenseKey ? deal.getLicenseRef() : null, + procurement.signedAgreementLabel(deal.getDealId()).orElse(null), + deal.getBusinessName(), + deal.getContactName(), + deal.getContactEmail(), latest); } diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java b/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java index 98ec0cd573..ad745cf5bc 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/config/ProcurementConfigurationProperties.java @@ -30,4 +30,17 @@ public class ProcurementConfigurationProperties { * /go-live is a stand-in for the invoice.paid webhook and would let a leader activate unpaid. */ private boolean demoControlsEnabled = false; + + /** + * Smallest annual fee, in minor units, that may be quoted. The pricing curve has no natural + * floor — a small enough committed volume rounds the meter to zero — and every registered user + * is the leader of their own team, so without this any signup could price a $0 enterprise + * quote, accept it, and be provisioned a committed licence. 12_000_00 is USD 12,000/yr, the + * self-hosted deploy fee, chosen so the floor cannot sit below a line item the quote itself can + * contain. + * + *

This is a commercial number, not a technical one: set it to whatever the smallest + * enterprise deal you will actually sign is. Zero disables the check. + */ + private long minAnnualNetMinor = 12_000_00L; } diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java new file mode 100644 index 0000000000..c641f2e596 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementAssembler.java @@ -0,0 +1,278 @@ +package stirling.software.saas.procurement.legal; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.saas.legal.LegalDocumentMeta; +import stirling.software.saas.legal.LegalDocumentRegistry; +import stirling.software.saas.procurement.model.ProcurementQuote; +import stirling.software.saas.procurement.pricing.ProcurementPricingService; +import stirling.software.saas.procurement.pricing.QuoteConfig; +import stirling.software.saas.procurement.pricing.QuoteLineItem; + +/** + * Builds the full Stirling Enterprise Agreement for a specific quote: the static MSA (Part A) and + * DPA (Part C) from the {@link LegalDocumentRegistry}, with the dynamic Order Form (Part B) + * generated from the quote and slotted where the manifest's {@code @order-form} part sits. + * + *

Only the Order Form varies per deal; the MSA and DPA bodies are rendered verbatim with token + * substitution. The set of values used is returned as {@code variablesJson} so a signature can pin + * exactly what was rendered. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AgreementAssembler { + + public static final String DOC_ID = "enterprise-agreement"; + + private static final DateTimeFormatter DATE = + DateTimeFormatter.ofPattern("MMMM d, yyyy", Locale.US); + private static final String BLANK = "\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_"; + + private final LegalDocumentRegistry registry; + private final ProcurementPricingService pricing; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Render the agreement for a quote. {@code signing} is null for a preview (before signing) — + * the effective date and signature block then read as blanks / "On signature". + */ + public AssembledAgreement assemble(ProcurementQuote quote, AgreementSigning signing) { + LegalDocumentMeta meta = + registry.meta(DOC_ID) + .orElseThrow( + () -> + new IllegalStateException( + "Enterprise agreement not registered")); + + Map tokens = tokens(quote, signing, meta); + + StringBuilder md = new StringBuilder(); + for (String part : meta.parts()) { + if (md.length() > 0) md.append("\n\n"); + if ("@order-form".equals(part)) { + md.append(LegalDocumentRegistry.fill(orderForm(quote, tokens), tokens)); + } else { + md.append(LegalDocumentRegistry.fill(registry.readPart(meta, part), tokens)); + } + } + + String variablesJson; + try { + variablesJson = objectMapper.writeValueAsString(tokens); + } catch (Exception e) { + variablesJson = "{}"; + } + + return new AssembledAgreement( + meta.id(), + meta.version(), + meta.versionLabel(), + meta.displayName(), + meta.effectiveDate(), + meta.status(), + md.toString(), + variablesJson); + } + + private Map tokens( + ProcurementQuote quote, AgreementSigning signing, LegalDocumentMeta meta) { + QuoteConfig cfg = toConfig(quote); + boolean signed = signing != null; + + String legalName = + signed && notBlank(signing.customerLegalName()) + ? signing.customerLegalName().trim() + : (notBlank(quote.getBusinessName()) + ? quote.getBusinessName().trim() + : "Customer"); + + Map t = new LinkedHashMap<>(registry.commonTokens(meta)); + t.put("effective_date", signed ? LocalDate.now().format(DATE) : "On signature"); + t.put("customer_legal_name", cell(legalName)); + t.put("quote_ref", nz(quote.getQuoteNumber())); + t.put("deployment", ProcurementPricingService.deploymentName(quote.getDeployment())); + t.put("committed_pdfs_yr", String.format(Locale.US, "%,d", Math.max(0, quote.getVolume()))); + t.put("posture", ProcurementPricingService.postureName(quote.getIntensity())); + t.put("processes_per_pdf", String.valueOf(Math.max(1, quote.getIntensity()))); + t.put("rate_per_pdf", String.format(Locale.US, "$%.4f", pricing.effectiveRatePerPdf(cfg))); + t.put("term_years", String.valueOf(quote.getTermYears())); + t.put("term_discount_pct", pricing.termDiscountPct(quote.getTermYears()) + "%"); + t.put("sla_tier", slaTier(quote.getServiceLevel())); + t.put("annual_fee_y1", money(quote.getAnnualNetMinor())); + t.put("contract_total", money(quote.getTcvMinor())); + t.put("elected_or_not", quote.isIndemnification() ? "Elected" : "Not elected"); + t.put("po_number", notBlank(quote.getPoNumber()) ? cell(quote.getPoNumber()) : "—"); + t.put( + "customer_signatory", + signed && notBlank(signing.signatoryName()) + ? cell(signing.signatoryName()) + : BLANK); + t.put( + "customer_signatory_title", + signed && notBlank(signing.signatoryTitle()) + ? cell(signing.signatoryTitle()) + : BLANK); + return t; + } + + /** + * Make a buyer-supplied value safe to slot into a markdown table cell. + * + *

An unescaped {@code |} or newline splits the cell and breaks the Order Form's table. That + * matters beyond appearance: this markdown is what gets hashed into the signature record, so a + * value that restructures the table means the SHA-256 we keep as proof covers a document + * reading differently from the one the signatory saw. + */ + private static String cell(String raw) { + return raw.trim().replace("|", "\\|").replaceAll("\\s*\\R+\\s*", " "); + } + + /** Part B — the Order Form. Generated from the quote; the only per-deal section. */ + private String orderForm(ProcurementQuote quote, Map t) { + String date = t.get("effective_date"); + String signatory = t.get("customer_signatory"); + String signatoryTitle = t.get("customer_signatory_title"); + + StringBuilder sb = new StringBuilder(); + sb.append("## Part B — Order Form · {{quote_ref}}\n\n"); + sb.append("| Term | Value |\n| --- | --- |\n"); + row(sb, "Customer", "{{customer_legal_name}}"); + row(sb, "Subscription", "Enterprise · {{deployment}}"); + row(sb, "Purchase order", "{{po_number}}"); + row(sb, "Committed Volume", "{{committed_pdfs_yr}} PDFs / year at the {{posture}} posture"); + row(sb, "Committed rate", "{{rate_per_pdf}} per PDF"); + row(sb, "Service level", "{{sla_tier}} (per SLA Exhibit)"); + row( + sb, + "Term", + "{{term_years}} year(s) · term discount {{term_discount_pct}} on committed processing"); + row(sb, "Itemized services", itemizedServices(quote)); + row(sb, "Annual Fee (year 1)", "{{annual_fee_y1}}"); + row(sb, "Total (paid in advance)", "{{contract_total}}"); + row(sb, "Escalator", "+3% at each anniversary during the Term"); + row( + sb, + "Payment", + "Full {{term_years}}-year term invoiced in advance on acceptance · net 30 · ACH," + + " wire, or check"); + row(sb, "Overage", "Committed rate, billed quarterly in arrears"); + row( + sb, + "Data schedule", + "First 25 MB per file included; each additional 25 MB or part thereof (decimal MB," + + " rounded up per file, measured once at ingestion) draws down 1 PDF Process." + + " Frozen for the Term (MSA §3.5)."); + row( + sb, + "Drawdown schedule", + "{{posture}}: {{processes_per_pdf}} PDF Processes per PDF (MSA §3.3, frozen for the Term)"); + row( + sb, + "Enhanced IP Protection", + "{{elected_or_not}} — extends §7.3 to patent claims at the §8.2 super-cap"); + row(sb, "Standard terms", "SSO, SCIM, RBAC, and audit logs included."); + + sb.append( + "\n**Itemized services menu (include as elected):** Self-hosted deployment $12,000/yr" + + " · Air-gapped deployment $36,000/yr · Dedicated SE/CSM $30,000/yr · Enhanced IP" + + " Protection (patent coverage, Section 7.3) 5% of committed processing fees ·" + + " Onboarding & training $7,500 one-time · Quarterly business reviews $8,000/yr." + + " Baseline IP indemnification (copyright, trademark, trade secret) is included at" + + " no charge.\n\n"); + sb.append( + "**Signatures.** By signing, each signatory represents they have authority to bind" + + " their Party. Signatures delivered electronically or in counterparts are" + + " effective as originals.\n\n"); + sb.append("| Provider | Customer |\n| --- | --- |\n"); + sb.append("| Stirling PDF, Inc. | {{customer_legal_name}} |\n"); + sb.append("| Name: Matt Joseph | Name: ").append(signatory).append(" |\n"); + sb.append("| Title: CEO | Title: ").append(signatoryTitle).append(" |\n"); + sb.append("| Date: ").append(date).append(" | Date: ").append(date).append(" |\n"); + return sb.toString(); + } + + /** + * The elected add-on lines, taken from the quote's stored breakdown (excludes the base meter). + */ + private String itemizedServices(ProcurementQuote quote) { + List lines = parseLineItems(quote.getLineItemsJson()); + List elected = new ArrayList<>(); + for (QuoteLineItem li : lines) { + if (li.key().equals("usage") + || li.key().equals("seats") + || li.key().equals("multi-year")) { + continue; + } + String suffix = li.kind() == QuoteLineItem.Kind.ONE_TIME ? " (one-time)" : "/yr"; + elected.add(li.label() + " " + money(li.amountMinor()) + suffix); + } + return elected.isEmpty() ? "None elected" : String.join(" · ", elected); + } + + private List parseLineItems(String json) { + if (json == null || json.isBlank()) return List.of(); + try { + return objectMapper.readValue( + json, + objectMapper + .getTypeFactory() + .constructCollectionType(List.class, QuoteLineItem.class)); + } catch (Exception e) { + log.warn("[legal] could not parse quote line items for the order form", e); + return List.of(); + } + } + + private static QuoteConfig toConfig(ProcurementQuote q) { + int users = q.getSeats() == null ? 0 : q.getSeats(); + return new QuoteConfig( + q.getVolume(), + users, + q.getIntensity(), + q.getSizeMult(), + q.getDeployment(), + q.getTermYears(), + q.getServiceLevel(), + q.isIndemnification(), + q.isTraining(), + q.isQbr(), + q.getCurrency()); + } + + private static void row(StringBuilder sb, String term, String value) { + sb.append("| ").append(term).append(" | ").append(value).append(" |\n"); + } + + private static String slaTier(String serviceLevel) { + if ("dedicated".equalsIgnoreCase(serviceLevel)) return "Dedicated"; + if ("priority".equalsIgnoreCase(serviceLevel)) return "Priority"; + return "Standard"; + } + + /** Minor units (cents) → whole-dollar display; the quote figures are whole dollars. */ + private static String money(long minor) { + return String.format(Locale.US, "$%,d", minor / 100L); + } + + private static boolean notBlank(String s) { + return s != null && !s.isBlank(); + } + + private static String nz(String s) { + return s == null ? "" : s; + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java new file mode 100644 index 0000000000..8204df6514 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementPdfRenderer.java @@ -0,0 +1,71 @@ +package stirling.software.saas.procurement.legal; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.commonmark.Extension; +import org.commonmark.ext.gfm.tables.TablesExtension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.CustomHtmlSanitizer; +import stirling.software.common.util.FileToPdf; +import stirling.software.common.util.TempFileManager; + +/** + * Renders an assembled agreement's markdown to a PDF, dogfooding Stirling's own conversion path: + * commonmark (markdown → HTML) then {@link FileToPdf#convertHtmlToPdf} (HTML → PDF via WeasyPrint), + * the same pipeline as the product's Markdown-to-PDF tool. + * + *

The signed PDF is a stored artifact, but it must never block signing: {@link #tryRender} + * returns {@code null} if the conversion runtime (WeasyPrint) is unavailable, so the signature is + * still recorded and the buyer keeps the on-the-fly download. + */ +@Service +@RequiredArgsConstructor +public class AgreementPdfRenderer { + + private final RuntimePathConfig runtimePathConfig; + private final TempFileManager tempFileManager; + private final CustomHtmlSanitizer customHtmlSanitizer; + private final CustomPDFDocumentFactory pdfDocumentFactory; + + private static final List EXTENSIONS = List.of(TablesExtension.create()); + + /** Render to PDF, or return null if the conversion runtime isn't available. */ + public byte[] tryRender(String markdown) { + try { + return render(markdown); + } catch (Exception e) { + org.slf4j.LoggerFactory.getLogger(AgreementPdfRenderer.class) + .warn( + "[legal] agreement PDF render unavailable; recording signature without a" + + " stored PDF: {}", + e.getMessage()); + return null; + } + } + + private byte[] render(String markdown) throws Exception { + Parser parser = Parser.builder().extensions(EXTENSIONS).build(); + Node document = parser.parse(markdown); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(EXTENSIONS).build(); + String html = renderer.render(document); + + byte[] pdfBytes = + FileToPdf.convertHtmlToPdf( + runtimePathConfig.getWeasyPrintPath(), + null, + html.getBytes(StandardCharsets.UTF_8), + "agreement.html", + tempFileManager, + customHtmlSanitizer); + return pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); + } +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java new file mode 100644 index 0000000000..600ecda415 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AgreementSigning.java @@ -0,0 +1,12 @@ +package stirling.software.saas.procurement.legal; + +/** + * The buyer-supplied inputs captured at the moment of signing the enterprise agreement: the legal + * entity name, the signatory's typed name and title, and their representation of authority to bind. + * Null when the agreement is rendered for preview (before signing). + */ +public record AgreementSigning( + String customerLegalName, + String signatoryName, + String signatoryTitle, + boolean authorityConfirmed) {} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java new file mode 100644 index 0000000000..3e88cf555a --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/legal/AssembledAgreement.java @@ -0,0 +1,16 @@ +package stirling.software.saas.procurement.legal; + +/** + * A rendered enterprise agreement: the full markdown the buyer sees (MSA + Order Form + DPA, tokens + * filled), plus the registry metadata that pins it. {@code variablesJson} is the exact set of + * Order-Form values as rendered, stored alongside a signature so the document is reproducible. + */ +public record AssembledAgreement( + String docId, + String version, + String versionLabel, + String displayName, + String effectiveDate, + String status, + String markdown, + String variablesJson) {} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java new file mode 100644 index 0000000000..425b100ba9 --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementAgreementSignature.java @@ -0,0 +1,86 @@ +package stirling.software.saas.procurement.model; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * An immutable record of a signed enterprise agreement. Each signature pins the exact legal + * document it was signed against — {@code documentId} + {@code documentVersion} + a SHA-256 {@code + * contentHash} of the rendered markdown — plus the Order-Form variable snapshot and the typed + * signatory details, so the agreement stays reproducible even after the templates version up. The + * rendered PDF is stored when the conversion runtime is available. + */ +@Entity +@Table(name = "procurement_agreement_signature") +@NoArgsConstructor +@Getter +@Setter +public class ProcurementAgreementSignature implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "signature_id") + private Long signatureId; + + @Column(name = "deal_id", nullable = false) + private Long dealId; + + @Column(name = "quote_id", nullable = false) + private Long quoteId; + + // Which legal document, and which version of it, was signed. + @Column(name = "document_id", nullable = false, length = 64) + private String documentId; + + @Column(name = "document_version", nullable = false, length = 32) + private String documentVersion; + + @Column(name = "document_label", length = 64) + private String documentLabel; + + // SHA-256 (hex) of the exact rendered agreement markdown the buyer accepted. + @Column(name = "content_hash", nullable = false, length = 64) + private String contentHash; + + // The Order-Form variable values as rendered, so the document can be reproduced. + @Column(name = "variables_json", columnDefinition = "text") + private String variablesJson; + + @Column(name = "customer_legal_name", length = 255) + private String customerLegalName; + + @Column(name = "signatory_name", nullable = false, length = 255) + private String signatoryName; + + @Column(name = "signatory_title", length = 255) + private String signatoryTitle; + + @Column(name = "authority_confirmed", nullable = false) + private boolean authorityConfirmed; + + @Column(name = "signer_ip", length = 64) + private String signerIp; + + // The rendered PDF artifact; null when the conversion runtime was unavailable at signing. + @Column(name = "pdf") + private byte[] pdf; + + @CreationTimestamp + @Column(name = "signed_at", nullable = false, updatable = false) + private LocalDateTime signedAt; +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java index 3a5d3db902..f0db2fb6db 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementDeal.java @@ -34,6 +34,13 @@ public class ProcurementDeal implements Serializable { private static final long serialVersionUID = 1L; + /** + * Interest, before any commitment: the account asked about enterprise but has not started a + * trial. Kept as a real stage so intent survives a refresh, so the enterprise surface is only + * shown to accounts that asked for it, and so drop-off at the cheapest step is measurable. + */ + public static final String STAGE_EXPLORING = "exploring"; + public static final String STAGE_TRIAL = "trial"; public static final String STAGE_QUOTE = "quote"; public static final String STAGE_AGREEMENT = "security"; @@ -69,12 +76,38 @@ public class ProcurementDeal implements Serializable { @Column(name = "trial_extensions_used", nullable = false) private int trialExtensionsUsed; + // Captured at trial setup, so the buying entity is known before any quote exists — the quote's + // own copies seed from these and may then diverge (a deal can change hands mid-cycle). + // Nullable: trials started before this step, and older clients, supply none. + @Column(name = "business_name", length = 255) + private String businessName; + + @Column(name = "contact_name", length = 255) + private String contactName; + + @Column(name = "contact_email", length = 320) + private String contactEmail; + + // Addresses the buyer named at setup. Kept as the record of what was asked for; the invitations + // themselves go out through the team-invite path when the trial starts. + @Column(name = "invite_emails", length = 2000) + private String inviteEmails; + @Column(name = "license_ref", length = 128) private String licenseRef; @Column(name = "subscription_id", length = 255) private String subscriptionId; + /** + * The last Stripe invoice whose payment was applied to this deal. Distinguishes a redelivered + * {@code invoice.paid} for a payment already handled from a genuine renewal, which has to + * re-issue: the committed licence expires term years from issue, so a renewal that doesn't + * re-issue leaves the licence lapsing after the customer has paid. + */ + @Column(name = "last_paid_invoice_id", length = 255) + private String lastPaidInvoiceId; + @Column(name = "accepted_quote_id") private Long acceptedQuoteId; diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java index aa94128172..19e9e01004 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/model/ProcurementQuote.java @@ -47,7 +47,11 @@ public class ProcurementQuote implements Serializable { @Column(name = "deal_id", nullable = false) private Long dealId; - @Column(name = "quote_number", nullable = false, length = 64) + /** + * Stripe's quote number, the deal's one buyer-facing reference. Null until the quote is issued: + * Stripe assigns it at finalisation, and the issue edge function writes it back then. + */ + @Column(name = "quote_number", length = 64) private String quoteNumber; @Column(name = "status", nullable = false, length = 24) diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java b/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java index 08ade5a157..5d1234e9fc 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/pricing/ProcurementPricingService.java @@ -60,12 +60,7 @@ public class ProcurementPricingService { rates.discountPerDoubling() * (Math.log(runVol / (double) RUN_CURVE_KNEE) / LOG2)) : 0.0; - double rate = Math.max(rates.floorRatePerRun(), rates.listRatePerRun() * (1.0 - volDisc)); - // File-size multiplier (D93): larger, image-heavy PDFs cost more OCR/compute/storage. Folds - // into the per-run rate after the floor, so it flows through the meter, TCV and renewal. - // QuoteConfig has already snapped it to a known tier, so a tampered request can't sneak a - // cheaper factor in. - rate *= cfg.sizeMult(); + double rate = perRunRate(cfg, rates); double termDisc = rates.termDiscount(cfg.termYears()); // The meter is a whole-dollar figure (the quote reads in dollars), then minor units. @@ -160,6 +155,48 @@ public class ProcurementPricingService { return new QuoteBreakdown(lines, annualNet, tcv, renewalAnnual, cfg.currency()); } + /** + * The per-run rate after the committed-volume curve, the half-cent floor, and the file-size + * multiplier — the same value {@link #price} meters against. Extracted so read-only callers + * (the Order Form) can quote it without re-deriving the curve. + */ + private static double perRunRate(QuoteConfig cfg, PricingRates rates) { + long runVol = Math.max(0, cfg.volume()) * (long) Math.max(1, cfg.intensity()); + double volDisc = + runVol > RUN_CURVE_KNEE + ? Math.min( + 0.5, + rates.discountPerDoubling() + * (Math.log(runVol / (double) RUN_CURVE_KNEE) / LOG2)) + : 0.0; + return Math.max(rates.floorRatePerRun(), rates.listRatePerRun() * (1.0 - volDisc)) + * cfg.sizeMult(); + } + + /** + * The effective per-PDF rate at the chosen posture, in dollars (4-decimal quote figure). This + * is what the Order Form and quote copy speak in — never the per-run rate. Read-only; does not + * affect billing. + */ + public double effectiveRatePerPdf(QuoteConfig cfg) { + return perRunRate(cfg, PricingRates.defaults()) * Math.max(1, cfg.intensity()); + } + + /** The multi-year term discount as a whole-percent figure for the Order Form (0.05 → 5). */ + public int termDiscountPct(int termYears) { + return (int) Math.round(PricingRates.defaults().termDiscount(termYears) * 100.0); + } + + /** Buyer-facing posture name (Essentials / Governed / Regulated) for the given intensity. */ + public static String postureName(int intensity) { + return postureLabel(intensity); + } + + /** Buyer-facing deployment name (Stirling Cloud / Self-hosted / Air-gapped). */ + public static String deploymentName(String deployment) { + return deploymentLabel(deployment); + } + /** The default CPI escalator (fraction) applied to the annual fee on each post-term renewal. */ public double cpiEscalator() { return PricingRates.defaults().cpiEscalator(); diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java b/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java new file mode 100644 index 0000000000..419189617e --- /dev/null +++ b/app/saas/src/main/java/stirling/software/saas/procurement/repository/ProcurementAgreementSignatureRepository.java @@ -0,0 +1,29 @@ +package stirling.software.saas.procurement.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; + +public interface ProcurementAgreementSignatureRepository + extends JpaRepository { + + Optional findFirstByDealIdOrderBySignedAtDesc(Long dealId); + + Optional findFirstByQuoteIdOrderBySignedAtDesc(Long quoteId); + + /** + * Version labels of a deal's signatures, newest first. Projects just the label column so the + * frequently-polled snapshot never loads the PDF bytes. A signature means the agreement is + * signed; the PDF is resolved (stored or re-rendered) at download time. + */ + @Query( + "SELECT s.documentLabel FROM ProcurementAgreementSignature s" + + " WHERE s.dealId = :dealId" + + " ORDER BY s.signedAt DESC") + List findSignedLabels(@Param("dealId") Long dealId); +} diff --git a/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java b/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java index cae1cf36fb..a739e538b2 100644 --- a/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java +++ b/app/saas/src/main/java/stirling/software/saas/procurement/service/ProcurementService.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.UUID; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -20,16 +19,24 @@ import stirling.software.common.model.enumeration.TeamRole; import stirling.software.proprietary.model.TeamMembership; import stirling.software.proprietary.security.repository.TeamMembershipRepository; import stirling.software.saas.procurement.config.ProcurementConfigurationProperties; +import stirling.software.saas.procurement.legal.AgreementAssembler; +import stirling.software.saas.procurement.legal.AgreementPdfRenderer; +import stirling.software.saas.procurement.legal.AgreementSigning; +import stirling.software.saas.procurement.legal.AssembledAgreement; import stirling.software.saas.procurement.license.EnterpriseLicenseService; import stirling.software.saas.procurement.license.LicenseEntitlements; +import stirling.software.saas.procurement.model.ProcurementAgreementSignature; import stirling.software.saas.procurement.model.ProcurementDeal; import stirling.software.saas.procurement.model.ProcurementQuote; import stirling.software.saas.procurement.model.QuoteDetails; import stirling.software.saas.procurement.pricing.ProcurementPricingService; import stirling.software.saas.procurement.pricing.QuoteBreakdown; import stirling.software.saas.procurement.pricing.QuoteConfig; +import stirling.software.saas.procurement.repository.ProcurementAgreementSignatureRepository; import stirling.software.saas.procurement.repository.ProcurementDealRepository; import stirling.software.saas.procurement.repository.ProcurementQuoteRepository; +import stirling.software.saas.service.SaasTeamService; +import stirling.software.saas.util.LogRedactionUtils; /** * Orchestrates a linked team's procurement journey: start a (mock-licensed) trial, build a @@ -52,6 +59,12 @@ public class ProcurementService { private final EnterpriseLicenseService licenses; private final ProcurementConfigurationProperties config; private final TeamMembershipRepository memberRepo; + private final AgreementAssembler agreementAssembler; + private final AgreementPdfRenderer agreementPdfRenderer; + private final ProcurementAgreementSignatureRepository signatureRepo; + // Trial-setup invitations run through the team-invite path, with its seat, role and + // rate-limit rules rather than a second implementation here. + private final SaasTeamService teams; public ProcurementService( ProcurementDealRepository dealRepo, @@ -59,13 +72,21 @@ public class ProcurementService { ProcurementPricingService pricing, EnterpriseLicenseService licenses, ProcurementConfigurationProperties config, - TeamMembershipRepository memberRepo) { + TeamMembershipRepository memberRepo, + AgreementAssembler agreementAssembler, + AgreementPdfRenderer agreementPdfRenderer, + ProcurementAgreementSignatureRepository signatureRepo, + SaasTeamService teams) { this.dealRepo = dealRepo; this.quoteRepo = quoteRepo; this.pricing = pricing; this.licenses = licenses; this.config = config; this.memberRepo = memberRepo; + this.agreementAssembler = agreementAssembler; + this.agreementPdfRenderer = agreementPdfRenderer; + this.signatureRepo = signatureRepo; + this.teams = teams; } /** @@ -89,11 +110,45 @@ public class ProcurementService { return dealRepo.findByTeamId(teamId); } + /** + * Starting a trial is only legitimate before one exists, while the buyer is still exploring, or + * to restart within the trial itself. A null stage is a deal that has just been constructed. + * + *

Package-private so the policy can be tested without the service's ten dependencies. Adding + * a later stage here would let a leader replace a paying customer's committed licence. + */ + static boolean canStartTrial(String stage) { + return stage == null + || ProcurementDeal.STAGE_EXPLORING.equals(stage) + || ProcurementDeal.STAGE_TRIAL.equals(stage); + } + @Transactional(readOnly = true) public List quotesForDeal(Long dealId) { return quoteRepo.findByDealIdOrderByCreatedAtDesc(dealId); } + /** + * Record that the account is looking at enterprise. Creates the deal at {@code exploring} when + * there is none; an existing deal is returned untouched, so this can never walk a live deal + * backwards or restart a trial. + */ + @Transactional + public ProcurementDeal recordInterest(Long teamId) { + return dealRepo.findByTeamId(teamId) + .orElseGet( + () -> { + ProcurementDeal deal = new ProcurementDeal(teamId); + deal.setStage(ProcurementDeal.STAGE_EXPLORING); + ProcurementDeal saved = dealRepo.save(deal); + log.info( + "[procurement] interest recorded team={} deal={}", + teamId, + saved.getDealId()); + return saved; + }); + } + /** * Start (or restart) the free trial for a team: issue a mock trial licence and stamp the trial * window on the deal. No Stripe: a no-card trial has no subscription; the entitlement is the @@ -101,10 +156,40 @@ public class ProcurementService { * ({@code cloud}/{@code selfhost}/{@code airgap}) and seat count are captured here so the quote * builder opens seeded to their environment; both are still editable when the quote is built. */ - @Transactional public ProcurementDeal startTrial(Long teamId, String deployment, int seats) { + return startTrial(teamId, deployment, seats, null, null, null, null); + } + + /** + * Start (or restart) the trial, capturing the buying entity if the setup step collected it. + * Blank details are ignored rather than written, so a re-run without them keeps what is there. + * + *

Only from before the trial or during it. Past that, {@code licenseRef} points at the + * committed annual licence, and this method would replace it with a fresh 14-day trial key + * while Stripe kept billing — the same hazard {@link #extendTrial} guards against, one step + * worse because it re-issues rather than re-dates. It would also rewind the stage and reset the + * extension counter. + * + *

The transaction is declared here rather than on the 3-arg overload: that one only + * delegates, and Spring's proxy cannot intercept a self-invocation, so an annotation there does + * nothing for either path. This is the method the controller calls, and it reaches out to + * Keygen between the read and the write. + */ + @Transactional + public ProcurementDeal startTrial( + Long teamId, + String deployment, + int seats, + String businessName, + String contactName, + String contactEmail, + String inviteEmails) { ProcurementDeal deal = dealRepo.findByTeamId(teamId).orElseGet(() -> new ProcurementDeal(teamId)); + if (!canStartTrial(deal.getStage())) { + throw new IllegalStateException( + "Trial cannot be started from stage " + deal.getStage()); + } LocalDateTime now = LocalDateTime.now(); LocalDateTime ends = now.plusDays(config.getTrialDurationDays()); deal.setStage(ProcurementDeal.STAGE_TRIAL); @@ -113,6 +198,10 @@ public class ProcurementService { deal.setTrialStartedAt(now); deal.setTrialEndsAt(ends); deal.setTrialExtensionsUsed(0); + if (isNotBlank(businessName)) deal.setBusinessName(businessName.trim()); + if (isNotBlank(contactName)) deal.setContactName(contactName.trim()); + if (isNotBlank(contactEmail)) deal.setContactEmail(contactEmail.trim()); + if (isNotBlank(inviteEmails)) deal.setInviteEmails(inviteEmails.trim()); deal.setLicenseRef(licenses.issueTrialLicense(teamId, leaderEmail(teamId), ends)); deal = dealRepo.save(deal); log.info( @@ -128,6 +217,45 @@ public class ProcurementService { /** * Constrain a caller-supplied deployment to the known set; anything else falls back to cloud. */ + /** + * Send the invitations named at trial setup. Best-effort per address: a rejection (already a + * member, an invitee with their own paid plan, the hourly rate limit) must not fail the trial, + * so each is logged and skipped rather than propagated. + * + *

Note the first accepted invitation converts a personal team into a shared one with + * unlimited seats — that is {@code inviteUserToTeam}'s own rule, and naming teammates here is + * the buyer asking for exactly that. + */ + public void sendTrialInvites( + Long teamId, stirling.software.proprietary.security.model.User inviter, String emails) { + if (inviter == null || !isNotBlank(emails)) return; + for (String raw : emails.split("[,;\s]+")) { + String email = raw.trim(); + if (email.isEmpty()) continue; + try { + teams.inviteUserToTeam(teamId, email, inviter); + // Redacted: an invitee list is third-party PII, and these logs are the one place it + // would otherwise be written in full. LogRedactionUtils is what the rest of the + // SaaS + // module uses for the same reason. + log.info( + "[procurement] trial invite sent team={} to={}", + teamId, + LogRedactionUtils.redactEmail(email)); + } catch (Exception e) { + log.warn( + "[procurement] trial invite skipped team={} to={}: {}", + teamId, + LogRedactionUtils.redactEmail(email), + e.getMessage()); + } + } + } + + private static boolean isNotBlank(String value) { + return value != null && !value.isBlank(); + } + private static String normalizeDeployment(String deployment) { if (deployment == null) return "cloud"; String d = deployment.trim().toLowerCase(Locale.ROOT); @@ -172,16 +300,32 @@ public class ProcurementService { } // (Re)building a quote returns the deal to the quote stage and drops any prior acceptance, // so a rebuild from security/payment can't leave a stale stage or accepted-quote pointer. + QuoteBreakdown breakdown = pricing.price(cfg); + // Enforced before anything is persisted, and server-side rather than in the builder: the + // pricing curve has no natural floor (a small enough committed volume rounds the meter to + // zero) and every registered user leads their own team, so without this any signup could + // price a $0 enterprise quote, accept it, and be provisioned a committed licence. + long floor = config.getMinAnnualNetMinor(); + if (floor > 0 && breakdown.annualNetMinor() < floor) { + throw new IllegalStateException( + "Quoted annual fee " + + breakdown.annualNetMinor() + + " is below the minimum enterprise deal size " + + floor); + } + deal.setStage(ProcurementDeal.STAGE_QUOTE); deal.setAcceptedQuoteId(null); deal = dealRepo.save(deal); - QuoteBreakdown breakdown = pricing.price(cfg); - ProcurementQuote quote = new ProcurementQuote(); quote.setDealId(deal.getDealId()); - quote.setQuoteNumber(nextQuoteNumber(deal.getDealId())); - // Priced but not yet issued: the edge fn creates the Stripe Quote and flips this to SENT. + // No quote number here: the deal's one reference is Stripe's, and Stripe does not assign it + // until the quote is finalised. The edge fn creates the Stripe Quote, flips this to SENT, + // and + // writes the number back. Nothing displays a reference in between — the builder only shows + // one + // for an issued quote, and the agreement is not assembled until after issue. quote.setStatus(ProcurementQuote.STATUS_DRAFT); quote.setCurrency(cfg.currency()); quote.setVolume(cfg.volume()); @@ -210,10 +354,11 @@ public class ProcurementService { quote.setLineItemsJson(writeLineItems(breakdown)); quote.setValidUntil(LocalDate.now().plusDays(30)); quote = quoteRepo.save(quote); + // Logged by id, not reference: a draft has no reference until Stripe issues it. log.info( "[procurement] quote built team={} quote={} annualNet={} tcv={}", teamId, - quote.getQuoteNumber(), + quote.getQuoteId(), quote.getAnnualNetMinor(), quote.getTcvMinor()); return quote; @@ -241,6 +386,164 @@ public class ProcurementService { return deal; } + /** The quote a team is currently transacting on: its accepted quote, else the most recent. */ + @Transactional(readOnly = true) + public Optional currentQuote(Long teamId) { + return dealRepo.findByTeamId(teamId) + .flatMap( + deal -> { + if (deal.getAcceptedQuoteId() != null) { + Optional accepted = + quoteRepo.findById(deal.getAcceptedQuoteId()); + if (accepted.isPresent()) return accepted; + } + return quoteRepo + .findByDealIdOrderByCreatedAtDesc(deal.getDealId()) + .stream() + .findFirst(); + }); + } + + /** + * The filled enterprise agreement for a team's current quote, rendered for review (unsigned). + */ + @Transactional(readOnly = true) + public Optional agreementDocument(Long teamId) { + return currentQuote(teamId).map(q -> agreementAssembler.assemble(q, null)); + } + + /** + * The current (unsigned) agreement rendered to PDF, for download at the sign step. Empty when + * there's no quote yet or the render runtime is unavailable. The signed PDF (with the signature + * block filled) is a separate artifact recorded at signing (see {@link #latestSignature}). + */ + @Transactional(readOnly = true) + public Optional agreementDocumentPdf(Long teamId) { + return currentQuote(teamId) + .map(q -> agreementAssembler.assemble(q, null)) + .map(a -> agreementPdfRenderer.tryRender(a.markdown())); + } + + /** + * Record a signed enterprise agreement: assemble the final document, hash it, render + store + * the PDF (best-effort), and persist an immutable signature pinned to the exact document + * version. Does not itself accept the quote into a subscription — the caller proceeds to accept + * as before. + * + *

Deliberately not {@code @Transactional}: rendering the PDF shells out to WeasyPrint, and + * holding the deal's row lock across an external process buys nothing here. The only write is a + * single insert, which {@code save} makes atomic on its own. + */ + public ProcurementAgreementSignature signAgreement( + Long teamId, AgreementSigning signing, String signerIp) { + ProcurementDeal deal = + dealRepo.findByTeamId(teamId) + .orElseThrow(() -> new IllegalStateException("No deal for team " + teamId)); + ProcurementQuote quote = + currentQuote(teamId) + .orElseThrow( + () -> + new IllegalStateException( + "No quote to sign for team " + teamId)); + + // Signing is only meaningful against an issued quote at the agreement stage. Without this a + // direct API call could record a signature over a draft (whose quote_ref is still empty), + // or + // re-sign a deal that has already moved on. + if (!ProcurementDeal.STAGE_AGREEMENT.equals(deal.getStage())) { + throw new IllegalStateException( + "Deal is not at the agreement stage for team " + teamId); + } + if (!ProcurementQuote.STATUS_SENT.equals(quote.getStatus())) { + throw new IllegalStateException("Quote is not issued for team " + teamId); + } + + AssembledAgreement assembled = agreementAssembler.assemble(quote, signing); + + ProcurementAgreementSignature sig = new ProcurementAgreementSignature(); + sig.setDealId(deal.getDealId()); + sig.setQuoteId(quote.getQuoteId()); + sig.setDocumentId(assembled.docId()); + sig.setDocumentVersion(assembled.version()); + sig.setDocumentLabel(assembled.versionLabel()); + sig.setContentHash(sha256(assembled.markdown())); + sig.setVariablesJson(assembled.variablesJson()); + sig.setCustomerLegalName(signing.customerLegalName()); + sig.setSignatoryName(signing.signatoryName()); + sig.setSignatoryTitle(signing.signatoryTitle()); + sig.setAuthorityConfirmed(signing.authorityConfirmed()); + sig.setSignerIp(signerIp); + sig.setPdf(agreementPdfRenderer.tryRender(assembled.markdown())); + sig = signatureRepo.save(sig); + log.info( + "[procurement] agreement signed team={} quote={} doc={} pdf={}", + teamId, + quote.getQuoteId(), + assembled.versionLabel(), + sig.getPdf() != null); + return sig; + } + + /** The latest recorded signature for a team's deal, if any (for the signed-PDF download). */ + @Transactional(readOnly = true) + public Optional latestSignature(Long teamId) { + return dealRepo.findByTeamId(teamId) + .flatMap( + deal -> + signatureRepo.findFirstByDealIdOrderBySignedAtDesc( + deal.getDealId())); + } + + /** + * The version label of the deal's latest signed agreement, if any. Used to surface the + * "download signed agreement" action once a signature exists; the snapshot polls this, so it + * deliberately avoids loading the PDF bytes. + * + *

Deliberately not conditional on a stored PDF: download re-renders from the pinned document + * version on demand, so the action works whether or not the render succeeded at signing time. + */ + @Transactional(readOnly = true) + public Optional signedAgreementLabel(Long dealId) { + return signatureRepo.findSignedLabels(dealId).stream().findFirst(); + } + + /** + * The signed agreement as a PDF for download: the artifact stored at signing, or — if the + * render runtime was unavailable then — re-rendered now from the signature's details. Empty + * when the team has no signature or the render runtime is still unavailable. + */ + @Transactional(readOnly = true) + public Optional signedAgreementPdf(Long teamId) { + return latestSignature(teamId) + .flatMap( + sig -> { + if (sig.getPdf() != null) return Optional.of(sig.getPdf()); + return quoteRepo + .findById(sig.getQuoteId()) + .map( + q -> + agreementAssembler.assemble( + q, + new AgreementSigning( + sig.getCustomerLegalName(), + sig.getSignatoryName(), + sig.getSignatoryTitle(), + sig.isAuthorityConfirmed()))) + .map(a -> agreementPdfRenderer.tryRender(a.markdown())); + }); + } + + private static String sha256(String s) { + try { + byte[] digest = + java.security.MessageDigest.getInstance("SHA-256") + .digest(s.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + return java.util.HexFormat.of().formatHex(digest); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 unavailable", e); + } + } + /** * Provision on accept: upgrade the team's licence to the committed annual term (valid * immediately), so the buyer can get going the moment they accept — before the invoice is paid. @@ -260,22 +563,58 @@ public class ProcurementService { } /** - * Mark the deal fully live (advance to the active stage) once payment settles. In production - * this is the {@code invoice.paid} webhook; here it's the demo/manual stand-in. Re-affirms the + * Mark the deal fully live (advance to the active stage) once payment settles — driven by the + * {@code invoice.paid} webhook, and by the demo control when those are enabled. Re-affirms the * annual licence in case provisioning didn't run at accept. + * + *

Idempotent per invoice rather than per stage, which matters because {@code invoice.paid} + * carries two different meanings. Stripe redelivers events, so the same invoice + * arriving twice must do nothing. But the renewal payment a year later is also an {@code + * invoice.paid}, and the committed licence expires term years from issue — so a + * different invoice has to re-issue, moving the expiry out, or the customer's licence + * lapses after they have paid. Keying on the stage alone couldn't tell those apart and treated + * every renewal as a duplicate. + * + * @param paidInvoiceId the Stripe invoice that was paid, or null when the caller has no invoice + * to identify the payment by (the demo control). Null keeps the old conservative behaviour: + * a live deal short-circuits, since there is nothing to tell a renewal from a repeat. */ @Transactional - public ProcurementDeal markLive(Long teamId) { + public ProcurementDeal markLive(Long teamId, String paidInvoiceId) { ProcurementDeal deal = dealRepo.findByTeamId(teamId) .orElseThrow(() -> new IllegalStateException("No deal for team " + teamId)); + if (ProcurementDeal.STAGE_LIVE.equals(deal.getStage()) + && (paidInvoiceId == null || paidInvoiceId.equals(deal.getLastPaidInvoiceId()))) { + log.debug( + "[procurement] invoice.paid already applied team={} deal={} invoice={}", + teamId, + deal.getDealId(), + paidInvoiceId); + return deal; + } + boolean renewal = ProcurementDeal.STAGE_LIVE.equals(deal.getStage()); deal.setLicenseRef(issueOrUpgradeAnnual(deal)); + if (paidInvoiceId != null) deal.setLastPaidInvoiceId(paidInvoiceId); deal.setStage(ProcurementDeal.STAGE_LIVE); deal = dealRepo.save(deal); - log.info("[procurement] deal live team={} deal={}", teamId, deal.getDealId()); + log.info( + "[procurement] deal live team={} deal={} renewal={} invoice={}", + teamId, + deal.getDealId(), + renewal, + paidInvoiceId); return deal; } + /** + * Go live with no invoice reference — the demo control. See {@link #markLive(Long, String)}. + */ + @Transactional + public ProcurementDeal markLive(Long teamId) { + return markLive(teamId, null); + } + /** * Issue or upgrade the committed annual licence from the deal's accepted (else latest) quote, * stamping the full entitlement snapshot onto it and upgrading the trial licence in place when @@ -327,15 +666,34 @@ public class ProcurementService { * before paying — that's bounded: the trial licence carries {@code expiry = trialEndsAt}, so * the file the verifier accepts self-expires at trial end. The buyer must re-download after * provisioning to get the committed-term file (the portal warns about this). + * + *

Once a quote exists, the quote's deployment decides — not the deal's. They are two + * different values: the deal's is chosen free at trial setup, the quote's is the one carrying + * the air-gap deploy fee. Reading the deal's here meant selecting air-gapped in the trial and + * then buying a cloud quote still yielded the offline file, and after provisioning it was + * checked out against the committed annual licence — so the self-expiry above no longer bounded + * it. */ @Transactional(readOnly = true) public Optional offlineLicenseFile(Long teamId) { ProcurementDeal deal = dealRepo.findByTeamId(teamId).orElse(null); if (deal == null || deal.getLicenseRef() == null) return Optional.empty(); - if (!"airgap".equalsIgnoreCase(deal.getDeployment())) return Optional.empty(); + if (!"airgap".equalsIgnoreCase(entitledDeployment(deal))) return Optional.empty(); return Optional.of(licenses.checkOutLicenseFile(deal.getLicenseRef())); } + /** + * The deployment the team is actually entitled to: the priced quote's once one exists, + * otherwise the trial's self-selected target. Paid entitlements must follow what was quoted. + */ + private String entitledDeployment(ProcurementDeal deal) { + ProcurementQuote quote = currentQuote(deal.getTeamId()).orElse(null); + if (quote != null && quote.getDeployment() != null && !quote.getDeployment().isBlank()) { + return quote.getDeployment(); + } + return deal.getDeployment(); + } + /** * Reset a team's procurement: delete the deal (quotes + activity cascade). For * re-demos/testing. @@ -346,12 +704,6 @@ public class ProcurementService { log.info("[procurement] deal reset team={}", teamId); } - private String nextQuoteNumber(Long dealId) { - int seq = quoteRepo.findByDealIdOrderByCreatedAtDesc(dealId).size() + 1; - String token = UUID.randomUUID().toString().substring(0, 4).toUpperCase(Locale.ROOT); - return String.format(Locale.ROOT, "QT-%s-%04d", token, seq); - } - private String writeLineItems(QuoteBreakdown breakdown) { try { return OBJECT_MAPPER.writeValueAsString(breakdown.lineItems()); diff --git a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java index 890bc081a2..162a5fe408 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java +++ b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java @@ -271,10 +271,8 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter { user.setUsername(supabaseUser.getEmail()); } try { - User saved = userService.saveUser(user); // Give the account its own team rather than the shared Default team. - saved.setTeam(saasTeamService.ensurePersonalTeam(saved)); - return saved; + return saasTeamService.saveUserWithPersonalTeam(user); } catch (DataIntegrityViolationException e) { log.warn( "Email collision upgrading anonymous user {} to {}: {}", @@ -293,7 +291,7 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter { if (!(raw instanceof List amrList) || amrList.isEmpty()) { return WEB; } - Object first = amrList.get(0); + Object first = amrList.getFirst(); if (!(first instanceof Map entry)) { return WEB; } @@ -372,38 +370,22 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter { throw new AuthenticationFailureException("Failed to create SupabaseUser", e); } - User savedUser; - boolean weCreatedThisUser = true; + // Guests get NO team: the editor is free and needs none. Everyone else is provisioned + // atomically, so a user visible to a parallel request always already has one. try { - savedUser = userService.saveUser(newUser); + return isAnonymous(jwt) + ? userService.saveUser(newUser) + : saasTeamService.saveUserWithPersonalTeam(newUser); } catch (DataIntegrityViolationException dup) { // Parallel filter won the race; fetch the winning row. - weCreatedThisUser = false; - savedUser = - userService - .findBySupabaseId(supabaseId) - .orElseThrow( - () -> - new AuthenticationFailureException( - "User creation conflict, but unable to find existing user", - dup)); + return userService + .findBySupabaseId(supabaseId) + .orElseThrow( + () -> + new AuthenticationFailureException( + "User creation conflict, but unable to find existing user", + dup)); } - - // Only the DB-race winner runs first-time init; the losers skip it. Guests (anonymous - // sessions) get NO team: the editor is free and needs none, and automation requires a - // real account. - if (weCreatedThisUser && !isAnonymous(jwt)) { - try { - savedUser.setTeam(saasTeamService.ensurePersonalTeam(savedUser)); - } catch (Exception e) { - log.warn( - "Failed to create personal team for new user {} ({}): {}", - LogRedactionUtils.redactSupabaseId(supabaseId), - LogRedactionUtils.redactEmail(savedUser.getUsername()), - e.getMessage()); - } - } - return savedUser; } private boolean apiKeyAuthenticated(HttpServletRequest request) throws AuthenticationException { diff --git a/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java b/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java index e2f5b65b47..0ce5e8e308 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java +++ b/app/saas/src/main/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthority.java @@ -25,6 +25,11 @@ public class TeamLeaderPolicyManagementAuthority implements PolicyManagementAuth return teamSecurity.isCurrentUserTeamLeader(); } + @Override + public boolean canTriggerPolicies() { + return teamSecurity.isCurrentUserTeamLeader(); + } + @Override public Long currentUserTeamId() { return teamSecurity.currentUserTeamId(); diff --git a/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java b/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java index bda0602261..0038cca42a 100644 --- a/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java +++ b/app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java @@ -53,6 +53,18 @@ public class SaasTeamService { public static final String DEFAULT_TEAM_NAME = "Default"; public static final String INTERNAL_TEAM_NAME = "Internal"; + /** + * Persist a user and their personal team atomically: an account with no team has no portal + * access and no way to acquire one, so a teamless user must never be committed. Constraint + * violations (the concurrent-signup race) propagate for the caller to resolve. + */ + @Transactional + public User saveUserWithPersonalTeam(User user) { + User saved = userService.saveUser(user); + saved.setTeam(ensurePersonalTeam(saved)); + return saved; + } + /** Returns the user's personal team, creating one if they have none. Idempotent. */ @Transactional public Team ensurePersonalTeam(User user) { @@ -60,9 +72,37 @@ public class SaasTeamService { if (existing != null && saasTeamExtensionService.isPersonal(existing)) { return existing; } + // An empty users.team_id does not prove there is no personal team; adopt one the user + // already owns rather than minting a second. + Team owned = existingPersonalTeam(user); + if (owned != null) { + user.setTeam(owned); + userService.saveUser(user); + return owned; + } return createPersonalTeam(user); } + /** + * The personal team the user already owns — their recorded home, else a solo team they lead. + */ + private Team existingPersonalTeam(User user) { + Long homeId = saasUserExtensionService.getHomeTeamId(user); + if (homeId != null) { + Team home = teamRepository.findById(homeId).orElse(null); + if (home != null) { + return home; + } + } + for (TeamMembership membership : membershipRepository.findByUserId(user.getId())) { + Team team = membership.getTeam(); + if (membership.isLeader() && membershipRepository.countByTeamId(team.getId()) == 1) { + return team; + } + } + return null; + } + /** * Create personal team for new user during signup or migrate existing user from Default team * diff --git a/app/saas/src/main/resources/application-dev.properties b/app/saas/src/main/resources/application-dev.properties index ee8bf80ff2..b289fc95cc 100644 --- a/app/saas/src/main/resources/application-dev.properties +++ b/app/saas/src/main/resources/application-dev.properties @@ -1,32 +1,40 @@ -# SaaS dev profile. Points at the dev Supabase project. -# Boot: java -jar stirling-pdf.jar --spring.profiles.include=dev +# SaaS dev profile: follows the Supabase preview branch of the SaaS PR under test. +# One variable switches PR, SAAS_DEV_PROJECT_REF; everything else derives from it. +# Want a stable shared environment instead? Use the staging profile. + spring.config.import=optional:classpath:application-dev-local.properties -app.supabase.project-ref=qacaivhsjtftfwtgjvva +# Let Hibernate reconcile the entity tables so a fresh preview branch heals itself. A branch is built +# from the Supabase migrations, which cover the SaaS-owned tables but not the ~28 inherited from the +# self-hosted app -- those have only ever been created by ddl-auto. Safe here because a preview branch +# is disposable and `update` only ever adds; staging pins `none`, so keep this profile-scoped. +spring.jpa.hibernate.ddl-auto=update -stirling.supabase.url=https://qacaivhsjtftfwtgjvva.supabase.co -stirling.supabase.publishable-key=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow +# From the PR's "Supabase Preview" check. Required with no fallback: ddl-auto=update above must never +# be aimed at the shared project. +app.supabase.project-ref=${SAAS_DEV_PROJECT_REF} -spring.datasource.url=${SAAS_DEV_DB_URL:jdbc:postgresql://db.qacaivhsjtftfwtgjvva.supabase.co:5432/postgres?ApplicationName=stirling-consolidation-${user.name}} +stirling.supabase.url=https://${app.supabase.project-ref}.supabase.co +# Per-branch, not derivable. Dashboard > Settings > API. +stirling.supabase.publishable-key=${SAAS_DEV_PUBLISHABLE_KEY} + +# Override the whole URL if the branch needs the pooler host rather than the direct one. +spring.datasource.url=${SAAS_DEV_DB_URL:jdbc:postgresql://db.${app.supabase.project-ref}.supabase.co:5432/postgres?ApplicationName=stirling-dev-${user.name}} spring.datasource.username=${SAAS_DEV_DB_USERNAME:postgres} -# Password not committed; export SAAS_DEV_DB_PASSWORD or pass --spring.datasource.password=... +# A preview branch has its own password; the parent project's will not authenticate. spring.datasource.password=${SAAS_DEV_DB_PASSWORD:} -# Conservative dev pool sizing. spring.datasource.hikari.maximum-pool-size=2 spring.datasource.hikari.minimum-idle=1 spring.datasource.hikari.idle-timeout=60000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.keepalive-time=300000 -spring.datasource.hikari.data-source-properties.ApplicationName=stirling-consolidation-${user.name} +spring.datasource.hikari.data-source-properties.ApplicationName=stirling-dev-${user.name} logging.level.stirling.software.saas=DEBUG logging.level.org.springframework.security.oauth2.jwt=WARN logging.level.org.springframework.security.oauth2.server.resource=WARN -# Supabase meter edge fn the Java backend calls (server-to-server, on job close). -# URL is not a secret; auth rides the existing SUPABASE_EDGE_FUNCTION_SECRET (same -# shared secret the team-invitation flow uses — no service-role key in the Java env). -# Blank secret → the meter service no-ops with a WARN, so the app still boots. -# The billing portal is NOT here — the FE calls create-customer-portal-session directly. -payg.meter.endpoint=https://qacaivhsjtftfwtgjvva.supabase.co/functions/v1/meter-payg-units +# Server-to-server meter call. Auth rides SUPABASE_EDGE_FUNCTION_SECRET; blank secret means the meter +# service no-ops with a WARN rather than failing the boot. +payg.meter.endpoint=https://${app.supabase.project-ref}.supabase.co/functions/v1/meter-payg-units diff --git a/app/saas/src/main/resources/application-saas.properties b/app/saas/src/main/resources/application-saas.properties index f0630aaac1..e3c32c429e 100644 --- a/app/saas/src/main/resources/application-saas.properties +++ b/app/saas/src/main/resources/application-saas.properties @@ -27,6 +27,13 @@ spring.jpa.properties.hibernate.hbm2ddl.create_namespaces=true spring.jpa.hibernate.ddl-auto=update +# ...but only over the tables Hibernate actually owns. The SaaS database has two writers: the +# Supabase migrations own the SaaS tables, Hibernate owns ~30 inherited from the self-hosted app that +# no migration has ever created. This filter hides the former from schema management, so ddl-auto can +# still heal a fresh preview branch without being free to reconcile a migration-owned table — which +# is how team_memberships.role ended up widened to varchar(255). Register: SaasSchemaOwnership. +spring.jpa.properties.hibernate.hbm2ddl.schema_filter_provider=stirling.software.saas.config.MigrationOwnedSchemaFilter + # ---------- Supabase JWT auth ---------- # Required: set SAAS_DB_PROJECT_REF via env. app.supabase.project-ref=${SAAS_DB_PROJECT_REF:} diff --git a/app/saas/src/main/resources/application-staging.properties b/app/saas/src/main/resources/application-staging.properties new file mode 100644 index 0000000000..4874ed3fb5 --- /dev/null +++ b/app/saas/src/main/resources/application-staging.properties @@ -0,0 +1,39 @@ +# SaaS staging profile: the long-lived shared v3 project, pinned so it is still there tomorrow. +# For work on an open SaaS PR use the dev profile, which follows that PR's preview branch. + +spring.config.import=optional:classpath:application-staging-local.properties + +# Stated rather than inherited: application-saas.properties defaults to `update`, and staging's +# schema is shared and RLS-dependent, so it must not be reconciled by Hibernate. +spring.jpa.hibernate.ddl-auto=none + +# Committed as a default rather than a literal, so staging needs no setup but stays repointable. +# Neither the ref nor the publishable key is secret: the ref is a public subdomain, the key ships in +# the browser bundle. Everything below derives from the ref, so an override follows through. +app.supabase.project-ref=${SAAS_STAGING_PROJECT_REF:qacaivhsjtftfwtgjvva} + +stirling.supabase.url=https://${app.supabase.project-ref}.supabase.co +stirling.supabase.publishable-key=${SAAS_STAGING_PUBLISHABLE_KEY:sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY} + +spring.datasource.url=${SAAS_STAGING_DB_URL:jdbc:postgresql://db.${app.supabase.project-ref}.supabase.co:5432/postgres?ApplicationName=stirling-staging-${user.name}} +spring.datasource.username=${SAAS_STAGING_DB_USERNAME:postgres} +# Password not committed; export SAAS_STAGING_DB_PASSWORD or pass --spring.datasource.password=... +spring.datasource.password=${SAAS_STAGING_DB_PASSWORD:} + +# Conservative pool sizing: this is a shared project, so don't hold connections others need. +spring.datasource.hikari.maximum-pool-size=2 +spring.datasource.hikari.minimum-idle=1 +spring.datasource.hikari.idle-timeout=60000 +spring.datasource.hikari.max-lifetime=1800000 +spring.datasource.hikari.keepalive-time=300000 +spring.datasource.hikari.data-source-properties.ApplicationName=stirling-staging-${user.name} + +logging.level.stirling.software.saas=DEBUG +logging.level.org.springframework.security.oauth2.jwt=WARN +logging.level.org.springframework.security.oauth2.server.resource=WARN + +# Supabase meter edge fn the Java backend calls (server-to-server, on job close). +# URL is not a secret; auth rides the existing SUPABASE_EDGE_FUNCTION_SECRET (same +# shared secret the team-invitation flow uses — no service-role key in the Java env). +# Blank secret → the meter service no-ops with a WARN, so the app still boots. +payg.meter.endpoint=https://${app.supabase.project-ref}.supabase.co/functions/v1/meter-payg-units diff --git a/app/saas/src/main/resources/db/migration/saas/V33__api_keys.sql b/app/saas/src/main/resources/db/migration/saas/V33__api_keys.sql deleted file mode 100644 index abced067e6..0000000000 --- a/app/saas/src/main/resources/db/migration/saas/V33__api_keys.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Named, multi-key personal API keys, plus per-key daily usage. --- Idempotent: Hibernate ddl-auto=update may already have created these on some deployments. - -CREATE TABLE IF NOT EXISTS api_keys ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - key_hash VARCHAR(64) NOT NULL, - prefix VARCHAR(32) NOT NULL, - owner_user_id BIGINT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL, - last_used_at TIMESTAMPTZ, - revoked_at TIMESTAMPTZ -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_api_key_hash ON api_keys (key_hash); -CREATE INDEX IF NOT EXISTS idx_api_key_owner ON api_keys (owner_user_id); - -CREATE TABLE IF NOT EXISTS api_key_daily_usage ( - api_key_id BIGINT NOT NULL, - epoch_day BIGINT NOT NULL, - count BIGINT NOT NULL DEFAULT 0, - PRIMARY KEY (api_key_id, epoch_day) -); diff --git a/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md new file mode 100644 index 0000000000..12cceaef65 --- /dev/null +++ b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/dpa.md @@ -0,0 +1,53 @@ +## Part C — Data Processing Addendum + +This DPA forms part of the Agreement and applies where Provider processes Personal Data on Customer's behalf. + +### C1. Roles; scope; instructions + +Customer is the controller (or a processor on behalf of its own controllers); Provider is a processor (or subprocessor, as applicable). Provider processes Personal Data only on Customer's documented instructions — including processing initiated by Customer's users, policies, pipelines, and API calls — unless required by law (in which case Provider informs Customer unless legally prohibited). **Provider will inform Customer without undue delay if, in Provider's opinion, an instruction infringes the GDPR, UK GDPR, or other applicable data-protection law.** Customer is responsible for the lawfulness of the Personal Data it submits and the instructions it gives; Customer's rights under this DPA include instruction, audit (C9), objection to subprocessors (C5), assistance (C6), and return or deletion of data (C10). + +### C2. Details of processing + +**Subject matter/nature:** PDF processing and governance (classification, redaction, routing, retention, conversion, signing, extraction, AI-assisted analysis). **Duration:** the Term plus the deletion period. **Categories of data:** any Personal Data contained in Customer files and metadata (names, contact details, identifiers, financial or health data if present in Customer files), account data of Customer users. **Data subjects:** Customer's employees, users, customers, and other persons appearing in Customer files. **Sensitive data:** may be present in Customer files at Customer's discretion; Customer is responsible for the lawful basis. + +### C3. Confidentiality; personnel + +Provider ensures persons authorized to process Personal Data are bound by confidentiality and receive security training. Zero-standing-access applies: content access is just-in-time, logged, and audited (MSA Section 4.2). + +### C4. Security measures (Annex II summary) + +Encryption in transit (TLS 1.2+) and at rest (AES-256); zero-standing-access with audited JIT elevation; role-based access control; SSO/SCIM; tenant isolation; vulnerability management and penetration testing; audit logging of processing events (including file name, hash, size, and operations); backup and recovery. For Self-hosted and Air-gapped deployments, Customer operates the runtime environment and is responsible for infrastructure-level controls; Provider's measures apply to license/metering services and support access. + +### C5. Subprocessors + +Customer generally authorizes the subprocessors listed at {{subprocessor_url}}: cloud infrastructure (Amazon Web Services), payment processing (Stripe, as independent controller for payment data), email delivery (Google), account infrastructure (Supabase), product telemetry (PostHog, EU-hosted; pseudonymous usage events, never file content), and AI model providers: **Anthropic** (Claude models — receives prompts and the document text or excerpts needed for the requested AI feature) and **Voyage AI** (embedding models — receives extracted text excerpts solely to generate embeddings where Customer enables Ingestion/RAG features). AI features are optional and may be disabled; where used, AI providers receive only the content needed for the requested feature. **Whole Customer files are never transmitted to any AI provider.** Neither AI provider trains on Customer data (verified against the signed provider agreements, Jul 10, 2026). Provider gives thirty (30) days' notice of new subprocessors; Customer may object on reasonable data-protection grounds, and if unresolved, may terminate the affected Services with a pro-rata refund. **Provider imposes data-protection obligations on each subprocessor by written contract that are at least as protective as this DPA, and remains fully responsible to Customer for each subprocessor's performance.** + +### C6. Data subject requests; assistance + +Taking into account the nature of the processing, Provider provides reasonable assistance (including through the Processor's search, redaction, and audit tools) for Customer's obligations under GDPR Articles 32–36: security of processing, breach notification to authorities and data subjects, data protection impact assessments, and prior consultations with supervisory authorities, as well as responses to data subject requests. Provider forwards requests received directly to Customer and does not respond except as legally required. **Provider makes available to Customer all information necessary to demonstrate compliance with this DPA and allows for and contributes to audits, including inspections, per Section C9.** + +### C7. Breach notification + +Per MSA Section 5.3: without undue delay after becoming aware of a Personal Data Breach, and in any event within forty-eight (48) hours of awareness, with information provided in phases as available — including the nature of the breach, categories and approximate volumes affected, likely consequences, and measures taken or proposed. + +### C8. International transfers + +Where Personal Data subject to GDPR/UK GDPR is transferred to countries without adequacy, the Parties incorporate the EU Standard Contractual Clauses (Commission Decision 2021/914): **Module 2** (controller-to-processor) where Customer is a controller, and **Module 3** (processor-to-processor) where Customer acts as a processor, with the following selections — Clause 7 (docking): included; Clause 9(a): Option 2 (general written authorization, 30 days' notice per C5); Clause 11(a) optional language: not used; Clause 17: the law of Ireland; Clause 18: the courts of Ireland; competent supervisory authority: the Irish Data Protection Commission (per Annex I.C). Annex I (parties, description of transfer: as per Section C2), Annex II (technical and organizational measures: as per Section C4), and Annex III (subprocessors: as per Section C5 and {{subprocessor_url}}) are completed by reference to this DPA. For UK transfers, the UK International Data Transfer Addendum applies with its Tables completed by reference to the foregoing. Provider is not certified under the EU-U.S. Data Privacy Framework; the SCCs are the transfer mechanism. + +**Note:** Provider does not currently offer contractual EU data residency for Stirling Cloud; residency is achieved via Self-hosted or Air-gapped deployment. + +### C9. Audits + +Provider's security reports and documentation (Section 5.1) are the ordinary means of demonstrating compliance. Customer may additionally audit — by itself or a mandated auditor — once per year on thirty (30) days' notice, and at any time where: (a) a security incident affecting Customer Personal Data has occurred; (b) provided documentation reveals a material deficiency; (c) a competent supervisory authority requires it; or (d) Customer reasonably suspects material noncompliance with this DPA. Audits are conducted during business hours, under confidentiality, at Customer's cost, with reasonable notice, without unreasonable interference with Provider's operations, and without access to other customers' data. + +### C10. Return & deletion + +On termination, at Customer's choice, Provider returns Customer file content and Personal Data (export of Customer files and the governed-record metadata) and/or deletes them — from live systems within thirty (30) days and from backups within ninety (90) days — except as retention is required by law, and certifies deletion on request. Where Customer uses HYOK, key destruction by Customer renders content cryptographically inaccessible immediately. + +### C11. CCPA/CPRA + +Provider is a "service provider" under the CCPA/CPRA. Provider: (a) processes Personal Information only for the business purposes specified in this Agreement — providing, securing, metering, and supporting the Services described in Section C2; (b) shall not sell or share Personal Information; (c) shall not retain, use, or disclose it for any purpose other than those business purposes, or outside the direct business relationship between the Parties; (d) shall not combine it with Personal Information received from other sources, except as permitted by CCPA regulations for the business purposes; (e) provides the same level of privacy protection required of businesses by the CCPA; (f) will notify Customer if it determines it can no longer meet its CCPA obligations; (g) grants Customer the right, upon reasonable notice, to take reasonable and appropriate steps to ensure Provider's use of Personal Information is consistent with Customer's obligations, and to stop and remediate any unauthorized use; and (h) flows these requirements down to its subprocessors per Section C5. Provider certifies that it understands these restrictions and will comply with them. + +### C12. Liability + +Liability under this DPA is subject to the MSA's limitations (Section 8). diff --git a/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md new file mode 100644 index 0000000000..cd3f6742eb --- /dev/null +++ b/app/saas/src/main/resources/legal/enterprise-agreement/0.9.1/msa.md @@ -0,0 +1,119 @@ +# Stirling Enterprise Agreement + +One signature executes all three parts: the Master Services Agreement (Part A), the Order Form (Part B), and the Data Processing Addendum (Part C). The Stirling EULA & Commercial Terms is incorporated by reference. + +## Part A — Master Services Agreement + +This Master Services Agreement (the "Agreement") is entered into as of {{effective_date}} (the "Effective Date") by and between **Stirling PDF, Inc.**, a Delaware corporation with offices at 548 Market Street PMB 887643, San Francisco, CA 94104 ("Provider"), and **{{customer_legal_name}}** ("Customer"). Each a "Party," together the "Parties." + +### 1. Services & License + +1.1 **The Services.** Provider will provide the Stirling PDF Processor (the "Processor") — the hosted or customer-deployed platform for distributing PDF editors and governing PDF processing, including policies, pipelines, the Stirling Agent, API access, and the administrative console — and the Stirling PDF Editor (the "Editor"), as described in the Order Form. + +1.2 **License grants.** Provider grants Customer, for the Term: (a) a non-exclusive, non-transferable right to access and use the Processor for Customer's internal business operations, up to the Committed Volume; and (b) a non-exclusive right to deploy and distribute the Editor to Customer's authorized users without limit on user count. Open-source components of the Editor remain governed by their own licenses, which control for those components. + +1.3 **Deployment.** The Services are delivered via the deployment stated in the Order Form (Stirling Cloud, Self-hosted, or Air-gapped). Self-hosted deployments validate their license and report metering data online; Air-gapped deployments verify a signed activation bundle offline and reconcile usage periodically as described in the Documentation. Customer shall not disable, circumvent, or falsify license validation or usage metering. **The metered rate does not vary by deployment**; deployment-specific services are priced as line items in the Order Form. + +1.4 **Restrictions.** Customer shall not: resell or provide the Services to third parties as a service bureau; reverse engineer non-open-source components; use the Services to violate law; or exceed the scope of the Order Form other than through Overage (Section 3.4). + +### 2. Term & Renewal + +2.1 **Initial Term.** {{term_years}} year(s) from the Effective Date. + +2.2 **Renewal.** The Agreement auto-renews for successive periods equal to the Initial Term unless either Party gives sixty (60) days' written notice of non-renewal before the end of the then-current term. The Annual Fee for each renewal year equals the immediately preceding year's Annual Fee increased by three percent (3%) — the same formula as Section 2.3. Itemized services escalate at the same rate unless restated in a superseding Order Form. + +2.3 **In-term escalator.** The Annual Fee (including itemized services) increases by a fixed three percent (3%) at each anniversary of the Effective Date during the Term. + +### 3. Fees & Payment + +3.1 **Annual Fee.** Customer shall pay the Annual Fee stated in the Order Form, calculated as the Committed Volume ({{committed_pdfs_yr}} PDFs per year) at {{rate_per_pdf}} per PDF at the {{posture}} governance posture, plus the itemized services in the Order Form, less the term discount stated there. + +3.2 **Invoicing.** Fees are invoiced annually in advance, due net thirty (30) days. Late amounts accrue interest at 1.5% per month or the maximum permitted by law, whichever is less. Fees are exclusive of taxes; Customer is responsible for all taxes other than Provider's income taxes. + +3.3 **Committed Volume; measurement.** The Committed Volume is denominated in PDFs processed per year at the stated posture, and converts to a drawdown allowance in PDF Processes at the fixed conversion schedule below, which is frozen for the Term: + +| Posture | PDF Processes per PDF | +| --- | --- | +| Essentials | 2 | +| Governed | 4 | +| Regulated | 7 | + +A **"PDF Process"** is one policy execution, one pipeline run, or one Stirling Agent returned artifact, applied to one file, plus Data Processing increments under Section 3.5. For clarity: a pipeline run counts as one PDF Process regardless of the number of operations in its chain; a Stirling Agent artifact counts as one regardless of the number of messages that produced it; failed processes (those that do not complete) are not counted; reprocessing the same file and duplicate submissions are counted; counts are whole numbers (no rounding). The Processor's audit log records each PDF Process and is the system of record, subject to Section 3.7. Provider will make a per-file usage statement (file identifier, size, processes, drawdown) available for audit. + +**Worked example.** At the Governed posture, a commitment of 90,000,000 PDFs/year provides a drawdown allowance of 360,000,000 PDF Processes. A 60 MB file that runs the four Governed policies draws down 4 PDF Processes plus 2 Data Processing increments (Section 3.5) = 6 PDF Processes. The allowance is a purchased quantity, not a feature limit: Customer may run any number of policies or pipelines; actual consumption simply draws the allowance down faster, and consumption beyond it bills as Overage (Section 3.4). + +3.4 **Overage.** Consumption beyond the Committed Volume in a contract year is billed quarterly in arrears at the committed rate stated in the Order Form. Overage does not increase subsequent years' Committed Volume. + +3.5 **Data Processing.** Each file includes its first twenty-five (25) megabytes (decimal, 1 MB = 1,000,000 bytes) at no additional drawdown. Each additional twenty-five (25) megabytes or part thereof (rounded up per file) draws down one (1) additional PDF Process. File size is measured once per file at ingestion, on the file as submitted. This schedule is stated here in full, is frozen for the Term, and is not subject to alteration through the Documentation. + +3.6 **No refunds.** Except as expressly stated (Sections 7.1, 7.3, 10.3, and DPA Section C5), fees are non-refundable and Committed Volume does not roll over between contract years. + +3.7 **Billing disputes.** Customer may dispute any invoice or metering record in good faith within sixty (60) days of the invoice date. Provider will investigate promptly, provide the relevant audit-log extracts and usage statements, and correct confirmed errors by credit or refund. The audit log is presumptively accurate but not conclusive; Customer may rebut it with reasonable evidence. Undisputed amounts remain payable when due. + +### 4. Data Protection + +4.1 The Data Processing Addendum at Part C (the "DPA") is incorporated into this Agreement and governs Provider's processing of Customer Personal Data, in compliance with the GDPR, UK GDPR, and CCPA/CPRA to the extent applicable. + +4.2 **Zero-standing-access.** Customer file content is encrypted in transit and at rest. Provider personnel have no standing access to Customer file content; access is granted just-in-time under audited elevation, solely as necessary to provide the Services or as instructed by Customer. Document metadata is maintained to operate the governed record. Where the Order Form includes BYOK or HYOK key management, the key terms in the Documentation apply. + +### 5. Security & Availability + +5.1 **Security program.** Provider maintains a written information security program including access controls, encryption (TLS 1.2+ in transit, AES-256 at rest), audit logging, vulnerability management, and personnel security. Provider will provide its available security documentation (including its security program overview and penetration-test attestation) upon request under confidentiality. + +5.2 **Availability.** For Stirling Cloud deployments, Provider targets 99.9% monthly uptime, excluding scheduled maintenance announced at least 48 hours in advance. The uptime figure is a target, not a credited commitment, and no service credits apply. Support response commitments for the {{sla_tier}} tier are set out in the SLA Exhibit referenced by the Order Form. + +5.3 **Breach notice.** Provider will notify Customer without undue delay after becoming aware of a Personal Data Breach affecting Customer Personal Data, and in any event within forty-eight (48) hours of awareness. Provider may provide information in phases as it becomes available and will supplement its notice as investigation proceeds. + +5.4 **Updates.** Provider will make security patches and product upgrades available to Customer at no additional charge for supported versions. + +### 6. Confidentiality + +6.1 Each Party shall protect the other's Confidential Information with at least the care it uses for its own similar information and no less than reasonable care, use it solely to perform under this Agreement, and disclose it only to personnel and advisors with a need to know who are bound by confidentiality obligations at least as protective. Confidential Information excludes information that is public without breach, independently developed, or rightfully received from a third party. + +6.2 Compelled disclosure is permitted with prompt notice (where lawful) and reasonable cooperation to seek protective treatment. + +6.3 Obligations survive three (3) years after termination; trade secrets survive as long as they remain trade secrets. + +### 7. Warranties & Indemnification + +7.1 **Performance warranty.** Provider warrants the Services will perform materially in accordance with the Documentation. Customer's exclusive remedy for breach is re-performance or, if Provider cannot re-perform within thirty (30) days, termination of the affected Services and a pro-rata refund of prepaid, unused fees for those Services. + +7.2 **Mutual warranties.** Each Party warrants it has the authority to enter this Agreement and will comply with applicable law in its performance. + +7.3 **IP indemnification.** Provider shall defend Customer against third-party claims that the Services, as provided and used per this Agreement, infringe a copyright or trademark or misappropriate a trade secret, and shall indemnify Customer for resulting damages finally awarded or agreed in settlement. Where **Enhanced IP Protection** is elected on the Order Form, this obligation extends to patent claims and carries the enhanced cap stated in Section 8.2. Exclusions: combinations with non-Provider materials, Customer content, modifications not made by Provider, and use after notice to stop. Provider may procure rights, modify, or replace the Services; if none is practicable, Provider may terminate the affected Services and refund prepaid, unused fees. This section states Customer's exclusive remedy for IP claims. + +7.4 **Customer indemnification.** Customer shall defend and indemnify Provider against third-party claims arising from Customer content, Customer's breach of Section 1.4, or Customer's violation of law. + +7.5 **Disclaimer.** EXCEPT AS EXPRESSLY STATED, THE SERVICES ARE PROVIDED WITHOUT OTHER WARRANTIES, EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. AI-ASSISTED OUTPUTS (INCLUDING CLASSIFICATION, EXTRACTION, AND AGENT ARTIFACTS) ARE PROBABILISTIC; CUSTOMER IS RESPONSIBLE FOR HUMAN REVIEW WHERE OUTPUTS HAVE LEGAL OR REGULATORY EFFECT. + +### 8. Limitation of Liability + +8.1 NEITHER PARTY IS LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR LOST PROFITS OR REVENUE. + +8.2 **General cap.** EACH PARTY'S AGGREGATE LIABILITY IS CAPPED AT THE FEES PAID OR PAYABLE UNDER THIS AGREEMENT IN THE TWELVE (12) MONTHS PRECEDING THE FIRST EVENT GIVING RISE TO LIABILITY. **Super-cap:** for breaches of Section 6 (Confidentiality), breaches of the DPA or Section 4–5 security obligations, and IP indemnification under Section 7.3, the cap is TWO TIMES (2x) such fees. **Uncapped:** fraud, willful misconduct, Customer's payment obligations, and Customer's indemnification under Section 7.4 for claims arising from Customer's willful violation of law. + +8.3 All claims arising from the same event or series of connected events count as a single claim for the purposes of the caps in Section 8.2. + +### 9. General + +9.1 **Entire agreement; precedence.** This Agreement (Parts A–C, the Order Form, the SLA Exhibit, and the Standard Contractual Clauses where applicable) is the entire agreement and supersedes prior proposals and quotes, including {{quote_ref}}. Only the following provisions of the Stirling EULA & Commercial Terms are incorporated: Section 3 (Definitions), Section 8 (AI features), Section 10 (Self-hosted and desktop software), and Section 11 (Fair use). The website Terms of Service do not apply to this Agreement; without limitation, their arbitration and class-waiver provisions, online auto-renewal rules, unilateral-amendment provision, self-serve pricing, and clickwrap acceptance mechanism are expressly excluded. Precedence: Order Form → Standard Contractual Clauses (for international transfers) → DPA → MSA → SLA Exhibit → incorporated EULA sections → Documentation. + +9.2 **Governing law; venue.** Delaware law, excluding conflicts rules. Exclusive jurisdiction and venue in the state or federal courts located in San Francisco County, California, and the Parties consent to personal jurisdiction there. + +9.3 **Assignment.** Neither Party may assign without the other's consent, except to a successor in a merger, acquisition, or sale of substantially all assets, with notice. + +9.4 **Notices.** Written notices to the addresses on the Order Form; email permitted with confirmation of receipt. + +9.5 **Force majeure; independent contractors; waiver; severability.** Standard terms apply: neither Party is liable for delay caused by events beyond reasonable control; the Parties are independent contractors; failure to enforce is not waiver; unenforceable provisions are severed with the remainder in effect. + +9.6 **Publicity.** Neither Party may use the other's name or marks publicly without prior written consent, except Provider may identify Customer as a customer with Customer's prior approval of the specific use. + +9.7 **Suspension.** Provider may suspend the Services for material breach that threatens the security or integrity of the Services, with notice and opportunity to cure where practicable. Undisputed unpaid fees more than thirty (30) days late are grounds for suspension after ten (10) days' notice. + +### 10. Termination + +10.1 Either Party may terminate for material breach uncured thirty (30) days after written notice, or immediately upon the other's insolvency. + +10.2 On termination: Customer's access ends (self-hosted licenses expire per the license mechanism); each Party returns or destroys the other's Confidential Information; the DPA's deletion terms govern Customer Personal Data; Sections 3 (accrued fees), 6, 7, 8, 9, and 10 survive. + +10.3 If Customer terminates for Provider's uncured material breach, Provider refunds prepaid fees for the unused remainder of the then-current contract year. diff --git a/app/saas/src/main/resources/legal/eula/1.0.0/eula.md b/app/saas/src/main/resources/legal/eula/1.0.0/eula.md new file mode 100644 index 0000000000..f6dd32d5e1 --- /dev/null +++ b/app/saas/src/main/resources/legal/eula/1.0.0/eula.md @@ -0,0 +1,75 @@ +# Stirling EULA & Commercial Terms + +This document fills the EULA slot the website Terms of Service §5 already contemplates ("If a separate end-user license (EULA) accompanies software, that license governs to the extent of any conflict"). It carries the commercial terms of the actual product: the PDF Process meter, spend limits, prepaid capacity, the free allotment, trials, self-hosted licensing, and AI features. For customers under a signed Stirling Enterprise Agreement, that agreement controls. + +**Effective:** {{version_date}} · **Version:** {{version}} + +## 1. Agreement; precedence + +These EULA & Commercial Terms ("EULA") supplement the Stirling Terms of Service (stirling.com/legal/terms-of-service). If they conflict, this EULA controls for the software and the commercial terms below. Open-source components are governed by their own licenses, which control for those components. By clicking accept, creating a workspace, or using the software, you agree on behalf of yourself and, if applicable, the organization you represent ("you"). + +## 2. The products + +**The Stirling PDF Editor** is free: manual editing, the tool catalog, and team administration (including SSO) carry no subscription fee or per-seat charge, whether used in the browser, as a desktop application, or self-hosted. Usage limits on automated processing, fair-use rules (Section 11), support levels, and the feature set may change over time, and third-party costs (such as your own hosting) are yours. The Editor's open-source components remain available under their own licenses independently of this EULA. **The Stirling PDF Processor** is the paid platform that processes PDFs automatically — policies, pipelines, the Stirling Agent, and API processing — billed on the meter below. + +## 3. Definitions + +**"PDF Process"** — one policy execution, one pipeline run (regardless of the number of operations in its chain), or one Stirling Agent returned artifact (a processed file or summary, regardless of the number of messages that produced it), applied to one file. **"Data Processing"** — the data-volume component of the meter: files carry their first 25 MB included per file; volume past that is billed per Section 4. **"file"** — a document processed by the Processor. Chatting with the Stirling Agent is free; only returned artifacts meter. + +## 4. Metered billing (pay as you go) + +4.1 **Rates.** 1¢ per PDF Process, plus Data Processing at 1¢ per 25 MB increment past the first 25 MB of each file. Megabytes are decimal (1 MB = 1,000,000 bytes); size is measured once per file as submitted; increments round up ("part thereof" counts — a 26 MB file incurs one Data Processing increment, a 60 MB file incurs two). Rates may change on thirty (30) days' notice; changes apply prospectively. **Example:** two policies on a 3 MB contract = 2¢; two policies on a 60 MB scan set = 2¢ + 2¢ data = 4¢. + +4.2 **Free allotment.** New workspaces receive a one-time allotment of 500 PDF Processes. A file processed by two processes consumes two of the 500. When the allotment is exhausted, processing pauses until the Processor is switched on. + +4.3 **Invoices.** Usage is invoiced monthly on the 1st for the prior cycle, charged to your payment method on file (card or ACH debit). You authorize these charges. + +4.4 **Usage records.** The Processor's audit log is the system of record, subject to Section 4.5. Your Usage & Billing page shows consumption, and a per-file usage statement (name, size, processes, charge) is available for download. + +4.5 **Billing disputes.** You may dispute a charge or metering record in good faith within sixty (60) days of the invoice or charge date. We will investigate, provide the relevant usage-statement detail, and correct confirmed errors by credit or refund. The audit log is presumptively accurate but not conclusive; reasonable contrary evidence will be considered. Undisputed amounts remain payable. + +## 5. Spend limits + +5.1 You may set a monthly spend limit. By default, processing pauses when usage reaches the limit; queued documents resume when you raise the limit or the cycle resets. Nothing already processed is lost. + +5.2 If you enable **keep-processing** ("Keep processing if you hit your limit"), usage past the limit continues to accrue and be billed per Section 4; the limit then functions as a notification threshold. You can change the limit or the toggle at any time in Usage & Billing. + +## 6. Cancellation; downgrade + +You may revert to the free Editor plan at any time from Usage & Billing. Accrued usage remains payable. Your policies, configuration, and history are retained per the Terms of Service data-retention practices. + +## 7. Prepaid capacity (self-serve annual) + +7.1 **Offer.** You may prepay twelve (12) months of processing capacity for the price of ten (10) (the "12-for-10 rate"), sized at purchase. Payment by card, or by bank transfer against a generated invoice (net 30); prepaid capacity activates when payment clears. + +7.2 **No renewal of prepaid capacity; automatic transition to pay-as-you-go.** Prepaid capacity does not renew for another prepaid term. At purchase, you affirmatively consent to the following transition, which is disclosed before you pay: when the term ends, metered billing (Section 4) applies automatically at then-current rates so processing does not pause. We remind you thirty (30) days before term end; the reminder states the metered rates that will apply and how to cancel or revert to the free Editor plan (one click in Usage & Billing). + +7.3 **Consumption; overage; expiry.** Capacity draws down in PDF Processes. If you exhaust capacity mid-term, you may top up at the same 12-for-10 rate, or metered billing applies at list rates (with a card on file) or processing pauses (without one). Unused capacity expires at term end and is not refunded and does not roll over. + +7.4 **Cap.** Self-serve prepaid capacity is limited to 1,000,000 PDF Processes per year; larger commitments are available under a Stirling Enterprise Agreement. + +## 8. AI features + +AI features — classification, extraction, redaction-assist, and the Stirling Agent — are optional. They run only when you invoke a feature that uses them, and an administrator can disable them for the workspace; the rest of the Processor works without them. When used, they call machine-learning models from the providers listed at {{subprocessor_url}}. Currently: **Anthropic** (Claude models), which receives prompts and the document text or excerpts needed to perform the requested task; and **Voyage AI** (embedding models), which receives extracted text excerpts solely to generate embeddings when you enable Ingestion/RAG features. Only the document text or excerpts needed for the requested feature are sent — not your whole files — and only when that feature runs. AI charges are included in the price of whatever runs — there is no separate AI surcharge. Your content is not used to train models, by us or by these providers (verified against our signed provider agreements). AI outputs are probabilistic; review outputs before relying on them where accuracy has legal effect. + +## 9. Evaluations and trials + +Enterprise trials run fourteen (14) days, require no payment method, and are provided for evaluation only, AS IS, without service level commitments. Either party may end an evaluation at any time; on expiry your workspace continues on the free Editor plan. + +## 10. Self-hosted and desktop software + +10.1 **License.** We grant you a non-exclusive, non-transferable license to install and run the Editor and, with an active plan, the self-hosted Processor, for your internal business use. Open-source components remain under their own licenses. + +10.2 **License validation and metering.** Self-hosted Processor deployments validate their license online and transmit usage metering data (process counts, file sizes, and file hashes for billing integrity, and diagnostic data — never file content or file names) to Stirling. File names used for unique PDF identification remain on your server and are not transmitted. Air-gapped deployments verify a signed activation bundle offline and reconcile usage periodically. You will not disable, circumvent, or falsify validation or metering. **The meter is the same regardless of where the software runs.** + +10.3 **Updates.** Security patches and upgrades are made available for supported versions; some updates may install automatically per Terms of Service §5. + +10.4 **Authorized users and administration.** "Authorized Users" are your employees, and the employees of your affiliates and contractors working on your behalf, whom you provision through your workspace. You are responsible for your users' credentials, your administrators' actions, and your users' compliance with this EULA. One workspace serves one legal entity and its affiliates; serving unrelated third parties requires a separate agreement. You may not redistribute the Processor or offer it as a hosted service to others. On termination or downgrade, self-hosted Processor licenses expire per the license mechanism; installed Editor copies remain usable under the free plan. We may verify license compliance through the validation mechanism in Section 10.2. + +## 11. Fair use + +Free-tier and flat-price features are subject to fair use: we may throttle or decline usage patterns that abuse free processing (for example, automation disguised as manual editing) after notice where practicable. + +## 12. Changes to this EULA + +We may update this EULA. Material changes take effect thirty (30) days after notice. Changes that materially increase your price or reduce your rights take effect at your next billing cycle or prepaid term start, or upon your affirmative acceptance — whichever comes first — except changes strictly necessary for legal compliance or security, which may take effect sooner with notice. Continued use after the effective date is acceptance. Version history is available at {{eula_url}}. diff --git a/app/saas/src/main/resources/legal/manifest.json b/app/saas/src/main/resources/legal/manifest.json new file mode 100644 index 0000000000..c34559275d --- /dev/null +++ b/app/saas/src/main/resources/legal/manifest.json @@ -0,0 +1,38 @@ +{ + "subprocessorUrl": "https://www.stirlingpdf.com/legal/subprocessors", + "eulaUrl": "https://www.stirlingpdf.com/legal/eula", + "documents": { + "enterprise-agreement": { + "label": "SEA", + "displayName": "Stirling Enterprise Agreement", + "version": "0.9.1", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["msa.md", "@order-form", "dpa.md"] + }, + "eula": { + "label": "EULA", + "displayName": "Stirling EULA & Commercial Terms", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["eula.md"] + }, + "sla": { + "label": "SLA", + "displayName": "Stirling SLA Exhibit", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["sla.md"] + }, + "subprocessors": { + "label": "SUBP", + "displayName": "Stirling Subprocessors", + "version": "1.0.0", + "effectiveDate": "2026-07-10", + "status": "draft", + "parts": ["subprocessors.md"] + } + } +} diff --git a/app/saas/src/main/resources/legal/sla/1.0.0/sla.md b/app/saas/src/main/resources/legal/sla/1.0.0/sla.md new file mode 100644 index 0000000000..42271171d5 --- /dev/null +++ b/app/saas/src/main/resources/legal/sla/1.0.0/sla.md @@ -0,0 +1,37 @@ +# SLA Exhibit — Stirling Enterprise Agreement + +Referenced by the Order Form's service-level row and MSA §5.2. One document, three tiers; the Order Form's tier selection determines the applicable column. Uptime is a target, not a credited commitment: no service credits apply at any tier. Tiers differentiate support response, channels, and people. + +## 1. Availability + +For Stirling Cloud deployments, Provider targets **99.9% monthly uptime**, measured at the API and console endpoints, excluding scheduled maintenance announced at least 48 hours in advance and events beyond Provider's reasonable control. Current and historical status is published at the system status page. No service credits apply; persistent material failure to meet the target is addressed through MSA §7.1 (performance warranty and remedies) and §10 (termination for material breach). + +Self-hosted and Air-gapped deployments: availability of the runtime is Customer's responsibility; this Section applies to Provider's license, metering, and update services. + +## 2. Support tiers + +| | **Standard** | **Priority** | **Dedicated** | +| --- | --- | --- | --- | +| Included with | Every Enterprise Agreement | Every Enterprise Agreement | The Dedicated SE/CSM line item ($30,000/yr) | +| Hours | Business hours (Mon–Fri, 9:00–18:00 US Eastern, excl. US holidays) | Business hours + extended (7:00–21:00 US Eastern) | 24×7 for Severity 1 | +| Channels | Email, in-product | Email, in-product, private Slack/Teams channel | All Priority channels + named Solutions Engineer and CSM | +| Severity 1 first response (production down / processing halted org-wide) | 8 business hours | 4 hours | 1 hour, 24×7 | +| Severity 2 (major feature degraded, no workaround) | Next business day | 8 business hours | 4 hours | +| Severity 3 (minor defect, workaround exists) | 3 business days | 2 business days | Next business day | +| Severity 4 (question, cosmetic) | 5 business days | 3 business days | 2 business days | +| Escalation path | Support queue | Support lead | Named SE → CSM → Provider executive | +| Business reviews | — | — | Quarterly, where the QBR line item is elected | + +First response = a qualified human engaging with the issue, not an acknowledgment autoresponder. Resolution times are not committed; Provider works Severity 1 issues continuously within the tier's hours until resolved or downgraded. + +## 3. Severity is set by Customer, subject to reasonable reclassification + +Customer designates severity at filing; Provider may reclassify with explanation. Severity 1 requires production impact in a live (non-evaluation) environment. + +## 4. Maintenance and updates + +Scheduled maintenance is announced at least 48 hours ahead and targeted at low-usage windows. Security patches for supported versions ship to all tiers at no charge (MSA §5.4). Trials and evaluations are provided AS IS and are outside this Exhibit (EULA §9). + +## 5. Exclusions + +This Exhibit does not apply to: issues caused by Customer's environment, modifications, or third-party systems; usage exceeding the fair-use provisions; Preview/Beta features; or Force Majeure events (MSA §9.5). diff --git a/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md b/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md new file mode 100644 index 0000000000..670ae53150 --- /dev/null +++ b/app/saas/src/main/resources/legal/subprocessors/1.0.0/subprocessors.md @@ -0,0 +1,19 @@ +# Stirling PDF — Subprocessors + +Referenced by DPA §C5 and Annex III, and by EULA §8. Changes to this list carry 30 days' notice per DPA §C5. Last updated: {{version_date}}. + +Stirling PDF, Inc. uses the following subprocessors to provide the Services. Customer files are processed within Stirling's own infrastructure. AI features are optional and can be disabled; where they are used, AI providers receive only the document text or excerpts needed for the requested feature, never whole customer files. + +| Subprocessor | Purpose | Data processed | Location | +| --- | --- | --- | --- | +| **Amazon Web Services (AWS)** | Cloud infrastructure and storage for Stirling Cloud | Customer files (encrypted at rest), account and usage data | United States (EU region availability per deployment — see DPA §C8 note) | +| **Stripe** | Payment processing | Billing contact and transaction data. Payment card details go directly to Stripe, which acts as an independent controller for them | United States | +| **Supabase** | Account and workspace data infrastructure | Account, workspace, and configuration data | United States | +| **Google** | Transactional and operational email delivery | Names, email addresses, message content of service emails | United States | +| **Anthropic** | AI models (Claude) powering the Stirling Agent and AI-assisted features | Prompts and the document text or excerpts needed for the requested feature — never whole files; only where AI features are used | United States | +| **Voyage AI** | Embedding models for Ingestion/RAG features | Extracted text excerpts, only where the customer enables Ingestion/RAG, solely to generate embeddings — never customer files | United States | +| **PostHog** | Product telemetry and usage analytics | Pseudonymous usage events and diagnostic data — never file content | European Union (EU-hosted) | + +Neither AI provider uses customer data for model training (contractually confirmed). + +Self-hosted and air-gapped deployments: customer files remain in the customer's environment; Stirling receives license-validation and metering data only (process counts, file sizes, file hashes — never file names or content). diff --git a/app/saas/src/test/java/stirling/software/saas/config/MigrationOwnedSchemaFilterTest.java b/app/saas/src/test/java/stirling/software/saas/config/MigrationOwnedSchemaFilterTest.java new file mode 100644 index 0000000000..956654384e --- /dev/null +++ b/app/saas/src/test/java/stirling/software/saas/config/MigrationOwnedSchemaFilterTest.java @@ -0,0 +1,129 @@ +package stirling.software.saas.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.util.Properties; + +import org.hibernate.mapping.Table; +import org.hibernate.tool.schema.spi.SchemaFilter; +import org.hibernate.tool.schema.spi.SchemaFilterProvider; +import org.junit.jupiter.api.Test; + +/** + * Covers {@link MigrationOwnedSchemaFilter} and, just as importantly, its wiring. + * + *

{@link SaasSchemaOwnershipTest} proves the register is complete; nothing proved the filter + * applies it, or that Hibernate is even asking. A typo in the {@code + * hibernate.hbm2ddl.schema_filter_provider} key, a stale fully-qualified name after a package move, + * or a getter returning null would all leave every migration-owned table exposed to {@code + * ddl-auto} with a fully green build. Hence the property assertion below, which is the only thing + * here that would catch that. + */ +class MigrationOwnedSchemaFilterTest { + + private static final String FILTER_PROPERTY = + "spring.jpa.properties.hibernate.hbm2ddl.schema_filter_provider"; + + private final MigrationOwnedSchemaFilter filter = new MigrationOwnedSchemaFilter(); + + /** "orm" is Hibernate's own default contributor; the value is irrelevant to the filter. */ + private static Table table(String name) { + return new Table("orm", name); + } + + @Test + void migrationOwnedTablesAreHiddenFromHibernate() { + assertThat(filter.includeTable(table("teams"))).isFalse(); + assertThat(filter.includeTable(table("users"))).isFalse(); + assertThat(filter.includeTable(table("team_memberships"))).isFalse(); + assertThat(filter.includeTable(table("payg_instance_usage"))).isFalse(); + } + + @Test + void inheritedTablesStayUnderHibernate() { + assertThat(filter.includeTable(table("folders"))).isTrue(); + assertThat(filter.includeTable(table("stored_files"))).isTrue(); + assertThat(filter.includeTable(table("api_keys"))).isTrue(); + } + + @Test + void anUnknownTableIsLeftToHibernate() { + // Fail-open is the right default: an unrecognised table is either brand new or from a + // module + // we do not know about, and SaasSchemaOwnershipTest is what stops it staying unrecognised. + assertThat(filter.includeTable(table("no_such_table"))).isTrue(); + } + + @Test + void casingDoesNotDefeatTheFilter() { + assertThat(filter.includeTable(table("TEAMS"))).isFalse(); + assertThat(filter.includeTable(table("Team_Memberships"))).isFalse(); + } + + /** + * Foreign keys from an inherited table into a migration-owned one survive the filter. + * + *

Worth pinning, because it is not obvious and it decides whether a preview branch keeps + * referential integrity. {@code folders}, {@code stored_files} and {@code file_shares} all + * reference {@code users}/{@code teams}, which the filter excludes. Hibernate 7.2's {@code + * SchemaCreatorImpl.createForeignKeys} (and {@code AbstractSchemaMigrator.applyForeignKeys}) + * tests {@code includeTable} against the *owning* table only, then emits every foreign key on + * it; the referenced table is never consulted. So the constraints are still created and a + * branch matches staging. + * + *

The one thing this depends on is ordering: the referenced tables have to exist first. They + * do, because a Supabase branch runs its migrations at build time and the app connects after. + */ + @Test + void foreignKeysIntoMigrationOwnedTablesAreStillEmitted() { + assertThat(filter.includeTable(table("folders"))).isTrue(); + assertThat(filter.includeTable(table("file_shares"))).isTrue(); + assertThat(filter.includeTable(table("users"))).isFalse(); + assertThat(filter.includeTable(table("teams"))).isFalse(); + } + + @Test + void everySchemaActionGetsTheSameFilter() { + assertThat(filter.getCreateFilter()).isSameAs(filter); + assertThat(filter.getMigrateFilter()).isSameAs(filter); + assertThat(filter.getDropFilter()).isSameAs(filter); + assertThat(filter.getTruncatorFilter()).isSameAs(filter); + assertThat(filter.getValidateFilter()).isSameAs(filter); + } + + @Test + void namespacesAndSequencesAreNeverFiltered() { + // Both share the stirling_pdf namespace, so filtering it would take the inherited tables + // with it. Neither argument is read, so nulls are fine and keep the test free of Hibernate + // bootstrap machinery. + assertThat(filter.includeNamespace(null)).isTrue(); + assertThat(filter.includeSequence(null)).isTrue(); + } + + @Test + void theFilterIsActuallyWiredIntoHibernate() throws Exception { + Properties properties = new Properties(); + try (InputStream in = getClass().getResourceAsStream("/application-saas.properties")) { + assertThat(in) + .as("application-saas.properties must be on the test classpath to check wiring") + .isNotNull(); + properties.load(in); + } + + String configured = properties.getProperty(FILTER_PROPERTY); + assertThat(configured) + .as( + "%s is unset, so Hibernate installs its default filter and every" + + " migration-owned table is back under ddl-auto", + FILTER_PROPERTY) + .isNotBlank(); + + Class wired = Class.forName(configured.trim()); + assertThat(SchemaFilterProvider.class) + .as("Hibernate only accepts a SchemaFilterProvider here") + .isAssignableFrom(wired); + assertThat(SchemaFilter.class).isAssignableFrom(wired); + assertThat(wired).isEqualTo(MigrationOwnedSchemaFilter.class); + } +} diff --git a/app/saas/src/test/java/stirling/software/saas/config/SaasSchemaOwnershipTest.java b/app/saas/src/test/java/stirling/software/saas/config/SaasSchemaOwnershipTest.java new file mode 100644 index 0000000000..e07b1042e7 --- /dev/null +++ b/app/saas/src/test/java/stirling/software/saas/config/SaasSchemaOwnershipTest.java @@ -0,0 +1,167 @@ +package stirling.software.saas.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.persistence.autoconfigure.EntityScan; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +import stirling.software.proprietary.security.configuration.DatabaseConfig; + +/** + * Makes {@link SaasSchemaOwnership} binding rather than decorative. + * + *

Every {@code @Entity} the SaaS app maps has to be declared as owned by either the Supabase + * migrations or Hibernate. Adding an entity without saying which fails here, at build time, instead + * of months later on a preview branch that has no such table. That is not hypothetical: {@code + * payg_instance_usage} shipped with an entity and no migration and went unnoticed until a branch + * tried to use it. + * + *

"Maps" is meant precisely: the scan covers the packages named by the {@code @EntityScan} + * declarations the app actually boots with, not everything under {@code stirling.software}. See + * {@link #mappedPackages()}. Note this only enforces one direction — {@link SaasSchemaOwnership} + * documents the drift it cannot see. + */ +class SaasSchemaOwnershipTest { + + /** + * The packages the running app actually maps, read off the two {@code @EntityScan} declarations + * that define them rather than hardcoded. + * + *

Scanning all of {@code stirling.software} would be easier and wrong in a quiet way: it is + * a superset, so it would force ownership declarations for entities Hibernate never sees and + * let the register claim tables that do not exist as far as the SaaS app is concerned. Deriving + * the list means this test measures the same set Hibernate does, and follows a package being + * added or moved without anyone updating it here. + */ + private static Set mappedPackages() { + Set packages = new TreeSet<>(); + for (Class config : List.of(SaasJpaConfig.class, DatabaseConfig.class)) { + EntityScan scan = config.getAnnotation(EntityScan.class); + assertThat(scan) + .as("%s must carry @EntityScan, or its entities are not mapped", config) + .isNotNull(); + packages.addAll(Arrays.asList(scan.value())); + } + return packages; + } + + private static TreeMap mappedTables() { + ClassPathScanningCandidateComponentProvider scanner = + new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class)); + TreeMap byTable = new TreeMap<>(); + for (String basePackage : mappedPackages()) { + for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) { + String className = bd.getBeanClassName(); + Class type; + try { + type = + ClassUtils.forName( + className, SaasSchemaOwnershipTest.class.getClassLoader()); + } catch (ClassNotFoundException | LinkageError e) { + continue; // not on this module's runtime classpath; nothing to own + } + Table table = type.getAnnotation(Table.class); + String name = + table != null && !table.name().isBlank() + ? table.name() + : camelToSnake(type.getSimpleName()); + byTable.put(name.toLowerCase(), className); + } + } + return byTable; + } + + /** Mirrors Spring Boot's default CamelCaseToUnderscoresNamingStrategy for an unnamed @Table. */ + private static String camelToSnake(String name) { + return name.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase(); + } + + @Test + void everyEntityTableIsOwnedByExactlyOneSide() { + TreeMap mapped = mappedTables(); + assertThat(mapped) + .as("entity scan found nothing, so this test proves nothing") + .isNotEmpty(); + // The scan is derived from @EntityScan now, so a package quietly dropped from either + // declaration would shrink it and weaken this test rather than fail it. These four straddle + // the two declarations, so losing either side fails here instead of silently checking less. + assertThat(mapped.keySet()) + .as("both @EntityScan declarations must have contributed to the scan") + .contains("users", "teams", "payg_instance_usage", "folders"); + + Set undeclared = new TreeSet<>(); + Set both = new TreeSet<>(); + for (String table : mapped.keySet()) { + boolean migration = SaasSchemaOwnership.MIGRATION_OWNED.contains(table); + boolean hibernate = SaasSchemaOwnership.HIBERNATE_MANAGED.contains(table); + if (migration && hibernate) both.add(table); + if (!migration && !hibernate) undeclared.add(table); + } + + assertThat(undeclared) + .as( + """ + These entity tables are not declared in SaasSchemaOwnership, so nobody owns \ + them. Decide and add each to exactly one set: + - MIGRATION_OWNED: also add a migration in Stirling-PDF-SaaS, or the table \ + will not exist on a fresh preview branch. + - HIBERNATE_MANAGED: only correct for a table inherited from the \ + self-hosted app that no Supabase migration creates. + Offending tables -> entities: %s""" + .formatted( + undeclared.stream() + .map(t -> t + " (" + mapped.get(t) + ")") + .toList())) + .isEmpty(); + + assertThat(both) + .as("declared as owned by both sides, which is the one thing it cannot be") + .isEmpty(); + } + + @Test + void theTwoSetsDoNotOverlap() { + Set overlap = new TreeSet<>(SaasSchemaOwnership.MIGRATION_OWNED); + overlap.retainAll(SaasSchemaOwnership.HIBERNATE_MANAGED); + assertThat(overlap).isEmpty(); + } + + @Test + void tableNamesAreLowercaseSoLookupsCannotMiss() { + // isMigrationOwned() lowercases its input; a capital in either set would be unreachable. + assertThat(SaasSchemaOwnership.MIGRATION_OWNED) + .allSatisfy(t -> assertThat(t).isEqualTo(t.toLowerCase())); + assertThat(SaasSchemaOwnership.HIBERNATE_MANAGED) + .allSatisfy(t -> assertThat(t).isEqualTo(t.toLowerCase())); + } + + @Test + void migrationOwnedTablesIncludeTheOnesThatBitUs() { + // team_memberships is the table an old ddl-auto run widened; payg_instance_usage is the one + // that had an entity and no migration. Both must be on the migrations' side of the line. + assertThat(SaasSchemaOwnership.MIGRATION_OWNED) + .contains("team_memberships", "payg_instance_usage", "teams", "users"); + } + + @Test + void isMigrationOwnedIsCaseInsensitiveAndNullSafe() { + assertThat(SaasSchemaOwnership.isMigrationOwned("TEAM_MEMBERSHIPS")).isTrue(); + assertThat(SaasSchemaOwnership.isMigrationOwned("team_memberships")).isTrue(); + assertThat(SaasSchemaOwnership.isMigrationOwned(null)).isFalse(); + assertThat(SaasSchemaOwnership.isMigrationOwned("no_such_table")).isFalse(); + } +} diff --git a/app/saas/src/test/java/stirling/software/saas/payg/api/WalletSnapshotResponseTest.java b/app/saas/src/test/java/stirling/software/saas/payg/api/WalletSnapshotResponseTest.java index 55dfdb40e3..7960def2a9 100644 --- a/app/saas/src/test/java/stirling/software/saas/payg/api/WalletSnapshotResponseTest.java +++ b/app/saas/src/test/java/stirling/software/saas/payg/api/WalletSnapshotResponseTest.java @@ -43,7 +43,8 @@ class WalletSnapshotResponseTest { /* prepaidUnitsRemaining= */ 40_000L, /* prepaidUnitsTotal= */ 120_000L, /* prepaidExpiresAt= */ "2027-06-01", - /* billingMode= */ "prepaid"); + /* billingMode= */ "prepaid", + /* bundleRatePerCreditMinor= */ new BigDecimal("1")); } @Test @@ -123,10 +124,12 @@ class WalletSnapshotResponseTest { 0L, 0L, null, - "payg"); + "payg", + null); assertThat(free.billableLimit()).isNull(); assertThat(free.pricePerDocMinor()).isNull(); + assertThat(free.bundleRatePerCreditMinor()).isNull(); assertThat(free.currency()).isNull(); assertThat(free.estimatedBillMinor()).isNull(); assertThat(free.capUsd()).isNull(); diff --git a/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java b/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java new file mode 100644 index 0000000000..86f361ff87 --- /dev/null +++ b/app/saas/src/test/java/stirling/software/saas/procurement/service/ProcurementTrialRestartPolicyTest.java @@ -0,0 +1,44 @@ +package stirling.software.saas.procurement.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import stirling.software.saas.procurement.model.ProcurementDeal; + +/** + * Which stages may (re)start a trial is a security policy, not a convenience. + * + *

From the agreement stage onward a deal's {@code licenseRef} points at the committed annual + * licence. Starting a trial replaces it with a fresh 14-day key, rewinds the stage and resets the + * extension counter — so an unguarded restart would downgrade a paying customer's entitlement while + * Stripe kept billing them. This pins the allowed set so widening it has to be deliberate. + */ +class ProcurementTrialRestartPolicyTest { + + @Test + @DisplayName("allowed before a deal exists, while exploring, and within the trial") + void allowsOnlyPreCommitmentStages() { + assertThat(ProcurementService.canStartTrial(null)).isTrue(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_EXPLORING)).isTrue(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_TRIAL)).isTrue(); + } + + @Test + @DisplayName("refused once the deal is quoting or beyond") + void refusesCommittedStages() { + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_QUOTE)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_AGREEMENT)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_PAYMENT)).isFalse(); + assertThat(ProcurementService.canStartTrial(ProcurementDeal.STAGE_LIVE)).isFalse(); + } + + @Test + @DisplayName("an unrecognised stage is refused, not waved through") + void refusesUnknownStages() { + // A stage added later, or a hand-edited row, must fail closed. + assertThat(ProcurementService.canStartTrial("renewal")).isFalse(); + assertThat(ProcurementService.canStartTrial("")).isFalse(); + } +} diff --git a/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterMoreTest.java b/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterMoreTest.java index 87e100375f..f6181dcebe 100644 --- a/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterMoreTest.java +++ b/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterMoreTest.java @@ -315,14 +315,13 @@ class SupabaseAuthenticationFilterMoreTest { local.setSupabaseId(supabaseId); local.setAuthenticationType(AuthenticationType.ANONYMOUS); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); - when(userService.saveUser(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); - when(saasTeamService.ensurePersonalTeam(any(User.class))).thenReturn(new Team()); + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) + .thenAnswer(inv -> inv.getArgument(0)); bearer("tok"); filter.doFilter(request, response, chain); - verify(userService).saveUser(any(User.class)); - verify(saasTeamService).ensurePersonalTeam(any(User.class)); + verify(saasTeamService).saveUserWithPersonalTeam(any(User.class)); assertThat(local.getEmail()).isEqualTo("real@example.com"); assertThat(local.getUsername()).isEqualTo("real@example.com"); assertThat(local.getAuthenticationType()) @@ -342,8 +341,8 @@ class SupabaseAuthenticationFilterMoreTest { local.setSupabaseId(supabaseId); local.setAuthenticationType(AuthenticationType.ANONYMOUS); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); - when(userService.saveUser(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); - when(saasTeamService.ensurePersonalTeam(any(User.class))).thenReturn(new Team()); + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) + .thenAnswer(inv -> inv.getArgument(0)); bearer("tok"); filter.doFilter(request, response, chain); @@ -365,7 +364,7 @@ class SupabaseAuthenticationFilterMoreTest { local.setSupabaseId(supabaseId); local.setAuthenticationType(AuthenticationType.ANONYMOUS); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenThrow(new DataIntegrityViolationException("email exists")); bearer("tok"); @@ -489,12 +488,13 @@ class SupabaseAuthenticationFilterMoreTest { org.mockito.Mockito.doThrow(new DataIntegrityViolationException("dup")) .when(supabaseUserService) .createSupabaseUser(eq(supabaseId), any(), eq(false)); - when(userService.saveUser(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) + .thenAnswer(inv -> inv.getArgument(0)); bearer("tok"); filter.doFilter(request, response, chain); - verify(userService, times(1)).saveUser(any(User.class)); + verify(saasTeamService, times(1)).saveUserWithPersonalTeam(any(User.class)); assertThat(SecurityContextHolder.getContext().getAuthentication()) .isInstanceOf(EnhancedJwtAuthenticationToken.class); } @@ -516,7 +516,7 @@ class SupabaseAuthenticationFilterMoreTest { filter.doFilter(request, response, chain); assertThat(response.getStatus()).isEqualTo(401); - verify(userService, never()).saveUser(any()); + verify(saasTeamService, never()).saveUserWithPersonalTeam(any()); } @Test @@ -533,13 +533,13 @@ class SupabaseAuthenticationFilterMoreTest { when(userService.findBySupabaseId(supabaseId)) .thenReturn(Optional.empty()) .thenReturn(Optional.of(winner)); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenThrow(new DataIntegrityViolationException("dup user")); bearer("tok"); filter.doFilter(request, response, chain); - // Race loser does not run first-time init (ensurePersonalTeam). + // The winner committed user and team together, so the loser just adopts its row. verify(saasTeamService, never()).ensurePersonalTeam(any()); assertThat(SecurityContextHolder.getContext().getAuthentication()) .isInstanceOf(EnhancedJwtAuthenticationToken.class); @@ -554,7 +554,7 @@ class SupabaseAuthenticationFilterMoreTest { when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUser(supabaseId, "lost@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenThrow(new DataIntegrityViolationException("dup user")); bearer("tok"); @@ -564,25 +564,99 @@ class SupabaseAuthenticationFilterMoreTest { } @Test - @DisplayName("personal team creation failure for a new user is swallowed") - void personalTeamFailureSwallowed() throws Exception { + @DisplayName("personal team creation failure fails the request, it is not swallowed") + void personalTeamFailureFailsAuth() throws Exception { UUID supabaseId = UUID.randomUUID(); Jwt jwt = fullJwt(supabaseId, "team@example.com", false, "email"); when(jwtDecoder.decode("tok")).thenReturn(jwt); when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUser(supabaseId, "team@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any(User.class))).thenAnswer(inv -> inv.getArgument(0)); - when(saasTeamService.ensurePersonalTeam(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenThrow(new IllegalStateException("team boom")); bearer("tok"); filter.doFilter(request, response, chain); - // Auth still succeeds even though team creation failed. - assertThat(SecurityContextHolder.getContext().getAuthentication()) - .isInstanceOf(EnhancedJwtAuthenticationToken.class); - verify(userService, times(1)).saveUser(any(User.class)); + // A teamless account has no portal access, so a failed provision must surface + // rather than admit a half-built user. + assertThat(response.getStatus()).isEqualTo(401); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + } + + @Nested + @DisplayName("Existing accounts are never re-provisioned on the request path") + class ExistingAccountProvisioning { + + private User existingWebUser(UUID supabaseId) { + User local = newUser("real@example.com"); + local.setSupabaseId(supabaseId); + local.setAuthenticationType(AuthenticationType.WEB); + return local; + } + + @Test + @DisplayName("a teamless account is left alone, not healed on every request") + void teamlessAccountIsNotHealed() throws Exception { + UUID supabaseId = UUID.randomUUID(); + when(jwtDecoder.decode("tok")) + .thenReturn(fullJwt(supabaseId, "real@example.com", false, "email")); + when(supabaseUserService.getUser(supabaseId)) + .thenReturn(supabaseUser(supabaseId, "real@example.com", false)); + + User local = existingWebUser(supabaseId); + when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); + + bearer("tok"); + filter.doFilter(request, response, chain); + + // Healing here would run per request with no mutual exclusion, so parallel + // requests would mint duplicate teams. Provisioning belongs to signup alone. + verify(saasTeamService, never()).ensurePersonalTeam(any(User.class)); + verify(saasTeamService, never()).saveUserWithPersonalTeam(any(User.class)); + assertThat(local.getTeam()).isNull(); + } + + @Test + @DisplayName("an account that already has a team is left alone") + void noOpWhenTeamPresent() throws Exception { + UUID supabaseId = UUID.randomUUID(); + when(jwtDecoder.decode("tok")) + .thenReturn(fullJwt(supabaseId, "real@example.com", false, "email")); + when(supabaseUserService.getUser(supabaseId)) + .thenReturn(supabaseUser(supabaseId, "real@example.com", false)); + + User local = existingWebUser(supabaseId); + Team existing = new Team(); + local.setTeam(existing); + when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); + + bearer("tok"); + filter.doFilter(request, response, chain); + + verify(saasTeamService, never()).ensurePersonalTeam(any(User.class)); + assertThat(local.getTeam()).isSameAs(existing); + } + + @Test + @DisplayName("a guest session is never given a team") + void guestStaysTeamless() throws Exception { + UUID supabaseId = UUID.randomUUID(); + when(jwtDecoder.decode("tok")).thenReturn(fullJwt(supabaseId, null, true, "email")); + when(supabaseUserService.getUser(supabaseId)) + .thenReturn(supabaseUser(supabaseId, null, true)); + + User local = newUser("anon_guest"); + local.setSupabaseId(supabaseId); + local.setAuthenticationType(AuthenticationType.ANONYMOUS); + when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.of(local)); + + bearer("tok"); + filter.doFilter(request, response, chain); + + verify(saasTeamService, never()).ensurePersonalTeam(any(User.class)); + assertThat(local.getTeam()).isNull(); } } } diff --git a/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterTest.java b/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterTest.java index 58cf2d72a0..d78be84189 100644 --- a/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterTest.java +++ b/app/saas/src/test/java/stirling/software/saas/security/SupabaseAuthenticationFilterTest.java @@ -168,7 +168,7 @@ class SupabaseAuthenticationFilterTest { when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUserMatching(supabaseId, "bob@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any())).thenAnswer(inv -> inv.getArgument(0)); + when(saasTeamService.saveUserWithPersonalTeam(any())).thenAnswer(inv -> inv.getArgument(0)); request.setRequestURI("/api/v1/something"); request.setMethod("POST"); @@ -176,10 +176,9 @@ class SupabaseAuthenticationFilterTest { filter.doFilter(request, response, chain); - verify(userService, times(1)).saveUser(any(User.class)); verify(supabaseUserService).createSupabaseUser(supabaseId, "bob@example.com", false); - // New users get their own personal team, never the shared Default team. - verify(saasTeamService).ensurePersonalTeam(any(User.class)); + // Own personal team, never the shared Default team, written with the user. + verify(saasTeamService, times(1)).saveUserWithPersonalTeam(any(User.class)); verify(teamService, never()).getOrCreateDefaultTeam(); assertThat(SecurityContextHolder.getContext().getAuthentication()) .isInstanceOf(EnhancedJwtAuthenticationToken.class); @@ -194,7 +193,7 @@ class SupabaseAuthenticationFilterTest { when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUserMatching(supabaseId, "carol@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenAnswer( inv -> { User u = inv.getArgument(0); @@ -210,7 +209,7 @@ class SupabaseAuthenticationFilterTest { filter.doFilter(request, response, chain); - verify(userService, times(1)).saveUser(any(User.class)); + verify(saasTeamService, times(1)).saveUserWithPersonalTeam(any(User.class)); } @Test @@ -222,7 +221,7 @@ class SupabaseAuthenticationFilterTest { when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUserMatching(supabaseId, "dave@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenAnswer( inv -> { User u = inv.getArgument(0); @@ -238,7 +237,7 @@ class SupabaseAuthenticationFilterTest { filter.doFilter(request, response, chain); - verify(userService, times(1)).saveUser(any(User.class)); + verify(saasTeamService, times(1)).saveUserWithPersonalTeam(any(User.class)); } @Test @@ -250,7 +249,7 @@ class SupabaseAuthenticationFilterTest { when(supabaseUserService.getUser(supabaseId)) .thenReturn(supabaseUserMatching(supabaseId, "eve@example.com", false)); when(userService.findBySupabaseId(supabaseId)).thenReturn(Optional.empty()); - when(userService.saveUser(any(User.class))) + when(saasTeamService.saveUserWithPersonalTeam(any(User.class))) .thenAnswer( inv -> { User u = inv.getArgument(0); @@ -266,7 +265,7 @@ class SupabaseAuthenticationFilterTest { filter.doFilter(request, response, chain); - verify(userService, times(1)).saveUser(any(User.class)); + verify(saasTeamService, times(1)).saveUserWithPersonalTeam(any(User.class)); } @Test diff --git a/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java b/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java index 70cd360d7c..2c37980a5c 100644 --- a/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java +++ b/app/saas/src/test/java/stirling/software/saas/security/TeamLeaderPolicyManagementAuthorityTest.java @@ -32,6 +32,18 @@ class TeamLeaderPolicyManagementAuthorityTest { assertFalse(authority().canEditPolicies()); } + @Test + void teamLeaderMayTriggerPolicies() { + when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(true); + assertTrue(authority().canTriggerPolicies()); + } + + @Test + void nonLeaderMayNotTriggerPolicies() { + when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(false); + assertFalse(authority().canTriggerPolicies()); + } + @Test void currentUserTeamIdDelegatesToTeamSecurity() { when(teamSecurity.currentUserTeamId()).thenReturn(9L); diff --git a/build.gradle b/build.gradle index f9bd5e060a..62417d45df 100644 --- a/build.gradle +++ b/build.gradle @@ -6,9 +6,9 @@ plugins { id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" id "com.diffplug.spotless" version "8.8.0" - id "com.github.jk1.dependency-license-report" version "3.1.2" + id "com.github.jk1.dependency-license-report" //id "nebula.lint" version "19.0.3" - id "org.sonarqube" version "7.2.3.7755" + id "org.sonarqube" version "7.3.1.8318" } import com.github.jk1.license.render.* @@ -18,32 +18,37 @@ import groovy.xml.XmlSlurper import org.gradle.api.JavaVersion import org.gradle.api.tasks.testing.Test import org.gradle.jvm.toolchain.JavaLanguageVersion +import stirling.software.gradle.ModuleLicenseOverrideFilter ext { springBootVersion = "4.0.6" - pdfboxVersion = "3.0.7" - imageioVersion = "3.13.1" + pdfboxVersion = "3.0.8" + imageioVersion = "3.14.0" lombokVersion = "1.18.46" - bouncycastleVersion = "1.84" - springSecuritySamlVersion = "7.0.5" + bouncycastleVersion = "1.85" + springSecuritySamlVersion = "7.1.0" openSamlVersion = "5.2.1" commonmarkVersion = "0.28.0" googleJavaFormatVersion = "1.35.0" - logback = "1.5.32" + logback = "1.6.1" commonsIoVersion = "2.22.0" commonsLang3 = "3.20.0" rhinoVersion = "1.9.1" okhttpBomVersion = "5.3.2" gsonVersion = "2.14.0" guavaVersion = "33.6.0-jre" - jinjavaVersion = "2.8.3" - jackson2Version = "2.21.4" + jinjavaVersion = "2.8.4" + jackson2Version = "2.22.1" bucket4jVersion = "8.19.0" archunitVersion = "1.4.2" batikVersion = "1.19" - jpdfiumVersion = "1.0.2" + jpdfiumVersion = "1.0.4" jwtVersion = "0.13.0" - awsSdkVersion = "2.44.12" + awsSdkVersion = "2.51.3" + jschVersion = "2.28.6" + commonsNetVersion = "3.13.0" + smbjVersion = "0.14.0" + tinkVersion = "1.23.0" testcontainersMinioVersion = "1.21.4" // junit-platform-launcher version managed by Spring Boot BOM modernJavaVersion = 25 @@ -90,6 +95,12 @@ bootJar { enabled = false } +// :saas is only included for SaaS builds, but a default-flavor clean must still remove artifacts +// left behind by an earlier SaaS build. +tasks.named('clean') { + delete layout.projectDirectory.dir('app/saas/build') +} + // Configure main class for the root project springBoot { mainClass = 'stirling.software.SPDF.SPDFApplication' @@ -97,7 +108,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.14.2' + version = '2.14.3' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" @@ -291,6 +302,17 @@ subprojects { tasks.withType(Test).configureEach { useJUnitPlatform() + jvmArgs '--enable-native-access=ALL-UNNAMED' + systemProperty 'java.awt.headless', 'true' + systemProperty 'apple.awt.UIElement', 'true' + + testLogging { + events "started", "failed" + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat "full" + } finalizedBy(jacocoReport) } @@ -544,6 +566,7 @@ gradle.taskGraph.whenReady { graph -> } def allProjects = ((subprojects as Set) + project) as Set +def moduleLicenseOverridesFile = project.layout.projectDirectory.file("app/license-overrides.json").asFile licenseReport { projects = allProjects @@ -551,6 +574,15 @@ licenseReport { allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ] + filters = [new ModuleLicenseOverrideFilter(moduleLicenseOverridesFile)] +} + +tasks.named('generateLicenseReport') { + inputs.file(moduleLicenseOverridesFile) +} + +tasks.named('checkLicensePreparation') { + inputs.file(moduleLicenseOverridesFile) } // Configure the forked spring boot run task to properly delegate to the stirling-pdf module diff --git a/buildSrc/README.md b/buildSrc/README.md new file mode 100644 index 0000000000..bbcfb0de6c --- /dev/null +++ b/buildSrc/README.md @@ -0,0 +1,168 @@ +# Dependency license overrides + +The backend dependency license report is generated by the +[`com.github.jk1.dependency-license-report`](https://github.com/jk1/Gradle-License-Report) +Gradle plugin. Most license information is read from dependency POM files, manifests, or packaged +license files. Some artifacts do not publish license metadata in a form the plugin can detect, even +though the artifact has a known license. + +This directory contains the build logic used to provide narrowly scoped fallback license metadata +for those artifacts. + +## Files + +- `build.gradle` makes version 3.1.4 of the license report plugin available to the custom build + logic. The root build applies that plugin without a second version declaration so both use the + same classpath. +- `src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy` implements the + plugin's `DependencyFilter` interface. +- `../app/license-overrides.json` contains the actual module-specific fallback values. +- `../app/allowed-licenses.json` defines which detected or supplied licenses are accepted by + `checkLicense`. + +## How it works + +The root `build.gradle` passes `app/license-overrides.json` to +`ModuleLicenseOverrideFilter`: + +```groovy +filters = [new ModuleLicenseOverrideFilter(moduleLicenseOverridesFile)] +``` + +For every dependency discovered by the license plugin, the filter builds an identifier in this +format: + +```text +group:artifact:version +``` + +The filter applies a populated override only when both conditions are true: + +1. The complete identifier, including the version, exists in `app/license-overrides.json`. +2. The plugin did not discover a non-empty license name for that dependency. + +When both conditions match, the filter adds the configured license as fallback manifest metadata. +The normal report renderer and `checkLicense` then consume that metadata in the same way as +metadata discovered from the dependency itself. + +An override never replaces a license that the plugin already detected. Updating a dependency also +does not silently reuse the override because a different version produces a different identifier. + +Overrides are temporary fallbacks, not a permanent license catalog. If the plugin starts detecting +the original license for an overridden module, the filter automatically removes that exact entry +from `app/license-overrides.json` and logs the cleanup. When the overridden version is no longer +resolved, a newer resolved version takes its place: if it declares a license, the stale entry is +removed; otherwise the entry moves to the new exact version and its values are cleared for +re-verification. An already populated entry for the new version is preserved. If no higher version +is resolved, the unused override is removed instead. + +Because the report aggregates several projects and configurations, multiple versions of the same +`group:artifact` can be present at once. An override is retained whenever its exact version is still +resolved. Only when that exact version is absent may the filter treat a higher version as an update; +version ordering then follows Gradle's own dependency version comparator. Overrides for dependency +versions that are no longer resolved and have no higher replacement are deleted automatically. + +The filter also records every resolved dependency without detected license metadata that has no +override yet. It writes a placeholder with `null` values for `name`, `url`, and `projectUrl`. +Placeholders deliberately do not affect the generated report until `name` is filled in. This makes +new missing metadata visible in the source-controlled override file instead of only in a generated +report. Review and fill or remove every new placeholder before committing the resulting JSON. + +## Adding an override + +First verify the license from an authoritative source such as the upstream repository, the +published artifact metadata, or the license file shipped inside the artifact. Do not infer a +license from the organization name or from a related artifact. + +Add an entry to `app/license-overrides.json`: + +```json +{ + "com.example:example-library:1.2.3": { + "name": "Apache License, Version 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0", + "projectUrl": "https://github.com/example/example-library/tree/0123456789abcdef0123456789abcdef01234567" + } +} +``` + +The key must contain the exact resolved version. `name` must be non-empty for the override to be +applied. `url` should point to the canonical license text. `projectUrl` must point to the immutable +Git tree for the exact module version, using the commit hash at which that version was introduced: + +```text +https://github.com///tree/ +``` + +Do not use the repository's default branch or another moving URL. See the existing entries in +`app/license-overrides.json` for concrete examples. + +If the license name is not already accepted, add a suitably narrow rule to +`app/allowed-licenses.json`. Adding an override and allowing a license are separate operations: + +- `license-overrides.json` supplies missing metadata for a specific artifact version. +- `allowed-licenses.json` defines the policy enforced by `checkLicense`. + +## Updating a dependency + +When an overridden dependency changes version: + +1. Verify the license for the new version again. +2. Run the license report so the filter can move the old key or add a placeholder for the new full + `group:artifact:version` key. +3. Re-verify and fill the license values and the version's immutable Git-tree `projectUrl`; moved + values are intentionally cleared because a license conclusion for one release is not assumed + for another. +4. Regenerate and inspect the report. + +If the new artifact publishes usable license metadata, no override is necessary. The next license +report or license check removes the old entry from `app/license-overrides.json` automatically. The +file must contain only overrides that are still needed. + +## Verification + +Run the filter unit tests: + +```powershell +.\gradlew.bat -p buildSrc test +``` + +The tests use `com.example:example-library` versions 1.4 and 1.7 to cover the missing +metadata fallback, placeholder creation, version migration, preservation of a populated newer +override, automatic cleanup after license metadata appears, exact-version matching, and concurrent +resolved versions. They also verify removal when a dependency version disappears. A separate `1.9` +to `1.11.0` case verifies numeric Gradle version ordering. + +Run the normal backend license workflow from the repository root: + +```powershell +task backend:licenses:generate +``` + +Then inspect: + +- `build/reports/dependency-license/index.json` for the rendered module, version, license name, and + URL. +- `build/reports/dependency-license/dependencies-without-allowed-license.json` when `checkLicense` + reports a policy failure. + +Also run the backend quality gate after changing the filter or its build wiring: + +```powershell +task backend:check +``` + +The override JSON is registered as an input of `generateLicenseReport` and +`checkLicensePreparation`, so changing the file invalidates the corresponding Gradle task outputs. + +## What not to do + +- Do not use an unversioned key. It cannot match the filter and would make the intended scope + ambiguous. +- Do not use an override to replace valid license metadata published by a dependency. +- Do not add an empty license to `allowed-licenses.json` merely to silence `checkLicense`; that + would still leave the generated report without useful license information. +- Do not exclude a dependency from the report solely because it is transitive. Runtime transitive + dependencies are still distributed components and their licenses remain relevant. +- Do not edit generated files under `build/reports/dependency-license` or the copied static license + report by hand. diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000000..25eee5c0f5 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'groovy' +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation localGroovy() + implementation gradleApi() + implementation 'com.github.jk1:gradle-license-report:3.1.4' + testImplementation platform('org.junit:junit-bom:6.1.2') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' + testLogging.showStandardStreams = true +} diff --git a/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy b/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy new file mode 100644 index 0000000000..df0d76158c --- /dev/null +++ b/buildSrc/src/main/groovy/stirling/software/gradle/ModuleLicenseOverrideFilter.groovy @@ -0,0 +1,173 @@ +package stirling.software.gradle + +import com.github.jk1.license.License +import com.github.jk1.license.ManifestData +import com.github.jk1.license.ModuleData +import com.github.jk1.license.ProjectData +import com.github.jk1.license.filter.DependencyFilter +import com.github.jk1.license.render.LicenseDataCollector +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.DefaultVersionComparator +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.Version +import org.gradle.api.internal.artifacts.ivyservice.ivyresolve.strategy.VersionParser + +class ModuleLicenseOverrideFilter implements DependencyFilter { + private static final VersionParser VERSION_PARSER = new VersionParser() + private static final Comparator VERSION_COMPARATOR = + new DefaultVersionComparator().asVersionComparator() + + private final File overridesFile + + ModuleLicenseOverrideFilter(File overridesFile) { + this.overridesFile = overridesFile + } + + @Override + ProjectData filter(ProjectData projectData) { + Map> overrides = loadOverrides() + List modules = projectData.configurations + .collectMany { configuration -> configuration.dependencies } + Map> modulesByCoordinate = modules + .groupBy { module -> moduleCoordinate(module) } + + boolean overridesChanged = false + overrides.keySet().toList().each { overrideId -> + ModuleCoordinates overrideModule = parseModuleId(overrideId) + List coordinateModules = modulesByCoordinate[overrideModule.coordinate] + ModuleData currentModule = coordinateModules + ?.find { module -> module.version == overrideModule.version } + if (currentModule == null) { + currentModule = newestModule(coordinateModules, overrideModule.version) + } + if (currentModule == null) { + overrides.remove(overrideId) + overridesChanged = true + projectData.project.logger.lifecycle( + "Removed unused license override for ${overrideId}: " + + 'dependency version is no longer resolved') + return + } + + if (hasDeclaredLicense(currentModule)) { + overrides.remove(overrideId) + overridesChanged = true + projectData.project.logger.lifecycle( + "Removed stale license override for ${overrideId}: " + + "${moduleId(currentModule)} now declares a license") + return + } + + if (compareVersions(currentModule.version, overrideModule.version) > 0) { + String currentModuleId = moduleId(currentModule) + overrides.remove(overrideId) + if (!overrides.containsKey(currentModuleId)) { + overrides[currentModuleId] = [name: null, url: null, projectUrl: null] + } + overridesChanged = true + projectData.project.logger.lifecycle( + "Updated license override from ${overrideId} to ${currentModuleId}: " + + 'newer dependency still declares no license') + } + } + + modules.groupBy { module -> moduleId(module) }.each { currentModuleId, matchingModules -> + ModuleData module = matchingModules.first() + if (!overrides.containsKey(currentModuleId) && !hasDeclaredLicense(module)) { + overrides[currentModuleId] = [name: null, url: null, projectUrl: null] + overridesChanged = true + projectData.project.logger.lifecycle( + "Added missing license override for ${currentModuleId}. " + + "Set 'name' and 'url' in ${overridesFile}.") + } + } + if (overridesChanged) { + saveOverrides(overrides) + } + + projectData.configurations.each { configuration -> + configuration.dependencies.each { module -> applyOverride(module, overrides) } + } + return projectData + } + + private void applyOverride( + ModuleData module, Map> overrides) { + String moduleId = moduleId(module) + Map override = overrides[moduleId] + if (override == null) { + return + } + + String licenseName = override.name + String licenseUrl = override.url + String projectUrl = override.projectUrl + if (licenseName == null || licenseName.isBlank()) { + return + } + + Set licenses = [new License(licenseName, licenseUrl)] as LinkedHashSet + ManifestData manifest = + new ManifestData(module.name, module.version, null, null, projectUrl, licenses, false) + Set manifests = new LinkedHashSet<>(module.manifests ?: []) + manifests.add(manifest) + module.manifests = manifests + } + + private Map> loadOverrides() { + Object parsed = new JsonSlurper().parse(overridesFile) + if (!(parsed instanceof Map)) { + throw new IllegalArgumentException( + "License overrides file ${overridesFile} must contain a JSON object") + } + return parsed as Map> + } + + private void saveOverrides(Map> overrides) { + String json = JsonOutput.prettyPrint(JsonOutput.toJson(overrides)) + System.lineSeparator() + overridesFile.setText(json, 'UTF-8') + } + + private static String moduleId(ModuleData module) { + return "${module.group}:${module.name}:${module.version}" + } + + private static String moduleCoordinate(ModuleData module) { + return "${module.group}:${module.name}" + } + + private static ModuleCoordinates parseModuleId(String moduleId) { + List parts = moduleId.split(':', 3) as List + if (parts.size() != 3 || parts.any { part -> part.isBlank() }) { + throw new IllegalArgumentException( + "License override key ${moduleId} must use group:module:version") + } + return new ModuleCoordinates("${parts[0]}:${parts[1]}", parts[2]) + } + + private static ModuleData newestModule(List modules, String minimumVersion) { + return modules + ?.findAll { module -> compareVersions(module.version, minimumVersion) > 0 } + ?.max { left, right -> compareVersions(left.version, right.version) } + } + + private static int compareVersions(String left, String right) { + return VERSION_COMPARATOR.compare( + VERSION_PARSER.transform(left), VERSION_PARSER.transform(right)) + } + + private static boolean hasDeclaredLicense(ModuleData module) { + Set licenses = LicenseDataCollector.multiModuleLicenseInfo(module).licenses + return licenses.any { license -> license.name != null && !license.name.isBlank() } + } + + private static class ModuleCoordinates { + final String coordinate + final String version + + ModuleCoordinates(String coordinate, String version) { + this.coordinate = coordinate + this.version = version + } + } +} diff --git a/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy b/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy new file mode 100644 index 0000000000..7536d08af0 --- /dev/null +++ b/buildSrc/src/test/groovy/stirling/software/gradle/ModuleLicenseOverrideFilterTest.groovy @@ -0,0 +1,265 @@ +package stirling.software.gradle + +import com.github.jk1.license.ConfigurationData +import com.github.jk1.license.License +import com.github.jk1.license.ManifestData +import com.github.jk1.license.ModuleData +import com.github.jk1.license.ProjectData +import com.github.jk1.license.render.LicenseDataCollector +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import java.nio.file.Path +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertTrue + +class ModuleLicenseOverrideFilterTest { + private static final String GROUP = 'com.example' + private static final String MODULE = 'example-library' + private static final String VERSION_WITHOUT_LICENSE = '1.4' + private static final String VERSION_WITH_LICENSE = '1.7' + private static final String APACHE_NAME = 'Apache License, Version 2.0' + private static final String APACHE_URL = 'https://www.apache.org/licenses/LICENSE-2.0' + private static final String PROJECT_URL = 'https://github.com/HubSpot/hubspot-immutables' + + @TempDir + Path temporaryDirectory + + @Test + void keepsOverrideForVersionWithoutLicenseMetadata() { + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, null) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertTrue(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenLaterVersionDeclaresLicense() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenOnlyOlderVersionIsResolved() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITH_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITH_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void removesOverrideWhenModuleIsNoLongerResolved() { + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData()) + + assertTrue(readOverrides(overridesFile).isEmpty()) + } + + @Test + void keepsOverrideWhenExactAndNewerVersionsAreBothResolved() { + ModuleData olderModule = createModule(VERSION_WITHOUT_LICENSE, null) + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData newerModule = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + new ModuleLicenseOverrideFilter(overridesFile) + .filter(createProjectData(olderModule, newerModule)) + + Map overrides = readOverrides(overridesFile) + assertTrue(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals([APACHE_NAME], licenseNames(olderModule)) + assertEquals([APACHE_NAME], licenseNames(newerModule)) + } + + @Test + void movesOverrideUsingGradleNumericVersionOrdering() { + String oldVersion = '1.9' + String newVersion = '1.11.0' + ModuleData module = createModule(newVersion, null) + File overridesFile = createOverridesFile(moduleId(oldVersion)) + + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(oldVersion))) + assertEquals( + [name: null, url: null, projectUrl: null], overrides[moduleId(newVersion)]) + } + + @Test + void movesOverrideToLaterVersionWithoutLicenseAndClearsLicenseData() { + ModuleData module = createModule(VERSION_WITH_LICENSE, null) + File overridesFile = createOverridesFile(moduleId(VERSION_WITHOUT_LICENSE)) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals( + [name: null, url: null, projectUrl: null], + overrides[moduleId(VERSION_WITH_LICENSE)]) + assertTrue(licenseNames(module).isEmpty()) + } + + @Test + void preservesExistingOverrideWhenRemovingOlderVersion() { + ModuleData module = createModule(VERSION_WITH_LICENSE, null) + File overridesFile = createOverridesFile( + [ + (moduleId(VERSION_WITHOUT_LICENSE)): [ + name: APACHE_NAME, url: APACHE_URL + ], + (moduleId(VERSION_WITH_LICENSE)): [ + name: APACHE_NAME, url: APACHE_URL, projectUrl: PROJECT_URL + ] + ]) + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertFalse(overrides.containsKey(moduleId(VERSION_WITHOUT_LICENSE))) + assertEquals( + [name: APACHE_NAME, url: APACHE_URL, projectUrl: PROJECT_URL], + overrides[moduleId(VERSION_WITH_LICENSE)]) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + @Test + void addsMissingOverrideForModuleWithoutLicense() { + ModuleData module = createModule(VERSION_WITHOUT_LICENSE, null) + File overridesFile = createEmptyOverridesFile() + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + Map overrides = readOverrides(overridesFile) + assertEquals( + [name: null, url: null, projectUrl: null], + overrides[moduleId(VERSION_WITHOUT_LICENSE)]) + assertTrue(licenseNames(module).isEmpty()) + } + + @Test + void doesNotAddOverrideForModuleWithLicense() { + License publishedLicense = new License(APACHE_NAME, APACHE_URL) + ModuleData module = createModule(VERSION_WITH_LICENSE, publishedLicense) + File overridesFile = createEmptyOverridesFile() + + debugState('before filter', module, overridesFile) + new ModuleLicenseOverrideFilter(overridesFile).filter(createProjectData(module)) + debugState('after filter', module, overridesFile) + + assertTrue(readOverrides(overridesFile).isEmpty()) + assertEquals([APACHE_NAME], licenseNames(module)) + } + + private File createOverridesFile(String moduleId) { + Map> overrides = [ + (moduleId): [name: APACHE_NAME, url: APACHE_URL] + ] + return createOverridesFile(overrides) + } + + private File createOverridesFile(Map> overrides) { + File overridesFile = temporaryDirectory.resolve('license-overrides.json').toFile() + overridesFile.setText(JsonOutput.prettyPrint(JsonOutput.toJson(overrides)), 'UTF-8') + return overridesFile + } + + private File createEmptyOverridesFile() { + File overridesFile = temporaryDirectory.resolve('license-overrides.json').toFile() + overridesFile.setText('{}', 'UTF-8') + return overridesFile + } + + private static Map readOverrides(File overridesFile) { + return new JsonSlurper().parse(overridesFile) as Map + } + + private static void debugState(String stage, ModuleData module, File overridesFile) { + String resolvedModuleId = "${module.group}:${module.name}:${module.version}" + Map overrides = readOverrides(overridesFile) + System.out.println( + "[license-override-test] ${stage}: module=${resolvedModuleId}, " + + "licenses=${licenseNames(module)}, " + + "matchingOverride=${overrides.containsKey(resolvedModuleId)}, " + + "overrideKeys=${overrides.keySet().sort()}") + } + + private static ProjectData createProjectData(ModuleData module) { + return createProjectData(module as ModuleData[]) + } + + private static ProjectData createProjectData(ModuleData... modules) { + ConfigurationData configuration = + new ConfigurationData( + 'runtimeClasspath', modules as LinkedHashSet) + return new ProjectData( + ProjectBuilder.builder().build(), + [configuration] as LinkedHashSet) + } + + private static ModuleData createModule(String version, License license) { + Set manifests = new LinkedHashSet<>() + if (license != null) { + manifests.add( + new ManifestData( + MODULE, + version, + null, + null, + null, + [license] as LinkedHashSet, + false)) + } + return new ModuleData( + GROUP, + MODULE, + version, + true, + manifests, + new LinkedHashSet<>(), + new LinkedHashSet<>()) + } + + private static String moduleId(String version) { + return "${GROUP}:${MODULE}:${version}" + } + + private static List licenseNames(ModuleData module) { + Set licenses = LicenseDataCollector.multiModuleLicenseInfo(module).licenses + return licenses.collect { license -> license.name }.sort() + } +} diff --git a/devGuide/README.md b/devGuide/README.md index 5c8f60577b..5e8486f013 100644 --- a/devGuide/README.md +++ b/devGuide/README.md @@ -9,6 +9,7 @@ This directory contains all development-related documentation for Stirling PDF. - **[Taskfile.yml](../Taskfile.yml)** - Unified task runner for all build/dev/test/lint commands - **[EXCEPTION_HANDLING_GUIDE.md](./EXCEPTION_HANDLING_GUIDE.md)** - Exception handling patterns and i18n best practices - **[HowToAddNewLanguage.md](./HowToAddNewLanguage.md)** - Internationalization and translation guide +- **[STORAGE_ENCRYPTION_AT_REST.md](./STORAGE_ENCRYPTION_AT_REST.md)** - Encryption at rest for stored files: key setup, migration, revocation, rotation ### Features & Documentation - **[AGENTS.md](./AGENTS.md)** - Agent-based functionality documentation diff --git a/devGuide/STORAGE_ENCRYPTION_AT_REST.md b/devGuide/STORAGE_ENCRYPTION_AT_REST.md new file mode 100644 index 0000000000..6ec8d047aa --- /dev/null +++ b/devGuide/STORAGE_ENCRYPTION_AT_REST.md @@ -0,0 +1,175 @@ +# Storage Encryption at Rest + +Encrypts files stored by Stirling (My Files, workflow files) so the bytes on disk, in the database, +or in S3 are unreadable without the master key. Requires a Pro or Enterprise licence to enable. + +> **Back up the master key.** Losing it makes every encrypted stored file permanently +> unrecoverable. There is no recovery path by design — that is what makes the encryption +> meaningful. + +> **Audit trail requires Enterprise.** Encryption itself works on Pro, but the audit events +> below (encrypt, decrypt, revocation, plaintext export, migration) are only recorded on an +> Enterprise licence — the audit subsystem is Enterprise-gated platform-wide. On Pro the files +> are encrypted exactly the same way, but there is no access trail, which matters if you are +> enabling this to satisfy an audit-logging requirement (HIPAA, CMMC). A warning is logged at +> startup when encryption is enabled without an Enterprise licence. + +## How it works + +Envelope encryption, three levels: + +| Level | What it is | Where it lives | +|---|---|---| +| Master key | Wraps the scope keys | Config property, env var, or `configs/file-encryption.key` | +| Scope key (KEK) | One per team; wraps each file's data key | `file_encryption_keys` table, master-key-wrapped | +| Data key (DEK) | One per stored blob; encrypts the bytes | Inside the blob's own header, scope-key-wrapped | + +Each blob is self-describing: an `SPDFEAR1` header carries the format version, the scope key's id, +the plaintext length, and the wrapped data key, followed by AES-256-GCM streaming ciphertext +(1 MiB segments). The header is bound as associated data to both the key wrap and the payload, so a +header cannot be transplanted between blobs. + +Consequences of that design worth knowing: + +- Blobs without the magic prefix are treated as plaintext and passed through, so enabling the + feature needs no migration and old files keep working. +- Because the key id is pinned per blob, moving a user between teams never breaks their existing + files. +- Plaintext sizes are what get recorded in the database, so quotas and `Content-Length` are + unaffected (ciphertext on disk is ~96 bytes + 16 bytes/MiB larger). +- Presigned S3 download URLs are suppressed once encrypted content can exist — a presigned GET + would hand raw ciphertext to the browser — so those downloads stream through the application. + +## Enabling it + +```yaml +storage: + encryption: + enabled: true +``` + +The master key is resolved in this order: + +1. `stirling.security.fileEncryptionKey` property +2. `STIRLING_FILE_ENCRYPTION_KEY` environment variable +3. an auto-generated `configs/file-encryption.key` (owner-only permissions) + +Generate a key with: + +```bash +openssl rand -base64 32 +``` + +It must decode to exactly 32 bytes; anything else fails at startup rather than silently +downgrading the cipher. The startup log prints a fingerprint (a SHA-256 prefix, never the key) so +you can verify a backup matches the live key. + +**Cluster mode** (`cluster.enabled=true`) requires the key to be set explicitly and identically on +every node; the auto-generated file is refused, because a node-local key would make files written +elsewhere unreadable. + +### Turning it off + +Disabling only stops encrypting *new* writes. Existing encrypted files stay readable as long as the +key material is present — the decrypt path is always active and is never licence-gated, so a lapsed +licence cannot lock you out of your own data. + +## Encrypting files that already exist + +Enabling the flag does not touch the existing plaintext backlog. To convert it: + +```bash +curl -X POST http://localhost:8080/api/v1/admin/storage-encryption/migrate +curl http://localhost:8080/api/v1/admin/storage-encryption/migrate/status +``` + +The job is throttled, resumable, and safe to re-run: for each file it writes the encrypted copy +under a new storage key, swaps the database row only if nothing else changed it, and deletes the old +blob last. If a user replaces a file mid-migration their copy wins and the job skips it. Progress is +in-memory, so a restart mid-run loses the counters and `migrate/status` reports `IDLE` again — just +start it again; already-encrypted files are skipped. There is currently no way to cancel a run, and +on a cluster the guard is per-node, so trigger the migration on one node only. + +## Revoking access (kill switch) + +Disabling a scope key makes every file already stored under it fail closed with `403` until it is +re-enabled: + +```bash +curl -X POST http://localhost:8080/api/v1/admin/storage-encryption/keys/{keyId}/disable +curl -X POST http://localhost:8080/api/v1/admin/storage-encryption/keys/{keyId}/enable +``` + +This revokes access to existing content; it does **not** stop the scope from storing new files. The +next upload finds no active key for the scope and mints one, so the team keeps working while its +history stays sealed. To stop new writes as well, turn encryption off (or take the scope's access +away at the application level) — the kill switch is aimed at stored bytes. + +Because of that, re-enabling is status-aware: the key returns to `ACTIVE` if its scope has no other +active key, and to `RETIRED` if one was minted while it was revoked. Both statuses decrypt existing +content; only `ACTIVE` wraps new writes, so a scope never ends up with two keys competing for new +uploads. The `enable` response reports which status was applied. + +This is reversible: the key material stays in the database and nothing is destroyed. No API path +deletes key material. On a cluster, other nodes pick the change up within their 60-second key-cache +window. + +## Rotating the master key + +Rotation only re-wraps the small `file_encryption_keys` table — file contents are never rewritten. + +1. Set the new key as `stirling.security.fileEncryptionKey`. +2. Keep the outgoing key in `stirling.security.fileEncryptionKeyPrevious`. +3. Bump `stirling.security.fileEncryptionKeyVersion`. +4. Restart. Startup warns about rows still wrapped by the previous key. On a cluster, wait until + **every** node carries both keys — a node still holding only the outgoing key cannot read a + re-wrapped row, so rotating mid-deploy makes the lagging nodes fail on those scopes until they + catch up. +5. `POST /api/v1/admin/storage-encryption/master/rotate`. +6. Confirm the response's `rewrapped` count and that `/status` shows every key row at the new + `masterKeyVersion`. +7. Remove `fileEncryptionKeyPrevious` and restart. + +**Do not skip step 6.** Until a row is re-wrapped it is still readable only with the outgoing key, so +removing that key while rows remain behind would seal the files under them. Startup verifies every +key row against the configured keys and refuses to start if any cannot be unwrapped, naming the count +and the first affected scope — so this shows up as a failed deploy, recoverable by putting the old key +back, rather than as unreadable files discovered later. Keep the outgoing key archived until a +restart has succeeded without it. + +Key material is never accepted over HTTP; the endpoint only performs the re-wrap step. + +## Auditing + +**Requires an Enterprise licence** (see the note at the top): on Pro these events are silently +dropped by the audit subsystem, and a warning is logged at startup. + +Encrypt, decrypt, denied-decrypt, key lifecycle, rotation, and migration events are written to the +audit trail, along with a `plaintextExport` marker whenever a plaintext copy of encrypted content is +served. Per-read decrypt events can be noisy on busy instances and can be turned off with +`storage.encryption.auditReads: false`; denials and key lifecycle events are always recorded. + +Two semantics worth knowing when reading the trail: + +- A `decrypt` event means a decryption was *authorised and opened*, not that bytes were read to + completion — a load that is discarded still records one, and a re-read of the same open resource + (e.g. an HTTP range request) does not record a second. +- `plaintextExport` is currently emitted for stored-file and share-link downloads. Workflow-file + downloads are not yet marked. + +## Status and backup verification + +```bash +curl http://localhost:8080/api/v1/admin/storage-encryption/status +``` + +Reports whether writes are encrypted, the master-key fingerprint, encrypted vs plaintext file +counts, and every key row with its status history. All endpoints under +`/api/v1/admin/storage-encryption` require an admin account. + +## What this protects against + +Stolen disks, database dumps, exposed object-storage buckets, decommissioned media, and platform +users who are not authorised for a file. It is not a defence against an attacker who already has +root on a running instance — at that point the key is in memory. No storage-level encryption product +claims otherwise. diff --git a/devTools/package-lock.json b/devTools/package-lock.json index 0555ac9543..394237a684 100644 --- a/devTools/package-lock.json +++ b/devTools/package-lock.json @@ -555,9 +555,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -894,9 +894,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -1027,9 +1027,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -1108,9 +1108,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -1128,7 +1128,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 7f262b840b..0d5a310c45 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -4,7 +4,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application (backend only, no frontend) -FROM gradle:9.6.1-jdk25@sha256:d25357611c559203299f3c6673101154156bdd28985858ff6f465fc69bd27ca4 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build # JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead ENV JDK_JAVA_OPTIONS="--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ @@ -17,6 +17,8 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ @@ -43,7 +45,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li --no-daemon # Stage 2: Extract Spring Boot Layers -FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 AS jar-extract +FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract WORKDIR /tmp COPY --from=app-build /app/app/core/build/libs/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index ce35373ef0..06e1b6e601 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -5,9 +5,9 @@ ARG TARGETPLATFORM # Stage 1: Build and strip Calibre -FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 AS calibre-build +FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS calibre-build ARG TARGETPLATFORM -ARG CALIBRE_VERSION=9.4.0 +ARG CALIBRE_VERSION=9.13.0 ARG CALIBRE_STRIP_WEBENGINE=false RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ @@ -17,7 +17,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ ca-certificates curl xz-utils libnss3 libfontconfig1 \ libgl1 libegl1 libdbus-1-3 libasound2t64 libxcomposite1 \ libxrandr2 libxkbcommon0 libxi6 libxtst6 libopengl0 \ - poppler-utils; \ + poppler-utils tzdata; \ rm -rf /var/lib/apt/lists/*; \ \ case "$(uname -m)" in \ @@ -218,8 +218,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # Keep iso639.calibre_msgpack (required) # Keep only en-US.pak from qtwebengine_locales (required for WebEngine) rm -rf /opt/calibre/lib/qt6/translations; \ - find /opt/calibre/translations -mindepth 1 -maxdepth 1 ! -name 'qtwebengine_locales' -exec rm -rf {} +; \ - find /opt/calibre/translations/qtwebengine_locales -type f ! -name 'en-US.pak' -delete 2>/dev/null || true; \ + if [ -d /opt/calibre/translations ]; then \ + find /opt/calibre/translations -mindepth 1 -maxdepth 1 ! -name 'qtwebengine_locales' -exec rm -rf {} + 2>/dev/null || true; \ + if [ -d /opt/calibre/translations/qtwebengine_locales ]; then \ + find /opt/calibre/translations/qtwebengine_locales -type f ! -name 'en-US.pak' -delete 2>/dev/null || true; \ + fi; \ + fi; \ if [ -d /opt/calibre/resources/localization ]; then \ rm -rf /opt/calibre/resources/localization/locales.zip \ /opt/calibre/resources/localization/stats.calibre_msgpack \ @@ -270,9 +274,9 @@ RUN if [ "${CALIBRE_STRIP_WEBENGINE}" = "true" ]; then \ # Stage 2: Build Ghostscript from source -FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 AS gs-build +FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS gs-build ARG TARGETPLATFORM -ARG GS_VERSION=10.06.0 +ARG GS_VERSION=10.07.1 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/tmp/gs-build,id=gs-build-${TARGETPLATFORM:-local} \ @@ -294,10 +298,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # Stage 3: Build PDF Tools (QPDF and ImageMagick 7) -FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 AS pdf-tools-build +FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS pdf-tools-build ARG TARGETPLATFORM -ARG QPDF_VERSION=12.3.2 -ARG IM_VERSION=7.1.2-13 +ARG QPDF_VERSION=12.4.0 +ARG IM_VERSION=7.1.2-29 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/tmp/pdf-tools-build,id=pdf-tools-${TARGETPLATFORM:-local} \ @@ -339,9 +343,9 @@ RUN mkdir -p /magick-export/usr/bin \ # Stage 4: Build Python venv -FROM ubuntu:noble@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 AS python-venv-build +FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea AS python-venv-build ARG TARGETPLATFORM -ARG UNOSERVER_VERSION=3.6 +ARG UNOSERVER_VERSION=3.7 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ @@ -364,7 +368,7 @@ RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \ # Final runtime image - the actual base image -FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 AS runtime +FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS runtime SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -373,7 +377,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ LC_ALL=C.UTF-8 \ TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata -ARG UNOSERVER_VERSION=3.6 +ARG UNOSERVER_VERSION=3.7 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ set -eux; \ diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 6655d7d4b8..7289d67bb5 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -5,9 +5,9 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.6.1-jdk25@sha256:d25357611c559203299f3c6673101154156bdd28985858ff6f465fc69bd27ca4 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build -ARG TASK_VERSION=3.49.1 +ARG TASK_VERSION=3.52.0 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && update-ca-certificates \ @@ -30,6 +30,8 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ @@ -59,7 +61,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li --no-daemon # Stage 2: Extract Spring Boot Layers -FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 AS jar-extract +FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract WORKDIR /tmp COPY --from=app-build /app/app/core/build/libs/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 058e824e1b..542bdec981 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:1 + # Stirling-PDF - Fat version (embedded frontend) # Extra fonts for air-gapped environments # Uses pre-built base image for fast builds @@ -6,9 +8,9 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.6.1-jdk25@sha256:d25357611c559203299f3c6673101154156bdd28985858ff6f465fc69bd27ca4 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build -ARG TASK_VERSION=3.49.1 +ARG TASK_VERSION=3.52.0 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && update-ca-certificates \ @@ -31,12 +33,15 @@ WORKDIR /app COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ # Use system gradle instead of gradlew to avoid SSL issues downloading gradle distribution on emulated arm64 -RUN gradle dependencies --no-daemon || true +RUN --mount=type=cache,id=stirling-pdf-gradle-cache,target=/home/gradle/.gradle,sharing=locked \ + gradle dependencies --no-daemon || true COPY . . @@ -44,7 +49,9 @@ COPY . . ARG BUILD_PORTAL=false # Bundle only the JPDFium native for this image's target arch. ARG TARGETARCH -RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo linux-x64)" && \ +RUN --mount=type=cache,id=stirling-pdf-npm-cache,target=/root/.npm,sharing=locked \ + --mount=type=cache,id=stirling-pdf-gradle-cache,target=/home/gradle/.gradle,sharing=locked \ + JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo linux-x64)" && \ DISABLE_ADDITIONAL_FEATURES=false \ gradle clean build \ -PbuildWithFrontend=true \ @@ -54,7 +61,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li --no-daemon # Stage 2: Extract Spring Boot Layers -FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 AS jar-extract +FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db AS jar-extract WORKDIR /tmp COPY --from=app-build /app/app/core/build/libs/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index 4095300ce6..c21ddf2f8d 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -1,11 +1,13 @@ +# syntax=docker/dockerfile:1 + # Stirling-PDF Dockerfile - Ultra-lite version with embedded frontend # Single JAR contains both frontend and backend with minimal dependencies # Stage 1: Build application with embedded frontend -FROM gradle:9.6.1-jdk25@sha256:d25357611c559203299f3c6673101154156bdd28985858ff6f465fc69bd27ca4 AS build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS build # Install Node.js and npm for frontend build -ARG TASK_VERSION=3.49.1 +ARG TASK_VERSION=3.52.0 RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ @@ -23,6 +25,8 @@ WORKDIR /app # Copy gradle files for dependency resolution COPY build.gradle settings.gradle gradlew ./ COPY gradle/ gradle/ +COPY buildSrc/build.gradle buildSrc/ +COPY buildSrc/src/main/ buildSrc/src/main/ COPY app/core/build.gradle app/core/ COPY app/common/build.gradle app/common/ COPY app/proprietary/build.gradle app/proprietary/ @@ -34,7 +38,8 @@ ENV JDK_JAVA_OPTIONS="--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNN --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" -RUN ./gradlew dependencies --no-daemon || true +RUN --mount=type=cache,id=stirling-pdf-gradle-cache,target=/home/gradle/.gradle,sharing=locked \ + ./gradlew dependencies --no-daemon || true # Copy entire project COPY . . @@ -44,7 +49,9 @@ COPY . . ARG BUILD_PORTAL=false # Bundle only the JPDFium native for this image's target arch. ARG TARGETARCH -RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo linux-x64)" && \ +RUN --mount=type=cache,id=stirling-pdf-npm-cache,target=/root/.npm,sharing=locked \ + --mount=type=cache,id=stirling-pdf-gradle-cache,target=/home/gradle/.gradle,sharing=locked \ + JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo linux-x64)" && \ DISABLE_ADDITIONAL_FEATURES=true \ ./gradlew clean build \ -PbuildWithFrontend=true \ @@ -55,7 +62,7 @@ RUN JPDFIUM_PLATFORM="$([ "$TARGETARCH" = arm64 ] && echo linux-arm64 || echo li # Stage 2: Runtime image # glibc base (not Alpine/musl): JPDFium's PDFium natives are glibc-linked. -FROM eclipse-temurin:25-jre-noble@sha256:b27ca47660a8fa837e47a8533b9b1a3a430295cf29ca28d91af4fd121572dc29 +FROM eclipse-temurin:25-jre-noble@sha256:fbcf915c585659b30eb766ada4d6d7cfc9ec1040bf521e95bf61b10a25af73db ENV DEBIAN_FRONTEND=noninteractive \ LANG=C.UTF-8 \ diff --git a/docker/unoserver/Dockerfile b/docker/unoserver/Dockerfile index 8adf0ab980..da5ba27371 100644 --- a/docker/unoserver/Dockerfile +++ b/docker/unoserver/Dockerfile @@ -1,9 +1,9 @@ # Standalone unoserver image for Stirling-PDF remote UNO mode. -# Pinned to unoserver 3.6 to match Stirling-PDF's client (avoids 3.4/3.6 wire mismatch). +# Pinned to unoserver 3.7 to match Stirling-PDF's client (avoids wire mismatch). -FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b +FROM ubuntu:noble@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea -ARG UNOSERVER_VERSION=3.6 +ARG UNOSERVER_VERSION=3.7 # ~120 MB of CJK fonts — opt-in. ARG INSTALL_CJK_FONTS=false diff --git a/docs/stirling.png b/docs/stirling.png index 5edc6eae26..1b043a72ec 100644 Binary files a/docs/stirling.png and b/docs/stirling.png differ diff --git a/docs/stirling.svg b/docs/stirling.svg index 0fef4393aa..b5455291c9 100644 --- a/docs/stirling.svg +++ b/docs/stirling.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/engine/Dockerfile b/engine/Dockerfile index 375983142a..17a917761b 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.5 FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim@sha256:531f855bda2c73cd6ef67d56b733b357cea384185b3022bd09f05e002cd144ca -ARG TASK_VERSION=3.49.1 +ARG TASK_VERSION=3.52.0 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && ARCH=$(dpkg --print-architecture) \ @@ -12,14 +12,12 @@ RUN apt-get update \ # Source under /app/engine/ to match root Taskfile's `includes.engine.dir: engine`. WORKDIR /app/engine - -COPY pyproject.toml uv.lock .env ./ -COPY scripts/ ./scripts/ +COPY engine/pyproject.toml engine/uv.lock engine/.env ./ +COPY engine/scripts/ ./scripts/ RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen --no-dev - -COPY src/ ./src/ + uv sync --frozen --no-dev --group engine +COPY engine/src/ ./src/ WORKDIR /app COPY Taskfile.yml ./ COPY .taskfiles/ ./.taskfiles/ diff --git a/engine/Dockerfile.dev b/engine/Dockerfile.dev index b3e7cc30a7..92d3401a2b 100644 --- a/engine/Dockerfile.dev +++ b/engine/Dockerfile.dev @@ -2,13 +2,13 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim@sha256:531f855bda2c73cd6ef67d56b733b357cea384185b3022bd09f05e002cd144ca WORKDIR /app -COPY pyproject.toml uv.lock ./ +COPY engine/pyproject.toml engine/uv.lock ./engine/ RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --frozen + cd engine && uv sync --frozen --group engine --group engine-dev -ENV PATH="/app/.venv/bin:$PATH" +ENV PATH="/app/engine/.venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 EXPOSE 5001 -CMD ["uv", "run", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--reload"] +CMD ["uv", "run", "--project", "/app/engine", "--frozen", "--group", "engine", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001", "--reload"] diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 8cbb94daf8..0e4ffb1586 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -1,33 +1,72 @@ [project] name = "engine" version = "0.1.0" -description = "AI Document Engine" -requires-python = ">=3.13" -dependencies = [ - "cryptography>=44.0.0", - "fastapi>=0.116.0", - "pgvector>=0.3.6", - "psycopg[binary,pool]>=3.2", - "pydantic>=2.0.0", - # <2 cap: 1.99.0 patches CVE-2026-46678; 2.0 is an untested major migration - "pydantic-ai>=1.99.0,<2.0.0", - "pydantic-ai-slim[voyageai]>=1.99.0,<2.0.0", - "pydantic-settings>=2.0.0", - "python-dotenv>=1.2.1", - "sqlite-vec>=0.1.6", - "uvicorn>=0.35.0", - "opentelemetry-sdk>=1.39.0", - "posthog>=3.0.0", -] +description = "Stirling PDF Python projects" +requires-python = ">=3.13,<3.14" +dependencies = [] [dependency-groups] -dev = [ - "anyio>=4.0.0", - "datamodel-code-generator[ruff]>=0.26.0", - "pytest>=8.0.0", - "pyright>=1.1.408", - "referencing>=0.35.0", - "ruff>=0.14.10", +# Runtime dependencies for the AI document engine service. +engine = [ + "cryptography>=50.0.0", + "fastapi>=0.141.1", + "opentelemetry-sdk>=1.39.1", + "pgvector>=0.5.0", + "posthog>=7.38.3", + "psycopg[binary,pool]>=3.3.4", + "pydantic>=2.13.4", + # <2 cap: 1.99.0 patches CVE-2026-46678; 2.0 is an untested major migration. + "pydantic-ai>=1.107.2,<2.0.0", + "pydantic-ai-slim[voyageai]>=1.107.2,<2.0.0", + "pydantic-settings>=2.15.0", + "python-dotenv>=1.2.2", + "sqlite-vec>=0.1.9", + "uvicorn>=0.52.3", +] +# Type checking, testing, model generation, and formatting tools for the engine. +engine-dev = [ + "anyio>=4.14.2", + "datamodel-code-generator[ruff]==0.64.0", + "pyright>=1.1.411", + "pytest>=9.1.1", + "referencing>=0.37.0", + "ruff==0.15.5", +] +# Dependencies for the Cucumber/Python integration test suite. +cucumber = [ + "behave>=1.3.3", + "behave-html-formatter>=0.9.10", + "opencv-python-headless>=5.0.0.93", + "pdf2image>=1.17.0", + "pillow>=12.3.0", + "pypdf[crypto]>=6.15.0", + "qrcode[pil]>=8.2", + "reportlab>=5.0.0", + "requests>=2.34.2", +] +# Shared Python utilities used by repository scripts and CI workflows. +tools = [ + "deep-translator>=1.11.4", + "defusedxml>=0.7.1", + "fonttools>=4.63.0", + "fpdf2>=2.8.8", + "openai>=2.53.0", + "requests>=2.34.2", + "tomli-w>=1.2.0", + "tomlkit>=0.15.1", + "urllib3>=2.7.0", + "unoserver>=3.7", + "weasyprint>=69.0", +] +# Release-signature verification dependencies. +updater-signatures = [ + "cryptography>=50.0.0", +] +# Pinned repository-wide pre-commit tooling. +pre-commit = [ + "codespell==2.4.3", + "ruff==0.15.5", + "tomli-w==1.2.0", ] [build-system] @@ -36,9 +75,10 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src"] -exclude = [ - "tests", -] +exclude = ["tests"] + +[tool.uv] +default-groups = [] [tool.ruff] line-length = 120 @@ -53,14 +93,16 @@ select = [ "W", "RUF100", "UP", - "PYI", # flake8-pyi: flags deprecated typing constructs - "FA", # flake8-future-annotations: flags missing future annotations imports - "BLE", # flake8-blind-except: flags bare `except Exception` + "PYI", # flake8-pyi: flags deprecated typing constructs + "FA", # flake8-future-annotations: flags missing future annotations imports + "BLE", # flake8-blind-except: flags bare `except Exception` ] +[tool.ruff.lint.isort] +known-first-party = ["stirling", "tests"] + [tool.pyright] pythonVersion = "3.13" - reportImportCycles = "warning" reportUnnecessaryCast = "warning" reportUnnecessaryTypeIgnoreComment = "warning" diff --git a/engine/scripts/generate_tool_models.py b/engine/scripts/generate_tool_models.py index dfe461fcca..c3cd9e7267 100644 --- a/engine/scripts/generate_tool_models.py +++ b/engine/scripts/generate_tool_models.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -"""Generate Python tool models from the Java backend's OpenAPI spec (SwaggerDoc.json). +"""Generate the Python files derived from the Java OpenAPI spec (SwaggerDoc.json). -Uses datamodel-code-generator to convert OpenAPI request schemas to Pydantic models. -Run via: +tool_models.py holds each tool's request model; tool_io.py holds what it accepts and produces, +from ``@ToolIO`` via the ``x-stirling-io`` extension. One pass over one spec, so the two cannot +drift apart. Run via: task engine:tool-models """ @@ -10,6 +11,7 @@ from __future__ import annotations import argparse import json +import subprocess from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path @@ -24,8 +26,71 @@ from referencing.jsonschema import DRAFT202012 # Fields inherited from PDFFile base class - not tool parameters. BASE_CLASS_FIELDS = frozenset({"fileInput", "fileId"}) +IO_EXTENSION = "x-stirling-io" +IO_VOCABULARY_EXTENSION = "x-stirling-io-vocabulary" + _ENGINE_ROOT = Path(__file__).resolve().parents[1] +_IO_TEMPLATE = '''# AUTO-GENERATED FILE. DO NOT EDIT. +# Generated by scripts/generate_tool_models.py from the Java OpenAPI spec (SwaggerDoc.json). +# Regenerate with: task engine:tool-models +"""What each tool endpoint accepts and produces, so a planned chain can be checked before it +is run. Declared in Java with ``@ToolIO``; see ``stirling.services.tool_io_compat`` for the +compatibility rules that read this table.""" + +from enum import StrEnum + +from pydantic import Field + +from stirling.models.base import ApiModel +from stirling.models.tool_models import ToolEndpoint + + +class ToolFormat(StrEnum): + """The kind of file a tool consumes or produces. ``ANY`` accepts or produces anything; + ``NONE`` means no file at all, such as a report or a status.""" + +{formats} + + +class ToolArity(StrEnum): + """How many files go in and come out (Single/Multiple In, Single/Multiple Out). A + multi-output tool returns its results zipped, and the caller unpacks them.""" + +{arities} + + +class ToolIOWhen(ApiModel): + """One condition on a request parameter, guarding a :class:`ToolIOCase`.""" + + param: str + matches: list[str] + # The value the endpoint uses when this parameter is absent; None when it has none. + default: str | None = None + + +class ToolIOCase(ApiModel): + """An output that applies when every condition in ``when`` holds.""" + + when: list[ToolIOWhen] + produces: ToolFormat + arity: ToolArity + + +class ToolIOSpec(ApiModel): + """What one endpoint accepts and produces.""" + + accepts: list[ToolFormat] + produces: ToolFormat + arity: ToolArity + cases: list[ToolIOCase] = Field(default_factory=list) + + +TOOL_IO: dict[ToolEndpoint, ToolIOSpec] = {{ +{declarations} +}} +''' + _FILE_HEADER = ( "# AUTO-GENERATED FILE. DO NOT EDIT.\n" "# Generated by scripts/generate_tool_models.py from Java OpenAPI spec (SwaggerDoc.json).\n" @@ -84,6 +149,9 @@ class ToolDiscovery: "/api/v1/misc/add-image", "/api/v1/misc/add-attachments", "/api/v1/general/overlay-pdfs", + # 5. Server maintenance, not a document operation: releases finished jobs and + # their stored files. Nothing an edit agent should ever call on its own. + "/api/v1/general/jobs/cleanup", ) def _is_excluded(self, path: str) -> bool: @@ -281,7 +349,7 @@ def generate_models_code(combined_schema: dict[str, Any]) -> str: return str(code or "") -def write_output(out_path: Path, tools: list[ToolSpec], models_code: str) -> None: +def render_models(tools: list[ToolSpec], models_code: str) -> str: union_lines = ["type ParamToolModel = ("] for i, tool in enumerate(tools): prefix = " | " if i > 0 else " " @@ -301,30 +369,122 @@ def write_output(out_path: Path, tools: list[ToolSpec], models_code: str) -> Non ] parts = [models_code, "\n", *union_lines, "\n", *enum_lines, "\n", *ops_lines, ""] - out_path.write_text("\n".join(parts), encoding="utf-8") + return "\n".join(parts) + + +def collect_tool_io(spec: dict[str, Any]) -> dict[str, dict[str, Any]]: + """The ``x-stirling-io`` declaration for every endpoint that carries one.""" + table: dict[str, dict[str, Any]] = {} + for path, path_item in sorted(spec.get("paths", {}).items()): + for operation in path_item.values(): + if isinstance(operation, dict) and operation.get(IO_EXTENSION): + table[path] = operation[IO_EXTENSION] + return table + + +def _render_when(condition: dict[str, Any]) -> str: + parts = [f"param={json.dumps(condition['param'])}", f"matches={json.dumps(condition['matches'])}"] + if "default" in condition: + parts.append(f"default={json.dumps(condition['default'])}") + return f"ToolIOWhen({', '.join(parts)})" + + +def _render_case(case: dict[str, Any]) -> str: + when = ", ".join(_render_when(condition) for condition in case["when"]) + return f"ToolIOCase(when=[{when}], produces=ToolFormat.{case['produces']}, arity=ToolArity.{case['arity']})" + + +def _render_spec(declaration: dict[str, Any]) -> str: + """One declaration as a constructor call, so a bad spec is a type error at import rather + than a validation failure at first use.""" + accepts = ", ".join(f"ToolFormat.{f}" for f in declaration["accepts"]) + parts = [ + f"accepts=[{accepts}]", + f"produces=ToolFormat.{declaration['produces']}", + f"arity=ToolArity.{declaration['arity']}", + ] + cases = declaration.get("cases") + if cases: + parts.append("cases=[" + ", ".join(_render_case(case) for case in cases) + "]") + return f"ToolIOSpec({', '.join(parts)})" + + +def _members(values: list[str]) -> str: + return "\n".join(f' {value} = "{value}"' for value in values) + + +def render_tool_io(spec: dict[str, Any], tools: list[ToolSpec]) -> str: + """Keyed by ``ToolEndpoint`` so the endpoint strings live in one place and lookups are checked. + + Declarations outside the enum - the filters, and the introspection endpoints the agent never + plans - are dropped. + """ + by_path = {tool.path: tool.enum_name for tool in tools} + table = {path: d for path, d in collect_tool_io(spec).items() if path in by_path} + if not table: + raise SystemExit( + f"No {IO_EXTENSION} declarations in the spec. The backend publishes these from @ToolIO; " + "regenerate the spec with 'task backend:swagger'." + ) + # Published separately: deriving the enums from the declarations present would shrink them + # whenever an endpoint is disabled in a build. + vocabulary = spec.get(IO_VOCABULARY_EXTENSION) + if not vocabulary: + raise SystemExit(f"No {IO_VOCABULARY_EXTENSION} in the spec; regenerate it from a current backend.") + + rendered = _IO_TEMPLATE.format( + formats=_members(vocabulary["formats"]), + arities=_members(vocabulary["arities"]), + declarations="\n".join( + f" ToolEndpoint.{by_path[path]}: {_render_spec(declaration)}," + for path, declaration in sorted(table.items()) + ), + ) + # Formatted before writing so --check compares like for like. + return subprocess.run( + ["ruff", "format", "--stdin-filename", "tool_io.py", "-"], + input=rendered, + capture_output=True, + text=True, + check=True, + cwd=_ENGINE_ROOT, + ).stdout + + +def write_or_check(out_path: Path, rendered: str, check: bool) -> None: + """In check mode, fail when the committed file is out of date.""" + if check: + current = out_path.read_text(encoding="utf-8") if out_path.exists() else "" + if current != rendered: + raise SystemExit(f"{out_path} is out of date. Run 'task engine:tool-models' and commit the result.") + return + out_path.write_text(rendered, encoding="utf-8") def main() -> None: - parser = argparse.ArgumentParser(description="Generate Python tool models from Java OpenAPI spec") + parser = argparse.ArgumentParser(description="Generate the Python files derived from the Java OpenAPI spec") parser.add_argument("--spec", required=True, help="Path to SwaggerDoc.json") parser.add_argument("--output", required=True, help="Path to output tool_models.py") + parser.add_argument("--io-output", required=True, help="Path to output tool_io.py") + parser.add_argument("--check", action="store_true", help="Fail if a committed file is out of date") args = parser.parse_args() spec_path = Path(args.spec) if not spec_path.exists(): - raise SystemExit(f"OpenAPI spec not found at {spec_path}\nRun 'task engine:tool-models' to generate it.") - output_path = Path(args.output) + raise SystemExit(f"OpenAPI spec not found at {spec_path}\nRun 'task backend:swagger' to generate it.") with open(spec_path, encoding="utf-8") as f: spec = json.load(f) result = ToolDiscovery(spec).discover() - models_code = generate_models_code(result.combined_schema) - write_output(output_path, result.tools, models_code) - print(f"Generated {len(result.tools)} tool models from {spec_path.name}") - for tool in result.tools: - print(f" {tool.enum_name}: {tool.path} -> {tool.class_name}") + io_table = render_tool_io(spec, result.tools) + write_or_check(Path(args.io_output), io_table, args.check) + print(f"{'Up to date' if args.check else 'Generated'}: {len(result.tools)} tool I/O declarations") + + models_code = generate_models_code(result.combined_schema) + write_or_check(Path(args.output), render_models(result.tools, models_code), args.check) + print(f"{'Up to date' if args.check else 'Generated'}: {len(result.tools)} tool models from {spec_path.name}") if __name__ == "__main__": diff --git a/engine/src/stirling/agents/pdf_edit.py b/engine/src/stirling/agents/pdf_edit.py index cdc4fea4f7..c46fdb9f24 100644 --- a/engine/src/stirling/agents/pdf_edit.py +++ b/engine/src/stirling/agents/pdf_edit.py @@ -27,7 +27,7 @@ from stirling.contracts import ( ) from stirling.logging import Pretty from stirling.models import OPERATIONS, ApiModel, ParamToolModel, ToolEndpoint -from stirling.services import AppRuntime +from stirling.services import AppRuntime, ToolChainStep, blocking, validate_tool_chain logger = logging.getLogger(__name__) @@ -209,25 +209,42 @@ class PdfEditAgent: supported_operations, unavailable_operations = self._classify_operations(request) if not supported_operations: return EditCannotDoResponse(reason="No PDF edit operations are available on this server.") - selection = await self._select_plan( - request, supported_operations, unavailable_operations, allow_need_content=allow_need_content - ) - if isinstance(selection, EditClarificationRequest | EditCannotDoResponse): - logger.info("[pdf-edit] selection -> %s: %s", selection.outcome, Pretty(selection)) - return selection - if isinstance(selection, PdfEditNeedContentSelection): - logger.info("[pdf-edit] selection -> need_content: %s", selection.reason) - return self._build_need_content_response(selection, request) - enabled = set(supported_operations) - unsupported = [op for op in selection.operations if op not in enabled] - if unsupported: - logger.warning("[pdf-edit] plan referenced unavailable operations: %s", [op.name for op in unsupported]) - return EditCannotDoResponse( - reason=( - "The following operations are not available on this server " - "(either disabled by the administrator or not installed): " - + ", ".join(op.name for op in unsupported) + # One retry, told exactly which transition fails. Cheaper than carrying the whole + # compatibility matrix in the prompt. + repair_note = "" + for attempt in range(2): + selection = await self._select_plan( + request, + supported_operations, + unavailable_operations, + allow_need_content=allow_need_content, + repair_note=repair_note, + ) + if isinstance(selection, EditClarificationRequest | EditCannotDoResponse): + logger.info("[pdf-edit] selection -> %s: %s", selection.outcome, Pretty(selection)) + return selection + if isinstance(selection, PdfEditNeedContentSelection): + logger.info("[pdf-edit] selection -> need_content: %s", selection.reason) + return self._build_need_content_response(selection, request) + enabled = set(supported_operations) + unsupported = [op for op in selection.operations if op not in enabled] + if unsupported: + logger.warning("[pdf-edit] plan referenced unavailable operations: %s", [op.name for op in unsupported]) + return EditCannotDoResponse( + reason=( + "The following operations are not available on this server " + "(either disabled by the administrator or not installed): " + + ", ".join(op.name for op in unsupported) + ) ) + problems = self._chain_problems(selection.operations) + if not problems: + break + logger.warning("[pdf-edit] plan rejected on attempt %d: %s", attempt + 1, problems) + repair_note = problems + else: + return EditCannotDoResponse( + reason=("No workable order of the available operations achieves this: " + repair_note) ) logger.info("[pdf-edit] plan: %s", [op.name for op in selection.operations]) steps: list[ToolOperationStep] = [] @@ -257,6 +274,7 @@ class PdfEditAgent: unavailable_operations: Iterable[ToolEndpoint], *, allow_need_content: bool = True, + repair_note: str = "", ) -> PdfEditPlanOutput: can_request_content = allow_need_content and not has_page_text(request.page_text) agent = self._build_selection_agent( @@ -264,7 +282,9 @@ class PdfEditAgent: unavailable_operations, allow_need_content=can_request_content, ) - return await agent.select(self._build_selection_prompt(request, supported_operations, unavailable_operations)) + return await agent.select( + self._build_selection_prompt(request, supported_operations, unavailable_operations, repair_note) + ) def _build_selection_agent( self, @@ -311,7 +331,18 @@ class PdfEditAgent: request: PdfEditRequest, supported_operations: Iterable[ToolEndpoint], unavailable_operations: Iterable[ToolEndpoint], + repair_note: str = "", ) -> str: + repair_line = ( + f"A previous attempt planned operations in an order that cannot run: {repair_note}\n" + "If running them in a different order still gives the user what they asked for, " + "return that plan. If it would not - reordering changes the result, or no order " + "works - return cannot_do and say plainly which step cannot accept the previous " + "step's output. This is the one case where cannot_do is right even though some " + "order of these operations would run.\n" + if repair_note + else "" + ) unavailable_line = ( "Unavailable operations (exist but not currently usable): " f"{self._get_operations_prompt(unavailable_operations)}\n" @@ -324,6 +355,7 @@ class PdfEditAgent: f"Files: {format_file_names(request.files)}\n" f"Supported operations:\n{self._get_supported_operations_prompt(supported_operations)}\n" f"{unavailable_line}" + f"{repair_line}" f"Extracted page text:\n{format_page_text(request.page_text)}" ) @@ -353,6 +385,20 @@ class PdfEditAgent: unavailable = [op for op in OPERATIONS if op not in enabled_set and op not in self._AGENT_HIDDEN_ENDPOINTS] return supported, unavailable + @staticmethod + def _chain_problems(operations: Iterable[ToolEndpoint]) -> str: + """Why this ordering cannot run, or empty if it can. + + Only errors count: parameters are not chosen yet, so a conditional output merely warns and + acting on that would reject plans that are fine once configured. + """ + diagnostics = validate_tool_chain([ToolChainStep(operation=op) for op in operations]) + errors = blocking(diagnostics) + if not errors: + return "" + steps = list(operations) + return "; ".join(f"step {d.step_index + 1} ({steps[d.step_index].name}) {d.message}" for d in errors) + @staticmethod def _get_operations_prompt(operations: Iterable[ToolEndpoint]) -> str: return ", ".join(f"{op.name} ({op.value})" for op in operations) diff --git a/engine/src/stirling/models/tool_io.py b/engine/src/stirling/models/tool_io.py new file mode 100644 index 0000000000..c9da9cce90 --- /dev/null +++ b/engine/src/stirling/models/tool_io.py @@ -0,0 +1,278 @@ +# AUTO-GENERATED FILE. DO NOT EDIT. +# Generated by scripts/generate_tool_models.py from the Java OpenAPI spec (SwaggerDoc.json). +# Regenerate with: task engine:tool-models +"""What each tool endpoint accepts and produces, so a planned chain can be checked before it +is run. Declared in Java with ``@ToolIO``; see ``stirling.services.tool_io_compat`` for the +compatibility rules that read this table.""" + +from enum import StrEnum + +from pydantic import Field + +from stirling.models.base import ApiModel +from stirling.models.tool_models import ToolEndpoint + + +class ToolFormat(StrEnum): + """The kind of file a tool consumes or produces. ``ANY`` accepts or produces anything; + ``NONE`` means no file at all, such as a report or a status.""" + + PDF = "PDF" + PDF_ENCRYPTED = "PDF_ENCRYPTED" + IMAGE = "IMAGE" + ZIP = "ZIP" + WORD = "WORD" + PPT = "PPT" + EXCEL = "EXCEL" + CSV = "CSV" + HTML = "HTML" + XML = "XML" + JSON = "JSON" + TEXT = "TEXT" + MARKDOWN = "MARKDOWN" + JAVASCRIPT = "JAVASCRIPT" + EBOOK = "EBOOK" + EMAIL = "EMAIL" + POSTSCRIPT = "POSTSCRIPT" + PCL = "PCL" + XPS = "XPS" + VIDEO = "VIDEO" + CBZ = "CBZ" + CBR = "CBR" + ANY = "ANY" + NONE = "NONE" + + +class ToolArity(StrEnum): + """How many files go in and come out (Single/Multiple In, Single/Multiple Out). A + multi-output tool returns its results zipped, and the caller unpacks them.""" + + SISO = "SISO" + SIMO = "SIMO" + MISO = "MISO" + MIMO = "MIMO" + + +class ToolIOWhen(ApiModel): + """One condition on a request parameter, guarding a :class:`ToolIOCase`.""" + + param: str + matches: list[str] + # The value the endpoint uses when this parameter is absent; None when it has none. + default: str | None = None + + +class ToolIOCase(ApiModel): + """An output that applies when every condition in ``when`` holds.""" + + when: list[ToolIOWhen] + produces: ToolFormat + arity: ToolArity + + +class ToolIOSpec(ApiModel): + """What one endpoint accepts and produces.""" + + accepts: list[ToolFormat] + produces: ToolFormat + arity: ToolArity + cases: list[ToolIOCase] = Field(default_factory=list) + + +TOOL_IO: dict[ToolEndpoint, ToolIOSpec] = { + ToolEndpoint.CBR_TO_PDF: ToolIOSpec(accepts=[ToolFormat.CBR], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.CBZ_TO_PDF: ToolIOSpec(accepts=[ToolFormat.CBZ], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.EBOOK_TO_PDF: ToolIOSpec(accepts=[ToolFormat.EBOOK], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.EML_TO_PDF: ToolIOSpec(accepts=[ToolFormat.EMAIL], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.FILE_TO_PDF: ToolIOSpec(accepts=[ToolFormat.ANY], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.HTML_TO_PDF: ToolIOSpec( + accepts=[ToolFormat.HTML, ToolFormat.ZIP], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.IMG_TO_PDF: ToolIOSpec(accepts=[ToolFormat.IMAGE], produces=ToolFormat.PDF, arity=ToolArity.MISO), + ToolEndpoint.MARKDOWN_TO_PDF: ToolIOSpec( + accepts=[ToolFormat.MARKDOWN, ToolFormat.ZIP], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.PDF_TO_CBR: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CBR, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_CBZ: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CBZ, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_CSV: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.CSV, arity=ToolArity.SIMO), + ToolEndpoint.PDF_TO_EPUB: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.EBOOK, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_HTML: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.ZIP, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_IMG: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.IMAGE, + arity=ToolArity.SIMO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="singleOrMultiple", matches=["single"], default="multiple")], + produces=ToolFormat.IMAGE, + arity=ToolArity.SISO, + ) + ], + ), + ToolEndpoint.PDF_TO_MARKDOWN: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.MARKDOWN, arity=ToolArity.SISO + ), + ToolEndpoint.PDF_TO_PDFA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_PRESENTATION: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PPT, arity=ToolArity.SISO + ), + ToolEndpoint.PDF_TO_TEXT: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.TEXT, + arity=ToolArity.SISO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="outputFormat", matches=["rtf"])], produces=ToolFormat.WORD, arity=ToolArity.SISO + ) + ], + ), + ToolEndpoint.PDF_TO_UA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_VECTOR: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.IMAGE, + arity=ToolArity.SISO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="outputFormat", matches=["ps"], default="eps")], + produces=ToolFormat.POSTSCRIPT, + arity=ToolArity.SISO, + ), + ToolIOCase( + when=[ToolIOWhen(param="outputFormat", matches=["pcl"], default="eps")], + produces=ToolFormat.PCL, + arity=ToolArity.SISO, + ), + ToolIOCase( + when=[ToolIOWhen(param="outputFormat", matches=["xps"], default="eps")], + produces=ToolFormat.XPS, + arity=ToolArity.SISO, + ), + ], + ), + ToolEndpoint.PDF_TO_WORD: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.WORD, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_XLSX: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.EXCEL, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_XML: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.XML, arity=ToolArity.SISO), + ToolEndpoint.SVG_TO_PDF: ToolIOSpec( + accepts=[ToolFormat.IMAGE], + produces=ToolFormat.PDF, + arity=ToolArity.MIMO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="combineIntoSinglePdf", matches=["true"], default="false")], + produces=ToolFormat.PDF, + arity=ToolArity.MISO, + ) + ], + ), + ToolEndpoint.URL_TO_PDF: ToolIOSpec(accepts=[ToolFormat.NONE], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.VECTOR_TO_PDF: ToolIOSpec( + accepts=[ToolFormat.POSTSCRIPT], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.BOOKLET_IMPOSITION: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.CROP: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.EDIT_TABLE_OF_CONTENTS: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.EDIT_TEXT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.MERGE_PDFS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.MISO), + ToolEndpoint.MULTI_PAGE_LAYOUT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.PDF_TO_SINGLE_PAGE: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.REARRANGE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REMOVE_IMAGE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REMOVE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.ROTATE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.SCALE_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.SPLIT_BY_SIZE_OR_COUNT: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO + ), + ToolEndpoint.SPLIT_FOR_POSTER_PRINT: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO + ), + ToolEndpoint.SPLIT_PAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO), + ToolEndpoint.SPLIT_PDF_BY_CHAPTERS: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO + ), + ToolEndpoint.SPLIT_PDF_BY_SECTIONS: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO + ), + ToolEndpoint.ADD_COMMENTS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.ADD_PAGE_NUMBERS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.ADD_STAMP: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.AUTO_RENAME: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.AUTO_ROTATE_PDF: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.PDF, + arity=ToolArity.SISO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="dryRun", matches=["true"], default="false")], + produces=ToolFormat.JSON, + arity=ToolArity.SISO, + ) + ], + ), + ToolEndpoint.AUTO_SPLIT_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO), + ToolEndpoint.COMPRESS_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.DELETE_ATTACHMENT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.EXTRACT_ATTACHMENTS: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.ZIP, arity=ToolArity.SISO + ), + ToolEndpoint.EXTRACT_IMAGE_SCANS: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.IMAGE, arity=ToolArity.SIMO + ), + ToolEndpoint.EXTRACT_IMAGES: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.IMAGE, arity=ToolArity.SIMO), + ToolEndpoint.FLATTEN: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.OCR_PDF: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.PDF, + arity=ToolArity.SISO, + cases=[ + ToolIOCase( + when=[ToolIOWhen(param="sidecar", matches=["true"], default="false")], + produces=ToolFormat.ZIP, + arity=ToolArity.SISO, + ) + ], + ), + ToolEndpoint.REMOVE_BLANKS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SIMO), + ToolEndpoint.RENAME_ATTACHMENT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REPAIR: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REPLACE_INVERT_PDF: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.SCANNER_EFFECT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.UNLOCK_PDF_FORMS: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.UPDATE_METADATA: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.ACCESSIBILITY_REPORT: ToolIOSpec( + accepts=[ToolFormat.PDF], produces=ToolFormat.JSON, arity=ToolArity.SISO + ), + ToolEndpoint.ADD_PASSWORD: ToolIOSpec( + accepts=[ToolFormat.PDF], + produces=ToolFormat.PDF_ENCRYPTED, + arity=ToolArity.SISO, + cases=[ + ToolIOCase( + when=[ + ToolIOWhen(param="password", matches=[""], default=""), + ToolIOWhen(param="ownerPassword", matches=[""], default=""), + ], + produces=ToolFormat.PDF, + arity=ToolArity.SISO, + ) + ], + ), + ToolEndpoint.ADD_WATERMARK: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.AUTO_REDACT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REDACT: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REDACT_EXECUTE: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REMOVE_CERT_SIGN: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.REMOVE_PASSWORD: ToolIOSpec( + accepts=[ToolFormat.PDF, ToolFormat.PDF_ENCRYPTED], produces=ToolFormat.PDF, arity=ToolArity.SISO + ), + ToolEndpoint.SANITIZE_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), + ToolEndpoint.TIMESTAMP_PDF: ToolIOSpec(accepts=[ToolFormat.PDF], produces=ToolFormat.PDF, arity=ToolArity.SISO), +} diff --git a/engine/src/stirling/models/tool_models.py b/engine/src/stirling/models/tool_models.py index 1827730283..6650942b6f 100644 --- a/engine/src/stirling/models/tool_models.py +++ b/engine/src/stirling/models/tool_models.py @@ -11,6 +11,19 @@ from pydantic import Field, RootModel, SecretStr from stirling.models.base import ApiModel +class Profile(StrEnum): + """ + Profile to check against + """ + + ua1 = "ua1" + ua2 = "ua2" + + +class AccessibilityReportParams(ApiModel): + profile: Profile = Field(Profile.ua1, description="Profile to check against") + + class AddCommentsParams(ApiModel): comments: str = Field( ..., @@ -241,6 +254,16 @@ class AutoRenameParams(ApiModel): ) +class DetectionMode(StrEnum): + """ + Detection method. 'auto' tries embedded-text direction first and falls back to Tesseract OSD for pages without usable text; 'text' uses only embedded-text direction; 'osd' forces Tesseract OSD for every page + """ + + auto = "auto" + text = "text" + osd = "osd" + + class AutoSplitPdfParams(ApiModel): duplex_mode: bool = Field( False, @@ -705,6 +728,19 @@ class OcrPdfParams(ApiModel): sidecar: bool | None = Field(None, description="Include OCR text in a sidecar text file if set to true") +class PageRotation(ApiModel): + """ + Optional pre-computed corrections to apply without running detection. Pages not listed are left unchanged, and a page may only appear once + """ + + page_number: int = Field(..., description="1-based page number to rotate", examples=[1]) + rotation: int = Field( + ..., + description="Additional clockwise rotation to add to the page's current rotation, in degrees. Must be a multiple of 90", + examples=[90], + ) + + class PdfToCbrParams(ApiModel): dpi: int = Field(..., description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150]) @@ -820,11 +856,18 @@ class OutputFormat1(StrEnum): pdfa_2b = "pdfa-2b" pdfa_3 = "pdfa-3" pdfa_3b = "pdfa-3b" + pdfa_1a = "pdfa-1a" + pdfa_2a = "pdfa-2a" + pdfa_3a = "pdfa-3a" pdfx = "pdfx" class PdfToPdfaParams(ApiModel): output_format: OutputFormat1 = Field(..., description="The output format type (PDF/A or PDF/X)") + pdf_ua: bool = Field( + False, + description="Also declare PDF/UA accessibility alongside PDF/A. Only applies to the level A formats, and the claim is written only if it validates.", + ) strict: bool | None = Field( None, description="If true, the conversion will fail if the output is not perfectly compliant" ) @@ -863,6 +906,65 @@ class PdfToTextParams(ApiModel): output_format: OutputFormat3 = Field(..., description="The output Text or RTF format") +class ExistingTags(StrEnum): + """ + What to do with an existing structure tree: keep it, rebuild it, or decide automatically + """ + + auto = "auto" + keep = "keep" + rebuild = "rebuild" + + +class FigurePolicy(StrEnum): + """ + How to treat images with no description. require-alt leaves them undescribed so the report asks for input; mark-decorative treats every image as decoration. + """ + + require_alt = "require-alt" + mark_decorative = "mark-decorative" + + +class Profile1(StrEnum): + """ + PDF/UA conformance level to target + """ + + ua1 = "ua1" + ua2 = "ua2" + + +class PdfToUaParams(ApiModel): + alt_text: str | None = Field( + None, + description='Alternative descriptions for figures, as key=text pairs separated by newlines. Keys come from the accessibility-report endpoint\'s figuresNeedingDescription list, for example "0:12=Bar chart of quarterly revenue". Descriptions are never invented, so without these an illustrated document cannot claim conformance.', + ) + embed_fonts: bool = Field( + True, + description="Embed fonts the document references but does not carry. Required for conformance and needs Ghostscript.", + ) + existing_tags: ExistingTags = Field( + ExistingTags.auto, + description="What to do with an existing structure tree: keep it, rebuild it, or decide automatically", + ) + figure_policy: FigurePolicy = Field( + FigurePolicy.require_alt, + description="How to treat images with no description. require-alt leaves them undescribed so the report asks for input; mark-decorative treats every image as decoration.", + ) + language: str = Field( + "en-GB", + description="Document language as a BCP-47 tag, for example en-GB. Applied only when the document does not already declare one, unless overrideLanguage is set.", + ) + override_language: bool = Field( + False, + description="Replace the language the document already declares. Off by default, so a document is never relabelled into a language it is not written in.", + ) + profile: Profile1 = Field(Profile1.ua1, description="PDF/UA conformance level to target") + title: str | None = Field( + None, description="Document title, required by PDF/UA. Falls back to the first heading, then the filename." + ) + + class OutputFormat4(StrEnum): """ Target vector format extension @@ -1348,6 +1450,30 @@ class VectorToPdfParams(ApiModel): prepress: Prepress = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") +class AutoRotatePdfParams(ApiModel): + confidence_threshold: float = Field( + 14.0, + description="Minimum Tesseract OSD orientation confidence required before a correction is applied. Matches OCRmyPDF's --rotate-pages-threshold scale", + ge=0.0, + ) + detection_mode: DetectionMode = Field( + DetectionMode.auto, + description="Detection method. 'auto' tries embedded-text direction first and falls back to Tesseract OSD for pages without usable text; 'text' uses only embedded-text direction; 'osd' forces Tesseract OSD for every page", + ) + dry_run: bool | None = Field( + None, + description="If true, no rotation is applied; returns a JSON report of the per-page detection results instead of a PDF", + ) + infer_undetected: bool = Field( + True, + description="When a page cannot be decided on its own but the pages that could be decided agree on a single correction for that same current rotation, apply that shared correction to the undecided page. Handles documents rotated uniformly where some pages are too sparse to detect alone", + ) + page_rotations: list[PageRotation] | None = Field( + None, + description="Optional pre-computed corrections to apply without running detection. Pages not listed are left unchanged, and a page may only appear once", + ) + + class RedactExecuteParams(ApiModel): image_boxes: list[ImageBox] | None = Field( None, description="Rectangular areas to black out, each defined by a page number and bounding box coordinates." @@ -1404,6 +1530,7 @@ class Model( | PdfToPdfaParams | PdfToPresentationParams | PdfToTextParams + | PdfToUaParams | PdfToVectorParams | PdfToWordParams | PdfToXlsxParams @@ -1432,6 +1559,7 @@ class Model( | AddPageNumbersParams | AddStampParams | AutoRenameParams + | AutoRotatePdfParams | AutoSplitPdfParams | CompressPdfParams | DeleteAttachmentParams @@ -1447,6 +1575,7 @@ class Model( | ScannerEffectParams | UnlockPdfFormsParams | UpdateMetadataParams + | AccessibilityReportParams | AddPasswordParams | AddWatermarkParams | AutoRedactParams @@ -1477,6 +1606,7 @@ class Model( | PdfToPdfaParams | PdfToPresentationParams | PdfToTextParams + | PdfToUaParams | PdfToVectorParams | PdfToWordParams | PdfToXlsxParams @@ -1505,6 +1635,7 @@ class Model( | AddPageNumbersParams | AddStampParams | AutoRenameParams + | AutoRotatePdfParams | AutoSplitPdfParams | CompressPdfParams | DeleteAttachmentParams @@ -1520,6 +1651,7 @@ class Model( | ScannerEffectParams | UnlockPdfFormsParams | UpdateMetadataParams + | AccessibilityReportParams | AddPasswordParams | AddWatermarkParams | AutoRedactParams @@ -1551,6 +1683,7 @@ type ParamToolModel = ( | PdfToPdfaParams | PdfToPresentationParams | PdfToTextParams + | PdfToUaParams | PdfToVectorParams | PdfToWordParams | PdfToXlsxParams @@ -1579,6 +1712,7 @@ type ParamToolModel = ( | AddPageNumbersParams | AddStampParams | AutoRenameParams + | AutoRotatePdfParams | AutoSplitPdfParams | CompressPdfParams | DeleteAttachmentParams @@ -1594,6 +1728,7 @@ type ParamToolModel = ( | ScannerEffectParams | UnlockPdfFormsParams | UpdateMetadataParams + | AccessibilityReportParams | AddPasswordParams | AddWatermarkParams | AutoRedactParams @@ -1626,6 +1761,7 @@ class ToolEndpoint(StrEnum): PDF_TO_PDFA = "/api/v1/convert/pdf/pdfa" PDF_TO_PRESENTATION = "/api/v1/convert/pdf/presentation" PDF_TO_TEXT = "/api/v1/convert/pdf/text" + PDF_TO_UA = "/api/v1/convert/pdf/ua" PDF_TO_VECTOR = "/api/v1/convert/pdf/vector" PDF_TO_WORD = "/api/v1/convert/pdf/word" PDF_TO_XLSX = "/api/v1/convert/pdf/xlsx" @@ -1654,6 +1790,7 @@ class ToolEndpoint(StrEnum): ADD_PAGE_NUMBERS = "/api/v1/misc/add-page-numbers" ADD_STAMP = "/api/v1/misc/add-stamp" AUTO_RENAME = "/api/v1/misc/auto-rename" + AUTO_ROTATE_PDF = "/api/v1/misc/auto-rotate-pdf" AUTO_SPLIT_PDF = "/api/v1/misc/auto-split-pdf" COMPRESS_PDF = "/api/v1/misc/compress-pdf" DELETE_ATTACHMENT = "/api/v1/misc/delete-attachment" @@ -1669,6 +1806,7 @@ class ToolEndpoint(StrEnum): SCANNER_EFFECT = "/api/v1/misc/scanner-effect" UNLOCK_PDF_FORMS = "/api/v1/misc/unlock-pdf-forms" UPDATE_METADATA = "/api/v1/misc/update-metadata" + ACCESSIBILITY_REPORT = "/api/v1/security/accessibility-report" ADD_PASSWORD = "/api/v1/security/add-password" ADD_WATERMARK = "/api/v1/security/add-watermark" AUTO_REDACT = "/api/v1/security/auto-redact" @@ -1699,6 +1837,7 @@ OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = { ToolEndpoint.PDF_TO_PDFA: PdfToPdfaParams, ToolEndpoint.PDF_TO_PRESENTATION: PdfToPresentationParams, ToolEndpoint.PDF_TO_TEXT: PdfToTextParams, + ToolEndpoint.PDF_TO_UA: PdfToUaParams, ToolEndpoint.PDF_TO_VECTOR: PdfToVectorParams, ToolEndpoint.PDF_TO_WORD: PdfToWordParams, ToolEndpoint.PDF_TO_XLSX: PdfToXlsxParams, @@ -1727,6 +1866,7 @@ OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = { ToolEndpoint.ADD_PAGE_NUMBERS: AddPageNumbersParams, ToolEndpoint.ADD_STAMP: AddStampParams, ToolEndpoint.AUTO_RENAME: AutoRenameParams, + ToolEndpoint.AUTO_ROTATE_PDF: AutoRotatePdfParams, ToolEndpoint.AUTO_SPLIT_PDF: AutoSplitPdfParams, ToolEndpoint.COMPRESS_PDF: CompressPdfParams, ToolEndpoint.DELETE_ATTACHMENT: DeleteAttachmentParams, @@ -1742,6 +1882,7 @@ OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = { ToolEndpoint.SCANNER_EFFECT: ScannerEffectParams, ToolEndpoint.UNLOCK_PDF_FORMS: UnlockPdfFormsParams, ToolEndpoint.UPDATE_METADATA: UpdateMetadataParams, + ToolEndpoint.ACCESSIBILITY_REPORT: AccessibilityReportParams, ToolEndpoint.ADD_PASSWORD: AddPasswordParams, ToolEndpoint.ADD_WATERMARK: AddWatermarkParams, ToolEndpoint.AUTO_REDACT: AutoRedactParams, diff --git a/engine/src/stirling/services/__init__.py b/engine/src/stirling/services/__init__.py index 1db54243f8..4488409142 100644 --- a/engine/src/stirling/services/__init__.py +++ b/engine/src/stirling/services/__init__.py @@ -7,11 +7,15 @@ from .progress import ( set_progress_emitter, ) from .runtime import AppRuntime, build_model_settings, build_runtime +from .tool_io_compat import ToolChainStep, ToolDiagnostic, blocking, validate_tool_chain from .tracking import current_user_id, require_current_user_id, setup_posthog_tracking __all__ = [ "AppRuntime", "ProgressEmitter", + "ToolChainStep", + "ToolDiagnostic", + "blocking", "build_model_settings", "build_runtime", "current_user_id", @@ -20,4 +24,5 @@ __all__ = [ "reset_progress_emitter", "set_progress_emitter", "setup_posthog_tracking", + "validate_tool_chain", ] diff --git a/engine/src/stirling/services/runtime.py b/engine/src/stirling/services/runtime.py index 06b8595afa..02ca83b5e4 100644 --- a/engine/src/stirling/services/runtime.py +++ b/engine/src/stirling/services/runtime.py @@ -4,7 +4,7 @@ import asyncio import json import logging import os -from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from dataclasses import dataclass from typing import Any, assert_never @@ -133,7 +133,7 @@ class ConcurrencyLimitedModel(WrapperModel): model_settings: ModelSettings | None, model_request_parameters: ModelRequestParameters, run_context: RunContext[Any] | None = None, - ) -> AsyncIterator[StreamedResponse]: + ) -> AsyncGenerator[StreamedResponse]: async with self._semaphore: async with super().request_stream( messages, model_settings, model_request_parameters, run_context diff --git a/engine/src/stirling/services/tool_io_compat.py b/engine/src/stirling/services/tool_io_compat.py new file mode 100644 index 0000000000..f65f0efc22 --- /dev/null +++ b/engine/src/stirling/services/tool_io_compat.py @@ -0,0 +1,221 @@ +"""Whether a chain of tool steps can actually run, decided from the generated tool I/O table +rather than by running it. + +The same rules exist in the backend (``ToolChainValidator``) and the frontend +(``utils/toolIoCompat.ts``). They are duplicated on purpose: the frontend checks a pipeline on +every keystroke, and the engine checks a plan before spending a call per step filling in its +parameters. ``testing/tool-io-cases.json`` runs against all three, so a rule that changes in one +place and not the others fails there. +""" + +from __future__ import annotations + +from enum import StrEnum + +from pydantic import Field + +from stirling.models.base import ApiModel +from stirling.models.tool_io import TOOL_IO, ToolArity, ToolFormat, ToolIOSpec +from stirling.models.tool_models import ToolEndpoint + + +class DiagnosticSeverity(StrEnum): + """Only ERROR means the chain cannot run as configured.""" + + ERROR = "ERROR" + WARN = "WARN" + INFO = "INFO" + + +class DiagnosticCode(StrEnum): + """Matching the backend and frontend implementations.""" + + UNDECLARED = "undeclared-operation" + FORMAT_MISMATCH = "format-mismatch" + OUTPUT_UNCERTAIN = "output-uncertain" + SOURCE_MISMATCH = "source-mismatch" + FAN_OUT = "fan-out" + FAN_IN = "fan-in" + + +class ToolDiagnostic(ApiModel): + """One problem, against the step that cannot run.""" + + step_index: int + severity: DiagnosticSeverity + code: DiagnosticCode + message: str + + +class ToolChainStep(ApiModel): + """``parameters`` is only used to resolve an output that depends on one.""" + + operation: ToolEndpoint + parameters: dict[str, object] = Field(default_factory=dict) + + +class ResolvedOutput(ApiModel): + """``certain`` is false when a case reads a parameter we cannot see.""" + + format: ToolFormat + arity: ToolArity + certain: bool + + +def _is_multi_input(arity: ToolArity) -> bool: + return arity in (ToolArity.MISO, ToolArity.MIMO) + + +def _is_multi_output(arity: ToolArity) -> bool: + return arity in (ToolArity.SIMO, ToolArity.MIMO) + + +def _accepts_format(spec: ToolIOSpec, candidate: ToolFormat) -> bool: + return candidate == ToolFormat.ANY or ToolFormat.ANY in spec.accepts or candidate in spec.accepts + + +def _normalise(value: object) -> str: + """Both sides of a condition go through this, so a declaration's casing cannot matter.""" + return "" if value is None else str(value).strip().lower() + + +def resolve_output(spec: ToolIOSpec, parameters: dict[str, object] | None) -> ResolvedOutput: + """First case whose conditions all hold wins. + + If none match but one reads a parameter we cannot see, the declared output comes back + uncertain: an unseen value might have picked another branch. + """ + saw_unknown_param = False + for rule in spec.cases: + all_hold = True + for condition in rule.when: + if parameters is not None and condition.param in parameters: + raw: object = parameters[condition.param] + elif condition.default is not None: + # The caller omitted it, so it takes the endpoint's default. + raw = condition.default + else: + saw_unknown_param = True + all_hold = False + continue + normalised = _normalise(raw) + all_hold = all_hold and any(_normalise(m) == normalised for m in condition.matches) + if all_hold: + return ResolvedOutput(format=rule.produces, arity=rule.arity, certain=True) + return ResolvedOutput(format=spec.produces, arity=spec.arity, certain=not saw_unknown_param) + + +def validate_tool_chain( + steps: list[ToolChainStep], + *, + source_format: ToolFormat | None = None, + tool_io: dict[ToolEndpoint, ToolIOSpec] | None = None, +) -> list[ToolDiagnostic]: + """Every problem with the chain, against the step that cannot run.""" + table = TOOL_IO if tool_io is None else tool_io + diagnostics: list[ToolDiagnostic] = [] + carried: ResolvedOutput | None = None + + for index, step in enumerate(steps): + spec = table.get(step.operation) + if spec is None: + diagnostics.append( + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.WARN, + code=DiagnosticCode.UNDECLARED, + message=( + f"{step.operation} does not declare what it accepts or produces, " + "so the rest of the chain cannot be checked." + ), + ) + ) + # Nothing is known past an undeclared step. + carried = None + continue + + # Only the first step is handed the pipeline's input. Every later step is handed the + # previous step's output, which is simply unknown once an undeclared step intervened - + # checking it against the input again would judge it on a format it never receives. + if index == 0: + if source_format is not None and not _accepts_format(spec, source_format): + diagnostics.append( + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.ERROR, + code=DiagnosticCode.SOURCE_MISMATCH, + message=(f"{step.operation} accepts {_describe(spec)} but the input is {source_format}."), + ) + ) + elif carried is not None: + diagnostics.extend(_check_transition(index, step, spec, carried)) + carried = resolve_output(spec, step.parameters or None) + + return diagnostics + + +def _check_transition( + index: int, step: ToolChainStep, spec: ToolIOSpec, previous: ResolvedOutput +) -> list[ToolDiagnostic]: + if previous.format == ToolFormat.NONE: + return [ + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.ERROR, + code=DiagnosticCode.FORMAT_MISMATCH, + message=( + f"The previous step returns a report rather than a file, so {step.operation} has nothing to run on." + ), + ) + ] + + if not _accepts_format(spec, previous.format): + message = f"{step.operation} accepts {_describe(spec)} but the previous step produces {previous.format}." + # Unresolved output: may yet be fine once the step is configured. + return [ + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.ERROR if previous.certain else DiagnosticSeverity.WARN, + code=DiagnosticCode.FORMAT_MISMATCH if previous.certain else DiagnosticCode.OUTPUT_UNCERTAIN, + message=message, + ) + ] + + if not previous.certain: + return [ + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.WARN, + code=DiagnosticCode.OUTPUT_UNCERTAIN, + message=( + "The previous step's output depends on how it is configured, " + f"so {step.operation} may not be able to run." + ), + ) + ] + + if _is_multi_output(previous.arity): + fan_in = _is_multi_input(spec.arity) + return [ + ToolDiagnostic( + step_index=index, + severity=DiagnosticSeverity.INFO, + code=DiagnosticCode.FAN_IN if fan_in else DiagnosticCode.FAN_OUT, + message=( + f"{step.operation} combines every file the previous step produced." + if fan_in + else f"{step.operation} runs once for each file the previous step produced." + ), + ) + ] + + return [] + + +def _describe(spec: ToolIOSpec) -> str: + return " or ".join(sorted(f.value for f in spec.accepts)) + + +def blocking(diagnostics: list[ToolDiagnostic]) -> list[ToolDiagnostic]: + """The diagnostics that mean the chain cannot run.""" + return [d for d in diagnostics if d.severity == DiagnosticSeverity.ERROR] diff --git a/engine/tests/test_config_routes.py b/engine/tests/test_config_routes.py index 7d30cd7635..37b3b4ef38 100644 --- a/engine/tests/test_config_routes.py +++ b/engine/tests/test_config_routes.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import threading -from collections.abc import Callable, Iterator +from collections.abc import Callable, Generator from contextlib import contextmanager from pathlib import Path from unittest.mock import patch @@ -31,7 +31,7 @@ def _client( settings_factory: Callable[[], AppSettings], *, client_addr: tuple[str, int] = ("127.0.0.1", 12345), -) -> Iterator[TestClient]: +) -> Generator[TestClient]: """Enter a TestClient whose lifespan builds app.state from ``settings_factory``.""" previous = app.dependency_overrides.get(load_settings) app.dependency_overrides[load_settings] = settings_factory diff --git a/engine/tests/test_pdf_edit_agent.py b/engine/tests/test_pdf_edit_agent.py index 0ba5eca6c9..15ca39369c 100644 --- a/engine/tests/test_pdf_edit_agent.py +++ b/engine/tests/test_pdf_edit_agent.py @@ -28,6 +28,7 @@ from stirling.models.tool_models import ( EditTextParams, FlattenParams, RotatePdfParams, + SplitPagesParams, ToolEndpoint, ) from stirling.services.runtime import AppRuntime @@ -76,9 +77,13 @@ class StubPdfEditAgent(PdfEditAgent): runtime: AppRuntime, selection: PdfEditPlanOutput, parameter_selector: RecordingParameterSelector | PdfEditParameterSelector | None = None, + later_selections: list[PdfEditPlanOutput] | None = None, ) -> None: super().__init__(runtime) self.selection = selection + # Selections handed out on retry, in order, so a test can drive the repair loop. + self.later_selections = list(later_selections or []) + self.repair_notes: list[str] = [] if parameter_selector is not None: self.parameter_selector = parameter_selector @@ -96,7 +101,11 @@ class StubPdfEditAgent(PdfEditAgent): unavailable_operations: Iterable[ToolEndpoint], *, allow_need_content: bool = True, + repair_note: str = "", ) -> PdfEditPlanOutput: + self.repair_notes.append(repair_note) + if repair_note and self.later_selections: + return self.later_selections.pop(0) return self.selection @@ -128,6 +137,121 @@ async def test_pdf_edit_agent_builds_multi_step_plan(runtime: AppRuntime) -> Non assert isinstance(response.steps[1].parameters, FlattenParams) +_ANY_SELECTION = PdfEditPlanSelection(operations=[ToolEndpoint.ROTATE_PDF], summary="s", rationale="r") + + +def test_selection_prompt_says_nothing_about_output_formats(runtime: AppRuntime) -> None: + # Compatibility is only raised once a plan has actually failed, so the operation list stays + # about what each tool does. Leaking format hints here re-inflates an already large prompt. + agent = StubPdfEditAgent(runtime, _ANY_SELECTION) + prompt = agent._build_selection_prompt(PdfEditRequest(user_message="anything", files=[]), list(OPERATIONS), []) + assert "outputs:" not in prompt + assert "IMAGE (several files)" not in prompt + + +def test_repair_prompt_offers_reorder_or_telling_the_user(runtime: AppRuntime) -> None: + # The model decides which: it has the user's intent, and a reorder that changes the result + # is worse than saying it cannot be done. + agent = StubPdfEditAgent(runtime, _ANY_SELECTION) + prompt = agent._build_selection_prompt( + PdfEditRequest(user_message="anything", files=[]), + list(OPERATIONS), + [], + "step 3 (SANITIZE_PDF) accepts PDF but the previous step produces IMAGE.", + ) + assert "SANITIZE_PDF" in prompt + assert "different order" in prompt + assert "cannot_do" in prompt + + +@pytest.mark.anyio +async def test_pdf_edit_agent_retries_a_plan_whose_steps_cannot_chain(runtime: AppRuntime) -> None: + # Extracting images emits images, which rotate cannot take. The agent should be told exactly + # that and get one chance to produce something workable rather than shipping a plan that + # would fail part-way through execution. + agent = StubPdfEditAgent( + runtime, + PdfEditPlanSelection( + operations=[ToolEndpoint.EXTRACT_IMAGES, ToolEndpoint.ROTATE_PDF], + summary="Extract the images, then rotate.", + rationale="Initial attempt.", + ), + later_selections=[ + PdfEditPlanSelection( + operations=[ToolEndpoint.ROTATE_PDF], + summary="Rotate the PDF.", + rationale="Repaired attempt.", + ) + ], + parameter_selector=RecordingParameterSelector(), + ) + + response = await agent.handle( + PdfEditRequest( + user_message="Pull out the images and rotate them.", + files=[AiFile(id=FileId("scan-id"), name="scan.pdf")], + ) + ) + + assert isinstance(response, EditPlanResponse) + assert [step.tool for step in response.steps] == [ToolEndpoint.ROTATE_PDF] + # First attempt gets no note; the retry is told which transition failed. + assert agent.repair_notes[0] == "" + assert "ROTATE_PDF" in agent.repair_notes[1] + + +@pytest.mark.anyio +async def test_pdf_edit_agent_gives_up_when_the_repaired_plan_still_cannot_chain( + runtime: AppRuntime, +) -> None: + agent = StubPdfEditAgent( + runtime, + PdfEditPlanSelection( + operations=[ToolEndpoint.EXTRACT_IMAGES, ToolEndpoint.ROTATE_PDF], + summary="Extract the images, then rotate.", + rationale="Initial attempt.", + ), + ) + + response = await agent.handle( + PdfEditRequest( + user_message="Pull out the images and rotate them.", + files=[AiFile(id=FileId("scan-id"), name="scan.pdf")], + ) + ) + + assert isinstance(response, EditCannotDoResponse) + assert "No workable order" in response.reason + # Exactly one retry, not an unbounded loop. + assert len(agent.repair_notes) == 2 + + +@pytest.mark.anyio +async def test_pdf_edit_agent_accepts_a_chain_that_lines_up(runtime: AppRuntime) -> None: + agent = StubPdfEditAgent( + runtime, + PdfEditPlanSelection( + operations=[ToolEndpoint.SPLIT_PAGES, ToolEndpoint.ROTATE_PDF], + summary="Split then rotate.", + rationale="Splitting fans out; rotate runs per file.", + ), + parameter_selector=RecordingParameterSelector( + [SplitPagesParams(page_numbers="all"), RotatePdfParams(angle=Angle(90))] + ), + ) + + response = await agent.handle( + PdfEditRequest( + user_message="Split the pages and rotate each one.", + files=[AiFile(id=FileId("scan-id"), name="scan.pdf")], + ) + ) + + # A fan-out is information, not a problem, so no retry. + assert isinstance(response, EditPlanResponse) + assert agent.repair_notes == [""] + + @pytest.mark.anyio async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector(runtime: AppRuntime) -> None: parameter_selector = RecordingParameterSelector() diff --git a/engine/tests/test_tool_io_compat.py b/engine/tests/test_tool_io_compat.py new file mode 100644 index 0000000000..18f9c79801 --- /dev/null +++ b/engine/tests/test_tool_io_compat.py @@ -0,0 +1,59 @@ +"""The shared cases in ``testing/tool-io-cases.json``, which all three implementations run.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from stirling.models.tool_io import ToolFormat, ToolIOSpec +from stirling.models.tool_models import ToolEndpoint +from stirling.services.tool_io_compat import ( + ToolChainStep, + ToolDiagnostic, + validate_tool_chain, +) + + +def _cases_file() -> Path: + """Shared with the backend and frontend, so it lives at the repo root.""" + for parent in Path(__file__).resolve().parents: + candidate = parent / "testing" / "tool-io-cases.json" + if candidate.exists(): + return candidate + raise RuntimeError("testing/tool-io-cases.json not found above the test directory") + + +_DATA: dict[str, Any] = json.loads(_cases_file().read_text()) +_SPECS: dict[str, ToolIOSpec] = {name: ToolIOSpec.model_validate(spec) for name, spec in _DATA["specs"].items()} +_ENDPOINTS: list[ToolEndpoint] = list(ToolEndpoint) + + +def _summarise(diagnostics: list[ToolDiagnostic]) -> list[str]: + """Messages are free text, so compare only the contractual parts.""" + return [f"{d.step_index}:{d.severity}:{d.code}" for d in diagnostics] + + +@pytest.mark.parametrize("case", _DATA["cases"], ids=lambda c: c["name"]) +def test_shared_cases(case: dict[str, Any]) -> None: + table: dict[ToolEndpoint, ToolIOSpec] = {} + steps: list[ToolChainStep] = [] + for index, step in enumerate(case["steps"]): + # Distinct endpoints so the same spec can appear twice in a chain. Which ones is + # irrelevant: the case supplies its own table, these are just keys. + operation = _ENDPOINTS[index] + if step.get("spec") is not None: + table[operation] = _SPECS[step["spec"]] + steps.append(ToolChainStep(operation=operation, parameters=step.get("parameters") or {})) + + source = case.get("sourceFormat") + actual = validate_tool_chain( + steps, + source_format=ToolFormat(source) if source else None, + tool_io=table, + ) + + expected = [f"{e['stepIndex']}:{e['severity']}:{e['code']}" for e in case["expected"]] + assert _summarise(actual) == expected, [d.message for d in actual] diff --git a/engine/uv.lock b/engine/uv.lock index 5e9d166af0..bb5b812fce 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.13" -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version < '3.14'", -] +requires-python = "==3.13.*" [[package]] name = "ag-ui-protocol" @@ -20,28 +16,28 @@ wheels = [ [[package]] name = "aiofile" -version = "3.9.0" +version = "3.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "caio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/31/edb06aabd8f8f0b56d659f30800795f40b93cba96be946ce179f6931e3a5/aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41", size = 21600, upload-time = "2026-08-04T22:59:27.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" }, ] [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -52,72 +48,31 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, - { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, - { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, - { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, - { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, - { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, - { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, - { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, - { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, - { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, - { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, - { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, - { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, - { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, ] [[package]] @@ -143,25 +98,25 @@ wheels = [ [[package]] name = "annotated-doc" -version = "0.0.4" +version = "0.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, ] [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "anthropic" -version = "0.112.0" +version = "0.121.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -173,30 +128,30 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/dd/808c144d4a883fcfd12fe0d7689b1d86bbbea6666c1cc957ad19f1017c22/anthropic-0.112.0.tar.gz", hash = "sha256:e180cd91aa5b9b32e4007fe69892ab128d8a86b9f90825103b1903fbc977d0af", size = 937460, upload-time = "2026-06-24T18:45:56.844Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/ca/3cb2c20ee729736fbd4546d5d8b67e818288529fe70cb7a80dbf80aef70b/anthropic-0.121.0.tar.gz", hash = "sha256:e79d6e08ab3376602fc9a70d4d5ea3540817c76cf7e16658bed790834e1833d6", size = 1013292, upload-time = "2026-08-07T17:11:07.241Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/26/ea71185027956325be1903d4fcaf7461d5ef40ca8f0e64f992e24ea9db0e/anthropic-0.112.0-py3-none-any.whl", hash = "sha256:bcc6268612c716dbb77133dd60fc41d26016d1b81dee9a52314d210193638751", size = 931954, upload-time = "2026-06-24T18:45:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/b3d41643f1f639927e8c5fb02c3bd8bffe6f1f29e219b3bd4c61e267b15c/anthropic-0.121.0-py3-none-any.whl", hash = "sha256:6048713fa441e59e1cba8363171cd2a86273b25bd213e9c7ac70a523af88b011", size = 1035493, upload-time = "2026-08-07T17:11:08.508Z" }, ] [[package]] name = "anyio" -version = "4.14.1" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "argcomplete" -version = "3.6.3" +version = "3.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, ] [[package]] @@ -239,9 +194,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "behave" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "cucumber-expressions" }, + { name = "cucumber-tag-expressions" }, + { name = "parse" }, + { name = "parse-type" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/51/f37442fe648b3e35ecf69bee803fa6db3f74c5b46d6c882d0bc5654185a2/behave-1.3.3.tar.gz", hash = "sha256:2b8f4b64ed2ea756a5a2a73e23defc1c4631e9e724c499e46661778453ebaf51", size = 892639, upload-time = "2025-09-04T12:12:02.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/71/06f74ffed6d74525c5cd6677c97bd2df0b7649e47a249cf6a0c2038083b2/behave-1.3.3-py2.py3-none-any.whl", hash = "sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0", size = 223594, upload-time = "2025-09-04T12:12:00.3Z" }, +] + +[[package]] +name = "behave-html-formatter" +version = "0.9.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "behave" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/53/42f94fcb2943d6fd56acc34aab0e65244c8f115b1ae9d98fa641a5874fd7/behave-html-formatter-0.9.10.tar.gz", hash = "sha256:c5a9ad3edcac7be5766b14aacce46794885c749c4741fc93f8fc3a4bf2a891aa", size = 22880, upload-time = "2022-06-30T18:44:09.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/03/36e1b71bff0b83fea22e747cee158942758c715bbd3d34a9fa31b44f0715/behave_html_formatter-0.9.10-py3-none-any.whl", hash = "sha256:fff7ac2118463701423645ad5a12636845bfd6c8a2dd52097b524b4f290aa7c8", size = 23268, upload-time = "2022-06-30T18:44:07.954Z" }, +] + [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -251,201 +248,182 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] name = "boto3" -version = "1.43.36" +version = "1.43.67" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/bf/ef5de9b55523bc2141d072fbe6614627088e3e6f97b47850216e99de6a1c/boto3-1.43.67.tar.gz", hash = "sha256:75fe983b70d39cfdc274dc51f9bb02b8a0a104bdad4fa073c1af85a35e707c91", size = 112665, upload-time = "2026-08-07T19:30:20.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" }, + { url = "https://files.pythonhosted.org/packages/77/0f/b41f8968452dc6bf3b83f15f5e9f76a27fd93e246906aeb2e0555f494375/boto3-1.43.67-py3-none-any.whl", hash = "sha256:082cf9df068168cb44028a1703822374c1eb7e48fa49470d7e8a76f0c977d0bc", size = 140025, upload-time = "2026-08-07T19:30:18.49Z" }, ] [[package]] name = "botocore" -version = "1.43.36" +version = "1.43.67" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/37/da9e7f6ca73ac73afd7f0bb7f238aa5daba35c081e98d7f48a7c399599c0/botocore-1.43.36.tar.gz", hash = "sha256:4cae47d1b2d426316b85a0087d9e69e048f13bc003b5177d74639fe9dfd28205", size = 15625488, upload-time = "2026-06-23T02:47:03.192Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/1c/3a75deae60e36bd0ee5c27d040384756b2ea1c90bd7c8c9658335a18b4f5/botocore-1.43.67.tar.gz", hash = "sha256:6fe5cfa0c8676ba809efe505b618ec00f30d1af2d014bf316a7aa4ee86accb20", size = 15889514, upload-time = "2026-08-07T19:30:15.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/19/934f81592527a3f7f9b943c893e334c721a4644948642bc33885d584e9ec/botocore-1.43.36-py3-none-any.whl", hash = "sha256:3c65fdc39ed01d8dfde1e961b34038aed03c459f8ddf80717a12ac006475e49d", size = 15313630, upload-time = "2026-06-23T02:46:59.327Z" }, + { url = "https://files.pythonhosted.org/packages/21/be/38af8e96f3d200c9d34eab8e9f69a4468388ed6030ea04ff012974e5af5a/botocore-1.43.67-py3-none-any.whl", hash = "sha256:48ab8e9fac26fbc2a700d57010251003e6a5f731cf74d8540fb796bc8f3fc0ef", size = 15575924, upload-time = "2026-08-07T19:30:12.116Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, ] [[package]] name = "cachetools" -version = "7.0.5" +version = "7.1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/d2/47e8bc06fe2a06d3f5bdf20f1126ab66c4e99dc48d940e7ba873f7ac7131/cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50", size = 40680, upload-time = "2026-08-01T21:20:40.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d8/767faeda872075724b95dd675466a645f1b92aadcdcf2d1429dcfd76c176/cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0", size = 16830, upload-time = "2026-08-01T21:20:38.977Z" }, ] [[package]] name = "caio" -version = "0.9.25" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/c8/82b3c760141a1076408164b03e8789b51809add6aecd48aa9d7651cf6b59/caio-0.12.2.tar.gz", hash = "sha256:87a67c0dccc60e432888bd532ec504b66e124a5d8b391aab894583b55abd39ea", size = 80927, upload-time = "2026-08-04T14:43:33.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, - { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, - { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9b/31f0b49a2542ffa2f9d6140267e2b568e722a1feeb05cfbffea97666c62b/caio-0.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40ebea9ebe3a3a66ae85fa00d4112d163654a33c82dcf9b26a99f7d30de13317", size = 84656, upload-time = "2026-08-04T14:43:10.513Z" }, + { url = "https://files.pythonhosted.org/packages/99/bc/62568d688af9712a34fe3f958d7a98c53bb2017e263260cd5deae67a90e9/caio-0.12.2-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:6003ec389a68d5ec8f089df82b2dc8915293dd630a4d11322d7e3455045981fd", size = 198443, upload-time = "2026-08-04T14:43:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e4/5ed627860285612e5307f06c109913c5918c947fbc223b55599e484c64b0/caio-0.12.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:eee9376d0e2af25b6defc5bce39f6efa90521c803aaf12eba931bd898a397cfc", size = 196356, upload-time = "2026-08-04T14:43:13.206Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/2a8cfc6ba3ef3f19e7c778e9fb6f98600f0971cca78bbdfc23a413a66349/caio-0.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78e3ccafc98e009fcb00a97ad441585551e52c0ae7ecc50427a3ccd9b11502fd", size = 195893, upload-time = "2026-08-04T14:43:14.649Z" }, + { url = "https://files.pythonhosted.org/packages/d1/87/77c40fb2301d0b5bb27c2e79ae42fce718ed75396d5fe3e1c09d8e1400b1/caio-0.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f2355db8917f5a0f3638bf332fe0d87549c80e978fca01db84a8a14b9df56a05", size = 195969, upload-time = "2026-08-04T14:43:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/0ceca97eb546fe6bbace3399c8b11dfc503efcc7509d708a7a3f09ab50e9/caio-0.12.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8054cba5e7ee623bea34946e2b59eb7c7c2be8872d0a5d12215d6ff564938d5f", size = 78621, upload-time = "2026-08-04T14:43:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/61/8a/71b0144f783468ba9f1bbf8a2f8e45c7d85ae31ec192f10650aa46f31702/caio-0.12.2-py3-none-any.whl", hash = "sha256:5233e797c9fe2b541914b1bc2e2df82677e2206b537e44e252188f3c2cbb0ea9", size = 62548, upload-time = "2026-08-04T14:43:32.394Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" +version = "3.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" }, ] [[package]] name = "cohere" -version = "7.0.4" +version = "7.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastavro" }, @@ -457,9 +435,9 @@ dependencies = [ { name = "types-requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/3c/670631ee223d7b64d157dc3f309bf93bde65efe0bb1a8341d9b575f407d3/cohere-7.0.4.tar.gz", hash = "sha256:35b6a397d35ae6eafa1a02921f42c2a98309a990874533e5238efaf3426b6a21", size = 208794, upload-time = "2026-06-11T15:17:52.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/16/62ba386903ea854a2fce2cc2f959b9e0c8fa1d3e5699a3a523a6c691ffc9/cohere-7.0.8.tar.gz", hash = "sha256:5d9986b9b13cfe5590021474540648ff3c9e9ce4bfddc3d7f8e31acf1898c4d7", size = 213501, upload-time = "2026-07-23T19:31:41.066Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5d/5b6845104240c13fc2530d052b6b59730dc208aa0c0e676e524a280657d6/cohere-7.0.4-py3-none-any.whl", hash = "sha256:23dfaa0790940174363dede0c646765e5a5ade3fe3d2415b19933df3d535e889", size = 352009, upload-time = "2026-06-11T15:17:51.247Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/255cefc27e058853b8750fcb22c549fd52684214d4fc1e4169ab23320644/cohere-7.0.8-py3-none-any.whl", hash = "sha256:a4fe9b9e148cb277ee152009d3566434b055db9d41fc7433abda78add22632ab", size = 357010, upload-time = "2026-07-23T19:31:39.432Z" }, ] [[package]] @@ -473,57 +451,75 @@ wheels = [ [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "cssselect2" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, +] + +[[package]] +name = "cucumber-expressions" +version = "20.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/a3/001d7725688d5f8ae7d73d746457c9d11b851a4bad3a315dc7762144ec96/cucumber_expressions-20.1.0.tar.gz", hash = "sha256:0d216ec26e36c71b3e5643f2e72c41f9b266ef04eaa0c7e47a6e3b2caf523b1a", size = 13826, upload-time = "2026-08-05T20:16:52.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/b1/fba2393968001b2307facb76e0bc47be5c67185df752a8f5b61926d26760/cucumber_expressions-20.1.0-py3-none-any.whl", hash = "sha256:640782ebaef82313dc64e4684d0e5c5efbab0611b23f0cfad64f90c7041cf73d", size = 20230, upload-time = "2026-08-05T20:16:51.565Z" }, +] + +[[package]] +name = "cucumber-tag-expressions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/e0/c2741558040293465d615a4f2555e9180c54a559119b96ff0251dda5fa90/cucumber_tag_expressions-11.0.1.tar.gz", hash = "sha256:f8304dd16e546517816e62ace6c486575023812f42e8a60526fcec1694016146", size = 8635, upload-time = "2026-08-05T20:33:20.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/2a/894aded5804c76cf148965721cb57fed0d923ddb2747a77c9147f20d58a9/cucumber_tag_expressions-11.0.1-py3-none-any.whl", hash = "sha256:8ee5433a3b1ad16ca607c905fa3bb6d85d57f087ba119b14ea5e82cd35ea98c5", size = 9757, upload-time = "2026-08-05T20:33:19.95Z" }, ] [[package]] name = "cyclopts" -version = "4.19.0" +version = "4.22.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -531,28 +527,28 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/e7/4b3048094559b86a800e0f0de7faf8e6d8213727cf31553ec58453f25abc/cyclopts-4.19.0.tar.gz", hash = "sha256:c7532803ab8560d4de8600769793c3de4b2dc8c3b23ec707b989d84d9bae6ff4", size = 189274, upload-time = "2026-06-22T23:58:54.176Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/05/689617b7e86503417c172f577d791524cb13b9697303d5d44409a971ba10/cyclopts-4.22.5.tar.gz", hash = "sha256:94044506317462cad90fb01a917dadce1f48a0915ba3605dc8d178dea1229e24", size = 195144, upload-time = "2026-08-04T13:53:00.303Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/e8/b84c32f35a19107b55b8ceed260de9469206464da26bd0b979db470782c3/cyclopts-4.19.0-py3-none-any.whl", hash = "sha256:a8c11adb45afae25db310122950c7639c70b56e2ce341e5b29848bf3a8ecc1d4", size = 228005, upload-time = "2026-06-22T23:58:52.495Z" }, + { url = "https://files.pythonhosted.org/packages/83/58/bcab9c33fb7a25a1f5970f357c5b19729bc81d50615d2f737b20c4255909/cyclopts-4.22.5-py3-none-any.whl", hash = "sha256:cf9ce285836053d156730ea4ea0ad0c75cf63beb3f3d8edf222a795bc57666ab", size = 234557, upload-time = "2026-08-04T13:52:58.509Z" }, ] [[package]] name = "datamodel-code-generator" -version = "0.56.0" +version = "0.64.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, - { name = "black" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, { name = "genson" }, { name = "inflect" }, - { name = "isort" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, { name = "jinja2" }, { name = "pydantic" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/7d/7fc2bb3d8946ca45851da3f23497a2c6e252e92558ccbd89d609cf1e13d4/datamodel_code_generator-0.56.0.tar.gz", hash = "sha256:e7c003fb5421b890aabe12f66ae65b57198b04cfe1da7c40810798020835b3a8", size = 837708, upload-time = "2026-04-04T09:46:19.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/d2/86c94a2836ed42231653a7ddaefa0a5bc23418167a876bba7376c96b3a35/datamodel_code_generator-0.64.0.tar.gz", hash = "sha256:9c592900a00b20e416494273c22435f5a9aef6ea8c7b9190747522a60497a1cb", size = 1316440, upload-time = "2026-06-14T17:24:50.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/3a/7f169ffc7a2d69a4f9158b1ac083f685b7f4a1a8a1db5d1e4abbb4e741b7/datamodel_code_generator-0.56.0-py3-none-any.whl", hash = "sha256:a0559683fbe90cdf2ce9b6637e3adae3e3a8056a8d0516df581d486e2834ead2", size = 256545, upload-time = "2026-04-04T09:46:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/23/94/71338e2f0146ac10747a5537b3a1e45256e66b7c229869eb0ee787111b41/datamodel_code_generator-0.64.0-py3-none-any.whl", hash = "sha256:b7cd8bd41a312aa997aec6150670bad781847c5b674f17e4d70e78208a0fb990", size = 374698, upload-time = "2026-06-14T17:24:48.809Z" }, ] [package.optional-dependencies] @@ -560,6 +556,28 @@ ruff = [ { name = "ruff" }, ] +[[package]] +name = "deep-translator" +version = "1.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/03/8fa7635c729a01de71151894cdf002ad6d245bfd6d1a731da864cf534dcf/deep_translator-1.11.4.tar.gz", hash = "sha256:801260c69231138707ea88a0955e484db7d40e210c9e0ae0f77372ffda5f4bf5", size = 36043, upload-time = "2023-06-28T19:55:23.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/61a8ef73236dbea83a1a063a8af2f8e1e41a0df64f122233938391d0f175/deep_translator-1.11.4-py3-none-any.whl", hash = "sha256:d635df037e23fa35d12fd42dab72a0b55c9dd19e6292009ee7207e3f30b9e60a", size = 42285, upload-time = "2023-06-28T19:55:20.928Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -580,11 +598,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -604,7 +622,20 @@ wheels = [ name = "engine" version = "0.1.0" source = { editable = "." } -dependencies = [ + +[package.dev-dependencies] +cucumber = [ + { name = "behave" }, + { name = "behave-html-formatter" }, + { name = "opencv-python-headless" }, + { name = "pdf2image" }, + { name = "pillow" }, + { name = "pypdf", extra = ["crypto"] }, + { name = "qrcode", extra = ["pil"] }, + { name = "reportlab" }, + { name = "requests" }, +] +engine = [ { name = "cryptography" }, { name = "fastapi" }, { name = "opentelemetry-sdk" }, @@ -619,9 +650,7 @@ dependencies = [ { name = "sqlite-vec" }, { name = "uvicorn" }, ] - -[package.dev-dependencies] -dev = [ +engine-dev = [ { name = "anyio" }, { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "pyright" }, @@ -629,33 +658,84 @@ dev = [ { name = "referencing" }, { name = "ruff" }, ] +pre-commit = [ + { name = "codespell" }, + { name = "ruff" }, + { name = "tomli-w" }, +] +tools = [ + { name = "deep-translator" }, + { name = "defusedxml" }, + { name = "fonttools" }, + { name = "fpdf2" }, + { name = "openai" }, + { name = "requests" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "unoserver" }, + { name = "urllib3" }, + { name = "weasyprint" }, +] +updater-signatures = [ + { name = "cryptography" }, +] [package.metadata] -requires-dist = [ - { name = "cryptography", specifier = ">=44.0.0" }, - { name = "fastapi", specifier = ">=0.116.0" }, - { name = "opentelemetry-sdk", specifier = ">=1.39.0" }, - { name = "pgvector", specifier = ">=0.3.6" }, - { name = "posthog", specifier = ">=3.0.0" }, - { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pydantic-ai", specifier = ">=1.99.0,<2.0.0" }, - { name = "pydantic-ai-slim", extras = ["voyageai"], specifier = ">=1.99.0,<2.0.0" }, - { name = "pydantic-settings", specifier = ">=2.0.0" }, - { name = "python-dotenv", specifier = ">=1.2.1" }, - { name = "sqlite-vec", specifier = ">=0.1.6" }, - { name = "uvicorn", specifier = ">=0.35.0" }, -] [package.metadata.requires-dev] -dev = [ - { name = "anyio", specifier = ">=4.0.0" }, - { name = "datamodel-code-generator", extras = ["ruff"], specifier = ">=0.26.0" }, - { name = "pyright", specifier = ">=1.1.408" }, - { name = "pytest", specifier = ">=8.0.0" }, - { name = "referencing", specifier = ">=0.35.0" }, - { name = "ruff", specifier = ">=0.14.10" }, +cucumber = [ + { name = "behave", specifier = ">=1.3.3" }, + { name = "behave-html-formatter", specifier = ">=0.9.10" }, + { name = "opencv-python-headless", specifier = ">=5.0.0.93" }, + { name = "pdf2image", specifier = ">=1.17.0" }, + { name = "pillow", specifier = ">=12.3.0" }, + { name = "pypdf", extras = ["crypto"], specifier = ">=6.15.0" }, + { name = "qrcode", extras = ["pil"], specifier = ">=8.2" }, + { name = "reportlab", specifier = ">=5.0.0" }, + { name = "requests", specifier = ">=2.34.2" }, ] +engine = [ + { name = "cryptography", specifier = ">=50.0.0" }, + { name = "fastapi", specifier = ">=0.141.1" }, + { name = "opentelemetry-sdk", specifier = ">=1.39.1" }, + { name = "pgvector", specifier = ">=0.5.0" }, + { name = "posthog", specifier = ">=7.38.3" }, + { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.4" }, + { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pydantic-ai", specifier = ">=1.107.2,<2.0.0" }, + { name = "pydantic-ai-slim", extras = ["voyageai"], specifier = ">=1.107.2,<2.0.0" }, + { name = "pydantic-settings", specifier = ">=2.15.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, + { name = "sqlite-vec", specifier = ">=0.1.9" }, + { name = "uvicorn", specifier = ">=0.52.3" }, +] +engine-dev = [ + { name = "anyio", specifier = ">=4.14.2" }, + { name = "datamodel-code-generator", extras = ["ruff"], specifier = "==0.64.0" }, + { name = "pyright", specifier = ">=1.1.411" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "referencing", specifier = ">=0.37.0" }, + { name = "ruff", specifier = "==0.15.5" }, +] +pre-commit = [ + { name = "codespell", specifier = "==2.4.3" }, + { name = "ruff", specifier = "==0.15.5" }, + { name = "tomli-w", specifier = "==1.2.0" }, +] +tools = [ + { name = "deep-translator", specifier = ">=1.11.4" }, + { name = "defusedxml", specifier = ">=0.7.1" }, + { name = "fonttools", specifier = ">=4.63.0" }, + { name = "fpdf2", specifier = ">=2.8.8" }, + { name = "openai", specifier = ">=2.53.0" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "tomli-w", specifier = ">=1.2.0" }, + { name = "tomlkit", specifier = ">=0.15.1" }, + { name = "unoserver", specifier = ">=3.7" }, + { name = "urllib3", specifier = ">=2.7.0" }, + { name = "weasyprint", specifier = ">=69.0" }, +] +updater-signatures = [{ name = "cryptography", specifier = ">=50.0.0" }] [[package]] name = "eval-type-backport" @@ -686,7 +766,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.135.2" +version = "0.141.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -695,9 +775,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] [[package]] @@ -718,35 +798,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/54/a0817d1d0236e9e0233f5c996f450cc795b056b8e06edb531f24b9df82ed/fastavro-1.12.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d4d66afad78e8f47feaa307728a6b71fe3effc63ba2b9eeb109ee687c9bd397", size = 3399232, upload-time = "2026-04-24T14:37:04.837Z" }, { url = "https://files.pythonhosted.org/packages/38/0a/650f256c15f5875b6081544b9ba7ed8254329213e7e49e3db0aec68b5bee/fastavro-1.12.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2328ec07925c04c89719e3971c9068a165c7fd474ea87675b1204de0440e71ff", size = 3320222, upload-time = "2026-04-24T14:37:07.281Z" }, { url = "https://files.pythonhosted.org/packages/f5/54/8351d388f94fbb0870e8cffaae41d3cc607acc8d6a8a6a217e2794829593/fastavro-1.12.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:55dea7e74b834d4b70467fc19c5b9ccb5509fe39abc4d26891187c1b22176423", size = 3337096, upload-time = "2026-04-24T14:37:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/da/eb/b36ba9a88826e8c272df02e2f8b5da717e88b6eb508fddca3ca450043731/fastavro-1.12.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8d37c87826ae7195cfbd20fcd448801f2f563bb38f2691ec6574e39cb9eca6c8", size = 963119, upload-time = "2026-04-24T14:37:11.557Z" }, - { url = "https://files.pythonhosted.org/packages/e1/02/3d7f540fb26ba4ea1f4ebd2783c586614da9ac00906a3092e92fd3f104a2/fastavro-1.12.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c463a3701f293e30d3d62e71e1989f112028d07f87432baf4507eeb57ec3831", size = 3266238, upload-time = "2026-04-24T14:37:13.84Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0b/b77be56c5109da0fc7dcfd7e6b6752fe0a61d0a5c58c6a65e38b4501946a/fastavro-1.12.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f604ba83498e209fff4c7ecc5063a39421dc538dace694bc592f9f338254f3dc", size = 3324020, upload-time = "2026-04-24T14:37:16.096Z" }, - { url = "https://files.pythonhosted.org/packages/e7/6e/951d41f244107e91bf2f59245b71783c03eaab4bdbc960d58316c19652bb/fastavro-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bfac2dada8ddc002e8b7d8289d6fad4f070bc1fec20371cec684a7d10d932e96", size = 3170160, upload-time = "2026-04-24T14:37:18.168Z" }, - { url = "https://files.pythonhosted.org/packages/94/6f/2adb571fda448d4afd2466e1cef2963fefdc6b37847da05249983e415f17/fastavro-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc44ba6289fb1f5ee318335958dde6ad6d742dcb4bb8930de843e9024c64b68c", size = 3281842, upload-time = "2026-04-24T14:37:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/17/07/4bad2e96c4c6bae40253be2573cc09c1e5b9ccf821e1ff74e0d33b64bf90/fastavro-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:a475418f71c5aed69899813ecccf392429c08c3a63df3030129db71760b0db8f", size = 450903, upload-time = "2026-04-24T14:37:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b7/180f67ba9a46ba23a1ff6432f48d3087d4f2048579ecc262b00426cb1c63/fastavro-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:daec9f9655a1d4636613c47d6d3343f6e039150d66cdce62543e20ca36612a8a", size = 391076, upload-time = "2026-04-24T14:37:24.756Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8f/18f60329b627d2118a4a2b19e8741fbd807d60bf0470554e1bbfb7f1bca3/fastavro-1.12.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:57594b72cf663bbd0f3ad8a319a999fc3d7c71065a6799b2c1d1a6a137894c5b", size = 1055430, upload-time = "2026-05-09T21:53:14.364Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ac/a1fa1fc29df0efc89d4946a743b09bdc9500591b5b92083eaf8e93664916/fastavro-1.12.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74412132bbfb153cbf704517f2c89f7d3e170feb681b13bceace690f66f8d5fa", size = 3503075, upload-time = "2026-04-24T14:37:26.826Z" }, - { url = "https://files.pythonhosted.org/packages/82/bf/4f669e10b6bc38a731ee3400aed1a1e2d0a3e3cf411e72f6b320d3af0eaf/fastavro-1.12.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e367a84c9133018e0a3bc822abe78d7f1f9a6092991a0ec409468cf4ef260282", size = 3410900, upload-time = "2026-04-24T14:37:29.233Z" }, - { url = "https://files.pythonhosted.org/packages/10/39/ecb19fdae4158a7730b5963fbf1b6d38d74678392d73083be518642af0c1/fastavro-1.12.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:044fafca0853e9ae14009de7763ac9e8e8f8b96f8a4e90bd58b695443266a370", size = 3335637, upload-time = "2026-04-24T14:37:31.472Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/f21bd5319113e89ceceed2df840df21e9c5150d181db74b6ba80400f9f48/fastavro-1.12.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afede7324822800e4f90e96b9514188a237a60f35e8e7a10b2129c10c78f6e4d", size = 3356664, upload-time = "2026-04-24T14:37:34.231Z" }, ] [[package]] name = "fastmcp" -version = "3.4.2" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastmcp-slim", extra = ["client", "server"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/18/46beaec18c9f86a599ae3f9cdf6677dd6b50240cfd844d18233710b47f13/fastmcp-3.4.2.tar.gz", hash = "sha256:b468722946fc467c3796a6572f7a14d93d48c014cf8fea12910245220cbbe4e1", size = 28756849, upload-time = "2026-06-06T01:30:35.694Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/e2c78e26233cd8a416b21513e1925435d54c008a0ec467dbdaa80369daf7/fastmcp-3.4.6.tar.gz", hash = "sha256:2287938da8364ad7071bec2d2393af6ae10fd4e836f06f506569f1456cc87eb4", size = 28808130, upload-time = "2026-08-05T14:54:42.177Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/4d/8b1ba42251160e11ca34686344572121432c23a082d56ef6bbdec5888fc1/fastmcp-3.4.2-py3-none-any.whl", hash = "sha256:c87a62b029f0c5400ada85f683629345d2466c39169f0cb853e487b2f7308c08", size = 8018, upload-time = "2026-06-06T01:30:38.118Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/c02275db111892388972edbb05fbcbfdf1e83cbd1fd03356b3a49b93f839/fastmcp-3.4.6-py3-none-any.whl", hash = "sha256:2a29967be9f68cdd1b4cefb413ede74f83adefd923919a37aaac3611eccdd749", size = 8017, upload-time = "2026-08-05T14:54:38.473Z" }, ] [[package]] name = "fastmcp-slim" -version = "3.4.2" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, @@ -756,9 +824,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/2e/d627b28b7403ecc526991ef732921b08bde010006e6148635f053fd29f4c/fastmcp_slim-3.4.2.tar.gz", hash = "sha256:290646e0955a516235a317151034559aa48336cb843d3f006131aedad8759bb4", size = 576291, upload-time = "2026-06-06T01:30:12.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/b6/b5b9e81e67a3f39534881d2af6aa3fbd2dc367eaa070c3c932770a0c062f/fastmcp_slim-3.4.6.tar.gz", hash = "sha256:6a1e6e42c697ba90abcb1be617a26947d07316f13c6fa138ae7b0de24558e32c", size = 594167, upload-time = "2026-08-05T14:54:15.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/58/22afebf18df7260b09148199cbeb90cdcc4b3a4e1b5d7460e3591c3a7add/fastmcp_slim-3.4.2-py3-none-any.whl", hash = "sha256:bdc72492212681ca502755fa8acc0457f559295da1fc3dfc0599adc1c04b82f3", size = 749195, upload-time = "2026-06-06T01:30:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/02c254c46b4323ae5262b76a29af73b50a863efc5739a3454d144d3605ee/fastmcp_slim-3.4.6-py3-none-any.whl", hash = "sha256:3e08e6acb03523a47aa17f4d2ab9943e648a41e20b24084da491a732c46dffdc", size = 769174, upload-time = "2026-08-05T14:54:14.663Z" }, ] [package.optional-dependencies] @@ -796,24 +864,50 @@ server = [ ] [[package]] -name = "ffmpeg-python" -version = "0.2.0" +name = "filelock" +version = "3.32.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "future", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, ] [[package]] -name = "filelock" -version = "3.25.2" +name = "fonttools" +version = "4.63.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[package.optional-dependencies] +woff = [ + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, + { name = "zopfli" }, +] + +[[package]] +name = "fpdf2" +version = "2.8.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "fonttools" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/bc/8fd4321aed40cadadddc8f311c65b6082346b252bca048f7b476d8f35d72/fpdf2-2.8.8.tar.gz", hash = "sha256:9e94e155e85e8053329a9a1fce8b566fd7a7c5bb79e98a1a3952d379b947c5b9", size = 374689, upload-time = "2026-08-09T23:32:45.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/af012eda9507494f28b99b077423806c43a11573eb6225dd46f19ae2d263/fpdf2-2.8.8-py3-none-any.whl", hash = "sha256:3557a478fc577a929c94aace9666aed4dcc432b5ab6764232e6a59f1ccd75f17", size = 337000, upload-time = "2026-08-09T23:32:43.728Z" }, ] [[package]] @@ -854,92 +948,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" -version = "2026.2.0" +version = "2026.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, -] - -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] [[package]] name = "genai-prices" -version = "0.0.67" +version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/9e/f96ad08d62f7bd33a5b24e65d4eb220569714b9a2a8813ada2e1fa47b4dd/genai_prices-0.0.67.tar.gz", hash = "sha256:54e07eb6541fda377187a471c5dba21a81b439c57f8dc44d89db3103c29ca343", size = 80015, upload-time = "2026-06-24T20:16:23.661Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/9b/85e646305a90a2da18f1edf055498668391e71f9849d3e1754d66559a311/genai_prices-0.1.1.tar.gz", hash = "sha256:54a2237691e0aaefb057d10a0c3c20160accc9fc09521c64c03fcdb7a4a69f68", size = 91182, upload-time = "2026-08-01T09:02:49.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/05/d1ca6b960a3305f86d1c5f4274f2ddf8c94611ec7edc436a01cd38a01742/genai_prices-0.0.67-py3-none-any.whl", hash = "sha256:08977f1e83b4132abcfc60dabf21ff13c2d25958afb9199e59c4407bf5c9ed3f", size = 82495, upload-time = "2026-06-24T20:16:22.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/cfe36dff790ffad6aeff8a069b6f36743987ac17053579035ee0a67635dd/genai_prices-0.1.1-py3-none-any.whl", hash = "sha256:de2e3d8ea3ca1d0d292025995c598da447a74e94f22cd3342df46941aeb5416b", size = 95300, upload-time = "2026-08-01T09:02:48.308Z" }, ] [[package]] name = "genson" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/53/de162dc8e03fccd9ebe59d17c7812378fe8bd2b604f6b1b94d00165140ac/genson-1.4.0.tar.gz", hash = "sha256:bc7f1c1bae87a21ca44d81149aec95a3f4468d676de9b8b08caa064f3c50b3da", size = 47908, upload-time = "2026-07-06T08:21:50.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/767f744ab6d4cb7761e5008acc3d534b7a0481af62563d52e391fbcb2140/genson-1.4.0-py3-none-any.whl", hash = "sha256:03bc71bbe52defde70660cc4dcd1ea1097997da5a1cbb90a9dbd3acc7c9e1b65", size = 24484, upload-time = "2026-07-06T08:21:49.046Z" }, ] [[package]] name = "google-auth" -version = "2.49.1" +version = "2.56.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, ] [package.optional-dependencies] @@ -949,7 +1002,7 @@ requests = [ [[package]] name = "google-genai" -version = "2.10.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -963,35 +1016,35 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/fe/b796087493c3c55371aa58b9f264841ace5bfdf8c668cafa7afa33c44bec/google_genai-2.10.0.tar.gz", hash = "sha256:77912cd558cd7dfd5b75c25fd1c609e78d7954dde583331104022a46ea90f9ee", size = 600039, upload-time = "2026-06-24T01:33:18.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/2e/03a0486e192cde27f7ffbb20c63fa934a132396a667b1a0e1b7e5f606ad7/google_genai-2.17.0.tar.gz", hash = "sha256:6b640a2390c82b4a240873eddb9f518c6d2c33244b2de16ed3d14526a6da7f57", size = 648676, upload-time = "2026-08-06T05:10:41.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/39/00bcfd94de255d24249401efff4f48d77bf6066b46447e519fa193c0c299/google_genai-2.10.0-py3-none-any.whl", hash = "sha256:d5350311567ae660c24cbc1752aee4b3d660f89c0106d2dcd2a69978c35afe1e", size = 957974, upload-time = "2026-06-24T01:33:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/b3/37/71e397a5b93d9a3c139e95acdcb00b4169ed59ae20e16616f2875170abb7/google_genai-2.17.0-py3-none-any.whl", hash = "sha256:a4835563c60aee646c9c4b261c507aa4a624710d25017012d20dc65abf3d9a54", size = 1044950, upload-time = "2026-08-06T05:10:39.308Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.73.0" +version = "1.75.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, ] [[package]] name = "griffelib" -version = "2.0.1" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/d7/2b805e89cdc609e5b304361d80586b272ef00f6287ee63de1e571b1f71ec/griffelib-2.0.1.tar.gz", hash = "sha256:59f39eabb4c777483a3823e39e8f9e03e69df271a7e49aee64e91a8cfa91bdf5", size = 166383, upload-time = "2026-03-23T21:05:25.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/4c/cc8c68196db727cfc1432f2ad5de50aa6707e630d44b2e6361dc06d8f134/griffelib-2.0.1-py3-none-any.whl", hash = "sha256:b769eed581c0e857d362fc8fcd8e57ecd2330c124b6104ac8b4c1c86d76970aa", size = 142377, upload-time = "2026-03-23T21:04:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] [[package]] name = "groq" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1001,40 +1054,30 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/3a/31c54e35d694ba9c263939ecae0874c78fc51f22b9fb13d2dab50edf6e3c/groq-1.5.0.tar.gz", hash = "sha256:8648388f8668629490bb0eab11252b4cc43316149fda9c6343e9a9577fe7df88", size = 158239, upload-time = "2026-06-21T22:57:30.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/687064918e861082cb02165872bbda31269126a108a78794dae86ddadae2/groq-1.6.0.tar.gz", hash = "sha256:c4237ecf0053ba85fd926bb31cb4b178996310474b4618867994e3e40d8e3c02", size = 158424, upload-time = "2026-07-24T17:25:49.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c3/cac2aee75198c0382a2e6070619a798de2288e01850abb6e999746bb92e3/groq-1.5.0-py3-none-any.whl", hash = "sha256:a927fd75b4a60cb066c408cb4f9647536caf74e241ea75bc57d1810501b8203f", size = 143690, upload-time = "2026-06-21T22:57:29.213Z" }, + { url = "https://files.pythonhosted.org/packages/20/40/164338d796cf41ea5930a42023e986d8c993720bed670d5aa143e5854873/groq-1.6.0-py3-none-any.whl", hash = "sha256:fa16db582455db324adcff1b5908519474736872e28cd5c8fa2b8bef6860e12a", size = 143693, upload-time = "2026-07-24T17:25:48.2Z" }, ] [[package]] name = "grpcio" -version = "1.81.1" +version = "1.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, - { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, - { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, - { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, - { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, - { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, - { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, + { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, ] [[package]] @@ -1048,34 +1091,26 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.2" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, - { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, - { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, - { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, - { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, - { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, - { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, - { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, - { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, - { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, - { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, ] [[package]] @@ -1093,15 +1128,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.4.0" +version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/9b/2b1d1833a58236d1f6ee755e027a3917da0db59cc9708554cefc440ee8b6/httpcore2-2.4.0.tar.gz", hash = "sha256:3093a8ab8980d9f910b9cb4351df9186a0ad2350a6284a9107ac9a362a584422", size = 64618, upload-time = "2026-06-11T06:35:53.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/83/a896fc59940fc5a6e2aff3a4be1d92fa890112936803b331cae75a993c34/httpcore2-2.10.0.tar.gz", hash = "sha256:13c0cc3d1919d4f28457f60cd2c2abe04113a8af184ccf1142811beba936f9dc", size = 67427, upload-time = "2026-08-09T09:11:32.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/72/4fdf2306143a92a471fad9f3655aa542d43aa9188a7c9534e82c9aecf837/httpcore2-2.4.0-py3-none-any.whl", hash = "sha256:5218779da5d6e3c2013ac706121abfb3815d450e0613495c0de50264dce58242", size = 80151, upload-time = "2026-06-11T06:35:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4f/d149104195a35e2853a2fc203a8e3477747e58c80e17dda686dace174383/httpcore2-2.10.0-py3-none-any.whl", hash = "sha256:7df06cfb34070cae4f7c89be69dc1095eca138e9704ceffb98d25c1912ab6f01", size = 83000, upload-time = "2026-08-09T09:11:29.555Z" }, ] [[package]] @@ -1130,24 +1165,35 @@ wheels = [ [[package]] name = "httpx2" -version = "2.4.0" +version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "httpcore2" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, { name = "idna" }, - { name = "truststore" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/60/b43ced4ccf26e95b396dbf67051d3e5042b645917d4da0469dd82a3bdd4f/httpx2-2.4.0.tar.gz", hash = "sha256:32e0734b61eb0824b3f56a9e98d6d92d381a3ef12c0045aa917ee63df6c411ef", size = 81691, upload-time = "2026-06-11T06:35:54.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/3d/f9a8c07a3884f3e5b26205e8436a18b3af61c5d53192c3bea235574dbbec/httpx2-2.10.0.tar.gz", hash = "sha256:8741d7329fe2c7885fc9ceb61c8217acfb87a85f75723714b89ebf7ad7196338", size = 98749, upload-time = "2026-08-09T09:11:33.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/45/82bc57c3d9c3314f663b67cc057f1c017a6450685dde513f4f8db5cf431f/httpx2-2.4.0-py3-none-any.whl", hash = "sha256:425acd99297829599decf6701386dd84db3542597d36d3e2e4def930ecd57fd9", size = 74941, upload-time = "2026-06-11T06:35:52.235Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6d/a637d52449d98a6892d9a4dc0262587afdb6a66f201871842dce5a97b1c1/httpx2-2.10.0-py3-none-any.whl", hash = "sha256:5e3194a432701e1cc6f69a8b1b2fa199ef907013fede8d9a09a2c5b7b8141a18", size = 94355, upload-time = "2026-08-09T09:11:30.882Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] name = "huggingface-hub" -version = "1.7.2" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { name = "filelock" }, { name = "fsspec" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, @@ -1158,9 +1204,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/15/eafc1c57bf0f8afffb243dcd4c0cceb785e956acc17bba4d9bf2ae21fc9c/huggingface_hub-1.7.2.tar.gz", hash = "sha256:7f7e294e9bbb822e025bdb2ada025fa4344d978175a7f78e824d86e35f7ab43b", size = 724684, upload-time = "2026-03-20T10:36:08.767Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/d8/748ea0a47f0fa15227fe682f7a80826b4b7c096e4818044b8f56d6cb66d6/huggingface_hub-1.18.0.tar.gz", hash = "sha256:f0c5ecd1ef8c6a60f86f61ee278f2c1570ba9e279c9f54de9094210723b3613b", size = 812699, upload-time = "2026-06-05T09:26:33.401Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" }, + { url = "https://files.pythonhosted.org/packages/0b/03/40a05316cb6616e5b7efd7773656441ab04b4b022c2199e79bb4622a92a3/huggingface_hub-1.18.0-py3-none-any.whl", hash = "sha256:729be4a976fb706dcc02d176bcda8a3f32bdf21a294e8f4b3dda6fbcbc9c1ab1", size = 684411, upload-time = "2026-06-05T09:26:31.48Z" }, ] [[package]] @@ -1238,14 +1284,14 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, ] [[package]] @@ -1271,53 +1317,24 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, ] [[package]] @@ -1331,14 +1348,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.7.1" +version = "1.7.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e0/27a6a081ae25420eda6768ceae05d7022a7f2447f420588843f2a44e4298/joserfc-1.7.4.tar.gz", hash = "sha256:b3bc561672ae541b17a9237053b48a03dacddd92d68047b3ecdfb4b5714a88ed", size = 234027, upload-time = "2026-07-19T15:43:02.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/249dcd99b3376375910b7fa922383b57792975c8758f50d44612e749226c/joserfc-1.7.4-py3-none-any.whl", hash = "sha256:32d46c2cd5e3203c13e87a6c61333cab310b1ba80cd54b4c4f386a848a122463", size = 71000, upload-time = "2026-07-19T15:43:01.299Z" }, ] [[package]] @@ -1346,7 +1363,7 @@ name = "jsonpatch" version = "1.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpointer", marker = "python_full_version < '3.14'" }, + { name = "jsonpointer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } wheels = [ @@ -1441,22 +1458,22 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.4.8" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpatch", marker = "python_full_version < '3.14'" }, - { name = "langchain-protocol", marker = "python_full_version < '3.14'" }, - { name = "langsmith", marker = "python_full_version < '3.14'" }, - { name = "packaging", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "pyyaml", marker = "python_full_version < '3.14'" }, - { name = "tenacity", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, - { name = "uuid-utils", marker = "python_full_version < '3.14'" }, + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/3e/63af6b9d76d9be907c7c524d6ec18a2efed7e0e2d123fea0230d78dbd73f/langchain_core-1.5.3.tar.gz", hash = "sha256:a56457ac444fef41e9404443c187f0ecea708d36e816ea4ba9573c027f7d1a2d", size = 972461, upload-time = "2026-07-30T14:55:55.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" }, + { url = "https://files.pythonhosted.org/packages/36/e6/c7c39efe0bc7e1b7c3d8f54f85846e04c901913c3d3e99068b218558c6f1/langchain_core-1.5.3-py3-none-any.whl", hash = "sha256:48b56fa580277209594dd7baf837f5b9a2a3651613f34ff9fb1728b429df015f", size = 561687, upload-time = "2026-07-30T14:55:54.419Z" }, ] [[package]] @@ -1464,7 +1481,7 @@ name = "langchain-protocol" version = "0.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } wheels = [ @@ -1476,7 +1493,7 @@ name = "langchain-text-splitters" version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core", marker = "python_full_version < '3.14'" }, + { name = "langchain-core" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ @@ -1485,32 +1502,32 @@ wheels = [ [[package]] name = "langsmith" -version = "0.9.1" +version = "0.10.17" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version < '3.14'" }, - { name = "distro", marker = "python_full_version < '3.14'" }, - { name = "httpx", marker = "python_full_version < '3.14'" }, - { name = "orjson", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, - { name = "packaging", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, - { name = "sniffio", marker = "python_full_version < '3.14'" }, - { name = "typing-extensions", marker = "python_full_version < '3.14'" }, - { name = "uuid-utils", marker = "python_full_version < '3.14'" }, - { name = "websockets", marker = "python_full_version < '3.14'" }, - { name = "xxhash", marker = "python_full_version < '3.14'" }, - { name = "zstandard", marker = "python_full_version < '3.14'" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/26/b72987d947278f63ec1e85f01ce85ca7ab2621c7efc0845d4a3a8e5d5dfb/langsmith-0.9.1.tar.gz", hash = "sha256:e5eb905224d156bcece4985285c55b51fffcb06c9353b2c4adb42e1c48b0d05d", size = 4557557, upload-time = "2026-06-23T17:04:23.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/56/73674bd922fe57509c38075d089017d65e02f50d687fd70ee6c0b153d729/langsmith-0.10.17.tar.gz", hash = "sha256:2bf0e69246ef77282ea93e78eb22dad1ec8d8a002d31cbd607d0d6448f9c6458", size = 4798702, upload-time = "2026-08-07T17:23:04.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/42/13c67eb24cddb368df2c8af13e319c86b1d270c90f5b8c9e8baed3593e04/langsmith-0.9.1-py3-none-any.whl", hash = "sha256:1160bf667af63d9bc081821f1df351cb84f7875740858f2a97ffef62b21800a9", size = 578856, upload-time = "2026-06-23T17:04:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/e264e87ce79467aa0cc6c5e945e74050d7880394da325b3559cfdb41a186/langsmith-0.10.17-py3-none-any.whl", hash = "sha256:2a1242a0500147ed846ecbd5fe2f41c21b1652f6833b1af0f209231006f67e5e", size = 734484, upload-time = "2026-08-07T17:23:02.598Z" }, ] [[package]] name = "logfire" -version = "4.30.0" +version = "4.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -1521,9 +1538,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/77/ed3b6453c0c8027724ceb968ca17e550c47e58cdb5dc27458392db40e327/logfire-4.30.0.tar.gz", hash = "sha256:460ed1a7433d88570659903f31b6f9b70903110addbb18b1cf7b414cdb516bb5", size = 1058676, upload-time = "2026-03-23T17:08:28.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/71/1ed06d124bd7905f60004d60cbe14c79bda00f0604bf0cb76214f8c96348/logfire-4.40.0.tar.gz", hash = "sha256:f50f9637f9b5cc3eb5f8526e473effa1992d45056c89adc7c821ee7ab2520c75", size = 1260846, upload-time = "2026-08-05T11:26:59.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/3a/ead5b87ff38292e0ef800b1d184a9a4eedf9f7ce1cf86264b4798a0a8b14/logfire-4.30.0-py3-none-any.whl", hash = "sha256:a520a2b6da7765bc15143fd4098c6f9ec56a836bf3a046f06c823c73af932f3a", size = 302618, upload-time = "2026-03-23T17:08:25.923Z" }, + { url = "https://files.pythonhosted.org/packages/e6/65/2ed148a656362ff3b5cce0bfae619cf57ca1df33d0c942b4c1461e57f57c/logfire-4.40.0-py3-none-any.whl", hash = "sha256:0ac2c950968812f27ee68ccec4a362230acffaffb28f04945ee07729d5866fa0", size = 409440, upload-time = "2026-08-05T11:26:56.745Z" }, ] [package.optional-dependencies] @@ -1533,23 +1550,23 @@ httpx = [ [[package]] name = "logfire-api" -version = "4.30.0" +version = "4.40.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/19/1400a6b1c79c05ccf8058e09f1b10201edce9b6f4ce3094c3200496f6e27/logfire_api-4.30.0.tar.gz", hash = "sha256:ab24c4283fbe90b0f275e59f69ce3b86712cbabec6aa7ae64f411e754c66e3fc", size = 76349, upload-time = "2026-03-23T17:08:30.448Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/5f/f4d0fb5c29d876c533daf415c0d961e1c4d0284167ed0834644a28581230/logfire_api-4.40.0.tar.gz", hash = "sha256:f4631d5ca6af95e9d4dadc4f63619ebb8f2300eecfca0ca99c84403d6ea605de", size = 90781, upload-time = "2026-08-05T11:27:00.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ca/7833b883059b85aef22a2a485e4a7a358c3dda02e1823f87913c411e6410/logfire_api-4.30.0-py3-none-any.whl", hash = "sha256:9138976dc30df4cf21617f1b188a84781683ef2ba88c46008774a7081f16f2c9", size = 121411, upload-time = "2026-03-23T17:08:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/8e/be/ebe35d94e7d567b58d79bd7e1085fe85195ad4b8c8df8882a5a46caa4984/logfire_api-4.40.0-py3-none-any.whl", hash = "sha256:f8b7309235a942368b927f00e0a1869ff0820833f264a30e77a35f1da829c130", size = 140593, upload-time = "2026-08-05T11:26:58.395Z" }, ] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -1580,33 +1597,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "mcp" -version = "1.28.1" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1624,9 +1619,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, ] [[package]] @@ -1640,7 +1635,7 @@ wheels = [ [[package]] name = "mistralai" -version = "2.5.0" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eval-type-backport" }, @@ -1652,18 +1647,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/88/960acba5e95998b4a09998d14ff95a25ada8c96ce8670091e3bf85f25696/mistralai-2.5.0.tar.gz", hash = "sha256:b2724154f62f6505d22027cbdb414e2bb143474bb51f89dc362afb1b141d2bae", size = 504089, upload-time = "2026-06-23T17:05:22.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/e0/9d3e8bcfa73357f997ea297148d16ac243d1a228838334179a9199b713ff/mistralai-2.9.1.tar.gz", hash = "sha256:5b3983c6fddc81b898f1dd5a61a96a59a0e18069c54940cac9b5220dd6b66486", size = 534224, upload-time = "2026-08-04T16:23:45.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/31/472fa3c766aa9b7869dceb6804344cc9945115ae2ee871258b404d0e7c84/mistralai-2.5.0-py3-none-any.whl", hash = "sha256:f29813f1c2e4c19d24707cb5f74ea5c071d106fcaa84dd856a7e4f2d36d908b2", size = 1216614, upload-time = "2026-06-23T17:05:20.819Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/11d537beb67fd7dc0f1028c4a9f4f8230c6328abf6e2f2c9d0447ea0e4a4/mistralai-2.9.1-py3-none-any.whl", hash = "sha256:6f83177b8c4fdffdd2a054cf9d96fc7c0d7a474e856ecd1f0b7070344128516b", size = 1270991, upload-time = "2026-08-04T16:23:43.001Z" }, ] [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] @@ -1708,42 +1703,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] @@ -1779,57 +1738,26 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.4" +version = "2.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, - { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, - { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, - { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, - { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, - { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, - { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, - { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, - { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, ] [[package]] name = "openai" -version = "2.29.0" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1841,9 +1769,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/cf/36e3e7235fdf6d125c052acc0970924611b17a20a4fe580596faf4566a65/openai-2.53.0.tar.gz", hash = "sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116", size = 1099435, upload-time = "2026-08-03T21:42:01.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/78/0f/cc6afea3542a5142c5d8fc8211c5e059a8375105d004a41dfa2c7948dbb0/openai-2.53.0-py3-none-any.whl", hash = "sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618", size = 1659829, upload-time = "2026-08-03T21:41:59.581Z" }, ] [[package]] @@ -1858,6 +1786,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] +[[package]] +name = "opencv-python-headless" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/99/76b7c80252aa83c1af16393454aafd125a0287101afe8deb0a6821af0e30/opencv_python_headless-5.0.0.93.tar.gz", hash = "sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c", size = 81817738, upload-time = "2026-07-02T07:01:06.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7c/8c8097891c509d98cd128493835c95631c80be6a8f37ed9d25716c2e16f1/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f", size = 48322581, upload-time = "2026-07-02T05:50:34.207Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/eab2ad388c3cbab2a350c10c2ef19ce6bd099240afc31789032c996bab52/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00", size = 34782894, upload-time = "2026-07-02T05:51:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/ec/78/afca939f40ffe2b2380bfa86f812b2f7d4acc5a27b27dc41b49cad7ce7b4/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f", size = 36521085, upload-time = "2026-07-02T06:55:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/2b/97/8170e9819764c47e436c130d3ff6cfb73b58f923eae9d3a03d8982b04aec/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4", size = 56563598, upload-time = "2026-07-02T06:55:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/1a28a7101e31801042b3098871a74b76c61581d328ef40774ff4edb53a56/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4", size = 39648433, upload-time = "2026-07-02T06:56:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/9b/21/f6ef335f6e65724aa78b8d792b48d40a48c381715f1e62f5a5049e09d07e/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37", size = 61204038, upload-time = "2026-07-02T06:56:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8f/b8756467ea991449a293797f6b3fa80fcfdd29598a0a60d1cd5715b96e61/opencv_python_headless-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9", size = 35411237, upload-time = "2026-07-02T05:50:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/b8/88/763b967f7efd7226b82c9fae16d560cba049b1f0c036647e65c610fd636e/opencv_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e", size = 43825962, upload-time = "2026-07-02T05:50:09.627Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.39.1" @@ -1982,40 +1929,25 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.8" +version = "3.11.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, - { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, - { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, - { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, - { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, - { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, - { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, ] [[package]] @@ -2027,6 +1959,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "parse" +version = "1.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/f2/0b504486c2a5564798607d3860e48ed19c6443d5e9cc3ec61cc6b8b4ef58/parse-1.22.1.tar.gz", hash = "sha256:d3a4740ec3da338e2b258b2d69741b731eadfddca59e24a14bc4ee5fce38c911", size = 36970, upload-time = "2026-05-26T03:44:52.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/c5/7c16e99869e1f422629092cfd23e3b58e461988c3f9c36fd3624bb4142e6/parse-1.22.1-py2.py3-none-any.whl", hash = "sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32", size = 20925, upload-time = "2026-05-26T03:44:51.156Z" }, +] + +[[package]] +name = "parse-type" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parse" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/ea/42ba6ce0abba04ab6e0b997dcb9b528a4661b62af1fe1b0d498120d5ea78/parse_type-0.6.6.tar.gz", hash = "sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2", size = 98012, upload-time = "2025-08-11T22:53:48.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8d/eef3d8cdccc32abdd91b1286884c99b8c3a6d3b135affcc2a7a0f383bb32/parse_type-0.6.6-py2.py3-none-any.whl", hash = "sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c", size = 27085, upload-time = "2025-08-11T22:53:46.396Z" }, +] + [[package]] name = "pathable" version = "0.6.0" @@ -2038,90 +1992,61 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pdf2image" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811, upload-time = "2024-01-07T20:33:01.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618, upload-time = "2024-01-07T20:32:59.957Z" }, ] [[package]] name = "pgvector" -version = "0.4.2" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/ec/6eb80aebc728200f95229219882994c1b0585b956ca47da5edb9d062627a/pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74", size = 35170, upload-time = "2026-07-06T18:27:27.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" }, ] [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, ] [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/0a/062135c9a98dac804265073cc3afdbec5ae1aa37980bb354f461bafe81b4/platformdirs-4.11.1.tar.gz", hash = "sha256:bb1af68078f25e2f3e111e2d43b8d536df41b73c8a684b40bb018223b66fae27", size = 32396, upload-time = "2026-08-07T23:06:48.516Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/85/9b31b44296cfa3bb56cddb35e6a0f6578bab0b490c0806c0245e32c6110c/platformdirs-4.11.1-py3-none-any.whl", hash = "sha256:2efd27d363e8dd2e661639ffb398865a5e0a46442a11d266bf375a0e0c10e386", size = 23261, upload-time = "2026-08-07T23:06:47.219Z" }, ] [[package]] @@ -2135,100 +2060,72 @@ wheels = [ [[package]] name = "posthog" -version = "7.9.12" +version = "7.38.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, { name = "distro" }, - { name = "python-dateutil" }, { name = "requests" }, - { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/4f/edfbe2aac190ade695693a452bb6d979e8ca5bc54236c28cb796a43ce79d/posthog-7.38.3.tar.gz", hash = "sha256:2a21f13eb48985be58d9533221ec032f579a0f5becf14264a3ea277888af21d1", size = 422163, upload-time = "2026-08-07T18:05:13.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/e5a968a7f843dc31215c694e2edfafa0644e9b9e833588c1f4adfc0175ee/posthog-7.38.3-py3-none-any.whl", hash = "sha256:099aeea324b6c20e352a5bf37a43428fce20c76459d3d27da3b8c8df7814179f", size = 496878, upload-time = "2026-08-07T18:05:12.178Z" }, ] [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "propcache" -version = "0.4.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -2248,14 +2145,14 @@ wheels = [ [[package]] name = "psycopg" -version = "3.3.3" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, ] [package.optional-dependencies] @@ -2268,31 +2165,20 @@ pool = [ [[package]] name = "psycopg-binary" -version = "3.3.3" +version = "3.3.4" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, - { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, - { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, - { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, - { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, - { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, - { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, - { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, - { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, - { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, - { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, - { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" }, ] [[package]] @@ -2309,15 +2195,15 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.4.4" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, ] [package.optional-dependencies] @@ -2334,11 +2220,11 @@ memory = [ [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -2364,7 +2250,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2372,9 +2258,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -2384,19 +2270,19 @@ email = [ [[package]] name = "pydantic-ai" -version = "1.107.0" +version = "1.107.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "spec", "temporal", "ui", "vertexai", "xai"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/71/7132293bc7ce791d8f3ee7f56b696b216a104bb30a08113f7b3db30a0c9a/pydantic_ai-1.107.0.tar.gz", hash = "sha256:254496d18451b2750a4dddc2591c6c42f25dd223d02ca4252812ab06e75f1044", size = 18414, upload-time = "2026-06-10T14:53:08.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/ac/2edbefc3b6787d4e96a5c2d9764ceb95113dd6c5ed1db96fb0a6b75620ba/pydantic_ai-1.107.2.tar.gz", hash = "sha256:324f8a67821100b51c7d26667bea73bd6c229c9298093cdaa133478f389964a0", size = 18414, upload-time = "2026-08-08T03:31:43.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/38/db37ab59fced191a75babbbbe99470e512c91fe730b27d113e3280fdbe44/pydantic_ai-1.107.0-py3-none-any.whl", hash = "sha256:e031880b44ad7ce3836b2f6aa8ce2a0bd733cdb0b89a34adba647e96ddcba788", size = 7588, upload-time = "2026-06-10T14:53:00.57Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/006df19f193276592def31b87be34d821376753f7a28cd993fde2c3b109f/pydantic_ai-1.107.2-py3-none-any.whl", hash = "sha256:d0705b09b7941f36d9859c6c4b2c32d2b477ba7c2326f33709f8869799cb5c21", size = 7588, upload-time = "2026-08-08T03:31:35.204Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "1.107.0" +version = "1.107.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "genai-prices" }, @@ -2407,9 +2293,9 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/26/ced63dfaabbc77f3beb86d59689cdea748e7ccffb6b419dbaf4780f211e8/pydantic_ai_slim-1.107.0.tar.gz", hash = "sha256:4616f689a92fcfecfecf2a7af27aca22f139a873cf6d7a8929eaeee9c0eedbb4", size = 779902, upload-time = "2026-06-10T14:53:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/30/84e55952dcca7630a2fb5ebb0db69065c5be85c26b286465bffda735a36d/pydantic_ai_slim-1.107.2.tar.gz", hash = "sha256:aea6d6fee68d8fb091e10c82b22c80e9f42278011d44b25ef632795306e08dcb", size = 782375, upload-time = "2026-08-08T03:31:45.548Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/57/71044e17f931b08cc3930bc0fe5a1e1fd37fa474ae826be004729ef1cb4a/pydantic_ai_slim-1.107.0-py3-none-any.whl", hash = "sha256:1af49bbae06a6c598f72c54d4734ba377100cac493c9a05fa8e089bebeae0da6", size = 964046, upload-time = "2026-06-10T14:53:03.333Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/a07de641328fee012b4540ba783881ce8aa8b8d177730bc38277a460364e/pydantic_ai_slim-1.107.2-py3-none-any.whl", hash = "sha256:ad61b4dac076fa933fc80f38150714b9276e1f197121422a85a9021c3d131782", size = 966503, upload-time = "2026-08-08T03:31:37.931Z" }, ] [package.optional-dependencies] @@ -2480,7 +2366,7 @@ vertexai = [ { name = "requests" }, ] voyageai = [ - { name = "voyageai", marker = "python_full_version < '3.14'" }, + { name = "voyageai" }, ] xai = [ { name = "xai-sdk" }, @@ -2488,60 +2374,33 @@ xai = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, ] [[package]] name = "pydantic-evals" -version = "1.107.0" +version = "1.107.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2551,14 +2410,14 @@ dependencies = [ { name = "pyyaml" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/ad/f33306cf668d33cabce8af5e82b58ae367cc8422a2078c2df13327c13532/pydantic_evals-1.107.0.tar.gz", hash = "sha256:153378eda4847d48ca5e6b995e6221e5f0e4f4641771a1eed244fa2065923b27", size = 78546, upload-time = "2026-06-10T14:53:12.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/09/ef127d1f56a33759c7f8ed8aa687d338fdc00be0bad07055044db8c2010e/pydantic_evals-1.107.2.tar.gz", hash = "sha256:ead8c28c3d85a5d43df34eab6da9f8ac848e97e20964494713f4605430931de0", size = 78543, upload-time = "2026-08-08T03:31:46.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/ea/ecdaab037479f3b26c0efd47598f7caaad506fc53837196c1cf7e4701263/pydantic_evals-1.107.0-py3-none-any.whl", hash = "sha256:2df2f6deb82226f0404137ebecf9389c2270e08e30c066e239ae9090173826ec", size = 93586, upload-time = "2026-06-10T14:53:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/27/f5/75a23d488201310504040443a9107ca8537d0f2c8c0422e49c143311351e/pydantic_evals-1.107.2-py3-none-any.whl", hash = "sha256:e72d062035cc9c1629476483443fc7e53bfc635c48f1d37bac6af3a86090ee17", size = 93584, upload-time = "2026-08-08T03:31:39.737Z" }, ] [[package]] name = "pydantic-graph" -version = "1.107.0" +version = "1.107.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -2566,9 +2425,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/c3/6e8c2d13b8701041f1b3eac5deb41f25d4dbfa479a190d5c6becc23f2a49/pydantic_graph-1.107.0.tar.gz", hash = "sha256:278dd89b3e33f3a2963ac949f27a53aef705c5d883a8ce5d06d23e6e3cfbd972", size = 62564, upload-time = "2026-06-10T14:53:13.366Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/d2b88e938cbc174c4a2320e4bc2c1a6a362779cf9e582f16ae18ffffec65/pydantic_graph-1.107.2.tar.gz", hash = "sha256:1a6c35e80110da72352c107bf9f8195b385a3297d7ed3f68c07394b940ce0953", size = 62567, upload-time = "2026-08-08T03:31:47.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/72/621556e3f5068400d43a0375d38e5963de30256eaa5a702aba12e82ed0ff/pydantic_graph-1.107.0-py3-none-any.whl", hash = "sha256:71add94fe7e14c703977a895117c475aae6c0b02a774a036c4d00d9a63c78b00", size = 80106, upload-time = "2026-06-10T14:53:06.543Z" }, + { url = "https://files.pythonhosted.org/packages/93/84/f790f84728cafba744d8d55cb1d1dd9a659d0cd74d55b49bc514763a006c/pydantic_graph-1.107.2-py3-none-any.whl", hash = "sha256:ec8866d61ec042988f05946140a226d6d8dd5b24a0cb4647a638fc87c493edfd", size = 80106, upload-time = "2026-08-08T03:31:41.271Z" }, ] [[package]] @@ -2585,16 +2444,25 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.2" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pydyf" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" }, ] [[package]] @@ -2620,6 +2488,20 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pypdf" +version = "6.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyperclip" version = "1.11.0" @@ -2629,17 +2511,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] +[[package]] +name = "pyphen" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, +] + [[package]] name = "pyright" -version = "1.1.408" +version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [[package]] @@ -2699,30 +2590,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, ] [[package]] @@ -2750,24 +2628,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, +] + +[package.optional-dependencies] +pil = [ + { name = "pillow" }, ] [[package]] @@ -2785,74 +2662,55 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" +version = "2026.7.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, +] + +[[package]] +name = "reportlab" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/d6/4b7b0cf56880eb96533e607967be6a939e344675601e033d113a0bfa1f4e/reportlab-5.0.0.tar.gz", hash = "sha256:e4494a0c6623ae213bb856fba523171b2b54a7bf629fda02d5e525a7b899a784", size = 3701928, upload-time = "2026-06-18T11:34:31.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/07/70085c17a369605f15e301d10ab902115019b1126c7253d964afc230c7d6/reportlab-5.0.0-py3-none-any.whl", hash = "sha256:9d5a3affa84919e1111ede580031266a570e93b1ce388219621347965ff1d93c", size = 1956710, upload-time = "2026-06-18T11:34:29.07Z" }, ] [[package]] @@ -2875,7 +2733,7 @@ name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -2884,94 +2742,51 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rich-rst" -version = "2.0.1" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/56/3191bae66b08ccc637ea8120426068bcb361cc323c96404c310886937067/rich_rst-2.0.1.tar.gz", hash = "sha256:cbe236ed0901d1ec8427cc6a50bf0a34353ba28ad014dc24def68bfe7f3b9e68", size = 300570, upload-time = "2026-05-16T00:47:57.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/d6/d0b9fafc73b65767200da027acab1db1bdb1048f4fea5ebf659df01c700e/rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd", size = 302732, upload-time = "2026-07-05T02:59:44.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/3d/55c17d3ebdf3cd81356002afe5bef9bb8af631db2819785b6eac845b925b/rich_rst-2.0.1-py3-none-any.whl", hash = "sha256:7ee15f345ce25fa02b582c272a6cdbaf0c21243e38061cea273cff659bf3ef61", size = 272922, upload-time = "2026-05-16T00:47:55.508Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/1fc93dd759605b5d00fc98b50200739e41ed32bd22d6ba35ca6c3932371b/rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255", size = 272987, upload-time = "2026-07-05T02:59:42.792Z" }, ] [[package]] name = "rpds-py" -version = "0.30.0" +version = "2026.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, ] [[package]] @@ -3001,14 +2816,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.19.0" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] [[package]] @@ -3051,6 +2866,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "soupsieve" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, +] + [[package]] name = "sqlite-vec" version = "0.1.9" @@ -3065,32 +2889,32 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.3.3" +version = "3.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/2f/9223c24f568bb7a0c03d751e609844dce0968f13b39a3f73fbb3a96cd27a/sse_starlette-3.3.3.tar.gz", hash = "sha256:72a95d7575fd5129bd0ae15275ac6432bb35ac542fdebb82889c24bb9f3f4049", size = 32420, upload-time = "2026-03-17T20:05:55.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/e2/b8cff57a67dddf9a464d7e943218e031617fb3ddc133aeeb0602ff5f6c85/sse_starlette-3.3.3-py3-none-any.whl", hash = "sha256:c5abb5082a1cc1c6294d89c5290c46b5f67808cfdb612b7ec27e8ba061c22e8d", size = 14329, upload-time = "2026-03-17T20:05:54.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, ] [[package]] name = "starlette" -version = "1.3.1" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] [[package]] name = "temporalio" -version = "1.29.0" +version = "1.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, @@ -3098,13 +2922,15 @@ dependencies = [ { name = "types-protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/95/22e2e19ad95d42d69320e8cd8d48c8543841f367d2d5a4dca48165e466d2/temporalio-1.29.0.tar.gz", hash = "sha256:3fbfddc5f847956ca20c2e671d3f09cefdb2ab6f0d6a6d59664c6102e922f80b", size = 2657001, upload-time = "2026-06-17T21:18:16.72Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/43/676b56efaa64def06d4a9282cfc08d5a5b1c89de0ded47b692f8eaa30ca8/temporalio-1.31.0.tar.gz", hash = "sha256:2993f4b880170825414116dec7d7159a5d199efc279f29570be6b5c4a9f6edd5", size = 2785306, upload-time = "2026-07-29T17:55:10.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/82/e0810e7e7f5f4eb97e40468abf6bc8d3df0dbc882c09a2c207c38c90d56d/temporalio-1.29.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d18f989fbea13014e3221de034fbd1945ec05499b663195f0eacb49cc922986", size = 14874156, upload-time = "2026-06-17T21:18:03.299Z" }, - { url = "https://files.pythonhosted.org/packages/41/48/55a2f948f4cd30d8dcaf1d37a15e23b34d90a68d0d1da6765a08c650bbb2/temporalio-1.29.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:28bc12b64a7ffc08f7709897d3a0c93faa8972227734669f248cb7de2ce3e69f", size = 14384773, upload-time = "2026-06-17T21:18:06.151Z" }, - { url = "https://files.pythonhosted.org/packages/1d/71/a13f427089b54de4e11f347b2043934d693baffda569c4b4914e588a6074/temporalio-1.29.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd02a107c3f5fa088ed5e4aeda9af91d11afbc53dfd5b28d730776354b159297", size = 14602055, upload-time = "2026-06-17T21:18:08.704Z" }, - { url = "https://files.pythonhosted.org/packages/73/6a/3ee32775c9b5172560add2ec69c554f476bc6c51ea7a3c24206e03f08cc5/temporalio-1.29.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:950b42e05793762eb7b4b3809e78c9e9dcdf2f23f797aa939ac17eca8ad9b3d3", size = 15075878, upload-time = "2026-06-17T21:18:11.499Z" }, - { url = "https://files.pythonhosted.org/packages/f5/92/24ef16052075a08e7ed071311704a244944485bc8581d82313f0c3d1b385/temporalio-1.29.0-cp310-abi3-win_amd64.whl", hash = "sha256:8969718f947c6b9df3b51ab4a71bad67abfce81a1023aa1a598a224b712b713a", size = 15333733, upload-time = "2026-06-17T21:18:14.201Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7c/b91d831df50651a26562285de7c434a14243504eed853513da6e6afa3f19/temporalio-1.31.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5f9aaf06000768eb80cf5774457d227379dae455a3099aa0a18c4dca631c0d27", size = 14505266, upload-time = "2026-07-29T17:54:52.932Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d9/4655fc5d7ea662aac2af972e114ffeae45800bcded5ca3d31b2bcd99179d/temporalio-1.31.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:abbfb486ba00053ccfa57c9c38b38f80f5dbfd08c67beb7a621515a57a9b9e9b", size = 14037187, upload-time = "2026-07-29T17:54:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/52bd6a90cba3d4b257d7aa7426598e622cf05ab39a174b477535eba59c14/temporalio-1.31.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e840b462125274cb0d4748e7cedc386c303b2a51bf76bab30b96f1ebbc657ae", size = 14445065, upload-time = "2026-07-29T17:54:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/d9/91/b644c2122943939e02c0e0ff1902b9c6ff7d5e0d6eec334abbfd4ee6bb17/temporalio-1.31.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04d26f0f634f5325f6a19a45cd67cc5a2da4bd35268d27997f6723134d38c8be", size = 14831115, upload-time = "2026-07-29T17:55:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/8f/42/abc82a89323234026753ac801a1dbc36b4322dcdec5f1e38bee6405f3493/temporalio-1.31.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f23f36d0e5d2e67f2129fc1cb876cf1e4e614f791a717d692f7c15fb732abe41", size = 14542828, upload-time = "2026-07-29T17:55:03.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/9260f4544eb5d44867ef58e4a0e63dd3d643e26b2a94e21e027ceaeb0059/temporalio-1.31.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52f8cc7f0b5a19d49f0c2d748420267aaacc2be0bb614743de8d10faf77a57c9", size = 15019517, upload-time = "2026-07-29T17:55:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/ce/96/b71e66a1921906f072038286e30946098531fd3363029f3d08c1723aa39a/temporalio-1.31.0-cp310-abi3-win_amd64.whl", hash = "sha256:aa6e9602829584b22037da6ccf99d2e089f02cec86ee7178206b1afb115cf092", size = 15527552, upload-time = "2026-07-29T17:55:08.287Z" }, ] [[package]] @@ -3118,80 +2944,109 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tinyhtml5" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" }, ] [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] @@ -3205,19 +3060,19 @@ wheels = [ [[package]] name = "typeguard" -version = "4.5.1" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/de/4420db493fa8fc0856d5e5c1b159c63a323d2de2317babe36b01568928e8/typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21", size = 82330, upload-time = "2026-07-26T08:40:23.207Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/461d5f167b6f5c7d97696f397c82f82e3480e003fce3f0a1cd1dd26e2eb2/typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7", size = 36884, upload-time = "2026-07-26T08:40:21.868Z" }, ] [[package]] name = "typer" -version = "0.24.1" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -3225,39 +3080,39 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] [[package]] name = "types-protobuf" -version = "6.32.1.20260221" +version = "7.34.1.20260518" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/59/e2b13b499d15e6720150c4b1a8d91e31fcacf716b432397475b3151ff7e4/types_protobuf-7.34.1.20260518.tar.gz", hash = "sha256:28cfaded25889cb83ebfb63cfb0a43628f0b6f3785767bec17287dc6468795f2", size = 68936, upload-time = "2026-05-18T06:01:47.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260518" +version = "2.33.0.20260712" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -3274,11 +3129,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.1" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] @@ -3290,6 +3145,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, ] +[[package]] +name = "unoserver" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/7c/9250bf071eb9d0012998b774bbf5743a09c715ab5dfd50460c8ba09c2564/unoserver-3.7.tar.gz", hash = "sha256:b05f9578506ac7374ae1b314c3a79528636c542ac78220a9ce99110584ca424b", size = 115811, upload-time = "2026-06-10T13:34:23.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/1d/8cfa91f04d5865ed6d30e1375c22fed98694aafd6171b4979dd7f5f9b16d/unoserver-3.7-py3-none-any.whl", hash = "sha256:fc44e6808071c9d2957e705ecf1742cea8a582aa5d5cc23babf36bb332ec6e8e", size = 28559, upload-time = "2026-06-10T13:34:21.911Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -3301,58 +3165,58 @@ wheels = [ [[package]] name = "uuid-utils" -version = "0.14.1" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, ] [[package]] name = "uvicorn" -version = "0.42.0" +version = "0.52.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, ] [[package]] name = "voyageai" -version = "0.3.7" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp", marker = "python_full_version < '3.14'" }, - { name = "aiolimiter", marker = "python_full_version < '3.14'" }, - { name = "ffmpeg-python", marker = "python_full_version < '3.14'" }, - { name = "langchain-text-splitters", marker = "python_full_version < '3.14'" }, - { name = "numpy", marker = "python_full_version < '3.14'" }, - { name = "pillow", marker = "python_full_version < '3.14'" }, - { name = "pydantic", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "tenacity", marker = "python_full_version < '3.14'" }, - { name = "tokenizers", marker = "python_full_version < '3.14'" }, + { name = "aiohttp" }, + { name = "aiolimiter" }, + { name = "langchain-text-splitters" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/16/1b46b3cd401e1717a68197c1fe336d7bb4e0a1833f8105e1738f5b1add05/voyageai-0.3.7.tar.gz", hash = "sha256:826cd97f97223f42b5babc5c459c9c80f3a8215ce5c0e007b0b276550f790d24", size = 26485, upload-time = "2025-12-16T18:43:05.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/d3/b96ecab961692dd55fc5b54fef916c2bc37a942ef33fcc937779d2aaa161/voyageai-0.5.0.tar.gz", hash = "sha256:ed2775fe9faeb96cc2b3931edc35d76185d19f34f1523d1f70535f0451126ae9", size = 37000, upload-time = "2026-07-10T20:13:14.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/64/89f6325666d6836979f94ac88b96fefc7527e02e61abc81359843585e088/voyageai-0.3.7-py3-none-any.whl", hash = "sha256:909f6c033001e5a3b3caf970525bf3614a1bfef9003cf3c3b68207dfdb53e86d", size = 34691, upload-time = "2025-12-16T18:43:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c4/732038d97a0ac02d690f295a9ee28f4d0553894478ca6944411428e544f7/voyageai-0.5.0-py3-none-any.whl", hash = "sha256:5d7e8dc3b74cac4646d4a835a65cf8c9764070c90353e322d84a91a16207e78b", size = 46561, upload-time = "2026-07-10T20:13:13.909Z" }, ] [[package]] @@ -3389,87 +3253,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, - { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, - { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, - { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, - { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, - { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "weasyprint" +version = "69.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "cssselect2" }, + { name = "fonttools", extra = ["woff"] }, + { name = "pillow" }, + { name = "pydyf" }, + { name = "pyphen" }, + { name = "tinycss2" }, + { name = "tinyhtml5" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/53/dcc3885c2f7a47faa45f6b8b801412f5f9e055173a52801ef01c09943c5a/weasyprint-69.0.tar.gz", hash = "sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c", size = 1549834, upload-time = "2026-06-02T14:42:17.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/cb/208525c6bd5033d7b2589b55e07bec23d9c61bb00703cbaf20ef52c3811f/weasyprint-69.0-py3-none-any.whl", hash = "sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6", size = 322872, upload-time = "2026-06-02T14:42:15.871Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] [[package]] name = "websockets" -version = "16.0" +version = "16.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] @@ -3488,26 +3334,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] @@ -3532,165 +3358,109 @@ wheels = [ [[package]] name = "xxhash" -version = "3.6.0" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562, upload-time = "2026-07-06T10:45:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656, upload-time = "2026-07-06T10:45:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169, upload-time = "2026-07-06T10:45:15.687Z" }, + { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177, upload-time = "2026-07-06T10:45:17.035Z" }, + { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642, upload-time = "2026-07-06T10:45:18.39Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684, upload-time = "2026-07-06T10:45:20.033Z" }, + { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401, upload-time = "2026-07-06T10:45:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617, upload-time = "2026-07-06T10:45:23.234Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295, upload-time = "2026-07-06T10:45:25.364Z" }, + { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688, upload-time = "2026-07-06T10:45:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740, upload-time = "2026-07-06T10:45:28.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599, upload-time = "2026-07-06T10:45:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559, upload-time = "2026-07-06T10:45:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383, upload-time = "2026-07-06T10:45:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228, upload-time = "2026-07-06T10:45:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137, upload-time = "2026-07-06T10:45:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068, upload-time = "2026-07-06T10:45:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874, upload-time = "2026-07-06T10:45:43.414Z" }, + { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702, upload-time = "2026-07-06T10:45:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351, upload-time = "2026-07-06T10:45:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396, upload-time = "2026-07-06T10:45:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974, upload-time = "2026-07-06T10:45:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739, upload-time = "2026-07-06T10:45:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239, upload-time = "2026-07-06T10:45:53.714Z" }, + { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964, upload-time = "2026-07-06T10:45:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697, upload-time = "2026-07-06T10:45:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954, upload-time = "2026-07-06T10:45:58.897Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776, upload-time = "2026-07-06T10:46:00.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776, upload-time = "2026-07-06T10:46:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056, upload-time = "2026-07-06T10:46:04.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108, upload-time = "2026-07-06T10:46:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739, upload-time = "2026-07-06T10:46:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658, upload-time = "2026-07-06T10:46:10.504Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059, upload-time = "2026-07-06T10:46:12.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535, upload-time = "2026-07-06T10:46:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292, upload-time = "2026-07-06T10:46:16.452Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490, upload-time = "2026-07-06T10:46:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861, upload-time = "2026-07-06T10:46:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739, upload-time = "2026-07-06T10:46:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657, upload-time = "2026-07-06T10:46:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377, upload-time = "2026-07-06T10:46:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274, upload-time = "2026-07-06T10:46:28.39Z" }, + { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375, upload-time = "2026-07-06T10:46:29.962Z" }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] + +[[package]] +name = "zopfli" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/21/3b6af43a663b22b00e738bb0642931a2579e15da6852613d56c6aa535d28/zopfli-0.4.3.tar.gz", hash = "sha256:d3a50f91a13cea9bafe025de8fd87a005eb26de02a4f0c193127ddbf23ac8ebe", size = 179156, upload-time = "2026-06-10T09:10:19.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/5f/b7d81b670daf990e15a0f7551da96c3c0700f69ae6d96b0245d6a19f51f3/zopfli-0.4.3-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:88f4fbe429aad72bc206275d81fab11a097e0f951a5848d1f51083c37ea73073", size = 291492, upload-time = "2026-06-10T09:10:06.621Z" }, + { url = "https://files.pythonhosted.org/packages/55/c8/d8d8d731e0b192024567b7198fb77b748821d355f3c8bf0109de27191f43/zopfli-0.4.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:769875152d0625c46707bcca57d4b2233fe653482067acd55fbf6ec525cb9bdc", size = 829354, upload-time = "2026-06-10T09:10:07.909Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2b/fbe8ba2ec40f5986b8983a4752f7a32672a80a10ea6e68213324a7055469/zopfli-0.4.3-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0c9c1d40a8cb1d58762d7e57290ccb753e0828c4d01be8acb59aae5d0ca206", size = 818436, upload-time = "2026-06-10T09:10:09.063Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/63568c54c8b68b9135f3456c5add83797a5528d596657f0e4f4910173b08/zopfli-0.4.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7fa3c35193475290e3f007bbcdebdbae64ba2f012d75c632da0d727e1da50d5e", size = 1778931, upload-time = "2026-06-10T09:10:10.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/8f3aac10a858e89c2146d3a1f6ce33634c3db757365b4148fef1b85784d2/zopfli-0.4.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47604eee5c6704bdf0e94d8391fe3b74ddb2abd84128fbcfdc3ee0fc265feaef", size = 1864132, upload-time = "2026-06-10T09:10:11.595Z" }, + { url = "https://files.pythonhosted.org/packages/8d/20/9ca59d14b91f9fbc631793b4b085b309777edadaca496aa518a180817827/zopfli-0.4.3-cp310-abi3-win32.whl", hash = "sha256:628c3e941752880b3491db8d44163d0aedb221944e22a17187ff7fc549b050f6", size = 271715, upload-time = "2026-06-10T09:10:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3a/4ff4fdead77ef30f5832b38a47eb7a1283e98b3c678576b83f8fdfff53eb/zopfli-0.4.3-cp310-abi3-win_amd64.whl", hash = "sha256:921c2c9907f4364963848da5ad194b46d68865e07fdb975d04fd09bc42d47357", size = 288550, upload-time = "2026-06-10T09:10:13.639Z" }, + { url = "https://files.pythonhosted.org/packages/e6/44/6264f929057236fde72dd6d271f54612b4811ce37288e002f5d5339d696a/zopfli-0.4.3-cp310-abi3-win_arm64.whl", hash = "sha256:7e9703ca6e7ef66c8d05e0826b6f558b680c9db8206f84f05a3ee93430a12e42", size = 451343, upload-time = "2026-06-10T09:10:14.72Z" }, ] [[package]] @@ -3716,19 +3486,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, ] diff --git a/frontend/.gitignore b/frontend/.gitignore index 0605e6e79f..9ab1c65091 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -48,3 +48,5 @@ test-results /scripts/dev-update-test/.update-dist/ /scripts/dev-update-test/screenshots/ /editor/src-tauri/tauri.conf.dev-update.json +.a11y-scan/ +.a11y-acc/ diff --git a/frontend/.storybook/a11y-baseline.dark.json b/frontend/.storybook/a11y-baseline.dark.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/frontend/.storybook/a11y-baseline.dark.json @@ -0,0 +1 @@ +{} diff --git a/frontend/.storybook/a11y-baseline.json b/frontend/.storybook/a11y-baseline.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/frontend/.storybook/a11y-baseline.json @@ -0,0 +1 @@ +{} diff --git a/frontend/.storybook/a11y-changed.mjs b/frontend/.storybook/a11y-changed.mjs new file mode 100644 index 0000000000..f6daef5dbb --- /dev/null +++ b/frontend/.storybook/a11y-changed.mjs @@ -0,0 +1,73 @@ +// Prints the story files a branch affects, one per line — the scan set for the +// pull-request a11y gate. A story is affected if its file changed against the +// base ref (or is uncommitted/untracked), or if a same-named sibling source +// file changed: stories render the live component, so editing Button.tsx or +// Button.css changes what Button.stories.tsx shows without touching it. +// +// node a11y-changed.mjs [base-ref] (default origin/main) +// +// Node rather than shell so the task works no matter what invokes it — Task's +// embedded interpreter runs on Windows, but sed/grep/sort do not exist for +// developers calling tasks from PowerShell. +import { execFileSync } from "node:child_process"; +import { readdirSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; + +const base = process.argv[2] || "origin/main"; + +const git = (...args) => + execFileSync("git", args, { encoding: "utf8" }) + .split("\n") + .map((l) => l.trim().replace(/^frontend\//, "")) + .filter(Boolean); + +const STORY = /\.stories\.tsx?$/; +const TEST = /\.test\.tsx?$/; +const SOURCE = /\.(ts|tsx|css)$/; + +// Committed changes vs the base, plus working-tree changes, plus untracked +// files — so the gate covers exactly what the branch would merge and what a +// developer is about to commit. +const changed = [ + ...git("diff", "--name-only", "--diff-filter=d", `${base}...HEAD`), + ...git("diff", "--name-only", "--diff-filter=d"), + ...git("ls-files", "--others", "--exclude-standard"), +]; + +const stories = new Set(); +for (const f of changed) { + if (STORY.test(f)) { + stories.add(f); + continue; + } + if (TEST.test(f) || !SOURCE.test(f)) continue; + // A story file does not have to match its source's case — tokens.css sits + // beside Tokens.stories.tsx. Deriving the name from the source and trusting + // existsSync silently skips those on a case-sensitive filesystem, and on a + // case-insensitive one feeds the scan a path no result will ever match. Read + // the directory instead and compare case-insensitively, then use the name as + // it is actually spelled on disk. + const dir = dirname(f) || "."; + const stem = basename(f).replace(SOURCE, "").toLowerCase(); + let entries; + try { + entries = readdirSync(dir); + } catch { + continue; + } + for (const entry of entries) { + if (!STORY.test(entry)) continue; + if (entry.replace(STORY, "").toLowerCase() !== stem) continue; + stories.add(join(dir, entry).split("\\").join("/")); + } +} + +// One line, each path quoted: the output is interpolated into a task command, +// where a newline would end the command after the first story and an unquoted +// space would split a path into two arguments. +process.stdout.write( + [...stories] + .sort() + .map((s) => `"${s}"`) + .join(" "), +); diff --git a/frontend/.storybook/a11y-check.mjs b/frontend/.storybook/a11y-check.mjs new file mode 100644 index 0000000000..43092812ff --- /dev/null +++ b/frontend/.storybook/a11y-check.mjs @@ -0,0 +1,232 @@ +// a11y regression gate — baseline diff. +// +// Consumes the Storybook Vitest scan's JSON reporter output (one or more files +// in --in) and compares each story's axe rule violations against +// .storybook/a11y-baseline.json. Existing violations are grandfathered; the gate +// fails when a story breaks a rule it wasn't already breaking, when a story +// crashes, or when the scan didn't cover everything it was supposed to. +// +// node a11y-check.mjs --in

--manifest # diff (the gate) +// node a11y-check.mjs --in --manifest --record # (re)write baseline +// node a11y-check.mjs --in --merge # union into baseline +// +// Exit codes: 0 pass · 1 regression · 2 unusable scan (incomplete/no input). +// +// The baseline records WHICH rules a story breaks, not how many nodes break +// them. Node counts drift between runs (stories fetch data asynchronously, so +// axe sometimes samples the DOM before late content lands), which made a +// count-based gate cry wolf on unrelated changes. Rule presence is stable, and +// "story now breaks a rule it didn't before" is the regression worth blocking. +// +// Run from frontend/. Baseline path defaults next to this script. + +import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const args = process.argv.slice(2); +const opt = (n, d) => { + const i = args.indexOf(n); + const v = i >= 0 ? args[i + 1] : undefined; + // A flag given without a value is a mistake, not a request for the default. + if (i >= 0 && (v === undefined || v.startsWith("--"))) { + console.error(`a11y-check: ${n} requires a value`); + process.exit(2); + } + return v ?? d; +}; +const record = args.includes("--record"); +const merge = args.includes("--merge"); +const inDir = opt("--in", ".a11y-scan"); +const manifestFile = opt("--manifest", ""); +const baselineFile = opt("--baseline", join(here, "a11y-baseline.json")); + +// Every axe violation block in the matcher's message ends with a link to the +// rule's docs, e.g. .../rules/axe/4.11/color-contrast?application=axeAPI. That +// URL is the only machine-stable rule id in the message — the human-readable +// help text around it is free to change between addon and axe versions. +const RULE_URL = /dequeuniversity\.com\/rules\/axe\/[\d.]+\/([a-z0-9-]+)/g; + +/** Reads every scan report in `dir`, keyed by " :: ". */ +function collect(dir) { + const rules = {}; // storyKey -> Set(ruleId) + const crashed = []; // storyKey[] — failed for a non-a11y reason + const unloadable = []; // storyFile[] — the file itself never ran + const seenFiles = new Set(); + let scanned = 0; + + for (const cf of readdirSync(dir).filter((f) => /\.json$/.test(f))) { + let report; + try { + report = JSON.parse(readFileSync(join(dir, cf), "utf8")); + } catch { + console.error(`a11y-check: unreadable scan report: ${cf}`); + process.exit(2); + } + for (const tf of report.testResults || []) { + const norm = tf.name.replace(/\\/g, "/"); + const idx = norm.search(/editor\/src\//); + const file = idx >= 0 ? norm.slice(idx) : norm; + seenFiles.add(file); + // A story file that fails to import produces a failed suite with no + // assertions at all. Every other check here reads assertions, so such a + // file satisfies the manifest and contributes nothing — its stories go + // unscanned while the run still reports clean. + if ((tf.assertionResults || []).length === 0 && tf.status !== "passed") { + unloadable.push(file); + continue; + } + for (const a of tf.assertionResults || []) { + scanned++; + if (a.status === "passed") continue; + const key = `${file} :: ${a.title || a.fullName || "?"}`; + const found = new Set(); + for (const m of a.failureMessages || []) + for (const hit of m.matchAll(RULE_URL)) found.add(hit[1]); + if (found.size) { + rules[key] = rules[key] || new Set(); + for (const id of found) rules[key].add(id); + } else { + // No rule link anywhere in the message: the story threw, timed out or + // otherwise failed. Silently dropping these is how a broken scan can + // look clean — and a crashed story reports no violations at all, + // which would also mask any it really has. + crashed.push(key); + } + } + } + } + return { rules, crashed, unloadable, seenFiles, scanned }; +} + +if (!existsSync(inDir)) { + console.error(`a11y-check: scan dir not found: ${inDir}`); + process.exit(2); +} +const { rules, crashed, unloadable, seenFiles, scanned } = collect(inDir); +const observed = {}; +for (const [k, set] of Object.entries(rules)) observed[k] = [...set].sort(); + +// Completeness: every story file the scan was asked to cover must appear in the +// reports. A dropped batch (killed process, empty report) would otherwise read +// as "those stories have no violations" and pass. +if (manifestFile) { + if (!existsSync(manifestFile)) { + console.error(`a11y-check: manifest not found: ${manifestFile}`); + process.exit(2); + } + const expected = readFileSync(manifestFile, "utf8") + .split("\n") + .map((l) => l.trim().replace(/\\/g, "/")) + .filter(Boolean); + const missing = expected.filter((f) => !seenFiles.has(f)); + if (missing.length) { + console.error( + `a11y-check: scan is incomplete — ${missing.length} of ${expected.length} story files produced no results:`, + ); + missing.slice(0, 20).forEach((f) => console.error(` ${f}`)); + if (missing.length > 20) + console.error(` … and ${missing.length - 20} more`); + console.error("Refusing to report on a partial scan."); + process.exit(2); + } +} + +if (record || merge) { + const base = + merge && existsSync(baselineFile) + ? JSON.parse(readFileSync(baselineFile, "utf8")) + : {}; + if (unloadable.length) { + console.error( + `a11y-check: refusing to record — ${unloadable.length} story file(s) failed to load:`, + ); + unloadable.slice(0, 20).forEach((f) => console.error(` ${f}`)); + console.error("Their stories never ran, so the baseline would lose them."); + process.exit(2); + } + if (crashed.length) { + console.error( + `a11y-check: refusing to record — ${crashed.length} story(ies) failed for a non-a11y reason:`, + ); + crashed.slice(0, 20).forEach((k) => console.error(` ${k}`)); + console.error("Fix those first; recording now would bake in blind spots."); + process.exit(2); + } + // Union, so a re-record can only widen what's grandfathered — it never drops + // a rule that a slower run happened to miss and then reports it as new. + for (const [k, list] of Object.entries(observed)) + base[k] = [...new Set([...(base[k] || []), ...list])].sort(); + const sorted = {}; + for (const k of Object.keys(base).sort()) sorted[k] = base[k]; + writeFileSync(baselineFile, JSON.stringify(sorted, null, 2) + "\n"); + const pairs = Object.values(sorted).reduce((s, l) => s + l.length, 0); + console.log( + `baseline ${merge ? "merged" : "recorded"}: ${scanned} stories scanned, ` + + `${Object.keys(sorted).length} with violations, ${pairs} story-rule pairs.`, + ); + process.exit(0); +} + +if (!existsSync(baselineFile)) { + console.error("a11y-check: no baseline file:", baselineFile); + process.exit(2); +} +const baseline = JSON.parse(readFileSync(baselineFile, "utf8")); + +const regressions = []; +for (const [key, list] of Object.entries(observed)) { + const base = baseline[key] || []; + for (const id of list) + if (!base.includes(id)) regressions.push(`NEW ${key} ${id}`); +} +// Stories that improved — reported so the baseline can be ratcheted down, never +// a failure. +const fixed = Object.keys(baseline).filter( + (k) => seenFiles.has(k.split(" :: ")[0]) && !observed[k], +); + +const pairs = Object.values(observed).reduce((s, l) => s + l.length, 0); +console.log( + `a11y scan: ${scanned} stories, ${Object.keys(observed).length} with violations, ` + + `${pairs} story-rule pairs (baselined).`, +); + +if (unloadable.length) { + console.error( + `\n✖ ${unloadable.length} story file(s) failed to load, so their stories never ran:`, + ); + unloadable.slice(0, 50).forEach((f) => console.error(` ${f}`)); + if (unloadable.length > 50) + console.error(` … and ${unloadable.length - 50} more`); + console.error( + "\nA file that cannot be imported reports no violations at all. The resolve " + + "or transform error is in the scan log (.a11y-scan/scan.log, uploaded as a " + + "run artifact); a missing generated asset is the usual cause.", + ); +} +if (crashed.length) { + console.error(`\n✖ ${crashed.length} story(ies) failed to render:`); + crashed.slice(0, 50).forEach((k) => console.error(` ${k}`)); + if (crashed.length > 50) console.error(` … and ${crashed.length - 50} more`); +} +if (regressions.length) { + console.error(`\n✖ ${regressions.length} a11y regression(s) vs baseline:`); + regressions.slice(0, 100).forEach((r) => console.error(` ${r}`)); + if (regressions.length > 100) + console.error(` … and ${regressions.length - 100} more`); + console.error( + "\nFix the new violation(s). If a story was renamed or moved its old " + + "baseline key no longer matches — re-record: task frontend:storybook:a11y:record", + ); +} +if (unloadable.length || crashed.length || regressions.length) process.exit(1); + +if (fixed.length) + console.log( + `✓ no a11y regressions. ${fixed.length} baselined story(ies) now clean — ` + + `re-record to lock that in: task frontend:storybook:a11y:record`, + ); +else console.log("✓ no a11y regressions vs baseline."); +process.exit(0); diff --git a/frontend/.storybook/a11y-scan.mjs b/frontend/.storybook/a11y-scan.mjs new file mode 100644 index 0000000000..77aed79d70 --- /dev/null +++ b/frontend/.storybook/a11y-scan.mjs @@ -0,0 +1,197 @@ +// Runs the Storybook Vitest scan in batches and emits one JSON report per batch +// into .a11y-scan/, plus a manifest of every story file the run was supposed to +// cover and a log of the raw output. Consumed by a11y-check.mjs, which fails if +// any manifest entry produced no results. Run from frontend/. +// +// node a11y-scan.mjs scan every story +// node a11y-scan.mjs [file…] scan only these story files +// +// Batching keeps each browser session small: a single run over the whole story +// set holds one Chromium context open for the entire scan, so one crash in it +// costs every story after that point. +// +// Node rather than shell so the task works no matter what invokes it — Task's +// embedded interpreter runs on Windows, but bash/sed/sort do not exist for +// developers calling tasks from PowerShell. +import { execFileSync, spawn } from "node:child_process"; +import { + existsSync, + mkdirSync, + openSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { join } from "node:path"; + +const OUT = ".a11y-scan"; +const CHUNK = 20; +const BATCH_TIMEOUT_MS = 300_000; + +const git = (...args) => + execFileSync("git", args, { encoding: "utf8" }) + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + +rmSync(OUT, { recursive: true, force: true }); +mkdirSync(OUT, { recursive: true }); +const manifestFile = join(OUT, "manifest.txt"); +const logFile = join(OUT, "scan.log"); + +const args = process.argv.slice(2); +let files; +if (args.length > 0) { + // Explicit list (the pull-request path passes just the stories a branch + // affects). Anything that no longer exists is dropped, so a deleted story + // doesn't fail the manifest check. + files = [...new Set(args.filter((f) => existsSync(f)))].sort(); + if (files.length === 0) { + console.log( + "a11y-scan: no existing story files in the given list — nothing to scan", + ); + writeFileSync(manifestFile, ""); + process.exit(0); + } +} else { + // Tracked story files plus any not yet committed, so a new story can be + // checked before it is added to the index. + files = [ + ...new Set([ + ...git( + "ls-files", + "--", + "editor/src/**/*.stories.ts", + "editor/src/**/*.stories.tsx", + ), + ...git( + "ls-files", + "--others", + "--exclude-standard", + "--", + "editor/src/**/*.stories.ts", + "editor/src/**/*.stories.tsx", + ), + ]), + ].sort(); + if (files.length === 0) { + console.error("a11y-scan: no story files found — check the glob"); + process.exit(2); + } +} +writeFileSync(manifestFile, files.join("\n") + "\n"); + +/** Failures carrying no axe rule — a throw, a timeout, a dropped browser page. */ +function crashCount(reportFile) { + try { + const report = JSON.parse(readFileSync(reportFile, "utf8")); + let crashes = 0; + for (const tf of report.testResults ?? []) + for (const a of tf.assertionResults ?? []) { + if (a.status === "passed") continue; + const msg = (a.failureMessages ?? []).join("\n"); + if (!/dequeuniversity\.com\/rules\/axe\//.test(msg)) crashes++; + } + return crashes; + } catch { + return -1; // unreadable report counts as a failed attempt + } +} + +/** Runs one vitest batch, tee'd to the log, killed (whole tree) on timeout. */ +function runBatch(filters, outputFile) { + return new Promise((resolve) => { + const cmd = + `npx vitest run --config .storybook/vitest.config.ts ` + + `--reporter=json --outputFile=${outputFile} ` + + filters.map((f) => `"${f}"`).join(" "); + const log = openSync(logFile, "a"); + const child = spawn(cmd, { + shell: true, + detached: process.platform !== "win32", + stdio: ["ignore", log, log], + }); + const timer = setTimeout(() => { + if (process.platform === "win32") { + try { + execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { + stdio: "ignore", + }); + } catch { + /* already gone */ + } + } else { + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + /* already gone */ + } + } + }, BATCH_TIMEOUT_MS); + child.on("exit", () => { + clearTimeout(timer); + resolve(); + }); + }); +} + +const hasReport = (f) => existsSync(f) && statSync(f).size > 0; + +const batches = []; +for (let i = 0; i < files.length; i += CHUNK) + batches.push(files.slice(i, i + CHUNK)); +console.log( + `a11y-scan: ${files.length} story files, ${batches.length} batches of ${CHUNK}`, +); + +const failed = []; +for (let bi = 0; bi < batches.length; bi++) { + const n = bi + 1; + const out = join(OUT, `chunk-${n}.json`); + // The scan exits non-zero whenever a story has a violation — expected here, + // so the report is what matters, not the status. + // + // A batch is retried once when it produced no report, or when its report + // contains crash-class failures. A one-off infrastructure death — the + // browser page dropping, a Vite dep re-optimize reloading mid-run — passes + // on the retry; a story that genuinely cannot render fails both attempts. + const filters = batches[bi].map((f) => f.replace(/\.tsx$/, "")); + for (let attempt = 1; attempt <= 2; attempt++) { + await runBatch(filters, out); + if (!hasReport(out)) { + console.error( + `a11y-scan: batch ${n} produced no report (attempt ${attempt})`, + ); + continue; + } + if (attempt === 1) { + const crashes = crashCount(out); + if (crashes !== 0) { + console.error( + `a11y-scan: batch ${n} has ${crashes} crash-class failure(s) — retrying once`, + ); + rmSync(out, { force: true }); + continue; + } + } + break; + } + if (hasReport(out)) console.log(` batch ${n}/${batches.length} done`); + else { + failed.push(n); + console.error(` batch ${n}/${batches.length} FAILED — no report`); + } +} + +const present = batches.filter((_, i) => + hasReport(join(OUT, `chunk-${i + 1}.json`)), +).length; +console.log(`a11y-scan: ${present}/${batches.length} batches produced reports`); +if (failed.length > 0) { + console.error( + `a11y-scan: ${failed.length} batch(es) produced no report: ${failed.join(" ")}`, + ); + console.error(`a11y-scan: see ${logFile}. Not reporting on a partial scan.`); + process.exit(2); +} diff --git a/frontend/.storybook/declarations.d.ts b/frontend/.storybook/declarations.d.ts index cc3fa9ae47..bfe637494f 100644 --- a/frontend/.storybook/declarations.d.ts +++ b/frontend/.storybook/declarations.d.ts @@ -1,4 +1,7 @@ -declare module "*.css" {} +// Shorthand (no body) so it doesn't shadow the editor's `*.module.css` +// declaration when both are in one program - an empty body would type every +// CSS-module class as a missing property. +declare module "*.css"; // Vite `?raw` suffix imports a file's contents as a string (used in preview.tsx // to load the English translation TOML synchronously). diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index be281b9ae3..e86a8636e3 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -10,12 +10,28 @@ import tsconfigPaths from "vite-tsconfig-paths"; * the portal layer at editor/src/portal/). MDX docs pages live in * editor/src/portal/docs/. */ +/** + * Editor stories import via `@app/*` (proprietary→core fallback), `@core/*` and + * `@proprietary/*`. Resolve them exactly the way the editor's own build does - + * through vite-tsconfig-paths against the proprietary vite tsconfig - so the + * shared Storybook can host editor components without duplicating the alias map + * here. Built per pass: the main bundle and the worker bundle each need their own. + */ +const editorPathAliases = () => + tsconfigPaths({ + projects: [resolve(__dirname, "../editor/tsconfig.proprietary.vite.json")], + }); + const config: StorybookConfig = { stories: [ "../editor/src/portal/**/*.mdx", "../editor/src/**/*.stories.@(ts|tsx)", ], - addons: ["@storybook/addon-themes", "@storybook/addon-a11y"], + addons: [ + "@storybook/addon-themes", + "@storybook/addon-a11y", + "@storybook/addon-vitest", + ], framework: { name: "@storybook/react-vite", options: {}, @@ -43,19 +59,15 @@ const config: StorybookConfig = { // than a relative path. "@public": resolve(__dirname, "../editor/public"), }; - // Editor stories import via @app/* (proprietary→core fallback), @core/* and - // @proprietary/*. Resolve them exactly the way the editor's own build does — - // through vite-tsconfig-paths against the proprietary vite tsconfig — so the - // shared Storybook can host editor components without duplicating the alias - // map here. config.plugins = config.plugins ?? []; - config.plugins.push( - tsconfigPaths({ - projects: [ - resolve(__dirname, "../editor/tsconfig.proprietary.vite.json"), - ], - }), - ); + config.plugins.push(editorPathAliases()); + // Worker bundles are a separate Rollup pass and do NOT inherit `plugins`, so + // without this a worker importing @app/* fails to resolve while the same + // import works everywhere else. Mirrors editor/vite.config.ts. + config.worker = { + ...(config.worker ?? {}), + plugins: () => [editorPathAliases()], + }; // Point apiClient.saas at a mock origin so the SaaS-backed billing stories // (SubscribedPlanView, PaymentMethodCard, InvoicesList) resolve a base URL and // their MSW handlers (which match "*/api/v1/payg/...") can intercept. The host diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index 339fe4f842..5d36a0ca16 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -13,11 +13,17 @@ import { withThemeByDataAttribute } from "@storybook/addon-themes"; // classic runtime needs it present even though it's not named in the JSX. void React; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { TierProvider, type Tier } from "@portal/contexts/TierContext"; import { LinkProvider, type LinkState } from "@portal/contexts/LinkContext"; import { ThemeProvider, useTheme } from "@portal/contexts/ThemeContext"; import { UIProvider } from "@portal/contexts/UIContext"; import { SuiProvider } from "@portal/theme/SuiProvider"; +import { MantineProvider } from "@mantine/core"; +import { + mantineTheme as editorMantineTheme, + editorCssVariablesResolver, +} from "@core/theme/mantineTheme"; import { handlers } from "@portal/mocks/handlers"; import { configureSupabase } from "@proprietary/auth/supabase/supabaseClient"; import i18next from "i18next"; @@ -28,7 +34,16 @@ import { rtlLanguages, supportedLanguages } from "@core/i18n/languages"; import "@mantine/core/styles.css"; import "@core/tokens/tokens.css"; import "@core/theme/index.css"; +// The editor's semantic token layer (--bg-surface, --onboarding-title, …). +// The app reaches it through its style entry; without it here, components +// styled on those variables render unthemed (e.g. transparent modal surfaces) +// and axe measures contrast against colours the app never shows. +import "@core/styles/theme.css"; import "@core/tokens/base.css"; +// Portal element reset + typography. Scoped to .portal-scope in the app so it +// can't leak into the editor; the decorator below adds that class around +// portal stories only, mirroring how PortalApp mounts. +import "@portal/theme/base.css"; // Storybook-only: bundle every shipped locale's TOML at build time via a ?raw // glob, so the toolbar language switcher can flip between all languages with no @@ -56,9 +71,9 @@ for (const [path, raw] of Object.entries(localeModules)) { } if (!i18next.isInitialized) { - // initImmediate: false → initialise synchronously from the inline resources - // (there's no async backend here), so i18next is ready before the first story - // renders. Without it the first render can beat init and stick on raw keys. + // initAsync: false → initialise synchronously from the inline resources. + // There is no asynchronous backend here, so i18next is ready before stories + // render. void i18next.use(initReactI18next).init({ lng: "en-US", fallbackLng: "en-US", @@ -66,7 +81,7 @@ if (!i18next.isInitialized) { resources, interpolation: { escapeValue: false }, react: { useSuspense: false }, - initImmediate: false, + initAsync: false, }); } else { // Something initialised i18next first (e.g. the app's async TOML backend): @@ -86,6 +101,14 @@ if (!i18next.isInitialized) { // Start MSW once. Storybook runs in a browser so this uses the service worker. initialize({ onUnhandledRequest: "bypass" }, handlers); +// PortalApp wraps the app in a QueryClientProvider, so any component reaching a +// shared query hook throws "No QueryClient set" without one here. `retry: false` +// matches the portal test providers: a story showing an error state should show +// it immediately rather than sitting through backoff retries. +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}); + // Storybook-only: stub a SaaS session so apiClient.saas reads (invoices, payment // method, wallet) clear the session check and reach the MSW handlers instead of // failing with "No SaaS session". VITE_SUPABASE_URL/KEY are defined empty (see @@ -181,6 +204,37 @@ const withLocale: Decorator = (Story, context) => { return ; }; +/** + * Applies the Mantine theme the story's component actually runs under in the + * app: PortalApp wraps the Processor in SuiProvider, while the editor wraps + * everything else in its own ThemeProvider. Getting this wrong is not just + * cosmetic — the two themes carry different neutral ramps, so rendering an + * editor component under the Processor's theme drops it onto Mantine's stock + * greys and reports contrast failures the app doesn't have. + */ +function StoryTheme({ + isPortalStory, + colorScheme, + children, +}: { + isPortalStory: boolean; + colorScheme: "light" | "dark"; + children: React.ReactNode; +}) { + if (isPortalStory) { + return {children}; + } + return ( + + {children} + + ); +} + const withProviders: Decorator = (Story, context) => { const tier = (context.globals.tier as Tier) ?? "pro"; const linkState = @@ -192,32 +246,58 @@ const withProviders: Decorator = (Story, context) => { // anything that isn't "dark" as light — matching the addon's own // `selected || defaultTheme` fallback where defaultTheme is light. const colorScheme = context.globals.theme === "dark" ? "dark" : "light"; + // PortalApp mounts its views inside a .portal-scope wrapper, which is what + // the portal's base.css keys its reset/typography on. Give portal stories + // the same wrapper (and only them — the scoping exists precisely so portal + // styles never apply to editor components). + // `fileName` is only injected by the dev/build pipeline — under the Vitest + // runner it is absent, so path alone would silently drop every portal story + // onto the editor theme (where portal-only palette entries like `amber` + // resolve to nothing and render unstyled). The title prefix is the fallback + // that survives both environments. + const isPortalStory = + (context.parameters.fileName ?? "").includes("/portal/") || + context.title.startsWith("Portal/"); return ( - - - - - {/* LinkProvider must wrap TierProvider: TierContext derives its tier - from useLink() (matches App.tsx's nesting). */} - - - - - - - - - - - - + + + + + + {/* LinkProvider must wrap TierProvider: TierContext derives its tier + from useLink() (matches App.tsx's nesting). */} + + + + + {isPortalStory ? ( +
+ +
+ ) : ( + + )} +
+
+
+
+
+
+
+
); }; const preview: Preview = { loaders: [mswLoader], + // The scan runs once per theme (SCAN_THEME=light|dark, forwarded by + // .storybook/vitest.config.ts); pinning the global here themes every story in + // the run. Unset — the Storybook UI — falls back to the toolbar default. + initialGlobals: { + theme: import.meta.env.VITE_SCAN_THEME === "dark" ? "dark" : "light", + }, parameters: { layout: "padded", controls: { @@ -231,12 +311,18 @@ const preview: Preview = { ], }, a11y: { - // Run axe automatically against the story root; violations show in the - // Accessibility panel. `context` replaced `element` in addon-a11y 9.x. - context: "#storybook-root", - config: {}, - options: {}, - test: "todo", + // Run axe against the rendered story; `test: "error"` fails the scan on + // any violation. Context is left at the addon default (the document root) + // so it resolves under both the Storybook UI and the Vitest browser mount. + test: "error", + context: { + // Nodes carrying this attribute render a facsimile of the user's own + // document — their stamp text, their watermark, in the colour and + // opacity they chose. WCAG contrast governs the interface, not the + // content authored through it, and the controls that set those values + // are checked normally. + exclude: ["[data-user-content-preview]"], + }, }, }, globalTypes: { diff --git a/frontend/.storybook/tsconfig.json b/frontend/.storybook/tsconfig.json index cb3f958b2a..52a07ca25f 100644 --- a/frontend/.storybook/tsconfig.json +++ b/frontend/.storybook/tsconfig.json @@ -1,25 +1,15 @@ { - "compilerOptions": { - "target": "es2022", - "jsx": "react-jsx", - "module": "esnext", - "moduleResolution": "bundler", - "paths": { - "@app/*": [ - "../editor/src/desktop/*", - "../editor/src/proprietary/*", - "../editor/src/core/*" - ], - "@core/*": ["../editor/src/core/*"], - "@proprietary/*": ["../editor/src/proprietary/*"], - "@portal/*": ["../editor/src/portal/*"], - "@public/*": ["../editor/public/*"] - }, - "resolveJsonModule": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - }, - "include": ["./**/*"] + // Extends the same tsconfig main.ts feeds to vite-tsconfig-paths, so the + // alias map can't drift from the one Storybook actually bundles with, and + // stories are checked under that resolution (@app/* = proprietary -> core). + // The per-layer typecheck projects also cover story files, but each resolves + // @app/* its own way - not the way the single Storybook build renders them. + "extends": "../editor/tsconfig.proprietary.vite.json", + "exclude": [], + "include": [ + "./**/*", + "../editor/src/global.d.ts", + "../editor/src/**/*.stories.ts", + "../editor/src/**/*.stories.tsx" + ] } diff --git a/frontend/.storybook/vitest.config.ts b/frontend/.storybook/vitest.config.ts new file mode 100644 index 0000000000..28d25f9732 --- /dev/null +++ b/frontend/.storybook/vitest.config.ts @@ -0,0 +1,84 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; +import { storybookTest } from "@storybook/addon-vitest/vitest-plugin"; + +/** + * Dedicated Vitest config that turns every story into a browser test: it mounts + * the story in real Chromium and runs axe against it, so a story fails if it + * throws on mount or trips an accessibility rule. Kept separate from + * editor/vitest.config.ts (the jsdom unit tests) so the two suites don't collide. + * + * The storybook test must live in a `test.projects[]` entry (not a flat config) + * so Vitest wires up the browser test runner correctly. + * + * Run with: npx vitest run --config .storybook/vitest.config.ts + */ +export default defineConfig({ + // Forwards the SCAN_THEME env var into the browser bundle, where preview.tsx + // uses it to pin the theme global for the whole run. The Storybook dev/build + // pipeline never sets it, so the toolbar default stays "light" there. + define: { + "import.meta.env.VITE_SCAN_THEME": JSON.stringify( + process.env.SCAN_THEME ?? "", + ), + }, + optimizeDeps: { + // Pre-scan every story + the preview so Vite discovers the story set's large + // dep surface (embedpdf plugins, @mui icons, …) in one pass up front. + entries: ["editor/src/**/*.stories.@(ts|tsx)", ".storybook/preview.tsx"], + // `entries` alone does not catch deps reached through a transformed JSX + // runtime import, nor the preview's own dependency graph (the test plugin + // injects the preview in a way the entry scanner doesn't crawl). Vite then + // optimizes them lazily mid-run and emits "optimized dependencies changed, + // reloading" — the reload tears down the browser worker and whichever + // stories were mid-load fail with a bogus "Failed to fetch dynamically + // imported module" that reads like a real crash. Only a cold dep cache + // hits this, which is every CI run. Naming them keeps a run deterministic. + include: [ + "react", + "react/jsx-runtime", + "react/jsx-dev-runtime", + "react-dom", + "react-dom/client", + "@storybook/react-vite", + "@storybook/addon-a11y/preview", + "@storybook/addon-themes", + "msw-storybook-addon", + "react-router-dom", + "@tanstack/react-query", + "i18next", + "react-i18next", + "smol-toml", + "@mantine/core", + "@supabase/supabase-js", + "axios", + ], + }, + test: { + projects: [ + { + extends: true, + // Reads .storybook/main.ts (stories glob + viteFinal aliases) so every + // discovered story becomes a test with the app's real module resolution. + plugins: [storybookTest({ configDir: resolve(__dirname) })], + test: { + name: "storybook", + // Mounting a story and running a full axe pass over it takes well + // over Vitest's 5s default on the heavier screens, and a story that + // trips the timeout is reported as a failure with no message — which + // reads like a crash and makes the run non-reproducible. Give it + // room; a genuinely hung story still fails, just later. + testTimeout: 60_000, + hookTimeout: 60_000, + browser: { + enabled: true, + headless: true, + provider: "playwright", + instances: [{ browser: "chromium" }], + }, + setupFiles: [resolve(__dirname, "vitest.setup.ts")], + }, + }, + ], + }, +}); diff --git a/frontend/.storybook/vitest.setup.ts b/frontend/.storybook/vitest.setup.ts new file mode 100644 index 0000000000..c6fe770b55 --- /dev/null +++ b/frontend/.storybook/vitest.setup.ts @@ -0,0 +1,15 @@ +import { beforeAll } from "vitest"; +import { setProjectAnnotations } from "@storybook/react-vite"; +import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; +// oxlint-disable-next-line no-restricted-imports -- Storybook-only: the sibling preview config has no @-alias. +import * as projectAnnotations from "./preview"; + +// Include addon-a11y's annotations so its axe checks run under Vitest, not only +// in the Storybook UI panel. projectAnnotations supplies the same +// decorators/parameters/globals (providers, i18n, theme) as the browser. +const project = setProjectAnnotations([ + a11yAddonAnnotations, + projectAnnotations, +]); + +beforeAll(project.beforeAll); diff --git a/frontend/README.md b/frontend/README.md index fafed9bede..ea759538e6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -6,7 +6,7 @@ All frontend commands are run from the repository root using [Task](https://task - `task frontend:build` — production build - `task frontend:test` — run tests - `task frontend:test:watch` — run tests in watch mode -- `task frontend:lint` — run ESLint + cycle detection +- `task frontend:lint` — run linting - `task frontend:typecheck` — run TypeScript type checking - `task frontend:check` — run typecheck + lint + test - `task frontend:install` — install npm dependencies @@ -18,7 +18,7 @@ For desktop app development, see the [Tauri](#tauri) section below. `frontend/` is a workspace containing one or more apps. Today it holds the PDF editor under `frontend/editor/`; new apps (the developer portal, etc.) will sit alongside it as siblings. Shared tooling — `package.json`, `node_modules`, -`.storybook/`, ESLint, Prettier — lives at `frontend/` so every app installs +`.storybook/`, oxlint, Prettier — lives at `frontend/` so every app installs once and lints with the same config. ## Environment Variables diff --git a/frontend/editor/index.html b/frontend/editor/index.html index 427b6d1449..1e782db2de 100644 --- a/frontend/editor/index.html +++ b/frontend/editor/index.html @@ -4,7 +4,10 @@ - + |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "إضافة إلى مساحة العمل" addToWorkspaceCount = "إضافة {{count}} إلى مساحة العمل" @@ -3749,7 +3971,6 @@ backToMyFiles = "الرجوع إلى ملفاتي" breadcrumbs = "مسار المجلد" cancel = "إلغاء" classification = "التصنيف" -clearSearch = "مسح البحث" clearSelection = "مسح التحديد" closeDetails = "إغلاق التفاصيل" create = "إنشاء" @@ -3781,6 +4002,7 @@ downloadAll = "تنزيل الكل" downloadVersion = "تنزيل هذا الإصدار" dropOverlay = "أفلِت الملفات للرفع" dropOverlaySub = "تبدأ الملفات محلياً. استخدم 'نقل إلى' أو 'حفظ إلى السحابة' لتنظيمها في مجلد." +duplicate = "Duplicate" file = "ملف" fileInfo = "File info" fileMenu = "إجراءات الملف" @@ -3808,13 +4030,12 @@ refresh = "التحديث من الخادم" remove = "حذف" removeVersion = "إزالة هذا الإصدار" rename = "إعادة التسمية" +renamed = "تمت إعادة التسمية" renameFolder = "إعادة تسمية المجلد" resizeFolderTree = "تغيير حجم شجرة المجلدات (مفاتيح الأسهم، مع Shift لخطوات أكبر؛ انقر مزدوجاً للملاءمة التلقائية)" save = "حفظ" saveToServer = "حفظ إلى الخادم" saveToServerDisabledHint = "الحفظ إلى الخادم غير مفعّل على هذا الخادم. اطلب من المسؤول تمكينه." -search = "بحث" -searchPlaceholder = "ابحث في هذا المجلد والمجلدات الفرعية" selectAll = "تحديد الكل" selectAllHint = "انقر لتحديد الكل. تلميح: اضغط Ctrl (أو Cmd) لإضافة الملفات واحداً تلو الآخر، وShift لتحديد نطاق." selectedCount = "{{count}} محدد" @@ -3874,6 +4095,10 @@ title = "لا توجد ملفات سحابية بعد" hint = "الملفات المحفوظة دون رفع تبقى هنا. أسقط ملفاً لإضافته." title = "لا توجد ملفات محلية فقط" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "الملفات التي تفتحها أو تعدّلها ستظهر هنا." title = "لم يتم تعديل أي شيء بعد" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "تعذّر حذف المجلد." deleteFolderFailedDetail = "تعذّر حذف المجلد: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "تعذّر تحديث مظهر المجلد." folderAppearanceFailedDetail = "تعذّر تحديث مظهر المجلد: {{message}}" moveFilesFailed = "تعذّر نقل الملفات." @@ -3910,9 +4137,7 @@ count = "ملفات" folder = "مجلد" labels = "التسميات" modified = "معدَّل" -name = "الاسم" size = "الحجم" -toolHistoryAtVersion = "سلسلة الأدوات التراكمية" totalSize = "الحجم الإجمالي" type = "النوع" versionHistory = "رحلة الإصدارات" @@ -3945,6 +4170,11 @@ localHint = "مخزّن فقط في هذا المتصفح" shared = "مُشترك" sharedHint = "تمت مشاركته معك عبر رابط" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "الأقدم أولاً" modifiedDesc = "الأحدث أولاً" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "لا توجد ملفات في التخزين. افتح open = "فتح" openFile = "فتح ملف" openFiles = "فتح ملفات" +selectFile = "Select {{name}}" selectFromStorage = "الاختيار من التخزين" upload = "تحميل" uploadFile = "تحميل ملف" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "تم حفظ الكل" +analyzingFields = "Analysing form fields..." +extractCsvError = "فشل استخراج CSV" +extractXlsxError = "فشل استخراج XLSX" +filled = "filled" flattenAfterFilling = "تسطيح بعد التعبئة" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "مطلوب" +requiredFieldsError = "يُرجى ملء جميع الحقول المطلوبة" rescanFields = "إعادة فحص الحقول" rescanFormFields = "إعادة فحص حقول النموذج" save = "حفظ" +saveShortcut = "Ctrl+S للحفظ" +unsavedChanges = "تغييرات غير محفوظة" [formFill.sidebar] close = "إغلاق الشريط الجانبي" @@ -4309,6 +4552,11 @@ desc = "إعادة تسمية ملف PDF تلقائيًا استنادًا إل tags = "كشف تلقائي،مستند للعناوين،تنظيم،إعادة وسم" title = "إعادة تسمية PDF تلقائيًا" +[home.autoRotate] +desc = "اكتشف اتجاه كل صفحة ودوّرها تلقائيًا لتكون مستقيمة." +tags = "تدوير تلقائي,إصلاح الاتجاه,مقلوب,جانبي,تسوية,توجيه,اكتشاف الاتجاه,تصحيح الدوران,توجيه تلقائي,ممسوح ضوئيًا,OSD" +title = "تدوير تلقائي" + [home.autoSizeSplitPDF] tags = "تلقائي،تقسيم،حجم" title = "تقسيم تلقائي بالحجم/العدد" @@ -4608,15 +4856,12 @@ hint = "ارفع ملف PNG أو JPG أو SVG أو صورة أخرى لوضعه label = "رفع صورة" placeholder = "اختر ملف صورة" -[imageToPdf] -tags = "تحويل,صورة,jpg,صورة,صورة فوتوغرافية" - [imageToPDF] header = "صورة إلى PDF" title = "صورة إلى PDF" -[infoBanner] -dismiss = "تجاهل" +[imageToPdf] +tags = "تحويل,صورة,jpg,صورة,صورة فوتوغرافية" [invite] acceptError = "فشل إنشاء الحساب" @@ -4647,11 +4892,9 @@ welcomeTitle = "لقد تمت دعوتك!" [landing] addFiles = "إضافة ملفات" -heroSubtitle = "اسحب وافلت أو أضف ملف PDF موجودا للبدء." mobileUpload = "رفع من الهاتف المحمول" openFromComputer = "فتح من الكمبيوتر" uploadFromComputer = "الرفع من الكمبيوتر" -workbenchEmptyStateHero = "أفلت ملف PDF في أي مكان" [language] direction = "rtl" @@ -4691,7 +4934,6 @@ enterUsername = "أدخل اسم المستخدم" failedToSignIn = "فشل تسجيل الدخول بواسطة {{provider}}: {{message}}" forgotPassword = "هل نسيت كلمة المرور؟" loggingIn = "جارٍ تسجيل الدخول..." -logIn = "تسجيل الدخول" login = "تسجيل الدخول" magicLinkSent = "تم إرسال الرابط السحري إلى {{email}}! تفقد بريدك واضغط الرابط لتسجيل الدخول." mfaCode = "رمز المصادقة" @@ -4715,26 +4957,10 @@ signInWith = "تسجيل الدخول بواسطة" title = "تسجيل الدخول" unexpectedError = "خطأ غير متوقع: {{message}}" updatePassword = "تحديث كلمة المرور" -useEmailInstead = "تسجيل الدخول بالبريد الإلكتروني" useMagicLink = "استخدم الرابط السحري بدلًا من ذلك" username = "اسم المستخدم" youAreLoggedIn = "لقد سجّلت الدخول!" -[login.slides.edit] -alt = "تحرير ملفات PDF" -subtitle = "مع أكثر من 12 أداة تساعدك على تنقيح ملفات PDF وتوقيعها وقراءتها ومعالجتها، ستجد بالتأكيد ما تبحث عنه." -title = "حرّر ملفات PDF لعرض/تأمين المعلومات التي تريدها" - -[login.slides.overview] -alt = "نظرة عامة على Stirling PDF" -subtitle = "حزمة سحابية تضع الخصوصية أولاً لملفات PDF تتيح لك تحويل المستندات وتوقيعها وتنقيحها وإدارتها، إلى جانب أكثر من 50 أداة قوية أخرى." -title = "كل ما تحتاجه لـ PDF في مكان واحد." - -[login.slides.secure] -alt = "حماية ملفات PDF" -subtitle = "أضِف كلمات مرور، ونقّح المحتوى، وأدر الشهادات بسهولة." -title = "احمِ المعلومات الحساسة في ملفات PDF الخاصة بك" - [margin] large = "كبير" medium = "متوسط" @@ -4818,6 +5044,34 @@ uploadSuccess = "تم الرفع بنجاح!" uploadSuccessMessage = "تم نقل صورك." validating = "جارٍ التحقق من الجلسة..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "امسح ضوئيًا لرفع الصور. تتحول الصور تلقائيًا إلى PDF." descriptionNoConvert = "امسح ضوئيًا لرفع الصور من جهازك المحمول." @@ -4836,8 +5090,13 @@ title = "رفع من الهاتف المحمول" tags = "أداة متعددة,عملية متعددة,واجهة مستخدم,النقر والسحب,واجهة أمامية,جانب العميل" title = "أداة متعددة PDF" -[navbar] -search = "البحث" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "لم تتم المصادقة بنجاح. يمكنك إغلاق هذه النافذة والمحاولة مجدداً." @@ -4966,6 +5225,7 @@ activeFiles = "تعرض واجهة الملفات النشطة allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "إغلاق" cropSettings = "بعد أن اخترنا الملف الذي نريد قصّه، يمكننا تهيئة أداة القص لاختيار المنطقة التي سنقصّ إليها ملف PDF." +dialogLabel = "Onboarding" fileCheckbox = "يؤدي النقر على أحد الملفات إلى اختياره للمعالجة. يمكنك تحديد عدة ملفات لتنفيذ العمليات الدُفعية." fileReplacement = "سيحل الملف المعدّل محل الملف الأصلي في منضدة العمل تلقائيًا، مما يتيح لك تشغيله بسهولة عبر مزيد من الأدوات." filesButton = "يتيح زر الملفات في شريط الوصول السريع رفع ملفات PDF لاستخدام الأدوات عليها." @@ -4976,6 +5236,7 @@ pinButton = "يمكنك استخدام زر التثبيت إذ results = "بعد انتهاء تشغيل الأداة، ستعرض خطوة المراجعة معاينة للنتائج في هذا اللوح، وتتيح لك التراجع عن العملية أو تنزيل الملف. " runButton = "بعد تهيئة الأداة، يتيح لك هذا الزر تشغيلها على جميع ملفات PDF المحددة." selectCropTool = "لنختر أداة القص لشرح كيفية استخدام إحدى الأدوات." +stepOf = "الخطوة {{current}} من {{total}}" toolInterface = "هذه هي واجهة أداة القص. كما ترى، لا يوجد الكثير هنا لأننا لم نُضِف بعد أي ملفات PDF للعمل عليها." workbench = "هذه هي منضدة العمل — المنطقة الرئيسية لعرض ملفات PDF وتحريرها." wrapUp = "أنت جاهز! تعرّفت على المناطق الرئيسية في التطبيق وكيفية استخدامها. انقر زر المساعدة متى شئت لعرض هذه الجولة مرة أخرى." @@ -4988,6 +5249,26 @@ showMeAround = "أرني الجولة" skipForNow = "تخطي الآن" skipTheTour = "تخطي الجولة" +[onboarding.checklist] +dismiss = "تجاهل" +title = "إعداد Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "شغّل Stirling أصليًا على جهازك" +title = "تنزيل Stirling لسطح المكتب" + +[onboarding.checklist.inviteTeam] +description = "تعاون مع فريقك" +title = "دعوة أعضاء الفريق" + +[onboarding.checklist.shareAnalytics] +description = "ساعد في تحسين Stirling" +title = "مشاركة بيانات استخدام مجهولة" + +[onboarding.checklist.takeTour] +description = "اطلع على كيفية عمل Stirling في جولة سريعة" +title = "ابدأ الجولة" + [onboarding.desktopInstall] body = "يعمل Stirling بشكل أفضل كتطبيق سطح مكتب. يمكنك استخدامه دون اتصال، والوصول إلى المستندات بسرعة أكبر، وإجراء التعديلات محلياً على جهازك." selectOs = "حدد نظام التشغيل" @@ -4996,7 +5277,21 @@ titleWithOs = "تنزيل لـ {{osLabel}}" [onboarding.mfa] authenticationCode = "رمز المصادقة" +description = "أمّن حسابك بربط تطبيق مصادقة. امسح QR code أو أدخل مفتاح الإعداد، ثم أكّد الرمز المكوّن من 6 أرقام للإنهاء." +enable = "تمكين MFA" +enableError = "تعذر تمكين المصادقة الثنائية. تحقق من الرمز وحاول مرة أخرى." +enterCode = "أدخل الرمز المكوّن من 6 أرقام من تطبيقك." +enterCodeError = "أدخل رمز المصادقة للمتابعة." +logout = "تسجيل الخروج" +openAuthenticator = "افتح Google Authenticator أو Authy أو 1Password." qrCodeLoading = "جارٍ إنشاء رمز QR…" +regenerateQrCode = "إعادة إنشاء QR code" +scanQrCode = "امسح QR code أو أدخل مفتاح الإعداد أدناه." +setupError = "تعذر بدء إعداد المصادقة الثنائية. يُرجى المحاولة مرة أخرى." +setupKey = "مفتاح الإعداد (إدخال يدوي)" +stepByStep = "خطوة بخطوة" +success = "تم تمكين MFA. يمكنك المتابعة الآن." +title = "إعداد المصادقة متعددة العوامل" [onboarding.planOverview] adminBodyLoginDisabled = "بمجرد تمكين وضع تسجيل الدخول، يمكنك إدارة المستخدمين، وضبط الإعدادات، ومراقبة حالة الخادم. أول {{freeTierLimit}} شخصاً على خادمك يستخدمون Stirling مجاناً." @@ -5005,6 +5300,11 @@ adminTitle = "نظرة عامة للمشرف" userBody = "ادعُ زملاءك، وخصص الأدوار، واحفظ مستنداتك منظمة في مساحة عمل آمنة واحدة. فعّل وضع تسجيل الدخول عندما تكون مستعداً للتوسع بعد الاستخدام الفردي." userTitle = "نظرة عامة على الخطة" +[onboarding.processorIntro] +body = "يشغّل Stirling الآن السياسات — قواعد آلية تصنف كل مستند وتؤمّنه وتعالجه عند وصوله. قم بإعدادها وراقب عمليات التشغيل في المعالج." +cta = "استكشاف المعالج" +title = "استكشف معالج Stirling" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "سنة مدفوعة مسبقًا" + +[payg.prepaid.banner] +exhausted = "تم استخدام سعتك المدفوعة مسبقًا؛ استؤنفت الفوترة المقاسة. اشحن للحفاظ على الخصم." +expiring_one = "تنتهي سنتك المدفوعة مسبقًا خلال {{count}} يوم. تنتهي السعة غير المستخدمة — اشحن لبدء سنة جديدة." +expiring_other = "تنتهي سنتك المدفوعة مسبقًا خلال {{count}} أيام. تنتهي السعة غير المستخدمة — اشحن لبدء سنة جديدة." +low = "تبقى لديك {{pct}}% من سعتك المدفوعة مسبقًا. اشحن في أي وقت للحفاظ على استمرار المعالجة دون انقطاع." + +[payg.prepaid.card] +subtitle = "تُستخدم قبل الفوترة المقاسة وخارج حد الإنفاق الخاص بك." +title = "السعة المدفوعة مسبقًا" + +[payg.prepaid.meter] +capSuffix = "من {{total}} رصيد مدفوع مسبقًا" +expires = "تنتهي في {{date}}" + +[payg.prepaid.state] +exhausted = "مستخدمة بالكامل" +healthy = "متبقٍ الكثير" +low = "توشك على النفاد" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF إلى PDF/A" tags = "أرشيف,طويل الأجل,معيار,تحويل,تخزين,حفظ" title = "PDF إلى PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "طباعة,معيار,تحويل,إنتاج,ما قبل الطباعة,أرشفة" title = "PDF إلى PDF/X" @@ -5822,6 +6151,7 @@ small = "500 رصيد" xsmall = "100 رصيد" [plan.availablePlans] +currency = "Billing currency" subtitle = "اختر الخطة التي تناسب احتياجاتك" title = "الخطط المتاحة" @@ -5937,49 +6267,12 @@ keyDescription = "الصق مفتاح الترخيص من بريدك الإلك success = "تم تفعيل الترخيص!" successMessage = "تم تفعيل ترخيصك بنجاح. يمكنك الآن إغلاق هذه النافذة." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "تم الفرض" -enforcing = "جارٍ الفرض..." -failed = "فشل الفرض" outputsUnavailable = "لم تعد مخرجات السياسة متاحة للتنزيل." partialOutputsUnavailable = "لم تعد بعض مخرجات السياسة متاحة للتنزيل." -retrying = "مشغول، جارٍ إعادة المحاولة..." runNotFound = "تعذّر العثور على تشغيل الفرض." -step = "الخطوة {{current}}/{{total}}" timedOut = "انتهت مهلة الفرض قبل أن يكتمل التشغيل." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "إغلاق" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "جارٍ تطبيق {{names}}" applyingProgress = "جارٍ تطبيق {{names}} ({{done}} من {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} من أصل {{total}} ملف، تعذّرت معال failureTitle = "تم التصدير دون فرض كامل" printPolicyAppliedBody = "تم تحديث هذا PDF ليتوافق مع سياسة. راجِع التغييرات، ثم اطبع مجددًا." printPolicyAppliedTitle = "تم تطبيق السياسة قبل الطباعة" -queued = "+{{count}} في قائمة الانتظار" successTitle = "تم تطبيق {{names}}" summaryMore = "{{first}}، {{second}} و{{more}} أخرى" summaryTwo = "{{first}} و{{second}}" -[policies.enforcement.triggerVerb] -convert = "فرض قبل التحويل" -default = "فرض" -export = "فرض قبل التصدير" -input = "فرض عند الاستيراد" -print = "فرض قبل الطباعة" - -[policies.field] -accessLog = "سجل الوصول" -archiveAfter = "الأرشفة بعد" -auditTrail = "مسار التدقيق" -belowThreshold = "أقل من الحد" -destination = "الوجهة" -frameworks = "الأطر" -immutableHold = "احتجاز غير قابل للتغيير" -keepFor = "الاحتفاظ لمدة" -minConfidence = "الحد الأدنى للثقة" -notify = "إشعار عند التوجيه" -onViolation = "عند عدم الامتثال" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "إضافة" -addPlaceholder = "إضافة تسمية…" -customNote = "مخصص لفريقك." -defaultNote = "يتم استخدام الإعداد الافتراضي المدمج، والمشترك مع فريقك." -duplicate = "\"{{name}}\" موجود بالفعل." -edit = "تعديل التسميات" -empty = "لا توجد تسميات بعد." -export = "تصدير JSON" -iconAria = "اختر أيقونة لـ {{name}}" -import = "استيراد JSON" -importError = "تعذر استيراد ذلك الملف." -managedNote = "تتم إدارة تسميات الفريق بواسطة قائد فريقك." -modalSubtitle = "مشتركة مع فريقك بالكامل. يختار المصنّف التسميات المناسبة لكل مستند." -modalTitle = "تسميات التصنيف" -pickIcon = "استخدام أيقونة {{label}}" -removeAria = "إزالة {{name}}" -resetToDefault = "إعادة التعيين إلى الافتراضي" -saveForTeam = "حفظ للفريق" -saving = "جارٍ الحفظ…" -sectionLabel = "تسميات التصنيف" -startFromScratch = "البدء من الصفر" -teamCount = "تسميات الفريق" -tooLong = "يمكن أن تتكون التسميات من {{max}} حرفًا كحد أقصى." -ungrouped = "غير مجمعة" -view = "عرض التسميات" +categoryCount = "فئات" +hideCategory = "إخفاء الفئة" +labelCount = "تسميات" +sharedNote = "هذه التسميات مدمجة ومشتركة عبر فريقك بأكمله." +showCategory = "إظهار الفئة" [policies.pii] account = "أرقام الحسابات (موسومة)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "أرقام التوجيه الأمريكية (ABA)" ssn = "أرقام الضمان الاجتماعي" -[policies.settings] -noneExport = "لا توجد سياسات تعمل حاليًا عند التصدير." -noneUpload = "لا توجد سياسات تعمل حاليًا عند الرفع." -onExport = "عند التصدير" -onUpload = "عند الرفع" -reorderHandle = "اسحب لإعادة الترتيب" -runOrderDesc = "عند تشغيل أكثر من سياسة على نفس المشغّل، يتم تشغيلها بهذا الترتيب — كل منها على مخرجات السياسة السابقة. اسحب لإعادة الترتيب." -title = "إعدادات السياسة" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "خيارات السياسة" -policySettings = "إعدادات السياسة" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "إعادة محاولة السياسات الفاشلة ({{count}})" -rowProgress = "تمت معالجة {{completed}} من {{total}} ملفات" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "ما هي السياسة؟" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "يعثر تلقائيًا ويحجب التفاصيل الحساسة — مثل أرقام الضمان الاجتماعي وأرقام البطاقات — حتى لا يمكن قراءتها في المستند." -sanitize = "يزيل JavaScript المخفي من الملف، بحيث لا يعمل أي شيء تلقائيًا عند فتحه." -watermark = "يضع علامة مرئية (مثل \"سري\") عبر كل صفحة." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "إغلاق" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "جارٍ فرض {{name}}..." badgeRan = "تم تشغيل سياسة {{name}} على هذا الملف" +badgeRunning = "{{name}} قيد التشغيل..." blockingAction = "تم حظر {{action}} أثناء فرض السياسة، يرجى الانتظار..." dismiss = "إغلاق التراكب" enforcingTitle = "جارٍ فرض السياسة..." @@ -6151,6 +6316,7 @@ viewAnyway = "عرض الملف (لا تزال السياسة قيد الفرض) [policyOption] autoRedactPhi = "تنقيح PHI تلقائيًا" blockExport = "حظر التصدير" +ccpa = "CCPA" contracts = "العقود" documents = "المستندات" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "خطة Editor" subscribed = "خطة Processor" unlinked = "غير مرتبط" -[portal.agentBuilder] -bootstrapFromDocument = "التمهيد من مستند" -sectionsAriaLabel = "أقسام منشئ الوكيل" -selectorAriaLabel = "الوكلاء" -subtitle = "صمّم واختبر وانشر وكلاء AI الذين يصنفون مستنداتك ويستخرجون منها ويوجهونها. عرّف السيناريوهات، وقيّد وصول الأدوات، وشغّل مجموعة ذهبية، وانشر إصدارًا." -title = "منشئ الوكلاء" - -[portal.agentBuilder.bootstrap] -cancel = "إلغاء" -dropzoneText = "اختر مستندًا نموذجيًا (PDF أو صورة)" -lead = "أفلت مستندًا نموذجيًا وسنقترح سيناريوهات ومخطط استخراج يمكنك تنقيحه. لا يتم نشر أي شيء حتى تراجعه." -submit = "تمهيد الوكيل" -subtitle = "ابدأ وكيلًا جديدًا من ملف تمثيلي واحد" -title = "التمهيد من مستند" - -[portal.agentBuilder.empty] -description = "مهّد وكيلًا من مستند نموذجي لبدء سيناريوهاته ومخطط الاستخراج، ثم نقّحه وانشره." -title = "لا توجد وكلاء بعد" - -[portal.agentBuilder.evals] -casesPassing = "الحالات الناجحة" -columnCase = "حالة التقييم" -columnLatency = "زمن الاستجابة" -columnResult = "النتيجة" -fail = "فشل" -goldenSetPassRate = "معدل نجاح المجموعة الذهبية" -latencyMs = "{{ms}} ms" -notRun = "لم يتم التشغيل" -pass = "نجاح" -passRate = "معدل النجاح" -runEvals = "تشغيل التقييمات" - -[portal.agentBuilder.evals.empty] -description = "تحوّل التقييمات سيناريوهاتك إلى مجموعة ذهبية قابلة للتكرار. قم بالترقية لالتقاط معدل النجاح عبر الوقت والتحكم في النشر بناءً عليه." -title = "لا توجد مجموعة ذهبية بعد" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "عبر المجموعات الذهبية" -activeAgents = "الوكلاء النشطون" -avgPassRate = "متوسط معدل نجاح التقييم" -fleetWide = "على مستوى الأسطول" -latestPublished = "آخر منشور" -scenarios = "السيناريوهات" -testCases = "حالات اختبار" -totalDescription_one = "{{count}} إجمالًا" -totalDescription_other = "{{count}} إجمالًا" - -[portal.agentBuilder.scenarios] -add = "إضافة" -addScenario = "إضافة سيناريو" -enable = "تمكين" -expectationLabel = "السلوك المتوقع" -expectationPlaceholder = "ما يجب أن يفعله الوكيل" -inEval = "في التقييم" -mute = "كتم" -muted = "مكتوم" -nameLabel = "الاسم" -namePlaceholder = "مثال: تصعيد الامتثال" - -[portal.agentBuilder.tabs] -evals = "التقييمات" -scenarios = "السيناريوهات" -tools = "الأدوات" -versions = "الإصدارات" - -[portal.agentBuilder.tools] -broadAccess = "وصول واسع" -deniedHint = "الأدوات المحددة محظورة. كل ما عدا ذلك يبقى قابلًا للاستدعاء." -deniedTools = "الأدوات المرفوضة" -governanceGate = "حوكمة الأدوات متاحة في خطة Enterprise." -restricted = "مقيّد" -restrictedAccess = "وصول مقيّد للأدوات" -restrictedDescription = "اسمح بكل أداة باستثناء الأدوات التي ترفضها أدناه." - -[portal.agentBuilder.versions] -current = "الحالي" -historyGate = "سجل الإصدارات الكامل والرجوع متاحان في خطة Enterprise." -publish = "نشر" -rollBack = "رجوع" - [portal.assistant] close = "إغلاق المساعد" error = "تعذر الوصول إلى المساعد." @@ -6365,9 +6451,21 @@ typing = "يكتب" loading = "جارٍ التحميل" redirectingToEditor = "جارٍ إعادة التوجيه إلى المحرر..." +[portal.billing.activation] +maybeLater = "ربما لاحقًا" +title = "تشغيل المعالج" + +[portal.billing.activation.payg] +sub = "عيّن حدًا شهريًا. ادفع مقابل ما تشغّله." +title = "الدفع حسب الاستخدام" + +[portal.billing.activation.prepay] +badge = "شهران مجانًا" +sub = "فاتورة واحدة، ببطاقة أو تحويل بنكي." +title = "ادفع سنة مقدمًا" + [portal.billing.checkout] noClientSecret = "أعادت Edge function قيمة client_secret غير موجودة." -subtitle = "أضف بطاقة للمتابعة بعد منحة خطة Editor المجانية. يتولى Stripe الباقي." title = "تشغيل خطة Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "تم الدفع بنجاح، لكن التفعيل يستغرق وقتً close = "إغلاق" title = "كدنا ننتهي" +[portal.billing.checkout.cap] +amountAria = "حد الإنفاق الشهري" +back = "رجوع" +continue = "المتابعة إلى الدفع" +error = "تعذر تعيين حد الإنفاق الخاص بك" +finePrint = "تُصدر الفواتير شهريًا. يمكنك الإلغاء في أي وقت." +noLimit = "بلا حد" +note = "لن تُفرض عليك رسوم تتجاوز حدك أبدًا. تتوقف المعالجة مؤقتًا فقط." +perMonth = "/ شهر" +title = "عيّن حد الإنفاق الخاص بك" + +[portal.billing.checkout.card] +fields = "رقم البطاقة · MM / YY · CVC · ZIP" +label = "تفاصيل البطاقة" +note = "تجمع Stripe تفاصيل البطاقة. لا يخزن Stirling أبدًا PAN أو CVC." + [portal.billing.checkout.error] title = "تعذر بدء الدفع" @@ -6383,10 +6497,9 @@ body = "تمت عملية الدفع. نحن نشغّل المعالجة الم hint = "يرجى إبقاء هذه النافذة مفتوحة." title = "جارٍ تفعيل خطة Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "في بيئة البوابة لتمكين الدفع داخل التطبيق." -bodyBefore = "عيّن" -title = "لم يتم تكوين Stripe" +[portal.billing.checkout.pay] +limit = "حد الإنفاق الشهري {{amount}}/شهر. تتوقف المعالجة مؤقتًا عند الحد." +limitNoCap = "لا يوجد حد للإنفاق. لن تتوقف المعالجة مؤقتًا. تُصدر الفواتير شهريًا." [portal.billing.enterpriseUpsell] cta = "أنشئ عرض سعر Enterprise الخاص بك" @@ -6409,10 +6522,12 @@ currentPlan = "الخطة الحالية" freeForever = "مجاني إلى الأبد" noTeamResolved = "لم يتم تحديد فريق في محفظتك بعد — حدّث وحاول مرة أخرى." ownerOnly = "يمكن لمالك الفريق فقط تشغيل خطة Processor." +payInvoice = "ادفع الفاتورة للإكمال" planName = "Editor" ssoIncluded = "SSO مضمن" switchOnProcessor = "تشغيل Processor ←" unlimitedUsers = "مستخدمون غير محدودين" +viewQuote = "عرض عرض السعر" [portal.billing.invoices] columnAmount = "المبلغ" @@ -6465,7 +6580,111 @@ segmentApiDesc = "طلبات API مباشرة" segmentApiLabel = "API" segmentAutomationDesc = "الأتمتة ومسارات المعالجة" segmentAutomationLabel = "الأتمتة" +sizeMultiplier = "استخدم {{formatted}} من ملفات PDF مضاعف حجم" +summary = "{{unique}} فريد · {{units}} وحدة قياس · متوسط {{avg}} لكل PDF" +summaryNoRate = "{{unique}} فريد · {{units}} وحدة قياس" unit = "ملفات PDF مقاسة" +unitsPending_one = "{{units}} وحدة قياس بانتظار المزامنة من المثيلات المرتبطة" +unitsPending_other = "{{units}} وحدات قياس بانتظار المزامنة من المثيلات المرتبطة" + +[portal.billing.prepaid] +capSuffix = "من {{total}} رصيد مدفوع مسبقًا" +expires = "تنتهي في {{date}}" +eyebrow = "السعة المدفوعة مسبقًا" +note = "تُستخدم قبل الفوترة المقاسة وخارج حد الإنفاق الخاص بك." +topUp = "شحن" + +[portal.billing.prepaid.buy] +back = "رجوع" +calcTitle = "احسب دفعتك السنوية" +cancel = "إلغاء" +cancelPurchase = "إلغاء الشراء" +close = "إغلاق" +continueToPayment = "المتابعة إلى الدفع" +finalise = "إنهاء" +invoiceErrorTitle = "تعذر إنشاء الفاتورة" +loadingTitle = "جارٍ تحميل عرض السعر" +payTitle = "ادفع مقابل سنتك" +step = "الخطوة {{current}} من {{total}}" + +[portal.billing.prepaid.calc] +change = "تغيير" +deployRow = "النشر" +done = "تم" +enterpriseHint = "هذا نطاق مؤسسي. عند هذا الحجم، تتفوق أسعار المؤسسات على أي خصم خدمة ذاتية. تثبيت السعر والشروط وعرض سعر خلال دقائق." +governanceRow = "الحوكمة" +governanceValue = "{{label}} · {{count}} سياسة" +handlesLabel = "المعالج الخاص بك" +handlesValue = "يعالج {{volume}} رصيد / شهر" +pipelinesLabel = "مسارات المعالجة" +policiesMeta = "{{count}} سياسة" +poolCaption = "مجموعة واحدة من {{credits}} رصيد للسنة. الأشهر الثقيلة تستعير من الأشهر الخفيفة." +priceLabel = "سنتك · 12 شهرًا بسعر 10" +rateUnknown = "سنعرض السعر الدقيق عند الدفع." +savings = "توفر {{amount}} · شهران مجانًا." +sizingRow = "التحجيم" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "إجمالي المستخدمين" + +[portal.billing.prepaid.consent] +label = "أفهم أنه عند استخدام سعتى المدفوعة مسبقًا بالكامل أو انتهاء صلاحيتها بعد 12 شهرًا، تستمر المعالجة تلقائيًا بالسعر القياسي المقاس للدفع حسب الاستخدام (حتى حد الإنفاق الخاص بي) ما لم ألغِ، وأنني أستطيع الإلغاء في أي وقت من بوابة الفوترة." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "تديره Stirling. يعمل خلال دقائق." +cloudValue = "Stirling Cloud · مُدار" +selfhost = "مستضاف ذاتيًا" +selfhostDesc = "يعمل على بنية تحتية خاصة. نفس السعر." +selfhostValue = "مستضاف ذاتيًا · بنية تحتية خاصة" + +[portal.billing.prepaid.offer] +cta = "مراجعة العرض" +subtitle = "ادفع مسبقًا سنة من معالجة PDF واحصل على شهرين مجانًا، تُستخدم قبل الفوترة المقاسة وخارج حد الإنفاق الخاص بك." +title = "احصل على 12 شهرًا بسعر 10" + +[portal.billing.prepaid.pay] +accountName = "اسم صاحب الحساب" +accountNamePlaceholder = "مثلًا Jane Smith" +companyName = "اسم الشركة (اختياري)" +companyNamePlaceholder = "مثلًا Acme Inc." +downloadInvoice = "تنزيل الفاتورة" +payOnline = "الدفع عبر الإنترنت" +poNumber = "رقم PO (اختياري)" +poNumberPlaceholder = "مثلًا PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "متقدم" +advancedDesc = "تقود مسارات المعالجة معظم معالجتك." +none = "لا شيء" +noneDesc = "لم تشغّل مسارات معالجة بعد. يمكنك تشغيلها في أي وقت." +standard = "قياسي" +standardDesc = "بعض مسارات المعالجة تعيد معالجة ملفات PDF الواردة." + +[portal.billing.prepaid.posture] +essentials = "الأساسيات" +essentialsDesc = "التصنيف، الافتراضي، بالإضافة إلى المشاركة" +governed = "محكوم" +governedDesc = "يضيف الأمان والتوجيه" +regulated = "منظّم" +regulatedDesc = "كل فئة، بما في ذلك الامتثال والاحتفاظ والاستيعاب" + +[portal.billing.prepaid.review] +download = "تنزيل عرض السعر (PDF)" +downloading = "جارٍ التحضير…" +share = "مشاركة للموافقة" + +[portal.billing.prepaid.size] +compact = "مضغوط" +compactDesc = "ملفات أقل من 25 MB، بدون رسوم بيانات" +heavy = "ثقيل" +heavyDesc = "ممسوح ضوئيًا أو كثيف الصور، عادةً 50 MB+" +standard = "قياسي" +standardDesc = "غالبًا صغيرة، وبعض عمليات المسح تتجاوز 25 MB" + +[portal.billing.prepaid.state] +exhausted = "مستخدمة بالكامل" +healthy = "متبقٍ الكثير" +low = "توشك على النفاد" [portal.billing.spendLimit] adjustLimit = "تعديل الحد" @@ -6494,6 +6713,8 @@ label = "متوقع تجاوزه." [portal.billing.spendThisMonth] eyebrow = "الإنفاق هذا الشهر" +freeRemaining_one = "متبقٍ {{formatted}} PDF مجاني" +freeRemaining_other = "متبقٍ {{formatted}} ملفات PDF مجانية" processed_one = "تمت معالجة {{formattedCount}} ملف PDF." processed_other = "تمت معالجة {{formattedCount}} ملفات PDF." processedWithRate_one = "تمت معالجة {{formattedCount}} ملف PDF، بسعر {{rate}} لكل واحد." @@ -6510,6 +6731,7 @@ reachedTitle = "تم بلوغ حد الإنفاق الشهري" title = "تعذر فتح بوابة Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "من {{allowance}} ملفات PDF مجانية مستخدمة" capSuffix_other = "من {{allowance}} ملفات PDF مجانية مستخدمة" eyebrow = "تجربة Processor" @@ -6534,6 +6756,376 @@ signature = "توقيع" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "اتصال جديد" +createTitleFor = "اتصال {{name}}" +delete = "حذف" +edit = "تعديل" +editTitle = "تعديل الاتصال" +unsupportedType = "لا يمكن تعديل نوع الاتصال هذا هنا." + +[portal.connections.categories.advanced] +description = "لخدمة لا ندعمها بعد." +label = "متقدم" + +[portal.connections.categories.audit] +description = "سجّل ما حدث للمستند حيث ينظر المدققون لديك بالفعل." +label = "تسجيل التدقيق والامتثال" + +[portal.connections.categories.notify] +description = "أخبر نظامًا آخر بأن شيئًا ما حدث." +label = "الإشعار وسير العمل" + +[portal.connections.categories.security] +description = "افحص مستندًا أو ضع عليه تسمية قبل أن ينتقل." +label = "الأمان والتصنيف" + +[portal.connections.categories.signing] +description = "أرسل مستندًا للتوقيع أو التوثيق." +label = "التوقيع الإلكتروني والتوثيق" + +[portal.connections.categories.storage] +description = "حيث تنتهي المستندات المعالجة - حاوية أو محرك أقراص أو تذكرة أو صفحة." +label = "ملف وإرفاق" + +[portal.connections.commonFields.accessToken] +helperText = "يخزن Stirling هذا مشفرًا ولا يعرضه مرة أخرى أبدًا." +label = "رمز الوصول" + +[portal.connections.commonFields.apiKey] +label = "مفتاح API" + +[portal.connections.commonFields.apiToken] +label = "رمز API" + +[portal.connections.commonFields.baseUrl] +helperText = "يمكن للخطوات التي تستخدم هذا الاتصال استدعاء المسارات ضمن URL هذا فقط، وليس مضيفًا آخر أبدًا." +label = "URL الخادم" + +[portal.connections.commonFields.email] +label = "البريد الإلكتروني" + +[portal.connections.commonFields.host] +label = "المضيف" + +[portal.connections.commonFields.password] +label = "كلمة المرور" + +[portal.connections.commonFields.port] +label = "المنفذ" + +[portal.connections.commonFields.username] +label = "اسم المستخدم" + +[portal.connections.commonFields.webhookUrl] +helperText = "الصق URL الذي أعطتك إياه الخدمة. لا يمكن استدعاء إلا المسارات الموجودة تحته." +label = "Webhook URL" + +[portal.connections.fields] +name = "اسم الاتصال" +namePlaceholder = "مثلًا الإنتاج" + +[portal.connections.picker] +cancel = "إلغاء" +createNew = "اتصال جديد..." +placeholder = "حدد اتصالًا" +save = "حفظ الاتصال" + +[portal.connections.picker2] +back = "تغيير التكامل" +heading = "ما الذي تريد توصيله؟" +noResultsBody = "لا ندعمه بعد - أخبرنا عنه وسننظر في إضافته." +noResultsTitle = "لا شيء يطابق \"{{query}}\"" +searchPlaceholder = "ابحث في التكاملات - جرّب \"sign\" أو \"ocr\" أو اسم منتج" + +[portal.connections.types.api] +description = "استدعِ أي HTTP API من سياسة. أنت تختار URL والمصادقة والحمولة." +label = "API مخصص" + +[portal.connections.types.api.fields.authType] +label = "المصادقة" + +[portal.connections.types.api.fields.authType.options.basic] +label = "اسم المستخدم وكلمة المرور" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "رمز في ترويسة مخصصة" + +[portal.connections.types.api.fields.authType.options.none] +label = "لا شيء" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "تسجيل الدخول للحصول على رمز" + +[portal.connections.types.api.fields.baseUrl] +helperText = "يمكن للخطوات التي تستخدم هذا الاتصال استدعاء المسارات ضمن URL هذا فقط، وليس مضيفًا آخر أبدًا." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "اسم الترويسة" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "مسار تسجيل الدخول" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "كلمة المرور" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "إذا ردّت API برابط تنزيل، فلا يمكن جلب إلا هؤلاء المضيفين. افصل بينها بفواصل. اتركه فارغًا للسماح بمضيف Base URL نفسه فقط." +label = "مضيفو النتائج المسموح بهم" + +[portal.connections.types.api.fields.token] +label = "الرمز" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "إرسال الرمز بهذه الترويسة" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "ترويسة الاستجابة التي يعيدها استدعاء تسجيل الدخول وفيها الرمز، مثل X-Auth-Token." +label = "ترويسة استجابة الرمز" + +[portal.connections.types.api.fields.username] +label = "اسم المستخدم" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "افحص المستندات بحثًا عن البرامج الضارة على خادم تقوم بتشغيله - لا يغادر شيء شبكتك." +label = "ClamAV (مستضاف ذاتيًا)" + +[portal.connections.types.cloudmersive] +description = "فحص الفيروسات والمحتوى للمستندات في سياسة." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "احظر المستندات التي تحتوي على وحدات ماكرو أو ملفات تنفيذية أو نصوص برمجية أو كائنات مضمنة - وليس فقط البرامج الضارة المعروفة." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "إرفاق المستندات المعالجة بصفحة Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "أرسل المستندات إلى ConsignO Cloud (Notarius) للتوقيع الإلكتروني والتوثيق." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "بيئة ConsignO Cloud الخاصة بك، بما في ذلك /api/v1." +label = "URL الخادم" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "مفتاح API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "سر API" + +[portal.connections.types.consigno.fields.password] +label = "كلمة المرور" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "مطلوب فقط إذا كانت مؤسستك تستخدم مستأجرين." +label = "المستأجر" + +[portal.connections.types.consigno.fields.username] +label = "اسم المستخدم" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "انشر رسالة إلى Discord عندما تعالج سياسة مستندًا." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "فهرس حدث تدقيق في Elasticsearch في كل مرة تتعامل فيها سياسة مع مستند." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "استطلع خادم FTP أو FTPS بحثًا عن مستندات جديدة." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "وضع الاتصال" + +[portal.connections.types.ftp.fields.passive.options] +active = "نشط" +passive = "سلبي (موصى به)" + +[portal.connections.types.ftp.fields.security] +helperText = "يرسل FTP العادي بيانات الاعتماد والملفات دون تشفير؛ اختر FTPS إذا كان الخادم يدعمه." +label = "التشفير" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (صريح / AUTH TLS)" +implicit = "FTPS (ضمني)" +none = "لا شيء (FTP عادي)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "انشر رسالة إلى مساحة Google Chat عندما تعمل سياسة." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "إرفاق المستندات المعالجة بمشكلة Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "أرسل مستندًا أو إشعارًا بالبريد الإلكتروني عندما تعمل سياسة." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "حفظ المستندات المعالجة في Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "كلمة مرور التطبيق" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "اكتشف البيانات الشخصية في نص المستند باستخدام خادم Presidio تقوم بتشغيله." +label = "Presidio (مستضاف ذاتيًا)" + +[portal.connections.types.purview] +description = "قراءة وتطبيق تسميات حساسية Microsoft Purview على ملفات PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "اختياري. مطلوب فقط لاختيار التسميات بالاسم؛ يعمل وضع التسميات بدونه." +label = "معرّف التطبيق (العميل)" + +[portal.connections.types.purview.fields.clientSecret] +label = "سر العميل" + +[portal.connections.types.purview.fields.tenantId] +helperText = "يُختم على كل تسمية باعتباره SiteId الخاص بها، حتى تعرف الأدوات المتوافقة مع Purview أي مؤسسة طبقتها." +label = "معرّف الدليل (المستأجر)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "حاوية S3 يمكن للمصادر القراءة منها ويمكن لمخرجات مسار المعالجة الكتابة إليها." +label = "تخزين S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "معرّف مفتاح الوصول" + +[portal.connections.types.s3.fields.bucket] +label = "الحاوية" + +[portal.connections.types.s3.fields.endpoint] +helperText = "اتركه فارغًا لـ AWS. عيّنه لـ MinIO أو مخزن آخر متوافق مع S3." +label = "نقطة النهاية" + +[portal.connections.types.s3.fields.region] +label = "المنطقة" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "مفتاح الوصول السري" + +[portal.connections.types.sendgrid] +description = "أرسل مستندًا أو إشعارًا بالبريد الإلكتروني عندما تعمل سياسة." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "استطلع خادم SFTP (SSH) بحثًا عن مستندات جديدة." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "اختياري. بصمة مفتاح مضيف SHA-256 للخادم (من `ssh-keyscan host | ssh-keygen -lf -`). عند تعيينها، لا يُقبل إلا خادم يقدم هذا المفتاح؛ وعند تركها فارغة، يتم تثبيت المفتاح الذي يظهر عند أول اتصال وتُرفض التغييرات اللاحقة." +label = "بصمة مفتاح المضيف" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "عبارة مرور المفتاح" + +[portal.connections.types.sftp.fields.password] +helperText = "اتركه فارغًا إذا كنت تصادق باستخدام مفتاح خاص بدلًا من ذلك." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "اختياري. الصق مفتاح OpenSSH خاصًا للمصادقة بمفتاح بدلًا من كلمة مرور." +label = "المفتاح الخاص" + +[portal.connections.types.slack] +description = "انشر رسالة إلى Slack عندما تعالج سياسة مستندًا." +label = "Slack" + +[portal.connections.types.smb] +description = "راقب مشاركة شبكة SMB / CIFS (Windows أو Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / مشاركة شبكة" + +[portal.connections.types.smb.fields.domain] +helperText = "اختياري. نطاق Windows أو مجموعة العمل، إذا كان خادمك يستخدم أحدهما." +label = "النطاق" + +[portal.connections.types.smb.fields.share] +label = "المشاركة" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "أرسل حدث تدقيق إلى Splunk في كل مرة تتعامل فيها سياسة مع مستند." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "أرسل حدث تدقيق إلى جامع HTTP في Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "انشر رسالة إلى قناة Teams عندما تعمل سياسة." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "انشر كل مستند، أو التفاصيل فقط، إلى URL تختاره." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "شغّل سيناريو Zap أو Make من سياسة." +label = "Zapier وMake" + +[portal.docs] +browse = "تصفح المستندات" +viewSource = "عرض المصدر على GitHub" + [portal.docs.authentication] codeCaption = "كل طلب" eyebrow = "البدء" @@ -6624,6 +7216,9 @@ eyebrow = "المهارات" lead = "قدرات مدمجة ومسمّاة يستدعيها وكيلك كأداة واحدة. كل مهارة هي سلسلة عمليات حتمية مع تقييمات مرفقة." title = "مهارات الوكيل" +[portal.docs.toc] +title = "في هذه الصفحة" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "مرجع API" @@ -6697,6 +7292,8 @@ type = "النوع" user = "المستخدم" [portal.documents.queue.empty] +connectSource = "توصيل مصدر" +createPipeline = "إنشاء مسار معالجة" description = "عندما تغذي المصادر المستندات إلى مسارات العمل لديك، ستظهر هنا للمراجعة." title = "لا توجد مستندات في قائمة الانتظار" @@ -6712,11 +7309,11 @@ editorAction = "المحرر" empty = "لا توجد مستندات تطابق عامل التصفية هذا." rowActions = "إجراءات الصف" sensitiveLabel = "حساس" -sensitiveTitle = "حساس — الوصول مطلوب" [portal.documents.table.columns] action = "مسار العمل / الإجراء" document = "المستند" +labels = "Labels" product = "المنتج" status = "الحالة" time = "الوقت" @@ -6732,6 +7329,7 @@ host = "المضيف" lastSeen = "آخر ظهور" region = "المنطقة" status = "الحالة" +target = "Target" version = "الإصدار" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "واجه هذا العرض خطأً غير متوقع. حاول retry = "حاول مرة أخرى" title = "حدث خطأ ما في هذه الصفحة" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "رجوع" +body = "نزّل تطبيق سطح المكتب أو استضف الخادم ذاتيًا." +done = "تم" +guide = "افتح دليل {{name}}" +openInBrowser = "افتح في المتصفح بدلًا من ذلك" +sectionDesktop = "تطبيق سطح المكتب" +sectionSelfHosted = "مستضاف ذاتيًا" +title = "تثبيت محرر Stirling PDF" + +[portal.home.download.docker] +detailBody = "شغّل الصورة الرسمية على خادمك." +note = "يفتح على :8080. تسجيل الدخول الافتراضي: admin / stirling." +tagline = "شغّل الخادم في حاوية" +title = "Docker" +variantFat = "كامل" +variantLite = "فائق الخفة" +variantStandard = "قياسي" + +[portal.home.download.kubernetes] +detailBody = "ثبّت مخطط Helm الرسمي." +note = "أضف PVC لـ /configs للاحتفاظ بالإعدادات." +tagline = "انشر إلى عنقود باستخدام Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "إصدارات أصلية لتوزيعتك." +guideBtn = "عرض خيارات تثبيت Linux" +note = "تتوفر إصدارات AppImage و.deb و.rpm للتوزيعات الشائعة." +tagline = "تطبيق سطح مكتب أصلي لتوزيعتك" +title = "Linux" + +[portal.home.download.mac] +altLabel = "أو عبر Homebrew" +detailBody = "شغّل المثبّت، أو استخدم Homebrew." +downloadBtn = "تنزيل لـ macOS" +tagline = "تطبيق أصلي لـ Apple silicon وIntel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "شغّل ملف .jar على خادم مادي مباشر." +downloadBtn = "تنزيل ملف .jar" +note = "يتطلب Java 21+. يفتح على :8080؛ تسجيل الدخول الافتراضي admin / stirling." +runLabel = "ثم شغّله" +tagline = "شغّل ملف .jar على خادم مادي مباشر" +title = "إعداد خادم يدوي" + +[portal.home.download.windows] +altLabel = "أو عبر winget" +detailBody = "شغّل المثبّت، أو استخدم مدير حزم." +downloadBtn = "تنزيل المثبّت" +tagline = "تطبيق أصلي لـ Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} نشط" -invite = "دعوة زملاء الفريق" +activeOfDeployed = "{{active}} من {{total}} نشط هذا الشهر" name = "Stirling PDF Editor" open = "فتح في المتصفح" updated = "تم التحديث {{time}}" +[portal.home.editor.deploy] +finish = "إنهاء النشر" +options = "عرض خيارات النشر" +start = "نشر المحرر" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "مستضاف ذاتيًا · Docker" @@ -6823,49 +7529,6 @@ afternoon = "مساء الخير" evening = "مساء الخير" morning = "صباح الخير" -[portal.home.onboarding] -dismiss = "تجاهل الإعداد" -notStarted = "لم يبدأ" -progress = "تم إنجاز {{done}} من {{total}}" -title = "إكمال الإعداد" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC على مستوى المؤسسة، وتسعير حجم ملتزم، ونشر معزول عن الشبكة." -cta = "بدء التجربة" -ctaQuote = "الحصول على عرض سعر" -lead = "لأكثر من 250 موظفًا." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "انشر تطبيق سطح المكتب، مجاني للأبد." -title = "تنزيل PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} نشطة · {{recommended}} موصى بها" -chip = "{{active}} نشطة" -title = "تشغيل السياسات" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} متصلة · كل PDF محكوم أينما وصل" -chip = "{{connected}} متصلة" -title = "ربط مصادرك" - -[portal.home.quickActions] -subtitle = "أهم مهام اليوم" -title = "إجراءات سريعة" - -[portal.home.quickActions.buildPipeline] -blurb = "منشئ من 3 خطوات فوق مكتبة العمليات المصنفة" -title = "إنشاء مسار عمل" - -[portal.home.quickActions.connectSource] -blurb = "S3، وكلاء، webhooks، مجلدات مراقبة" -title = "ربط مصدر" - -[portal.home.quickActions.issueApiKey] -blurb = "مفتاح محدد النطاق مع حدود معدل وقائمة سماح IP" -title = "إصدار مفتاح API" - [portal.infrastructure] manageEditorDeployment = "إدارة نشر Editor" sectionsAriaLabel = "أقسام البنية التحتية" @@ -6878,13 +7541,9 @@ heading = "مفاتيح API" subheading = "بيانات اعتماد محددة النطاق مع حدود معدل لكل مفتاح وأذونات وقوائم سماح IP." [portal.infrastructure.apiKeys.card] -allowedIps = "عناوين IP المسموح بها" -anyIp = "أي IP (بدون قائمة سماح)" created = "تم الإنشاء" lastUsed = "آخر استخدام" -permissions = "الأذونات" -rateLimit = "حد المعدل" -rateLimitValue = "{{value}} طلب/دقيقة" +revoke = "إبطال المفتاح" usageMonth = "الاستخدام هذا الشهر" usageToday = "الاستخدام اليوم" @@ -6892,6 +7551,15 @@ usageToday = "الاستخدام اليوم" description = "أنشئ مفتاحًا محدد النطاق للبدء في استدعاء Stirling API." title = "لا توجد مفاتيح API بعد" +[portal.infrastructure.apiKeys.error] +load = "تعذر تحميل مفاتيح API الخاصة بك. يُرجى المحاولة مرة أخرى." + +[portal.infrastructure.apiKeys.revoke] +body = "هل تريد إبطال \"{{name}}\"؟ أي تكامل لا يزال يستخدم هذا المفتاح سيبدأ فورًا بتلقي أخطاء 401. لا يمكن التراجع عن ذلك." +cancel = "إلغاء" +confirm = "إبطال المفتاح" +title = "إبطال مفتاح API" + [portal.infrastructure.audit] filterAriaLabel = "تصفية أحداث التدقيق حسب الفئة" heading = "سجلات التدقيق" @@ -6935,6 +7603,7 @@ all = "الكل" auth = "المصادقة" config = "التكوين" elevation = "رفع الصلاحية" +policy = "السياسة" processing = "المعالجة" security = "الأمان" @@ -6944,19 +7613,30 @@ title = "ليس لديك وصول" [portal.infrastructure.audit.metrics] config = "التكوين" -elevation = "رفع الصلاحية" +policy = "السياسة" processing = "المعالجة" totalEvents = "إجمالي الأحداث · 24 ساعة" +[portal.infrastructure.auditCatLabel] +auth = "المصادقة" +config = "التكوين" +elevation = "رفع الصلاحيات" +policy = "السياسة" +processing = "المعالجة" +security = "الأمان" + +[portal.infrastructure.auditStatusLabel] +danger = "خطأ" +info = "معلومات" +success = "نجاح" +warning = "تحذير" + [portal.infrastructure.createKey] cancel = "إلغاء" createKey = "إنشاء مفتاح" done = "تم" -ipAllowlistHelper = "نطاقات CIDR مفصولة بفواصل. اتركه فارغًا للسماح بأي IP." -ipAllowlistLabel = "قائمة سماح IP" keyNameLabel = "اسم المفتاح" keyNamePlaceholder = "مثال: الإنتاج · الاستيعاب" -permissionsLabel = "الأذونات" secretKeyCaption = "المفتاح السري" secretWarning = "خزّن هذا في مدير أسرار. لا يخزن Stirling إلا تجزئة — ولا توجد طريقة لاستعادته لاحقًا." subtitle = "حدد نطاق المفتاح إلى الحد الأدنى الذي يحتاجه. يمكنك تدويره أو إلغاؤه في أي وقت." @@ -6964,207 +7644,9 @@ subtitleCreated = "انسخ هذا السر الآن — لن يتم عرضه م title = "إنشاء مفتاح API" titleCreated = "تم إنشاء المفتاح" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/دقيقة" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "تم النشر بواسطة" -environment = "البيئة" -product = "المنتج" -status = "الحالة" -version = "الإصدار" -when = "متى" - -[portal.infrastructure.deployments.recent] -heading = "عمليات النشر الأخيرة" -subheading = "أحدث عمليات الطرح عبر المنتجات والبيئات." - -[portal.infrastructure.deployments.regionColumns] -instances = "المثيلات" -latency = "زمن الاستجابة" -load = "الحمل" -p99 = "P99" -region = "المنطقة" -status = "الحالة" -throughput = "الإنتاجية" -uptime = "وقت التشغيل" -version = "الإصدار" - -[portal.infrastructure.deployments.regions] -heading = "المناطق" -subheading = "صحة حية لكل منطقة Stirling منشورة — زمن الاستجابة والحمل وإصدار الطرح." - -[portal.infrastructure.deployments.regions.empty] -description = "ستظهر المناطق المنشورة هنا بمجرد تزويد مساحة عملك." -title = "لا توجد مناطق منشورة" - -[portal.infrastructure.models] -heading = "النماذج" -msValue = "{{value}} ms" -subheading = "كتالوج النماذج والتوجيه الذي يشغل معالجة المستندات عبر مساحة عملك." - -[portal.infrastructure.models.byom] -description = "سجّل نموذجًا داخل المؤسسة أو مستضافًا ذاتيًا وثبّته في منطقة للمعالجة المقيدة بإقامة البيانات." -title = "أحضر نموذجك الخاص" - -[portal.infrastructure.models.catalogue] -heading = "الكتالوج" -sub = "النماذج المُدارة المتاحة لمساحة عملك، مع زمن استجابة وتكلفة حيين." -subEnterprise = "نماذج مُدارة، وأحضر نموذجك الخاص، ونماذج داخل المؤسسة — مع إمكانية التثبيت لكل منطقة." - -[portal.infrastructure.models.catalogue.empty] -description = "ستظهر النماذج في كتالوج مساحة عملك هنا." -title = "لا توجد نماذج متاحة" - -[portal.infrastructure.models.columns] -cost = "التكلفة" -latency = "زمن الاستجابة" -load = "الحمل" -model = "النموذج" -status = "الحالة" -type = "النوع" -version = "الإصدار" - -[portal.infrastructure.models.metrics] -activeModels = "النماذج النشطة" -avgLatency = "متوسط زمن الاستجابة" -included = "مضمن" -monthlySpend = "الإنفاق الشهري على النماذج" - -[portal.infrastructure.models.routing] -empty = "لا توجد قواعد توجيه مكوّنة." -heading = "قواعد التوجيه" -sub = "أي نموذج يتولى كل عملية. ينطبق الافتراضي عندما لا تطابق قاعدة أضيق." -subLocked = "وجّه العمليات إلى نماذج محددة — متاح في الخطط المدفوعة." - -[portal.infrastructure.models.routing.lockedBanner] -description = "قم بالترقية إلى Pro للتحكم في النموذج الذي يتولى كل عملية ونوع مستند." -title = "توجيه النماذج ميزة مدفوعة" - -[portal.infrastructure.models.routingColumns] -default = "افتراضي" -docType = "نوع المستند" -modelForAria = "نموذج لـ {{operation}}" -operation = "العملية" -routedTo = "موجّه إلى" - -[portal.infrastructure.security.access.byok] -description = "قدّم مفتاحًا من KMS الخاص بك. يقوم Stirling بالتشفير به لكنه لا يزال قادرًا على القراءة." -label = "أحضر مفتاحك الخاص (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "لا تغادر المفاتيح KMS الخاص بك أبدًا. يحتفظ Stirling بالنص المشفر فقط." -label = "احتفظ بمفتاحك الخاص (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "يدير Stirling مفاتيح التشفير. الأبسط — لا عمليات مفاتيح من جانبك." -label = "مفاتيح يحتفظ بها Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "سياسة الوصول إلى المستندات" -subheading = "تتحكم في من يمكنه فك تشفير المستندات المعالجة أثناء السكون." - -[portal.infrastructure.security.attestations] -heading = "إثباتات الامتثال" -noReport = "لا يوجد تقرير متاح" -subheading = "وضع التدقيق حسب إطار العمل، مع تقارير متاحة للضوابط المثبتة." -viewReport = "عرض التقرير ←" - -[portal.infrastructure.security.compliance] -heading = "الامتثال" -subheading = "إثباتات وشهادات تغطي منصة Stirling." - -[portal.infrastructure.security.empty] -description = "سيظهر تكوين الأمان لمساحة عملك هنا." -title = "وضع الأمان غير متاح" - -[portal.infrastructure.security.hyokBanner] -description = "مع HYOK، لا تغادر مفاتيح التشفير KMS الخاص بك أبدًا. يخزن Stirling ويعالج فقط النص المشفر الذي يمكنك إلغاؤه في أي وقت." -title = "لا يستطيع Stirling فك تشفير مستنداتك" - -[portal.infrastructure.security.ipAllowlist] -empty = "لا توجد نطاقات IP مكوّنة — كل عناوين IP مسموح بها." -heading = "قائمة سماح IP" -sub = "وصول API مقيد بهذه نطاقات CIDR." -subLocked = "قيّد وصول API إلى نطاقات IP معروفة — متاح في الخطط المدفوعة." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "قم بالترقية إلى Pro لتقييد وصول API إلى شبكات محددة." -title = "قائمة سماح IP ميزة مدفوعة" - -[portal.infrastructure.security.ipColumns] -added = "تمت الإضافة" -addedBy = "أضيف بواسطة" -cidr = "CIDR" -label = "التسمية" - -[portal.infrastructure.security.keyManagement] -algorithm = "الخوارزمية" -heading = "إدارة مفاتيح التشفير" -keyId = "معرّف المفتاح" -lastRotated = "آخر تدوير" -rotateKey = "تدوير المفتاح" -rotationPolicy = "سياسة التدوير" -subheading = "حيازة المفاتيح التي تشفر المستندات أثناء السكون — من يمكنه فك التشفير، وكيف يتم تدوير المفاتيح." - -[portal.infrastructure.security.managedBanner] -description = "حيازة أحضر مفتاحك الخاص (BYOK) واحتفظ بمفتاحك الخاص (HYOK) متاحة في Enterprise. قم بالترقية لتقديم مفاتيح من KMS الخاص بك." -title = "يدير Stirling المفاتيح في خطتك" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "آسيا والمحيط الهادئ" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · حدود بيانات GDPR" -label = "الاتحاد الأوروبي" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "الولايات المتحدة" - -[portal.infrastructure.security.residencyHeader] -heading = "إقامة البيانات" -subheading = "مكان تخزين المستندات ومعالجتها." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} مستخدم" - -[portal.infrastructure.storage.empty] -description = "سيظهر التخزين المتصل والاستخدام هنا." -title = "لا يوجد تخزين مكوّن" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "نشط" -activeRange = "0–{{value}} يوم" -archived = "مؤرشف" -coldStorage = "تخزين بارد" -deleted = "محذوف" -never = "أبدًا" -purged = "تمت إزالته نهائيًا" - -[portal.infrastructure.storage.providers] -connect = "ربط" -connected = "متصل" -heading = "الموفرون المتصلون" -subheading = "مكان كتابة المخرجات المعالجة." - -[portal.infrastructure.storage.retention] -heading = "الاحتفاظ" -subheading = "مدة الاحتفاظ بالمخرجات قبل حذف دورة الحياة." -windowLabel = "نافذة الاحتفاظ الافتراضية" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} يوم" -days_other = "{{count}} أيام" -never = "عدم الحذف أبدًا" - -[portal.infrastructure.storage.totalUsage] -heading = "إجمالي الاستخدام" -progressLabel = "التخزين المستخدم" -subheading = "التخزين المستهلك عبر جميع الموفرين المتصلين." +revoked = "مُبطل" [portal.infrastructure.tabs] apiKeys = "مفاتيح API" @@ -7174,6 +7656,49 @@ models = "النماذج" security = "الأمان" storage = "التخزين" +[portal.integrations] +availableHeading = "متاح" +comingSoonHeading = "قريبًا" +connect = "اتصال" +connectionCount_one = "{{count}} اتصال" +connectionCount_other = "{{count}} اتصالات" +customApi = "API مخصص" +searchPlaceholder = "البحث في التكاملات" +subtitle = "كل ما يمكن لـ Stirling التواصل معه. تُنشأ بيانات الاعتماد هنا مرة واحدة، ثم تُستخدم في المصادر والسياسات وخطوط المعالجة." +title = "التكاملات" +typedName = "اسم {{tool}}" + +[portal.integrations.filters] +all = "الكل" +audit = "التدقيق" +connected = "متصل" +notify = "إشعار" +security = "الأمان" +signing = "التوقيع" +storage = "التخزين" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "متصل" + +[portal.integrations.table] +integration = "التكامل" +worksWith = "يعمل مع" + +[portal.integrations.worksWith] +pipelines = "خطوط المعالجة" +policies = "السياسات" +sources = "المصادر" + +[portal.legal] +draft = "{{label}} · مسودة" +loadError = "تعذر تحميل هذا المستند. يُرجى المحاولة مرة أخرى." +loading = "جارٍ التحميل…" +title = "مستند قانوني" + [portal.nav] agent-builder = "منشئ الوكلاء" components = "المكونات" @@ -7182,33 +7707,17 @@ documents = "المستندات" editor = "المحرر" home = "الرئيسية" infrastructure = "البنية التحتية" +integrations = "التكاملات" pipelines = "مسارات العمل" policies = "السياسات" procurement = "المشتريات" -settings = "الإعدادات" sources = "المصادر" usage = "الاستخدام والفوترة" users = "المستخدمون" -[portal.notifications] -markAllRead = "وضع علامة على الكل كمقروء" -title = "الإشعارات" -viewAll = "عرض الكل" - -[portal.notifications.ariaLabel] -none = "الإشعارات، لا توجد غير مقروءة" -unread_one = "الإشعارات، {{count}} غير مقروء" -unread_other = "الإشعارات، {{count}} غير مقروءة" - -[portal.notifications.count] -allRead = "الكل مقروء" -loading = "جارٍ التحميل" -new_one = "{{count}} جديد" -new_other = "{{count}} جديدة" - -[portal.notifications.empty] -description = "لا توجد إشعارات جديدة." -title = "أنت مطّلع على كل شيء" +[portal.nav.section] +platform = "منصة PDF" +processor = "معالج PDF" [portal.pipelines] subtitle = "كل مسار عمل مستندات آلي على الواجهة الخلفية: سلسلة مرتبة من العمليات على مجموعة من المصادر، تعمل عند مشغّل. انقر على صف لرؤية خطواته ومصادره." @@ -7218,54 +7727,87 @@ title = "مسارات العمل" newPipeline = "مسار عمل جديد" [portal.pipelines.builder] -addStep = "إضافة أداة" +activate = "Activate" back = "العودة إلى مسارات العمل" +cannotFollow = "لا يمكن أخذ {{produced}}" +chooseAccount = "اختر حسابًا" +chooseDestination = "اختر وجهة" +chooseOperation = "اختر ما تفعله هذه الخطوة" +chooseSource = "اختر مصدرًا" discard = "تجاهل التغييرات" -enabled = "مفعّل" +inputs = "الإدخال" +inputSource = "مصدر الإدخال" +inputTrigger = "المشغّل" keepEditing = "متابعة التحرير" -needsUpload = "يحتاج إلى ملف مرفوع" +moreActions = "More actions" +needsConfiguring = "يحتاج إلى إعداد" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "لا توجد أدوات تطابق بحثك." -pipelineSettings = "إعدادات مسار العمل" +pause = "Pause" +rename = "Rename pipeline" searchTools = "البحث في الأدوات" -selectToolBody = "أضف أداة لإنشاء مسار عملك." -selectToolTitle = "لا توجد أدوات بعد" -toolSettings = "إعدادات الأداة" +sendToSystem = "إرسال إلى نظام آخر" +stepsIncompatible = "لا يمكن تشغيل هذه الخطوات على ما تنتجه الخطوة السابقة: {{tools}}." +stepsNeedSetup = "لا تزال هذه الخطوات بحاجة إلى إعداد قبل الحفظ: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "عملية غير معروفة، تم الإبقاء عليها كما هي." unsavedBody = "لديك تغييرات غير محفوظة. احفظها قبل المغادرة، أم تتجاهلها؟" unsavedTitle = "تغييرات غير محفوظة" -uploadUnsupported = "الملفات المرفوعة غير مدعومة في مسارات العمل بعد، لذلك لا يمكن حفظ هذه الخطوات: {{tools}}." usesDefaults = "يعمل بالإعدادات الافتراضية" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "يجمع كل ملف من الخطوة السابقة" +fan-out = "يعمل مرة واحدة لكل ملف من الخطوة السابقة" +format-mismatch = "يحتاج إلى {{accepts}}، لكن الخطوة السابقة تنتج {{produced}}" +output-uncertain = "قد لا يعمل: يعتمد ناتج الخطوة السابقة على كيفية إعدادها" +source-mismatch = "يحتاج إلى {{accepts}}، لكن إدخال خط المعالجة هذا هو {{produced}}" +undeclared-operation = "لا يمكن التحقق مما تقبله هذه الخطوة" [portal.pipelines.composer] addTool = "إضافة أداة" -cancel = "إلغاء" -chainEmpty = "أضف أداة لبدء إنشاء مسار عملك." create = "إنشاء مسار عمل" -directory = "مجلد الإخراج" -directoryHelp = "مسار مطلق على الخادم. يجب أن يكون ضمن المجلدات المسموح بها المكوّنة." +createPaused = "Create paused" editingUnsupported = "عرض معاملات هذه الأداة للتحرير غير مدعوم بعد." -moveDown = "نقل إلى الأسفل" -moveUp = "نقل إلى الأعلى" +editSource = "Edit source" name = "الاسم" namePlaceholder = "مثال: مسح التنقيح" -noSources = "لا توجد مصادر متصلة بعد. لا يزال بإمكان مسار العمل العمل على الملفات المقدمة إليه مباشرة." noToolSettings = "لا تحتوي هذه الأداة على إعدادات قابلة للتكوين." -operations_one = "عملية ({{count}})" -operations_other = "عمليات ({{count}})" output = "الإخراج" -removeStep = "إزالة العملية" save = "حفظ التغييرات" scheduleEvery = "تشغيل كل" -sources = "المصادر" -sourcesLoading = "جارٍ تحميل المصادر..." trigger = "المشغّل" triggerManual = "يدوي فقط" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "أيام" hours = "ساعات" minutes = "دقائق" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "حذف \"{{name}}\"؟ لا يمكن التراجع عن هذا." cancel = "إلغاء" @@ -7273,28 +7815,62 @@ confirm = "حذف" title = "حذف مسار العمل؟" [portal.pipelines.detail] +clearHistory = "مسح السجل" delete = "حذف مسار العمل" run = "تشغيل الآن" [portal.pipelines.empty] action = "إنشاء مسار عمل" +connectSource = "اتصال بمصدر" description = "أنشئ مسار عملك الأول: اختر المصادر التي يعمل عليها، وسلسلة العمليات، ومكان انتقال الإخراج." title = "لا توجد مسارات عمل بعد" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "نشطة" paused = "متوقفة مؤقتًا" total = "مسارات العمل" -[portal.pipelines.output] -folder = "كتابة إلى مجلد" -inline = "إرجاع الملفات" - [portal.pipelines.run] +allProcessed_one = "لا يوجد ما يمكن تشغيله: تمّت معالجة مستند {{count}} الخاص بالمصدر بالفعل." +allProcessed_other = "لا يوجد ما يمكن تشغيله: تمّت معالجة جميع المستندات وعددها {{count}} في المصادر بالفعل." completed_one = "اكتمل التشغيل." completed_other = "اكتملت كل عمليات التشغيل البالغ عددها {{count}}." empty = "لا يوجد شيء لتشغيله: لم تكن لدى المصادر مستندات للمعالجة." failed = "فشل التشغيل: {{error}}" +inFlight = "لا يوجد شيء جديد لتشغيله: لا تزال المستندات قيد المعالجة من عملية تشغيل سابقة." +parked_one = "لا يوجد ما يمكن تشغيله: فشل مستند {{count}} سابقًا وتم إيقافه. أصلح السبب، ثم امسح السجل لإعادة المحاولة." +parked_other = "لا يوجد ما يمكن تشغيله: فشلت مستندات عددها {{count}} سابقًا وتم إيقافها. أصلح السبب، ثم امسح السجل لإعادة المحاولة." running = "بدأ التشغيل؛ لا يزال قيد التقدم." timeout = "يستغرق التشغيل وقتًا أطول من المتوقع؛ قد يظل ينتهي في الخلفية." @@ -7307,6 +7883,7 @@ name = "مسار العمل" sources = "المصادر" status = "الحالة" steps = "الخطوات" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "مراقبة المجلد" @@ -7314,12 +7891,18 @@ manual = "يدوي" schedule = "مجدول" [portal.policies] +defaultName = "سياسة {{category}}" subtitle = "أتمتات دائمة تفرض مسار أدوات على كل مستند. يتم تشغيل كل سياسة عند الرفع أو التصدير، وتشغّل سلسلة أدواتها، وتحفظ النسخة المفروضة بجانب الأصل." title = "السياسات" [portal.policies.card] comingSoon = "الترقية إلى Enterprise" notSetUp = "غير معدّة" +requiresAiEngine = "يتطلب محرك AI" + +[portal.policies.categories.classification] +desc = "تعرّف على نوع كل مستند عند الرفع ووسم بياناته الوصفية للأرشفة والبحث." +label = "التصنيف" [portal.policies.categories.compliance] desc = "فرض متطلبات HIPAA أو GDPR أو SOC 2 أو FedRAMP على كل مستند." @@ -7344,6 +7927,13 @@ label = "الأمان" [portal.policies.config] scopeAll = "كل المستندات" +[portal.policies.config.classification] +summary = "يصنّف كل مستند مرفوع وفق تسميات فريقك ويوسم بياناته الوصفية." + +[portal.policies.config.classification.rules] +0 = "تصنيف" +1 = "وسم البيانات الوصفية" + [portal.policies.config.compliance] summary = "يتحقق من المستندات مقابل الأطر التنظيمية قبل مغادرتها النظام." @@ -7371,6 +7961,29 @@ minConfidence = "الحد الأدنى للثقة" 2 = "تسمية" 3 = "تطبيع" +[portal.policies.config.purview] +label = "تطبيق تسمية حساسية Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "مستأجر Purview" +labelId = "معرّف التسمية" +labelIdHelp = "GUID الخاص بالتسمية، من بوابة Purview. يؤدي تطبيق تسمية إلى كتابة بياناتها الوصفية على PDF؛ ولا يمكنه التشفير، إذ يتطلب ذلك عميل Microsoft." +labelName = "اسم التسمية (اختياري)" +labelNameHelp = "يُسجل إلى جانب المعرّف كي يتمكن الأشخاص والأدوات من التعرف على التسمية." + +[portal.policies.config.purview.fields.method] +help = "يميز Purview بين تسمية طبقتها سياسة وتسمية اختارها شخص." +label = "كيفية تسجيل التسمية" +privileged = "كما اختارها شخص" +standard = "كما طُبقت تلقائيًا" + +[portal.policies.config.purviewRead] +label = "قراءة تسمية Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "مستأجر Purview" +connectionHelp = "يقرأ التسمية التي يحملها المستند بالفعل، لتتمكن الخطوات اللاحقة من التصرف بناءً عليها." + [portal.policies.config.retention] summary = "يفرض مدة الاحتفاظ بالمستندات، ومتى تُؤرشف، ومتى تُحذف." @@ -7438,6 +8051,7 @@ title = "لا يوجد نشاط بعد" [portal.policies.endpoints] addWatermark = "علامة مائية" autoRedact = "تنقيح PII" +classifyAndLabel = "تصنيف" compressPdf = "ضغط" flatten = "تسطيح" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "تم حفظ سياساتك وستظهر بمجرد استعادة retry = "إعادة المحاولة" title = "الخلفية غير متاحة" +[portal.policies.operations] +change = "تغيير ما تفعله هذه الخطوة" +noResults = "لا توجد خطوة مطابقة. جرّب اسم منتج، أو \"scan\" أو \"notify\" أو \"attach\"." +searchPlaceholder = "ابحث عن الخطوات - جرّب \"scan\" أو \"attach\" أو اسم منتج" + +[portal.policies.operations.bodyMode] +binary = "بايتات الملف الخام" +json = "داخل حمولة JSON" +multipart = "كرَفع ملف (multipart)" + +[portal.policies.operations.clamavScan] +description = "يفحص خادم ClamAV الخاص بك المستند؛ لا يغادر شيء شبكتك." +label = "فحص الفيروسات (مستضاف ذاتيًا)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "يرفض المستندات التي تحتوي على وحدات ماكرو أو ملفات تنفيذية أو نصوص برمجية أو كائنات مضمنة." +label = "حظر المحتوى النشط" + +[portal.policies.operations.cloudmersiveScan] +description = "يفحص Cloudmersive المستند وتتوقف العملية إذا لم يكن نظيفًا." +label = "فحص الفيروسات" + +[portal.policies.operations.confluenceAttach] +description = "يُودع المستند المعالج في صفحة." +label = "إرفاق بصفحة Confluence" + +[portal.policies.operations.consignoSubmit] +description = "يرسل المستند إلى ConsignO Cloud للتوقيع." +label = "إرسال للتوقيع (ConsignO)" +note = "يرسل المستند. استرداد النسخة الموقعة غير مدعوم بعد، لأن الخطوة لا يمكنها تمرير نتيجة إلى خطوة لاحقة." + +[portal.policies.operations.customApiCall] +description = "أنشئ الاستدعاء بنفسك: المسار والطريقة والجسم ومعالجة الاستجابة." +label = "استدعاء API مخصص" + +[portal.policies.operations.discordNotify] +description = "يخبر قناة بأن السياسة عالجت مستندًا." +label = "نشر رسالة إلى Discord" + +[portal.policies.operations.elasticIndex] +description = "يسجل ما فعلته السياسة كمستند قابل للبحث." +label = "فهرسة حدث تدقيق في Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "يناسب Multipart واجهات API الخاصة بالرفع؛ ويناسب JSON واجهات API التي تريد الملف داخل حمولة؛ ويرسل binary البايتات الخام." +label = "كيفية إرسال المستند" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "جسم JSON يُرسل كما هو. يحمل {{document.base64}} الملف؛ ويتم ملء {{document.*}} و{{run.*}} لكل مستند." +label = "جسم JSON" + +[portal.policies.operations.fields.connection] +label = "الحساب" + +[portal.policies.operations.fields.domain] +label = "نطاق Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "حقل النموذج الذي تتوقع API وجود المستند تحته." +label = "اسم حقل الملف" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "من" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "كائن JSON من أزواج اسم/قيمة للترويسات. تتم إضافة بيانات اعتماد الاتصال نيابةً عنك." +label = "الترويسات" + +[portal.policies.operations.fields.index] +label = "الفهرس" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "مفتاح المشكلة" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "يُرسل كنص الرسالة. يتم ملء {{document.*}} و{{run.*}} لكل مستند." +label = "الرسالة" + +[portal.policies.operations.fields.method] +label = "الطريقة" + +[portal.policies.operations.fields.pageId] +label = "معرّف الصفحة" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "يُضاف إلى URL الأساسي للاتصال. لا يمكنه مطلقًا الوصول إلى مضيف آخر." +label = "المسار" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "المكان في محرك الأقراص لكتابته. يتم ملء {{document.filename}} لكل مستند." +label = "مسار الوجهة" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "يترك التقرير المستند دون تغيير. أما الاستبدال فيبدله بما يعود في الرد." +label = "ما الذي يجب فعله بالرد" + +[portal.policies.operations.fields.signerEmail] +label = "البريد الإلكتروني للموقّع" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "الموضوع" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "يحلل Presidio هذا النص. استخدم {{document.title}} أو الصق النص الذي تريد فحصه." +label = "النص المطلوب فحصه" + +[portal.policies.operations.fields.to] +label = "إلى" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "اسم مستخدم Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "يخبر مساحة بأن السياسة عالجت مستندًا." +label = "نشر رسالة إلى Google Chat" + +[portal.policies.operations.jiraAttach] +description = "يُودع المستند المعالج في مشكلة." +label = "إرفاق بمشكلة Jira" + +[portal.policies.operations.mailgunEmail] +description = "يرسل المستند المعالج كمرفق." +label = "إرسال المستند بالبريد الإلكتروني (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "يكتب المستند المعالج في مجلد." +label = "رفع إلى Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "يبلغ خادم Presidio الخاص بك عن البيانات الشخصية التي يجدها." +label = "اكتشاف البيانات الشخصية" +note = "يقرأ Presidio النصوص، وليس الملفات. أعطه النص الذي تريد فحصه — لا يتم رفع المستند نفسه." + +[portal.policies.operations.responseMode] +replace = "استبدال المستند بالرد" +report = "الاحتفاظ بالمستند وتسجيل الرد" + +[portal.policies.operations.sendgridEmail] +description = "يرسل المستند المعالج كمرفق." +label = "إرسال المستند بالبريد الإلكتروني (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "يخبر قناة بأن السياسة عالجت مستندًا." +label = "نشر رسالة إلى Slack" + +[portal.policies.operations.splunkEvent] +description = "يسجل ما فعلته السياسة حيث يبحث المدققون لديك بالفعل." +label = "إرسال حدث تدقيق إلى Splunk" + +[portal.policies.operations.sumologicEvent] +description = "يسجل ما فعلته السياسة إلى مجمّع HTTP." +label = "إرسال حدث تدقيق إلى Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "يخبر قناة بأن السياسة عالجت مستندًا." +label = "نشر رسالة إلى Teams" + +[portal.policies.operations.webhookPost] +description = "يرسل المستند نفسه إلى URL تختاره." +label = "نشر المستند إلى webhook" + +[portal.policies.operations.zapierNotify] +description = "يمرر تفاصيل التشغيل إلى أتمتتك." +label = "تشغيل سيناريو Zap أو Make" + [portal.policies.stats] activeFor = "نشطة" dataProcessed = "البيانات المعالجة" @@ -7473,6 +8282,13 @@ label = "المستندات المطبقة" description = "مكوّنة لكنها لا تعمل" label = "متوقفة مؤقتًا" +[portal.policies.table] +appliesTo = "ينطبق على" +docs = "المستندات المطبقة" +enforces = "يفرض" +policy = "السياسة" +status = "الحالة" + [portal.policies.wizard.actions] back = "رجوع" cancel = "إلغاء" @@ -7480,6 +8296,10 @@ continue = "متابعة" enablePolicy = "تفعيل السياسة" saveChanges = "حفظ التغييرات" +[portal.policies.wizard.capability.classify] +desc = "يحدد نوع المستند من تسميات فريقك ويوسمه، بحيث تتم أرشفته والبحث عنه حسب الفئة." +label = "تصنيف المستند" + [portal.policies.wizard.capability.compress] desc = "يضغط المستند إلى حجم ملف أصغر." label = "تقليل حجم الملف" @@ -7500,10 +8320,18 @@ label = "تنقيح المعلومات الحساسة" desc = "يزيل JavaScript المخفي حتى لا يتم تشغيل أي شيء تلقائيًا عند فتح المستند." label = "إزالة المحتوى النشط" +[portal.policies.wizard.capability.timestampPdf] +desc = "يثبت أن المستند كان موجودًا بهذا الشكل الدقيق في وقت معين، باستخدام جهة ختم زمني مستقلة. يتم إرسال hash فقط - ولا يغادر المستند خادمك أبدًا." +label = "إضافة ختم زمني موثوق" + [portal.policies.wizard.capability.watermark] desc = "يضع علامة مرئية (مثل “سري”) على كل صفحة." label = "تطبيق علامة مائية" +[portal.policies.wizard.classification] +description = "يتم تصنيف كل مستند مرفوع وفق التسميات المضمنة ووسمه بالأنواع المناسبة. تتم مشاركة مجموعة التسميات عبر فريقك بالكامل." +labelsHeading = "تسميات التصنيف" + [portal.policies.wizard.errors] noTools = "فعّل أداة واحدة على الأقل في سير العمل أولًا." saveFailed = "تعذّر حفظ السياسة. يُرجى المحاولة مرة أخرى." @@ -7548,73 +8376,118 @@ setUp = "إعداد سياسة {{category}}" [portal.policies.wizard.workflow] description = "اختر ما تفعله هذه السياسة بكل مستند تعالجه." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} نشطة" -noRule = "لم يتم تطبيق أي قاعدة بعد" -subtitle = "أتمتة دائمة يمر عبرها كل مستند، بغض النظر عن مسار المعالجة الذي يتولاه." -title = "ما يعمل على ملفات PDF الخاصة بك" - [portal.policySummary.action] -comingSoon = "الترقية إلى Enterprise" -configure = "تكوين" setUp = "إعداد" -[portal.policySummary.column] -activeRule = "القاعدة النشطة" -policy = "السياسة" -status = "الحالة" +[portal.processorFlow] +footnote = "الأعداد محسوبة خلال آخر 24 ساعة. سرعة التدفق توضيحية." +liveBadge = "مباشر" +stats = "{{connected}} متصل · تمت معالجة {{processed}} ملفات PDF" +title = "معالج PDF" -[portal.policySummary.empty] -description = "بمجرد تكوين السياسات، ستظهر الفئات هنا." -title = "لا توجد سياسات بعد" +[portal.processorFlow.lens] +ariaLabel = "وضع العرض" +flow = "التدفق" +sankey = "Sankey" -[portal.policySummary.state] -active = "نشطة" -off = "متوقفة" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 ساعة" +failed = "فشل" +heading = "النتائج" +success = "تم التسليم" + +[portal.processorFlow.policies] +activeCount = "{{n}} نشط" +count = "{{n}} · 24 ساعة" +heading = "السياسات" +setUp = "إعداد" soon = "قريبًا" -[portal.processingStatus] -managePlan = "إدارة الخطة" -volumeSuffix = "ملفات PDF تمت معالجتها · آخر 30 يومًا" +[portal.processorFlow.sankey] +waist = "السياسات · {{n}} نشط" + +[portal.processorFlow.sankey.empty] +description = "اتصل بمصدر وشغّل سياسة لمشاهدة تدفق المستندات عبر المعالج." +title = "لا يوجد تدفق بعد" + +[portal.processorFlow.sources] +comingSoonTag = "اتصال" +editor = "محرر Stirling PDF" +heading = "المصادر" +perDay = "{{n}} / 24 ساعة" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "استقبال البريد الإلكتروني" [portal.procurement] -reset = "إعادة ضبط المشتريات (تجريبي)" subtitle = "احصل على تقييم فريقك، والتعاقد معه، وتهيئته. كل مستند في مكان واحد." title = "المشتريات" -[portal.procurement.action] -download = "تنزيل" -pay = "ادفع الآن" -request = "طلب" -sign = "مراجعة وتوقيع" -upload = "رفع" - [portal.procurement.agreement] agreeCta = "الموافقة والاشتراك" +confidential = "سري" confirm = "لقد قرأت ووافقت على اتفاقية Stirling Enterprise." -eyebrow = "الاتفاقية" -intro = "تغطي اتفاقية واحدة مدمجة صفقتك: اتفاقية الخدمة الرئيسية، ونموذج الطلب، وEULA، واتفاقية معالجة البيانات. راجعها، ثم وافق لقبول عرض السعر ضمن اشتراك ملتزم." -title = "راجع اتفاقية المؤسسة الخاصة بك" +docName = "اتفاقية Stirling Enterprise" +docSub = "MSA ونموذج الطلب وEULA وDPA، مدمجة في توقيع واحد." +download = "تنزيل" +downloadDraftError = "تعذر إنشاء PDF الاتفاقية - عارض المستندات غير متاح. يُرجى المحاولة قريبًا أو الاتصال بالدعم." +downloadError = "تعذر تنزيل الاتفاقية الموقعة. يُرجى المحاولة مرة أخرى." +legalName = "اسم الكيان القانوني" +legalNamePlaceholder = "الكيان القانوني الذي سيوقّع" +loadError = "تعذر تحميل الاتفاقية. يُرجى المحاولة مرة أخرى." +loading = "جارٍ تحميل الاتفاقية..." +ref = "مرجع {{ref}} · {{version}}" +requestChanges = "طلب تغييرات" +scrollHint = "مرّر للمراجعة" +signatory = "اسم الموقّع" +signatoryPlaceholder = "الاسم الكامل" +signatoryTitle = "المسمى الوظيفي للموقّع" +signatoryTitlePlaceholder = "مثلًا: المستشار القانوني العام" +signError = "تعذر تسجيل توقيعك. يُرجى المحاولة مرة أخرى." [portal.procurement.builder] addons = "الإضافات" +addressLine1 = "سطر العنوان 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "سطر العنوان 2" +addressLine2Placeholder = "الجناح، الطابق، المبنى" back = "رجوع" businessName = "اسم النشاط التجاري" businessNamePlaceholder = "شركتك" +city = "المدينة" +cityPlaceholder = "San Francisco" +completeRequired = "يرجى إكمال الحقول المطلوبة (المميزة بـ *) باستخدام بريد إلكتروني صالح قبل إنشاء عرض السعر." +contactEmail = "البريد الإلكتروني لجهة الاتصال" +contactEmailPlaceholder = "jane@acme.com" +contactName = "اسم جهة الاتصال" +contactNamePlaceholder = "Jane Doe" continue = "متابعة" -country = "الدولة" -countryEuro = "منطقة اليورو (EUR €)" -countryUK = "المملكة المتحدة (GBP £)" -countryUS = "الولايات المتحدة (USD $)" +done = "تم" eula = "لقد قرأت ووافقت على Stirling Enterprise EULA. وهي تحكم الاتفاقية التي يتم إنشاؤها من عرض السعر هذا." generate = "إنشاء عرض سعر" -included = "مضمن" indemnification = "تعويض IP" indemnificationSub = "ندافع عن مطالبات IP المؤهلة، وفقًا لـ EULA" -offlineLicense = "ترخيص Offline / air-gapped" -offlineLicenseSub = "ملف ترخيص قابل للتنزيل لمثيل self-hosted معزول air-gapped" +paperEyebrow = "عرض سعر للمؤسسات" +paperFor = "مُعدّ لـ" +pdfSize = "حجم PDF" +poNumber = "رقم أمر الشراء" +poNumberPlaceholder = "اختياري" +postalCode = "الرمز البريدي" +postalCodePlaceholder = "94105" +posture = "الحوكمة" +posture_count = "~{{count}} سياسات" +posture_essentials = "الأساسيات" +posture_essentialsSub = "التصنيف + المشاركة — الإعدادات الافتراضية" +posture_governed = "محكوم" +posture_governedSub = "يضيف الأمان والتوجيه" +posture_regulated = "منظم" +posture_regulatedSub = "كل فئة — الامتثال، الاحتفاظ، الإدخال" qbr = "مراجعات أعمال ربع سنوية" qbrSub = "يراجع SE الخاص بك الاستخدام وخريطة الطريق كل ربع سنة" +region = "الولاية / المنطقة" +regionPlaceholder = "California" running = "{{annual}} / سنة · {{years}}-سنة {{tcv}}" s1Sub = "فريقك وملفات PDF التي تتوقع تشغيلها كل عام." s1Title = "كم ستعالج؟" @@ -7623,6 +8496,12 @@ s2Title = "الالتزام والخدمة" s3Sub = "لعرض السعر والاتفاقية التي ينشئها." s3Title = "تفاصيلك" serviceLevel = "مستوى الخدمة" +size_compact = "مضغوط" +size_compactSub = "نص في الغالب، أقل من 1 MB" +size_heavy = "ثقيل" +size_heavySub = "ممسوح ضوئيًا أو كثيف الصور، 5 MB+" +size_standard = "قياسي" +size_standardSub = "نصوص وصور مختلطة، من 1 إلى 5 MB" slDedicated = "مخصص" slDedicatedSub = "4 ساعات عمل · مدير حساب مخصص · +30%" slPriority = "أولوية" @@ -7630,6 +8509,8 @@ slPrioritySub = "نفس يوم العمل · CSM مُسمّى · +15%" slStandard = "قياسي" slStandardSub = "يوم العمل التالي · CSM مشترك · مضمن" stepOf = "الخطوة {{n}} من {{total}}" +taxId = "VAT / المعرّف الضريبي" +taxIdPlaceholder = "اختياري" term = "المدة" termDiscount = "تم تطبيق خصم {{pct}}% لالتزام متعدد السنوات" title = "أنشئ عرض السعر الخاص بك" @@ -7637,6 +8518,7 @@ training = "التهيئة والتدريب" trainingSub = "جلسات مباشرة لتشغيل فريقك" users = "إجمالي المستخدمين" usersPlaceholder = "مثال: 250" +viewEula = "اقرأها" volEstimated = "مقدّر من {{count}} مستخدمين (~2,000 ملف PDF لكل منهم، بما في ذلك الأتمتة). عدّل إذا كنت تعرف رقمًا أدق." volManual = "باستخدام الرقم الذي أدخلته. أعد التقدير من حجم فريقك في أي وقت." volNoUsers = "لست متأكدًا؟ أدخل حجم فريقك وسنقدّره." @@ -7645,55 +8527,55 @@ volumePlaceholder = "مثال: 1,000,000" years_one = "{{count}} سنة" years_other = "{{count}} سنوات" -[portal.procurement.docs] -count_one = "{{count}} مستند" -count_other = "{{count}} مستندات" -done = "تم" -here = "أنت هنا" -hide = "إخفاء" -optional = "اختياري" -paidAddon = "إضافة مدفوعة" -show = "إظهار" -subtitle = "كل ما تحتاجه في كل خطوة من الرحلة، يظهر مع تقدم الصفقة خلالها." -supportingSubtitle = "SOC 2، ومراجعات الأمان، ونماذج الضرائب والمزيد، جاهزة عندما يطلبها فريق الأمان أو المشتريات لديك. بعضها يتضمن رسومًا لمرة واحدة." -supportingTitle = "دعم تقييمك" +[portal.procurement.documents] +agreement = "اتفاقية المؤسسات" +agreementSub = "MSA ونموذج الطلب وDPA في توقيع واحد." +download = "تنزيل" +eula = "EULA والشروط التجارية" +eulaSub = "البرنامج والشروط التجارية للخدمة الذاتية." +invoice = "الفاتورة" +invoiceSub = "فاتورة اشتراكك الأولى." +laterInvoice = "متاح بعد قبولك" +laterQuote = "متاح بمجرد إصدار عرض السعر الخاص بك" +quote = "عرض السعر" +quoteSub = "عرض السعر المفصل للمؤسسات الخاص بك." +sla = "ملحق SLA" +slaSub = "مستويات الدعم وأهداف الاستجابة." +subprocessors = "المعالِجون الفرعيون" +subprocessorsSub = "جهات خارجية تعالج البيانات نيابةً عنك." +subtitle = "مستندات صفقتك، متاحة في أي وقت." title = "المستندات" -upcoming = "قادم" +view = "عرض" [portal.procurement.error] title = "حدث خطأ ما" [portal.procurement.hero] -company = "صفقة المؤسسة الخاصة بك" +barAria = "المرحلة {{current}} من {{total}}" ctaAgreement = "مراجعة الاتفاقية وتوقيعها" -ctaLive = "أنت جاهز للعمل" -ctaPayment = "إضافة الدفع" +ctaExploring = "إعداد نسختك التجريبية" ctaQuote = "مراجعة عرض السعر" +ctaReviewQuote = "مراجعة عرض السعر" ctaTrial = "أنشئ عرض السعر" +documents = "المستندات" eyebrow = "مشتريات Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "دعوة زملاء الفريق" -keyDocs = "المستندات الرئيسية" -nextStep = "الخطوة التالية: {{action}}" -notStarted = "لم يبدأ" +licenseKey = "مفتاح الترخيص" +liveSub = "تم توفير مؤسستك وترخيصك نشط." +liveTitle = "أنت الآن مباشر على Stirling Enterprise" +next = "التالي: {{stage}}" open = "فتح المشتريات" scheduleCall = "جدولة مكالمة" -setup1Sub = "ادعُ زملاء فريقك" -setup1Title = "نشر محرر PDF" -setup2Sub = "تشغيل المعالجة عبر المحررين والمصادر الأخرى" -setup2Title = "توصيل معالج PDF" -setup3Sub = "فعّل الأمان أو الامتثال أو التوجيه أو الاحتفاظ عند الحاجة إليها" -setup3Title = "إضافة السياسات الموصى بها" +sentenceAgreement = "مراجعة الاتفاقية وتوقيعها" +sentenceLive = "التوفير والطرح" +sentencePayment = "أمر الشراء والدفع" +sentenceQuote = "مراجعة عرض السعر الخاص بك وقبوله" +sentenceTrial = "إعداد مساحة العمل التجريبية الخاصة بك" [portal.procurement.journey] daysLeft_one = "باقي {{count}} يوم" daysLeft_other = "باقي {{count}} أيام" -engineerLabel = "مهندس الحلول الخاص بك" -eyebrow = "إطلاقك" -live = "أنت جاهز للعمل على Stirling Enterprise" -nextStep = "الخطوة التالية: {{action}}" -subtitle = "مهندس الحلول الخاص بك معك في كل خطوة. إجراء تالٍ واحد في كل مرة؛ وقائمة التحقق الكاملة أدناه." -title = "من التجربة إلى التشغيل، مسار موجه واحد" -trialTitle = "تجربة Enterprise" [portal.procurement.journeySteps.agreement] blurb = "توقيع واحد يغطي MSA، ونموذج الطلب، وEULA، وDPA." @@ -7727,6 +8609,9 @@ downloadError = "تعذّر إنشاء ملف الترخيص Offline الآن downloadOffline = "تنزيل ترخيص Offline (.lic)" hint = "الصق هذا المفتاح في مثيل self-hosted لتفعيله، أو احتفظ به لسجلاتك. حافظ عليه آمنًا." label = "مفتاح الترخيص الخاص بك" +subtitle = "فعّل نسخة مستضافة ذاتيًا باستخدام هذا المفتاح، أو احتفظ به في سجلاتك." +title = "مفتاح الترخيص الخاص بك" +trialFileHint = "هذا ملف الترخيص التجريبي الخاص بك. بمجرد اكتمال اتفاقيتك، عُد ونزّله مرة أخرى — فعلى عكس مفتاح الترخيص عبر الإنترنت، لن يتم تحديث ملف .lic من تلقاء نفسه." [portal.procurement.link] cta = "ربط الحساب" @@ -7739,65 +8624,62 @@ description = "اشتراكك نشط وتم إصدار ترخيصك. تم توف eyebrow = "مباشر" title = "أنت جاهز للعمل على Stirling Enterprise" -[portal.procurement.locked] -description = "مفاتيح التجربة، وعروض الأسعار للحجم الملتزم، والاتفاقية ذات التوقيع الواحد، والدفع، وسجل مستنداتك كلها هنا بمجرد بدء تقييم Enterprise." -eyebrow = "Enterprise فقط" -talkToSales = "تحدث إلى المبيعات" -title = "يفتح مسار المشتريات مع Enterprise" - [portal.procurement.milestone] -accept = "قبول ومتابعة" -description = "نزّل PDF لمشاركته مع فريقك، وعد لقبوله عندما تكون جاهزًا، أو أجرِ تغييرات." -download = "تنزيل PDF" downloadError = "تعذّر تنزيل عرض السعر PDF الآن — يُرجى المحاولة مرة أخرى بعد قليل." -edit = "تعديل عرض السعر" -eyebrow = "عرض السعر {{number}}" -perYear = " / سنة" -preparedFor = "مُعدّ لـ {{company}}" -tcv = "{{value}} إجمالي قيمة العقد" -title = "عرض السعر الخاص بك جاهز" - -[portal.procurement.modal] -cancel = "إلغاء" -chooseFile = "اختيار ملف" -close = "إغلاق" -downloadBody = "سيبدأ التنزيل بعد قليل." -downloadCta = "تنزيل" -downloadTitle = "تنزيل" -noFile = "لم يتم تحديد ملف" -payBody = "ادفع عقدك الملتزم بالبطاقة أو التحويل البنكي عبر Stripe. يتم توفير مساحة العمل الخاصة بك بمجرد تسوية الدفع." -payCta = "المتابعة إلى Stripe" -payTitle = "تأكيد الدفع" -requestBodyFree = "سننشئ هذا عند الطلب. أكّد وسيقوم مهندس الحلول الخاص بك بإرساله قريبًا." -requestBodyPaid = "هذه إضافة مدفوعة. أكّد وسيحدد مهندس الحلول الخاص بك نطاقها ويرسل المستندات." -requestCta = "طلب" -requestTitle = "طلب هذا المستند" -signBody = "يفتح اتفاقية Stirling Enterprise للتوقيع الإلكتروني: توقيع واحد يغطي MSA، ونموذج الطلب، وEULA، وDPA. نوقّع بالمقابل تلقائيًا وتنتقل إلى الدفع." -signCta = "فتح للتوقيع" -signTitle = "راجع اتفاقيتك ووقّعها" -uploadBody = "أرسل لنا PO الخاص بك وسنصدر فاتورة مقابله وفق شروطك. اسحب PDF أو اختر ملفًا أدناه." -uploadCta = "رفع أمر الشراء" -uploadTitle = "رفع أمر الشراء الخاص بك" [portal.procurement.payment] description = "تم قبول عرض السعر الخاص بك وترخيصك نشط بالفعل — يمكن لفريقك البدء فورًا. ادفع الفاتورة الأولى عندما تكون جاهزًا؛ يمكنك الدفع أو تنزيلها هنا، دون الحاجة إلى بريد إلكتروني." +downloadAgreement = "تنزيل الاتفاقية الموقعة" downloadInvoice = "تنزيل الفاتورة" -simulate = "محاكاة استلام الدفع (تجريبي)" +eyebrow = "الدفع" title = "تم إنشاء الاشتراك" viewInvoice = "عرض الفاتورة ودفعها" +[portal.procurement.review] +acceptCta = "قبول عرض السعر" +annual = "الرسوم السنوية (السنة 1)" +downloadCta = "تنزيل عرض السعر" +poNumber = "أمر الشراء: {{po}}" +renewal = "يتجدد بقيمة {{amount}}/سنة بعد المدة (+{{pct}}% CPI)" +tcv = "مدة {{years}} سنوات، مدفوعة بالكامل مقدمًا: {{tcv}}" +validUntil = "صالح حتى {{date}}" + [portal.procurement.schedule] fallback = "تعذّر تحميل المجدول." fallbackLink = "فتح الجدولة في تبويب جديد" +loading = "جارٍ تحميل المجدول..." subtitle = "سيرشد مهندس الحلول الخاص بك فريقك خلال الإطلاق. اختر وقتًا يناسبك." title = "جدولة مكالمة" -[portal.procurement.status] -action = "إجراء مطلوب" -available = "متاح" -complete = "مكتمل" -pending = "معلّق" -request = "عند الطلب" +[portal.procurement.setup] +airgap = "معزول شبكيًا" +airgapSub = "مغلق، دون اتصال صادر" +back = "رجوع" +businessName = "اسم النشاط التجاري" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "مُدار، أسرع بداية" +continue = "متابعة" +deployment = "أين يجب نشره؟" +eula = "أوافق على Stirling EULA والشروط التجارية." +fullName = "الاسم الكامل" +fullNamePlaceholder = "اسمك" +invites = "دعوة زملاء الفريق · اختياري" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "جدولة مكالمة" +seats = "الأشخاص الذين يختبرون" +seatsPlaceholder = "مثلًا: 15" +selfhost = "مستضاف ذاتيًا" +selfhostSub = "Docker أو Kubernetes" +start = "بدء التجربة" +stepOf = "الخطوة {{n}} من {{total}}" +subtitle = "مجاني لمدة 14 يومًا، دون الحاجة إلى بطاقة." +subtitleDetails = "بعض التفاصيل لعرض السعر والاتفاقية الخاصة بك." +talkFirst = "تفضل التحدث أولًا؟" +title = "إعداد نسختك التجريبية" +viewEula = "اقرأها" +workEmail = "البريد الإلكتروني للعمل" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "يضيف التمديد 7 أيام ويُخطر مهندس الحلول الخاص بك." @@ -7808,109 +8690,113 @@ maxed = "بلغ الحد الأقصى" subtitle = "تستمر تجربتك المجانية حتى {{date}}. لا حاجة إلى بطاقة." title = "تجربة Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "تسعير حجم ملتزم، وSSO + SCIM + RBAC على مستوى المؤسسة، وتدقيق غير قابل للتغيير لمدة 90 يومًا، وSE مخصص." -homeCta = "بدء التجربة →" -homeHeadline = "عالج ملايين ملفات PDF." - -[portal.recentActivity] -title = "النشاط الأخير" -viewAll = "عرض الكل" - -[portal.recentActivity.empty] -description = "ستظهر هنا عمليات تشغيل المسارات وعمليات النشر وأحداث الوكلاء." -title = "لا شيء هنا بعد" - -[portal.search] -ariaLabel = "بحث" -placeholder = "ابحث في Stirling — endpoints، المسارات، المستندات…" - -[portal.search.empty] -noActionsDescription = "ستظهر الإجراءات السريعة هنا بمجرد توفرها." -noActionsTitle = "لا توجد إجراءات سريعة" -noMatches = "لا توجد نتائج لـ \"{{query}}\"" -noMatchesDescription = "جرّب كلمة مفتاحية مختلفة أو تصفح الفهرس." - [portal.settings.groups] admin = "المسؤول" [portal.settings.sections] account-link = "ربط الحساب" -[portal.shell.header] -accountFallback = "الحساب" -accountMenu = "قائمة الحساب" -darkMode = "الوضع الداكن" -lightMode = "الوضع الفاتح" -search = "بحث" -searchPlaceholder = "بحث…" -signOut = "تسجيل الخروج" -switchToDark = "التبديل إلى السمة الداكنة" -switchToLight = "التبديل إلى السمة الفاتحة" - [portal.shell.sidebar] appEditor = "المحرر" appProcessor = "المعالج" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} مستند" linkAccount = "ربط حساب Stirling" -planEditor = "خطة المحرر" -planEnterprise = "خطة Enterprise" -planProcessor = "خطة المعالج" primaryNav = "التنقل الرئيسي" switchApp = "تبديل التطبيق" +[portal.shell.topbar] +closeNav = "إغلاق التنقل" +openNav = "فتح التنقل" +search = "بحث" + [portal.sources] subtitle = "اتصالات إدخال قابلة لإعادة الاستخدام تغذي المستندات إلى Stirling. كوّن الاتصال مرة واحدة، ثم أشر إليه من أي عدد من السياسات. انقر على صف لمعرفة تكوينه والسياسات التي تستخدمه." title = "المصادر" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "توصيل مصدر" +[portal.sources.builder] +back = "العودة إلى المصادر" +backToTypes = "كل أنواع المصادر" +cancel = "إلغاء" +chooseHint = "اختر من أين تأتي المستندات. الموصلات باللون الرمادي قادمة قريبًا." +comingSoon = "قريبًا" +comingSoonHeading = "قريبًا" +create = "إنشاء مصدر" +createTitle = "اتصال بمصدر" +delete = "حذف" +editTitle = "تعديل المصدر" +enabled = "مفعل" +save = "حفظ التغييرات" + +[portal.sources.builder.folderAccess] +description = "لا يمكن لأتمتة المجلدات استخدام إلا المجلدات التي سمح بها مسؤول. أضفه ضمن إعدادات الوصول إلى المجلد، ثم حاول مرة أخرى." +openSettings = "إعدادات الوصول إلى المجلد" +title = "هذا المجلد غير مسموح به" + [portal.sources.delete] body = "حذف \"{{name}}\"؟ لا يمكن التراجع عن ذلك. يجب تحديث السياسات التي تشير إليه." cancel = "إلغاء" confirm = "حذف" title = "حذف المصدر؟" -[portal.sources.detail] -closeAriaLabel = "إغلاق التفاصيل" -delete = "حذف المصدر" -docs24h = "آخر 24 ساعة" -docs30d = "آخر 30 يومًا" -docsTotal = "الإجمالي المشاهَد" -docsTrend = "المستندات خلال آخر 30 يومًا" -documents = "المستندات" -edit = "تعديل" -notReferenced = "لا تتم الإشارة إليه من أي سياسة، لذا من الآمن حذفه." -pause = "إيقاف مؤقت" -resume = "استئناف" -subtitle = "{{type}} · {{status}}" -usedBy = "مستخدم بواسطة" - -[portal.sources.empty] -description = "وصّل مجلدًا (وقريبًا، التخزين السحابي) حتى يكون لدى سياساتك مكان تسحب منه المستندات." -title = "لا توجد مصادر متصلة بعد" - [portal.sources.kpi] inUse = "قيد الاستخدام" total = "الاتصالات" unused = "غير مستخدم" +[portal.sources.networkFields.connection] +helperText = "الاتصال المخزن مع عنوان الخادم وبيانات الاعتماد. يُعاد استخدامه بواسطة كل مصدر يشير إليه." +label = "الاتصال" + +[portal.sources.networkFields.directory] +helperText = "مجلد على الخادم للاستطلاع، نسبةً إلى مجلد تسجيل الدخول الرئيسي أو جذر المشاركة. اتركه فارغًا للجذر." +label = "المجلد" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "يزيل وضع الاستهلاك كل ملف من الخادم بمجرد أن تعالجه كل سياسة." +label = "وضع القراءة" + +[portal.sources.networkFields.mode.options] +consume = "استهلاك: معالجة كل ملف مرة واحدة" +snapshot = "لقطة: إعادة قراءة المجلد في كل تشغيل" + +[portal.sources.networkFields.recursive] +label = "عمق المجلد" + +[portal.sources.networkFields.recursive.options] +all = "تضمين المجلدات الفرعية" +top = "المستوى العلوي فقط" + [portal.sources.status] active = "نشط" disabled = "معطل" unused = "غير مستخدم" [portal.sources.table] +documents = "المستندات" source = "المصدر" status = "الحالة" +type = "Type" usedBy = "السياسات" +[portal.sources.types.box] +description = "اسحب المستندات من مجلدات Box التي يشاركها فريقك بالفعل." +label = "Box" + +[portal.sources.types.dropbox] +description = "اسحب المستندات من مجلد Dropbox مشترك." +label = "Dropbox" + [portal.sources.types.editor] +description = "المستندات التي عالجها فريقك في المحرر، عبر تشغيلات السياسات وAI." label = "المحرر" +noPolicies = "لم يتم تشغيل أي سياسات من المحرر بعد." + +[portal.sources.types.email] +description = "حوّل المرفقات المرسلة إلى صندوق بريد مخصص إلى مستندات." +label = "صندوق وارد البريد الإلكتروني" [portal.sources.types.folder] description = "مراقبة دليل على الخادم للمستندات الجديدة." @@ -7943,25 +8829,80 @@ label = "عمق المجلد" all = "تضمين المجلدات الفرعية" top = "المستوى الأعلى فقط" +[portal.sources.types.ftp] +description = "استطلع مجلد خادم FTP أو FTPS بحثًا عن مستندات جديدة." +label = "FTP" + +[portal.sources.types.googledrive] +description = "اسحب المستندات من مجلدات Google Drive المشتركة." +label = "Google Drive" + +[portal.sources.types.network] +description = "راقب مشاركة شبكة SMB بحثًا عن مستندات جديدة." +label = "محرك أقراص الشبكة" + +[portal.sources.types.onedrive] +description = "راقب مجلدات OneDrive بحثًا عن مستندات جديدة." +label = "OneDrive" + +[portal.sources.types.s3] +description = "اسحب المستندات من حاوية Amazon S3 أو حاوية متوافقة مع S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "الاتصال المخزن الذي يحتوي على الحاوية وبيانات الاعتماد. يُعاد استخدامه بواسطة كل مصدر ومخرج خط معالجة يشير إليه." +label = "الاتصال" + +[portal.sources.types.s3.fields.mode] +helperText = "يزيل وضع الاستهلاك كل كائن من الحاوية بمجرد أن تعالجه كل سياسة." +label = "وضع القراءة" + +[portal.sources.types.s3.fields.mode.options] +consume = "استهلاك: معالجة كل كائن مرة واحدة" +snapshot = "لقطة: إعادة قراءة الحاوية في كل تشغيل" + +[portal.sources.types.s3.fields.prefix] +helperText = "تتم معالجة الكائنات التي تبدأ مفاتيحها بهذه البادئة فقط." +label = "بادئة المفتاح" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "استطلع مجلد إسقاط SFTP بحثًا عن مستندات جديدة." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "اسحب المستندات من مكتبات مستندات SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "المصدر" +[portal.sources.types.webhook] +createNote = "يتم إنشاء URL للتسليم وسر توقيع عند إنشاء هذا webhook. يرسل المرسلون المستندات باستخدام POST إلى ذلك URL وتعمل السياسات المرجعية عند الوصول." +description = "استلم المستندات عبر HTTP POST موقّع من أي نظام خارجي." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL التسليم" +secretNote = "يُعرض سر التوقيع مرة واحدة عند إنشاء webhook. أعد إنشاء المصدر لتغييره." + +[portal.sources.types.webhook.reveal] +copy = "نسخ" +done = "تم" +secret = "سر التوقيع" +secretHelp = "وقّع الجسم الخام لكل تسليم بهذا المفتاح (HMAC-SHA256) وأرسله كترويسة X-Stirling-Signature." +secretWarning = "انسخ سر التوقيع الآن. لأمانك، يُعرض مرة واحدة فقط ولا يمكن استرداده لاحقًا." +title = "تم إنشاء Webhook" +url = "URL التسليم" +usage = "أرسل كل مستند باستخدام POST كجسم الطلب الخام إلى URL التسليم مع نوع محتوى binary (application/pdf أو application/octet-stream). تعمل السياسات المرجعية تلقائيًا عند الوصول." + [portal.sources.wizard] -back = "رجوع" -cancel = "إلغاء" -continue = "متابعة" -editTitle = "تعديل المصدر" name = "الاسم" namePlaceholder = "مثال: استقبال المطالبات" -save = "حفظ التغييرات" -subtitle = "الخطوة {{current}} من {{total}} · {{label}}" -title = "توصيل مصدر" type = "النوع" -[portal.sources.wizard.steps] -chooseType = "اختيار النوع" -configure = "تكوين" -review = "مراجعة وتوصيل" +[portal.stepModal] +close = "إغلاق" [portal.usage] managePayment = "إدارة الدفع" @@ -8021,16 +8962,9 @@ summary = "يمتلك فريقًا — يدير موارد أعضائه والت 2 = "الوصول إلى البوابة عبر السياسة الافتراضية" 3 = "كل ما يمكن للعضو القيام به" -[portal.welcome] -ariaLabel = "مرحبًا بك في Stirling PDF" -badge = "Open-source" -installEditor = "تثبيت المحرر" -inviteTeammates = "دعوة زملاء الفريق" -openInBrowser = "فتح في المتصفح" -perks = "مجاني للأبد · قابل للاستضافة الذاتية" -subtitle = "محرر PDF الأكثر أمانًا في العالم مجاني للفرق بجميع أحجامها. يتضمن أكثر من 60 عملية PDF وSSO." -title = "مرحبًا بك في" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · غير محدود" [printFile] title = "طباعة ملف" @@ -8613,6 +9547,14 @@ text = "دوّر صفحات PDF بزيادات 90 درجة مع أو عكس عق [rotate.tooltip.header] title = "نظرة عامة على إعدادات التدوير" +[ruler] +clearAll = "مسح الكل" +hideAllLabels = "إخفاء كل التسميات" +hideSmallLabels = "إخفاء التسميات الصغيرة" +physicalValues = "فعلي" +scaled = "مقاس" +showAllLabels = "إظهار كل التسميات" + [sanitize] desc = "إزالة العناصر المحتملة الضرر من ملفات PDF." sanitizationResults = "نتائج التنقية" @@ -8654,31 +9596,45 @@ label = "إزالة بيانات XMP الوصفية" [sanitize.steps] settings = "إعدادات" -[sanitizePdf] -tags = "تنظيف,تأمين,آمن,إزالة التهديدات" - [sanitizePDF] title = "تنظيف PDF" +[sanitizePdf] +tags = "تنظيف,تأمين,آمن,إزالة التهديدات" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "ضبط مقياس الصفحة" +[scaleSettings] +activeScale = "المقياس النشط" +apply = "تطبيق المقياس" +applyCalibration = "تطبيق المعايرة" +calculatedScale = "المقياس المحسوب" +calibrate = "معايرة" +calibrating = "جارٍتتم المعايرة" +calibrationDistanceRequired = "أدخل مسافة فعلية أكبر من صفر" +calibrationInvalid = "تعذّر حساب المقياس من هذا القياس" +calibrationPaperDistance = "مسافة الصفحة المقاسة: {{distance}}" +calibrationTitle = "معايرة المقياس" +calibrationTooltip = "قس مسافة معروفة لحساب المقياس تلقائيًا" +customScale = "مقياس مخصص" +noneSet = "لم يتم تعيين مقياس مخصص" +presets = "المقاييس المعدّة مسبقًا" +ratio = "نسبة المقياس" +ratioHelp = "النسبة: 1 وحدة صفحة = X وحدات فعلية" +ratioPlaceholder = "مثال: 100" +ratioPositive = "يجب أن تكون النسبة أكبر من صفر" +ratioRequired = "النسبة مطلوبة" +realDistance = "المسافة الفعلية" +realDistancePlaceholder = "مثال: 5" +reset = "إعادة التعيين إلى الافتراضيات" +unit = "الوحدة" +unitInvalid = "وحدة غير صالحة: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "عتبة الزاوية:" -10 = "يضبط حجم الحدود المضافة والمزالة لمنع الحدود البيضاء في الإخراج (الافتراضي: 1)." -2 = "تعيين الحد الأدنى للزاوية المطلقة المطلوبة لتدوير الصورة (الافتراضي: 10)." -3 = "التسامح:" -4 = "يحدد نطاق تباين اللون حول لون الخلفية المقدر (الافتراضي: 30)." -5 = "أدنى مساحة:" -6 = "تعيين الحد الأدنى لمنطقة الصورة (الافتراضي: 10000)." -7 = "الحد الأدنى لمنطقة المحيط:" -8 = "تعيين الحد الأدنى لمنطقة المحيط للصورة" -9 = "حجم الحدود:" - [scannerImageSplit] submit = "استخراج صور المسح" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "ما الذي تفعله" whatThisDoesDesc = "يعثر تلقائيًا على كل صورة ويستخرجها من صفحة ممسوحة أو صورة مركبة—دون قص يدوي." whenToUse = "متى تُستخدم" +[ScannerImageSplit.selectText] +1 = "عتبة الزاوية:" +10 = "يضبط حجم الحدود المضافة والمزالة لمنع الحدود البيضاء في الإخراج (الافتراضي: 1)." +2 = "تعيين الحد الأدنى للزاوية المطلقة المطلوبة لتدوير الصورة (الافتراضي: 10)." +3 = "التسامح:" +4 = "يحدد نطاق تباين اللون حول لون الخلفية المقدر (الافتراضي: 30)." +5 = "أدنى مساحة:" +6 = "تعيين الحد الأدنى لمنطقة الصورة (الافتراضي: 10000)." +7 = "الحد الأدنى لمنطقة المحيط:" +8 = "تعيين الحد الأدنى لمنطقة المحيط للصورة" +9 = "حجم الحدود:" + [search] noResults = "لا توجد نتائج" placeholder = "أدخل مصطلح البحث..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "خادم Stirling-PDF الخاص بك غير متصل [session] expired = "لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى" -[sessionManagement.tooltip] -header = "إدارة جلسات التوقيع" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "يُضاف المشاركون الجدد إلى نهاية ترتيب التوقيع" -bullet2 = "لا يمكن إضافة مشاركين بعد إنهاء الجلسة" -bullet3 = "يتلقى كل مشارك إشعارًا عندما يحين دوره" -description = "يمكنك إضافة المزيد من المشاركين إلى جلسة نشطة في أي وقت قبل الإنهاء." -title = "إضافة مشاركين" - -[sessionManagement.tooltip.finalization] -bullet1 = "إنهاء كامل: وقّع جميع المشاركين" -bullet2 = "إنهاء جزئي: لم يوقّع بعض المشاركين بعد" -bullet3 = "سيتم استبعاد المشاركين غير الموقّعين من المستند النهائي" -bullet4 = "بعد الإنهاء، يمكنك تحميل ملف PDF الموقّع إلى الملفات النشطة" -description = "يجمع الإنهاء جميع التواقيع في ملف PDF واحد موقّع. لا يمكن التراجع عن هذا الإجراء." -title = "إنهاء الجلسة" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "لا يمكن إزالة المشاركين الذين قاموا بالتوقيع بالفعل" -bullet2 = "لن يتلقى المشاركون الذين تمت إزالتهم إشعارات بعد ذلك" -bullet3 = "يُعدل ترتيب التوقيع تلقائيًا" -description = "يمكن إزالة المشاركين من الجلسات قبل أن يوقّعوا." -title = "إزالة المشاركين" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "يُطبَّق كل توقيع بالتسلسل على ملف PDF" -bullet2 = "يمكن للموقّعين اللاحقين رؤية التواقيع السابقة" -bullet3 = "أمر حاسم لتدفقات الموافقات والسلاسل القانونية للحيازة" -description = "الترتيب الذي تحدده عند إنشاء الجلسة يحدد من يوقّع أولًا." -title = "ترتيب التوقيع" - [settings] close = "إغلاق" +[settings.ai] +documents = "المستندات و RAG" +general = "عام" +limits = "الحدود والأداء" +models = "النماذج والمزوّدون" +title = "الذكاء الاصطناعي" + [settings.configuration] advanced = "متقدم" database = "قاعدة البيانات" endpoints = "نقاط النهاية" features = "الميزات" +folderAccess = "الوصول إلى المجلد" mcp = "MCP Server" storageSharing = "تخزين الملفات والمشاركة" systemSettings = "إعدادات النظام" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "محرر PDF الافتراضي" defaultPdfEditorActive = "Stirling PDF هو محرر PDF الافتراضي لديك" defaultPdfEditorChecking = "جارٍ الفحص..." defaultPdfEditorInactive = "تطبيق آخر معيّن كافتراضي" +defaultPdfEditorRemind = "ذكّرني بتعيينه كافتراضي" +defaultPdfEditorRemindDescription = "اعرض لافتة عندما لا يكون Stirling PDF هو تطبيق PDF الافتراضي لديك." defaultPdfEditorSet = "افتراضي بالفعل" defaultStartupView = "العرض الافتراضي عند التشغيل" defaultStartupViewDescription = "اختر أي تبويب يكون نشطا في العمود الأيسر عند بدء التطبيق" @@ -8817,9 +9763,18 @@ languageDescription = "اختر لغة العرض" logout = "تسجيل الخروج" setAsDefault = "تعيين كافتراضي" theme = "السمة" +themeAccent = "لون التمييز" +themeAccentCustom = "لون مخصص" +themeAccentCustomHint = "مخصص (يوصى بالافتراضيات)" +themeAccentDark = "لون التمييز للوضع الداكن" +themeAccentDefault = "افتراضي" +themeAccentDefaultHint = "السمة الافتراضية (موصى بها)" +themeAccentDescription = "تتبعه الأزرار والروابط والتمييزات، كما يلوّن التطبيق بشكل خفيف. لكل من الوضع الفاتح والداكن لون خاص به. يستخدم النظام أيهما نشط." +themeAccentLight = "لون التمييز للوضع الفاتح" themeDark = "داكنة" themeDescription = "التبديل بين الوضع الفاتح والداكن" themeLight = "فاتحة" +themeReset = "استعادة السمة إلى الافتراضية" themeSystem = "System" title = "عام" user = "المستخدم" @@ -8833,6 +9788,12 @@ intro = "فعّل مصادقة المستخدمين وإدارة الفرق وم learnMore = "تعرّف أكثر في التوثيق" title = "لمدراء النظام" +[settings.general.loginLanding] +description = "اختر التطبيق الذي يفتح عند تسجيل الدخول إلى Stirling Cloud." +editor = "المحرر" +processor = "المعالج" +title = "بعد تسجيل الدخول" + [settings.general.mode] fullscreen = "ملء الشاشة" sidebar = "الشريط الجانبي" @@ -9013,9 +9974,6 @@ title = "السياسات والخصوصية" [settings.preferences] title = "التفضيلات" -[settings.search] -placeholder = "ابحث في صفحات الإعدادات..." - [settings.security] description = "حدّث كلمة مرورك للحفاظ على أمان حسابك." title = "الأمان" @@ -9041,6 +9999,11 @@ security = "الأمان" telegram = "Telegram" title = "الأمان والمصادقة" +[settings.signOut] +confirm = "هل أنت متأكد من رغبتك في تسجيل الخروج؟" +submit = "تسجيل الخروج" +title = "تسجيل الخروج" + [settings.team] title = "الفريق" @@ -9156,6 +10119,7 @@ last = "الصفحة الأخيرة" maintainRatio = "تبديل الحفاظ على نسبة الأبعاد" next = "الصفحة التالية" noSavedSigs = "لم يتم العثور على توقيعات محفوظة" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "توقيعات شخصية" previous = "الصفحة السابقة" redo = "إعادة" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "فشل إزالة الخلفية" hint = "ارفع صورة PNG أو JPG لتوقيعك" label = "رفع صورة التوقيع" placeholder = "اختر ملف صورة" +previewAlt = "Current image signature" processing = "جارٍ معالجة الصورة..." removeBackground = "إزالة الخلفية البيضاء (جعلها شفافة)" @@ -9207,6 +10172,17 @@ saved = "اختر توقيعاً محفوظاً أعلاه، ثم انقر في text = "بعد إدخال اسمك أعلاه، انقر في أي مكان على ملف PDF لوضع توقيعك." title = "كيفية إضافة توقيع" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "تحريك التوقيع" pause = "إيقاف الوضع مؤقتاً" @@ -9653,10 +10629,30 @@ title = "التحميل إلى الخادم" updateButton = "تحديث على الخادم" uploadButton = "تحميل إلى الخادم" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "انقر هنا" +fallback = "إذا لم يفتح تلقائيًا،" +opening = "جارٍ فتح Swagger UI في علامة تبويب جديدة..." tags = "api,documentation,swagger,endpoints,development" title = "توثيق API" @@ -9764,12 +10760,39 @@ selectFilesHint = "حدد الملفات في الملفات النشطة لتش singleFileScope = "سيطبق فقط على: {{fileName}}" viewerMode = "انتقل إلى محرر الملفات لاختيار عدة ملفات." +[toolFormat] +ANY = "أي ملف" +CBR = "أرشيف قصص مصورة (CBR)" +CBZ = "أرشيف قصص مصورة (CBZ)" +CSV = "ملف CSV" +EBOOK = "كتاب إلكتروني" +EMAIL = "رسالة بريد إلكتروني" +EXCEL = "جدول بيانات" +HTML = "صفحة ويب" +IMAGE = "صور" +JAVASCRIPT = "ملف JavaScript" +JSON = "ملف JSON" +MARKDOWN = "ملف Markdown" +NONE = "لا يوجد ملف" +PCL = "ملف طباعة PCL" +PDF = "ملف PDF" +PDF_ENCRYPTED = "ملف PDF محمي بكلمة مرور" +POSTSCRIPT = "ملف PostScript" +PPT = "عرض تقديمي" +TEXT = "ملف نصي" +VIDEO = "فيديو" +WORD = "مستند Word" +XML = "ملف XML" +XPS = "مستند XPS" +ZIP = "أرشيف ZIP" + [toolPanel] alpha = "ألفا" backToAllTools = "العودة إلى كل الأدوات" collapse = "طيّ اللوحة" expand = "توسيع اللوحة" goBack = "رجوع" +pdfTools = "أدوات PDF" placeholder = "اختر أداة للبدء" premiumFeature = "ميزة Premium:" toolsHeader = "الأدوات" @@ -9806,11 +10829,6 @@ noToolsFound = "لم يتم العثور على أدوات" quickAccess = "وصول سريع" searchPlaceholder = "ابحث عن الأدوات..." -[toolPicker.categories] -advancedTools = "أدوات متقدمة" -recommendedTools = "أدوات موصى بها" -standardTools = "أدوات قياسية" - [toolPicker.subcategories] advancedFormatting = "تنسيق متقدم" automation = "أتمتة" @@ -9824,6 +10842,9 @@ removal = "إزالة" signing = "توقيع" verification = "تحقق" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "لم يتم العثور على أدوات" noTools = "لا توجد أدوات متاحة" @@ -9950,28 +10971,53 @@ title = "المستخدمون" you = "(أنت)" [users.action] +cancelInvite = "إلغاء الدعوة" deleteTeam = "حذف الفريق" disableMfa = "إعادة ضبط MFA" move = "نقل إلى فريق" reinstate = "إعادة التفعيل" remove = "إزالة من المؤسسة" +removeTeam = "إزالة من الفريق" rename = "إعادة تسمية الفريق" resetPw = "إعادة ضبط كلمة المرور" suspend = "تعليق" unlock = "فتح الحساب" +[users.activity] +daysAgo = "قبل {{count}} يوم" +hoursAgo = "قبل {{count}} ساعة" +justNow = "الآن" +minutesAgo = "قبل {{count}} دقيقة" +monthsAgo_one = "قبل شهر واحد" +monthsAgo_other = "قبل {{count}} أشهر" +never = "أبدًا" +weeksAgo_one = "قبل أسبوع واحد" +weeksAgo_other = "قبل {{count}} أسابيع" +yearsAgo_one = "قبل سنة واحدة" +yearsAgo_other = "قبل {{count}} سنوات" + [users.cap] addProcessor = "+ المعالج" approver = "يعتمد السياسة" editor = "المحرر" processor = "المعالج" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "إلغاء الدعوة إلى {{email}}؟ لن يتمكنوا من الانضمام باستخدام الرابط الحالي." +cancelInviteTitle = "إلغاء الدعوة" deleteTeamBody = "حذف فريق {{name}}؟ يجب أن يكون الفريق فارغًا أولًا - انقل أعضاءه إلى فريق آخر، ولا يمكن أن يظل مالكًا لأي تكوينات تكامل." deleteTeamTitle = "حذف الفريق" disableMfaBody = "إزالة تسجيل MFA الخاص بـ {{name}}؟ سيقوم بإعداده مرة أخرى عند تسجيل الدخول التالي إذا كان مطلوبًا." disableMfaTitle = "إعادة ضبط MFA" removeBody = "هل تريد إزالة {{name}} نهائيًا من المؤسسة؟ لا يمكن التراجع عن ذلك." +removeTeamBody = "إزالة {{name}} من الفريق؟ سيحتفظون بحسابهم لكنهم سيفقدون الوصول إلى موارد هذا الفريق." removeTitle = "إزالة العضو" [users.empty] @@ -9982,10 +11028,8 @@ title = "لا يوجد أعضاء بعد" addToTeam = "إضافة إلى فريق" guestCount = "{{count}} ضيف" guests = "الضيوف" -guestsDesc = "متعاونون خارجيون، ضمن نطاق ما شاركته. المحرر فقط." ledBy = "بقيادة {{owner}}" org = "المؤسسة" -orgDesc = "مالكون بسلطة على مستوى المؤسسة واعتماد السياسات" owners = "{{count}} مالك" team = "فريق {{name}}" teamMeta = "{{count}} شخص" @@ -10022,6 +11066,18 @@ username = "اسم المستخدم" usernameError = "يجب أن يتكون اسم المستخدم من 3 أحرف على الأقل" usernamePlaceholder = "jsmith" +[users.invites] +by = "تمت الدعوة بواسطة {{who}}" +cancel = "إلغاء" +expiresInDays_one = "تنتهي خلال يوم واحد" +expiresInDays_other = "تنتهي خلال {{count}} أيام" +expiresToday = "تنتهي اليوم" +title = "الدعوات المعلقة" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "حدث خطأ ما أثناء الوصول إلى الخادم الخلفي، أو ليس لديك صلاحية الوصول. حاول مرة أخرى." title = "تعذر تحميل الأعضاء" @@ -10211,10 +11267,15 @@ searchPlaceholder = "ابحث في المرفقات" title = "المرفقات" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "عنوان الإشارة المرجعية" +bookmarkTitleRequired = "عنوان الإشارة المرجعية مطلوب" closeSidebar = "إغلاق الشريط الجانبي للإشارات المرجعية" collapseAll = "طي جميع الإشارات المرجعية" +empty = "No bookmarks in this document" expandAll = "توسيع جميع الإشارات المرجعية" +searchPlaceholder = "البحث في الإشارات المرجعية" +title = "Bookmarks" [viewer.comments] addComment = "إضافة تعليق" @@ -10236,10 +11297,12 @@ locateAnnotation = "تحديد الموقع في المستند" moreActions = "إجراءات أخرى" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "الصفحة {{page}}" placingHint = "انقر على صفحة لوضع… (إلغاء)" removeCommentOnly = "إزالة التعليق فقط" saveReply = "حفظ الرد" +searchPlaceholder = "Search comments" title = "التعليقات" typeComment = "تعليق" typeInsertText = "إدراج نص" @@ -10262,6 +11325,7 @@ unsavedDesc = "لديك تغييرات غير محفوظة" closeSidebar = "إغلاق الشريط الجانبي للطبقات" hideAll = "إخفاء جميع الطبقات" showAll = "إظهار جميع الطبقات" +title = "Layers" [viewer.link] delete = "حذف الرابط" @@ -10605,30 +11669,6 @@ title = "نص العلامة المائية" image = "صورة" text = "نص" -[wetSignature.tooltip] -header = "طرق إنشاء التوقيع" - -[wetSignature.tooltip.draw] -bullet1 = "تخصيص لون القلم وسماكته" -bullet2 = "امسح وأعد الرسم حتى ترضى" -bullet3 = "يعمل على أجهزة اللمس (الأجهزة اللوحية، الهواتف)" -description = "أنشئ توقيعًا بخط اليد باستخدام الماوس أو شاشة اللمس. الأفضل للتوقيعات الشخصية الأصيلة." -title = "رسم التوقيع" - -[wetSignature.tooltip.type] -bullet1 = "اختر من عدة خطوط" -bullet2 = "خصص حجم النص ولونه" -bullet3 = "مثالي للتوقيعات الموحّدة" -description = "أنشئ توقيعًا من نص مكتوب. سريع ومتّسق، ومناسب للمستندات التجارية." -title = "كتابة التوقيع" - -[wetSignature.tooltip.upload] -bullet1 = "يدعم PNG وJPG وغيرها من صيغ الصور" -bullet2 = "يُوصى بالخلفيات الشفافة للحصول على أفضل النتائج" -bullet3 = "سيتم تغيير حجم الصورة لتناسب مساحة التوقيع" -description = "حمّل صورة توقيع مُنشأة مسبقًا. مثالي إذا كان لديك توقيع ممسوح ضوئيًا أو شعار شركة." -title = "تحميل صورة التوقيع" - [workbenchBar] activeFiles = "الملفات النشطة" annotations = "الشروح" @@ -10642,6 +11682,7 @@ exitRedaction = "إنهاء وضع التنقيح" exportAll = "تصدير PDF" exportSelected = "تصدير الصفحات المحددة" formFill = "تعبئة النموذج" +hideToolbar = "Hide toolbar" multiTool = "الأداة المتعددة" panMode = "وضع التحريك" print = "طباعة PDF" @@ -10652,6 +11693,7 @@ redact = "تنقيح" rotateLeft = "تدوير لليسار" rotateRight = "تدوير لليمين" ruler = "مسطرة / قياس" +rulerSettings = "إعدادات المقياس" save = "حفظ" saveAll = "حفظ الكل" saveAs = "حفظ باسم" @@ -10661,6 +11703,7 @@ selectAll = "تحديد الكل" selectByNumber = "تحديد حسب أرقام الصفحات" selectLanguage = "اختر اللغة" share = "مشاركة" +showToolbar = "Show toolbar" toggleAnnotations = "تبديل ظهور الشروح" toggleAttachments = "تبديل عرض المرفقات" toggleBookmarks = "تبديل الإشارات المرجعية" diff --git a/frontend/editor/public/locales/az-AZ/translation.toml b/frontend/editor/public/locales/az-AZ/translation.toml index 0add38451a..c89316720f 100644 --- a/frontend/editor/public/locales/az-AZ/translation.toml +++ b/frontend/editor/public/locales/az-AZ/translation.toml @@ -1,7 +1,6 @@ alphabet = "Əlifba" apply = "Tətbiq et" applyAndContinue = "Saxla və çıx" -areYouSure = "Çıxmaq istədiyinizə əminsiniz?" back = "Geri" black = "Qara" blue = "Mavi" @@ -46,10 +45,8 @@ filesSelected = "seçilmiş fayllar" font = "Şrift" fontSizeTooltip = "Səhifə nömrəsi mətninin ölçüsü (pt). Daha böyük rəqəmlər daha böyük mətn yaradır." fontTypeTooltip = "Səhifə nömrələri üçün şrift ailəsi. Sənəd üslubunuza uyğun seçin." -genericSubmit = "Təsdiq Et" green = "Yaşıl" help = "Yardım" -imgPrompt = "Şəkil(lər)i Seç" incorrectPasswordMessage = "Cari şifr yanlışdır." info = "Məlumat" invalidUndoData = "Geri alma mümkün deyil: etibarsız əməliyyat məlumatı" @@ -62,7 +59,6 @@ loginAgreementTitle = "Giriş razılaşması" logOut = "Hesabdan çıx" marginTooltip = "Səhifə nömrəsi ilə səhifənin kənarı arasındakı məsafə." moreOptions = "Daha çox seçim" -multiPdfPrompt = "PDFləri Seç (2+)" never = "Heç vaxt" no = "Xeyr" noFileSelected = "Fayl seçilməyib. Zəhmət olmasa bir fayl yükləyin." @@ -80,7 +76,6 @@ pageOrderPrompt = "Xüsusi Səhifə Ardıcıllığı (Vergüllə ayrılmış sə pages = "Səhifələr" pageSelectionPrompt = "Xüsusi Səhifə Seçimi (1, 5, 6 tərzində vergüllə ayrılmış səhifə nömrələri listini və ya 2n+1 tərzində Funksiyalar daxil edin) :" password = "Şifr" -pdfPrompt = "PDF(lər)i Seç" pin = "Faylı bərkid (alət icrasından sonra aktiv saxla)" pro = "Pro" processingComplete = "Faylınız hazırdır." @@ -110,6 +105,7 @@ undoSuccess = "Əməliyyat uğurla geri alındı" unknown = "Bilinməyən" unpin = "Faylın bərkidilməsini ləğv et (alət icrasından sonra əvəzlə)" unsavedChanges = "PDF-də saxlanmamış dəyişiklikləriniz var." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Saxlanmamış dəyişikliklər" unsupported = "Dəstəklənmir" username = "İstifadəçi Adı" @@ -356,6 +352,7 @@ text = "Bu icazələr istifadəçilərin PDF ilə nə edə biləcəyini idarə e title = "İcazələri Dəyişdir" [addStamp] +preview = "Möhürə önbaxış" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Əl ilə endirmələr: keçidlərə klikləyin və faylları tess noLanguages = "Konfiqurasiya edilmiş qovluqda tessdata dilləri tapılmadı." permissionNotice = "tessdata yolu yazıla bilən deyil. Endirmələr brauzerdə açılacaq; .traineddata fayllarını əl ilə tessdata qovluğuna yadda saxlayın." +[admin.settings.ai.documents] +description = "Sənədlər üzrə suallara cavab vermək üçün istifadə olunan embedding modeli və axtarış parametrlərini konfiqurasiya edin. Saxlanıldıqda AI mühərrikinə tətbiq olunur." +title = "Sənədlər və RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Mühərrikin öz mühit etimad məlumatından istifadə etmək üçün boş buraxın. Özünüzdə yerləşdirilən tək mühərrikli quraşdırmalara tətbiq olunur." +generic = "Embedding API açarı" +openai = "OpenAI API açarı" +setPlaceholder = "Saxlanılıb - cari açarı saxlamaq üçün boş buraxın" +voyageai = "VoyageAI API açarı" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI-uyğun / Ollama embeddings endpoint-in baza URL-i, məsələn http://ollama:11434/v1. Etibarlı daxili endpoint-ə yönəlməlidir (SSRF-həssas)." +label = "Embedding baza URL-i" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding modelinin adı. Sərbəst mətn; təkliflər yalnız ipucudur." +label = "Embedding modeli" + +[admin.settings.ai.documents.embeddingProvider] +description = "Sənəd mətnini vektor embedding-lərə çevirmək üçün istifadə olunan provayder." +label = "Embedding provayderi" + +[admin.settings.ai.documents.maxSearches] +description = "Agentin hər sorğu üçün işlədə biləcəyi maksimum axtarış sayı." +label = "Maks. axtarışlar" + +[admin.settings.ai.documents.reindexNote] +body = "Embedding modelinin dəyişdirilməsi dərhal qüvvəyə minir, lakin əvvəlki modellə indekslənmiş sənədlər axtarışın düzgün nəticə qaytarması üçün yenidən indekslənməlidir." +title = "Yenidən indeksləmə tələb olunur" + +[admin.settings.ai.documents.saved] +reindexNote = "Embedding modelini dəyişmisinizsə, axtarışın yeni modeldən istifadə etməsi üçün mövcud sənədləri yenidən indeksləyin." + +[admin.settings.ai.documents.topK] +description = "Hər axtarış üçün əldə edilən ən uyğun fraqmentlərin sayı." +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling-i Python AI mühərrikinə qoşun və hansı AI imkanlarının göstəriləcəyini seçin. Dəyişikliklər yenidən başladıldıqda tətbiq olunur." +title = "AI mühərriki" + +[admin.settings.ai.general.capabilities] +description = "Ayrı-ayrı AI funksiyalarını aktiv və ya deaktiv edin. Deaktiv funksiyalar tətbiqdə gizlədilir." +title = "İmkanlar" + +[admin.settings.ai.general.enabled] +description = "Əsas açar. Söndürülü olduqda heç bir AI aləti, agenti və ya mühərrik çağırışı əlçatan deyil." +label = "AI-ni aktiv et" + +[admin.settings.ai.general.features.chat] +description = "PDF-lərlə işləmək üçün danışıq köməkçisi." +label = "Çat köməkçisi" + +[admin.settings.ai.general.features.classify] +description = "Sənədləri növünə və ya məzmununa görə avtomatik kateqoriyalaşdırın." +label = "Sənəd təsnifatı" + +[admin.settings.ai.general.features.createPdf] +description = "AI agenti vasitəsilə yeni PDF yaradın (məsələn, HTML-dən)." +label = "Sorğudan PDF yarat" + +[admin.settings.ai.general.features.documentQuestions] +description = "Yüklənmiş sənədə əsaslanan suallar verin və cavablar alın." +label = "Sənəd sualları" + +[admin.settings.ai.general.features.mathAuditor] +description = "Sənədləri riyazi və ədədi səhvlərə görə yoxlayın." +label = "Riyaziyyat auditoru" + +[admin.settings.ai.general.features.pdfComment] +description = "PDF-ə AI tərəfindən yazılmış rəy şərhləri və annotasiyalar əlavə edin." +label = "PDF şərh agenti" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Sənəd yaratma kimi daha ağır agent əməliyyatları üçün vaxt limiti." +label = "Uzunmüddətli vaxt limiti (saniyə)" + +[admin.settings.ai.general.note] +body = "AI mühərriki ayrıca xidmət kimi işləyir. Onun paylaşılan sirri" +body2 = "konteyner mühiti dəyişəni vasitəsilə təyin olunur. Provayder API açarları bu səhifələrdə daxil edilə və ya mühərrik mühiti dəyişənləri kimi verilə bilər; burada daxil edilən açarlar saxlanıldıqda mühərrikə göndərilir." +title = "AI mühərriki haqqında" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Axınla (token-token) verilən çat cavabları üçün vaxt limiti." +label = "Axın vaxt limiti (saniyə)" + +[admin.settings.ai.general.test] +button = "Bağlantını test et" +failBody = "AI mühərriki cavab vermədi. URL-i, mühərrik konteynerinin işlədiyini və AI-nin aktiv olduğunu yoxlayın (aktiv etdikdən sonra yenidən başlatmaq lazımdır)." +failTitle = "AI mühərrikinə çatmaq olmur" +okBody = "AI mühərriki sağlamlıq yoxlamasına cavab verdi." +okTitle = "AI mühərrikinə çatmaq olur" + +[admin.settings.ai.general.timeoutSeconds] +description = "Mühərrikə standart AI sorğuları üçün vaxt limiti." +label = "Sorğu vaxt limiti (saniyə)" + +[admin.settings.ai.general.url] +description = "Python AI mühərrikinin daxili URL-i, məsələn http://stirling-pdf-engine:5001." +label = "AI mühərriki URL-i" + +[admin.settings.ai.limits] +description = "AI sorğularının nə qədər iş görə biləcəyi və eyni vaxtda neçə sorğunun işləyə biləcəyi üçün qoruyucu limitlər. Saxlanıldıqda AI mühərrikinə tətbiq olunur." +title = "Limitlər və performans" + +[admin.settings.ai.limits.maxCharacters] +description = "Qoruyucu limit: çıxarılan mətni bu uzunluğu aşan AI sorğularını rədd edin." +label = "Sorğu başına maks. simvol" + +[admin.settings.ai.limits.maxPages] +description = "Qoruyucu limit: bu qədər PDF səhifəsindən çox olan AI sorğularını rədd edin." +label = "Sorğu başına maks. səhifə" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Bütün mühərrik üzrə eyni vaxtda işləyən model çağırışlarının maksimum sayı." +label = "Modelin maks. paralelliyi" + +[admin.settings.ai.models] +description = "LLM provayderini və AI mühərrikinin istifadə etdiyi ağıllı/sürətli modelləri seçin. Saxlanıldıqda AI mühərrikinə tətbiq olunur." +title = "Modellər və provayderlər" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API açarı" +description = "Mühərrikin öz mühit etimad məlumatından istifadə etmək üçün boş buraxın. Özünüzdə yerləşdirilən tək mühərrikli quraşdırmalara tətbiq olunur." +generic = "API açarı" +openai = "OpenAI API açarı" +setPlaceholder = "Saxlanılıb - cari açarı saxlamaq üçün boş buraxın" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI-uyğun / Ollama endpoint-in baza URL-i, məsələn http://ollama:11434/v1." +label = "Provayder baza URL-i" +warning = "Baza URL etibarlı daxili endpoint-ə yönəlməlidir. Mühərrik ona server tərəfli sorğular göndərəcək, buna görə etibarsız dəyər SSRF-həssasdır." + +[admin.settings.ai.models.fastMaxTokens] +description = "Sürətli model üçün maksimum çıxış tokenləri." +label = "Sürətli model maks. tokenləri" + +[admin.settings.ai.models.fastModel] +description = "Yüngül tapşırıqlar üçün daha ucuz, daha sürətli model. Sərbəst mətn; təkliflər yalnız ipucudur." +label = "Sürətli model" + +[admin.settings.ai.models.provider] +description = "Mühərrikin hansı LLM provayderi ilə əlaqə saxlaması." +label = "Provayder" + +[admin.settings.ai.models.smartMaxTokens] +description = "Ağıllı model üçün maksimum çıxış tokenləri." +label = "Ağıllı model maks. tokenləri" + +[admin.settings.ai.models.smartModel] +description = "Mürəkkəb əsaslandırma üçün yüksək imkanlı model. Sərbəst mətn; təkliflər yalnız ipucudur." +label = "Ağıllı model" + +[admin.settings.ai.saved] +body = "Dəyişikliklər avtomatik olaraq AI mühərrikinə göndərilir." +bodyNoPush = "Parametrlər saxlanıldı. AI mühərriki konfiqurasiyasını növbəti dəfə götürəndə tətbiq olunacaq." +title = "AI parametrləri saxlanıldı" + [admin.settings.badge] clickToUpgrade = "Plan detalları üçün klikləyin" @@ -938,6 +1094,25 @@ label = "Başlanğıcda yenidən yarat" description = "Sertifikatın etibarlı olacağı gün sayı" label = "Sertifikatın etibarlılığı (gün)" +[admin.settings.folderAccess] +description = "Qovluq mənbələri və qovluq çıxışlarının oxuya və yaza biləcəyi direktoriyalar. Bu təhlükəsizlik sərhədidir: avtomatlaşdırmalar heç vaxt bu siyahıdan kənar server yoluna yönəldilə bilməz." +securityNote = "Qovluq mənbələri və çıxışlarını tamamilə deaktiv etmək üçün bunu boş buraxın. Stirling-in öz konfiqurasiya direktoriyası həmişə qadağandır." +title = "Qovluq girişi" + +[admin.settings.folderAccess.implied] +description = "Stirling tərəfindən idarə olunan bu direktoriyalara həmişə icazə verilir və burada dəyişdirilə bilməz." +serverStorage = "Server fayl yaddaşı" +title = "Həmişə icazəlidir" +watchedFolder = "Pipeline izlənən qovluğu" + +[admin.settings.folderAccess.roots] +add = "Əlavə et" +empty = "Heç bir qovluğa icazə verilmir. Qovluq mənbələri və çıxışları hazırda deaktivdir." +hint = "Mütləq yollar daxil edin, məsələn /data/inbox." +label = "İcazə verilən qovluq kökləri" +placeholder = "/data/inbox" +remove = "Qovluq kökünü sil" + [admin.settings.general] description = "Brendinq və standart davranış daxil olmaqla sistem üzrə tətbiq ayarlarını konfiqurasiya edin." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Genişlik" [app] description = "Pulsuz Adobe Acrobat alternativi (10M+ yükləmə)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b-ə çevir" convertToPdfA3bDescription = "Daxil edilmiş qoşmalarla arxivləmə üçün PDF yaradır" @@ -1842,6 +2020,15 @@ export = "İxrac" accessDenied = "Giriş rədd edildi" insufficientPermissions = "Bu əməliyyatı yerinə yetirmək üçün icazəniz yoxdur." +[auth.callback] +completing = "Autentifikasiya tamamlanır" +invalidToken = "OAuth girişi uğursuz oldu - etibarsız token." +missingToken = "OAuth girişi uğursuz oldu - token alınmadı." +oauthFailed = "OAuth girişi uğursuz oldu. Yenidən cəhd edin." +pleaseWait = "Girişinizi tamamlayana qədər gözləyin." +signedOut = "Siz hesabdan çıxmısınız. Yenidən daxil olun." +windowMayClose = "Tamamlandıqdan sonra bu pəncərəni bağlaya bilərsiniz." + [auth.displayName] guest = "Qonaq" user = "İstifadəçi" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "PDF sənədlərini JavaScript və daxil edilmiş fayllar k description = "Bu alət məzmununa əsasən PDF fayllarını avtomatik olaraq yenidən adlandırır. Sənədi analiz edərək mətndən ən uyğun başlığı tapır." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Avtomatik döndər" +tags = "avtomatik döndər,orientasiyanı düzəlt,tərsinə,yan tərəfə,düzləndir,orientasiya,orientasiyanı aşkar et,dönməni düzəlt,avtomatik orientasiya,skan edilmiş,OSD" + +[autoRotate.confidenceThreshold] +title = "Etibar həddi" + +[autoRotate.detectionMode] +auto = "Avtomatik" +osd = "Yalnız OCR" +text = "Yalnız mətn" +title = "Aşkarlama üsulu" + +[autoRotate.error] +failed = "PDF avtomatik döndərilərkən xəta baş verdi." + +[autoRotate.inferUndetected] +title = "Aşağı etibarlı səhifələri uyğunlaşdırmaq üçün döndər" + +[autoRotate.report] +applied = "{{degrees}}° saat əqrəbi istiqamətində" +confidenceValue = "Etibar {{value}}" +noChange = "Dəyişiklik yoxdur" +pageLabel = "Səhifə {{number}}" +summary = "{{total}} səhifədən {{rotated}} döndərildi, {{unchanged}} olduğu kimi saxlanıldı" +title = "Aşkarlama hesabatı" + +[autoRotate.report.method] +inferred = "Uyğunlaşdırıldı" +none = "Buraxıldı" +osd = "OCR" +text = "Mətn" + +[autoRotate.report.note] +belowThreshold = "Həddən aşağı" +blankPage = "Səhifə boşdur" +inferredFromDocument = "Digər səhifələrlə eyni" +noDominantDirection = "Qarışıq mətn istiqamətləri" +osdFailed = "Oxuna bilən mətn tapılmadı" +osdNoVerdict = "Hansı tərəfin yuxarı olduğunu müəyyən etmək olmadı" +tesseractUnavailable = "OCR quraşdırılmayıb" +tooFewGlyphs = "Çox az mətn" + +[autoRotate.results] +title = "Avtomatik döndərmə nəticələri" + +[autoRotate.settings] +title = "Parametrlər" + +[autoRotate.tooltip.confidenceThreshold] +text = "Səhifə döndərilməzdən əvvəl yoxlamanın nə qədər əmin olmalı olduğu. Daha çox səhifə döndərmək üçün azaldın, daha çox səhifəni toxunulmaz saxlamaq üçün artırın." +title = "Etibar həddi" + +[autoRotate.tooltip.description] +text = "Hər səhifənin hansı tərəfinin yuxarı olmalı olduğunu müəyyən edir və onu düzgün istiqamətə döndərir. Artıq düz olan səhifələrə toxunulmur." + +[autoRotate.tooltip.detectionMode] +text = "Avtomatik əvvəlcə səhifədəki mətni oxuyur, bu kifayət etməzsə səhifəyə şəkil kimi baxır. Yalnız mətn şəkil yoxlamasını buraxır və heç vaxt skan edilməmiş sənədlər üçün ən sürətlisidir. Yalnız OCR hər səhifəni şəkil kimi qəbul edir." +title = "Aşkarlama üsulu" + +[autoRotate.tooltip.header] +title = "Avtomatik döndər" + +[autoRotate.tooltip.inferUndetected] +text = "Boş və ya demək olar ki, boş səhifələr təkbaşına yoxlanıla bilməz. Oxuna bilən bütün səhifələr eyni döndərməni tələb edirsə, bu səhifələr də onlara uyğun döndərilir." +title = "Aşağı etibarlı səhifələri uyğunlaşdırmaq üçün döndər" + [autoSizeSplitPDF] tags = "pdf,ayır,sənəd,nizamla" @@ -2189,33 +2443,6 @@ keywords = "Açar sözlər: odd, even" numbers = "Nömrələr/aralıqlar: 5, 10-20" progressions = "Proqressiyalar: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Sertifikat Növləri" - -[certificateChoice.tooltip.organization] -bullet1 = "Sistem administratorları tərəfindən idarə olunur" -bullet2 = "Avtorizə olunmuş istifadəçilər arasında paylaşılır" -bullet3 = "Fərdi deyil, şirkət kimliyini təmsil edir" -bullet4 = "Ən uyğunu: Rəsmi sənədlər, komanda imzaları" -description = "Təşkilatınız tərəfindən təqdim olunan paylaşılmış sertifikat. Şirkət miqyasında imzalama səlahiyyəti üçün istifadə olunur." -title = "Təşkilat Sertifikatı" - -[certificateChoice.tooltip.personal] -bullet1 = "İlk istifadə zamanı avtomatik yaradılır" -bullet2 = "İstifadəçi hesabınızla bağlıdır" -bullet3 = "Başqa istifadəçilərlə paylaşıla bilməz" -bullet4 = "Ən uyğunu: Şəxsi sənədlər, fərdi məsuliyyət" -description = "İstifadəçi hesabınız üçün unikal, avtomatik yaradılan sertifikat. Fərdi imzalar üçün uyğundur." -title = "Şəxsi Sertifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX faylı və şifrə tələb edir" -bullet2 = "Xarici Sertifikat Orqanları tərəfindən verilə bilər" -bullet3 = "Hüquqi sənədlər üçün daha yüksək etibar səviyyəsi" -bullet4 = "Ən uyğunu: Hüquqi qüvvəli müqavilələr, xarici təsdiqləmə" -description = "Öz PKCS#12 sertifikat faylınızdan istifadə edin. Sertifikat xüsusiyyətlərinə tam nəzarət verir." -title = "Fərdi P12 yüklə" - [certSign] awaitingSignatures = "İmzalar gözlənir" chooseCertificate = "Sertifikat faylını seçin" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Sertifikat mənbəyi" upload = "Yüklə" -[certSign.tooltip.header] -title = "İmzaların idarə olunması haqqında" - -[certSign.tooltip.overview] -bullet1 = "Mövcud imzaları və onların etibarlılığını yoxlayın" -bullet2 = "İmzalayanlar və sertifikatlar haqqında ətraflı məlumatı görün" -bullet3 = "Sənədlərinizi qorumaq üçün yeni rəqəmsal imzalar əlavə edin" -bullet4 = "Bir neçə fayl dəstəyi və asan naviqasiya" -text = "Bu alət PDF-lərinizin rəqəmsal imza ilə imzalanıb-imzalanmadığını yoxlamağa və yeni rəqəmsal imzalar əlavə etməyə imkan verir. Rəqəmsal imzalar sənədi kimin yaratdığını və ya təsdiqlədiyini sübut edir və imzadan sonra dəyişdirilib-dəyişdirilmədiyini göstərir." -title = "Bu alət nə edə bilər?" - -[certSign.tooltip.signing] -bullet1 = "PEM, PKCS12, JKS və server sertifikatı formatlarını dəstəkləyir" -bullet2 = "PDF-də imzanı göstərmək və ya gizlətmək seçimi" -bullet3 = "Səbəb, məkan və imzalayanın adını əlavə edin" -bullet4 = "Görünən imzaların yerləşdiriləcəyi səhifəni seçin" -bullet5 = "Sadə 'Stirling-PDF ilə imzala' seçimi üçün server sertifikatından istifadə edin" -text = "PDF-i imzalamaq üçün rəqəmsal sertifikat lazımdır (məsələn, PEM, PKCS12 və ya JKS). İmzanı sənəddə görünən etmək və ya yalnız təhlükəsizlik məqsədi ilə görünməz saxlamaq seçiminiz var." -title = "İmzaların əlavə edilməsi" - -[certSign.tooltip.validation] -bullet1 = "İmzaların etibarlı və ya etibarsız olduğunu göstərir" -bullet2 = "İmzalayan barədə məlumatı və imzalama tarixini göstərir" -bullet3 = "Sənədin imzadan sonra dəyişdirilib-dəyişdirilməsini yoxlayır" -bullet4 = "Doğrulama üçün xüsusi sertifikatlardan istifadə edə bilər" -text = "İmzaları yoxladıqda alət onların etibarlı olub-olmadığını, sənədi kimin və nə vaxt imzaladığını və imzadan sonra sənədin dəyişdirilib-dəyişdirilmədiyini bildirir." -title = "İmzaların yoxlanması" - [changeCreds] changeUsername = "İstifadəçi adınızı yeniləyin. Yenilədikdən sonra çıxış ediləcəksiniz." credsUpdated = "Hesab yeniləndi" @@ -2737,12 +2936,16 @@ text = "Bu icazələri dəyişdirilməz etmək üçün, Parol əlavə etmə alə [chat.actions] copy = "Mesajı kopyala" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling agent seçimləri" +assistant = "AI Assistant" clearChat = "Söhbəti təmizlə" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Bu əməliyyat bulud kreditlərinizi istifadə edəcək" tooltip = "Ekrandan rəng seç" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Rəng seçin" [common] +actions = "Actions" back = "Geri" cancel = "Ləğv et" close = "Bağla" +codeSample = "Code sample" collapse = "Yığ" confirm = "Təsdiqlə" continue = "Davam et" copied = "Kopyalandı!" copy = "Kopyala" done = "Bitdi" +enterValue = "Enter value" error = "Xəta" expand = "Genişləndir" loading = "Yüklənir..." next = "Növbəti" +open = "Open" preview = "Önizləmə" previous = "Əvvəlki" refresh = "Yenilə" +remaining = "Qalıb" retry = "Yenidən cəhd et" save = "Yadda saxla" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Seçilənləri təmizlə" @@ -2988,6 +3199,7 @@ title = "Sıxma metodu" [compress.settings] desiredSize = "İstənilən fayl ölçüsü" desiredSizePlaceholder = "Ölçünü daxil edin" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Sıxma fayl ölçüsünü azaltmağın asan yoludur. 'Fayl ölçüsü'nü seçərək hədəf ölçünü daxil edin və keyfiyyəti sizin üçün tənzimləyək. 'Keyfiyyət'i seçərək sıxma gücünü əl ilə təyin edin." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF seçimləri" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Əvvəlcə mənbə formatını seçin" settings = "Parametrlər" single = "Tək" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analitika" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Qabaqcıl monitorinq" -allInOneWorkspace = "Hər şey bir yerdə PDF iş məkanı (baxış, alətlər və agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Ayrı-ayrı dəstək və SLA-lar" -fasterThroughput = "10x daha sürətli işləmə" -fullyPrivateFiles = "Tam məxfi fayllar" -largeFileProcessing = "Böyük faylların emalı" -orgWideAccess = "Təşkilat üzrə giriş nəzarətləri" -premiumAiModels = "Premium AI modelləri" -prioritySupport = "Prioritet dəstək" -privateDocCloud = "Məxfi Sənəd Buludu" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Təhlükəsiz API giriş" -standardThroughput = "Standart işləmə" -unlimitedApiAccess = "Limitsiz API girişi" -unlimitedSeats = "Limitsiz istifadəçi yerləri" - [crop] autoCrop = "Boşluqları avtomatik qırp" noFileSelected = "Kəsməyə başlamaq üçün bir PDF faylı seçin" @@ -3346,6 +3558,7 @@ message = "Standart PDF işləyicisini təyin etmək alınmadı" title = "Xəta" [defaultApp.prompt] +dontRemind = "Mənə bir daha xatırlatma" message = "PDF fayllarını açmaq üçün Stirling PDF-i standart tətbiqiniz edin." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Axtar..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Redaktə et" @@ -3644,11 +3861,13 @@ shareSelected = "Seçilənləri paylaş" sharing = "Paylaşma" showAll = "Hamısını göstər" showHistory = "Tarixçəni göstər" +sortBy = "Sort files" sortByDate = "Tarixə görə sırala" sortByName = "Ada görə sırala" sortBySize = "Ölçüyə görə sırala" storage = "Saxlama" storageState = "Saxlama" +storageUsed = "Storage used" synced = "Sinxronlaşdırılıb" title = "PDF Fayllarını Yüklə" toolChain = "Tətbiq olunan alətlər" @@ -3694,13 +3913,17 @@ addFiles = "Fayllar əlavə et" addingFiles = "Fayllar əlavə edilir…" collapse = "Yan paneli yığ" customizeGroups = "Qrupları fərdiləşdir" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Başlamaq üçün faylları açın" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Yan paneli genişləndir" -files = "Fayllar" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive konfiqurasiya edilməyib" leaveMyFiles = "Mənim Fayllarımdan çıx" +library = "PDF kitabxanası" myFiles = "Mənim Fayllarım" noFiles = "Hələ fayl yoxdur" openFileManager = "Fayl menecerini aç" @@ -3708,37 +3931,36 @@ openFromComputer = "Kompüterdən aç" openSettings = "Ayarları aç" other = "Digər" recent = "Son" -search = "Axtar" -searchPlaceholder = "Fayllarda axtar..." viewAll = "Bütün {{count}} faylı göstər" [fileSidebar.fileItem] closeViewer = "Görüntüləyicini bağla" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Görüntüləyicidə aç" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Əlavə et" -addLabel = "Etiket əlavə et…" -categoryIconAria = "{{name}} üçün ikon seç" -createCategory = "Kateqoriya yarat" -delete = "Kateqoriyanı sil" done = "Hazırdır" -hide = "Kateqoriyanı gizlət" -newCategory = "Yeni kateqoriya adı…" -removeLabel = "{{name}} sil" -rename = "Adını dəyiş" reset = "Defoltlara sıfırla" -search = "Etiketləri axtar…" -show = "Kateqoriyanı göstər" subtitle = "Fayllarınızı əsas kateqoriyalara qruplaşdırın. Kateqoriyaya mövcud və ya yeni etiketlər əlavə edin, adını dəyişin və ya özünüz yaradın. Kateqoriyalarınızdan heç birində olmayan fayllar “Digər” altında görünür." title = "Yan panel kateqoriyaları" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "İş sahəsinə əlavə et" addToWorkspaceCount = "{{count}} faylı iş sahəsinə əlavə et" @@ -3749,7 +3971,6 @@ backToMyFiles = "Mənim Fayllarıma geri" breadcrumbs = "Qovluq yolu" cancel = "Ləğv et" classification = "Təsnifat" -clearSearch = "Axtarışı təmizlə" clearSelection = "Seçimi təmizlə" closeDetails = "Təfərrüatları bağla" create = "Yarat" @@ -3781,6 +4002,7 @@ downloadAll = "Hamısını endir" downloadVersion = "Bu versiyanı endir" dropOverlay = "Yükləmək üçün faylları buraxın" dropOverlaySub = "Fayllar əvvəlcə Lokal bölmədə başlayır. Onları qovluğa yerləşdirmək üçün 'Köçür' və ya 'Buluda saxla'dan istifadə edin." +duplicate = "Duplicate" file = "Fayl" fileInfo = "File info" fileMenu = "Fayl əməliyyatları" @@ -3808,13 +4030,12 @@ refresh = "Serverdən yenilə" remove = "Sil" removeVersion = "Bu versiyanı sil" rename = "Yenidən adlandır" +renamed = "Adı dəyişdirildi" renameFolder = "Qovluğu yenidən adlandır" resizeFolderTree = "Qovluq ağacının ölçüsünü dəyiş (ox düymələri, daha böyük addımlar üçün Shift; avto-uyğunlaşdırmaq üçün iki dəfə kliklə)" save = "Saxla" saveToServer = "Serverə saxla" saveToServerDisabledHint = "Bu serverdə serverə saxlama aktiv deyil. Administratorunuzdan bunu aktivləşdirməsini xahiş edin." -search = "Axtar" -searchPlaceholder = "Bu qovluqda və alt qovluqlarda axtar" selectAll = "Hamısını seç" selectAllHint = "Hamısını seçmək üçün klikləyin. Məsləhət: faylları tək-tək əlavə etmək üçün Ctrl (və ya Cmd), aralığı seçmək üçün Shift basın." selectedCount = "{{count}} seçilib" @@ -3874,6 +4095,10 @@ title = "Hələ bulud faylı yoxdur" hint = "Yüklənmədən saxlanılan fayllar burada qalır. Əlavə etmək üçün faylı buraxın." title = "Yalnız lokal fayl yoxdur" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Açdığınız və ya redaktə etdiyiniz fayllar burada görünəcək." title = "Hələ heç nə dəyişdirilməyib" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Qovluğu silmək mümkün olmadı." deleteFolderFailedDetail = "Qovluğu silmək mümkün olmadı: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Qovluğun görünüşünü yeniləmək mümkün olmadı." folderAppearanceFailedDetail = "Qovluğun görünüşünü yeniləmək mümkün olmadı: {{message}}" moveFilesFailed = "Faylları köçürmək mümkün olmadı." @@ -3910,9 +4137,7 @@ count = "Fayllar" folder = "Qovluq" labels = "Etiketlər" modified = "Dəyişdirilib" -name = "Ad" size = "Ölçü" -toolHistoryAtVersion = "Toplam alət zənciri" totalSize = "Ümumi ölçü" type = "Növ" versionHistory = "Versiya yolu" @@ -3945,6 +4170,11 @@ localHint = "Yalnız bu brauzerdə saxlanılır" shared = "Paylaşılan" sharedHint = "Keçid vasitəsilə sizinlə paylaşılıb" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Ən köhnə əvvəlcə" modifiedDesc = "Ən yenilər əvvəlcə" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Yaddaşda fayl yoxdur. Əvvəlcə bəzi faylları açın open = "Aç" openFile = "Faylı aç" openFiles = "Faylları aç" +selectFile = "Select {{name}}" selectFromStorage = "Saxlamadan seç" upload = "Yüklə" uploadFile = "Fayl yüklə" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Hamısı saxlanıldı" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV çıxarmaq alınmadı" +extractXlsxError = "XLSX çıxarmaq alınmadı" +filled = "filled" flattenAfterFilling = "Doldurduqdan sonra yastılaşdır" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "təl." +requiredFieldsError = "Bütün tələb olunan sahələri doldurun" rescanFields = "Sahələri yenidən skan et" rescanFormFields = "Forma sahələrini yenidən skan et" save = "Saxla" +saveShortcut = "Saxlamaq üçün Ctrl+S" +unsavedChanges = "Saxlanılmamış dəyişikliklər" [formFill.sidebar] close = "Yan paneli bağla" @@ -4309,6 +4552,11 @@ desc = "Aşkarlanmış başlığa əsasən PDF faylını avtomatik adlandırır" tags = "avto-aşkarlama,başlığa əsasən,təşkil et,yenidən adlandır" title = "PDF faylını avtomatik adlandır" +[home.autoRotate] +desc = "Hər səhifənin orientasiyasını aşkar edin və avtomatik olaraq düz vəziyyətə döndərin." +tags = "avtomatik döndər,orientasiyanı düzəlt,tərsinə,yan tərəfə,düzləndir,orientasiya,orientasiyanı aşkar et,dönməni düzəlt,avtomatik orientasiya,skan edilmiş,OSD" +title = "Avtomatik döndər" + [home.autoSizeSplitPDF] tags = "avto,böl,ölçü" title = "Say/Ölçüyə Əsasən Avtomatik Ayır" @@ -4608,15 +4856,12 @@ hint = "PDF üzərinə yerləşdirmək üçün PNG, JPG, SVG və ya başqa şək label = "Şəkil yüklə" placeholder = "Şəkil faylını seç" -[imageToPdf] -tags = "çevirmə,şəkil,jpg,fotoşəkil,foto" - [imageToPDF] header = "Şəkli PDF-ə" title = "Şəkli PDF-ə" -[infoBanner] -dismiss = "Bağla" +[imageToPdf] +tags = "çevirmə,şəkil,jpg,fotoşəkil,foto" [invite] acceptError = "Hesabın yaradılması alınmadı" @@ -4647,11 +4892,9 @@ welcomeTitle = "Siz dəvət olunmusunuz!" [landing] addFiles = "Fayllar əlavə et" -heroSubtitle = "Başlamaq üçün mövcud PDF-i sürüyüb buraxın və ya əlavə edin." mobileUpload = "Mobil cihazdan yüklə" openFromComputer = "Komputerdən aç" uploadFromComputer = "Kompüterdən yüklə" -workbenchEmptyStateHero = "PDF-i istənilən yerə buraxın" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "İstifadəçi adını daxil edin" failedToSignIn = "{{provider}} ilə daxil olmaq alınmadı: {{message}}" forgotPassword = "Parolunuzu unutmusunuz?" loggingIn = "Daxil olunur..." -logIn = "Daxil ol" login = "Daxil ol" magicLinkSent = "Sehrli link {{email}} ünvanına göndərildi! Zəhmət olmasa e-poçtunuzu yoxlayın və daxil olmaq üçün linkə klikləyin." mfaCode = "Doğrulama kodu" @@ -4715,26 +4957,10 @@ signInWith = "Bununla daxil ol" title = "Daxil olun" unexpectedError = "Gözlənilməz xəta: {{message}}" updatePassword = "Şifrəni yenilə" -useEmailInstead = "E-poçt ilə daxil olun" useMagicLink = "Sehrli linkdən istifadə et" username = "İstifadəçi adı" youAreLoggedIn = "Daxil oldunuz!" -[login.slides.edit] -alt = "PDF-ləri redaktə et" -subtitle = "PDF-ləri məxfiləşdirmək, imzalamaq, oxumaq və onlarla işləmək üçün onlarla alətlə axtardığınızı tapacaqsınız." -title = "İstədiyiniz məlumatı göstərmək/qorumaq üçün PDF-ləri redaktə edin" - -[login.slides.overview] -alt = "Stirling PDF icmalı" -subtitle = "PDF-lər üçün məxfilik-öncəli bulud paketi: sənədləri çevirməyə, imzalamağa, məzmunu məxfiləşdirməyə və idarə etməyə, üstəlik 50+ güclü alət." -title = "PDF ehtiyaclarınız üçün tək ünvan." - -[login.slides.secure] -alt = "PDF-ləri qoruyun" -subtitle = "Şifrələr əlavə edin, məzmunu məxfiləşdirin və sertifikatları asanlıqla idarə edin." -title = "PDF-lərinizdə həssas məlumatları qoruyun" - [margin] large = "Böyük" medium = "Orta" @@ -4818,6 +5044,34 @@ uploadSuccess = "Uğurla yükləndi!" uploadSuccessMessage = "Şəkilləriniz köçürüldü." validating = "Sessiya yoxlanılır..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skan edib fotoları yükləyin. Şəkillər avtomatik olaraq PDF-ə çevrilir." descriptionNoConvert = "Mobil cihazınızdan fotoları yükləmək üçün skan edin." @@ -4836,8 +5090,13 @@ title = "Mobil cihazdan yüklə" tags = "Multi-alət,Çoxlu əməliyyat,UI,tut-sürüşdür,front end,istifadəçi-tərəf,interaktiv,qarşılıqlı,yerini dəyiş,sil,köçür,böl" title = "PDF Multi-Alət" -[navbar] -search = "Axtar" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Təsdiqləmə uğurlu olmadı. Bu pəncərəni bağlayıb yenidən cəhd edə bilərsiniz." @@ -4966,6 +5225,7 @@ activeFiles = "Aktiv Fayllar görünüşü alətə yüklədiyin allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Bağla" cropSettings = "Artıq kəsmək istədiyimiz faylı seçdiyimizə görə, PDF-i kəsmək istədiyimiz sahəni seçmək üçün Kəsmə alətini tənzimləyə bilərik." +dialogLabel = "Onboarding" fileCheckbox = "Fayllardan birinə klikləmək onu emal üçün seçir. Toplu əməliyyatlar üçün bir neçə faylı seçə bilərsiniz." fileReplacement = "Dəyişdirilmiş fayl İş Masasında orijinal faylı avtomatik əvəz edəcək və onu asanlıqla daha çox alətdən keçirməyə imkan verəcək." filesButton = "Sürətli Giriş panelindəki Fayllar düyməsi PDF-ləri alətlərdə istifadə etmək üçün yükləməyə imkan verir." @@ -4976,6 +5236,7 @@ pinButton = "Alətlər işləndikdən sonra fayllarınızın aktiv qalmasını i results = "Alət işləməyi bitirdikdən sonra, Baxış addımı nəticələrin önizləməsini bu paneldə göstərəcək və əməliyyatı geri almağa və ya faylı endirməyə imkan verəcək." runButton = "Alət tənzimləndikdən sonra, bu düymə seçilmiş bütün PDF-lərdə aləti işə salmağa imkan verir." selectCropTool = "Gəlin, Kəsmə alətini seçərək alətlərdən birinin istifadəsini nümayiş etdirək." +stepOf = "Addım {{current}} / {{total}}" toolInterface = "Bu, Kəsmə alətinin interfeysidir. Gördüyünüz kimi, hələ işləmək üçün PDF faylı əlavə etmədiyimiz üçün burada çox şey yoxdur." workbench = "Bu, İş Masasıdır - PDF-lərinizi gördüyünüz və redaktə etdiyiniz əsas sahə." wrapUp = "Hər şey hazırdır! Tətbiqin əsas sahələri və onlardan necə istifadə etməyi öyrəndiniz. Bu turu yenidən görmək istədiyiniz zaman Kömək düyməsini klikləyin." @@ -4988,6 +5249,26 @@ showMeAround = "Turu göstər" skipForNow = "Hələlik ötür" skipTheTour = "Turu keç" +[onboarding.checklist] +dismiss = "Bağla" +title = "Stirling PDF-i qurun" + +[onboarding.checklist.downloadDesktop] +description = "Stirling-i kompüterinizdə yerli olaraq işlədin" +title = "Stirling for Desktop yüklə" + +[onboarding.checklist.inviteTeam] +description = "Komandanızla əməkdaşlıq edin" +title = "Komanda üzvlərini dəvət et" + +[onboarding.checklist.shareAnalytics] +description = "Stirling-i yaxşılaşdırmağa kömək edin" +title = "Anonim istifadə məlumatlarını paylaş" + +[onboarding.checklist.takeTour] +description = "Qısa bələdçi ilə Stirling-in necə işlədiyini görün" +title = "Tura başlayın" + [onboarding.desktopInstall] body = "Stirling masaüstü tətbiq kimi ən yaxşı işləyir. Onu oflayn istifadə edə, sənədlərə daha sürətli baxa və düzəlişləri yerli olaraq kompüterinizdə edə bilərsiniz." selectOs = "Əməliyyat sistemini seçin" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}} üçün yüklə" [onboarding.mfa] authenticationCode = "Təsdiqləmə kodu" +description = "Autentifikator tətbiqini əlaqələndirərək hesabınızı qoruyun. QR kodunu skan edin və ya quraşdırma açarını daxil edin, sonra tamamlanması üçün 6 rəqəmli kodu təsdiqləyin." +enable = "MFA-ni aktiv et" +enableError = "İkifaktorlu autentifikasiyanı aktiv etmək mümkün olmadı. Kodu yoxlayın və yenidən cəhd edin." +enterCode = "Tətbiqinizdən 6 rəqəmli kodu daxil edin." +enterCodeError = "Davam etmək üçün autentifikasiya kodunu daxil edin." +logout = "Çıxış" +openAuthenticator = "Google Authenticator, Authy və ya 1Password açın." qrCodeLoading = "QR kodunuz yaradılır…" +regenerateQrCode = "QR kodunu yenidən yarat" +scanQrCode = "QR kodunu skan edin və ya aşağıdakı quraşdırma açarını daxil edin." +setupError = "İkifaktorlu quraşdırmanı başlatmaq mümkün olmadı. Yenidən cəhd edin." +setupKey = "Quraşdırma açarı (əllə daxil etmə)" +stepByStep = "Addım-addım" +success = "MFA aktiv edildi. İndi davam edə bilərsiniz." +title = "Çoxfaktorlu autentifikasiya quraşdırması" [onboarding.planOverview] adminBodyLoginDisabled = "Giriş rejimini aktivləşdirdikdən sonra istifadəçiləri idarə edə, ayarları konfiqurasiya edə və serverin vəziyyətini izləyə bilərsiniz. Serverinizdə ilk {{freeTierLimit}} nəfər Stirling-dən pulsuz istifadə edir." @@ -5005,6 +5300,11 @@ adminTitle = "Admin icmalı" userBody = "Komanda yoldaşlarını dəvət edin, rollar təyin edin və sənədlərinizi bir təhlükəsiz iş mühitində səliqəli saxlayın. Tək istifadədən böyüməyə hazır olduqda giriş rejimini aktivləşdirin." userTitle = "Plan icmalı" +[onboarding.processorIntro] +body = "Stirling indi Siyasətlər işlədir — hər sənəd daxil olduqda onu təsnif edən, qoruyan və emal edən avtomatlaşdırılmış qaydalar. Onları qurun və icraları Processor-də izləyin." +cta = "Processor-ə baxın" +title = "Stirling Processor-ə baxın" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Öncədən ödənilmiş il" + +[payg.prepaid.banner] +exhausted = "Öncədən ödənilmiş tutumunuz bitib; ölçülü hesablaşma yenidən başladı. Endirimi saxlamaq üçün balansı artırın." +expiring_one = "Öncədən ödənilmiş iliniz {{count}} günə bitir. İstifadə olunmamış tutumun müddəti bitəcək — yeni il başlatmaq üçün balansı artırın." +expiring_other = "Öncədən ödənilmiş iliniz {{count}} günə bitir. İstifadə olunmamış tutumun müddəti bitəcək — yeni il başlatmaq üçün balansı artırın." +low = "Öncədən ödənilmiş tutumunuzun {{pct}}%-i qalıb. Emalı fasiləsiz saxlamaq üçün istənilən vaxt balansı artırın." + +[payg.prepaid.card] +subtitle = "Ölçülü hesablaşmadan əvvəl və xərc limitinizdən kənar istifadə olunur." +title = "Öncədən ödənilmiş tutum" + +[payg.prepaid.meter] +capSuffix = "{{total}} öncədən ödənilmiş kreditdən" +expires = "Bitmə tarixi {{date}}" + +[payg.prepaid.state] +exhausted = "Bitib" +healthy = "Kifayət qədər qalıb" +low = "Azalır" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF-i PDF/A-ya" tags = "arxiv,uzunmüddətli,standard,çevirmə,yaddaş,saxlama" title = "PDF-i PDF/A-ya" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "çap,standart,çevrilmə,istehsal,prepress,arxiv" title = "PDF-dən PDF/X-ə" @@ -5822,6 +6151,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Ehtiyaclarınıza uyğun planı seçin" title = "Mövcud Planlar" @@ -5937,49 +6267,12 @@ keyDescription = "E-poçtunuzdakı lisenziya açarını yapışdırın" success = "Lisenziya aktivləşdirildi!" successMessage = "Lisenziyanız uğurla aktivləşdirildi. Bu pəncərəni indi bağlaya bilərsiniz." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "tətbiq edildi" -enforcing = "Tətbiq edilir..." -failed = "Tətbiq etmə uğursuz oldu" outputsUnavailable = "Siyasət çıxışlarını yükləmək artıq mümkün deyil." partialOutputsUnavailable = "Bəzi siyasət çıxışlarını yükləmək artıq mümkün deyil." -retrying = "Məşğuldur, yenidən cəhd edilir..." runNotFound = "Tətbiq etmə icrası artıq tapılmır." -step = "addım {{current}}/{{total}}" timedOut = "Tətbiq etmə, icra tamamlanmadan vaxt aşımı oldu." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Bağla" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} tətbiq edilir" applyingProgress = "{{names}} tətbiq edilir ({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} fayldan {{failures}} fayl işlənə bilmədi və olduğ failureTitle = "Tam tətbiq edilmədən ixrac edildi" printPolicyAppliedBody = "Bu PDF siyasətə uyğunlaşdırmaq üçün yeniləndi. Dəyişiklikləri yoxlayın, sonra yenidən çap edin." printPolicyAppliedTitle = "Çapdan əvvəl siyasət tətbiq edildi" -queued = "+{{count}} növbədə" successTitle = "{{names}} tətbiq edildi" summaryMore = "{{first}}, {{second}} və daha {{more}}" summaryTwo = "{{first}} və {{second}}" -[policies.enforcement.triggerVerb] -convert = "Çevirmədən əvvəl tətbiq edilir" -default = "Tətbiq edilir" -export = "İxracdan əvvəl tətbiq edilir" -input = "İdxal zamanı tətbiq edilir" -print = "Çapdan əvvəl tətbiq edilir" - -[policies.field] -accessLog = "Giriş jurnalı" -archiveAfter = "Sonra arxivlə" -auditTrail = "Audit izi" -belowThreshold = "Həddən aşağı" -destination = "Təyinat" -frameworks = "Çərçivələr" -immutableHold = "Dəyişdirilməz saxlama" -keepFor = "Bu müddət saxla" -minConfidence = "Min etibar" -notify = "Yönləndirmədə bildir" -onViolation = "Uyğunsuzluq olduqda" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Əlavə et" -addPlaceholder = "Etiket əlavə et…" -customNote = "Komandanız üçün fərdiləşdirilib." -defaultNote = "Daxili defolt istifadə olunur, komandanızla paylaşılır." -duplicate = "\"{{name}}\" artıq mövcuddur." -edit = "Etiketləri redaktə et" -empty = "Hələ etiket yoxdur." -export = "JSON eksport et" -iconAria = "{{name}} üçün ikon seç" -import = "JSON import et" -importError = "Həmin faylı import etmək mümkün olmadı." -managedNote = "Komanda etiketləri komanda rəhbəriniz tərəfindən idarə olunur." -modalSubtitle = "Bütün komandanızla paylaşılır. Təsnifatçı hər sənədə uyğun etiketləri seçir." -modalTitle = "Təsnifat etiketləri" -pickIcon = "{{label}} ikonundan istifadə et" -removeAria = "{{name}} sil" -resetToDefault = "Defolta sıfırla" -saveForTeam = "Komanda üçün saxla" -saving = "Saxlanılır…" -sectionLabel = "Təsnifat etiketləri" -startFromScratch = "Sıfırdan başla" -teamCount = "komanda etiketləri" -tooLong = "Etiketlər ən çox {{max}} simvol ola bilər." -ungrouped = "Qruplaşdırılmamış" -view = "Etiketləri göstər" +categoryCount = "kateqoriyalar" +hideCategory = "Kateqoriyanı gizlət" +labelCount = "etiketlər" +sharedNote = "Bu etiketlər daxili quruludur və bütün komandanızla paylaşılır." +showCategory = "Kateqoriyanı göstər" [policies.pii] account = "Hesab nömrələri (etiketlənmiş)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "ABŞ routing nömrələri (ABA)" ssn = "Sosial Təminat nömrələri" -[policies.settings] -noneExport = "Hazırda eksport zamanı heç bir siyasət işləmir." -noneUpload = "Hazırda yükləmə zamanı heç bir siyasət işləmir." -onExport = "Eksport zamanı" -onUpload = "Yükləmə zamanı" -reorderHandle = "Sıralamaq üçün sürüklə" -runOrderDesc = "Eyni tetikləyicidə birdən çox siyasət işlədikdə, onlar bu ardıcıllıqla işləyir — hər biri əvvəlki siyasətin çıxışında. Sıralamaq üçün sürükləyin." -title = "Siyasət ayarları" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Siyasət seçimləri" -policySettings = "Siyasət ayarları" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Uğursuz siyasətləri yenidən cəhd et ({{count}})" -rowProgress = "{{total}} fayldan {{completed}} emal edildi" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Siyasət nədir?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Sosial Təminat və kart nömrələri kimi həssas detalları avtomatik tapır və qaraldır ki, sənəddə oxunmasın." -sanitize = "Fayldan gizli JavaScript-i silir ki, kimsə onu açanda heç nə avtomatik işləməsin." -watermark = "Hər səhifənin üzərinə görünən bir işarə (məs., \"Confidential\") qoyur." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Bağla" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} tətbiq edilir..." badgeRan = "{{name}} siyasəti bu faylda işlədi" +badgeRunning = "{{name}} işləyir..." blockingAction = "Siyasət tətbiq edilərkən {{action}} bloklandı, lütfən gözləyin..." dismiss = "Örtüyü bağla" enforcingTitle = "Siyasət tətbiq edilir..." @@ -6151,6 +6316,7 @@ viewAnyway = "Faylı göstər (siyasət hələ tətbiq edilir)" [policyOption] autoRedactPhi = "PHI-ni avtomatik redaktə et" blockExport = "Eksportu blokla" +ccpa = "CCPA" contracts = "Müqavilələr" documents = "Sənədlər" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor planı" subscribed = "Processor planı" unlinked = "Bağlanmayıb" -[portal.agentBuilder] -bootstrapFromDocument = "Sənəddən başlat" -sectionsAriaLabel = "Agent qurucusu bölmələri" -selectorAriaLabel = "Agentlər" -subtitle = "Sənədlərinizi təsnif edən, onlardan çıxarış edən və yönləndirən AI agentlərini dizayn edin, test edin və yayımlayın. Ssenarilər müəyyən edin, alət girişini məhdudlaşdırın, qızıl dəst işlədin və versiya dərc edin." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Ləğv et" -dropzoneText = "Nümunə sənəd seçin (PDF və ya şəkil)" -lead = "Nümunə sənəd buraxın, biz isə təkmilləşdirə biləcəyiniz ssenarilər və çıxarış sxemi təklif edək. Siz nəzərdən keçirməyənə qədər heç nə dərc edilmir." -submit = "Agenti başlat" -subtitle = "Bir təmsilçi fayldan yeni agent üçün əsas yaradın" -title = "Sənəddən başlat" - -[portal.agentBuilder.empty] -description = "Nümunə sənəddən agenti başladın ki, ssenariləri və çıxarış sxemi üçün əsas yaransın, sonra təkmilləşdirib dərc edin." -title = "Hələ agent yoxdur" - -[portal.agentBuilder.evals] -casesPassing = "Keçən hallar" -columnCase = "Qiymətləndirmə halı" -columnLatency = "Gecikmə" -columnResult = "Nəticə" -fail = "uğursuz" -goldenSetPassRate = "Qızıl dəst keçid faizi" -latencyMs = "{{ms}} ms" -notRun = "işlədilməyib" -pass = "keçdi" -passRate = "Keçid faizi" -runEvals = "Qiymətləndirmələri işə sal" - -[portal.agentBuilder.evals.empty] -description = "Qiymətləndirmələr ssenarilərinizi təkrarlana bilən qızıl dəstə çevirir. Keçid faizini zamanla izləmək və dərc etmələri onunla məhdudlaşdırmaq üçün yüksəldin." -title = "Hələ qızıl dəst yoxdur" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "qızıl dəstlər üzrə" -activeAgents = "Aktiv agentlər" -avgPassRate = "Orta qiymətləndirmə keçid faizi" -fleetWide = "bütün park üzrə" -latestPublished = "Son dərc edilən" -scenarios = "Ssenarilər" -testCases = "test halları" -totalDescription_one = "cəmi {{count}}" -totalDescription_other = "cəmi {{count}}" - -[portal.agentBuilder.scenarios] -add = "Əlavə et" -addScenario = "Ssenari əlavə et" -enable = "Aktiv et" -expectationLabel = "Gözlənilən davranış" -expectationPlaceholder = "Agent nə etməlidir" -inEval = "qiymətləndirmədə" -mute = "Səssizə al" -muted = "səssizə alınıb" -nameLabel = "Ad" -namePlaceholder = "məs. Uyğunluq eskalasiyası" - -[portal.agentBuilder.tabs] -evals = "Qiymətləndirmələr" -scenarios = "Ssenarilər" -tools = "Alətlər" -versions = "Versiyalar" - -[portal.agentBuilder.tools] -broadAccess = "Geniş giriş" -deniedHint = "Seçilmiş alətlər bloklanıb. Qalan hər şey çağırıla bilər." -deniedTools = "Rədd edilmiş alətlər" -governanceGate = "Alət idarəetməsi Enterprise planında mövcuddur." -restricted = "Məhdudlaşdırılıb" -restrictedAccess = "Məhdud alət girişi" -restrictedDescription = "Aşağıda rədd etdiklərinizdən başqa bütün alətlərə icazə verin." - -[portal.agentBuilder.versions] -current = "cari" -historyGate = "Tam versiya tarixçəsi və geri qaytarma Enterprise planında mövcuddur." -publish = "Dərc et" -rollBack = "Geri qaytar" - [portal.assistant] close = "Köməkçini bağla" error = "Köməkçiyə çatmaq mümkün olmadı." @@ -6365,9 +6451,21 @@ typing = "Yazır" loading = "Yüklənir" redirectingToEditor = "Redaktora yönləndirilir..." +[portal.billing.activation] +maybeLater = "Bəlkə sonra" +title = "Processor-i aktiv edin" + +[portal.billing.activation.payg] +sub = "Aylıq limit təyin edin. İşlətdiyiniz qədər ödəyin." +title = "İstifadə etdikcə ödə" + +[portal.billing.activation.prepay] +badge = "2 ay pulsuz" +sub = "Bir faktura, kart və ya bank köçürməsi." +title = "Bir ili öncədən ödə" + [portal.billing.checkout] noClientSecret = "Edge funksiyası client_secret qaytarmadı." -subtitle = "Pulsuz Editor planı limitinizi keçdikdən sonra davam etmək üçün kart əlavə edin. Qalanını Stripe idarə edir." title = "Processor planını aktiv edin" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Ödənişiniz uğurlu oldu, lakin aktivləşdirmə adətən olduğundan close = "Bağla" title = "Demək olar hazırdır" +[portal.billing.checkout.cap] +amountAria = "Aylıq xərc limiti" +back = "Geri" +continue = "Ödənişə davam et" +error = "Xərc limitinizi təyin etmək alınmadı" +finePrint = "Fakturalar aylıq göndərilir. İstənilən vaxt ləğv edin." +noLimit = "Limitsiz" +note = "Heç vaxt limitinizdən artıq hesablanmır. Emal sadəcə dayandırılır." +perMonth = "/ ay" +title = "Xərc limitinizi təyin edin" + +[portal.billing.checkout.card] +fields = "Kart nömrəsi · MM / YY · CVC · ZIP" +label = "Kart məlumatları" +note = "Kart məlumatları Stripe tərəfindən toplanır. Stirling heç vaxt PAN və ya CVC saxlamır." + [portal.billing.checkout.error] title = "Checkout başlatmaq mümkün olmadı" @@ -6383,10 +6497,9 @@ body = "Ödənişiniz keçdi. Bağlanmış instansiyalarınızda ölçülü emal hint = "Lütfən, bu pəncərəni açıq saxlayın." title = "Processor planınız aktivləşdirilir..." -[portal.billing.checkout.notConfigured] -bodyAfter = "portal env-də tətbiqdaxili checkout-u aktivləşdirmək üçün." -bodyBefore = "Ayarla" -title = "Stripe konfiqurasiya edilməyib" +[portal.billing.checkout.pay] +limit = "Aylıq xərc limiti {{amount}}/ay. Limitə çatdıqda emal dayandırılır." +limitNoCap = "Xərc limiti yoxdur. Emal dayandırılmayacaq. Fakturalar aylıq göndərilir." [portal.billing.enterpriseUpsell] cta = "Enterprise təklifinizi qurun" @@ -6409,10 +6522,12 @@ currentPlan = "Cari plan" freeForever = "Həmişə pulsuz" noTeamResolved = "Pul kisənizdə hələ komanda müəyyən edilməyib — yeniləyin və yenidən cəhd edin." ownerOnly = "Yalnız komanda sahibi Processor planını aktiv edə bilər." +payInvoice = "Tamamlamaq üçün fakturanı ödə" planName = "Editor" ssoIncluded = "SSO daxildir" switchOnProcessor = "Processor-u aktiv et →" unlimitedUsers = "Limitsiz istifadəçi" +viewQuote = "Təklifi göstər" [portal.billing.invoices] columnAmount = "Məbləğ" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Birbaşa API sorğuları" segmentApiLabel = "API" segmentAutomationDesc = "Avtomatlaşdırmalar və boru xətləri" segmentAutomationLabel = "Avtomatlaşdırma" +sizeMultiplier = "{{formatted}} PDF ölçü çarpanından istifadə etdi" +summary = "{{unique}} unikal · {{units}} sayğac vahidi · PDF başına orta {{avg}}" +summaryNoRate = "{{unique}} unikal · {{units}} sayğac vahidi" unit = "ölçülü PDF-lər" +unitsPending_one = "{{units}} sayğac vahidi əlaqələndirilmiş instansiyalardan sinxronizasiya gözləyir" +unitsPending_other = "{{units}} sayğac vahidi əlaqələndirilmiş instansiyalardan sinxronizasiya gözləyir" + +[portal.billing.prepaid] +capSuffix = "{{total}} öncədən ödənilmiş kreditdən" +expires = "Bitmə tarixi {{date}}" +eyebrow = "Öncədən ödənilmiş tutum" +note = "Ölçülü hesablaşmadan əvvəl və xərc limitinizdən kənar istifadə olunur." +topUp = "Balansı artır" + +[portal.billing.prepaid.buy] +back = "Geri" +calcTitle = "İllik ödənişinizi hesablayın" +cancel = "Ləğv et" +cancelPurchase = "Alışı ləğv et" +close = "Bağla" +continueToPayment = "Ödənişə davam et" +finalise = "Tamamla" +invoiceErrorTitle = "Fakturanı yaratmaq alınmadı" +loadingTitle = "Təklifiniz yüklənir" +payTitle = "İliniz üçün ödəniş edin" +step = "Addım {{current}} / {{total}}" + +[portal.billing.prepaid.calc] +change = "Dəyiş" +deployRow = "Yerləşdirmə" +done = "Hazır" +enterpriseHint = "Bu, enterprise miqyasındadır. Bu həcmdə enterprise tarifləri istənilən özünəxidmət endirimindən daha sərfəlidir. Tarif kilidi, şərtlər və dəqiqələr ərzində təklif." +governanceRow = "İdarəetmə" +governanceValue = "{{label}} · {{count}} siyasət" +handlesLabel = "Processor-iniz" +handlesValue = "{{volume}} kredit / ay emal edir" +pipelinesLabel = "Pipeline-lar" +policiesMeta = "{{count}} siyasət" +poolCaption = "İl üçün {{credits}} kreditdən ibarət bir hovuz. Ağır aylar yüngül aylardan borc götürür." +priceLabel = "İliniz · 10 ay qiymətinə 12 ay" +rateUnknown = "Dəqiq qiyməti checkout-da göstərəcəyik." +savings = "{{amount}} qənaət edirsiniz · 2 ay pulsuz." +sizingRow = "Ölçüləndirmə" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Ümumi istifadəçilər" + +[portal.billing.prepaid.consent] +label = "Anlayıram ki, öncədən ödənilmiş tutumum bitdikdə və ya 12 aydan sonra müddəti başa çatdıqda, ləğv etməsəm, emal standart ölçülü istifadə etdikcə ödə tarifi ilə (xərc limitimə qədər) avtomatik davam edəcək və istənilən vaxt billing portalından ləğv edə bilərəm." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling tərəfindən idarə olunur. Dəqiqələr ərzində hazırdır." +cloudValue = "Stirling Cloud · İdarə olunur" +selfhost = "Self-hosted" +selfhostDesc = "Şəxsi infrastrukturda işləyir. Eyni tarif." +selfhostValue = "Self-hosted · Şəxsi infrastruktur" + +[portal.billing.prepaid.offer] +cta = "Təklifi nəzərdən keçir" +subtitle = "PDF emalının bir ilini öncədən ödəyin və iki ay pulsuz əldə edin; ölçülü hesablaşmadan əvvəl, xərc limitinizdən kənar istifadə olunur." +title = "10 ay qiymətinə 12 ay əldə edin" + +[portal.billing.prepaid.pay] +accountName = "Hesab sahibinin adı" +accountNamePlaceholder = "məs. Jane Smith" +companyName = "Şirkət adı (istəyə bağlı)" +companyNamePlaceholder = "məs. Acme Inc." +downloadInvoice = "Fakturanı yüklə" +payOnline = "Onlayn ödə" +poNumber = "PO nömrəsi (istəyə bağlı)" +poNumberPlaceholder = "məs. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Təkmil" +advancedDesc = "Pipeline-lar emalınızın çoxunu idarə edir." +none = "Heç biri" +noneDesc = "Hələ Pipeline işlətmirsiniz. İstənilən vaxt aktiv edin." +standard = "Standart" +standardDesc = "Bir neçə Pipeline daxil olan PDF-ləri yenidən emal edir." + +[portal.billing.prepaid.posture] +essentials = "Əsaslar" +essentialsDesc = "Təsnifat, standart olaraq, üstəlik Paylaşım" +governed = "İdarə olunan" +governedDesc = "Təhlükəsizlik və Marşrutlaşdırma əlavə edir" +regulated = "Tənzimlənən" +regulatedDesc = "Bütün kateqoriyalar, o cümlədən Uyğunluq, Saxlama, Daxiletmə" + +[portal.billing.prepaid.review] +download = "Təklifi yüklə (PDF)" +downloading = "Hazırlanır…" +share = "təsdiq üçün paylaş" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "25 MB-dan kiçik fayllar, məlumat haqqı yoxdur" +heavy = "Ağır" +heavyDesc = "Skan edilmiş və ya şəkil-ağırlıqlı, adətən 50 MB+" +standard = "Standart" +standardDesc = "Əsasən kiçik, bəzi skanlar 25 MB-dan yuxarı" + +[portal.billing.prepaid.state] +exhausted = "Bitib" +healthy = "Kifayət qədər qalıb" +low = "Azalır" [portal.billing.spendLimit] adjustLimit = "Limiti tənzimlə" @@ -6494,6 +6713,8 @@ label = "Aşacağı proqnozlaşdırılır." [portal.billing.spendThisMonth] eyebrow = "Bu ay xərc" +freeRemaining_one = "{{formatted}} pulsuz PDF qalıb" +freeRemaining_other = "{{formatted}} pulsuz PDF qalıb" processed_one = "{{formattedCount}} PDF emal edildi." processed_other = "{{formattedCount}} PDF emal edildi." processedWithRate_one = "{{formattedCount}} PDF emal edildi, hər biri {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "Aylıq xərc limitinə çatıldı" title = "Stripe portalını açmaq mümkün olmadı" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} pulsuz PDF-dən istifadə olunub" capSuffix_other = "{{allowance}} pulsuz PDF-dən istifadə olunub" eyebrow = "Processor sınağı" @@ -6534,6 +6756,376 @@ signature = "imza" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Yeni bağlantı" +createTitleFor = "{{name}} qoş" +delete = "Sil" +edit = "Redaktə et" +editTitle = "Bağlantını redaktə et" +unsupportedType = "Bu bağlantı növü burada redaktə edilə bilməz." + +[portal.connections.categories.advanced] +description = "Hələ dəstəkləmədiyimiz xidmət üçün." +label = "Təkmil" + +[portal.connections.categories.audit] +description = "Sənədlə nə baş verdiyini auditorlarınızın artıq baxdığı yerdə qeyd edin." +label = "Audit və uyğunluq jurnalı" + +[portal.connections.categories.notify] +description = "Başqa sistemə nəyinsə baş verdiyini bildirin." +label = "Bildiriş və iş axını" + +[portal.connections.categories.security] +description = "Sənəd hərəkət etməzdən əvvəl onu skan edin və ya etiketləyin." +label = "Təhlükəsizlik və təsnifat" + +[portal.connections.categories.signing] +description = "Sənədi imzalanmaq və ya notarial təsdiq üçün göndərin." +label = "Elektron imza və notarial təsdiq" + +[portal.connections.categories.storage] +description = "Emal edilmiş sənədlərin getdiyi yer - bucket, drive, ticket və ya səhifə." +label = "Fayl və əlavə" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling bunu şifrəli saxlayır və bir daha göstərmir." +label = "Giriş tokeni" + +[portal.connections.commonFields.apiKey] +label = "API açarı" + +[portal.connections.commonFields.apiToken] +label = "API tokeni" + +[portal.connections.commonFields.baseUrl] +helperText = "Bu bağlantıdan istifadə edən addımlar yalnız bu URL altındakı yollara müraciət edə bilər, başqa host-a yox." +label = "Server URL-i" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Şifrə" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "İstifadəçi adı" + +[portal.connections.commonFields.webhookUrl] +helperText = "Xidmətin sizə verdiyi URL-i yapışdırın. Yalnız onun altındakı yollara müraciət edilə bilər." +label = "Webhook URL" + +[portal.connections.fields] +name = "Bağlantı adı" +namePlaceholder = "məs. Production" + +[portal.connections.picker] +cancel = "Ləğv et" +createNew = "Yeni bağlantı..." +placeholder = "Bağlantı seçin" +save = "Bağlantını saxla" + +[portal.connections.picker2] +back = "İnteqrasiyanı dəyiş" +heading = "Nəyə qoşulmaq istəyirsiniz?" +noResultsBody = "Bunu hələ dəstəkləmirik - bizə bu barədə məlumat verin və əlavə etməyi nəzərdən keçirək." +noResultsTitle = "\"{{query}}\" ilə uyğun heç nə yoxdur" +searchPlaceholder = "İnteqrasiyaları axtarın - \"sign\", \"ocr\" və ya məhsul adı sınayın" + +[portal.connections.types.api] +description = "Siyasətdən istənilən HTTP API-ni çağırın. URL-i, autentifikasiyanı və payload-u siz seçirsiniz." +label = "Fərdi API" + +[portal.connections.types.api.fields.authType] +label = "Autentifikasiya" + +[portal.connections.types.api.fields.authType.options.basic] +label = "İstifadəçi adı və şifrə" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Fərdi header-də token" + +[portal.connections.types.api.fields.authType.options.none] +label = "Heç biri" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Token üçün daxil ol" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Bu bağlantıdan istifadə edən addımlar yalnız bu URL altındakı yollara müraciət edə bilər, başqa host-a yox." +label = "Baza URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header adı" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Giriş yolu" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Şifrə" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API yükləmə linki ilə cavab verərsə, yalnız bu host-lar əldə edilə bilər. Vergüllə ayırın. Yalnız baza URL-in öz host-una icazə vermək üçün boş buraxın." +label = "İcazə verilən nəticə host-ları" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Tokeni bu header kimi göndər" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Giriş çağırışının tokeni qaytardığı cavab header-i, məsələn X-Auth-Token." +label = "Token cavab header-i" + +[portal.connections.types.api.fields.username] +label = "İstifadəçi adı" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "İşlətdiyiniz serverdə sənədləri zərərli proqramlara görə skan edin - heç nə şəbəkənizdən çıxmır." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Siyasətdə sənədlər üçün virus və məzmun skanı." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Yalnız məlum zərərli proqramları deyil, makroslar, icra edilə bilən fayllar, skriptlər və ya daxil edilmiş obyektlər olan sənədləri bloklayın." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Emal edilmiş sənədləri Confluence səhifəsinə əlavə edin." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Sənədləri elektron imza və notarial təsdiq üçün ConsignO Cloud (Notarius)-a göndərin." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "/api/v1 daxil olmaqla ConsignO Cloud mühitiniz." +label = "Server URL-i" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API açarı" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API sirri" + +[portal.connections.types.consigno.fields.password] +label = "Şifrə" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Yalnız təşkilatınız tenant-lardan istifadə edirsə lazımdır." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "İstifadəçi adı" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Siyasət sənədi emal etdikdə Discord-a mesaj göndərin." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Siyasət hər sənədi işlədikdə Elasticsearch-də audit hadisəsini indeksləyin." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Yeni sənədlər üçün FTP və ya FTPS serverini yoxlayın." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Bağlantı rejimi" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiv" +passive = "Passiv (tövsiyə olunur)" + +[portal.connections.types.ftp.fields.security] +helperText = "Adi FTP etimad məlumatlarını və faylları şifrələmədən göndərir; server dəstəkləyirsə FTPS seçin." +label = "Şifrələmə" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (açıq / AUTH TLS)" +implicit = "FTPS (gizli)" +none = "Heç biri (adi FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Siyasət işlədikdə Google Chat məkanına mesaj göndərin." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Emal edilmiş sənədləri Jira məsələsinə əlavə edin." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Siyasət işlədikdə sənəd və ya bildirişi email ilə göndərin." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Emal edilmiş sənədləri Nextcloud-a yerləşdirin." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Tətbiq şifrəsi" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "İşlətdiyiniz Presidio serverindən istifadə edərək sənəd mətnində şəxsi məlumatları aşkar edin." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "PDF-lərdə Microsoft Purview həssaslıq etiketlərini oxuyun və tətbiq edin." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "İstəyə bağlı. Yalnız etiketləri adla seçmək üçün lazımdır; etiketləmə onsuz da işləyir." +label = "Tətbiq (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client sirri" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Hər etiketə SiteId kimi vurulur ki, Purview-uyğun alətlər onu hansı təşkilatın tətbiq etdiyini bilsin." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Mənbələrin oxuya və pipeline çıxışlarının yaza biləcəyi S3 bucket." +label = "S3 yaddaş" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Giriş açarı ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS üçün boş buraxın. MinIO və ya başqa S3-uyğun yaddaş üçün bunu təyin edin." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Gizli giriş açarı" + +[portal.connections.types.sendgrid] +description = "Siyasət işlədikdə sənəd və ya bildirişi email ilə göndərin." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Yeni sənədlər üçün SFTP (SSH) serverini yoxlayın." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "İstəyə bağlı. Serverin SHA-256 host açarı barmaq izi (`ssh-keyscan host | ssh-keygen -lf -` nəticəsindən). Təyin edildikdə yalnız bu açarı təqdim edən server qəbul olunur; boş olduqda ilk qoşulmada görünən açar sabitlənir və sonrakı dəyişikliklər rədd edilir." +label = "Host açarı barmaq izi" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Açar passphrase-i" + +[portal.connections.types.sftp.fields.password] +helperText = "Əvəzində private key ilə autentifikasiya edirsinizsə boş buraxın." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "İstəyə bağlı. Şifrə əvəzinə açarla autentifikasiya etmək üçün OpenSSH private key yapışdırın." +label = "Private key" + +[portal.connections.types.slack] +description = "Siyasət sənədi emal etdikdə Slack-ə mesaj göndərin." +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS şəbəkə paylaşımını izləyin (Windows və ya Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Şəbəkə paylaşımı" + +[portal.connections.types.smb.fields.domain] +helperText = "İstəyə bağlı. Serveriniz istifadə edirsə Windows domain-i və ya iş qrupu." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Paylaşım" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Siyasət hər sənədi işlədikdə Splunk-a audit hadisəsi göndərin." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Sumo Logic HTTP kollektoruna audit hadisəsi göndərin." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Siyasət işlədikdə Teams kanalına mesaj göndərin." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Hər sənədi və ya sadəcə detalları seçdiyiniz URL-ə göndərin." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Siyasətdən Zap və ya Make ssenarisini tetikləyin." +label = "Zapier və Make" + +[portal.docs] +browse = "Sənədlərə bax" +viewSource = "Mənbəni GitHub-da göstər" + [portal.docs.authentication] codeCaption = "hər sorğu" eyebrow = "BAŞLANĞIC" @@ -6624,6 +7216,9 @@ eyebrow = "BACARIQLAR" lead = "Agentinizin tək alət kimi çağırdığı paketlənmiş, adlandırılmış imkanlar. Hər bacarıq əlavə edilmiş qiymətləndirmələri olan deterministik əməliyyat zənciridir." title = "Agent bacarıqları" +[portal.docs.toc] +title = "Bu səhifədə" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API İSTİNADI" @@ -6697,6 +7292,8 @@ type = "Növ" user = "İstifadəçi" [portal.documents.queue.empty] +connectSource = "Mənbə qoş" +createPipeline = "Pipeline yarat" description = "Mənbələr sənədləri pipeline-larınıza ötürdükcə onlar nəzərdən keçirmək üçün burada görünəcək." title = "Növbədə sənəd yoxdur" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Bu filtrə uyğun sənəd yoxdur." rowActions = "Sətir əməliyyatları" sensitiveLabel = "Həssas" -sensitiveTitle = "Həssas — giriş tələb olunur" [portal.documents.table.columns] action = "Pipeline / Əməliyyat" document = "Sənəd" +labels = "Labels" product = "Məhsul" status = "Status" time = "Vaxt" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Son görünmə" region = "Region" status = "Status" +target = "Target" version = "Versiya" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Bu görünüşdə gözlənilməz xəta baş verdi. Yenidən cəhd retry = "Yenidən cəhd et" title = "Bu səhifədə nəsə səhv getdi" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Geri" +body = "Desktop tətbiqini yükləyin və ya serveri self-host edin." +done = "Hazır" +guide = "{{name}} bələdçisini aç" +openInBrowser = "Əvəzinə brauzerdə aç" +sectionDesktop = "Desktop tətbiqi" +sectionSelfHosted = "Self-hosted" +title = "Stirling PDF Editor quraşdır" + +[portal.home.download.docker] +detailBody = "Rəsmi image-i serverinizdə işlədin." +note = ":8080-da açılır. Standart giriş: admin / stirling." +tagline = "Serveri konteynerdə işlədin" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standart" + +[portal.home.download.kubernetes] +detailBody = "Rəsmi Helm chart-ını quraşdırın." +note = "Ayarları saxlamaq üçün /configs üçün PVC əlavə edin." +tagline = "Helm ilə cluster-ə yerləşdirin" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Distribusiyanız üçün native build-lər." +guideBtn = "Linux quraşdırma seçimlərinə baxın" +note = "Ümumi distro-lar üçün AppImage, .deb və .rpm build-ləri mövcuddur." +tagline = "Distro-nuz üçün native desktop tətbiqi" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Və ya Homebrew vasitəsilə" +detailBody = "Quraşdırıcını işə salın və ya Homebrew istifadə edin." +downloadBtn = "macOS üçün endir" +tagline = "Apple silicon və Intel üçün native tətbiq" +title = "macOS" + +[portal.home.download.manual] +detailBody = ".jar faylını bare metal serverdə işə salın." +downloadBtn = ".jar faylını endir" +note = "Java 21+ tələb olunur. :8080-də açılır; standart giriş admin / stirling." +runLabel = "Sonra bunu işə salın" +tagline = ".jar faylını bare metal serverdə işə salın" +title = "Manual server quraşdırması" + +[portal.home.download.windows] +altLabel = "Və ya winget vasitəsilə" +detailBody = "Quraşdırıcını işə salın və ya paket meneceri istifadə edin." +downloadBtn = "Quraşdırıcını endir" +tagline = "Microsoft Windows üçün native tətbiq" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktiv" -invite = "Komanda yoldaşlarını dəvət et" +activeOfDeployed = "Bu ay {{total}}-dən {{active}} aktivdir" name = "Stirling PDF Editor" open = "Brauzerdə aç" updated = "{{time}} yeniləndi" +[portal.home.editor.deploy] +finish = "Yerləşdirməni tamamla" +options = "Yerləşdirmə seçimlərinə bax" +start = "Editor-u yerləşdir" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Öz hostinqinizdə · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Günortanız xeyir" evening = "Axşamınız xeyir" morning = "Sabahınız xeyir" -[portal.home.onboarding] -dismiss = "Quraşdırmanı bağla" -notStarted = "Başlanmayıb" -progress = "{{total}} tapşırıqdan {{done}} tamamlandı" -title = "Quraşdırmanı tamamlayın" - -[portal.home.onboarding.enterprise] -body = "Təşkilat üzrə SSO + SCIM + RBAC, öhdəlikli həcm qiymətləri və şəbəkədən təcrid olunmuş yerləşdirmə." -cta = "Sınağa başla" -ctaQuote = "Qiymət təklifi al" -lead = "250+ əməkdaş üçün." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Desktop tətbiqini yerləşdirin, həmişəlik pulsuz." -title = "PDF Editor endirin" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktiv · {{recommended}} tövsiyə olunur" -chip = "{{active}} aktiv" -title = "Siyasətləri aktivləşdirin" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} qoşulub · hər PDF düşdüyü yerdə idarə olunur" -chip = "{{connected}} qoşulub" -title = "Mənbələrinizi qoşun" - -[portal.home.quickActions] -subtitle = "Bu gün üçün əsas tapşırıqlar" -title = "Sürətli əməliyyatlar" - -[portal.home.quickActions.buildPipeline] -blurb = "Tipli əməliyyat kitabxanası üzərində 3 addımlı konstruktor" -title = "Pipeline qur" - -[portal.home.quickActions.connectSource] -blurb = "S3, agentlər, webhooks, izlənən qovluqlar" -title = "Mənbə qoş" - -[portal.home.quickActions.issueApiKey] -blurb = "Tezlik limitləri və IP allowlist ilə əhatəli açar" -title = "API açarı ver" - [portal.infrastructure] manageEditorDeployment = "Editor yerləşdirilməsini idarə et" sectionsAriaLabel = "İnfrastruktur bölmələri" @@ -6878,13 +7541,9 @@ heading = "API açarları" subheading = "Hər açar üzrə tezlik limitləri, icazələr və IP allowlist-ləri olan əhatəli giriş məlumatları." [portal.infrastructure.apiKeys.card] -allowedIps = "İcazəli IP-lər" -anyIp = "İstənilən IP (allowlist yoxdur)" created = "Yaradılıb" lastUsed = "Son istifadə" -permissions = "İcazələr" -rateLimit = "Tezlik limiti" -rateLimitValue = "{{value}} sorğu/dəq" +revoke = "Açarı ləğv et" usageMonth = "Bu ay istifadə" usageToday = "Bu gün istifadə" @@ -6892,6 +7551,15 @@ usageToday = "Bu gün istifadə" description = "Stirling API çağırmağa başlamaq üçün əhatəli açar yaradın." title = "Hələ API açarı yoxdur" +[portal.infrastructure.apiKeys.error] +load = "API açarlarınızı yükləmək mümkün olmadı. Zəhmət olmasa, yenidən cəhd edin." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" ləğv edilsin? Bu açarı hələ də istifadə edən hər hansı inteqrasiya dərhal 401 xətaları almağa başlayacaq. Bunu geri qaytarmaq mümkün deyil." +cancel = "Ləğv et" +confirm = "Açarı ləğv et" +title = "API açarını ləğv et" + [portal.infrastructure.audit] filterAriaLabel = "Audit hadisələrini kateqoriyaya görə filtrlə" heading = "Audit jurnalları" @@ -6935,6 +7603,7 @@ all = "Hamısı" auth = "Auth" config = "Konfiqurasiya" elevation = "Səlahiyyət artırımı" +policy = "Siyasət" processing = "Emal" security = "Təhlükəsizlik" @@ -6944,19 +7613,30 @@ title = "Girişiniz yoxdur" [portal.infrastructure.audit.metrics] config = "Konfiqurasiya" -elevation = "Səlahiyyət artırımı" +policy = "Siyasət" processing = "Emal" totalEvents = "Ümumi hadisələr · 24s" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Konfiqurasiya" +elevation = "Səlahiyyət artırma" +policy = "Siyasət" +processing = "Emal" +security = "Təhlükəsizlik" + +[portal.infrastructure.auditStatusLabel] +danger = "Xəta" +info = "Məlumat" +success = "Uğur" +warning = "Xəbərdarlıq" + [portal.infrastructure.createKey] cancel = "Ləğv et" createKey = "Açar yarat" done = "Hazırdır" -ipAllowlistHelper = "Vergüllə ayrılmış CIDR aralıqları. İstənilən IP-yə icazə vermək üçün boş buraxın." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Açar adı" keyNamePlaceholder = "məs. Production · ingest" -permissionsLabel = "İcazələr" secretKeyCaption = "Gizli açar" secretWarning = "Bunu secrets manager-də saxlayın. Stirling yalnız hash saxlayır — onu sonradan bərpa etmək mümkün deyil." subtitle = "Açarı lazım olan minimum əhatə ilə məhdudlaşdırın. İstənilən vaxt dövr edə və ya ləğv edə bilərsiniz." @@ -6964,207 +7644,9 @@ subtitleCreated = "Bu sirri indi kopyalayın — bir daha göstərilməyəcək." title = "API açarı yarat" titleCreated = "Açar yaradıldı" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/dəq" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Yerləşdirən" -environment = "Mühit" -product = "Məhsul" -status = "Status" -version = "Versiya" -when = "Nə vaxt" - -[portal.infrastructure.deployments.recent] -heading = "Son yerləşdirmələr" -subheading = "Məhsullar və mühitlər üzrə ən son rollout-lar." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instansiyalar" -latency = "Gecikmə" -load = "Yük" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Ötürücülük" -uptime = "İşləmə vaxtı" -version = "Versiya" - -[portal.infrastructure.deployments.regions] -heading = "Regionlar" -subheading = "Hər yerləşdirilmiş Stirling regionu üçün canlı sağlamlıq — gecikmə, yük və rollout versiyası." - -[portal.infrastructure.deployments.regions.empty] -description = "İş sahəniz təmin edildikdən sonra yerləşdirilmiş regionlar burada görünəcək." -title = "Yerləşdirilmiş region yoxdur" - -[portal.infrastructure.models] -heading = "Modellər" -msValue = "{{value}} ms" -subheading = "İş sahəniz üzrə sənəd emalını təmin edən model kataloqu və yönləndirmə." - -[portal.infrastructure.models.byom] -description = "On-prem və ya öz hostinqinizdə olan modeli qeydiyyatdan keçirin və data residency ilə bağlı emal üçün onu regiona bərkidin." -title = "Öz modelinizi gətirin" - -[portal.infrastructure.models.catalogue] -heading = "Kataloq" -sub = "İş sahəniz üçün əlçatan idarə olunan modellər, canlı gecikmə və xərclə." -subEnterprise = "İdarə olunan, özünüzün gətirdiyiniz və on-prem modellər — region üzrə bərkitmə imkanı ilə." - -[portal.infrastructure.models.catalogue.empty] -description = "İş sahənizin kataloqundakı modellər burada görünür." -title = "Əlçatan model yoxdur" - -[portal.infrastructure.models.columns] -cost = "Xərc" -latency = "Gecikmə" -load = "Yük" -model = "Model" -status = "Status" -type = "Növ" -version = "Versiya" - -[portal.infrastructure.models.metrics] -activeModels = "Aktiv modellər" -avgLatency = "Orta gecikmə" -included = "Daxildir" -monthlySpend = "Aylıq model xərci" - -[portal.infrastructure.models.routing] -empty = "Yönləndirmə qaydası konfiqurasiya edilməyib." -heading = "Yönləndirmə qaydaları" -sub = "Hər əməliyyatı hansı modelin idarə etdiyi. Daha dar qayda uyğun gəlmədikdə standart tətbiq olunur." -subLocked = "Əməliyyatları konkret modellərə yönləndirin — ödənişli planlarda əlçatandır." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Hər əməliyyatı və sənəd növünü hansı modelin idarə etdiyinə nəzarət etmək üçün Pro-ya yüksəldin." -title = "Model yönləndirməsi ödənişli funksiyadır" - -[portal.infrastructure.models.routingColumns] -default = "Standart" -docType = "Sənəd növü" -modelForAria = "{{operation}} üçün model" -operation = "Əməliyyat" -routedTo = "Yönləndirilib" - -[portal.infrastructure.security.access.byok] -description = "Öz KMS-inizdən açar təqdim edin. Stirling onunla şifrələyir, lakin yenə də oxuya bilir." -label = "Öz açarınızı gətirin (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Açarlar KMS-inizi heç vaxt tərk etmir. Stirling yalnız ciphertext saxlayır." -label = "Öz açarınızı saxlayın (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling şifrələmə açarlarını idarə edir. Ən sadə variant — sizin tərəfdə sıfır açar əməliyyatı." -label = "Stirling-də saxlanılan açarlar" - -[portal.infrastructure.security.accessPolicy] -heading = "Sənəd giriş siyasəti" -subheading = "Emal edilmiş sənədləri saxlanma vəziyyətində kimin deşifr edə biləcəyinə nəzarət edir." - -[portal.infrastructure.security.attestations] -heading = "Uyğunluq attestatları" -noReport = "Hesabat əlçatan deyil" -subheading = "Attestasiya edilmiş nəzarətlər üzrə hesabatlarla framework üzrə audit vəziyyəti." -viewReport = "Hesabata bax →" - -[portal.infrastructure.security.compliance] -heading = "Uyğunluq" -subheading = "Stirling platformasını əhatə edən attestatlar və sertifikatlar." - -[portal.infrastructure.security.empty] -description = "İş sahənizin təhlükəsizlik konfiqurasiyası burada görünəcək." -title = "Təhlükəsizlik vəziyyəti əlçatan deyil" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK ilə şifrələmə açarları KMS-inizi heç vaxt tərk etmir. Stirling yalnız istənilən vaxt ləğv edə biləcəyiniz ciphertext-i saxlayır və emal edir." -title = "Stirling sənədlərinizi deşifr edə bilmir" - -[portal.infrastructure.security.ipAllowlist] -empty = "IP aralığı konfiqurasiya edilməyib — bütün IP-lərə icazə verilir." -heading = "IP allowlist" -sub = "API girişi bu CIDR aralıqları ilə məhdudlaşdırılıb." -subLocked = "API girişini tanınmış IP aralıqları ilə məhdudlaşdırın — ödənişli planlarda əlçatandır." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "API girişini konkret şəbəkələrlə məhdudlaşdırmaq üçün Pro-ya yüksəldin." -title = "IP allowlisting ödənişli funksiyadır" - -[portal.infrastructure.security.ipColumns] -added = "Əlavə edilib" -addedBy = "Əlavə edən" -cidr = "CIDR" -label = "Etiket" - -[portal.infrastructure.security.keyManagement] -algorithm = "Alqoritm" -heading = "Şifrələmə açarlarının idarə edilməsi" -keyId = "Açar identifikatoru" -lastRotated = "Son dövr edilmə" -rotateKey = "Açarı dövr et" -rotationPolicy = "Dövr etmə siyasəti" -subheading = "Sənədləri saxlanma vəziyyətində şifrələyən açarların saxlanması — kim deşifr edə bilər və açarlar necə dövr edilir." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) və hold-your-own-key (HYOK) saxlanması Enterprise-də əlçatandır. Öz KMS-inizdən açarlar təqdim etmək üçün yüksəldin." -title = "Planınızda açarlar Stirling tərəfindən idarə olunur" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asiya-Sakit okean" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "Avropa İttifaqı" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Amerika Birləşmiş Ştatları" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "Sənədlərin saxlandığı və emal edildiyi yer." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} istifadə edilib" - -[portal.infrastructure.storage.empty] -description = "Qoşulmuş saxlama və istifadə burada görünəcək." -title = "Saxlama konfiqurasiya edilməyib" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktiv" -activeRange = "0–{{value}}g" -archived = "Arxivləşdirilib" -coldStorage = "cold storage" -deleted = "Silinib" -never = "heç vaxt" -purged = "təmizlənib" - -[portal.infrastructure.storage.providers] -connect = "Qoş" -connected = "Qoşulub" -heading = "Qoşulmuş provayderlər" -subheading = "Emal edilmiş artefaktların yazıldığı yer." - -[portal.infrastructure.storage.retention] -heading = "Saxlama müddəti" -subheading = "Artefaktların lifecycle silinməsindən əvvəl nə qədər saxlanıldığı." -windowLabel = "Standart saxlama pəncərəsi" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} gün" -days_other = "{{count}} gün" -never = "Heç vaxt silmə" - -[portal.infrastructure.storage.totalUsage] -heading = "Ümumi istifadə" -progressLabel = "İstifadə edilmiş saxlama" -subheading = "Bütün qoşulmuş provayderlər üzrə istifadə edilmiş saxlama." +revoked = "Ləğv edilib" [portal.infrastructure.tabs] apiKeys = "API açarları" @@ -7174,6 +7656,49 @@ models = "Modellər" security = "Təhlükəsizlik" storage = "Saxlama" +[portal.integrations] +availableHeading = "Mövcud" +comingSoonHeading = "Tezliklə" +connect = "Qoşul" +connectionCount_one = "{{count}} bağlantı" +connectionCount_other = "{{count}} bağlantı" +customApi = "Xüsusi API" +searchPlaceholder = "İnteqrasiyaları axtar" +subtitle = "Stirling-in danışa bildiyi hər şey. Məlumatlar burada bir dəfə yaradılır, sonra mənbələr, siyasətlər və pipeline-lar tərəfindən istinad edilir." +title = "İnteqrasiyalar" +typedName = "{{tool}} adı" + +[portal.integrations.filters] +all = "Hamısı" +audit = "Audit" +connected = "Qoşulub" +notify = "Bildiriş" +security = "Təhlükəsizlik" +signing = "İmzalama" +storage = "Yaddaş" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Qoşulub" + +[portal.integrations.table] +integration = "İnteqrasiya" +worksWith = "Bununla işləyir" + +[portal.integrations.worksWith] +pipelines = "Pipeline-lar" +policies = "Siyasətlər" +sources = "Mənbələr" + +[portal.legal] +draft = "{{label}} · qaralama" +loadError = "Bu sənədi yükləmək mümkün olmadı. Zəhmət olmasa, yenidən cəhd edin." +loading = "Yüklənir…" +title = "Hüquqi sənəd" + [portal.nav] agent-builder = "Agent Builder" components = "Komponentlər" @@ -7182,33 +7707,17 @@ documents = "Sənədlər" editor = "Editor" home = "Əsas səhifə" infrastructure = "İnfrastruktur" +integrations = "İnteqrasiyalar" pipelines = "Pipeline-lar" policies = "Siyasətlər" procurement = "Satınalma" -settings = "Parametrlər" sources = "Mənbələr" usage = "İstifadə və fakturalama" users = "İstifadəçilər" -[portal.notifications] -markAllRead = "Hamısını oxunmuş kimi işarələ" -title = "Bildirişlər" -viewAll = "Hamısına bax" - -[portal.notifications.ariaLabel] -none = "Bildirişlər, oxunmamış yoxdur" -unread_one = "Bildirişlər, {{count}} oxunmamış" -unread_other = "Bildirişlər, {{count}} oxunmamış" - -[portal.notifications.count] -allRead = "hamısı oxunub" -loading = "yüklənir" -new_one = "{{count}} yeni" -new_other = "{{count}} yeni" - -[portal.notifications.empty] -description = "Yeni bildiriş yoxdur." -title = "Hər şey qaydasındadır" +[portal.nav.section] +platform = "PDF Platforması" +processor = "PDF Prosessoru" [portal.pipelines] subtitle = "Backend-də hər avtomatlaşdırılmış sənəd pipeline-ı: mənbələr dəsti üzərində əməliyyatların sıralı zənciri, trigger ilə işə salınır. Addımlarını və mənbələrini görmək üçün sətirə klikləyin." @@ -7218,54 +7727,87 @@ title = "Pipeline-lar" newPipeline = "Yeni pipeline" [portal.pipelines.builder] -addStep = "Alət əlavə et" +activate = "Activate" back = "Pipeline-lara qayıt" +cannotFollow = "{{produced}} qəbul edə bilmir" +chooseAccount = "Hesab seçin" +chooseDestination = "Təyinat seçin" +chooseOperation = "Bu addımın nə edəcəyini seçin" +chooseSource = "Mənbə seçin" discard = "Dəyişiklikləri at" -enabled = "Aktivdir" +inputs = "Giriş" +inputSource = "Giriş mənbəyi" +inputTrigger = "Trigger" keepEditing = "Redaktəyə davam et" -needsUpload = "Yüklənmiş fayl tələb olunur" +moreActions = "More actions" +needsConfiguring = "Quraşdırma tələb olunur" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Axtarışınıza uyğun alət yoxdur." -pipelineSettings = "Pipeline parametrləri" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Alətləri axtar" -selectToolBody = "Pipeline qurmaq üçün alət əlavə edin." -selectToolTitle = "Hələ alət yoxdur" -toolSettings = "Alət parametrləri" +sendToSystem = "Başqa sistemə göndər" +stepsIncompatible = "Bu addımlar əvvəlki addımın yaratdığı ilə işləyə bilmir: {{tools}}." +stepsNeedSetup = "Saxlamadan əvvəl bu addımların hələ quraşdırılması lazımdır: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Tanınmayan əməliyyat, olduğu kimi saxlanıldı." unsavedBody = "Saxlanmamış dəyişiklikləriniz var. Çıxmadan əvvəl onları saxlayırsınız, yoxsa atırsınız?" unsavedTitle = "Saxlanmamış dəyişikliklər" -uploadUnsupported = "Yüklənmiş fayllar hələ pipeline-larda dəstəklənmir, buna görə bu addımlar saxlanıla bilməz: {{tools}}." usesDefaults = "Standart parametrlərlə işləyir" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Əvvəlki addımdan bütün faylları birləşdirir" +fan-out = "Əvvəlki addımdan hər fayl üçün bir dəfə işləyir" +format-mismatch = "{{accepts}} tələb olunur, lakin əvvəlki addım {{produced}} yaradır" +output-uncertain = "İşləməyə bilər: əvvəlki addımın çıxışı onun necə quraşdırılmasından asılıdır" +source-mismatch = "{{accepts}} tələb olunur, lakin bu pipeline-ın girişi {{produced}}-dir" +undeclared-operation = "Bu addımın nə qəbul etdiyini yoxlamaq olmur" [portal.pipelines.composer] addTool = "Alət əlavə et" -cancel = "Ləğv et" -chainEmpty = "Pipeline qurmağa başlamaq üçün alət əlavə edin." create = "Pipeline yarat" -directory = "Çıxış qovluğu" -directoryHelp = "Serverdə mütləq yol. Konfiqurasiya edilmiş icazəli qovluqlar daxilində olmalıdır." +createPaused = "Create paused" editingUnsupported = "Bu alət parametrlərinin redaktə üçün göstərilməsi hələ dəstəklənmir." -moveDown = "Aşağı köçür" -moveUp = "Yuxarı köçür" +editSource = "Edit source" name = "Ad" namePlaceholder = "məs. Redaction sweep" -noSources = "Hələ mənbə qoşulmayıb. Pipeline yenə də birbaşa təqdim edilən fayllar üzərində işləyə bilər." noToolSettings = "Bu alətin konfiqurasiya edilə bilən parametri yoxdur." -operations_one = "Əməliyyat ({{count}})" -operations_other = "Əməliyyatlar ({{count}})" output = "Çıxış" -removeStep = "Əməliyyatı sil" save = "Dəyişiklikləri saxla" scheduleEvery = "Hər" -sources = "Mənbələr" -sourcesLoading = "Mənbələr yüklənir..." trigger = "Trigger" triggerManual = "Yalnız manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "gün" hours = "saat" minutes = "dəqiqə" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" pipeline-ı silinsin? Bu əməliyyat geri qaytarıla bilməz." cancel = "Ləğv et" @@ -7273,28 +7815,62 @@ confirm = "Sil" title = "Pipeline silinsin?" [portal.pipelines.detail] +clearHistory = "Tarixçəni təmizlə" delete = "Pipeline-ı sil" run = "İndi işə sal" [portal.pipelines.empty] action = "Pipeline yarat" +connectSource = "Mənbə qoş" description = "İlk pipeline-ınızı yaradın: işləyəcəyi mənbələri seçin, əməliyyatları zəncirləyin və çıxışın hara gedəcəyini seçin." title = "Hələ pipeline yoxdur" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktiv" paused = "Dayandırılıb" total = "Pipeline-lar" -[portal.pipelines.output] -folder = "Qovluğa yaz" -inline = "Faylları qaytar" - [portal.pipelines.run] +allProcessed_one = "İşlədiləcək heç nə yoxdur: mənbənin {{count}} sənədi artıq emal edilib." +allProcessed_other = "İşlədiləcək heç nə yoxdur: mənbələrdəki bütün {{count}} sənəd artıq emal edilib." completed_one = "İşlətmə tamamlandı." completed_other = "Bütün {{count}} işlətmə tamamlandı." empty = "İşlətmək üçün heç nə yoxdur: mənbələrdə emal ediləcək sənəd yox idi." failed = "İşlətmə alınmadı: {{error}}" +inFlight = "İşlədiləcək yeni heç nə yoxdur: sənədlər əvvəlki işə salmadan hələ də emal olunur." +parked_one = "İşlədiləcək heç nə yoxdur: {{count}} sənəd əvvəl uğursuz olub və gözləməyə qoyulub. Səbəbi düzəldin, sonra yenidən cəhd etmək üçün tarixçəni təmizləyin." +parked_other = "İşlədiləcək heç nə yoxdur: {{count}} sənəd əvvəl uğursuz olub və gözləməyə qoyulub. Səbəbi düzəldin, sonra yenidən cəhd etmək üçün tarixçəni təmizləyin." running = "İşlətmə başladı; hələ davam edir." timeout = "İşlətmə gözləniləndən uzun çəkir; arxa planda hələ tamamlana bilər." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Mənbələr" status = "Status" steps = "Addımlar" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Qovluq izləmə" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Planlaşdırılmış" [portal.policies] +defaultName = "{{category}} Siyasəti" subtitle = "Hər sənədə alət pipeline-ını tətbiq edən daimi avtomatlaşdırmalar. Hər siyasət upload və ya export zamanı işə düşür, alət zəncirini icra edir və tətbiq edilmiş versiyanı orijinalın yanında saxlayır." title = "Siyasətlər" [portal.policies.card] comingSoon = "Enterprise-ə yüksəldin" notSetUp = "Qurulmayıb" +requiresAiEngine = "AI mühərriki tələb olunur" + +[portal.policies.categories.classification] +desc = "Yükləmə zamanı hər sənədin növünü müəyyən edin və arxivləşdirmə və axtarış üçün metadatasını etiketləyin." +label = "Təsnifat" [portal.policies.categories.compliance] desc = "Hər sənəddə HIPAA, GDPR, SOC 2 və ya FedRAMP tələblərini tətbiq edin." @@ -7344,6 +7927,13 @@ label = "Təhlükəsizlik" [portal.policies.config] scopeAll = "Bütün sənədlər" +[portal.policies.config.classification] +summary = "Hər yüklənmiş sənədi komandanızın etiketlərinə görə təsnif edir və metadatasını etiketləyir." + +[portal.policies.config.classification.rules] +0 = "Təsnif et" +1 = "Metadatanı etiketlə" + [portal.policies.config.compliance] summary = "Sənədlər sistemi tərk etməzdən əvvəl onları tənzimləyici framework-lərə qarşı yoxlayır." @@ -7371,6 +7961,29 @@ minConfidence = "Minimum etibarlılıq" 2 = "Adlandır" 3 = "Normallaşdır" +[portal.policies.config.purview] +label = "Microsoft Purview həssaslıq etiketi tətbiq et" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Etiket ID-si" +labelIdHelp = "Purview portalından etiketin GUID-i. Etiketin tətbiqi onun metadatasını PDF üzərinə yazır; şifrələyə bilməz, bunun üçün Microsoft müştərisi lazımdır." +labelName = "Etiket adı (istəyə bağlı)" +labelNameHelp = "ID ilə yanaşı qeyd olunur ki, insanlar və alətlər etiketi tanıya bilsin." + +[portal.policies.config.purview.fields.method] +help = "Purview siyasətin tətbiq etdiyi etiketi şəxsin seçdiyi etiketdən fərqləndirir." +label = "Etiket necə qeyd olunur" +privileged = "Şəxs tərəfindən seçilmiş kimi" +standard = "Avtomatik tətbiq edilmiş kimi" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview etiketini oxu" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Sənədin artıq daşıdığı etiketi oxuyur ki, sonrakı addımlar ona görə hərəkət edə bilsin." + [portal.policies.config.retention] summary = "Sənədlərin nə qədər saxlanacağını, nə vaxt arxivləşdiriləcəyini və nə vaxt silinəcəyini tətbiq edir." @@ -7438,6 +8051,7 @@ title = "Hələ fəaliyyət yoxdur" [portal.policies.endpoints] addWatermark = "Su nişanı" autoRedact = "PII redaktə et" +classifyAndLabel = "Təsnif et" compressPdf = "Sıxışdır" flatten = "Yastılaşdır" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Siyasətləriniz saxlanılıb və bağlantı bərpa olunanda gör retry = "Yenidən cəhd et" title = "Backend əlçatan deyil" +[portal.policies.operations] +change = "Bu addımın nə edəcəyini dəyiş" +noResults = "Heç bir addım buna uyğun gəlmir. Məhsul adı və ya \"scan\", \"notify\", \"attach\" sınayın." +searchPlaceholder = "Addımları axtarın - \"scan\", \"attach\" və ya məhsul adı sınayın" + +[portal.policies.operations.bodyMode] +binary = "Xam fayl baytları" +json = "JSON payload daxilində" +multipart = "Fayl yükləməsi kimi (multipart)" + +[portal.policies.operations.clamavScan] +description = "Öz ClamAV serveriniz sənədi yoxlayır; şəbəkənizdən heç nə çıxmır." +label = "Viruslara görə yoxla (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Makrolar, icra edilə bilən fayllar, skriptlər və ya yerləşdirilmiş obyektlər daşıyan sənədləri rədd edir." +label = "Aktiv məzmunu blokla" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive sənədi yoxlayır və təmiz deyilsə, icra dayanır." +label = "Viruslara görə yoxla" + +[portal.policies.operations.confluenceAttach] +description = "Emal edilmiş sənədi səhifəyə yerləşdirir." +label = "Confluence səhifəsinə əlavə et" + +[portal.policies.operations.consignoSubmit] +description = "Sənədi imzalamaq üçün ConsignO Cloud-a təqdim edir." +label = "İmzaya göndər (ConsignO)" +note = "Sənədi təqdim edir. İmzalanmış nüsxənin əldə edilməsi hələ dəstəklənmir, çünki addım nəticəni sonrakı addıma ötürə bilmir." + +[portal.policies.operations.customApiCall] +description = "Zəngi özünüz hazırlayın: path, method, body və cavabın emalı." +label = "Xüsusi API çağır" + +[portal.policies.operations.discordNotify] +description = "Siyasətin sənədi emal etdiyini kanala bildirir." +label = "Discord-a mesaj göndər" + +[portal.policies.operations.elasticIndex] +description = "Siyasətin nə etdiyini axtarıla bilən sənəd kimi qeyd edir." +label = "Audit hadisəsini Elasticsearch-də indekslə" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart upload API-lərinə uyğundur; JSON faylı payload daxilində istəyən API-lərə uyğundur; binary xam baytları göndərir." +label = "Sənədi necə göndərmək" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Olduğu kimi göndərilən JSON body. {{document.base64}} faylı daşıyır; {{document.*}} və {{run.*}} hər sənəd üçün doldurulur." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Hesab" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API-nin sənədi altında gözlədiyi forma sahəsi." +label = "Fayl sahəsinin adı" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Kimdən" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Header adı/dəyəri cütlərindən ibarət JSON obyekt. Bağlantının credential-ı sizin üçün əlavə olunur." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "İndeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue açarı" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Mesaj body-si kimi göndərilir. {{document.*}} və {{run.*}} hər sənəd üçün doldurulur." +label = "Mesaj" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Səhifə ID-si" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Bağlantının əsas URL-nə əlavə olunur. Heç vaxt başqa host-a çata bilməz." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Drive-da hara yazılacağı. {{document.filename}} hər sənəd üçün doldurulur." +label = "Təyinat yolu" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Hesabat sənədə toxunmur. Əvəz et qayıdanla dəyişdirir." +label = "Cavabla nə etmək" + +[portal.policies.operations.fields.signerEmail] +label = "İmzalayanın email-i" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Mövzu" +placeholder = "Emal edildi: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio bu mətni təhlil edir. {{document.title}} istifadə edin və ya yoxlanmasını istədiyiniz mətni yapışdırın." +label = "Yoxlanacaq mətn" + +[portal.policies.operations.fields.to] +label = "Kimə" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud istifadəçi adı" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Siyasətin sənədi emal etdiyini space-ə bildirir." +label = "Google Chat-a mesaj göndər" + +[portal.policies.operations.jiraAttach] +description = "Emal edilmiş sənədi issue-ya yerləşdirir." +label = "Jira issue-suna əlavə et" + +[portal.policies.operations.mailgunEmail] +description = "Emal edilmiş sənədi əlavə kimi göndərir." +label = "Sənədi email ilə göndər (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Emal edilmiş sənədi qovluğa yazır." +label = "Nextcloud-a yüklə" + +[portal.policies.operations.presidioAnalyze] +description = "Öz Presidio serveriniz tapdığı şəxsi məlumatları bildirir." +label = "Şəxsi məlumatları aşkar et" +note = "Presidio faylları deyil, mətni oxuyur. Yoxlanmasını istədiyiniz mətni verin — sənədin özü yüklənmir." + +[portal.policies.operations.responseMode] +replace = "Sənədi cavabla əvəz et" +report = "Sənədi saxla, cavabı qeyd et" + +[portal.policies.operations.sendgridEmail] +description = "Emal edilmiş sənədi əlavə kimi göndərir." +label = "Sənədi email ilə göndər (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Siyasətin sənədi emal etdiyini kanala bildirir." +label = "Slack-ə mesaj göndər" + +[portal.policies.operations.splunkEvent] +description = "Siyasətin nə etdiyini auditorlarınızın artıq baxdığı yerdə qeyd edir." +label = "Audit hadisəsini Splunk-a göndər" + +[portal.policies.operations.sumologicEvent] +description = "Siyasətin nə etdiyini HTTP collector-a qeyd edir." +label = "Audit hadisəsini Sumo Logic-ə göndər" + +[portal.policies.operations.teamsNotify] +description = "Siyasətin sənədi emal etdiyini kanala bildirir." +label = "Teams-ə mesaj göndər" + +[portal.policies.operations.webhookPost] +description = "Sənədin özünü seçdiyiniz URL-ə göndərir." +label = "Sənədi webhook-a göndər" + +[portal.policies.operations.zapierNotify] +description = "İcra detalları avtomatlaşdırmanıza ötürür." +label = "Zap və ya Make ssenarisini trigger et" + [portal.policies.stats] activeFor = "Aktiv" dataProcessed = "Emal edilmiş data" @@ -7473,6 +8282,13 @@ label = "Tətbiq edilmiş sənədlər" description = "Konfiqurasiya edilib, lakin işə düşmür" label = "Fasilədə" +[portal.policies.table] +appliesTo = "Buna tətbiq olunur" +docs = "Tətbiq edilmiş sənədlər" +enforces = "Tətbiq edir" +policy = "Siyasət" +status = "Status" + [portal.policies.wizard.actions] back = "Geri" cancel = "Ləğv et" @@ -7480,6 +8296,10 @@ continue = "Davam et" enablePolicy = "Siyasəti aktiv et" saveChanges = "Dəyişiklikləri saxla" +[portal.policies.wizard.capability.classify] +desc = "Komandanızın etiketlərindən sənədin növünü müəyyən edir və onu etiketləyir ki, kateqoriyaya görə arxivlənsin və axtarılsın." +label = "Sənədi təsnif et" + [portal.policies.wizard.capability.compress] desc = "Sənədi daha kiçik fayl ölçüsünə sıxışdırır." label = "Fayl ölçüsünü azalt" @@ -7500,10 +8320,18 @@ label = "Həssas məlumatları redaktə et" desc = "Gizli JavaScript-i silir ki, sənəd açıldıqda heç nə avtomatik işləməsin." label = "Aktiv məzmunu çıxar" +[portal.policies.wizard.capability.timestampPdf] +desc = "Müstəqil timestamp authority istifadə edərək sənədin müəyyən vaxtda məhz bu formada mövcud olduğunu sübut edir. Yalnız hash göndərilir - sənəd serverinizi heç vaxt tərk etmir." +label = "Etibarlı timestamp əlavə et" + [portal.policies.wizard.capability.watermark] desc = "Hər səhifəyə görünən nişan (məs. “Məxfi”) vurur." label = "Su nişanı tətbiq et" +[portal.policies.wizard.classification] +description = "Hər yüklənmiş sənəd daxili etiketlərə görə təsnif edilir və uyğun növlərlə etiketlənir. Etiket dəsti bütün komandanız üzrə paylaşılır." +labelsHeading = "Təsnifat etiketləri" + [portal.policies.wizard.errors] noTools = "Əvvəlcə iş axınında ən azı bir aləti aktiv edin." saveFailed = "Siyasəti saxlamaq mümkün olmadı. Lütfən, yenidən cəhd edin." @@ -7548,73 +8376,118 @@ setUp = "{{category}} siyasətini qur" [portal.policies.wizard.workflow] description = "Bu siyasətin emal etdiyi hər sənədə nə edəcəyini seçin." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktiv" -noRule = "Hələ heç bir qayda tətbiq edilmir" -subtitle = "Hansı pipeline-ın idarə etməsindən asılı olmayaraq, hər sənədin keçdiyi daimi avtomatlaşdırmalar." -title = "PDF-lərinizdə nə işləyir" - [portal.policySummary.action] -comingSoon = "Enterprise-ə yüksəldin" -configure = "Konfiqurasiya et" setUp = "Qur" -[portal.policySummary.column] -activeRule = "Aktiv qayda" -policy = "Siyasət" -status = "Status" +[portal.processorFlow] +footnote = "Saylar son 24 saat üzrədir. Axın sürəti illüstrativdir." +liveBadge = "Canlı" +stats = "{{connected}} qoşulub · {{processed}} PDF emal edilib" +title = "PDF Prosessoru" -[portal.policySummary.empty] -description = "Siyasətlər konfiqurasiya edildikdən sonra kateqoriyalar burada görünəcək." -title = "Hələ siyasət yoxdur" +[portal.processorFlow.lens] +ariaLabel = "Baxış rejimi" +flow = "Axın" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktiv" -off = "Söndürülü" +[portal.processorFlow.outcomes] +count = "{{n}} · 24s" +failed = "Uğursuz" +heading = "Nəticələr" +success = "Çatdırılıb" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktiv" +count = "{{n}} · 24s" +heading = "Siyasətlər" +setUp = "Quraşdır" soon = "Tezliklə" -[portal.processingStatus] -managePlan = "Planı idarə et" -volumeSuffix = "emal edilmiş PDF-lər · son 30 gün" +[portal.processorFlow.sankey] +waist = "Siyasətlər · {{n}} aktiv" + +[portal.processorFlow.sankey.empty] +description = "Sənədlərin prosessordan keçən axınını izləmək üçün mənbə qoşun və siyasəti aktiv edin." +title = "Hələ axın yoxdur" + +[portal.processorFlow.sources] +comingSoonTag = "Qoş" +editor = "Stirling PDF Editor" +heading = "Mənbələr" +perDay = "{{n}} / 24s" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email qəbulu" [portal.procurement] -reset = "Satınalmanı sıfırla (demo)" subtitle = "Komandanızın qiymətləndirilməsi, müqavilələşdirilməsi və işə qoşulmasını təmin edin. Hər sənəd bir yerdə." title = "Satınalma" -[portal.procurement.action] -download = "Endir" -pay = "İndi ödə" -request = "Sorğu göndər" -sign = "Nəzərdən keçir və imzala" -upload = "Yüklə" - [portal.procurement.agreement] agreeCta = "Razılaş və abunə ol" +confidential = "Məxfi" confirm = "Stirling Enterprise Agreement-i oxudum və razıyam." -eyebrow = "Razılaşma" -intro = "Bir birləşdirilmiş razılaşma sövdələşmənizi əhatə edir: Master Service Agreement, Order Form, EULA və Data Processing Agreement. Onu nəzərdən keçirin, sonra təklifi öhdəlikli abunəliyə qəbul etmək üçün razılaşın." -title = "Enterprise razılaşmanızı nəzərdən keçirin" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA və DPA bir imzada birləşdirilib." +download = "Endir" +downloadDraftError = "Razılaşma PDF-i yaratmaq mümkün olmadı - sənəd renderer-i əlçatan deyil. Zəhmət olmasa, bir az sonra yenidən cəhd edin və ya dəstək ilə əlaqə saxlayın." +downloadError = "İmzalanmış razılaşmanı endirmək mümkün olmadı. Zəhmət olmasa, yenidən cəhd edin." +legalName = "Hüquqi şəxsin adı" +legalNamePlaceholder = "İmzalayacaq hüquqi şəxs" +loadError = "Razılaşmanı yükləmək mümkün olmadı. Zəhmət olmasa, yenidən cəhd edin." +loading = "Razılaşma yüklənir..." +ref = "İstinad {{ref}} · {{version}}" +requestChanges = "Dəyişikliklər istə" +scrollHint = "Nəzərdən keçirmək üçün sürüşdürün" +signatory = "İmzalayanın adı" +signatoryPlaceholder = "Tam ad" +signatoryTitle = "İmzalayanın vəzifəsi" +signatoryTitlePlaceholder = "məs. Baş hüquqşünas" +signError = "İmzanızı qeyd etmək mümkün olmadı. Zəhmət olmasa, yenidən cəhd edin." [portal.procurement.builder] addons = "Əlavələr" +addressLine1 = "Ünvan sətri 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Ünvan sətri 2" +addressLine2Placeholder = "Suite, mərtəbə, bina" back = "Geri" businessName = "Biznes adı" businessNamePlaceholder = "Şirkətiniz" +city = "Şəhər" +cityPlaceholder = "San Francisco" +completeRequired = "Zəhmət olmasa, təklif yaratmadan əvvəl tələb olunan sahələri ( * ilə işarələnmiş) etibarlı email ilə tamamlayın." +contactEmail = "Əlaqə email-i" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Əlaqə şəxsi" +contactNamePlaceholder = "Jane Doe" continue = "Davam et" -country = "Ölkə" -countryEuro = "Avrozona (EUR €)" -countryUK = "Birləşmiş Krallıq (GBP £)" -countryUS = "Amerika Birləşmiş Ştatları (USD $)" +done = "Hazır" eula = "Stirling Enterprise EULA-nı oxudum və razıyam. O, bu təklifdən yaradılan razılaşmanı tənzimləyir." generate = "Təklif yarat" -included = "Daxildir" indemnification = "IP kompensasiyası" indemnificationSub = "EULA-ya əsasən uyğun IP iddialarını müdafiə edirik" -offlineLicense = "Offline / air-gapped lisenziya" -offlineLicenseSub = "Air-gapped self-hosted instansiya üçün endirilə bilən lisenziya faylı" +paperEyebrow = "Enterprise təklifi" +paperFor = "Bunun üçün hazırlanıb" +pdfSize = "PDF ölçüsü" +poNumber = "PO nömrəsi" +poNumberPlaceholder = "İstəyə bağlı" +postalCode = "Poçt indeksi" +postalCodePlaceholder = "94105" +posture = "İdarəetmə" +posture_count = "~{{count}} siyasət" +posture_essentials = "Əsaslar" +posture_essentialsSub = "Təsnifat + Paylaşım — standartlar" +posture_governed = "İdarə olunan" +posture_governedSub = "Təhlükəsizlik və Yönləndirmə əlavə edir" +posture_regulated = "Tənzimlənən" +posture_regulatedSub = "Hər kateqoriya — Uyğunluq, Saxlama, Qəbul" qbr = "Rüblük biznes icmalları" qbrSub = "SE-niz hər rüb istifadəni və yol xəritəsini nəzərdən keçirir" +region = "Ştat / region" +regionPlaceholder = "California" running = "{{annual}} / il · {{years}} illik {{tcv}}" s1Sub = "Komandanız və hər il emal etməyi gözlədiyiniz PDF-lər." s1Title = "Nə qədər emal edəcəksiniz?" @@ -7623,6 +8496,12 @@ s2Title = "Öhdəlik və xidmət" s3Sub = "Təklif və onun yaratdığı razılaşma üçün." s3Title = "Məlumatlarınız" serviceLevel = "Xidmət səviyyəsi" +size_compact = "Yığcam" +size_compactSub = "Əsasən mətn, 1 MB-dan az" +size_heavy = "Ağır" +size_heavySub = "Skan edilmiş və ya şəkil ağırlıqlı, 5 MB+" +size_standard = "Standart" +size_standardSub = "Qarışıq mətn və şəkillər, 1–5 MB" slDedicated = "Ayrılmış" slDedicatedSub = "4 iş saatı · ayrılmış hesab meneceri · +30%" slPriority = "Prioritet" @@ -7630,6 +8509,8 @@ slPrioritySub = "Eyni iş günü · adlı CSM · +15%" slStandard = "Standart" slStandardSub = "Növbəti iş günü · paylaşılan CSM · daxildir" stepOf = "Addım {{n}} / {{total}}" +taxId = "VAT / Vergi ID-si" +taxIdPlaceholder = "İstəyə bağlı" term = "Müddət" termDiscount = "{{pct}}% çoxillik öhdəlik endirimi tətbiq edildi" title = "Təklifinizi qurun" @@ -7637,6 +8518,7 @@ training = "İşə qoşulma və təlim" trainingSub = "Komandanızı işə salmaq üçün canlı sessiyalar" users = "Ümumi istifadəçilər" usersPlaceholder = "məs. 250" +viewEula = "Oxu" volEstimated = "{{count}} istifadəçidən təxmin edilib (hər biri ~2,000 PDF, avtomatlaşdırma daxil). Daha yaxşı bilirsinizsə, redaktə edin." volManual = "Sizin rəqəminiz istifadə olunur. İstənilən vaxt komanda ölçüsündən yenidən təxmin edin." volNoUsers = "Əmin deyilsiniz? Komanda ölçüsünü daxil edin, biz təxmin edək." @@ -7645,55 +8527,55 @@ volumePlaceholder = "məs. 1,000,000" years_one = "{{count}} il" years_other = "{{count}} il" -[portal.procurement.docs] -count_one = "{{count}} sənəd" -count_other = "{{count}} sənəd" -done = "Tamamlandı" -here = "Siz buradasınız" -hide = "Gizlət" -optional = "İstəyə bağlı" -paidAddon = "Ödənişli əlavə" -show = "Göstər" -subtitle = "Səyahətin hər addımında sizə lazım olan hər şey, sövdələşmə irəlilədikcə üzə çıxarılır." -supportingSubtitle = "SOC 2, təhlükəsizlik icmalları, vergi formaları və daha çoxu, təhlükəsizlik və ya satınalma komandanız soruşduqda hazırdır. Bəziləri birdəfəlik rüsum tələb edir." -supportingTitle = "Qiymətləndirmənizi dəstəkləyir" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form və DPA bir imzada." +download = "Endir" +eula = "EULA və Kommersiya Şərtləri" +eulaSub = "Proqram təminatı və self-serve kommersiya şərtləri." +invoice = "Faktura" +invoiceSub = "İlk abunə fakturanız." +laterInvoice = "Qəbul etdikdən sonra əlçatan olacaq" +laterQuote = "Təklifiniz verildikdən sonra əlçatan olacaq" +quote = "Təklif" +quoteSub = "Maddələnmiş enterprise təklifiniz." +sla = "SLA Əlavəsi" +slaSub = "Dəstək səviyyələri və cavab hədəfləri." +subprocessors = "Subprocessor-lar" +subprocessorsSub = "Sizin adınıza məlumatları emal edən üçüncü tərəflər." +subtitle = "Sövdələşmə sənədləriniz, istənilən vaxt əlçatan." title = "Sənədlər" -upcoming = "Qarşıdan gələn" +view = "Bax" [portal.procurement.error] title = "Nəsə səhv getdi" [portal.procurement.hero] -company = "Enterprise sövdələşməniz" +barAria = "Mərhələ {{current}} / {{total}}" ctaAgreement = "Razılaşmanı nəzərdən keçir və imzala" -ctaLive = "Siz canlıdasınız" -ctaPayment = "Ödəniş əlavə et" +ctaExploring = "Sınağınızı quraşdırın" ctaQuote = "Təklifinizi nəzərdən keçirin" +ctaReviewQuote = "Təklifi nəzərdən keçir" ctaTrial = "Təklifinizi qurun" +documents = "Sənədlər" eyebrow = "Enterprise satınalması" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Komanda yoldaşlarını dəvət et" -keyDocs = "Əsas sənədlər" -nextStep = "Növbəti addım: {{action}}" -notStarted = "Başlanmayıb" +licenseKey = "Lisenziya açarı" +liveSub = "Təşkilatınız təmin edilib və lisenziyanız aktivdir." +liveTitle = "Stirling Enterprise-də canlıdasınız" +next = "Növbəti: {{stage}}" open = "Satınalmanı aç" scheduleCall = "Zəng planlaşdır" -setup1Sub = "Komanda yoldaşlarınızı dəvət edin" -setup1Title = "PDF Editor-u yerləşdirin" -setup2Sub = "Redaktorlar və digər mənbələr üzrə emalı aktiv edin" -setup2Title = "PDF Processor-u qoşun" -setup3Sub = "Ehtiyac olduqda Təhlükəsizlik, Uyğunluq, Yönləndirmə və ya Saxlama funksiyalarını aktiv edin" -setup3Title = "Tövsiyə olunan siyasətləri əlavə edin" +sentenceAgreement = "razılaşmanı nəzərdən keçirin və imzalayın" +sentenceLive = "təminetmə və yayım" +sentencePayment = "satınalma sifarişi və ödəniş" +sentenceQuote = "təklifinizi nəzərdən keçirin və qəbul edin" +sentenceTrial = "sınaq iş məkanınızı quraşdırın" [portal.procurement.journey] daysLeft_one = "{{count}} gün qalıb" daysLeft_other = "{{count}} gün qalıb" -engineerLabel = "Həllər mühəndisiniz" -eyebrow = "Yayımınız" -live = "Stirling Enterprise-də canlıdasınız" -nextStep = "Növbəti addım: {{action}}" -subtitle = "Həllər mühəndisiniz hər addımdadır. Hər dəfə bir növbəti əməliyyat; tam yoxlama siyahısı aşağıdadır." -title = "Sınaqdan canlıya, bir yönləndirilmiş yol" -trialTitle = "Enterprise sınağı" [portal.procurement.journeySteps.agreement] blurb = "Bir imza MSA, order form, EULA və DPA-nı əhatə edir." @@ -7727,6 +8609,9 @@ downloadError = "Offline lisenziya faylını hələ yaratmaq mümkün olmadı downloadOffline = "Offline lisenziyanı endir (.lic)" hint = "Aktivləşdirmək üçün bu açarı self-hosted instansiyaya yapışdırın və ya qeydləriniz üçün saxlayın. Onu təhlükəsiz saxlayın." label = "Lisenziya açarınız" +subtitle = "Bu açarla self-hosted instansiyanı aktivləşdirin və ya qeydləriniz üçün saxlayın." +title = "Lisenziya açarınız" +trialFileHint = "Bu, sınaq lisenziya faylınızdır. Razılaşmanız qüvvəyə mindikdən sonra qayıdıb onu yenidən endirin — onlayn lisenziya açarından fərqli olaraq, .lic faylı öz-özünə yenilənməyəcək." [portal.procurement.link] cta = "Hesabı əlaqələndir" @@ -7739,65 +8624,62 @@ description = "Abunəliyiniz aktivdir və lisenziyanız verilib. Komandanız tə eyebrow = "Canlı" title = "Stirling Enterprise-də canlıdasınız" -[portal.procurement.locked] -description = "Sınaq açarları, öhdəlikli həcm təklifləri, bir imzalı razılaşma, ödəniş və sənəd reyestriniz Enterprise qiymətləndirməsinə başladıqdan sonra hamısı burada olur." -eyebrow = "Yalnız Enterprise" -talkToSales = "Satışla danışın" -title = "Satınalma xətti Enterprise ilə açılır" - [portal.procurement.milestone] -accept = "Qəbul et və davam et" -description = "PDF-i komandanızla paylaşmaq üçün endirin, hazır olanda qəbul etmək üçün geri qayıdın və ya dəyişiklik edin." -download = "PDF endir" downloadError = "Təklif PDF-ini hələ endirmək mümkün olmadı — lütfən, bir azdan yenidən cəhd edin." -edit = "Təklifi redaktə et" -eyebrow = "Təklif {{number}}" -perYear = " / il" -preparedFor = "{{company}} üçün hazırlanıb" -tcv = "{{value}} ümumi müqavilə dəyəri" -title = "Təklifiniz hazırdır" - -[portal.procurement.modal] -cancel = "Ləğv et" -chooseFile = "Fayl seç" -close = "Bağla" -downloadBody = "Endirməniz qısa müddətdə başlayacaq." -downloadCta = "Endir" -downloadTitle = "Endir" -noFile = "Fayl seçilməyib" -payBody = "Öhdəlikli müqavilənizi Stripe vasitəsilə kart və ya bank köçürməsi ilə ödəyin. Ödəniş təsdiqlənən kimi iş sahəniz təmin edilir." -payCta = "Stripe-a davam et" -payTitle = "Ödənişi təsdiqlə" -requestBodyFree = "Bunu tələb əsasında yaradırıq. Təsdiqləyin və həllər mühəndisiniz qısa müddətdə göndərəcək." -requestBodyPaid = "Bu ödənişli əlavədir. Təsdiqləyin və həllər mühəndisiniz onun əhatəsini müəyyən edib sənədləri göndərəcək." -requestCta = "Sorğu göndər" -requestTitle = "Bu sənədi tələb et" -signBody = "Stirling Enterprise Agreement-i e-imza üçün açır: bir imza MSA, order form, EULA və DPA-nı əhatə edir. Biz avtomatik qarşı imzalayırıq və siz ödənişə keçirsiniz." -signCta = "İmza üçün aç" -signTitle = "Razılaşmanızı nəzərdən keçirin və imzalayın" -uploadBody = "PO-nuzu bizə göndərin və şərtlərinizə uyğun olaraq ona əsasən faktura yazırıq. PDF-i sürükləyin və ya aşağıdan fayl seçin." -uploadCta = "Satınalma sifarişini yüklə" -uploadTitle = "Satınalma sifarişinizi yükləyin" [portal.procurement.payment] description = "Təklifiniz qəbul edilib və lisenziyanız artıq aktivdir — komandanız dərhal başlaya bilər. Hazır olanda ilk fakturanı ödəyin; onu burada ödəyə və ya endirə bilərsiniz, email lazım deyil." +downloadAgreement = "İmzalanmış razılaşmanı endir" downloadInvoice = "Fakturanı endir" -simulate = "Ödənişin alındığını simulyasiya et (demo)" +eyebrow = "Ödəniş" title = "Abunəlik yaradıldı" viewInvoice = "Fakturaya bax və ödə" +[portal.procurement.review] +acceptCta = "Təklifi qəbul et" +annual = "İllik haqq (1-ci il)" +downloadCta = "Təklifi endir" +poNumber = "Satınalma sifarişi: {{po}}" +renewal = "Müddətdən sonra {{amount}}/il ilə yenilənir (+{{pct}}% CPI)" +tcv = "{{years}} illik müddət, əvvəlcədən tam ödənilib: {{tcv}}" +validUntil = "{{date}} tarixinədək etibarlıdır" + [portal.procurement.schedule] fallback = "Planlaşdırıcını yükləmək mümkün olmadı." fallbackLink = "Planlaşdırmanı yeni tabda aç" +loading = "Planlayıcı yüklənir..." subtitle = "Həllər mühəndisiniz komandanızı yayım boyunca yönləndirəcək. Sizə uyğun vaxt seçin." title = "Zəng planlaşdır" -[portal.procurement.status] -action = "Əməliyyat tələb olunur" -available = "Əlçatandır" -complete = "Tamamlandı" -pending = "Gözləmədə" -request = "Sorğu əsasında" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Bağlı, outbound yoxdur" +back = "Geri" +businessName = "Biznes adı" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "İdarə olunan, ən sürətli başlanğıc" +continue = "Davam et" +deployment = "Harada yerləşdirilməlidir?" +eula = "Stirling EULA və Kommersiya Şərtləri ilə razıyam." +fullName = "Tam ad" +fullNamePlaceholder = "Adınız" +invites = "Komanda yoldaşlarını dəvət et · istəyə bağlı" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Zəng planlaşdır" +seats = "Test edən insanlar" +seatsPlaceholder = "məs. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker və ya Kubernetes" +start = "Sınağa başla" +stepOf = "Addım {{n}} / {{total}}" +subtitle = "14 gün pulsuz, kart tələb olunmur." +subtitleDetails = "Təklifiniz və razılaşmanız üçün bir neçə detal." +talkFirst = "Əvvəlcə danışmağa üstünlük verirsiniz?" +title = "Sınağınızı quraşdırın" +viewEula = "Oxu" +workEmail = "İş email-i" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Uzatma 7 gün əlavə edir və həllər mühəndisinizi xəbərdar edir." @@ -7808,109 +8690,113 @@ maxed = "Limitə çatıb" subtitle = "Pulsuz sınağınız {{date}} tarixinə qədər davam edir. Kart tələb olunmur." title = "Enterprise sınağı" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Öhdəlikli həcm qiymətləndirməsi, təşkilat üzrə SSO + SCIM + RBAC, 90 günlük dəyişməz audit və ayrılmış SE." -homeCta = "Sınağa başla →" -homeHeadline = "Milyonlarla PDF emal edin." - -[portal.recentActivity] -title = "Son fəaliyyət" -viewAll = "Hamısına bax" - -[portal.recentActivity.empty] -description = "Pipeline işləri, deploy-lar və agent hadisələri burada görünəcək." -title = "Hələ burada heç nə yoxdur" - -[portal.search] -ariaLabel = "Axtar" -placeholder = "Stirling-də axtar — son nöqtələr, pipeline-lar, sənədlər…" - -[portal.search.empty] -noActionsDescription = "Sürətli əməliyyatlar əlçatan olduqda burada görünəcək." -noActionsTitle = "Sürətli əməliyyat yoxdur" -noMatches = "\"{{query}}\" üçün uyğunluq yoxdur" -noMatchesDescription = "Fərqli açar söz sınayın və ya kataloqa baxın." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Hesab bağlantısı" -[portal.shell.header] -accountFallback = "Hesab" -accountMenu = "Hesab menyusu" -darkMode = "Qaranlıq rejim" -lightMode = "İşıqlı rejim" -search = "Axtar" -searchPlaceholder = "Axtar…" -signOut = "Çıxış et" -switchToDark = "Qaranlıq mövzuya keç" -switchToLight = "İşıqlı mövzuya keç" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} sənəd" linkAccount = "Stirling hesabını əlaqələndir" -planEditor = "Editor planı" -planEnterprise = "Enterprise planı" -planProcessor = "Processor planı" primaryNav = "Əsas naviqasiya" switchApp = "Tətbiqi dəyiş" +[portal.shell.topbar] +closeNav = "Naviqasiyanı bağla" +openNav = "Naviqasiyanı aç" +search = "Axtar" + [portal.sources] subtitle = "Sənədləri Stirling-ə ötürən təkrar istifadə edilə bilən giriş bağlantıları. Bağlantını bir dəfə konfiqurasiya edin, sonra istənilən sayda siyasətdə ona istinad edin. Konfiqurasiyası və hansı siyasətlərin istifadə etməsi üçün sətrə klikləyin." title = "Mənbələr" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Mənbə qoş" +[portal.sources.builder] +back = "Mənbələrə qayıt" +backToTypes = "Bütün mənbə növləri" +cancel = "Ləğv et" +chooseHint = "Sənədlərin haradan gələcəyini seçin. Bozlaşdırılmış connector-lar yoldadır." +comingSoon = "Tezliklə" +comingSoonHeading = "Tezliklə" +create = "Mənbə yarat" +createTitle = "Mənbə qoş" +delete = "Sil" +editTitle = "Mənbəni redaktə et" +enabled = "Aktivləşdirilib" +save = "Dəyişiklikləri saxla" + +[portal.sources.builder.folderAccess] +description = "Qovluq avtomatlaşdırmaları yalnız administratorun icazə verdiyi qovluqlardan istifadə edə bilər. Onu Qovluq Girişi ayarlarına əlavə edin, sonra yenidən cəhd edin." +openSettings = "Qovluq Girişi ayarları" +title = "Bu qovluğa icazə verilmir" + [portal.sources.delete] body = "\"{{name}}\" silinsin? Bu əməliyyatı geri qaytarmaq mümkün deyil. Ona istinad edən siyasətlər yenilənməlidir." cancel = "Ləğv et" confirm = "Sil" title = "Mənbə silinsin?" -[portal.sources.detail] -closeAriaLabel = "Detalı bağla" -delete = "Mənbəni sil" -docs24h = "Son 24 saat" -docs30d = "Son 30 gün" -docsTotal = "Ümumi görülən" -docsTrend = "Son 30 gündə sənədlər" -documents = "Sənədlər" -edit = "Redaktə et" -notReferenced = "Heç bir siyasət tərəfindən istinad edilmir, buna görə silmək təhlükəsizdir." -pause = "Fasilə ver" -resume = "Davam etdir" -subtitle = "{{type}} · {{status}}" -usedBy = "İstifadə edən" - -[portal.sources.empty] -description = "Qovluğu (və tezliklə cloud storage-i) qoşun ki, siyasətlərinizin sənədləri götürəcəyi yer olsun." -title = "Hələ heç bir mənbə qoşulmayıb" - [portal.sources.kpi] inUse = "İstifadədə" total = "Bağlantılar" unused = "İstifadə olunmur" +[portal.sources.networkFields.connection] +helperText = "Server ünvanı və credential-ları olan saxlanmış bağlantı. Ona istinad edən hər mənbə tərəfindən təkrar istifadə olunur." +label = "Bağlantı" + +[portal.sources.networkFields.directory] +helperText = "Serverdə yoxlanılacaq qovluq, login home və ya share root-a nisbətən. Root üçün boş buraxın." +label = "Qovluq" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume hər siyasət onu emal etdikdən sonra hər faylı serverdən silir." +label = "Oxuma rejimi" + +[portal.sources.networkFields.mode.options] +consume = "Consume: hər faylı bir dəfə emal et" +snapshot = "Snapshot: hər icrada qovluğu yenidən oxu" + +[portal.sources.networkFields.recursive] +label = "Qovluq dərinliyi" + +[portal.sources.networkFields.recursive.options] +all = "Alt qovluqları daxil et" +top = "Yalnız üst səviyyə" + [portal.sources.status] active = "Aktiv" disabled = "Deaktiv" unused = "İstifadə olunmur" [portal.sources.table] +documents = "Sənədlər" source = "Mənbə" status = "Status" +type = "Type" usedBy = "Siyasətlər" +[portal.sources.types.box] +description = "Komandanızın artıq paylaşdığı Box qovluqlarından sənədləri çəkin." +label = "Box" + +[portal.sources.types.dropbox] +description = "Paylaşılan Dropbox qovluğundan sənədləri çəkin." +label = "Dropbox" + [portal.sources.types.editor] +description = "Komandanızın editorda, siyasət və AI icraları üzrə emal etdiyi sənədlər." label = "Editor" +noPolicies = "Editordan hələ heç bir siyasət işə salınmayıb." + +[portal.sources.types.email] +description = "Xüsusi mailbox-a göndərilən əlavələri sənədlərə çevirin." +label = "Email inbox" [portal.sources.types.folder] description = "Yeni sənədlər üçün serverdəki kataloqu izləyin." @@ -7943,25 +8829,80 @@ label = "Qovluq dərinliyi" all = "Alt qovluqları daxil et" top = "Yalnız üst səviyyə" +[portal.sources.types.ftp] +description = "Yeni sənədlər üçün FTP və ya FTPS server qovluğunu yoxlayın." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Paylaşılan Google Drive qovluqlarından sənədləri çəkin." +label = "Google Drive" + +[portal.sources.types.network] +description = "Yeni sənədlər üçün SMB network share-i izləyin." +label = "Şəbəkə drive-ı" + +[portal.sources.types.onedrive] +description = "Yeni sənədlər üçün OneDrive qovluqlarını izləyin." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 və ya S3-uyğun bucket-dən sənədləri çəkin." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Bucket və credential-ları saxlayan bağlantı. Ona istinad edən hər mənbə və pipeline çıxışı tərəfindən təkrar istifadə olunur." +label = "Bağlantı" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume hər siyasət onu emal etdikdən sonra hər object-i bucket-dən silir." +label = "Oxuma rejimi" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: hər object-i bir dəfə emal et" +snapshot = "Snapshot: hər icrada bucket-i yenidən oxu" + +[portal.sources.types.s3.fields.prefix] +helperText = "Yalnız açarları bu prefix ilə başlayan object-lər emal olunur." +label = "Açar prefix-i" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Yeni sənədlər üçün SFTP drop qovluğunu yoxlayın." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint sənəd kitabxanalarından sənədləri çəkin." +label = "SharePoint" + [portal.sources.types.unknown] label = "Mənbə" +[portal.sources.types.webhook] +createNote = "Bu webhook-u yaratdığınız zaman delivery URL və signing secret yaradılır. Göndərənlər sənədləri həmin URL-ə POST edir və istinad edən siyasətlər gəlişdə işləyir." +description = "İstənilən xarici sistemdən imzalanmış HTTP POST ilə sənədləri qəbul edin." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Signing secret webhook yaradıldıqda bir dəfə göstərilir. Onu dəyişmək üçün mənbəni yenidən yaradın." + +[portal.sources.types.webhook.reveal] +copy = "Kopyala" +done = "Hazır" +secret = "Signing secret" +secretHelp = "Hər delivery-nin raw body-sini bu açarla imzalayın (HMAC-SHA256) və X-Stirling-Signature header-i kimi göndərin." +secretWarning = "Signing secret-i indi kopyalayın. Təhlükəsizliyiniz üçün o yalnız bir dəfə göstərilir və sonra əldə edilə bilməz." +title = "Webhook yaradıldı" +url = "Delivery URL" +usage = "Hər sənədi binary content type (application/pdf və ya application/octet-stream) ilə raw request body kimi delivery URL-ə POST edin. İstinad edən siyasətlər gəlişdə avtomatik işləyir." + [portal.sources.wizard] -back = "Geri" -cancel = "Ləğv et" -continue = "Davam et" -editTitle = "Mənbəni redaktə et" name = "Ad" namePlaceholder = "məs. İddiaların qəbulu" -save = "Dəyişiklikləri saxla" -subtitle = "Addım {{current}} / {{total}} · {{label}}" -title = "Mənbə qoş" type = "Növ" -[portal.sources.wizard.steps] -chooseType = "Növ seç" -configure = "Konfiqurasiya et" -review = "Nəzərdən keçir və qoş" +[portal.stepModal] +close = "Bağla" [portal.usage] managePayment = "Ödənişi idarə et" @@ -8021,16 +8962,9 @@ summary = "Komandaya sahibdir — onun üzvlərinin resurslarını və paylaşı 2 = "Defolt siyasət vasitəsilə portal girişi" 3 = "Member-in edə bildiyi hər şey" -[portal.welcome] -ariaLabel = "Stirling PDF-ə xoş gəlmisiniz" -badge = "Open-source" -installEditor = "Editor-u quraşdır" -inviteTeammates = "Komanda yoldaşlarını dəvət et" -openInBrowser = "Brauzerdə aç" -perks = "Həmişə pulsuz · Self-hostable" -subtitle = "Dünyanın ən təhlükəsiz PDF Editor-u bütün ölçülü komandalar üçün pulsuzdur. 60+ PDF əməliyyatı və SSO daxildir." -title = "Xoş gəlmisiniz" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Limitsiz" [printFile] title = "Faylı çap edin" @@ -8613,6 +9547,14 @@ text = "PDF səhifələrinizi 90 dərəcəlik addımlarla saat istiqamətində v [rotate.tooltip.header] title = "Döndürmə parametrlərinin icmalı" +[ruler] +clearAll = "Hamısını təmizlə" +hideAllLabels = "Bütün etiketləri gizlət" +hideSmallLabels = "Kiçik etiketləri gizlət" +physicalValues = "Fiziki" +scaled = "Miqyaslı" +showAllLabels = "Bütün etiketləri göstər" + [sanitize] desc = "PDF fayllarından potensial zərərli elementləri silin." sanitizationResults = "Sanitizasiya Nəticələri" @@ -8654,31 +9596,45 @@ label = "XMP Metaməlumatını sil" [sanitize.steps] settings = "Parametrlər" -[sanitizePdf] -tags = "təmiz,təhlükəsiz,güvənli,sil" - [sanitizePDF] title = "PDF-i Təmizlə" +[sanitizePdf] +tags = "təmiz,təhlükəsiz,güvənli,sil" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Səhifə miqyasını tənzimləyin" +[scaleSettings] +activeScale = "Aktiv miqyas" +apply = "Miqyası tətbiq et" +applyCalibration = "Kalibrləməni tətbiq et" +calculatedScale = "Hesablanmış miqyas" +calibrate = "Kalibrlə" +calibrating = "Kalibrlənir" +calibrationDistanceRequired = "Sıfırdan böyük həqiqi məsafə daxil edin" +calibrationInvalid = "Bu ölçmədən miqyası hesablamaq mümkün deyil" +calibrationPaperDistance = "Ölçülmüş səhifə məsafəsi: {{distance}}" +calibrationTitle = "Miqyası kalibrləyin" +calibrationTooltip = "Miqyası avtomatik hesablamaq üçün məlum məsafəni ölçün" +customScale = "Fərdi miqyas" +noneSet = "Fərdi miqyas təyin edilməyib" +presets = "Hazır miqyaslar" +ratio = "Miqyas nisbəti" +ratioHelp = "Nisbət: 1 səhifə vahidi = X həqiqi vahid" +ratioPlaceholder = "məs., 100" +ratioPositive = "Nisbət sıfırdan böyük olmalıdır" +ratioRequired = "Nisbət tələb olunur" +realDistance = "Həqiqi məsafə" +realDistancePlaceholder = "məs., 5" +reset = "Defolt ayarlara sıfırla" +unit = "Vahid" +unitInvalid = "Yanlış vahid: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Bucaq Aşağı Limiti:" -10 = "Faylda ağ sərhədlərin olmasının qarşısını almaq üçün əlavə ediləcək sərhədin ölçüsünü təyin edir (defolt: 1)." -2 = "Şəklin fırladılması üçün lazım olan minimal mütləq bucağı təyin edir (defolt: 10)." -3 = "Rəng Toleransı:" -4 = "Təxmin olunan arxaplan rənginin ətrafındakı rəng fərqliliyi intervalını təyin edir (defolt: 30)." -5 = "Minimal Sahə:" -6 = "Foto üçün minimal sahənin aşağı limitini təyin edir (defolt: 10000)." -7 = "Minimal Kontur Sahəsi:" -8 = "Fotonun kontur sahəsi üçün minimal aşağı limiti təyin edir" -9 = "Sərhəd Ölçüsü:" - [scannerImageSplit] submit = "Şəkil skanlarını çıxar" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Nə edir" whatThisDoesDesc = "Skan edilmiş səhifə və ya kompozit şəkildəki hər fotonu avtomatik aşkar edib çıxarır — əl ilə kəsmə tələb olunmur." whenToUse = "Nə zaman istifadə etməli" +[ScannerImageSplit.selectText] +1 = "Bucaq Aşağı Limiti:" +10 = "Faylda ağ sərhədlərin olmasının qarşısını almaq üçün əlavə ediləcək sərhədin ölçüsünü təyin edir (defolt: 1)." +2 = "Şəklin fırladılması üçün lazım olan minimal mütləq bucağı təyin edir (defolt: 10)." +3 = "Rəng Toleransı:" +4 = "Təxmin olunan arxaplan rənginin ətrafındakı rəng fərqliliyi intervalını təyin edir (defolt: 30)." +5 = "Minimal Sahə:" +6 = "Foto üçün minimal sahənin aşağı limitini təyin edir (defolt: 10000)." +7 = "Minimal Kontur Sahəsi:" +8 = "Fotonun kontur sahəsi üçün minimal aşağı limiti təyin edir" +9 = "Sərhəd Ölçüsü:" + [search] noResults = "Nəticə tapılmadı" placeholder = "Axtarış sözünü daxil edin..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Sizin Stirling-PDF serveriniz oflayndır və \"{{endp [session] expired = "Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin." -[sessionManagement.tooltip] -header = "İmzalama sessiyalarının idarə edilməsi" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Yeni iştirakçılar imzalama sırasının sonuna əlavə olunur" -bullet2 = "Sessiya yekunlaşdırıldıqdan sonra iştirakçı əlavə etmək olmaz" -bullet3 = "Növbəsi çatanda hər iştirakçıya bildiriş göndərilir" -description = "Yekunlaşdırmadan əvvəl istənilən vaxt aktiv sessiyaya daha çox iştirakçı əlavə edə bilərsiniz." -title = "İştirakçıların əlavə edilməsi" - -[sessionManagement.tooltip.finalization] -bullet1 = "Tam yekunlaşdırma: Bütün iştirakçılar imzalayıb" -bullet2 = "Qismən yekunlaşdırma: Bəzi iştirakçılar hələ imzalamayıb" -bullet3 = "İmzalamayan iştirakçılar yekun sənəddən çıxarılacaq" -bullet4 = "Yekunlaşdırdıqdan sonra imzalanmış PDF-i aktiv fayllara yükləyə bilərsiniz" -description = "Yekunlaşdırma bütün imzaları tək imzalanmış PDF-də birləşdirir. Bu əməliyyatı geri qaytarmaq olmaz." -title = "Sessiyanın Yekunlaşdırılması" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Artıq imzalamış iştirakçıları silmək olmaz" -bullet2 = "Silinən iştirakçılar artıq bildiriş almayacaq" -bullet3 = "İmzalama sırası avtomatik tənzimlənir" -description = "İştirakçılar imzalamadan əvvəl sessiyalardan silinə bilər." -title = "İştirakçıların Silinməsi" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Hər imza PDF-ə ardıcıllıqla tətbiq olunur" -bullet2 = "Sonrakı imzalayanlar əvvəlki imzaları görə bilir" -bullet3 = "Təsdiq axınları və hüquqi nəzarət zəncirləri üçün vacibdir" -description = "Sessiyanı yaradan zaman göstərdiyiniz sıra birincinin kim olacağını müəyyən edir." -title = "İmza Sırası" - [settings] close = "Bağla" +[settings.ai] +documents = "Sənədlər və RAG" +general = "Ümumi" +limits = "Limitlər və performans" +models = "Modellər və təminatçılar" +title = "AI" + [settings.configuration] advanced = "Qabaqcıl" database = "Məlumat bazası" endpoints = "Endpointlər" features = "Funksiyalar" +folderAccess = "Qovluğa giriş" mcp = "MCP Server" storageSharing = "Fayl Saxlama və Paylaşma" systemSettings = "Sistem parametrləri" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Standart PDF redaktoru" defaultPdfEditorActive = "Stirling PDF standart PDF redaktorunuzdur" defaultPdfEditorChecking = "Yoxlanılır..." defaultPdfEditorInactive = "Başqa tətbiq standart kimi təyin edilib" +defaultPdfEditorRemind = "Defolt olaraq təyin etməyi xatırlat" +defaultPdfEditorRemindDescription = "Stirling PDF defolt PDF tətbiqiniz olmadıqda banner göstər." defaultPdfEditorSet = "Artıq standartdır" defaultStartupView = "Başlanğıcda standart görünüş" defaultStartupViewDescription = "Tətbiq işə düşəndə sol sütunda hansı vərəqin aktiv olacağını seçin" @@ -8817,9 +9763,18 @@ languageDescription = "Görüntüləmə dilini seçin" logout = "Hesabdan çıx" setAsDefault = "Standart et" theme = "Mövzu" +themeAccent = "Vurğu rəngi" +themeAccentCustom = "Fərdi rəng" +themeAccentCustomHint = "Fərdi (defolt tövsiyə olunur)" +themeAccentDark = "Qaranlıq rejim vurğu rəngi" +themeAccentDefault = "Defolt" +themeAccentDefaultHint = "Defolt tema (tövsiyə olunur)" +themeAccentDescription = "Düymələr, linklər və vurğulamalar ona uyğunlaşır və o, tətbiqə incə rəng çaları verir. Açıq və qaranlıq rejimlərin hərəsinin öz rəngi var. Sistem hansı aktivdirsə, ondan istifadə edir." +themeAccentLight = "Açıq rejim vurğu rəngi" themeDark = "Tünd" themeDescription = "Açıq və tünd rejim arasında keçid et" themeLight = "Açıq" +themeReset = "Temanı defolt vəziyyətə bərpa et" themeSystem = "System" title = "Ümumi" user = "İstifadəçi" @@ -8833,6 +9788,12 @@ intro = "Təşkilatınız üçün istifadəçi identifikasiyası, komanda idarə learnMore = "Daha çoxu üçün sənədlərə baxın" title = "Sistem administratorları üçün" +[settings.general.loginLanding] +description = "Stirling Cloud-a daxil olduğunuzda hansı tətbiqin açılacağını seçin." +editor = "Redaktor" +processor = "Prosessor" +title = "Daxil olduqdan sonra" + [settings.general.mode] fullscreen = "Tam ekran" sidebar = "Yan panel" @@ -9013,9 +9974,6 @@ title = "Siyasətlər və Məxfilik" [settings.preferences] title = "Seçimlər" -[settings.search] -placeholder = "Ayar səhifələrində axtarın..." - [settings.security] description = "Hesabınızı təhlükəsiz saxlamaq üçün parolunuzu yeniləyin." title = "Təhlükəsizlik" @@ -9041,6 +9999,11 @@ security = "Təhlükəsizlik" telegram = "Telegram" title = "Təhlükəsizlik və Autentifikasiya" +[settings.signOut] +confirm = "Çıxış etmək istədiyinizə əminsiniz?" +submit = "Çıxış et" +title = "Çıxış" + [settings.team] title = "Komanda" @@ -9156,6 +10119,7 @@ last = "Son səhifə" maintainRatio = "Nisbətin qorunmasını dəyiş" next = "Növbəti səhifə" noSavedSigs = "Saxlanmış imza tapılmadı" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Şəxsi İmzalar" previous = "Əvvəlki səhifə" redo = "Yenidən et" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Arxa fonun silinməsi alınmadı" hint = "İmzanızın PNG və ya JPG şəklini yükləyin" label = "İmza şəklini yüklə" placeholder = "Şəkil faylı seç" +previewAlt = "Current image signature" processing = "Şəkil emal edilir..." removeBackground = "Ağ arxa fonu sil (şəffaf et)" @@ -9207,6 +10172,17 @@ saved = "Yuxarıdan saxlanmış imza seçin, sonra yerləşdirmək üçün PDF-i text = "Yuxarıda adınızı daxil etdikdən sonra, imzanızı yerləşdirmək üçün PDF üzərində istənilən yerə klikləyin." title = "İmza necə əlavə olunur" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "İmzanı hərəkət etdir" pause = "Yerləşdirməni dayandır" @@ -9653,10 +10629,30 @@ title = "Serverə yüklə" updateButton = "Serverdə yenilə" uploadButton = "Serverə yüklə" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "buraya klikləyin" +fallback = "Əgər avtomatik açılmadısa," +opening = "Swagger UI yeni tabda açılır..." tags = "api,sənədləşmə,swagger,endpointlər,inkişaf" title = "API sənədləşməsi" @@ -9764,12 +10760,39 @@ selectFilesHint = "Bu aləti işə salmaq üçün Aktiv Fayllarda faylları seç singleFileScope = "Yalnız buna tətbiq olunur: {{fileName}}" viewerMode = "Bir neçə fayl seçmək üçün fayl redaktoruna keçin." +[toolFormat] +ANY = "istənilən fayl" +CBR = "komiks arxivi (CBR)" +CBZ = "komiks arxivi (CBZ)" +CSV = "CSV faylı" +EBOOK = "elektron kitab" +EMAIL = "e-poçt" +EXCEL = "elektron cədvəl" +HTML = "veb səhifə" +IMAGE = "şəkillər" +JAVASCRIPT = "JavaScript faylı" +JSON = "JSON faylı" +MARKDOWN = "Markdown faylı" +NONE = "fayl yoxdur" +PCL = "PCL çap faylı" +PDF = "PDF" +PDF_ENCRYPTED = "parolla qorunan PDF" +POSTSCRIPT = "PostScript faylı" +PPT = "təqdimat" +TEXT = "mətn faylı" +VIDEO = "video" +WORD = "Word sənədi" +XML = "XML faylı" +XPS = "XPS sənədi" +ZIP = "ZIP arxivi" + [toolPanel] alpha = "Alfa" backToAllTools = "Bütün alətlərə qayıt" collapse = "Paneli yığ" expand = "Paneli genişləndir" goBack = "Geri qayıt" +pdfTools = "PDF alətləri" placeholder = "Başlamaq üçün alət seçin" premiumFeature = "Premium xüsusiyyət:" toolsHeader = "Alətlər" @@ -9806,11 +10829,6 @@ noToolsFound = "Alət tapılmadı" quickAccess = "TEZ GİRİŞ" searchPlaceholder = "Alətlərdə axtar..." -[toolPicker.categories] -advancedTools = "Qabaqcıl Alətlər" -recommendedTools = "Tövsiyə olunan Alətlər" -standardTools = "Standart Alətlər" - [toolPicker.subcategories] advancedFormatting = "Qabaqcıl Formatlama" automation = "Avtomatlaşdırma" @@ -9824,6 +10842,9 @@ removal = "Silmə" signing = "İmzalama" verification = "Yoxlama" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Heç bir alət tapılmadı" noTools = "Heç bir alət mövcud deyil" @@ -9950,28 +10971,53 @@ title = "İstifadəçilər" you = "(siz)" [users.action] +cancelInvite = "Dəvəti ləğv et" deleteTeam = "Komandanı sil" disableMfa = "MFA sıfırla" move = "Komandaya köçür" reinstate = "Bərpa et" remove = "Təşkilatdan çıxar" +removeTeam = "Komandadan sil" rename = "Komandanın adını dəyiş" resetPw = "Şifrəni sıfırla" suspend = "Dayandır" unlock = "Hesabı kiliddən çıxar" +[users.activity] +daysAgo = "{{count}}g əvvəl" +hoursAgo = "{{count}}s əvvəl" +justNow = "Elə indi" +minutesAgo = "{{count}}dəq əvvəl" +monthsAgo_one = "{{count}} ay əvvəl" +monthsAgo_other = "{{count}} ay əvvəl" +never = "Heç vaxt" +weeksAgo_one = "{{count}} həftə əvvəl" +weeksAgo_other = "{{count}} həftə əvvəl" +yearsAgo_one = "{{count}} il əvvəl" +yearsAgo_other = "{{count}} il əvvəl" + [users.cap] addProcessor = "+ Processor" approver = "Siyasəti təsdiqləyir" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}} ünvanına dəvət ləğv edilsin? Onlar cari linklə qoşula bilməyəcəklər." +cancelInviteTitle = "Dəvəti ləğv et" deleteTeamBody = "{{name}} komandası silinsin? Komanda əvvəlcə boş olmalıdır - üzvlərini başqa komandaya köçürün və o, hələ də heç bir inteqrasiya konfiqurasiyasına sahib olmamalıdır." deleteTeamTitle = "Komandanı sil" disableMfaBody = "{{name}} üçün MFA qeydiyyatı silinsin? Tələb olunarsa, növbəti girişdə yenidən quracaqlar." disableMfaTitle = "MFA sıfırla" removeBody = "{{name}} təşkilatdan həmişəlik çıxarılsın? Bu əməliyyatı geri qaytarmaq mümkün deyil." +removeTeamBody = "{{name}} komandadan silinsin? Hesabı qalacaq, lakin bu komandanın resurslarına girişini itirəcək." removeTitle = "Üzvü çıxar" [users.empty] @@ -9982,10 +11028,8 @@ title = "Hələ üzv yoxdur" addToTeam = "Komandaya əlavə et" guestCount = "{{count}} qonaq" guests = "Qonaqlar" -guestsDesc = "Xarici əməkdaşlar, paylaşdıqlarınızla məhdudlaşdırılıb. Yalnız Editor." ledBy = "{{owner}} tərəfindən idarə olunur" org = "Təşkilat" -orgDesc = "Təşkilat üzrə səlahiyyətə və siyasət təsdiqinə malik sahiblər" owners = "{{count}} sahib" team = "{{name}} komandası" teamMeta = "{{count}} nəfər" @@ -10022,6 +11066,18 @@ username = "İstifadəçi adı" usernameError = "İstifadəçi adı ən azı 3 simvoldan ibarət olmalıdır" usernamePlaceholder = "jsmith" +[users.invites] +by = "{{who}} tərəfindən dəvət olunub" +cancel = "Ləğv et" +expiresInDays_one = "Müddəti {{count}} gün sonra bitir" +expiresInDays_other = "Müddəti {{count}} gün sonra bitir" +expiresToday = "Müddəti bu gün bitir" +title = "Gözləmədə olan dəvətlər" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Backend-ə qoşularkən xəta baş verdi və ya giriş icazəniz yoxdur. Yenidən cəhd edin." title = "Üzvlər yüklənə bilmədi" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Qoşmalarda axtar" title = "Qoşmalar" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Əlfəcin başlığı" +bookmarkTitleRequired = "Əlfəcin başlığı tələb olunur" closeSidebar = "Əlfəcinlər yan panelini bağla" collapseAll = "Bütün əlfəcinləri yığ" +empty = "No bookmarks in this document" expandAll = "Bütün əlfəcinləri genişləndir" +searchPlaceholder = "Əlfəcinləri axtar" +title = "Bookmarks" [viewer.comments] addComment = "Şərh əlavə et" @@ -10236,10 +11297,12 @@ locateAnnotation = "Sənəddə tap" moreActions = "Daha çox əməl" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Səhifə {{page}}" placingHint = "Yerləşdirmək üçün bir səhifəyə klikləyin… (ləğv et)" removeCommentOnly = "Yalnız şərhi sil" saveReply = "Cavabı saxla" +searchPlaceholder = "Search comments" title = "Şərhlər" typeComment = "Şərh" typeInsertText = "Mətn əlavə et" @@ -10262,6 +11325,7 @@ unsavedDesc = "Yadda saxlanılmamış dəyişiklikləriniz var" closeSidebar = "Qatlar yan panelini bağla" hideAll = "Bütün qatları gizlət" showAll = "Bütün qatları göstər" +title = "Layers" [viewer.link] delete = "Keçidi sil" @@ -10605,30 +11669,6 @@ title = "Filigran mətni" image = "Şəkil" text = "Yazı" -[wetSignature.tooltip] -header = "İmza yaratma üsulları" - -[wetSignature.tooltip.draw] -bullet1 = "Qələm rəngini və qalınlığını fərdiləşdirin" -bullet2 = "Bəyənənədək silin və yenidən çəkin" -bullet3 = "Sensor qurğularda işləyir (planşetlər, telefonlar)" -description = "Siçan və ya sensor ekran vasitəsilə əl yazısı imza yaradın. Şəxsi, autentik imzalar üçün ən uyğundur." -title = "İmzanı çək" - -[wetSignature.tooltip.type] -bullet1 = "Bir neçə şriftdən seçin" -bullet2 = "Mətn ölçüsünü və rəngini fərdiləşdirin" -bullet3 = "Standartlaşdırılmış imzalar üçün idealdır" -description = "Yazılmış mətndən imza yaradın. Sürətli və ardıcıl, iş sənədləri üçün uyğundur." -title = "İmzanı yaz" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG və digər şəkil formatlarını dəstəkləyir" -bullet2 = "Ən yaxşı nəticə üçün şəffaf fon məsləhətdir" -bullet3 = "Şəkil imza sahəsinə uyğun ölçüləndiriləcək" -description = "Öncədən yaradılmış imza şəklini yükləyin. Skan edilmiş imzanız və ya şirkət loqonuz varsa idealdır." -title = "İmza şəklini yüklə" - [workbenchBar] activeFiles = "Aktiv fayllar" annotations = "Annotasiyalar" @@ -10642,6 +11682,7 @@ exitRedaction = "Gizlətmə rejimindən çıx" exportAll = "PDF-i ixrac et" exportSelected = "Seçilmiş səhifələri ixrac et" formFill = "Formu doldur" +hideToolbar = "Hide toolbar" multiTool = "Çoxfunksiyalı alət" panMode = "Sürüşdürmə rejimi" print = "PDF-i çap et" @@ -10652,6 +11693,7 @@ redact = "Gizlət" rotateLeft = "Sola çevir" rotateRight = "Sağa çevir" ruler = "Xətkeş / Ölçmə" +rulerSettings = "Miqyas ayarları" save = "Yadda saxla" saveAll = "Hamısını yadda saxla" saveAs = "Fərqli yadda saxla" @@ -10661,6 +11703,7 @@ selectAll = "Hamısını seç" selectByNumber = "Səhifə nömrələrinə görə seç" selectLanguage = "Dili seçin" share = "Paylaş" +showToolbar = "Show toolbar" toggleAnnotations = "Annotasiyaların görünməsini dəyiş" toggleAttachments = "Qoşmaları aç/bağla" toggleBookmarks = "Əlfəcinləri aç/bağla" diff --git a/frontend/editor/public/locales/bg-BG/translation.toml b/frontend/editor/public/locales/bg-BG/translation.toml index ea0e46cd1d..512acbe237 100644 --- a/frontend/editor/public/locales/bg-BG/translation.toml +++ b/frontend/editor/public/locales/bg-BG/translation.toml @@ -1,7 +1,6 @@ alphabet = "Азбука" apply = "Приложи" applyAndContinue = "Запази и излез" -areYouSure = "Сигурни ли сте, че искате да напуснете?" back = "Назад" black = "Черно" blue = "Синьо" @@ -46,10 +45,8 @@ filesSelected = "избрани файлове" font = "Шрифт" fontSizeTooltip = "Размер на текста за номера на страници в точки. По-големите числа правят по-голям текст." fontTypeTooltip = "Семейство шрифтове за номерата на страниците. Изберете спрямо стила на документа." -genericSubmit = "Подайте" green = "Зелено" help = "Помощ" -imgPrompt = "Изберете изображение(я)" incorrectPasswordMessage = "Текущата парола е неправилна." info = "Информация" invalidUndoData = "Не може да се отмени: невалидни данни за операцията" @@ -62,7 +59,6 @@ loginAgreementTitle = "Споразумение за влизане" logOut = "Изход" marginTooltip = "Разстоянието между номера на страницата и ръба на страницата." moreOptions = "Още опции" -multiPdfPrompt = "Изберете PDF (2+)" never = "Никога" no = "Не" noFileSelected = "Няма избран файл. Моля, качете един." @@ -80,7 +76,6 @@ pageOrderPrompt = "Персонализиран ред на страниците pages = "Страници" pageSelectionPrompt = "Персонализиран избор на страница (Въведете списък с номера на страници 1,5,6, разделени със запетая, или функции като 2n+1) :" password = "Парола" -pdfPrompt = "Изберете PDF(и)" pin = "Закачи файл (запазва активен след изпълнение на инструмента)" pro = "Pro" processingComplete = "Вашият файл е готов." @@ -110,6 +105,7 @@ undoSuccess = "Операцията е отменена успешно" unknown = "Непознат" unpin = "Откачи файл (замяна след изпълнение на инструмента)" unsavedChanges = "Имате незаписани промени във вашия PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Незаписани промени" unsupported = "Неподдържано" username = "Потребителско име" @@ -356,6 +352,7 @@ text = "Тези разрешения контролират какво мога title = "Промяна на правата" [addStamp] +preview = "Преглед на печата" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ръчни изтегляния: щракнете върху вр noLanguages = "Няма намерени tessdata езици в конфигурираната директория." permissionNotice = "Пътят към tessdata няма права за запис. Изтеглянията ще се отварят в браузъра; моля, запишете файловете .traineddata ръчно в папката tessdata." +[admin.settings.ai.documents] +description = "Конфигурирайте модела за вграждания и настройките за извличане, използвани за отговаряне на въпроси по документи. Прилага се към AI engine при запазване." +title = "Документи и RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Оставете празно, за да използвате собствените идентификационни данни от средата на engine. Прилага се за self-hosted внедрявания с един engine." +generic = "API key за вграждания" +openai = "OpenAI API key" +setPlaceholder = "Запазено - оставете празно, за да запазите текущия ключ" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Базов URL на OpenAI-съвместимия / Ollama endpoint за вграждания, напр. http://ollama:11434/v1. Трябва да сочи към доверен вътрешен endpoint (чувствително към SSRF)." +label = "Базов URL за вграждания" + +[admin.settings.ai.documents.embeddingModel] +description = "Име на модела за вграждания. Свободен текст; предложенията са само подсказки." +label = "Модел за вграждания" + +[admin.settings.ai.documents.embeddingProvider] +description = "Доставчик, използван за превръщане на текста от документа във векторни вграждания." +label = "Доставчик на вграждания" + +[admin.settings.ai.documents.maxSearches] +description = "Максимален брой търсения за извличане, които агентът може да изпълни за заявка." +label = "Макс. търсения" + +[admin.settings.ai.documents.reindexNote] +body = "Промяната на модела за вграждания влиза в сила незабавно, но документите, индексирани с предишния модел, трябва да бъдат преиндексирани, за да връща търсенето правилни резултати." +title = "Изисква се преиндексиране" + +[admin.settings.ai.documents.saved] +reindexNote = "Ако сте променили модела за вграждания, преиндексирайте съществуващите документи, за да използва търсенето новия модел." + +[admin.settings.ai.documents.topK] +description = "Брой най-релевантни фрагменти, извлечени при търсене." +label = "Top K" + +[admin.settings.ai.general] +description = "Свържете Stirling с Python AI engine и изберете кои AI възможности да са достъпни. Промените се прилагат при рестартиране." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Включвайте или изключвайте отделни AI функции. Изключените функции са скрити в приложението." +title = "Възможности" + +[admin.settings.ai.general.enabled] +description = "Главен превключвател. Когато е изключен, няма достъпни AI инструменти, агенти или извиквания към engine." +label = "Активиране на AI" + +[admin.settings.ai.general.features.chat] +description = "Разговорен асистент за работа с PDF файлове." +label = "Чат асистент" + +[admin.settings.ai.general.features.classify] +description = "Автоматично категоризиране на документи по тип или съдържание." +label = "Класификация на документи" + +[admin.settings.ai.general.features.createPdf] +description = "Генериране на нов PDF (напр. от HTML) чрез AI агент." +label = "Създаване на PDF от prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Задавайте въпроси и получавайте отговори, основани на качен документ." +label = "Въпроси по документ" + +[admin.settings.ai.general.features.mathAuditor] +description = "Преглед на документи за математически и числови грешки." +label = "Математически одитор" + +[admin.settings.ai.general.features.pdfComment] +description = "Добавяне на коментари за преглед и анотации, написани от AI, към PDF." +label = "PDF агент за коментари" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Време за изчакване за по-тежки операции на агента, като генериране на документи." +label = "Време за изчакване при дълги операции (секунди)" + +[admin.settings.ai.general.note] +body = "AI engine работи като отделна услуга. Неговата споделена тайна" +body2 = "се задава чрез променлива на средата в container. API keys на доставчиците могат да бъдат въведени на тези страници или предоставени като променливи на средата за engine; въведените тук ключове се изпращат към engine при запазване." +title = "Относно AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Време за изчакване за streaming (token-by-token) чат отговори." +label = "Време за изчакване на stream (секунди)" + +[admin.settings.ai.general.test] +button = "Тест на връзката" +failBody = "AI engine не отговори. Проверете URL, дали container-ът на engine работи и дали AI е активиран (след активиране е нужно рестартиране)." +failTitle = "AI engine е недостъпен" +okBody = "AI engine отговори на health check." +okTitle = "AI engine е достъпен" + +[admin.settings.ai.general.timeoutSeconds] +description = "Време за изчакване за стандартни AI заявки към engine." +label = "Време за изчакване на заявка (секунди)" + +[admin.settings.ai.general.url] +description = "Вътрешен URL на Python AI engine, напр. http://stirling-pdf-engine:5001." +label = "URL на AI engine" + +[admin.settings.ai.limits] +description = "Предпазни ограничения за това колко работа могат да извършват AI заявките и колко да се изпълняват едновременно. Прилага се към AI engine при запазване." +title = "Ограничения и производителност" + +[admin.settings.ai.limits.maxCharacters] +description = "Предпазно ограничение: отхвърля AI заявки, чийто извлечен текст надвишава тази дължина." +label = "Макс. знаци на заявка" + +[admin.settings.ai.limits.maxPages] +description = "Предпазно ограничение: отхвърля AI заявки над толкова PDF страници." +label = "Макс. страници на заявка" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Максимален брой едновременни активни извиквания на модели в целия engine." +label = "Макс. едновременност на модела" + +[admin.settings.ai.models] +description = "Изберете LLM доставчика и smart/fast моделите, които AI engine използва. Прилага се към AI engine при запазване." +title = "Модели и доставчици" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Оставете празно, за да използвате собствените идентификационни данни от средата на engine. Прилага се за self-hosted внедрявания с един engine." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Запазено - оставете празно, за да запазите текущия ключ" + +[admin.settings.ai.models.baseUrl] +description = "Базов URL на OpenAI-съвместимия / Ollama endpoint, напр. http://ollama:11434/v1." +label = "Базов URL на доставчика" +warning = "Базовият URL трябва да сочи към доверен вътрешен endpoint. Engine ще прави server-side заявки към него, затова недоверена стойност е чувствителна към SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Максимален брой изходни tokens за бързия модел." +label = "Макс. tokens за бърз модел" + +[admin.settings.ai.models.fastModel] +description = "По-евтин, по-бърз модел за леки задачи. Свободен текст; предложенията са само подсказки." +label = "Бърз модел" + +[admin.settings.ai.models.provider] +description = "С кой LLM доставчик комуникира engine." +label = "Доставчик" + +[admin.settings.ai.models.smartMaxTokens] +description = "Максимален брой изходни tokens за smart модела." +label = "Макс. tokens за smart модел" + +[admin.settings.ai.models.smartModel] +description = "Модел с високи възможности за сложно разсъждение. Свободен текст; предложенията са само подсказки." +label = "Smart модел" + +[admin.settings.ai.saved] +body = "Промените се изпращат автоматично към AI engine." +bodyNoPush = "Настройките са запазени. Те ще се приложат следващия път, когато AI engine зареди конфигурацията си." +title = "AI настройките са запазени" + [admin.settings.badge] clickToUpgrade = "Щракнете, за да видите подробности за плана" @@ -938,6 +1094,25 @@ label = "Генерирай наново при стартиране" description = "Брой дни, през които сертификатът ще е валиден" label = "Валидност на сертификата (дни)" +[admin.settings.folderAccess] +description = "Директории, от които папковите източници и папковите изходи могат да четат и в които могат да записват. Това е граница за сигурност: автоматизациите никога не могат да бъдат насочени към server път извън този списък." +securityNote = "Оставете това празно, за да деактивирате напълно папковите източници и изходи. Собствената конфигурационна директория на Stirling винаги е забранена." +title = "Достъп до папки" + +[admin.settings.folderAccess.implied] +description = "Тези управлявани от Stirling директории са винаги разрешени и не могат да бъдат променяни тук." +serverStorage = "Съхранение на файлове на server" +title = "Винаги разрешени" +watchedFolder = "Наблюдавана папка на pipeline" + +[admin.settings.folderAccess.roots] +add = "Добавяне" +empty = "Няма разрешени папки. Папковите източници и изходи в момента са деактивирани." +hint = "Въведете абсолютни пътища, например /data/inbox." +label = "Разрешени корени на папки" +placeholder = "/data/inbox" +remove = "Премахване на корен на папка" + [admin.settings.general] description = "Конфигурирайте настройки на приложението за цялата система, включително брандиране и поведение по подразбиране." system = "Система" @@ -1704,6 +1879,9 @@ width = "Ширина" [app] description = "Безплатната алтернатива на Adobe Acrobat (10M+ изтегляния)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Конвертиране в PDF/A-3b" convertToPdfA3bDescription = "Създава архивен PDF с вградени прикачени файлове" @@ -1842,6 +2020,15 @@ export = "Експорт" accessDenied = "Достъпът е отказан" insufficientPermissions = "Нямате разрешение да извършите това действие." +[auth.callback] +completing = "Завършване на удостоверяването" +invalidToken = "OAuth входът не бе успешен - невалиден token." +missingToken = "OAuth входът не бе успешен - не е получен token." +oauthFailed = "OAuth входът не бе успешен. Моля, опитайте отново." +pleaseWait = "Моля, изчакайте, докато завършим влизането ви." +signedOut = "Излязохте от профила си. Моля, влезте отново." +windowMayClose = "Можете да затворите този прозорец, след като завърши." + [auth.displayName] guest = "Гост" user = "Потребител" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Защитава PDF документи чрез прем description = "Този инструмент автоматично ще преименува PDF файлове според тяхното съдържание. Анализира документа, за да намери най-подходящото заглавие от текста." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Автоматично завъртане" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Праг на увереност" + +[autoRotate.detectionMode] +auto = "Автоматично" +osd = "Само OCR" +text = "Само текст" +title = "Метод на откриване" + +[autoRotate.error] +failed = "Възникна грешка при автоматичното завъртане на PDF." + +[autoRotate.inferUndetected] +title = "Завъртане на страници с ниска увереност за съвпадение" + +[autoRotate.report] +applied = "{{degrees}}° по часовниковата стрелка" +confidenceValue = "Увереност {{value}}" +noChange = "Без промяна" +pageLabel = "Страница {{number}}" +summary = "{{rotated}} от {{total}} страници завъртени, {{unchanged}} оставени без промяна" +title = "Отчет за откриването" + +[autoRotate.report.method] +inferred = "Съвпаднато" +none = "Пропуснато" +osd = "OCR" +text = "Текст" + +[autoRotate.report.note] +belowThreshold = "Под прага" +blankPage = "Страницата е празна" +inferredFromDocument = "Същото като другите страници" +noDominantDirection = "Смесени посоки на текста" +osdFailed = "Не е намерен четим текст" +osdNoVerdict = "Не може да се определи правилната ориентация" +tesseractUnavailable = "OCR не е инсталиран" +tooFewGlyphs = "Твърде малко текст" + +[autoRotate.results] +title = "Резултати от автоматичното завъртане" + +[autoRotate.settings] +title = "Настройки" + +[autoRotate.tooltip.confidenceThreshold] +text = "Колко сигурна трябва да е проверката, преди страница да бъде завъртяна. Намалете го, за да завъртате повече страници, или го увеличете, за да оставяте повече страници непроменени." +title = "Праг на увереност" + +[autoRotate.tooltip.description] +text = "Определя коя страна на всяка страница трябва да е нагоре и я завърта правилно. Страниците, които вече са изправени, се оставят без промяна." + +[autoRotate.tooltip.detectionMode] +text = "Автоматично първо чете текста на страницата, след което разглежда страницата като изображение, ако това не е достатъчно. Само текст пропуска проверката на изображение, което е най-бързо за документи, които никога не са били сканирани. Само OCR третира всяка страница като изображение." +title = "Метод на откриване" + +[autoRotate.tooltip.header] +title = "Автоматично завъртане" + +[autoRotate.tooltip.inferUndetected] +text = "Празни или почти празни страници не могат да бъдат проверени самостоятелно. Когато всяка прочетена страница се нуждае от едно и също завъртане, тези страници се завъртат, за да съвпаднат с тях." +title = "Завъртане на страници с ниска увереност за съвпадение" + [autoSizeSplitPDF] tags = "pdf,разделяне,документ,организация" @@ -2189,33 +2443,6 @@ keywords = "Ключови думи: odd, even" numbers = "Числа/диапазони: 5, 10-20" progressions = "Прогресии: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Видове сертификати" - -[certificateChoice.tooltip.organization] -bullet1 = "Управляван от системни администратори" -bullet2 = "Споделян между оторизирани потребители" -bullet3 = "Представлява идентичността на компанията, а не на индивида" -bullet4 = "Най-подходящ за: Официални документи, екипни подписи" -description = "Споделен сертификат, предоставен от вашата организация. Използва се за подписване от името на компанията." -title = "Сертификат на организацията" - -[certificateChoice.tooltip.personal] -bullet1 = "Генерира се автоматично при първа употреба" -bullet2 = "Привързан към вашия потребителски акаунт" -bullet3 = "Не може да се споделя с други потребители" -bullet4 = "Най-подходящ за: Лични документи, индивидуална отговорност" -description = "Автоматично генериран сертификат, уникален за вашия акаунт. Подходящ за индивидуални подписи." -title = "Личен сертификат" - -[certificateChoice.tooltip.upload] -bullet1 = "Изисква P12/PFX файл и парола" -bullet2 = "Може да бъде издаден от външни сертифициращи органи" -bullet3 = "По-високо ниво на доверие за правни документи" -bullet4 = "Най-подходящ за: Правно обвързващи договори, външна валидация" -description = "Използвайте собствен PKCS#12 сертификатен файл. Осигурява пълен контрол върху свойствата на сертификата." -title = "Качване на потребителски P12" - [certSign] awaitingSignatures = "Изчакват се подписи" chooseCertificate = "Изберете файл със сертификат" @@ -2542,34 +2769,6 @@ server = "Сървър" stepTitle = "Източник на сертификата" upload = "Качване" -[certSign.tooltip.header] -title = "За управлението на подписи" - -[certSign.tooltip.overview] -bullet1 = "Проверявайте съществуващи подписи и тяхната валидност" -bullet2 = "Вижте детайлна информация за подписващи и сертификати" -bullet3 = "Добавяйте нови цифрови подписи за защита на документите" -bullet4 = "Поддържа множество файлове с лесна навигация" -text = "Този инструмент ви позволява да проверявате дали вашите PDF-и са цифрово подписани и да добавяте нови цифрови подписи. Цифровите подписи доказват кой е създал или одобрил документ и показват дали е променян след подписване." -title = "Какво може този инструмент?" - -[certSign.tooltip.signing] -bullet1 = "Поддържа формати PEM, PKCS12, JKS и сървърен сертификат" -bullet2 = "Опция за показване или скриване на подписа в PDF" -bullet3 = "Добавете причина, местоположение и име на подписващия" -bullet4 = "Изберете на коя страница да поставите видими подписи" -bullet5 = "Използвайте сървърния сертификат за простата опция 'Sign with Stirling-PDF'" -text = "За да подпишете PDF, ви е необходим цифров сертификат (като PEM, PKCS12 или JKS). Можете да изберете подписът да е видим в документа или да остане невидим само за сигурност." -title = "Добавяне на подписи" - -[certSign.tooltip.validation] -bullet1 = "Показва дали подписите са валидни или невалидни" -bullet2 = "Показва информация за подписващия и дата на подписване" -bullet3 = "Проверява дали документът е модифициран след подписване" -bullet4 = "Може да използва потребителски сертификати за проверка" -text = "При проверка инструментът показва дали подписите са валидни, кой е подписал документа, кога е подписан и дали документът е променян след подписване." -title = "Проверка на подписи" - [changeCreds] changeUsername = "Актуализирайте потребителското си име. След актуализиране ще бъдете изведени от профила." credsUpdated = "Акаунтът е актуализиран" @@ -2737,12 +2936,16 @@ text = "За да направите тези разрешения непром [chat.actions] copy = "Копирайте съобщение" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Опции на агента Stirling" +assistant = "AI Assistant" clearChat = "Изчистете чата" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Тази операция ще използва вашите обла tooltip = "Изберете цвят от екрана" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Изберете цвят" [common] +actions = "Actions" back = "Назад" cancel = "Отказ" close = "Затвори" +codeSample = "Code sample" collapse = "Свий" confirm = "Потвърди" continue = "Продължи" copied = "Копирано!" copy = "Копирай" done = "Готово" +enterValue = "Enter value" error = "Грешка" expand = "Разгъни" loading = "Зареждане..." next = "Напред" +open = "Open" preview = "Преглед" previous = "Назад" refresh = "Опресни" +remaining = "Оставащи" retry = "Повтори" save = "Запазване" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Изчисти избраните" @@ -2988,6 +3199,7 @@ title = "Метод на компресиране" [compress.settings] desiredSize = "Желан размер на файла" desiredSizePlaceholder = "Въведете размер" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Компресията е лесен начин да намалите размера на файла си. Изберете File Size, за да въведете целеви размер и ние ще нагласим качеството вместо вас. Изберете Quality, за да зададете силата на компресията ръчно." @@ -3190,6 +3402,24 @@ pdfOptions = "Опции за PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Първо изберете изходен формат" settings = "Настройки" single = "Единичен" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Разширено наблюдение" -allInOneWorkspace = "Всичко-в-едно работно място за PDF (преглед, инструменти и агент)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Специализирана поддръжка и SLA" -fasterThroughput = "10x по-висока пропускателност" -fullyPrivateFiles = "Напълно частни файлове" -largeFileProcessing = "Обработка на големи файлове" -orgWideAccess = "Контроли за достъп на ниво организация" -premiumAiModels = "Премиум AI модели" -prioritySupport = "Приоритетна поддръжка" -privateDocCloud = "Частен облак за документи" -ragFineTuning = "RAG + фина настройка" -secureApiAccess = "Сигурен достъп до API" -standardThroughput = "Стандартна пропускателност" -unlimitedApiAccess = "Неограничен достъп до API" -unlimitedSeats = "Неограничени места" - [crop] autoCrop = "Автоматично изрязване на бели полета" noFileSelected = "Изберете PDF файл, за да започнете изрязването" @@ -3346,6 +3558,7 @@ message = "Неуспешно задаване на обработчик на PD title = "Грешка" [defaultApp.prompt] +dontRemind = "Не ми напомняй отново" message = "Направете Stirling PDF приложение по подразбиране за отваряне на PDF файлове." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Търсене..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Редактиране" @@ -3644,11 +3861,13 @@ shareSelected = "Сподели избраните" sharing = "Споделяне" showAll = "Покажи всички" showHistory = "Покажи история" +sortBy = "Sort files" sortByDate = "Сортирай по дата" sortByName = "Сортирай по име" sortBySize = "Сортирай по размер" storage = "Хранилище" storageState = "Съхранение" +storageUsed = "Storage used" synced = "Синхронизирано" title = "Качване на PDF файлове" toolChain = "Приложени инструменти" @@ -3694,13 +3913,17 @@ addFiles = "Добавяне на файлове" addingFiles = "Добавяне на файлове…" collapse = "Свиване на страничната лента" customizeGroups = "Персонализиране на групи" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Отворете файлове, за да започнете" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Разгъване на страничната лента" -files = "Файлове" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive не е конфигуриран" leaveMyFiles = "Напуснете Моите файлове" +library = "PDF библиотека" myFiles = "Моите файлове" noFiles = "Все още няма файлове" openFileManager = "Отваряне на мениджъра на файлове" @@ -3708,37 +3931,36 @@ openFromComputer = "Отваряне от компютър" openSettings = "Отваряне на настройките" other = "Други" recent = "Скорошни" -search = "Търсене" -searchPlaceholder = "Търсене на файлове..." viewAll = "Виж всички {{count}} файла" [fileSidebar.fileItem] closeViewer = "Затворете визуализатора" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Отворете във визуализатора" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Добави" -addLabel = "Добавяне на етикет…" -categoryIconAria = "Изберете икона за {{name}}" -createCategory = "Създай категория" -delete = "Изтрий категория" done = "Готово" -hide = "Скрий категория" -newCategory = "Име на нова категория…" -removeLabel = "Премахни {{name}}" -rename = "Преименувай" reset = "Възстанови по подразбиране" -search = "Търсене на етикети…" -show = "Покажи категория" subtitle = "Групирайте файловете си в родителски категории. Добавете съществуващи или нови етикети към категория, преименувайте я или създайте своя. Файловете, които не са в нито една от вашите категории, се показват под „Други“." title = "Категории в страничната лента" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Добавете към работната зона" addToWorkspaceCount = "Добавете {{count}} към работната зона" @@ -3749,7 +3971,6 @@ backToMyFiles = "Назад към Моите файлове" breadcrumbs = "Път на папката" cancel = "Отказ" classification = "Класификация" -clearSearch = "Изчистете търсенето" clearSelection = "Изчистете избора" closeDetails = "Затворете подробностите" create = "Създайте" @@ -3781,6 +4002,7 @@ downloadAll = "Изтеглете всички" downloadVersion = "Изтеглете тази версия" dropOverlay = "Пуснете файлове за качване" dropOverlaySub = "Файловете започват като Local. Използвайте 'Move to' или 'Save to cloud', за да ги организирате в папка." +duplicate = "Duplicate" file = "Файл" fileInfo = "File info" fileMenu = "Действия за файл" @@ -3808,13 +4030,12 @@ refresh = "Опреснете от сървъра" remove = "Изтриване" removeVersion = "Премахнете тази версия" rename = "Преименувайте" +renamed = "Преименувано" renameFolder = "Преименувайте папката" resizeFolderTree = "Преоразмерете дървото на папките (клавиши със стрелки, Shift за по-големи стъпки; двойно щракване за авто-побиране)" save = "Запазете" saveToServer = "Запазете на сървъра" saveToServerDisabledHint = "Запазването към сървъра не е активирано на този сървър. Помолете администратора да го активира." -search = "Търсене" -searchPlaceholder = "Търсене в тази папка и подпапките" selectAll = "Изберете всички" selectAllHint = "Кликнете, за да изберете всички. Съвет: задръжте Ctrl (или Cmd), за да добавяте файлове поотделно, Shift за избор на диапазон." selectedCount = "{{count}} избрани" @@ -3874,6 +4095,10 @@ title = "Още няма облачни файлове" hint = "Файловете, записани без качване, остават тук. Пуснете файл, за да добавите такъв." title = "Няма само локални файлове" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Файловете, които отваряте или редактирате, ще се показват тук." title = "Все още няма промени" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Папката не можа да бъде изтрита." deleteFolderFailedDetail = "Папката не можа да бъде изтрита: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Неуспешно обновяване на външния вид на папката." folderAppearanceFailedDetail = "Неуспешно обновяване на външния вид на папката: {{message}}" moveFilesFailed = "Файловете не можаха да бъдат преместени." @@ -3910,9 +4137,7 @@ count = "Файлове" folder = "Папка" labels = "Етикети" modified = "Променен" -name = "Име" size = "Размер" -toolHistoryAtVersion = "Кумулативна верига от инструменти" totalSize = "Общ размер" type = "Тип" versionHistory = "Път на версиите" @@ -3945,6 +4170,11 @@ localHint = "Съхранява се само в този браузър" shared = "Споделени" sharedHint = "Споделени с вас чрез връзка" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Най-старите първо" modifiedDesc = "Най-новите първо" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Няма налични файлове в хранили open = "Отвори" openFile = "Отвори файл" openFiles = "Отвори файлове" +selectFile = "Select {{name}}" selectFromStorage = "Изберете от хранилище" upload = "Качване" uploadFile = "Качване на файл" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Всичко е запазено" +analyzingFields = "Analysing form fields..." +extractCsvError = "Неуспешно извличане на CSV" +extractXlsxError = "Неуспешно извличане на XLSX" +filled = "filled" flattenAfterFilling = "Фиксиране след попълване" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "зад." +requiredFieldsError = "Моля, попълнете всички задължителни полета" rescanFields = "Пресканирайте полетата" rescanFormFields = "Пресканирайте полетата на формуляра" save = "Запазете" +saveShortcut = "Ctrl+S за запазване" +unsavedChanges = "Незапазени промени" [formFill.sidebar] close = "Затворете страничната лента" @@ -4309,6 +4552,11 @@ desc = "Автоматично преименува PDF файл според о tags = "авто-откриване,по заглавие,организиране,преименуване" title = "Авто преименуване на PDF" +[home.autoRotate] +desc = "Открива ориентацията на всяка страница и автоматично я завърта изправена." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Автоматично завъртане" + [home.autoSizeSplitPDF] tags = "авто,разделяне,размер" title = "Авто делене по размер/брой" @@ -4608,15 +4856,12 @@ hint = "Качете PNG, JPG, SVG или друг файл с изображе label = "Качване на изображение" placeholder = "Изберете файл с изображение" -[imageToPdf] -tags = "преобразуване,img,jpg,изображение,снимка" - [imageToPDF] header = "Изображение към PDF" title = "Изображение към PDF" -[infoBanner] -dismiss = "Отхвърлете" +[imageToPdf] +tags = "преобразуване,img,jpg,изображение,снимка" [invite] acceptError = "Неуспешно създаване на акаунт" @@ -4647,11 +4892,9 @@ welcomeTitle = "Поканени сте!" [landing] addFiles = "Добавяне на файлове" -heroSubtitle = "Плъзнете и пуснете или добавете съществуващ PDF, за да започнете." mobileUpload = "Качване от мобилно устройство" openFromComputer = "Отвори от компютър" uploadFromComputer = "Качване от компютър" -workbenchEmptyStateHero = "Пуснете PDF навсякъде" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Въведете потребителско име" failedToSignIn = "Неуспешен вход с {{provider}}: {{message}}" forgotPassword = "Забравена парола?" loggingIn = "Влизане..." -logIn = "Вход" login = "Вход" magicLinkSent = "Магическа връзка е изпратена до {{email}}! Проверете пощата си и щракнете върху връзката, за да влезете." mfaCode = "Код за удостоверяване" @@ -4715,26 +4957,10 @@ signInWith = "Вход с" title = "Вход" unexpectedError = "Неочаквана грешка: {{message}}" updatePassword = "Актуализиране на парола" -useEmailInstead = "Вход с имейл" useMagicLink = "Използвай магическа връзка" username = "Потребителско име" youAreLoggedIn = "Успешно влязохте!" -[login.slides.edit] -alt = "Редактиране на PDF" -subtitle = "С над дузина инструменти за зачертаване, подписване, четене и обработка на PDF, със сигурност ще намерите това, което търсите." -title = "Редактирайте PDF, за да показвате/защитавате желаната информация" - -[login.slides.overview] -alt = "Преглед на Stirling PDF" -subtitle = "Облачна платформа с приоритет на поверителността за PDF, която позволява да конвертирате, подписвате, зачертавате и управлявате документи, плюс 50+ други мощни инструменти." -title = "Всичко за PDF на едно място." - -[login.slides.secure] -alt = "Защита на PDF" -subtitle = "Добавяйте пароли, зачертавайте съдържание и управлявайте сертификати с лекота." -title = "Защитете чувствителна информация в PDF файловете си" - [margin] large = "Голямо" medium = "Средно" @@ -4818,6 +5044,34 @@ uploadSuccess = "Качването е успешно!" uploadSuccessMessage = "Вашите изображения бяха прехвърлени." validating = "Валидиране на сесията..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Сканирайте, за да качите снимки. Изображенията се конвертират автоматично в PDF." descriptionNoConvert = "Сканирайте, за да качвате снимки от мобилното си устройство." @@ -4836,8 +5090,13 @@ title = "Качване от мобилно устройство" tags = "Мултиинструмент,Мулти операции,UI,плъзгане с щракване,потребителска част,страна на клиента,интерактивен,неразрешим,преместване" title = "PDF Мулти инструмент" -[navbar] -search = "Търсене" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Удостоверяването беше неуспешно. Можете да затворите този прозорец и да опитате отново." @@ -4966,6 +5225,7 @@ activeFiles = "Изгледът Активни файлове allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Затвори" cropSettings = "След като избрахме файла за изрязване, можем да конфигурираме инструмента Изрязване, за да изберем областта, до която да изрежем PDF." +dialogLabel = "Onboarding" fileCheckbox = "Кликването върху файл го избира за обработка. Можете да избирате множество файлове за пакетни операции." fileReplacement = "Промененият файл автоматично ще замени оригинала в Работната зона, което позволява лесно да го пуснете през още инструменти." filesButton = "Бутонът Файлове в лентата за бърз достъп ви позволява да качите PDF файлове, върху които да използвате инструментите." @@ -4976,6 +5236,7 @@ pinButton = "Можете да използвате бутона Зак results = "След като инструментът приключи, стъпката Преглед ще покаже предварителен изглед на резултатите в този панел и ще ви позволи да отмените операцията или да изтеглите файла. " runButton = "След като инструментът е конфигуриран, този бутон позволява да го пуснете върху всички избрани PDF файлове." selectCropTool = "Нека изберем инструмента Изрязване, за да демонстрираме как се използва инструмент." +stepOf = "Стъпка {{current}} от {{total}}" toolInterface = "Това е интерфейсът на инструмента Изрязване. Както виждате, няма много неща, защото още не сме добавили PDF файлове за работа." workbench = "Това е Работната зона – основната област, където преглеждате и редактирате вашите PDF файлове." wrapUp = "Готови сте! Научихте за основните области на приложението и как да ги използвате. Кликнете бутона Помощ по всяко време, за да видите обиколката отново." @@ -4988,6 +5249,26 @@ showMeAround = "Разходи ме из новото" skipForNow = "Пропусни засега" skipTheTour = "Пропусни обиколката" +[onboarding.checklist] +dismiss = "Отхвърляне" +title = "Настройте Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Стартирайте Stirling нативно на вашата машина" +title = "Изтеглете Stirling за Desktop" + +[onboarding.checklist.inviteTeam] +description = "Сътрудничете с екипа си" +title = "Поканете членове на екипа" + +[onboarding.checklist.shareAnalytics] +description = "Помогнете за подобряването на Stirling" +title = "Споделяне на анонимни данни за употреба" + +[onboarding.checklist.takeTour] +description = "Вижте как работи Stirling в кратка обиколка" +title = "Направете обиколката" + [onboarding.desktopInstall] body = "Stirling работи най-добре като десктоп приложение. Можете да го ползвате офлайн, да отваряте документите по-бързо и да правите промени локално на компютъра си." selectOs = "Изберете операционна система" @@ -4996,7 +5277,21 @@ titleWithOs = "Изтегляне за {{osLabel}}" [onboarding.mfa] authenticationCode = "Код за удостоверяване" +description = "Защитете акаунта си, като свържете приложение за удостоверяване. Сканирайте QR кода или въведете ключа за настройка, след което потвърдете 6-цифрения код, за да завършите." +enable = "Активиране на MFA" +enableError = "Неуспешно активиране на двуфакторно удостоверяване. Проверете кода и опитайте отново." +enterCode = "Въведете 6-цифрения код от приложението си." +enterCodeError = "Въведете кода за удостоверяване, за да продължите." +logout = "Изход" +openAuthenticator = "Отворете Google Authenticator, Authy или 1Password." qrCodeLoading = "Генериране на вашия QR код…" +regenerateQrCode = "Генериране на QR код отново" +scanQrCode = "Сканирайте QR кода или въведете ключа за настройка по-долу." +setupError = "Неуспешно стартиране на настройката за двуфакторно удостоверяване. Моля, опитайте отново." +setupKey = "Ключ за настройка (ръчно въвеждане)" +stepByStep = "Стъпка по стъпка" +success = "MFA е активирано. Сега можете да продължите." +title = "Настройка на многофакторно удостоверяване" [onboarding.planOverview] adminBodyLoginDisabled = "След като активирате режим вход, ще можете да управлявате потребители, да конфигурирате настройки и да следите състоянието на сървъра. Първите {{freeTierLimit}} души на вашия сървър използват Stirling безплатно." @@ -5005,6 +5300,11 @@ adminTitle = "Админ преглед" userBody = "Поканете колеги, задавайте роли и поддържайте документите си организирани в едно защитено работно пространство. Активирайте режима за вход, когато сте готови да надскочите самостоятелното ползване." userTitle = "Преглед на плана" +[onboarding.processorIntro] +body = "Stirling вече изпълнява Правила — автоматизирани правила, които класифицират, защитават и обработват всеки документ при пристигането му. Настройте ги и наблюдавайте изпълненията в Processor." +cta = "Вижте Processor" +title = "Вижте Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Предплатена година" + +[payg.prepaid.banner] +exhausted = "Вашият предплатен капацитет е изчерпан; таксуването по измерване е възобновено. Допълнете, за да запазите отстъпката." +expiring_one = "Вашата предплатена година приключва след {{count}} ден. Неизползваният капацитет изтича — допълнете, за да започнете нова година." +expiring_other = "Вашата предплатена година приключва след {{count}} дни. Неизползваният капацитет изтича — допълнете, за да започнете нова година." +low = "Остават ви {{pct}}% от предплатения капацитет. Допълнете по всяко време, за да продължи обработката без прекъсване." + +[payg.prepaid.card] +subtitle = "Използва се преди таксуването по измерване и извън лимита ви за разходи." +title = "Предплатен капацитет" + +[payg.prepaid.meter] +capSuffix = "от {{total}} предплатени кредита" +expires = "Изтича {{date}}" + +[payg.prepaid.state] +exhausted = "Изчерпан" +healthy = "Остава много" +low = "На изчерпване" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF към PDF/A" tags = "архив,дълготраен,стандартен,преобразуване,съхранение,консервиране" title = "PDF към PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "печат,стандарт,конвертиране,производство,предпечат,архив" title = "PDF към PDF/X" @@ -5822,6 +6151,7 @@ small = "500 кредита" xsmall = "100 кредита" [plan.availablePlans] +currency = "Billing currency" subtitle = "Изберете план, който отговаря на нуждите ви" title = "Налични планове" @@ -5937,49 +6267,12 @@ keyDescription = "Поставете лицензионния ключ от им success = "Лицензът е активиран!" successMessage = "Лицензът ви беше активиран успешно. Можете да затворите този прозорец." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "приложено" -enforcing = "Прилагане..." -failed = "Прилагането е неуспешно" outputsUnavailable = "Резултатите от политиките вече не са налични за изтегляне." partialOutputsUnavailable = "Някои резултати от политиките вече не са налични за изтегляне." -retrying = "Заето, нов опит..." runNotFound = "Процесът по прилагане вече не може да бъде намерен." -step = "стъпка {{current}}/{{total}}" timedOut = "Времето за прилагане изтече, преди процесът да приключи." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Затвори" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Прилагане на {{names}}" applyingProgress = "Прилагане на {{names}} ({{done}} от {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} от {{total}} файл(а) не можаха да failureTitle = "Експортирано без пълно прилагане" printPolicyAppliedBody = "Този PDF беше актуализиран, за да отговаря на политика. Прегледайте промените и след това отпечатайте отново." printPolicyAppliedTitle = "Политика приложена преди печат" -queued = "+{{count}} в опашката" successTitle = "{{names}} приложени" summaryMore = "{{first}}, {{second}} и още {{more}}" summaryTwo = "{{first}} и {{second}}" -[policies.enforcement.triggerVerb] -convert = "Прилагане преди конвертиране" -default = "Прилагане" -export = "Прилагане преди експортиране" -input = "Прилагане при импортиране" -print = "Прилагане преди печат" - -[policies.field] -accessLog = "Дневник за достъп" -archiveAfter = "Архивирай след" -auditTrail = "Одитна следа" -belowThreshold = "Под прага" -destination = "Дестинация" -frameworks = "Рамки" -immutableHold = "Неизменяемо задържане" -keepFor = "Запази за" -minConfidence = "Мин. увереност" -notify = "Уведоми при маршрутизиране" -onViolation = "При несъответствие" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Добави" -addPlaceholder = "Добавяне на етикет…" -customNote = "Персонализирано за вашия екип." -defaultNote = "Използва се вградената настройка по подразбиране, споделена с вашия екип." -duplicate = "\"{{name}}\" вече съществува." -edit = "Редактирай етикети" -empty = "Все още няма етикети." -export = "Експортирай JSON" -iconAria = "Изберете икона за {{name}}" -import = "Импортирай JSON" -importError = "Този файл не можа да бъде импортиран." -managedNote = "Екипните етикети се управляват от ръководителя на екипа." -modalSubtitle = "Споделено с целия ви екип. Класификаторът избира етикетите, които съответстват на всеки документ." -modalTitle = "Етикети за класификация" -pickIcon = "Използвай иконата {{label}}" -removeAria = "Премахни {{name}}" -resetToDefault = "Възстанови по подразбиране" -saveForTeam = "Запази за екипа" -saving = "Запазване…" -sectionLabel = "Етикети за класификация" -startFromScratch = "Започни отначало" -teamCount = "екипни етикети" -tooLong = "Етикетите могат да бъдат най-много {{max}} знака." -ungrouped = "Негрупирани" -view = "Виж етикети" +categoryCount = "категории" +hideCategory = "Скриване на категория" +labelCount = "етикети" +sharedNote = "Тези етикети са вградени и споделени в целия ви екип." +showCategory = "Показване на категория" [policies.pii] account = "Номера на сметки (с етикет)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Номера за маршрутизация в САЩ (ABA)" ssn = "Социалноосигурителни номера" -[policies.settings] -noneExport = "В момента няма политики, които се изпълняват при експортиране." -noneUpload = "В момента няма политики, които се изпълняват при качване." -onExport = "При експортиране" -onUpload = "При качване" -reorderHandle = "Плъзнете за пренареждане" -runOrderDesc = "Когато повече от една политика се изпълнява при един и същ тригер, те се изпълняват в този ред — всяка върху изхода от предишната политика. Плъзнете за пренареждане." -title = "Настройки на политики" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Опции за политики" -policySettings = "Настройки на политики" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Повтори неуспешните политики ({{count}})" -rowProgress = "Обработени {{completed}} от {{total}} файла" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Какво е политика?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Автоматично открива и зачернява чувствителни данни — като социалноосигурителни и номера на карти — за да не могат да се четат в документа." -sanitize = "Премахва скрит JavaScript от файла, така че нищо да не се изпълнява автоматично при отваряне." -watermark = "Поставя видим воден знак (напр. „Поверително“) върху всяка страница." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Затвори" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} се прилага..." badgeRan = "Политиката {{name}} е изпълнена върху този файл" +badgeRunning = "{{name}} се изпълнява..." blockingAction = "{{action}} е блокирано, докато се прилага политиката, моля изчакайте..." dismiss = "Скрий наслагването" enforcingTitle = "Прилагане на политика..." @@ -6151,6 +6316,7 @@ viewAnyway = "Виж файла (политиката все още се при [policyOption] autoRedactPhi = "Автоматично заличаване на PHI" blockExport = "Блокирай експортирането" +ccpa = "CCPA" contracts = "Договори" documents = "Документи" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "План Editor" subscribed = "План Processor" unlinked = "Не е свързан" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap от документ" -sectionsAriaLabel = "Секции на Agent Builder" -selectorAriaLabel = "Агенти" -subtitle = "Проектирайте, тествайте и публикувайте AI агенти, които класифицират, извличат от и маршрутизират вашите документи. Дефинирайте сценарии, ограничете достъпа до инструменти, изпълнете golden set и публикувайте версия." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Отказ" -dropzoneText = "Изберете примерен документ (PDF или изображение)" -lead = "Пуснете примерен документ и ще предложим сценарии и схема за извличане, които можете да прецизирате. Нищо не се публикува, докато не го прегледате." -submit = "Bootstrap агент" -subtitle = "Създайте основа за нов агент от един представителен файл" -title = "Bootstrap от документ" - -[portal.agentBuilder.empty] -description = "Направете bootstrap на агент от примерен документ, за да създадете основа за неговите сценарии и схема за извличане, след което прецизирайте и публикувайте." -title = "Все още няма агенти" - -[portal.agentBuilder.evals] -casesPassing = "Успешни случаи" -columnCase = "Eval случай" -columnLatency = "Закъснение" -columnResult = "Резултат" -fail = "неуспех" -goldenSetPassRate = "Процент на успех за golden set" -latencyMs = "{{ms}} ms" -notRun = "не е изпълнено" -pass = "успех" -passRate = "Процент на успех" -runEvals = "Изпълни evals" - -[portal.agentBuilder.evals.empty] -description = "Evals превръщат вашите сценарии в повторяем golden set. Надстройте, за да проследявате процента на успех във времето и да ограничавате публикуванията според него." -title = "Все още няма golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "в golden sets" -activeAgents = "Активни агенти" -avgPassRate = "Среден процент на успех на eval" -fleetWide = "за целия fleet" -latestPublished = "Последно публикувано" -scenarios = "Сценарии" -testCases = "тестови случаи" -totalDescription_one = "общо {{count}}" -totalDescription_other = "общо {{count}}" - -[portal.agentBuilder.scenarios] -add = "Добави" -addScenario = "Добави сценарий" -enable = "Активирай" -expectationLabel = "Очаквано поведение" -expectationPlaceholder = "Какво трябва да направи агентът" -inEval = "в eval" -mute = "Заглуши" -muted = "заглушен" -nameLabel = "Име" -namePlaceholder = "напр. ескалация за съответствие" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Сценарии" -tools = "Инструменти" -versions = "Версии" - -[portal.agentBuilder.tools] -broadAccess = "Широк достъп" -deniedHint = "Избраните инструменти са блокирани. Всичко останало остава достъпно за извикване." -deniedTools = "Забранени инструменти" -governanceGate = "Управлението на инструменти е налично в план Enterprise." -restricted = "Ограничен" -restrictedAccess = "Ограничен достъп до инструменти" -restrictedDescription = "Разрешете всеки инструмент освен тези, които забранявате по-долу." - -[portal.agentBuilder.versions] -current = "текуща" -historyGate = "Пълна история на версиите и връщане назад са налични в план Enterprise." -publish = "Публикувай" -rollBack = "Върни назад" - [portal.assistant] close = "Затвори асистента" error = "Асистентът не можа да бъде достигнат." @@ -6365,9 +6451,21 @@ typing = "Писане" loading = "Зареждане" redirectingToEditor = "Пренасочване към редактора..." +[portal.billing.activation] +maybeLater = "Може би по-късно" +title = "Включете Processor" + +[portal.billing.activation.payg] +sub = "Задайте месечен лимит. Плащате за това, което изпълнявате." +title = "Плащане според употребата" + +[portal.billing.activation.prepay] +badge = "2 месеца безплатно" +sub = "Една фактура, карта или банков превод." +title = "Предплатете година" + [portal.billing.checkout] noClientSecret = "Edge функцията не върна client_secret." -subtitle = "Добавете карта, за да продължите след безплатния грант за план Editor. Stripe ще се погрижи за останалото." title = "Включете план Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Плащането ви е успешно, но активирането close = "Затвори" title = "Почти готово" +[portal.billing.checkout.cap] +amountAria = "Месечен лимит за разходи" +back = "Назад" +continue = "Продължи към плащане" +error = "Неуспешно задаване на лимита ви за разходи" +finePrint = "Фактурите се издават месечно. Отмяна по всяко време." +noLimit = "Без лимит" +note = "Никога няма да бъдете таксувани над лимита си. Обработката просто спира на пауза." +perMonth = "/ мес." +title = "Задайте лимита си за разходи" + +[portal.billing.checkout.card] +fields = "Номер на карта · MM / YY · CVC · ZIP" +label = "Данни за карта" +note = "Данните за картата се събират от Stripe. Stirling никога не съхранява PAN или CVC." + [portal.billing.checkout.error] title = "Checkout не можа да стартира" @@ -6383,10 +6497,9 @@ body = "Плащането ви премина. Включваме измерв hint = "Моля, оставете този прозорец отворен." title = "Активиране на вашия план Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "в portal env, за да активирате checkout в приложението." -bodyBefore = "Задайте" -title = "Stripe не е конфигуриран" +[portal.billing.checkout.pay] +limit = "Месечен лимит за разходи {{amount}}/мес. Обработката спира на пауза при достигане на лимита." +limitNoCap = "Без лимит за разходи. Обработката няма да спира на пауза. Фактурите се издават месечно." [portal.billing.enterpriseUpsell] cta = "Създайте своята Enterprise оферта" @@ -6409,10 +6522,12 @@ currentPlan = "Текущ план" freeForever = "Безплатно завинаги" noTeamResolved = "Все още няма определен екип за вашия портфейл — обновете и опитайте отново." ownerOnly = "Само собственикът на екипа може да включи план Processor." +payInvoice = "Платете фактурата, за да завършите" planName = "Editor" ssoIncluded = "SSO включено" switchOnProcessor = "Включи Processor →" unlimitedUsers = "Неограничени потребители" +viewQuote = "Преглед на оферта" [portal.billing.invoices] columnAmount = "Сума" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Директни API заявки" segmentApiLabel = "API" segmentAutomationDesc = "Автоматизации и pipelines" segmentAutomationLabel = "Автоматизация" +sizeMultiplier = "{{formatted}} PDF файла използваха множител за размер" +summary = "{{unique}} уникални · {{units}} мерни единици · {{avg}} средно на PDF" +summaryNoRate = "{{unique}} уникални · {{units}} мерни единици" unit = "измервани PDF" +unitsPending_one = "{{units}} мерна единица очаква синхронизация от свързани instances" +unitsPending_other = "{{units}} мерни единици очакват синхронизация от свързани instances" + +[portal.billing.prepaid] +capSuffix = "от {{total}} предплатени кредита" +expires = "Изтича {{date}}" +eyebrow = "Предплатен капацитет" +note = "Използва се преди таксуването по измерване и извън лимита ви за разходи." +topUp = "Допълване" + +[portal.billing.prepaid.buy] +back = "Назад" +calcTitle = "Изчислете годишното си плащане" +cancel = "Отказ" +cancelPurchase = "Отказ на покупката" +close = "Затваряне" +continueToPayment = "Продължи към плащане" +finalise = "Финализиране" +invoiceErrorTitle = "Неуспешно генериране на фактурата" +loadingTitle = "Зареждане на офертата ви" +payTitle = "Платете за годината си" +step = "Стъпка {{current}} от {{total}}" + +[portal.billing.prepaid.calc] +change = "Промяна" +deployRow = "Внедряване" +done = "Готово" +enterpriseHint = "Това е enterprise мащаб. При този обем enterprise тарифите са по-добри от всяка self-serve отстъпка. Заключване на тарифата, условия и оферта за минути." +governanceRow = "Управление" +governanceValue = "{{label}} · {{count}} правила" +handlesLabel = "Вашият Processor" +handlesValue = "обработва {{volume}} кредита / мес." +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} правила" +poolCaption = "Един пул от {{credits}} кредита за годината. Натоварените месеци заемат от по-леките." +priceLabel = "Вашата година · 12 месеца на цената на 10" +rateUnknown = "Ще покажем точната цена при checkout." +savings = "Спестявате {{amount}} · 2 месеца безплатно." +sizingRow = "Оразмеряване" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Общо потребители" + +[portal.billing.prepaid.consent] +label = "Разбирам, че когато предплатеният ми капацитет бъде изчерпан или изтече след 12 месеца, обработката автоматично продължава по стандартната такса за pay-as-you-go според измерването (до лимита ми за разходи), освен ако не отменя, и че мога да отменя по всяко време от billing portal." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Управлявано от Stirling. Активно за минути." +cloudValue = "Stirling Cloud · Управлявано" +selfhost = "Self-hosted" +selfhostDesc = "Работи върху частна инфраструктура. Същата тарифа." +selfhostValue = "Self-hosted · Частна инфраструктура" + +[portal.billing.prepaid.offer] +cta = "Преглед на офертата" +subtitle = "Предплатете година PDF обработка и получете два месеца безплатно, използвани преди таксуването по измерване, извън лимита ви за разходи." +title = "Вземете 12 месеца на цената на 10" + +[portal.billing.prepaid.pay] +accountName = "Име на титуляр на акаунта" +accountNamePlaceholder = "напр. Jane Smith" +companyName = "Име на компания (по избор)" +companyNamePlaceholder = "напр. Acme Inc." +downloadInvoice = "Изтегляне на фактура" +payOnline = "Плащане онлайн" +poNumber = "PO номер (по избор)" +poNumberPlaceholder = "напр. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Разширени" +advancedDesc = "Pipelines управляват по-голямата част от обработката ви." +none = "Няма" +noneDesc = "Все още не изпълнявате pipelines. Включете ги по всяко време." +standard = "Стандартни" +standardDesc = "Няколко pipelines преобработват пристигащи PDF файлове." + +[portal.billing.prepaid.posture] +essentials = "Основни" +essentialsDesc = "Класификация, по подразбиране, плюс Споделяне" +governed = "Управлявани" +governedDesc = "Добавя Сигурност и Маршрутизиране" +regulated = "Регулирани" +regulatedDesc = "Всяка категория, вкл. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Изтегляне на оферта (PDF)" +downloading = "Подготовка…" +share = "споделяне за одобрение" + +[portal.billing.prepaid.size] +compact = "Компактни" +compactDesc = "Файлове под 25 MB, без такси за данни" +heavy = "Тежки" +heavyDesc = "Сканирани или с много изображения, редовно 50 MB+" +standard = "Стандартни" +standardDesc = "Предимно малки, някои сканирания над 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Изчерпан" +healthy = "Остава много" +low = "На изчерпване" [portal.billing.spendLimit] adjustLimit = "Коригирай лимита" @@ -6494,6 +6713,8 @@ label = "Прогнозира се надвишаване." [portal.billing.spendThisMonth] eyebrow = "Разходи този месец" +freeRemaining_one = "Остава {{formatted}} безплатен PDF" +freeRemaining_other = "Остават {{formatted}} безплатни PDF файла" processed_one = "Обработен {{formattedCount}} PDF." processed_other = "Обработени {{formattedCount}} PDF." processedWithRate_one = "Обработен {{formattedCount}} PDF, по {{rate}} всеки." @@ -6510,6 +6731,7 @@ reachedTitle = "Месечният лимит на разходите е дос title = "Stripe порталът не можа да бъде отворен" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "от {{allowance}} безплатни PDF използвани" capSuffix_other = "от {{allowance}} безплатни PDF използвани" eyebrow = "Пробен Processor" @@ -6534,6 +6756,376 @@ signature = "подпис" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Нова връзка" +createTitleFor = "Свързване на {{name}}" +delete = "Изтриване" +edit = "Редактиране" +editTitle = "Редактиране на връзка" +unsupportedType = "Този тип връзка не може да се редактира тук." + +[portal.connections.categories.advanced] +description = "За услуга, която все още не поддържаме." +label = "Разширени" + +[portal.connections.categories.audit] +description = "Записвайте какво се е случило с документ там, където вашите одитори вече проверяват." +label = "Одит и compliance logging" + +[portal.connections.categories.notify] +description = "Уведомете друга система, че нещо се е случило." +label = "Известяване и workflow" + +[portal.connections.categories.security] +description = "Сканирайте или етикетирайте документ, преди да продължи." +label = "Сигурност и класификация" + +[portal.connections.categories.signing] +description = "Изпратете документ за подписване или нотариална заверка." +label = "Електронен подпис и нотариална заверка" + +[portal.connections.categories.storage] +description = "Къде попадат обработените документи - bucket, drive, ticket или page." +label = "Файл и прикачване" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling съхранява това криптирано и никога не го показва отново." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Стъпките, използващи тази връзка, могат да извикват само пътища под този URL, никога друг host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "Поставете URL, който услугата ви е дала. Могат да се извикват само пътища под него." +label = "Webhook URL" + +[portal.connections.fields] +name = "Име на връзка" +namePlaceholder = "напр. Production" + +[portal.connections.picker] +cancel = "Отказ" +createNew = "Нова връзка..." +placeholder = "Изберете връзка" +save = "Запазване на връзка" + +[portal.connections.picker2] +back = "Промяна на интеграция" +heading = "Какво искате да свържете?" +noResultsBody = "Все още не го поддържаме - кажете ни за него и ще разгледаме добавянето му." +noResultsTitle = "Нищо не съвпада с \"{{query}}\"" +searchPlaceholder = "Търсене на интеграции - опитайте \"sign\", \"ocr\" или име на продукт" + +[portal.connections.types.api] +description = "Извикайте произволен HTTP API от правило. Вие избирате URL, удостоверяването и payload." +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "Удостоверяване" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username и password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token в custom header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Няма" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Вход за token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Стъпките, използващи тази връзка, могат да извикват само пътища под този URL, никога друг host." +label = "Базов URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Име на header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Път за вход" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Ако API отговори с връзка за изтегляне, могат да се извличат само тези hosts. Разделете със запетаи. Оставете празно, за да разрешите само собствения host на базовия URL." +label = "Разрешени hosts за резултати" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Изпращане на token като този header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Header-ът на отговора, в който login извикването връща token, напр. X-Auth-Token." +label = "Header за token в отговора" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Сканирайте документи за malware на server, който управлявате - нищо не напуска мрежата ви." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Сканиране за вируси и съдържание за документи в правило." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Блокирайте документи, съдържащи macros, executables, scripts или embedded objects - не само познат malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Прикачване на обработени документи към Confluence page." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Изпращайте документи към ConsignO Cloud (Notarius) за електронен подпис и нотариална заверка." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Вашата ConsignO Cloud среда, включително /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Необходимо само ако организацията ви използва tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Публикувайте съобщение в Discord, когато правило обработи документ." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Индексирайте audit event в Elasticsearch всеки път, когато правило обработи документ." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Проверявайте FTP или FTPS server за нови документи." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Режим на връзка" + +[portal.connections.types.ftp.fields.passive.options] +active = "Активен" +passive = "Пасивен (препоръчително)" + +[portal.connections.types.ftp.fields.security] +helperText = "Обикновеният FTP изпраща идентификационни данни и файлове некриптирани; изберете FTPS, ако server го поддържа." +label = "Криптиране" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Няма (обикновен FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Публикувайте съобщение в Google Chat space, когато правило се изпълнява." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Прикачване на обработени документи към Jira issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Изпратете документ или известие по email, когато правило се изпълнява." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Записвайте обработени документи в Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Откривайте лични данни в текста на документа чрез Presidio server, който управлявате." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Четете и прилагайте Microsoft Purview sensitivity labels върху PDF файлове." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "По избор. Нужно само за избор на етикети по име; етикетирането работи и без него." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Поставя се върху всеки етикет като негов SiteId, така че Purview-aware tools да знаят коя организация го е приложила." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, от който източниците могат да четат и в който pipeline изходите могат да записват." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Оставете празно за AWS. Задайте това за MinIO или друго S3-съвместимо хранилище." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Изпратете документ или известие по email, когато правило се изпълнява." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Проверявайте SFTP (SSH) server за нови документи." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "По избор. SHA-256 fingerprint на host key на server-а (от `ssh-keyscan host | ssh-keygen -lf -`). Когато е зададен, се приема само server, представящ този ключ; когато е празно, ключът, видян при първото свързване, се pin-ва и по-късни промени се отказват." +label = "Fingerprint на host key" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Passphrase за ключ" + +[portal.connections.types.sftp.fields.password] +helperText = "Оставете празно, ако се удостоверявате с private key вместо това." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "По избор. Поставете OpenSSH private key, за да се удостоверите с ключ вместо с password." +label = "Private key" + +[portal.connections.types.slack] +description = "Публикувайте съобщение в Slack, когато правило обработи документ." +label = "Slack" + +[portal.connections.types.smb] +description = "Наблюдавайте SMB / CIFS network share (Windows или Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "По избор. Windows domain или workgroup, ако server-ът ви използва такъв." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Изпращайте audit event към Splunk всеки път, когато правило обработи документ." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Изпращайте audit event към Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Публикувайте съобщение в Teams channel, когато правило се изпълнява." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Публикувайте всеки документ или само детайлите към URL, който изберете." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Задействайте Zap или Make scenario от правило." +label = "Zapier & Make" + +[portal.docs] +browse = "Преглед на документи" +viewSource = "Преглед на source в GitHub" + [portal.docs.authentication] codeCaption = "всяка заявка" eyebrow = "GETTING STARTED" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Включени, именувани способности, които вашият агент извиква като един инструмент. Всяко skill е детерминирана верига от операции с прикачени evals." title = "Умения на агент" +[portal.docs.toc] +title = "На тази страница" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Тип" user = "Потребител" [portal.documents.queue.empty] +connectSource = "Свързване на източник" +createPipeline = "Създаване на pipeline" description = "Когато източниците подават документи към вашите pipelines, те ще се появят тук за преглед." title = "Няма документи в опашката" @@ -6712,11 +7309,11 @@ editorAction = "Редактор" empty = "Няма документи, отговарящи на този филтър." rowActions = "Действия за реда" sensitiveLabel = "Чувствителен" -sensitiveTitle = "Чувствителен — необходим е достъп" [portal.documents.table.columns] action = "Pipeline / Действие" document = "Документ" +labels = "Labels" product = "Продукт" status = "Статус" time = "Време" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Последно видян" region = "Регион" status = "Статус" +target = "Target" version = "Версия" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Този изглед срещна неочаквана греш retry = "Опитайте отново" title = "Нещо се обърка на тази страница" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Назад" +body = "Изтеглете desktop app или self-host-нете server-а." +done = "Готово" +guide = "Отворете ръководството за {{name}}" +openInBrowser = "Отваряне в browser вместо това" +sectionDesktop = "Desktop app" +sectionSelfHosted = "Self-hosted" +title = "Инсталирайте Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Стартирайте официалния image на вашия server." +note = "Отваря се на :8080. Вход по подразбиране: admin / stirling." +tagline = "Стартирайте server-а в container" +title = "Docker" +variantFat = "Пълен" +variantLite = "Ultra-Lite" +variantStandard = "Стандартен" + +[portal.home.download.kubernetes] +detailBody = "Инсталирайте официалния Helm chart." +note = "Добавете PVC за /configs, за да запазвате настройките." +tagline = "Разположете в клъстер с Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Нативни компилации за вашата дистрибуция." +guideBtn = "Вижте опциите за инсталиране на Linux" +note = "Налични са AppImage, .deb и .rpm компилации за популярни дистрибуции." +tagline = "Нативно настолно приложение за вашата дистрибуция" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Или чрез Homebrew" +detailBody = "Стартирайте инсталатора или използвайте Homebrew." +downloadBtn = "Изтегляне за macOS" +tagline = "Нативно приложение за Apple silicon и Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Стартирайте .jar на bare metal сървър." +downloadBtn = "Изтегляне на .jar" +note = "Изисква Java 21+. Отваря се на :8080; вход по подразбиране admin / stirling." +runLabel = "След това го стартирайте" +tagline = "Стартирайте .jar на bare metal сървър" +title = "Ръчна настройка на сървър" + +[portal.home.download.windows] +altLabel = "Или чрез winget" +detailBody = "Стартирайте инсталатора или използвайте мениджър на пакети." +downloadBtn = "Изтегляне на инсталатор" +tagline = "Нативно приложение за Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} активни" -invite = "Поканете съотборници" +activeOfDeployed = "{{active}} от {{total}} активни този месец" name = "Stirling PDF Editor" open = "Отвори в браузър" updated = "актуализирано {{time}}" +[portal.home.editor.deploy] +finish = "Завършете разполагането" +options = "Вижте опциите за разполагане" +start = "Разполагане на Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Самостоятелно хостван · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Добър ден" evening = "Добър вечер" morning = "Добро утро" -[portal.home.onboarding] -dismiss = "Отхвърли настройката" -notStarted = "Не е започнато" -progress = "{{done}} от {{total}} завършени" -title = "Завършете настройката" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC за цялата организация, ангажирано обемно ценообразуване и разгръщане в изолирана мрежа." -cta = "Започнете пробен период" -ctaQuote = "Получете оферта" -lead = "За 250+ служители." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Разгърнете desktop app, безплатно завинаги." -title = "Изтеглете PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} активни · {{recommended}} препоръчани" -chip = "{{active}} активни" -title = "Включете политики" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} свързани · всеки PDF се управлява там, където попадне" -chip = "{{connected}} свързани" -title = "Свържете вашите източници" - -[portal.home.quickActions] -subtitle = "Основни задачи за днес" -title = "Бързи действия" - -[portal.home.quickActions.buildPipeline] -blurb = "Композитор в 3 стъпки върху библиотеката с типизирани операции" -title = "Създайте pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, наблюдавани папки" -title = "Свържете източник" - -[portal.home.quickActions.issueApiKey] -blurb = "Ограничен ключ с лимити на заявките и IP allowlist" -title = "Издай API ключ" - [portal.infrastructure] manageEditorDeployment = "Управление на разгръщането на Editor" sectionsAriaLabel = "Секции на инфраструктурата" @@ -6878,13 +7541,9 @@ heading = "API ключове" subheading = "Ограничени удостоверения с лимити на заявките за всеки ключ, разрешения и IP allowlists." [portal.infrastructure.apiKeys.card] -allowedIps = "Разрешени IP адреси" -anyIp = "Всеки IP (без allowlist)" created = "Създаден" lastUsed = "Последно използван" -permissions = "Разрешения" -rateLimit = "Лимит на заявките" -rateLimitValue = "{{value}} req/min" +revoke = "Отмяна на ключ" usageMonth = "Използване този месец" usageToday = "Използване днес" @@ -6892,6 +7551,15 @@ usageToday = "Използване днес" description = "Създайте ограничен ключ, за да започнете да извиквате Stirling API." title = "Все още няма API ключове" +[portal.infrastructure.apiKeys.error] +load = "API ключовете ви не можаха да бъдат заредени. Моля, опитайте отново." + +[portal.infrastructure.apiKeys.revoke] +body = "Да се отмени ли „{{name}}“? Всяка интеграция, която все още използва този ключ, веднага ще започне да получава грешки 401. Това не може да бъде отменено." +cancel = "Отказ" +confirm = "Отмяна на ключ" +title = "Отмяна на API ключ" + [portal.infrastructure.audit] filterAriaLabel = "Филтриране на одитни събития по категория" heading = "Одитни logs" @@ -6935,6 +7603,7 @@ all = "Всички" auth = "Auth" config = "Config" elevation = "Повишаване на достъпа" +policy = "Политика" processing = "Обработка" security = "Сигурност" @@ -6944,19 +7613,30 @@ title = "Нямате достъп" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Повишаване на достъпа" +policy = "Политика" processing = "Обработка" totalEvents = "Общо събития · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Удостоверяване" +config = "Конфигурация" +elevation = "Повишаване" +policy = "Политика" +processing = "Обработка" +security = "Сигурност" + +[portal.infrastructure.auditStatusLabel] +danger = "Грешка" +info = "Информация" +success = "Успех" +warning = "Предупреждение" + [portal.infrastructure.createKey] cancel = "Отказ" createKey = "Създай ключ" done = "Готово" -ipAllowlistHelper = "CIDR диапазони, разделени със запетая. Оставете празно, за да разрешите всеки IP." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Име на ключ" keyNamePlaceholder = "напр. Production · ingest" -permissionsLabel = "Разрешения" secretKeyCaption = "Secret key" secretWarning = "Съхранявайте това в secrets manager. Stirling винаги съхранява само hash — няма начин да го възстановите по-късно." subtitle = "Ограничете ключа до минимално необходимото. Можете да го ротирате или отнемете по всяко време." @@ -6964,207 +7644,9 @@ subtitleCreated = "Копирайте тази тайна сега — няма title = "Създай API ключ" titleCreated = "Ключът е създаден" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Разгърнато от" -environment = "Среда" -product = "Продукт" -status = "Статус" -version = "Версия" -when = "Кога" - -[portal.infrastructure.deployments.recent] -heading = "Скорошни разгръщания" -subheading = "Най-новите внедрявания в продукти и среди." - -[portal.infrastructure.deployments.regionColumns] -instances = "Инстанции" -latency = "Латентност" -load = "Натоварване" -p99 = "P99" -region = "Регион" -status = "Статус" -throughput = "Пропускателност" -uptime = "Uptime" -version = "Версия" - -[portal.infrastructure.deployments.regions] -heading = "Региони" -subheading = "Здраве на живо за всеки разгърнат Stirling регион — латентност, натоварване и версия на внедряването." - -[portal.infrastructure.deployments.regions.empty] -description = "Разгърнатите региони ще се появят тук, след като работното ви пространство бъде provisioned." -title = "Няма разгърнати региони" - -[portal.infrastructure.models] -heading = "Модели" -msValue = "{{value}} ms" -subheading = "Каталогът с модели и routing, които задвижват обработката на документи във вашето работно пространство." - -[portal.infrastructure.models.byom] -description = "Регистрирайте on-prem или самостоятелно хостван модел и го фиксирайте към регион за обработка, обвързана с местонахождението на данните." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Каталог" -sub = "Управлявани модели, достъпни за вашето работно пространство, с латентност и цена на живо." -subEnterprise = "Управлявани, bring-your-own и on-prem модели — с налично фиксиране по регион." - -[portal.infrastructure.models.catalogue.empty] -description = "Моделите в каталога на вашето работно пространство ще се появят тук." -title = "Няма налични модели" - -[portal.infrastructure.models.columns] -cost = "Цена" -latency = "Латентност" -load = "Натоварване" -model = "Модел" -status = "Статус" -type = "Тип" -version = "Версия" - -[portal.infrastructure.models.metrics] -activeModels = "Активни модели" -avgLatency = "Средна латентност" -included = "Включено" -monthlySpend = "Месечни разходи за модели" - -[portal.infrastructure.models.routing] -empty = "Няма конфигурирани правила за routing." -heading = "Правила за routing" -sub = "Кой модел обработва всяка операция. Стойността по подразбиране се прилага, когато няма по-тясно правило." -subLocked = "Насочвайте операции към конкретни модели — достъпно в платени планове." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Надстройте до Pro, за да контролирате кой модел обработва всяка операция и тип документ." -title = "Model routing е платена функция" - -[portal.infrastructure.models.routingColumns] -default = "По подразбиране" -docType = "Тип документ" -modelForAria = "Модел за {{operation}}" -operation = "Операция" -routedTo = "Насочено към" - -[portal.infrastructure.security.access.byok] -description = "Предоставете ключ от вашия собствен KMS. Stirling криптира с него, но все още може да чете." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ключовете никога не напускат вашия KMS. Stirling държи само ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling управлява ключовете за криптиране. Най-просто — нулеви операции с ключове от ваша страна." -label = "Ключове, държани от Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Политика за достъп до документи" -subheading = "Контролира кой може да декриптира обработени документи в покой." - -[portal.infrastructure.security.attestations] -heading = "Атестации за съответствие" -noReport = "Няма наличен отчет" -subheading = "Одитно състояние по рамки, с налични отчети за атестирани контроли." -viewReport = "Вижте отчета →" - -[portal.infrastructure.security.compliance] -heading = "Съответствие" -subheading = "Атестации и сертификации, покриващи платформата Stirling." - -[portal.infrastructure.security.empty] -description = "Конфигурацията за сигурност на вашето работно пространство ще се появи тук." -title = "Състоянието на сигурността не е налично" - -[portal.infrastructure.security.hyokBanner] -description = "С HYOK ключовете за криптиране никога не напускат вашия KMS. Stirling съхранява и обработва само ciphertext, който можете да оттеглите по всяко време." -title = "Stirling не може да декриптира вашите документи" - -[portal.infrastructure.security.ipAllowlist] -empty = "Няма конфигурирани IP диапазони — всички IP адреси са разрешени." -heading = "IP allowlist" -sub = "API достъпът е ограничен до тези CIDR диапазони." -subLocked = "Ограничете API достъпа до известни IP диапазони — достъпно в платени планове." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Надстройте до Pro, за да ограничите API достъпа до конкретни мрежи." -title = "IP allowlisting е платена функция" - -[portal.infrastructure.security.ipColumns] -added = "Добавено" -addedBy = "Добавено от" -cidr = "CIDR" -label = "Етикет" - -[portal.infrastructure.security.keyManagement] -algorithm = "Алгоритъм" -heading = "Управление на ключове за криптиране" -keyId = "Идентификатор на ключ" -lastRotated = "Последно ротиран" -rotateKey = "Ротирай ключ" -rotationPolicy = "Политика за ротация" -subheading = "Попечителство на ключовете, които криптират документи в покой — кой може да декриптира и как се ротират ключовете." - -[portal.infrastructure.security.managedBanner] -description = "Попечителството bring-your-own-key (BYOK) и hold-your-own-key (HYOK) е достъпно в Enterprise. Надстройте, за да предоставяте ключове от вашия собствен KMS." -title = "Ключовете се управляват от Stirling във вашия план" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Азиатско-тихоокеански регион" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR граница на данните" -label = "Европейски съюз" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Съединени щати" - -[portal.infrastructure.security.residencyHeader] -heading = "Местонахождение на данните" -subheading = "Къде се съхраняват и обработват документите." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} използвани" - -[portal.infrastructure.storage.empty] -description = "Свързаното storage и използване ще се появят тук." -title = "Няма конфигурирано storage" - -[portal.infrastructure.storage.lifecycle] -active = "Активно" -activeRange = "0–{{value}}d" -archived = "Архивирано" -coldStorage = "cold storage" -deleted = "Изтрито" -never = "никога" -purged = "изчистено" - -[portal.infrastructure.storage.providers] -connect = "Свържи" -connected = "Свързано" -heading = "Свързани доставчици" -subheading = "Къде се записват обработените артефакти." - -[portal.infrastructure.storage.retention] -heading = "Задържане" -subheading = "Колко дълго се пазят артефактите преди изтриване по lifecycle." -windowLabel = "Прозорец за задържане по подразбиране" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} ден" -days_other = "{{count}} дни" -never = "Никога не изтривай" - -[portal.infrastructure.storage.totalUsage] -heading = "Общо използване" -progressLabel = "Използвано storage" -subheading = "Storage, използвано във всички свързани доставчици." +[portal.infrastructure.keyLabel] +active = "Активен" +revoked = "Отменен" [portal.infrastructure.tabs] apiKeys = "API ключове" @@ -7174,6 +7656,49 @@ models = "Модели" security = "Сигурност" storage = "Storage" +[portal.integrations] +availableHeading = "Налични" +comingSoonHeading = "Очаквайте скоро" +connect = "Свързване" +connectionCount_one = "{{count}} връзка" +connectionCount_other = "{{count}} връзки" +customApi = "Персонализиран API" +searchPlaceholder = "Търсене на интеграции" +subtitle = "Всичко, с което Stirling може да комуникира. Идентификационните данни се създават веднъж тук, след което се използват от източници, политики и конвейери." +title = "Интеграции" +typedName = "Име на {{tool}}" + +[portal.integrations.filters] +all = "Всички" +audit = "Одит" +connected = "Свързани" +notify = "Известяване" +security = "Сигурност" +signing = "Подписване" +storage = "Съхранение" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Свързано" + +[portal.integrations.table] +integration = "Интеграция" +worksWith = "Работи с" + +[portal.integrations.worksWith] +pipelines = "Конвейери" +policies = "Политики" +sources = "Източници" + +[portal.legal] +draft = "{{label}} · чернова" +loadError = "Този документ не можа да бъде зареден. Моля, опитайте отново." +loading = "Зареждане…" +title = "Правен документ" + [portal.nav] agent-builder = "Agent Builder" components = "Компоненти" @@ -7182,33 +7707,17 @@ documents = "Документи" editor = "Editor" home = "Начало" infrastructure = "Инфраструктура" +integrations = "Интеграции" pipelines = "Pipelines" policies = "Политики" procurement = "Снабдяване" -settings = "Настройки" sources = "Източници" usage = "Използване и фактуриране" users = "Потребители" -[portal.notifications] -markAllRead = "Маркирай всички като прочетени" -title = "Известия" -viewAll = "Виж всички" - -[portal.notifications.ariaLabel] -none = "Известия, няма непрочетени" -unread_one = "Известия, {{count}} непрочетено" -unread_other = "Известия, {{count}} непрочетени" - -[portal.notifications.count] -allRead = "всички прочетени" -loading = "зареждане" -new_one = "{{count}} ново" -new_other = "{{count}} нови" - -[portal.notifications.empty] -description = "Няма нови известия." -title = "Всичко е наваксано" +[portal.nav.section] +platform = "PDF платформа" +processor = "PDF Processor" [portal.pipelines] subtitle = "Всеки автоматизиран document pipeline на backend: подредена верига от операции върху набор от източници, стартирана от trigger. Щракнете върху ред за неговите стъпки и източници." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Нов pipeline" [portal.pipelines.builder] -addStep = "Добави инструмент" +activate = "Activate" back = "Назад към pipelines" +cannotFollow = "Не може да приеме {{produced}}" +chooseAccount = "Изберете акаунт" +chooseDestination = "Изберете дестинация" +chooseOperation = "Изберете какво прави тази стъпка" +chooseSource = "Изберете източник" discard = "Отхвърли промените" -enabled = "Активирано" +inputs = "Вход" +inputSource = "Входен източник" +inputTrigger = "Тригер" keepEditing = "Продължи редактирането" -needsUpload = "Нуждае се от качен файл" +moreActions = "More actions" +needsConfiguring = "Нуждае се от настройка" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Няма инструменти, отговарящи на търсенето ви." -pipelineSettings = "Настройки на pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Търсене на инструменти" -selectToolBody = "Добавете инструмент, за да изградите вашия pipeline." -selectToolTitle = "Все още няма инструменти" -toolSettings = "Настройки на инструмента" +sendToSystem = "Изпращане към друга система" +stepsIncompatible = "Тези стъпки не могат да се изпълнят върху това, което произвежда предишната им стъпка: {{tools}}." +stepsNeedSetup = "Тези стъпки все още трябва да бъдат настроени преди запазване: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Неразпозната операция, запазена както е." unsavedBody = "Имате незапазени промени. Запазете ги преди да напуснете или ги отхвърлете?" unsavedTitle = "Незапазени промени" -uploadUnsupported = "Качените файлове все още не се поддържат в pipelines, затова тези стъпки не могат да бъдат запазени: {{tools}}." usesDefaults = "Изпълнява се с настройки по подразбиране" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Комбинира всеки файл от предишната стъпка" +fan-out = "Изпълнява се веднъж за всеки файл от предишната стъпка" +format-mismatch = "Нуждае се от {{accepts}}, но предишната стъпка произвежда {{produced}}" +output-uncertain = "Може да не се изпълни: изходът на предишната стъпка зависи от начина, по който е настроена" +source-mismatch = "Нуждае се от {{accepts}}, но входът на този конвейер е {{produced}}" +undeclared-operation = "Не може да се провери какво приема тази стъпка" [portal.pipelines.composer] addTool = "Добави инструмент" -cancel = "Отказ" -chainEmpty = "Добавете инструмент, за да започнете да изграждате вашия pipeline." create = "Създай pipeline" -directory = "Изходна папка" -directoryHelp = "Абсолютен път на сървъра. Трябва да е в рамките на конфигурираните разрешени папки." +createPaused = "Create paused" editingUnsupported = "Показването на тези параметри на инструмента за редактиране все още не се поддържа." -moveDown = "Премести надолу" -moveUp = "Премести нагоре" +editSource = "Edit source" name = "Име" namePlaceholder = "напр. Redaction sweep" -noSources = "Все още няма свързани източници. Pipeline все още може да се изпълнява върху файлове, подадени директно към него." noToolSettings = "Този инструмент няма конфигурируеми настройки." -operations_one = "Операция ({{count}})" -operations_other = "Операции ({{count}})" output = "Изход" -removeStep = "Премахни операция" save = "Запази промените" scheduleEvery = "Изпълнявай на всеки" -sources = "Източници" -sourcesLoading = "Зареждане на източници..." trigger = "Trigger" triggerManual = "Само ръчно" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "дни" hours = "часа" minutes = "минути" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Да се изтрие ли \"{{name}}\"? Това не може да бъде отменено." cancel = "Отказ" @@ -7273,28 +7815,62 @@ confirm = "Изтрий" title = "Изтриване на pipeline?" [portal.pipelines.detail] +clearHistory = "Изчистване на историята" delete = "Изтрий pipeline" run = "Изпълни сега" [portal.pipelines.empty] action = "Създай pipeline" +connectSource = "Свързване на източник" description = "Създайте първия си pipeline: изберете източниците, върху които се изпълнява, свържете операциите и изберете къде да отива изходът." title = "Все още няма pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Активни" paused = "Поставени на пауза" total = "Pipelines" -[portal.pipelines.output] -folder = "Запис в папка" -inline = "Върни файлове" - [portal.pipelines.run] +allProcessed_one = "Няма нищо за изпълнение: {{count}} документът на източника вече е обработен." +allProcessed_other = "Няма нищо за изпълнение: всички {{count}} документа в източниците вече са обработени." completed_one = "Изпълнението завърши." completed_other = "Всички {{count}} изпълнения завършиха." empty = "Няма какво да се изпълни: източниците нямаха документи за обработка." failed = "Изпълнението не бе успешно: {{error}}" +inFlight = "Няма нищо ново за изпълнение: документи все още се обработват от предишно изпълнение." +parked_one = "Няма нищо за изпълнение: {{count}} документ е бил неуспешен преди и е паркиран. Отстранете причината, след това изчистете историята, за да опитате отново." +parked_other = "Няма нищо за изпълнение: {{count}} документа са били неуспешни преди и са паркирани. Отстранете причината, след това изчистете историята, за да опитате отново." running = "Изпълнението започна; все още е в ход." timeout = "Изпълнението отнема повече от очакваното; може все още да завърши във фонов режим." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Източници" status = "Статус" steps = "Стъпки" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Наблюдение на папка" @@ -7314,12 +7891,18 @@ manual = "Ръчно" schedule = "По график" [portal.policies] +defaultName = "Политика за {{category}}" subtitle = "Постоянни автоматизации, които налагат tool pipeline върху всеки документ. Всяка политика се задейства при качване или експорт, изпълнява веригата си от инструменти и запазва наложената версия до оригинала." title = "Политики" [portal.policies.card] comingSoon = "Надстройте до Enterprise" notSetUp = "Не е настроено" +requiresAiEngine = "Изисква AI engine" + +[portal.policies.categories.classification] +desc = "Идентифицирайте типа на всеки документ при качване и маркирайте метаданните му за архивиране и търсене." +label = "Класификация" [portal.policies.categories.compliance] desc = "Налагайте изискванията на HIPAA, GDPR, SOC 2 или FedRAMP върху всеки документ." @@ -7344,6 +7927,13 @@ label = "Сигурност" [portal.policies.config] scopeAll = "Всички документи" +[portal.policies.config.classification] +summary = "Класифицира всеки качен документ спрямо етикетите на вашия екип и маркира метаданните му." + +[portal.policies.config.classification.rules] +0 = "Класифициране" +1 = "Маркиране на метаданни" + [portal.policies.config.compliance] summary = "Валидира документи спрямо регулаторни рамки, преди да напуснат системата." @@ -7371,6 +7961,29 @@ minConfidence = "Минимална увереност" 2 = "Именуване" 3 = "Нормализиране" +[portal.policies.config.purview] +label = "Прилагане на етикет за чувствителност на Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "GUID на етикета от портала Purview. Прилагането на етикет записва метаданните му върху PDF; не може да криптира, за което е нужен Microsoft клиентът." +labelName = "Име на етикет (по избор)" +labelNameHelp = "Записва се заедно с ID, за да могат хората и инструментите да разпознаят етикета." + +[portal.policies.config.purview.fields.method] +help = "Purview различава етикет, приложен от политика, от такъв, избран от човек." +label = "Как се записва етикетът" +privileged = "Като избран от човек" +standard = "Като приложен автоматично" + +[portal.policies.config.purviewRead] +label = "Четене на етикет на Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Чете етикета, който документът вече носи, така че следващите стъпки да могат да действат според него." + [portal.policies.config.retention] summary = "Налага колко дълго се пазят документите, кога да се архивират и кога да се изтриват." @@ -7438,6 +8051,7 @@ title = "Все още няма активност" [portal.policies.endpoints] addWatermark = "Воден знак" autoRedact = "Редактиране на PII" +classifyAndLabel = "Класифициране" compressPdf = "Компресиране" flatten = "Изравняване" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Вашите политики са запазени и ще се retry = "Повторен опит" title = "Backend не е наличен" +[portal.policies.operations] +change = "Промяна на това, което прави тази стъпка" +noResults = "Няма стъпка, която да съвпада с това. Опитайте име на продукт или \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Търсене на стъпки - опитайте \"scan\", \"attach\" или име на продукт" + +[portal.policies.operations.bodyMode] +binary = "Сурови байтове на файла" +json = "Вътре в JSON payload" +multipart = "Като качване на файл (multipart)" + +[portal.policies.operations.clamavScan] +description = "Вашият собствен ClamAV сървър проверява документа; нищо не напуска мрежата ви." +label = "Сканиране за вируси (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Отхвърля документи, съдържащи макроси, изпълними файлове, скриптове или вградени обекти." +label = "Блокиране на активно съдържание" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive проверява документа и изпълнението спира, ако той не е чист." +label = "Сканиране за вируси" + +[portal.policies.operations.confluenceAttach] +description = "Архивира обработения документ на страница." +label = "Прикачване към Confluence страница" + +[portal.policies.operations.consignoSubmit] +description = "Изпраща документа към ConsignO Cloud за подписване." +label = "Изпращане за подпис (ConsignO)" +note = "Изпраща документа. Получаването на подписаното копие все още не се поддържа, защото стъпка не може да предаде резултат към следваща стъпка." + +[portal.policies.operations.customApiCall] +description = "Създайте извикването сами: път, метод, тяло и обработка на отговора." +label = "Извикване на персонализиран API" + +[portal.policies.operations.discordNotify] +description = "Уведомява канал, че политиката е обработила документ." +label = "Публикуване на съобщение в Discord" + +[portal.policies.operations.elasticIndex] +description = "Записва какво е направила политиката като документ с възможност за търсене." +label = "Индексиране на одитно събитие в Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart е подходящ за API за качване; JSON е подходящ за API, които искат файла вътре в payload; binary изпраща суровите байтове." +label = "Как да се изпрати документът" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON body, изпратено както е. {{document.base64}} съдържа файла; {{document.*}} и {{run.*}} се попълват за всеки документ." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Акаунт" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Полето във формуляра, под което API очаква документа." +label = "Име на поле за файл" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "От" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON обект с двойки име/стойност на headers. Идентификационните данни на връзката се добавят автоматично." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Индекс" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ключ на issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Изпраща се като тяло на съобщението. {{document.*}} и {{run.*}} се попълват за всеки документ." +label = "Съобщение" + +[portal.policies.operations.fields.method] +label = "Метод" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Добавя се към базовия URL на връзката. Никога не може да достигне друг host." +label = "Път" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Къде в drive да бъде записано. {{document.filename}} се попълва за всеки документ." +label = "Път на дестинацията" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Отчетът оставя документа непроменен. Замяната го сменя с това, което се върне." +label = "Какво да се направи с отговора" + +[portal.policies.operations.fields.signerEmail] +label = "Имейл на подписващия" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Тема" +placeholder = "Обработено: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio анализира този текст. Използвайте {{document.title}} или поставете текста, който искате да бъде проверен." +label = "Текст за проверка" + +[portal.policies.operations.fields.to] +label = "До" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud потребителско име" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Уведомява пространство, че политиката е обработила документ." +label = "Публикуване на съобщение в Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Архивира обработения документ към issue." +label = "Прикачване към Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "Изпраща обработения документ като прикачен файл." +label = "Изпращане на документа по имейл (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Записва обработения документ в папка." +label = "Качване в Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Вашият собствен Presidio сървър отчита личните данни, които намира." +label = "Откриване на лични данни" +note = "Presidio чете текст, не файлове. Дайте му текста, който искате да бъде проверен — самият документ не се качва." + +[portal.policies.operations.responseMode] +replace = "Замяна на документа с отговора" +report = "Запазване на документа, записване на отговора" + +[portal.policies.operations.sendgridEmail] +description = "Изпраща обработения документ като прикачен файл." +label = "Изпращане на документа по имейл (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Уведомява канал, че политиката е обработила документ." +label = "Публикуване на съобщение в Slack" + +[portal.policies.operations.splunkEvent] +description = "Записва какво е направила политиката там, където вашите одитори вече проверяват." +label = "Изпращане на одитно събитие към Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Записва какво е направила политиката към HTTP collector." +label = "Изпращане на одитно събитие към Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Уведомява канал, че политиката е обработила документ." +label = "Публикуване на съобщение в Teams" + +[portal.policies.operations.webhookPost] +description = "Изпраща самия документ към URL, който изберете." +label = "Публикуване на документа към webhook" + +[portal.policies.operations.zapierNotify] +description = "Предава детайлите за изпълнението към вашата автоматизация." +label = "Задействане на Zap или Make сценарий" + [portal.policies.stats] activeFor = "Активна" dataProcessed = "Обработени данни" @@ -7473,6 +8282,13 @@ label = "Приложени документи" description = "Конфигурирани, но не се задействат" label = "На пауза" +[portal.policies.table] +appliesTo = "Прилага се за" +docs = "Наложени документи" +enforces = "Налага" +policy = "Политика" +status = "Състояние" + [portal.policies.wizard.actions] back = "Назад" cancel = "Отказ" @@ -7480,6 +8296,10 @@ continue = "Продължи" enablePolicy = "Активиране на политика" saveChanges = "Запазване на промените" +[portal.policies.wizard.capability.classify] +desc = "Идентифицира типа на документа от етикетите на вашия екип и го маркира, така че да се архивира и търси по категория." +label = "Класифициране на документа" + [portal.policies.wizard.capability.compress] desc = "Компресира документа до по-малък размер на файла." label = "Намаляване на размера на файла" @@ -7500,10 +8320,18 @@ label = "Заличаване на чувствителна информация desc = "Премахва скрит JavaScript, така че нищо да не може да се стартира автоматично при отваряне на документа." label = "Премахване на активно съдържание" +[portal.policies.wizard.capability.timestampPdf] +desc = "Доказва, че документът е съществувал в точно тази форма в даден момент, използвайки независим орган за timestamp. Изпраща се само hash - документът никога не напуска вашия сървър." +label = "Добавяне на доверен timestamp" + [portal.policies.wizard.capability.watermark] desc = "Поставя видим знак (напр. „Поверително“) върху всяка страница." label = "Прилагане на воден знак" +[portal.policies.wizard.classification] +description = "Всеки качен документ се класифицира спрямо вградените етикети и се маркира с типовете, които му подхождат. Наборът от етикети е споделен за целия ви екип." +labelsHeading = "Етикети за класификация" + [portal.policies.wizard.errors] noTools = "Първо активирайте поне един инструмент в работния процес." saveFailed = "Политиката не можа да бъде запазена. Моля, опитайте отново." @@ -7548,73 +8376,118 @@ setUp = "Настройване на политика за {{category}}" [portal.policies.wizard.workflow] description = "Изберете какво прави тази политика с всеки документ, който обработва." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} активни" -noRule = "Все още няма приложено правило" -subtitle = "Постоянни автоматизации, през които преминава всеки документ, независимо кой pipeline го обработва." -title = "Какво се изпълнява върху вашите PDFs" - [portal.policySummary.action] -comingSoon = "Надстройка до Enterprise" -configure = "Конфигуриране" setUp = "Настройване" -[portal.policySummary.column] -activeRule = "Активно правило" -policy = "Политика" -status = "Статус" +[portal.processorFlow] +footnote = "Броят е за последните 24 часа. Скоростта на потока е илюстративна." +liveBadge = "На живо" +stats = "{{connected}} свързани · {{processed}} PDF обработени" +title = "PDF Processor" -[portal.policySummary.empty] -description = "След като политиките бъдат конфигурирани, категориите ще се появят тук." -title = "Все още няма политики" +[portal.processorFlow.lens] +ariaLabel = "Режим на преглед" +flow = "Поток" +sankey = "Sankey" -[portal.policySummary.state] -active = "Активна" -off = "Изключена" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Неуспешни" +heading = "Резултати" +success = "Доставени" + +[portal.processorFlow.policies] +activeCount = "{{n}} активни" +count = "{{n}} · 24h" +heading = "Политики" +setUp = "Настройване" soon = "Скоро" -[portal.processingStatus] -managePlan = "Управление на план" -volumeSuffix = "Обработени PDFs · последните 30 дни" +[portal.processorFlow.sankey] +waist = "Политики · {{n}} активни" + +[portal.processorFlow.sankey.empty] +description = "Свържете източник и включете политика, за да наблюдавате как документите преминават през processor." +title = "Все още няма поток" + +[portal.processorFlow.sources] +comingSoonTag = "Свързване" +editor = "Stirling PDF Editor" +heading = "Източници" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Приемане по имейл" [portal.procurement] -reset = "Нулиране на procurement (demo)" subtitle = "Оценете, договорете и въведете екипа си в работа. Всеки документ на едно място." title = "Procurement" -[portal.procurement.action] -download = "Изтегляне" -pay = "Плати сега" -request = "Заявка" -sign = "Преглед и подпис" -upload = "Качване" - [portal.procurement.agreement] agreeCta = "Съгласие и абониране" +confidential = "Поверително" confirm = "Прочетох и приемам Stirling Enterprise Agreement." -eyebrow = "Споразумение" -intro = "Едно обединено споразумение покрива вашата сделка: Master Service Agreement, Order Form, EULA и Data Processing Agreement. Прегледайте го, след което се съгласете, за да приемете офертата като ангажиран абонамент." -title = "Прегледайте вашето enterprise споразумение" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA и DPA, комбинирани в един подпис." +download = "Изтегляне" +downloadDraftError = "Не можа да бъде генериран PDF на споразумението - renderer-ът на документа не е наличен. Моля, опитайте отново след малко или се свържете с поддръжката." +downloadError = "Подписаното споразумение не можа да бъде изтеглено. Моля, опитайте отново." +legalName = "Име на юридическото лице" +legalNamePlaceholder = "Юридическото лице, което ще подпише" +loadError = "Споразумението не можа да бъде заредено. Моля, опитайте отново." +loading = "Зареждане на споразумението..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Заявка за промени" +scrollHint = "Превъртете за преглед" +signatory = "Име на подписващия" +signatoryPlaceholder = "Пълно име" +signatoryTitle = "Длъжност на подписващия" +signatoryTitlePlaceholder = "напр. главен юрисконсулт" +signError = "Подписът ви не можа да бъде записан. Моля, опитайте отново." [portal.procurement.builder] addons = "Добавки" +addressLine1 = "Адресен ред 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Адресен ред 2" +addressLine2Placeholder = "Офис, етаж, сграда" back = "Назад" businessName = "Име на фирмата" businessNamePlaceholder = "Вашата компания" +city = "Град" +cityPlaceholder = "San Francisco" +completeRequired = "Моля, попълнете задължителните полета (маркирани с *) с валиден имейл, преди да генерирате офертата." +contactEmail = "Имейл за контакт" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Име за контакт" +contactNamePlaceholder = "Jane Doe" continue = "Продължи" -country = "Държава" -countryEuro = "Еврозона (EUR €)" -countryUK = "Обединено кралство (GBP £)" -countryUS = "Съединени щати (USD $)" +done = "Готово" eula = "Прочетох и приемам Stirling Enterprise EULA. Тя урежда споразумението, генерирано от тази оферта." generate = "Генериране на оферта" -included = "Включено" indemnification = "Обезщетение за IP" indemnificationSub = "Ние защитаваме квалифицирани IP претенции съгласно EULA" -offlineLicense = "Офлайн / air-gapped лиценз" -offlineLicenseSub = "Файл с лиценз за изтегляне за air-gapped self-hosted инстанция" +paperEyebrow = "Enterprise оферта" +paperFor = "Подготвено за" +pdfSize = "Размер на PDF" +poNumber = "PO number" +poNumberPlaceholder = "По избор" +postalCode = "Пощенски код" +postalCodePlaceholder = "94105" +posture = "Управление" +posture_count = "~{{count}} политики" +posture_essentials = "Essentials" +posture_essentialsSub = "Класификация + споделяне — настройките по подразбиране" +posture_governed = "Governed" +posture_governedSub = "Добавя сигурност и маршрутизиране" +posture_regulated = "Regulated" +posture_regulatedSub = "Всяка категория — съответствие, задържане, приемане" qbr = "Тримесечни бизнес прегледи" qbrSub = "Вашият SE преглежда употребата и roadmap всяко тримесечие" +region = "Щат / регион" +regionPlaceholder = "California" running = "{{annual}} / год. · {{years}}-год. {{tcv}}" s1Sub = "Вашият екип и PDFs, които очаквате да обработвате всяка година." s1Title = "Колко ще обработвате?" @@ -7623,6 +8496,12 @@ s2Title = "Ангажимент и обслужване" s3Sub = "За офертата и споразумението, което тя генерира." s3Title = "Вашите данни" serviceLevel = "Ниво на обслужване" +size_compact = "Компактен" +size_compactSub = "Предимно текст, под 1 MB" +size_heavy = "Тежък" +size_heavySub = "Сканиран или с много изображения, 5 MB+" +size_standard = "Стандартен" +size_standardSub = "Смес от текст и изображения, 1 до 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 работни часа · dedicated account manager · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "Същия работен ден · named CSM · +15%" slStandard = "Standard" slStandardSub = "Следващ работен ден · shared CSM · включено" stepOf = "Стъпка {{n}} от {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "По избор" term = "Срок" termDiscount = "Приложена е {{pct}}% отстъпка за многогодишен ангажимент" title = "Създайте вашата оферта" @@ -7637,6 +8518,7 @@ training = "Въвеждане и обучение" trainingSub = "Сесии на живо, за да може екипът ви да започне работа" users = "Общо потребители" usersPlaceholder = "напр. 250" +viewEula = "Прочетете го" volEstimated = "Оценено от {{count}} потребители (~2 000 PDFs всеки, включително автоматизация). Редактирайте, ако имате по-точна информация." volManual = "Използваме вашата стойност. Можете по всяко време да преизчислите по размера на екипа си." volNoUsers = "Не сте сигурни? Въведете размера на екипа си и ще го оценим." @@ -7645,55 +8527,55 @@ volumePlaceholder = "напр. 1 000 000" years_one = "{{count}} година" years_other = "{{count}} години" -[portal.procurement.docs] -count_one = "{{count}} документ" -count_other = "{{count}} документа" -done = "Готово" -here = "Вие сте тук" -hide = "Скриване" -optional = "По избор" -paidAddon = "Платена добавка" -show = "Показване" -subtitle = "Всичко необходимо на всяка стъпка от процеса, показвано според напредъка на сделката." -supportingSubtitle = "SOC 2, прегледи на сигурността, данъчни формуляри и други, готови когато вашият екип по сигурност или procurement ги поиска. Някои имат еднократна такса." -supportingTitle = "Подкрепа за вашата оценка" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form и DPA в един подпис." +download = "Изтегляне" +eula = "EULA & Commercial Terms" +eulaSub = "Софтуерът и условията за самостоятелно търговско обслужване." +invoice = "Фактура" +invoiceSub = "Вашата първа фактура за абонамент." +laterInvoice = "Налично, след като приемете" +laterQuote = "Налично, след като офертата ви бъде издадена" +quote = "Оферта" +quoteSub = "Вашата детайлизирана enterprise оферта." +sla = "SLA Exhibit" +slaSub = "Нива на поддръжка и целеви времена за отговор." +subprocessors = "Подобработващи" +subprocessorsSub = "Трети страни, които обработват данни от ваше име." +subtitle = "Вашите документи по сделката, налични по всяко време." title = "Документи" -upcoming = "Предстоящи" +view = "Преглед" [portal.procurement.error] title = "Нещо се обърка" [portal.procurement.hero] -company = "Вашата enterprise сделка" +barAria = "Етап {{current}} от {{total}}" ctaAgreement = "Преглед и подпис на споразумението" -ctaLive = "Вече сте на живо" -ctaPayment = "Добавяне на плащане" +ctaExploring = "Настройте пробния си период" ctaQuote = "Преглед на офертата" +ctaReviewQuote = "Преглед на офертата" ctaTrial = "Създайте офертата си" +documents = "Документи" eyebrow = "Enterprise procurement" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Поканете съотборници" -keyDocs = "Ключови документи" -nextStep = "Следваща стъпка: {{action}}" -notStarted = "Не е започнато" +licenseKey = "Лицензионен ключ" +liveSub = "Вашата организация е осигурена и лицензът ви е активен." +liveTitle = "Вече сте на живо със Stirling Enterprise" +next = "Следващо: {{stage}}" open = "Отваряне на procurement" scheduleCall = "Насрочване на разговор" -setup1Sub = "Поканете съотборниците си" -setup1Title = "Внедряване на PDF Editor" -setup2Sub = "Включете обработката в редакторите и други източници" -setup2Title = "Свързване на PDF Processor" -setup3Sub = "Включете Security, Compliance, Routing или Retention, когато са ви необходими" -setup3Title = "Добавяне на препоръчани политики" +sentenceAgreement = "преглед и подписване на споразумението" +sentenceLive = "осигуряване и внедряване" +sentencePayment = "поръчка за покупка и плащане" +sentenceQuote = "преглед и приемане на вашата оферта" +sentenceTrial = "настройване на пробното ви работно пространство" [portal.procurement.journey] daysLeft_one = "Остава {{count}} ден" daysLeft_other = "Остават {{count}} дни" -engineerLabel = "Вашият solutions engineer" -eyebrow = "Вашето внедряване" -live = "Вече сте на живо със Stirling Enterprise" -nextStep = "Следваща стъпка: {{action}}" -subtitle = "Вашият solutions engineer е с вас на всяка стъпка. По едно следващо действие наведнъж; пълният списък е по-долу." -title = "От trial до live, един насочван път" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "Един подпис покрива MSA, order form, EULA и DPA." @@ -7727,6 +8609,9 @@ downloadError = "Файлът за офлайн лиценз все още не downloadOffline = "Изтегляне на офлайн лиценз (.lic)" hint = "Поставете този ключ в self-hosted инстанция, за да я активирате, или го запазете за вашите записи. Пазете го на сигурно място." label = "Вашият лицензен ключ" +subtitle = "Активирайте self-hosted инстанция с този ключ или го запазете за архив." +title = "Вашият лицензионен ключ" +trialFileHint = "Това е вашият пробен лицензионен файл. След като споразумението ви е в сила, върнете се и го изтеглете отново — за разлика от онлайн лицензионния ключ, .lic файлът няма да се актуализира сам." [portal.procurement.link] cta = "Свързване на акаунт" @@ -7739,65 +8624,62 @@ description = "Вашият абонамент е активен и лиценз eyebrow = "Live" title = "Вече сте на живо със Stirling Enterprise" -[portal.procurement.locked] -description = "Trial ключове, оферти за ангажиран обем, споразумение с един подпис, плащане и вашият регистър на документи са тук, след като започнете enterprise оценка." -eyebrow = "Само Enterprise" -talkToSales = "Свържете се с продажби" -title = "Procurement пътят се отваря с Enterprise" - [portal.procurement.milestone] -accept = "Приемане и продължаване" -description = "Изтеглете PDF, за да го споделите с екипа си, върнете се да приемете, когато сте готови, или направете промени." -download = "Изтегляне на PDF" downloadError = "PDF офертата все още не можа да бъде изтеглена — моля, опитайте отново след малко." -edit = "Редактиране на оферта" -eyebrow = "Оферта {{number}}" -perYear = " / год." -preparedFor = "Подготвено за {{company}}" -tcv = "{{value}} обща стойност на договора" -title = "Вашата оферта е готова" - -[portal.procurement.modal] -cancel = "Отказ" -chooseFile = "Избор на файл" -close = "Затваряне" -downloadBody = "Изтеглянето ви ще започне скоро." -downloadCta = "Изтегляне" -downloadTitle = "Изтегляне" -noFile = "Няма избран файл" -payBody = "Платете ангажирания си договор с карта или банков превод чрез Stripe. Работното ви пространство се осигурява веднага щом плащането бъде изчистено." -payCta = "Продължи към Stripe" -payTitle = "Потвърждаване на плащането" -requestBodyFree = "Генерираме това при поискване. Потвърдете и вашият solutions engineer ще го изпрати скоро." -requestBodyPaid = "Това е платена добавка. Потвърдете и вашият solutions engineer ще определи обхвата и ще изпрати документите." -requestCta = "Заявка" -requestTitle = "Заявка за този документ" -signBody = "Отваря Stirling Enterprise Agreement за електронен подпис: един подпис покрива MSA, order form, EULA и DPA. Ние подписваме насрещно автоматично и преминавате към плащане." -signCta = "Отваряне за подпис" -signTitle = "Прегледайте и подпишете споразумението си" -uploadBody = "Изпратете ни вашата PO и ние ще фактурираме спрямо нея по вашите условия. Плъзнете PDF или изберете файл по-долу." -uploadCta = "Качване на purchase order" -uploadTitle = "Качете вашата purchase order" [portal.procurement.payment] description = "Вашата оферта е приета и лицензът ви вече е активен — екипът ви може да започне веднага. Платете първата фактура, когато сте готови; можете да я платите или изтеглите тук, без да е нужен email." +downloadAgreement = "Изтегляне на подписаното споразумение" downloadInvoice = "Изтегляне на фактура" -simulate = "Симулиране на получено плащане (demo)" +eyebrow = "Плащане" title = "Абонаментът е създаден" viewInvoice = "Преглед и плащане на фактура" +[portal.procurement.review] +acceptCta = "Приемане на офертата" +annual = "Годишна такса (година 1)" +downloadCta = "Изтегляне на офертата" +poNumber = "Поръчка за покупка: {{po}}" +renewal = "Подновява се на {{amount}}/година след срока (+{{pct}}% CPI)" +tcv = "{{years}}-годишен срок, платен изцяло предварително: {{tcv}}" +validUntil = "Валидно до {{date}}" + [portal.procurement.schedule] fallback = "Планировчикът не можа да бъде зареден." fallbackLink = "Отваряне на планирането в нов раздел" +loading = "Зареждане на планировчика..." subtitle = "Вашият solutions engineer ще преведе екипа ви през внедряването. Изберете удобно за вас време." title = "Насрочване на разговор" -[portal.procurement.status] -action = "Необходимо е действие" -available = "Налично" -complete = "Завършено" -pending = "В изчакване" -request = "При заявка" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Изолирано, без изходяща връзка" +back = "Назад" +businessName = "Име на фирмата" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Управлявано, най-бърз старт" +continue = "Продължи" +deployment = "Къде трябва да бъде разположено?" +eula = "Съгласен/съгласна съм със Stirling EULA & Commercial Terms." +fullName = "Пълно име" +fullNamePlaceholder = "Вашето име" +invites = "Поканете съотборници · по избор" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Насрочване на разговор" +seats = "Хора, които тестват" +seatsPlaceholder = "напр. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker или Kubernetes" +start = "Стартиране на пробен период" +stepOf = "Стъпка {{n}} от {{total}}" +subtitle = "Безплатно за 14 дни, без карта." +subtitleDetails = "Няколко детайла за вашата оферта и споразумение." +talkFirst = "Предпочитате първо да поговорим?" +title = "Настройте пробния си период" +viewEula = "Прочетете го" +workEmail = "Служебен имейл" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Удължаването добавя 7 дни и уведомява вашия solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Достигнат максимум" subtitle = "Вашият безплатен trial продължава до {{date}}. Не се изисква карта." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Ценообразуване за ангажиран обем, SSO + SCIM + RBAC за цялата организация, 90-дневен неизменяем одит и dedicated SE." -homeCta = "Стартиране на Trial →" -homeHeadline = "Обработвайте милиони PDFs." - -[portal.recentActivity] -title = "Скорошна активност" -viewAll = "Преглед на всички" - -[portal.recentActivity.empty] -description = "Изпълнения на pipeline, внедрявания и събития от агенти ще се появят тук." -title = "Тук все още няма нищо" - -[portal.search] -ariaLabel = "Търсене" -placeholder = "Търсене в Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Бързите действия ще се появят тук, когато са налични." -noActionsTitle = "Няма бързи действия" -noMatches = "Няма съвпадения за „{{query}}“" -noMatchesDescription = "Опитайте с друга ключова дума или разгледайте каталога." - [portal.settings.groups] admin = "Администратор" [portal.settings.sections] account-link = "Свързване на акаунт" -[portal.shell.header] -accountFallback = "Акаунт" -accountMenu = "Меню на акаунта" -darkMode = "Тъмен режим" -lightMode = "Светъл режим" -search = "Търсене" -searchPlaceholder = "Търсене…" -signOut = "Изход" -switchToDark = "Превключване към тъмна тема" -switchToLight = "Превключване към светла тема" - [portal.shell.sidebar] appEditor = "Редактор" appProcessor = "Процесор" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} документа" linkAccount = "Свързване на Stirling акаунт" -planEditor = "План Editor" -planEnterprise = "План Enterprise" -planProcessor = "План Processor" primaryNav = "Основна навигация" switchApp = "Превключване на приложение" +[portal.shell.topbar] +closeNav = "Затваряне на навигацията" +openNav = "Отваряне на навигацията" +search = "Търсене" + [portal.sources] subtitle = "Повторно използваеми входни връзки, които подават документи към Stirling. Конфигурирайте връзка веднъж, след това я използвайте в произволен брой политики. Щракнете върху ред за конфигурацията ѝ и кои политики я използват." title = "Източници" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Свързване на източник" +[portal.sources.builder] +back = "Назад към източниците" +backToTypes = "Всички типове източници" +cancel = "Отказ" +chooseHint = "Изберете откъде идват документите. Конекторите в сиво са в процес на разработка." +comingSoon = "Очаквайте скоро" +comingSoonHeading = "Очаквайте скоро" +create = "Създаване на източник" +createTitle = "Свързване на източник" +delete = "Изтриване" +editTitle = "Редактиране на източник" +enabled = "Активирано" +save = "Запазване на промените" + +[portal.sources.builder.folderAccess] +description = "Автоматизациите за папки могат да използват само папки, разрешени от администратор. Добавете я в настройките за достъп до папки, след това опитайте отново." +openSettings = "Настройки за достъп до папки" +title = "Тази папка не е разрешена" + [portal.sources.delete] body = "Да се изтрие ли „{{name}}“? Това не може да бъде отменено. Политиките, които го използват, ще трябва да бъдат актуализирани." cancel = "Отказ" confirm = "Изтриване" title = "Да се изтрие ли източникът?" -[portal.sources.detail] -closeAriaLabel = "Затваряне на детайлите" -delete = "Изтриване на източник" -docs24h = "Последните 24 ч." -docs30d = "Последните 30 дни" -docsTotal = "Общо видени" -docsTrend = "Документи през последните 30 дни" -documents = "Документи" -edit = "Редактиране" -notReferenced = "Не се използва от никоя политика, така че е безопасно да бъде изтрит." -pause = "Пауза" -resume = "Възобновяване" -subtitle = "{{type}} · {{status}}" -usedBy = "Използва се от" - -[portal.sources.empty] -description = "Свържете папка (а скоро и cloud storage), така че вашите политики да имат откъде да вземат документи." -title = "Все още няма свързани източници" - [portal.sources.kpi] inUse = "В употреба" total = "Връзки" unused = "Неизползвани" +[portal.sources.networkFields.connection] +helperText = "Запазената връзка със сървърния адрес и идентификационните данни. Използва се повторно от всеки източник, който я посочва." +label = "Връзка" + +[portal.sources.networkFields.directory] +helperText = "Папка на сървъра за проверка, относително към home при вход или корена на споделянето. Оставете празно за корена." +label = "Папка" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume премахва всеки файл от сървъра, след като всяка политика го обработи." +label = "Режим на четене" + +[portal.sources.networkFields.mode.options] +consume = "Consume: обработване на всеки файл веднъж" +snapshot = "Snapshot: повторно четене на папката при всяко изпълнение" + +[portal.sources.networkFields.recursive] +label = "Дълбочина на папката" + +[portal.sources.networkFields.recursive.options] +all = "Включване на подпапки" +top = "Само най-горно ниво" + [portal.sources.status] active = "Активен" disabled = "Деактивиран" unused = "Неизползван" [portal.sources.table] +documents = "Документи" source = "Източник" status = "Статус" +type = "Type" usedBy = "Политики" +[portal.sources.types.box] +description = "Изтегляйте документи от Box папки, които екипът ви вече споделя." +label = "Box" + +[portal.sources.types.dropbox] +description = "Изтегляйте документи от споделена Dropbox папка." +label = "Dropbox" + [portal.sources.types.editor] +description = "Документи, които екипът ви е обработил в editor, в изпълнения на политики и AI." label = "Редактор" +noPolicies = "Все още няма политики, изпълнени от editor." + +[portal.sources.types.email] +description = "Превръщайте прикачени файлове, изпратени до специална пощенска кутия, в документи." +label = "Имейл входяща кутия" [portal.sources.types.folder] description = "Наблюдава директория на сървъра за нови документи." @@ -7943,25 +8829,80 @@ label = "Дълбочина на папката" all = "Включване на подпапки" top = "Само най-горно ниво" +[portal.sources.types.ftp] +description = "Проверявайте папка на FTP или FTPS сървър за нови документи." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Изтегляйте документи от споделени Google Drive папки." +label = "Google Drive" + +[portal.sources.types.network] +description = "Наблюдавайте SMB network share за нови документи." +label = "Мрежово устройство" + +[portal.sources.types.onedrive] +description = "Наблюдавайте OneDrive папки за нови документи." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Изтегляйте документи от Amazon S3 или S3-съвместим bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Запазената връзка, съдържаща bucket и идентификационните данни. Използва се повторно от всеки източник и изход на конвейер, който я посочва." +label = "Връзка" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume премахва всеки object от bucket, след като всяка политика го обработи." +label = "Режим на четене" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: обработване на всеки object веднъж" +snapshot = "Snapshot: повторно четене на bucket при всяко изпълнение" + +[portal.sources.types.s3.fields.prefix] +helperText = "Обработват се само object-и, чиито ключове започват с този prefix." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Проверявайте SFTP drop папка за нови документи." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Изтегляйте документи от библиотеки с документи на SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Източник" +[portal.sources.types.webhook] +createNote = "Delivery URL и тайна за подписване се генерират, когато създадете този webhook. Подателите изпращат POST документи към този URL и посочващите политики се изпълняват при пристигане." +description = "Получавайте документи чрез подписан HTTP POST от всяка външна система." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Тайната за подписване се показва веднъж, когато webhook бъде създаден. Създайте източника отново, за да я смените." + +[portal.sources.types.webhook.reveal] +copy = "Копиране" +done = "Готово" +secret = "Тайна за подписване" +secretHelp = "Подписвайте суровото тяло на всяка доставка с този ключ (HMAC-SHA256) и го изпращайте като X-Stirling-Signature header." +secretWarning = "Копирайте тайната за подписване сега. За ваша сигурност тя се показва само веднъж и не може да бъде извлечена по-късно." +title = "Webhook създаден" +url = "Delivery URL" +usage = "POST всеки документ като сурово тяло на заявката към delivery URL с binary content type (application/pdf или application/octet-stream). Посочващите политики се изпълняват автоматично при пристигане." + [portal.sources.wizard] -back = "Назад" -cancel = "Отказ" -continue = "Продължи" -editTitle = "Редактиране на източник" name = "Име" namePlaceholder = "напр. Прием на искове" -save = "Запазване на промените" -subtitle = "Стъпка {{current}} от {{total}} · {{label}}" -title = "Свързване на източник" type = "Тип" -[portal.sources.wizard.steps] -chooseType = "Избор на тип" -configure = "Конфигуриране" -review = "Преглед и свързване" +[portal.stepModal] +close = "Затваряне" [portal.usage] managePayment = "Управление на плащане" @@ -8021,16 +8962,9 @@ summary = "Притежава екип — управлява ресурсите 2 = "Достъп до портала чрез политиката по подразбиране" 3 = "Всичко, което може членът" -[portal.welcome] -ariaLabel = "Добре дошли в Stirling PDF" -badge = "Open-source" -installEditor = "Инсталиране на редактора" -inviteTeammates = "Поканете съотборници" -openInBrowser = "Отваряне в браузър" -perks = "Безплатно завинаги · Self-hostable" -subtitle = "Най-сигурният PDF редактор в света е безплатен за екипи от всякакъв размер. Включва 60+ PDF операции и SSO." -title = "Добре дошли в" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Неограничено" [printFile] title = "Печат на файл" @@ -8613,6 +9547,14 @@ text = "Завъртете страниците на вашия PDF по или [rotate.tooltip.header] title = "Преглед на настройките за завъртане" +[ruler] +clearAll = "Изчистване на всички" +hideAllLabels = "Скриване на всички етикети" +hideSmallLabels = "Скриване на малките етикети" +physicalValues = "Физически" +scaled = "Мащабирани" +showAllLabels = "Показване на всички етикети" + [sanitize] desc = "Премахнете потенциално вредни елементи от PDF файлове." sanitizationResults = "Резултати от санирането" @@ -8654,31 +9596,45 @@ label = "Премахни XMP метаданни" [sanitize.steps] settings = "Настройки" -[sanitizePdf] -tags = "чисти,сигурни,безопасни,премахване-заплахи" - [sanitizePDF] title = "Дезинфектирай PDF" +[sanitizePdf] +tags = "чисти,сигурни,безопасни,премахване-заплахи" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Коригиране на мащаба на страницата" +[scaleSettings] +activeScale = "Активен мащаб" +apply = "Прилагане на мащаб" +applyCalibration = "Прилагане на калибриране" +calculatedScale = "Изчислен мащаб" +calibrate = "Калибриране" +calibrating = "Калибриране" +calibrationDistanceRequired = "Въведете реално разстояние, по-голямо от нула" +calibrationInvalid = "Не може да се изчисли мащабът от това измерване" +calibrationPaperDistance = "Измерено разстояние на страницата: {{distance}}" +calibrationTitle = "Калибриране на мащаба" +calibrationTooltip = "Измерете известно разстояние, за да изчислите мащаба автоматично" +customScale = "Персонализиран мащаб" +noneSet = "Няма зададен персонализиран мащаб" +presets = "Предварително зададени мащаби" +ratio = "Съотношение на мащаба" +ratioHelp = "Съотношение: 1 единица на страницата = X реални единици" +ratioPlaceholder = "напр. 100" +ratioPositive = "Съотношението трябва да е по-голямо от нула" +ratioRequired = "Съотношението е задължително" +realDistance = "Реално разстояние" +realDistancePlaceholder = "напр. 5" +reset = "Нулиране до стойности по подразбиране" +unit = "Единица" +unitInvalid = "Невалидна единица: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Праг на ъгъла:" -10 = "Задава размера на добавената и премахната граница, за да предотврати бели граници към изхода (по подразбиране: 1)." -2 = "Задава минималния абсолютен ъгъл, необходим за завъртане на изображението (по подразбиране: 10)." -3 = "Толеранс:" -4 = "Определя обхвата на цветовата вариация около предполагаемия фонов цвят (по подразбиране: 30)." -5 = "Минимална площ:" -6 = "Задава минималния праг на площ за изображение (по подразбиране: 10000)." -7 = "Минимална контурна площ:" -8 = "Задава минималния праг на контурната площ за изображение" -9 = "Размер на рамката:" - [scannerImageSplit] submit = "Извлечи сканирани изображения" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Какво прави" whatThisDoesDesc = "Автоматично открива и извлича всяка снимка от сканирана страница или композитно изображение—без ръчно изрязване." whenToUse = "Кога да се използва" +[ScannerImageSplit.selectText] +1 = "Праг на ъгъла:" +10 = "Задава размера на добавената и премахната граница, за да предотврати бели граници към изхода (по подразбиране: 1)." +2 = "Задава минималния абсолютен ъгъл, необходим за завъртане на изображението (по подразбиране: 10)." +3 = "Толеранс:" +4 = "Определя обхвата на цветовата вариация около предполагаемия фонов цвят (по подразбиране: 30)." +5 = "Минимална площ:" +6 = "Задава минималния праг на площ за изображение (по подразбиране: 10000)." +7 = "Минимална контурна площ:" +8 = "Задава минималния праг на контурната площ за изображение" +9 = "Размер на рамката:" + [search] noResults = "Няма намерени резултати" placeholder = "Въведете търсен термин..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Вашият Stirling-PDF сървър е офлай [session] expired = "Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново." -[sessionManagement.tooltip] -header = "Управление на сесиите за подписване" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Новите участници се добавят в края на реда за подписване" -bullet2 = "Не могат да се добавят участници след финализиране на сесията" -bullet3 = "Всеки участник получава известие, когато му дойде редът" -description = "Можете да добавяте още участници към активна сесия по всяко време преди финализиране." -title = "Добавяне на участници" - -[sessionManagement.tooltip.finalization] -bullet1 = "Пълно финализиране: Всички участници са подписали" -bullet2 = "Частично финализиране: Някои участници все още не са подписали" -bullet3 = "Неподписалите участници ще бъдат изключени от крайния документ" -bullet4 = "След финализиране можете да заредите подписания PDF в активните файлове" -description = "Финализирането комбинира всички подписи в един подписан PDF. Това действие не може да бъде отменено." -title = "Финализиране на сесия" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Не могат да бъдат премахнати участници, които вече са подписали" -bullet2 = "Премахнатите участници повече няма да получават известия" -bullet3 = "Редът за подписване се коригира автоматично" -description = "Участниците могат да бъдат премахнати от сесии преди да подпишат." -title = "Премахване на участници" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Всеки подпис се прилага последователно върху PDF" -bullet2 = "По-късните подписващи виждат по-ранните подписи" -bullet3 = "Критично за процеси по одобрение и правни вериги на отговорност" -description = "Редът, който посочвате при създаване на сесията, определя кой подписва първи." -title = "Ред на подписите" - [settings] close = "Затворете" +[settings.ai] +documents = "Документи и RAG" +general = "Общи" +limits = "Ограничения и производителност" +models = "Модели и доставчици" +title = "AI" + [settings.configuration] advanced = "Разширени" database = "База данни" endpoints = "Крайни точки" features = "Функции" +folderAccess = "Достъп до папки" mcp = "MCP Server" storageSharing = "Съхранение на файлове и споделяне" systemSettings = "Системни настройки" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Редактор на PDF по подразбиране" defaultPdfEditorActive = "Stirling PDF е вашият редактор на PDF по подразбиране" defaultPdfEditorChecking = "Проверка..." defaultPdfEditorInactive = "Друго приложение е зададено по подразбиране" +defaultPdfEditorRemind = "Напомняне за задаване по подразбиране" +defaultPdfEditorRemindDescription = "Показване на банер, когато Stirling PDF не е вашето PDF приложение по подразбиране." defaultPdfEditorSet = "Вече е по подразбиране" defaultStartupView = "Начален изглед по подразбиране" defaultStartupViewDescription = "Изберете кой раздел в лявата колона да е активен при стартиране на приложението" @@ -8817,9 +9763,18 @@ languageDescription = "Изберете езика на показване" logout = "Изход" setAsDefault = "Задай като по подразбиране" theme = "Тема" +themeAccent = "Акцентен цвят" +themeAccentCustom = "Персонализиран цвят" +themeAccentCustomHint = "Персонализиран (препоръчват се стойностите по подразбиране)" +themeAccentDark = "Акцентен цвят за тъмен режим" +themeAccentDefault = "По подразбиране" +themeAccentDefaultHint = "Тема по подразбиране (препоръчително)" +themeAccentDescription = "Бутоните, връзките и акцентите го следват и той леко оцветява приложението. Светлият и тъмният режим имат свои собствени. Системният използва активния." +themeAccentLight = "Акцентен цвят за светъл режим" themeDark = "Тъмна" themeDescription = "Превключвайте между светъл и тъмен режим" themeLight = "Светла" +themeReset = "Възстановяване на темата по подразбиране" themeSystem = "System" title = "Общи" user = "Потребител" @@ -8833,6 +9788,12 @@ intro = "Активирайте удостоверяване на потреби learnMore = "Научете повече в документацията" title = "За системни администратори" +[settings.general.loginLanding] +description = "Изберете кое приложение да се отваря, когато влизате в Stirling Cloud." +editor = "Редактор" +processor = "Обработчик" +title = "След влизане" + [settings.general.mode] fullscreen = "Цял екран" sidebar = "Странична лента" @@ -9013,9 +9974,6 @@ title = "Политики и поверителност" [settings.preferences] title = "Предпочитания" -[settings.search] -placeholder = "Търсене в страниците с настройки..." - [settings.security] description = "Актуализирайте паролата си, за да запазите акаунта си защитен." title = "Сигурност" @@ -9041,6 +9999,11 @@ security = "Сигурност" telegram = "Telegram" title = "Сигурност и удостоверяване" +[settings.signOut] +confirm = "Сигурни ли сте, че искате да излезете?" +submit = "Изход" +title = "Изход" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Последна страница" maintainRatio = "Превключване за поддържане на съотношението на страните" next = "Следваща страница" noSavedSigs = "Не са намерени запазени подписи" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Лични подписи" previous = "Предишна стараница" redo = "Повтори" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Неуспешно премахване на ф hint = "Качете PNG или JPG изображение на вашия подпис" label = "Качете изображение на подпис" placeholder = "Изберете файл с изображение" +previewAlt = "Current image signature" processing = "Обработка на изображението..." removeBackground = "Премахване на бял фон (прозрачен)" @@ -9207,6 +10172,17 @@ saved = "Изберете запазен подпис по-горе, след т text = "След като въведете името си по-горе, щракнете навсякъде върху PDF-а, за да поставите подписа." title = "Как да добавите подпис" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Преместване на подпис" pause = "Пауза на поставянето" @@ -9653,10 +10629,30 @@ title = "Качване на сървъра" updateButton = "Актуализиране на сървъра" uploadButton = "Качване на сървъра" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "щракнете тук" +fallback = "Ако не се отвори автоматично," +opening = "Отваряне на Swagger UI в нов раздел..." tags = "api,документация,swagger,крайни точки,разработка" title = "API документация" @@ -9764,12 +10760,39 @@ selectFilesHint = "Изберете файлове в Активни файло singleFileScope = "Прилага се само за: {{fileName}}" viewerMode = "Преминете към редактора на файлове, за да изберете няколко файла." +[toolFormat] +ANY = "всякакъв файл" +CBR = "архив с комикси (CBR)" +CBZ = "архив с комикси (CBZ)" +CSV = "CSV файл" +EBOOK = "електронна книга" +EMAIL = "имейл" +EXCEL = "електронна таблица" +HTML = "уеб страница" +IMAGE = "изображения" +JAVASCRIPT = "JavaScript файл" +JSON = "JSON файл" +MARKDOWN = "Markdown файл" +NONE = "няма файл" +PCL = "PCL файл за печат" +PDF = "PDF" +PDF_ENCRYPTED = "защитен с парола PDF" +POSTSCRIPT = "PostScript файл" +PPT = "презентация" +TEXT = "текстов файл" +VIDEO = "видео" +WORD = "Word документ" +XML = "XML файл" +XPS = "XPS документ" +ZIP = "ZIP архив" + [toolPanel] alpha = "Алфа" backToAllTools = "Назад към всички инструменти" collapse = "Свиване на панела" expand = "Разгъване на панела" goBack = "Върнете се" +pdfTools = "PDF инструменти" placeholder = "Изберете инструмент, за да започнете" premiumFeature = "Премиум функция:" toolsHeader = "Инструменти" @@ -9806,11 +10829,6 @@ noToolsFound = "Няма намерени инструменти" quickAccess = "БЪРЗ ДОСТЪП" searchPlaceholder = "Търсене на инструменти..." -[toolPicker.categories] -advancedTools = "Разширени инструменти" -recommendedTools = "Препоръчани инструменти" -standardTools = "Стандартни инструменти" - [toolPicker.subcategories] advancedFormatting = "Разширено форматиране" automation = "Автоматизация" @@ -9824,6 +10842,9 @@ removal = "Премахване" signing = "Подписване" verification = "Проверка" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Няма намерени инструменти" noTools = "Няма налични инструменти" @@ -9950,28 +10971,53 @@ title = "Потребители" you = "(вие)" [users.action] +cancelInvite = "Отмяна на поканата" deleteTeam = "Изтриване на екип" disableMfa = "Нулиране на MFA" move = "Преместване в екип" reinstate = "Възстановяване" remove = "Премахване от организацията" +removeTeam = "Премахване от екипа" rename = "Преименуване на екип" resetPw = "Нулиране на парола" suspend = "Спиране" unlock = "Отключване на акаунт" +[users.activity] +daysAgo = "преди {{count}} д." +hoursAgo = "преди {{count}} ч." +justNow = "Току-що" +minutesAgo = "преди {{count}} мин." +monthsAgo_one = "преди {{count}} месец" +monthsAgo_other = "преди {{count}} месеца" +never = "Никога" +weeksAgo_one = "преди {{count}} седмица" +weeksAgo_other = "преди {{count}} седмици" +yearsAgo_one = "преди {{count}} година" +yearsAgo_other = "преди {{count}} години" + [users.cap] addProcessor = "+ Processor" approver = "Одобрява политика" editor = "Редактор" processor = "Процесор" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Да се отмени ли поканата до {{email}}? Те няма да могат да се присъединят с текущата връзка." +cancelInviteTitle = "Отмяна на поканата" deleteTeamBody = "Да се изтрие ли екипът {{name}}? Екипът първо трябва да е празен - преместете членовете му в друг екип и той не трябва все още да притежава конфигурации на интеграции." deleteTeamTitle = "Изтриване на екип" disableMfaBody = "Да се премахне ли MFA регистрацията на {{name}}? Ако се изисква, ще я настроят отново при следващото влизане." disableMfaTitle = "Нулиране на MFA" removeBody = "Да се премахне ли окончателно {{name}} от организацията? Това не може да бъде отменено." +removeTeamBody = "Да се премахне ли {{name}} от екипа? Те запазват акаунта си, но губят достъп до ресурсите на този екип." removeTitle = "Премахване на член" [users.empty] @@ -9982,10 +11028,8 @@ title = "Все още няма членове" addToTeam = "Добавяне към екип" guestCount = "{{count}} гост" guests = "Гости" -guestsDesc = "Външни сътрудници, ограничени до това, което сте споделили. Само редактор." ledBy = "воден от {{owner}}" org = "Организация" -orgDesc = "Собственици с правомощия в цялата организация и одобрение на политики" owners = "{{count}} собственик" team = "Екип {{name}}" teamMeta = "{{count}} души" @@ -10022,6 +11066,18 @@ username = "Потребителско име" usernameError = "Потребителското име трябва да е поне 3 знака" usernamePlaceholder = "jsmith" +[users.invites] +by = "Поканен от {{who}}" +cancel = "Отказ" +expiresInDays_one = "Изтича след {{count}} ден" +expiresInDays_other = "Изтича след {{count}} дни" +expiresToday = "Изтича днес" +title = "Чакащи покани" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Нещо се обърка при свързването със сървърната част или нямате достъп. Опитайте отново." title = "Членовете не можаха да бъдат заредени" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Търсене в прикачените файлове" title = "Прикачени файлове" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Заглавие на отметката" +bookmarkTitleRequired = "Заглавието на отметката е задължително" closeSidebar = "Затвори страничната лента с отметки" collapseAll = "Свий всички отметки" +empty = "No bookmarks in this document" expandAll = "Разгъни всички отметки" +searchPlaceholder = "Търсене в отметките" +title = "Bookmarks" [viewer.comments] addComment = "Добавяне на коментар" @@ -10236,10 +11297,12 @@ locateAnnotation = "Намиране в документа" moreActions = "Още действия" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Страница {{page}}" placingHint = "Щракнете върху страница, за да поставите… (отказ)" removeCommentOnly = "Премахване само на коментара" saveReply = "Запис на отговора" +searchPlaceholder = "Search comments" title = "Коментари" typeComment = "Коментар" typeInsertText = "Вмъкване на текст" @@ -10262,6 +11325,7 @@ unsavedDesc = "Имате незаписани промени" closeSidebar = "Затвори страничната лента със слоеве" hideAll = "Скрий всички слоеве" showAll = "Покажи всички слоеве" +title = "Layers" [viewer.link] delete = "Изтриване на връзка" @@ -10605,30 +11669,6 @@ title = "Текст на водния знак" image = "Изображение" text = "Текст" -[wetSignature.tooltip] -header = "Методи за създаване на подпис" - -[wetSignature.tooltip.draw] -bullet1 = "Персонализирайте цвета и дебелината на писалката" -bullet2 = "Изтрийте и начертайте отново, докато сте доволни" -bullet3 = "Работи на устройства с докосване (таблети, телефони)" -description = "Създайте подпис на ръка, използвайки мишка или сензорен екран. Най-подходящ за лични, автентични подписи." -title = "Нарисувайте подпис" - -[wetSignature.tooltip.type] -bullet1 = "Изберете от множество шрифтове" -bullet2 = "Персонализирайте размера и цвета на текста" -bullet3 = "Идеално за стандартизирани подписи" -description = "Генерирайте подпис от въведен текст. Бърз и последователен, подходящ за бизнес документи." -title = "Въведете подпис" - -[wetSignature.tooltip.upload] -bullet1 = "Поддържа PNG, JPG и други формати на изображения" -bullet2 = "Препоръчват се прозрачни фонове за най-добри резултати" -bullet3 = "Изображението ще бъде преоразмерено, за да пасне в областта за подпис" -description = "Качете предварително създадено изображение на подпис. Идеално, ако имате сканиран подпис или фирмено лого." -title = "Качване на изображение на подпис" - [workbenchBar] activeFiles = "Активни файлове" annotations = "Анотации" @@ -10642,6 +11682,7 @@ exitRedaction = "Изход от режим на заличаване" exportAll = "Експортиране на PDF" exportSelected = "Експортиране на избраните страници" formFill = "Попълване на формуляр" +hideToolbar = "Hide toolbar" multiTool = "Мултиинструмент" panMode = "Режим на преместване" print = "Печат на PDF" @@ -10652,6 +11693,7 @@ redact = "Заличаване" rotateLeft = "Завъртане наляво" rotateRight = "Завъртане надясно" ruler = "Линийка / Измерване" +rulerSettings = "Настройки на мащаба" save = "Запазване" saveAll = "Запазване на всички" saveAs = "Запазване като" @@ -10661,6 +11703,7 @@ selectAll = "Избиране на всички" selectByNumber = "Избор по номера на страници" selectLanguage = "Избор на език" share = "Сподели" +showToolbar = "Show toolbar" toggleAnnotations = "Превключване на видимостта на анотациите" toggleAttachments = "Превключване на прикачените файлове" toggleBookmarks = "Превключване на отметките" diff --git a/frontend/editor/public/locales/bo-CN/translation.toml b/frontend/editor/public/locales/bo-CN/translation.toml index 4e73822bf5..7d0c65d14b 100644 --- a/frontend/editor/public/locales/bo-CN/translation.toml +++ b/frontend/editor/public/locales/bo-CN/translation.toml @@ -1,12 +1,12 @@ alphabet = "ཡི་གེ།" apply = "འཇུག" applyAndContinue = "སྲུང་དང་ཕྱིར་འབུད།" -areYouSure = "ཁྱེད་རང་འགྲོ་འདོད་ཡོད་དམ།" back = "ཕྱིར་ལོག" black = "ནག་པོ" blue = "སྔོན་པོ" cancel = "འདོར་བ" chooseFile = "ཡིག་ཆ་འདེམས།" +clear = "གཙང་སེལ" close = "སྒོ་རྒྱག" comingSoon = "འབྱོར་ལ་ཉེ" confirm = "གཏན་འཁེལ" @@ -45,10 +45,8 @@ filesSelected = "{{count}} ཡིག་ཆ་བདམས་པ།" font = "ཡིག་གཟུགས་པ།" fontSizeTooltip = "ཤོག་ངོས་ཨང་གྲངས་ཀྱི་ཚིག་ཡིག་གི་ཚད་གཞི། ཨང་གྲངས་ཆེ་བ་དེས་ཡིག་ཆ་ཆེ་བ་བཟོས།" fontTypeTooltip = "ཤོག་ངོས་ཨང་གྲངས་ལ་ཁྱིམ་ཚང་། ཁྱེད་རང་གི་ཡིག་ཆའི་རྣམ་པ་ལ་བརྟེན་ནས་འདེམས།" -genericSubmit = "སྤྲོད་པ" green = "ལྗང་ཁུ" help = "རོགས་རམ" -imgPrompt = "པར་རིས་འདེམས།" incorrectPasswordMessage = "ད་ལྟའི་གསང་གྲངས་ནོར་འཁྲུལ་རེད།" info = "གནས་ཚུལ།" invalidUndoData = "ཕྱིར་འཐེན་བྱེད་མི་ཐུབ་པ། ནུས་མེད་བཀོལ་སྤྱོད་ཀྱི་གཞི་གྲངས་མེད་པ།" @@ -61,7 +59,6 @@ loginAgreementTitle = "ཐོ་འགོད་གྲོས་ཆིངས" logOut = "ཕྱིར་ཐོན།" marginTooltip = "ཤོག་ངོས་ཨང་གྲངས་དང་ཤོག་ངོས་ཀྱི་མཐའ་མཚམས་བར་ཐག་རིང་།" moreOptions = "གདམ་ག་མང་བ།" -multiPdfPrompt = "PDF ༢ ཡང་ན་མང་བ་འདེམས།" never = "གཏན་ནས" no = "མིན" noFileSelected = "ཡིག་ཆ་མི་འདེམས་པ། ཡར་བསྐུར་རོགས་གནང་།" @@ -79,7 +76,6 @@ pageOrderPrompt = "སྲོལ་རྒྱུན་གྱི་ཤོག་ང pages = "ཤོག་ངོས།" pageSelectionPrompt = "སྲོལ་རྒྱུན་གྱི་ཤོག་ངོས་འདེམས་སྒྲུག (ཤོག་ངོས་ཨང་གྲངས་ ༡,༥,༦ ཡང་ན་ 2n+1 ལྟ་བུའི་ལས་འགན་གྱི་ཐོ་འགོད་ཐོ་གཞུང་ནང་འཇུག་བྱེད།)" password = "གསང་གྲངས" -pdfPrompt = "PDF(s) འདེམས།" pin = "སྦྱར་ཡིག་(ལག་ཆའི་རྒྱུག་རྗེས་ཤུགས་ལྡན་)" pro = "མཁས་པ།" processingComplete = "ཁྱེད་ཀྱི་ཡིག་ཆ་གྲ་སྒྲིག་ཟིན།" @@ -87,6 +83,7 @@ processingCompleteMultiple = "{{count}} ཡིག་ཆ་གྲ་སྒྲི property = "རྒྱུ་ནོམ" quickPosition = "མགྱོགས་མྱུར་གནས་སྟངས།" red = "དམར་པོ" +remove = "འདོར" reset = "བསྐྱར་འཛུགས།" review = "བསྐྱར་ཞིབ" save = "སྲུང་བ།" @@ -108,6 +105,7 @@ undoSuccess = "ལས་དོན་ལེགས་འགྲུབ་བྱུ unknown = "མི་ཤེས" unpin = "ཨན་པིན་ཡིག་ཆ། (ལག་ཆའི་བཀོལ་སྤྱོད་རྗེས་ཚབ་བྱེད་པ)" unsavedChanges = "ཁྱེད་རང་ལ་ཁྱེད་རང་གི་PDFལ་འགྱུར་བ་མེད་པའི་འགྱུར་བ་ཡོད།" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "གསོག་འཇོག་མེད་པའི་འགྱུར་བ།" unsupported = "རྒྱབ་སྐྱོར་མེད་པ།" username = "མིང་།" @@ -180,6 +178,7 @@ addMoreFiles = "ཡིག་ཆ་མང་བ་ཁ་སྣོན་བྱེ attachments = "མཉམ་སྦྲེལ་འདེམས།" info = "ཁྱེད་རང་གི་PDFལ་མཐུད་པའི་ཡིག་ཆ་འདེམས། ཡིག་ཆ་འདི་དག་PDFཡི་མཐུད་སྣེ་བརྒྱུད་ནས་བཙུགས་ནས་འཛུལ་ཐུབ།" placeholder = "ཡིག་ཆ་འདེམས་དགོས།" +removeFile = "ཡིག་སྣོད་འདོར" selectedFiles = "འདེམས་སྒྲུག་བྱས་པའི་ཡིག་ཆ།" submit = "སྦྲེལ་མཐུད་ཁ་སྣོན།" @@ -353,6 +352,7 @@ text = "ཆོག་མཆན་འདི་དག་གིས་བེད་ས title = "འཕོ་འགྱུར།" [addStamp] +preview = "ཐམ་གའི་སྔོན་ལྟ" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -688,6 +688,165 @@ manualLinks = "ལག་ཐོག་ཕབ་ལེན: འབྲེལ་མཐ noLanguages = "སྒྲིག་བཀོད་ཟིན་པའི་སྣོད་འཛིན་ནང་ tessdata སྐད་ཡིག་མ་རྙེད།" permissionNotice = "tessdata འགྲུལ་ལམ་འུར་བྱེད་མི་ཐུབ། ཕབ་ལེན་ཚུལ་དེ་བརྡ་འཚོལ་ཆས་ནས་ཁ་ཕྱེའོ། .traineddata ཡིག་ཆ་དེ་རང་ལས་ tessdata སྣོད་འཛིན་ནང་ཉར་ཚགས་བྱོས།" +[admin.settings.ai.documents] +description = "ཡིག་ཆའི་ཐོག་དྲི་བར་ལན་འདེབས་པར་སྤྱོད་པའི embedding model དང retrieval སྒྲིག་འགོད་སྒྲིག་སྦྱོར་བྱོས། ཉར་ཚགས་བྱས་རྗེས AI engine ལ་སྤྱོད་འཐུས།" +title = "ཡིག་ཆ་དང RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "engine རང་གི environment credential སྤྱོད་དགོས་ན་སྟོང་པར་བཞག རང་གིས་གཉེར་བའི single-engine deployments ལ་སྤྱོད་འཐུས།" +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "ཉར་ཚགས་བྱས་ཟིན - ད་ལྟའི key ཉར་དགོས་ན་སྟོང་པར་བཞག" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI-compatible / Ollama embeddings endpoint གི Base URL, དཔེར་ན http://ollama:11434/v1. ཡིད་ཆེས་རུང་བའི་ནང་ཁུལ endpoint ལ་ངེས་པར་དུ་སྟོན་དགོས (SSRF-sensitive)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding model གྱི་མིང་། རང་མོས་ཡི་གེ; འོས་སྦྱོར་ནི་གསལ་བརྡ་ཙམ་ཡིན།" +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "ཡིག་ཆའི་ཡི་གེ་དེ vector embeddings ལ་སྒྱུར་བར་སྤྱོད་པའི provider།" +label = "Embedding provider" + +[admin.settings.ai.documents.maxSearches] +description = "རེ་ཞུ་རེར agent གིས་སྤེལ་ཆོག་པའི retrieval འཚོལ་ཞིབ་ཀྱི་གྲངས་མང་ཤོས།" +label = "འཚོལ་ཞིབ་མང་ཤོས" + +[admin.settings.ai.documents.reindexNote] +body = "embedding model བསྒྱུར་བ་དེ་འཕྲལ་དུ་ནུས་པ་འཐོན་ཡང་། སྔོན་མའི model གྱིས indexed བྱས་པའི་ཡིག་ཆ་རྣམས་ལ་འཚོལ་ཞིབ་ཀྱིས་འབྲས་བུ་ཡང་དག་སློག་པར re-index བྱ་དགོས།" +title = "Re-index དགོས" + +[admin.settings.ai.documents.saved] +reindexNote = "གལ་ཏེ embedding model བསྒྱུར་ཡོད་ན། འཚོལ་ཞིབ་ཀྱིས model གསར་པ་སྤྱོད་པར་ད་ཡོད་ཡིག་ཆ་རྣམས re-index བྱོས།" + +[admin.settings.ai.documents.topK] +description = "འཚོལ་ཞིབ་རེར་ལེན་པའི་འབྲེལ་ཆེ་ཤོས chunks ཀྱི་གྲངས།" +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling དེ Python AI engine དང་མཐུད་ནས AI ནུས་པ་གང་དག་སྟོན་དགོས་འདེམས། བསྒྱུར་བ་རྣམས restart སྐབས་ནུས་པ་འཐོན།" +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "AI ཁྱད་ནུས་རེ་རེ་སྒོ་ཕྱེའམ་སྒོ་རྒྱོབ། སྒོ་བརྒྱབ་པའི་ཁྱད་ནུས་རྣམས app ནང་སྦས་ཡོད།" +title = "ནུས་པ" + +[admin.settings.ai.general.enabled] +description = "སྤྱིའི་སྒོ་འབྱེད། སྒོ་བརྒྱབ་སྐབས AI tools, agents, ཡང་ན engine calls གང་ཡང་མི་རུང་།" +label = "AI སྒོ་ཕྱེ" + +[admin.settings.ai.general.features.chat] +description = "PDFs དང་ལས་ཀ་བྱེད་པའི་གླེང་མོལ་རོགས་རམ་པ།" +label = "གླེང་མོལ་རོགས་རམ་པ" + +[admin.settings.ai.general.features.classify] +description = "ཡིག་ཆ་རྣམས་རིགས་སམ་ནང་དོན་ལྟར་རང་འགུལ་གྱིས་རིགས་དབྱེ་བྱེད།" +label = "ཡིག་ཆའི་རིགས་དབྱེ" + +[admin.settings.ai.general.features.createPdf] +description = "AI agent བརྒྱུད་ནས PDF གསར་པ་བཟོ (དཔེར་ན HTML ནས)།" +label = "prompt ནས PDF བཟོ" + +[admin.settings.ai.general.features.documentQuestions] +description = "དྲི་བ་འདྲི་ཞིང་ཡར་སྤྲད་པའི་ཡིག་ཆར་གཞི་བཅོལ་བའི་ལན་ལེན།" +label = "ཡིག་ཆའི་དྲི་བ" + +[admin.settings.ai.general.features.mathAuditor] +description = "ཡིག་ཆ་རྣམས་ལ་རྩིས་རིག་དང་གྲངས་ཀའི་ནོར་འཁྲུལ་ཡོད་མེད་ཞིབ་བཤེར།" +label = "རྩིས་རིག་ཞིབ་བཤེར་པ" + +[admin.settings.ai.general.features.pdfComment] +description = "PDF ལ AI ཡིས་བྲིས་པའི་ཞིབ་བཤེར་མཆན་དང་མཆན་འགྲེལ་སྣོན།" +label = "PDF མཆན་འགྲེལ་ agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "ཡིག་ཆ་བཟོ་བ་ལྟ་བུའི་ལྕི་བའི agent བཀོལ་སྤྱོད་ཀྱི timeout།" +label = "དུས་ཡུན་རིང་བའི timeout (སྐར་ཆ)" + +[admin.settings.ai.general.note] +body = "AI engine ནི་ཞབས་ཞུ་ཁག་ཅིག་ལྟར་འགྲོ། དེའི་མཉམ་སྤྱོད་གསང་བ" +body2 = "container environment variable བརྒྱུད་ནས་སྒྲིག་ཡོད། Provider API keys རྣམས་ཤོག་ངོས་འདི་དག་ཏུ་འཇུག་ཆོག ཡང་ན engine environment variables ལྟར་མཁོ་སྤྲོད་བྱ་ཆོག; འདིར་བཅུག་པའི keys རྣམས་ཉར་ཚགས་བྱས་སྐབས engine ལ་སྐྱེལ་ཡོང་།" +title = "AI engine སྐོར" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "streamed (token-by-token) གླེང་མོལ་ལན་གྱི timeout།" +label = "Stream timeout (སྐར་ཆ)" + +[admin.settings.ai.general.test] +button = "མཐུད་འབྲེལ་ཚོད་ལྟ" +failBody = "AI engine གིས་ལན་མ་སྤྲད། URL དང་། engine container འགྲོ་བཞིན་ཡོད་མེད། AI སྒོ་ཕྱེ་ཡོད་མེད་བརྟག (སྒོ་ཕྱེས་རྗེས restart དགོས)།" +failTitle = "AI engine ལ་སླེབས་མི་ཐུབ" +okBody = "AI engine གིས health check ལ་ལན་སྤྲད།" +okTitle = "AI engine ལ་སླེབས་ཐུབ" + +[admin.settings.ai.general.timeoutSeconds] +description = "engine ལ་སྐྱེལ་བའི་ཚད་ལྡན AI རེ་ཞུའི timeout།" +label = "Request timeout (སྐར་ཆ)" + +[admin.settings.ai.general.url] +description = "Python AI engine གི་ནང་ཁུལ URL, དཔེར་ན http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "AI རེ་ཞུས་ལས་ཀ་ག་ཚོད་བྱེད་ཆོག་པ་དང་མཉམ་དུ་ག་ཚོད་འགྲོ་ཆོག་པའི་སྲུང་ར། ཉར་ཚགས་བྱས་རྗེས AI engine ལ་སྤྱོད་འཐུས།" +title = "ཚད་བཀག་དང་ལས་ཆོད" + +[admin.settings.ai.limits.maxCharacters] +description = "སྲུང་ར: ཕྱུང་བའི་ཡི་གེ་རིང་ཚད་འདི་ལས་བརྒལ་བའི AI རེ་ཞུ་དང་ལེན་མི་བྱེད།" +label = "རེ་ཞུ་རེར་ཡིག་འབྲུ་མང་ཤོས" + +[admin.settings.ai.limits.maxPages] +description = "སྲུང་ར: PDF ཤོག་ངོས་འདི་ཙམ་ལས་མང་བའི AI རེ་ཞུ་དང་ལེན་མི་བྱེད།" +label = "རེ་ཞུ་རེར་ཤོག་ངོས་མང་ཤོས" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "engine ཡོངས་ཀྱི་དུས་མཉམ་དུ་འགྲོ་བཞིན་པའི model calls མང་ཤོས།" +label = "Model max concurrency" + +[admin.settings.ai.models] +description = "AI engine གིས་སྤྱོད་པའི LLM provider དང smart/fast models འདེམས། ཉར་ཚགས་བྱས་རྗེས AI engine ལ་སྤྱོད་འཐུས།" +title = "Models & Providers" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "engine རང་གི environment credential སྤྱོད་དགོས་ན་སྟོང་པར་བཞག རང་གིས་གཉེར་བའི single-engine deployments ལ་སྤྱོད་འཐུས།" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "ཉར་ཚགས་བྱས་ཟིན - ད་ལྟའི key ཉར་དགོས་ན་སྟོང་པར་བཞག" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI-compatible / Ollama endpoint གི Base URL, དཔེར་ན http://ollama:11434/v1." +label = "Provider base URL" +warning = "base URL དེ་ཡིད་ཆེས་རུང་བའི་ནང་ཁུལ endpoint ལ་ངེས་པར་དུ་སྟོན་དགོས། engine གིས་དེར server-side རེ་ཞུ་བྱེད་པས། ཡིད་མི་ཆེས་པའི་ཐང་ནི SSRF-sensitive ཡིན།" + +[admin.settings.ai.models.fastMaxTokens] +description = "fast model གྱི་ཕྱིར་འདོན tokens མང་ཤོས།" +label = "Fast model max tokens" + +[admin.settings.ai.models.fastModel] +description = "ལས་འགན་ཡང་བར་རིན་ཆུང་ཞིང་མགྱོགས་པའི model། རང་མོས་ཡི་གེ; འོས་སྦྱོར་ནི་གསལ་བརྡ་ཙམ་ཡིན།" +label = "Fast model" + +[admin.settings.ai.models.provider] +description = "engine གིས་གང་གི LLM provider དང་འབྲེལ་བ་བྱེད་མིན།" +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "smart model གྱི་ཕྱིར་འདོན tokens མང་ཤོས།" +label = "Smart model max tokens" + +[admin.settings.ai.models.smartModel] +description = "རྙོག་འཛིང་ཆེ་བའི་རིགས་འདེད་ལ་ནུས་པ་མཐོ་བའི model། རང་མོས་ཡི་གེ; འོས་སྦྱོར་ནི་གསལ་བརྡ་ཙམ་ཡིན།" +label = "Smart model" + +[admin.settings.ai.saved] +body = "བསྒྱུར་བ་རྣམས AI engine ལ་རང་འགུལ་གྱིས་སྐྱེལ་ཡོང་།" +bodyNoPush = "སྒྲིག་འགོད་ཉར་ཚགས་བྱས་ཟིན། AI engine གིས་རང་གི་སྒྲིག་འགོད་ལེན་སྐབས་རྗེས་མ་ནས་ནུས་པ་འཐོན་ཡོང་།" +title = "AI སྒྲིག་འགོད་ཉར་ཚགས་བྱས་ཟིན" + [admin.settings.badge] clickToUpgrade = "མནན་ནས་པར་འཆར་ཞིབ་ལྟ" @@ -935,6 +1094,25 @@ label = "འགོ་འཛུགས་སྐབས་བསྐྱར་གས description = "ཉིན་གྲངས་ཀྱི་གྲངས་འབོར། ལག་འཁྱེར་དེ་ནུས་ལྡན་ཡིན།" label = "ལག་འཁྱེར་ནུས་ལྡན་(ཉིན་མོ།)།" +[admin.settings.folderAccess] +description = "folder sources དང folder outputs ཀྱིས་ཀློག་པ་དང་འབྲི་བ་ཆོག་པའི་དཀར་ཆག འདི་ནི་བདེ་འཇགས་མཚམས་ཤིག་ཡིན: automations ནམ་ཡང་ཐོ་གཞུང་འདིའི་ཕྱི་ཡི server path ལ་སྟོན་མི་ཐུབ།" +securityNote = "folder sources དང outputs རྣམས་ཡོངས་སུ་སྒོ་རྒྱག་དགོས་ན་འདི་སྟོང་པར་བཞག Stirling རང་གི་སྒྲིག་འགོད་དཀར་ཆག་ནམ་ཡང་འགྲོ་མི་ཆོག་པ་ཡིན།" +title = "Folder Access" + +[admin.settings.folderAccess.implied] +description = "Stirling གིས་དོ་དམ་བྱས་པའི་དཀར་ཆག་འདི་དག་ནམ་ཡང་ཆོག་མཆན་ཡོད་ཅིང་འདིར་བསྒྱུར་མི་ཐུབ།" +serverStorage = "Server ཡིག་སྣོད་གསོག་ཉར" +title = "ནམ་ཡང་ཆོག" +watchedFolder = "Pipeline གྱིས་ལྟ་སྲུང་བྱས་པའི་ཡིག་ཁུག" + +[admin.settings.folderAccess.roots] +add = "སྣོན" +empty = "ཆོག་མཆན་ཡོད་པའི་ཡིག་ཁུག་མེད། folder sources དང outputs ད་ལྟ་སྒོ་བརྒྱབ་ཡོད།" +hint = "absolute paths འཇུག དཔེར་ན /data/inbox." +label = "ཆོག་མཆན་ཡོད་པའི folder roots" +placeholder = "/data/inbox" +remove = "folder root འདོར" + [admin.settings.general] description = "ཚོང་རྟགས་བཟོ་སྟངས་དང་སྔོན་སྒྲིག་སྤྱོད་ཚུལ་ཚུད་པའི་མ་ལག་ཆ་ཚང་གི་བཀོལ་སྤྱོད་སྒྲིག་སྟངས་སྒྲིག་བཀོད་བྱེད།" system = "མ་ལག" @@ -1628,6 +1806,9 @@ title = "ཁྱེད་རང་གིས་སི་ཊར་ལིང་PDF tags = "annotate,highlight,draw,markup,comment,notes,review,redline,feedback,markup tools,sticky notes,shapes,arrows,text box,freehand" [annotation] +alignCenter = "དཀྱིལ་སྒྲིག" +alignLeft = "གཡོན་སྒྲིག" +alignRight = "གཡས་སྒྲིག" annotationStyle = "མཆན་བཟོ་སྒྲིག" backgroundColor = "རྒྱབ་ལྗོངས་ཁ་དོག།" borderOff = "ས་མཚམས། ཁ་བརྒྱབ་པ།" @@ -1698,6 +1879,9 @@ width = "ཞེང་ཚད" [app] description = "ཕི་རི་ཨེ་ཌོབ་ཨེ་ཀྲོ་བྷཊ་ཐབས་གཞན་ཞིག (10M+ ཕབ་ལེན།)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b ལ་སྒྱུར་བ།" convertToPdfA3bDescription = "ཡིག་ཚགས་ཀྱི་PDFཞིག་བཙུགས་ནས་བཙུགས་པའི་མཐུད་ཁ་བཟོས།" @@ -1836,6 +2020,15 @@ export = "ཕྱི་ཚོང" accessDenied = "འཛུལ་སྤྱོད་ཁས་མ་བླངས་པ།" insufficientPermissions = "ཁྱེད་ལ་བྱ་སྤྱོད་འདི་སྒྲུབ་པའི་ཆོག་མཆན་མེད།" +[auth.callback] +completing = "ཐོབ་ཐང་ར་སྤྲོད་ལེགས་སྒྲུབ་བྱེད་བཞིན" +invalidToken = "OAuth ནང་འཛུལ་ཕམ - token མི་འགྲིག" +missingToken = "OAuth ནང་འཛུལ་ཕམ - token འབྱོར་མ་བྱུང་།" +oauthFailed = "OAuth ནང་འཛུལ་ཕམ། ཡང་བསྐྱར་ཚོད་ལྟ་གནང་།" +pleaseWait = "ཁྱེད་རང་ནང་འཛུལ་ལེགས་སྒྲུབ་བྱེད་རིང་སྒུག་རོགས།" +signedOut = "ཁྱེད་ཕྱིར་འཐོན་བྱས་ཟིན། ཡང་བསྐྱར་ནང་འཛུལ་གནང་།" +windowMayClose = "ལེགས་སྒྲུབ་བྱས་རྗེས་སྒེའུ་ཁུང་འདི་ཁ་རྒྱག་ཆོག" + [auth.displayName] guest = "མགྲོན་པོ" user = "སྤྱོད་མཁན" @@ -1981,6 +2174,73 @@ secureWorkflowDesc = "ཇ་ཝ་སི་ཁི་རིཔ་ཊི་དང description = "ལག་ཆ་འདིས་རང་འགུལ་གྱིས་PDFཡིག་ཆ་དེ་ཚོའི་ནང་དོན་ལ་གཞི་བཅོལ་ནས་མིང་བསྒྱུར་བྱེད། དེས་ཡིག་ཆ་དེ་དབྱེ་ཞིབ་བྱས་ནས་ཡིག་ཆ་ལས་འཚམ་ཤོས་ཀྱི་མིང་འཚོལ་ཐུབ།" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "རང་འགུལ་སྐོར" +tags = "རང་འགུལ་སྐོར,ཁ་ཕྱོགས་བཅོས,མགོ་མཇུག་ལོག་པ,ཟུར་ཕྱོགས,དྲང་བཟོ,ཁ་ཕྱོགས,ཁ་ཕྱོགས་ངོས་འཛིན,སྐོར་བ་བཅོས,རང་འགུལ་ཁ་ཕྱོགས,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "ཡིད་ཆེས་ཚད་ཐིག" + +[autoRotate.detectionMode] +auto = "རང་འགུལ" +osd = "OCR ཁོ་ན" +text = "ཡི་གེ་ཁོ་ན" +title = "ངོས་འཛིན་ཐབས" + +[autoRotate.error] +failed = "PDF རང་འགུལ་སྐོར་སྐབས་ནོར་འཁྲུལ་བྱུང་།" + +[autoRotate.inferUndetected] +title = "ཡིད་ཆེས་དམའ་བའི་ཤོག་ངོས་མཐུན་པར་སྐོར" + +[autoRotate.report] +applied = "{{degrees}}° གཡས་སྐོར" +confidenceValue = "ཡིད་ཆེས {{value}}" +noChange = "བསྒྱུར་བ་མེད" +pageLabel = "ཤོག་ངོས {{number}}" +summary = "ཤོག་ངོས {{total}} ལས {{rotated}} སྐོར་ཟིན། {{unchanged}} སྔར་བཞིན་བཞག" +title = "ངོས་འཛིན་སྙན་ཐོ" + +[autoRotate.report.method] +inferred = "མཐུན་སྒྲིག" +none = "མཆོངས" +osd = "OCR" +text = "ཡི་གེ" + +[autoRotate.report.note] +belowThreshold = "ཚད་ཐིག་ལས་དམའ" +blankPage = "ཤོག་ངོས་སྟོང་པ་ཡིན" +inferredFromDocument = "ཤོག་ངོས་གཞན་དང་འདྲ" +noDominantDirection = "ཡི་གེའི་ཁ་ཕྱོགས་འདྲེས་མ" +osdFailed = "ཀློག་ཐུབ་པའི་ཡི་གེ་མ་རྙེད" +osdNoVerdict = "གང་ཕྱོགས་སྟེང་ཡིན་མིན་ཤེས་མ་ཐུབ" +tesseractUnavailable = "OCR སྒྲིག་འཇུག་མ་བྱས" +tooFewGlyphs = "ཡི་གེ་ཉུང་དྲགས" + +[autoRotate.results] +title = "རང་འགུལ་སྐོར་འབྲས་བུ" + +[autoRotate.settings] +title = "སྒྲིག་འགོད" + +[autoRotate.tooltip.confidenceThreshold] +text = "ཤོག་ངོས་སྐོར་བའི་སྔོན་ལ་ཞིབ་བཤེར་གྱི་ངེས་ཤེས་ག་ཚོད་དགོས་མིན། ཤོག་ངོས་མང་ཙམ་སྐོར་དགོས་ན་དམའ་རུ་གཏོང་། ཤོག་ངོས་མང་ཙམ་མ་རེག་པར་བཞག་དགོས་ན་མཐོ་རུ་གཏོང་།" +title = "ཡིད་ཆེས་ཚད་ཐིག" + +[autoRotate.tooltip.description] +text = "ཤོག་ངོས་རེ་རེ་གང་ཕྱོགས་སྟེང་ཡིན་དགོས་མིན་རྩིས་བརྒྱབ་ནས་དྲང་པོར་སྐོར། སྔར་ནས་དྲང་པོ་ཡིན་པའི་ཤོག་ངོས་རྣམས་མ་རེག་པར་བཞག" + +[autoRotate.tooltip.detectionMode] +text = "རང་འགུལ་གྱིས་ཐོག་མར་ཤོག་ངོས་ཀྱི་ཡི་གེ་ཀློག དེས་མ་འདང་ན་ཤོག་ངོས་དེ་དཔར་རིས་ལྟར་བལྟ། ཡི་གེ་ཁོ་ནས་དཔར་རིས་ཞིབ་བཤེར་མཆོངས་པས། ནམ་ཡང scanned མ་བྱས་པའི་ཡིག་ཆར་མགྱོགས་ཤོས་ཡིན། OCR ཁོ་ནས་ཤོག་ངོས་རེ་རེ་དཔར་རིས་ལྟར་འཛིན།" +title = "ངོས་འཛིན་ཐབས" + +[autoRotate.tooltip.header] +title = "རང་འགུལ་སྐོར" + +[autoRotate.tooltip.inferUndetected] +text = "སྟོང་པའམ་སྟོང་པར་ཉེ་བའི་ཤོག་ངོས་རྣམས་རང་ཉིད་ཁོ་ནས་ཞིབ་བཤེར་བྱ་མི་ཐུབ། ཀློག་ཐུབ་པའི་ཤོག་ངོས་ཚང་མ་སྐོར་ཕྱོགས་གཅིག་དགོས་སྐབས། ཤོག་ངོས་འདི་དག་ཀྱང་དེ་དང་མཐུན་པར་སྐོར།" +title = "ཡིད་ཆེས་དམའ་བའི་ཤོག་ངོས་མཐུན་པར་སྐོར" + [autoSizeSplitPDF] tags = "pdf,split, ཡིག་ཆ། སྒྲིག་འཛུགས།" @@ -2183,33 +2443,6 @@ keywords = "འབྲེལ་ཡོད་ཚིག: ཁྱད་མཚར་བ numbers = "ཨང་གྲངས།/ཁྱབ་ཁོངས།: ༥, ༡༠-༢༠" progressions = "ཡར་རྒྱས། 3n, 4n+1" -[certificateChoice.tooltip] -header = "ལག་འཁྱེར་རིགས་གྲས" - -[certificateChoice.tooltip.organization] -bullet1 = "རིམ་སྤྱོད་སྐྱོང་བ་ཚོས་དོ་དམ" -bullet2 = "དབང་ཚད་ཡོད་པའི་མི་ཚུལ་དང་མཉམ་སྤྱོད" -bullet3 = "མི་སྒེར་མིན་པ། ཚོགས་སྡེའི་ངོ་སྤྲོད" -bullet4 = "ལེགས་པ: གཞུང་མིང་ཡིག་ཆ། ཚོགས་མི་མཛུབ་འགན" -description = "ཁྱེད་ཀྱི་ཚོགས་སྡེས་བྱིན་པའི་མཉམ་སྤྱོད་ལག་འཁྱེར། ཚོགས་སྡེ་ཡོངས་ཀྱི་མཛུབ་འགན་དབང་ཆེད་སྤྱོད།" -title = "སྒྲིག་སྡེའི་ལག་འཁྱེར" - -[certificateChoice.tooltip.personal] -bullet1 = "ཐེངས་དང་པོ་སྤྱོད་སྐབས་རང་འགུལ་བཟོས" -bullet2 = "ཁྱེད་ཀྱི་ཁ་བྱང་ལ་འབྲེལ" -bullet3 = "མི་སྣ་གཞན་དང་མཉམ་སྤྱོད་མི་ཐུབ" -bullet4 = "ལེགས་པ: མི་སྒེར་ཡིག་ཆ། སོ་སོའི་ཁྲིམས་འཁྲིལ" -description = "ཁྱེད་ཀྱི་ཁ་བྱང་ལ་དམིགས་བཀོད་བྱས་པའི་རང་འགུལ་ལག་འཁྱེར། སོ་སོའི་མཛུབ་འགན་ལ་འཚམ།" -title = "མི་དབང་ལག་འཁྱེར" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX ཡིག་ཆ་དང་གསང་ཨང་དགོས" -bullet2 = "ཕྱིས་ཀྱི Certificate Authorities ཡིས་ཁྱབས་སྤྲོད་ཆོག" -bullet3 = "ཁྲིམས་འཁྲིན་ཡིག་ཆའི་མཐོ་རིམ་བློ་གྲོས" -bullet4 = "ལེགས་པ: ཁྲིམས་ལྡན་དམ་ཚིག ཕྱིས་ཀྱི་བདེན་བསྟན" -description = "ཁྱེད་རང་གི PKCS#12 ལག་འཁྱེར་ཡིག་ཆ་སྤྱོད། ལག་འཁྱེར་གཏོགས་སྒྲིག་ལ་ཚད་བཟོ་ཆོག." -title = "P12 སྲོལ་གསར་ཡར་འཇུག" - [certSign] awaitingSignatures = "མཛུབ་འགན་བསྒུག་བཞིན" chooseCertificate = "ལག་འཁྱེར་ཡིག་ཆ་འདེམས།" @@ -2531,38 +2764,11 @@ title = "མཐེ་བོང་གི་སྒྲིག་གཞི།" [certSign.source] device = "ཐབས་འཕྲུལ་འདི" +noOtherSources = "ལག་ཁྱེར་འབྱུང་ཁུངས་གཞན་མེད།" server = "སར་བར" stepTitle = "དཔང་ཡིག་ཁུངས" upload = "ཡར་འཇུག" -[certSign.tooltip.header] -title = "མཚན་རྟགས་འཛིན་སྐྱོང་སྐོར།" - -[certSign.tooltip.overview] -bullet1 = "ད་ཡོད་ཀྱི་མཚན་རྟགས་དང་དེའི་ནུས་ལྡན་ལ་ཞིབ་བཤེར་བྱེད་པ།" -bullet2 = "མིང་རྟགས་བཀོད་མཁན་དང་ལག་འཁྱེར་སྐོར་གྱི་ཞིབ་ཕྲའི་གནས་ཚུལ་ལ་གཟིགས།" -bullet3 = "ཁྱེད་རང་གི་ཡིག་ཆ་བདེ་འཇགས་ཆེད་དུ་དྲ་རྒྱའི་མིང་རྟགས་གསར་པ་ཁ་སྣོན་བྱས།" -bullet4 = "འགྲིམ་འགྲུལ་སྟབས་བདེ་བའི་ཐོག་ནས་རྒྱབ་སྐྱོར་བྱེད་པའི་ཡིག་ཆ་སྣ་ཚོགས་ཡོད།" -text = "ལག་ཆ་འདིས་ཁྱེད་ཀྱི་PDFs ལ་དྲ་རྒྱའི་ཐོག་ནས་མིང་རྟགས་བཀོད་ཡོད་མེད་བརྟག་དཔྱད་བྱེད་ཐུབ་པ་དང་དྲ་རྒྱའི་མིང་རྟགས་གསར་པ་ཁ་སྣོན་བྱེད་ཐུབ། དྲ་རྒྱའི་མིང་རྟགས་ཀྱིས་ཡིག་ཆ་ཞིག་བཟོས་པའམ་ཡང་ན་ཆོག་མཆན་སྤྲད་ནས་མིང་རྟགས་བཀོད་རྗེས་བསྒྱུར་ཡོད་མེད་སྟོན་ཡོད།" -title = "ལག་ཆ་འདིས་ཅི་ཞིག་བྱེད་ཐུབ་བམ།" - -[certSign.tooltip.signing] -bullet1 = "PEMདང་། PKCS12། JKS། སར་བར་གྱི་ལག་འཁྱེར་རྣམ་གཞག་ལ་རྒྱབ་སྐྱོར་བྱེད།" -bullet2 = "PDF ཐོག་མིང་རྟགས་སྟོན་པའམ་ཡང་ན་སྦས་སྐུང་བྱེད་པའི་གདམ་ག།" -bullet3 = "རྒྱུ་མཚན་དང་ས་གནས་དང་མིང་རྟགས་བཀོད་པའི་མིང་ཁ་སྣོན་བྱེད།" -bullet4 = "མཐོང་ཐུབ་པའི་མཚན་རྟགས་བཀོད་དགོས་མིན་ཤོག་ལྷེ་གང་འདེམས་དགོས།" -bullet5 = "'Sign with Stirling-PDF' གདམ་ཁ་སྟབས་བདེ་ལ་སར་བར་གྱི་ལག་འཁྱེར་བེད་སྤྱོད་བྱེད་དགོས།" -text = "PDF ལ་མིང་རྟགས་བཀོད་པར་ཁྱེད་ལ་དྲ་རྒྱའི་ལག་ཁྱེར་དགོས། (དཔེར་ན། PEM, PKCS12, ཡང་ན་ JKS ལྟ་བུ) ཁྱེད་ཀྱིས་མིང་རྟགས་དེ་ཡིག་ཆའི་སྟེང་མཐོང་ཐུབ་པ་བྱ་ཆོག་པའམ་ཡང་ན་བདེ་འཇགས་ཆེད་དུ་མཐོང་མི་ཐུབ་པའི་གནས་སུ་འཇོག་ཐུབ།" -title = "མཚན་རྟགས་ཁ་སྣོན་བྱེད་པ།" - -[certSign.tooltip.validation] -bullet1 = "མཚན་རྟགས་ནུས་པའམ་ནུས་མེད་ཡིན་ན་མངོན་ཚུལ་ནི།" -bullet2 = "མིང་རྟགས་བཀོད་མཁན་གྱི་ཆ་འཕྲིན་དང་མིང་རྟགས་བཀོད་པའི་ཚེས་གྲངས།" -bullet3 = "ཡིག་ཆ་དེ་མིང་རྟགས་བཀོད་རྗེས་བསྒྱུར་བཅོས་བྱས་ཡོད་མེད་ཞིབ་བཤེར།" -bullet4 = "བདེན་དཔང་ཆེད་རང་སྒྲིག་ལག་འཁྱེར་བེད་སྤྱོད་གཏོང་ཐུབ།" -text = "ཁྱེད་ཀྱིས་མིང་རྟགས་བརྟག་དཔྱད་བྱེད་སྐབས་ལག་ཆ་དེས་ཁྱེད་ལ་དེ་དག་ནུས་ལྡན་ཡིན་མིན་དང་། ཡིག་ཆ་དེ་ལ་མིང་རྟགས་བཀོད་པ། མིང་རྟགས་བཀོད་པ།" -title = "མཚན་རྟགས་ལ་ཞིབ་བཤེར་བྱེད་པ།" - [changeCreds] changeUsername = "ཁྱེད་རང་གི་སྤྱོད་མཁན་གྱི་མིང་གསར་བརྗེ། ཁྱེད་རང་གསར་བརྗེ་བྱས་རྗེས་ཕྱིར་ཐོན་འགྲོ་གི་རེད།" credsUpdated = "རྩིས་ཁྲ་གསར་བརྗེ་བྱས།" @@ -2730,12 +2936,16 @@ text = "ཆོག་མཆན་འདི་དག་འགྱུར་བ་མ [chat.actions] copy = "འཕྲིན་དོན་འདྲ་བཤུས" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling agent འདེམས་ཚན" +assistant = "AI Assistant" clearChat = "གླེང་མོལ་བཤོལ" [chat.input] @@ -2798,26 +3008,35 @@ tooltip = "བཀོལ་སྤྱོད་འདིས་ཁྱེད་ཀྱ tooltip = "བརྙན་ཡོལ་ནས་ཁ་དོག་འདེམས" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "ཁ་དོག་འདེམས།" [common] +actions = "Actions" back = "རྒྱབ་ངོས" cancel = "འདོར་བ" close = "གཏན་པ" +codeSample = "Code sample" collapse = "རྡིབ་པ" +confirm = "གཏན་འཁེལ" continue = "མུ་མཐུད" copied = "འདྲ་བཤུས་བྱས།" copy = "འདྲ་བཤུས།" done = "བགྱིས་པ" +enterValue = "Enter value" error = "ནོར་འཁྲུལ" expand = "རྒྱ་སྐྱེད་གཏོང" loading = "ཁུར་པོ།" next = "རྗེས་མ" +open = "Open" preview = "སྔོན་ལྟ།" previous = "སྔོན་ཆད་ཀྱི" refresh = "གསར་སྐྲུན་བྱེད་པ།" +remaining = "ལྷག་མ" retry = "ངལ་གསོ་ཡང་བསྐྱར་བྱེད་པ།" save = "སྐྱོབ་པ" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "བདམས་ཐོན་བྱུང་བར་གསལ་བ།" @@ -2980,6 +3199,7 @@ title = "བསྡམས་སྒྲིལ་ཐབས་ལམ།" [compress.settings] desiredSize = "དགོས་མཁོའི་ཡིག་ཆའི་ཆེ་ཆུང" desiredSizePlaceholder = "ཆེ་ཆུང་བཙུགས" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "ཁྱེད་ཀྱི་ཡིག་ཆའི་ཆེ་ཆུང་ཉུང་དུ་གཏོང་བའི་ཐབས་ཤེས་སྟབས་བདེ་ཞིག་ཡིན། དམིགས་འབེན་གྱི་ཚད་གཞི་ནང་འཇུག་བྱེད་པར་ཡིག་ཆའི་ཚད་གཞི་འདེམས་ནས་ང་ཚོ་ཁྱེད་ཀྱི་ཆེད་དུ་སྤུས་ཚད་སྙོམ་སྒྲིག་བྱེད་དགོས། ལག་ཐོག་ནས་བསྡམས་སྒྲིལ་གྱི་ཤུགས་ཚད་སྒྲིག་པའི་སྤུས་ཚད་འདེམས།" @@ -3182,6 +3402,24 @@ pdfOptions = "PDF གདམ་ག།" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "ཐོན་ཁུངས་རྣམ་གཞག་ཐོག་མར་འདེམས།" settings = "སྒྲིག་སྟངས།" single = "ཁེར་ཧྲེང" @@ -3256,24 +3494,6 @@ description = "ཀུ་ཀི་འདི་དག་དྲ་ཚིགས་ posthog = "རྗེས་ཀྱི་ཧོག་དབྱེ་ཞིབ།" scarf = "སི་ཀར་ཕི་པིག་སེལ།" -[credits.modal] -advancedMonitoring = "མཐོ་རིམ་ལྟ་སྲུང་" -allInOneWorkspace = "ཡོངས་ཀྱིས་སྒྲུབ་པའི PDF ལས་གནས (viewer, ལག་ཆ & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "དམིགས་བསལ་རོགས་རམ་ & SLAs" -fasterThroughput = "མགྱོགས་ཚད 10x མགྱོགས་པ" -fullyPrivateFiles = "གང་གིས་ཀྱང་སྒེར་དབང་ཡིག་ཆ" -largeFileProcessing = "ཡིག་ཆ་ཆེན་པོ་ལས་སྦྱོར" -orgWideAccess = "སྤྱི་ཚོགས་ཡོངས་ཁྱབ་དོན་སྤྱོད་ཚད" -premiumAiModels = "མཐོ་རིམ AI དཔེ་མཚོན" -prioritySupport = "གཙོ་རིམ་རོགས་རམ" -privateDocCloud = "སྒེར་དབང་ཡིག་ཆ Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "བདེ་འཇགས API དོན་སྤྱོད" -standardThroughput = "ཚད་ལྡན་མགྱོགས་ཚད" -unlimitedApiAccess = "API དོན་སྤྱོད་ཚད་མེད" -unlimitedSeats = "ཚོགས་མི་ཚད་མེད" - [crop] autoCrop = "རང་འགུལ་གྱིས་ས་ཞིང་དཀར་པོའི་བར་སྟོང་།" noFileSelected = "འདེབས་འཛུགས་བྱེད་འགོ་ཚུགས་པར་PDFཡིག་ཆ་འདེམས།" @@ -3338,6 +3558,7 @@ message = "སྔོན་སྒྲིག་PDFའཛིན་སྐྱོང་ title = "ནོར་འཁྲུལ" [defaultApp.prompt] +dontRemind = "ཡང་བསྐྱར་དྲན་སྐུལ་མ་བྱེད" message = "ཁྱེད་རང་གི་སྔོན་སྒྲིག་མཉེན་ཆས་PDFཡིག་ཆ་ཁ་ཕྱེ་བའི་ཆེད་དུ་PDF stirling PDF བཟོས།" [defaultApp.settingsOpened] @@ -3377,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "འཚོལ..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "ཞུན་དག" @@ -3595,7 +3820,9 @@ makeCopy = "པར་འདེབས་བྱེད" mobileShort = "ཁ་པར" mobileUpload = "ཁ་པར་ཡར་འཇུག" mobileUploadNotAvailable = "ཁ་པར་ཡར་འཇུག་མ་འགོ་སློངས།" +moreOptions = "འདེམས་ཚན་དེ་བས་མང་བ" myFiles = "ངའི་ཡིག་ཆ།" +nextFile = "ཡིག་ཆ་རྗེས་མ" noFiles = "ཡིག་ཆ་མེད།" noFilesFound = "ཁྱེད་རང་གི་འཚོལ་ཞིབ་ལ་མཐུན་པའི་ཡིག་ཆ་གང་ཡང་མཐོང་མ་བྱུང་།" noRecentFiles = "ཉེ་ཆར་ཡིག་ཆ་གང་ཡང་མཐོང་མ་བྱུང་།" @@ -3605,6 +3832,7 @@ openInFileEditor = "ཡིག་ཆའི་རྩོམ་སྒྲིག་པ openInPageEditor = "ཤོག་ངོས་རྩོམ་སྒྲིག་པར་ཁ་ཕྱེ།" owner = "བདག་པོ" ownerUnknown = "མ་ཤེས" +previousFile = "ཡིག་ཆ་སྔོན་མ" recent = "ཉེ་ཆར" removeBoth = "གཉིས་ཀ་ནས་བསུབ" removeFilePrompt = "ཡིག་ཆ་འདི་ཐབས་འཕྲུལ་འདི་དང་ཁྱེད་ཀྱི་སར་བར་གཉིས་ཀ་ལ་ཉར་ཡོད། གནས་ས་གང་ནས་བསུབ་དགོས?" @@ -3633,11 +3861,13 @@ shareSelected = "འདེམས་པ་མཉམ་སྤྱོད" sharing = "མཉམ་སྤྱོད" showAll = "ཚང་མ་སྟོན་དང་།" showHistory = "ལོ་རྒྱུས།" +sortBy = "Sort files" sortByDate = "ཚེས་གྲངས་ཀྱིས་དབྱེ་བ།" sortByName = "མིང་གིས་དབྱེ་བ།" sortBySize = "ཚད་གཞི་ལྟར་དབྱེ་བ།" storage = "ཤོང་ཚད" storageState = "གསོག་འཇོག" +storageUsed = "Storage used" synced = "མཉམ་བསྒྲིག" title = "Upload PDF ཡིག་ཆ།" toolChain = "ལག་ཆས་བཀོལ་སྤྱོད་བྱས།" @@ -3680,32 +3910,57 @@ workbench = "ལས་ལག་གནས" [fileSidebar] addFiles = "ཡིག་ཆ་ཁ་སྣོན" +addingFiles = "ཡིག་ཆ་སྣོན་བཞིན་པ…" collapse = "ཟུར་པང་བསྡམས" +customizeGroups = "ཚོགས་པ་རང་སྒྲིག" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "འགོ་རྩོམ་བྱེད་པར་ཡིག་ཆ་ཁ་ཕྱེ" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "ཟུར་པང་རྒྱས" -files = "ཡིག་ཆ" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive སྒྲིག་འགོད་མ་བྱས" leaveMyFiles = "ངའི་ཡིག་ཆ་ནས་ཕྱིར་འདོན" +library = "PDF དཔེ་མཛོད" myFiles = "ངའི་ཡིག་ཆ" noFiles = "ད་ཚུན་ཡིག་ཆ་མེད" openFileManager = "ཡིག་ཆ་དོ་དམ་ཁ་ཕྱེ" openFromComputer = "གློག་ཀླད་ནས་ཁ་ཕྱེ" openSettings = "སྒྲིག་སྟངས་ཁ་ཕྱེ" -search = "འཚོལ" -searchPlaceholder = "ཡིག་ཆ་འཚོལ..." +other = "གཞན" +recent = "ཉེ་ཆར" +viewAll = "{{count}} ཡིག་ཆ་ཚང་མ་ལྟ" [fileSidebar.fileItem] closeViewer = "མཐོང་ཆས་ཁ་རྒྱག" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "མཐོང་ཆས་ནང་ཁ་ཕྱེ" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" +[fileSidebar.groupsModal] +done = "ཚར" +reset = "ཚང་མ་སྟོན" +subtitle = "ཡིག་ཆའི་ཟུར་སྟོན་ནང་གི་རིགས་དབྱེ་སྟོན་པའམ་སྦས་པ།" +title = "ཟུར་སྟོན་རིགས་དབྱེ" + +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "ལས་གནས་ལ་ཁ་སྣོན" addToWorkspaceCount = "{{count}} ལས་གནས་ལ་ཁ་སྣོན" @@ -3715,7 +3970,7 @@ backToFolder = "{{folder}} ལ་ཕྱིར་ལོག" backToMyFiles = "ངའི་ཡིག་ཆར་ཕྱིར་ལོག" breadcrumbs = "སྣོད་ཁུངས་ལམ" cancel = "འདོར" -clearSearch = "འཚོལ་བཤོལ" +classification = "རིགས་དབྱེ" clearSelection = "འདེམས་བཤོལ" closeDetails = "ཞིབ་ཕྲ་ཁ་རྒྱག" create = "གསར་བཟོ" @@ -3747,6 +4002,7 @@ downloadAll = "ཚང་མ་ཕབ་ལེན" downloadVersion = "པར་གཞི་འདི་ཕབ་ལེན" dropOverlay = "ཡིག་ཆ་བཀོག་ནས་ཡར་འཇུག" dropOverlaySub = "ཡིག་ཆ་ཚང་མ Local ནས་འགོ་བཙུགས། 'Move to' ཡང་ན 'Save to cloud' བེད་སྤྱོད་ནས་སྣོད་འགེངས་ནང་སྒྲིག་འགོད་བྱེད།" +duplicate = "Duplicate" file = "ཡིག་ཆ" fileInfo = "File info" fileMenu = "ཡིག་ཆའི་བྱ་བ" @@ -3774,13 +4030,12 @@ refresh = "སར་བར་ནས་གསར་སྒྱུར" remove = "སུབ" removeVersion = "པར་གཞི་སུབ" rename = "མིང་བསྐྱར་བཏགས" +renamed = "མིང་བསྐྱར་བཏགས" renameFolder = "སྣོད་འགེངས་མིང་བསྐྱར་བཏགས" resizeFolderTree = "སྣོད་འགེངས་རྩ་འབྲེལ་ཆེ་ཆུང་བཟོ (arrow keys, Shift ཕལ་ཆེར་གོམ་པ; ཐེབས་ཚར་གཉིས་གནོན་ནས་རང་འགུལ་སྒྲིག)" save = "སྲུངས" saveToServer = "སར་བར་ཉར" saveToServerDisabledHint = "སར་བར་ལ་ཉར་བ་འདི་སར་བར་འདིར་སྤྱོད་མེད། དོ་དམ་པ་ལ་དབང་སྤྲོད་རོགས།" -search = "འཚོལ" -searchPlaceholder = "སྣོད་འགེངས་འདི་དང་ཡན་ལག་སྣོད་འགེངས་ནང་འཚོལ" selectAll = "སྤྱིར་འདེམས" selectAllHint = "མཐེབ་གནོན་ནས་ཚང་མ་འདེམས། ཕན་བསླབ: Ctrl (ཡང་ན Cmd) བཟུང་ནས་ཡིག་ཆ་རྡོག་རྡོག་སྣོན ཡང་ན Shift བཟུང་ནས་ཁྱབ་ཁོངས་འདེམས།" selectedCount = "{{count}} འདེམས་ཟིན" @@ -3840,6 +4095,10 @@ title = "cloud ཡིག་ཆ་ད་དུང་མེད" hint = "ཡར་འཇུག་མ་བྱས་པའི་ཡིག་ཆ་ཚང་མ་འདིར་སྲུངས། ཡིག་ཆ་བཀོག་ནས་ཁ་སྣོན།" title = "Local ཙམ་གྱི་ཡིག་ཆ་མེད" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "ཁྱེད་ཀྱིས་ཁ་ཕྱེ་བ་དང་ཞུན་དག་བྱས་པ་ཚང་མ་འདིར་མངོན།" title = "ད་དུང་བཟོ་བཅོས་མི་བྱས" @@ -3859,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "སྣོད་འགེངས་སུབ་མ་ཐུབ།" deleteFolderFailedDetail = "སྣོད་འགེངས་སུབ་མ་ཐུབ: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "སྣོད་འགེངས་ཚོར་སྣང་བསྒྱུར་མ་ཐུབ།" folderAppearanceFailedDetail = "སྣོད་འགེངས་ཚོར་སྣང་བསྒྱུར་མ་ཐུབ: {{message}}" moveFilesFailed = "ཡིག་ཆ་སྤོ་མ་ཐུབ།" @@ -3874,11 +4135,9 @@ uploadFilesFailedDetail = "ཡིག་ཆ་ཡར་འཇུག་མ་ཐ added = "ཁ་སྣོན" count = "ཡིག་ཆ" folder = "སྣོད་འགེངས" +labels = "ཤོ་བྱང" modified = "བཟོ་བཅོས" -name = "མིང" size = "ཆེ་ཆུང" -toolHistory = "ལག་ཆས་ལོ་རྒྱུས" -toolHistoryAtVersion = "ལག་ཆས་བརྒྱུད་ལས་བསྡུས" totalSize = "སྤྱི་ཆེ་ཆུང" type = "རིགས" versionHistory = "པར་གཞིའི་འགྲོ་ལམ" @@ -3911,6 +4170,11 @@ localHint = "མི་སྣང་ཆས་འདི་ནང་ཙམ་ཉར" shared = "མཉམ་སྤྱོད" sharedHint = "འབྲེལ་མཐུད་བརྒྱུད་ནས་མཉམ་སྤྱོད" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "དུས་རྙིང་བ་སོང་མ" modifiedDesc = "ཉེ་ཆར་སྔོན" @@ -3966,6 +4230,7 @@ noFilesInStorageOpen = "གསོག་འཇོག་ནང་ཡིག་ཆ open = "འབྱེད" openFile = "ཁ་ཕྱེ་བའི་ཡིག་ཆ།" openFiles = "ཁ་ཕྱེ་བའི་ཡིག་ཆ།" +selectFile = "Select {{name}}" selectFromStorage = "གསོག་འཇོག་ཁང་ནས་འདེམས།" upload = "Upན།" uploadFile = "Upload ཡིག་ཆ།" @@ -4038,10 +4303,22 @@ discord = "མི་མཐུན་པ།" issues = "གྷི་ཊི་ཧབ།" [formFill] +allSaved = "ཚང་མ་ཉར་ཚགས་བྱས་ཟིན" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV ཕྱིར་འདོན་མ་ཐུབ" +extractXlsxError = "XLSX ཕྱིར་འདོན་མ་ཐུབ" +filled = "filled" flattenAfterFilling = "བཀངས་རྗེས་རྡོག་བཟོ" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "དགོས" +requiredFieldsError = "ངེས་པར་དགོས་པའི་ས་སྒོ་ཚང་མ་སྐོང་རོགས" rescanFields = "ས་ཁོངས་བསྐྱར་ཞིབ" rescanFormFields = "ཐོ་འགོད་ས་ཁོངས་བསྐྱར་ཞིབ" save = "སྲུངས" +saveShortcut = "Ctrl+S སྤྱད་ནས་ཉར་ཚགས" +unsavedChanges = "ཉར་ཚགས་མ་བྱས་པའི་འགྱུར་བ" [formFill.sidebar] close = "ཟུར་ཚན་ཁ་རྒྱག" @@ -4275,6 +4552,11 @@ desc = "རང་འགུལ་གྱིས་མགོ་ཡིག་ཤེས tags = "རང་འགུལ་ཤེས་རྟོགས། མགོ་བརྗོད་ལ་གཞི་བཅོལ་བ། གོ་སྒྲིག་བྱས་པ། རེ་ལེ་བེལ།" title = "རང་འགུལ་གྱིས་མིང་བསྒྱུར་བའི་PDFཡིག་ཆ།" +[home.autoRotate] +desc = "ཤོག་ངོས་རེ་རེའི་ཁ་ཕྱོགས་ངོས་འཛིན་བྱས་ནས་རང་འགུལ་གྱིས་དྲང་པོར་བསྐོར།" +tags = "རང་འགུལ་བསྐོར་བ,ཁ་ཕྱོགས་བཅོས,མགོ་མཇུག་ལོག་པ,ཟུར་ཕྱོགས,དྲང་བཟོ,ཁ་ཕྱོགས,ཁ་ཕྱོགས་ངོས་འཛིན,བསྐོར་བ་བཅོས,རང་འགུལ་ཁ་ཕྱོགས,སྐེན་བྱས་པ,OSD" +title = "རང་འགུལ་བསྐོར་བ" + [home.autoSizeSplitPDF] tags = "རང་འགུལ་གྱིས་ཁ་ཕྲལ་དང་ཆེ་ཆུང་།" title = "ཆེ་ཆུང་/གྲངས་ཚད་ཀྱིས་རང་འགུལ་ཆ་བགོས་བྱས།" @@ -4574,15 +4856,12 @@ hint = "PDF སྟེང་དུ་བཞག་ནིའི་ PNG, JPG, SVG label = "བརྙན་པ་ཡར་འཇུག" placeholder = "བརྙན་པའི་ཡིག་ཆ་འདེམས" -[imageToPdf] -tags = "སྒྱུར་བཅོས། ཨམ་གྷི་,jpg, པར་རིས། པར་རིས།" - [imageToPDF] header = "PDF ནས་པར་རིས།" title = "PDF ནས་པར་རིས།" -[infoBanner] -dismiss = "འདོར" +[imageToPdf] +tags = "སྒྱུར་བཅོས། ཨམ་གྷི་,jpg, པར་རིས། པར་རིས།" [invite] acceptError = "རྩིས་ཁྲ་བཟོ་མ་ཐུབ་པ།" @@ -4613,8 +4892,6 @@ welcomeTitle = "ཁྱེད་རང་གདན་འདྲེན་ཞུས [landing] addFiles = "ཡིག་ཆ་ཁ་སྣོན་བྱེད།" -heroSubtitle = "འགོ་སློང་བྱེད་དུ PDF ཞིག་འདྲུད་ནས་བཙུགས་ ཡང་ན་ ཡོད་པའི་ PDF ཞུགས་སྣོན་བྱས།" -heroTitle = "Stirling PDF" mobileUpload = "ཁ་པར་ནས་ཡར་འཇུག" openFromComputer = "གློག་ཀླད་ནས་ཁ་ཕྱེ་བ།" uploadFromComputer = "གློག་ཀླད་ནས་ཡར་བླངས།" @@ -4657,7 +4934,6 @@ enterUsername = "སྤྱོད་མཁན་གྱི་མིང་ནང་ failedToSignIn = "{{provider}} བེད་སྤྱད་ནས་ནང་འཇུག་མ་ཐུབ་པ: {{message}}" forgotPassword = "ཁྱེད་རང་གི་གསང་གྲངས་བརྗེད་སོང་ངམ།" loggingIn = "ཤིང་བཟོ་བ།..." -logIn = "དྲན་ཐོ།" login = "ནང་འཇུག" magicLinkSent = "མེ་ཇིག་དྲ་སྦྲེལ་དེ་{{email}} ལ་བཏང་ཡོད། གློག་འཕྲིན་ལ་ལྟས་ཏེ་སྦྲེལ་མཐུད་སྣུན་ནས་ནང་འཇུག་གནང་རོགས།" mfaCode = "ངོས་འཛིན་ཨང་རྟགས" @@ -4681,26 +4957,10 @@ signInWith = "ནང་དུ་ཞུགས་པ།" title = "མཚན་རྟགས་བཀོད་པ།" unexpectedError = "རྗེས་འགྲོ་མ་ངེས་པའི་ནོར་འཁྲུལ: {{message}}" updatePassword = "གསང་ཨང་གསར་སྒྱུར" -useEmailInstead = "གློག་འཕྲིན་བརྒྱུད་ནས་ནང་འཛུལ་བྱེད་པ།" useMagicLink = "དེའི་ཚབ་ཏུ་མུན་ནག་གི་སྦྲེལ་མཐུད་བེད་སྤྱོད་བྱེད།" username = "མིང་།" youAreLoggedIn = "ཁྱེད་རང་ནང་འཛུལ་འདུག" -[login.slides.edit] -alt = "PDF རྩོམ་སྒྲིག་བྱས།" -subtitle = "ཁྱེད་རང་ལ་PDFs བསྐྱར་བཟོ་དང་། རྟགས་མཚན། ཀློག་པ། བཀོལ་སྤྱོད་བྱེད་པར་རོགས་བྱེད་པའི་ལག་ཆ་བཅུ་ཕྲག་ལྷག་ཙམ་ཡོད་ན།" -title = "ཁྱེད་ལ་དགོས་པའི་ཆ་འཕྲིན་དེ་བསྟན་/བརྟན་པོར་ཡོང་བར་PDFsརྩོམ་སྒྲིག་བྱེད།" - -[login.slides.overview] -alt = "PDF སྤྱི་བསྡོམས།" -subtitle = "ཡིག་ཆ་སྒྱུར་བ་དང་། རྟགས་བཀོད། གསར་བརྗེ། དོ་དམ་བཅས་བྱེད་པའི་PDFཡི་སྒེར་གྱི་གནས་ཚུལ་གྱི་སྤྲིན་པའི་ཁང་མིག་གཅིག་དང་། དེ་བཞིན་ལག་ཆ་གཞན་༥༠ལྷག་ཡོད།" -title = "ཁྱེད་རང་གི་PDFདགོས་མཁོ་ཚང་མར་ཁྱེད་རང་གི་ཚོང་ཁང་གཅིག་གིས་ཉོ་ཆ་རྒྱག་དགོས།" - -[login.slides.secure] -alt = "བདེ་འཇགས་ཀྱི་PDFs" -subtitle = "གསང་གྲངས་ཁ་སྣོན་དང་། ནང་དོན་གསར་བརྗེ། ལག་འཁྱེར་ལ་སྟབས་བདེ་པོའི་ངང་འཛིན་སྐྱོང་བྱེད།" -title = "ཁྱེད་རང་གི་PDFནང་དུ་ཚོར་ཤུགས་ཆེ་བའི་ཆ་འཕྲིན་སྲུང་སྐྱོབ་བྱེད་དགོས།" - [margin] large = "ཤིན་ཏུ་ཆེན་པོ" medium = "འབྲིང" @@ -4784,6 +5044,34 @@ uploadSuccess = "ཡར་འཇུག་ལེགས་འགྲུབ!" uploadSuccessMessage = "ཁྱེད་ཀྱི་པར་རིས་སྤོ་ལེན་བྱས་ཟིན།" validating = "Session བདེན་བཤད་བཤེར་བཞིན..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "བཤར་འབེབས་བསྣུན་ནས་པར་རིས་ཡར་འཇུག། པར་རིས་ PDF ལ་རང་འགུལ་བརྗེ་སྒྱུར།" descriptionNoConvert = "ཁ་པར་ཐབས་འཕྲུལ་ནས་པར་རིས་བསྣུན་ནས་ཡར་འཇུག།" @@ -4802,8 +5090,13 @@ title = "ཁ་པར་ནས་ཡར་འཇུག" tags = "ལག་ཆ་སྣ་མང་། སྣ་མང་བཀོལ་སྤྱོད། UI། སྣུན་དྲུག མདུན་གྱི་མཐའ་མཚམས། འབྲེལ་གཏུག་བྱེད་མཁན།" title = "PDF སྣ་མང་ལག་ཆ།" -[navbar] -search = "བཤེར་འཚོལ" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "བདེན་དཔང་དེ་གྲུབ་འབྲས་ཐོབ་མེད། ཁྱོད་ཀྱིས་སྒེའུ་ཁུང་འདི་ཁ་རྒྱག་ནས་བསྐྱར་དུ་ཚོད་ལྟ་བྱེད་ཐུབ།" @@ -4930,7 +5223,9 @@ title = "ཐོན་འབྲས།" [onboarding] activeFiles = "Active Files མཐོང་སྣང་གིས་ཁྱེད་ཀྱིས་ལག་ཆའི་ནང་དུ་བླུགས་པའི་PDF ཚང་མ་སྟོན་པ་དང་།" allTools = "འདི་ནི་ལག་ཆ གྱི་སྒྲོམ་གཞི་ཡིན། དེར་ཁྱེད་ཀྱིས་ཡོད་པའི་PDFལག་ཆ་ཚང་མ་ནས་བལྟ་ཐུབ་པ་དང་འདེམས་ཐུབ།" +close = "ཁ་རྒྱག" cropSettings = "ད་ལྟ་ང་ཚོས་ང་ཚོར་སྟོན་ཐོག་དགོས་པའི་ཡིག་ཆ་དེ་འདེམས་སྒྲུག་བྱས་པས་ང་ཚོས་PDFདེ་བསྡུ་རུབ་བྱེད་འདོད་པའི་ས་ཁུལ་དེ་འདེམས་རྒྱུའི་ཆེད་དུ་Cropལག་ཆ་སྒྲིག་བཀོད་བྱེད་ཐུབ།" +dialogLabel = "Onboarding" fileCheckbox = "ཡིག་ཆ་གཅིག་ལ་སྣུན་ན་ལས་སྣོན་ཆེད་དུ་དེ་འདེམས་སྒྲུག་བྱེད། ཁྱེད་ཀྱིས་སྡེ་ཚན་བཀོལ་སྤྱོད་ཀྱི་ཆེད་དུ་ཡིག་ཆ་སྣ་ཚོགས་འདེམས་ཐུབ།" fileReplacement = "བསྒྱུར་བཅོས་བྱས་པའི་ཡིག་ཆ་དེས་ལས་ཀའི་སྟེགས་བུའི་ནང་དུ་ཡིག་ཆ་ངོ་མ་དེ་རང་འགུལ་གྱིས་ཚབ་བྱེད་པ་དང་།" filesButton = "མགྱོགས་མྱུར་འཛུལ་སྤྱོད་ཕྲ་རིང་སྟེང་གི་ ཡིག་ཆ་ མཐེབ་གཞོང་གིས་ཁྱེད་ལ་ལག་ཆ་བཀོལ་སྤྱོད་བྱེད་པར་PDFs ཡར་བསྐུར་ཐུབ།" @@ -4941,6 +5236,7 @@ pinButton = "གལ་ཏེ་ཁྱེད་རང་གིས་ཁྱེད results = "ལག་ཆ་དེ་བཀོལ་སྤྱོད་བྱེད་ཚར་བའི་རྗེས་སུ་Review རིམ་པ་དེས་པར་རིས་འདིའི་ནང་གྲུབ་འབྲས་ཀྱི་སྔོན་ལྟ་སྟོན་ནས་བཀོལ་སྤྱོད་དེ་ཕྱིར་འཐེན་བྱེད་པའམ་ཡང་ན་ཡིག་ཆ་ཕབ་ལེན་བྱེད་ཐུབ།" runButton = "ལག་ཆ་དེ་སྒྲིག་བཀོད་བྱས་ཚར་རྗེས། མཐེབ་འདིས་འདེམས་པའི་PDFཚང་མའི་སྟེང་ལ་ལག་ཆ་དེ་བཀོལ་སྤྱོད་བྱེད་ཐུབ།" selectCropTool = "ལག་ཆ་གཅིག་བེད་སྤྱོད་གཏོང་སྟངས་སྟོན་པའི་ཆེད་དུ་Copp ལག་ཆ་འདེམས་དགོས།" +stepOf = "{{total}} ལས་གོམ་པ་ {{current}}" toolInterface = "འདི་ནི་Cropལག་ཆའི་ངོས་འདྲ་བ་ཡིན། ཁྱེད་ཀྱིས་མཐོང་བ་ལྟར། དེར་མང་པོ་མེད། རྒྱུ་མཚན་ནི་ང་ཚོས་ད་དུང་PDFཡིག་ཆ་གང་ཡང་ཁ་སྣོན་བྱས་མེད།" workbench = "འདི་ནི་Workbench ཞེས་པའི་ས་ཁུལ་གཙོ་བོ་ཡིན།" wrapUp = "ཁྱེད་ཚོ་ཚང་མ་སྒྲིག་ཟིན་འདུག ཁྱེད་ཀྱིས་མཉེན་ཆས་དེའི་ཁྱབ་ཁོངས་གཙོ་བོ་དང་དེ་དག་བེད་སྤྱོད་བྱེད་སྟངས་ཤེས་ཡོད། ཁྱེད་རང་ལྟ་སྐོར་འདི་ཡང་བསྐྱར་མཐོང་འདོད་ཡོད་པའི་སྐབས་ལ་རོགས་རམ་ མཐེབ་གཞོང་ལ་སྣུན་རོགས།" @@ -4953,14 +5249,49 @@ showMeAround = "ང་ལ་སྟོན་དང་།" skipForNow = "ད་ལྟ་མཆོང་།" skipTheTour = "སྐོར་སྐྱོད་ལ་བསྐྱོད་པ།" +[onboarding.checklist] +dismiss = "སེལ" +title = "Stirling PDF སྒྲིག" + +[onboarding.checklist.downloadDesktop] +description = "Stirling ཁྱེད་ཀྱི་འཕྲུལ་ཆས་ཐོག་ཐད་ཀར་སྤེལ" +title = "Desktop ལ་སྤྱོད་པའི Stirling ཕབ་ལེན" + +[onboarding.checklist.inviteTeam] +description = "ཁྱེད་ཀྱི་ཚོགས་ཆུང་དང་མཉམ་ལས་བྱེད" +title = "ཚོགས་ཆུང་ཁོངས་མི་གདན་འདྲེན" + +[onboarding.checklist.shareAnalytics] +description = "Stirling ལེགས་བཅོས་ལ་རོགས་བྱེད" +title = "མིང་མེད་སྤྱོད་སྟངས་གཞི་གྲངས་མཉམ་སྤྱོད" + +[onboarding.checklist.takeTour] +description = "མགྱོགས་པོའི་སྟོན་སྐོར་ནང Stirling ཇི་ལྟར་ལས་ཀ་བྱེད་པ་ལྟ" +title = "སྟོན་སྐོར་ལ་ཞུགས" + [onboarding.desktopInstall] body = "སི་ཊར་ལིང་གིས་ཀམ་པུ་ཊར་གྱི་མཉེན་ཆས་ལྟར་ལས་ཀ་ཡག་ཤོས་བྱེད་ཀྱི་ཡོད། ཁྱེད་ཀྱིས་དེ་དྲ་རྒྱ་ལས་ཕྱིར་སྤྱོད་ཐུབ་པ་དང་། ཡིག་ཆ་མགྱོགས་པོ་འཛུལ་ཐུབ་པ། དེ་བཞིན་ཁྱེད་རང་གི་གློག་ཀླད་ཐོག་ས་གནས་སུ་རྩོམ་སྒྲིག་བྱེད་ཐུབ།" +selectOs = "བཀོལ་སྤྱོད་མ་ལག་འདེམས" title = "ཕབ་ལེན" titleWithOs = "{{osLabel}} ལ་ཕབ་ལེན།" [onboarding.mfa] authenticationCode = "ར་སྤྲོད་ཨང་རྟགས" +description = "བདེན་སྦྱོར་ཉེར་སྤྱོད་ཅིག་སྦྲེལ་ནས་ཁྱེད་ཀྱི་རྩིས་ཐོ་སྲུང་སྐྱོབ་བྱེད། QR code སྐེན་བྱེད་པའམ་སྒྲིག་སྟངས་ལྡེ་མིག་ནང་འཇུག་བྱས་ནས། མཇུག་སྒྲིལ་ཆེད་ཨང་གྲངས་ 6 ཅན་གྱི་ཨང་རྟགས་གཏན་འཁེལ་བྱེད།" +enable = "MFA སྤྱོད་འགོ་འཛུགས" +enableError = "རྐྱེན་གཉིས་བདེན་སྦྱོར་སྤྱོད་འགོ་འཛུགས་མ་ཐུབ། ཨང་རྟགས་བརྟག་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད།" +enterCode = "ཁྱེད་ཀྱི་ཉེར་སྤྱོད་ནས་ཨང་གྲངས་ 6 ཅན་གྱི་ཨང་རྟགས་ནང་འཇུག་བྱེད།" +enterCodeError = "མུ་མཐུད་ཆེད་བདེན་སྦྱོར་ཨང་རྟགས་ནང་འཇུག་བྱེད།" +logout = "ཕྱིར་འཐོན" +openAuthenticator = "Google Authenticator, Authy, ཡང་ན 1Password ཁ་ཕྱེ།" qrCodeLoading = "ཁྱེད་ཀྱི QR ཨང་རྟགས་བསྐྲུན་བཞིན་པ…" +regenerateQrCode = "QR code བསྐྱར་སྐྱེད" +scanQrCode = "QR code སྐེན་བྱེད་པའམ་གཤམ་གྱི་སྒྲིག་སྟངས་ལྡེ་མིག་ནང་འཇུག་བྱེད།" +setupError = "རྐྱེན་གཉིས་སྒྲིག་སྟངས་འགོ་འཛུགས་མ་ཐུབ། ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།" +setupKey = "སྒྲིག་སྟངས་ལྡེ་མིག (ལག་པས་ནང་འཇུག)" +stepByStep = "གོམ་པ་རེ་རེ" +success = "MFA སྤྱོད་འགོ་བཙུགས་ཟིན། ད་ཁྱེད་མུ་མཐུད་ཐུབ།" +title = "Multi-Factor Authentication སྒྲིག་སྟངས" [onboarding.planOverview] adminBodyLoginDisabled = "ནང་འཇུག་ཐབས་ལམ་ལྕོགས་ཅན་བཟོ་རྗེས་སྤྱོད་མཁན་དོ་དམ་, སྒྲིག་བཀོད་དང་སར་བར་གྱི་བདེ་འཇགས་གནས་བབ་ལ་ལྟ་ཞིབ་བྱེད་ཐུབ། ཁྱེད་ཀྱི་སར་བར་ནང་གི་མི་དང་པོ {{freeTierLimit}} ལ་སི་ཊར་ལིང་དེ་རིན་མེད་དུ་བཀོལ་ཆོག" @@ -4969,6 +5300,11 @@ adminTitle = "འཛིན་སྐྱོང་སྤྱི་བསྡོམས userBody = "རུ་མི་གདན་འདྲེན་ཞུ་བ་དང་། ལས་འགན་སྤྲོད་པ། ཁྱེད་ཀྱི་ཡིག་ཆ་བདེ་འཇགས་ལྡན་པའི་ལས་ཀའི་ས་ཆ་གཅིག་ཏུ་སྒྲིག་འཛུགས་བྱེད་དགོས། ཁྱེད་རང་གཅིག་པོ་བེད་སྤྱོད་ལས་བརྒལ་བར་འཕེལ་རྒྱས་འགྲོ་རྒྱུར་གྲ་སྒྲིག་ཡོད་པའི་སྐབས་ལ་ནང་འཇུག་ཐབས་ལམ་ལ་ནུས་ཤུགས་སྤྲོད་དགོས།" userTitle = "འཆར་གཞིའི་སྤྱི་བསྡོམས།" +[onboarding.processorIntro] +body = "Stirling ད་ལྟ སྲིད་ཇུས སྤེལ་བཞིན་ཡོད — ཡིག་ཆ་རེ་རེ་འབྱོར་སྐབས་དེ་དག་རིགས་དབྱེ། བདེ་འཇགས་སྲུང་སྐྱོབ། དང་ལས་སྣོན་བྱེད་པའི་རང་འགུལ་སྒྲིག་སྲོལ་ཡིན། དེ་དག་སྒྲིག་ནས ལས་སྣོན་ཆས ནང་སྤེལ་བ་ལྟ་ཞིབ་བྱེད།" +cta = "ལས་སྣོན་ཆས་ལ་ལྟ" +title = "Stirling Processor ལ་ལྟ" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5326,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5365,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "སྔོན་སྤྲོད་ལོ" + +[payg.prepaid.banner] +exhausted = "ཁྱེད་ཀྱི་སྔོན་སྤྲོད་ནུས་ཚད་སྤྱད་ཚར་འདུག ཚད་འཇལ་རྩིས་འབུལ་བསྐྱར་སྤེལ་བྱས་ཟིན། ཆག་ཡང་མུ་མཐུད་ཉར་ཆེད་ཁ་སྣོན་བྱེད།" +expiring_one = "ཁྱེད་ཀྱི་སྔོན་སྤྲོད་ལོ་ཉིན་ {{count}} ནང་ཚར་རྒྱུ་རེད། མ་སྤྱད་པའི་ནུས་ཚད་དུས་ཡོལ་འགྲོ་རྒྱུ་རེད — ལོ་གསར་པ་འགོ་འཛུགས་ཆེད་ཁ་སྣོན་བྱེད།" +expiring_other = "ཁྱེད་ཀྱི་སྔོན་སྤྲོད་ལོ་ཉིན་ {{count}} ནང་ཚར་རྒྱུ་རེད། མ་སྤྱད་པའི་ནུས་ཚད་དུས་ཡོལ་འགྲོ་རྒྱུ་རེད — ལོ་གསར་པ་འགོ་འཛུགས་ཆེད་ཁ་སྣོན་བྱེད།" +low = "ཁྱེད་ཀྱི་སྔོན་སྤྲོད་ནུས་ཚད་ {{pct}}% ཙམ་ལས་མི་འདུག ལས་སྣོན་མ་ཆད་པར་མུ་མཐུད་ཆེད་དུས་ནམ་ཡང་ཁ་སྣོན་བྱེད།" + +[payg.prepaid.card] +subtitle = "ཚད་འཇལ་རྩིས་འབུལ་གྱི་སྔོན་དང་། ཁྱེད་ཀྱི་འགྲོ་གྲོན་ཚད་ཀྱི་ཕྱི་རོལ་དུ་སྤྱོད།" +title = "སྔོན་སྤྲོད་ནུས་ཚད" + +[payg.prepaid.meter] +capSuffix = "{{total}} སྔོན་སྤྲོད་སྤྱོད་ཐོབ་ལས" +expires = "{{date}} ལ་དུས་ཡོལ" + +[payg.prepaid.state] +exhausted = "སྤྱད་ཚར" +healthy = "མང་པོ་ལྷག" +low = "ཉུང་དུ་འགྲོ་བཞིན" + [payg.role] leader = "Team owner" member = "Member" @@ -5383,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5566,7 +5926,9 @@ viewLabel = "PDF རྩོམ་སྒྲིག་པ།" [pdfTextEditor.actions] applyChanges = "འགྱུར་བ་འཇུག་པ།" +clearText = "ཡི་གེ་གཙང་སེལ" downloadCopy = "འདྲ་བཤུས་ཕབ་ལེན།" +moreOptions = "འདེམས་ཚན་དེ་བས་མང་བ" reset = "བསྐྱར་སྒྲིག་བསྐྱར་སྒྲིག་བྱེད་པ།" [pdfTextEditor.badges] @@ -5709,6 +6071,11 @@ header = "PDF ནས་PDF/A" tags = "ཡིག་ཚགས། ཡུན་རིང་ཚད་ལྡན་དང་། བསྒྱུར་བཅོས། དངོས་ཟོག་སྲུང་སྐྱོབ། སྲུང་སྐྱོབ།" title = "PDF ནས་PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "པར་འདེབས,ཚད་ལྡན,བརྗེ་སྤོ,བཟོ་ལས,པར་སྔོན་འགྲིལ,མཛོད་སྣོད" title = "PDF ལས་ PDF/X ལ་བརྗེ་བ" @@ -5784,6 +6151,7 @@ small = "བཀོལ་གྲངས 500" xsmall = "བཀོལ་གྲངས 100" [plan.availablePlans] +currency = "Billing currency" subtitle = "ཁྱེད་རང་གི་དགོས་མཁོ་དང་འཚམ་པའི་འཆར་གཞི་འདེམས།" title = "འཆར་གཞི།" @@ -5899,49 +6267,12 @@ keyDescription = "ཁྱེད་རང་གི་གློག་འཕྲི success = "ལག་འཁྱེར་སྒུལ་ཤུགས་ལྡན་པ།" successMessage = "ཁྱེད་ཀྱི་ཆོག་མཆན་དེ་ལེགས་འགྲུབ་བྱུང་ཡོད། ཁྱོད་ཀྱིས་ད་ལྟ་སྒེའུ་ཁུང་འདི་ཁ་རྒྱག་ཐུབ།" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "བཀོལ་སྤྱོད་ཚར།" -enforcing = "བཀོལ་སྤྱོད་བཞིན..." -failed = "བཀོལ་སྤྱོད་ཕམ་པ།" outputsUnavailable = "སྲིད་བྱང་མཇུག་འབྲས་དེ་རྣམས་ད་དུང་ཕབ་ལེན་མི་ཐུབ།" partialOutputsUnavailable = "སྲིད་བྱང་མཇུག་འབྲས་ཁ་ཤས་ད་དུང་ཕབ་ལེན་མི་ཐུབ།" -retrying = "ཁོམ་མེད་པས་ ཡང་བསྐྱར་ཚོད་ལྟ་བཞིན..." runNotFound = "བཀོལ་སྤྱོད་འགུལ་སྐྱོད་དེ་ད་དུང་མ་རྙེད།" -step = "རིམ་པ {{current}}/{{total}}" timedOut = "མཇུག་མ་བསྡུ་བའི་སྔོན་དུ་ བཀོལ་སྤྱོད་དུས་ཚོད་ཡོལ་བ།" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "སྒོ་བསྡམས" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} བཀོལ་སྤྱོད་བཞིན" applyingProgress = "{{names}} བཀོལ་སྤྱོད་བཞིན ({{done}}/{{total}})" @@ -5951,20 +6282,16 @@ failureBody = "{{total}} ཡིག་ཆ་ནས་ {{failures}} དག་བ failureTitle = "བཀོལ་སྤྱོད་ཆ་ཚང་མེད་པར་ཕྱིར་འདྲེན་བཟོ།" printPolicyAppliedBody = "འདི་ PDF ཞིག་སྲིད་བྱང་གཅིག་ལ་འཚམ་པར་གསར་བཅོས་བྱས་ཟིན། བཟོ་བཅོས་ལ་བསྐྱར་ཞིབ་བྱས་ནས་ ཡང་བསྐྱར་དཔར་བྱེད།" printPolicyAppliedTitle = "དཔར་བྱེད་སྔོན་ལ་ སྲིད་བྱང་བཀོལ་སྤྱོད" -queued = "+{{count}} གྲལ་ཐོ་ནང་བཞག" successTitle = "{{names}} བཀོལ་སྤྱོད་ཚར།" summaryMore = "{{first}} དང་ {{second}} དེ་ལས་མང་བ་ {{more}}" summaryTwo = "{{first}} དང་ {{second}}" -[policies.enforcement.triggerVerb] -convert = "བརྗེ་སྒྱུར་སྔོན་ལ་ བཀོལ་སྤྱོད" -default = "བཀོལ་སྤྱོད" -export = "ཕྱིར་འདྲེན་བཟོ་སྔོན་ལ་ བཀོལ་སྤྱོད" -input = "ནང་འདྲེན་སྐབས་ བཀོལ་སྤྱོད" -print = "དཔར་བྱེད་སྔོན་ལ་ བཀོལ་སྤྱོད" - -[policies.fields] -selectedCount = "{{count}} selected" +[policies.labels] +categoryCount = "རིགས་དབྱེ" +hideCategory = "རིགས་དབྱེ་སྦས" +labelCount = "ཤོ་བྱང" +sharedNote = "ཤོ་བྱང་འདི་དག་ནང་སྒྲིག་ཡིན་ཞིང་ཁྱེད་ཀྱི་ཚོགས་ཆུང་ཡོངས་དང་མཉམ་སྤྱོད་བྱས་ཡོད།" +showCategory = "རིགས་དབྱེ་སྟོན" [policies.pii] account = "རྩིས་ཐོའི་ཨང་ (མཚོན་བྱེད་ཡོད་པ)" @@ -5977,76 +6304,2667 @@ placeholder = "Select PII types" routing = "US routing numbers (ABA)" ssn = "Social Security ཨང་" -[policies.sidebar] -activeCount = "{{count}} active" -infoAriaLabel = "What is a policy?" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" +[policy] +badgeEnforcing = "{{name}} ལག་བསྟར་བཞིན..." +badgeRan = "{{name}} སྲིད་ཇུས་ཡིག་ཆ་འདིའི་ཐོག་སྤེལ་ཟིན" +badgeRunning = "{{name}} སྤེལ་བཞིན..." +blockingAction = "སྲིད་ཇུས་ལག་བསྟར་སྐབས་ {{action}} འགོག་བྱས། སྒུག་རོགས..." +dismiss = "ཁེབས་རིས་སེལ" +enforcingTitle = "སྲིད་ཇུས་ལག་བསྟར་བཞིན..." +viewAnyway = "ཡིག་ཆ་ལྟ (སྲིད་ཇུས་ད་དུང་ལག་བསྟར་བཞིན)" -[policies.status] +[policyOption] +autoRedactPhi = "PHI རང་འགུལ་ནག་བསུབ" +blockExport = "ཕྱིར་འདོན་འགོག" +ccpa = "CCPA" +contracts = "གན་རྒྱ" +documents = "ཡིག་ཆ" +fedramp = "FedRAMP" +financialReports = "དངུལ་རྩིས་སྙན་ཐོ" +flagForReview = "བསྐྱར་ཞིབ་ཆེད་རྟགས་འགོད" +gdpr = "GDPR" +hipaa = "HIPAA" +hold = "ཉར་འཇོག" +hrRecords = "HR ཟིན་ཐོ" +indefinite = "དུས་ཚད་མེད་པ" +insurance = "འགན་བཅོལ" +invoices = "འཛིན་ཤོག" +iso27001 = "ISO 27001" +legalFilings = "ཁྲིམས་ལུགས་འབུལ་ཡིག" +medicalPhi = "སྨན་བཅོས / PHI" +never = "ནམ་ཡང་མིན" +ninetyDays = "ཉིན་ 90" +oneYear = "ལོ་ 1" +p60 = "60%" +p70 = "70%" +p80 = "80%" +p90 = "90%" +p95 = "95%" +pciDss = "PCI DSS" +quarantineDocument = "ཡིག་ཆ་ཟུར་འཇོག" +routeToBucket = "bucket ལ་ལམ་སྒྲིག" +s3Bucket = "S3 bucket" +sevenYears = "ལོ་ 7" +sharePoint = "SharePoint" +soc2 = "SOC 2" +taxDocuments = "ཁྲལ་ཡིག་ཆ" +thirtyDays = "ཉིན་ 30" +threeYears = "ལོ་ 3" +webhook = "Webhook" + +[portal] +comingSoon = "མགྱོགས་པོར་ཡོང་རྒྱུ" + +[portal.accountLink.card] +billingNote = "ལྟ་སྐྱོང་མེད་པའི་ལས་སྣོན་གྱི་རིན་འབབ་ཁྱེད་ཀྱི་ཚོགས་པའི་དངུལ་ཁུག་ནས་རྩིས་འབུལ་བྱེད།" +eyebrow = "རྩིས་ཐོ་སྦྲེལ་མཐུད" +linkButton = "ཁྱེད་ཀྱི Stirling རྩིས་ཐོ་སྦྲེལ" +linked = "སྦྲེལ་ཟིན" +linkedAs = "{{name}} ཡི་མིང་ཐོག་སྦྲེལ་ཟིན།" +linkedGeneric = "དཔེ་མཚོན་འདི་སྦྲེལ་ཟིན།" +notLinked = "སྦྲེལ་མེད" +title = "ཚོགས་པ་འདི་དེའི Stirling རྩིས་ཐོ་དང་སྦྲེལ" +unlink = "སྦྲེལ་མཐུད་གཅོད" + +[portal.accountLink.card.error] +title = "སྦྲེལ་མ་ཐུབ" + +[portal.accountLink.card.loginNotConfigured] +after = "hosted Stirling རྩིས་ཐོ་དང་རྩིས་ཐོ་སྦྲེལ་མཐུད་སྤྱོད་ཐུབ་པར། dev ནང་ཁྱེད་ཀྱིས་སྦྲེལ་མཐུད་གླེང་སྒྲོམ་ནས་ནང་འཛུལ་དཔེ་སྟོན་བྱེད་ཐུབ།" +before = "སྒྲིག" +title = "SaaS ནང་འཛུལ་སྒྲིག་མེད" + +[portal.accountLink.gate] +action = "རྩིས་ཐོ་སྦྲེལ" +description = "རིན་འབབ་ཡོད་པའི་ཁྱད་ཆོས་སྤྱོད་ཆེད་ཚོགས་པ་འདིའི Stirling རྩིས་ཐོ་སྦྲེལ།" +title = "སྦྲེལ་ནས་སྒོ་འབྱེད" +titleFeature = "{{feature}} སྒོ་འབྱེད་ཆེད་སྦྲེལ" + +[portal.accountLink.instances] +active = "འགུལ་སྐྱོད" +revoke = "ཕྱིར་འཐེན" +revoked = "ཕྱིར་འཐེན་ཟིན" +unnamed = "མིང་མེད་དཔེ་མཚོན" + +[portal.accountLink.instances.columns] +instance = "དཔེ་མཚོན" +lastSeen = "མཐའ་མར་མཐོང་བ" +linked = "སྦྲེལ་ཟིན" +status = "གནས་ཚུལ" + +[portal.accountLink.instances.empty] +description = "ཚོགས་པ་འདིའི་རྩིས་ཐོ་སྦྲེལ་ནས། དེ་རྗེས་ཁྱེད་ཀྱི་རང་གིས་གནས་སྤེལ་བྱས་པའི་དཔེ་མཚོན་ཐོ་འགོད་བྱས་ན་འདིར་མཐོང་ཐུབ།" +title = "སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་མེད" + +[portal.accountLink.instances.time] +daysAgo_one = "{{count}} ཉིན་སྔོན" +daysAgo_other = "{{count}} ཉིན་སྔོན" +hoursAgo_one = "{{count}} ཆུ་ཚོད་སྔོན" +hoursAgo_other = "{{count}} ཆུ་ཚོད་སྔོན" +justNow = "ད་ལྟ་རང" +minutesAgo_one = "{{count}} སྐར་མ་སྔོན" +minutesAgo_other = "{{count}} སྐར་མ་སྔོན" +never = "ནམ་ཡང་མིན" + +[portal.accountLink.modal] +linkSubtitle = "server འདིས་རིན་འབབ་བསྡུ་དགོས་པའི་རྩིས་ཐོར་ནང་འཛུལ་བྱོས།" +linkTitle = "ཁྱེད་ཀྱི Stirling རྩིས་ཐོ་སྦྲེལ" +reauthSubtitle = "ཁྱེད་ཀྱི་ལས་ཡུན་དུས་ཡོལ་སོང་ — ཁྱེད་ཀྱི Stirling རྩིས་ཐོར་ཡང་བསྐྱར་ནང་འཛུལ་བྱོས། ཁྱེད་ཀྱི་དཔེ་མཚོན་སྦྲེལ་ཟིན་པར་གནས་ཡོད།" +reauthTitle = "ཡང་བསྐྱར་ནང་འཛུལ" +simulateSignIn = "ནང་འཛུལ་དཔེ་བཟོ (dev)" + +[portal.accountLink.modal.loginNotConfigured] +after = "hosted Stirling རྩིས་ཐོ་དང app ནང་སྦྲེལ་བ་ནུས་སྤེལ་བྱེད་པར།" +and = "དང" +before = "སྒྲིག" +title = "SaaS ནང་འཛུལ་སྒྲིག་མེད" + +[portal.accountLink.panel] +instancesSub = "ཚོགས་པ་འདིར་ཐོ་འགོད་བྱས་པའི་རང་གིས་གཉེར་བའི་དཔེ་མཚོན་ཚང་མ། དེའི་ལྟ་མེད་འཛུལ་སྤྱོད་འཕྲལ་དུ་གཅོད་པར་དཔང་ཡིག་ཕྱིར་འཐེན་བྱོས།" +instancesTitle = "སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན" +revokeError = "དཔེ་མཚོན་ཕྱིར་འཐེན་མ་ཐུབ" +sub = "རང་གིས་གཉེར་བའི་ཚོགས་པ་འདི་དེའི Stirling རྩིས་ཐོ་དང་སྦྲེལ་ནས། ལྟ་མེད་ལས་སྒྲུབ་ཀྱི་རིན་འབབ་ཁྱེད་ཀྱི་ཚོགས་པའི་དངུལ་ཁུག་ལ་བརྩི་རུ་འཇུག" + +[portal.accountLink.panel.loadError] +forbidden = "ཚོགས་པའི་བདག་པོ་ཁོ་ནས་ཚོགས་པའི་སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་ལྟ་ཐུབ།" +generic = "རུ་ཁག་གི་སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་འཇུག་ལེན་མ་ཐུབ། སྐབས་ཤིག་རྗེས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" +title = "སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་འཇུག་ལེན་མ་ཐུབ" + +[portal.accountLink.state] +free = "རྩོམ་སྒྲིག་འཆར་གཞི" +subscribed = "Processor འཆར་གཞི" +unlinked = "སྦྲེལ་མེད" + +[portal.assistant] +close = "Assistant ཁ་རྒྱག" +error = "Assistant ལ་འབྲེལ་མ་ཐུབ།" +errorWithDetail = "Assistant ལ་འབྲེལ་མ་ཐུབ། {{detail}}" +inputAriaLabel = "Assistant ལ་དྲིས" +inputPlaceholder = "Stirling སྐོར་དྲིས…" +open = "Assistant ཁ་འབྱེད" +send = "སྐུར" +title = "Assistant" +tryAsking = "དྲི་བ་ཚོད་ལྟ་བྱོས" +typing = "ཡིག་འཇུག་བྱེད་བཞིན" + +[portal.auth] +loading = "འཇུག་ལེན་བྱེད་བཞིན" +redirectingToEditor = "རྩོམ་སྒྲིག་ཆས་ལ་ཕྱོགས་སྒྱུར་བྱེད་བཞིན..." + +[portal.billing.activation] +maybeLater = "ཕྱིས་སུ་འགྲིག" +title = "Processor སྤོར" + +[portal.billing.activation.payg] +sub = "ཟླ་རེའི་ཚད་བཀོད། ཁྱེད་ཀྱིས་སྤེལ་བའི་ལས་ཀར་རིན་སྤྲོད།" +title = "སྤྱད་ཚད་ལྟར་རིན་སྤྲོད" + +[portal.billing.activation.prepay] +badge = "ཟླ་བ 2 རིན་མེད" +sub = "འཛིན་ཤོག་གཅིག གཱ་ཡང་ན་དངུལ་ཁང་བརྒྱུད་སྤོ་བ།" +title = "ལོ་གཅིག་སྔོན་སྤྲོད" + +[portal.billing.checkout] +noClientSecret = "Edge function གིས client_secret ཕྱིར་སློག་མ་བྱས།" +title = "སྤྲོད་ཐབས་ཁ་སྣོན" + +[portal.billing.checkout.activationSlow] +body = "ཁྱེད་ཀྱི་སྤྲོད་དངུལ་ལེགས་གྲུབ་བྱུང་ཡང་། ནུས་སྤེལ་དེ་རྒྱུན་ལས་ཅུང་རིང་ཙམ་འགོར་བཞིན་འདུག དེ་རང་འགུལ་གྱིས་སྤོར་རྒྱུ་ཡིན — འདི་ཁ་རྒྱག་ན། མི་རིང་བར་འདིར་མངོན་ཡོང་།" +close = "ཁ་རྒྱག" +title = "ཧ་ལམ་འགྲུབ་ཚར" + +[portal.billing.checkout.cap] +amountAria = "ཟླ་རེའི་སྤྱོད་འགྲོ་ཚད" +back = "ཕྱིར" +continue = "སྤྲོད་དངུལ་ལ་མུ་མཐུད" +error = "ཁྱེད་ཀྱི་སྤྱོད་འགྲོ་ཚད་སྒྲིག་མ་ཐུབ" +finePrint = "འཛིན་ཤོག་ཟླ་རེར་སྤེལ་ཡོང་། དུས་ནམ་ཡང་མེད་པར་བཟོ་ཆོག" +noLimit = "ཚད་མེད" +note = "ཁྱེད་ལ་ཚད་ལས་བརྒལ་ནས་རིན་འབབ་ནམ་ཡང་མི་བསྡུ། ལས་སྒྲུབ་ཙམ་མཚམས་འཇོག་བྱེད།" +perMonth = "/ ཟླ" +title = "ཁྱེད་ཀྱི་སྤྱོད་འགྲོ་ཚད་སྒྲིག" + +[portal.billing.checkout.card] +fields = "གཱ་ཨང་ · MM / YY · CVC · ZIP" +label = "གཱ་ཞིབ་ཆ" +note = "གཱ་ཞིབ་ཆ་ Stripe ཡིས་བསྡུ་ལེན་བྱེད། Stirling གིས PAN ཡང་ན CVC ནམ་ཡང་ཉར་མི་བྱེད།" + +[portal.billing.checkout.error] +title = "checkout འགོ་འཛུགས་མ་ཐུབ" + +[portal.billing.checkout.finalizing] +body = "ཁྱེད་ཀྱི་སྤྲོད་དངུལ་འགྲུབ་སོང་། ང་ཚོས་ཁྱེད་ཀྱི་སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་ཚང་མར་ཚད་འཇལ་ལས་སྒྲུབ་སྤོར་བཞིན་ཡོད — འདི་ལ་སྤྱིར་བཏང་སྐར་ཆ་འགའ་ལས་མི་འགོར།" +hint = "སྒེའུ་ཁུང་འདི་ཁ་མ་རྒྱག་པར་ཉར་རོགས།" +title = "ཁྱེད་ཀྱི Processor འཆར་གཞི་ནུས་སྤེལ་བྱེད་བཞིན..." + +[portal.billing.checkout.pay] +limit = "ཟླ་རེའི་སྤྱོད་འགྲོ་ཚད {{amount}}/mo། ཚད་ལ་སླེབས་དུས་ལས་སྒྲུབ་མཚམས་འཇོག" +limitNoCap = "སྤྱོད་འགྲོ་ཚད་མེད། ལས་སྒྲུབ་མཚམས་མི་འཇོག འཛིན་ཤོག་ཟླ་རེར་སྤེལ་ཡོང་།" + +[portal.billing.enterpriseUpsell] +cta = "Enterprise ལ་ཞིབ་ལྟ" +description = "ཁས་ལེན་བྱས་པའི་སྤྱོད་ཚད་ཀྱི་ཆག་ཡང་། air-gapped deployment། མངགས་བཟོ MSA དང་བདེ་འཇགས་ཞིབ་བཤེར། 3rd-party བཀྲམ་སྤེལ་མཉམ་ལས།" +eyebrow = "སྤྱོད་ཚད་ཆག་ཡང་ · 1M+ PDFs" +title = "Stirling Enterprise" + +[portal.billing.freeEditors] +activeThisMonth = "ཟླ་འདིར་སྤྱོད་བཞིན" +cost = "རིན་གོང" +editorsDeployed = "བཀོད་སྤེལ་བྱས་པའི་རྩོམ་སྒྲིག་ཆས" +inviteTeammates = "རུ་གྲོགས་གདན་འདྲེན" +pdfsEdited = "རྩོམ་སྒྲིག་བྱས་པའི PDFs" +subtitle = "གང་དུ་ཡང་བཀོད་སྤེལ་བྱོས། ཁྱེད་ཀྱི་རུ་ཁག་ཚང་མའི་ཆེད།" +title = "རིན་མེད PDF རྩོམ་སྒྲིག་ཆས" + +[portal.billing.freePlan] +checkoutErrorTitle = "checkout འགོ་འཛུགས་མ་ཐུབ" +currentPlan = "མིག་སྔའི་འཆར་གཞི" +freeForever = "རྟག་ཏུ་རིན་མེད" +noTeamResolved = "ཁྱེད་ཀྱི་དངུལ་ཁུག་སྟེང་རུ་ཁག་ད་དུང་གཏན་འཁེལ་མ་བྱུང་ — བསྐྱར་གསོ་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" +ownerOnly = "རུ་ཁག་གི་བདག་པོ་ཁོ་ནས Processor འཆར་གཞི་སྤོར་ཐུབ།" +payInvoice = "མཇུག་སྒྲུབ་ཆེད་འཛིན་ཤོག་སྤྲོད" +planName = "Editor" +ssoIncluded = "SSO ཚུད་ཡོད" +switchOnProcessor = "Processor སྤོར →" +unlimitedUsers = "སྤྱོད་མཁན་ཚད་མེད" +viewQuote = "རིན་གོང་ལྟ" + +[portal.billing.invoices] +columnAmount = "དངུལ་འབོར" +columnDate = "ཚེས་གྲངས" +columnDescription = "འགྲེལ་བཤད" +columnPdfsProcessed = "ལས་སྒྲུབ་བྱས་པའི PDFs" +columnStatus = "གནས་ཚུལ" +descriptionFallback = "འཛིན་ཤོག" +downloadAriaLabel = "འཛིན་ཤོག {{number}} PDF ལྟར་ཕབ་ལེན" +emptyDescription = "ཁྱེད་ཀྱི་རུ་ཁག་གིས་མངགས་ཉོ་བྱས་ནས་འཁོར་ཡུན་དང་པོ་མཇུག་སྒྲིལ་རྗེས། ཁྱེད་ཀྱི་འཛིན་ཤོག་འདིར་མངོན་ཡོང་།" +emptyTitle = "ད་དུང་འཛིན་ཤོག་མེད" +fetchLimitNote_one = "ཁྱེད་ཀྱི་ཉེ་ཆར་གྱི་འཛིན་ཤོག {{count}} སྟོན་བཞིན་ཡོད། འཛིན་ཤོག་རྙིང་པ་རྣམས་ Stripe portal ནང་ཡོད།" +fetchLimitNote_other = "ཁྱེད་ཀྱི་ཉེ་ཆར་གྱི་འཛིན་ཤོག {{count}} སྟོན་བཞིན་ཡོད། འཛིན་ཤོག་རྙིང་པ་རྣམས་ Stripe portal ནང་ཡོད།" +loadError = "འཛིན་ཤོག་འཇུག་ལེན་མ་ཐུབ། {{error}}" +pdfLink = "PDF ↓" +showAll_one = "ཚང་མ {{count}} སྟོན" +showAll_other = "ཚང་མ {{count}} སྟོན" +showFewer_one = "ཉུང་ཙམ་སྟོན (མགོའི {{count}})" +showFewer_other = "ཉུང་ཙམ་སྟོན (མགོའི {{count}})" +showMostRecent_one = "ཉེ་ཆར་གྱི {{count}} སྟོན" +showMostRecent_other = "ཉེ་ཆར་གྱི {{count}} སྟོན" +title = "འཛིན་ཤོག་ལོ་རྒྱུས" +viewAriaLabel = "Stripe ནང་འཛིན་ཤོག {{number}} ལྟ" +viewLink = "ལྟ ↗" + +[portal.billing.linkPrompt] +cta = "Stirling རྩིས་ཐོ་སྦྲེལ" +description = "ལག་སྒུལ PDF རྩོམ་སྒྲིག — ལྟ་བ། མིང་རྟགས་འགོད་པ། མཉམ་སྒྲིལ། དབྱེ་བ། ཆུ་རྟགས། སྡུད་བཙིར། བསྒྱུར་བ། ལག་སྒུལ OCR — ནམ་ཡང་རིན་མེད་ཡིན། སྦྲེལ་ཡོད་མེད་གང་ཡང་འདྲ། ཚད་འཇལ་ལས་སྒྲུབ (automation, AI, དང API) གི་རིན་མེད PDFs 500 ལེན་པར་སྦྲེལ། དེ་ལས་མང་བ་དགོས་དུས། Processor འཆར་གཞི་སྤོར་ནས་ཁྱེད་ཀྱིས་སྤྱད་པ་ཁོ་ནར་རིན་སྤྲོད།" +title = "ཁྱེད་ཀྱི Stirling རྩིས་ཐོ་སྦྲེལ" + +[portal.billing.paymentMethod] +billedMonthly = "ཟླ་རེར་རིན་འབབ་བསྡུ" +cardEnding = "{{brand}} མཇུག་ཨང་ {{last4}}" +cardFallback = "གཱ" +expiresBilledMonthly = "དུས་ཚོད {{expiry}} ལ་ཟད · ཟླ་རེར་རིན་འབབ་བསྡུ" +eyebrow = "སྤྲོད་ཐབས" +managedSub = "ཁྱེད་ཀྱི་གཱ་དང་རིན་འབབ་ཞིབ་ཆ་ Stripe ཡི་མཁོ་མཁན portal ནང་བདེ་འཇགས་ངང་ཉར་ཡོད།" +managedTitle = "Stripe ནང་དོ་དམ་བྱས" +update = "གསར་སྒྱུར" + +[portal.billing.pdfsProcessed] +emptyPeriod = "དུས་ཡུན་འདིར་ད་དུང་ལས་སྒྲུབ་མེད།" +eyebrow = "དུས་ཡུན་འདིར་ལས་སྒྲུབ་བྱས་པའི PDFs" +legendValue_one = "{{formatted}} PDFs" +legendValue_other = "{{formatted}} PDFs" +segbarAriaLabel = "PDFs རིགས་དབྱེ་ལྟར་དབྱེ་བ" +segmentAgentsDesc = "AI agent བྱ་སྤྱོད" +segmentAgentsLabel = "Agents" +segmentApiDesc = "ཐད་ཀར API རེ་ཞུ" +segmentApiLabel = "API" +segmentAutomationDesc = "Automations & pipelines" +segmentAutomationLabel = "Automation" +sizeMultiplier = "{{formatted}} PDFs ལ་ཆེ་ཆུང་སྒྱུར་རྟེན་སྤྱད་ཡོད" +summary = "{{unique}} ཁེར་རྐྱང་ · {{units}} meter units · PDF རེར་ཆ་སྙོམས {{avg}}" +summaryNoRate = "{{unique}} ཁེར་རྐྱང་ · {{units}} meter units" +unit = "PDFs" +unitsPending_one = "{{units}} meter unit སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་ནས་མཉམ་སྒྲིག་སྒུག་བཞིན" +unitsPending_other = "{{units}} meter units སྦྲེལ་ཟིན་པའི་དཔེ་མཚོན་ནས་མཉམ་སྒྲིག་སྒུག་བཞིན" + +[portal.billing.prepaid] +capSuffix = "སྔོན་སྤྲོད་དངུལ་ལྷག {{total}} ནས" +expires = "དུས་ཚོད {{date}} ལ་ཟད" +eyebrow = "སྔོན་སྤྲོད་ཤོང་ཚད" +note = "ཚད་འཇལ་རིན་འབབ་དང་ཁྱེད་ཀྱི་སྤྱོད་འགྲོ་ཚད་ཕྱིར་སྔོན་ལ་སྤྱོད།" +topUp = "ཁ་སྣོན" + +[portal.billing.prepaid.buy] +back = "ཕྱིར" +calcTitle = "ཁྱེད་ཀྱི་ལོ་འཁོར་སྤྲོད་དངུལ་རྩིས་རྒྱག" +cancel = "མེད་པར་བཟོ" +cancelPurchase = "ཉོ་སྒྲུབ་རྩིས་མེད་གཏོང་" +close = "ཁ་རྒྱག" +continueToPayment = "དངུལ་སྤྲོད་ལ་མུ་མཐུད་" +finalise = "མཐའ་མཇུག་སྒྲུབ་" +invoiceErrorTitle = "འཛིན་ཤོག་བཟོ་མ་ཐུབ་" +loadingTitle = "ཁྱེད་ཀྱི་རིན་གོང་གསལ་ཐོ་འཇུག་བཞིན་" +payTitle = "ཁྱེད་ཀྱི་ལོ་གཅིག་ལ་དངུལ་སྤྲོད་" +step = "གོམ་རིམ་ {{current}} / {{total}}" + +[portal.billing.prepaid.calc] +change = "བསྒྱུར་" +deployRow = "བཀོད་འཇོག" +done = "ཚར་" +enterpriseHint = "འདི་ནི་ཁེ་ལས་རིམ་པའི་ཚད་རྒྱ་ཡིན། བོངས་ཚད་འདིར་ཁེ་ལས་རིན་ཚད་ཀྱིས་རང་ཞབས་ཆག་ཡང་གང་ལས་ཀྱང་ལེགས། སྐར་མ་འགའི་ནང་རིན་ཚད་གཏན་འཇགས། ཆ་རྐྱེན། དང་རིན་གོང་གསལ་ཐོ།" +governanceRow = "དོ་དམ" +governanceValue = "{{label}} · སྲིད་ཇུས་ {{count}}" +handlesLabel = "ཁྱེད་ཀྱི་སྒྲིག་གཅོད་ཆས་" +handlesValue = "ཟླ་རེར {{volume}} credits སྒྲིག་གཅོད་བྱེད་" +pipelinesLabel = "Pipelines" +policiesMeta = "སྲིད་ཇུས་ {{count}}" +poolCaption = "ལོ་གཅིག་ལ་ {{credits}} credits ཀྱི་མཉམ་སྤྱོད་མཛོད་གཅིག བེད་སྤྱོད་མང་བའི་ཟླ་བས་ཉུང་བའི་ཟླ་བ་ནས་གཡར་སྤྱོད་བྱེད།" +priceLabel = "ཁྱེད་ཀྱི་ལོ་གཅིག · ཟླ་ 10 ཡི་རིན་གོང་ལ་ཟླ་ 12" +rateUnknown = "རྩིས་སྤྲོད་སྐབས་ང་ཚོས་རིན་གོང་ཏག་ཏག་སྟོན་རྒྱུ་ཡིན།" +savings = "ཁྱེད་ཀྱིས་ {{amount}} གྲོན་ཆུང་བྱེད · ཟླ་ 2 རིན་མེད།" +sizingRow = "ཆེ་ཆུང་ཚད་འཛིན་" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "སྤྱོད་མཁན་ཁྱོན་བསྡོམས་" + +[portal.billing.prepaid.consent] +label = "ངས་ཤེས་རྟོགས་བྱས་པ་ནི། ངའི་སྔོན་སྤྲོད་ཤོང་ཚད་ཚར་བའམ་ཟླ་ 12 རྗེས་དུས་ཡོལ་བྱུང་ན། ངས་རྩིས་མེད་མ་བཏང་ཚེ་སྒྲིག་གཅོད་རང་འགུལ་གྱིས་ཚད་ལྡན་ཚད་རྩིས་སྤྱད་ཚད་ལྟར་སྤྲོད་པའི་རིན་ཚད་ལྟར་(ངའི་འགྲོ་གྲོན་ཚད་བཀག་བར) མུ་མཐུད་འགྲོ་རྒྱུ་དང་། ངས་དུས་ནམ་ཡང་དངུལ་རྩིས་སྒོ་མོ་ནས་རྩིས་མེད་གཏོང་ཐུབ།" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling གིས་དོ་དམ་བྱེད། སྐར་མ་འགའི་ནང་སྤྱོད་ཐུབ།" +cloudValue = "Stirling Cloud · དོ་དམ་བྱས་པ" +selfhost = "རང་གིས་གནས་སྐྱོང་" +selfhostDesc = "སྒེར་གྱི་རྨང་གཞིའི་སྟེང་འཁོར། རིན་ཚད་གཅིག་པ།" +selfhostValue = "རང་གིས་གནས་སྐྱོང་ · སྒེར་གྱི་རྨང་གཞི་" + +[portal.billing.prepaid.offer] +cta = "མཁོ་འདོན་ལ་བསྐྱར་ཞིབ་" +subtitle = "PDF སྒྲིག་གཅོད་ལ་ལོ་གཅིག་སྔོན་སྤྲོད་བྱས་ནས་ཟླ་གཉིས་རིན་མེད་ཐོབ། དེ་ཚད་རྩིས་དངུལ་རྩིས་སྔོན་ལ་སྤྱོད་པ་དང་། ཁྱེད་ཀྱི་འགྲོ་གྲོན་ཚད་བཀག་ཕྱི་རོལ་དུ་ཡིན།" +title = "ཟླ་ 10 ཡི་རིན་གོང་ལ་ཟླ་ 12 ཐོབ་" + +[portal.billing.prepaid.pay] +accountName = "རྩིས་ཐོ་བདག་པོའི་མིང་" +accountNamePlaceholder = "དཔེར་ན། Jane Smith" +companyName = "ཀུང་སིའི་མིང་ (འདེམས་རུང)" +companyNamePlaceholder = "དཔེར་ན། Acme Inc." +downloadInvoice = "འཛིན་ཤོག་ཕབ་ལེན་" +payOnline = "དྲ་ཐོག་ནས་དངུལ་སྤྲོད་" +poNumber = "PO ཨང་རྟགས་ (འདེམས་རུང)" +poNumberPlaceholder = "དཔེར་ན། PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "མཐོ་རིམ་" +advancedDesc = "Pipelines ཀྱིས་ཁྱེད་ཀྱི་སྒྲིག་གཅོད་མང་ཆེ་བ་སྐུལ་སློང་བྱེད།" +none = "མེད་" +noneDesc = "ད་དུང Pipelines འཁོར་གཏོང་བྱེད་མེད། དུས་ནམ་ཡང་སྒོ་ཕྱེ་ཆོག" +standard = "Standard" +standardDesc = "Pipelines འགའ་ཞིག་གིས་འབྱོར་བའི PDFs བསྐྱར་སྒྲིག་གཅོད་བྱེད།" + +[portal.billing.prepaid.posture] +essentials = "གཞི་རྩ་" +essentialsDesc = "དབྱེ་སྡེ་འབྱེད་པ། སྔོན་སྒྲིག དེ་མིན་མཉམ་སྤྱོད་" +governed = "དོ་དམ་བྱས་པ་" +governedDesc = "བདེ་འཇགས་དང་ལམ་སྟོན་ཁ་སྣོན་བྱེད་" +regulated = "སྒྲིག་སྲོལ་ཅན་" +regulatedDesc = "དབྱེ་སྡེ་ཚང་མ། མཐུན་སྒྲིག ཉར་ཚགས། འཇུག་ལེན་བཅས་ཚུད་" + +[portal.billing.prepaid.review] +download = "རིན་གོང་གསལ་ཐོ་ཕབ་ལེན་ (PDF)" +downloading = "སྒྲིག་བཞིན…" +share = "ཆོག་མཆན་ཆེད་མཉམ་སྤྱོད་" + +[portal.billing.prepaid.size] +compact = "ཆུང་བསྡུས་" +compactDesc = "ཡིག་ཆ་ 25 MB འོག གཞི་གྲངས་རིན་འཐུས་མེད་" +heavy = "ལྗིད་པོ་" +heavyDesc = "བཤེར་འབེབས་བྱས་པའམ་པར་མང་བ། རྒྱུན་དུ་ 50 MB+" +standard = "Standard" +standardDesc = "མང་ཆེ་བ་ཆུང་ངུ་། བཤེར་འབེབས་ཁ་ཤས་ 25 MB ལས་བརྒལ་" + +[portal.billing.prepaid.state] +exhausted = "སྤྱད་ཚར་" +healthy = "མང་པོ་ལྷག་" +low = "ཉུང་དུ་འགྲོ་བཞིན་" + +[portal.billing.spendLimit] +adjustLimit = "ཚད་བཀག་སྒྲིག་" +cancel = "རྩིས་མེད་" +capControlNote = "བསྒྱུར་བ་འཕྲལ་དུ་ནུས་པ་ཐོན་ — དུས་ནམ་ཡང་མཐོ་ཚད་སྤར་རམ་དམའ་རུ་གཏོང་།" +capSuffix = "/ ཟླ་རེར" +capSuffixWithDocs = "/ ཟླ་རེར · ≈ ཡིག་ཆ་ {{documents}}" +displaySub = "ཁྱེད་ཀྱིས་རང་འགུལ་སྒྲིག་གཅོད་བྱས་པ་ཙམ་ལ་རིན་འཐུས་བསྡུ་བ་ཡིན — མཐོ་ཚད་ནས་ནམ་ཡང་མི་བརྒལ།" +editTitle = "ཁྱེད་ཀྱི་ཟླ་རེའི་མཐོ་ཚད་སྒྲིག་" +eyebrow = "འགྲོ་གྲོན་ཚད་བཀག་" +guardrailBody = "མཐོ་ཚད་གཏན་པོ་ཞིག ཁྱེད་ལ་དེ་ལས་བརྒལ་ནས་ནམ་ཡང་རིན་འཐུས་མི་བསྡུ། ཚད་བཀག་ལ་སླེབས་སྐབས། ཁྱེད་ཀྱིས་དེ་སྤར་བའམ་འཁོར་ཡུན་བསྐྱར་སྒྲིག་བྱེད་པ་བར་ཚད་རྩིས་སྒྲིག་གཅོད་སྐབས་འཇོག་བྱེད་ (PDF རྩོམ་སྒྲིག་ཚད་མེད་མུ་མཐུད་ལས་ཀ་བྱེད)། གང་ཡང་བརླག་མི་འགྲོ།" +guardrailLabel = "ཁྱེད་ཀྱི་སྲུང་ཚད།" +noCap = "མཐོ་ཚད་མེད་" +pctUsed = "{{pct}}% སྤྱད་ཟིན་" +remaining = "{{amount}} ལྷག་" +save = "ཚད་བཀག་ཉར་" +saveError = "ཚད་བཀག་ཉར་མ་ཐུབ་" +thisPeriodUncapped = "དུས་ཡུན་འདིར་ {{amount}} · མཐོ་ཚད་མེད་" +usedThisMonth = "ཟླ་འདིར་ {{amount}} སྤྱད་" +useSuggested = "འོས་སྦྱོར་སྤྱོད་ · {{amount}} / ཟླ་རེར" + +[portal.billing.spendLimit.projection] +body_one = "{{rate}}/ཉིན་རེར་ལྟར་ན། ཁྱེད་ ~{{count}} ཉིན་ནང་ཚད་བཀག་ལ་སླེབས་ (~{{monthEnd}} ཟླ་མཇུག)། འོས་སྦྱོར་ཚད་བཀག་ ~{{suggested}}།" +body_other = "{{rate}}/ཉིན་རེར་ལྟར་ན། ཁྱེད་ ~{{count}} ཉིན་ནང་ཚད་བཀག་ལ་སླེབས་ (~{{monthEnd}} ཟླ་མཇུག)། འོས་སྦྱོར་ཚད་བཀག་ ~{{suggested}}།" +label = "བརྒལ་རྒྱུར་སྔོན་དཔག་བྱས་པ།" + +[portal.billing.spendThisMonth] +eyebrow = "ཟླ་འདིའི་འགྲོ་གྲོན་" +freeRemaining_one = "རིན་མེད PDF {{formatted}} ལྷག་" +freeRemaining_other = "རིན་མེད PDFs {{formatted}} ལྷག་" +processed_one = "PDF {{formattedCount}} སྒྲིག་གཅོད་བྱས།" +processed_other = "PDFs {{formattedCount}} སྒྲིག་གཅོད་བྱས།" +processedWithRate_one = "PDF {{formattedCount}} སྒྲིག་གཅོད་བྱས། རེ་རེར་ {{rate}}།" +processedWithRate_other = "PDFs {{formattedCount}} སྒྲིག་གཅོད་བྱས། རེ་རེར་ {{rate}}།" + +[portal.billing.subscribedPlan.capWarn] +approachingBody = "རང་འགུལ་སྒྲིག་གཅོད་ནམ་ཡང་མཚམས་མི་འཇོག་པའི་ཆེད་ད་ལྟ་དེ་སྤར།" +approachingTitle = "ཁྱེད་ཟླ་རེའི་འགྲོ་གྲོན་ཚད་བཀག་གི་ {{pct}}% ལ་སླེབས་འདུག" +raiseLimit = "ཚད་བཀག་སྤར་" +reachedBody = "ཁྱེད་ཀྱིས་ཚད་བཀག་སྤར་བའམ་འཁོར་ཡུན་བསྐྱར་སྒྲིག་བྱེད་པ་བར། ཚད་རྩིས་སྒྲིག་གཅོད་སྐབས་འཇོག་བྱས་ཡོད། PDF རྩོམ་སྒྲིག་ཚད་མེད་མུ་མཐུད་ལས་ཀ་བྱེད།" +reachedTitle = "ཟླ་རེའི་འགྲོ་གྲོན་ཚད་བཀག་ལ་སླེབས་ཟིན་" + +[portal.billing.subscribedPlan.portalError] +title = "Stripe སྒོ་མོ་ཁ་ཕྱེ་མ་ཐུབ་" + +[portal.billing.walletMeter] +barAria = "Free PDFs remaining" +capSuffix_one = "རིན་མེད PDFs {{allowance}} ནས་སྤྱད་པ་" +capSuffix_other = "རིན་མེད PDFs {{allowance}} ནས་སྤྱད་པ་" +eyebrow = "སྒྲིག་གཅོད་ཆས་ཚོད་སྤྱོད་" +statusLabel_one = "{{remaining}} ལྷག་" +statusLabel_other = "{{remaining}} ལྷག་" +sub = "PDF རྩོམ་སྒྲིག་ཆས་རིན་མེད་སྤྱོད། PDFs རང་འགུལ་སྒྲིག་གཅོད་ལ་དངུལ་སྤྲོད།" +title_one = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད་" +title_other = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད་" +titleWithRate_one = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད། དེ་རྗེས་ {{rate}}/PDF" +titleWithRate_other = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད། དེ་རྗེས་ {{rate}}/PDF" + +[portal.components.billingUnit] +approval = "ཆོག་མཆན་" +check = "ཞིབ་བཤེར་" +event = "དོན་རྐྱེན་" +render = "འཆར་སྟོན་" +review = "བསྐྱར་ཞིབ་" +session = "གླེང་མོལ་དུས་ཡུན་" +signature = "མིང་རྟགས་" + +[portal.components.maturity] +beta = "Beta" +ga = "GA" + +[portal.connections] +createTitle = "མཐུད་སྦྲེལ་གསར་པ་" +createTitleFor = "{{name}} མཐུད་སྦྲེལ་" +delete = "སུབ་" +edit = "རྩོམ་སྒྲིག་" +editTitle = "མཐུད་སྦྲེལ་རྩོམ་སྒྲིག་" +unsupportedType = "མཐུད་སྦྲེལ་རིགས་འདི་འདིར་རྩོམ་སྒྲིག་བྱེད་མི་ཐུབ།" + +[portal.connections.categories.advanced] +description = "ང་ཚོས་ད་དུང་རྒྱབ་སྐྱོར་མ་བྱས་པའི་ཞབས་ཞུའི་ཆེད།" +label = "མཐོ་རིམ་" + +[portal.connections.categories.audit] +description = "ཡིག་ཆ་ལ་གང་བྱུང་བ་ཟིན་ཐོར་འགོད། ཁྱེད་ཀྱི་རྩིས་ཞིབ་པས་སྔར་ནས་ལྟ་སའི་གནས་སུ།" +label = "རྩིས་ཞིབ་དང་མཐུན་སྒྲིག་ཟིན་ཐོ་" + +[portal.connections.categories.notify] +description = "དོན་རྐྱེན་ཞིག་བྱུང་བ་མ་ལག་གཞན་ཞིག་ལ་བརྡ་སྤྲོད།" +label = "བརྡ་ཐོ་དང་ལས་རིམ་" + +[portal.connections.categories.security] +description = "ཡིག་ཆ་མུ་མཐུད་སྤོ་བའི་སྔོན་ལ་བཤེར་འབེབས་སམ་མཚོན་རྟགས་འགོད།" +label = "བདེ་འཇགས་དང་དབྱེ་སྡེ་འབྱེད་པ་" + +[portal.connections.categories.signing] +description = "མིང་རྟགས་འགོད་པའམ་གཞུང་དཔང་བྱེད་པའི་ཆེད་ཡིག་ཆ་ཕྱིར་གཏོང་།" +label = "གློག་རྡུལ་མིང་རྟགས་དང་གཞུང་དཔང་" + +[portal.connections.categories.storage] +description = "སྒྲིག་གཅོད་བྱས་པའི་ཡིག་ཆ་མཐར་སླེབས་ས་གནས་ - སྣོད་གཅིག སྐུལ་ཆས་གཅིག འཛིན་བྱང་གཅིག་གམ་ཤོག་ངོས་གཅིག" +label = "ཡིག་ཆ་དང་ཟུར་སྦྱར་" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling གིས་འདི་གསང་སྡོམ་བྱས་ནས་ཉར་བ་དང་སླར་ཡང་མི་སྟོན།" +label = "འཛུལ་སྤྱོད token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "མཐུད་སྦྲེལ་འདི་སྤྱོད་པའི་གོམ་རིམ་གྱིས URL འདིའི་འོག་གི་ལམ་ཁ་ཁོ་ནར་འབོད་ཐུབ། host གཞན་ནམ་ཡང་མིན།" +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "སྤྱོད་མཁན་མིང་" + +[portal.connections.commonFields.webhookUrl] +helperText = "ཞབས་ཞུས་ཁྱེད་ལ་སྤྲད་པའི URL སྦྱར། དེའི་འོག་གི་ལམ་ཁ་ཁོ་ནར་འབོད་ཐུབ།" +label = "Webhook URL" + +[portal.connections.fields] +name = "འབྲེལ་མཐུད་མིང་།" +namePlaceholder = "དཔེར་ན། Production" + +[portal.connections.picker] +cancel = "འདོར།" +createNew = "འབྲེལ་མཐུད་གསར་པ་..." +placeholder = "འབྲེལ་མཐུད་ཞིག་འདེམས།" +save = "འབྲེལ་མཐུད་ཉར།" + +[portal.connections.picker2] +back = "མཉམ་སྒྲིལ་བསྒྱུར།" +heading = "ཁྱེད་ཀྱིས་གང་ཞིག་འབྲེལ་མཐུད་བྱེད་འདོད་དམ།" +noResultsBody = "ང་ཚོས་ད་དུང་དེ་ལ་རྒྱབ་སྐྱོར་མི་བྱེད་ — དེའི་སྐོར་ང་ཚོར་བཤད་རོགས། ང་ཚོས་དེ་སྣོན་རྒྱུར་བལྟ་ངེས།" +noResultsTitle = "\"{{query}}\" དང་མཐུན་པ་ཅི་ཡང་མེད།" +searchPlaceholder = "མཉམ་སྒྲིལ་འཚོལ། — \"sign\", \"ocr\" ཡང་ན་ཐོན་རྫས་མིང་ཞིག་ཚོད་ལྟ་བྱོས།" + +[portal.connections.types.api] +description = "སྲིད་ཇུས་ནས HTTP API གང་རུང་འབོད། URL, བདེན་སྦྱོར་དང payload ཁྱེད་ཀྱིས་འདེམས།" +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "བདེན་སྦྱོར།" + +[portal.connections.types.api.fields.authType.options.basic] +label = "སྤྱོད་མཁན་མིང་དང་གསང་ཚིག" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "header རང་སྒྲིག་ནང་གི Token" + +[portal.connections.types.api.fields.authType.options.none] +label = "མེད།" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Token ཐོབ་ཆེད་ནང་འཛུལ།" + +[portal.connections.types.api.fields.baseUrl] +helperText = "འབྲེལ་མཐུད་འདི་སྤྱོད་པའི་གོམ་སྟབས་ཀྱིས URL འདིའི་འོག་གི་ལམ་ཁོ་ན་འབོད་ཐུབ། host གཞན་ནམ་ཡང་མིན།" +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header མིང་།" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "ནང་འཛུལ་ལམ།" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "གསང་ཚིག" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API ཡིས་ཕབ་ལེན་སྦྲེལ་ཐག་ཞིག་ལན་སྤྲད་ན། host འདི་ཚོ་ཁོ་ན་ལེན་ཆོག འབྲིང་རྟགས་ཀྱིས་ཁ་ཕྲལ། སྟོང་པར་བཞག་ན Base URL རང་གི host ཁོ་ན་ཆོག" +label = "ཆོག་པའི་མཇུག་འབྲས host" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Token འདི header འདི་ལྟར་སྐུར།" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "ནང་འཛུལ་འབོད་སྐབས Token ཕྱིར་སྤྲོད་པའི་ལན་གྱི header, དཔེར་ན X-Auth-Token." +label = "Token ལན་གྱི header" + +[portal.connections.types.api.fields.username] +label = "སྤྱོད་མཁན་མིང་།" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "ཁྱེད་ཀྱིས་གཉེར་བའི་ཞབས་ཞུ་ཆས་ཐོག་ཡིག་ཆར malware འཚོལ་ཞིབ་བྱེད། — ཅི་ཡང་ཁྱེད་ཀྱི་དྲ་རྒྱ་ལས་ཕྱིར་མི་འགྲོ།" +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "སྲིད་ཇུས་ནང་ཡིག་ཆའི་ནད་དུག་དང་ནང་དོན་འཚོལ་ཞིབ།" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "ངོས་ཟིན་ཟིན་པའི malware ཙམ་མ་ཡིན་པར། macros, executables, scripts ཡང་ན་ནང་འཇུག་བྱ་ཡུལ་ཡོད་པའི་ཡིག་ཆ་འགོག" +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ Confluence ཤོག་ངོས་ལ་ཟུར་སྣོན་བྱེད།" +label = "Confluence" + +[portal.connections.types.consigno] +description = "ཡིག་ཆ་ ConsignO Cloud (Notarius) ལ་གློག་རྡུལ་མཚན་རྟགས་དང་གཞུང་འཛིན་ལག་ཁྱེར་ཆེད་སྐུར།" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "ཁྱེད་ཀྱི ConsignO Cloud ཁོར་ཡུག, /api/v1 ཚུད་པ།" +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "གསང་ཚིག" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "ཁྱེད་ཀྱི་ཚོགས་པས tenant སྤྱོད་ན་ཁོ་ན་དགོས།" +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "སྤྱོད་མཁན་མིང་།" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ལས་སྣོན་བྱེད་སྐབས Discord ལ་བརྡ་འཕྲིན་ཞིག་སྤེལ།" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་སྒྲིག་སྦྱོར་བྱེད་ཐེངས་རེར Elasticsearch ནང་ཞིབ་རྩིས་བྱུང་རིམ་ཞིག་དཀར་ཆག་བཟོ།" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "ཡིག་ཆ་གསར་པའི་ཆེད FTP ཡང་ན FTPS ཞབས་ཞུ་ཆས་ལ་འཚོལ་ཞིབ་བྱེད།" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "འབྲེལ་མཐུད་རྣམ་པ།" + +[portal.connections.types.ftp.fields.passive.options] active = "Active" -paused = "Paused" -setup = "Set up" +passive = "Passive (འོས་སྦྱོར)" -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" +[portal.connections.types.ftp.fields.security] +helperText = "Plain FTP ཡིས་དཔང་ཡིག་དང་ཡིག་ཆ་གསང་སྡོམ་མེད་པར་སྐུར། ཞབས་ཞུ་ཆས་ཀྱིས་རྒྱབ་སྐྱོར་བྱེད་ན FTPS འདེམས།" +label = "གསང་སྡོམ།" -[policies.toolConfig.info] -redact = "Social Security དང་དངུལ་ཤོག་ཨང་ལྟ་བུ་ལྟ་བུའི་གསང་བའི་ཞིབ་ཕྲ་རང་འགུལ་གྱིས་འཚོལ་ཏེ་ནག་པོར་སྒྲིག་པས་ ཡིག་ཆ་ནང་ལྟ་མི་ཐུབ།" -sanitize = "ཡིག་ཆ་ནས་སྦས་པའི JavaScript བསུབ་པས་ མི་ཞིག་ཁ་ཕྱེས་སྐབས་རང་འགུལ་ངང་གང་ཡང་འགུལ་བྱེད་མི་ཚུགས།" -watermark = "ཤོག་ངོས་རེ་རེ་ལ་མཐོང་ཐུབ་པའི་མཚོན་རྟགས་ (e.g. \"Confidential\") བཀོད་བཀྲམ་བྱེད།" +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "མེད། (plain FTP)" -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "སྒོ་བསྡམས" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "སྲིད་ཇུས་འཁོར་སྐབས Google Chat སྟོང་ཆ་ལ་བརྡ་འཕྲིན་ཞིག་སྤེལ།" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ Jira issue ལ་ཟུར་སྣོན་བྱེད།" +label = "Jira" + +[portal.connections.types.mailgun] +description = "སྲིད་ཇུས་འཁོར་སྐབས་ཡིག་ཆ་ཞིག་ཡང་ན་བརྡ་ཐོ་ཞིག་གློག་འཕྲིན་གྱིས་སྐུར།" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ Nextcloud ནང་ཉར།" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App གསང་ཚིག" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "ཁྱེད་ཀྱིས་གཉེར་བའི Presidio ཞབས་ཞུ་ཆས་སྤྱད་ནས་ཡིག་ཆའི་ཡི་གེའི་ནང་སྒེར་གྱི་གནས་ཚུལ་ངོས་འཛིན།" +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "PDFs ཐོག Microsoft Purview sensitivity labels ཀློག་དང་སྤྱོད།" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "གདམ་རུང་། མིང་གིས label འདེམས་དགོས་ན་ཁོ་ན་དགོས། དེ་མེད་ཀྱང labelling ལས་ཀ་བྱེད།" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "label རེ་རེའི SiteId ལ་ཐམ་འགོད་བྱེད་པས། Purview-aware ལག་ཆས་ཀྱིས་ཚོགས་པ་གང་གིས་སྤྱད་པ་ཤེས་ཐུབ།" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "source ཀྱིས་ཀློག་ཐུབ་པ་དང pipeline ཐོན་འབྲས་ཀྱིས་འབྲི་ཐུབ་པའི S3 bucket ཞིག" +label = "S3 ཉར་ཚགས།" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS ཆེད་སྟོང་པར་བཞག MinIO ཡང་ན S3-compatible ཉར་ཁང་གཞན་ཆེད་འདི་སྒྲིག" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "ས་ཁུལ།" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "སྲིད་ཇུས་འཁོར་སྐབས་ཡིག་ཆ་ཞིག་ཡང་ན་བརྡ་ཐོ་ཞིག་གློག་འཕྲིན་གྱིས་སྐུར།" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "ཡིག་ཆ་གསར་པའི་ཆེད SFTP (SSH) ཞབས་ཞུ་ཆས་ལ་འཚོལ་ཞིབ་བྱེད།" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "གདམ་རུང་། ཞབས་ཞུ་ཆས་ཀྱི SHA-256 host key fingerprint (`ssh-keyscan host | ssh-keygen -lf -` ནས)། སྒྲིག་ན་ལྡེ་མིག་འདི་སྟོན་པའི་ཞབས་ཞུ་ཆས་ཁོ་ན་དང་ལེན་བྱེད། སྟོང་པ་ཡིན་ན་ཐོག་མར་འབྲེལ་སྐབས་མཐོང་བའི་ལྡེ་མིག་དེ་བརྟན་འཛིན་བྱས་ཏེ་རྗེས་ཀྱི་འགྱུར་བ་དང་ལེན་མི་བྱེད།" +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "སྒེར་ལྡེ་མིག་གིས་བདེན་སྦྱོར་བྱེད་ན་སྟོང་པར་བཞག" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "གདམ་རུང་། གསང་ཚིག་གི་ཚབ་ཏུ་ལྡེ་མིག་གིས་བདེན་སྦྱོར་བྱེད་ཆེད OpenSSH སྒེར་ལྡེ་མིག་སྦྱར།" +label = "སྒེར་ལྡེ་མིག" + +[portal.connections.types.slack] +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ལས་སྣོན་བྱེད་སྐབས Slack ལ་བརྡ་འཕྲིན་ཞིག་སྤེལ།" +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS དྲ་རྒྱ་མཉམ་སྤྱོད (Windows ཡང་ན Samba) ལ་ལྟ་ཞིབ་བྱེད།" +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "གདམ་རུང་། ཁྱེད་ཀྱི་ཞབས་ཞུ་ཆས་ཀྱིས་སྤྱོད་ན Windows domain ཡང་ན workgroup།" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་སྒྲིག་སྦྱོར་བྱེད་ཐེངས་རེར Splunk ལ་ཞིབ་རྩིས་བྱུང་རིམ་ཞིག་སྐུར།" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Sumo Logic HTTP བསྡུ་ལེན་པ་ལ་ཞིབ་རྩིས་བྱུང་རིམ་ཞིག་སྐུར།" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "སྲིད་ཇུས་འཁོར་སྐབས Teams channel ལ་བརྡ་འཕྲིན་ཞིག་སྤེལ།" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "ཡིག་ཆ་རེ་རེ་ཡང་ན་ཞིབ་ཕྲ་ཁོ་ན་ཁྱེད་ཀྱིས་འདེམས་པའི URL ལ་སྤེལ།" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "སྲིད་ཇུས་ནས Zap ཡང་ན Make scenario སྐུལ་སློང་བྱེད།" +label = "Zapier & Make" + +[portal.docs] +browse = "ཡིག་ཚགས་ལ་ལྟ།" +viewSource = "GitHub ཐོག source ལ་ལྟ།" + +[portal.docs.authentication] +codeCaption = "རེ་ཞུ་རེ་རེ།" +eyebrow = "འགོ་འཛུགས།" +lead = "རེ་ཞུ་ཡོངས་ཀྱིས bearer token གྱིས་བདེན་སྦྱོར་བྱེད། Key རྣམས་ཁོར་ཡུག་རེ་རེའི་ཁྱབ་ཁོངས་ཡིན་ཞིང་། rotate མ་བྱས་ན་ནམ་ཡང་དུས་ཚོད་མི་ཡོལ།" +liveKey = "Production keys — རིན་འབབ་བསྡུ་བ། ཁྱེད་ཀྱི plan ལྟར rate-limited།" +testKey = "Sandbox keys — རིན་མེད། synthetic fixtures ཕྱིར་སྤྲོད།" +title = "བདེན་སྦྱོར།" + +[portal.docs.components] +codeCaption = "viewer ནང་འཇུག" +eyebrow = "COMPONENTS" +lead = "བསྐྱར་ཞིབ་གྲལ་སྒྲིག་དང་ཡིག་ཆ་ཞིབ་བཤེར་ལ་འཇུག་སྒྲིལ་ཆོག་པའི UI། རང་གི་བཟོ་ལྟ་སྤྱོད་པའམ་མཉམ་སྐྱེལ་བྱས་པའི theme སྤྱོད།" +title = "ཐད་ཀར་སྤྱོད་ཆོག་པའི་ལྟ་ཆས" + +[portal.docs.endpoints] +eyebrow = "API ཟུར་བལྟ" +fieldCount_one = "{{count}} ས་ཁོངས" +fieldCount_other = "{{count}} ས་ཁོངས" +filterAll = "ཚང་མ" +filterAriaLabel = "vertical ལྟར endpoints འཚག" +lead = "ཡིག་ཆའི་རིགས་ཚང་མ་རིགས་ཅན endpoint ཡིན། ཡིག་ཆ POST བྱས་ནས schema ཡིས་ར་སྤྲོད་ཟིན་པའི JSON ལེན། གཤམ་ནས vertical ལྟར་འཚག" +title = "Endpoints" + +[portal.docs.errors] +codeCaption = "422 Unprocessable Entity" +eyebrow = "API ཟུར་བལྟ" +lead = "ནོར་འཁྲུལ་གྱིས་བརྟན་པའི་འཕྲུལ་ཆས་ཀྱིས་ཀློག་ཐུབ་པའི code དང་མིའི་འཕྲིན་ཐུང་ཕྱིར་སྤྲོད་བྱེད། རམ་འདེགས་ཆེད་4xx body ནང་རྟག་ཏུ request_id ཚུད་ཡོད།" +title = "ནོར་འཁྲུལ" + +[portal.docs.nav] +ariaLabel = "ཡིག་ཚགས" + +[portal.docs.nav.empty] +description = "ཡིག་ཚགས་དཀར་ཆག་འགེལ་མ་ཐུབ།" +title = "Docs སྤྱོད་མི་ཐུབ" + +[portal.docs.quickstart] +eyebrow = "འགོ་འཛུགས" +lead = "རང་གི་ཡིག་ཆ་ཐོག་མ་རིགས་ཅན endpoint ལ་སྐུར་ནས་གོམ་པ་གསུམ་ནང་སྒྲིག་འགོད་ཅན JSON ཕྱིར་ལེན། དཔེ་གཞི་སྦྱོང་བརྡར་མེད། prompt engineering མེད།" +title = "མྱུར་འགོ" + +[portal.docs.quickstart.callout] +bodyAfterLink = "ལྟོས།" +bodyBeforeLink = "validation, redaction, delivery བཅས་སྦྲེལ་བར་འབོད་སྐུར་གཅིག་པ་དེ pipeline ནང་མཐུད་པའམ MCP ཐོག agent ལ་མངོན་པར་བྱོས — copy-paste recipes ཆེད་" +label = "རྗེས་མ:" +link = "Playbooks" + +[portal.docs.quickstart.step1] +body = "Infrastructure tab ནས scoped key ཞིག་བཟོས། Keys ལ rate limits དང་གདམ་གསེས་ཅན IP allowlist ཡོད། དེ་རང་གི shell ནང་ཕྱིར་འདྲེན་བྱོས:" +title = "API key སྤྲོད" + +[portal.docs.quickstart.step2] +body = "ཡིག་ཆ་ཞིག་རིགས་ཅན endpoint གང་རུང་ལ POST བྱོས། endpoint ཡིས་ཁྱེད་ལ་ཕྱིར་འཐོབ་པའི schema གཏན་འཁེལ་བྱེད — འདིར་འཛིན་ཤོག་འདོན་ཆས་ཡིན།" +snippetCaption = "འཛིན་ཤོག་འདོན" +title = "ཡིག་ཆ་སྐུར" + +[portal.docs.quickstart.step3] +body = "ལན་ཚང་མ endpoint schema ལ་བསྡུར་ནས་ར་སྤྲོད་བྱེད་པ་དང་། confidence score དང་ས་ཁོངས་རེ་རེའི་ཁུངས་རྗེས་ཡོད།" +codeCaption = "200 OK" +title = "སྒྲིག་འགོད་ཅན་གྲུབ་འབྲས་ཀློག" + +[portal.docs.rateLimits] +burst = "Burst" +codeCaption = "429 Too Many Requests" +concurrency = "Concurrency" +eyebrow = "འགོ་འཛུགས" +lead = "ཚད་བཀག་ཁྱེད་ཀྱི plan དང་མཉམ་དུ་རྒྱ་སྐྱེད། 429 ལན་ནང Retry-After header ཚུད་ཡོད། SDKs རང་འགུལ་གྱིས་ཕྱིར་འཐེན་བྱེད།" +requestsPerMinute = "སྐར་མ་རེའི་རེ་ཞུ" +title = "Rate limits དང quotas" + +[portal.docs.recipes] +cloneButton = "Recipe clone བྱོས" +eyebrow = "PLAYBOOKS" +lead = "འབྱུང་ཁུངས། operations དང་དམིགས་ས་སྦྲེལ་བའི་མགོ་ནས་མཇུག་བར་གྱི patterns། རེ་རེ་ཁྱེད་ཀྱིས clone བྱེད་ཐུབ་པའི pipeline ཞིག་ལ་སྦྱོར།" +title = "Recipes" + +[portal.docs.sdks] +eyebrow = "SDKS" +lead = "typed responses, automatic retries དང streaming uploads ཡོད་པའི first-party clients། ཚང་མས endpoint catalogue གཅིག་པ་རྗེས་འདེད་བྱེད།" +title = "དངོས་གཞི SDKs" + +[portal.docs.sdks.status] +beta = "Beta" +deprecated = "Deprecated" + +[portal.docs.skills] +eyebrow = "SKILLS" +lead = "ཁྱེད་ཀྱི agent གིས་ལག་ཆ་གཅིག་ལྟར་འབོད་སྤྱོད་བྱེད་པའི་མཉམ་སྒྲིལ་དང་མིང་ཅན་ནུས་པ། skill རེ་རེ་ནི evals མཐུད་པའི་གཏན་འཁེལ་ཅན op chain ཡིན།" +title = "Agent skills" + +[portal.docs.toc] +title = "ཤོག་ངོས་འདིར" + +[portal.docs.webhooks] +codeCaption = "document.processed" +eyebrow = "API ཟུར་བལྟ" +lead = "document.processed, pipeline.completed, དང quota.threshold བྱུང་རྐྱེན་ལ subscribe བྱོས། Payloads ལ HMAC-SHA256 ཡིས་མིང་རྟགས་བཀོད་ཡོད།" +title = "Webhooks" + +[portal.docs.webhooks.callout] +afterHelper = "helper མཉམ་སྐྱེལ་བྱེད།" +beforeHelper = " header དེ་ཁྱེད་ཀྱི signing secret དང་བསྡུར་ནས་ར་སྤྲོད་བྱོས། SDKs ཡིས་" +beforeSignature = "payload ལ་ཡིད་མ་ཆེས་གོང་" + +[portal.documents] +exportCsv = "CSV ཕྱིར་འདྲེན" +search = "ཡིག་ཆའི་མིང་། ID... ལྟར་འཚོལ" +subtitle = "ཁྱེད་ཀྱི org གིས་ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ཚང་མ། དེའི་ལས་སྣོན་ཟིན་ཐོ་ཆ་ཚང་དང་བཅས། ནང་དོན་ཐོབ་དབང་ནི་རེ་ཞུ་དགོས་པས་བཀག་ཡོད།" +title = "ཡིག་ཆ" + +[portal.documents.audit] +approved = "ཆོག་མཆན་ཐོབ་པ" +archived = "ཡིག་མཛོད་དུ་བཅུག་པ" +elevation = "ཐོབ་དབང་སྤར་བ" +empty = "ད་དུང་བྱུང་རྐྱེན་ཟིན་ཐོ་མེད།" +extracted = "ལས་སྣོན་ཟིན་པ" +flagged = "བསྐྱར་ཞིབ་དགོས" +ingested = "ནང་འཇུག་ཟིན་པ" +reviewed = "བསྐྱར་ཞིབ་ནང" + +[portal.documents.drawer] +sectionsAriaLabel = "ཡིག་ཆའི་ཞིབ་ཕྲའི་སྡེ་ཚན" + +[portal.documents.drawer.tabs] +audit = "Audit" +extractions = "འདོན་ལེན" +overview = "སྤྱི་བཤད" + +[portal.documents.elevation] +requestAccess = "ཐོབ་དབང་རེ་ཞུ" + +[portal.documents.elevation.active] +description = "གནས་སྐབས་ཆོག་མཆན — ཐོབ་དབང་ཟིན་ཐོར་འགོད་ཅིང་དུས་ཚད་ཡོད།" +descriptionFourEyes = "གནས་སྐབས་ཆོག་མཆན — ལས་རོགས་བསྐྱར་ཞིབ་པར་བརྡ་ཐོ་བཏང་ཟིན (four-eyes)།" +title = "ཐོབ་དབང {{time}} ནང་དུ་དུས་ཡོལ་འགྲོ" + +[portal.documents.elevation.gated] +description = "ནང་དོན་ནི zero-standing-access ཡིས་བཀག་ཡོད། རེ་ཞུ་བྱས་ན་དུས་ཚད་ཅན་གྱི་ཆོག་མཆན་འགོ་འཛུགས།" +descriptionFourEyes = "ནང་དོན་ནི zero-standing-access ཡིས་བཀག་ཡོད། རེ་ཞུ་བྱས་ན་དུས་ཚད་ཅན་གྱི་ཆོག་མཆན་འགོ་འཛུགས་ཤིང་ལས་རོགས་བསྐྱར་ཞིབ་པར་བརྡ་ཐོ་གཏོང (four-eyes)།" +title = "ཚོར་སླའི་ཡིག་ཆ" + +[portal.documents.extractions] +empty = "ཡིག་ཆ་འདི་ནས་ས་ཁོངས་གང་ཡང་འདོན་ལེན་མ་བྱས།" +masked = "འདོན་ལེན་བྱས་པའི་ས་ཁོངས་སྦས་ཡོད། ཡིག་ཆ་འདིའི་ནང་དོན་ལྟ་བར་དུས་ཚད་ཅན་གྱི་ཐོབ་དབང་རེ་ཞུ་བྱོས།" + +[portal.documents.extractions.columns] +confidence = "ཡིད་ཆེས་ཚད" +field = "ས་ཁོངས" +value = "རིན་ཐང" + +[portal.documents.filters] +all = "ཚང་མ" +ariaLabel = "གནས་ཚུལ་ལྟར་ཡིག་ཆ་འཚག" +flagged = "རྟགས་བཀོད་པ" +inReview = "བསྐྱར་ཞིབ་ནང" +processed = "ལས་སྣོན་ཟིན་པ" + +[portal.documents.overview] +action = "Pipeline / བྱ་སྤྱོད" +product = "ཐོན་རྫས" +received = "དུས་ཚོད" +status = "གནས་ཚུལ" +type = "རིགས" +user = "སྤྱོད་མཁན" + +[portal.documents.queue.empty] +connectSource = "འབྱུང་ཁུངས་མཐུད" +createPipeline = "Pipeline བཟོ" +description = "འབྱུང་ཁུངས་ཀྱིས་ཡིག་ཆ་ཁྱེད་ཀྱི pipelines ནང་མཁོ་སྤྲོད་བྱས་ན་དེ་ཚོ་བསྐྱར་ཞིབ་ཆེད་འདིར་མངོན།" +title = "གྲལ་སྒྲིག་ནང་ཡིག་ཆ་མེད" + +[portal.documents.status] +error = "ནོར་འཁྲུལ" +flagged = "བསྐྱར་ཞིབ་དགོས" +inReview = "བསྐྱར་ཞིབ་ནང" +processed = "ལས་སྣོན་ཟིན་པ" + +[portal.documents.table] +auto = "Auto" +editorAction = "Editor" +empty = "ཡིག་ཆ་གང་ཡང་འཚག་ཆས་འདི་དང་མི་མཐུན།" +rowActions = "ཕྲེང་བྱ་སྤྱོད" +sensitiveLabel = "ཚོར་སླ" + +[portal.documents.table.columns] +action = "Pipeline / བྱ་སྤྱོད" +document = "ཡིག་ཆ" +labels = "Labels" +product = "ཐོན་རྫས" +status = "གནས་ཚུལ" +time = "དུས་ཚོད" +user = "སྤྱོད་མཁན" + +[portal.editorAdmin] +subtitle = "Stirling PDF Editor འགྲེམ་སྤེལ་བྱོས། self-hosted instances རང་གི org དང་སྦྱོར། གནས་གཅིག་ནས་འཁོར་བཞིན་པའི fleet — targets, health, credentials དང offline activation སྐྱོང་།" +title = "Editor འགྲེམ་སྤེལ" + +[portal.editorAdmin.health.columns] +activeUsers = "འགུལ་སྐྱོད་སྤྱོད་མཁན" +host = "Host" +lastSeen = "མཐའ་མར་མཐོང་བ" +region = "ས་ཁུལ" +status = "གནས་ཚུལ" +target = "Target" +version = "Version" + +[portal.editorAdmin.health.empty] +description = "Target ཞིག་འགྲེམ་སྤེལ་དང་སྦྱོར་བ་བྱས་ནས་འདིར live instance health ལྟོས།" +title = "ཡར་ཞུ་བྱེད་པའི instances མེད" + +[portal.editorAdmin.offlineActivation] +enterpriseTag = "Enterprise" +generateButton = "Offline bundle བཟོ" +lockCopy = "Offline དང on-prem activation ནི Enterprise ཡི་ཆ་ཤས་ཡིན།" +subtitle = "ཕྱིར་འབྲེལ་དྲ་ལམ་མེད་པའི offline ཡང་ན on-prem install ཆེད་མིང་རྟགས་བཀོད་པའི activation bundle བཟོ། དེ instance ལ་སྤོ་བཤུད་བྱས་ནས first-run setup སྐབས་སྤྱོད།" +talkToSales = "Sales ལ་འབྲེལ་གཏུག" +title = "Air-gapped activation" + +[portal.editorAdmin.offlineActivation.readyBanner] +description = "{{file}} ལ་མིང་རྟགས་བཀོད་ཟིན་ཞིང་སྤོ་བཤུད་བྱེད་རུང་ཡིན། དེས instance གཅིག་སྐུལ་སློང་བྱེད་ཅིང་ཉིན་14 ནང་དུ་དུས་ཡོལ་འགྲོ" +title = "Bundle གྲ་སྒྲིག" + +[portal.editorAdmin.pairing] +generated = "བཟོས་ཟིན ✓" +generateNewCode = "Code གསར་པ་བཟོ" +lockCopy = "IaC provisioning ནི Enterprise ཡི་ཆ་ཤས་ཡིན།" +rotate = "Rotate" +talkToSales = "Sales ལ་འབྲེལ་གཏུག" + +[portal.editorAdmin.sections.health] +sub = "Editor instance ཚང་མས་ཡར་ཞུ་བྱེད — version, region, status, last seen དང active users།" +title = "Instance health" + +[portal.editorAdmin.sections.pairing] +sub = "Self-hosted editor ཞིག org འདི་དང་མཐུད། token བཟོ། code ཐུང་ངུ་སྤྲོད། ཡང་ན IaC བརྒྱུད་མཐུད།" +title = "Pairing" + +[portal.editorAdmin.sections.targets] +sub = "Editor འཁོར་ས། self-hosted instance སློང་བར snippet འདྲ་བཤུས་བྱོས། ཡང་ན ops མེད་པའི Managed Cloud སྤྱོད།" +title = "Deployment targets" + +[portal.editorAdmin.serviceToken] +currentToken = "ད་ལྟའི token" +lastRotated = "མཐའ་མཇུག་ཏུ་བསྐོར་བ" +rotateButton = "ཞབས་ཞུའི་མཚོན་རྟགས་བསྐོར་བ" +subtitle = "དཔེ་གཞི་ཁག་གིས་དཔང་ཡིག་འདི་སྤྱད་ནས་རྩ་འཛུགས་ལ་ཐོབ་ཐང་ར་སྤྲོད་བྱེད། དུས་བཀག་ལྟར་བསྐོར་བའམ། ཕྱིར་ཤོར་བ་དོགས་པའི་རྗེས་སུ་འཕྲལ་མར་བསྐོར་བྱོས།" +title = "ཞབས་ཞུའི་མཚོན་རྟགས" + +[portal.editorAdmin.serviceToken.rotatedBanner] +description = "མཚོན་རྟགས་གསར་པ་སྤྲད་ཟིན། ཆུ་ཚོད་24ཡི་བར་ཡངས་ནང་རང་གིས་གནས་སྡོད་བྱས་པའི་དཔེ་གཞི་རེ་རེའི STIRLING_SERVICE_TOKEN གསར་སྒྱུར་བྱོས། དེ་མིན་ན་དེ་དག་དྲ་ཕྱིར་ལྷུང་སྲིད།" +title = "འཁོར་སྐྱོད་ཀྱི་དཔེ་གཞི་བསྐོར་བ" + +[portal.editorAdmin.status] +degraded = "སྤུས་ཚད་ཉམས་པ" +healthy = "བདེ་ཐང" +offline = "དྲ་ཕྱི" +pairing = "ཆ་སྒྲིག་བཞིན" + +[portal.editorAdmin.targets] +instanceCount_one = "{{count}} དཔེ་གཞི" +instanceCount_other = "{{count}} དཔེ་གཞི" +talkToSales = "ཚོང་འཚོང་ཚན་པར་འབྲེལ་བ་བྱོས" +upgradePlan = "འཆར་གཞི་རིམ་སྤར" + +[portal.editorAdmin.targets.lock] +enterprise = "On-prem དང Kubernetes རང་གིས་གནས་སྡོད་བྱེད་པ་ནི Enterprise ཡི་ཆ་ཤས་ཡིན།" +paid = "Docker དང Kubernetes སྤྱད་པའི་རང་གིས་གནས་སྡོད་བྱེད་པ་ནི་སྤྲོད་དངུལ་འཆར་གཞི་ཐོག་ནས་ཁ་ཕྱེ་ཐུབ།" + +[portal.editorAdmin.targets.state] +available = "སྤྱོད་རུང" +locked = "བསྡམས་པ" +running = "འཁོར་སྐྱོད་བཞིན" + +[portal.errorBoundary] +description = "མཐོང་ངོས་འདིར་མ་བསམ་པའི་ནོར་འཁྲུལ་ཞིག་བྱུང་། ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས། ཡང་ན་ཟུར་གྱི་དཀར་ཆག་ནས་སྡེ་ཚན་གཞན་ཞིག་འདེམས།" +retry = "ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས" +title = "ཤོག་ངོས་འདིར་གནད་དོན་ཞིག་བྱུང་།" + +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "ཕྱིར་ལོག" +body = "ཅོག་ངོས་ཉེར་སྤྱོད་ཕབ་ལེན་བྱོས། ཡང་ན server རང་གིས་གནས་སྡོད་བྱོས།" +done = "ལེགས་གྲུབ" +guide = "{{name}} ལམ་སྟོན་ཁ་ཕྱེ" +openInBrowser = "དེའི་ཚབ་ཏུ་བཤེར་ཆས་ནང་ཁ་ཕྱེ" +sectionDesktop = "ཅོག་ངོས་ཉེར་སྤྱོད" +sectionSelfHosted = "རང་གིས་གནས་སྡོད" +title = "Stirling PDF Editor སྒྲིག་འཇུག" + +[portal.home.download.docker] +detailBody = "ཁྱེད་ཀྱི server ཐོག་དངོས་སུ་འགྲེམས་པའི image འཁོར་སྐྱོད་བྱོས།" +note = " :8080 ལ་ཁ་ཕྱེ། སྔོན་སྒྲིག་ནང་འཛུལ: admin / stirling." +tagline = "server དེ container ནང་འཁོར་སྐྱོད་བྱོས" +title = "Docker" +variantFat = "ཆེན་པོ" +variantLite = "ཤིན་ཏུ་ཡང་མོ" +variantStandard = "ཚད་ལྡན" + +[portal.home.download.kubernetes] +detailBody = "དངོས་སུ་འགྲེམས་པའི Helm chart སྒྲིག་འཇུག་བྱོས།" +note = "སྒྲིག་འགོད་རྒྱུན་ཉར་བྱེད་པར /configs ལ PVC ཁ་སྣོན་བྱོས།" +tagline = "Helm སྤྱད་ནས cluster ལ་འགྲེམས་སྤེལ་བྱོས" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "ཁྱེད་ཀྱི distribution ཆེད་ཀྱི native builds།" +guideBtn = "Linux སྒྲིག་འཇུག་གདམ་གསེས་ལྟ་བ" +note = "སྤྱིར་བཏང distros ལ AppImage, .deb དང .rpm builds ཡོད།" +tagline = "ཁྱེད་ཀྱི distro ཆེད་ཀྱི native desktop app" +title = "Linux" + +[portal.home.download.mac] +altLabel = "ཡང་ན Homebrew བརྒྱུད་ནས" +detailBody = "installer འཁོར་སྐྱོད་བྱོས། ཡང་ན Homebrew སྤྱོད།" +downloadBtn = "macOS ཆེད་ཕབ་ལེན" +tagline = "Apple silicon དང Intel ཆེད་ཀྱི native app" +title = "macOS" + +[portal.home.download.manual] +detailBody = "bare metal server ཐོག .jar འཁོར་སྐྱོད་བྱོས།" +downloadBtn = ".jar ཕབ་ལེན" +note = "Java 21+ དགོས། :8080 ལ་ཁ་ཕྱེ། སྔོན་སྒྲིག་ནང་འཛུལ admin / stirling།" +runLabel = "དེ་ནས་འཁོར་སྐྱོད་བྱོས" +tagline = "bare metal server ཐོག .jar འཁོར་སྐྱོད་བྱོས" +title = "ལག་སྒྲིག server སྒྲིག་འགོད" + +[portal.home.download.windows] +altLabel = "ཡང་ན winget བརྒྱུད་ནས" +detailBody = "installer འཁོར་སྐྱོད་བྱོས། ཡང་ན package manager སྤྱོད།" +downloadBtn = "installer ཕབ་ལེན" +tagline = "Microsoft Windows ཆེད་ཀྱི native app" +title = "Windows" + +[portal.home.editor] +activeOfDeployed = "ཟླ་འདིར {{total}} ནས {{active}} སྤྱོད་བཞིན" +name = "PDF Editor" +open = "བཤེར་ཆས་ནང་ཁ་ཕྱེ" +updated = "{{time}} ལ་གསར་སྒྱུར་བྱས" + +[portal.home.editor.deploy] +finish = "འགྲེམས་སྤེལ་མཇུག་སྒྲིལ" +options = "འགྲེམས་སྤེལ་གདམ་གསེས་ལྟ་བ" +start = "Editor འགྲེམས་སྤེལ་བྱོས" + +[portal.home.editor.target] +cloud = "དོ་དམ་བྱས་པའི Cloud" +docker = "རང་གིས་གནས་སྡོད · Docker" +kubernetes = "རང་གིས་གནས་སྡོད · Kubernetes" + +[portal.home.greeting] +afternoon = "ཉིན་གུང་བདེ་ལེགས" +evening = "དགོང་དྲོ་བདེ་ལེགས" +morning = "སྔ་དྲོ་བདེ་ལེགས" + +[portal.infrastructure] +manageEditorDeployment = "Editor འགྲེམས་སྤེལ་དོ་དམ" +sectionsAriaLabel = "རྨང་གཞི་སྒྲིག་བཀོད་སྡེ་ཚན" +subtitle = "ཁྱེད་ཀྱི Stirling ལས་ཁུལ་གྱི་འགྲེམས་སྤེལ། དཔང་ཡིག བདེ་འཇགས་གནས་བབ། གསོག་ཉར། དང་ཞིབ་བཤེར་རྗེས་ཤུལ།" +title = "རྨང་གཞི་སྒྲིག་བཀོད" + +[portal.infrastructure.apiKeys] +createKey = "ལྡེ་མིག་གསར་བཟོ" +heading = "API ལྡེ་མིག" +subheading = "རེ་རེར་རང་གི་སྤྱོད་ཚད་རྗེས་འདེད་ཡོད་པའི་མི་སྒེར་གྱི་དཔང་ཡིག" + +[portal.infrastructure.apiKeys.card] +created = "བཟོས་པ" +lastUsed = "མཐའ་མཇུག་སྤྱད་པ" +revoke = "ལྡེ་མིག་ཕྱིར་འཐེན" +usageMonth = "ཟླ་འདིའི་སྤྱོད་ཚད" +usageToday = "དེ་རིང་གི་སྤྱོད་ཚད" + +[portal.infrastructure.apiKeys.empty] +description = "Stirling API འབོད་འགོ་འཛུགས་པར་ལྡེ་མིག་ཅིག་གསར་བཟོ་བྱོས།" +title = "ད་དུང API ལྡེ་མིག་མེད" + +[portal.infrastructure.apiKeys.error] +load = "ཁྱེད་ཀྱི API ལྡེ་མིག་འཇུག་མ་ཐུབ། ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" ཕྱིར་འཐེན་ནམ? ལྡེ་མིག་འདི་ད་དུང་སྤྱོད་བཞིན་པའི integration གང་ཡིན་ཡང་འཕྲལ་མར 401 ནོར་འཁྲུལ་འབྱོར་འགོ་འཛུགས་སྲིད། འདི་ཕྱིར་སློག་མི་ཐུབ།" +cancel = "དོར་བ" +confirm = "ལྡེ་མིག་ཕྱིར་འཐེན" +title = "API ལྡེ་མིག་ཕྱིར་འཐེན" + +[portal.infrastructure.audit] +filterAriaLabel = "རིགས་དབྱེ་ལྟར་ཞིབ་བཤེར་དོན་རྐྱེན་འཚག་བྱོས" +heading = "ཞིབ་བཤེར་ཉིན་ཐོ" +latencyValue = "{{value}} ms" +noEventsInCategory = "རིགས་དབྱེ་འདིར་དོན་རྐྱེན་མེད།" +subheading = "ཁྱེད་ཀྱི་ལས་ཁུལ་ཡོངས་ཀྱི་ཐོབ་ཐང་ར་སྤྲོད། སྒྲིག་འགོད། དང་ལས་སྒྲུབ་དོན་རྐྱེན་ཚང་མ།" + +[portal.infrastructure.audit.columns] +actor = "བྱེད་པ་པོ" +event = "དོན་རྐྱེན" +latency = "དུས་འགྱངས" +status = "གནས་ཚུལ" +target = "ཐོན་ཁུངས" +timestamp = "དུས་ཐམ" + +[portal.infrastructure.audit.empty] +description = "ལས་ཁུལ་གྱི་བྱེད་སྒོ་བྱུང་བ་དང་ལྷན་འདིར་མངོན་ཡོང་།" +title = "ཞིབ་བཤེར་དོན་རྐྱེན་མེད" + +[portal.infrastructure.audit.export] +cancel = "དོར་བ" +error = "ཕྱིར་འདྲེན་ཕམ་པ། ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" +exportButton = "ཕྱིར་འདྲེན" +exporting = "ཕྱིར་འདྲེན་བཞིན…" +fieldsLegend = "ཀ་ཐིག" +format = "Format" +open = "ཕྱིར་འདྲེན" +subtitle = "ཞིབ་བཤེར་དོན་རྐྱེན CSV ཡང་ན JSON དུ་ཕབ་ལེན་བྱོས། ཚུད་དགོས་པའི་ཀ་ཐིག་འདེམས།" +title = "ཞིབ་བཤེར་ཉིན་ཐོ་ཕྱིར་འདྲེན" + +[portal.infrastructure.audit.export.fields] +date = "ཚེས་གྲངས" +documentName = "ཡིག་ཆའི་མིང" +ipAddress = "IP address" +outcome = "མཇུག་འབྲས" +tool = "Tool" +username = "སྤྱོད་མཁན་མིང" + +[portal.infrastructure.audit.filters] +all = "ཚང་མ" +auth = "ཐོབ་ཐང་ར་སྤྲོད" +config = "སྒྲིག་འགོད" +elevation = "དབང་ཚད་སྤར་བ" +policy = "སྲིད་ཇུས" +processing = "ལས་སྒྲུབ" +security = "བདེ་འཇགས" + +[portal.infrastructure.audit.forbidden] +description = "ཞིབ་བཤེར་ཉིན་ཐོ་ནི་རུ་ཁག་འགོ་ཁྲིད་དང admin ལ་སྤྱོད་རུང་། ཐོབ་ཐང་ཆེད་ཁྱེད་ཀྱི་རུ་ཁག་འགོ་ཁྲིད་དམ admin ལ་ཞུས།" +title = "ཁྱེད་ལ་སྤྱོད་ཐོབ་མེད" + +[portal.infrastructure.audit.metrics] +config = "སྒྲིག་འགོད" +policy = "སྲིད་ཇུས" +processing = "ལས་སྒྲུབ" +totalEvents = "དོན་རྐྱེན་སྤྱི་བསྡོམས · 24h" + +[portal.infrastructure.auditCatLabel] +auth = "ཐོབ་ཐང་ར་སྤྲོད" +config = "སྒྲིག་འགོད" +elevation = "དབང་ཚད་སྤར་བ" +policy = "སྲིད་ཇུས" +processing = "ལས་སྒྲུབ" +security = "བདེ་འཇགས" + +[portal.infrastructure.auditStatusLabel] +danger = "ནོར་འཁྲུལ" +info = "ཆ་འཕྲིན" +success = "ལེགས་གྲུབ" +warning = "ཉེན་བརྡ" + +[portal.infrastructure.createKey] +cancel = "དོར་བ" +createKey = "ལྡེ་མིག་གསར་སྐྲུན" +done = "ལེགས་གྲུབ" +keyNameLabel = "ལྡེ་མིག་གི་མིང་" +keyNamePlaceholder = "དཔེར་ན། ཐོན་སྐྱེད་ནང་འཇུག" +secretKeyCaption = "གསང་བའི་ལྡེ་མིག" +secretWarning = "འདི་གསང་བའི་དོ་དམ་ཆས་ནང་ཉར་ཚགས་བྱོས། Stirling གིས་ནམ་ཡང་ཧེ་ཤ་ཁོ་ན་ཉར་བས། རྗེས་སུ་དེ་སླར་གསོ་བྱེད་ཐབས་མེད།" +subtitle = "རྗེས་སུ་ངོས་འཛིན་ཐུབ་པའི་ཆེད་ལྡེ་མིག་ལ་མིང་ཞིག་སྤྲོད། ཁྱེད་ཀྱིས་དུས་ནམ་ཡང་དེ་ཕྱིར་འཐེན་བྱེད་ཆོག" +subtitleCreated = "ད་ལྟ་གསང་བ་འདི་འདྲ་བཤུས་བྱོས — འདི་ཡང་བསྐྱར་མི་སྟོན།" +title = "API ལྡེ་མིག་གསར་སྐྲུན" +titleCreated = "ལྡེ་མིག་གསར་སྐྲུན་ཟིན་པ" + +[portal.infrastructure.keyLabel] +active = "ཤུགས་ལྡན" +revoked = "ཕྱིར་འཐེན་བྱས་པ" + +[portal.infrastructure.tabs] +apiKeys = "API ལྡེ་མིག" +audit = "རྩིས་བཤེར་ཉིན་ཐོ" +deployments = "སྤེལ་འཇོག" +models = "དཔེ་དབྱིབས" +security = "བདེ་འཇགས" +storage = "གསོག་ཉར" + +[portal.integrations] +availableHeading = "སྤྱོད་རུང" +comingSoonHeading = "མི་འགྱངས་པར་འཐོན་རྒྱུ" +connect = "འབྲེལ་མཐུད" +connectionCount_one = "{{count}} འབྲེལ་མཐུད" +connectionCount_other = "{{count}} འབྲེལ་མཐུད" +customApi = "རང་སྒྲིག API" +searchPlaceholder = "མཉམ་འདྲེས་འཚོལ" +subtitle = "Stirling གིས་འབྲེལ་བ་བྱེད་ཐུབ་པ་ཚང་མ། དཔང་ཡིག་འདིར་ཐེངས་གཅིག་བཟོས་རྗེས། ཁུངས། སྲིད་ཇུས། ལས་རིམ་རྒྱུན་བཅས་ཀྱིས་དེ་ལ་གཞི་འཛིན་བྱེད།" +title = "མཉམ་འདྲེས" +typedName = "{{tool}} མིང" + +[portal.integrations.filters] +all = "ཚང་མ" +audit = "རྩིས་བཤེར" +connected = "འབྲེལ་མཐུད་ཟིན" +notify = "བརྡ་ཐོ" +security = "བདེ་འཇགས" +signing = "མིང་རྟགས་འགོད་པ" +storage = "གསོག་ཉར" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "འབྲེལ་མཐུད་ཟིན" + +[portal.integrations.table] +integration = "མཉམ་འདྲེས" +worksWith = "མཉམ་དུ་ལས་ཀ་བྱེད་པ" + +[portal.integrations.worksWith] +pipelines = "ལས་རིམ་རྒྱུན" +policies = "སྲིད་ཇུས" +sources = "ཁུངས" + +[portal.legal] +draft = "{{label}} · ཟིན་བྲིས" +loadError = "ཡིག་ཆ་འདི་འཇུག་མ་ཐུབ། ཡང་བསྐྱར་ཚོད་ལྟ་གནང་རོགས།" +loading = "འཇུག་བཞིན…" +title = "ཁྲིམས་འབྲེལ་ཡིག་ཆ" + +[portal.nav] +agent-builder = "ལས་ཚབ་བཟོ་ཆས" +components = "གྲུབ་ཆ" +docs = "ཡིག་ཚགས" +documents = "ཡིག་ཆ" +editor = "རྩོམ་སྒྲིག་ཆས" +home = "གཙོ་ངོས" +infrastructure = "གཞི་རྟེན་སྒྲིག་བཀོད" +integrations = "མཉམ་འདྲེས" +pipelines = "ལས་རིམ་རྒྱུན" +policies = "སྲིད་ཇུས" +procurement = "ཉོ་སྒྲུབ" +sources = "ཁུངས" +usage = "བེད་སྤྱོད་དང་རྩིས་འབོར" +users = "སྤྱོད་མཁན" + +[portal.nav.section] +platform = "PDF སྟེགས་བུ" +processor = "PDF སྒྲིག་གཅོད་ཆས" + +[portal.pipelines] +subtitle = "རྒྱབ་སྣེའི་ཡིག་ཆ་རང་འགུལ་ལས་རིམ་རྒྱུན་ཚང་མ། སྐུལ་རྐྱེན་སྟེང་འཁོར་བའི་ཁུངས་ཚོགས་ཤིག་སྟེང་གི་བཀོལ་སྤྱོད་རིམ་པ་ཅན་གྱི་ཕྲེང་བ་ཞིག དེའི་གོམ་པ་དང་ཁུངས་ལ་ལྟ་ཕྱིར་ཕྲེང་ཐིག་ཅིག་ལ་གནོན།" +title = "ལས་རིམ་རྒྱུན" + +[portal.pipelines.actions] +newPipeline = "ལས་རིམ་རྒྱུན་གསར་པ" + +[portal.pipelines.builder] +activate = "Activate" +back = "ལས་རིམ་རྒྱུན་ལ་ཕྱིར་ལོག" +cannotFollow = "{{produced}} ལེན་མི་ཐུབ" +chooseAccount = "རྩིས་ཐོ་ཞིག་འདེམས" +chooseDestination = "དམིགས་ས་ཞིག་འདེམས" +chooseOperation = "གོམ་པ་འདིས་ཅི་བྱེད་འདེམས" +chooseSource = "ཁུངས་ཤིག་འདེམས" +discard = "འགྱུར་བ་དོར" +inputs = "ནང་འཇུག" +inputSource = "ནང་འཇུག་ཁུངས" +inputTrigger = "སྐུལ་རྐྱེན" +keepEditing = "རྩོམ་སྒྲིག་མུ་མཐུད" +moreActions = "More actions" +needsConfiguring = "སྒྲིག་སྦྱོར་དགོས" +needsDestination = "No destination chosen" +needsSource = "No source chosen" +noToolMatches = "ཁྱེད་ཀྱི་འཚོལ་བཤེར་དང་མཐུན་པའི་ལག་ཆ་མེད།" +pause = "Pause" +rename = "Rename pipeline" +searchTools = "ལག་ཆ་འཚོལ" +sendToSystem = "མ་ལག་གཞན་ཞིག་ལ་སྐུར" +stepsIncompatible = "གོམ་པ་འདི་དག་དེའི་གོམ་པ་སྔོན་མས་བཟོས་པའི་ཐོན་འབྲས་སྟེང་འཁོར་མི་ཐུབ། {{tools}}" +stepsNeedSetup = "གོམ་པ་འདི་དག་ཉར་ཚགས་མ་བྱས་གོང་ད་དུང་སྒྲིག་སྦྱོར་དགོས། {{tools}}" +supportingFiles = "Supporting files" +testRun = "Test with a file" +unknownStep = "ངོས་འཛིན་མ་ཐུབ་པའི་བཀོལ་སྤྱོད། རང་འཇགས་ཉར་ཡོད།" +unsavedBody = "ཁྱེད་ལ་ཉར་ཚགས་མ་བྱས་པའི་འགྱུར་བ་ཡོད། ཕྱིར་མ་ཐོན་གོང་དེ་དག་ཉར་ཚགས་བྱེད་དམ། ཡང་ན་དོར་རམ།" +unsavedTitle = "ཉར་ཚགས་མ་བྱས་པའི་འགྱུར་བ" +usesDefaults = "སྔོན་སྒྲིག་སྒྲིག་འགོད་ཀྱིས་འཁོར" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "གོམ་པ་སྔོན་མ་ནས་ཡིག་ཆ་ཚང་མ་ཟླ་སྒྲིལ་བྱེད" +fan-out = "གོམ་པ་སྔོན་མ་ནས་ཡིག་ཆ་རེ་རེར་ཐེངས་རེ་འཁོར" +format-mismatch = "{{accepts}} དགོས། འོན་ཀྱང་གོམ་པ་སྔོན་མས་{{produced}} བཟོ་བཞིན་ཡོད" +output-uncertain = "འཁོར་མི་སྲིད། གོམ་པ་སྔོན་མའི་ཐོན་འབྲས་ནི་དེ་ཇི་ལྟར་སྒྲིག་པར་རག་ལས" +source-mismatch = "{{accepts}} དགོས། འོན་ཀྱང་ལས་རིམ་རྒྱུན་འདིའི་ནང་འཇུག་ནི་{{produced}} ཡིན" +undeclared-operation = "གོམ་པ་འདིས་གང་ལེན་ཐུབ་མིན་ཞིབ་བཤེར་བྱེད་མི་ཐུབ" + +[portal.pipelines.composer] +addTool = "ལག་ཆ་ཁ་སྣོན" +create = "ལས་རིམ་རྒྱུན་བཟོ" +createPaused = "Create paused" +editingUnsupported = "རྩོམ་སྒྲིག་ཆེད་ལག་ཆ་འདི་དག་གི་ཞུགས་གྲངས་སྟོན་པ་ད་དུང་རྒྱབ་སྐྱོར་མེད།" +editSource = "Edit source" +name = "མིང" +namePlaceholder = "དཔེར་ན། ནག་སུབ་བཤེར་བ" +noToolSettings = "ལག་ཆ་འདིར་སྒྲིག་སྦྱོར་བྱ་རུང་གི་སྒྲིག་འགོད་མེད།" +output = "དམིགས་ས" +save = "འགྱུར་བ་ཉར་ཚགས" +scheduleEvery = "འཁོར་ཚད" +trigger = "སྐུལ་རྐྱེན" +triggerManual = "ལག་བཟོ་ཁོ་ན" + +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + +[portal.pipelines.composer.unit] +days = "ཉིན" +hours = "ཆུ་ཚོད" +minutes = "སྐར་མ" + +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + +[portal.pipelines.delete] +body = "\"{{name}}\" སུབ་བམ། འདི་ཕྱིར་ལྡོག་བྱེད་མི་ཐུབ།" +cancel = "དོར" +confirm = "སུབ" +title = "ལས་རིམ་རྒྱུན་སུབ་བམ།" + +[portal.pipelines.detail] +clearHistory = "ལོ་རྒྱུས་གཙང་སེལ" +delete = "ལས་རིམ་རྒྱུན་སུབ" +run = "ད་ལྟ་འཁོར" + +[portal.pipelines.empty] +action = "ལས་རིམ་རྒྱུན་ཞིག་བཟོ" +connectSource = "ཁུངས་ཤིག་འབྲེལ་མཐུད" +description = "ཁྱེད་ཀྱི་ལས་རིམ་རྒྱུན་དང་པོ་བཟོ། དེ་འཁོར་སའི་ཁུངས་འདེམས། བཀོལ་སྤྱོད་ཕྲེང་བར་སྦྲེལ། དེ་ནས་ཐོན་འབྲས་གང་དུ་འགྲོ་བ་འདེམས།" +title = "ད་དུང་ལས་རིམ་རྒྱུན་མེད" + +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + +[portal.pipelines.kpi] +active = "འཁོར་བཞིན" +paused = "མཚམས་བཞག" +total = "ལས་རིམ་རྒྱུན" + +[portal.pipelines.run] +allProcessed_one = "འཁོར་རྒྱུ་མེད། ཁུངས་ཀྱི་ཡིག་ཆ་{{count}} སྔོན་ནས་སྒྲིག་གཅོད་ཟིན།" +allProcessed_other = "འཁོར་རྒྱུ་མེད། ཁུངས་ནང་གི་ཡིག་ཆ་{{count}} ཚང་མ་སྔོན་ནས་སྒྲིག་གཅོད་ཟིན།" +completed_one = "འཁོར་བ་ལེགས་གྲུབ།" +completed_other = "འཁོར་བ་{{count}} ཚང་མ་ལེགས་གྲུབ།" +empty = "འཁོར་རྒྱུ་མེད། ཁུངས་ནང་སྒྲིག་གཅོད་བྱ་རྒྱུའི་ཡིག་ཆ་མེད།" +failed = "འཁོར་བ་ཕམ་སོང་། {{error}}" +inFlight = "འཁོར་རྒྱུ་གསར་པ་མེད། སྔོན་གྱི་འཁོར་ཐེངས་ཤིག་ནས་ཡིག་ཆ་ད་དུང་སྒྲིག་གཅོད་བྱེད་བཞིན་ཡོད།" +parked_one = "འཁོར་རྒྱུ་མེད། ཡིག་ཆ་{{count}} སྔོན་མར་ཕམ་སོང་ནས་ཟུར་དུ་བཞག་ཡོད། རྒྱུ་རྐྱེན་བཅོས་རྗེས། ཡང་བསྐྱར་ཚོད་ལྟ་བྱ་ཕྱིར་ལོ་རྒྱུས་གཙང་སེལ་བྱོས།" +parked_other = "འཁོར་རྒྱུ་མེད། ཡིག་ཆ་{{count}} སྔོན་མར་ཕམ་སོང་ནས་ཟུར་དུ་བཞག་ཡོད། རྒྱུ་རྐྱེན་བཅོས་རྗེས། ཡང་བསྐྱར་ཚོད་ལྟ་བྱ་ཕྱིར་ལོ་རྒྱུས་གཙང་སེལ་བྱོས།" +running = "འཁོར་བ་འགོ་ཚུགས་ཟིན། ད་དུང་འགྲོ་བཞིན་ཡོད།" +timeout = "འཁོར་བར་སྔོན་དཔག་ལས་དུས་ཚོད་རིང་བ་འགོར་བཞིན་ཡོད། དེ་རྒྱབ་ལྗོངས་སུ་མུ་མཐུད་མཇུག་སྒྲིལ་སྲིད།" + +[portal.pipelines.status] +active = "འཁོར་བཞིན" +paused = "མཚམས་བཞག" + +[portal.pipelines.table] +name = "ལས་རིམ་རྒྱུན" +sources = "ཁུངས" +status = "གནས་ཚུལ" +steps = "གོམ་པ" +trigger = "Trigger" + +[portal.pipelines.trigger] +folder-watch = "ཡིག་ཁུག་ལྟ་སྲུང" +manual = "ལག་སྒུལ" +schedule = "དུས་ཚོད་བཀོད་པ" + +[portal.policies] +defaultName = "{{category}} སྲིད་ཇུས" +subtitle = "ཡིག་ཆ་རེ་རེའི་ཐོག་ལག་ཆའི་རྒྱུད་རིམ་བཙན་སྐུལ་ལག་བསྟར་བྱེད་པའི་རྒྱུན་གནས་རང་འགུལ་ལས་རིམ། སྲིད་ཇུས་རེ་རེ་ཡར་སྐུར་རམ་ཕྱིར་འདོན་སྐབས་འགོ་སློང་བྱེད་ཅིང་། ལག་ཆའི་རྒྱུད་རིམ་སྒྲུབ་ནས་བཙན་སྐུལ་བྱས་པའི་པར་གཞི་དེ་མ་ཡིག་དང་མཉམ་དུ་ཉར་ཚགས་བྱེད།" +title = "སྲིད་ཇུས" + +[portal.policies.card] +comingSoon = "Enterprise ལ་རིམ་སྤར་བྱེད" +notSetUp = "སྒྲིག་འགོད་བྱས་མེད" +requiresAiEngine = "AI engine དགོས" + +[portal.policies.categories.classification] +desc = "ཡར་སྐུར་སྐབས་ཡིག་ཆ་རེ་རེའི་རིགས་ངོས་འཛིན་བྱས་ཏེ། ཡིག་ཚགས་དང་འཚོལ་ཞིབ་ཆེད་དེའི་མེ་ཊ་གཞི་གྲངས་ལ་རྟགས་འདེབས་བྱེད།" +label = "རིགས་དབྱེ" + +[portal.policies.categories.compliance] +desc = "ཡིག་ཆ་རེ་རེའི་ཐོག HIPAA, GDPR, SOC 2, ཡང་ན FedRAMP དགོས་མཁོ་བཙན་སྐུལ་ལག་བསྟར་བྱེད།" +label = "ཚད་མཐུན" + +[portal.policies.categories.ingestion] +desc = "ཡིག་ཆ་རིགས་དབྱེ་བྱེད། སྒྲིག་ཡོད་གཞི་གྲངས་འདོན། མིང་འདོགས་སྲོལ་བཙན་སྐུལ་བྱེད། ཤོག་ངོས་ཚད་ལྡན་བཟོ།" +label = "ནང་འཇུག" + +[portal.policies.categories.retention] +desc = "ཡིག་ཆ་ཇི་ཙམ་ཉར་དགོས་པ། ག་དུས་ཡིག་ཚགས་སུ་ཉར་དགོས་པ། ག་དུས་སུབ་དགོས་པ་སྒྲིག" +label = "ཉར་ཚགས" + +[portal.policies.categories.routing] +desc = "ཡིག་ཆ་རང་འགུལ་གྱིས་ཚོགས་པ། ཡིག་ཁུག ཡང་ན་མ་ལག་འོས་འཚམ་ལ་སྐྱེལ།" +label = "ལམ་སྟོན" + +[portal.policies.categories.security] +desc = "PII འཚོལ་རྙེད་བྱེད། སྦས་འགེབས་བྱེད། འགུལ་སྐྱོད་ནང་དོན་སེལ། ཡིག་ཆ་ལ་ཆུ་རྟགས་འཇོག" +label = "བདེ་འཇགས" + +[portal.policies.config] +scopeAll = "ཡིག་ཆ་ཚང་མ" + +[portal.policies.config.classification] +summary = "ཡར་སྐུར་བྱས་པའི་ཡིག་ཆ་ཚང་མ་ཁྱེད་ཀྱི་ཚོགས་པའི་ཤོག་བྱང་དང་བསྡུར་ནས་རིགས་དབྱེ་བྱེད་ཅིང་། དེའི་མེ་ཊ་གཞི་གྲངས་ལ་རྟགས་འདེབས་བྱེད།" + +[portal.policies.config.classification.rules] +0 = "རིགས་དབྱེ" +1 = "མེ་ཊ་གཞི་གྲངས་ལ་རྟགས་འདེབས" + +[portal.policies.config.compliance] +summary = "ཡིག་ཆ་མ་ལག་ནས་མ་ཐོན་གོང་སྒྲིག་ཁྲིམས་སྒྲོམ་གཞི་དང་བསྡུར་ནས་ར་སྤྲོད་བྱེད།" + +[portal.policies.config.compliance.fields] +accessLog = "འཛུལ་སྤྱོད་ཟིན་ཐོ" +auditTrail = "རྩིས་ཞིབ་ཤུལ་ལམ" +frameworks = "སྒྲོམ་གཞི" +onViolation = "ཚད་མི་མཐུན་སྐབས" + +[portal.policies.config.compliance.rules] +0 = "སྒྲོམ་གཞི་བཤེར་འབེབས" +1 = "བྱ་ཐབས་བཙན་སྐུལ" +2 = "རྩིས་ཞིབ་ཤུལ་ལམ" + +[portal.policies.config.ingestion] +summary = "ཡིག་ཆ་རིགས་དབྱེ་བྱེད། སྒྲིག་ཡོད་གཞི་གྲངས་འདོན། མིང་འདོགས་བཙན་སྐུལ་བྱེད། ཤོག་ངོས་ཚད་ལྡན་བཟོ།" + +[portal.policies.config.ingestion.fields] +belowThreshold = "མཚམས་ཐང་ལས་དམའ་བ" +minConfidence = "ཡིད་ཆེས་ཉུང་མཐའ" + +[portal.policies.config.ingestion.rules] +0 = "རིགས་དབྱེ" +1 = "འདོན" +2 = "མིང་འདོགས" +3 = "ཚད་ལྡན་བཟོ" + +[portal.policies.config.purview] +label = "Microsoft Purview ཚོར་སྐྱེན་ཤོག་བྱང་སྦྱར" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "Purview portal ནས་ཐོབ་པའི་ཤོག་བྱང་གི GUID། ཤོག་བྱང་སྦྱར་བས་དེའི་མེ་ཊ་གཞི་གྲངས་PDF ཐོག་འབྲི་བ་ཡིན། དེས་གསང་སྡོམ་བྱེད་མི་ཐུབ། དེ་ལ Microsoft client དགོས།" +labelName = "ཤོག་བྱང་མིང་། (འདམ་ག)" +labelNameHelp = "མི་དང་ལག་ཆས་ཤོག་བྱང་ངོས་འཛིན་ཐུབ་པའི་ཆེད ID དང་མཉམ་དུ་ཐོ་འགོད་བྱེད།" + +[portal.policies.config.purview.fields.method] +help = "Purview ཡིས་སྲིད་ཇུས་ཀྱིས་སྦྱར་བའི་ཤོག་བྱང་དང་མི་ཞིག་གིས་འདམས་པའི་ཤོག་བྱང་གཉིས་དབྱེ་འབྱེད་བྱེད།" +label = "ཤོག་བྱང་ཇི་ལྟར་ཐོ་འགོད་བྱེད" +privileged = "མི་ཞིག་གིས་འདམས་པ་ལྟར" +standard = "རང་འགུལ་གྱིས་སྦྱར་བ་ལྟར" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview ཤོག་བྱང་ཀློག" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "ཡིག་ཆ་ལ་སྔོན་ནས་ཡོད་པའི་ཤོག་བྱང་ཀློག་སྟེ། རྗེས་ཀྱི་གོམ་པས་དེའི་ཐོག་བྱ་སྤྱོད་བྱེད་ཐུབ།" + +[portal.policies.config.retention] +summary = "ཡིག་ཆ་ཇི་ཙམ་ཉར་དགོས་པ། ག་དུས་ཡིག་ཚགས་སུ་ཉར་དགོས་པ། ག་དུས་སུབ་དགོས་པ་བཙན་སྐུལ་བྱེད།" + +[portal.policies.config.retention.fields] +archiveAfter = "རྗེས་སུ་ཡིག་ཚགས་སུ་ཉར" +immutableHold = "བསྒྱུར་མི་རུང་བའི་འཛིན་ཉར" +keepFor = "ཉར་ཡུན" + +[portal.policies.config.retention.rules] +0 = "ཉར་ཚགས་འཛིན་ཉར" +1 = "རང་འགུལ་ཡིག་ཚགས་སུ་ཉར" +2 = "སུབ་པ་འགོག" + +[portal.policies.config.routing] +summary = "རིགས་དང་རིགས་དབྱེ་ལ་གཞིགས་ནས་ཡིག་ཆ་འོས་འཚམ་གྱི་དམིགས་ཡུལ་ལ་སྐྱེལ།" + +[portal.policies.config.routing.fields] +destination = "དམིགས་ཡུལ" +notify = "ལམ་སྟོན་སྐབས་བརྡ་ཐོངས" +webhookUrl = "Webhook URL" + +[portal.policies.config.routing.rules] +0 = "རང་འགུལ་རིགས་དབྱེ" +1 = "ཡིག་ཁུག་ལ་སྐྱེལ" +2 = "Webhook བརྡ་ཐོངས" + +[portal.policies.config.security] +summary = "PII འཚོལ་རྙེད་དང་སྦས་འགེབས་བྱེད། འགུལ་སྐྱོད་ནང་དོན་(JavaScript) སེལ། ཡིག་ཆ་ལ་ཆུ་རྟགས་འཇོག" + +[portal.policies.config.security.rules] +0 = "PII སྦས་འགེབས" +1 = "JavaScript སེལ" +2 = "ཆུ་རྟགས" + +[portal.policies.detail] +enforces = "བཙན་སྐུལ་ལག་བསྟར" +onEveryExport = "ཕྱིར་འདོན་རེ་རེའི་སྐབས" +onEveryUpload = "ཡར་སྐུར་རེ་རེའི་སྐབས" +outputAsNewFile = "ཡིག་ཆ་གསར་པ་ཞིག་ཏུ" +outputAsNewVersion = "པར་གཞི་གསར་པ་ཞིག་ཏུ" +recentActivity = "ཉེ་ཆར་གྱི་བྱ་འགུལ" +retry = "ཡང་བསྐྱར་ཚོད་ལྟ" +showLess = "ཉུང་ཙམ་སྟོན" +showMore = "མང་ཙམ་སྟོན" +sources = "ཁུངས" + +[portal.policies.detail.actions] +clearHistory = "སྒྲིག་གཅོད་ཟིན་པའི་ལོ་རྒྱུས་གཙང་སེལ" +delete = "སུབ" +editSettings = "སྒྲིག་འགོད་རྩོམ་སྒྲིག" +pause = "གནས་སྐབས་མཚམས་འཇོག" +resume = "མུ་མཐུད" +runNow = "ད་ལྟ་སྒྲུབ" + +[portal.policies.detail.clearHistory] +body = "སྲིད་ཇུས་འདིས་སྔོན་མ་སྒྲིག་གཅོད་བྱས་ཟིན་པའི་ཡིག་ཆ་ཚང་མ་བརྗེད་ནས། རྗེས་མའི་སྒྲུབ་ཐེངས་སུ་ད་ལྟ་དེའི་ཁུངས་ནང་ཡོད་པ་ཚང་མ་ཡང་བསྐྱར་སྒྲིག་གཅོད་བྱེད། ཡིག་ཆ་རང་ཉིད་ལ་འགྱུར་བ་མེད། འདི་ཕྱིར་ལོག་བྱེད་མི་ཐུབ།" +cancel = "དོར" +confirm = "ལོ་རྒྱུས་གཙང་སེལ" +title = "སྒྲིག་གཅོད་ཟིན་པའི་ལོ་རྒྱུས་གཙང་སེལ་བྱེད་དམ།" + +[portal.policies.detail.emptyActivity] +description = "སྲིད་ཇུས་འདི་སྒྲུབ་རྗེས་ཡིག་ཆ་འདིར་མངོན་ཡོང་།" +title = "ད་དུང་བྱ་འགུལ་མེད" + +[portal.policies.endpoints] +addWatermark = "ཆུ་རྟགས" +autoRedact = "PII སྦས་འགེབས" +classifyAndLabel = "རིགས་དབྱེ" +compressPdf = "སྡུད་སྐུང" +flatten = "ལེབ་བཟོ" +ocrPdf = "OCR" +sanitizePdf = "JavaScript སེལ" + +[portal.policies.offline] +description = "ཁྱེད་ཀྱི་སྲིད་ཇུས་ཉར་ཚགས་བྱས་ཟིན་པས། འབྲེལ་མཐུད་སླར་གསོ་བྱས་རྗེས་མངོན་ཡོང་།" +retry = "ཡང་བསྐྱར་ཚོད་ལྟ" +title = "Backend སྤྱོད་མི་ཐུབ" + +[portal.policies.operations] +change = "གོམ་པ་འདིས་ཅི་བྱེད་པ་བསྒྱུར" +noResults = "དེ་དང་མཐུན་པའི་གོམ་པ་མེད། ཐོན་རྫས་མིང་། ཡང་ན \"scan\", \"notify\", \"attach\" ཚོད་ལྟ་བྱེད།" +searchPlaceholder = "གོམ་པ་འཚོལ - \"scan\", \"attach\" ཡང་ན་ཐོན་རྫས་མིང་ཚོད་ལྟ་བྱེད" + +[portal.policies.operations.bodyMode] +binary = "ཡིག་ཆའི bytes རྗེན་པ" +json = "JSON payload ནང་" +multipart = "ཡིག་ཆ་ཡར་སྐུར་(multipart) ལྟར" + +[portal.policies.operations.clamavScan] +description = "ཁྱེད་རང་གི ClamAV server ཡིས་ཡིག་ཆ་ཞིབ་བཤེར་བྱེད། ཁྱེད་ཀྱི་དྲ་རྒྱ་ནས་གང་ཡང་ཕྱིར་མི་འགྲོ" +label = "ནད་དུག་བཤེར་འབེབས་(རང་གིས་གཉེར་བ)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "macros, executables, scripts ཡང་ན་ནང་འཇུག་དངོས་པོ་ཡོད་པའི་ཡིག་ཆ་དང་ལེན་མི་བྱེད།" +label = "འགུལ་སྐྱོད་ནང་དོན་འགོག" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive ཡིས་ཡིག་ཆ་ཞིབ་བཤེར་བྱེད་ཅིང་། དེ་གཙང་མ་མིན་ན་སྒྲུབ་ཐེངས་མཚམས་འཇོག་བྱེད།" +label = "ནད་དུག་བཤེར་འབེབས" + +[portal.policies.operations.confluenceAttach] +description = "སྒྲིག་གཅོད་ཟིན་པའི་ཡིག་ཆ་ཤོག་ངོས་ཤིག་ལ་བསྣན་འཇོག་བྱེད།" +label = "Confluence ཤོག་ངོས་ཤིག་ལ་ཟུར་སྣོན་བྱེད" + +[portal.policies.operations.consignoSubmit] +description = "མཚན་རྟགས་ཆེད་ཡིག་ཆ་ConsignO Cloud ལ་སྤྲོད།" +label = "མཚན་རྟགས་ཆེད་སྐུར་(ConsignO)" +note = "ཡིག་ཆ་སྤྲོད། མཚན་རྟགས་བཀོད་པའི་འདྲ་བཤུས་ཕྱིར་ལེན་པ་ད་དུང་རྒྱབ་སྐྱོར་མེད། རྒྱུ་མཚན་ནི་གོམ་པ་ཞིག་གིས་འབྲས་བུ་རྗེས་ཀྱི་གོམ་པ་ལ་སྤྲོད་མི་ཐུབ་པས་ཡིན།" + +[portal.policies.operations.customApiCall] +description = "འབོད་སྐུལ་ཁྱེད་རང་གིས་བཟོ། path, method, body དང་ལན་སྒྲིག་གཅོད།" +label = "རང་སྒྲིག API འབོད" + +[portal.policies.operations.discordNotify] +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་སྒྲིག་གཅོད་བྱས་པ་རྒྱུ་ལམ་ཞིག་ལ་བརྡ་སྤྲོད།" +label = "Discord ལ་འཕྲིན་ཐུང་སྤེལ" + +[portal.policies.operations.elasticIndex] +description = "སྲིད་ཇུས་ཀྱིས་བྱས་པ་དེ་འཚོལ་ཐུབ་པའི་ཡིག་ཆ་ཞིག་ལྟར་ཐོ་འགོད་བྱེད།" +label = "Elasticsearch ནང་རྩིས་ཞིབ་བྱུང་རིམ Index བྱེད" + +[portal.policies.operations.fields.bodyMode] +helperText = "multipart ནི་ཡར་སྐུར API ལ་འཚམ། JSON ནི་ཡིག་ཆ་payload ནང་དགོས་པའི API ལ་འཚམ། binary ཡིས bytes རྗེན་པ་སྐུར།" +label = "ཡིག་ཆ་ཇི་ལྟར་སྐུར་དགོས" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON body ཇི་ཡོད་ལྟར་སྐུར། {{document.base64}} ཡིས་ཡིག་ཆ་འཁྱེར། {{document.*}} དང་ {{run.*}} ནི་ཡིག་ཆ་རེ་རེར་ཁ་སྐོང་བྱེད།" +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "རྩིས་ཁྲ" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API ཡིས་ཡིག་ཆ་འཇོག་སའི form field།" +label = "File field མིང་" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "From" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "header མིང་/ཐང་ཆ་ཚན་གྱི JSON object། འབྲེལ་མཐུད་ཀྱི credential ཁྱེད་ཆེད་ཁ་སྣོན་བྱེད།" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "འཕྲིན་ཐུང་གི body ལྟར་སྐུར། {{document.*}} དང་ {{run.*}} ནི་ཡིག་ཆ་རེ་རེར་ཁ་སྐོང་བྱེད།" +label = "འཕྲིན་ཐུང" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "འབྲེལ་མཐུད་ཀྱི base URL ལ་མཇུག་སྣོན་བྱེད། དེས host གཞན་ཞིག་ལ་ནམ་ཡང་སླེབས་མི་ཐུབ།" +label = "ལམ" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "drive ནང་གི་གང་དུ་འབྲི་དགོས་པ། {{document.filename}} ནི་ཡིག་ཆ་རེ་རེར་བསྐང་རྒྱུ་ཡིན།" +label = "དམིགས་ཡུལ་ལམ" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "སྙན་ཐོས་ཡིག་ཆ་ལ་འགྱུར་བ་མི་གཏོང་། ཚབ་བརྗེས་ཀྱིས་ཕྱིར་ལན་གང་ཡོད་པ་དེས་ཚབ་བརྗེ་བྱེད།" +label = "ཕྱིར་ལན་ལ་གང་བྱེད་དགོས་པ" + +[portal.policies.operations.fields.signerEmail] +label = "མིང་རྟགས་འགོད་མཁན་གྱི email" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "བརྗོད་དོན" +placeholder = "ལས་སྣོན་བྱས་པ། {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio ཡིས་ཡི་གེ་འདི་དབྱེ་ཞིབ་བྱེད། {{document.title}} སྤྱོད་པའམ་ཞིབ་བཤེར་བྱ་དགོས་པའི་ཡི་གེ་སྦྱར་རོགས།" +label = "ཞིབ་བཤེར་བྱ་རྒྱུའི་ཡི་གེ" + +[portal.policies.operations.fields.to] +label = "ལ" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud སྤྱོད་མཁན་མིང" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་ལས་སྣོན་བྱས་པ་སྟོང་ཆ་ཞིག་ལ་བརྡ་སྤྲོད།" +label = "Google Chat ལ་འཕྲིན་ཐུང་སྤེལ" + +[portal.policies.operations.jiraAttach] +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་དོན་ཚན་ཞིག་ལ་སྦྱར།" +label = "Jira དོན་ཚན་ཞིག་ལ་སྦྱར" + +[portal.policies.operations.mailgunEmail] +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ཟུར་སྦྱར་ལྟར་སྐུར།" +label = "ཡིག་ཆ email སྐུར (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ཡིག་ཁུག་ཞིག་ནང་འབྲི།" +label = "Nextcloud ལ་ཡར་འཇུག" + +[portal.policies.operations.presidioAnalyze] +description = "ཁྱེད་ཀྱི Presidio server རང་གིས་རྙེད་པའི་མི་སྒེར་གཞི་གྲངས་སྙན་ཞུ་བྱེད།" +label = "མི་སྒེར་གཞི་གྲངས་རྟོགས་འཛིན" +note = "Presidio ཡིས་ཡི་གེ་ཀློག་པ་ལས་ཡིག་ཆ་མི་ཀློག ཞིབ་བཤེར་བྱ་འདོད་པའི་ཡི་གེ་སྤྲོད་རོགས — ཡིག་ཆ་རང་ཉིད་ཡར་འཇུག་མི་བྱེད།" + +[portal.policies.operations.responseMode] +replace = "ཕྱིར་ལན་གྱིས་ཡིག་ཆ་ཚབ་བརྗེ་བྱེད" +report = "ཡིག་ཆ་ཉར་ནས་ཕྱིར་ལན་ཐོ་འགོད་བྱེད" + +[portal.policies.operations.sendgridEmail] +description = "ལས་སྣོན་བྱས་པའི་ཡིག་ཆ་ཟུར་སྦྱར་ལྟར་སྐུར།" +label = "ཡིག་ཆ email སྐུར (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་ལས་སྣོན་བྱས་པ་རྒྱུ་ལམ་ཞིག་ལ་བརྡ་སྤྲོད།" +label = "Slack ལ་འཕྲིན་ཐུང་སྤེལ" + +[portal.policies.operations.splunkEvent] +description = "ཁྱེད་ཀྱི་རྩིས་ཞིབ་པས་སྔར་ནས་ལྟ་སའི་གནས་སུ་སྲིད་ཇུས་ཀྱིས་གང་བྱས་པ་ཐོ་འགོད་བྱེད།" +label = "Splunk ལ་རྩིས་ཞིབ་དོན་རྐྱེན་སྐུར" + +[portal.policies.operations.sumologicEvent] +description = "སྲིད་ཇུས་ཀྱིས་གང་བྱས་པ་དེ HTTP བསྡུ་ལེན་ཆས་ཤིག་ལ་ཐོ་འགོད་བྱེད།" +label = "Sumo Logic ལ་རྩིས་ཞིབ་དོན་རྐྱེན་སྐུར" + +[portal.policies.operations.teamsNotify] +description = "སྲིད་ཇུས་ཀྱིས་ཡིག་ཆ་ཞིག་ལས་སྣོན་བྱས་པ་རྒྱུ་ལམ་ཞིག་ལ་བརྡ་སྤྲོད།" +label = "Teams ལ་འཕྲིན་ཐུང་སྤེལ" + +[portal.policies.operations.webhookPost] +description = "ཁྱེད་ཀྱིས་འདེམས་པའི URL ཞིག་ལ་ཡིག་ཆ་རང་ཉིད་སྐུར།" +label = "webhook ལ་ཡིག་ཆ་སྤེལ" + +[portal.policies.operations.zapierNotify] +description = "འཁོར་སྐྱོད་ཀྱི་ཞིབ་ཕྲ་ཁྱེད་ཀྱི་རང་འགུལ་ལ་སྤྲོད།" +label = "Zap ཡང་ན Make scenario སྐུལ་སློང" + +[portal.policies.stats] +activeFor = "བེད་སྤྱོད་ནང" +dataProcessed = "ལས་སྣོན་བྱས་པའི་གཞི་གྲངས" +docsEnforced = "ལག་བསྟར་བྱས་པའི་ཡིག་ཆ" + +[portal.policies.status] +active = "བེད་སྤྱོད་ནང" +paused = "མཚམས་བཞག" + +[portal.policies.summary.active] +description = "ཡར་འཇུག/ཕྱིར་འདྲེན་སྐབས་ལག་བསྟར" +label = "བེད་སྤྱོད་ནང་གི་སྲིད་ཇུས" + +[portal.policies.summary.categories] +description = "སྒྲིག་འགོད་ལ་སྤྱོད་རུང" +label = "རིགས་དབྱེ" + +[portal.policies.summary.docsEnforced] +description = "བེད་སྤྱོད་ནང་གི་སྲིད་ཇུས་ཡོངས་སུ" +label = "ལག་བསྟར་བྱས་པའི་ཡིག་ཆ" + +[portal.policies.summary.paused] +description = "སྒྲིག་འགོད་བྱས་ཡོད་ཀྱང་སྤེལ་མེད" +label = "མཚམས་བཞག" + +[portal.policies.table] +appliesTo = "ལ་ཁྱབ" +docs = "ལག་བསྟར་བྱས་པའི་ཡིག་ཆ" +enforces = "ལག་བསྟར་བྱེད" +policy = "སྲིད་ཇུས" +status = "གནས་ཚུལ" + +[portal.policies.wizard.actions] +back = "ཕྱིར་ལོག" +cancel = "འདོར་བ" +continue = "མུ་མཐུད" +enablePolicy = "སྲིད་ཇུས་སྤེལ" +saveChanges = "འགྱུར་བ་ཉར" + +[portal.policies.wizard.capability.classify] +desc = "ཁྱེད་ཀྱི་ཚོགས་པའི་མཚོན་རྟགས་དང་ཤོག་བྱང་ནས་ཡིག་ཆའི་རིགས་ངོས་འཛིན་བྱས་ཏེ་ཤོག་བྱང་འགོད་པས། རིགས་དབྱེ་ལྟར་ཡིག་ཚགས་དང་འཚོལ་བྱེད་ཐུབ།" +label = "ཡིག་ཆ་རིགས་དབྱེ" + +[portal.policies.wizard.capability.compress] +desc = "ཡིག་ཆ་སྔར་ལས་ཆུང་བའི་ཡིག་ཆའི་ཆེ་ཆུང་ལ་བསྡུ་གནོན་བྱེད།" +label = "ཡིག་ཆའི་ཆེ་ཆུང་ཉུང་དུ་གཏོང" + +[portal.policies.wizard.capability.flatten] +desc = "འགེངས་ཤོག་གི་ཡིག་སྒམ་དང་མཆན་འགྲེལ་ཤོག་ངོས་ནང་སྒྲིལ་ནས་རྩོམ་སྒྲིག་མི་ཐུབ་པར་བྱེད།" +label = "ཡིག་ཆ་ཁོད་སྙོམས་བྱེད" + +[portal.policies.wizard.capability.ocr] +desc = "OCR སྤེལ་ནས་བཤར་འབེབས་ཤོག་ངོས་རྣམས་འདེམས་ཐུབ་དང་འཚོལ་ཐུབ་པའི་ཡི་གེར་འགྱུར།" +label = "ཡི་གེ་འཚོལ་ཐུབ་པར་བྱེད" + +[portal.policies.wizard.capability.redact] +desc = "སྤྱི་ཚོགས་བདེ་སྲུང་ཨང་གྲངས་དང་བྱང་བུའི་ཨང་གྲངས་ལྟ་བུའི་ཚོར་སྐྱེན་ཞིབ་ཕྲ་རྙེད་ནས་ནག་པོས་བཀག་པས་ཀློག་མི་ཐུབ།" +label = "ཚོར་སྐྱེན་ཆ་འཕྲིན་གསང་བསྒྲིབ" + +[portal.policies.wizard.capability.sanitize] +desc = "སྦས་པའི JavaScript སུབ་ནས་ཡིག་ཆ་ཁ་ཕྱེ་སྐབས་ཅི་ཡང་རང་འགུལ་གྱིས་འཁོར་མི་ཐུབ་པར་བྱེད།" +label = "འགུལ་ཡོད་ནང་དོན་འདོར" + +[portal.policies.wizard.capability.timestampPdf] +desc = "རང་བཙན་དུས་ཐམ་དབང་ཚད་སྤྱད་ནས། དུས་ཚོད་གཅིག་ཏུ་ཡིག་ཆ་འདི་རྣམ་པ་འདི་རང་གིས་གནས་ཡོད་པ་བདེན་སྤྲོད་བྱེད། hash ཁོ་ན་སྐུར་བ་ཡིན - ཡིག་ཆ་ཁྱེད་ཀྱི server ནས་ནམ་ཡང་མི་འཐོན།" +label = "ཡིད་ཆེས་རུང་བའི་དུས་ཐམ་སྣོན" + +[portal.policies.wizard.capability.watermark] +desc = "ཤོག་ངོས་རེ་རེའི་སྟེང་མཐོང་ཐུབ་པའི་རྟགས་ཤིག དཔེར་ན “གསང་བ” འདེབས།" +label = "ཆུ་རྟགས་སྤྱོད" + +[portal.policies.wizard.classification] +description = "ཡར་འཇུག་བྱས་པའི་ཡིག་ཆ་རེ་རེ་ནང་སྒྲིག་མཚོན་རྟགས་ལ་བསྡུར་ནས་རིགས་དབྱེ་བྱེད་ཅིང་། འཚམ་པའི་རིགས་དབྱེ་རྣམས་ཀྱི་ཤོག་བྱང་འགོད། མཚོན་རྟགས་ཚོགས་ཁྱེད་ཀྱི་ཚོགས་པ་ཡོངས་དང་མཉམ་སྤྱོད་ཡིན།" +labelsHeading = "རིགས་དབྱེ་མཚོན་རྟགས" + +[portal.policies.wizard.errors] +noTools = "སྔོན་ལ workflow ནང་ལག་ཆ་ཉུང་མཐར་གཅིག་སྤེལ།" +saveFailed = "སྲིད་ཇུས་ཉར་མ་ཐུབ། སླར་ཚོད་ལྟ་གནང་རོགས།" + +[portal.policies.wizard.output] +heading = "ཐོན་སྐྱེད་དང་འཁོར་སྐྱོད" + +[portal.policies.wizard.output.filenameRule] +autoNumber = "རང་འགུལ་ཨང་འགོད" +label = "ཡིག་ཆའི་མིང་སྒྲིག་སྲོལ" +placeholder = "སྣོན་རྒྱུའི་ཡི་གེ (འདེམས་རུང)" +prefix = "སྔོན་འཇུག" +suffix = "རྗེས་འཇུག" + +[portal.policies.wizard.output.outputAs] +label = "ཐོན་སྐྱེད་རྣམ་པ" +newFile = "ཡིག་ཆ་གསར་པ" +newVersion = "པར་གཞི་གསར་པ" + +[portal.policies.wizard.output.runOn] +export = "ཕྱིར་འདྲེན" +helper = "སྲིད་ཇུས་སྤེལ་སྐབས། ཡར་འཇུག་སྐབས་སམ་ཕྱིར་འདྲེན་གོང་།" +label = "གང་དུ་འཁོར་སྐྱོད" +upload = "ཡར་འཇུག" + +[portal.policies.wizard.settings] +heading = "སྒྲིག་འགོད" + +[portal.policies.wizard.sources] +heading = "ཁུངས" +loading = "ཁུངས་འགེལ་བཞིན་པ…" + +[portal.policies.wizard.tabs] +ariaLabel = "སྒྲིག་འགོད་གོམ་སྟབས" +settings = "སྒྲིག་འགོད" +workflow = "བྱ་སྤྱོད" + +[portal.policies.wizard.title] +edit = "{{category}} སྲིད་ཇུས་རྩོམ་སྒྲིག" +setUp = "{{category}} སྲིད་ཇུས་སྒྲིག" + +[portal.policies.wizard.workflow] +description = "སྲིད་ཇུས་འདིས་ལས་སྣོན་བྱེད་པའི་ཡིག་ཆ་རེ་རེར་གང་བྱེད་པ་འདེམས།" + +[portal.policySummary.action] +setUp = "སྒྲིག" + +[portal.processorFlow] +footnote = "གྲངས་ཀ་ནི་ཆུ་ཚོད་24 མཐའ་མ་ལ་བརྩིས་པ་ཡིན། རྒྱུན་རིམ་མྱུར་ཚད་ནི་མཚོན་བྱེད་ཙམ་ཡིན།" +liveBadge = "Live" +stats = "{{connected}} མཐུད་ཡོད · PDF {{processed}} ལས་སྣོན་བྱས" +title = "PDF ལས་སྣོན་ཆས" + +[portal.processorFlow.lens] +ariaLabel = "ལྟ་སྟངས" +flow = "རྒྱུན་རིམ" +sankey = "Sankey" + +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "ཕམ" +heading = "འབྲས་བུ" +success = "འབྱོར་ཟིན" + +[portal.processorFlow.policies] +activeCount = "{{n}} བེད་སྤྱོད་ནང" +count = "{{n}} · 24h" +heading = "སྲིད་ཇུས" +setUp = "སྒྲིག" +soon = "མི་འགྱངས་པར" + +[portal.processorFlow.sankey] +waist = "སྲིད་ཇུས · {{n}} བེད་སྤྱོད་ནང" + +[portal.processorFlow.sankey.empty] +description = "ཁུངས་ཤིག་མཐུད་ནས་སྲིད་ཇུས་ཤིག་སྤེལ་ཏེ། ཡིག་ཆ་ལས་སྣོན་ཆས་བརྒྱུད་རྒྱུ་བར་ལྟོས།" +title = "ད་དུང་རྒྱུན་རིམ་མེད" + +[portal.processorFlow.sources] +comingSoonTag = "མཐུད" +editor = "Stirling PDF Editor" +heading = "ཁུངས" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email ལེན་འཇུག" + +[portal.procurement] +subtitle = "ཁྱེད་ཀྱི་ཚོགས་པ་དཔྱད་ཞིབ། གན་རྒྱ་འཇོག་པ། དང་འགོ་འཛུགས་བྱེད། ཡིག་ཆ་ཡོངས་གནས་གཅིག་ཏུ།" +title = "ཉོ་སྒྲུབ" + +[portal.procurement.agreement] +agreeCta = "གྲོས་མཐུན་ལ་མིང་རྟགས་འགོད" +confidential = "གསང་བ" +confirm = "ངས་གྲོས་མཐུན་འདི་ཀློག་ཟིན་ཞིང་མོས་མཐུན་ཡོད། དེ་མ་ཟད་གོང་དུ་མིང་འཁོད་པའི་ཚོགས་པའི་ཚབ་ཏུ་མིང་རྟགས་འགོད་དབང་ཡོད་པར་ངས་མཚོན།" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA དང DPA བཅས་མིང་རྟགས་གཅིག་ཏུ་མཉམ་སྒྲིལ།" +download = "ཕབ་ལེན" +downloadDraftError = "གྲོས་མཐུན PDF བཟོ་མ་ཐུབ - ཡིག་ཆ་མངོན་འགྱུར་ཆས་སྤྱོད་མི་ཐུབ། ཏོག་ཙམ་རྗེས་སླར་ཚོད་ལྟ་གནང་བའམ་རྒྱབ་སྐྱོར་ལ་འབྲེལ་བ་གནང་རོགས།" +downloadError = "མིང་རྟགས་བཀོད་པའི་གྲོས་མཐུན་ཕབ་ལེན་བྱ་མ་ཐུབ། སླར་ཚོད་ལྟ་གནང་རོགས།" +legalName = "ཁྲིམས་མཐུན་ཚོགས་པའི་མིང" +legalNamePlaceholder = "མིང་རྟགས་འགོད་རྒྱུའི་ཁྲིམས་མཐུན་ཚོགས་པ" +loadError = "གྲོས་མཐུན་འགེལ་མ་ཐུབ། སླར་ཚོད་ལྟ་གནང་རོགས།" +loading = "གྲོས་མཐུན་འགེལ་བཞིན་པ..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "བསྒྱུར་བ་ཞུ་བ" +scrollHint = "ཞིབ་བཤེར་ཆེད་འདྲིལ་སྒུལ་བྱེད" +signatory = "མིང་རྟགས་འགོད་མཁན་གྱི་མིང" +signatoryPlaceholder = "མིང་ཆ་ཚང" +signatoryTitle = "མིང་རྟགས་འགོད་མཁན་གྱི་གོ་གནས" +signatoryTitlePlaceholder = "དཔེར་ན། ཁྲིམས་དོན་སྤྱི་ཁྱབ་བློ་འདོན་པ" +signError = "ཁྱེད་ཀྱི་མིང་རྟགས་ཟིན་ཐོར་འགོད་མ་ཐུབ། སླར་ཚོད་ལྟ་གནང་རོགས།" + +[portal.procurement.builder] +addons = "ཁ་སྣོན" +addressLine1 = "ཁ་བྱང་ཕྲེང་ 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "ཁ་བྱང་ཕྲེང་ 2" +addressLine2Placeholder = "ཁང་མིག་། ཐོག་རྩེག ཁང་པ" +back = "ཕྱིར་ལོག" +businessName = "ཚོང་ལས་མིང" +businessNamePlaceholder = "ཁྱེད་ཀྱི་ཀུང་སི" +city = "གྲོང་ཁྱེར" +cityPlaceholder = "San Francisco" +completeRequired = "རིན་གོང་འདྲེན་ཡིག་བཟོ་སྔོན། དགོས་ངེས་ཀྱི་ས་སྟོང་ (* རྟགས་ཅན) རྣམས་ནུས་ལྡན་གྱི email དང་བཅས་ཆ་ཚང་སྐོང་རོགས།" +contactEmail = "འབྲེལ་གཏུག email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "འབྲེལ་གཏུག་མིང" +contactNamePlaceholder = "Jane Doe" +continue = "མུ་མཐུད" +done = "ལེགས་གྲུབ" +eula = "ངས Stirling Enterprise EULA ཀློག་ཟིན་ཞིང་དེར་མོས་མཐུན་བྱེད། དེས་རིན་གོང་འདྲེན་ཡིག་འདི་ནས་བཟོས་པའི་གྲོས་མཐུན་ལ་སྟངས་འཛིན་བྱེད།" +generate = "རིན་གོང་འདྲེན་ཡིག་བཟོ" +indemnification = "IP སྲུང་སྐྱོབ་ཇེ་ལེགས" +indemnificationSub = "ང་ཚོའི IP སྲུང་སྐྱོབ་དེ་པེ་ཊེན་ཊི་རེ་འདུན་བར་རྒྱ་བསྐྱེད་བྱེད། པར་དབང་། ཚོང་རྟགས། ཚོང་ལས་གསང་བའི་རྩ་བའི་གུན་གསབ་ནི་རིན་མེད་དུ་ཚུད་ཡོད།" +paperEyebrow = "ཁེ་ལས་རིམ་པའི་རིན་གོང་འདྲེན་ཡིག" +paperFor = "གྲ་སྒྲིག་བྱས་ཡུལ" +pdfSize = "PDF ཆེ་ཆུང" +poNumber = "PO ཨང་གྲངས" +poNumberPlaceholder = "འདེམས་རུང" +postalCode = "སྦྲག་ཨང" +postalCodePlaceholder = "94105" +posture = "སྐྱོང་སྡོམ" +posture_count = "~{{count}} སྲིད་ཇུས" +posture_essentials = "གལ་ཆེའི་ཆ་ཤས" +posture_essentialsSub = "རིགས་དབྱེ + མཉམ་སྤྱོད — སྔོན་སྒྲིག" +posture_governed = "སྐྱོང་སྡོམ་བྱས་པ" +posture_governedSub = "བདེ་འཇགས་དང་ལམ་སྟོན་ཁ་སྣོན་བྱེད" +posture_regulated = "སྒྲིག་སྲོལ་འོག" +posture_regulatedSub = "རིགས་ཚན་ཚང་མ — མཐུན་སྒྲུབ། ཉར་ཚགས། ནང་འཇུག" +qbr = "དུས་ཚིགས་རེའི་ཚོང་ལས་ཞིབ་བཤེར" +qbrSub = "ཁྱེད་ཀྱི SE ཡིས་དུས་ཚིགས་རེར་སྤྱོད་ཚུལ་དང་ལམ་དཀར་ཞིབ་བཤེར་བྱེད" +region = "མངའ་སྡེ / ས་ཁུལ" +regionPlaceholder = "California" +running = "{{annual}} / ལོ · {{years}}-ལོ {{tcv}}" +s1Sub = "ཁྱེད་ཀྱི་ཚོགས་པ་དང་། ལོ་རེར་སྒྲུབ་རྒྱུར་རེ་བ་ཡོད་པའི PDF རྣམས།" +s1Title = "ཁྱེད་ཀྱིས་ག་ཚོད་ལས་སྣོན་བྱེད་རྒྱུ་ཡིན།" +s2Sub = "དུས་ཡུན་རིང་བས་རིན་ཚད་ལ་ཆག་ཡང་ཡོད། ཁྱེད་ཀྱི་ཞབས་ཞུའི་རིམ་པས་རམ་འདེགས་གཏན་འཁེལ་བྱེད།" +s2Title = "ཁས་ལེན་དང་ཞབས་ཞུ" +s3Sub = "རིན་གོང་འདྲེན་ཡིག་དང་དེས་བཟོ་བའི་གྲོས་མཐུན་ཆེད།" +s3Title = "ཁྱེད་ཀྱི་ཞིབ་ཕྲ" +serviceLevel = "ཞབས་ཞུའི་རིམ་པ" +size_compact = "ཆུང་བསྡུས" +size_compactSub = "གཙོ་བོ་ཡི་གེ། 1 MB འོག" +size_heavy = "ལྗིད་པ" +size_heavySub = "བསྐྱར་པར་བྱས་པའམ་པར་རིས་མང་བ། 5 MB+" +size_standard = "ཚད་ལྡན" +size_standardSub = "ཡི་གེ་དང་པར་རིས་མཉམ་བསྲེས། 1 ནས 5 MB བར" +slDedicated = "ཆེད་སྤྱོད" +slDedicatedSub = "ལས་ཀའི་ཆུ་ཚོད་ 4 · ཆེད་སྤྱོད SE / CSM · +$30,000/ལོ" +slPriority = "གཙོ་རིམ" +slPrioritySub = "ལས་ཀའི་ཉིན་དེ་རང · མིང་འཁོད་ CSM · ཚུད་ཡོད" +slStandard = "ཚད་ལྡན" +slStandardSub = "ལས་ཀའི་ཉིན་རྗེས་མ · མཉམ་སྤྱོད་ CSM · ཚུད་ཡོད" +stepOf = "གོམ་པ་ {{n}} / {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "འདེམས་རུང" +term = "དུས་ཡུན" +termDiscount = "ལོ་མང་ཁས་ལེན་གྱི་ཆག་ཡང་ {{pct}}% སྤྱད་ཟིན" +title = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་བཟོ" +training = "འགོ་འཛུགས་དང་སྦྱོང་བརྡར" +trainingSub = "ཁྱེད་ཀྱི་ཚོགས་པ་འགོ་འཛུགས་ཐུབ་པར་དངོས་ཐོག་སློབ་ཚན" +users = "སྤྱོད་མཁན་ཁྱོན" +usersPlaceholder = "དཔེར་ན། 250" +viewEula = "ཀློག" +volEstimated = "{{count}} སྤྱོད་མཁན་ལས་ཚོད་དཔག་བྱས་པ (~PDF རེ་ 2,000 རེ། རང་འགུལ་ཚུད)། ཁྱེད་ཀྱིས་དེ་ལས་གསལ་བར་ཤེས་ན་རྩོམ་སྒྲིག་གནང་།" +volManual = "ཁྱེད་ཀྱི་གྲངས་ཀ་སྤྱོད་བཞིན་ཡོད། དུས་གང་རུང་ལ་ཚོགས་པའི་ཆེ་ཆུང་ནས་སླར་ཚོད་དཔག་བྱེད་ཆོག" +volNoUsers = "གསལ་མེད་དམ། ཁྱེད་ཀྱི་ཚོགས་པའི་ཆེ་ཆུང་ནང་འཇུག་གནང་ན། ང་ཚོས་ཚོད་དཔག་བྱེད།" +volume = "ལོ་རེའི PDF འབོར་ཚད" +volumePlaceholder = "དཔེར་ན། 1,000,000" +years_one = "ལོ་ {{count}}" +years_other = "ལོ་ {{count}}" + +[portal.procurement.documents] +agreement = "ཁེ་ལས་རིམ་པའི་གྲོས་མཐུན" +agreementSub = "MSA, མངགས་ཐོ་དང DPA མིང་རྟགས་གཅིག་ནང་།" +download = "ཕབ་ལེན" +eula = "EULA དང་ཚོང་ལས་ཆ་རྐྱེན" +eulaSub = "software དང་རང་ཞབས་ཚོང་ལས་ཆ་རྐྱེན།" +invoice = "འཛིན་ཤོག" +invoiceSub = "ཁྱེད་ཀྱི་དང་པོའི་མངགས་ཉོའི་འཛིན་ཤོག" +laterInvoice = "ཁྱེད་ཀྱིས་ད स्वीकार་བྱས་རྗེས་ཐོབ་ཐུབ" +laterQuote = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་སྤྲད་རྗེས་ཐོབ་ཐུབ" +quote = "རིན་གོང་འདྲེན་ཡིག" +quoteSub = "ཁྱེད་ཀྱི་རྣམ་གྲངས་བགོས་པའི་ཁེ་ལས་རིམ་པའི་རིན་གོང་འདྲེན་ཡིག" +sla = "SLA ཟུར་སྦྱར" +slaSub = "རམ་འདེགས་རིམ་པ་དང་ལན་འདེབས་དམིགས་ཚད།" +subprocessors = "ཡན་ལག་ལས་སྣོན་པ" +subprocessorsSub = "ཁྱེད་ཀྱི་ཚབ་ཏུ་གནས་སྡུད་ལས་སྣོན་བྱེད་པའི་ཕྱི་ཕྱོགས་གསུམ་པ།" +subtitle = "ཁྱེད་ཀྱི་ཚོང་འབྲེལ་ཡིག་ཆ། དུས་གང་རུང་ཐོབ་ཐུབ།" +title = "ཡིག་ཆ" +view = "ལྟ" + +[portal.procurement.error] +title = "གནད་དོན་ཞིག་བྱུང་སོང" + +[portal.procurement.hero] +barAria = "གོ་རིམ་ {{current}} / {{total}}" +ctaAgreement = "གྲོས་མཐུན་ཞིབ་བཤེར་དང་མིང་རྟགས་འགོད" +ctaExploring = "ཁྱེད་ཀྱི་ཚོད་ལྟ་སྒྲིག་སྦྱོར" +ctaQuote = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་ཞིབ་བཤེར" +ctaReviewQuote = "རིན་གོང་འདྲེན་ཡིག་ཞིབ་བཤེར" +ctaTrial = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་བཟོ" +documents = "ཡིག་ཆ" +eyebrow = "ཁེ་ལས་རིམ་པ" +eyebrowCompany = "{{company}} Enterprise" +inviteTeammates = "ཚོགས་གྲོགས་གདན་འདྲེན" +licenseKey = "ཆོག་མཆན་ལྡེ་མིག" +liveSub = "ཁྱེད་ཀྱི་ཚོགས་སྡེ་མཁོ་སྤྲོད་ཟིན་ཞིང་ཆོག་མཆན་ནུས་ལྡན་ཡིན།" +liveTitle = "ཁྱེད་ Stirling Enterprise ཐོག་དངོས་སུ་སྤྱོད་བཞིན་ཡོད" +next = "རྗེས་མ། {{stage}}" +open = "ཉོ་སྒྲུབ་ཁ་ཕྱེ" +scheduleCall = "ཁ་པར་གྲོས་མོལ་དུས་བཀག" +sentenceAgreement = "གྲོས་མཐུན་ཞིབ་བཤེར་དང་མིང་རྟགས་འགོད" +sentenceLive = "མཁོ་སྤྲོད་དང་ཁྱབ་སྤེལ" +sentencePayment = "ཉོ་སྒྲུབ་མངགས་ཐོ་དང་དངུལ་སྤྲོད" +sentenceQuote = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་ཞིབ་བཤེར་དང་དང་ལེན" +sentenceTrial = "ཁྱེད་ཀྱི་ཚོད་ལྟའི་ལས་ཁུལ་སྒྲིག་སྦྱོར" + +[portal.procurement.journey] +daysLeft_one = "{{count}} ཉིན་ལྷག་ཡོད" +daysLeft_other = "{{count}} ཉིན་ལྷག་ཡོད" + +[portal.procurement.journeySteps.agreement] +blurb = "མིང་རྟགས་གཅིག་གིས MSA, མངགས་ཐོ། EULA དང DPA ཚང་མ་ཁེབས།" +gatingAction = "ཁྱེད་ཀྱི་གྲོས་མཐུན་ཞིབ་བཤེར་དང་མིང་རྟགས་འགོད" +label = "གྲོས་མཐུན" + +[portal.procurement.journeySteps.implementation] +blurb = "ཁྱེད་ཀྱི་ལས་ཁུལ་མཁོ་སྤྲོད་དང go-live playbook སྒྲུབ།" +gatingAction = "ཁྱེད་ཀྱི་ལས་ཁུལ་མཁོ་སྤྲོད་བྱེད་བཞིན" +label = "ལག་བསྟར" + +[portal.procurement.journeySteps.payment] +blurb = "བྱང་བུ། དངུལ་ཁང་བརྒྱུད་སྤོ། ཡང་ན་ཉོ་སྒྲུབ་མངགས་ཐོ་ལྟར་དངུལ་སྤྲོད།" +gatingAction = "དངུལ་སྤྲོད་གཏན་འཁེལ" +label = "དངུལ་སྤྲོད" + +[portal.procurement.journeySteps.quote] +blurb = "ཁས་ལེན་འབོར་ཚད་ཀྱི་རིན་གོང་དང་གན་རྒྱའི་དུས་ཡུན་ཞིབ་བཤེར།" +gatingAction = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་དང་ལེན" +label = "རིན་གོང་འདྲེན་ཡིག" + +[portal.procurement.journeySteps.trial] +blurb = "ཁྱེད་ཀྱི་ཡིག་ཆ་དང་ལས་རིམ་ཐོག Stirling ཚོད་དཔག་བྱེད།" +gatingAction = "ཁྱེད་ཀྱི་རིན་གོང་འདྲེན་ཡིག་བཟོ" +label = "ཚོད་ལྟ" + +[portal.procurement.license] +copied = "འདྲ་བཤུས་བྱས་ཟིན" +copy = "ལྡེ་མིག་འདྲ་བཤུས" +downloadError = "ད་ལྟ་དྲ་མེད་ཆོག་མཆན་ཡིག་ཆ་བཟོ་མ་ཐུབ — ཅུང་ཙམ་རྗེས་སླར་ཚོད་ལྟ་གནང་རོགས།" +downloadOffline = "དྲ་མེད་ཆོག་མཆན་ཕབ་ལེན (.lic)" +hint = "ལྡེ་མིག་འདི་རང་སྐྱོང་དཔེ་མཚོན་ནང་སྦྱར་ནས་ནུས་སློང་བྱེད་ཆོག ཡང་ན་ཁྱེད་ཀྱི་ཟིན་ཐོའི་ཆེད་ཉར་ཆོག དེ་བདེ་འཇགས་སུ་ཉར་རོགས།" +label = "ཁྱེད་ཀྱི་ཆོག་མཆན་ལྡེ་མིག" +subtitle = "ལྡེ་མིག་འདིས་རང་སྐྱོང་དཔེ་མཚོན་ནུས་སློང་བྱེད་ཆོག ཡང་ན་ཁྱེད་ཀྱི་ཟིན་ཐོའི་ཆེད་ཉར་ཆོག" +title = "ཁྱེད་ཀྱི་ཆོག་མཆན་ལྡེ་མིག" +trialFileHint = "འདི་ནི་ཁྱེད་ཀྱི་ཚོད་སྤྱོད་ཆོག་མཆན་ཡིག་ཆ་ཡིན། གྲོས་མཐུན་གཏན་འཁེལ་བྱས་རྗེས། ཕྱིར་ལོག་ནས་ཡང་བསྐྱར་ཕབ་ལེན་བྱོས — དྲ་ཐོག་ཆོག་མཆན་ལྡེ་མིག་དང་མི་འདྲ་བར། .lic ཡིག་ཆ་རང་འགུལ་གྱིས་གསར་སྒྱུར་མི་བྱེད།" + +[portal.procurement.link] +cta = "རྩིས་ཐོ་འབྲེལ་མཐུད" +description = "ཉོ་སྒྲུབ་ནི་ཁྱེད་ཀྱི་འབྲེལ་མཐུད་བྱས་པའི Stirling རྩིས་ཐོ་ཐོག་འགྲོ་སྐྱོང་བྱེད། འདིས་ང་ཚོས་ཚོད་སྤྱོད་སྤྲོད་པ། གོང་འབོལ་རིན་གོང་རྩིས་པ། དང་རྩིས་བསྡུ་འགོ་འཛུགས་པའི་ཐབས་ཡིན། འགོ་འཛུགས་ཆེད་རྩིས་ཐོ་ཞིག་འབྲེལ་མཐུད་བྱོས།" +eyebrow = "ཁེ་ལས" +title = "འགོ་འཛུགས་ཆེད་ཁྱེད་ཀྱི་རྩིས་ཐོ་འབྲེལ་མཐུད་བྱོས" + +[portal.procurement.live] +description = "ཁྱེད་ཀྱི་མངགས་ཉོ་འགུལ་སྤྱོད་ནང་ཡོད་ཅིང་ཆོག་མཆན་སྤྲད་ཟིན། ཁྱེད་ཀྱི་ཚོགས་པ་སྤྲོད་སྒྲིག་བྱས་ཟིན་ཞིང་རྩིས་བསྡུ་འགོ་ཚུགས་ཟིན།" +eyebrow = "དངོས་སྤྱོད" +title = "ཁྱེད་རང Stirling Enterprise ཐོག་དངོས་སྤྱོད་ལ་ཞུགས་ཟིན" + +[portal.procurement.milestone] +downloadError = "ད་ལྟ་གོང་འབོལ PDF ཕབ་ལེན་མ་ཐུབ — སྐད་ཅིག་རྗེས་སླར་ཚོད་ལྟ་བྱོས།" + +[portal.procurement.payment] +description = "ཁྱེད་ཀྱི་གོང་འབོལ་དང་ལེན་བྱས་ཟིན་ཞིང་ཆོག་མཆན་ཡང་ད་སྔ་ནས་འགུལ་སྤྱོད་ནང་ཡོད — ཁྱེད་ཀྱི་ཚོགས་པས་འཕྲལ་དུ་འགོ་འཛུགས་ཆོག གྲ་སྒྲིག་ཡོད་སྐབས་རྩིས་ཤོག་དང་པོ་སྤྲོད། འདི་ནས་སྤྲོད་པའམ་ཕབ་ལེན་བྱེད་ཆོག་ཅིང་། email མི་དགོས།" +downloadAgreement = "མིང་རྟགས་བཀོད་པའི་གྲོས་མཐུན་ཕབ་ལེན" +downloadInvoice = "རྩིས་ཤོག་ཕབ་ལེན" +eyebrow = "དངུལ་སྤྲོད" +title = "མངགས་ཉོ་བཟོས་ཟིན" +viewInvoice = "རྩིས་ཤོག་ལྟ་བ་དང་སྤྲོད་པ" + +[portal.procurement.review] +acceptCta = "གོང་འབོལ་དང་ལེན" +annual = "ལོ་རེའི་རིན་གླ་། (ལོ་ 1)" +downloadCta = "གོང་འབོལ་ཕབ་ལེན" +poNumber = "ཉོ་སྒྲུབ་མངགས་ཐོ། {{po}}" +renewal = "དུས་ཡུན་རྗེས་སུ {{amount}}/ལོ ཐོག་སླར་གསོ་བྱེད། (+{{pct}}% CPI)" +tcv = "{{years}}-ལོའི་དུས་ཡུན། སྔོན་ནས་ཆ་ཚང་སྤྲད་པ། {{tcv}}" +validUntil = "{{date}} བར་གོ་ཆོད།" + +[portal.procurement.schedule] +fallback = "དུས་ཚོད་སྒྲིག་ཆས་འཇུག་མ་ཐུབ།" +fallbackLink = "དུས་ཚོད་སྒྲིག་པ་མཆོང་ལྡེ་གསར་པའི་ནང་ཁ་ཕྱེ" +loading = "དུས་ཚོད་སྒྲིག་ཆས་འཇུག་བཞིན་པ..." +subtitle = "ཁྱེད་ཀྱི་ཐབས་ལམ་བཟོ་འགོད་པས་ཁྱེད་ཀྱི་ཚོགས་པར་འགྲེམས་སྤེལ་གོ་རིམ་འགྲེལ་བཤད་བྱེད། ཁྱེད་ལ་འཚམ་པའི་དུས་ཚོད་ཞིག་འདེམས།" +title = "ཁ་པར་གྲོས་མོལ་དུས་ཚོད་སྒྲིག" + +[portal.procurement.setup] +airgap = "དྲ་བ་ལས་ཆོད་པ" +airgapSub = "བཀག་སྡོམ་བྱས་པ། ཕྱིར་འབྲེལ་མེད" +back = "ཕྱིར" +businessName = "ཚོང་ལས་མིང" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "དོ་དམ་བྱས་པ། འགོ་འཛུགས་མགྱོགས་ཤོས" +continue = "མུ་མཐུད" +deployment = "གང་དུ་འགྲེམས་སྤེལ་བྱ་དགོས།" +eula = "ངས Stirling EULA & Commercial Terms ལ་མོས་མཐུན་བྱེད།" +fullName = "མིང་ཆ་ཚང" +fullNamePlaceholder = "ཁྱེད་ཀྱི་མིང" +invites = "ཚོགས་གཏོགས་གདན་འདྲེན · འདེམས་རུང" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "ཁ་པར་གྲོས་མོལ་དུས་ཚོད་སྒྲིག" +seats = "ཚོད་སྤྱོད་མི་གྲངས" +seatsPlaceholder = "དཔེར་ན། 15" +selfhost = "རང་ཉིད་ཀྱིས་གཙོ་སྐྱོང" +selfhostSub = "Docker ཡང་ན Kubernetes" +start = "ཚོད་སྤྱོད་འགོ་འཛུགས" +stepOf = "གོམ་པ {{n}} / {{total}}" +subtitle = "ཉིན་ 14 རིང་རིན་མེད། སྤྱོད་བྱང་མི་དགོས།" +subtitleDetails = "ཁྱེད་ཀྱི་གོང་འབོལ་དང་གྲོས་མཐུན་ཆེད་ཞིབ་ཕྲ་འགའ།" +talkFirst = "སྔོན་ལ་གྲོས་མོལ་བྱ་འདོད་དམ།" +title = "ཁྱེད་ཀྱི་ཚོད་སྤྱོད་སྒྲིག" +viewEula = "ཀློག" +workEmail = "ལས་ཀའི email" +workEmailPlaceholder = "you@company.com" + +[portal.procurement.trial] +body = "དུས་འགྱངས་བྱས་ན་ཉིན་ 7 ཁ་སྣོན་བྱེད་ཅིང་ཁྱེད་ཀྱི་ཐབས་ལམ་བཟོ་འགོད་པར་བརྡ་ཐོ་གཏོང་།" +bodyMaxed = "ཁྱེད་ཀྱིས་དུས་འགྱངས་ཚང་མ་སྤྱད་ཟིན — དུས་ཚོད་མང་བ་དགོས་ན་ཁྱེད་ཀྱི་ཐབས་ལམ་བཟོ་འགོད་པར་གྲོས་མོལ་བྱོས།" +cancel = "ཚོད་སྤྱོད་འདོར" +extend = "ཉིན་ 7 དུས་འགྱངས" +maxed = "མཐོ་ཚད་ལ་སླེབས" +subtitle = "ཁྱེད་ཀྱི་རིན་མེད་ཚོད་སྤྱོད {{date}} བར་འགྲོ སྤྱོད་བྱང་མི་དགོས།" +title = "ཁེ་ལས་ཚོད་སྤྱོད" + +[portal.settings.groups] +admin = "དོ་དམ་པ" + +[portal.settings.sections] +account-link = "རྩིས་ཐོ་འབྲེལ་མཐུད" + +[portal.shell.sidebar] +appEditor = "རྩོམ་སྒྲིག་ཆས" +appProcessor = "ལས་སྒྲུབ་ཆས" +linkAccount = "Stirling རྩིས་ཐོ་འབྲེལ་མཐུད" +primaryNav = "གཙོ་བོའི་ཁ་ཕྱོགས་འདྲེན" +switchApp = "app བརྗེ" + +[portal.shell.topbar] +closeNav = "ཁ་ཕྱོགས་འདྲེན་ཁ་རྒྱག" +openNav = "ཁ་ཕྱོགས་འདྲེན་ཁ་ཕྱེ" +search = "འཚོལ" + +[portal.sources] +subtitle = "ཡིག་ཆ་རྣམས Stirling ནང་འཇུག་པའི་བསྐྱར་སྤྱོད་ཆོག་པའི་ནང་འཇུག་འབྲེལ་མཐུད། ཁུངས་ཞིག་ཐེངས་གཅིག་སྒྲིག་རྗེས། ལས་རིམ་རྒྱུན་གྲངས་མང་ནས་དེ་ལ་གཞི་བཅོལ་བྱེད་ཆོག" +title = "ཁུངས" + +[portal.sources.actions] +connectSource = "ཁུངས་འབྲེལ་མཐུད" + +[portal.sources.builder] +back = "ཁུངས་ལ་ཕྱིར་ལོག" +backToTypes = "ཁུངས་རིགས་ཚང་མ" +cancel = "འདོར" +chooseHint = "ཡིག་ཆ་གང་ནས་ཡོང་བ་འདེམས། སྐྱ་མདོག་གི connectors རྣམས་འོང་བཞིན་པ་ཡིན།" +comingSoon = "མགྱོགས་པོ་འོང་རྒྱུ" +comingSoonHeading = "མགྱོགས་པོ་འོང་རྒྱུ" +create = "ཁུངས་བཟོ" +createTitle = "ཁུངས་ཞིག་འབྲེལ་མཐུད" +delete = "སུབ" +editTitle = "ཁུངས་རྩོམ་སྒྲིག" +enabled = "ནུས་ཡོད" +save = "འགྱུར་བ་ཉར" + +[portal.sources.builder.folderAccess] +description = "ཡིག་སྣོད་རང་འགུལ་བྱ་རིམ་གྱིས་དོ་དམ་པས་ཆོག་མཆན་སྤྲད་པའི་ཡིག་སྣོད་ཁོ་ན་སྤྱོད་ཐུབ། དེ་ཡིག་སྣོད་འཛུལ་སྤྱོད་སྒྲིག་འགོད་འོག་ཁ་སྣོན་བྱས་ནས། ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" +openSettings = "ཡིག་སྣོད་འཛུལ་སྤྱོད་སྒྲིག་འགོད" +title = "ཡིག་སྣོད་འདི་ལ་ཆོག་མཆན་མེད" + +[portal.sources.delete] +body = "\"{{name}}\" སུབ་བམ། འདི་ཕྱིར་འཐེན་མི་ཐུབ། དེ་ལ་གཞི་བཅོལ་བྱེད་པའི་སྲིད་ཇུས་རྣམས་གསར་སྒྱུར་དགོས་སྲིད།" +cancel = "འདོར" +confirm = "སུབ" +title = "ཁུངས་སུབ་བམ།" + +[portal.sources.kpi] +inUse = "སྤྱོད་བཞིན" +total = "འབྲེལ་མཐུད" +unused = "སྤྱོད་མེད" + +[portal.sources.networkFields.connection] +helperText = "server ཁ་བྱང་དང་དཔང་ཡིག་ཡོད་པའི་ཉར་ཚགས་འབྲེལ་མཐུད། དེ་ལ་གཞི་བཅོལ་བྱེད་པའི་ཁུངས་ཚང་མས་བསྐྱར་སྤྱོད་བྱེད།" +label = "འབྲེལ་མཐུད" + +[portal.sources.networkFields.directory] +helperText = "server ཐོག་བསྐོར་ཞིབ་བྱ་སའི་ཡིག་སྣོད། ནང་འཛུལ་ཁྱིམ་མམ་མཉམ་སྤྱོད་རྩ་བ་ལ་ལྟོས་པའི་ལམ་བུ། རྩ་བའི་ཆེད་སྟོང་བཞག" +label = "ཡིག་སྣོད" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume ཡིས་སྲིད་ཇུས་ཚང་མས་བཟོ་སྦྱོར་བྱས་རྗེས་ཡིག་ཆ་རེ་རེ server ནས་སུབ་སྐྱེལ་བྱེད།" +label = "ཀློག་སྟངས" + +[portal.sources.networkFields.mode.options] +consume = "Consume: ཡིག་ཆ་རེ་རེ་ཐེངས་གཅིག་བཟོ་སྦྱོར" +snapshot = "Snapshot: འཁོར་རེར་ཡིག་སྣོད་སླར་ཀློག" + +[portal.sources.networkFields.recursive] +label = "ཡིག་སྣོད་གཏིང་ཚད" + +[portal.sources.networkFields.recursive.options] +all = "ཡན་ལག་ཡིག་སྣོད་ཚུད" +top = "རིམ་པ་མཐོ་ཤོས་ཁོ་ན" + +[portal.sources.status] +active = "འགུལ་སྤྱོད" +disabled = "སྤྱོད་མི་ཐུབ" +unused = "སྤྱོད་མེད" + +[portal.sources.table] +documents = "ཡིག་ཆ" +source = "ཁུངས" +status = "གནས་ཚུལ" +type = "Type" +usedBy = "སྲིད་ཇུས" + +[portal.sources.types.box] +description = "ཁྱེད་ཀྱི་ཚོགས་པས་སྔ་ནས་མཉམ་སྤྱོད་བྱེད་པའི Box ཡིག་སྣོད་ནས་ཡིག་ཆ་འཐེན།" +label = "Box" + +[portal.sources.types.dropbox] +description = "མཉམ་སྤྱོད Dropbox ཡིག་སྣོད་ནས་ཡིག་ཆ་འཐེན།" +label = "Dropbox" + +[portal.sources.types.editor] +description = "སྲིད་ཇུས་དང AI འཁོར་སྐྱོད་ཁྱབ་ཁོངས་སུ། ཁྱེད་ཀྱི་ཚོགས་པས་རྩོམ་སྒྲིག་ཆས་ནང་བཟོ་སྦྱོར་བྱས་པའི་ཡིག་ཆ།" +label = "རྩོམ་སྒྲིག་ཆས" +noPolicies = "ད་དུང་རྩོམ་སྒྲིག་ཆས་ནས་སྲིད་ཇུས་འཁོར་སྐྱོད་བྱས་མེད།" + +[portal.sources.types.email] +description = "ཆེད་སྤྱོད email སྒམ་ལ་བསྐུར་བའི་ཟུར་སྦྱར་རྣམས་ཡིག་ཆར་སྒྱུར།" +label = "Email འབྱོར་སྒམ" + +[portal.sources.types.folder] +description = "server ཐོག་གི་ཡིག་སྣོད་ཞིག་ལ་ཡིག་ཆ་གསར་པ་ཡོད་མེད་ལྟ་སྲུང་བྱོས།" +label = "ཡིག་སྣོད" + +[portal.sources.types.folder.fields.directory] +helperText = "Stirling གིས་བཟོ་སྦྱོར་བྱ་རྒྱུའི་ཡིག་ཆ་ལྟ་སྲུང་བྱ་སའི་ལམ་བུ་ཆ་ཚང་།" +label = "དཀར་ཆག་ལམ་བུ" +placeholder = "/data/incoming" + +[portal.sources.types.folder.fields.identity] +helperText = "ནང་དོན་ཞིབ་བཤེར་གྱིས་འགྱུར་བ་བྱུང་བའི་ཡིག་ཆ་རེ་རེ་ཀློག་པས། ནང་དོན་མི་འགྱུར་བའི་མིང་བསྒྱུར་དང་རེག་པ་རྣམས་སླར་བཟོ་སྦྱོར་མི་བྱེད།" +label = "འགྱུར་བ་ངོས་འཛིན" + +[portal.sources.types.folder.fields.identity.options] +hash = "ཆེ་ཆུང་། ཚེས་གྲངས་དང་ནང་དོན་ཞིབ་བཤེར" +stat = "ཆེ་ཆུང་དང་བཟོ་བཅོས་ཚེས་གྲངས" + +[portal.sources.types.folder.fields.mode] +label = "ཀློག་སྟངས" + +[portal.sources.types.folder.fields.mode.options] +consume = "Consume: ཡིག་ཆ་རེ་རེ་ཐེངས་གཅིག་བཟོ་སྦྱོར" +snapshot = "Snapshot: འཁོར་རེར་ཡིག་སྣོད་སླར་ཀློག" + +[portal.sources.types.folder.fields.recursive] +label = "ཡིག་སྣོད་གཏིང་ཚད" + +[portal.sources.types.folder.fields.recursive.options] +all = "ཡན་ལག་ཡིག་སྣོད་ཚུད" +top = "རིམ་པ་མཐོ་ཤོས་ཁོ་ན" + +[portal.sources.types.ftp] +description = "FTP ཡང་ན FTPS server ཡིག་སྣོད་ལ་ཡིག་ཆ་གསར་པ་ཡོད་མེད་བསྐོར་ཞིབ་བྱོས།" +label = "FTP" + +[portal.sources.types.googledrive] +description = "མཉམ་སྤྱོད་ Google Drive ཡིག་ཁུག་ནས་ཡིག་ཆ་འཐེན་ལེན་བྱེད།" +label = "Google Drive" + +[portal.sources.types.network] +description = "ཡིག་ཆ་གསར་པའི་ཆེད་ SMB དྲ་རྒྱ་མཉམ་སྤྱོད་ལ་ལྟ་ཞིབ་བྱེད།" +label = "དྲ་རྒྱའི drive" + +[portal.sources.types.onedrive] +description = "ཡིག་ཆ་གསར་པའི་ཆེད་ OneDrive ཡིག་ཁུག་ལ་ལྟ་ཞིབ་བྱེད།" +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 ཡང་ན་ S3-compatible bucket ནས་ཡིག་ཆ་འཐེན་ལེན་བྱེད།" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "bucket དང་དཔང་ཡིག་ཉར་ཚགས་བྱས་པའི་འབྲེལ་མཐུད། དེ་ལ་ཟུར་སྟོན་བྱེད་པའི་འབྱུང་ཁུངས་དང pipeline ཕྱིར་འདོན་ཚང་མས་བསྐྱར་སྤྱོད་བྱེད།" +label = "འབྲེལ་མཐུད" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume ཡིས་སྲིད་ཇུས་ཚང་མས་བ处理་བྱས་རྗེས bucket ནས object རེ་རེ་འདོར་བ་ཡིན།" +label = "ཀློག་སྟངས" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: object རེ་རེ་ཐེངས་གཅིག་བ处理་བྱེད" +snapshot = "Snapshot: འཁོར་ཐེངས་རེར bucket བསྐྱར་ཀློག་བྱེད" + +[portal.sources.types.s3.fields.prefix] +helperText = "གཙོ་ཚིག་འདིས་འགོ་ཚུགས་པའི་ལྡེ་མིག་ཡོད་པའི object ཁོ་ན་བ处理་བྱེད།" +label = "ལྡེ་མིག་གི་སྔོན་ཚིག" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "ཡིག་ཆ་གསར་པའི་ཆེད་ SFTP drop ཡིག་ཁུག་ལ་དུས་བཅད་འདྲི་ཞིབ་བྱེད།" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint ཡིག་ཆ་དཔེ་མཛོད་ནས་ཡིག་ཆ་འཐེན་ལེན་བྱེད།" +label = "SharePoint" + +[portal.sources.types.unknown] +label = "འབྱུང་ཁུངས" + +[portal.sources.types.webhook] +createNote = "webhook འདི་གསར་སྐྲུན་བྱེད་སྐབས delivery URL དང signing secret བង្កើត་འོང་། སྐུར་མཁན་ཚོས URL དེར་ཡིག་ཆ་ POST བྱེད་ཅིང་། ཟུར་སྟོན་སྲིད་ཇུས་རྣམས་འབྱོར་སྐབས་འཁོར་སྐྱོད་བྱེད།" +description = "ཕྱིའི་མ་ལག་གང་རུང་ནས་མིང་རྟགས་ཅན་ HTTP POST བརྒྱུད་ཡིག་ཆ་ལེན།" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "webhook བង្កើត་སྐབས་ signing secret ཐེངས་གཅིག་སྟོན། དེ་བསྒྱུར་དགོས་ན་འབྱུང་ཁུངས་བསྐྱར་སྐྲུན་བྱེད།" + +[portal.sources.types.webhook.reveal] +copy = "འདྲ་བཤུས" +done = "ཚར" +secret = "Signing secret" +secretHelp = "ལྡེ་མིག་འདིས་སྐྱེལ་སྤྲོད་རེ་རེའི raw body ལ་མིང་རྟགས་རྒྱོབ (HMAC-SHA256) ཏེ X-Stirling-Signature header ལྟར་སྐུར།" +secretWarning = "ད་ལྟ་ signing secret འདྲ་བཤུས་བྱེད། ཁྱེད་ཀྱི་བདེ་འཇགས་ཆེད་དེ་ཐེངས་གཅིག་ཁོ་ན་སྟོན་པ་དང་རྗེས་སུ་ཕྱིར་ལེན་མི་ཐུབ།" +title = "Webhook བង្កើត་ཟིན" +url = "Delivery URL" +usage = "ཡིག་ཆ་རེ་རེ raw request body ལྟར delivery URL ལ་ POST བྱེད། binary content type (application/pdf or application/octet-stream) སྤྱོད། ཟུར་སྟོན་སྲིད་ཇུས་རྣམས་འབྱོར་སྐབས་རང་འགུལ་འཁོར་སྐྱོད་བྱེད།" + +[portal.sources.wizard] +name = "མིང་།" +namePlaceholder = "དཔེར་ན། Claims intake" +type = "རིགས" + +[portal.stepModal] +close = "ཁ་རྒྱག" + +[portal.usage] +managePayment = "དངུལ་སྤྲོད་དོ་དམ" +subtitle = "PDF ཚང་མར Stirling གིས་རིན་བསྡུས་པའི་སྤྱོད་ཚད། འཛིན་ཤོག དང་འཆར་གཞི་དོ་དམ་ཚང་མ་འཛིན་སྟེགས་གཅིག་ནང་།" +title = "སྤྱོད་ཚད་ & རིན་བསྡུ" + +[portal.usage.error] +loadWallet = "wallet འཇུག་མ་ཐུབ" +openStripePortal = "Stripe portal ཁ་འབྱེད་མ་ཐུབ" +walletUnavailable = "wallet མེད་པ། {{status}} {{statusText}}" + +[portal.usage.sessionExpired] +action = "ཡང་བསྐྱར་ནང་འཛུལ" +body = "ཁྱེད་ཀྱི Stirling རྩིས་ཁྲའི session དུས་ཡོལ་ཟིན། རིན་བསྡུ་ལྟ་བར་ཡང་བསྐྱར་ནང་འཛུལ་བྱེད — ཁྱེད་ཀྱི instance འབྲེལ་མཐུད་རང་སོར་གནས།" +title = "Session དུས་ཡོལ" + +[portal.users] +title = "སྤྱོད་མཁན" + +[portal.users.roles.admin] +label = "དོ་དམ་པ (Org owner)" +summary = "ལས་གནས། སྒྲིག་འགོད། དང་ཚོགས་མི་ཐོག་ལ་སྤྱི་ཡོངས་དོ་དམ་དབང་ཆ།" + +[portal.users.roles.admin.permissions] +0 = "སྤྱོད་མཁན། རུ་ཁག དང་འགན་དབང་དོ་དམ" +1 = "S3 འབྲེལ་མཐུད་ཚུད་པའི integration ཚང་མ་དོ་དམ" +2 = "portal འཛུལ་སྤྱོད་སྤྲོད་པའམ་ཕྱིར་འཐེན" +3 = "Team Owner གྱིས་བྱེད་ཐུབ་པ་ཚང་མ" + +[portal.users.roles.guest] +label = "མགྲོན་པོ" +summary = "ཚད་བཀག་ཡོད་པའམ web-only འཛུལ་སྤྱོད། སྒེར་གྱི configs ཉར་མི་ཐུབ།" + +[portal.users.roles.guest.permissions] +0 = "Web-only / demo སྤྱོད་སྟངས" +1 = "API ལྡེ་མིག་ཡང་ན integration མེད" +2 = "portal འཛུལ་སྤྱོད་མེད" +3 = "མཉམ་སྤྱོད་ས་ཁུལ་དུ་ཀློག་ཙམ" + +[portal.users.roles.member] +label = "ཚོགས་མི" +summary = "སྤྱིར་བཏང་སྤྱོད་མཁན — མཉམ་སྤྱོད་ཐོན་ཁུངས་དང་རང་གི configs དང་མཉམ་ལས་བྱེད།" + +[portal.users.roles.member.permissions] +0 = "editor དང་མཉམ་སྤྱོད integration སྤྱོད" +1 = "སྒེར་གྱི API & MCP configs བង្កើត" +2 = "ཁོང་ཚོར་མཉམ་སྤྱོད་བྱས་པའི་རུ་ཁག configs མཐོང" +3 = "S3 ཡང་ན་ལས་གནས་དོ་དམ་མེད" + +[portal.users.roles.team_owner] +label = "རུ་ཁག་བདག་པོ" +summary = "རུ་ཁག་ཅིག་བདག་སྐྱོང་བྱེད — དེའི་ཚོགས་མིའི་ཐོན་ཁུངས་དང་མཉམ་སྤྱོད configs དོ་དམ་བྱེད།" + +[portal.users.roles.team_owner.permissions] +0 = "རུ་ཁག་གི S3 འབྲེལ་མཐུད་བង្កើត་ & དོ་དམ" +1 = "རུ་ཁག་བདག་དབང་གི integration configs དོ་དམ" +2 = "སྔོན་སྒྲིག་སྲིད་ཇུས་བརྒྱུད portal འཛུལ་སྤྱོད" +3 = "Member གྱིས་བྱེད་ཐུབ་པ་ཚང་མ" + +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · ཚད་མེད" [printFile] title = "པར་སྐྲུན།" @@ -6629,6 +9547,14 @@ text = "ཁྱེད་རང་གི་PDFཤོག་ངོས་ཆུ་ཚ [rotate.tooltip.header] title = "འཁོར་སྐྱོད་སྒྲིག་སྟངས་ཀྱི་སྤྱི་བསྡོམས།" +[ruler] +clearAll = "ཚང་མ་གསལ་བཤིག" +hideAllLabels = "མཚན་བྱང་ཚང་མ་སྦེད" +hideSmallLabels = "མཚན་བྱང་ཆུང་བ་སྦེད" +physicalValues = "དངོས་གཟུགས" +scaled = "ཚད་བསྒྱུར་ཟིན" +showAllLabels = "མཚན་བྱང་ཚང་མ་སྟོན" + [sanitize] desc = "PDFཡིག་ཆ་ནས་གནོད་འཚེ་ཅན་གྱི་འབྱུང་བ་རྣམས་མེད་པར་བཟོ་དགོས།" sanitizationResults = "གཙང་སྦྲའི་གྲུབ་འབྲས།" @@ -6670,31 +9596,45 @@ label = "XMP མེ་ཊ་ཌ་ཊ་མེད་པར་བཟོས།" [sanitize.steps] settings = "སྒྲིག་སྟངས།" -[sanitizePdf] -tags = "གཙང་མ་དང་བདེ་འཇགས་དང་བདེ་འཇགས་དང་སེལ་བའི་ཉེན་ཁ།" - [sanitizePDF] title = "PDF གཙང་བཟོ་བྱེད་དགོས།" +[sanitizePdf] +tags = "གཙང་མ་དང་བདེ་འཇགས་དང་བདེ་འཇགས་དང་སེལ་བའི་ཉེན་ཁ།" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "ཤོག་ངོས་གཞི་ཁྱོན་སྙོམ་སྒྲིག་བྱེད་པ།" +[scaleSettings] +activeScale = "ལས་འགུལ་ཚད་ཤིང" +apply = "ཚད་ཤིང་སྤྱོད" +applyCalibration = "Calibration སྤྱོད" +calculatedScale = "རྩིས་རྒྱག་ཟིན་པའི་ཚད་ཤིང" +calibrate = "Calibrate" +calibrating = "Calibrating" +calibrationDistanceRequired = "དངོས་ཡོད་རྒྱང་ཐག་ཀླད་ཀོར་ལས་ཆེ་བ་ཞིག་འཇུག" +calibrationInvalid = "ཚད་འཇལ་འདི་ནས་ཚད་ཤིང་རྩིས་རྒྱག་མི་ཐུབ" +calibrationPaperDistance = "ཚད་འཇལ་བྱས་པའི་ཤོག་ངོས་རྒྱང་ཐག: {{distance}}" +calibrationTitle = "ཚད་ཤིང Calibrate" +calibrationTooltip = "ཤེས་ཟིན་པའི་རྒྱང་ཐག་ཅིག་ཚད་འཇལ་བྱས་ནས་ཚད་ཤིང་རང་འགུལ་རྩིས་རྒྱག" +customScale = "རང་སྒྲིག་ཚད་ཤིང" +noneSet = "རང་སྒྲིག་ཚད་ཤིང་སྒྲིག་མེད" +presets = "སྔོན་སྒྲིག་ཚད་ཤིང" +ratio = "ཚད་ཤིང་བསྡུར་ཚད" +ratioHelp = "བསྡུར་ཚད: ཤོག་ངོས་འཇལ་ཚད་ 1 = དངོས་ཡོད་འཇལ་ཚད་ X" +ratioPlaceholder = "དཔེར་ན། 100" +ratioPositive = "བསྡུར་ཚད་ཀླད་ཀོར་ལས་ཆེ་དགོས" +ratioRequired = "བསྡུར་ཚད་དགོས་ངེས" +realDistance = "དངོས་ཡོད་རྒྱང་ཐག" +realDistancePlaceholder = "དཔེར་ན། 5" +reset = "སྔོན་སྒྲིག་ཐང་ལ་སླར་སྒྲིག" +unit = "འཇལ་ཚད" +unitInvalid = "ནུས་མེད་འཇལ་ཚད: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "ཟུར་ཚད་ཚད་གཞི།" -10 = "བཀག་སྡོམ་བྱས་པའི་པར་རིས་རེ་རེའི་མཐའ་འཁོར་དུ་(པིག་སེལ་ནང་) མཐའ་མཚམས་གཅོད་མི་དགོས།" -2 = "ང་ཚོས་པར་ཞིག་རང་འགུལ་གྱིས་ཐད་ཀར་མ་བརྒྱབ་གོང་ལ་གཡོ་འགུལ་(ཚད་གཞི་ནང་)དགོས།" -3 = "བཟོད་སྲན:" -4 = "ཚོས་གཞི་གང་འདྲའི་ཉེ་པོ་དེ་ཤོག་ངོས་རྒྱབ་ལྗོངས་ལ་མཐུན་དགོས། རྒྱབ་ལྗོངས་སུ་བརྩི་དགོས། མཐོ་བ་ = ལྷོད་པོ། དམའ་བ། = དམ་དྲག་ཅན།" -5 = "ཆུང་ཤོས་ས་ཁུལ།" -6 = "པར་རིས་ཀྱི་ཚད་གཞི་ཆུང་ཤོས་དེ་(པར་རིས་༢ནང་)ང་ཚོས་ཆ་ཤས་ཆུང་ཆུང་ལས་གཡོལ་ཐབས་བྱེད།" -7 = "ཆེས་ཆུང་བའི་བར་མཚམས་ཀྱི་ས་ཁུལ།" -8 = "མཐའ་ཆུང་ཤོས་དང་བཟོ་ལྟ་ནི་ང་ཚོས་པར་རིས་འཚོལ་སྐབས་བསམ་གཞིག་བྱེད་ཀྱི་ཡོད།" -9 = "མཐའ་མཚམས་ཀྱི་ཚད་གཞི།" - [scannerImageSplit] submit = "པར་རིས་པར་ལེན་འདོན་པ།" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -6726,6 +9666,18 @@ whatThisDoes = "འདིས་ཅི་ཞིག་བྱེད་ཀྱིན whatThisDoesDesc = "རང་འགུལ་གྱིས་པར་ལེན་བྱས་པའི་ཤོག་ངོས་ཡང་ན་མཉམ་སྦྱོར་གྱི་པར་རིས་ནས་པར་རེ་རེ་འཚོལ་བ་དང་འདོན་ཐུབ།" whenToUse = "བེད་སྤྱོད་གཏོང་བའི་དུས་ཚོད།" +[ScannerImageSplit.selectText] +1 = "ཟུར་ཚད་ཚད་གཞི།" +10 = "བཀག་སྡོམ་བྱས་པའི་པར་རིས་རེ་རེའི་མཐའ་འཁོར་དུ་(པིག་སེལ་ནང་) མཐའ་མཚམས་གཅོད་མི་དགོས།" +2 = "ང་ཚོས་པར་ཞིག་རང་འགུལ་གྱིས་ཐད་ཀར་མ་བརྒྱབ་གོང་ལ་གཡོ་འགུལ་(ཚད་གཞི་ནང་)དགོས།" +3 = "བཟོད་སྲན:" +4 = "ཚོས་གཞི་གང་འདྲའི་ཉེ་པོ་དེ་ཤོག་ངོས་རྒྱབ་ལྗོངས་ལ་མཐུན་དགོས། རྒྱབ་ལྗོངས་སུ་བརྩི་དགོས། མཐོ་བ་ = ལྷོད་པོ། དམའ་བ། = དམ་དྲག་ཅན།" +5 = "ཆུང་ཤོས་ས་ཁུལ།" +6 = "པར་རིས་ཀྱི་ཚད་གཞི་ཆུང་ཤོས་དེ་(པར་རིས་༢ནང་)ང་ཚོས་ཆ་ཤས་ཆུང་ཆུང་ལས་གཡོལ་ཐབས་བྱེད།" +7 = "ཆེས་ཆུང་བའི་བར་མཚམས་ཀྱི་ས་ཁུལ།" +8 = "མཐའ་ཆུང་ཤོས་དང་བཟོ་ལྟ་ནི་ང་ཚོས་པར་རིས་འཚོལ་སྐབས་བསམ་གཞིག་བྱེད་ཀྱི་ཡོད།" +9 = "མཐའ་མཚམས་ཀྱི་ཚད་གཞི།" + [search] noResults = "གྲུབ་འབྲས་མ་ཐོབ།" placeholder = "མིང་ཚིག་འཚོལ་ཞིབ་ནང་འཇུག་དགོས།" @@ -6743,46 +9695,22 @@ toolNotAvailableLocally = "ཁྱེད་ཀྱི Stirling-PDF ཞབས་ཞ [session] expired = "ཁྱེད་རང་གི་ཚོགས་ཐུན་དུས་ཚོད་རྫོགས་ཡོད། ཤོག་བུ་གསར་བཟོ་བྱས་ནས་བསྐྱར་དུ་ཚོད་ལྟ་གནང་རོགས།" -[sessionManagement.tooltip] -header = "མཛུབ་འགན་ལས་འཁུར་དོ་དམ" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "ཞུགས་མཁན་གསར་པ་ཚོས་མཛུབ་འགན་རིམ་པའི་མཇུག་ཏུ་ཁ་སྣོན" -bullet2 = "ལས་འཁུར་མཇུག་བསྡུས་རྗེས་ཞུགས་མཁན་ཁ་སྣོན་མི་ཐུབ" -bullet3 = "ཞུགས་མཁན་རེ་རེ་ལ་ཁོང་ཚོའི་རྗེས་རུ་གསར་བརྡ་འབྱོར" -description = "མཇུག་བསྡུ་སྔོན་ལ་དུས་གང་རུང་ལས་ལས་འཁུར་ན་ཞུགས་མཁན་ཁ་སྣོན་ཆོག." -title = "ཞུགས་མཁན་ཁ་སྣོན" - -[sessionManagement.tooltip.finalization] -bullet1 = "མཇུག་བསྡུས་པ་ཡོངས: ཞུགས་མཁན་ཚང་མ་མཛུབ་འགན་བཀལ་ཟིན" -bullet2 = "མཇུག་བསྡུས་པ་ཕྱོགས་མེད: ཞུགས་མཁན་ཁ་ཤས་ད་མ་མཛུབ་འགན" -bullet3 = "མཛུབ་འགན་མི་བཀལ་མི་ཚོ་མཇུག་ཡིག་ཆ་ནས་བུ་གཏོར་བྱེད" -bullet4 = "མཇུག་བསྡུས་རྗེས མཛུབ་འགན་PDF འགུལ་སྤྱོད་ཡིག་ཆ་ནང་འཇུག་ཆོག" -description = "མཇུག་བསྡུས་པས་མཛུབ་འགན་ཡོངས་ཀྱིས་གཅིག་པའི་མཛུབ་འགན་PDF བཟོ་བྱེད། འདི་བསྐྱར་མེད།" -title = "ལས་འཁུར་མཇུག་བསྡུས" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "མཛུབ་འགན་བཀལ་ཟིན་པའི་ཞུགས་མཁན་བསུབ་མི་ཐུབ" -bullet2 = "བསུབ་ཟིན་པའི་ཞུགས་མཁན་ལ་གསར་བརྡ་མི་འབྱོར" -bullet3 = "མཛུབ་འགན་རིམ་པ་རང་འགུལ་གྱིས་བཅོས" -description = "ཞུགས་མཁན་ཚོ་མཛུབ་འགན་བཀལ་མ་སྔོན་ལས་ལས་འཁུར་ནས་བསུབ་ཆོག." -title = "ཞུགས་མཁན་བསུབ" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "མཛུབ་འགན་རེ་རེ་བསྟུན་པར PDF ལ་འཇུག" -bullet2 = "རྗེས་མའི་མཛུབ་འགན་པས་སྔོན་མའི་མཛུབ་འགན་ལྟ་ཆོག" -bullet3 = "ཁྲིམས་ལྡན་གྲོས་མཐུན་དང་ལག་ལེན་ལ་གལ་ཆེ།" -description = "ཁྱེད་ཀྱིས་ལས་འཁུར་བཟོས་སྐབས་བཀོད་པའི་རིམ་པས་སོ་སོ་ཇི་ལྟར་མཛུབ་འགན་བཀལ་དགོས་ཆེད་གཏན་འཁེལ།" -title = "མཛུབ་འགན་རིམ་པ" - [settings] close = "ཁ་རྒྱག" +[settings.ai] +documents = "ཡིག་ཆ་ & RAG" +general = "སྤྱིར་བཏང" +limits = "ཚད་བཀག་ & ལས་ཆོད" +models = "དཔེ་གཞི་ & མཁོ་སྤྲོད་པ" +title = "AI" + [settings.configuration] advanced = "ཡན་ཐོན་ཅན" database = "གཞི་གྲངས་རྟེན་གཞི།" endpoints = "མཇུག་སྡོམ།" features = "ཁྱད་ཆོས" +folderAccess = "ཡིག་ཁུག་འཛུལ་སྤྱོད" mcp = "MCP Server" storageSharing = "ཡིག་ཆའི་གསོག་འཇོག་དང་མཉམ་སྤྱོད" systemSettings = "མ་ལག་སྒྲིག་བཀོད།" @@ -6816,6 +9744,8 @@ defaultPdfEditor = "སྔོན་སྒྲིག་PDFརྩོམ་སྒྲ defaultPdfEditorActive = "སི་ཊར་ལིང་PDFནི་ཁྱེད་ཀྱི་སྔོན་སྒྲིག་PDFརྩོམ་སྒྲིག་པ་ཡིན།" defaultPdfEditorChecking = "ཞིབ་བཤེར་བྱེད་པ་..." defaultPdfEditorInactive = "མཉེན་ཆས་གཞན་ཞིག་ནི་སྔོན་སྒྲིག་ལྟར་སྒྲིག་ཡོད།" +defaultPdfEditorRemind = "སྔོན་སྒྲིག་ལྟར་སྒྲིག་དགོས་པ་དྲན་སྐུལ" +defaultPdfEditorRemindDescription = "Stirling PDF ཁྱེད་ཀྱི་སྔོན་སྒྲིག PDF ཉེར་སྤྱོད་མིན་སྐབས་བརྡ་ཁྱབ་ཤར་སྟོན།" defaultPdfEditorSet = "ད་ལྟ་སྔོན་སྒྲིག་བྱས་ཟིན།" defaultStartupView = "མཉེན་ཆས་འགོ་སློང་སྐབས་སྔོན་སྒྲིག་མཐོང་རིས" defaultStartupViewDescription = "མཉེན་ཆས་འགོ་སློང་སྐབས་གཡོན་ཕྱོགས་ཀྱི་མཆོང་ལྡེ་གང་ཞིག་འགུལ་སྐྱོད་ཡོད་པར་བཟོ་ནའི་གདམ་གསེས་བྱ།" @@ -6833,9 +9763,18 @@ languageDescription = "མངོན་འཆར་སྐད་ཡིག་འད logout = "ཕྱིར་ཐོབ།" setAsDefault = "སྔོན་སྒྲིག་ལྟར་སྒྲིག་དགོས།" theme = "དབྱིབས" +themeAccent = "ནན་སྟོན་ཁ་དོག" +themeAccentCustom = "རང་སྒྲིག་ཁ་དོག" +themeAccentCustomHint = "རང་སྒྲིག (སྔོན་སྒྲིག་འོས་སྦྱོར)" +themeAccentDark = "མདོག་ནག་རྣམ་པའི་ནན་སྟོན་ཁ་དོག" +themeAccentDefault = "སྔོན་སྒྲིག" +themeAccentDefaultHint = "སྔོན་སྒྲིག་བརྗོད་གཞི (འོས་སྦྱོར)" +themeAccentDescription = "མཐེབ་གནོན། link དང་ནན་སྟོན་ཁག་དེ་ལ་བསྟུན་ཞིང་། app ལ་འཇམ་པོའི་མདོག་སྣོན་བྱེད། མདོག་སྐྱ་དང་མདོག་ནག་རེ་རེར་རང་སོའི་ཁ་དོག་ཡོད། System གིས་ལས་འགུལ་ཡོད་པ་དེ་སྤྱོད།" +themeAccentLight = "མདོག་སྐྱ་རྣམ་པའི་ནན་སྟོན་ཁ་དོག" themeDark = "ནག་པོ" themeDescription = "དཀར་པོའི་ཐབས་ལམ་དང་ནག་པོའི་ཐབས་ལམ་བར་བསྒྱུར" themeLight = "དཀར་པོ" +themeReset = "བརྗོད་གཞི་སྔོན་སྒྲིག་ལ་སླར་གསོ" themeSystem = "System" title = "སྤྱིར་བཏང་གི" user = "བེད་སྤྱོད།" @@ -6849,6 +9788,12 @@ intro = "ཁྱེད་རང་གི་སྒྲིག་འཛུགས་ learnMore = "ཡིག་ཚགས་ནང་དུ་མང་ཙམ་ཤེས་དགོས།" title = "མ་ལག་འཛིན་སྐྱོང་པ་ལ།" +[settings.general.loginLanding] +description = "Stirling Cloud ལ་ནང་འཛུལ་བྱས་རྗེས app གང་ཁ་འབྱེད་དགོས་འདེམས།" +editor = "Editor" +processor = "Processor" +title = "ནང་འཛུལ་རྗེས" + [settings.general.mode] fullscreen = "བརྙན་ཤེལ་ཆ་ཚང་།" sidebar = "ཟུར་པར།" @@ -6934,6 +9879,21 @@ manage = "Manage" description = "Policies and legal information for this service." title = "Legal Documents" +[settings.licenses] +backendDescription = "server འདིར་མཉམ་སྡེབ་བྱས་པའི backend dependencies ཡི licenses།" +backendLabel = "Backend Licenses" +backendTitle = "Backend 3rd Party Licenses" +empty = "dependencies མ་རྙེད།" +frontendDescription = "release build ནང་མཉམ་སྡེབ་བྱས་པའི frontend dependencies ཡི licenses།" +frontendLabel = "Frontend Licenses" +frontendTitle = "Frontend 3rd Party Licenses" +license = "License" +listDescription = "ཐོ་འདི release bundle ཡང་ན backend endpoint ནས UI ནང་ཐད་ཀར་སྟོན།" +listTitle = "མཉམ་སྡེབ dependencies" +loadError = "third-party licenses འཇུག་མ་ཐུབ" +module = "Module" +version = "Version" + [settings.licensingAnalytics] audit = "རྩིས་ཞིབ་པ།" plan = "འཆར་གཞི" @@ -7014,9 +9974,6 @@ title = "སྲིད་ཇུས་དང་གསང་བ།" [settings.preferences] title = "འདོད་པ།" -[settings.search] -placeholder = "སྒྲིག་འགོད་ཤོག་ངོས་འཚོལ..." - [settings.security] description = "ཁྱེད་རང་གི་རྩིས་ཁྲ་དེ་བདེ་འཇགས་ཡོང་བའི་ཆེད་དུ་གསང་གྲངས་གསར་བརྗེ་བྱེད་དགོས།" title = "ཉེན་སྲུང" @@ -7042,6 +9999,11 @@ security = "ཉེན་སྲུང" telegram = "Telegram" title = "བདེ་འཇགས་དང་བདེན་དཔང་།" +[settings.signOut] +confirm = "ཁྱེད་རང་ཕྱིར་འཐོན་བྱེད་གཏན་འཁེལ་ཡིན་ནམ།" +submit = "ཕྱིར་འཐོན" +title = "ཕྱིར་འཐོན" + [settings.team] title = "ཚོགས་པ" @@ -7157,6 +10119,7 @@ last = "ཤོག་ངོས་མཐའ་མ།" maintainRatio = "འགྱུར་ལྡོག་རྒྱུན་འཛིན་གྱི་ཆ་སྙོམས།" next = "ཤོག་ངོས་རྗེས་མ།" noSavedSigs = "ཉར་ཚགས་བྱས་པའི་མཚན་རྟགས་རྙེད་མེད།" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "མི་སྒེར་གྱི་མཚན་རྟགས།" previous = "ཤོག་ངོས་གོང་མ།" redo = "བསྐྱར་དུ་བྱེད།" @@ -7195,6 +10158,7 @@ backgroundRemovalFailedTitle = "རྒྱབ་ལྗོངས་མེད་པ hint = "ཁྱེད་རང་གི་མིང་རྟགས་བཀོད་པའི་PNG ཡང་ན་JPG པར་རིས་ཤིག་བླངས།" label = "Upload མཚན་རྟགས་པར་རིས།" placeholder = "པར་རིས་ཡིག་ཆ་འདེམས།" +previewAlt = "Current image signature" processing = "ལས་སྣོན་པར་རིས་..." removeBackground = "རྒྱབ་ཤ་དཀར་པོའི་རྒྱབ་ལྗོངས་མེད་པར་བཟོས།" @@ -7208,6 +10172,17 @@ saved = "གོང་དུ་ཉར་ཚགས་བྱས་པའི་མ text = "གོང་དུ་ཁྱེད་ཀྱི་མིང་བླུགས་རྗེས་PDFསྟེང་གང་དུ་ཡང་སྣུན་ནས་མིང་རྟགས་བཀོད་དགོས།" title = "མཚན་རྟགས་ཁ་སྣོན་བྱེད་ཐབས།" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "སྒུལ་བདེའི་མཚན་རྟགས།" pause = "བར་མཚམས་བཞག་པ།" @@ -7654,10 +10629,30 @@ title = "སར་བར་ལ་ཡར་བསྐུར།" updateButton = "སར་བར་ནང་གསར་སྒྱུར།" uploadButton = "སར་བར་ལ་ཡར་བསྐུར།" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "འདིར་གནོན" +fallback = "གལ་ཏེ་རང་འགུལ་ཁ་མ་ཕྱེ་ན།" +opening = "Swagger UI འདི tab གསར་པར་ཁ་འབྱེད་བཞིན་པ..." tags = "api, ཡིག་ཆ། ཐང་ག། མཐའ་སྟོན། འཕེལ་རྒྱས།" title = "APIཡིག་ཆ།" @@ -7686,7 +10681,9 @@ memberRemoved = "སྡེ་ཚོགས་མི་སུབ་ལེགས་ namePlaceholder = "ཚོགས་པའི་མིང་འཇུག" personal = "རང་དོན" removeError = "སྡེ་ཚོགས་མི་སུབ་མ་ཐུབ" +renameCancel = "མིང་བསྐྱར་འདོགས་དོར" renameError = "ཚོགས་པའི་མིང་བསྐྱར་བཏགས་ཕམ་པ" +renameSubmit = "རུ་ཁག་མིང་ཉར" renameSuccess = "ཚོགས་པའི་མིང་བསྐྱར་བཏགས་ལེགས་པར།" [team.invitationBanner] @@ -7702,6 +10699,7 @@ sendButton = "འདྲེན་བརྡ་གཏོང་" title = "ཚོགས་མི་འདྲེན་བརྡ" [team.members] +actions = "ཚོགས་མིའི་བྱ་སྤྱོད" emailColumn = "Email" empty = "ཚོགས་མི་ད་དུང་མེད" nameColumn = "མིང་" @@ -7762,12 +10760,39 @@ selectFilesHint = "ལག་ཆ་འདི་སྒྲུབ་བར Active Fi singleFileScope = "འཇུག་སྤྱོད་: {{fileName}} ཁོ་ན" viewerMode = "ཡིག་ཆ་མང་པོ་སེལ་བར་ཡིག་ཆ་ཞུན་དག་ཆས་ལ་བསྒྱུར།" +[toolFormat] +ANY = "ཡིག་ཆ་གང་རུང" +CBR = "འདྲ་རིས་དེབ་ཀྱི་ཉར་ཚགས་ (CBR)" +CBZ = "འདྲ་རིས་དེབ་ཀྱི་ཉར་ཚགས་ (CBZ)" +CSV = "CSV ཡིག་ཆ་ཞིག" +EBOOK = "གློག་དེབ་ཅིག" +EMAIL = "གློག་ཡིག་ཅིག" +EXCEL = "རེའུ་མིག་ཡིག་ཆ་ཞིག" +HTML = "དྲ་ངོས་ཤིག" +IMAGE = "པར་རིས།" +JAVASCRIPT = "JavaScript ཡིག་ཆ་ཞིག" +JSON = "JSON ཡིག་ཆ་ཞིག" +MARKDOWN = "Markdown ཡིག་ཆ་ཞིག" +NONE = "ཡིག་ཆ་མེད།" +PCL = "PCL པར་འདེབས་ཡིག་ཆ་ཞིག" +PDF = "PDF ཞིག" +PDF_ENCRYPTED = "གསང་ཨང་གིས་སྲུང་བའི PDF ཞིག" +POSTSCRIPT = "PostScript ཡིག་ཆ་ཞིག" +PPT = "གསལ་སྟོན་ཡིག་ཆ་ཞིག" +TEXT = "ཡིག་རྐྱང་ཡིག་ཆ་ཞིག" +VIDEO = "བརྙན་ཞིག" +WORD = "Word ཡིག་ཆ་ཞིག" +XML = "XML ཡིག་ཆ་ཞིག" +XPS = "XPS ཡིག་ཆ་ཞིག" +ZIP = "ZIP ཉར་ཚགས་ཤིག" + [toolPanel] alpha = "ཨལ་ཕ།" backToAllTools = "ལག་ཆས་ཚང་མར་ཕྱིར་ལོག" collapse = "པང་གཞི་བསྡམས" expand = "པང་གཞི་རྒྱས" goBack = "ཕྱིར་ལོག" +pdfTools = "PDF ལག་ཆ།" placeholder = "འགོ་འཛུགས་བྱེད་པའི་ལག་ཆ་ཞིག་འདེམས།" premiumFeature = "གོང་ཚད་མཐོ་བའི་ཁྱད་ཆོས།" toolsHeader = "ལག་ཆས" @@ -7804,11 +10829,6 @@ noToolsFound = "ལག་ཆ་གང་ཡང་མཐོང་མ་བྱུ quickAccess = "མགྱོགས་མྱུར་ངང་འགྲོ་འོང་།" searchPlaceholder = "འཚོལ་ཞིབ་ལག་ཆ།" -[toolPicker.categories] -advancedTools = "ཡར་ཐོན་ལག་ཆ།" -recommendedTools = "འོས་སྦྱོར་བྱས་པའི་ལག་ཆ།" -standardTools = "ཚད་ལྡན་གྱི་ལག་ཆ།" - [toolPicker.subcategories] advancedFormatting = "ཡར་ཐོན་རྣམ་གཞག་བཟོ་བ།" automation = "རང་འགུལ་ཅན།" @@ -7822,6 +10842,9 @@ removal = "བཏོན་པ།" signing = "མིང་རྟགས་བཀོད་པ།" verification = "བདེན་དཔང་།" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "ལག་ཆ་གང་ཡང་མཐོང་མ་བྱུང་།" noTools = "ལག་ཆ་མེད་པ།" @@ -7933,6 +10956,181 @@ title = "ཞིབ་ཕྲའི་གྲངས་གཞི།" unknownEndpoint = "མཇུག་སྣེ་མི་ཤེས" visits = "གཟིགས་སྐོར་བ།" +[users] +lastActive = "མཐའ་མའི་བྱ་འགུལ།" +learnMore = "འགན་འཁྲི་དང་འཛུལ་སྤྱོད་སྐོར་སྔར་ལས་མང་བ་ཤེས་པ།" +locked = "སྒོ་བརྒྱབ་ཟིན།" +roleFor = "{{name}} ཡི་འགན་འཁྲི།" +rowActions = "{{name}} ཡི་བྱ་སྤྱོད།" +showAll = "{{count}} ཚང་མ་སྟོན།" +showLess = "ཉུང་ཙམ་སྟོན།" +subtitle2 = "ཁྱེད་ཀྱི་མི་སྣ། ཚོགས་ཆུང་། དང་འཛུལ་སྤྱོད་རིམ་པ།" +suspended = "གནས་སྐབས་མཚམས་བཞག་ཟིན།" +teamActions = "ཚོགས་ཆུང་བྱ་སྤྱོད།" +title = "སྤྱོད་མཁན།" +you = "(ཁྱེད།)" + +[users.action] +cancelInvite = "གདན་ཞུ་མེད་པར་བཟོ།" +deleteTeam = "ཚོགས་ཆུང་སུབ།" +disableMfa = "MFA བསྐྱར་སྒྲིག" +move = "ཚོགས་ཆུང་ལ་སྤོ།" +reinstate = "བསྐྱར་སྐྱར་སྤྱོད།" +remove = "རྩ་འཛུགས་ནས་སྤོར།" +removeTeam = "ཚོགས་ཆུང་ནས་སྤོར།" +rename = "ཚོགས་ཆུང་གི་མིང་བསྐྱར་འདོགས།" +resetPw = "གསང་ཨང་བསྐྱར་སྒྲིག" +suspend = "གནས་སྐབས་མཚམས་འཇོག" +unlock = "རྩིས་ཁྲའི་སྒོ་ཕྱེ།" + +[users.activity] +daysAgo = "ཉིན་{{count}} སྔོན།" +hoursAgo = "ཆུ་ཚོད་{{count}} སྔོན།" +justNow = "ད་ལྟ་རང་།" +minutesAgo = "སྐར་མ་{{count}} སྔོན།" +monthsAgo_one = "ཟླ་{{count}} སྔོན།" +monthsAgo_other = "ཟླ་{{count}} སྔོན།" +never = "གཏན་ནས་མེད།" +weeksAgo_one = "གཟའ་འཁོར་{{count}} སྔོན།" +weeksAgo_other = "གཟའ་འཁོར་{{count}} སྔོན།" +yearsAgo_one = "ལོ་{{count}} སྔོན།" +yearsAgo_other = "ལོ་{{count}} སྔོན།" + +[users.cap] +addProcessor = "+ ལས་སྣོན་པ།" +approver = "སྲིད་ཇུས་ཆོག་མཆན་སྤྲོད་པ།" +editor = "རྩོམ་སྒྲིག་པ།" +processor = "ལས་སྣོན་པ།" + +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + +[users.confirm] +cancelInviteBody = "{{email}} ལ་བསྐུར་བའི་གདན་ཞུ་མེད་པར་བཟོ་དགོས་སམ། ཁོང་ཚོས་ད་ལྟའི་སྦྲེལ་ཐག་སྤྱད་ནས་ཞུགས་མི་ཐུབ།" +cancelInviteTitle = "གདན་ཞུ་མེད་པར་བཟོ།" +deleteTeamBody = "{{name}} ཚོགས་ཆུང་སུབ་དགོས་སམ། སྔོན་ལ་ཚོགས་ཆུང་དེ་སྟོང་པ་ཡིན་དགོས་ - ཁོང་གི་ཁོངས་མི་རྣམས་ཚོགས་ཆུང་གཞན་ལ་སྤོས། ད་དུང་མཉམ་སྦྲེལ་སྒྲིག་འགོད་གང་ཡང་བདག་བཟུང་བྱས་མི་ཆོག" +deleteTeamTitle = "ཚོགས་ཆུང་སུབ།" +disableMfaBody = "{{name}} ཡི MFA ཐོ་འགོད་སུབ་དགོས་སམ། དགོས་མཁོ་ཡོད་ན། ཁོང་ཚོས་ཐེངས་རྗེས་མར་ནང་འཛུལ་སྐབས་ཡང་བསྐྱར་སྒྲིག་དགོས།" +disableMfaTitle = "MFA བསྐྱར་སྒྲིག" +removeBody = "{{name}} རྩ་འཛུགས་ནས་གཏན་འཇགས་སུ་སྤོར་དགོས་སམ། འདི་ཕྱིར་བསྒྱུར་མི་ཐུབ།" +removeTeamBody = "{{name}} ཚོགས་ཆུང་ནས་སྤོར་དགོས་སམ། ཁོང་ཚོས་རྩིས་ཁྲ་ཉར་བ་ཡིན་ཡང་། ཚོགས་ཆུང་འདིའི་ཐོན་ཁུངས་ལ་འཛུལ་སྤྱོད་དབང་ཚད་ཤོར་འགྲོ" +removeTitle = "ཁོངས་མི་སྤོར།" + +[users.empty] +description = "མཉམ་ལས་འགོ་འཛུགས་ཆེད་ཁྱེད་ཀྱི་ཚོགས་ཆུང་ལ་གདན་ཞུ་བྱོས།" +title = "ད་དུང་ཁོངས་མི་མེད།" + +[users.group] +addToTeam = "ཚོགས་ཆུང་ལ་སྣོན།" +guestCount = "མགྲོན་པོ་{{count}}" +guests = "མགྲོན་པོ།" +ledBy = "{{owner}} ཡིས་འགོ་ཁྲིད་བྱས།" +org = "རྩ་འཛུགས།" +owners = "བདག་པོ་{{count}}" +team = "{{name}} ཚོགས་ཆུང་།" +teamMeta = "མི་{{count}}" + +[users.invite] +access = "འཛུལ་སྤྱོད་དབང་ཚད།" +action = "མི་སྣ་གདན་ཞུ།" +authOauth = "OAuth2 / SSO" +authSaml = "SAML 2.0" +authType = "ནང་འཛུལ་ཐབས་ལམ།" +authWeb = "གསང་ཨང་།" +create = "རྩིས་ཁྲ་བཟོ།" +createSubtitle = "གསང་ཨང་ངམ SSO སྤྱད་ནས་རང་གིས་སྐྱོང་བའི་རྩིས་ཁྲ་བཟོ།" +createTitle = "རྩིས་ཁྲ་བཟོ།" +editorDesc = "Stirling PDF Editor ནང PDF རྩོམ་སྒྲིག་བྱོས། མི་ཚང་མར་འདི་ཐོབ།" +email = "གློག་ཡིག་ཁ་བྱང་།" +emailError = "ནུས་ལྡན་གློག་ཡིག་ཁ་བྱང་ཞིག་འཇུག་རོགས།" +emailPlaceholder2 = "name@company.com" +forceChange = "ཐེངས་དང་པོར་ནང་འཛུལ་སྐབས་གསང་ཨང་བརྗེ་དགོས་པར་བཟོ།" +forceMfa = "ཐེངས་དང་པོར་ནང་འཛུལ་སྐབས MFA སྒྲིག་དགོས་པར་བཟོ།" +method = "ཁོང་ཚོ་ཇི་ལྟར་སྣོན་པ།" +methodDirect = "ཐད་ཀར་རྩིས་ཁྲ་བཟོ།" +methodEmail = "གློག་ཡིག་བརྒྱུད་གདན་ཞུ།" +password = "གསང་ཨང་།" +passwordError = "གསང་ཨང་ཉུང་མཐར་ཡིག་འབྲུ་8 ཡིན་དགོས།" +processorDeferred = "གདན་ཞུ་བསྐུར་ཟིན། འོན་ཀྱང་ལས་སྣོན་པའི་འཛུལ་སྤྱོད་ད་དུང་སྤྲོད་མ་ཐུབ་ - ཁོང་ཚོ་ཞུགས་རྗེས་མིང་ཐོ་ནས་སྒྲིག་རོགས།" +processorDesc = "སྐྱོང་སྡོམ་ངོས། pipelines འཁོར་སྐྱོད། agents དང API།" +role = "འགན་འཁྲི།" +send2 = "གདན་ཞུ་བསྐུར།" +subtitle2 = "ཁོང་ཚོས་ཁྱེད་ཀྱི Stirling ལས་ཁུལ་ལ་ཞུགས་ཆེད་གློག་ཡིག་ཞིག་འཐོབ།" +team = "ཚོགས་ཆུང་།" +title = "མི་སྣ་གདན་ཞུ།" +username = "སྤྱོད་མིང་།" +usernameError = "སྤྱོད་མིང་ཉུང་མཐར་ཡིག་འབྲུ་3 ཡིན་དགོས།" +usernamePlaceholder = "jsmith" + +[users.invites] +by = "{{who}} ཡིས་གདན་ཞུ་བྱས།" +cancel = "མེད་པར་བཟོ།" +expiresInDays_one = "ཉིན་{{count}} ནང་དུས་ཚོད་ཡོལ།" +expiresInDays_other = "ཉིན་{{count}} ནང་དུས་ཚོད་ཡོལ།" +expiresToday = "དེ་རིང་དུས་ཚོད་ཡོལ།" +title = "ངང་སྒུག་གདན་ཞུ།" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + +[users.loadError] +description = "backend ལ་འབྲེལ་སྐབས་གནད་དོན་ཞིག་བྱུང་ངམ། ཡང་ན་ཁྱེད་ལ་འཛུལ་སྤྱོད་དབང་ཚད་མེད། ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།" +title = "ཁོངས་མི་འཇུག་མ་ཐུབ།" + +[users.moveTeam] +apply = "སྤོ།" +team = "ཚོགས་ཆུང་།" +title = "ཚོགས་ཆུང་ལ་སྤོ།" + +[users.newTeam] +action = "+ ཚོགས་ཆུང་གསར་པ།" +create = "ཚོགས་ཆུང་བཟོ།" +emailError = "ནུས་ལྡན་གློག་ཡིག་ཁ་བྱང་ཞིག་འཇུག་རོགས།" +name = "ཚོགས་ཆུང་མིང་།" +namePlaceholder = "དཔེར་ན། དངུལ་རྩིས།" +nameRequired = "ཚོགས་ཆུང་མིང་དགོས་ངེས་ཡིན།" +owner = "ཚོགས་ཆུང་བདག་པོ།" +ownerFailed = "ཚོགས་ཆུང་བཟོས་ཟིན། འོན་ཀྱང་བདག་པོ་གདན་ཞུ་བྱ་མ་ཐུབ། མིང་ཐོ་ནས་གཅིག་བསྐོ་རོགས།" +ownerHelper = "ཚོགས་ཆུང་རེ་རེར་འགོ་ཁྲིད་མཁན་ཞིག་ཡོད། ཁོང་ཚོ་ཚོགས་ཆུང་བདག་པོར་གདན་ཞུ་བྱེད། ལས་སྣོན་པ་སྤྱོད་ནུས་ཡོད། དེས་གཞན་རྣམས་སྣོན་ཐུབ། རྗེས་སུ་བསྐྱར་བསྐོ་བྱ་ཐུབ།" +ownerPlaceholder = "owner@company.com" +subtitle = "ཁོང་ཚོའི་འཛུལ་སྤྱོད་སྐྱོང་མཁན་ཚོགས་ཆུང་བདག་པོའི་འོག་མི་སྣ་སྡེབ་སྒྲིག་བྱོས།" +title = "ཚོགས་ཆུང་གསར་པ།" + +[users.renameTeam] +apply = "མིང་བསྐྱར་འདོགས།" +name = "ཚོགས་ཆུང་མིང་།" +title = "ཚོགས་ཆུང་གི་མིང་བསྐྱར་འདོགས།" + +[users.resetPw] +apply = "གསང་ཨང་བསྐྱར་སྒྲིག" +confirm = "གསང་ཨང་གཏན་འཁེལ།" +copied = "དྲས་སྦྱར་པང་ལ་འདྲ་བཤུས་ཟིན།" +copyHint = "ད་ལྟ་འདི་འདྲ་བཤུས་བྱོས་ - ཡང་བསྐྱར་མི་སྟོན།" +email = "བསྐྱར་སྒྲིག་སྐོར་སྤྱོད་མཁན་ལ་གློག་ཡིག་བསྐུར།" +forceChange = "ཐེངས་རྗེས་མར་ནང་འཛུལ་སྐབས་གསང་ཨང་བརྗེ་དགོས་པར་བཟོ།" +generate = "བདེ་འཇགས་གསང་ཨང་ཞིག་སྐྱེད་སྐྲུན།" +includePw = "གློག་ཡིག་ནང་གསང་ཨང་གསར་པ་ཚུད་པར་བྱོས།" +mismatch = "གསང་ཚིག་མི་མཐུན།" +newPassword = "གསང་ཚིག་གསར་པ" +regen = "བསྐྱར་སྐྲུན" +title = "གསང་ཚིག་བསྐྱར་སྒྲིག" +tooShort = "གསང་ཚིག་ལ་ཉུང་མཐར་ཡིག་འབྲུ་8 ཡོད་དགོས།" + +[users.role] +guest = "མགྲོན་པོ" +member = "ཚོགས་མི" +orgOwner = "རྩ་འཛུགས་བདག་པོ" +teamOwner = "རུ་ཁག་བདག་པོ" + +[users.team] +grantProcessor = "རུ་ཁག་ལ་ལས་སྒྲིག་པའི་དབང་ཚད་སྤྲོད" +revokeProcessor = "རུ་ཁག་ནས་ལས་སྒྲིག་པའི་དབང་ཚད་ཕྱིར་འཐེན" + [validateSignature] date = "ཚེས་པ" downloadCsv = "CSV ཕབ་ལེན་བྱེད་པ།" @@ -8057,14 +11255,28 @@ zoomOut = "ཆུང་དུ་བཟོ།" [viewer.attachments] addAttachment = "ཟུར་སྣོན་ཁ་སྣོན" close = "ཟུར་སྣོན་ཁ་རྒྱག" +closeSidebar = "ཟུར་སྦྱར་ཡིག་ཆའི་ཟུར་སྒྲོམ་ཁ་རྒྱོབ" +download = "ཟུར་སྦྱར་ཡིག་ཆ་ཕབ་ལེན" empty = "ཡིག་ཆ་འདིར་ཟུར་སྣོན་མེད།" loading = "ཟུར་སྣོན་ཡར་འཇུག་བཞིན..." noDocument = "PDF ཞིག་ཁ་ཕྱེས་ནས་ཟུར་སྣོན་ལ་ལྟ།" noMatch = "ཁྱེད་ཀྱི་འཚོལ་བ་དང་མཐུན་པའི་ཟུར་སྣོན་མེད།" noSupport = "སྟོན་ཆས་འདིའི་དོན་ལ་ཟུར་སྣོན་རྒྱབ་སྐྱོར་མེད།" +retry = "ཡང་བསྐྱར་ཚོད་ལྟ" searchPlaceholder = "ཟུར་སྣོན་འཚོལ" title = "ཟུར་སྣོན" +[viewer.bookmarks] +addBookmark = "Add bookmark" +bookmarkTitle = "དཔེ་རྟགས་ཁ་བྱང" +bookmarkTitleRequired = "དཔེ་རྟགས་ཁ་བྱང་དགོས་ངེས་ཡིན།" +closeSidebar = "དཔེ་རྟགས་ཟུར་སྒྲོམ་ཁ་རྒྱོབ" +collapseAll = "དཔེ་རྟགས་ཚང་མ་བསྡུ" +empty = "No bookmarks in this document" +expandAll = "དཔེ་རྟགས་ཚང་མ་རྒྱ་སྐྱེད" +searchPlaceholder = "དཔེ་རྟགས་འཚོལ" +title = "Bookmarks" + [viewer.comments] addComment = "བསམ་བཀོད་ཁ་སྣོན" addCommentPlaceholder = "བསམ་བཀོད་ཁ་སྣོན..." @@ -8075,6 +11287,7 @@ clearAll = "མཆན་འགྲེལ་ཡོངས་སུ་བསལ" clearAllDescription = "ཟུར་སྟེགས་ནས་མཆན་འགྲེལ་དང་ལན་ཚང་མ་བསལ་བྱེད་པ་དང་ ཡིག་ཆ་ནང་ཟུར་སྣོན་བྱས་པའི་དཔྱད་མཆན་ནི་བཞག་ཡོད།" clearAllTitle = "མཆན་འགྲེལ་ཡོངས་སུ་བསལ་དམ།" close = "མཆན་འགྲེལ་ཁ་རྒྱག" +closeSidebar = "མཆན་འགྲེལ་ཟུར་སྒྲོམ་ཁ་རྒྱོབ" deleteAnnotationAndComment = "མཆན་དང་བསམ་བཀོད་ཀུན་སུབ" deleteDescription = "མཆན་འདི་ལ་བསམ་བཀོད་ཞིག་སྦྲེལ་ཡོད། ཟུར་ཁྲམ་ནས་བསམ་བཀོད་ཙམ་སུབ་ནས་མཆན་བཞག་ཡང་ན་ཀུན་སུབ་ཆོག" deleteTitle = "བསམ་བཀོད་ནས་མཆན་སུབ་དམ?" @@ -8084,10 +11297,12 @@ locateAnnotation = "ཡིག་ཆ་ནང་ས་གནས་འཚོལ" moreActions = "བྱ་བ་མང་བ" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "ཤོག་ངོས་ {{page}}" placingHint = "ཤོག་ངོས་ཞིག་ལ་གནས་བཞག་བྱས་ནས་མནན… (འདོར)" removeCommentOnly = "བསམ་བཀོད་ཙམ་སུབ" saveReply = "ལན་སློག་སྲུང་" +searchPlaceholder = "Search comments" title = "བསམ་བཀོད" typeComment = "བསམ་བཀོད" typeInsertText = "ཡིག་གེ་འཇུག" @@ -8106,6 +11321,12 @@ title = "གཞུང་འགྲེམས་ས་ཁོངས" unsavedBadge = "མ་ཉར" unsavedDesc = "ཁྱེད་ལ་མ་ཉར་པའི་བཟོ་བཅོས་ཡོད།" +[viewer.layers] +closeSidebar = "རིམ་པ་ཟུར་སྒྲོམ་ཁ་རྒྱོབ" +hideAll = "རིམ་པ་ཚང་མ་སྦས" +showAll = "རིམ་པ་ཚང་མ་སྟོན" +title = "Layers" + [viewer.link] delete = "འབྲེལ་མཐུད་སུབ" @@ -8136,6 +11357,9 @@ resultsOf = "{{total}} ནས" [viewer.signature] delete = "མཛད་རྟགས་སུབ" +[viewer.thumbnails] +closeSidebar = "རིས་ཆུང་ཟུར་སྒྲོམ་ཁ་རྒྱོབ" + [viewPdf] tags = "མཐོང་སྣང་། ཀློག་པ། མཆན་འགྲེལ། ཚིག་ཕྲན། པར་རིས་དང་། འོད་མདངས།" title = "ལྟ་ཚུལ།/རྩོམ་སྒྲིག་པ། PDF" @@ -8445,30 +11669,6 @@ title = "ཆུའི་མཚོན་རྟགས་ཡིག་གནས།" image = "པར་རིས" text = "ཡིག་གཞི" -[wetSignature.tooltip] -header = "མིང་འགོད་བཟོ་ཐབས།" - -[wetSignature.tooltip.draw] -bullet1 = "སྨྱུག་གུའི་ཁ་དོག་དང་ཞེང་ཚད་སྲོལ་སྒྲིག" -bullet2 = "གནོན་སུབས་བྱས་ནས་དགའ་ཚུལ་དེ་བྱེད་ཚར་བའི་བར་བསྐྱར་འབྲི།" -bullet3 = "touch སྒྲིག་ཆས་(tablet, phone) ལ་བཀོལ་ཐུབ།" -description = "mouse ཡང་ན་ touchscreen སྤྱོད་ནས་ལག་འབྲིའི་མིང་འགོད་གསར་བཟོ། རང་སོའི་དངོས་མཐོང་ཡོད་པའི་མིང་འགོད་ལ་མོས་ཆེ།" -title = "མིང་འགོད་འབྲི།" - -[wetSignature.tooltip.type] -bullet1 = "ཡིག་གཟུགས་མང་པོ་ནས་བདམས།" -bullet2 = "ཡི་གེའི་ཆེ་ཆུང་དང་ཁ་དོག་སྲོལ་སྒྲིག" -bullet3 = "ཚད་ལྡན་མིང་འགོད་ལ་འོས་འཚམ།" -description = "ཡིག་འབྲུས་བྱས་པའི་ཚིག་ཡིག་ནས་མིང་འགོད་གསར་བཟོ། མགྱོགས་དྲག་དང་མཐུན་མོང་ ཚོང་ལས་ཡིག་ཆར་འཚམ།" -title = "ཡིག་འབྲུས་ཀྱི་མིང་འགོད།" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG དང་གཞན་དག་བརྙན་རྣམ་གྱི་རྣམ་གཞག་རྒྱབ་སྐྱོར།" -bullet2 = "འབྲས་བུ་ཡག་པོ་དགོས་པས་རྒྱབ་ལྗོངས་དྭངས་པོ་སྤྱོད་དགོས།" -bullet3 = "མིང་འགོད་ཁོངས་ལ་མཚུངས་པར་པར་རིས་ཀྱི་ཆེ་ཆུང་བསྐྱར་བཟོ་བྱེད།" -description = "སྔོན་བཟོས་པའི་མིང་འགོད་པར་ཡར་བསྐུར། མིང་འགོད་བཤར་འབེབས་ཡང་ན་ཚོང་ལས་ལས་རྟགས་ཡོད་ན་འོས་འཚམ།" -title = "མིང་འགོད་པར་ཡར་བསྐུར།" - [workbenchBar] activeFiles = "སྐྱོད་བཞིན་པའི་ཡིག་ཆ" annotations = "མཆན་བཀོད" @@ -8482,6 +11682,7 @@ exitRedaction = "སྦ་བཅོས་ཐབས་ལམ་ནས་ཕྱི exportAll = "PDF ཕྱིར་འདྲེན" exportSelected = "འདེམས་ཟིན་པའི་ཤོག་ངོས་ཕྱིར་འདྲེན" formFill = "རྣམ་གཞག་ཁ་སྐོང་" +hideToolbar = "Hide toolbar" multiTool = "སྣ་ཚོགས་ལག་ཆས" panMode = "སྤོ་བཤུད་ཐབས་ལམ" print = "PDF དཔར་བསྐྲུན" @@ -8492,6 +11693,7 @@ redact = "སྦ་བཅོས" rotateLeft = "གཡོན་སྐོར" rotateRight = "གཡས་སྐོར" ruler = "འཇལ་ཆས / ཚད་འཇལ" +rulerSettings = "བསྡུར་ཚད་སྒྲིག་འགོད" save = "སྲུངས" saveAll = "ཚང་མ་སྲུངས" saveAs = "མིང་གསར་པས་སྲུངས" @@ -8501,6 +11703,7 @@ selectAll = "ཚང་མ་འདེམས" selectByNumber = "ཤོག་ངོས་གྲངས་ཀྱིས་འདེམས" selectLanguage = "སྐད་ཡིག་འདེམས" share = "མཉམ་སྤྱོད" +showToolbar = "Show toolbar" toggleAnnotations = "མཆན་བཀོད་མཐོང་རུང་བ་བསྒྱུར" toggleAttachments = "ཟུང་འདྲེན་བསྒྱུར" toggleBookmarks = "མཚོན་རྟ་བསྒྱུར" @@ -8562,6 +11765,7 @@ loading = "མི་ལ་སྐྱེལ་འདྲེན་བྱེད་པ locked = "བཀག་ཡོད" lockedBadge = "བཀག་ཡོད" loginRequired = "ནང་འཇུག་ཐབས་ལམ་ཐོག་མར་ལྕོགས་ཅན་བཟོ་བ།" +memberActions = "ཚོགས་མིའི་བྱ་སྤྱོད" noMembersFound = "འཐུས་མི་གང་ཡང་མ་ཐོབ།" role = "འཁྲབ་ཚན" searchMembers = "འཚོལ་ཞིབ་ཚོགས་མི།..." @@ -8571,6 +11775,7 @@ unlockAccount = "རྩིས་ཁྲོད་ཕྱེ" unlockUserError = "སྤྱོད་པའི་རྩིས་ཁྲོད་ཕྱེ་མི་ཐུབ།" unlockUserSuccess = "སྤྱོད་པའི་རྩིས་ཁྲོད་ཕྱེ་ཚར་བ།" user = "བེད་སྤྱོད།" +userInfo = "སྤྱོད་མཁན་ཆ་འཕྲིན" [workspace.people.actions] upgrade = "ལེགས་བཅོས" @@ -8716,6 +11921,7 @@ removeMemberError = "སྤྱོད་མཁན་རུ་ཁག་ནས་ removeMemberSuccess = "བེད་སྤྱོད་པ་རུ་ཁག་ནས་ཕྱིར་འཐེན་བྱས།" renameTeamLabel = "མིང་བསྒྱུར་བའི་རུ་ཁག" system = "མ་ལག" +teamActions = "རུ་ཁག་གི་བྱ་སྤྱོད" teamName = "རུ་ཁག་གི་མིང་།" teamNotFound = "རུ་ཁག་མ་རྙེད་པ།" title = "རུ་ཁག" diff --git a/frontend/editor/public/locales/ca-CA/translation.toml b/frontend/editor/public/locales/ca-CA/translation.toml index 6efadcd4ec..3a1ccb7e35 100644 --- a/frontend/editor/public/locales/ca-CA/translation.toml +++ b/frontend/editor/public/locales/ca-CA/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Aplica" applyAndContinue = "Desa i surt" -areYouSure = "Segur que vols sortir?" back = "Enrere" black = "Negre" blue = "Blau" @@ -46,10 +45,8 @@ filesSelected = "fitxers seleccionats" font = "Tipus de lletra" fontSizeTooltip = "Mida del text del número de pàgina en punts. Valors més alts generen text més gran." fontTypeTooltip = "Família del tipus de lletra per als números de pàgina. Tria-la segons l'estil del teu document." -genericSubmit = "Envia" green = "Verd" help = "Ajuda" -imgPrompt = "Selecciona Imatge(s)" incorrectPasswordMessage = "La contrasenya actual és incorrecta." info = "Informació" invalidUndoData = "No es pot desfer: dades d’operació no vàlides" @@ -62,7 +59,6 @@ loginAgreementTitle = "Acord d'inici de sessió" logOut = "Tanca la sessió" marginTooltip = "Distància entre el número de pàgina i la vora de la pàgina." moreOptions = "Més opcions" -multiPdfPrompt = "Selecciona PDFs (2+)" never = "Mai" no = "No" noFileSelected = "No s'ha seleccionat cap fitxer. Puja'n un." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordre de Pàgines (Llista separada per comes) :" pages = "Pàgines" pageSelectionPrompt = "Selecció de pàgines personalitzada (Introdueix una llista separada per comes de números de pàgina, 1,5,6 o funcions com 2n+1):" password = "Contrasenya" -pdfPrompt = "Selecciona PDF(s)" pin = "Fixa el fitxer (mantén-lo actiu després d'executar l'eina)" pro = "Pro" processingComplete = "El fitxer està llest." @@ -110,6 +105,7 @@ undoSuccess = "Operació desfeta correctament" unknown = "Desconegut" unpin = "Desfixa el fitxer (substitueix-lo després d'executar l'eina)" unsavedChanges = "Tens canvis sense desar al teu PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Canvis sense desar" unsupported = "No compatible" username = "Usuari" @@ -356,6 +352,7 @@ text = "Aquests permisos controlen què poden fer els usuaris amb el PDF. Són m title = "Canviar Permissos" [addStamp] +preview = "Previsualitza el segell" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Descàrregues manuals: feu clic als enllaços i col·loqueu els f noLanguages = "No s'ha trobat cap idioma de tessdata al directori configurat." permissionNotice = "El camí de tessdata no té permisos d'escriptura. Les descàrregues s'obriran al navegador; deseu manualment els fitxers .traineddata a la carpeta tessdata." +[admin.settings.ai.documents] +description = "Configura el model d'embeddings i els paràmetres de recuperació utilitzats per respondre preguntes sobre documents. S'aplica al motor d'IA en desar." +title = "Documents i RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Deixa-ho en blanc per utilitzar la credencial d'entorn pròpia del motor. S'aplica a desplegaments autoallotjats d'un sol motor." +generic = "Clau API d'embeddings" +openai = "Clau API d'OpenAI" +setPlaceholder = "Desada - deixa-ho en blanc per conservar la clau actual" +voyageai = "Clau API de VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL base de l'endpoint d'embeddings compatible amb OpenAI / Ollama, p. ex. http://ollama:11434/v1. Ha d'apuntar a un endpoint intern de confiança (sensible a SSRF)." +label = "URL base d'embeddings" + +[admin.settings.ai.documents.embeddingModel] +description = "Nom del model d'embeddings. Text lliure; els suggeriments només són orientatius." +label = "Model d'embeddings" + +[admin.settings.ai.documents.embeddingProvider] +description = "Proveïdor utilitzat per convertir el text del document en embeddings vectorials." +label = "Proveïdor d'embeddings" + +[admin.settings.ai.documents.maxSearches] +description = "Nombre màxim de cerques de recuperació que l'agent pot executar per sol·licitud." +label = "Cerques màximes" + +[admin.settings.ai.documents.reindexNote] +body = "Canviar el model d'embeddings té efecte immediatament, però els documents indexats amb el model anterior s'han de tornar a indexar perquè la cerca retorni resultats correctes." +title = "Cal tornar a indexar" + +[admin.settings.ai.documents.saved] +reindexNote = "Si has canviat el model d'embeddings, torna a indexar els documents existents perquè la cerca utilitzi el nou model." + +[admin.settings.ai.documents.topK] +description = "Nombre de fragments més rellevants recuperats per cerca." +label = "Top K" + +[admin.settings.ai.general] +description = "Connecta Stirling al motor d'IA de Python i tria quines capacitats d'IA s'exposen. Els canvis s'apliquen en reiniciar." +title = "Motor d'IA" + +[admin.settings.ai.general.capabilities] +description = "Activa o desactiva funcions d'IA individuals. Les funcions desactivades s'amaguen a l'aplicació." +title = "Capacitats" + +[admin.settings.ai.general.enabled] +description = "Interruptor principal. Quan està desactivat, no hi ha eines d'IA, agents ni crides al motor disponibles." +label = "Activa la IA" + +[admin.settings.ai.general.features.chat] +description = "Assistent conversacional per treballar amb PDFs." +label = "Assistent de xat" + +[admin.settings.ai.general.features.classify] +description = "Categoritza automàticament els documents per tipus o contingut." +label = "Classificació de documents" + +[admin.settings.ai.general.features.createPdf] +description = "Genera un PDF nou (p. ex. des d'HTML) mitjançant un agent d'IA." +label = "Crea un PDF a partir d'una indicació" + +[admin.settings.ai.general.features.documentQuestions] +description = "Fes preguntes i obtén respostes basades en un document pujat." +label = "Preguntes sobre documents" + +[admin.settings.ai.general.features.mathAuditor] +description = "Revisa documents per detectar errors matemàtics i numèrics." +label = "Auditor matemàtic" + +[admin.settings.ai.general.features.pdfComment] +description = "Afegeix comentaris de revisió i anotacions escrits per IA a un PDF." +label = "Agent de comentaris PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Temps d'espera per a operacions d'agent més pesades, com ara la generació de documents." +label = "Temps d'espera d'operacions llargues (segons)" + +[admin.settings.ai.general.note] +body = "El motor d'IA s'executa com un servei separat. El seu secret compartit" +body2 = "s'estableix mitjançant una variable d'entorn del contenidor. Les claus API del proveïdor es poden introduir en aquestes pàgines o proporcionar com a variables d'entorn del motor; les claus introduïdes aquí s'envien al motor en desar." +title = "Sobre el motor d'IA" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Temps d'espera per a respostes de xat en streaming (token per token)." +label = "Temps d'espera de streaming (segons)" + +[admin.settings.ai.general.test] +button = "Prova la connexió" +failBody = "El motor d'IA no ha respost. Comprova la URL, que el contenidor del motor estigui en execució i que la IA estigui activada (cal reiniciar després d'activar-la)." +failTitle = "Motor d'IA inaccessible" +okBody = "El motor d'IA ha respost a una comprovació d'estat." +okTitle = "Motor d'IA accessible" + +[admin.settings.ai.general.timeoutSeconds] +description = "Temps d'espera per a sol·licituds d'IA estàndard al motor." +label = "Temps d'espera de sol·licitud (segons)" + +[admin.settings.ai.general.url] +description = "URL interna del motor d'IA de Python, p. ex. http://stirling-pdf-engine:5001." +label = "URL del motor d'IA" + +[admin.settings.ai.limits] +description = "Límits de seguretat per a la quantitat de feina que poden fer les sol·licituds d'IA i quantes s'executen simultàniament. S'apliquen al motor d'IA en desar." +title = "Límits i rendiment" + +[admin.settings.ai.limits.maxCharacters] +description = "Límit de seguretat: rebutja les sol·licituds d'IA el text extret de les quals superi aquesta longitud." +label = "Caràcters màxims per sol·licitud" + +[admin.settings.ai.limits.maxPages] +description = "Límit de seguretat: rebutja les sol·licituds d'IA amb més d'aquest nombre de pàgines PDF." +label = "Pàgines màximes per sol·licitud" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Nombre màxim de crides simultànies al model en curs a tot el motor." +label = "Concurrència màxima del model" + +[admin.settings.ai.models] +description = "Tria el proveïdor LLM i els models intel·ligent/ràpid que utilitza el motor d'IA. S'aplica al motor d'IA en desar." +title = "Models i proveïdors" + +[admin.settings.ai.models.apiKey] +anthropic = "Clau API d'Anthropic" +description = "Deixa-ho en blanc per utilitzar la credencial d'entorn pròpia del motor. S'aplica a desplegaments autoallotjats d'un sol motor." +generic = "Clau API" +openai = "Clau API d'OpenAI" +setPlaceholder = "Desada - deixa-ho en blanc per conservar la clau actual" + +[admin.settings.ai.models.baseUrl] +description = "URL base de l'endpoint compatible amb OpenAI / Ollama, p. ex. http://ollama:11434/v1." +label = "URL base del proveïdor" +warning = "La URL base ha d'apuntar a un endpoint intern de confiança. El motor hi farà sol·licituds del costat del servidor, de manera que un valor no fiable és sensible a SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Tokens de sortida màxims per al model ràpid." +label = "Tokens màxims del model ràpid" + +[admin.settings.ai.models.fastModel] +description = "Model més barat i ràpid per a tasques lleugeres. Text lliure; els suggeriments només són orientatius." +label = "Model ràpid" + +[admin.settings.ai.models.provider] +description = "Amb quin proveïdor LLM es comunica el motor." +label = "Proveïdor" + +[admin.settings.ai.models.smartMaxTokens] +description = "Tokens de sortida màxims per al model intel·ligent." +label = "Tokens màxims del model intel·ligent" + +[admin.settings.ai.models.smartModel] +description = "Model d'alta capacitat per a raonaments complexos. Text lliure; els suggeriments només són orientatius." +label = "Model intel·ligent" + +[admin.settings.ai.saved] +body = "Els canvis s'envien automàticament al motor d'IA." +bodyNoPush = "Configuració desada. S'aplicarà la propera vegada que el motor d'IA reculli la seva configuració." +title = "Configuració d'IA desada" + [admin.settings.badge] clickToUpgrade = "Fes clic per veure els detalls del pla" @@ -938,6 +1094,25 @@ label = "Regenera a l'arrencada" description = "Nombre de dies que el certificat serà vàlid" label = "Validesa del certificat (dies)" +[admin.settings.folderAccess] +description = "Directoris des dels quals les fonts de carpetes i les sortides de carpetes poden llegir i als quals poden escriure. Això és un límit de seguretat: les automatitzacions mai no poden apuntar a una ruta del servidor fora d'aquesta llista." +securityNote = "Deixa-ho buit per desactivar completament les fonts i sortides de carpetes. El directori de configuració propi de Stirling sempre està fora de límits." +title = "Accés a carpetes" + +[admin.settings.folderAccess.implied] +description = "Aquests directoris gestionats per Stirling sempre estan permesos i no es poden canviar aquí." +serverStorage = "Emmagatzematge de fitxers del servidor" +title = "Sempre permès" +watchedFolder = "Carpeta vigilada de pipeline" + +[admin.settings.folderAccess.roots] +add = "Afegeix" +empty = "No hi ha carpetes permeses. Les fonts i sortides de carpetes estan desactivades actualment." +hint = "Introdueix rutes absolutes, per exemple /data/inbox." +label = "Arrels de carpeta permeses" +placeholder = "/data/inbox" +remove = "Elimina l'arrel de carpeta" + [admin.settings.general] description = "Configureu els paràmetres de l'aplicació a nivell de sistema, incloent-hi la marca i el comportament per defecte." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Amplada" [app] description = "L'alternativa gratuïta a Adobe Acrobat (més de 10 M de descàrregues)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Converteix a PDF/A-3b" convertToPdfA3bDescription = "Crea un PDF d'arxiu amb fitxers adjunts incrustats" @@ -1842,6 +2020,15 @@ export = "Exporta" accessDenied = "Accés denegat" insufficientPermissions = "No tens permís per fer aquesta acció." +[auth.callback] +completing = "S'està completant l'autenticació" +invalidToken = "L'inici de sessió OAuth ha fallat - token no vàlid." +missingToken = "L'inici de sessió OAuth ha fallat - no s'ha rebut cap token." +oauthFailed = "L'inici de sessió OAuth ha fallat. Torna-ho a provar." +pleaseWait = "Espera mentre acabem d'iniciar la sessió." +signedOut = "S'ha tancat la sessió. Torna a iniciar-la." +windowMayClose = "Pots tancar aquesta finestra un cop es completi." + [auth.displayName] guest = "Convidat" user = "Usuari" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Assegura els documents PDF eliminant contingut potencialme description = "Aquesta eina canviarà automàticament el nom dels fitxers PDF segons el seu contingut. Analitza el document per trobar el títol més adequat a partir del text." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Gira automàticament" +tags = "gir automàtic,corregir orientació,cap per avall,de costat,redreçar,orientar,detectar orientació,corregir rotació,orientació automàtica,escanejat,OSD" + +[autoRotate.confidenceThreshold] +title = "Llindar de confiança" + +[autoRotate.detectionMode] +auto = "Automàtic" +osd = "Només OCR" +text = "Només text" +title = "Mètode de detecció" + +[autoRotate.error] +failed = "S'ha produït un error en girar automàticament el PDF." + +[autoRotate.inferUndetected] +title = "Gira les pàgines de baixa confiança perquè coincideixin" + +[autoRotate.report] +applied = "{{degrees}}° en sentit horari" +confidenceValue = "Confiança {{value}}" +noChange = "Sense canvis" +pageLabel = "Pàgina {{number}}" +summary = "{{rotated}} de {{total}} pàgines girades, {{unchanged}} deixades tal com estan" +title = "Informe de detecció" + +[autoRotate.report.method] +inferred = "Coincident" +none = "Omesa" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Per sota del llindar" +blankPage = "La pàgina és en blanc" +inferredFromDocument = "Igual que les altres pàgines" +noDominantDirection = "Direccions de text mixtes" +osdFailed = "No s'ha trobat text llegible" +osdNoVerdict = "No s'ha pogut determinar l'orientació" +tesseractUnavailable = "OCR no instal·lat" +tooFewGlyphs = "Massa poc text" + +[autoRotate.results] +title = "Resultats del gir automàtic" + +[autoRotate.settings] +title = "Configuració" + +[autoRotate.tooltip.confidenceThreshold] +text = "Com de segura ha de ser la comprovació abans de girar una pàgina. Redueix-lo per girar més pàgines, augmenta'l per deixar-ne més sense tocar." +title = "Llindar de confiança" + +[autoRotate.tooltip.description] +text = "Determina quina orientació hauria de tenir cada pàgina i la gira correctament. Les pàgines que ja estan dretes es deixen igual." + +[autoRotate.tooltip.detectionMode] +text = "Automàtic llegeix primer el text de la pàgina i després mira la pàgina com a imatge si no n'hi ha prou. Només text omet la comprovació d'imatge, que és més ràpida per a documents que mai no s'han escanejat. Només OCR tracta cada pàgina com una imatge." +title = "Mètode de detecció" + +[autoRotate.tooltip.header] +title = "Gir automàtic" + +[autoRotate.tooltip.inferUndetected] +text = "Les pàgines en blanc o gairebé en blanc no es poden comprovar per si soles. Quan totes les pàgines que es poden llegir necessiten el mateix gir, aquestes pàgines es giren perquè coincideixin." +title = "Gira les pàgines de baixa confiança perquè coincideixin" + [autoSizeSplitPDF] tags = "pdf,dividir,document,organització" @@ -2189,33 +2443,6 @@ keywords = "Paraules clau: odd, even" numbers = "Números/intervals: 5, 10-20" progressions = "Progressions: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipus de certificat" - -[certificateChoice.tooltip.organization] -bullet1 = "Gestionat pels administradors del sistema" -bullet2 = "Compartit entre els usuaris autoritzats" -bullet3 = "Representa la identitat de l'empresa, no la individual" -bullet4 = "Ideal per a: documents oficials, signatures d'equip" -description = "Un certificat compartit proporcionat per la teva organització. S'utilitza per a la signatura a nivell d'empresa." -title = "Certificat de l'organització" - -[certificateChoice.tooltip.personal] -bullet1 = "Es genera automàticament en el primer ús" -bullet2 = "Vinculat al teu compte d'usuari" -bullet3 = "No es pot compartir amb altres usuaris" -bullet4 = "Ideal per a: documents personals, responsabilitat individual" -description = "Un certificat generat automàticament i únic per al teu compte d'usuari. Adequat per a signatures individuals." -title = "Certificat personal" - -[certificateChoice.tooltip.upload] -bullet1 = "Requereix fitxer P12/PFX i contrasenya" -bullet2 = "Pot ser emès per Autoritats de Certificació externes" -bullet3 = "Nivell de confiança més alt per a documents legals" -bullet4 = "Ideal per a: contractes legalment vinculants, validació externa" -description = "Utilitza el teu propi fitxer de certificat PKCS#12. Proporciona control total sobre les propietats del certificat." -title = "Puja P12 personalitzat" - [certSign] awaitingSignatures = "En espera de signatures" chooseCertificate = "Trieu el fitxer de certificat" @@ -2542,34 +2769,6 @@ server = "Servidor" stepTitle = "Origen del certificat" upload = "Puja" -[certSign.tooltip.header] -title = "Sobre la gestió de signatures" - -[certSign.tooltip.overview] -bullet1 = "Comprova les signatures existents i la seva validesa" -bullet2 = "Mostra informació detallada sobre signants i certificats" -bullet3 = "Afegeix noves signatures digitals per protegir els documents" -bullet4 = "S'admeten múltiples fitxers amb navegació senzilla" -text = "Aquesta eina us permet comprovar si els vostres PDFs estan signats digitalment i afegir noves signatures digitals. Les signatures digitals proven qui ha creat o aprovat un document i indiquen si ha estat modificat des que es va signar." -title = "Què pot fer aquesta eina?" - -[certSign.tooltip.signing] -bullet1 = "Admet formats PEM, PKCS12, JKS i certificats de servidor" -bullet2 = "Opció per mostrar o amagar la signatura al PDF" -bullet3 = "Afegeix motiu, ubicació i nom del signant" -bullet4 = "Trieu en quina pàgina col·locar les signatures visibles" -bullet5 = "Utilitza el certificat del servidor per a l'opció senzilla 'Sign with Stirling-PDF'" -text = "Per signar un PDF, necessiteu un certificat digital (com PEM, PKCS12 o JKS). Podeu triar fer visible la signatura al document o mantenir-la invisible només per a seguretat." -title = "Afegir signatures" - -[certSign.tooltip.validation] -bullet1 = "Mostra si les signatures són vàlides o no vàlides" -bullet2 = "Mostra la informació del signant i la data de signatura" -bullet3 = "Comprova si el document s'ha modificat després de signar" -bullet4 = "Pot usar certificats personalitzats per a la verificació" -text = "Quan comproveu les signatures, l'eina us indica si són vàlides, qui ha signat el document, quan es va signar i si el document s'ha modificat des de la signatura." -title = "Comprovació de signatures" - [changeCreds] changeUsername = "Actualitza el nom d'usuari. Es tancarà la sessió després d’actualitzar." credsUpdated = "Compte actualitzat" @@ -2737,12 +2936,16 @@ text = "Per fer que aquests permisos no es puguin canviar, utilitzeu l’eina Af [chat.actions] copy = "Copia el missatge" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opcions de l'agent Stirling" +assistant = "AI Assistant" clearChat = "Neteja el xat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Aquesta operació utilitzarà els vostres crèdits del núvol" tooltip = "Tria el color de la pantalla" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Tria el color" [common] +actions = "Actions" back = "Enrere" cancel = "Cancel·la" close = "Tanca" +codeSample = "Code sample" collapse = "Replega" confirm = "Confirma" continue = "Continua" copied = "Copiat!" copy = "Copia" done = "Fet" +enterValue = "Enter value" error = "Error" expand = "Desplega" loading = "Carregant..." next = "Següent" +open = "Open" preview = "Vista prèvia" previous = "Anterior" refresh = "Actualitza" +remaining = "Restant" retry = "Reintenta" save = "Desa" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Esborra la selecció" @@ -2988,6 +3199,7 @@ title = "Mètode de compressió" [compress.settings] desiredSize = "Mida de fitxer desitjada" desiredSizePlaceholder = "Introdueix la mida" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "La compressió és una manera fàcil de reduir la mida del fitxer. Trieu Mida de fitxer per introduir una mida objectiu i que ajustem la qualitat per vosaltres. Trieu Qualitat per establir manualment la intensitat de la compressió." @@ -3190,6 +3402,24 @@ pdfOptions = "Opcions de PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Selecciona primer un format d'origen" settings = "Opcions" single = "Únic" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitoratge avançat" -allInOneWorkspace = "Espai de treball PDF tot en un (visualitzador, eines i agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Suport dedicat i SLA" -fasterThroughput = "Rendiment 10x més ràpid" -fullyPrivateFiles = "Fitxers totalment privats" -largeFileProcessing = "Processament de fitxers grans" -orgWideAccess = "Controls d'accés per a tota l'organització" -premiumAiModels = "Models d'IA premium" -prioritySupport = "Suport prioritari" -privateDocCloud = "Núvol de documents privat" -ragFineTuning = "RAG + afinament" -secureApiAccess = "Accés d'API segur" -standardThroughput = "Rendiment estàndard" -unlimitedApiAccess = "Accés d'API il·limitat" -unlimitedSeats = "Usuaris il·limitats" - [crop] autoCrop = "Retalla automàticament els espais en blanc" noFileSelected = "Seleccioneu un fitxer PDF per començar a retallar" @@ -3346,6 +3558,7 @@ message = "No s'ha pogut establir el gestor PDF per defecte" title = "Error" [defaultApp.prompt] +dontRemind = "No m'ho tornis a recordar" message = "Fes que Stirling PDF sigui l'aplicació per defecte per obrir fitxers PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Cerca..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Edita" @@ -3644,11 +3861,13 @@ shareSelected = "Comparteix els seleccionats" sharing = "Compartició" showAll = "Mostra-ho tot" showHistory = "Mostra l'historial" +sortBy = "Sort files" sortByDate = "Ordena per data" sortByName = "Ordena per nom" sortBySize = "Ordena per mida" storage = "Emmagatzematge" storageState = "Emmagatzematge" +storageUsed = "Storage used" synced = "Sincronitzat" title = "Puja fitxers PDF" toolChain = "Eines aplicades" @@ -3694,13 +3913,17 @@ addFiles = "Afegeix fitxers" addingFiles = "S'estan afegint fitxers…" collapse = "Redueix la barra lateral" customizeGroups = "Personalitza els grups" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Obre fitxers per començar" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Amplia la barra lateral" -files = "Fitxers" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive no està configurat" leaveMyFiles = "Surt de Els meus fitxers" +library = "Biblioteca de PDF" myFiles = "Els meus fitxers" noFiles = "Encara no hi ha fitxers" openFileManager = "Obre el gestor de fitxers" @@ -3708,37 +3931,36 @@ openFromComputer = "Obre des de l’ordinador" openSettings = "Obre la configuració" other = "Altres" recent = "Recents" -search = "Cerca" -searchPlaceholder = "Cerca fitxers..." viewAll = "Mostra tots els {{count}} fitxers" [fileSidebar.fileItem] closeViewer = "Tanca el visor" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Obre al visor" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Afegeix" -addLabel = "Afegeix una etiqueta…" -categoryIconAria = "Tria una icona per a {{name}}" -createCategory = "Crea una categoria" -delete = "Elimina la categoria" done = "Fet" -hide = "Amaga la categoria" -newCategory = "Nom de la categoria nova…" -removeLabel = "Elimina {{name}}" -rename = "Canvia el nom" reset = "Restableix els valors predeterminats" -search = "Cerca etiquetes…" -show = "Mostra la categoria" subtitle = "Agrupa els teus fitxers en categories principals. Afegeix etiquetes existents o noves a una categoria, canvia'n el nom o crea'n una de pròpia. Els fitxers que no pertanyen a cap de les teves categories apareixen a “Altres”." title = "Categories de la barra lateral" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Afegeix a l'espai de treball" addToWorkspaceCount = "Afegeix {{count}} a l'espai de treball" @@ -3749,7 +3971,6 @@ backToMyFiles = "Torna a Els meus fitxers" breadcrumbs = "Ruta de la carpeta" cancel = "Cancel·la" classification = "Classificació" -clearSearch = "Neteja la cerca" clearSelection = "Neteja la selecció" closeDetails = "Tanca els detalls" create = "Crea" @@ -3781,6 +4002,7 @@ downloadAll = "Descarrega-ho tot" downloadVersion = "Descarrega aquesta versió" dropOverlay = "Deixa anar fitxers per pujar-los" dropOverlaySub = "Els fitxers comencen a Local. Utilitza 'Mou a' o 'Desa al núvol' per organitzar-los en una carpeta." +duplicate = "Duplicate" file = "Fitxer" fileInfo = "File info" fileMenu = "Accions del fitxer" @@ -3808,13 +4030,12 @@ refresh = "Actualitza des del servidor" remove = "Suprimeix" removeVersion = "Suprimeix aquesta versió" rename = "Canvia el nom" +renamed = "Reanomenat" renameFolder = "Canvia el nom de la carpeta" resizeFolderTree = "Canvia la mida de l'arbre de carpetes (tecles de fletxa, Maj per a passos més grans; doble clic per ajustar automàticament)" save = "Desa" saveToServer = "Desa al servidor" saveToServerDisabledHint = "Desar al servidor no està habilitat en aquest servidor. Demana a l'administrador que ho activi." -search = "Cerca" -searchPlaceholder = "Cerca en aquesta carpeta i subcarpetes" selectAll = "Selecciona-ho tot" selectAllHint = "Fes clic per seleccionar-ho tot. Consell: mantén Ctrl (o Cmd) per afegir fitxers d'un en un, Maj per seleccionar un interval." selectedCount = "{{count}} seleccionats" @@ -3874,6 +4095,10 @@ title = "Encara no hi ha fitxers al núvol" hint = "Els fitxers desats sense pujar es queden aquí. Deixa anar un fitxer per afegir-ne un." title = "Sense fitxers només locals" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Els fitxers que obris o editis apareixeran aquí." title = "Encara no s'ha modificat res" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "No s'ha pogut suprimir la carpeta." deleteFolderFailedDetail = "No s'ha pogut suprimir la carpeta: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "No s'ha pogut actualitzar l'aparença de la carpeta." folderAppearanceFailedDetail = "No s'ha pogut actualitzar l'aparença de la carpeta: {{message}}" moveFilesFailed = "No s'han pogut moure els fitxers." @@ -3910,9 +4137,7 @@ count = "Fitxers" folder = "Carpeta" labels = "Etiquetes" modified = "Modificat" -name = "Nom" size = "Mida" -toolHistoryAtVersion = "Cadena d'eines acumulada" totalSize = "Mida total" type = "Tipus" versionHistory = "Recorregut de versions" @@ -3945,6 +4170,11 @@ localHint = "Només emmagatzemat en aquest navegador" shared = "Compartit" sharedHint = "Compartit amb tu mitjançant enllaç" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Més antic primer" modifiedDesc = "Més recent primer" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "No hi ha fitxers disponibles a l'emmagatzematge. Obre al open = "Obre" openFile = "Obre fitxer" openFiles = "Obre fitxers" +selectFile = "Select {{name}}" selectFromStorage = "Seleccioneu de l'emmagatzematge" upload = "Puja" uploadFile = "Puja fitxer" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tot desat" +analyzingFields = "Analysing form fields..." +extractCsvError = "No s'ha pogut extreure el CSV" +extractXlsxError = "No s'ha pogut extreure l'XLSX" +filled = "filled" flattenAfterFilling = "Aplana després d'emplenar" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl." +requiredFieldsError = "Emplena tots els camps obligatoris" rescanFields = "Torna a analitzar els camps" rescanFormFields = "Torna a analitzar els camps del formulari" save = "Desa" +saveShortcut = "Ctrl+S per desar" +unsavedChanges = "Canvis no desats" [formFill.sidebar] close = "Tanca la barra lateral" @@ -4309,6 +4552,11 @@ desc = "Canvia automàticament el nom d'un fitxer PDF en funció de la capçaler tags = "detecció automàtica,basat en capçalera,organitza,reenomena" title = "Reanomena PDF automàticament" +[home.autoRotate] +desc = "Detecta l'orientació de cada pàgina i la gira automàticament perquè quedi dreta." +tags = "gir automàtic,corregir orientació,cap per avall,de costat,redreçar,orientar,detectar orientació,corregir rotació,orientació automàtica,escanejat,OSD" +title = "Gir automàtic" + [home.autoSizeSplitPDF] tags = "automàtic,separa,mida" title = "Divideix auto per mida/pàg." @@ -4608,15 +4856,12 @@ hint = "Puja un PNG, JPG, SVG o un altre fitxer d'imatge per col·locar-lo al PD label = "Puja imatge" placeholder = "Selecciona el fitxer d'imatge" -[imageToPdf] -tags = "conversió,img,jpg,imatge,foto" - [imageToPDF] header = "Imatge a PDF" title = "Imatge a PDF" -[infoBanner] -dismiss = "Descarta" +[imageToPdf] +tags = "conversió,img,jpg,imatge,foto" [invite] acceptError = "No s’ha pogut crear el compte" @@ -4647,11 +4892,9 @@ welcomeTitle = "Heu estat convidats!" [landing] addFiles = "Afegeix fitxers" -heroSubtitle = "Arrossega i deixa anar o afegeix un PDF existent per començar." mobileUpload = "Puja des del mòbil" openFromComputer = "Obre des de l'ordinador" uploadFromComputer = "Puja des de l'ordinador" -workbenchEmptyStateHero = "Deixa anar un PDF a qualsevol lloc" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Introduïu el nom d'usuari" failedToSignIn = "No s'ha pogut iniciar sessió amb {{provider}}: {{message}}" forgotPassword = "Heu oblidat la contrasenya?" loggingIn = "Iniciant sessió..." -logIn = "Inicia sessió" login = "Inicia sessió" magicLinkSent = "Enllaç màgic enviat a {{email}}! Reviseu el vostre correu i feu clic a l'enllaç per iniciar sessió." mfaCode = "Codi d'autenticació" @@ -4715,26 +4957,10 @@ signInWith = "Inicia sessió amb" title = "Accedir" unexpectedError = "Error inesperat: {{message}}" updatePassword = "Actualitza la contrasenya" -useEmailInstead = "Inicia sessió amb correu" useMagicLink = "Utilitza un enllaç màgic" username = "Nom d'usuari" youAreLoggedIn = "Heu iniciat sessió!" -[login.slides.edit] -alt = "Edita PDF" -subtitle = "Amb més d'una dotzena d'eines per censurar, signar, llegir i manipular PDF, segur que hi trobaràs el que busques." -title = "Edita PDF per mostrar/protegir la informació que vulguis" - -[login.slides.overview] -alt = "Visió general de Stirling PDF" -subtitle = "Una suite al núvol centrada en la privadesa per a PDF que et permet convertir, signar, censurar i gestionar documents, amb més de 50 eines potents." -title = "Tot el que necessites per als teus PDF en un sol lloc." - -[login.slides.secure] -alt = "Protegeix PDF" -subtitle = "Afegeix contrasenyes, censura contingut i gestiona certificats amb facilitat." -title = "Protegeix la informació sensible dels teus PDF" - [margin] large = "Gran" medium = "Mitjana" @@ -4818,6 +5044,34 @@ uploadSuccess = "Pujada correcta!" uploadSuccessMessage = "Les imatges s'han transferit." validating = "Validant la sessió..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Escaneja per pujar fotos. Les imatges es converteixen automàticament a PDF." descriptionNoConvert = "Escaneja per pujar fotos des del dispositiu mòbil." @@ -4836,8 +5090,13 @@ title = "Puja des del mòbil" tags = "Eina Multifunció,operacions múltiples,UI,clic i arrossega,costat frontal,client" title = "Eina multifunció de PDF" -[navbar] -search = "Cerca" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "L'autenticació no ha estat correcta. Pots tancar aquesta finestra i tornar-ho a provar." @@ -4966,6 +5225,7 @@ activeFiles = "La vista de Fitxers actius mostra tots els PDF q allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Tanca" cropSettings = "Ara que hem seleccionat el fitxer que volem retallar, podem configurar l’eina Retalla per triar l’àrea a la qual volem retallar el PDF." +dialogLabel = "Onboarding" fileCheckbox = "Si feu clic en un dels fitxers, es selecciona per al processament. Podeu seleccionar diversos fitxers per a operacions en lot." fileReplacement = "El fitxer modificat substituirà automàticament l’original a la Taula de treball, cosa que us permetrà passar-lo fàcilment per més eines." filesButton = "El botó de Fitxers a la barra d’accés ràpid us permet pujar PDF per utilitzar les eines." @@ -4976,6 +5236,7 @@ pinButton = "Podeu utilitzar el botó de Fixar si preferiu que results = "Després que l’eina hagi acabat d’executar-se, el pas de Revisió mostrarà una previsualització dels resultats en aquest panell i us permetrà desfer l’operació o descarregar el fitxer. " runButton = "Un cop configurada l’eina, aquest botó us permet executar-la en tots els PDF seleccionats." selectCropTool = "Seleccionem l’eina de Retalla per demostrar com utilitzar una de les eines." +stepOf = "Pas {{current}} de {{total}}" toolInterface = "Aquesta és la interfície de l’eina de Retalla. Com podeu veure, no hi ha gaire res perquè encara no hem afegit cap fitxer PDF per treballar-hi." workbench = "Aquesta és la Taula de treball - l’àrea principal on visualitzeu i editeu els vostres PDF." wrapUp = "Ja està tot! Heu après les àrees principals de l’aplicació i com utilitzar-les. Feu clic al botó Ajuda quan vulgueu per tornar a veure aquesta visita." @@ -4988,6 +5249,26 @@ showMeAround = "Fes-me un recorregut" skipForNow = "Omet per ara" skipTheTour = "Omet la visita guiada" +[onboarding.checklist] +dismiss = "Descarta" +title = "Configura Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Executa Stirling de manera nativa al teu equip" +title = "Baixa Stirling per a escriptori" + +[onboarding.checklist.inviteTeam] +description = "Col·labora amb el teu equip" +title = "Convida membres de l'equip" + +[onboarding.checklist.shareAnalytics] +description = "Ajuda a millorar Stirling" +title = "Comparteix dades d'ús anònimes" + +[onboarding.checklist.takeTour] +description = "Mira com funciona Stirling en una visita guiada ràpida" +title = "Fes la visita guiada" + [onboarding.desktopInstall] body = "Stirling funciona millor com a aplicació d'escriptori. Pots usar-la fora de línia, accedir als documents més ràpid i fer edicions localment a l'ordinador." selectOs = "Selecciona el sistema operatiu" @@ -4996,7 +5277,21 @@ titleWithOs = "Baixa per a {{osLabel}}" [onboarding.mfa] authenticationCode = "Codi d'autenticació" +description = "Protegeix el teu compte enllaçant una aplicació d'autenticació. Escaneja el codi QR o introdueix la clau de configuració i després confirma el codi de 6 dígits per acabar." +enable = "Activa MFA" +enableError = "No s'ha pogut activar l'autenticació de dos factors. Comprova el codi i torna-ho a provar." +enterCode = "Introdueix el codi de 6 dígits de la teva aplicació." +enterCodeError = "Introdueix el codi d'autenticació per continuar." +logout = "Tanca la sessió" +openAuthenticator = "Obre Google Authenticator, Authy o 1Password." qrCodeLoading = "Generant el codi QR…" +regenerateQrCode = "Regenera el codi QR" +scanQrCode = "Escaneja el codi QR o introdueix la clau de configuració de sota." +setupError = "No s'ha pogut iniciar la configuració de dos factors. Torna-ho a provar." +setupKey = "Clau de configuració (entrada manual)" +stepByStep = "Pas a pas" +success = "MFA s'ha activat. Ara pots continuar." +title = "Configuració de l'autenticació multifactor" [onboarding.planOverview] adminBodyLoginDisabled = "Quan habilitis el mode d'inici de sessió, podràs gestionar usuaris, configurar paràmetres i supervisar la salut del servidor. Les primeres {{freeTierLimit}} persones del teu servidor poden usar Stirling gratuïtament." @@ -5005,6 +5300,11 @@ adminTitle = "Resum d'administració" userBody = "Convida companys, assigna rols i mantén els documents organitzats en un espai de treball segur. Habilita el mode d'inici de sessió quan estiguis llest per anar més enllà de l'ús individual." userTitle = "Resum del pla" +[onboarding.processorIntro] +body = "Stirling ara executa Polítiques — regles automatitzades que classifiquen, protegeixen i processen cada document quan arriba. Configura-les i supervisa les execucions al Processador." +cta = "Consulta el Processador" +title = "Consulta el Processador de Stirling" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Any prepagat" + +[payg.prepaid.banner] +exhausted = "La teva capacitat prepagada s'ha esgotat; la facturació per ús s'ha reprès. Recarrega per mantenir el descompte." +expiring_one = "El teu any prepagat acaba d'aquí a {{count}} dia. La capacitat no utilitzada caduca — recarrega per començar un any nou." +expiring_other = "El teu any prepagat acaba d'aquí a {{count}} dies. La capacitat no utilitzada caduca — recarrega per començar un any nou." +low = "Et queda el {{pct}}% de la teva capacitat prepagada. Recarrega en qualsevol moment per mantenir el processament sense interrupcions." + +[payg.prepaid.card] +subtitle = "S'utilitza abans de la facturació per ús i fora del teu límit de despesa." +title = "Capacitat prepagada" + +[payg.prepaid.meter] +capSuffix = "de {{total}} crèdits prepagats" +expires = "Caduca el {{date}}" + +[payg.prepaid.state] +exhausted = "Esgotada" +healthy = "En queda força" +low = "S'està esgotant" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF a PDF/A" tags = "arxiu,llarg termini,estàndard,conversió,emmagatzematge,preservació" title = "PDF a PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "impressió,estàndard,conversió,producció,preimpressió,arxiu" title = "PDF a PDF/X" @@ -5822,6 +6151,7 @@ small = "500 crèdits" xsmall = "100 crèdits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Trieu el pla que s’adapta a les vostres necessitats" title = "Plans disponibles" @@ -5937,49 +6267,12 @@ keyDescription = "Enganxa la clau de llicència del correu electrònic" success = "Llicència activada!" successMessage = "La llicència s'ha activat correctament. Ara pots tancar aquesta finestra." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "aplicada" -enforcing = "Aplicant..." -failed = "Ha fallat l'aplicació" outputsUnavailable = "Els resultats de la política ja no estan disponibles per a descarregar." partialOutputsUnavailable = "Alguns resultats de la política ja no estan disponibles per a descarregar." -retrying = "Ocupat, reintentant..." runNotFound = "Ja no s'ha pogut trobar l'execució de l'aplicació." -step = "pas {{current}}/{{total}}" timedOut = "S'ha esgotat el temps d'aplicació abans que l'execució pogués finalitzar." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Tanca" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Aplicant {{names}}" applyingProgress = "Aplicant {{names}} ({{done}} de {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} de {{total}} fitxer(s) no s'han pogut processar i s' failureTitle = "Exportat sense aplicació completa" printPolicyAppliedBody = "S'ha actualitzat aquest PDF per complir una política. Reviseu els canvis i torneu a imprimir." printPolicyAppliedTitle = "Política aplicada abans d'imprimir" -queued = "+{{count}} en cua" successTitle = "S'ha aplicat {{names}}" summaryMore = "{{first}}, {{second}} i {{more}} més" summaryTwo = "{{first}} i {{second}}" -[policies.enforcement.triggerVerb] -convert = "Aplicant abans de convertir" -default = "Aplicant" -export = "Aplicant abans d'exportar" -input = "Aplicant en importar" -print = "Aplicant abans d'imprimir" - -[policies.field] -accessLog = "Registre d'accés" -archiveAfter = "Arxiva després de" -auditTrail = "Traça d'auditoria" -belowThreshold = "Per sota del llindar" -destination = "Destinació" -frameworks = "Frameworks" -immutableHold = "Retenció immutable" -keepFor = "Conserva durant" -minConfidence = "Confiança mínima" -notify = "Notifica en encaminar" -onViolation = "Quan no compleix" -webhookUrl = "URL del webhook" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Afegeix" -addPlaceholder = "Afegeix una etiqueta…" -customNote = "Personalitzat per al teu equip." -defaultNote = "S'està utilitzant el valor predeterminat integrat, compartit amb el teu equip." -duplicate = "\"{{name}}\" ja existeix." -edit = "Edita les etiquetes" -empty = "Encara no hi ha etiquetes." -export = "Exporta JSON" -iconAria = "Tria una icona per a {{name}}" -import = "Importa JSON" -importError = "No s'ha pogut importar aquest fitxer." -managedNote = "Les etiquetes d'equip les gestiona el líder de l'equip." -modalSubtitle = "Compartides amb tot l'equip. El classificador tria les etiquetes que s'ajusten a cada document." -modalTitle = "Etiquetes de classificació" -pickIcon = "Utilitza la icona {{label}}" -removeAria = "Elimina {{name}}" -resetToDefault = "Restableix el valor predeterminat" -saveForTeam = "Desa per a l'equip" -saving = "S'està desant…" -sectionLabel = "Etiquetes de classificació" -startFromScratch = "Comença des de zero" -teamCount = "etiquetes d'equip" -tooLong = "Les etiquetes poden tenir com a màxim {{max}} caràcters." -ungrouped = "Sense agrupar" -view = "Mostra les etiquetes" +categoryCount = "categories" +hideCategory = "Amaga la categoria" +labelCount = "etiquetes" +sharedNote = "Aquestes etiquetes són integrades i compartides amb tot el teu equip." +showCategory = "Mostra la categoria" [policies.pii] account = "Números de compte (etiquetats)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Números d'encaminament bancari dels EUA (ABA)" ssn = "Números de la Seguretat Social" -[policies.settings] -noneExport = "Actualment no s'executa cap política en exportar." -noneUpload = "Actualment no s'executa cap política en carregar." -onExport = "En exportar" -onUpload = "En carregar" -reorderHandle = "Arrossega per reordenar" -runOrderDesc = "Quan s'executa més d'una política amb el mateix activador, s'executen en aquest ordre: cadascuna sobre la sortida de la política anterior. Arrossega per reordenar." -title = "Configuració de polítiques" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opcions de política" -policySettings = "Configuració de polítiques" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Torna a intentar les polítiques fallides ({{count}})" -rowProgress = "{{completed}} de {{total}} fitxers processats" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Què és una política?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Troba i ennegreix automàticament detalls sensibles — com números de la Seguretat Social i de targetes — perquè no es puguin llegir al document." -sanitize = "Elimina JavaScript ocult del fitxer, perquè no s'executi res automàticament quan algú l'obri." -watermark = "Estampa una marca visible (p. ex., \"Confidencial\") a cada pàgina." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Tanca" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "S'està aplicant {{name}}..." badgeRan = "La política {{name}} s'ha executat en aquest fitxer" +badgeRunning = "{{name}} s'està executant..." blockingAction = "{{action}} bloquejat mentre s'aplica la política; espera, si us plau..." dismiss = "Descarta la superposició" enforcingTitle = "S'està aplicant la política..." @@ -6151,6 +6316,7 @@ viewAnyway = "Mostra el fitxer (la política encara s'està aplicant)" [policyOption] autoRedactPhi = "Oculta automàticament PHI" blockExport = "Bloqueja l'exportació" +ccpa = "CCPA" contracts = "Contractes" documents = "Documents" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Pla Editor" subscribed = "Pla Processor" unlinked = "No enllaçat" -[portal.agentBuilder] -bootstrapFromDocument = "Inicialitza des d'un document" -sectionsAriaLabel = "Seccions del constructor d'agents" -selectorAriaLabel = "Agents" -subtitle = "Dissenya, prova i publica agents d'AI que classifiquen, extreuen informació i encaminen els teus documents. Defineix escenaris, limita l'accés a eines, executa un conjunt de referència i publica'n una versió." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Cancel·la" -dropzoneText = "Tria un document de mostra (PDF o imatge)" -lead = "Deixa anar un document de mostra i proposarem escenaris i un esquema d'extracció que podràs refinar. No es publica res fins que ho revisis." -submit = "Inicialitza l'agent" -subtitle = "Crea la base d'un agent nou a partir d'un fitxer representatiu" -title = "Inicialitza des d'un document" - -[portal.agentBuilder.empty] -description = "Inicialitza un agent a partir d'un document de mostra per crear la base dels seus escenaris i l'esquema d'extracció, i després refina'l i publica'l." -title = "Encara no hi ha agents" - -[portal.agentBuilder.evals] -casesPassing = "Casos aprovats" -columnCase = "Cas d'avaluació" -columnLatency = "Latència" -columnResult = "Resultat" -fail = "fallida" -goldenSetPassRate = "Percentatge d'aprovació del conjunt de referència" -latencyMs = "{{ms}} ms" -notRun = "no executat" -pass = "aprovat" -passRate = "Percentatge d'aprovació" -runEvals = "Executa avaluacions" - -[portal.agentBuilder.evals.empty] -description = "Les avaluacions converteixen els escenaris en un conjunt de referència repetible. Actualitza per capturar el percentatge d'aprovació al llarg del temps i condicionar-hi les publicacions." -title = "Encara no hi ha cap conjunt de referència" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "entre conjunts de referència" -activeAgents = "Agents actius" -avgPassRate = "Percentatge mitjà d'aprovació d'avaluacions" -fleetWide = "a tota la flota" -latestPublished = "Darrer publicat" -scenarios = "Escenaris" -testCases = "casos de prova" -totalDescription_one = "{{count}} en total" -totalDescription_other = "{{count}} en total" - -[portal.agentBuilder.scenarios] -add = "Afegeix" -addScenario = "Afegeix un escenari" -enable = "Activa" -expectationLabel = "Comportament esperat" -expectationPlaceholder = "Què hauria de fer l'agent" -inEval = "en avaluació" -mute = "Silencia" -muted = "silenciat" -nameLabel = "Nom" -namePlaceholder = "p. ex. Escalada de compliment" - -[portal.agentBuilder.tabs] -evals = "Avaluacions" -scenarios = "Escenaris" -tools = "Eines" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "Accés ampli" -deniedHint = "Les eines seleccionades estan bloquejades. Tota la resta continua disponible per cridar." -deniedTools = "Eines denegades" -governanceGate = "La governança d'eines està disponible al pla Enterprise." -restricted = "Restringit" -restrictedAccess = "Accés restringit a eines" -restrictedDescription = "Permet totes les eines excepte les que deneguis a continuació." - -[portal.agentBuilder.versions] -current = "actual" -historyGate = "L'historial complet de versions i la reversió estan disponibles al pla Enterprise." -publish = "Publica" -rollBack = "Reverteix" - [portal.assistant] close = "Tanca l'assistent" error = "No s'ha pogut contactar amb l'assistent." @@ -6365,9 +6451,21 @@ typing = "Escrivint" loading = "S'està carregant" redirectingToEditor = "S'està redirigint a l'editor..." +[portal.billing.activation] +maybeLater = "Potser més tard" +title = "Activa el Processador" + +[portal.billing.activation.payg] +sub = "Estableix un límit mensual. Paga pel que executis." +title = "Paga per ús" + +[portal.billing.activation.prepay] +badge = "2 mesos gratis" +sub = "Una factura, targeta o transferència bancària." +title = "Prepaga un any" + [portal.billing.checkout] noClientSecret = "La funció Edge no ha retornat cap client_secret." -subtitle = "Afegeix una targeta per continuar més enllà de la concessió gratuïta del pla Editor. Stripe s'encarrega de la resta." title = "Activa el pla Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "El pagament s'ha efectuat correctament, però l'activació triga una mic close = "Tanca" title = "Ja gairebé hi som" +[portal.billing.checkout.cap] +amountAria = "Límit de despesa mensual" +back = "Enrere" +continue = "Continua al pagament" +error = "No s'ha pogut establir el teu límit de despesa" +finePrint = "Les factures s'emeten mensualment. Cancel·la en qualsevol moment." +noLimit = "Sense límit" +note = "Mai no se't factura més enllà del teu límit. El processament simplement es pausa." +perMonth = "/ mes" +title = "Estableix el teu límit de despesa" + +[portal.billing.checkout.card] +fields = "Número de targeta · MM / YY · CVC · ZIP" +label = "Dades de la targeta" +note = "Les dades de la targeta les recull Stripe. Stirling mai no emmagatzema PAN ni CVC." + [portal.billing.checkout.error] title = "No s'ha pogut iniciar el pagament" @@ -6383,10 +6497,9 @@ body = "El pagament s'ha efectuat. Estem activant el processament mesurat a tote hint = "Mantén aquesta finestra oberta." title = "S'està activant el pla Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "a l'env del portal per habilitar el pagament dins de l'aplicació." -bodyBefore = "Defineix" -title = "Stripe no està configurat" +[portal.billing.checkout.pay] +limit = "Límit de despesa mensual {{amount}}/mes. El processament es pausa en arribar al límit." +limitNoCap = "Sense límit de despesa. El processament no es pausarà. Les factures s'emeten mensualment." [portal.billing.enterpriseUpsell] cta = "Crea el teu pressupost Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Pla actual" freeForever = "Gratuït per sempre" noTeamResolved = "Encara no s'ha resolt cap equip al teu moneder: actualitza i torna-ho a provar." ownerOnly = "Només el propietari de l'equip pot activar el pla Processor." +payInvoice = "Paga la factura per completar" planName = "Editor" ssoIncluded = "SSO inclòs" switchOnProcessor = "Activa el Processor →" unlimitedUsers = "Usuaris il·limitats" +viewQuote = "Mostra el pressupost" [portal.billing.invoices] columnAmount = "Import" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Sol·licituds directes a l'API" segmentApiLabel = "API" segmentAutomationDesc = "Automatitzacions i pipelines" segmentAutomationLabel = "Automatització" +sizeMultiplier = "{{formatted}} PDFs han utilitzat un multiplicador de mida" +summary = "{{unique}} únics · {{units}} unitats de mesurador · {{avg}} mitjana per PDF" +summaryNoRate = "{{unique}} únics · {{units}} unitats de mesurador" unit = "PDF mesurats" +unitsPending_one = "{{units}} unitat de mesurador pendent de sincronització des d'instàncies enllaçades" +unitsPending_other = "{{units}} unitats de mesurador pendents de sincronització des d'instàncies enllaçades" + +[portal.billing.prepaid] +capSuffix = "de {{total}} crèdits prepagats" +expires = "Caduca el {{date}}" +eyebrow = "Capacitat prepagada" +note = "S'utilitza abans de la facturació per ús i fora del teu límit de despesa." +topUp = "Recarrega" + +[portal.billing.prepaid.buy] +back = "Enrere" +calcTitle = "Calcula el teu pagament anual" +cancel = "Cancel·la" +cancelPurchase = "Cancel·la la compra" +close = "Tanca" +continueToPayment = "Continua al pagament" +finalise = "Finalitza" +invoiceErrorTitle = "No s'ha pogut generar la factura" +loadingTitle = "S'està carregant el teu pressupost" +payTitle = "Paga el teu any" +step = "Pas {{current}} de {{total}}" + +[portal.billing.prepaid.calc] +change = "Canvia" +deployRow = "Desplegament" +done = "Fet" +enterpriseHint = "Això és escala empresarial. Amb aquest volum, les tarifes empresarials superen qualsevol descompte d'autoservei. Bloqueig de tarifa, condicions i un pressupost en minuts." +governanceRow = "Governança" +governanceValue = "{{label}} · {{count}} polítiques" +handlesLabel = "El teu Processador" +handlesValue = "gestiona {{volume}} crèdits / mes" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} polítiques" +poolCaption = "Un sol fons de {{credits}} crèdits per a l'any. Els mesos intensius prenen crèdits dels més lleugers." +priceLabel = "El teu any · 12 mesos pel preu de 10" +rateUnknown = "Mostrarem el preu exacte en finalitzar la compra." +savings = "Estalvies {{amount}} · 2 mesos gratis." +sizingRow = "Dimensionament" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Usuaris totals" + +[portal.billing.prepaid.consent] +label = "Entenc que quan la meva capacitat prepagada s'esgoti o caduqui al cap de 12 mesos, el processament continuarà automàticament a la tarifa estàndard per ús mesurat (fins al meu límit de despesa) tret que cancel·li, i que puc cancel·lar en qualsevol moment des del portal de facturació." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gestionat per Stirling. Actiu en minuts." +cloudValue = "Stirling Cloud · Gestionat" +selfhost = "Autoallotjat" +selfhostDesc = "S'executa en infraestructura privada. Mateixa tarifa." +selfhostValue = "Autoallotjat · Infraestructura privada" + +[portal.billing.prepaid.offer] +cta = "Revisa l'oferta" +subtitle = "Prepaga un any de processament de PDF i obtén dos mesos gratis, utilitzats abans de la facturació per ús i fora del teu límit de despesa." +title = "Aconsegueix 12 mesos pel preu de 10" + +[portal.billing.prepaid.pay] +accountName = "Nom del titular del compte" +accountNamePlaceholder = "p. ex. Jane Smith" +companyName = "Nom de l'empresa (opcional)" +companyNamePlaceholder = "p. ex. Acme Inc." +downloadInvoice = "Baixa la factura" +payOnline = "Paga en línia" +poNumber = "Número de comanda (opcional)" +poNumberPlaceholder = "p. ex. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avançat" +advancedDesc = "Els pipelines impulsen la major part del teu processament." +none = "Cap" +noneDesc = "Encara no executes pipelines. Activa'ls en qualsevol moment." +standard = "Estàndard" +standardDesc = "Alguns pipelines tornen a processar els PDFs entrants." + +[portal.billing.prepaid.posture] +essentials = "Essencials" +essentialsDesc = "Classificació, per defecte, més Compartició" +governed = "Governat" +governedDesc = "Afegeix Seguretat i Encaminament" +regulated = "Regulat" +regulatedDesc = "Totes les categories, incl. Compliment, Retenció, Ingestió" + +[portal.billing.prepaid.review] +download = "Baixa el pressupost (PDF)" +downloading = "S'està preparant…" +share = "comparteix per a aprovació" + +[portal.billing.prepaid.size] +compact = "Compacte" +compactDesc = "Fitxers de menys de 25 MB, sense càrrecs per dades" +heavy = "Pesat" +heavyDesc = "Escanejat o amb moltes imatges, habitualment 50 MB+" +standard = "Estàndard" +standardDesc = "Majoritàriament petit, alguns escanejos per sobre de 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Esgotada" +healthy = "En queda força" +low = "S'està esgotant" [portal.billing.spendLimit] adjustLimit = "Ajusta el límit" @@ -6494,6 +6713,8 @@ label = "Es preveu que se superi." [portal.billing.spendThisMonth] eyebrow = "Despesa aquest mes" +freeRemaining_one = "{{formatted}} PDF gratuït restant" +freeRemaining_other = "{{formatted}} PDFs gratuïts restants" processed_one = "{{formattedCount}} PDF processat." processed_other = "{{formattedCount}} PDF processats." processedWithRate_one = "{{formattedCount}} PDF processat, a {{rate}} cadascun." @@ -6510,6 +6731,7 @@ reachedTitle = "S'ha assolit el límit de despesa mensual" title = "No s'ha pogut obrir el portal de Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "de {{allowance}} PDF gratuïts utilitzats" capSuffix_other = "de {{allowance}} PDF gratuïts utilitzats" eyebrow = "Prova de Processor" @@ -6534,6 +6756,376 @@ signature = "signatura" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Connexió nova" +createTitleFor = "Connecta {{name}}" +delete = "Suprimeix" +edit = "Edita" +editTitle = "Edita la connexió" +unsupportedType = "Aquest tipus de connexió no es pot editar aquí." + +[portal.connections.categories.advanced] +description = "Per a un servei que encara no admetem." +label = "Avançat" + +[portal.connections.categories.audit] +description = "Registra què ha passat a un document, on els teus auditors ja ho consulten." +label = "Registre d'auditoria i compliment" + +[portal.connections.categories.notify] +description = "Informa un altre sistema que ha passat alguna cosa." +label = "Notificació i workflow" + +[portal.connections.categories.security] +description = "Escaneja o etiqueta un document abans que continuï." +label = "Seguretat i classificació" + +[portal.connections.categories.signing] +description = "Envia un document perquè se signi o es notaritzi." +label = "Signatura electrònica i notarització" + +[portal.connections.categories.storage] +description = "On acaben els documents processats - un bucket, una unitat, un tiquet o una pàgina." +label = "Fitxer i adjunt" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling ho emmagatzema xifrat i no ho torna a mostrar mai." +label = "Token d'accés" + +[portal.connections.commonFields.apiKey] +label = "Clau API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Els passos que utilitzen aquesta connexió només poden cridar rutes sota aquesta URL, mai un altre host." +label = "URL del servidor" + +[portal.connections.commonFields.email] +label = "Correu electrònic" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Contrasenya" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Nom d'usuari" + +[portal.connections.commonFields.webhookUrl] +helperText = "Enganxa la URL que t'ha donat el servei. Només es poden cridar rutes sota aquesta URL." +label = "URL de Webhook" + +[portal.connections.fields] +name = "Nom de la connexió" +namePlaceholder = "p. ex. Producció" + +[portal.connections.picker] +cancel = "Cancel·la" +createNew = "Connexió nova..." +placeholder = "Selecciona una connexió" +save = "Desa la connexió" + +[portal.connections.picker2] +back = "Canvia la integració" +heading = "Què vols connectar?" +noResultsBody = "Encara no ho admetem - explica'ns-ho i mirarem d'afegir-ho." +noResultsTitle = "No coincideix res amb \"{{query}}\"" +searchPlaceholder = "Cerca integracions - prova \"sign\", \"ocr\" o el nom d'un producte" + +[portal.connections.types.api] +description = "Crida qualsevol HTTP API des d'una política. Tries la URL, l'autenticació i la càrrega útil." +label = "API personalitzada" + +[portal.connections.types.api.fields.authType] +label = "Autenticació" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nom d'usuari i contrasenya" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token en una capçalera personalitzada" + +[portal.connections.types.api.fields.authType.options.none] +label = "Cap" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Inicia sessió per obtenir un token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Els passos que utilitzen aquesta connexió només poden cridar rutes sota aquesta URL, mai un altre host." +label = "URL base" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nom de la capçalera" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Ruta d'inici de sessió" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Contrasenya" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Si l'API respon amb un enllaç de baixada, només es poden obtenir aquests hosts. Separa'ls amb comes. Deixa-ho en blanc per permetre només el host propi de la URL base." +label = "Hosts de resultat permesos" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Envia el token com aquesta capçalera" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "La capçalera de resposta en què la crida d'inici de sessió retorna el token, p. ex. X-Auth-Token." +label = "Capçalera de resposta del token" + +[portal.connections.types.api.fields.username] +label = "Nom d'usuari" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Escaneja documents per detectar malware en un servidor que executes - res no surt de la teva xarxa." +label = "ClamAV (autoallotjat)" + +[portal.connections.types.cloudmersive] +description = "Escaneig de virus i contingut per a documents en una política." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloqueja documents que contenen macros, executables, scripts o objectes incrustats - no només malware conegut." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Adjunta documents processats a una pàgina de Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Envia documents a ConsignO Cloud (Notarius) per a signatura electrònica i notarització." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "El teu entorn ConsignO Cloud, incloent /api/v1." +label = "URL del servidor" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Clau API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Secret API" + +[portal.connections.types.consigno.fields.password] +label = "Contrasenya" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Només cal si la teva organització utilitza tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nom d'usuari" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publica un missatge a Discord quan una política processi un document." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexa un esdeveniment d'auditoria a Elasticsearch cada vegada que una política gestiona un document." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Consulta un servidor FTP o FTPS per trobar documents nous." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Mode de connexió" + +[portal.connections.types.ftp.fields.passive.options] +active = "Actiu" +passive = "Passiu (recomanat)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP sense xifrar envia credencials i fitxers sense xifratge; tria FTPS si el servidor ho admet." +label = "Xifratge" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explícit / AUTH TLS)" +implicit = "FTPS (implícit)" +none = "Cap (FTP sense xifrar)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publica un missatge en un espai de Google Chat quan s'executi una política." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Adjunta documents processats a una incidència de Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Envia un document o una notificació per correu electrònic quan s'executi una política." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Arxiva documents processats a Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Contrasenya de l'aplicació" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detecta dades personals en el text del document utilitzant un servidor Presidio que executes." +label = "Presidio (autoallotjat)" + +[portal.connections.types.purview] +description = "Llegeix i aplica etiquetes de sensibilitat de Microsoft Purview als PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcional. Només cal per triar etiquetes pel nom; l'etiquetatge funciona sense això." +label = "ID d'aplicació (client)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Secret de client" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Estampat a cada etiqueta com el seu SiteId, perquè les eines compatibles amb Purview sàpiguen quina organització l'ha aplicat." +label = "ID de directori (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Un bucket S3 del qual les fonts poden llegir i al qual les sortides de pipeline poden escriure." +label = "Emmagatzematge S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID de clau d'accés" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Deixa-ho en blanc per a AWS. Estableix-ho per a MinIO o un altre emmagatzematge compatible amb S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Regió" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Clau d'accés secreta" + +[portal.connections.types.sendgrid] +description = "Envia un document o una notificació per correu electrònic quan s'executi una política." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Consulta un servidor SFTP (SSH) per trobar documents nous." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcional. L'empremta digital SHA-256 de la clau de host del servidor (de `ssh-keyscan host | ssh-keygen -lf -`). Quan s'estableix, només s'accepta un servidor que presenti aquesta clau; quan és en blanc, la clau vista a la primera connexió queda fixada i es rebutgen canvis posteriors." +label = "Empremta digital de la clau de host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frase de pas de la clau" + +[portal.connections.types.sftp.fields.password] +helperText = "Deixa-ho en blanc si t'autentiques amb una clau privada." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcional. Enganxa una clau privada OpenSSH per autenticar-te amb una clau en lloc d'una contrasenya." +label = "Clau privada" + +[portal.connections.types.slack] +description = "Publica un missatge a Slack quan una política processi un document." +label = "Slack" + +[portal.connections.types.smb] +description = "Vigila un recurs compartit de xarxa SMB / CIFS (Windows o Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Recurs compartit de xarxa" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcional. El domini o grup de treball de Windows, si el teu servidor n'utilitza un." +label = "Domini" + +[portal.connections.types.smb.fields.share] +label = "Recurs compartit" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Envia un esdeveniment d'auditoria a Splunk cada vegada que una política gestiona un document." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Envia un esdeveniment d'auditoria a un col·lector HTTP de Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publica un missatge en un canal de Teams quan s'executi una política." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publica cada document, o només els detalls, a una URL que triïs." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Activa un escenari de Zap o Make des d'una política." +label = "Zapier i Make" + +[portal.docs] +browse = "Explora la documentació" +viewSource = "Mostra el codi font a GitHub" + [portal.docs.authentication] codeCaption = "cada sol·licitud" eyebrow = "PRIMERS PASSOS" @@ -6624,6 +7216,9 @@ eyebrow = "HABILITATS" lead = "Capacitats incloses i amb nom que el teu agent invoca com una sola eina. Cada habilitat és una cadena d'operacions determinista amb avaluacions associades." title = "Habilitats d'agent" +[portal.docs.toc] +title = "En aquesta pàgina" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERÈNCIA DE L'API" @@ -6697,6 +7292,8 @@ type = "Tipus" user = "Usuari" [portal.documents.queue.empty] +connectSource = "Connecta una font" +createPipeline = "Crea un pipeline" description = "Quan les fonts introdueixin documents a les teves canalitzacions, apareixeran aquí per revisar-los." title = "No hi ha documents a la cua" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Cap document coincideix amb aquest filtre." rowActions = "Accions de fila" sensitiveLabel = "Sensible" -sensitiveTitle = "Sensible — cal accés" [portal.documents.table.columns] action = "Canalització / Acció" document = "Document" +labels = "Labels" product = "Producte" status = "Estat" time = "Hora" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Vist per última vegada" region = "Regió" status = "Estat" +target = "Target" version = "Versió" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Aquesta vista ha trobat un error inesperat. Torna-ho a provar o t retry = "Torna-ho a provar" title = "Alguna cosa ha anat malament en aquesta pàgina" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Enrere" +body = "Baixa l'aplicació d'escriptori o autoallotja el servidor." +done = "Fet" +guide = "Obre la guia de {{name}}" +openInBrowser = "Obre al navegador en lloc d'això" +sectionDesktop = "Aplicació d'escriptori" +sectionSelfHosted = "Autoallotjat" +title = "Instal·la l'editor Stirling PDF" + +[portal.home.download.docker] +detailBody = "Executa la imatge oficial al teu servidor." +note = "S'obre a :8080. Inici de sessió predeterminat: admin / stirling." +tagline = "Executa el servidor en un contenidor" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Estàndard" + +[portal.home.download.kubernetes] +detailBody = "Instal·la el chart oficial de Helm." +note = "Afegeix un PVC per a /configs per conservar la configuració." +tagline = "Desplega en un clúster amb Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Compilacions natives per a la teva distribució." +guideBtn = "Mostra les opcions d'instal·lació de Linux" +note = "Hi ha compilacions AppImage, .deb i .rpm disponibles per a distribucions habituals." +tagline = "Aplicació d'escriptori nativa per a la teva distribució" +title = "Linux" + +[portal.home.download.mac] +altLabel = "O mitjançant Homebrew" +detailBody = "Executa l'instal·lador o utilitza Homebrew." +downloadBtn = "Descarrega per a macOS" +tagline = "Aplicació nativa per a Apple silicon i Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Executa el .jar en un servidor físic." +downloadBtn = "Descarrega el .jar" +note = "Requereix Java 21+. S'obre a :8080; inici de sessió predeterminat admin / stirling." +runLabel = "Després executa'l" +tagline = "Executa el .jar en un servidor físic" +title = "Configuració manual del servidor" + +[portal.home.download.windows] +altLabel = "O mitjançant winget" +detailBody = "Executa l'instal·lador o utilitza un gestor de paquets." +downloadBtn = "Descarrega l'instal·lador" +tagline = "Aplicació nativa per a Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} actius" -invite = "Convida companys d'equip" +activeOfDeployed = "{{active}} de {{total}} actius aquest mes" name = "Stirling PDF Editor" open = "Obre al navegador" updated = "actualitzat {{time}}" +[portal.home.editor.deploy] +finish = "Finalitza el desplegament" +options = "Mostra les opcions de desplegament" +start = "Desplega l'Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Bona tarda" evening = "Bon vespre" morning = "Bon dia" -[portal.home.onboarding] -dismiss = "Descarta la configuració" -notStarted = "No iniciat" -progress = "{{done}} de {{total}} fet" -title = "Acaba la configuració" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC a tota l'organització, preus per volum compromès i desplegament air-gapped." -cta = "Inicia la prova" -ctaQuote = "Obtén un pressupost" -lead = "Per a més de 250 empleats." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Desplega l'aplicació d'escriptori, gratuïta per sempre." -title = "Descarrega l'Editor PDF" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} actives · {{recommended}} recomanades" -chip = "{{active}} actives" -title = "Activa les polítiques" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} connectades · cada PDF governat allà on arriba" -chip = "{{connected}} connectades" -title = "Connecta les teves fonts" - -[portal.home.quickActions] -subtitle = "Tasques principals per a avui" -title = "Accions ràpides" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositor de 3 passos sobre la biblioteca d'operacions tipades" -title = "Crea una canalització" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, carpetes supervisades" -title = "Connecta una font" - -[portal.home.quickActions.issueApiKey] -blurb = "Clau amb abast amb límits de velocitat i llista d'IP permeses" -title = "Emet una clau API" - [portal.infrastructure] manageEditorDeployment = "Gestiona el desplegament de l'Editor" sectionsAriaLabel = "Seccions d'infraestructura" @@ -6878,13 +7541,9 @@ heading = "Claus API" subheading = "Credencials amb abast amb límits de velocitat per clau, permisos i llistes d'IP permeses." [portal.infrastructure.apiKeys.card] -allowedIps = "IP permeses" -anyIp = "Qualsevol IP (sense llista de permeses)" created = "Creada" lastUsed = "Utilitzada per última vegada" -permissions = "Permisos" -rateLimit = "Límit de velocitat" -rateLimitValue = "{{value}} req/min" +revoke = "Revoca la clau" usageMonth = "Ús aquest mes" usageToday = "Ús avui" @@ -6892,6 +7551,15 @@ usageToday = "Ús avui" description = "Crea una clau amb abast per començar a cridar la Stirling API." title = "Encara no hi ha claus API" +[portal.infrastructure.apiKeys.error] +load = "No s'han pogut carregar les teves claus API. Torna-ho a provar." + +[portal.infrastructure.apiKeys.revoke] +body = "Vols revocar \"{{name}}\"? Qualsevol integració que encara utilitzi aquesta clau començarà a rebre errors 401 immediatament. Això no es pot desfer." +cancel = "Cancel·la" +confirm = "Revoca la clau" +title = "Revoca la clau API" + [portal.infrastructure.audit] filterAriaLabel = "Filtra esdeveniments d'auditoria per categoria" heading = "Registres d'auditoria" @@ -6935,6 +7603,7 @@ all = "Tots" auth = "Autenticació" config = "Configuració" elevation = "Elevació" +policy = "Política" processing = "Processament" security = "Seguretat" @@ -6944,19 +7613,30 @@ title = "No tens accés" [portal.infrastructure.audit.metrics] config = "Configuració" -elevation = "Elevació" +policy = "Política" processing = "Processament" totalEvents = "Esdeveniments totals · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autenticació" +config = "Configuració" +elevation = "Elevació" +policy = "Política" +processing = "Processament" +security = "Seguretat" + +[portal.infrastructure.auditStatusLabel] +danger = "Error" +info = "Info" +success = "Correcte" +warning = "Advertiment" + [portal.infrastructure.createKey] cancel = "Cancel·la" createKey = "Crea una clau" done = "Fet" -ipAllowlistHelper = "Intervals CIDR separats per comes. Deixa-ho en blanc per permetre qualsevol IP." -ipAllowlistLabel = "Llista d'IP permeses" keyNameLabel = "Nom de la clau" keyNamePlaceholder = "p. ex. Producció · ingestió" -permissionsLabel = "Permisos" secretKeyCaption = "Clau secreta" secretWarning = "Desa-ho en un gestor de secrets. Stirling només emmagatzema un hash — no hi ha manera de recuperar-ho més tard." subtitle = "Limita la clau al mínim que necessita. La pots rotar o revocar en qualsevol moment." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copia aquest secret ara — no es tornarà a mostrar." title = "Crea una clau API" titleCreated = "Clau creada" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Desplegat per" -environment = "Entorn" -product = "Producte" -status = "Estat" -version = "Versió" -when = "Quan" - -[portal.infrastructure.deployments.recent] -heading = "Desplegaments recents" -subheading = "Els darrers desplegaments en productes i entorns." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instàncies" -latency = "Latència" -load = "Càrrega" -p99 = "P99" -region = "Regió" -status = "Estat" -throughput = "Rendiment" -uptime = "Temps d'activitat" -version = "Versió" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "Salut en directe de cada regió Stirling desplegada — latència, càrrega i versió del desplegament." - -[portal.infrastructure.deployments.regions.empty] -description = "Les regions desplegades apareixen aquí un cop s'hagi proveït el teu espai de treball." -title = "No hi ha regions desplegades" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "El catàleg de models i l'encaminament que impulsen el processament de documents al teu espai de treball." - -[portal.infrastructure.models.byom] -description = "Registra un model on-prem o self-hosted i fixa'l a una regió per a processament subjecte a residència de dades." -title = "Porta el teu propi model" - -[portal.infrastructure.models.catalogue] -heading = "Catàleg" -sub = "Models gestionats disponibles al teu espai de treball, amb latència i cost en directe." -subEnterprise = "Models gestionats, porta-el-teu-propi i on-prem — amb fixació per regió disponible." - -[portal.infrastructure.models.catalogue.empty] -description = "Els models del catàleg del teu espai de treball apareixen aquí." -title = "No hi ha models disponibles" - -[portal.infrastructure.models.columns] -cost = "Cost" -latency = "Latència" -load = "Càrrega" -model = "Model" -status = "Estat" -type = "Tipus" -version = "Versió" - -[portal.infrastructure.models.metrics] -activeModels = "Models actius" -avgLatency = "Latència mitjana" -included = "Inclòs" -monthlySpend = "Despesa mensual en models" - -[portal.infrastructure.models.routing] -empty = "No hi ha regles d'encaminament configurades." -heading = "Regles d'encaminament" -sub = "Quin model gestiona cada operació. El valor predeterminat s'aplica quan no coincideix cap regla més específica." -subLocked = "Encamina operacions a models específics — disponible en plans de pagament." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Actualitza a Pro per controlar quin model gestiona cada operació i tipus de document." -title = "L'encaminament de models és una funció de pagament" - -[portal.infrastructure.models.routingColumns] -default = "Predeterminat" -docType = "Tipus de document" -modelForAria = "Model per a {{operation}}" -operation = "Operació" -routedTo = "Encaminat a" - -[portal.infrastructure.security.access.byok] -description = "Proporciona una clau del teu propi KMS. Stirling hi xifra però encara pot llegir." -label = "Porta la teva pròpia clau (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Les claus no surten mai del teu KMS. Stirling només conserva text xifrat." -label = "Conserva la teva pròpia clau (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling gestiona les claus de xifratge. El més senzill — zero operacions de claus per part teva." -label = "Claus conservades per Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Política d'accés a documents" -subheading = "Controla qui pot desxifrar documents processats en repòs." - -[portal.infrastructure.security.attestations] -heading = "Atestacions de compliment" -noReport = "No hi ha cap informe disponible" -subheading = "Postura d'auditoria per marc, amb informes disponibles sobre controls atestats." -viewReport = "Mostra l'informe →" - -[portal.infrastructure.security.compliance] -heading = "Compliment" -subheading = "Atestacions i certificacions que cobreixen la plataforma Stirling." - -[portal.infrastructure.security.empty] -description = "La configuració de seguretat del teu espai de treball apareixerà aquí." -title = "Postura de seguretat no disponible" - -[portal.infrastructure.security.hyokBanner] -description = "Amb HYOK, les claus de xifratge no surten mai del teu KMS. Stirling emmagatzema i processa només text xifrat que pots revocar en qualsevol moment." -title = "Stirling no pot desxifrar els teus documents" - -[portal.infrastructure.security.ipAllowlist] -empty = "No hi ha intervals IP configurats — es permeten totes les IP." -heading = "Llista d'IP permeses" -sub = "L'accés API està restringit a aquests intervals CIDR." -subLocked = "Restringeix l'accés API a intervals IP coneguts — disponible en plans de pagament." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Actualitza a Pro per restringir l'accés API a xarxes específiques." -title = "La llista d'IP permeses és una funció de pagament" - -[portal.infrastructure.security.ipColumns] -added = "Afegit" -addedBy = "Afegit per" -cidr = "CIDR" -label = "Etiqueta" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorisme" -heading = "Gestió de claus de xifratge" -keyId = "Identificador de clau" -lastRotated = "Rotada per última vegada" -rotateKey = "Rota la clau" -rotationPolicy = "Política de rotació" -subheading = "Custòdia de les claus que xifren documents en repòs — qui pot desxifrar i com roten les claus." - -[portal.infrastructure.security.managedBanner] -description = "La custòdia porta-la-teva-pròpia-clau (BYOK) i conserva-la-teva-pròpia-clau (HYOK) està disponible a Enterprise. Actualitza per proporcionar claus del teu propi KMS." -title = "Les claus les gestiona Stirling al teu pla" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Àsia-Pacífic" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · límit de dades GDPR" -label = "Unió Europea" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Estats Units" - -[portal.infrastructure.security.residencyHeader] -heading = "Residència de dades" -subheading = "On s'emmagatzemen i es processen els documents." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} utilitzat" - -[portal.infrastructure.storage.empty] -description = "L'emmagatzematge connectat i l'ús apareixen aquí." -title = "No hi ha emmagatzematge configurat" - -[portal.infrastructure.storage.lifecycle] -active = "Actiu" -activeRange = "0–{{value}}d" -archived = "Arxivat" -coldStorage = "emmagatzematge fred" -deleted = "Eliminat" -never = "mai" -purged = "purgat" - -[portal.infrastructure.storage.providers] -connect = "Connecta" -connected = "Connectat" -heading = "Proveïdors connectats" -subheading = "On s'escriuen els artefactes processats." - -[portal.infrastructure.storage.retention] -heading = "Retenció" -subheading = "Quant de temps es conserven els artefactes abans de l'eliminació del cicle de vida." -windowLabel = "Finestra de retenció predeterminada" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dia" -days_other = "{{count}} dies" -never = "No eliminis mai" - -[portal.infrastructure.storage.totalUsage] -heading = "Ús total" -progressLabel = "Emmagatzematge utilitzat" -subheading = "Emmagatzematge consumit a tots els proveïdors connectats." +[portal.infrastructure.keyLabel] +active = "Activa" +revoked = "Revocada" [portal.infrastructure.tabs] apiKeys = "Claus API" @@ -7174,6 +7656,49 @@ models = "Models" security = "Seguretat" storage = "Emmagatzematge" +[portal.integrations] +availableHeading = "Disponibles" +comingSoonHeading = "Properament" +connect = "Connecta" +connectionCount_one = "{{count}} connexió" +connectionCount_other = "{{count}} connexions" +customApi = "API personalitzada" +searchPlaceholder = "Cerca integracions" +subtitle = "Tot allò amb què Stirling es pot comunicar. Les credencials es creen aquí una sola vegada i després es referencien des de fonts, polítiques i pipelines." +title = "Integracions" +typedName = "Nom de {{tool}}" + +[portal.integrations.filters] +all = "Totes" +audit = "Auditoria" +connected = "Connectades" +notify = "Notifica" +security = "Seguretat" +signing = "Signatura" +storage = "Emmagatzematge" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Connectada" + +[portal.integrations.table] +integration = "Integració" +worksWith = "Funciona amb" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Polítiques" +sources = "Fonts" + +[portal.legal] +draft = "{{label}} · esborrany" +loadError = "No s'ha pogut carregar aquest document. Torna-ho a provar." +loading = "S'està carregant…" +title = "Document legal" + [portal.nav] agent-builder = "Constructor d'agents" components = "Components" @@ -7182,33 +7707,17 @@ documents = "Documents" editor = "Editor" home = "Inici" infrastructure = "Infraestructura" +integrations = "Integracions" pipelines = "Canalitzacions" policies = "Polítiques" procurement = "Compres" -settings = "Configuració" sources = "Fonts" usage = "Ús i facturació" users = "Usuaris" -[portal.notifications] -markAllRead = "Marca-ho tot com a llegit" -title = "Notificacions" -viewAll = "Mostra-ho tot" - -[portal.notifications.ariaLabel] -none = "Notificacions, cap sense llegir" -unread_one = "Notificacions, {{count}} sense llegir" -unread_other = "Notificacions, {{count}} sense llegir" - -[portal.notifications.count] -allRead = "tot llegit" -loading = "carregant" -new_one = "{{count}} nova" -new_other = "{{count}} noves" - -[portal.notifications.empty] -description = "No hi ha notificacions noves." -title = "Estàs al dia" +[portal.nav.section] +platform = "Plataforma PDF" +processor = "Processador PDF" [portal.pipelines] subtitle = "Cada canalització de documents automatitzada al backend: una cadena ordenada d'operacions sobre un conjunt de fonts, executada amb un disparador. Fes clic en una fila per veure'n els passos i les fonts." @@ -7218,54 +7727,87 @@ title = "Canalitzacions" newPipeline = "Canalització nova" [portal.pipelines.builder] -addStep = "Afegeix una eina" +activate = "Activate" back = "Torna a les canalitzacions" +cannotFollow = "No es pot prendre {{produced}}" +chooseAccount = "Tria un compte" +chooseDestination = "Tria una destinació" +chooseOperation = "Tria què fa aquest pas" +chooseSource = "Tria una font" discard = "Descarta els canvis" -enabled = "Activat" +inputs = "Entrada" +inputSource = "Font d'entrada" +inputTrigger = "Activador" keepEditing = "Continua editant" -needsUpload = "Cal un fitxer pujat" +moreActions = "More actions" +needsConfiguring = "Cal configurar-ho" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Cap eina coincideix amb la cerca." -pipelineSettings = "Configuració de la canalització" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Cerca eines" -selectToolBody = "Afegeix una eina per construir la teva canalització." -selectToolTitle = "Encara no hi ha eines" -toolSettings = "Configuració de l'eina" +sendToSystem = "Envia a un altre sistema" +stepsIncompatible = "Aquests passos no es poden executar amb el que produeix el pas anterior: {{tools}}." +stepsNeedSetup = "Aquests passos encara s'han de configurar abans de desar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operació no reconeguda, conservada tal com està." unsavedBody = "Tens canvis sense desar. Desa'ls abans de sortir o els vols descartar?" unsavedTitle = "Canvis sense desar" -uploadUnsupported = "Els fitxers pujats encara no són compatibles amb les canalitzacions, així que aquests passos no es poden desar: {{tools}}." usesDefaults = "S'executa amb la configuració predeterminada" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combina tots els fitxers del pas anterior" +fan-out = "S'executa una vegada per cada fitxer del pas anterior" +format-mismatch = "Necessita {{accepts}}, però el pas anterior produeix {{produced}}" +output-uncertain = "Pot ser que no s'executi: la sortida del pas anterior depèn de com estigui configurat" +source-mismatch = "Necessita {{accepts}}, però l'entrada d'aquest pipeline és {{produced}}" +undeclared-operation = "No es pot comprovar què accepta aquest pas" [portal.pipelines.composer] addTool = "Afegeix una eina" -cancel = "Cancel·la" -chainEmpty = "Afegeix una eina per començar a construir la teva canalització." create = "Crea una canalització" -directory = "Carpeta de sortida" -directoryHelp = "Camí absolut al servidor. Ha d'estar dins de les carpetes permeses configurades." +createPaused = "Create paused" editingUnsupported = "Encara no és compatible mostrar aquests paràmetres d'eina per editar-los." -moveDown = "Mou avall" -moveUp = "Mou amunt" +editSource = "Edit source" name = "Nom" namePlaceholder = "p. ex. Revisió de redacció" -noSources = "Encara no hi ha fonts connectades. La canalització encara es pot executar sobre fitxers proporcionats directament." noToolSettings = "Aquesta eina no té configuració ajustable." -operations_one = "Operació ({{count}})" -operations_other = "Operacions ({{count}})" output = "Sortida" -removeStep = "Elimina l'operació" save = "Desa els canvis" scheduleEvery = "Executa cada" -sources = "Fonts" -sourcesLoading = "Carregant fonts..." trigger = "Disparador" triggerManual = "Només manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dies" hours = "hores" minutes = "minuts" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Vols eliminar \"{{name}}\"? Això no es pot desfer." cancel = "Cancel·la" @@ -7273,28 +7815,62 @@ confirm = "Elimina" title = "Vols eliminar la canalització?" [portal.pipelines.detail] +clearHistory = "Esborra l'historial" delete = "Elimina la canalització" run = "Executa ara" [portal.pipelines.empty] action = "Crea una canalització" +connectSource = "Connecta una font" description = "Crea la teva primera canalització: tria les fonts sobre les quals s'executa, encadena les operacions i tria on va la sortida." title = "Encara no hi ha canalitzacions" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Actives" paused = "En pausa" total = "Canalitzacions" -[portal.pipelines.output] -folder = "Escriu a la carpeta" -inline = "Retorna fitxers" - [portal.pipelines.run] +allProcessed_one = "No hi ha res per executar: el document {{count}} de la font ja s'ha processat." +allProcessed_other = "No hi ha res per executar: tots els {{count}} documents de les fonts ja s'han processat." completed_one = "Execució completada." completed_other = "Totes les {{count}} execucions completades." empty = "No hi ha res per executar: les fonts no tenien documents per processar." failed = "L'execució ha fallat: {{error}}" +inFlight = "No hi ha res nou per executar: encara s'estan processant documents d'una execució anterior." +parked_one = "No hi ha res per executar: {{count}} document ha fallat anteriorment i està aparcat. Corregeix-ne la causa i després esborra l'historial per tornar-ho a provar." +parked_other = "No hi ha res per executar: {{count}} documents han fallat anteriorment i estan aparcats. Corregeix-ne la causa i després esborra l'historial per tornar-ho a provar." running = "Execució iniciada; encara està en curs." timeout = "L'execució triga més del previst; pot ser que encara acabi en segon pla." @@ -7307,6 +7883,7 @@ name = "Canalització" sources = "Fonts" status = "Estat" steps = "Passos" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Supervisió de carpeta" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Programat" [portal.policies] +defaultName = "Política de {{category}}" subtitle = "Automatitzacions permanents que apliquen una canalització d'eines a cada document. Cada política s'activa en pujar o exportar, executa la seva cadena d'eines i desa la versió aplicada al costat de l'original." title = "Polítiques" [portal.policies.card] comingSoon = "Actualitza a Enterprise" notSetUp = "No configurat" +requiresAiEngine = "Requereix motor d'IA" + +[portal.policies.categories.classification] +desc = "Identifica el tipus de cada document en pujar-lo i etiqueta'n les metadades per arxivar-lo i cercar-lo." +label = "Classificació" [portal.policies.categories.compliance] desc = "Aplica requisits HIPAA, GDPR, SOC 2 o FedRAMP a cada document." @@ -7344,6 +7927,13 @@ label = "Seguretat" [portal.policies.config] scopeAll = "Tots els documents" +[portal.policies.config.classification] +summary = "Classifica cada document pujat segons les etiquetes del teu equip i n'etiqueta les metadades." + +[portal.policies.config.classification.rules] +0 = "Classifica" +1 = "Etiqueta les metadades" + [portal.policies.config.compliance] summary = "Valida documents contra marcs reguladors abans que surtin del sistema." @@ -7371,6 +7961,29 @@ minConfidence = "Confiança mínima" 2 = "Anomena" 3 = "Normalitza" +[portal.policies.config.purview] +label = "Aplica una etiqueta de sensibilitat de Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant de Purview" +labelId = "ID de l'etiqueta" +labelIdHelp = "El GUID de l'etiqueta, del portal de Purview. En aplicar una etiqueta, s'escriuen les seves metadades al PDF; no el pot xifrar, això requereix el client de Microsoft." +labelName = "Nom de l'etiqueta (opcional)" +labelNameHelp = "S'enregistra juntament amb l'ID perquè les persones i les eines puguin reconèixer l'etiqueta." + +[portal.policies.config.purview.fields.method] +help = "Purview distingeix una etiqueta aplicada per una política d'una triada per una persona." +label = "Com s'enregistra l'etiqueta" +privileged = "Com a triada per una persona" +standard = "Com a aplicada automàticament" + +[portal.policies.config.purviewRead] +label = "Llegeix una etiqueta de Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant de Purview" +connectionHelp = "Llegeix l'etiqueta que el document ja porta, perquè els passos posteriors hi puguin actuar." + [portal.policies.config.retention] summary = "Aplica quant de temps es conserven els documents, quan s'arxiven i quan s'eliminen." @@ -7438,6 +8051,7 @@ title = "Encara no hi ha activitat" [portal.policies.endpoints] addWatermark = "Marca d'aigua" autoRedact = "Censura PII" +classifyAndLabel = "Classifica" compressPdf = "Comprimeix" flatten = "Aplana" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Les teves polítiques s'han desat i apareixeran quan es restablei retry = "Torna-ho a provar" title = "Backend no disponible" +[portal.policies.operations] +change = "Canvia què fa aquest pas" +noResults = "Cap pas no coincideix amb això. Prova amb un nom de producte, o \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Cerca passos: prova \"scan\", \"attach\" o un nom de producte" + +[portal.policies.operations.bodyMode] +binary = "Bytes de fitxer en brut" +json = "Dins d'una càrrega JSON" +multipart = "Com a pujada de fitxer (multipart)" + +[portal.policies.operations.clamavScan] +description = "El teu propi servidor ClamAV comprova el document; res no surt de la teva xarxa." +label = "Escaneja per detectar virus (autoallotjat)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rebutja documents que continguin macros, executables, scripts o objectes incrustats." +label = "Bloqueja el contingut actiu" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive comprova el document i l'execució s'atura si no està net." +label = "Escaneja per detectar virus" + +[portal.policies.operations.confluenceAttach] +description = "Arxiva el document processat en una pàgina." +label = "Adjunta a una pàgina de Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Envia el document a ConsignO Cloud per signar-lo." +label = "Envia per a signatura (ConsignO)" +note = "Envia el document. Encara no s'admet recuperar la còpia signada, perquè un pas no pot passar un resultat a un pas posterior." + +[portal.policies.operations.customApiCall] +description = "Defineix tu mateix la crida: camí, mètode, cos i gestió de la resposta." +label = "Crida una API personalitzada" + +[portal.policies.operations.discordNotify] +description = "Indica a un canal que la política ha gestionat un document." +label = "Publica un missatge a Discord" + +[portal.policies.operations.elasticIndex] +description = "Enregistra el que ha fet la política com a document cercable." +label = "Indexa un esdeveniment d'auditoria a Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart és adient per a API de pujada; JSON és adient per a API que volen el fitxer dins d'una càrrega; binary envia els bytes en brut." +label = "Com enviar el document" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Un cos JSON enviat tal qual. {{document.base64}} porta el fitxer; {{document.*}} i {{run.*}} s'emplenen per document." +label = "Cos JSON" + +[portal.policies.operations.fields.connection] +label = "Compte" + +[portal.policies.operations.fields.domain] +label = "Domini de Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "El camp del formulari sota el qual l'API espera el document." +label = "Nom del camp de fitxer" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Un objecte JSON de parells nom/valor de capçaleres. La credencial de la connexió s'afegeix automàticament." +label = "Capçaleres" + +[portal.policies.operations.fields.index] +label = "Índex" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Clau de la incidència" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "S'envia com a cos del missatge. {{document.*}} i {{run.*}} s'emplenen per document." +label = "Missatge" + +[portal.policies.operations.fields.method] +label = "Mètode" + +[portal.policies.operations.fields.pageId] +label = "ID de pàgina" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "S'afegeix a l'URL base de la connexió. Mai no pot arribar a un altre host." +label = "Camí" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "On escriure'l a la unitat. {{document.filename}} s'emplena per document." +label = "Camí de destinació" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report deixa el document intacte. Replace el substitueix pel que torni." +label = "Què fer amb la resposta" + +[portal.policies.operations.fields.signerEmail] +label = "Correu electrònic del signant" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Assumpte" +placeholder = "Processat: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analitza aquest text. Utilitza {{document.title}} o enganxa el text que vols comprovar." +label = "Text per comprovar" + +[portal.policies.operations.fields.to] +label = "A" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nom d'usuari de Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Indica a un espai que la política ha gestionat un document." +label = "Publica un missatge a Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arxiva el document processat en una incidència." +label = "Adjunta a una incidència de Jira" + +[portal.policies.operations.mailgunEmail] +description = "Envia el document processat com a fitxer adjunt." +label = "Envia el document per correu electrònic (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Escriu el document processat en una carpeta." +label = "Puja a Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "El teu propi servidor Presidio informa de les dades personals que troba." +label = "Detecta dades personals" +note = "Presidio llegeix text, no fitxers. Dona-li el text que vols comprovar; el document en si no es puja." + +[portal.policies.operations.responseMode] +replace = "Substitueix el document per la resposta" +report = "Mantén el document, enregistra la resposta" + +[portal.policies.operations.sendgridEmail] +description = "Envia el document processat com a fitxer adjunt." +label = "Envia el document per correu electrònic (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Indica a un canal que la política ha gestionat un document." +label = "Publica un missatge a Slack" + +[portal.policies.operations.splunkEvent] +description = "Enregistra el que ha fet la política on els teus auditors ja ho consulten." +label = "Envia un esdeveniment d'auditoria a Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Enregistra el que ha fet la política en un col·lector HTTP." +label = "Envia un esdeveniment d'auditoria a Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Indica a un canal que la política ha gestionat un document." +label = "Publica un missatge a Teams" + +[portal.policies.operations.webhookPost] +description = "Envia el document en si a una URL que triïs." +label = "Publica el document en un webhook" + +[portal.policies.operations.zapierNotify] +description = "Passa els detalls de l'execució a la teva automatització." +label = "Activa un Zap o un escenari de Make" + [portal.policies.stats] activeFor = "Activa" dataProcessed = "Dades processades" @@ -7473,6 +8282,13 @@ label = "Documents aplicats" description = "Configurades però sense executar-se" label = "En pausa" +[portal.policies.table] +appliesTo = "S'aplica a" +docs = "Docs aplicats" +enforces = "Aplica" +policy = "Política" +status = "Estat" + [portal.policies.wizard.actions] back = "Enrere" cancel = "Cancel·la" @@ -7480,6 +8296,10 @@ continue = "Continua" enablePolicy = "Activa la política" saveChanges = "Desa els canvis" +[portal.policies.wizard.capability.classify] +desc = "Identifica el tipus del document a partir de les etiquetes del teu equip i l'etiqueta, perquè s'arxivi i es cerqui per categoria." +label = "Classifica el document" + [portal.policies.wizard.capability.compress] desc = "Comprimeix el document per reduir la mida del fitxer." label = "Redueix la mida del fitxer" @@ -7500,10 +8320,18 @@ label = "Censura informació sensible" desc = "Elimina JavaScript ocult perquè no es pugui executar res automàticament quan s'obri el document." label = "Elimina contingut actiu" +[portal.policies.wizard.capability.timestampPdf] +desc = "Demostra que el document existia exactament en aquesta forma en un moment determinat, utilitzant una autoritat de segell de temps independent. Només s'envia un hash: el document no surt mai del teu servidor." +label = "Afegeix un segell de temps de confiança" + [portal.policies.wizard.capability.watermark] desc = "Estampa una marca visible (p. ex. “Confidencial”) a totes les pàgines." label = "Aplica una marca d'aigua" +[portal.policies.wizard.classification] +description = "Cada document pujat es classifica segons les etiquetes integrades i s'etiqueta amb els tipus que hi encaixen. El conjunt d'etiquetes es comparteix amb tot el teu equip." +labelsHeading = "Etiquetes de classificació" + [portal.policies.wizard.errors] noTools = "Activa primer almenys una eina al flux de treball." saveFailed = "No s'ha pogut desar la política. Torna-ho a provar." @@ -7548,73 +8376,118 @@ setUp = "Configura la política de {{category}}" [portal.policies.wizard.workflow] description = "Tria què fa aquesta política a cada document que processa." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} actives" -noRule = "Encara no s'ha aplicat cap regla" -subtitle = "Automatitzacions permanents per les quals passa cada document, independentment de quin pipeline el gestioni." -title = "Què s'executa als teus PDFs" - [portal.policySummary.action] -comingSoon = "Actualitza a Enterprise" -configure = "Configura" setUp = "Configura" -[portal.policySummary.column] -activeRule = "Regla activa" -policy = "Política" -status = "Estat" +[portal.processorFlow] +footnote = "Els recomptes són de les darreres 24 hores. La velocitat del flux és il·lustrativa." +liveBadge = "En directe" +stats = "{{connected}} connectats · {{processed}} PDFs processats" +title = "Processador PDF" -[portal.policySummary.empty] -description = "Quan es configurin les polítiques, les categories apareixeran aquí." -title = "Encara no hi ha polítiques" +[portal.processorFlow.lens] +ariaLabel = "Mode de visualització" +flow = "Flux" +sankey = "Sankey" -[portal.policySummary.state] -active = "Activa" -off = "Desactivada" -soon = "Aviat" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Fallits" +heading = "Resultats" +success = "Lliurats" -[portal.processingStatus] -managePlan = "Gestiona el pla" -volumeSuffix = "PDFs processats · darrers 30 dies" +[portal.processorFlow.policies] +activeCount = "{{n}} actives" +count = "{{n}} · 24 h" +heading = "Polítiques" +setUp = "Configura" +soon = "Properament" + +[portal.processorFlow.sankey] +waist = "Polítiques · {{n}} actives" + +[portal.processorFlow.sankey.empty] +description = "Connecta una font i activa una política per veure com els documents flueixen pel processador." +title = "Encara no hi ha flux" + +[portal.processorFlow.sources] +comingSoonTag = "Connecta" +editor = "Editor de Stirling PDF" +heading = "Fonts" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Entrada de correu electrònic" [portal.procurement] -reset = "Restableix la contractació (demo)" subtitle = "Aconsegueix que el teu equip sigui avaluat, contractat i incorporat. Tots els documents en un sol lloc." title = "Contractació" -[portal.procurement.action] -download = "Baixa" -pay = "Paga ara" -request = "Sol·licita" -sign = "Revisa i signa" -upload = "Carrega" - [portal.procurement.agreement] agreeCta = "Accepta i subscriu-te" +confidential = "Confidencial" confirm = "He llegit i accepto el Stirling Enterprise Agreement." -eyebrow = "Acord" -intro = "Un únic acord combinat cobreix el teu tracte: Master Service Agreement, Order Form, EULA i Data Processing Agreement. Revisa'l i després accepta per convertir el pressupost en una subscripció compromesa." -title = "Revisa el teu acord enterprise" +docName = "Acord de Stirling Enterprise" +docSub = "MSA, formulari de comanda, EULA i DPA, combinats en una sola signatura." +download = "Descarrega" +downloadDraftError = "No s'ha pogut generar el PDF de l'acord: el renderitzador de documents no està disponible. Torna-ho a provar d'aquí a poc o contacta amb el suport." +downloadError = "No s'ha pogut descarregar l'acord signat. Torna-ho a provar." +legalName = "Nom de l'entitat legal" +legalNamePlaceholder = "L'entitat legal que signarà" +loadError = "No s'ha pogut carregar l'acord. Torna-ho a provar." +loading = "S'està carregant l'acord..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Sol·licita canvis" +scrollHint = "Desplaça't per revisar" +signatory = "Nom del signant" +signatoryPlaceholder = "Nom complet" +signatoryTitle = "Càrrec del signant" +signatoryTitlePlaceholder = "p. ex. director jurídic" +signError = "No s'ha pogut enregistrar la teva signatura. Torna-ho a provar." [portal.procurement.builder] addons = "Complements" +addressLine1 = "Línia d'adreça 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Línia d'adreça 2" +addressLine2Placeholder = "Suite, planta, edifici" back = "Enrere" businessName = "Nom de l'empresa" businessNamePlaceholder = "La teva empresa" +city = "Ciutat" +cityPlaceholder = "San Francisco" +completeRequired = "Completa els camps obligatoris (marcats amb *) amb un correu electrònic vàlid abans de generar el pressupost." +contactEmail = "Correu electrònic de contacte" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nom de contacte" +contactNamePlaceholder = "Jane Doe" continue = "Continua" -country = "País" -countryEuro = "Eurozona (EUR €)" -countryUK = "Regne Unit (GBP £)" -countryUS = "Estats Units (USD $)" +done = "Fet" eula = "He llegit i accepto l'EULA de Stirling Enterprise. Regeix l'acord generat a partir d'aquest pressupost." generate = "Genera pressupost" -included = "Inclòs" indemnification = "Indemnització de PI" indemnificationSub = "Defensem les reclamacions de PI que compleixin els requisits, segons l'EULA" -offlineLicense = "Llicència offline / air-gapped" -offlineLicenseSub = "Un fitxer de llicència descarregable per a una instància autoallotjada air-gapped" +paperEyebrow = "Pressupost Enterprise" +paperFor = "Preparat per a" +pdfSize = "Mida del PDF" +poNumber = "Número de PO" +poNumberPlaceholder = "Opcional" +postalCode = "Codi postal" +postalCodePlaceholder = "94105" +posture = "Governança" +posture_count = "~{{count}} polítiques" +posture_essentials = "Essencials" +posture_essentialsSub = "Classificació + Compartició — els valors predeterminats" +posture_governed = "Governat" +posture_governedSub = "Afegeix Seguretat i Encaminament" +posture_regulated = "Regulat" +posture_regulatedSub = "Totes les categories — Compliment, Retenció, Ingesta" qbr = "Revisions empresarials trimestrals" qbrSub = "El teu SE revisa l'ús i el full de ruta cada trimestre" +region = "Estat / regió" +regionPlaceholder = "Califòrnia" running = "{{annual}} / any · {{years}} anys {{tcv}}" s1Sub = "El teu equip i els PDFs que preveus processar cada any." s1Title = "Quant processareu?" @@ -7623,6 +8496,12 @@ s2Title = "Compromís i servei" s3Sub = "Per al pressupost i l'acord que genera." s3Title = "Les teves dades" serviceLevel = "Nivell de servei" +size_compact = "Compacte" +size_compactSub = "Principalment text, menys d'1 MB" +size_heavy = "Pesant" +size_heavySub = "Escanejat o amb moltes imatges, 5 MB+" +size_standard = "Estàndard" +size_standardSub = "Text i imatges barrejats, d'1 a 5 MB" slDedicated = "Dedicat" slDedicatedSub = "4 hores laborables · gestor de compte dedicat · +30%" slPriority = "Prioritari" @@ -7630,6 +8509,8 @@ slPrioritySub = "Mateix dia laborable · CSM assignat · +15%" slStandard = "Estàndard" slStandardSub = "Següent dia laborable · CSM compartit · inclòs" stepOf = "Pas {{n}} de {{total}}" +taxId = "IVA / ID fiscal" +taxIdPlaceholder = "Opcional" term = "Termini" termDiscount = "S'ha aplicat un descompte del {{pct}}% per compromís multianual" title = "Crea el teu pressupost" @@ -7637,6 +8518,7 @@ training = "Incorporació i formació" trainingSub = "Sessions en directe perquè el teu equip comenci a treballar" users = "Usuaris totals" usersPlaceholder = "p. ex. 250" +viewEula = "Llegeix-ho" volEstimated = "Estimació a partir de {{count}} usuaris (~2,000 PDFs cadascun, incloent-hi automatització). Edita-ho si tens una dada millor." volManual = "S'utilitza la teva xifra. Pots tornar a estimar-ho a partir de la mida del teu equip en qualsevol moment." volNoUsers = "No n'estàs segur? Introdueix la mida del teu equip i ho estimarem." @@ -7645,55 +8527,55 @@ volumePlaceholder = "p. ex. 1,000,000" years_one = "{{count}} any" years_other = "{{count}} anys" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Fet" -here = "Ets aquí" -hide = "Amaga" -optional = "Opcional" -paidAddon = "Complement de pagament" -show = "Mostra" -subtitle = "Tot el que necessites a cada pas del recorregut, presentat a mesura que el tracte avança." -supportingSubtitle = "SOC 2, revisions de seguretat, formularis fiscals i més, a punt quan el teu equip de seguretat o contractació els demani. Alguns tenen una tarifa única." -supportingTitle = "Suport a la teva avaluació" +[portal.procurement.documents] +agreement = "Acord Enterprise" +agreementSub = "MSA, formulari de comanda i DPA en una sola signatura." +download = "Descarrega" +eula = "EULA i termes comercials" +eulaSub = "El programari i els termes comercials d'autoservei." +invoice = "Factura" +invoiceSub = "La factura de la teva primera subscripció." +laterInvoice = "Disponible després que acceptis" +laterQuote = "Disponible un cop s'emeti el teu pressupost" +quote = "Pressupost" +quoteSub = "El teu pressupost enterprise detallat." +sla = "Annex SLA" +slaSub = "Nivells de suport i objectius de resposta." +subprocessors = "Subencarregats" +subprocessorsSub = "Tercers que processen dades en nom teu." +subtitle = "La documentació del teu acord, disponible en qualsevol moment." title = "Documents" -upcoming = "Properament" +view = "Mostra" [portal.procurement.error] title = "Alguna cosa ha anat malament" [portal.procurement.hero] -company = "El teu tracte enterprise" +barAria = "Etapa {{current}} de {{total}}" ctaAgreement = "Revisa i signa l'acord" -ctaLive = "Ja estàs en producció" -ctaPayment = "Afegeix pagament" +ctaExploring = "Configura la teva prova" ctaQuote = "Revisa el teu pressupost" +ctaReviewQuote = "Revisa el pressupost" ctaTrial = "Crea el teu pressupost" +documents = "Documents" eyebrow = "Contractació enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Convida companys d'equip" -keyDocs = "Documents clau" -nextStep = "Pas següent: {{action}}" -notStarted = "No iniciat" +licenseKey = "Clau de llicència" +liveSub = "La teva organització està provisionada i la teva llicència és activa." +liveTitle = "Ja estàs en directe a Stirling Enterprise" +next = "Següent: {{stage}}" open = "Obre la contractació" scheduleCall = "Programa una trucada" -setup1Sub = "Convida els teus companys d'equip" -setup1Title = "Desplega l'editor PDF" -setup2Sub = "Activa el processament en editors i altres fonts" -setup2Title = "Connecta el processador PDF" -setup3Sub = "Activa Seguretat, Compliment, Encaminament o Retenció quan els necessitis" -setup3Title = "Afegeix polítiques recomanades" +sentenceAgreement = "revisar i signar l'acord" +sentenceLive = "provisionament i desplegament" +sentencePayment = "ordre de compra i pagament" +sentenceQuote = "revisar i acceptar el teu pressupost" +sentenceTrial = "configurar el teu espai de prova" [portal.procurement.journey] daysLeft_one = "Queda {{count}} dia" daysLeft_other = "Queden {{count}} dies" -engineerLabel = "El teu enginyer de solucions" -eyebrow = "El teu desplegament" -live = "Ja estàs en producció amb Stirling Enterprise" -nextStep = "Pas següent: {{action}}" -subtitle = "El teu enginyer de solucions és present en cada pas. Una acció següent cada vegada; la llista completa és a sota." -title = "De la prova a producció, un camí guiat" -trialTitle = "Prova Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Una signatura cobreix MSA, formulari de comanda, EULA i DPA." @@ -7727,6 +8609,9 @@ downloadError = "Encara no s'ha pogut generar el fitxer de llicència offline downloadOffline = "Baixa la llicència offline (.lic)" hint = "Enganxa aquesta clau en una instància autoallotjada per activar-la, o conserva-la per als teus registres. Guarda-la en un lloc segur." label = "La teva clau de llicència" +subtitle = "Activa una instància autoallotjada amb aquesta clau o conserva-la per als teus registres." +title = "La teva clau de llicència" +trialFileHint = "Aquest és el teu fitxer de llicència de prova. Quan l'acord estigui en vigor, torna i descarrega'l de nou; a diferència de la clau de llicència en línia, el fitxer .lic no s'actualitzarà tot sol." [portal.procurement.link] cta = "Enllaça el compte" @@ -7739,65 +8624,62 @@ description = "La teva subscripció és activa i la teva llicència s'ha emès. eyebrow = "En producció" title = "Ja estàs en producció amb Stirling Enterprise" -[portal.procurement.locked] -description = "Les claus de prova, els pressupostos per volum compromès, l'acord d'una sola signatura, el pagament i el teu registre de documents seran aquí quan iniciïs una avaluació enterprise." -eyebrow = "Només Enterprise" -talkToSales = "Parla amb vendes" -title = "El recorregut de contractació s'obre amb Enterprise" - [portal.procurement.milestone] -accept = "Accepta i continua" -description = "Baixa el PDF per compartir-lo amb el teu equip, torna per acceptar-lo quan estiguis a punt o fes-hi canvis." -download = "Baixa el PDF" downloadError = "Encara no s'ha pogut baixar el PDF del pressupost — torna-ho a provar d'aquí a un moment." -edit = "Edita el pressupost" -eyebrow = "Pressupost {{number}}" -perYear = " / any" -preparedFor = "Preparat per a {{company}}" -tcv = "{{value}} de valor total del contracte" -title = "El teu pressupost està a punt" - -[portal.procurement.modal] -cancel = "Cancel·la" -chooseFile = "Tria un fitxer" -close = "Tanca" -downloadBody = "La baixada començarà aviat." -downloadCta = "Baixa" -downloadTitle = "Baixa" -noFile = "No s'ha seleccionat cap fitxer" -payBody = "Paga el teu contracte compromès amb targeta o transferència bancària a través de Stripe. El teu espai de treball s'aprovisiona tan bon punt es liquida el pagament." -payCta = "Continua a Stripe" -payTitle = "Confirma el pagament" -requestBodyFree = "Ho generem sota demanda. Confirma-ho i el teu enginyer de solucions t'ho enviarà aviat." -requestBodyPaid = "Aquest és un complement de pagament. Confirma-ho i el teu enginyer de solucions el dimensionarà i t'enviarà la documentació." -requestCta = "Sol·licita" -requestTitle = "Sol·licita aquest document" -signBody = "Obre el Stirling Enterprise Agreement per a signatura electrònica: una signatura cobreix l'MSA, el formulari de comanda, l'EULA i el DPA. Contrasignem automàticament i avances al pagament." -signCta = "Obre per signar" -signTitle = "Revisa i signa el teu acord" -uploadBody = "Envia'ns la teva PO i emetrem la factura segons les teves condicions. Arrossega el PDF o tria un fitxer a sota." -uploadCta = "Carrega l'ordre de compra" -uploadTitle = "Carrega la teva ordre de compra" [portal.procurement.payment] description = "El teu pressupost s'ha acceptat i la teva llicència ja és activa — el teu equip pot començar de seguida. Paga la primera factura quan estiguis a punt; pots pagar-la o baixar-la aquí, sense correu electrònic." +downloadAgreement = "Descarrega l'acord signat" downloadInvoice = "Baixa la factura" -simulate = "Simula el pagament rebut (demo)" +eyebrow = "Pagament" title = "Subscripció creada" viewInvoice = "Mostra i paga la factura" +[portal.procurement.review] +acceptCta = "Accepta el pressupost" +annual = "Quota anual (any 1)" +downloadCta = "Descarrega el pressupost" +poNumber = "Ordre de compra: {{po}}" +renewal = "Es renova a {{amount}}/any després del termini (+{{pct}}% IPC)" +tcv = "Termini de {{years}} anys, pagat íntegrament per avançat: {{tcv}}" +validUntil = "Vàlid fins a {{date}}" + [portal.procurement.schedule] fallback = "No s'ha pogut carregar el planificador." fallbackLink = "Obre la programació en una pestanya nova" +loading = "S'està carregant el planificador..." subtitle = "El teu enginyer de solucions guiarà el teu equip pel desplegament. Tria una hora que et vagi bé." title = "Programa una trucada" -[portal.procurement.status] -action = "Acció necessària" -available = "Disponible" -complete = "Complet" -pending = "Pendent" -request = "A petició" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Segellat, sense sortida" +back = "Enrere" +businessName = "Nom de l'empresa" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gestionat, inici més ràpid" +continue = "Continua" +deployment = "On s'ha de desplegar?" +eula = "Accepto l'EULA i els termes comercials de Stirling." +fullName = "Nom complet" +fullNamePlaceholder = "El teu nom" +invites = "Convida companys d'equip · opcional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Programa una trucada" +seats = "Persones que ho provaran" +seatsPlaceholder = "p. ex. 15" +selfhost = "Autoallotjat" +selfhostSub = "Docker o Kubernetes" +start = "Inicia la prova" +stepOf = "Pas {{n}} de {{total}}" +subtitle = "Gratuït durant 14 dies, sense targeta." +subtitleDetails = "Alguns detalls per al teu pressupost i acord." +talkFirst = "Prefereixes parlar abans?" +title = "Configura la teva prova" +viewEula = "Llegeix-ho" +workEmail = "Correu electrònic de feina" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "L'extensió afegeix 7 dies i notifica el teu enginyer de solucions." @@ -7808,109 +8690,113 @@ maxed = "Límit assolit" subtitle = "La teva prova gratuïta dura fins al {{date}}. No cal targeta." title = "Prova Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Preus per volum compromès, SSO + SCIM + RBAC per a tota l'organització, auditoria immutable de 90 dies i un SE dedicat." -homeCta = "Inicia la prova →" -homeHeadline = "Processa milions de PDFs." - -[portal.recentActivity] -title = "Activitat recent" -viewAll = "Mostra-ho tot" - -[portal.recentActivity.empty] -description = "Les execucions de pipeline, els desplegaments i els esdeveniments d'agent apareixeran aquí." -title = "Encara no hi ha res aquí" - -[portal.search] -ariaLabel = "Cerca" -placeholder = "Cerca a Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Les accions ràpides apareixeran aquí quan estiguin disponibles." -noActionsTitle = "No hi ha accions ràpides" -noMatches = "No hi ha coincidències per a \"{{query}}\"" -noMatchesDescription = "Prova una paraula clau diferent o navega pel catàleg." - [portal.settings.groups] admin = "Administració" [portal.settings.sections] account-link = "Enllaç del compte" -[portal.shell.header] -accountFallback = "Compte" -accountMenu = "Menú del compte" -darkMode = "Mode fosc" -lightMode = "Mode clar" -search = "Cerca" -searchPlaceholder = "Cerca…" -signOut = "Tanca la sessió" -switchToDark = "Canvia al tema fosc" -switchToLight = "Canvia al tema clar" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processador" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Enllaça el compte de Stirling" -planEditor = "Pla Editor" -planEnterprise = "Pla Enterprise" -planProcessor = "Pla Processor" primaryNav = "Navegació principal" switchApp = "Canvia d'aplicació" +[portal.shell.topbar] +closeNav = "Tanca la navegació" +openNav = "Obre la navegació" +search = "Cerca" + [portal.sources] subtitle = "Connexions d'entrada reutilitzables que alimenten documents a Stirling. Configura una connexió una vegada i després referencia-la des de qualsevol nombre de polítiques. Fes clic en una fila per veure'n la configuració i quines polítiques la utilitzen." title = "Fonts" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Connecta una font" +[portal.sources.builder] +back = "Torna a les fonts" +backToTypes = "Tots els tipus de font" +cancel = "Cancel·la" +chooseHint = "Tria d'on venen els documents. Els connectors en gris estan en camí." +comingSoon = "Properament" +comingSoonHeading = "Properament" +create = "Crea una font" +createTitle = "Connecta una font" +delete = "Suprimeix" +editTitle = "Edita la font" +enabled = "Activada" +save = "Desa els canvis" + +[portal.sources.builder.folderAccess] +description = "Les automatitzacions de carpetes només poden utilitzar carpetes que un administrador hagi permès. Afegeix-la a la configuració d'accés a carpetes i torna-ho a provar." +openSettings = "Configuració d'accés a carpetes" +title = "Aquesta carpeta no està permesa" + [portal.sources.delete] body = "Vols suprimir \"{{name}}\"? Això no es pot desfer. Les polítiques que hi fan referència s'haurien d'actualitzar." cancel = "Cancel·la" confirm = "Suprimeix" title = "Vols suprimir la font?" -[portal.sources.detail] -closeAriaLabel = "Tanca el detall" -delete = "Suprimeix la font" -docs24h = "Darreres 24 h" -docs30d = "Darrers 30 dies" -docsTotal = "Total vist" -docsTrend = "Documents dels darrers 30 dies" -documents = "Documents" -edit = "Edita" -notReferenced = "No la referencia cap política, així que és segur suprimir-la." -pause = "Posa en pausa" -resume = "Reprèn" -subtitle = "{{type}} · {{status}}" -usedBy = "Utilitzada per" - -[portal.sources.empty] -description = "Connecta una carpeta (i, aviat, emmagatzematge al núvol) perquè les teves polítiques tinguin un lloc d'on obtenir documents." -title = "Encara no hi ha fonts connectades" - [portal.sources.kpi] inUse = "En ús" total = "Connexions" unused = "Sense utilitzar" +[portal.sources.networkFields.connection] +helperText = "La connexió desada amb l'adreça del servidor i les credencials. Es reutilitza per cada font que la referencia." +label = "Connexió" + +[portal.sources.networkFields.directory] +helperText = "Carpeta del servidor a consultar, relativa a l'inici de sessió o a l'arrel de la compartició. Deixa-ho en blanc per a l'arrel." +label = "Carpeta" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume elimina cada fitxer del servidor un cop totes les polítiques l'han processat." +label = "Mode de lectura" + +[portal.sources.networkFields.mode.options] +consume = "Consume: processa cada fitxer una vegada" +snapshot = "Snapshot: torna a llegir la carpeta a cada execució" + +[portal.sources.networkFields.recursive] +label = "Profunditat de carpeta" + +[portal.sources.networkFields.recursive.options] +all = "Inclou subcarpetes" +top = "Només el nivell superior" + [portal.sources.status] active = "Activa" disabled = "Desactivada" unused = "Sense utilitzar" [portal.sources.table] +documents = "Documents" source = "Font" status = "Estat" +type = "Type" usedBy = "Polítiques" +[portal.sources.types.box] +description = "Extreu documents de carpetes de Box que el teu equip ja comparteix." +label = "Box" + +[portal.sources.types.dropbox] +description = "Extreu documents d'una carpeta compartida de Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documents que el teu equip ha processat a l'editor, entre polítiques i execucions d'IA." label = "Editor" +noPolicies = "Encara no s'ha executat cap política des de l'editor." + +[portal.sources.types.email] +description = "Converteix els adjunts enviats a una bústia dedicada en documents." +label = "Safata d'entrada de correu electrònic" [portal.sources.types.folder] description = "Vigila un directori del servidor per detectar documents nous." @@ -7943,25 +8829,80 @@ label = "Profunditat de carpeta" all = "Inclou subcarpetes" top = "Només nivell superior" +[portal.sources.types.ftp] +description = "Consulta una carpeta d'un servidor FTP o FTPS per detectar documents nous." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Extreu documents de carpetes compartides de Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Supervisa una compartició de xarxa SMB per detectar documents nous." +label = "Unitat de xarxa" + +[portal.sources.types.onedrive] +description = "Supervisa carpetes de OneDrive per detectar documents nous." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Extreu documents d'un bucket Amazon S3 o compatible amb S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "La connexió desada que conté el bucket i les credencials. Es reutilitza per cada font i sortida de pipeline que la referencia." +label = "Connexió" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume elimina cada objecte del bucket un cop totes les polítiques l'han processat." +label = "Mode de lectura" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: processa cada objecte una vegada" +snapshot = "Snapshot: torna a llegir el bucket a cada execució" + +[portal.sources.types.s3.fields.prefix] +helperText = "Només es processen els objectes les claus dels quals comencen amb aquest prefix." +label = "Prefix de clau" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Consulta una carpeta de lliurament SFTP per detectar documents nous." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Extreu documents de biblioteques de documents de SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Font" +[portal.sources.types.webhook] +createNote = "Es generen una URL de lliurament i un secret de signatura quan crees aquest webhook. Els remitents fan POST de documents a aquesta URL i les polítiques que el referencien s'executen en arribar." +description = "Rep documents mitjançant HTTP POST signat des de qualsevol sistema extern." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de lliurament" +secretNote = "El secret de signatura es mostra una vegada, quan es crea el webhook. Torna a crear la font per renovar-lo." + +[portal.sources.types.webhook.reveal] +copy = "Copia" +done = "Fet" +secret = "Secret de signatura" +secretHelp = "Signa el cos en brut de cada lliurament amb aquesta clau (HMAC-SHA256) i envia'l com a capçalera X-Stirling-Signature." +secretWarning = "Copia ara el secret de signatura. Per seguretat, es mostra només una vegada i no es podrà recuperar més endavant." +title = "Webhook creat" +url = "URL de lliurament" +usage = "Fes POST de cada document com a cos de sol·licitud en brut a la URL de lliurament amb un tipus de contingut binari (application/pdf o application/octet-stream). Les polítiques que el referencien s'executen automàticament en arribar." + [portal.sources.wizard] -back = "Enrere" -cancel = "Cancel·la" -continue = "Continua" -editTitle = "Edita la font" name = "Nom" namePlaceholder = "p. ex. Entrada de reclamacions" -save = "Desa els canvis" -subtitle = "Pas {{current}} de {{total}} · {{label}}" -title = "Connecta una font" type = "Tipus" -[portal.sources.wizard.steps] -chooseType = "Tria el tipus" -configure = "Configura" -review = "Revisa i connecta" +[portal.stepModal] +close = "Tanca" [portal.usage] managePayment = "Gestiona el pagament" @@ -8021,16 +8962,9 @@ summary = "Posseeix un equip — en gestiona els recursos dels membres i les con 2 = "Accés al portal mitjançant la política predeterminada" 3 = "Tot el que pot fer un membre" -[portal.welcome] -ariaLabel = "Benvingut a Stirling PDF" -badge = "Open-source" -installEditor = "Instal·la l'editor" -inviteTeammates = "Convida companys d'equip" -openInBrowser = "Obre al navegador" -perks = "Gratuït per sempre · Autoallotjable" -subtitle = "L'editor PDF més segur del món és gratuït per a equips de totes les mides. Inclou més de 60 operacions PDF i SSO." -title = "Benvingut a" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Il·limitat" [printFile] title = "Imprimir Fitxer" @@ -8613,6 +9547,14 @@ text = "Gira les pàgines del teu PDF en sentit horari o antihorari en increment [rotate.tooltip.header] title = "Resum de la configuració de rotació" +[ruler] +clearAll = "Esborra-ho tot" +hideAllLabels = "Amaga totes les etiquetes" +hideSmallLabels = "Amaga les etiquetes petites" +physicalValues = "Físic" +scaled = "Escalat" +showAllLabels = "Mostra totes les etiquetes" + [sanitize] desc = "Elimina elements potencialment nocius dels fitxers PDF." sanitizationResults = "Resultats de la sanitització" @@ -8654,31 +9596,45 @@ label = "Elimina metadades XMP" [sanitize.steps] settings = "Opcions" -[sanitizePdf] -tags = "netejar,segur,sense riscos,eliminar amenaces" - [sanitizePDF] title = "Neteja PDF" +[sanitizePdf] +tags = "netejar,segur,sense riscos,eliminar amenaces" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Ajusta l'escala de la pàgina" +[scaleSettings] +activeScale = "Escala activa" +apply = "Aplica l'escala" +applyCalibration = "Aplica la calibració" +calculatedScale = "Escala calculada" +calibrate = "Calibra" +calibrating = "S'està calibrant" +calibrationDistanceRequired = "Introdueix una distància real superior a zero" +calibrationInvalid = "No s'ha pogut calcular l'escala a partir d'aquesta mesura" +calibrationPaperDistance = "Distància mesurada a la pàgina: {{distance}}" +calibrationTitle = "Calibra l'escala" +calibrationTooltip = "Mesura una distància coneguda per calcular l'escala automàticament" +customScale = "Escala personalitzada" +noneSet = "No s'ha definit cap escala personalitzada" +presets = "Escales predefinides" +ratio = "Relació d'escala" +ratioHelp = "Relació: 1 unitat de pàgina = X unitats reals" +ratioPlaceholder = "p. ex., 100" +ratioPositive = "La relació ha de ser superior a zero" +ratioRequired = "La relació és obligatòria" +realDistance = "Distància real" +realDistancePlaceholder = "p. ex., 5" +reset = "Restableix els valors predeterminats" +unit = "Unitat" +unitInvalid = "Unitat no vàlida: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Llindar d'angle:" -10 = "Estableix la mida de la vora afegida i eliminada per evitar vores blanques a la sortida (per defecte: 1)." -2 = "Estableix l'angle mínim absolut necessari perquè la imatge es giri (per defecte: 10)." -3 = "Tolerància:" -4 = "Determina l'interval de variació de color al voltant del color de fons estimat (per defecte: 30)." -5 = "Àrea Mínima:" -6 = "Estableix el llindar d'àrea mínima per a una foto (per defecte: 10000)." -7 = "Àrea de contorn mínima:" -8 = "Estableix el llindar mínim de l'àrea de contorn per a una foto" -9 = "Mida de Vora:" - [scannerImageSplit] submit = "Extreu escanejos d'imatge" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Què fa" whatThisDoesDesc = "Troba i extreu automàticament cada foto d'una pàgina escanejada o d'una imatge composta, sense retall manual." whenToUse = "Quan utilitzar-lo" +[ScannerImageSplit.selectText] +1 = "Llindar d'angle:" +10 = "Estableix la mida de la vora afegida i eliminada per evitar vores blanques a la sortida (per defecte: 1)." +2 = "Estableix l'angle mínim absolut necessari perquè la imatge es giri (per defecte: 10)." +3 = "Tolerància:" +4 = "Determina l'interval de variació de color al voltant del color de fons estimat (per defecte: 30)." +5 = "Àrea Mínima:" +6 = "Estableix el llindar d'àrea mínima per a una foto (per defecte: 10000)." +7 = "Àrea de contorn mínima:" +8 = "Estableix el llindar mínim de l'àrea de contorn per a una foto" +9 = "Mida de Vora:" + [search] noResults = "No s'ha trobat cap resultat" placeholder = "Escriu el terme de cerca..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "El teu servidor de Stirling-PDF és fora de línia i [session] expired = "La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho." -[sessionManagement.tooltip] -header = "Gestió de sessions de signatura" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nous participants afegits al final de l'ordre de signatura" -bullet2 = "No es poden afegir participants després que la sessió estigui finalitzada" -bullet3 = "Cada participant rep una notificació quan és el seu torn" -description = "Pots afegir més participants a una sessió activa en qualsevol moment abans de la finalització." -title = "Afegir participants" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalització completa: Tots els participants han signat" -bullet2 = "Finalització parcial: Alguns participants encara no han signat" -bullet3 = "Els participants sense signar s'exclouran del document final" -bullet4 = "Un cop finalitzada, pots carregar el PDF signat als fitxers actius" -description = "La finalització combina totes les signatures en un únic PDF signat. Aquesta acció no es pot desfer." -title = "Finalització de la sessió" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "No es poden eliminar participants que ja hagin signat" -bullet2 = "Els participants eliminats ja no rebran notificacions" -bullet3 = "L'ordre de signatura s'ajusta automàticament" -description = "Els participants es poden eliminar de les sessions abans que signin." -title = "Eliminació de participants" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Cada signatura s'aplica de manera seqüencial al PDF" -bullet2 = "Els signants posteriors poden veure les signatures anteriors" -bullet3 = "Crític per a fluxos d'aprovació i cadenes de custòdia legals" -description = "L'ordre que especifiquis en crear la sessió determina qui signa primer." -title = "Ordre de signatura" - [settings] close = "Tanca" +[settings.ai] +documents = "Documents i RAG" +general = "General" +limits = "Límits i rendiment" +models = "Models i proveïdors" +title = "IA" + [settings.configuration] advanced = "Avançat" database = "Base de dades" endpoints = "Endpoints" features = "Funcions" +folderAccess = "Accés a carpetes" mcp = "MCP Server" storageSharing = "Emmagatzematge i compartició de fitxers" systemSettings = "Configuració del sistema" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Editor PDF per defecte" defaultPdfEditorActive = "Stirling PDF és el teu editor PDF per defecte" defaultPdfEditorChecking = "Comprovant..." defaultPdfEditorInactive = "Una altra aplicació està establerta per defecte" +defaultPdfEditorRemind = "Recorda'm que l'estableixi com a predeterminada" +defaultPdfEditorRemindDescription = "Mostra un bàner quan Stirling PDF no sigui la teva aplicació PDF predeterminada." defaultPdfEditorSet = "Ja és per defecte" defaultStartupView = "Vista predeterminada en iniciar" defaultStartupViewDescription = "Tria quina pestanya està activa a la columna esquerra quan s'inicia l'aplicació" @@ -8817,9 +9763,18 @@ languageDescription = "Tria l’idioma de visualització" logout = "Tanca la sessió" setAsDefault = "Estableix per defecte" theme = "Tema" +themeAccent = "Color d'accent" +themeAccentCustom = "Color personalitzat" +themeAccentCustomHint = "Personalitzat (es recomanen els valors predeterminats)" +themeAccentDark = "Color d'accent del mode fosc" +themeAccentDefault = "Predeterminat" +themeAccentDefaultHint = "Tema predeterminat (recomanat)" +themeAccentDescription = "Els botons, els enllaços i els ressaltats el segueixen, i tenyeix subtilment l'aplicació. El mode clar i el mode fosc en tenen un de propi. El sistema fa servir el que estigui actiu." +themeAccentLight = "Color d'accent del mode clar" themeDark = "Fosc" themeDescription = "Canvia entre el mode clar i el mode fosc" themeLight = "Clar" +themeReset = "Restaura el tema predeterminat" themeSystem = "System" title = "General" user = "Usuari" @@ -8833,6 +9788,12 @@ intro = "Habilita l'autenticació d'usuaris, la gestió d'equips i funcions d'es learnMore = "Més informació a la documentació" title = "Per als administradors del sistema" +[settings.general.loginLanding] +description = "Tria quina aplicació s'obre quan inicies la sessió a Stirling Cloud." +editor = "Editor" +processor = "Processador" +title = "Després d'iniciar la sessió" + [settings.general.mode] fullscreen = "Pantalla completa" sidebar = "Barra lateral" @@ -9013,9 +9974,6 @@ title = "Polítiques i privadesa" [settings.preferences] title = "Preferències" -[settings.search] -placeholder = "Cerca a les pàgines de configuració..." - [settings.security] description = "Actualitza la contrasenya per mantenir segur el compte." title = "Seguretat" @@ -9041,6 +9999,11 @@ security = "Seguretat" telegram = "Telegram" title = "Seguretat i autenticació" +[settings.signOut] +confirm = "Segur que vols tancar la sessió?" +submit = "Tanca la sessió" +title = "Tanca la sessió" + [settings.team] title = "Equip" @@ -9156,6 +10119,7 @@ last = "Última pàgina" maintainRatio = "Activa/desactiva mantenir la relació d'aspecte" next = "Pàgina següent" noSavedSigs = "No s'han trobat signatures desades" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Signatures Personals" previous = "Pàgina anterior" redo = "Refer" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "No s'ha pogut eliminar el fons" hint = "Carrega una imatge PNG o JPG de la teva signatura" label = "Carrega la imatge de la signatura" placeholder = "Selecciona el fitxer d'imatge" +previewAlt = "Current image signature" processing = "Processant la imatge..." removeBackground = "Elimina el fons blanc (fes-lo transparent)" @@ -9207,6 +10172,17 @@ saved = "Selecciona una signatura desada a dalt i fes clic a qualsevol lloc del text = "Després d'introduir el teu nom a dalt, fes clic en qualsevol lloc del PDF per col·locar la signatura." title = "Com afegir la signatura" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Mou la signatura" pause = "Pausa la col·locació" @@ -9653,10 +10629,30 @@ title = "Pujar al servidor" updateButton = "Actualitza al servidor" uploadButton = "Puja al servidor" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "fes clic aquí" +fallback = "Si no s'ha obert automàticament," +opening = "S'està obrint Swagger UI en una pestanya nova..." tags = "api,documentació,swagger,endpoints,desenvolupament" title = "Documentació de l'API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Selecciona fitxers a Fitxers actius per executar aquesta eina singleFileScope = "S'aplica només a: {{fileName}}" viewerMode = "Canvia a l'editor de fitxers per seleccionar diversos fitxers." +[toolFormat] +ANY = "qualsevol fitxer" +CBR = "un arxiu de còmic (CBR)" +CBZ = "un arxiu de còmic (CBZ)" +CSV = "un fitxer CSV" +EBOOK = "un llibre electrònic" +EMAIL = "un correu electrònic" +EXCEL = "un full de càlcul" +HTML = "una pàgina web" +IMAGE = "imatges" +JAVASCRIPT = "un fitxer JavaScript" +JSON = "un fitxer JSON" +MARKDOWN = "un fitxer Markdown" +NONE = "cap fitxer" +PCL = "un fitxer d'impressió PCL" +PDF = "un PDF" +PDF_ENCRYPTED = "un PDF protegit amb contrasenya" +POSTSCRIPT = "un fitxer PostScript" +PPT = "una presentació" +TEXT = "un fitxer de text" +VIDEO = "un vídeo" +WORD = "un document Word" +XML = "un fitxer XML" +XPS = "un document XPS" +ZIP = "un arxiu ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Torna a totes les eines" collapse = "Redueix el panell" expand = "Amplia el panell" goBack = "Torna enrere" +pdfTools = "Eines PDF" placeholder = "Tria una eina per començar" premiumFeature = "Funció premium:" toolsHeader = "Eines" @@ -9806,11 +10829,6 @@ noToolsFound = "No s'ha trobat cap eina" quickAccess = "ACCÉS RÀPID" searchPlaceholder = "Cerca eines..." -[toolPicker.categories] -advancedTools = "Eines avançades" -recommendedTools = "Eines recomanades" -standardTools = "Eines estàndard" - [toolPicker.subcategories] advancedFormatting = "Formatació avançada" automation = "Automatització" @@ -9824,6 +10842,9 @@ removal = "Eliminació" signing = "Signatura" verification = "Verificació" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "No s’han trobat eines" noTools = "No hi ha eines disponibles" @@ -9950,28 +10971,53 @@ title = "Usuaris" you = "(tu)" [users.action] +cancelInvite = "Cancel·la la invitació" deleteTeam = "Suprimeix l'equip" disableMfa = "Restableix MFA" move = "Mou a l'equip" reinstate = "Restableix" remove = "Elimina de l'org" +removeTeam = "Elimina de l'equip" rename = "Canvia el nom de l'equip" resetPw = "Restableix la contrasenya" suspend = "Suspèn" unlock = "Desbloqueja el compte" +[users.activity] +daysAgo = "fa {{count}} d" +hoursAgo = "fa {{count}} h" +justNow = "Ara mateix" +minutesAgo = "fa {{count}} min" +monthsAgo_one = "fa {{count}} mes" +monthsAgo_other = "fa {{count}} mesos" +never = "Mai" +weeksAgo_one = "fa {{count}} setmana" +weeksAgo_other = "fa {{count}} setmanes" +yearsAgo_one = "fa {{count}} any" +yearsAgo_other = "fa {{count}} anys" + [users.cap] addProcessor = "+ Processador" approver = "Aprova la política" editor = "Editor" processor = "Processador" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Vols cancel·lar la invitació a {{email}}? No es podrà unir amb l'enllaç actual." +cancelInviteTitle = "Cancel·la la invitació" deleteTeamBody = "Vols suprimir l'equip {{name}}? Primer l'equip ha d'estar buit: mou-ne els membres a un altre equip, i no pot continuar posseint cap configuració d'integració." deleteTeamTitle = "Suprimeix l'equip" disableMfaBody = "Vols eliminar la inscripció MFA de {{name}}? La tornarà a configurar en el proper inici de sessió si és necessari." disableMfaTitle = "Restableix MFA" removeBody = "Vols eliminar permanentment {{name}} de l'organització? Això no es pot desfer." +removeTeamBody = "Vols eliminar {{name}} de l'equip? Mantindrà el compte, però perdrà l'accés als recursos d'aquest equip." removeTitle = "Elimina el membre" [users.empty] @@ -9982,10 +11028,8 @@ title = "Encara no hi ha membres" addToTeam = "Afegeix a l'equip" guestCount = "{{count}} convidat" guests = "Convidats" -guestsDesc = "Col·laboradors externs, limitats al que has compartit. Només Editor." ledBy = "dirigit per {{owner}}" org = "Organització" -orgDesc = "Propietaris amb autoritat a tota l'organització i aprovació de polítiques" owners = "{{count}} propietari" team = "Equip {{name}}" teamMeta = "{{count}} persones" @@ -10022,6 +11066,18 @@ username = "Nom d'usuari" usernameError = "El nom d'usuari ha de tenir com a mínim 3 caràcters" usernamePlaceholder = "jsmith" +[users.invites] +by = "Convidat per {{who}}" +cancel = "Cancel·la" +expiresInDays_one = "Caduca d'aquí a {{count}} dia" +expiresInDays_other = "Caduca d'aquí a {{count}} dies" +expiresToday = "Caduca avui" +title = "Invitacions pendents" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "S'ha produït un error en contactar amb el backend, o no hi tens accés. Torna-ho a provar." title = "No s'han pogut carregar els membres" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Cerca adjunts" title = "Adjunts" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Títol del marcador" +bookmarkTitleRequired = "El títol del marcador és obligatori" closeSidebar = "Tanca la barra lateral de marcadors" collapseAll = "Replega tots els marcadors" +empty = "No bookmarks in this document" expandAll = "Desplega tots els marcadors" +searchPlaceholder = "Cerca marcadors" +title = "Bookmarks" [viewer.comments] addComment = "Afegeix un comentari" @@ -10236,10 +11297,12 @@ locateAnnotation = "Localitza al document" moreActions = "Més accions" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Pàgina {{page}}" placingHint = "Feu clic a una pàgina per col·locar… (cancel·la)" removeCommentOnly = "Elimina només el comentari" saveReply = "Desa la resposta" +searchPlaceholder = "Search comments" title = "Comentaris" typeComment = "Comentari" typeInsertText = "Insereix text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Tens canvis no desats" closeSidebar = "Tanca la barra lateral de capes" hideAll = "Amaga totes les capes" showAll = "Mostra totes les capes" +title = "Layers" [viewer.link] delete = "Suprimeix l'enllaç" @@ -10605,30 +11669,6 @@ title = "Text de la marca d'aigua" image = "Imatge" text = "Text" -[wetSignature.tooltip] -header = "Mètodes de creació de signatura" - -[wetSignature.tooltip.draw] -bullet1 = "Personalitzeu el color i el gruix del traç" -bullet2 = "Esborreu i torneu a dibuixar fins que estigueu satisfets" -bullet3 = "Funciona en dispositius tàctils (tauletes, telèfons)" -description = "Creeu una signatura manuscrita amb el ratolí o la pantalla tàctil. Ideal per a signatures personals i autèntiques." -title = "Dibuixar la signatura" - -[wetSignature.tooltip.type] -bullet1 = "Trieu entre diversos tipus de lletra" -bullet2 = "Personalitzeu la mida i el color del text" -bullet3 = "Perfecte per a signatures estandarditzades" -description = "Genereu una signatura a partir de text teclejat. Ràpid i coherent, adequat per a documents empresarials." -title = "Escriure la signatura" - -[wetSignature.tooltip.upload] -bullet1 = "Admet PNG, JPG i altres formats d'imatge" -bullet2 = "Es recomana fons transparents per obtenir els millors resultats" -bullet3 = "La imatge es redimensionarà per ajustar-se a l'àrea de la signatura" -description = "Pugeu una imatge de signatura ja creada. Ideal si teniu una signatura escanejada o el logotip de l'empresa." -title = "Pujar imatge de la signatura" - [workbenchBar] activeFiles = "Fitxers actius" annotations = "Anotacions" @@ -10642,6 +11682,7 @@ exitRedaction = "Surt del mode de censura" exportAll = "Exporta el PDF" exportSelected = "Exporta les pàgines seleccionades" formFill = "Omple el formulari" +hideToolbar = "Hide toolbar" multiTool = "Eina múltiple" panMode = "Mode de desplaçament" print = "Imprimeix el PDF" @@ -10652,6 +11693,7 @@ redact = "Censura" rotateLeft = "Gira a l’esquerra" rotateRight = "Gira a la dreta" ruler = "Regle / Mesura" +rulerSettings = "Configuració de l'escala" save = "Desa" saveAll = "Desa-ho tot" saveAs = "Desa com a" @@ -10661,6 +11703,7 @@ selectAll = "Selecciona-ho tot" selectByNumber = "Selecciona per números de pàgina" selectLanguage = "Selecciona l’idioma" share = "Comparteix" +showToolbar = "Show toolbar" toggleAnnotations = "Mostra/oculta les anotacions" toggleAttachments = "Mostra/oculta els adjunts" toggleBookmarks = "Mostra/oculta els marcadors" diff --git a/frontend/editor/public/locales/cs-CZ/translation.toml b/frontend/editor/public/locales/cs-CZ/translation.toml index 7ff170eff4..ead7dcab24 100644 --- a/frontend/editor/public/locales/cs-CZ/translation.toml +++ b/frontend/editor/public/locales/cs-CZ/translation.toml @@ -1,7 +1,6 @@ alphabet = "Abeceda" apply = "Použít" applyAndContinue = "Uložit a odejít" -areYouSure = "Opravdu chcete odejít?" back = "Zpět" black = "Černá" blue = "Modrá" @@ -46,10 +45,8 @@ filesSelected = "vybraných souborů" font = "Písmo" fontSizeTooltip = "Velikost textu čísla stránky v bodech. Větší čísla znamenají větší text." fontTypeTooltip = "Rodina písma pro čísla stránek. Zvolte podle stylu dokumentu." -genericSubmit = "Odeslat" green = "Zelená" help = "Nápověda" -imgPrompt = "Vyberte obrázek(y)" incorrectPasswordMessage = "Současné heslo není správné." info = "Informace" invalidUndoData = "Nelze vrátit: neplatná data operace" @@ -62,7 +59,6 @@ loginAgreementTitle = "Přihlašovací ujednání" logOut = "Odhlásit se" marginTooltip = "Vzdálenost mezi číslem stránky a okrajem stránky." moreOptions = "Další možnosti" -multiPdfPrompt = "Vyberte PDF soubory (2+)" never = "Nikdy" no = "Ne" noFileSelected = "Není vybrán žádný soubor. Nahrajte soubor." @@ -80,7 +76,6 @@ pageOrderPrompt = "Vlastní pořadí stránek (Zadejte seznam čísel stránek o pages = "Stránky" pageSelectionPrompt = "Vlastní výběr stránek (Zadejte seznam čísel stránek oddělených čárkou jako 1,5,6 nebo funkci jako např. 2n+1):" password = "Heslo" -pdfPrompt = "Vyberte PDF soubor(y)" pin = "Připnout soubor (ponechat aktivní po spuštění nástroje)" pro = "Pro" processingComplete = "Váš soubor je připraven." @@ -110,6 +105,7 @@ undoSuccess = "Operace úspěšně vrácena" unknown = "Neznámý" unpin = "Odepnout soubor (nahradit po spuštění nástroje)" unsavedChanges = "Máte neuložené změny v PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Neuložené změny" unsupported = "Nepodporováno" username = "Uživatelské jméno" @@ -356,6 +352,7 @@ text = "Tato oprávnění určují, co mohou uživatelé s PDF dělat. Nejlépe title = "Změnit oprávnění" [addStamp] +preview = "Náhled razítka" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ruční stažení: klikněte na odkazy a umístěte soubory do sl noLanguages = "V nakonfigurovaném adresáři nebyly nalezeny žádné jazyky tessdata." permissionNotice = "Cesta k tessdata není zapisovatelná. Stažení se otevře v prohlížeči; soubory .traineddata prosím uložte ručně do složky tessdata." +[admin.settings.ai.documents] +description = "Nakonfigurujte embedding model a nastavení vyhledávání používané k odpovídání na otázky nad dokumenty. Po uložení se použije pro AI engine." +title = "Dokumenty a RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Ponechte prázdné, chcete-li použít vlastní přihlašovací údaj prostředí enginu. Platí pro self-hosted nasazení s jedním enginem." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Uloženo – ponechte prázdné pro zachování aktuálního klíče" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Základní URL endpointu embeddingů kompatibilního s OpenAI / Ollama, např. http://ollama:11434/v1. Musí odkazovat na důvěryhodný interní endpoint (citlivé na SSRF)." +label = "Základní URL embeddingů" + +[admin.settings.ai.documents.embeddingModel] +description = "Název embedding modelu. Volný text; návrhy jsou pouze nápovědou." +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "Poskytovatel používaný k převodu textu dokumentu na vektorové embeddingy." +label = "Poskytovatel embeddingů" + +[admin.settings.ai.documents.maxSearches] +description = "Maximální počet vyhledávání, která může agent spustit na jeden požadavek." +label = "Max. počet vyhledávání" + +[admin.settings.ai.documents.reindexNote] +body = "Změna embedding modelu se projeví okamžitě, ale dokumenty indexované předchozím modelem musí být znovu indexovány, aby vyhledávání vracelo správné výsledky." +title = "Vyžadována opětovná indexace" + +[admin.settings.ai.documents.saved] +reindexNote = "Pokud jste změnili embedding model, znovu indexujte existující dokumenty, aby vyhledávání používalo nový model." + +[admin.settings.ai.documents.topK] +description = "Počet nejrelevantnějších částí načtených na jedno vyhledávání." +label = "Top K" + +[admin.settings.ai.general] +description = "Připojte Stirling k Python AI enginu a zvolte, které AI schopnosti budou dostupné. Změny se projeví po restartu." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Zapněte nebo vypněte jednotlivé AI funkce. Vypnuté funkce jsou v aplikaci skryté." +title = "Schopnosti" + +[admin.settings.ai.general.enabled] +description = "Hlavní přepínač. Když je vypnutý, nejsou dostupné žádné AI nástroje, agenti ani volání enginu." +label = "Povolit AI" + +[admin.settings.ai.general.features.chat] +description = "Konverzační asistent pro práci s PDF." +label = "Chatovací asistent" + +[admin.settings.ai.general.features.classify] +description = "Automaticky kategorizovat dokumenty podle typu nebo obsahu." +label = "Klasifikace dokumentů" + +[admin.settings.ai.general.features.createPdf] +description = "Vygenerovat nové PDF (např. z HTML) pomocí AI agenta." +label = "Vytvořit PDF z promptu" + +[admin.settings.ai.general.features.documentQuestions] +description = "Pokládejte otázky a získávejte odpovědi založené na nahraném dokumentu." +label = "Otázky k dokumentu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Kontrolovat dokumenty na matematické a číselné chyby." +label = "Matematický auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "Přidat do PDF komentáře a anotace k revizi vytvořené AI." +label = "Agent pro komentáře PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Časový limit pro náročnější operace agenta, například generování dokumentů." +label = "Časový limit dlouhých operací (sekundy)" + +[admin.settings.ai.general.note] +body = "AI engine běží jako samostatná služba. Jeho sdílené tajemství" +body2 = "se nastavuje pomocí proměnné prostředí kontejneru. Provider API keys lze zadat na těchto stránkách nebo dodat jako proměnné prostředí enginu; zde zadané klíče se po uložení odešlou do enginu." +title = "O AI enginu" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Časový limit pro streamované (token po tokenu) odpovědi chatu." +label = "Časový limit streamu (sekundy)" + +[admin.settings.ai.general.test] +button = "Otestovat připojení" +failBody = "AI engine neodpověděl. Zkontrolujte URL, zda kontejner enginu běží a zda je AI povolena (po povolení je nutný restart)." +failTitle = "AI engine je nedostupný" +okBody = "AI engine odpověděl na kontrolu stavu." +okTitle = "AI engine je dostupný" + +[admin.settings.ai.general.timeoutSeconds] +description = "Časový limit pro standardní AI požadavky na engine." +label = "Časový limit požadavku (sekundy)" + +[admin.settings.ai.general.url] +description = "Interní URL Python AI enginu, např. http://stirling-pdf-engine:5001." +label = "URL AI enginu" + +[admin.settings.ai.limits] +description = "Ochranné limity určující, kolik práce mohou AI požadavky vykonat a kolik jich může běžet současně. Po uložení se použije pro AI engine." +title = "Limity a výkon" + +[admin.settings.ai.limits.maxCharacters] +description = "Ochranný limit: odmítnout AI požadavky, jejichž extrahovaný text překročí tuto délku." +label = "Max. znaků na požadavek" + +[admin.settings.ai.limits.maxPages] +description = "Ochranný limit: odmítnout AI požadavky s více než tímto počtem PDF stránek." +label = "Max. stránek na požadavek" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximální počet současných probíhajících volání modelu v celém enginu." +label = "Max. souběžnost modelu" + +[admin.settings.ai.models] +description = "Vyberte poskytovatele LLM a chytré/rychlé modely, které AI engine používá. Po uložení se použije pro AI engine." +title = "Modely a poskytovatelé" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Ponechte prázdné, chcete-li použít vlastní přihlašovací údaj prostředí enginu. Platí pro self-hosted nasazení s jedním enginem." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Uloženo – ponechte prázdné pro zachování aktuálního klíče" + +[admin.settings.ai.models.baseUrl] +description = "Základní URL endpointu kompatibilního s OpenAI / Ollama, např. http://ollama:11434/v1." +label = "Základní URL poskytovatele" +warning = "Základní URL musí odkazovat na důvěryhodný interní endpoint. Engine na něj bude odesílat požadavky ze serveru, takže nedůvěryhodná hodnota je citlivá na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximální počet výstupních tokenů pro rychlý model." +label = "Max. tokenů rychlého modelu" + +[admin.settings.ai.models.fastModel] +description = "Levnější, rychlejší model pro lehčí úlohy. Volný text; návrhy jsou pouze nápovědou." +label = "Rychlý model" + +[admin.settings.ai.models.provider] +description = "Se kterým poskytovatelem LLM engine komunikuje." +label = "Poskytovatel" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximální počet výstupních tokenů pro chytrý model." +label = "Max. tokenů chytrého modelu" + +[admin.settings.ai.models.smartModel] +description = "Vysoce schopný model pro složité uvažování. Volný text; návrhy jsou pouze nápovědou." +label = "Chytrý model" + +[admin.settings.ai.saved] +body = "Změny se automaticky odesílají do AI enginu." +bodyNoPush = "Nastavení uložena. Použijí se při příštím načtení konfigurace AI enginem." +title = "Nastavení AI uložena" + [admin.settings.badge] clickToUpgrade = "Klikněte pro zobrazení detailů tarifu" @@ -938,6 +1094,25 @@ label = "Znovu vygenerovat při startu" description = "Počet dní, po které bude certifikát platný" label = "Platnost certifikátu (dny)" +[admin.settings.folderAccess] +description = "Adresáře, ze kterých mohou zdroje složek a výstupy složek číst a do kterých mohou zapisovat. Toto je bezpečnostní hranice: automatizace nikdy nelze nasměrovat na cestu na serveru mimo tento seznam." +securityNote = "Ponechte prázdné, chcete-li zdroje složek a výstupy úplně zakázat. Vlastní konfigurační adresář Stirling je vždy nepřístupný." +title = "Přístup ke složkám" + +[admin.settings.folderAccess.implied] +description = "Tyto adresáře spravované Stirling jsou vždy povolené a zde je nelze změnit." +serverStorage = "Úložiště souborů serveru" +title = "Vždy povoleno" +watchedFolder = "Sledovaná složka pipeline" + +[admin.settings.folderAccess.roots] +add = "Přidat" +empty = "Nejsou povoleny žádné složky. Zdroje složek a výstupy jsou aktuálně zakázány." +hint = "Zadejte absolutní cesty, například /data/inbox." +label = "Povolené kořenové složky" +placeholder = "/data/inbox" +remove = "Odebrat kořenovou složku" + [admin.settings.general] description = "Nakonfigurujte celosystémová nastavení aplikace včetně brandingu a výchozího chování." system = "Systém" @@ -1704,6 +1879,9 @@ width = "Šířka" [app] description = "Bezplatná alternativa k Adobe Acrobat (10+ mil. stažení)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convert to PDF/A-3b" convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments" @@ -1842,6 +2020,15 @@ export = "Export" accessDenied = "Přístup odepřen" insufficientPermissions = "Nemáte oprávnění k provedení této akce." +[auth.callback] +completing = "Dokončování ověření" +invalidToken = "Přihlášení přes OAuth selhalo – neplatný token." +missingToken = "Přihlášení přes OAuth selhalo – nebyl přijat žádný token." +oauthFailed = "Přihlášení přes OAuth selhalo. Zkuste to prosím znovu." +pleaseWait = "Počkejte prosím, než dokončíme vaše přihlášení." +signedOut = "Byli jste odhlášeni. Přihlaste se prosím znovu." +windowMayClose = "Po dokončení můžete toto okno zavřít." + [auth.displayName] guest = "Host" user = "Uživatel" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Zabezpečí PDF dokumenty odstraněním potenciálně ško description = "Tento nástroj automaticky přejmenuje PDF soubory podle jejich obsahu. Analyzuje dokument a najde nejvhodnější název z textu." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automaticky otočit" +tags = "automatické otočení,opravit orientaci,vzhůru nohama,na bok,narovnat,orientovat,detekovat orientaci,opravit otočení,automatická orientace,skenované,OSD" + +[autoRotate.confidenceThreshold] +title = "Práh spolehlivosti" + +[autoRotate.detectionMode] +auto = "Automaticky" +osd = "Pouze OCR" +text = "Pouze text" +title = "Metoda detekce" + +[autoRotate.error] +failed = "Při automatickém otáčení PDF došlo k chybě." + +[autoRotate.inferUndetected] +title = "Otočit stránky s nízkou spolehlivostí podle ostatních" + +[autoRotate.report] +applied = "{{degrees}}° po směru hodinových ručiček" +confidenceValue = "Spolehlivost {{value}}" +noChange = "Beze změny" +pageLabel = "Stránka {{number}}" +summary = "Otočeno {{rotated}} z {{total}} stránek, {{unchanged}} ponecháno beze změny" +title = "Zpráva o detekci" + +[autoRotate.report.method] +inferred = "Sladěno" +none = "Přeskočeno" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Pod prahem" +blankPage = "Stránka je prázdná" +inferredFromDocument = "Stejně jako ostatní stránky" +noDominantDirection = "Smíšené směry textu" +osdFailed = "Nebyl nalezen čitelný text" +osdNoVerdict = "Nelze určit správnou orientaci" +tesseractUnavailable = "OCR není nainstalováno" +tooFewGlyphs = "Příliš málo textu" + +[autoRotate.results] +title = "Výsledky automatického otočení" + +[autoRotate.settings] +title = "Nastavení" + +[autoRotate.tooltip.confidenceThreshold] +text = "Jak jistá musí kontrola být, než se stránka otočí. Snižte hodnotu pro otočení více stránek, zvyšte ji pro ponechání více stránek beze změny." +title = "Práh spolehlivosti" + +[autoRotate.tooltip.description] +text = "Určí, jak má být každá stránka správně orientována, a otočí ji správným směrem. Stránky, které už jsou správně, ponechá beze změny." + +[autoRotate.tooltip.detectionMode] +text = "Automaticky nejdříve čte text na stránce a poté se podívá na stránku jako na obrázek, pokud to nestačí. Pouze text přeskočí kontrolu obrázku, což je nejrychlejší pro dokumenty, které nikdy nebyly skenovány. Pouze OCR považuje každou stránku za obrázek." +title = "Metoda detekce" + +[autoRotate.tooltip.header] +title = "Automatické otočení" + +[autoRotate.tooltip.inferUndetected] +text = "Prázdné nebo téměř prázdné stránky nelze samostatně zkontrolovat. Když všechny čitelné stránky vyžadují stejné otočení, tyto stránky se otočí podle nich." +title = "Otočit stránky s nízkou spolehlivostí podle ostatních" + [autoSizeSplitPDF] tags = "pdf,rozdělit,dokument,organizace" @@ -2189,33 +2443,6 @@ keywords = "Klíčová slova: odd, even" numbers = "Čísla/rozsahy: 5, 10-20" progressions = "Progrese: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Typy certifikátů" - -[certificateChoice.tooltip.organization] -bullet1 = "Spravováno správci systému" -bullet2 = "Sdíleno mezi oprávněnými uživateli" -bullet3 = "Reprezentuje identitu společnosti, nikoli jednotlivce" -bullet4 = "Vhodné pro: Oficiální dokumenty, týmové podpisy" -description = "Sdílený certifikát poskytovaný vaší organizací. Používá se pro firemní oprávnění k podepisování." -title = "Certifikát organizace" - -[certificateChoice.tooltip.personal] -bullet1 = "Vygenerován automaticky při prvním použití" -bullet2 = "Navázán na váš uživatelský účet" -bullet3 = "Nelze sdílet s jinými uživateli" -bullet4 = "Vhodné pro: Osobní dokumenty, individuální odpovědnost" -description = "Automaticky generovaný certifikát jedinečný pro váš uživatelský účet. Vhodný pro individuální podpisy." -title = "Osobní certifikát" - -[certificateChoice.tooltip.upload] -bullet1 = "Vyžaduje soubor P12/PFX a heslo" -bullet2 = "Může být vydán externími certifikačními autoritami" -bullet3 = "Vyšší úroveň důvěry pro právní dokumenty" -bullet4 = "Vhodné pro: Právně závazné smlouvy, externí ověření" -description = "Použijte svůj vlastní certifikát PKCS#12. Poskytuje plnou kontrolu nad vlastnostmi certifikátu." -title = "Nahrát vlastní P12" - [certSign] awaitingSignatures = "Čeká na podpisy" chooseCertificate = "Vyberte soubor s certifikátem" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Zdroj certifikátu" upload = "Nahrát" -[certSign.tooltip.header] -title = "O správě podpisů" - -[certSign.tooltip.overview] -bullet1 = "Kontrolujte existující podpisy a jejich platnost" -bullet2 = "Zobrazujte podrobné informace o podepisujících a certifikátech" -bullet3 = "Přidávejte nové digitální podpisy pro zabezpečení dokumentů" -bullet4 = "Podpora více souborů s jednoduchou navigací" -text = "Tento nástroj umožňuje zjistit, zda jsou vaše PDF digitálně podepsaná, a přidat nové digitální podpisy. Digitální podpisy prokazují, kdo dokument vytvořil nebo schválil, a ukazují, zda byl po podpisu změněn." -title = "Co tento nástroj umí?" - -[certSign.tooltip.signing] -bullet1 = "Podporuje formáty PEM, PKCS12, JKS a serverové certifikáty" -bullet2 = "Možnost zobrazit nebo skrýt podpis v PDF" -bullet3 = "Přidat důvod, místo a jméno podepisujícího" -bullet4 = "Vyberte, na kterou stránku umístit viditelné podpisy" -bullet5 = "Použijte serverový certifikát pro jednoduchou možnost 'Podepsat pomocí Stirling-PDF'" -text = "K podepsání PDF potřebujete digitální certifikát (např. PEM, PKCS12 nebo JKS). Můžete zvolit, zda bude podpis na dokumentu viditelný, nebo zůstane neviditelný pouze pro ověření." -title = "Přidávání podpisů" - -[certSign.tooltip.validation] -bullet1 = "Zobrazuje, zda jsou podpisy platné či neplatné" -bullet2 = "Zobrazuje informace o podepisujícím a datum podpisu" -bullet3 = "Kontroluje, zda byl dokument po podpisu upraven" -bullet4 = "Může použít vlastní certifikáty k ověření" -text = "Při kontrole podpisů nástroj sdělí, zda jsou platné, kdo dokument podepsal, kdy byl podepsán a zda byl po podpisu změněn." -title = "Kontrola podpisů" - [changeCreds] changeUsername = "Update your username. You will be logged out after updating." credsUpdated = "Account updated" @@ -2737,12 +2936,16 @@ text = "Abyste tato oprávnění učinili nezměnitelnými, použijte nástroj P [chat.actions] copy = "Kopírovat zprávu" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Možnosti agenta Stirling" +assistant = "AI Assistant" clearChat = "Vymazat chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Tato operace využije vaše cloudové kredity" tooltip = "Vybrat barvu z obrazovky" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Zvolte barvu" [common] +actions = "Actions" back = "Zpět" cancel = "Zrušit" close = "Zavřít" +codeSample = "Code sample" collapse = "Sbalit" confirm = "Potvrdit" continue = "Pokračovat" copied = "Zkopírováno!" copy = "Kopírovat" done = "Hotovo" +enterValue = "Enter value" error = "Chyba" expand = "Rozbalit" loading = "Načítání..." next = "Další" +open = "Open" preview = "Náhled" previous = "Předchozí" refresh = "Obnovit" +remaining = "Zbývá" retry = "Zkusit znovu" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Vymazat vybrané" @@ -2988,6 +3199,7 @@ title = "Metoda komprese" [compress.settings] desiredSize = "Požadovaná velikost souboru" desiredSizePlaceholder = "Zadejte velikost" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Komprese je snadný způsob, jak zmenšit velikost souboru. Zvolte Velikost souboru pro zadání cílové velikosti a my upravíme kvalitu za vás. Zvolte Kvalitu pro ruční nastavení síly komprese." @@ -3190,6 +3402,24 @@ pdfOptions = "Možnosti PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Nejprve vyberte zdrojový formát" settings = "Nastavení" single = "Jeden" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Pokročilé monitorování" -allInOneWorkspace = "Vše v jednom: pracovní prostor pro PDF (prohlížeč, nástroje a agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Vyhrazená podpora a SLA" -fasterThroughput = "10× rychlejší propustnost" -fullyPrivateFiles = "Plně soukromé soubory" -largeFileProcessing = "Zpracování velkých souborů" -orgWideAccess = "Řízení přístupu napříč organizací" -premiumAiModels = "Prémiové AI modely" -prioritySupport = "Prioritní podpora" -privateDocCloud = "Soukromé úložiště dokumentů" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Zabezpečený přístup k API" -standardThroughput = "Standardní propustnost" -unlimitedApiAccess = "Neomezený přístup k API" -unlimitedSeats = "Neomezený počet uživatelů" - [crop] autoCrop = "Auto-crop whitespace" noFileSelected = "Vyberte soubor PDF pro zahájení ořezu" @@ -3346,6 +3558,7 @@ message = "Nepodařilo se nastavit výchozí aplikaci pro PDF" title = "Chyba" [defaultApp.prompt] +dontRemind = "Už mi to nepřipomínat" message = "Nastavte Stirling PDF jako výchozí aplikaci pro otevírání souborů PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Hledat..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Upravit" @@ -3644,11 +3861,13 @@ shareSelected = "Sdílet vybrané" sharing = "Sdílení" showAll = "Zobrazit vše" showHistory = "Zobrazit historii" +sortBy = "Sort files" sortByDate = "Seřadit podle data" sortByName = "Seřadit podle názvu" sortBySize = "Seřadit podle velikosti" storage = "Úložiště" storageState = "Úložiště" +storageUsed = "Storage used" synced = "Synchronizováno" title = "Nahrát soubory PDF" toolChain = "Použité nástroje" @@ -3694,13 +3913,17 @@ addFiles = "Přidat soubory" addingFiles = "Přidávání souborů…" collapse = "Sbalit postranní panel" customizeGroups = "Přizpůsobit skupiny" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Otevřete soubory a začněte" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Rozbalit postranní panel" -files = "Soubory" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive není nakonfigurován" leaveMyFiles = "Opustit Moje soubory" +library = "Knihovna PDF" myFiles = "Moje soubory" noFiles = "Zatím žádné soubory" openFileManager = "Otevřít správce souborů" @@ -3708,37 +3931,36 @@ openFromComputer = "Otevřít z počítače" openSettings = "Otevřít nastavení" other = "Ostatní" recent = "Nedávné" -search = "Hledat" -searchPlaceholder = "Hledat soubory..." viewAll = "Zobrazit všech {{count}} souborů" [fileSidebar.fileItem] closeViewer = "Zavřít prohlížeč" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Otevřít v prohlížeči" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Přidat" -addLabel = "Přidat štítek…" -categoryIconAria = "Vyberte ikonu pro {{name}}" -createCategory = "Vytvořit kategorii" -delete = "Smazat kategorii" done = "Hotovo" -hide = "Skrýt kategorii" -newCategory = "Název nové kategorie…" -removeLabel = "Odebrat {{name}}" -rename = "Přejmenovat" reset = "Obnovit výchozí nastavení" -search = "Hledat štítky…" -show = "Zobrazit kategorii" subtitle = "Seskupte soubory do nadřazených kategorií. Přidejte do kategorie existující nebo nové štítky, přejmenujte ji nebo si vytvořte vlastní. Soubory, které nejsou v žádné z vašich kategorií, se zobrazí pod „Ostatní“." title = "Kategorie postranního panelu" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Přidat do pracovního prostoru" addToWorkspaceCount = "Přidat {{count}} do pracovního prostoru" @@ -3749,7 +3971,6 @@ backToMyFiles = "Zpět na Moje soubory" breadcrumbs = "Cesta ke složce" cancel = "Zrušit" classification = "Klasifikace" -clearSearch = "Vymazat hledání" clearSelection = "Zrušit výběr" closeDetails = "Zavřít podrobnosti" create = "Vytvořit" @@ -3781,6 +4002,7 @@ downloadAll = "Stáhnout vše" downloadVersion = "Stáhnout tuto verzi" dropOverlay = "Přetáhněte soubory pro nahrání" dropOverlaySub = "Soubory začínají v Místní. K jejich uspořádání do složky použijte 'Přesunout do' nebo 'Uložit do cloudu'." +duplicate = "Duplicate" file = "Soubor" fileInfo = "File info" fileMenu = "Akce souboru" @@ -3808,13 +4030,12 @@ refresh = "Obnovit ze serveru" remove = "Smazat" removeVersion = "Odebrat tuto verzi" rename = "Přejmenovat" +renamed = "Přejmenováno" renameFolder = "Přejmenovat složku" resizeFolderTree = "Změnit velikost stromu složek (šipky, Shift pro větší kroky; dvojklik pro automatické přizpůsobení)" save = "Uložit" saveToServer = "Uložit na server" saveToServerDisabledHint = "Ukládání na server není na tomto serveru povoleno. Požádejte správce o povolení." -search = "Hledat" -searchPlaceholder = "Hledat v této složce a podsložkách" selectAll = "Vybrat vše" selectAllHint = "Kliknutím vyberete vše. Tip: podržte Ctrl (nebo Cmd) pro přidání jednotlivých souborů, Shift pro výběr rozsahu." selectedCount = "Vybráno: {{count}}" @@ -3874,6 +4095,10 @@ title = "Zatím žádné cloudové soubory" hint = "Soubory uložené bez nahrání zůstávají zde. Přetáhněte soubor pro přidání." title = "Žádné pouze místní soubory" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Soubory, které otevřete nebo upravíte, se zobrazí zde." title = "Zatím nic nebylo upraveno" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nepodařilo se smazat složku." deleteFolderFailedDetail = "Nepodařilo se smazat složku: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nepodařilo se upravit vzhled složky." folderAppearanceFailedDetail = "Nepodařilo se upravit vzhled složky: {{message}}" moveFilesFailed = "Nepodařilo se přesunout soubory." @@ -3910,9 +4137,7 @@ count = "Soubory" folder = "Složka" labels = "Štítky" modified = "Změněno" -name = "Název" size = "Velikost" -toolHistoryAtVersion = "Kumulativní řetězec nástrojů" totalSize = "Celková velikost" type = "Typ" versionHistory = "Vývoj verzí" @@ -3945,6 +4170,11 @@ localHint = "Uloženo pouze v tomto prohlížeči" shared = "Sdílené" sharedHint = "Sdíleno s vámi přes odkaz" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Od nejstarších" modifiedDesc = "Od nejnovějších" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "V úložišti nejsou k dispozici žádné soubory. Nejpr open = "Otevřít" openFile = "Otevřít soubor" openFiles = "Otevřít soubory" +selectFile = "Select {{name}}" selectFromStorage = "Vybrat z úložiště" upload = "Nahrát" uploadFile = "Nahrát soubor" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Vše uloženo" +analyzingFields = "Analysing form fields..." +extractCsvError = "Nepodařilo se extrahovat CSV" +extractXlsxError = "Nepodařilo se extrahovat XLSX" +filled = "filled" flattenAfterFilling = "Zploštit po vyplnění" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "pov." +requiredFieldsError = "Vyplňte prosím všechna povinná pole" rescanFields = "Znovu detekovat pole" rescanFormFields = "Znovu detekovat pole formuláře" save = "Uložit" +saveShortcut = "Ctrl+S pro uložení" +unsavedChanges = "Neuložené změny" [formFill.sidebar] close = "Zavřít postranní panel" @@ -4309,6 +4552,11 @@ desc = "Automaticky přejmenuje soubor PDF podle detekovaného záhlaví" tags = "automatická detekce,na základě záhlaví,uspořádat,přejmenovat" title = "Automaticky přejmenovat soubor PDF" +[home.autoRotate] +desc = "Detekujte orientaci každé stránky a automaticky ji otočte do správné polohy." +tags = "automatické otočení,opravit orientaci,vzhůru nohama,na bok,narovnat,orientovat,detekovat orientaci,opravit otočení,automatická orientace,skenované,OSD" +title = "Automatické otočení" + [home.autoSizeSplitPDF] tags = "auto,rozdělit,velikost" title = "Automaticky rozdělit podle velikosti/počtu" @@ -4608,15 +4856,12 @@ hint = "Nahrajte PNG, JPG, SVG nebo jiný obrazový soubor pro vložení do PDF. label = "Nahrát obrázek" placeholder = "Vyberte soubor obrázku" -[imageToPdf] -tags = "převod,img,jpg,obrázek,fotka" - [imageToPDF] header = "Obrázek na PDF" title = "Obrázek na PDF" -[infoBanner] -dismiss = "Zavřít" +[imageToPdf] +tags = "převod,img,jpg,obrázek,fotka" [invite] acceptError = "Nepodařilo se vytvořit účet" @@ -4647,11 +4892,9 @@ welcomeTitle = "Byli jste pozváni!" [landing] addFiles = "Přidat soubory" -heroSubtitle = "Pro začátek přetáhněte nebo přidejte existující PDF." mobileUpload = "Nahrát z mobilu" openFromComputer = "Otevřít z počítače" uploadFromComputer = "Nahrát z počítače" -workbenchEmptyStateHero = "Přetáhněte PDF kamkoli" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Zadejte uživatelské jméno" failedToSignIn = "Nepodařilo se přihlásit pomocí {{provider}}: {{message}}" forgotPassword = "Zapomněli jste heslo?" loggingIn = "Přihlašování..." -logIn = "Přihlásit se" login = "Přihlášení" magicLinkSent = "Magický odkaz byl odeslán na {{email}}! Zkontrolujte e-mail a kliknutím na odkaz se přihlaste." mfaCode = "Ověřovací kód" @@ -4715,26 +4957,10 @@ signInWith = "Přihlásit se pomocí" title = "Přihlášení" unexpectedError = "Neočekávaná chyba: {{message}}" updatePassword = "Aktualizovat heslo" -useEmailInstead = "Přihlásit pomocí e-mailu" useMagicLink = "Použít místo toho magický odkaz" username = "Uživatelské jméno" youAreLoggedIn = "Jste přihlášeni!" -[login.slides.edit] -alt = "Úprava PDF" -subtitle = "Více než desítka nástrojů pro začerňování, podepisování, čtení a manipulaci s PDF — najdete, co potřebujete." -title = "Upravujte PDF pro zobrazení/ochranu požadovaných informací" - -[login.slides.overview] -alt = "Přehled Stirling PDF" -subtitle = "Cloudová sada s důrazem na soukromí pro práci s PDF, která umožňuje konvertovat, podepisovat, začerňovat a spravovat dokumenty, a nabízí přes 50 dalších užitečných nástrojů." -title = "Vše pro práci s PDF na jednom místě." - -[login.slides.secure] -alt = "Zabezpečení PDF" -subtitle = "Snadno přidávejte hesla, začerňujte obsah a spravujte certifikáty." -title = "Chraňte citlivé informace ve svých PDF" - [margin] large = "Velké" medium = "Střední" @@ -4818,6 +5044,34 @@ uploadSuccess = "Nahrávání úspěšné!" uploadSuccessMessage = "Vaše obrázky byly přeneseny." validating = "Ověřuje se relace..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Naskenujte pro nahrání fotografií. Obrázky se automaticky převedou do PDF." descriptionNoConvert = "Naskenujte pro nahrání fotografií z mobilního zařízení." @@ -4836,8 +5090,13 @@ title = "Nahrát z mobilu" tags = "Multi nástroj,Více operací,UI,kliknutí a přetažení,přední strana,klientská strana,interaktivní,pohyb,smazat,přesunout,rozdělit" title = "PDF Multi nástroj" -[navbar] -search = "Hledat" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Ověření nebylo úspěšné. Toto okno můžete zavřít a zkusit to znovu." @@ -4966,6 +5225,7 @@ activeFiles = "Zobrazení Aktivní soubory ukazuje všechna PDF allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zavřít" cropSettings = "Teď, když jsme vybrali soubor, který chceme oříznout, můžeme nakonfigurovat nástroj Oříznout a zvolit oblast, na kterou PDF ořízneme." +dialogLabel = "Onboarding" fileCheckbox = "Kliknutím na soubor ho vyberete ke zpracování. Pro dávkové operace můžete vybrat více souborů." fileReplacement = "Upravený soubor automaticky nahradí původní soubor na Pracovní ploše, takže jej můžete snadno poslat do dalších nástrojů." filesButton = "Tlačítko Soubory na panelu Rychlý přístup umožňuje nahrát PDF, na kterých chcete nástroje použít." @@ -4976,6 +5236,7 @@ pinButton = "Můžete použít tlačítko Připnout, pokud chce results = "Po dokončení nástroje zobrazí krok Kontrola v tomto panelu náhled výsledků a umožní vám operaci vrátit nebo soubor stáhnout." runButton = "Jakmile je nástroj nakonfigurován, tímto tlačítkem jej spustíte na všech vybraných PDF." selectCropTool = "Vyberme nástroj Oříznout, abychom ukázali, jak nástroje používat." +stepOf = "Krok {{current}} z {{total}}" toolInterface = "Toto je rozhraní nástroje Oříznout. Jak vidíte, zatím tu toho moc není, protože jsme ještě nepřidali žádné PDF k úpravám." workbench = "Toto je Pracovní plocha – hlavní oblast, kde prohlížíte a upravujete svá PDF." wrapUp = "Máte hotovo! Seznámili jste se s hlavními oblastmi aplikace a jejich použitím. Kdykoli klikněte na tlačítko Nápověda a prohlídku si zobrazíte znovu." @@ -4988,6 +5249,26 @@ showMeAround = "Proveďte mě" skipForNow = "Zatím přeskočit" skipTheTour = "Přeskočit průvodce" +[onboarding.checklist] +dismiss = "Zavřít" +title = "Nastavte Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Spusťte Stirling nativně na svém počítači" +title = "Stáhnout Stirling pro Desktop" + +[onboarding.checklist.inviteTeam] +description = "Spolupracujte se svým týmem" +title = "Pozvat členy týmu" + +[onboarding.checklist.shareAnalytics] +description = "Pomozte zlepšit Stirling" +title = "Sdílet anonymní údaje o používání" + +[onboarding.checklist.takeTour] +description = "Podívejte se v krátké prohlídce, jak Stirling funguje" +title = "Spustit prohlídku" + [onboarding.desktopInstall] body = "Stirling nejlépe funguje jako desktopová aplikace. Můžete ji používat offline, rychleji přistupovat k dokumentům a provádět úpravy lokálně ve svém počítači." selectOs = "Vyberte operační systém" @@ -4996,7 +5277,21 @@ titleWithOs = "Stáhnout pro {{osLabel}}" [onboarding.mfa] authenticationCode = "Ověřovací kód" +description = "Zabezpečte svůj účet propojením s autentizační aplikací. Naskenujte QR kód nebo zadejte klíč nastavení a poté dokončete potvrzením 6místného kódu." +enable = "Povolit MFA" +enableError = "Nelze povolit dvoufaktorové ověření. Zkontrolujte kód a zkuste to znovu." +enterCode = "Zadejte 6místný kód ze své aplikace." +enterCodeError = "Zadejte ověřovací kód pro pokračování." +logout = "Odhlásit se" +openAuthenticator = "Otevřete Google Authenticator, Authy nebo 1Password." qrCodeLoading = "Generování vašeho QR kódu…" +regenerateQrCode = "Znovu vygenerovat QR kód" +scanQrCode = "Naskenujte QR kód nebo zadejte níže uvedený klíč nastavení." +setupError = "Nelze spustit nastavení dvoufaktorového ověření. Zkuste to prosím znovu." +setupKey = "Klíč nastavení (ruční zadání)" +stepByStep = "Krok za krokem" +success = "MFA bylo povoleno. Nyní můžete pokračovat." +title = "Nastavení vícefaktorového ověření" [onboarding.planOverview] adminBodyLoginDisabled = "Po povolení režimu přihlášení můžete spravovat uživatele, konfigurovat nastavení a sledovat stav serveru. Prvních {{freeTierLimit}} uživatelů na vašem serveru může Stirling používat zdarma." @@ -5005,6 +5300,11 @@ adminTitle = "Přehled pro správce" userBody = "Pozvěte kolegy, přiřazujte role a udržujte dokumenty uspořádané v jednom zabezpečeném prostoru. Režim přihlášení zapněte, jakmile budete chtít růst nad rámec sólo použití." userTitle = "Přehled plánu" +[onboarding.processorIntro] +body = "Stirling nyní spouští Zásady — automatizovaná pravidla, která klasifikují, zabezpečují a zpracovávají každý dokument při jeho příchodu. Nastavte je a sledujte běhy v Processoru." +cta = "Podívat se na Processor" +title = "Podívejte se na Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Předplacený rok" + +[payg.prepaid.banner] +exhausted = "Vaše předplacená kapacita je vyčerpána; účtování podle spotřeby bylo obnoveno. Doplňte kapacitu, abyste si zachovali slevu." +expiring_one = "Váš předplacený rok končí za {{count}} den. Nevyužitá kapacita vyprší — doplňte ji a začněte nový rok." +expiring_other = "Váš předplacený rok končí za {{count}} dní. Nevyužitá kapacita vyprší — doplňte ji a začněte nový rok." +low = "Zbývá vám {{pct}} % předplacené kapacity. Doplňte ji kdykoli, aby zpracování pokračovalo bez přerušení." + +[payg.prepaid.card] +subtitle = "Používá se před účtováním podle spotřeby a mimo váš limit výdajů." +title = "Předplacená kapacita" + +[payg.prepaid.meter] +capSuffix = "z {{total}} předplacených kreditů" +expires = "Vyprší {{date}}" + +[payg.prepaid.state] +exhausted = "Vyčerpáno" +healthy = "Zbývá dost" +low = "Dochází" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF na PDF/A" tags = "archiv,dlouhodobý,standard,převod,úložiště,uchování" title = "PDF na PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tisk,standard,konverze,produkce,předtisk,archiv" title = "PDF na PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kreditů" xsmall = "100 kreditů" [plan.availablePlans] +currency = "Billing currency" subtitle = "Vyberte plán, který vám vyhovuje" title = "Dostupné plány" @@ -5937,49 +6267,12 @@ keyDescription = "Paste the license key from your email" success = "License Activated!" successMessage = "Your license has been successfully activated. You can now close this window." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "vynuceno" -enforcing = "Probíhá vynucování..." -failed = "Vynucení selhalo" outputsUnavailable = "Výstupy zásad již nejsou k dispozici ke stažení." partialOutputsUnavailable = "Některé výstupy zásad již nejsou k dispozici ke stažení." -retrying = "Zaneprázdněno, opakuji..." runNotFound = "Běh vynucení již nelze najít." -step = "krok {{current}}/{{total}}" timedOut = "Časový limit vynucení vypršel dříve, než se běh mohl dokončit." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zavřít" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Probíhá použití {{names}}" applyingProgress = "Probíhá použití {{names}} ({{done}} z {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} z {{total}} souborů se nepodařilo zpracovat a byly failureTitle = "Exportováno bez úplného vynucení" printPolicyAppliedBody = "Tento soubor PDF byl upraven tak, aby vyhověl zásadě. Zkontrolujte změny a poté znovu vytiskněte." printPolicyAppliedTitle = "Zásada použita před tiskem" -queued = "+{{count}} ve frontě" successTitle = "{{names}} použito" summaryMore = "{{first}}, {{second}} a dalších {{more}}" summaryTwo = "{{first}} a {{second}}" -[policies.enforcement.triggerVerb] -convert = "Vynucování před převodem" -default = "Vynucování" -export = "Vynucování před exportem" -input = "Vynucování při importu" -print = "Vynucování před tiskem" - -[policies.field] -accessLog = "Protokol přístupů" -archiveAfter = "Archivovat po" -auditTrail = "Auditní stopa" -belowThreshold = "Pod prahem" -destination = "Cíl" -frameworks = "Frameworks" -immutableHold = "Neměnné zadržení" -keepFor = "Uchovat po dobu" -minConfidence = "Min. spolehlivost" -notify = "Upozornit při směrování" -onViolation = "Při nesouladu" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Přidat" -addPlaceholder = "Přidat štítek…" -customNote = "Přizpůsobeno pro váš tým." -defaultNote = "Používá se vestavěné výchozí nastavení sdílené s vaším týmem." -duplicate = "„{{name}}“ již existuje." -edit = "Upravit štítky" -empty = "Zatím žádné štítky." -export = "Exportovat JSON" -iconAria = "Vyberte ikonu pro {{name}}" -import = "Importovat JSON" -importError = "Tento soubor se nepodařilo importovat." -managedNote = "Týmové štítky spravuje vedoucí vašeho týmu." -modalSubtitle = "Sdíleno s celým vaším týmem. Klasifikátor vybere štítky, které odpovídají každému dokumentu." -modalTitle = "Klasifikační štítky" -pickIcon = "Použít ikonu {{label}}" -removeAria = "Odebrat {{name}}" -resetToDefault = "Obnovit výchozí" -saveForTeam = "Uložit pro tým" -saving = "Ukládání…" -sectionLabel = "Klasifikační štítky" -startFromScratch = "Začít od začátku" -teamCount = "týmové štítky" -tooLong = "Štítky mohou mít nejvýše {{max}} znaků." -ungrouped = "Neseskupené" -view = "Zobrazit štítky" +categoryCount = "kategorie" +hideCategory = "Skrýt kategorii" +labelCount = "štítky" +sharedNote = "Tyto štítky jsou vestavěné a sdílené napříč celým vaším týmem." +showCategory = "Zobrazit kategorii" [policies.pii] account = "Čísla účtů (označená)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Směrovací čísla v USA (ABA)" ssn = "Čísla sociálního zabezpečení" -[policies.settings] -noneExport = "Při exportu aktuálně neběží žádné zásady." -noneUpload = "Při nahrávání aktuálně neběží žádné zásady." -onExport = "Při exportu" -onUpload = "Při nahrávání" -reorderHandle = "Přetažením změňte pořadí" -runOrderDesc = "Když se při stejné spouštěcí události spustí více než jedna zásada, běží v tomto pořadí — každá na výstupu předchozí zásady. Přetažením změňte pořadí." -title = "Nastavení zásad" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Možnosti zásad" -policySettings = "Nastavení zásad" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Opakovat neúspěšné zásady ({{count}})" -rowProgress = "Zpracováno {{completed}} z {{total}} souborů" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Co je zásada?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automaticky najde a začerní citlivé údaje — například čísla sociálního zabezpečení a karet — aby je v dokumentu nešlo přečíst." -sanitize = "Odstraní skrytý JavaScript ze souboru, aby se při otevření někým nic nespouštělo automaticky." -watermark = "Označí viditelnou značkou (např. \"Důvěrné\") přes každou stránku." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zavřít" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} se vynucuje..." badgeRan = "Na tomto souboru proběhla zásada {{name}}" +badgeRunning = "{{name}} běží..." blockingAction = "{{action}} je blokováno během vynucování zásady, počkejte prosím..." dismiss = "Zavřít překryvnou vrstvu" enforcingTitle = "Vynucování zásady..." @@ -6151,6 +6316,7 @@ viewAnyway = "Zobrazit soubor (zásada se stále vynucuje)" [policyOption] autoRedactPhi = "Automaticky redigovat PHI" blockExport = "Blokovat export" +ccpa = "CCPA" contracts = "Smlouvy" documents = "Dokumenty" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plán Editor" subscribed = "Plán Processor" unlinked = "Nepropojeno" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap z dokumentu" -sectionsAriaLabel = "Sekce nástroje Agent Builder" -selectorAriaLabel = "Agenti" -subtitle = "Navrhujte, testujte a nasazujte AI agenty, kteří klasifikují, extrahují z dokumentů a směrují je. Definujte scénáře, omezte přístup k nástrojům, spusťte golden set a publikujte verzi." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Zrušit" -dropzoneText = "Vyberte ukázkový dokument (PDF nebo obrázek)" -lead = "Přetáhněte ukázkový dokument a navrhneme scénáře a extrakční schéma, které můžete doladit. Nic nebude publikováno, dokud to nezkontrolujete." -submit = "Bootstrap agenta" -subtitle = "Vytvořit základ nového agenta z jednoho reprezentativního souboru" -title = "Bootstrap z dokumentu" - -[portal.agentBuilder.empty] -description = "Vytvořte bootstrap agenta z ukázkového dokumentu, abyste založili jeho scénáře a extrakční schéma, poté jej dolaďte a publikujte." -title = "Zatím žádní agenti" - -[portal.agentBuilder.evals] -casesPassing = "Úspěšné případy" -columnCase = "Eval případ" -columnLatency = "Latence" -columnResult = "Výsledek" -fail = "neúspěch" -goldenSetPassRate = "Míra úspěšnosti golden-set" -latencyMs = "{{ms}} ms" -notRun = "nespuštěno" -pass = "úspěch" -passRate = "Míra úspěšnosti" -runEvals = "Spustit evals" - -[portal.agentBuilder.evals.empty] -description = "Evals promění vaše scénáře v opakovatelný golden set. Upgradujte, abyste mohli sledovat míru úspěšnosti v čase a podmiňovat jí publikování." -title = "Zatím žádný golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "napříč golden sety" -activeAgents = "Aktivní agenti" -avgPassRate = "Průměrná míra úspěšnosti eval" -fleetWide = "v rámci celé flotily" -latestPublished = "Naposledy publikováno" -scenarios = "Scénáře" -testCases = "testovací případy" -totalDescription_one = "{{count}} celkem" -totalDescription_other = "{{count}} celkem" - -[portal.agentBuilder.scenarios] -add = "Přidat" -addScenario = "Přidat scénář" -enable = "Povolit" -expectationLabel = "Očekávané chování" -expectationPlaceholder = "Co má agent udělat" -inEval = "v eval" -mute = "Ztlumit" -muted = "ztlumeno" -nameLabel = "Název" -namePlaceholder = "např. eskalace souladu" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scénáře" -tools = "Nástroje" -versions = "Verze" - -[portal.agentBuilder.tools] -broadAccess = "Široký přístup" -deniedHint = "Vybrané nástroje jsou blokovány. Vše ostatní zůstává volatelné." -deniedTools = "Zakázané nástroje" -governanceGate = "Správa nástrojů je dostupná v plánu Enterprise." -restricted = "Omezeno" -restrictedAccess = "Omezený přístup k nástrojům" -restrictedDescription = "Povolit všechny nástroje kromě těch, které níže zakážete." - -[portal.agentBuilder.versions] -current = "aktuální" -historyGate = "Úplná historie verzí a rollback jsou dostupné v plánu Enterprise." -publish = "Publikovat" -rollBack = "Vrátit zpět" - [portal.assistant] close = "Zavřít asistenta" error = "Asistenta se nepodařilo kontaktovat." @@ -6365,9 +6451,21 @@ typing = "Píše" loading = "Načítání" redirectingToEditor = "Přesměrování do editoru..." +[portal.billing.activation] +maybeLater = "Možná později" +title = "Zapnout Processor" + +[portal.billing.activation.payg] +sub = "Nastavte měsíční limit. Plaťte za to, co spustíte." +title = "Platba podle využití" + +[portal.billing.activation.prepay] +badge = "2 měsíce zdarma" +sub = "Jedna faktura, karta nebo bankovní převod." +title = "Předplatit rok" + [portal.billing.checkout] noClientSecret = "Edge funkce nevrátila client_secret." -subtitle = "Přidejte kartu, abyste mohli pokračovat po vyčerpání bezplatného grantu plánu Editor. O zbytek se postará Stripe." title = "Zapnout plán Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Platba proběhla úspěšně, ale aktivace trvá o něco déle než obvy close = "Zavřít" title = "Už jsme skoro hotovi" +[portal.billing.checkout.cap] +amountAria = "Měsíční limit výdajů" +back = "Zpět" +continue = "Pokračovat k platbě" +error = "Nepodařilo se nastavit váš limit výdajů" +finePrint = "Faktury se vystavují měsíčně. Zrušit můžete kdykoli." +noLimit = "Bez limitu" +note = "Nikdy vám nebude účtováno více než váš limit. Zpracování se pouze pozastaví." +perMonth = "/ měs." +title = "Nastavte svůj limit výdajů" + +[portal.billing.checkout.card] +fields = "Číslo karty · MM / RR · CVC · ZIP" +label = "Údaje o kartě" +note = "Údaje o kartě shromažďuje Stripe. Stirling nikdy neukládá PAN ani CVC." + [portal.billing.checkout.error] title = "Nepodařilo se spustit checkout" @@ -6383,10 +6497,9 @@ body = "Vaše platba proběhla. Zapínáme měřené zpracování ve vašich pro hint = "Nechte prosím toto okno otevřené." title = "Aktivace plánu Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "v env portálu pro povolení checkoutu v aplikaci." -bodyBefore = "Nastavte" -title = "Stripe není nakonfigurován" +[portal.billing.checkout.pay] +limit = "Měsíční limit výdajů {{amount}}/měs. Zpracování se na limitu pozastaví." +limitNoCap = "Bez limitu výdajů. Zpracování se nepozastaví. Faktury se vystavují měsíčně." [portal.billing.enterpriseUpsell] cta = "Vytvořit nabídku Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Aktuální plán" freeForever = "Navždy zdarma" noTeamResolved = "Ve vaší peněžence ještě není vyřešen žádný tým — obnovte stránku a zkuste to znovu." ownerOnly = "Plán Processor může zapnout pouze vlastník týmu." +payInvoice = "Zaplatit fakturu pro dokončení" planName = "Editor" ssoIncluded = "SSO zahrnuto" switchOnProcessor = "Zapnout Processor →" unlimitedUsers = "Neomezený počet uživatelů" +viewQuote = "Zobrazit nabídku" [portal.billing.invoices] columnAmount = "Částka" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Přímé API požadavky" segmentApiLabel = "API" segmentAutomationDesc = "Automatizace a pipelines" segmentAutomationLabel = "Automatizace" +sizeMultiplier = "{{formatted}} PDF použilo násobitel velikosti" +summary = "{{unique}} jedinečných · {{units}} účtovacích jednotek · průměr {{avg}} na PDF" +summaryNoRate = "{{unique}} jedinečných · {{units}} účtovacích jednotek" unit = "měřené PDFs" +unitsPending_one = "{{units}} účtovací jednotka čeká na synchronizaci z propojených instancí" +unitsPending_other = "{{units}} účtovacích jednotek čeká na synchronizaci z propojených instancí" + +[portal.billing.prepaid] +capSuffix = "z {{total}} předplacených kreditů" +expires = "Vyprší {{date}}" +eyebrow = "Předplacená kapacita" +note = "Používá se před účtováním podle spotřeby a mimo váš limit výdajů." +topUp = "Doplnit" + +[portal.billing.prepaid.buy] +back = "Zpět" +calcTitle = "Spočítejte si roční platbu" +cancel = "Zrušit" +cancelPurchase = "Zrušit nákup" +close = "Zavřít" +continueToPayment = "Pokračovat k platbě" +finalise = "Dokončit" +invoiceErrorTitle = "Nepodařilo se vygenerovat fakturu" +loadingTitle = "Načítání vaší nabídky" +payTitle = "Zaplaťte za svůj rok" +step = "Krok {{current}} z {{total}}" + +[portal.billing.prepaid.calc] +change = "Změnit" +deployRow = "Nasazení" +done = "Hotovo" +enterpriseHint = "Toto je podnikové měřítko. Při tomto objemu jsou podnikové sazby výhodnější než jakákoli samoobslužná sleva. Zafixování sazby, podmínky a nabídka během několika minut." +governanceRow = "Správa" +governanceValue = "{{label}} · {{count}} zásad" +handlesLabel = "Váš Processor" +handlesValue = "zpracuje {{volume}} kreditů / měs." +pipelinesLabel = "Pipeline" +policiesMeta = "{{count}} zásad" +poolCaption = "Jeden fond {{credits}} kreditů na rok. Náročné měsíce čerpají z lehčích." +priceLabel = "Váš rok · 12 měsíců za cenu 10" +rateUnknown = "Přesnou cenu zobrazíme při dokončení objednávky." +savings = "Ušetříte {{amount}} · 2 měsíce zdarma." +sizingRow = "Velikost" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Celkem uživatelů" + +[portal.billing.prepaid.consent] +label = "Rozumím, že když se moje předplacená kapacita vyčerpá nebo po 12 měsících vyprší, zpracování bude automaticky pokračovat za standardní účtovanou sazbu pay-as-you-go (až do mého limitu výdajů), pokud službu nezruším, a že ji mohu kdykoli zrušit z fakturačního portálu." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Spravováno Stirling. Spuštěno během minut." +cloudValue = "Stirling Cloud · Spravováno" +selfhost = "Self-hosted" +selfhostDesc = "Běží na soukromé infrastruktuře. Stejná sazba." +selfhostValue = "Self-hosted · Soukromá infrastruktura" + +[portal.billing.prepaid.offer] +cta = "Zkontrolovat nabídku" +subtitle = "Předplaťte si rok zpracování PDF a získejte dva měsíce zdarma, použité před účtováním podle spotřeby, mimo váš limit výdajů." +title = "Získejte 12 měsíců za cenu 10" + +[portal.billing.prepaid.pay] +accountName = "Jméno držitele účtu" +accountNamePlaceholder = "např. Jana Nováková" +companyName = "Název společnosti (volitelné)" +companyNamePlaceholder = "např. Acme Inc." +downloadInvoice = "Stáhnout fakturu" +payOnline = "Zaplatit online" +poNumber = "Číslo objednávky (volitelné)" +poNumberPlaceholder = "např. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Pokročilé" +advancedDesc = "Pipeline zajišťují většinu vašeho zpracování." +none = "Žádné" +noneDesc = "Pipeline zatím neběží. Můžete je kdykoli zapnout." +standard = "Standardní" +standardDesc = "Několik pipeline znovu zpracovává příchozí PDF." + +[portal.billing.prepaid.posture] +essentials = "Základní" +essentialsDesc = "Klasifikace, výchozí nastavení, plus sdílení" +governed = "Řízené" +governedDesc = "Přidává zabezpečení a směrování" +regulated = "Regulované" +regulatedDesc = "Každá kategorie, včetně Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Stáhnout nabídku (PDF)" +downloading = "Připravuje se…" +share = "sdílet ke schválení" + +[portal.billing.prepaid.size] +compact = "Kompaktní" +compactDesc = "Soubory do 25 MB, bez poplatků za data" +heavy = "Náročné" +heavyDesc = "Skenované nebo obrazově náročné, běžně 50 MB+" +standard = "Standardní" +standardDesc = "Většinou malé, některé skeny přes 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Vyčerpáno" +healthy = "Zbývá dost" +low = "Dochází" [portal.billing.spendLimit] adjustLimit = "Upravit limit" @@ -6494,6 +6713,8 @@ label = "Očekává se překročení." [portal.billing.spendThisMonth] eyebrow = "Výdaje tento měsíc" +freeRemaining_one = "Zbývá {{formatted}} bezplatné PDF" +freeRemaining_other = "Zbývá {{formatted}} bezplatných PDF" processed_one = "Zpracováno {{formattedCount}} PDF." processed_other = "Zpracováno {{formattedCount}} PDFs." processedWithRate_one = "Zpracováno {{formattedCount}} PDF, za {{rate}} za kus." @@ -6510,6 +6731,7 @@ reachedTitle = "Měsíční limit výdajů dosažen" title = "Nepodařilo se otevřít portál Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "z {{allowance}} bezplatných PDFs použito" capSuffix_other = "z {{allowance}} bezplatných PDFs použito" eyebrow = "Zkušební Processor" @@ -6534,6 +6756,376 @@ signature = "podpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nové připojení" +createTitleFor = "Připojit {{name}}" +delete = "Smazat" +edit = "Upravit" +editTitle = "Upravit připojení" +unsupportedType = "Tento typ připojení zde nelze upravit." + +[portal.connections.categories.advanced] +description = "Pro službu, kterou zatím nepodporujeme." +label = "Pokročilé" + +[portal.connections.categories.audit] +description = "Zaznamenat, co se s dokumentem stalo, tam, kde už vaši auditoři hledají." +label = "Audit a protokolování compliance" + +[portal.connections.categories.notify] +description = "Informovat jiný systém, že se něco stalo." +label = "Oznámení a workflow" + +[portal.connections.categories.security] +description = "Naskenovat nebo označit dokument předtím, než pokračuje dál." +label = "Zabezpečení a klasifikace" + +[portal.connections.categories.signing] +description = "Odeslat dokument k podpisu nebo notářskému ověření." +label = "Elektronický podpis a notářské ověření" + +[portal.connections.categories.storage] +description = "Kam zpracované dokumenty skončí – bucket, disk, tiket nebo stránka." +label = "Soubor a příloha" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling to ukládá šifrovaně a už to nikdy nezobrazí." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Kroky používající toto připojení mohou volat pouze cesty pod touto URL, nikdy jiného hostitele." +label = "URL serveru" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Heslo" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Uživatelské jméno" + +[portal.connections.commonFields.webhookUrl] +helperText = "Vložte URL, kterou vám služba poskytla. Volat lze pouze cesty pod ní." +label = "Webhook URL" + +[portal.connections.fields] +name = "Název připojení" +namePlaceholder = "např. Produkce" + +[portal.connections.picker] +cancel = "Zrušit" +createNew = "Nové připojení..." +placeholder = "Vyberte připojení" +save = "Uložit připojení" + +[portal.connections.picker2] +back = "Změnit integraci" +heading = "Co chcete připojit?" +noResultsBody = "Zatím to nepodporujeme – řekněte nám o tom a podíváme se na přidání podpory." +noResultsTitle = "Nic neodpovídá „{{query}}“" +searchPlaceholder = "Hledat integrace – zkuste „sign“, „ocr“ nebo název produktu" + +[portal.connections.types.api] +description = "Volejte libovolné HTTP API ze zásady. Vyberete URL, ověření a payload." +label = "Vlastní API" + +[portal.connections.types.api.fields.authType] +label = "Ověření" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Uživatelské jméno a heslo" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token ve vlastním záhlaví" + +[portal.connections.types.api.fields.authType.options.none] +label = "Žádné" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Přihlásit se pro token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Kroky používající toto připojení mohou volat pouze cesty pod touto URL, nikdy jiného hostitele." +label = "Základní URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Název záhlaví" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Cesta pro přihlášení" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Heslo" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Pokud API odpoví odkazem ke stažení, mohou se načíst pouze tito hostitelé. Oddělte čárkami. Ponechte prázdné, chcete-li povolit pouze vlastního hostitele základní URL." +label = "Povolení hostitelé výsledků" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Odeslat token jako toto záhlaví" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Záhlaví odpovědi, ve kterém přihlašovací volání vrací token, např. X-Auth-Token." +label = "Záhlaví odpovědi s tokenem" + +[portal.connections.types.api.fields.username] +label = "Uživatelské jméno" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skenujte dokumenty na malware na serveru, který provozujete – nic neopustí vaši síť." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Skenování virů a obsahu pro dokumenty v zásadě." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokujte dokumenty obsahující makra, spustitelné soubory, skripty nebo vložené objekty – nejen známý malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Připojit zpracované dokumenty ke stránce Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Odesílejte dokumenty do ConsignO Cloud (Notarius) k elektronickému podpisu a notářskému ověření." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Vaše prostředí ConsignO Cloud, včetně /api/v1." +label = "URL serveru" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Heslo" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potřebné pouze v případě, že vaše organizace používá tenanty." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Uživatelské jméno" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Odeslat zprávu do Discord, když zásada zpracuje dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexovat auditní událost v Elasticsearch pokaždé, když zásada zpracuje dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Kontrolovat FTP nebo FTPS server pro nové dokumenty." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Režim připojení" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktivní" +passive = "Pasivní (doporučeno)" + +[portal.connections.types.ftp.fields.security] +helperText = "Prosté FTP odesílá přihlašovací údaje a soubory nešifrovaně; zvolte FTPS, pokud ho server podporuje." +label = "Šifrování" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicitní / AUTH TLS)" +implicit = "FTPS (implicitní)" +none = "Žádné (prosté FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Odeslat zprávu do prostoru Google Chat, když se spustí zásada." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Připojit zpracované dokumenty k Jira issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Odeslat dokument nebo oznámení e-mailem, když se spustí zásada." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Ukládat zpracované dokumenty do Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Heslo aplikace" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detekujte osobní údaje v textu dokumentu pomocí serveru Presidio, který provozujete." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Číst a používat štítky citlivosti Microsoft Purview na PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Volitelné. Potřebné pouze pro výběr štítků podle názvu; označování funguje i bez něj." +label = "ID aplikace (klienta)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Otiskne se do každého štítku jako jeho SiteId, aby nástroje kompatibilní s Purview věděly, která organizace ho použila." +label = "ID adresáře (tenanta)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, ze kterého mohou zdroje číst a do kterého mohou výstupy pipeline zapisovat." +label = "S3 úložiště" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Pro AWS ponechte prázdné. Nastavte pro MinIO nebo jiné úložiště kompatibilní s S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Odeslat dokument nebo oznámení e-mailem, když se spustí zásada." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Kontrolovat SFTP (SSH) server pro nové dokumenty." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Volitelné. Otisk klíče hostitele serveru SHA-256 (z `ssh-keyscan host | ssh-keygen -lf -`). Pokud je nastaven, přijme se pouze server předkládající tento klíč; pokud je prázdný, klíč zjištěný při prvním připojení se připne a pozdější změny budou odmítnuty." +label = "Otisk klíče hostitele" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Přístupová fráze klíče" + +[portal.connections.types.sftp.fields.password] +helperText = "Ponechte prázdné, pokud se místo hesla ověřujete soukromým klíčem." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Volitelné. Vložte soukromý klíč OpenSSH pro ověření klíčem místo hesla." +label = "Soukromý klíč" + +[portal.connections.types.slack] +description = "Odeslat zprávu do Slack, když zásada zpracuje dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Sledovat síťové sdílení SMB / CIFS (Windows nebo Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Síťové sdílení" + +[portal.connections.types.smb.fields.domain] +helperText = "Volitelné. Windows doména nebo pracovní skupina, pokud ji váš server používá." +label = "Doména" + +[portal.connections.types.smb.fields.share] +label = "Sdílení" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Odeslat auditní událost do Splunk pokaždé, když zásada zpracuje dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Odeslat auditní událost do HTTP kolektoru Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Odeslat zprávu do kanálu Teams, když se spustí zásada." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Odeslat každý dokument, nebo jen jeho údaje, na vámi zvolenou URL." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Spustit Zap nebo scénář Make ze zásady." +label = "Zapier a Make" + +[portal.docs] +browse = "Procházet dokumentaci" +viewSource = "Zobrazit zdroj na GitHub" + [portal.docs.authentication] codeCaption = "každý požadavek" eyebrow = "ZAČÍNÁME" @@ -6624,6 +7216,9 @@ eyebrow = "DOVEDNOSTI" lead = "Přibalené pojmenované schopnosti, které váš agent vyvolává jako jeden nástroj. Každá dovednost je deterministický řetězec operací s připojenými evals." title = "Dovednosti agenta" +[portal.docs.toc] +title = "Na této stránce" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Typ" user = "Uživatel" [portal.documents.queue.empty] +connectSource = "Připojit zdroj" +createPipeline = "Vytvořit pipeline" description = "Jakmile zdroje předají dokumenty do vašich pipeline, zobrazí se zde ke kontrole." title = "Ve frontě nejsou žádné dokumenty" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Tomuto filtru neodpovídají žádné dokumenty." rowActions = "Akce řádku" sensitiveLabel = "Citlivé" -sensitiveTitle = "Citlivé — je vyžadován přístup" [portal.documents.table.columns] action = "Pipeline / Akce" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Stav" time = "Čas" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Naposledy viděno" region = "Region" status = "Stav" +target = "Target" version = "Verze" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "V tomto zobrazení došlo k neočekávané chybě. Zkuste to znov retry = "Zkusit znovu" title = "Na této stránce se něco pokazilo" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Zpět" +body = "Stáhněte si desktopovou aplikaci nebo provozujte server self-hosted." +done = "Hotovo" +guide = "Otevřít průvodce {{name}}" +openInBrowser = "Místo toho otevřít v prohlížeči" +sectionDesktop = "Desktopová aplikace" +sectionSelfHosted = "Self-hosted" +title = "Nainstalovat Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Spusťte oficiální image na svém serveru." +note = "Otevře se na :8080. Výchozí přihlášení: admin / stirling." +tagline = "Spusťte server v kontejneru" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Nainstalujte oficiální Helm chart." +note = "Přidejte PVC pro /configs, aby se nastavení zachovala." +tagline = "Nasaďte do clusteru pomocí Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Nativní sestavení pro vaši distribuci." +guideBtn = "Zobrazit možnosti instalace pro Linux" +note = "Pro běžné distribuce jsou k dispozici sestavení AppImage, .deb a .rpm." +tagline = "Nativní desktopová aplikace pro vaši distribuci" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Nebo přes Homebrew" +detailBody = "Spusťte instalátor nebo použijte Homebrew." +downloadBtn = "Stáhnout pro macOS" +tagline = "Nativní aplikace pro Apple silicon a Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Spusťte .jar na bare metal serveru." +downloadBtn = "Stáhnout .jar" +note = "Vyžaduje Java 21+. Otevře se na :8080; výchozí přihlášení admin / stirling." +runLabel = "Poté jej spusťte" +tagline = "Spusťte .jar na bare metal serveru" +title = "Ruční nastavení serveru" + +[portal.home.download.windows] +altLabel = "Nebo přes winget" +detailBody = "Spusťte instalátor nebo použijte správce balíčků." +downloadBtn = "Stáhnout instalátor" +tagline = "Nativní aplikace pro Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktivních" -invite = "Pozvat členy týmu" +activeOfDeployed = "{{active}} z {{total}} aktivních tento měsíc" name = "Stirling PDF Editor" open = "Otevřít v prohlížeči" updated = "aktualizováno {{time}}" +[portal.home.editor.deploy] +finish = "Dokončit nasazení" +options = "Zobrazit možnosti nasazení" +start = "Nasadit Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dobré odpoledne" evening = "Dobrý večer" morning = "Dobré ráno" -[portal.home.onboarding] -dismiss = "Zavřít nastavení" -notStarted = "Nezahájeno" -progress = "{{done}} z {{total}} hotovo" -title = "Dokončete nastavení" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC v rámci organizace, závazkové objemové ceny a nasazení v izolované síti." -cta = "Zahájit zkušební verzi" -ctaQuote = "Získat nabídku" -lead = "Pro 250+ zaměstnanců." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Nasaďte desktopovou aplikaci, navždy zdarma." -title = "Stáhnout PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktivních · {{recommended}} doporučeno" -chip = "{{active}} aktivních" -title = "Zapnout zásady" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} připojeno · každý PDF je řízen tam, kam dorazí" -chip = "{{connected}} připojeno" -title = "Připojte své zdroje" - -[portal.home.quickActions] -subtitle = "Hlavní úkoly na dnešek" -title = "Rychlé akce" - -[portal.home.quickActions.buildPipeline] -blurb = "Tříkrokový návrhář nad knihovnou typovaných operací" -title = "Vytvořit pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooky, sledované složky" -title = "Připojit zdroj" - -[portal.home.quickActions.issueApiKey] -blurb = "Klíč s vymezeným rozsahem, limity rychlosti a IP allowlist" -title = "Vydat API klíč" - [portal.infrastructure] manageEditorDeployment = "Spravovat nasazení Editoru" sectionsAriaLabel = "Sekce infrastruktury" @@ -6878,13 +7541,9 @@ heading = "API klíče" subheading = "Přihlašovací údaje s vymezeným rozsahem, limity rychlosti pro jednotlivé klíče, oprávněními a IP allowlisty." [portal.infrastructure.apiKeys.card] -allowedIps = "Povolené IP" -anyIp = "Jakákoli IP (bez allowlistu)" created = "Vytvořeno" lastUsed = "Naposledy použito" -permissions = "Oprávnění" -rateLimit = "Limit rychlosti" -rateLimitValue = "{{value}} pož./min" +revoke = "Odvolat klíč" usageMonth = "Využití tento měsíc" usageToday = "Využití dnes" @@ -6892,6 +7551,15 @@ usageToday = "Využití dnes" description = "Vytvořte klíč s vymezeným rozsahem a začněte volat Stirling API." title = "Zatím žádné API klíče" +[portal.infrastructure.apiKeys.error] +load = "Nepodařilo se načíst vaše API klíče. Zkuste to prosím znovu." + +[portal.infrastructure.apiKeys.revoke] +body = "Odvolat „{{name}}“? Každá integrace, která tento klíč stále používá, začne okamžitě dostávat chyby 401. Tuto akci nelze vrátit zpět." +cancel = "Zrušit" +confirm = "Odvolat klíč" +title = "Odvolat API klíč" + [portal.infrastructure.audit] filterAriaLabel = "Filtrovat auditní události podle kategorie" heading = "Auditní protokoly" @@ -6935,6 +7603,7 @@ all = "Vše" auth = "Ověření" config = "Konfigurace" elevation = "Zvýšení oprávnění" +policy = "Zásada" processing = "Zpracování" security = "Zabezpečení" @@ -6944,19 +7613,30 @@ title = "Nemáte přístup" [portal.infrastructure.audit.metrics] config = "Konfigurace" -elevation = "Zvýšení oprávnění" +policy = "Zásada" processing = "Zpracování" totalEvents = "Celkem událostí · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Konfigurace" +elevation = "Zvýšení oprávnění" +policy = "Zásada" +processing = "Zpracování" +security = "Zabezpečení" + +[portal.infrastructure.auditStatusLabel] +danger = "Chyba" +info = "Info" +success = "Úspěch" +warning = "Varování" + [portal.infrastructure.createKey] cancel = "Zrušit" createKey = "Vytvořit klíč" done = "Hotovo" -ipAllowlistHelper = "CIDR rozsahy oddělené čárkami. Ponechte prázdné pro povolení jakékoli IP." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Název klíče" keyNamePlaceholder = "např. Produkce · ingest" -permissionsLabel = "Oprávnění" secretKeyCaption = "Tajný klíč" secretWarning = "Uložte jej do správce tajemství. Stirling vždy ukládá pouze hash — později jej nelze obnovit." subtitle = "Omezte rozsah klíče na minimum, které potřebuje. Můžete jej kdykoli rotovat nebo odvolat." @@ -6964,207 +7644,9 @@ subtitleCreated = "Zkopírujte si toto tajemství nyní — znovu se nezobrazí. title = "Vytvořit API klíč" titleCreated = "Klíč vytvořen" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Nasadil" -environment = "Prostředí" -product = "Produkt" -status = "Stav" -version = "Verze" -when = "Kdy" - -[portal.infrastructure.deployments.recent] -heading = "Nedávná nasazení" -subheading = "Nejnovější rollouty napříč produkty a prostředími." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instance" -latency = "Latence" -load = "Zatížení" -p99 = "P99" -region = "Region" -status = "Stav" -throughput = "Propustnost" -uptime = "Doba provozu" -version = "Verze" - -[portal.infrastructure.deployments.regions] -heading = "Regiony" -subheading = "Živý stav každého nasazeného regionu Stirling — latence, zatížení a verze rolloutů." - -[portal.infrastructure.deployments.regions.empty] -description = "Nasazené regiony se zde zobrazí, jakmile bude váš pracovní prostor zřízen." -title = "Žádné regiony nejsou nasazeny" - -[portal.infrastructure.models] -heading = "Modely" -msValue = "{{value}} ms" -subheading = "Katalog modelů a směrování, které pohání zpracování dokumentů napříč vaším pracovním prostorem." - -[portal.infrastructure.models.byom] -description = "Zaregistrujte on-prem nebo self-hosted model a připněte jej k regionu pro zpracování vázané na datovou rezidenci." -title = "Přineste si vlastní model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Spravované modely dostupné pro váš pracovní prostor, s živou latencí a náklady." -subEnterprise = "Spravované, vlastní a on-prem modely — s možností připnutí podle regionu." - -[portal.infrastructure.models.catalogue.empty] -description = "Modely v katalogu vašeho pracovního prostoru se zobrazí zde." -title = "Žádné modely nejsou dostupné" - -[portal.infrastructure.models.columns] -cost = "Náklady" -latency = "Latence" -load = "Zatížení" -model = "Model" -status = "Stav" -type = "Typ" -version = "Verze" - -[portal.infrastructure.models.metrics] -activeModels = "Aktivní modely" -avgLatency = "Průměrná latence" -included = "Zahrnuto" -monthlySpend = "Měsíční výdaje za modely" - -[portal.infrastructure.models.routing] -empty = "Nejsou nakonfigurována žádná pravidla směrování." -heading = "Pravidla směrování" -sub = "Který model zpracovává jednotlivé operace. Výchozí se použije, když neodpovídá žádné užší pravidlo." -subLocked = "Směrujte operace na konkrétní modely — dostupné v placených plánech." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgradujte na Pro a určete, který model zpracovává jednotlivé operace a typy dokumentů." -title = "Směrování modelů je placená funkce" - -[portal.infrastructure.models.routingColumns] -default = "Výchozí" -docType = "Typ dokumentu" -modelForAria = "Model pro {{operation}}" -operation = "Operace" -routedTo = "Směrováno na" - -[portal.infrastructure.security.access.byok] -description = "Poskytněte klíč z vlastního KMS. Stirling s ním šifruje, ale stále může číst." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Klíče nikdy neopustí váš KMS. Stirling uchovává pouze ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling spravuje šifrovací klíče. Nejjednodušší — na vaší straně žádné operace s klíči." -label = "Klíče držené Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Zásady přístupu k dokumentům" -subheading = "Řídí, kdo může dešifrovat zpracované dokumenty v klidu." - -[portal.infrastructure.security.attestations] -heading = "Atestace souladu" -noReport = "Není dostupná žádná zpráva" -subheading = "Auditní postoj podle jednotlivých rámců, se zprávami dostupnými u atestovaných kontrol." -viewReport = "Zobrazit zprávu →" - -[portal.infrastructure.security.compliance] -heading = "Soulad" -subheading = "Atestace a certifikace pokrývající platformu Stirling." - -[portal.infrastructure.security.empty] -description = "Konfigurace zabezpečení vašeho pracovního prostoru se zobrazí zde." -title = "Bezpečnostní postoj není dostupný" - -[portal.infrastructure.security.hyokBanner] -description = "S HYOK šifrovací klíče nikdy neopustí váš KMS. Stirling ukládá a zpracovává pouze ciphertext, který můžete kdykoli odvolat." -title = "Stirling nemůže dešifrovat vaše dokumenty" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nejsou nakonfigurovány žádné IP rozsahy — všechny IP jsou povoleny." -heading = "IP allowlist" -sub = "API přístup je omezen na tyto CIDR rozsahy." -subLocked = "Omezte API přístup na známé IP rozsahy — dostupné v placených plánech." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgradujte na Pro a omezte API přístup na konkrétní sítě." -title = "IP allowlisting je placená funkce" - -[portal.infrastructure.security.ipColumns] -added = "Přidáno" -addedBy = "Přidal" -cidr = "CIDR" -label = "Štítek" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmus" -heading = "Správa šifrovacích klíčů" -keyId = "Identifikátor klíče" -lastRotated = "Naposledy rotováno" -rotateKey = "Rotovat klíč" -rotationPolicy = "Zásady rotace" -subheading = "Správa klíčů, které šifrují dokumenty v klidu — kdo může dešifrovat a jak se klíče rotují." - -[portal.infrastructure.security.managedBanner] -description = "Správa klíčů bring-your-own-key (BYOK) a hold-your-own-key (HYOK) je dostupná v Enterprise. Upgradujte a poskytněte klíče z vlastního KMS." -title = "Klíče jsou ve vašem plánu spravovány Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asie a Pacifik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · hranice dat GDPR" -label = "Evropská unie" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Spojené státy" - -[portal.infrastructure.security.residencyHeader] -heading = "Datová rezidence" -subheading = "Kde jsou dokumenty ukládány a zpracovávány." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} využito" - -[portal.infrastructure.storage.empty] -description = "Připojené úložiště a využití se zobrazí zde." -title = "Není nakonfigurováno žádné úložiště" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktivní" -activeRange = "0–{{value}} d" -archived = "Archivováno" -coldStorage = "cold storage" -deleted = "Smazáno" -never = "nikdy" -purged = "vymazáno" - -[portal.infrastructure.storage.providers] -connect = "Připojit" -connected = "Připojeno" -heading = "Připojení poskytovatelé" -subheading = "Kam se zapisují zpracované artefakty." - -[portal.infrastructure.storage.retention] -heading = "Uchovávání" -subheading = "Jak dlouho se artefakty uchovávají před odstraněním v rámci životního cyklu." -windowLabel = "Výchozí okno uchovávání" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} den" -days_other = "{{count}} dní" -never = "Nikdy nemazat" - -[portal.infrastructure.storage.totalUsage] -heading = "Celkové využití" -progressLabel = "Využité úložiště" -subheading = "Úložiště spotřebované napříč všemi připojenými poskytovateli." +revoked = "Odvoláno" [portal.infrastructure.tabs] apiKeys = "API klíče" @@ -7174,6 +7656,49 @@ models = "Modely" security = "Zabezpečení" storage = "Úložiště" +[portal.integrations] +availableHeading = "Dostupné" +comingSoonHeading = "Již brzy" +connect = "Připojit" +connectionCount_one = "{{count}} připojení" +connectionCount_other = "{{count}} připojení" +customApi = "Vlastní API" +searchPlaceholder = "Hledat integrace" +subtitle = "Vše, s čím může Stirling komunikovat. Přihlašovací údaje se vytvoří jednou zde a poté se na ně odkazuje ve zdrojích, zásadách a pipeline." +title = "Integrace" +typedName = "Název {{tool}}" + +[portal.integrations.filters] +all = "Vše" +audit = "Audit" +connected = "Připojeno" +notify = "Oznámení" +security = "Zabezpečení" +signing = "Podepisování" +storage = "Úložiště" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Připojeno" + +[portal.integrations.table] +integration = "Integrace" +worksWith = "Funguje s" + +[portal.integrations.worksWith] +pipelines = "Pipeline" +policies = "Zásady" +sources = "Zdroje" + +[portal.legal] +draft = "{{label}} · koncept" +loadError = "Tento dokument se nepodařilo načíst. Zkuste to prosím znovu." +loading = "Načítání…" +title = "Právní dokument" + [portal.nav] agent-builder = "Agent Builder" components = "Komponenty" @@ -7182,33 +7707,17 @@ documents = "Dokumenty" editor = "Editor" home = "Domů" infrastructure = "Infrastruktura" +integrations = "Integrace" pipelines = "Pipeline" policies = "Zásady" procurement = "Nákup" -settings = "Nastavení" sources = "Zdroje" usage = "Využití a fakturace" users = "Uživatelé" -[portal.notifications] -markAllRead = "Označit vše jako přečtené" -title = "Oznámení" -viewAll = "Zobrazit vše" - -[portal.notifications.ariaLabel] -none = "Oznámení, žádná nepřečtená" -unread_one = "Oznámení, {{count}} nepřečtené" -unread_other = "Oznámení, {{count}} nepřečtených" - -[portal.notifications.count] -allRead = "vše přečteno" -loading = "načítání" -new_one = "{{count}} nové" -new_other = "{{count}} nových" - -[portal.notifications.empty] -description = "Žádná nová oznámení." -title = "Vše máte vyřízeno" +[portal.nav.section] +platform = "Platforma PDF" +processor = "Procesor PDF" [portal.pipelines] subtitle = "Každá automatizovaná dokumentová pipeline na backendu: seřazený řetězec operací nad sadou zdrojů, spuštěný triggerem. Klikněte na řádek pro zobrazení jejích kroků a zdrojů." @@ -7218,54 +7727,87 @@ title = "Pipeline" newPipeline = "Nová pipeline" [portal.pipelines.builder] -addStep = "Přidat nástroj" +activate = "Activate" back = "Zpět na pipeline" +cannotFollow = "Nelze přijmout {{produced}}" +chooseAccount = "Vyberte účet" +chooseDestination = "Vyberte cíl" +chooseOperation = "Vyberte, co tento krok provede" +chooseSource = "Vyberte zdroj" discard = "Zahodit změny" -enabled = "Povoleno" +inputs = "Vstup" +inputSource = "Vstupní zdroj" +inputTrigger = "Spouštěč" keepEditing = "Pokračovat v úpravách" -needsUpload = "Vyžaduje nahraný soubor" +moreActions = "More actions" +needsConfiguring = "Vyžaduje nastavení" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Vašemu hledání neodpovídají žádné nástroje." -pipelineSettings = "Nastavení pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Hledat nástroje" -selectToolBody = "Přidejte nástroj a vytvořte svou pipeline." -selectToolTitle = "Zatím žádné nástroje" -toolSettings = "Nastavení nástroje" +sendToSystem = "Odeslat do jiného systému" +stepsIncompatible = "Tyto kroky nelze spustit nad tím, co vytváří jejich předchozí krok: {{tools}}." +stepsNeedSetup = "Tyto kroky je před uložením stále nutné nastavit: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Nerozpoznaná operace, ponechána beze změny." unsavedBody = "Máte neuložené změny. Uložte je před odchodem, nebo je zahoďte?" unsavedTitle = "Neuložené změny" -uploadUnsupported = "Nahrané soubory zatím nejsou v pipeline podporovány, takže tyto kroky nelze uložit: {{tools}}." usesDefaults = "Spouští se s výchozím nastavením" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinuje každý soubor z předchozího kroku" +fan-out = "Spustí se jednou pro každý soubor z předchozího kroku" +format-mismatch = "Vyžaduje {{accepts}}, ale předchozí krok vytváří {{produced}}" +output-uncertain = "Nemusí se spustit: výstup předchozího kroku závisí na jeho nastavení" +source-mismatch = "Vyžaduje {{accepts}}, ale vstup této pipeline je {{produced}}" +undeclared-operation = "Nelze zkontrolovat, co tento krok přijímá" [portal.pipelines.composer] addTool = "Přidat nástroj" -cancel = "Zrušit" -chainEmpty = "Přidejte nástroj a začněte vytvářet svou pipeline." create = "Vytvořit pipeline" -directory = "Výstupní složka" -directoryHelp = "Absolutní cesta na serveru. Musí být v rámci nakonfigurovaných povolených složek." +createPaused = "Create paused" editingUnsupported = "Zobrazení těchto parametrů nástroje pro úpravy zatím není podporováno." -moveDown = "Přesunout dolů" -moveUp = "Přesunout nahoru" +editSource = "Edit source" name = "Název" namePlaceholder = "např. Kontrola redakcí" -noSources = "Zatím nejsou připojeny žádné zdroje. Pipeline může stále běžet na souborech dodaných přímo do ní." noToolSettings = "Tento nástroj nemá žádná konfigurovatelná nastavení." -operations_one = "Operace ({{count}})" -operations_other = "Operace ({{count}})" output = "Výstup" -removeStep = "Odebrat operaci" save = "Uložit změny" scheduleEvery = "Spustit každých" -sources = "Zdroje" -sourcesLoading = "Načítání zdrojů..." trigger = "Trigger" triggerManual = "Pouze ručně" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dní" hours = "hodin" minutes = "minut" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Smazat „{{name}}“? Tuto akci nelze vrátit zpět." cancel = "Zrušit" @@ -7273,28 +7815,62 @@ confirm = "Smazat" title = "Smazat pipeline?" [portal.pipelines.detail] +clearHistory = "Vymazat historii" delete = "Smazat pipeline" run = "Spustit nyní" [portal.pipelines.empty] action = "Vytvořit pipeline" +connectSource = "Připojit zdroj" description = "Vytvořte svou první pipeline: vyberte zdroje, nad kterými běží, zřetězte operace a zvolte, kam půjde výstup." title = "Zatím žádné pipeline" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktivní" paused = "Pozastaveno" total = "Pipeline" -[portal.pipelines.output] -folder = "Zapsat do složky" -inline = "Vrátit soubory" - [portal.pipelines.run] +allProcessed_one = "Není co spustit: {{count}} dokument ze zdroje již byl zpracován." +allProcessed_other = "Není co spustit: všech {{count}} dokumentů ve zdrojích již bylo zpracováno." completed_one = "Běh dokončen." completed_other = "Všech {{count}} běhů dokončeno." empty = "Není co spustit: zdroje neměly žádné dokumenty ke zpracování." failed = "Běh selhal: {{error}}" +inFlight = "Není co nového spustit: dokumenty se stále zpracovávají z dřívějšího spuštění." +parked_one = "Není co spustit: {{count}} dokument dříve selhal a je odložen. Opravte příčinu a poté vymažte historii, abyste jej zkusili znovu." +parked_other = "Není co spustit: {{count}} dokumentů dříve selhalo a jsou odloženy. Opravte příčinu a poté vymažte historii, abyste je zkusili znovu." running = "Běh zahájen; stále probíhá." timeout = "Běh trvá déle, než se očekávalo; může se stále dokončit na pozadí." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Zdroje" status = "Stav" steps = "Kroky" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Sledování složky" @@ -7314,12 +7891,18 @@ manual = "Ruční" schedule = "Naplánováno" [portal.policies] +defaultName = "Zásada {{category}}" subtitle = "Trvalé automatizace, které vynucují pipeline nástrojů na každém dokumentu. Každá zásada se spustí při nahrání nebo exportu, provede svůj řetězec nástrojů a uloží vynucenou verzi vedle originálu." title = "Zásady" [portal.policies.card] comingSoon = "Upgradovat na Enterprise" notSetUp = "Nenastaveno" +requiresAiEngine = "Vyžaduje AI engine" + +[portal.policies.categories.classification] +desc = "Identifikujte typ každého dokumentu při nahrání a označte jeho metadata pro zařazení a vyhledávání." +label = "Klasifikace" [portal.policies.categories.compliance] desc = "Vynucujte požadavky HIPAA, GDPR, SOC 2 nebo FedRAMP na každém dokumentu." @@ -7344,6 +7927,13 @@ label = "Zabezpečení" [portal.policies.config] scopeAll = "Všechny dokumenty" +[portal.policies.config.classification] +summary = "Klasifikuje každý nahraný dokument podle štítků vašeho týmu a označí jeho metadata." + +[portal.policies.config.classification.rules] +0 = "Klasifikovat" +1 = "Označit metadata" + [portal.policies.config.compliance] summary = "Ověřuje dokumenty vůči regulatorním rámcům předtím, než opustí systém." @@ -7371,6 +7961,29 @@ minConfidence = "Min. spolehlivost" 2 = "Pojmenovat" 3 = "Normalizovat" +[portal.policies.config.purview] +label = "Použít štítek citlivosti Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "ID štítku" +labelIdHelp = "GUID štítku z portálu Purview. Použití štítku zapíše jeho metadata do PDF; nedokáže jej zašifrovat, k tomu je potřeba klient Microsoft." +labelName = "Název štítku (volitelné)" +labelNameHelp = "Uloženo spolu s ID, aby lidé a nástroje mohli štítek rozpoznat." + +[portal.policies.config.purview.fields.method] +help = "Purview rozlišuje štítek použitý zásadou od štítku vybraného osobou." +label = "Jak je štítek zaznamenán" +privileged = "Jako vybraný osobou" +standard = "Jako použitý automaticky" + +[portal.policies.config.purviewRead] +label = "Přečíst štítek Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Přečte štítek, který dokument již nese, aby na něj mohly navazující kroky reagovat." + [portal.policies.config.retention] summary = "Vynucuje, jak dlouho se dokumenty uchovávají, kdy se archivují a kdy se mažou." @@ -7438,6 +8051,7 @@ title = "Zatím žádná aktivita" [portal.policies.endpoints] addWatermark = "Vodoznak" autoRedact = "Redigovat PII" +classifyAndLabel = "Klasifikovat" compressPdf = "Komprimovat" flatten = "Sloučit vrstvy" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vaše zásady jsou uloženy a zobrazí se po obnovení připojen retry = "Zkusit znovu" title = "Backend není dostupný" +[portal.policies.operations] +change = "Změnit, co tento krok provede" +noResults = "Žádný krok tomu neodpovídá. Zkuste název produktu nebo „scan“, „notify“, „attach“." +searchPlaceholder = "Hledat kroky – zkuste „scan“, „attach“ nebo název produktu" + +[portal.policies.operations.bodyMode] +binary = "Nezpracované bajty souboru" +json = "Uvnitř JSON payload" +multipart = "Jako nahrání souboru (multipart)" + +[portal.policies.operations.clamavScan] +description = "Váš vlastní ClamAV server zkontroluje dokument; nic neopustí vaši síť." +label = "Vyhledat viry (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Odmítá dokumenty obsahující makra, spustitelné soubory, skripty nebo vložené objekty." +label = "Blokovat aktivní obsah" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive zkontroluje dokument a běh se zastaví, pokud není čistý." +label = "Vyhledat viry" + +[portal.policies.operations.confluenceAttach] +description = "Uloží zpracovaný dokument na stránku." +label = "Připojit ke stránce Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Odešle dokument do ConsignO Cloud k podpisu." +label = "Odeslat k podpisu (ConsignO)" +note = "Odešle dokument. Načtení podepsané kopie zatím není podporováno, protože krok nemůže předat výsledek pozdějšímu kroku." + +[portal.policies.operations.customApiCall] +description = "Vytvořte volání sami: cestu, metodu, tělo a zpracování odpovědi." +label = "Volat vlastní API" + +[portal.policies.operations.discordNotify] +description = "Informuje kanál, že zásada zpracovala dokument." +label = "Odeslat zprávu do Discord" + +[portal.policies.operations.elasticIndex] +description = "Zaznamená, co zásada provedla, jako prohledávatelný dokument." +label = "Indexovat auditní událost v Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart se hodí pro upload API; JSON se hodí pro API, která chtějí soubor uvnitř payload; binary odesílá nezpracované bajty." +label = "Jak odeslat dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON tělo odeslané beze změny. {{document.base64}} obsahuje soubor; {{document.*}} a {{run.*}} se vyplní pro každý dokument." +label = "JSON tělo" + +[portal.policies.operations.fields.connection] +label = "Účet" + +[portal.policies.operations.fields.domain] +label = "Mailgun doména" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Pole formuláře, pod kterým API očekává dokument." +label = "Název pole souboru" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON objekt dvojic název/hodnota hlavičky. Přihlašovací údaje připojení se přidají automaticky." +label = "Hlavičky" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Klíč issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Odesláno jako tělo zprávy. {{document.*}} a {{run.*}} se vyplní pro každý dokument." +label = "Zpráva" + +[portal.policies.operations.fields.method] +label = "Metoda" + +[portal.policies.operations.fields.pageId] +label = "ID stránky" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Připojí se k základní URL připojení. Nikdy nemůže dosáhnout na jiného hostitele." +label = "Cesta" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Kam jej na disku zapsat. {{document.filename}} se vyplní pro každý dokument." +label = "Cílová cesta" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report ponechá dokument beze změny. Replace jej nahradí tím, co se vrátí." +label = "Co udělat s odpovědí" + +[portal.policies.operations.fields.signerEmail] +label = "E-mail podepisující osoby" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Předmět" +placeholder = "Zpracováno: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyzuje tento text. Použijte {{document.title}} nebo vložte text, který chcete zkontrolovat." +label = "Text ke kontrole" + +[portal.policies.operations.fields.to] +label = "Komu" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Uživatelské jméno Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informuje prostor, že zásada zpracovala dokument." +label = "Odeslat zprávu do Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Uloží zpracovaný dokument k issue." +label = "Připojit k issue Jira" + +[portal.policies.operations.mailgunEmail] +description = "Odešle zpracovaný dokument jako přílohu." +label = "Odeslat dokument e-mailem (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Zapíše zpracovaný dokument do složky." +label = "Nahrát do Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Váš vlastní Presidio server nahlásí osobní údaje, které najde." +label = "Detekovat osobní údaje" +note = "Presidio čte text, ne soubory. Zadejte text, který chcete zkontrolovat — samotný dokument se nenahrává." + +[portal.policies.operations.responseMode] +replace = "Nahradit dokument odpovědí" +report = "Ponechat dokument, zaznamenat odpověď" + +[portal.policies.operations.sendgridEmail] +description = "Odešle zpracovaný dokument jako přílohu." +label = "Odeslat dokument e-mailem (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informuje kanál, že zásada zpracovala dokument." +label = "Odeslat zprávu do Slack" + +[portal.policies.operations.splunkEvent] +description = "Zaznamená, co zásada provedla, tam, kde vaši auditoři už hledají." +label = "Odeslat auditní událost do Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Zaznamená, co zásada provedla, do HTTP kolektoru." +label = "Odeslat auditní událost do Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informuje kanál, že zásada zpracovala dokument." +label = "Odeslat zprávu do Teams" + +[portal.policies.operations.webhookPost] +description = "Odešle samotný dokument na URL, kterou zvolíte." +label = "Odeslat dokument do webhook" + +[portal.policies.operations.zapierNotify] +description = "Předá podrobnosti běhu vaší automatizaci." +label = "Spustit Zap nebo scénář Make" + [portal.policies.stats] activeFor = "Aktivní" dataProcessed = "Zpracovaná data" @@ -7473,6 +8282,13 @@ label = "Vynucené dokumenty" description = "Nakonfigurováno, ale nespouští se" label = "Pozastaveno" +[portal.policies.table] +appliesTo = "Platí pro" +docs = "Vynucené dokumenty" +enforces = "Vynucuje" +policy = "Zásada" +status = "Stav" + [portal.policies.wizard.actions] back = "Zpět" cancel = "Zrušit" @@ -7480,6 +8296,10 @@ continue = "Pokračovat" enablePolicy = "Povolit zásadu" saveChanges = "Uložit změny" +[portal.policies.wizard.capability.classify] +desc = "Identifikuje typ dokumentu podle štítků vašeho týmu a označí jej, aby se zařazoval a vyhledával podle kategorie." +label = "Klasifikovat dokument" + [portal.policies.wizard.capability.compress] desc = "Komprimuje dokument na menší velikost souboru." label = "Zmenšit velikost souboru" @@ -7500,10 +8320,18 @@ label = "Redigovat citlivé informace" desc = "Odstraní skrytý JavaScript, aby se při otevření dokumentu nemohlo nic automaticky spustit." label = "Odstranit aktivní obsah" +[portal.policies.wizard.capability.timestampPdf] +desc = "Prokazuje, že dokument existoval přesně v této podobě v daném okamžiku, pomocí nezávislé autority časových razítek. Odešle se pouze hash – dokument nikdy neopustí váš server." +label = "Přidat důvěryhodné časové razítko" + [portal.policies.wizard.capability.watermark] desc = "Přidá viditelné označení (např. „Důvěrné“) přes každou stránku." label = "Použít vodoznak" +[portal.policies.wizard.classification] +description = "Každý nahraný dokument je klasifikován podle vestavěných štítků a označen typy, které odpovídají. Sada štítků je sdílena napříč celým vaším týmem." +labelsHeading = "Klasifikační štítky" + [portal.policies.wizard.errors] noTools = "Nejprve povolte ve workflow alespoň jeden nástroj." saveFailed = "Zásadu se nepodařilo uložit. Zkuste to prosím znovu." @@ -7548,73 +8376,118 @@ setUp = "Nastavit zásadu {{category}}" [portal.policies.wizard.workflow] description = "Vyberte, co tato zásada provede s každým dokumentem, který zpracuje." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktivních" -noRule = "Zatím není vynuceno žádné pravidlo" -subtitle = "Trvalé automatizace, kterými prochází každý dokument bez ohledu na to, která pipeline jej zpracovává." -title = "Co se spouští na vašich PDFs" - [portal.policySummary.action] -comingSoon = "Upgradovat na Enterprise" -configure = "Konfigurovat" setUp = "Nastavit" -[portal.policySummary.column] -activeRule = "Aktivní pravidlo" -policy = "Zásada" -status = "Stav" +[portal.processorFlow] +footnote = "Počty jsou za posledních 24 hodin. Rychlost toku je ilustrační." +liveBadge = "Živě" +stats = "{{connected}} připojeno · zpracováno {{processed}} PDF souborů" +title = "Procesor PDF" -[portal.policySummary.empty] -description = "Jakmile budou zásady nakonfigurovány, kategorie se zobrazí zde." -title = "Zatím žádné zásady" +[portal.processorFlow.lens] +ariaLabel = "Režim zobrazení" +flow = "Tok" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktivní" -off = "Vypnuto" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Selhalo" +heading = "Výsledky" +success = "Doručeno" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktivní" +count = "{{n}} · 24 h" +heading = "Zásady" +setUp = "Nastavit" soon = "Brzy" -[portal.processingStatus] -managePlan = "Spravovat plán" -volumeSuffix = "PDFs zpracováno · posledních 30 dní" +[portal.processorFlow.sankey] +waist = "Zásady · {{n}} aktivní" + +[portal.processorFlow.sankey.empty] +description = "Připojte zdroj a zapněte zásadu, abyste mohli sledovat tok dokumentů procesorem." +title = "Zatím žádný tok" + +[portal.processorFlow.sources] +comingSoonTag = "Připojit" +editor = "Stirling PDF Editor" +heading = "Zdroje" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Příjem e-mailů" [portal.procurement] -reset = "Resetovat pořízení (demo)" subtitle = "Nechte svůj tým vyhodnotit, uzavřít smlouvu a zahájit používání. Každý dokument na jednom místě." title = "Pořízení" -[portal.procurement.action] -download = "Stáhnout" -pay = "Zaplatit nyní" -request = "Vyžádat" -sign = "Zkontrolovat a podepsat" -upload = "Nahrát" - [portal.procurement.agreement] agreeCta = "Souhlasit a předplatit" +confidential = "Důvěrné" confirm = "Přečetl(a) jsem si smlouvu Stirling Enterprise Agreement a souhlasím s ní." -eyebrow = "Smlouva" -intro = "Jedna kombinovaná smlouva pokrývá vaši dohodu: hlavní servisní smlouvu, objednávkový formulář, EULA a smlouvu o zpracování dat. Zkontrolujte ji a poté souhlasem přijměte nabídku jako závazné předplatné." -title = "Zkontrolujte svou enterprise smlouvu" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA a DPA, sloučené do jednoho podpisu." +download = "Stáhnout" +downloadDraftError = "Nepodařilo se vygenerovat PDF dohody – renderer dokumentů není dostupný. Zkuste to prosím brzy znovu nebo kontaktujte podporu." +downloadError = "Nepodařilo se stáhnout podepsanou dohodu. Zkuste to prosím znovu." +legalName = "Název právnické osoby" +legalNamePlaceholder = "Právnická osoba, která bude podepisovat" +loadError = "Dohodu se nepodařilo načíst. Zkuste to prosím znovu." +loading = "Načítání dohody..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Požádat o změny" +scrollHint = "Posuňte pro kontrolu" +signatory = "Jméno podepisující osoby" +signatoryPlaceholder = "Celé jméno" +signatoryTitle = "Funkce podepisující osoby" +signatoryTitlePlaceholder = "např. General Counsel" +signError = "Nepodařilo se zaznamenat váš podpis. Zkuste to prosím znovu." [portal.procurement.builder] addons = "Doplňky" +addressLine1 = "Řádek adresy 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Řádek adresy 2" +addressLine2Placeholder = "Apartmá, patro, budova" back = "Zpět" businessName = "Název firmy" businessNamePlaceholder = "Vaše společnost" +city = "Město" +cityPlaceholder = "San Francisco" +completeRequired = "Před vygenerováním nabídky prosím vyplňte povinná pole (označená *) platným e-mailem." +contactEmail = "Kontaktní e-mail" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktní jméno" +contactNamePlaceholder = "Jane Doe" continue = "Pokračovat" -country = "Země" -countryEuro = "Eurozóna (EUR €)" -countryUK = "Spojené království (GBP £)" -countryUS = "Spojené státy (USD $)" +done = "Hotovo" eula = "Přečetl(a) jsem si Stirling Enterprise EULA a souhlasím s ní. Řídí smlouvu vygenerovanou z této nabídky." generate = "Vygenerovat nabídku" -included = "Zahrnuto" indemnification = "Odškodnění za IP nároky" indemnificationSub = "Bráníme kvalifikované IP nároky podle EULA" -offlineLicense = "Offline / air-gapped licence" -offlineLicenseSub = "Stažitelný licenční soubor pro air-gapped self-hosted instanci" +paperEyebrow = "Enterprise nabídka" +paperFor = "Připraveno pro" +pdfSize = "Velikost PDF" +poNumber = "Číslo PO" +poNumberPlaceholder = "Volitelné" +postalCode = "PSČ" +postalCodePlaceholder = "94105" +posture = "Správa" +posture_count = "~{{count}} zásad" +posture_essentials = "Essentials" +posture_essentialsSub = "Klasifikace + sdílení — výchozí nastavení" +posture_governed = "Governed" +posture_governedSub = "Přidává zabezpečení a směrování" +posture_regulated = "Regulated" +posture_regulatedSub = "Každá kategorie — Compliance, Retention, Ingestion" qbr = "Čtvrtletní obchodní hodnocení" qbrSub = "Váš SE každý kvartál zkontroluje používání a roadmapu" +region = "Stát / region" +regionPlaceholder = "California" running = "{{annual}} / rok · {{years}}leté {{tcv}}" s1Sub = "Váš tým a PDFs, které očekáváte zpracovat každý rok." s1Title = "Kolik toho budete zpracovávat?" @@ -7623,6 +8496,12 @@ s2Title = "Závazek a služba" s3Sub = "Pro nabídku a smlouvu, kterou vygeneruje." s3Title = "Vaše údaje" serviceLevel = "Úroveň služby" +size_compact = "Kompaktní" +size_compactSub = "Převážně text, méně než 1 MB" +size_heavy = "Velké" +size_heavySub = "Skenované nebo s velkým množstvím obrázků, 5 MB+" +size_standard = "Standardní" +size_standardSub = "Smíšený text a obrázky, 1 až 5 MB" slDedicated = "Dedikovaná" slDedicatedSub = "4 pracovní hodiny · dedikovaný account manager · +30 %" slPriority = "Prioritní" @@ -7630,6 +8509,8 @@ slPrioritySub = "Stejný pracovní den · jmenovaný CSM · +15 %" slStandard = "Standardní" slStandardSub = "Následující pracovní den · sdílený CSM · zahrnuto" stepOf = "Krok {{n}} z {{total}}" +taxId = "DIČ / Tax ID" +taxIdPlaceholder = "Volitelné" term = "Období" termDiscount = "Uplatněna sleva {{pct}} % za víceletý závazek" title = "Sestavte svou nabídku" @@ -7637,6 +8518,7 @@ training = "Onboarding a školení" trainingSub = "Živé relace, aby se váš tým rychle rozběhl" users = "Celkový počet uživatelů" usersPlaceholder = "např. 250" +viewEula = "Přečíst" volEstimated = "Odhadnuto z {{count}} uživatelů (~2 000 PDFs na každého, včetně automatizace). Upravte, pokud máte přesnější údaj." volManual = "Používá se váš údaj. Kdykoli můžete znovu odhadnout podle velikosti týmu." volNoUsers = "Nejste si jisti? Zadejte velikost týmu a my to odhadneme." @@ -7645,55 +8527,55 @@ volumePlaceholder = "např. 1 000 000" years_one = "{{count}} rok" years_other = "{{count}} let" -[portal.procurement.docs] -count_one = "{{count}} dok." -count_other = "{{count}} dok." -done = "Hotovo" -here = "Jste zde" -hide = "Skrýt" -optional = "Volitelné" -paidAddon = "Placený doplněk" -show = "Zobrazit" -subtitle = "Vše, co potřebujete v každém kroku cesty, zobrazené podle toho, jak se dohoda posouvá." -supportingSubtitle = "SOC 2, bezpečnostní prověrky, daňové formuláře a další, připravené, když se váš bezpečnostní nebo nákupní tým zeptá. Některé mají jednorázový poplatek." -supportingTitle = "Podpora vašeho vyhodnocení" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form a DPA v jednom podpisu." +download = "Stáhnout" +eula = "EULA a obchodní podmínky" +eulaSub = "Software a samoobslužné obchodní podmínky." +invoice = "Faktura" +invoiceSub = "Vaše první faktura za předplatné." +laterInvoice = "Dostupné po přijetí" +laterQuote = "Dostupné po vystavení vaší nabídky" +quote = "Nabídka" +quoteSub = "Vaše položková enterprise nabídka." +sla = "SLA příloha" +slaSub = "Úrovně podpory a cílové doby odezvy." +subprocessors = "Subprocesory" +subprocessorsSub = "Třetí strany, které zpracovávají data vaším jménem." +subtitle = "Vaše dokumentace k obchodu, dostupná kdykoli." title = "Dokumenty" -upcoming = "Nadcházející" +view = "Zobrazit" [portal.procurement.error] title = "Něco se pokazilo" [portal.procurement.hero] -company = "Vaše enterprise dohoda" +barAria = "Fáze {{current}} z {{total}}" ctaAgreement = "Zkontrolovat a podepsat smlouvu" -ctaLive = "Jste live" -ctaPayment = "Přidat platbu" +ctaExploring = "Nastavit zkušební verzi" ctaQuote = "Zkontrolovat nabídku" +ctaReviewQuote = "Zkontrolovat nabídku" ctaTrial = "Sestavit nabídku" +documents = "Dokumenty" eyebrow = "Enterprise pořízení" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Pozvat členy týmu" -keyDocs = "Klíčové dokumenty" -nextStep = "Další krok: {{action}}" -notStarted = "Nezahájeno" +licenseKey = "Licenční klíč" +liveSub = "Vaše organizace je zprovozněna a vaše licence je aktivní." +liveTitle = "Jste živě na Stirling Enterprise" +next = "Další: {{stage}}" open = "Otevřít pořízení" scheduleCall = "Naplánovat hovor" -setup1Sub = "Pozvěte členy svého týmu" -setup1Title = "Nasadit PDF Editor" -setup2Sub = "Zapněte zpracování napříč editory a dalšími zdroji" -setup2Title = "Připojit PDF Processor" -setup3Sub = "Zapněte zabezpečení, soulad, směrování nebo uchovávání, když je potřebujete" -setup3Title = "Přidat doporučené zásady" +sentenceAgreement = "zkontrolujte a podepište dohodu" +sentenceLive = "zprovoznění a nasazení" +sentencePayment = "objednávka a platba" +sentenceQuote = "zkontrolujte a přijměte svou nabídku" +sentenceTrial = "nastavte svůj zkušební pracovní prostor" [portal.procurement.journey] daysLeft_one = "Zbývá {{count}} den" daysLeft_other = "Zbývá {{count}} dní" -engineerLabel = "Váš solutions engineer" -eyebrow = "Vaše zavádění" -live = "Jste live na Stirling Enterprise" -nextStep = "Další krok: {{action}}" -subtitle = "Váš solutions engineer je u každého kroku. Vždy jedna další akce; úplný kontrolní seznam je níže." -title = "Od trialu po live provoz, jedna vedená cesta" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "Jeden podpis pokrývá MSA, objednávkový formulář, EULA a DPA." @@ -7727,6 +8609,9 @@ downloadError = "Offline licenční soubor zatím nešlo vygenerovat — zkuste downloadOffline = "Stáhnout offline licenci (.lic)" hint = "Vložte tento klíč do self-hosted instance pro její aktivaci, nebo si ho ponechte pro své záznamy. Uchovejte ho v bezpečí." label = "Váš licenční klíč" +subtitle = "Aktivujte self-hosted instanci tímto klíčem nebo si jej ponechte pro své záznamy." +title = "Váš licenční klíč" +trialFileHint = "Toto je váš soubor zkušební licence. Jakmile bude vaše dohoda uzavřena, vraťte se a stáhněte si jej znovu — na rozdíl od online licenčního klíče se soubor .lic sám neaktualizuje." [portal.procurement.link] cta = "Propojit účet" @@ -7739,65 +8624,62 @@ description = "Vaše předplatné je aktivní a licence byla vydána. Váš tým eyebrow = "Live" title = "Jste live na Stirling Enterprise" -[portal.procurement.locked] -description = "Trial klíče, nabídky se závazným objemem, smlouva s jedním podpisem, platba a váš registr dokumentů budou všechny zde, jakmile zahájíte enterprise vyhodnocení." -eyebrow = "Pouze Enterprise" -talkToSales = "Kontaktovat prodej" -title = "Proces pořízení se otevírá s Enterprise" - [portal.procurement.milestone] -accept = "Přijmout a pokračovat" -description = "Stáhněte PDF a sdílejte ho se svým týmem, vraťte se přijmout nabídku, až budete připraveni, nebo proveďte změny." -download = "Stáhnout PDF" downloadError = "PDF nabídky zatím nešlo stáhnout — zkuste to prosím za chvíli znovu." -edit = "Upravit nabídku" -eyebrow = "Nabídka {{number}}" -perYear = " / rok" -preparedFor = "Připraveno pro {{company}}" -tcv = "{{value}} celková hodnota smlouvy" -title = "Vaše nabídka je připravena" - -[portal.procurement.modal] -cancel = "Zrušit" -chooseFile = "Vybrat soubor" -close = "Zavřít" -downloadBody = "Stahování brzy začne." -downloadCta = "Stáhnout" -downloadTitle = "Stáhnout" -noFile = "Není vybrán žádný soubor" -payBody = "Zaplaťte svůj závazný kontrakt kartou nebo bankovním převodem přes Stripe. Váš workspace se zřídí, jakmile bude platba připsána." -payCta = "Pokračovat na Stripe" -payTitle = "Potvrdit platbu" -requestBodyFree = "Vygenerujeme to na vyžádání. Potvrďte a váš solutions engineer vám to brzy pošle." -requestBodyPaid = "Toto je placený doplněk. Potvrďte a váš solutions engineer ho nacení a pošle dokumenty." -requestCta = "Vyžádat" -requestTitle = "Vyžádat tento dokument" -signBody = "Otevře Stirling Enterprise Agreement pro e-podpis: jeden podpis pokrývá MSA, objednávkový formulář, EULA a DPA. Automaticky připojíme podpis za nás a postoupíte k platbě." -signCta = "Otevřít k podpisu" -signTitle = "Zkontrolujte a podepište svou smlouvu" -uploadBody = "Pošlete nám svou PO a vystavíme fakturu podle vašich podmínek. Přetáhněte PDF nebo vyberte soubor níže." -uploadCta = "Nahrát nákupní objednávku" -uploadTitle = "Nahrát nákupní objednávku" [portal.procurement.payment] description = "Vaše nabídka je přijata a vaše licence je už aktivní — váš tým může začít hned. Zaplaťte první fakturu, až budete připraveni; můžete ji zaplatit nebo stáhnout zde, bez nutnosti e-mailu." +downloadAgreement = "Stáhnout podepsanou dohodu" downloadInvoice = "Stáhnout fakturu" -simulate = "Simulovat přijetí platby (demo)" +eyebrow = "Platba" title = "Předplatné vytvořeno" viewInvoice = "Zobrazit a zaplatit fakturu" +[portal.procurement.review] +acceptCta = "Přijmout nabídku" +annual = "Roční poplatek (rok 1)" +downloadCta = "Stáhnout nabídku" +poNumber = "Objednávka: {{po}}" +renewal = "Obnovuje se za {{amount}}/rok po skončení období (+{{pct}} % CPI)" +tcv = "{{years}}leté období, zaplaceno celé předem: {{tcv}}" +validUntil = "Platné do {{date}}" + [portal.procurement.schedule] fallback = "Plánovač se nepodařilo načíst." fallbackLink = "Otevřít plánování v nové kartě" +loading = "Načítání plánovače..." subtitle = "Váš solutions engineer provede váš tým zavedením. Vyberte čas, který vám vyhovuje." title = "Naplánovat hovor" -[portal.procurement.status] -action = "Vyžadována akce" -available = "Dostupné" -complete = "Dokončeno" -pending = "Čeká" -request = "Na vyžádání" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Uzavřené, bez odchozí komunikace" +back = "Zpět" +businessName = "Název firmy" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Spravované, nejrychlejší start" +continue = "Pokračovat" +deployment = "Kam se má nasadit?" +eula = "Souhlasím se Stirling EULA a obchodními podmínkami." +fullName = "Celé jméno" +fullNamePlaceholder = "Vaše jméno" +invites = "Pozvat spolupracovníky · volitelné" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Naplánovat hovor" +seats = "Testující osoby" +seatsPlaceholder = "např. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker nebo Kubernetes" +start = "Spustit zkušební verzi" +stepOf = "Krok {{n}} z {{total}}" +subtitle = "Zdarma na 14 dní, karta není potřeba." +subtitleDetails = "Několik údajů pro vaši nabídku a dohodu." +talkFirst = "Chcete si nejdřív promluvit?" +title = "Nastavte svou zkušební verzi" +viewEula = "Přečíst" +workEmail = "Pracovní e-mail" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Prodloužení přidá 7 dní a upozorní vašeho solutions engineera." @@ -7808,109 +8690,113 @@ maxed = "Vyčerpáno" subtitle = "Váš bezplatný trial běží do {{date}}. Karta není vyžadována." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Ceny podle závazného objemu, SSO + SCIM + RBAC v celé organizaci, 90denní neměnný audit a dedikovaný SE." -homeCta = "Spustit Trial →" -homeHeadline = "Zpracovávejte miliony PDFs." - -[portal.recentActivity] -title = "Nedávná aktivita" -viewAll = "Zobrazit vše" - -[portal.recentActivity.empty] -description = "Zde se zobrazí běhy pipeline, nasazení a události agentů." -title = "Zatím zde nic není" - -[portal.search] -ariaLabel = "Hledat" -placeholder = "Hledat ve Stirling — endpointy, pipeline, dokumentace…" - -[portal.search.empty] -noActionsDescription = "Rychlé akce se zde zobrazí, jakmile budou dostupné." -noActionsTitle = "Žádné rychlé akce" -noMatches = "Žádné shody pro „{{query}}“" -noMatchesDescription = "Zkuste jiné klíčové slovo nebo procházejte katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Propojení účtu" -[portal.shell.header] -accountFallback = "Účet" -accountMenu = "Nabídka účtu" -darkMode = "Tmavý režim" -lightMode = "Světlý režim" -search = "Hledat" -searchPlaceholder = "Hledat…" -signOut = "Odhlásit se" -switchToDark = "Přepnout na tmavé téma" -switchToLight = "Přepnout na světlé téma" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dok." linkAccount = "Propojit účet Stirling" -planEditor = "Plán Editor" -planEnterprise = "Plán Enterprise" -planProcessor = "Plán Processor" primaryNav = "Primární navigace" switchApp = "Přepnout aplikaci" +[portal.shell.topbar] +closeNav = "Zavřít navigaci" +openNav = "Otevřít navigaci" +search = "Hledat" + [portal.sources] subtitle = "Opakovaně použitelná vstupní připojení, která posílají dokumenty do Stirling. Nakonfigurujte připojení jednou a poté ho použijte v libovolném počtu zásad. Kliknutím na řádek zobrazíte jeho konfiguraci a zásady, které ho používají." title = "Zdroje" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Připojit zdroj" +[portal.sources.builder] +back = "Zpět na zdroje" +backToTypes = "Všechny typy zdrojů" +cancel = "Zrušit" +chooseHint = "Vyberte, odkud dokumenty pocházejí. Zašedlé konektory jsou na cestě." +comingSoon = "Již brzy" +comingSoonHeading = "Již brzy" +create = "Vytvořit zdroj" +createTitle = "Připojit zdroj" +delete = "Smazat" +editTitle = "Upravit zdroj" +enabled = "Povoleno" +save = "Uložit změny" + +[portal.sources.builder.folderAccess] +description = "Automatizace složek mohou používat pouze složky, které povolil správce. Přidejte ji v nastavení přístupu ke složkám a zkuste to znovu." +openSettings = "Nastavení přístupu ke složkám" +title = "Tato složka není povolena" + [portal.sources.delete] body = "Odstranit „{{name}}“? Tuto akci nelze vrátit zpět. Zásady, které na něj odkazují, bude potřeba aktualizovat." cancel = "Zrušit" confirm = "Odstranit" title = "Odstranit zdroj?" -[portal.sources.detail] -closeAriaLabel = "Zavřít detail" -delete = "Odstranit zdroj" -docs24h = "Posledních 24 h" -docs30d = "Posledních 30 dní" -docsTotal = "Celkem viděno" -docsTrend = "Dokumenty za posledních 30 dní" -documents = "Dokumenty" -edit = "Upravit" -notReferenced = "Neodkazuje na něj žádná zásada, takže je bezpečné ho odstranit." -pause = "Pozastavit" -resume = "Obnovit" -subtitle = "{{type}} · {{status}}" -usedBy = "Používáno v" - -[portal.sources.empty] -description = "Připojte složku (a brzy i cloudové úložiště), aby vaše zásady měly odkud dokumenty načítat." -title = "Zatím nejsou připojeny žádné zdroje" - [portal.sources.kpi] inUse = "Používá se" total = "Připojení" unused = "Nepoužito" +[portal.sources.networkFields.connection] +helperText = "Uložené připojení s adresou serveru a přihlašovacími údaji. Znovu se používá každým zdrojem, který na něj odkazuje." +label = "Připojení" + +[portal.sources.networkFields.directory] +helperText = "Složka na serveru ke kontrole, relativní k domovské složce přihlášení nebo kořeni sdílení. Pro kořen ponechte prázdné." +label = "Složka" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume odstraní každý soubor ze serveru poté, co jej zpracuje každá zásada." +label = "Režim čtení" + +[portal.sources.networkFields.mode.options] +consume = "Consume: zpracovat každý soubor jednou" +snapshot = "Snapshot: znovu načíst složku při každém spuštění" + +[portal.sources.networkFields.recursive] +label = "Hloubka složky" + +[portal.sources.networkFields.recursive.options] +all = "Zahrnout podsložky" +top = "Pouze nejvyšší úroveň" + [portal.sources.status] active = "Aktivní" disabled = "Zakázáno" unused = "Nepoužito" [portal.sources.table] +documents = "Dokumenty" source = "Zdroj" status = "Stav" +type = "Type" usedBy = "Zásady" +[portal.sources.types.box] +description = "Načítejte dokumenty ze složek Box, které váš tým již sdílí." +label = "Box" + +[portal.sources.types.dropbox] +description = "Načítejte dokumenty ze sdílené složky Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenty, které váš tým zpracoval v editoru, napříč běhy zásad a AI." label = "Editor" +noPolicies = "Z editoru zatím neběžely žádné zásady." + +[portal.sources.types.email] +description = "Převádějte přílohy odeslané do vyhrazené schránky na dokumenty." +label = "E-mailová schránka" [portal.sources.types.folder] description = "Sledovat adresář na serveru pro nové dokumenty." @@ -7943,25 +8829,80 @@ label = "Hloubka složky" all = "Zahrnout podsložky" top = "Pouze nejvyšší úroveň" +[portal.sources.types.ftp] +description = "Kontrolujte složku FTP nebo FTPS serveru na nové dokumenty." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Načítejte dokumenty ze sdílených složek Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Sledujte SMB síťové sdílení pro nové dokumenty." +label = "Síťový disk" + +[portal.sources.types.onedrive] +description = "Sledujte složky OneDrive pro nové dokumenty." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Načítejte dokumenty z bucketu Amazon S3 nebo S3-kompatibilního bucketu." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Uložené připojení obsahující bucket a přihlašovací údaje. Znovu se používá každým zdrojem a výstupem pipeline, který na něj odkazuje." +label = "Připojení" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume odstraní každý objekt z bucketu poté, co jej zpracuje každá zásada." +label = "Režim čtení" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: zpracovat každý objekt jednou" +snapshot = "Snapshot: znovu načíst bucket při každém spuštění" + +[portal.sources.types.s3.fields.prefix] +helperText = "Zpracují se pouze objekty, jejichž klíče začínají tímto prefixem." +label = "Prefix klíče" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Kontrolujte SFTP drop složku na nové dokumenty." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Načítejte dokumenty z knihoven dokumentů SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Zdroj" +[portal.sources.types.webhook] +createNote = "Delivery URL a podpisový tajný klíč se vygenerují při vytvoření tohoto webhook. Odesílatelé POST dokumenty na tuto URL a odkazující zásady se spustí při přijetí." +description = "Přijímejte dokumenty podepsaným HTTP POST z libovolného externího systému." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Podpisový tajný klíč se zobrazí jednou, při vytvoření webhook. Pro jeho obměnu zdroj znovu vytvořte." + +[portal.sources.types.webhook.reveal] +copy = "Kopírovat" +done = "Hotovo" +secret = "Podpisový tajný klíč" +secretHelp = "Podepište nezpracované tělo každého doručení tímto klíčem (HMAC-SHA256) a odešlete jej jako hlavičku X-Stirling-Signature." +secretWarning = "Zkopírujte podpisový tajný klíč nyní. Kvůli vaší bezpečnosti se zobrazí pouze jednou a později jej nelze získat." +title = "Webhook vytvořen" +url = "Delivery URL" +usage = "Odešlete každý dokument metodou POST jako nezpracované tělo požadavku na delivery URL s binárním typem obsahu (application/pdf nebo application/octet-stream). Odkazující zásady se při přijetí spustí automaticky." + [portal.sources.wizard] -back = "Zpět" -cancel = "Zrušit" -continue = "Pokračovat" -editTitle = "Upravit zdroj" name = "Název" namePlaceholder = "např. Příjem škodních událostí" -save = "Uložit změny" -subtitle = "Krok {{current}} z {{total}} · {{label}}" -title = "Připojit zdroj" type = "Typ" -[portal.sources.wizard.steps] -chooseType = "Vybrat typ" -configure = "Konfigurovat" -review = "Zkontrolovat a připojit" +[portal.stepModal] +close = "Zavřít" [portal.usage] managePayment = "Spravovat platbu" @@ -8021,16 +8962,9 @@ summary = "Vlastní tým — spravuje prostředky jeho členů a sdílené konfi 2 = "Přístup k portálu přes výchozí zásadu" 3 = "Vše, co může člen" -[portal.welcome] -ariaLabel = "Vítejte ve Stirling PDF" -badge = "Open-source" -installEditor = "Nainstalovat Editor" -inviteTeammates = "Pozvat členy týmu" -openInBrowser = "Otevřít v prohlížeči" -perks = "Navždy zdarma · Self-hostable" -subtitle = "Nejbezpečnější PDF Editor na světě je zdarma pro týmy všech velikostí. Zahrnuje 60+ PDF operací a SSO." -title = "Vítejte v" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Neomezeně" [printFile] title = "Tisk souboru" @@ -8613,6 +9547,14 @@ text = "Otočte stránky PDF po nebo proti směru hodinových ručiček v krocí [rotate.tooltip.header] title = "Přehled nastavení otáčení" +[ruler] +clearAll = "Vymazat vše" +hideAllLabels = "Skrýt všechny popisky" +hideSmallLabels = "Skrýt malé popisky" +physicalValues = "Fyzické" +scaled = "Škálované" +showAllLabels = "Zobrazit všechny popisky" + [sanitize] desc = "Odstraňte potenciálně škodlivé prvky ze souborů PDF." sanitizationResults = "Výsledky vyčištění" @@ -8654,31 +9596,45 @@ label = "Odstranit metadata XMP" [sanitize.steps] settings = "Nastavení" -[sanitizePdf] -tags = "vyčistit,zabezpečit,bezpečný,odstranit-hrozby" - [sanitizePDF] title = "Sanitizovat PDF" +[sanitizePdf] +tags = "vyčistit,zabezpečit,bezpečný,odstranit-hrozby" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Upravit měřítko stránky" +[scaleSettings] +activeScale = "Aktivní měřítko" +apply = "Použít měřítko" +applyCalibration = "Použít kalibraci" +calculatedScale = "Vypočtené měřítko" +calibrate = "Kalibrovat" +calibrating = "Kalibrace" +calibrationDistanceRequired = "Zadejte skutečnou vzdálenost větší než nula" +calibrationInvalid = "Z tohoto měření nelze vypočítat měřítko" +calibrationPaperDistance = "Naměřená vzdálenost na stránce: {{distance}}" +calibrationTitle = "Kalibrovat měřítko" +calibrationTooltip = "Změřte známou vzdálenost pro automatický výpočet měřítka" +customScale = "Vlastní měřítko" +noneSet = "Není nastaveno žádné vlastní měřítko" +presets = "Přednastavená měřítka" +ratio = "Poměr měřítka" +ratioHelp = "Poměr: 1 jednotka stránky = X skutečných jednotek" +ratioPlaceholder = "např. 100" +ratioPositive = "Poměr musí být větší než nula" +ratioRequired = "Poměr je povinný" +realDistance = "Skutečná vzdálenost" +realDistancePlaceholder = "např. 5" +reset = "Obnovit výchozí nastavení" +unit = "Jednotka" +unitInvalid = "Neplatná jednotka: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Prahová hodnota úhlu:" -10 = "Nastaví velikost okraje přidaného a odebraného k zabránění bílých okrajů ve výstupu (výchozí: 1)." -2 = "Nastaví minimální absolutní úhel požadovaný pro otočení obrázku (výchozí: 10)." -3 = "Tolerance:" -4 = "Určuje rozsah barevné variace kolem odhadované barvy pozadí (výchozí: 30)." -5 = "Minimální plocha:" -6 = "Nastaví minimální prahovou hodnotu plochy pro fotografii (výchozí: 10000)." -7 = "Minimální plocha obrysu:" -8 = "Nastaví minimální prahovou hodnotu plochy obrysu pro fotografii" -9 = "Velikost okraje:" - [scannerImageSplit] submit = "Extrahovat obrazové skeny" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Co to dělá" whatThisDoesDesc = "Automaticky najde a vyextrahuje každou fotografii ze skenované stránky nebo složeného obrázku — bez ručního ořezávání." whenToUse = "Kdy použít" +[ScannerImageSplit.selectText] +1 = "Prahová hodnota úhlu:" +10 = "Nastaví velikost okraje přidaného a odebraného k zabránění bílých okrajů ve výstupu (výchozí: 1)." +2 = "Nastaví minimální absolutní úhel požadovaný pro otočení obrázku (výchozí: 10)." +3 = "Tolerance:" +4 = "Určuje rozsah barevné variace kolem odhadované barvy pozadí (výchozí: 30)." +5 = "Minimální plocha:" +6 = "Nastaví minimální prahovou hodnotu plochy pro fotografii (výchozí: 10000)." +7 = "Minimální plocha obrysu:" +8 = "Nastaví minimální prahovou hodnotu plochy obrysu pro fotografii" +9 = "Velikost okraje:" + [search] noResults = "Nebyly nalezeny žádné výsledky" placeholder = "Zadejte hledaný výraz..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Váš server Stirling-PDF je offline a \"{{endpoint}} [session] expired = "Vaše relace vypršela. Obnovte prosím stránku a zkuste to znovu." -[sessionManagement.tooltip] -header = "Správa relací podepisování" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Noví účastníci jsou přidáni na konec pořadí podpisů" -bullet2 = "Po dokončení relace nelze účastníky přidávat" -bullet3 = "Každý účastník dostane oznámení, když je na řadě" -description = "Do aktivní relace můžete kdykoli před dokončením přidat další účastníky." -title = "Přidávání účastníků" - -[sessionManagement.tooltip.finalization] -bullet1 = "Plné dokončení: Všichni účastníci podepsali" -bullet2 = "Částečné dokončení: Někteří účastníci ještě nepodepsali" -bullet3 = "Nepodepsaní účastníci budou z finálního dokumentu vynecháni" -bullet4 = "Po dokončení můžete načíst podepsané PDF mezi aktivní soubory" -description = "Dokončení sloučí všechny podpisy do jednoho podepsaného PDF. Tuto akci nelze vrátit." -title = "Dokončení relace" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Nelze odebrat účastníky, kteří již podepsali" -bullet2 = "Odebraní účastníci již nebudou dostávat oznámení" -bullet3 = "Pořadí podpisů se automaticky upraví" -description = "Účastníky lze z relací odebrat předtím, než podepíší." -title = "Odebírání účastníků" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Každý podpis je na PDF aplikován postupně" -bullet2 = "Pozdější podepisující vidí dřívější podpisy" -bullet3 = "Zásadní pro schvalovací procesy a právní řetězce předání" -description = "Pořadí, které určíte při vytvoření relace, určuje, kdo podepisuje jako první." -title = "Pořadí podpisů" - [settings] close = "Zavřít" +[settings.ai] +documents = "Dokumenty a RAG" +general = "Obecné" +limits = "Limity a výkon" +models = "Modely a poskytovatelé" +title = "AI" + [settings.configuration] advanced = "Pokročilé" database = "Databáze" endpoints = "Endpoints" features = "Funkce" +folderAccess = "Přístup ke složkám" mcp = "MCP Server" storageSharing = "Ukládání souborů a sdílení" systemSettings = "Systémová nastavení" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Výchozí editor PDF" defaultPdfEditorActive = "Stirling PDF je váš výchozí editor PDF" defaultPdfEditorChecking = "Kontrola..." defaultPdfEditorInactive = "Jiná aplikace je nastavena jako výchozí" +defaultPdfEditorRemind = "Připomenout nastavení jako výchozí" +defaultPdfEditorRemindDescription = "Zobrazit banner, když Stirling PDF není vaše výchozí aplikace pro PDF." defaultPdfEditorSet = "Již výchozí" defaultStartupView = "Výchozí zobrazení při spuštění" defaultStartupViewDescription = "Zvolte, která karta bude při spuštění aplikace aktivní v levém sloupci" @@ -8817,9 +9763,18 @@ languageDescription = "Zvolte zobrazovaný jazyk" logout = "Odhlásit se" setAsDefault = "Nastavit jako výchozí" theme = "Motiv" +themeAccent = "Barva zvýraznění" +themeAccentCustom = "Vlastní barva" +themeAccentCustomHint = "Vlastní (doporučeno výchozí)" +themeAccentDark = "Barva zvýraznění tmavého režimu" +themeAccentDefault = "Výchozí" +themeAccentDefaultHint = "Výchozí motiv (doporučeno)" +themeAccentDescription = "Tlačítka, odkazy a zvýraznění ji používají a jemně zabarvuje aplikaci. Světlý a tmavý režim mají každý svou vlastní. Systém použije tu, která je aktivní." +themeAccentLight = "Barva zvýraznění světlého režimu" themeDark = "Tmavý" themeDescription = "Přepínejte mezi světlým a tmavým režimem" themeLight = "Světlý" +themeReset = "Obnovit motiv na výchozí" themeSystem = "System" title = "Obecné" user = "Uživatel" @@ -8833,6 +9788,12 @@ intro = "Povolte ověřování uživatelů, správu týmů a funkce pracovního learnMore = "Více v dokumentaci" title = "Pro správce systému" +[settings.general.loginLanding] +description = "Vyberte, která aplikace se otevře po přihlášení do Stirling Cloud." +editor = "Editor" +processor = "Procesor" +title = "Po přihlášení" + [settings.general.mode] fullscreen = "Celá obrazovka" sidebar = "Postranní panel" @@ -9013,9 +9974,6 @@ title = "Zásady a soukromí" [settings.preferences] title = "Předvolby" -[settings.search] -placeholder = "Hledat na stránkách nastavení..." - [settings.security] description = "Update your password to keep your account secure." title = "Security" @@ -9041,6 +9999,11 @@ security = "Zabezpečení" telegram = "Telegram" title = "Zabezpečení a ověřování" +[settings.signOut] +confirm = "Opravdu se chcete odhlásit?" +submit = "Odhlásit se" +title = "Odhlásit se" + [settings.team] title = "Tým" @@ -9156,6 +10119,7 @@ last = "Poslední stránka" maintainRatio = "Přepnout zachování poměru stran" next = "Další stránka" noSavedSigs = "Nebyly nalezeny žádné uložené podpisy" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Osobní podpisy" previous = "Předchozí stránka" redo = "Znovu" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Background removal failed" hint = "Nahrajte PNG nebo JPG s vaším podpisem" label = "Nahrát obrázek podpisu" placeholder = "Vyberte obrazový soubor" +previewAlt = "Current image signature" processing = "Processing image..." removeBackground = "Remove white background (make transparent)" @@ -9207,6 +10172,17 @@ saved = "Vyberte výše uložený podpis a poté klikněte kamkoli do PDF pro je text = "Po zadání jména výše klikněte kamkoli do PDF pro umístění podpisu." title = "Jak přidat podpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Přesunout podpis" pause = "Pozastavit umisťování" @@ -9653,10 +10629,30 @@ title = "Nahrát na server" updateButton = "Aktualizovat na serveru" uploadButton = "Nahrát na server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klikněte sem" +fallback = "Pokud se neotevřelo automaticky," +opening = "Otevírání Swagger UI v nové kartě..." tags = "api,dokumentace,swagger,endpointy,vývoj" title = "API Dokumentace" @@ -9764,12 +10760,39 @@ selectFilesHint = "Vyberte soubory v Aktivních souborech pro spuštění tohoto singleFileScope = "Použije se pouze pro: {{fileName}}" viewerMode = "Chcete-li vybrat více souborů, přepněte do editoru souborů." +[toolFormat] +ANY = "libovolný soubor" +CBR = "archiv komiksu (CBR)" +CBZ = "archiv komiksu (CBZ)" +CSV = "soubor CSV" +EBOOK = "e-kniha" +EMAIL = "e-mail" +EXCEL = "tabulka" +HTML = "webová stránka" +IMAGE = "obrázky" +JAVASCRIPT = "soubor JavaScript" +JSON = "soubor JSON" +MARKDOWN = "soubor Markdown" +NONE = "žádný soubor" +PCL = "tiskový soubor PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF chráněný heslem" +POSTSCRIPT = "soubor PostScript" +PPT = "prezentace" +TEXT = "textový soubor" +VIDEO = "video" +WORD = "dokument Word" +XML = "soubor XML" +XPS = "dokument XPS" +ZIP = "archiv ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Zpět na všechny nástroje" collapse = "Sbalit panel" expand = "Rozbalit panel" goBack = "Vrátit se" +pdfTools = "Nástroje PDF" placeholder = "Vyberte nástroj pro začátek" premiumFeature = "Prémiová funkce:" toolsHeader = "Nástroje" @@ -9806,11 +10829,6 @@ noToolsFound = "Nebyly nalezeny žádné nástroje" quickAccess = "RYCHLÝ PŘÍSTUP" searchPlaceholder = "Hledat nástroje..." -[toolPicker.categories] -advancedTools = "Pokročilé nástroje" -recommendedTools = "Doporučené nástroje" -standardTools = "Standardní nástroje" - [toolPicker.subcategories] advancedFormatting = "Pokročilé formátování" automation = "Automatizace" @@ -9824,6 +10842,9 @@ removal = "Odstranění" signing = "Podepisování" verification = "Ověření" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nebyly nalezeny žádné nástroje" noTools = "Žádné dostupné nástroje" @@ -9950,28 +10971,53 @@ title = "Uživatelé" you = "(vy)" [users.action] +cancelInvite = "Zrušit pozvánku" deleteTeam = "Odstranit tým" disableMfa = "Resetovat MFA" move = "Přesunout do týmu" reinstate = "Obnovit" remove = "Odebrat z org." +removeTeam = "Odebrat z týmu" rename = "Přejmenovat tým" resetPw = "Resetovat heslo" suspend = "Pozastavit" unlock = "Odemknout účet" +[users.activity] +daysAgo = "před {{count}} d" +hoursAgo = "před {{count}} h" +justNow = "Právě teď" +minutesAgo = "před {{count}} min" +monthsAgo_one = "před {{count}} měsícem" +monthsAgo_other = "před {{count}} měsíci" +never = "Nikdy" +weeksAgo_one = "před {{count}} týdnem" +weeksAgo_other = "před {{count}} týdny" +yearsAgo_one = "před {{count}} rokem" +yearsAgo_other = "před {{count}} lety" + [users.cap] addProcessor = "+ Processor" approver = "Schvaluje zásadu" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Zrušit pozvánku pro {{email}}? S aktuálním odkazem se nebudou moci připojit." +cancelInviteTitle = "Zrušit pozvánku" deleteTeamBody = "Odstranit tým {{name}}? Tým musí být nejprve prázdný – přesuňte jeho členy do jiného týmu a nesmí stále vlastnit žádné integrační konfigurace." deleteTeamTitle = "Odstranit tým" disableMfaBody = "Odebrat MFA registraci uživatele {{name}}? Pokud je vyžadována, nastaví ji znovu při příštím přihlášení." disableMfaTitle = "Resetovat MFA" removeBody = "Trvale odebrat {{name}} z organizace? Tuto akci nelze vrátit zpět." +removeTeamBody = "Odebrat {{name}} z týmu? Účet jim zůstane, ale ztratí přístup ke zdrojům tohoto týmu." removeTitle = "Odebrat člena" [users.empty] @@ -9982,10 +11028,8 @@ title = "Zatím žádní členové" addToTeam = "Přidat do týmu" guestCount = "{{count}} host" guests = "Hosté" -guestsDesc = "Externí spolupracovníci, omezení na to, co jste sdíleli. Pouze Editor." ledBy = "vedeno uživatelem {{owner}}" org = "Organizace" -orgDesc = "Vlastníci s pravomocí v celé org. a schvalováním zásad" owners = "{{count}} vlastník" team = "Tým {{name}}" teamMeta = "{{count}} lidí" @@ -10022,6 +11066,18 @@ username = "Uživatelské jméno" usernameError = "Uživatelské jméno musí mít alespoň 3 znaky" usernamePlaceholder = "jsmith" +[users.invites] +by = "Pozval(a) {{who}}" +cancel = "Zrušit" +expiresInDays_one = "Vyprší za {{count}} den" +expiresInDays_other = "Vyprší za {{count}} dní" +expiresToday = "Vyprší dnes" +title = "Čekající pozvánky" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Při spojení s backendem došlo k chybě, nebo nemáte přístup. Zkuste to znovu." title = "Členy se nepodařilo načíst" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Hledat přílohy" title = "Přílohy" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Název záložky" +bookmarkTitleRequired = "Název záložky je povinný" closeSidebar = "Zavřít postranní panel záložek" collapseAll = "Sbalit všechny záložky" +empty = "No bookmarks in this document" expandAll = "Rozbalit všechny záložky" +searchPlaceholder = "Hledat záložky" +title = "Bookmarks" [viewer.comments] addComment = "Přidat komentář" @@ -10236,10 +11297,12 @@ locateAnnotation = "Najít v dokumentu" moreActions = "Další akce" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Stránka {{page}}" placingHint = "Klikněte na stránku pro umístění… (zrušit)" removeCommentOnly = "Odebrat pouze komentář" saveReply = "Uložit odpověď" +searchPlaceholder = "Search comments" title = "Komentáře" typeComment = "Komentář" typeInsertText = "Vložit text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Máte neuložené změny" closeSidebar = "Zavřít postranní panel vrstev" hideAll = "Skrýt všechny vrstvy" showAll = "Zobrazit všechny vrstvy" +title = "Layers" [viewer.link] delete = "Smazat odkaz" @@ -10605,30 +11669,6 @@ title = "Text vodoznaku" image = "Obrázek" text = "Text" -[wetSignature.tooltip] -header = "Způsoby vytvoření podpisu" - -[wetSignature.tooltip.draw] -bullet1 = "Přizpůsobte barvu a tloušťku pera" -bullet2 = "Vymažte a nakreslete znovu, dokud nebudete spokojeni" -bullet3 = "Funguje na dotykových zařízeních (tablety, telefony)" -description = "Vytvořte vlastnoruční podpis pomocí myši nebo dotykové obrazovky. Nejvhodnější pro osobní, autentické podpisy." -title = "Nakreslit podpis" - -[wetSignature.tooltip.type] -bullet1 = "Vyberte si z několika písem" -bullet2 = "Přizpůsobte velikost a barvu textu" -bullet3 = "Ideální pro standardizované podpisy" -description = "Vygenerujte podpis z napsaného textu. Rychlé a konzistentní, vhodné pro obchodní dokumenty." -title = "Napsat podpis" - -[wetSignature.tooltip.upload] -bullet1 = "Podporuje PNG, JPG a další obrazové formáty" -bullet2 = "Pro nejlepší výsledek se doporučuje průhledné pozadí" -bullet3 = "Obrázek bude velikostně přizpůsoben oblasti podpisu" -description = "Nahrajte předem vytvořený obrázek podpisu. Ideální, pokud máte naskenovaný podpis nebo firemní logo." -title = "Nahrát obrázek podpisu" - [workbenchBar] activeFiles = "Aktivní soubory" annotations = "Anotace" @@ -10642,6 +11682,7 @@ exitRedaction = "Opustit režim začernění" exportAll = "Exportovat PDF" exportSelected = "Exportovat vybrané stránky" formFill = "Vyplnit formulář" +hideToolbar = "Hide toolbar" multiTool = "Multinástroj" panMode = "Režim posunu" print = "Tisk PDF" @@ -10652,6 +11693,7 @@ redact = "Začernit" rotateLeft = "Otočit doleva" rotateRight = "Otočit doprava" ruler = "Pravítko / měření" +rulerSettings = "Nastavení měřítka" save = "Uložit" saveAll = "Uložit vše" saveAs = "Uložit jako" @@ -10661,6 +11703,7 @@ selectAll = "Vybrat vše" selectByNumber = "Vybrat podle čísel stránek" selectLanguage = "Vyberte jazyk" share = "Sdílet" +showToolbar = "Show toolbar" toggleAnnotations = "Přepnout viditelnost anotací" toggleAttachments = "Přepnout přílohy" toggleBookmarks = "Přepnout záložky" diff --git a/frontend/editor/public/locales/da-DK/translation.toml b/frontend/editor/public/locales/da-DK/translation.toml index e758a10c5f..90f17c7d0f 100644 --- a/frontend/editor/public/locales/da-DK/translation.toml +++ b/frontend/editor/public/locales/da-DK/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Anvend" applyAndContinue = "Gem og forlad" -areYouSure = "Er du sikker på, at du vil forlade?" back = "Tilbage" black = "Sort" blue = "Blå" @@ -46,10 +45,8 @@ filesSelected = "Filer valgt" font = "Skrifttype" fontSizeTooltip = "Størrelse på sidetalsteksten i punkter. Større tal giver større tekst." fontTypeTooltip = "Skrifttypefamilie til sidetal. Vælg efter din dokuments stil." -genericSubmit = "Indsend" green = "Grøn" help = "Hjælp" -imgPrompt = "Vælg Billede(r)" incorrectPasswordMessage = "Nuværende adgangskode er forkert." info = "Info" invalidUndoData = "Kan ikke fortryde: ugyldige handlingsdata" @@ -62,7 +59,6 @@ loginAgreementTitle = "Login-aftale" logOut = "Log ud" marginTooltip = "Afstand mellem sidetallet og sidens kant." moreOptions = "Flere muligheder" -multiPdfPrompt = "Vælg PDF-filerne (2+)" never = "Aldrig" no = "Nej" noFileSelected = "Ingen fil valgt. Upload venligst en." @@ -80,7 +76,6 @@ pageOrderPrompt = "Brugerdefineret siderækkefølge (Indtast en kommasepareret l pages = "Sideantal" pageSelectionPrompt = "Brugerdefineret sidevalg (Indtast en kommasepareret liste af sidenumre 1,5,6 eller funktioner som 2n+1) :" password = "Adgangskode" -pdfPrompt = "Vælg PDF-fil(er)" pin = "Fastgør fil (behold aktiv efter værktøjskørsel)" pro = "Pro" processingComplete = "Din fil er klar." @@ -110,6 +105,7 @@ undoSuccess = "Handling fortrudt" unknown = "Ukendt" unpin = "Frigør fil (erstat efter værktøjskørsel)" unsavedChanges = "Du har ikke-gemte ændringer i din PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Ikke-gemte ændringer" unsupported = "Ikke understøttet" username = "Brugernavn" @@ -356,6 +352,7 @@ text = "Disse tilladelser styrer, hvad brugere kan gøre med PDF'en. Mest effekt title = "Ændre Tilladelser" [addStamp] +preview = "Forhåndsvis stempel" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manuelle downloads: klik på linkene og placer filerne i tessdata noLanguages = "Ingen tessdata-sprog fundet i den konfigurerede mappe." permissionNotice = "Stien til tessdata er ikke skrivbar. Downloads åbnes i browseren; gem venligst .traineddata-filerne manuelt i tessdata-mappen." +[admin.settings.ai.documents] +description = "Konfigurer embedding-modellen og indstillingerne for hentning, der bruges til at besvare spørgsmål om dokumenter. Anvendes på AI-motoren, når der gemmes." +title = "Dokumenter og RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Lad stå tomt for at bruge motorens egne miljølegitimationsoplysninger. Gælder for selvhostede single-engine-installationer." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Gemt - lad stå tomt for at beholde den aktuelle nøgle" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL for det OpenAI-kompatible / Ollama embeddings-endpoint, f.eks. http://ollama:11434/v1. Skal pege på et betroet internt endpoint (SSRF-følsomt)." +label = "Embedding-base-URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Navn på embedding-model. Fri tekst; forslag er kun tips." +label = "Embedding-model" + +[admin.settings.ai.documents.embeddingProvider] +description = "Udbyder, der bruges til at omdanne dokumenttekst til vector embeddings." +label = "Embedding-udbyder" + +[admin.settings.ai.documents.maxSearches] +description = "Maksimalt antal hentningssøgninger, agenten må køre pr. anmodning." +label = "Maks. søgninger" + +[admin.settings.ai.documents.reindexNote] +body = "Ændring af embedding-modellen træder i kraft med det samme, men dokumenter, der er indekseret med den tidligere model, skal genindekseres, for at søgning returnerer korrekte resultater." +title = "Genindeksering påkrævet" + +[admin.settings.ai.documents.saved] +reindexNote = "Hvis du ændrede embedding-modellen, skal du genindeksere eksisterende dokumenter, så søgning bruger den nye model." + +[admin.settings.ai.documents.topK] +description = "Antal mest relevante bidder, der hentes pr. søgning." +label = "Top K" + +[admin.settings.ai.general] +description = "Forbind Stirling med Python AI-motoren, og vælg hvilke AI-funktioner der vises. Ændringer anvendes ved genstart." +title = "AI-motor" + +[admin.settings.ai.general.capabilities] +description = "Slå individuelle AI-funktioner til eller fra. Deaktiverede funktioner skjules i appen." +title = "Funktioner" + +[admin.settings.ai.general.enabled] +description = "Hovedafbryder. Når den er slået fra, er ingen AI-værktøjer, agenter eller motorkald tilgængelige." +label = "Aktivér AI" + +[admin.settings.ai.general.features.chat] +description = "Samtaleassistent til arbejde med PDFs." +label = "Chatassistent" + +[admin.settings.ai.general.features.classify] +description = "Kategoriser automatisk dokumenter efter type eller indhold." +label = "Dokumentklassificering" + +[admin.settings.ai.general.features.createPdf] +description = "Generér en ny PDF (f.eks. fra HTML) via en AI-agent." +label = "Opret PDF fra prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Stil spørgsmål, og få svar baseret på et uploadet dokument." +label = "Dokumentspørgsmål" + +[admin.settings.ai.general.features.mathAuditor] +description = "Gennemgå dokumenter for matematiske og numeriske fejl." +label = "Matematikrevisor" + +[admin.settings.ai.general.features.pdfComment] +description = "Tilføj AI-skrevne gennemgangskommentarer og annoteringer til en PDF." +label = "PDF-kommentaragent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout for tungere agentoperationer såsom dokumentgenerering." +label = "Timeout for langvarige handlinger (sekunder)" + +[admin.settings.ai.general.note] +body = "AI-motoren kører som en separat tjeneste. Dens delte hemmelighed" +body2 = "indstilles via en container-miljøvariabel. Udbyderens API keys kan indtastes på disse sider eller leveres som motorens miljøvariabler; nøgler, der indtastes her, sendes til motoren, når der gemmes." +title = "Om AI-motoren" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout for streamede (token-for-token) chatsvar." +label = "Stream-timeout (sekunder)" + +[admin.settings.ai.general.test] +button = "Test forbindelse" +failBody = "AI-motoren svarede ikke. Kontrollér URL'en, at motor-containeren kører, og at AI er aktiveret (en genstart er nødvendig efter aktivering)." +failTitle = "AI-motoren kan ikke nås" +okBody = "AI-motoren svarede på et sundhedstjek." +okTitle = "AI-motoren kan nås" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout for standard AI-anmodninger til motoren." +label = "Anmodningstimeout (sekunder)" + +[admin.settings.ai.general.url] +description = "Intern URL for Python AI-motoren, f.eks. http://stirling-pdf-engine:5001." +label = "AI-motor-URL" + +[admin.settings.ai.limits] +description = "Sikkerhedsgrænser for hvor meget arbejde AI-anmodninger må udføre, og hvor mange der kører samtidigt. Anvendes på AI-motoren, når der gemmes." +title = "Grænser og ydeevne" + +[admin.settings.ai.limits.maxCharacters] +description = "Sikkerhedsgrænse: afvis AI-anmodninger, hvis udtrukne tekst overstiger denne længde." +label = "Maks. tegn pr. anmodning" + +[admin.settings.ai.limits.maxPages] +description = "Sikkerhedsgrænse: afvis AI-anmodninger over så mange PDF-sider." +label = "Maks. sider pr. anmodning" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maksimalt antal samtidige igangværende modelkald på tværs af hele motoren." +label = "Maks. samtidighed for model" + +[admin.settings.ai.models] +description = "Vælg LLM-udbyderen og de smarte/hurtige modeller, AI-motoren bruger. Anvendes på AI-motoren, når der gemmes." +title = "Modeller og udbydere" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Lad stå tomt for at bruge motorens egne miljølegitimationsoplysninger. Gælder for selvhostede single-engine-installationer." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Gemt - lad stå tomt for at beholde den aktuelle nøgle" + +[admin.settings.ai.models.baseUrl] +description = "Base URL for det OpenAI-kompatible / Ollama-endpoint, f.eks. http://ollama:11434/v1." +label = "Udbyderens base-URL" +warning = "Base URL'en skal pege på et betroet internt endpoint. Motoren foretager server-side-anmodninger til den, så en ikke-betroet værdi er SSRF-følsom." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maksimalt antal output-tokens for den hurtige model." +label = "Maks. tokens for hurtig model" + +[admin.settings.ai.models.fastModel] +description = "Billigere, hurtigere model til lette opgaver. Fri tekst; forslag er kun tips." +label = "Hurtig model" + +[admin.settings.ai.models.provider] +description = "Hvilken LLM-udbyder motoren kommunikerer med." +label = "Udbyder" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maksimalt antal output-tokens for den smarte model." +label = "Maks. tokens for smart model" + +[admin.settings.ai.models.smartModel] +description = "Model med høj kapacitet til kompleks ræsonnering. Fri tekst; forslag er kun tips." +label = "Smart model" + +[admin.settings.ai.saved] +body = "Ændringer sendes automatisk til AI-motoren." +bodyNoPush = "Indstillinger gemt. De anvendes næste gang AI-motoren indlæser sin konfiguration." +title = "AI-indstillinger gemt" + [admin.settings.badge] clickToUpgrade = "Klik for at se plandetaljer" @@ -938,6 +1094,25 @@ label = "Genskab ved opstart" description = "Antal dage certifikatet er gyldigt" label = "Certifikatgyldighed (dage)" +[admin.settings.folderAccess] +description = "Mapper, som mappekilder og mappeoutput har tilladelse til at læse fra og skrive til. Dette er en sikkerhedsgrænse: automatiseringer kan aldrig peges mod en serversti uden for denne liste." +securityNote = "Lad dette stå tomt for at deaktivere mappekilder og output helt. Stirlings egen konfigurationsmappe er altid forbudt område." +title = "Mappeadgang" + +[admin.settings.folderAccess.implied] +description = "Disse Stirling-administrerede mapper er altid tilladt og kan ikke ændres her." +serverStorage = "Serverfillager" +title = "Altid tilladt" +watchedFolder = "Pipeline-overvåget mappe" + +[admin.settings.folderAccess.roots] +add = "Tilføj" +empty = "Ingen mapper tilladt. Mappekilder og output er i øjeblikket deaktiveret." +hint = "Indtast absolutte stier, for eksempel /data/inbox." +label = "Tilladte mapperødder" +placeholder = "/data/inbox" +remove = "Fjern mapperod" + [admin.settings.general] description = "Konfigurer programindstillinger for hele systemet, herunder branding og standardadfærd." system = "System" @@ -1704,6 +1879,9 @@ width = "Bredde" [app] description = "Det gratis alternativ til Adobe Acrobat (10M+ downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konverter til PDF/A-3b" convertToPdfA3bDescription = "Opretter en arkiv-PDF med indlejrede vedhæftninger" @@ -1842,6 +2020,15 @@ export = "Eksport" accessDenied = "Adgang nægtet" insufficientPermissions = "Du har ikke tilladelse til at udføre denne handling." +[auth.callback] +completing = "Fuldfører godkendelse" +invalidToken = "OAuth-login mislykkedes - ugyldigt token." +missingToken = "OAuth-login mislykkedes - intet token modtaget." +oauthFailed = "OAuth-login mislykkedes. Prøv igen." +pleaseWait = "Vent venligst, mens vi afslutter din loginproces." +signedOut = "Du er blevet logget ud. Log ind igen." +windowMayClose = "Du kan lukke dette vindue, når det er færdigt." + [auth.displayName] guest = "Gæst" user = "Bruger" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Sikrer PDF-dokumenter ved at fjerne potentielt ondsindet i description = "Dette værktøj omdøber automatisk PDF-filer baseret på deres indhold. Det analyserer dokumentet for at finde den mest passende titel fra teksten." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatisk rotation" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Tærskel for sikkerhed" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Kun OCR" +text = "Kun tekst" +title = "Detektionsmetode" + +[autoRotate.error] +failed = "Der opstod en fejl under automatisk rotation af PDF'en." + +[autoRotate.inferUndetected] +title = "Rotér sider med lav sikkerhed, så de matcher" + +[autoRotate.report] +applied = "{{degrees}}° med uret" +confidenceValue = "Sikkerhed {{value}}" +noChange = "Ingen ændring" +pageLabel = "Side {{number}}" +summary = "{{rotated}} af {{total}} sider roteret, {{unchanged}} efterladt som de er" +title = "Detektionsrapport" + +[autoRotate.report.method] +inferred = "Matchet" +none = "Sprunget over" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Under tærskel" +blankPage = "Siden er tom" +inferredFromDocument = "Samme som de andre sider" +noDominantDirection = "Blandede tekstretninger" +osdFailed = "Ingen læsbar tekst fundet" +osdNoVerdict = "Kunne ikke afgøre, hvilken vej der vender op" +tesseractUnavailable = "OCR ikke installeret" +tooFewGlyphs = "For lidt tekst" + +[autoRotate.results] +title = "Resultater for automatisk rotation" + +[autoRotate.settings] +title = "Indstillinger" + +[autoRotate.tooltip.confidenceThreshold] +text = "Hvor sikker kontrollen skal være, før en side vendes. Sænk den for at rotere flere sider, hæv den for at lade flere sider være urørte." +title = "Tærskel for sikkerhed" + +[autoRotate.tooltip.description] +text = "Finder ud af, hvilken vej hver side skal vende, og drejer den korrekt. Sider, der allerede vender rigtigt, efterlades uændrede." + +[autoRotate.tooltip.detectionMode] +text = "Auto læser først teksten på siden og ser derefter på siden som et billede, hvis det ikke er nok. Kun tekst springer billedkontrollen over, hvilket er hurtigst for dokumenter, der aldrig er blevet scannet. Kun OCR behandler hver side som et billede." +title = "Detektionsmetode" + +[autoRotate.tooltip.header] +title = "Automatisk rotation" + +[autoRotate.tooltip.inferUndetected] +text = "Tomme eller næsten tomme sider kan ikke kontrolleres alene. Når hver side, der kunne læses, kræver samme rotation, roteres disse sider, så de matcher." +title = "Rotér sider med lav sikkerhed, så de matcher" + [autoSizeSplitPDF] tags = "pdf,opdel,dokument,organisation" @@ -2189,33 +2443,6 @@ keywords = "Nøgleord: odd, even" numbers = "Tal/intervaller: 5, 10-20" progressions = "Progressioner: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Certifikattyper" - -[certificateChoice.tooltip.organization] -bullet1 = "Administreres af systemadministratorer" -bullet2 = "Deles mellem autoriserede brugere" -bullet3 = "Repræsenterer virksomhedens identitet, ikke en person" -bullet4 = "Bedst til: Officielle dokumenter, teamunderskrifter" -description = "Et delt certifikat leveret af din organisation. Bruges til underskriftsfuldmagt på tværs af virksomheden." -title = "Organisationscertifikat" - -[certificateChoice.tooltip.personal] -bullet1 = "Genereres automatisk ved første brug" -bullet2 = "Knyttet til din brugerkonto" -bullet3 = "Kan ikke deles med andre brugere" -bullet4 = "Bedst til: Personlige dokumenter, individuel ansvarlighed" -description = "Et automatisk genereret certifikat, der er unikt for din brugerkonto. Velegnet til individuelle underskrifter." -title = "Personligt certifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Kræver P12/PFX-fil og adgangskode" -bullet2 = "Kan udstedes af eksterne Certificate Authorities" -bullet3 = "Højere tillidsniveau til juridiske dokumenter" -bullet4 = "Bedst til: Juridisk bindende kontrakter, ekstern validering" -description = "Brug din egen PKCS#12-certifikatfil. Giver fuld kontrol over certifikategenskaber." -title = "Upload brugerdefineret P12" - [certSign] awaitingSignatures = "Afventer underskrifter" chooseCertificate = "Vælg certifikatfil" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Certifikatkilde" upload = "Upload" -[certSign.tooltip.header] -title = "Om håndtering af signaturer" - -[certSign.tooltip.overview] -bullet1 = "Tjek eksisterende signaturer og deres gyldighed" -bullet2 = "Se detaljerede oplysninger om underskrivere og certifikater" -bullet3 = "Tilføj nye digitale signaturer for at sikre dine dokumenter" -bullet4 = "Flere filer understøttes med nem navigation" -text = "Dette værktøj lader dig tjekke, om dine PDF'er er digitalt underskrevet, og tilføje nye digitale signaturer. Digitale signaturer beviser, hvem der oprettede eller godkendte et dokument, og viser, om det er ændret siden underskriften." -title = "Hvad kan dette værktøj?" - -[certSign.tooltip.signing] -bullet1 = "Understøtter PEM, PKCS12, JKS og servercertifikatformater" -bullet2 = "Mulighed for at vise eller skjule signatur på PDF'en" -bullet3 = "Tilføj årsag, sted og underskrivers navn" -bullet4 = "Vælg hvilken side synlige signaturer skal placeres på" -bullet5 = "Brug servercertifikat for en enkel 'Sign with Stirling-PDF'-mulighed" -text = "For at signere en PDF skal du bruge et digitalt certifikat (som PEM, PKCS12 eller JKS). Du kan vælge at gøre signaturen synlig i dokumentet eller holde den usynlig kun af sikkerhedshensyn." -title = "Tilføjelse af signaturer" - -[certSign.tooltip.validation] -bullet1 = "Viser om signaturer er gyldige eller ugyldige" -bullet2 = "Viser oplysninger om underskriver og underskrivningsdato" -bullet3 = "Tjekker om dokumentet blev ændret efter underskrivning" -bullet4 = "Kan bruge brugerdefinerede certifikater til verifikation" -text = "Når du tjekker signaturer, fortæller værktøjet, om de er gyldige, hvem der underskrev dokumentet, hvornår det blev underskrevet, og om dokumentet er ændret siden underskrivning." -title = "Kontrol af signaturer" - [changeCreds] changeUsername = "Opdater dit brugernavn. Du bliver logget ud efter opdatering." credsUpdated = "Konto opdateret" @@ -2737,12 +2936,16 @@ text = "For at gøre disse tilladelser uændrerlige skal du bruge værktøjet Ti [chat.actions] copy = "Kopiér besked" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling-agentindstillinger" +assistant = "AI Assistant" clearChat = "Ryd chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Denne handling vil bruge dine cloud-kreditter" tooltip = "Vælg farve fra skærmen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Vælg farve" [common] +actions = "Actions" back = "Tilbage" cancel = "Annullér" close = "Luk" +codeSample = "Code sample" collapse = "Skjul" confirm = "Bekræft" continue = "Fortsæt" copied = "Kopieret!" copy = "Kopiér" done = "Færdig" +enterValue = "Enter value" error = "Fejl" expand = "Udvid" loading = "Indlæser..." next = "Næste" +open = "Open" preview = "Forhåndsvisning" previous = "Forrige" refresh = "Opdatér" +remaining = "Resterende" retry = "Prøv igen" save = "Gem" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Ryd valgte" @@ -2988,6 +3199,7 @@ title = "Komprimeringsmetode" [compress.settings] desiredSize = "Ønsket filstørrelse" desiredSizePlaceholder = "Indtast størrelse" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Komprimering er en nem måde at reducere filstørrelsen på. Vælg filstørrelse for at angive en målstørrelse og lad os justere kvaliteten for dig. Vælg kvalitet for at indstille komprimeringsstyrken manuelt." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF-indstillinger" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Vælg først et kildeformat" settings = "Indstillinger" single = "Enkelt" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Avanceret overvågning" -allInOneWorkspace = "Alt-i-én PDF-arbejdsområde (fremviser, værktøjer og agent)" -apiSandbox = "API-sandkasse" -dedicatedSupportSlas = "Dedikeret support og SLA'er" -fasterThroughput = "10x hurtigere gennemløb" -fullyPrivateFiles = "Fuldt private filer" -largeFileProcessing = "Behandling af store filer" -orgWideAccess = "Organisationsdækkende adgangskontrol" -premiumAiModels = "Premium AI-modeller" -prioritySupport = "Prioriteret support" -privateDocCloud = "Privat dokumentcloud" -ragFineTuning = "RAG + finjustering" -secureApiAccess = "Sikker API-adgang" -standardThroughput = "Standard gennemløb" -unlimitedApiAccess = "Ubegrænset API-adgang" -unlimitedSeats = "Ubegrænsede pladser" - [crop] autoCrop = "Auto-beskær hvidt område" noFileSelected = "Vælg en PDF for at begynde beskæring" @@ -3346,6 +3558,7 @@ message = "Kunne ikke angive standard PDF-håndterer" title = "Fejl" [defaultApp.prompt] +dontRemind = "Påmind mig ikke igen" message = "Gør Stirling PDF til din standardapp til at åbne PDF-filer." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Søg..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Redigér" @@ -3644,11 +3861,13 @@ shareSelected = "Del valgte" sharing = "Deling" showAll = "Vis alle" showHistory = "Vis historik" +sortBy = "Sort files" sortByDate = "Sorter efter dato" sortByName = "Sorter efter navn" sortBySize = "Sorter efter størrelse" storage = "Lagerplads" storageState = "Lagring" +storageUsed = "Storage used" synced = "Synkroniseret" title = "Upload PDF-filer" toolChain = "Anvendte værktøjer" @@ -3694,13 +3913,17 @@ addFiles = "Tilføj filer" addingFiles = "Tilføjer filer…" collapse = "Sammenfold sidepanel" customizeGroups = "Tilpas grupper" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Åbn filer for at komme i gang" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Udvid sidepanel" -files = "Filer" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive er ikke konfigureret" leaveMyFiles = "Forlad Mine filer" +library = "PDF-bibliotek" myFiles = "Mine filer" noFiles = "Ingen filer endnu" openFileManager = "Åbn filhåndtering" @@ -3708,37 +3931,36 @@ openFromComputer = "Åbn fra computer" openSettings = "Åbn indstillinger" other = "Andet" recent = "Seneste" -search = "Søg" -searchPlaceholder = "Søg i filer..." viewAll = "Vis alle {{count}} filer" [fileSidebar.fileItem] closeViewer = "Luk fremviser" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Åbn i fremviser" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Tilføj" -addLabel = "Tilføj en etiket…" -categoryIconAria = "Vælg et ikon for {{name}}" -createCategory = "Opret kategori" -delete = "Slet kategori" done = "Færdig" -hide = "Skjul kategori" -newCategory = "Nyt kategorinavn…" -removeLabel = "Fjern {{name}}" -rename = "Omdøb" reset = "Nulstil til standarder" -search = "Søg efter etiketter…" -show = "Vis kategori" subtitle = "Gruppér dine filer i overordnede kategorier. Føj eksisterende eller nye etiketter til en kategori, omdøb den, eller opret din egen. Filer, der ikke er i nogen af dine kategorier, vises under “Andet”." title = "Sidebjælkekategorier" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Føj til arbejdsområde" addToWorkspaceCount = "Føj {{count}} til arbejdsområde" @@ -3749,7 +3971,6 @@ backToMyFiles = "Tilbage til Mine filer" breadcrumbs = "Mappesti" cancel = "Annuller" classification = "Klassifikation" -clearSearch = "Ryd søgning" clearSelection = "Ryd markering" closeDetails = "Luk detaljer" create = "Opret" @@ -3781,6 +4002,7 @@ downloadAll = "Download alle" downloadVersion = "Download denne version" dropOverlay = "Slip filer for at uploade" dropOverlaySub = "Filer starter i Lokal. Brug 'Flyt til' eller 'Gem til skyen' for at organisere dem i en mappe." +duplicate = "Duplicate" file = "Fil" fileInfo = "File info" fileMenu = "Filhandlinger" @@ -3808,13 +4030,12 @@ refresh = "Opdatér fra server" remove = "Slet" removeVersion = "Fjern denne version" rename = "Omdøb" +renamed = "Omdøbt" renameFolder = "Omdøb mappe" resizeFolderTree = "Ændr størrelse på mappetræ (piletaster, Shift for større trin; dobbeltklik for auto-tilpas)" save = "Gem" saveToServer = "Gem til server" saveToServerDisabledHint = "Lagring til serveren er ikke aktiveret på denne server. Bed din administrator om at aktivere det." -search = "Søg" -searchPlaceholder = "Søg i denne mappe og undermapper" selectAll = "Vælg alle" selectAllHint = "Klik for at vælge alle. Tip: hold Ctrl (eller Cmd) for at tilføje filer én ad gangen, Shift for at vælge et område." selectedCount = "{{count}} valgt" @@ -3874,6 +4095,10 @@ title = "Ingen skyfiler endnu" hint = "Filer gemt uden upload bliver her. Slip en fil for at tilføje en." title = "Ingen filer kun lokale" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Filer, du åbner eller redigerer, vil blive vist her." title = "Intet ændret endnu" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Kunne ikke slette mappen." deleteFolderFailedDetail = "Kunne ikke slette mappen: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Kunne ikke opdatere mappens udseende." folderAppearanceFailedDetail = "Kunne ikke opdatere mappens udseende: {{message}}" moveFilesFailed = "Kunne ikke flytte filer." @@ -3910,9 +4137,7 @@ count = "Filer" folder = "Mappe" labels = "Etiketter" modified = "Ændret" -name = "Navn" size = "Størrelse" -toolHistoryAtVersion = "Kumulativ værktøjskæde" totalSize = "Samlet størrelse" type = "Type" versionHistory = "Versionsrejse" @@ -3945,6 +4170,11 @@ localHint = "Kun gemt i denne browser" shared = "Delt" sharedHint = "Delt med dig via link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Ældst først" modifiedDesc = "Nyest først" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Ingen filer tilgængelige i lageret. Åbn nogle filer f open = "Åbn" openFile = "Åbn fil" openFiles = "Åbn filer" +selectFile = "Select {{name}}" selectFromStorage = "Vælg fra lager" upload = "Upload" uploadFile = "Upload fil" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Alt gemt" +analyzingFields = "Analysing form fields..." +extractCsvError = "Kunne ikke udtrække CSV" +extractXlsxError = "Kunne ikke udtrække XLSX" +filled = "filled" flattenAfterFilling = "Fladgør efter udfyldning" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl" +requiredFieldsError = "Udfyld alle obligatoriske felter" rescanFields = "Scan felter igen" rescanFormFields = "Scan formularfelter igen" save = "Gem" +saveShortcut = "Ctrl+S for at gemme" +unsavedChanges = "Ikke-gemte ændringer" [formFill.sidebar] close = "Luk sidepanel" @@ -4309,6 +4552,11 @@ desc = "Omdøber automatisk en PDF-fil baseret på dens detekterede overskrift" tags = "auto-detektér,header-baseret,organiser,omdøb" title = "Omdøb PDF-fil automatisk" +[home.autoRotate] +desc = "Registrér hver sides retning, og rotér den automatisk opret." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Automatisk rotation" + [home.autoSizeSplitPDF] tags = "auto,opdel,størrelse" title = "Auto-opdel størrelse/antal" @@ -4608,15 +4856,12 @@ hint = "Upload en PNG, JPG, SVG eller anden billedfil for at placere på PDF'en. label = "Upload billede" placeholder = "Vælg billedfil" -[imageToPdf] -tags = "konvertering,img,jpg,billede,foto" - [imageToPDF] header = "Billede til PDF" title = "Billede til PDF" -[infoBanner] -dismiss = "Afvis" +[imageToPdf] +tags = "konvertering,img,jpg,billede,foto" [invite] acceptError = "Kunne ikke oprette konto" @@ -4647,11 +4892,9 @@ welcomeTitle = "Du er blevet inviteret!" [landing] addFiles = "Tilføj filer" -heroSubtitle = "Træk en PDF ind eller tilføj en eksisterende PDF for at komme i gang." mobileUpload = "Upload fra mobil" openFromComputer = "Åbn fra computer" uploadFromComputer = "Upload fra computer" -workbenchEmptyStateHero = "Slip en PDF hvor som helst" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Indtast brugernavn" failedToSignIn = "Kunne ikke logge ind med {{provider}}: {{message}}" forgotPassword = "Glemt din adgangskode?" loggingIn = "Logger ind..." -logIn = "Log ind" login = "Log ind" magicLinkSent = "Magisk link sendt til {{email}}! Tjek din e-mail og klik på linket for at logge ind." mfaCode = "Godkendelseskode" @@ -4715,26 +4957,10 @@ signInWith = "Log ind med" title = "Log ind" unexpectedError = "Uventet fejl: {{message}}" updatePassword = "Opdater adgangskode" -useEmailInstead = "Log ind med e-mail" useMagicLink = "Brug i stedet magisk link" username = "Brugernavn" youAreLoggedIn = "Du er logget ind!" -[login.slides.edit] -alt = "Rediger PDF'er" -subtitle = "Med over et dusin værktøjer til at redigere, signere, læse og manipulere PDF'er skal du nok finde det, du leder efter." -title = "Rediger PDF'er for at vise/beskytte de oplysninger, du ønsker" - -[login.slides.overview] -alt = "Stirling PDF oversigt" -subtitle = "En privacy-first cloud suite til PDF'er, der lader dig konvertere, signere, redigere og administrere dokumenter samt 50+ andre kraftfulde værktøjer." -title = "Din alt-i-en-løsning til alle dine PDF-behov." - -[login.slides.secure] -alt = "Sikr PDF'er" -subtitle = "Tilføj adgangskoder, slør indhold, og håndter certifikater nemt." -title = "Beskyt følsomme oplysninger i dine PDF'er" - [margin] large = "Stor" medium = "Mellem" @@ -4818,6 +5044,34 @@ uploadSuccess = "Upload gennemført!" uploadSuccessMessage = "Dine billeder er overført." validating = "Validerer session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan for at uploade fotos. Billeder konverteres automatisk til PDF." descriptionNoConvert = "Scan for at uploade fotos fra din mobilenhed." @@ -4836,8 +5090,13 @@ title = "Upload fra mobil" tags = "Multi Værktøj,Multi operation,UI,klik træk,front end,klient side,interaktiv,interagerbar,flyt" title = "PDF Multi Værktøj" -[navbar] -search = "Søg" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Godkendelsen var ikke vellykket. Du kan lukke dette vindue og prøve igen." @@ -4966,6 +5225,7 @@ activeFiles = "Visningen Aktive filer viser alle PDF'er, du har allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Luk" cropSettings = "Nu hvor vi har valgt den fil, vi vil beskære, kan vi konfigurere Beskær-værktøjet til at vælge det område, vi vil beskære PDF'en til." +dialogLabel = "Onboarding" fileCheckbox = "Klik på en af filerne for at vælge den til behandling. Du kan vælge flere filer til batchhandlinger." fileReplacement = "Den ændrede fil erstatter automatisk den originale fil i Arbejdsbordet, så du nemt kan køre den gennem flere værktøjer." filesButton = "Knappen Filer på værktøjslinjen til hurtig adgang lader dig uploade PDF'er til brug i værktøjerne." @@ -4976,6 +5236,7 @@ pinButton = "Du kan bruge knappen Fastgør, hvis du hellere vil results = "Når værktøjet er færdigt, viser trinnet Gennemse en forhåndsvisning af resultaterne i dette panel og giver dig mulighed for at fortryde handlingen eller downloade filen." runButton = "Når værktøjet er konfigureret, kan du køre det på alle de valgte PDF'er med denne knap." selectCropTool = "Lad os vælge Beskær-værktøjet for at demonstrere, hvordan man bruger et af værktøjerne." +stepOf = "Trin {{current}} af {{total}}" toolInterface = "Dette er Beskær-værktøjets grænseflade. Som du kan se, er der ikke meget at se, fordi vi endnu ikke har tilføjet nogen PDF-filer at arbejde med." workbench = "Dette er Arbejdsbordet – hovedområdet, hvor du ser og redigerer dine PDF'er." wrapUp = "Så er du klar! Du har lært om appens hovedområder og hvordan du bruger dem. Klik på knappen Hjælp, når som helst du vil se denne rundtur igen." @@ -4988,6 +5249,26 @@ showMeAround = "Vis mig rundt" skipForNow = "Spring over for nu" skipTheTour = "Spring rundvisningen over" +[onboarding.checklist] +dismiss = "Afvis" +title = "Opsæt Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Kør Stirling lokalt på din maskine" +title = "Download Stirling til desktop" + +[onboarding.checklist.inviteTeam] +description = "Samarbejd med dit team" +title = "Inviter teammedlemmer" + +[onboarding.checklist.shareAnalytics] +description = "Hjælp med at forbedre Stirling" +title = "Del anonyme brugsdata" + +[onboarding.checklist.takeTour] +description = "Se, hvordan Stirling fungerer, i en hurtig gennemgang" +title = "Tag rundvisningen" + [onboarding.desktopInstall] body = "Stirling fungerer bedst som desktop-app. Du kan bruge den offline, få hurtigere adgang til dokumenter og redigere lokalt på din computer." selectOs = "Vælg operativsystem" @@ -4996,7 +5277,21 @@ titleWithOs = "Download til {{osLabel}}" [onboarding.mfa] authenticationCode = "Godkendelseskode" +description = "Sikr din konto ved at tilknytte en authenticator-app. Scan QR-koden, eller indtast opsætningsnøglen, og bekræft derefter den 6-cifrede kode for at afslutte." +enable = "Aktivér MFA" +enableError = "Kunne ikke aktivere tofaktorgodkendelse. Kontrollér koden, og prøv igen." +enterCode = "Indtast den 6-cifrede kode fra din app." +enterCodeError = "Indtast godkendelseskoden for at fortsætte." +logout = "Log ud" +openAuthenticator = "Åbn Google Authenticator, Authy eller 1Password." qrCodeLoading = "Genererer din QR-kode…" +regenerateQrCode = "Generér QR-kode igen" +scanQrCode = "Scan QR-koden, eller indtast opsætningsnøglen nedenfor." +setupError = "Kunne ikke starte tofaktoropsætning. Prøv igen." +setupKey = "Opsætningsnøgle (manuel indtastning)" +stepByStep = "Trin for trin" +success = "MFA er blevet aktiveret. Du kan nu fortsætte." +title = "Opsætning af multifaktorgodkendelse" [onboarding.planOverview] adminBodyLoginDisabled = "Når du aktiverer login-tilstand, kan du administrere brugere, konfigurere indstillinger og overvåge serverens tilstand. De første {{freeTierLimit}} personer på din server kan bruge Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Admin-overblik" userBody = "Invitér teammedlemmer, tildel roller, og hold dine dokumenter organiseret i ét sikkert arbejdsområde. Aktiver login-tilstand, når du er klar til at vokse ud over solo-brug." userTitle = "Plan-overblik" +[onboarding.processorIntro] +body = "Stirling kører nu Politikker — automatiserede regler, der klassificerer, sikrer og behandler hvert dokument, når det ankommer. Opsæt dem, og overvåg kørsler i Processor." +cta = "Se Processor" +title = "Se Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Forudbetalt år" + +[payg.prepaid.banner] +exhausted = "Din forudbetalte kapacitet er brugt op; målt fakturering er genoptaget. Fyld op for at beholde rabatten." +expiring_one = "Dit forudbetalte år slutter om {{count}} dag. Ubrugt kapacitet udløber — fyld op for at starte et nyt år." +expiring_other = "Dit forudbetalte år slutter om {{count}} dage. Ubrugt kapacitet udløber — fyld op for at starte et nyt år." +low = "Du er nede på {{pct}}% af din forudbetalte kapacitet. Fyld op når som helst for at fortsætte behandlingen uden afbrydelser." + +[payg.prepaid.card] +subtitle = "Bruges før målt fakturering og uden for din forbrugsgrænse." +title = "Forudbetalt kapacitet" + +[payg.prepaid.meter] +capSuffix = "af {{total}} forudbetalte credits" +expires = "Udløber {{date}}" + +[payg.prepaid.state] +exhausted = "Brugt op" +healthy = "Rigeligt tilbage" +low = "Ved at løbe tør" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF Til PDF/A" tags = "arkiv,langtids,standard,konvertering,opbevaring,bevaring" title = "PDF Til PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "udskrivning,standard,konvertering,produktion,førtryk,arkiv" title = "PDF til PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kreditter" xsmall = "100 kreditter" [plan.availablePlans] +currency = "Billing currency" subtitle = "Vælg den plan, der passer til dine behov" title = "Tilgængelige planer" @@ -5937,49 +6267,12 @@ keyDescription = "Indsæt licensnøglen fra din e-mail" success = "Licens aktiveret!" successMessage = "Din licens er aktiveret. Du kan nu lukke dette vindue." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "håndhævet" -enforcing = "Håndhæver..." -failed = "Håndhævelsen mislykkedes" outputsUnavailable = "Politikoutput er ikke længere tilgængelige til download." partialOutputsUnavailable = "Nogle politikoutput er ikke længere tilgængelige til download." -retrying = "Optaget, prøver igen..." runNotFound = "Håndhævelseskørslen kunne ikke længere findes." -step = "trin {{current}}/{{total}}" timedOut = "Håndhævelsen fik timeout, før kørslen kunne afsluttes." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Luk" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Anvender {{names}}" applyingProgress = "Anvender {{names}} ({{done}} af {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} af {{total}} fil(er) kunne ikke behandles og blev ek failureTitle = "Eksporteret uden fuld håndhævelse" printPolicyAppliedBody = "Denne PDF blev opdateret for at opfylde en politik. Gennemse ændringerne, og udskriv derefter igen." printPolicyAppliedTitle = "Politik anvendt før udskrivning" -queued = "+{{count}} i kø" successTitle = "{{names}} anvendt" summaryMore = "{{first}}, {{second}} og {{more}} mere" summaryTwo = "{{first}} og {{second}}" -[policies.enforcement.triggerVerb] -convert = "Håndhæver før konvertering" -default = "Håndhæver" -export = "Håndhæver før eksport" -input = "Håndhæver ved import" -print = "Håndhæver før udskrivning" - -[policies.field] -accessLog = "Adgangslog" -archiveAfter = "Arkivér efter" -auditTrail = "Revisionsspor" -belowThreshold = "Under tærskel" -destination = "Destination" -frameworks = "Frameworks" -immutableHold = "Uforanderlig tilbageholdelse" -keepFor = "Behold i" -minConfidence = "Min. tillid" -notify = "Giv besked ved routing" -onViolation = "Når ikke-kompatibel" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Tilføj" -addPlaceholder = "Tilføj en etiket…" -customNote = "Tilpasset til dit team." -defaultNote = "Bruger den indbyggede standard, delt med dit team." -duplicate = "\"{{name}}\" findes allerede." -edit = "Rediger etiketter" -empty = "Ingen etiketter endnu." -export = "Eksportér JSON" -iconAria = "Vælg et ikon for {{name}}" -import = "Importér JSON" -importError = "Kunne ikke importere den fil." -managedNote = "Teametiketter administreres af din teamleder." -modalSubtitle = "Delt med hele dit team. Klassifikatoren vælger de etiketter, der passer til hvert dokument." -modalTitle = "Klassifikationsetiketter" -pickIcon = "Brug {{label}}-ikon" -removeAria = "Fjern {{name}}" -resetToDefault = "Nulstil til standard" -saveForTeam = "Gem for team" -saving = "Gemmer…" -sectionLabel = "Klassifikationsetiketter" -startFromScratch = "Start fra bunden" -teamCount = "teametiketter" -tooLong = "Etiketter må højst være {{max}} tegn." -ungrouped = "Ikke grupperet" -view = "Vis etiketter" +categoryCount = "kategorier" +hideCategory = "Skjul kategori" +labelCount = "etiketter" +sharedNote = "Disse etiketter er indbyggede og deles på tværs af hele dit team." +showCategory = "Vis kategori" [policies.pii] account = "Kontonumre (mærkede)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US routingnumre (ABA)" ssn = "Social Security-numre" -[policies.settings] -noneExport = "Ingen politikker kører i øjeblikket ved eksport." -noneUpload = "Ingen politikker kører i øjeblikket ved upload." -onExport = "Ved eksport" -onUpload = "Ved upload" -reorderHandle = "Træk for at ændre rækkefølge" -runOrderDesc = "Når mere end én politik kører på den samme trigger, kører de i denne rækkefølge — hver på den forrige politiks output. Træk for at ændre rækkefølge." -title = "Politikindstillinger" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Politikindstillinger" -policySettings = "Politikindstillinger" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Prøv mislykkede politikker igen ({{count}})" -rowProgress = "{{completed}} af {{total}} filer behandlet" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Hvad er en politik?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Finder og maskerer automatisk følsomme oplysninger — som Social Security- og kortnumre — så de ikke kan læses i dokumentet." -sanitize = "Fjerner skjult JavaScript fra filen, så intet kan køre automatisk, når nogen åbner den." -watermark = "Sætter et synligt mærke (f.eks. \"Fortroligt\") på hver side." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Luk" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} håndhæves..." badgeRan = "{{name}}-politikken kørte på denne fil" +badgeRunning = "{{name}} kører..." blockingAction = "{{action}} blokeret under håndhævelse af politik, vent venligst..." dismiss = "Afvis overlay" enforcingTitle = "Håndhæver politik..." @@ -6151,6 +6316,7 @@ viewAnyway = "Vis fil (politik håndhæves stadig)" [policyOption] autoRedactPhi = "Auto-rediger PHI" blockExport = "Bloker eksport" +ccpa = "CCPA" contracts = "Kontrakter" documents = "Dokumenter" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor-plan" subscribed = "Processor-plan" unlinked = "Ikke linket" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap fra dokument" -sectionsAriaLabel = "Agent Builder-sektioner" -selectorAriaLabel = "Agenter" -subtitle = "Design, test og udgiv de AI-agenter, der klassificerer, udtrækker fra og router dine dokumenter. Definér scenarier, afgræns værktøjsadgang, kør et golden set, og udgiv en version." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Annuller" -dropzoneText = "Vælg et eksempeldokument (PDF eller billede)" -lead = "Slip et eksempeldokument, så foreslår vi scenarier og et udtræksskema, du kan finpudse. Intet udgives, før du har gennemgået det." -submit = "Bootstrap agent" -subtitle = "Start en ny agent fra én repræsentativ fil" -title = "Bootstrap fra et dokument" - -[portal.agentBuilder.empty] -description = "Bootstrap en agent fra et eksempeldokument for at starte dens scenarier og udtræksskema, og finpuds og udgiv derefter." -title = "Ingen agenter endnu" - -[portal.agentBuilder.evals] -casesPassing = "Cases bestået" -columnCase = "Eval-case" -columnLatency = "Ventetid" -columnResult = "Resultat" -fail = "fejl" -goldenSetPassRate = "Beståelsesrate for golden set" -latencyMs = "{{ms}} ms" -notRun = "ikke kørt" -pass = "bestået" -passRate = "Beståelsesrate" -runEvals = "Kør evals" - -[portal.agentBuilder.evals.empty] -description = "Evals gør dine scenarier til et gentageligt golden set. Opgrader for at registrere beståelsesrate over tid og styre udgivelser efter den." -title = "Intet golden set endnu" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "på tværs af golden sets" -activeAgents = "Aktive agenter" -avgPassRate = "Gns. eval-beståelsesrate" -fleetWide = "på tværs af flåden" -latestPublished = "Senest udgivet" -scenarios = "Scenarier" -testCases = "testcases" -totalDescription_one = "{{count}} i alt" -totalDescription_other = "{{count}} i alt" - -[portal.agentBuilder.scenarios] -add = "Tilføj" -addScenario = "Tilføj scenarie" -enable = "Aktivér" -expectationLabel = "Forventet adfærd" -expectationPlaceholder = "Hvad agenten skal gøre" -inEval = "i eval" -mute = "Slå fra" -muted = "slået fra" -nameLabel = "Navn" -namePlaceholder = "f.eks. Compliance-eskalering" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarier" -tools = "Værktøjer" -versions = "Versioner" - -[portal.agentBuilder.tools] -broadAccess = "Bred adgang" -deniedHint = "Valgte værktøjer er blokeret. Alt andet kan stadig kaldes." -deniedTools = "Afviste værktøjer" -governanceGate = "Værktøjsstyring er tilgængelig på Enterprise-planen." -restricted = "Begrænset" -restrictedAccess = "Begrænset værktøjsadgang" -restrictedDescription = "Tillad alle værktøjer undtagen dem, du afviser nedenfor." - -[portal.agentBuilder.versions] -current = "aktuel" -historyGate = "Fuld versionshistorik og rollback er tilgængelige på Enterprise-planen." -publish = "Udgiv" -rollBack = "Rul tilbage" - [portal.assistant] close = "Luk assistent" error = "Kunne ikke nå assistenten." @@ -6365,9 +6451,21 @@ typing = "Skriver" loading = "Indlæser" redirectingToEditor = "Omdirigerer til editoren..." +[portal.billing.activation] +maybeLater = "Måske senere" +title = "Slå Processor til" + +[portal.billing.activation.payg] +sub = "Angiv en månedlig grænse. Betal for det, du kører." +title = "Betal efter forbrug" + +[portal.billing.activation.prepay] +badge = "2 måneder gratis" +sub = "Én faktura, kort eller bankoverførsel." +title = "Forudbetal et år" + [portal.billing.checkout] noClientSecret = "Edge-funktionen returnerede ingen client_secret." -subtitle = "Tilføj et kort for at fortsætte efter din gratis Editor-planbevilling. Stripe håndterer resten." title = "Slå Processor-planen til" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Din betaling lykkedes, men aktiveringen tager lidt længere end normalt. close = "Luk" title = "Næsten færdig" +[portal.billing.checkout.cap] +amountAria = "Månedlig forbrugsgrænse" +back = "Tilbage" +continue = "Fortsæt til betaling" +error = "Kunne ikke angive din forbrugsgrænse" +finePrint = "Fakturaer udsendes månedligt. Annullér når som helst." +noLimit = "Ingen grænse" +note = "Du faktureres aldrig over din grænse. Behandling sættes blot på pause." +perMonth = "/ md." +title = "Angiv din forbrugsgrænse" + +[portal.billing.checkout.card] +fields = "Kortnummer · MM / ÅÅ · CVC · ZIP" +label = "Kortoplysninger" +note = "Kortoplysninger indsamles af Stripe. Stirling gemmer aldrig PAN eller CVC." + [portal.billing.checkout.error] title = "Kunne ikke starte checkout" @@ -6383,10 +6497,9 @@ body = "Din betaling gik igennem. Vi slår målt behandling til på tværs af di hint = "Hold venligst dette vindue åbent." title = "Aktiverer din Processor-plan..." -[portal.billing.checkout.notConfigured] -bodyAfter = "i portal env for at aktivere checkout i appen." -bodyBefore = "Indstil" -title = "Stripe er ikke konfigureret" +[portal.billing.checkout.pay] +limit = "Månedlig forbrugsgrænse {{amount}}/md. Behandling sættes på pause ved grænsen." +limitNoCap = "Ingen forbrugsgrænse. Behandling sættes ikke på pause. Fakturaer udsendes månedligt." [portal.billing.enterpriseUpsell] cta = "Byg dit Enterprise-tilbud" @@ -6409,10 +6522,12 @@ currentPlan = "Nuværende plan" freeForever = "Gratis for altid" noTeamResolved = "Intet team er endnu knyttet til din wallet — opdater, og prøv igen." ownerOnly = "Kun teamejeren kan slå Processor-planen til." +payInvoice = "Betal faktura for at fuldføre" planName = "Editor" ssoIncluded = "SSO inkluderet" switchOnProcessor = "Slå Processor til →" unlimitedUsers = "Ubegrænsede brugere" +viewQuote = "Vis tilbud" [portal.billing.invoices] columnAmount = "Beløb" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direkte API-forespørgsler" segmentApiLabel = "API" segmentAutomationDesc = "Automatiseringer og pipelines" segmentAutomationLabel = "Automatisering" +sizeMultiplier = "{{formatted}} PDFs brugte en størrelsesmultiplikator" +summary = "{{unique}} unikke · {{units}} måleenheder · {{avg}} gns. pr. PDF" +summaryNoRate = "{{unique}} unikke · {{units}} måleenheder" unit = "målte PDFs" +unitsPending_one = "{{units}} måleenhed afventer synkronisering fra tilknyttede instanser" +unitsPending_other = "{{units}} måleenheder afventer synkronisering fra tilknyttede instanser" + +[portal.billing.prepaid] +capSuffix = "af {{total}} forudbetalte credits" +expires = "Udløber {{date}}" +eyebrow = "Forudbetalt kapacitet" +note = "Bruges før målt fakturering og uden for din forbrugsgrænse." +topUp = "Fyld op" + +[portal.billing.prepaid.buy] +back = "Tilbage" +calcTitle = "Beregn din årlige betaling" +cancel = "Annullér" +cancelPurchase = "Annullér køb" +close = "Luk" +continueToPayment = "Fortsæt til betaling" +finalise = "Færdiggør" +invoiceErrorTitle = "Kunne ikke generere fakturaen" +loadingTitle = "Indlæser dit tilbud" +payTitle = "Betal for dit år" +step = "Trin {{current}} af {{total}}" + +[portal.billing.prepaid.calc] +change = "Skift" +deployRow = "Udrulning" +done = "Færdig" +enterpriseHint = "Dette er enterprise-skala. Ved denne volumen slår enterprise-priser enhver selvbetjeningsrabat. Prisfastlåsning, vilkår og et tilbud på få minutter." +governanceRow = "Styring" +governanceValue = "{{label}} · {{count}} politikker" +handlesLabel = "Din Processor" +handlesValue = "håndterer {{volume}} credits / md." +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} politikker" +poolCaption = "Én pulje på {{credits}} credits for året. Tunge måneder låner fra lette." +priceLabel = "Dit år · 12 måneder til prisen for 10" +rateUnknown = "Vi viser den nøjagtige pris ved checkout." +savings = "Du sparer {{amount}} · 2 måneder gratis." +sizingRow = "Dimensionering" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Samlet antal brugere" + +[portal.billing.prepaid.consent] +label = "Jeg forstår, at når min forudbetalte kapacitet er brugt op eller udløber efter 12 måneder, fortsætter behandlingen automatisk til den almindelige målte pay-as-you-go-pris (op til min forbrugsgrænse), medmindre jeg annullerer, og at jeg kan annullere når som helst fra faktureringsportalen." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Administreret af Stirling. Live på få minutter." +cloudValue = "Stirling Cloud · Administreret" +selfhost = "Selvhostet" +selfhostDesc = "Kører på privat infrastruktur. Samme pris." +selfhostValue = "Selvhostet · Privat infrastruktur" + +[portal.billing.prepaid.offer] +cta = "Gennemgå tilbud" +subtitle = "Forudbetal et år med PDF-behandling og få to måneder gratis, brugt før målt fakturering, uden for din forbrugsgrænse." +title = "Få 12 måneder til prisen for 10" + +[portal.billing.prepaid.pay] +accountName = "Kontohavers navn" +accountNamePlaceholder = "f.eks. Jane Smith" +companyName = "Firmanavn (valgfrit)" +companyNamePlaceholder = "f.eks. Acme Inc." +downloadInvoice = "Download faktura" +payOnline = "Betal online" +poNumber = "PO-nummer (valgfrit)" +poNumberPlaceholder = "f.eks. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avanceret" +advancedDesc = "Pipelines driver det meste af din behandling." +none = "Ingen" +noneDesc = "Kører ikke pipelines endnu. Slå dem til når som helst." +standard = "Standard" +standardDesc = "Nogle få pipelines genbehandler ankommende PDFs." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Klassificering, standarden, plus deling" +governed = "Governed" +governedDesc = "Tilføjer sikkerhed og routing" +regulated = "Regulated" +regulatedDesc = "Hver kategori, inkl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Download tilbud (PDF)" +downloading = "Forbereder…" +share = "del til godkendelse" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "Filer under 25 MB, ingen dataafgifter" +heavy = "Tung" +heavyDesc = "Scannet eller billedtung, rutinemæssigt 50 MB+" +standard = "Standard" +standardDesc = "Mest små, nogle scanninger over 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Brugt op" +healthy = "Rigeligt tilbage" +low = "Ved at løbe tør" [portal.billing.spendLimit] adjustLimit = "Juster grænse" @@ -6494,6 +6713,8 @@ label = "Forventes at blive overskredet." [portal.billing.spendThisMonth] eyebrow = "Forbrug denne måned" +freeRemaining_one = "{{formatted}} gratis PDF tilbage" +freeRemaining_other = "{{formatted}} gratis PDFs tilbage" processed_one = "{{formattedCount}} PDF behandlet." processed_other = "{{formattedCount}} PDFs behandlet." processedWithRate_one = "{{formattedCount}} PDF behandlet, til {{rate}} hver." @@ -6510,6 +6731,7 @@ reachedTitle = "Månedlig forbrugsgrænse nået" title = "Kunne ikke åbne Stripe-portalen" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "af {{allowance}} gratis PDFs brugt" capSuffix_other = "af {{allowance}} gratis PDFs brugt" eyebrow = "Processor-prøveperiode" @@ -6534,6 +6756,376 @@ signature = "signatur" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Ny forbindelse" +createTitleFor = "Forbind {{name}}" +delete = "Slet" +edit = "Rediger" +editTitle = "Rediger forbindelse" +unsupportedType = "Denne forbindelsestype kan ikke redigeres her." + +[portal.connections.categories.advanced] +description = "Til en tjeneste, vi endnu ikke understøtter." +label = "Avanceret" + +[portal.connections.categories.audit] +description = "Registrér, hvad der skete med et dokument, hvor dine revisorer allerede kigger." +label = "Audit- og compliance-logning" + +[portal.connections.categories.notify] +description = "Fortæl et andet system, at noget er sket." +label = "Notifikation og workflow" + +[portal.connections.categories.security] +description = "Scan eller mærk et dokument, før det går videre." +label = "Sikkerhed og klassificering" + +[portal.connections.categories.signing] +description = "Send et dokument ud til signering eller notarbekræftelse." +label = "E-signatur og notarbekræftelse" + +[portal.connections.categories.storage] +description = "Hvor behandlede dokumenter ender - en bucket, et drev, en ticket eller en side." +label = "Fil og vedhæft" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling gemmer dette krypteret og viser det aldrig igen." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Trin, der bruger denne forbindelse, kan kun kalde stier under denne URL, aldrig en anden host." +label = "Server-URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Adgangskode" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Brugernavn" + +[portal.connections.commonFields.webhookUrl] +helperText = "Indsæt den URL, tjenesten gav dig. Kun stier under den kan kaldes." +label = "Webhook URL" + +[portal.connections.fields] +name = "Forbindelsesnavn" +namePlaceholder = "f.eks. Produktion" + +[portal.connections.picker] +cancel = "Annullér" +createNew = "Ny forbindelse..." +placeholder = "Vælg en forbindelse" +save = "Gem forbindelse" + +[portal.connections.picker2] +back = "Skift integration" +heading = "Hvad vil du forbinde?" +noResultsBody = "Vi understøtter det ikke endnu - fortæl os om det, så ser vi på at tilføje det." +noResultsTitle = "Intet matcher \"{{query}}\"" +searchPlaceholder = "Søg i integrationer - prøv \"sign\", \"ocr\" eller et produktnavn" + +[portal.connections.types.api] +description = "Kald enhver HTTP API fra en politik. Du vælger URL'en, godkendelse og payload." +label = "Brugerdefineret API" + +[portal.connections.types.api.fields.authType] +label = "Godkendelse" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Brugernavn og adgangskode" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token i en brugerdefineret header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ingen" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Log ind for et token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Trin, der bruger denne forbindelse, kan kun kalde stier under denne URL, aldrig en anden host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Headernavn" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Login-sti" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Adgangskode" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Hvis API'en svarer med et downloadlink, må kun disse hosts hentes. Adskil med kommaer. Lad stå tomt for kun at tillade base URL'ens egen host." +label = "Tilladte resultat-hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Send tokenet som denne header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Response-headeren, som login-kaldet returnerer tokenet i, f.eks. X-Auth-Token." +label = "Token-response-header" + +[portal.connections.types.api.fields.username] +label = "Brugernavn" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scan dokumenter for malware på en server, du kører - intet forlader dit netværk." +label = "ClamAV (selvhostet)" + +[portal.connections.types.cloudmersive] +description = "Virus- og indholdsscanning for dokumenter i en politik." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloker dokumenter, der indeholder makroer, eksekverbare filer, scripts eller indlejrede objekter - ikke kun kendt malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Vedhæft behandlede dokumenter til en Confluence-side." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Send dokumenter til ConsignO Cloud (Notarius) til elektronisk signatur og notarbekræftelse." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Dit ConsignO Cloud-miljø, inklusive /api/v1." +label = "Server-URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Adgangskode" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Kun nødvendigt, hvis din organisation bruger tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Brugernavn" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Send en besked til Discord, når en politik behandler et dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indekser en audit-hændelse i Elasticsearch, hver gang en politik håndterer et dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Poll en FTP- eller FTPS-server for nye dokumenter." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Forbindelsestilstand" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiv" +passive = "Passiv (anbefalet)" + +[portal.connections.types.ftp.fields.security] +helperText = "Almindelig FTP sender legitimationsoplysninger og filer ukrypteret; vælg FTPS, hvis serveren understøtter det." +label = "Kryptering" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (eksplicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Ingen (almindelig FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Send en besked til et Google Chat-rum, når en politik kører." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Vedhæft behandlede dokumenter til en Jira-issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Send et dokument eller en notifikation via email, når en politik kører." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Arkivér behandlede dokumenter i Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App-adgangskode" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Registrér persondata i dokumenttekst ved hjælp af en Presidio-server, du kører." +label = "Presidio (selvhostet)" + +[portal.connections.types.purview] +description = "Læs og anvend Microsoft Purview-følsomhedsetiketter på PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Valgfrit. Kun nødvendigt for at vælge etiketter efter navn; mærkning fungerer uden det." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stemples på hver etiket som dens SiteId, så Purview-aware-værktøjer ved, hvilken organisation der anvendte den." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "En S3 bucket, som kilder kan læse fra, og pipeline-output kan skrive til." +label = "S3-lager" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Lad stå tomt for AWS. Angiv dette for MinIO eller et andet S3-kompatibelt lager." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Send et dokument eller en notifikation via email, når en politik kører." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Poll en SFTP-server (SSH) for nye dokumenter." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Valgfrit. Serverens SHA-256 host key fingerprint (fra `ssh-keyscan host | ssh-keygen -lf -`). Når det er angivet, accepteres kun en server, der præsenterer denne nøgle; når det er tomt, fastgøres nøglen, der ses ved første forbindelse, og senere ændringer afvises." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Nøgle-passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Lad stå tomt, hvis du i stedet godkender med en privat nøgle." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Valgfrit. Indsæt en OpenSSH privat nøgle for at godkende med en nøgle i stedet for en adgangskode." +label = "Privat nøgle" + +[portal.connections.types.slack] +description = "Send en besked til Slack, når en politik behandler et dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Overvåg en SMB / CIFS-netværksdeling (Windows eller Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Netværksdeling" + +[portal.connections.types.smb.fields.domain] +helperText = "Valgfrit. Windows-domænet eller arbejdsgruppen, hvis din server bruger en." +label = "Domæne" + +[portal.connections.types.smb.fields.share] +label = "Deling" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Send en audit-hændelse til Splunk, hver gang en politik håndterer et dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Send en audit-hændelse til en Sumo Logic HTTP-collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Send en besked til en Teams-kanal, når en politik kører." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Send hvert dokument, eller kun detaljerne, til en URL, du vælger." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Udløs et Zap- eller Make-scenarie fra en politik." +label = "Zapier og Make" + +[portal.docs] +browse = "Gennemse docs" +viewSource = "Vis kilde på GitHub" + [portal.docs.authentication] codeCaption = "hver forespørgsel" eyebrow = "KOM GODT I GANG" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Medfølgende, navngivne kapabiliteter, som din agent kalder som ét enkelt værktøj. Hver skill er en deterministisk operationskæde med evals tilknyttet." title = "Agent-skills" +[portal.docs.toc] +title = "På denne side" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Type" user = "Bruger" [portal.documents.queue.empty] +connectSource = "Forbind en kilde" +createPipeline = "Opret en pipeline" description = "Når kilder sender dokumenter ind i dine pipelines, vises de her til gennemgang." title = "Ingen dokumenter i køen" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Ingen dokumenter matcher dette filter." rowActions = "Rækkehandlinger" sensitiveLabel = "Følsom" -sensitiveTitle = "Følsom — adgang påkrævet" [portal.documents.table.columns] action = "Pipeline / handling" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Status" time = "Tidspunkt" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Sidst set" region = "Region" status = "Status" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Denne visning ramte en uventet fejl. Prøv igen, eller vælg en a retry = "Prøv igen" title = "Noget gik galt på denne side" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Tilbage" +body = "Download desktop-appen, eller self-host serveren." +done = "Færdig" +guide = "Åbn {{name}}-guiden" +openInBrowser = "Åbn i browser i stedet" +sectionDesktop = "Desktop-app" +sectionSelfHosted = "Selvhostet" +title = "Installer Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Kør det officielle image på din server." +note = "Åbner på :8080. Standardlogin: admin / stirling." +tagline = "Kør serveren i en container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installer det officielle Helm chart." +note = "Tilføj en PVC til /configs for at bevare indstillinger." +tagline = "Udrul til en cluster med Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native builds til din distribution." +guideBtn = "Se Linux-installationsmuligheder" +note = "AppImage, .deb og .rpm builds er tilgængelige til almindelige distros." +tagline = "Native desktop-app til din distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Eller via Homebrew" +detailBody = "Kør installationsprogrammet, eller brug Homebrew." +downloadBtn = "Download til macOS" +tagline = "Native app til Apple silicon og Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Kør .jar på en bare metal-server." +downloadBtn = "Download .jar" +note = "Kræver Java 21+. Åbner på :8080; standardlogin admin / stirling." +runLabel = "Kør den derefter" +tagline = "Kør .jar på en bare metal-server" +title = "Manuel serveropsætning" + +[portal.home.download.windows] +altLabel = "Eller via winget" +detailBody = "Kør installationsprogrammet, eller brug en pakkehåndtering." +downloadBtn = "Download installationsprogram" +tagline = "Native app til Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktive" -invite = "Inviter teammedlemmer" +activeOfDeployed = "{{active}} af {{total}} aktive denne måned" name = "Stirling PDF Editor" open = "Åbn i browser" updated = "opdateret {{time}}" +[portal.home.editor.deploy] +finish = "Afslut udrulning" +options = "Se udrulningsmuligheder" +start = "Udrul Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Selvhostet · Docker" @@ -6823,49 +7529,6 @@ afternoon = "God eftermiddag" evening = "God aften" morning = "God morgen" -[portal.home.onboarding] -dismiss = "Afvis opsætning" -notStarted = "Ikke startet" -progress = "{{done}} af {{total}} færdig" -title = "Færdiggør opsætning" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC på organisationsniveau, forpligtende volumenpriser og air-gapped udrulning." -cta = "Start prøveperiode" -ctaQuote = "Få tilbud" -lead = "For 250+ medarbejdere." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Udrul desktopappen, gratis for altid." -title = "Download PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktive · {{recommended}} anbefalede" -chip = "{{active}} aktive" -title = "Slå politikker til" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} forbundne · hver PDF styres, hvor den lander" -chip = "{{connected}} forbundne" -title = "Forbind dine kilder" - -[portal.home.quickActions] -subtitle = "Vigtigste opgaver i dag" -title = "Hurtige handlinger" - -[portal.home.quickActions.buildPipeline] -blurb = "3-trins composer over det typede op-bibliotek" -title = "Byg en pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenter, webhooks, overvågede mapper" -title = "Forbind en kilde" - -[portal.home.quickActions.issueApiKey] -blurb = "Afgrænset nøgle med hastighedsgrænser og IP allowlist" -title = "Udsted en API-nøgle" - [portal.infrastructure] manageEditorDeployment = "Administrer Editor-udrulning" sectionsAriaLabel = "Infrastruktursektorer" @@ -6878,13 +7541,9 @@ heading = "API-nøgler" subheading = "Afgrænsede legitimationsoplysninger med hastighedsgrænser pr. nøgle, tilladelser og IP allowlists." [portal.infrastructure.apiKeys.card] -allowedIps = "Tilladte IP'er" -anyIp = "Enhver IP (ingen allowlist)" created = "Oprettet" lastUsed = "Sidst brugt" -permissions = "Tilladelser" -rateLimit = "Hastighedsgrænse" -rateLimitValue = "{{value}} req/min" +revoke = "Tilbagekald nøgle" usageMonth = "Forbrug denne måned" usageToday = "Forbrug i dag" @@ -6892,6 +7551,15 @@ usageToday = "Forbrug i dag" description = "Opret en afgrænset nøgle for at begynde at kalde Stirling API." title = "Ingen API-nøgler endnu" +[portal.infrastructure.apiKeys.error] +load = "Kunne ikke indlæse dine API-nøgler. Prøv igen." + +[portal.infrastructure.apiKeys.revoke] +body = "Tilbagekald \"{{name}}\"? Enhver integration, der stadig bruger denne nøgle, vil straks begynde at modtage 401-fejl. Dette kan ikke fortrydes." +cancel = "Annuller" +confirm = "Tilbagekald nøgle" +title = "Tilbagekald API-nøgle" + [portal.infrastructure.audit] filterAriaLabel = "Filtrer revisionshændelser efter kategori" heading = "Revisionslogge" @@ -6935,6 +7603,7 @@ all = "Alle" auth = "Godkendelse" config = "Konfiguration" elevation = "Forhøjet adgang" +policy = "Politik" processing = "Behandling" security = "Sikkerhed" @@ -6944,19 +7613,30 @@ title = "Du har ikke adgang" [portal.infrastructure.audit.metrics] config = "Konfiguration" -elevation = "Forhøjet adgang" +policy = "Politik" processing = "Behandling" totalEvents = "Hændelser i alt · 24 t" +[portal.infrastructure.auditCatLabel] +auth = "Godkendelse" +config = "Konfiguration" +elevation = "Ophøjelse" +policy = "Politik" +processing = "Behandling" +security = "Sikkerhed" + +[portal.infrastructure.auditStatusLabel] +danger = "Fejl" +info = "Info" +success = "Succes" +warning = "Advarsel" + [portal.infrastructure.createKey] cancel = "Annuller" createKey = "Opret nøgle" done = "Færdig" -ipAllowlistHelper = "Kommaseparerede CIDR-intervaller. Lad stå tomt for at tillade enhver IP." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Nøglenavn" keyNamePlaceholder = "f.eks. Produktion · ingest" -permissionsLabel = "Tilladelser" secretKeyCaption = "Hemmelig nøgle" secretWarning = "Gem dette i en secrets manager. Stirling gemmer kun nogensinde en hash — der er ingen måde at gendanne den senere på." subtitle = "Afgræns nøglen til det minimum, den behøver. Du kan rotere eller tilbagekalde den når som helst." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopiér denne hemmelighed nu — den vises ikke igen." title = "Opret API-nøgle" titleCreated = "Nøgle oprettet" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Udrullet af" -environment = "Miljø" -product = "Produkt" -status = "Status" -version = "Version" -when = "Hvornår" - -[portal.infrastructure.deployments.recent] -heading = "Seneste udrulninger" -subheading = "De seneste udrulninger på tværs af produkter og miljøer." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanser" -latency = "Latens" -load = "Belastning" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Gennemløb" -uptime = "Oppetid" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regioner" -subheading = "Live tilstand for hver udrullet Stirling-region — latens, belastning og udrulningsversion." - -[portal.infrastructure.deployments.regions.empty] -description = "Udrullede regioner vises her, når dit arbejdsområde er provisioneret." -title = "Ingen regioner udrullet" - -[portal.infrastructure.models] -heading = "Modeller" -msValue = "{{value}} ms" -subheading = "Modelkataloget og routingen, der driver dokumentbehandling på tværs af dit arbejdsområde." - -[portal.infrastructure.models.byom] -description = "Registrer en on-prem- eller selvhostet model, og fastgør den til en region til behandling bundet af dataplacering." -title = "Brug din egen model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Administrerede modeller tilgængelige for dit arbejdsområde med live latens og omkostning." -subEnterprise = "Administrerede, bring-your-own og on-prem-modeller — med fastgørelse pr. region tilgængelig." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeller i dit arbejdsområdes katalog vises her." -title = "Ingen modeller tilgængelige" - -[portal.infrastructure.models.columns] -cost = "Omkostning" -latency = "Latens" -load = "Belastning" -model = "Model" -status = "Status" -type = "Type" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "Aktive modeller" -avgLatency = "Gns. latens" -included = "Inkluderet" -monthlySpend = "Månedligt modelforbrug" - -[portal.infrastructure.models.routing] -empty = "Ingen routingregler konfigureret." -heading = "Routingregler" -sub = "Hvilken model håndterer hver operation. Standarden anvendes, når ingen snævrere regel matcher." -subLocked = "Route operationer til specifikke modeller — tilgængeligt på betalte planer." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Opgrader til Pro for at styre, hvilken model der håndterer hver operation og dokumenttype." -title = "Model-routing er en betalt funktion" - -[portal.infrastructure.models.routingColumns] -default = "Standard" -docType = "Dokumenttype" -modelForAria = "Model for {{operation}}" -operation = "Operation" -routedTo = "Routet til" - -[portal.infrastructure.security.access.byok] -description = "Lever en nøgle fra dit eget KMS. Stirling krypterer med den, men kan stadig læse." -label = "Brug din egen nøgle (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Nøgler forlader aldrig dit KMS. Stirling har kun ciphertext." -label = "Behold din egen nøgle (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling administrerer krypteringsnøgler. Enklest — nul nøgleoperationer fra din side." -label = "Stirling-holdte nøgler" - -[portal.infrastructure.security.accessPolicy] -heading = "Politik for dokumentadgang" -subheading = "Styrer, hvem der kan dekryptere behandlede dokumenter i hvile." - -[portal.infrastructure.security.attestations] -heading = "Complianceattestationer" -noReport = "Ingen rapport tilgængelig" -subheading = "Revisionsstatus framework for framework med rapporter tilgængelige for attesterede kontroller." -viewReport = "Se rapport →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestationer og certificeringer, der dækker Stirling-platformen." - -[portal.infrastructure.security.empty] -description = "Dit arbejdsområdes sikkerhedskonfiguration vises her." -title = "Sikkerhedsstatus utilgængelig" - -[portal.infrastructure.security.hyokBanner] -description = "Med HYOK forlader krypteringsnøgler aldrig dit KMS. Stirling gemmer og behandler kun ciphertext, som du kan tilbagekalde når som helst." -title = "Stirling kan ikke dekryptere dine dokumenter" - -[portal.infrastructure.security.ipAllowlist] -empty = "Ingen IP-intervaller konfigureret — alle IP'er tilladt." -heading = "IP allowlist" -sub = "API-adgang er begrænset til disse CIDR-intervaller." -subLocked = "Begræns API-adgang til kendte IP-intervaller — tilgængeligt på betalte planer." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Opgrader til Pro for at begrænse API-adgang til specifikke netværk." -title = "IP allowlisting er en betalt funktion" - -[portal.infrastructure.security.ipColumns] -added = "Tilføjet" -addedBy = "Tilføjet af" -cidr = "CIDR" -label = "Etiket" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritme" -heading = "Administration af krypteringsnøgler" -keyId = "Nøgleidentifikator" -lastRotated = "Sidst roteret" -rotateKey = "Rotér nøgle" -rotationPolicy = "Rotationspolitik" -subheading = "Forvaltning af nøglerne, der krypterer dokumenter i hvile — hvem der kan dekryptere, og hvordan nøgler roteres." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) og hold-your-own-key (HYOK) forvaltning er tilgængelige på Enterprise. Opgrader for at levere nøgler fra dit eget KMS." -title = "Nøgler administreres af Stirling på din plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asien-Stillehavsområdet" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR-datagrænse" -label = "Den Europæiske Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "USA" - -[portal.infrastructure.security.residencyHeader] -heading = "Dataplacering" -subheading = "Hvor dokumenter gemmes og behandles." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} brugt" - -[portal.infrastructure.storage.empty] -description = "Forbundet lager og forbrug vises her." -title = "Intet lager konfigureret" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktiv" -activeRange = "0–{{value}}d" -archived = "Arkiveret" -coldStorage = "kold lagring" -deleted = "Slettet" -never = "aldrig" -purged = "renset" - -[portal.infrastructure.storage.providers] -connect = "Forbind" -connected = "Forbundet" -heading = "Forbundne udbydere" -subheading = "Hvor behandlede artefakter skrives." - -[portal.infrastructure.storage.retention] -heading = "Opbevaring" -subheading = "Hvor længe artefakter bevares før livscyklussletning." -windowLabel = "Standard opbevaringsvindue" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dag" -days_other = "{{count}} dage" -never = "Slet aldrig" - -[portal.infrastructure.storage.totalUsage] -heading = "Samlet forbrug" -progressLabel = "Lager brugt" -subheading = "Lager forbrugt på tværs af alle forbundne udbydere." +revoked = "Tilbagekaldt" [portal.infrastructure.tabs] apiKeys = "API-nøgler" @@ -7174,6 +7656,49 @@ models = "Modeller" security = "Sikkerhed" storage = "Lager" +[portal.integrations] +availableHeading = "Tilgængelige" +comingSoonHeading = "Kommer snart" +connect = "Forbind" +connectionCount_one = "{{count}} forbindelse" +connectionCount_other = "{{count}} forbindelser" +customApi = "Brugerdefineret API" +searchPlaceholder = "Søg efter integrationer" +subtitle = "Alt, Stirling kan tale med. Legitimationsoplysninger oprettes én gang her og refereres derefter af kilder, politikker og pipelines." +title = "Integrationer" +typedName = "{{tool}}-navn" + +[portal.integrations.filters] +all = "Alle" +audit = "Audit" +connected = "Forbundet" +notify = "Giv besked" +security = "Sikkerhed" +signing = "Signering" +storage = "Lager" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Forbundet" + +[portal.integrations.table] +integration = "Integration" +worksWith = "Fungerer med" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Politikker" +sources = "Kilder" + +[portal.legal] +draft = "{{label}} · udkast" +loadError = "Kunne ikke indlæse dette dokument. Prøv igen." +loading = "Indlæser…" +title = "Juridisk dokument" + [portal.nav] agent-builder = "Agent Builder" components = "Komponenter" @@ -7182,33 +7707,17 @@ documents = "Dokumenter" editor = "Editor" home = "Hjem" infrastructure = "Infrastruktur" +integrations = "Integrationer" pipelines = "Pipelines" policies = "Politikker" procurement = "Indkøb" -settings = "Indstillinger" sources = "Kilder" usage = "Forbrug og fakturering" users = "Brugere" -[portal.notifications] -markAllRead = "Markér alle som læst" -title = "Notifikationer" -viewAll = "Vis alle" - -[portal.notifications.ariaLabel] -none = "Notifikationer, ingen ulæste" -unread_one = "Notifikationer, {{count}} ulæst" -unread_other = "Notifikationer, {{count}} ulæste" - -[portal.notifications.count] -allRead = "alle læst" -loading = "indlæser" -new_one = "{{count}} ny" -new_other = "{{count}} nye" - -[portal.notifications.empty] -description = "Ingen nye notifikationer." -title = "Du er helt ajour" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Hver automatiseret dokumentpipeline på backenden: en ordnet kæde af operationer over et sæt kilder, kørt på en trigger. Klik på en række for dens trin og kilder." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Ny pipeline" [portal.pipelines.builder] -addStep = "Tilføj værktøj" +activate = "Activate" back = "Tilbage til pipelines" +cannotFollow = "Kan ikke tage {{produced}}" +chooseAccount = "Vælg en konto" +chooseDestination = "Vælg en destination" +chooseOperation = "Vælg, hvad dette trin gør" +chooseSource = "Vælg en kilde" discard = "Kassér ændringer" -enabled = "Aktiveret" +inputs = "Input" +inputSource = "Inputkilde" +inputTrigger = "Trigger" keepEditing = "Fortsæt redigering" -needsUpload = "Kræver en uploadet fil" +moreActions = "More actions" +needsConfiguring = "Skal opsættes" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Ingen værktøjer matcher din søgning." -pipelineSettings = "Pipelineindstillinger" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Søg i værktøjer" -selectToolBody = "Tilføj et værktøj for at bygge din pipeline." -selectToolTitle = "Ingen værktøjer endnu" -toolSettings = "Værktøjsindstillinger" +sendToSystem = "Send til et andet system" +stepsIncompatible = "Disse trin kan ikke køre på det, deres forrige trin producerer: {{tools}}." +stepsNeedSetup = "Disse trin skal stadig opsættes før lagring: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Ukendt operation, bevaret som den er." unsavedBody = "Du har ændringer, der ikke er gemt. Gem dem, før du forlader siden, eller kassér dem?" unsavedTitle = "Ikke-gemte ændringer" -uploadUnsupported = "Uploadede filer understøttes endnu ikke i pipelines, så disse trin kan ikke gemmes: {{tools}}." usesDefaults = "Kører med standardindstillinger" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinerer alle filer fra det forrige trin" +fan-out = "Kører én gang pr. fil fra det forrige trin" +format-mismatch = "Kræver {{accepts}}, men det forrige trin producerer {{produced}}" +output-uncertain = "Kører måske ikke: outputtet fra det forrige trin afhænger af, hvordan det er opsat" +source-mismatch = "Kræver {{accepts}}, men denne pipelines input er {{produced}}" +undeclared-operation = "Kan ikke kontrollere, hvad dette trin accepterer" [portal.pipelines.composer] addTool = "Tilføj værktøj" -cancel = "Annuller" -chainEmpty = "Tilføj et værktøj for at begynde at bygge din pipeline." create = "Opret pipeline" -directory = "Outputmappe" -directoryHelp = "Absolut sti på serveren. Skal være inden for de konfigurerede tilladte mapper." +createPaused = "Create paused" editingUnsupported = "Visning af disse værktøjsparametre til redigering understøttes endnu ikke." -moveDown = "Flyt ned" -moveUp = "Flyt op" +editSource = "Edit source" name = "Navn" namePlaceholder = "f.eks. Redaction sweep" -noSources = "Ingen kilder forbundet endnu. Pipelinen kan stadig køre på filer, der leveres direkte til den." noToolSettings = "Dette værktøj har ingen konfigurerbare indstillinger." -operations_one = "Operation ({{count}})" -operations_other = "Operationer ({{count}})" output = "Output" -removeStep = "Fjern operation" save = "Gem ændringer" scheduleEvery = "Kør hver" -sources = "Kilder" -sourcesLoading = "Indlæser kilder..." trigger = "Trigger" triggerManual = "Kun manuel" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dage" hours = "timer" minutes = "minutter" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Slet \"{{name}}\"? Dette kan ikke fortrydes." cancel = "Annuller" @@ -7273,28 +7815,62 @@ confirm = "Slet" title = "Slet pipeline?" [portal.pipelines.detail] +clearHistory = "Ryd historik" delete = "Slet pipeline" run = "Kør nu" [portal.pipelines.empty] action = "Opret en pipeline" +connectSource = "Forbind en kilde" description = "Opret din første pipeline: vælg de kilder, den kører over, kæd operationerne sammen, og vælg, hvor output skal placeres." title = "Ingen pipelines endnu" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktive" paused = "Sat på pause" total = "Pipelines" -[portal.pipelines.output] -folder = "Skriv til mappe" -inline = "Returner filer" - [portal.pipelines.run] +allProcessed_one = "Intet at køre: kildens {{count}} dokument er allerede behandlet." +allProcessed_other = "Intet at køre: alle {{count}} dokumenter i kilderne er allerede behandlet." completed_one = "Kørsel fuldført." completed_other = "Alle {{count}} kørsler fuldført." empty = "Intet at køre: kilderne havde ingen dokumenter at behandle." failed = "Kørsel mislykkedes: {{error}}" +inFlight = "Intet nyt at køre: dokumenter behandles stadig fra en tidligere kørsel." +parked_one = "Intet at køre: {{count}} dokument fejlede tidligere og er parkeret. Ret årsagen, og ryd derefter historikken for at prøve igen." +parked_other = "Intet at køre: {{count}} dokumenter fejlede tidligere og er parkeret. Ret årsagen, og ryd derefter historikken for at prøve igen." running = "Kørsel startet; stadig i gang." timeout = "Kørslen tager længere end forventet; den kan stadig blive færdig i baggrunden." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Kilder" status = "Status" steps = "Trin" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Mappeovervågning" @@ -7314,12 +7891,18 @@ manual = "Manuel" schedule = "Planlagt" [portal.policies] +defaultName = "{{category}}-politik" subtitle = "Faste automatiseringer, der håndhæver en værktøjspipeline på hvert dokument. Hver politik udløses ved upload eller eksport, kører sin værktøjskæde og gemmer den håndhævede version ved siden af originalen." title = "Politikker" [portal.policies.card] comingSoon = "Opgrader til Enterprise" notSetUp = "Ikke opsat" +requiresAiEngine = "Kræver AI-motor" + +[portal.policies.categories.classification] +desc = "Identificer hvert dokuments type ved upload, og tag dets metadata til arkivering og søgning." +label = "Klassifikation" [portal.policies.categories.compliance] desc = "Håndhæv HIPAA-, GDPR-, SOC 2- eller FedRAMP-krav på hvert dokument." @@ -7344,6 +7927,13 @@ label = "Sikkerhed" [portal.policies.config] scopeAll = "Alle dokumenter" +[portal.policies.config.classification] +summary = "Klassificerer hvert uploadet dokument mod dit teams labels og tagger dets metadata." + +[portal.policies.config.classification.rules] +0 = "Klassificer" +1 = "Tag metadata" + [portal.policies.config.compliance] summary = "Validerer dokumenter mod regulatoriske frameworks, før de forlader systemet." @@ -7371,6 +7961,29 @@ minConfidence = "Min. konfidens" 2 = "Navngiv" 3 = "Normaliser" +[portal.policies.config.purview] +label = "Anvend en Microsoft Purview-følsomhedslabel" + +[portal.policies.config.purview.fields] +connection = "Purview-tenant" +labelId = "Label ID" +labelIdHelp = "Labelens GUID fra Purview-portalen. Anvendelse af en label skriver dens metadata på PDF'en; den kan ikke kryptere, hvilket kræver Microsoft-klienten." +labelName = "Labelnavn (valgfrit)" +labelNameHelp = "Registreres sammen med ID'et, så personer og værktøjer kan genkende labelen." + +[portal.policies.config.purview.fields.method] +help = "Purview skelner mellem en label, som en politik anvendte, og en, som en person valgte." +label = "Hvordan labelen registreres" +privileged = "Som valgt af en person" +standard = "Som anvendt automatisk" + +[portal.policies.config.purviewRead] +label = "Læs en Microsoft Purview-label" + +[portal.policies.config.purviewRead.fields] +connection = "Purview-tenant" +connectionHelp = "Læser den label, dokumentet allerede har, så senere trin kan handle på den." + [portal.policies.config.retention] summary = "Håndhæver, hvor længe dokumenter bevares, hvornår de arkiveres, og hvornår de slettes." @@ -7438,6 +8051,7 @@ title = "Ingen aktivitet endnu" [portal.policies.endpoints] addWatermark = "Vandmærke" autoRedact = "Rediger PII" +classifyAndLabel = "Klassificer" compressPdf = "Komprimer" flatten = "Udflad" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Dine politikker er gemt og vises, når forbindelsen er gendannet. retry = "Prøv igen" title = "Backend utilgængelig" +[portal.policies.operations] +change = "Ændr, hvad dette trin gør" +noResults = "Ingen trin matcher det. Prøv et produktnavn eller \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Søg efter trin - prøv \"scan\", \"attach\" eller et produktnavn" + +[portal.policies.operations.bodyMode] +binary = "Rå filbytes" +json = "Inde i en JSON-payload" +multipart = "Som en filupload (multipart)" + +[portal.policies.operations.clamavScan] +description = "Din egen ClamAV-server kontrollerer dokumentet; intet forlader dit netværk." +label = "Scan for virus (selvhostet)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Afviser dokumenter, der indeholder makroer, eksekverbare filer, scripts eller indlejrede objekter." +label = "Bloker aktivt indhold" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive kontrollerer dokumentet, og kørslen stopper, hvis det ikke er rent." +label = "Scan for virus" + +[portal.policies.operations.confluenceAttach] +description = "Arkiverer det behandlede dokument på en side." +label = "Vedhæft til en Confluence-side" + +[portal.policies.operations.consignoSubmit] +description = "Indsender dokumentet til ConsignO Cloud til signering." +label = "Send til signatur (ConsignO)" +note = "Indsender dokumentet. Hentning af den signerede kopi understøttes endnu ikke, fordi et trin ikke kan videregive et resultat til et senere trin." + +[portal.policies.operations.customApiCall] +description = "Udform selv kaldet: sti, metode, body og håndtering af svar." +label = "Kald en brugerdefineret API" + +[portal.policies.operations.discordNotify] +description = "Fortæller en kanal, at politikken håndterede et dokument." +label = "Post en besked til Discord" + +[portal.policies.operations.elasticIndex] +description = "Registrerer, hvad politikken gjorde, som et søgbart dokument." +label = "Indekser en audit-hændelse i Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart passer til upload-API'er; JSON passer til API'er, der vil have filen inde i en payload; binær sender de rå bytes." +label = "Sådan sendes dokumentet" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "En JSON-body sendt som den er. {{document.base64}} indeholder filen; {{document.*}} og {{run.*}} udfyldes pr. dokument." +label = "JSON-body" + +[portal.policies.operations.fields.connection] +label = "Konto" + +[portal.policies.operations.fields.domain] +label = "Mailgun-domæne" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Formularfeltet, som API'en forventer dokumentet under." +label = "Filfeltnavn" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Fra" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Et JSON-objekt med header-navn/værdi-par. Forbindelsens legitimationsoplysninger tilføjes for dig." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Sagsnøgle" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Sendes som beskedens body. {{document.*}} og {{run.*}} udfyldes pr. dokument." +label = "Besked" + +[portal.policies.operations.fields.method] +label = "Metode" + +[portal.policies.operations.fields.pageId] +label = "Side-ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Tilføjes til forbindelsens base-URL. Den kan aldrig nå en anden host." +label = "Sti" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Hvor på drevet den skal skrives. {{document.filename}} udfyldes pr. dokument." +label = "Destinationssti" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapport efterlader dokumentet urørt. Erstatning bytter det ud med det, der kommer tilbage." +label = "Hvad der skal gøres med svaret" + +[portal.policies.operations.fields.signerEmail] +label = "Underskriverens email" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Emne" +placeholder = "Behandlet: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyserer denne tekst. Brug {{document.title}}, eller indsæt den tekst, du vil have kontrolleret." +label = "Tekst til kontrol" + +[portal.policies.operations.fields.to] +label = "Til" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud-brugernavn" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Fortæller et space, at politikken håndterede et dokument." +label = "Post en besked til Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arkiverer det behandlede dokument på en sag." +label = "Vedhæft til en Jira-sag" + +[portal.policies.operations.mailgunEmail] +description = "Sender det behandlede dokument som en vedhæftning." +label = "Send dokumentet via email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Skriver det behandlede dokument til en mappe." +label = "Upload til Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Din egen Presidio-server rapporterer de persondata, den finder." +label = "Registrer persondata" +note = "Presidio læser tekst, ikke filer. Giv den den tekst, du vil have kontrolleret — selve dokumentet uploades ikke." + +[portal.policies.operations.responseMode] +replace = "Erstat dokumentet med svaret" +report = "Behold dokumentet, registrer svaret" + +[portal.policies.operations.sendgridEmail] +description = "Sender det behandlede dokument som en vedhæftning." +label = "Send dokumentet via email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Fortæller en kanal, at politikken håndterede et dokument." +label = "Post en besked til Slack" + +[portal.policies.operations.splunkEvent] +description = "Registrerer, hvad politikken gjorde, hvor dine auditører allerede kigger." +label = "Send en audit-hændelse til Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registrerer, hvad politikken gjorde, til en HTTP-collector." +label = "Send en audit-hændelse til Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Fortæller en kanal, at politikken håndterede et dokument." +label = "Post en besked til Teams" + +[portal.policies.operations.webhookPost] +description = "Sender selve dokumentet til en URL, du vælger." +label = "Post dokumentet til en webhook" + +[portal.policies.operations.zapierNotify] +description = "Overdrager kørselsdetaljerne til din automatisering." +label = "Udløs et Zap- eller Make-scenarie" + [portal.policies.stats] activeFor = "Aktiv" dataProcessed = "Data behandlet" @@ -7473,6 +8282,13 @@ label = "Dokumenter håndhævet" description = "Konfigureret, men ikke aktiv" label = "Sat på pause" +[portal.policies.table] +appliesTo = "Gælder for" +docs = "Docs håndhævet" +enforces = "Håndhæver" +policy = "Politik" +status = "Status" + [portal.policies.wizard.actions] back = "Tilbage" cancel = "Annuller" @@ -7480,6 +8296,10 @@ continue = "Fortsæt" enablePolicy = "Aktivér politik" saveChanges = "Gem ændringer" +[portal.policies.wizard.capability.classify] +desc = "Identificerer dokumentets type ud fra dit teams labels og tagger det, så det arkiveres og søges efter kategori." +label = "Klassificer dokumentet" + [portal.policies.wizard.capability.compress] desc = "Komprimerer dokumentet til en mindre filstørrelse." label = "Reducer filstørrelse" @@ -7500,10 +8320,18 @@ label = "Rediger følsomme oplysninger" desc = "Fjerner skjult JavaScript, så intet kan køre automatisk, når dokumentet åbnes." label = "Fjern aktivt indhold" +[portal.policies.wizard.capability.timestampPdf] +desc = "Beviser, at dokumentet fandtes i præcis denne form på et bestemt tidspunkt, ved hjælp af en uafhængig tidsstempelmyndighed. Kun en hash sendes - dokumentet forlader aldrig din server." +label = "Tilføj et betroet tidsstempel" + [portal.policies.wizard.capability.watermark] desc = "Sætter et synligt mærke (f.eks. “Fortroligt”) på hver side." label = "Anvend et vandmærke" +[portal.policies.wizard.classification] +description = "Hvert uploadet dokument klassificeres mod de indbyggede labels og tagges med de typer, der passer. Labelsættet deles på tværs af hele dit team." +labelsHeading = "Klassifikationslabels" + [portal.policies.wizard.errors] noTools = "Aktivér først mindst ét værktøj i workflowet." saveFailed = "Kunne ikke gemme politikken. Prøv igen." @@ -7548,73 +8376,118 @@ setUp = "Opsæt {{category}}-politik" [portal.policies.wizard.workflow] description = "Vælg, hvad denne politik gør ved hvert dokument, den behandler." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktive" -noRule = "Ingen regel håndhævet endnu" -subtitle = "Faste automatiseringer, som hvert dokument passerer gennem, uanset hvilken pipeline der håndterer det." -title = "Hvad der kører på dine PDFs" - [portal.policySummary.action] -comingSoon = "Opgrader til Enterprise" -configure = "Konfigurer" setUp = "Opsæt" -[portal.policySummary.column] -activeRule = "Aktiv regel" -policy = "Politik" -status = "Status" +[portal.processorFlow] +footnote = "Optællinger er for de seneste 24 timer. Flowhastighed er illustrativ." +liveBadge = "Live" +stats = "{{connected}} forbundet · {{processed}} PDF'er behandlet" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Når politikker er konfigureret, vises kategorierne her." -title = "Ingen politikker endnu" +[portal.processorFlow.lens] +ariaLabel = "Visningstilstand" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktiv" -off = "Fra" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 t" +failed = "Mislykkedes" +heading = "Resultater" +success = "Leveret" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktive" +count = "{{n}} · 24 t" +heading = "Politikker" +setUp = "Opsæt" soon = "Snart" -[portal.processingStatus] -managePlan = "Administrer abonnement" -volumeSuffix = "PDFs behandlet · sidste 30 dage" +[portal.processorFlow.sankey] +waist = "Politikker · {{n}} aktive" + +[portal.processorFlow.sankey.empty] +description = "Forbind en kilde, og slå en politik til for at se dokumenter flyde gennem processoren." +title = "Intet flow endnu" + +[portal.processorFlow.sources] +comingSoonTag = "Forbind" +editor = "Stirling PDF Editor" +heading = "Kilder" +perDay = "{{n}} / 24 t" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email-indtag" [portal.procurement] -reset = "Nulstil indkøb (demo)" subtitle = "Få dit team evalueret, kontraheret og onboardet. Alle dokumenter samlet ét sted." title = "Indkøb" -[portal.procurement.action] -download = "Download" -pay = "Betal nu" -request = "Anmod" -sign = "Gennemgå og underskriv" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Accepter og abonnér" +confidential = "Fortroligt" confirm = "Jeg har læst og accepterer Stirling Enterprise Agreement." -eyebrow = "Aftale" -intro = "Én samlet aftale dækker din aftale: Master Service Agreement, Order Form, EULA og Data Processing Agreement. Gennemgå den, og accepter derefter tilbuddet som et bindende abonnement." -title = "Gennemgå din enterprise-aftale" +docName = "Stirling Enterprise-aftale" +docSub = "MSA, Order Form, EULA og DPA, samlet i én signatur." +download = "Download" +downloadDraftError = "Kunne ikke generere aftale-PDF'en - dokumentrendereren er utilgængelig. Prøv igen om lidt, eller kontakt support." +downloadError = "Kunne ikke downloade den signerede aftale. Prøv igen." +legalName = "Juridisk enhedsnavn" +legalNamePlaceholder = "Den juridiske enhed, der skal underskrive" +loadError = "Kunne ikke indlæse aftalen. Prøv igen." +loading = "Indlæser aftalen..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Anmod om ændringer" +scrollHint = "Rul for at gennemgå" +signatory = "Underskriverens navn" +signatoryPlaceholder = "Fulde navn" +signatoryTitle = "Underskriverens titel" +signatoryTitlePlaceholder = "f.eks. General Counsel" +signError = "Kunne ikke registrere din signatur. Prøv igen." [portal.procurement.builder] addons = "Tilføjelser" +addressLine1 = "Adresselinje 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresselinje 2" +addressLine2Placeholder = "Suite, etage, bygning" back = "Tilbage" businessName = "Virksomhedsnavn" businessNamePlaceholder = "Din virksomhed" +city = "By" +cityPlaceholder = "San Francisco" +completeRequired = "Udfyld de påkrævede felter (markeret *) med en gyldig email, før tilbuddet genereres." +contactEmail = "Kontakt-email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktnavn" +contactNamePlaceholder = "Jane Doe" continue = "Fortsæt" -country = "Land" -countryEuro = "Eurozonen (EUR €)" -countryUK = "Storbritannien (GBP £)" -countryUS = "USA (USD $)" +done = "Færdig" eula = "Jeg har læst og accepterer Stirling Enterprise EULA. Den regulerer den aftale, der genereres ud fra dette tilbud." generate = "Generer tilbud" -included = "Inkluderet" indemnification = "IP-skadesløsholdelse" indemnificationSub = "Vi forsvarer kvalificerende IP-krav i henhold til EULA" -offlineLicense = "Offline / air-gapped licens" -offlineLicenseSub = "En downloadbar licensfil til en air-gapped self-hosted instans" +paperEyebrow = "Enterprise-tilbud" +paperFor = "Udarbejdet til" +pdfSize = "PDF-størrelse" +poNumber = "PO-nummer" +poNumberPlaceholder = "Valgfrit" +postalCode = "Postnummer" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} politikker" +posture_essentials = "Essentials" +posture_essentialsSub = "Klassifikation + Deling — standardindstillingerne" +posture_governed = "Governed" +posture_governedSub = "Tilføjer Sikkerhed og Routing" +posture_regulated = "Regulated" +posture_regulatedSub = "Hver kategori — Compliance, Retention, Ingestion" qbr = "Kvartalsvise forretningsgennemgange" qbrSub = "Din SE gennemgår brug og roadmap hvert kvartal" +region = "Stat / region" +regionPlaceholder = "California" running = "{{annual}} / år · {{years}} år {{tcv}}" s1Sub = "Dit team og de PDFs, I forventer at køre hvert år." s1Title = "Hvor meget vil I behandle?" @@ -7623,6 +8496,12 @@ s2Title = "Forpligtelse og service" s3Sub = "Til tilbuddet og den aftale, det genererer." s3Title = "Dine oplysninger" serviceLevel = "Serviceniveau" +size_compact = "Kompakt" +size_compactSub = "Mest tekst, under 1 MB" +size_heavy = "Tung" +size_heavySub = "Scannet eller billedtung, 5 MB+" +size_standard = "Standard" +size_standardSub = "Blandet tekst og billeder, 1 til 5 MB" slDedicated = "Dedikeret" slDedicatedSub = "4 arbejdstimer · dedikeret account manager · +30%" slPriority = "Prioritet" @@ -7630,6 +8509,8 @@ slPrioritySub = "Samme arbejdsdag · navngiven CSM · +15%" slStandard = "Standard" slStandardSub = "Næste arbejdsdag · delt CSM · inkluderet" stepOf = "Trin {{n}} af {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Valgfrit" term = "Periode" termDiscount = "{{pct}}% rabat for flerårig forpligtelse anvendt" title = "Byg dit tilbud" @@ -7637,6 +8518,7 @@ training = "Onboarding og træning" trainingSub = "Live-sessioner til at få dit team i gang" users = "Samlet antal brugere" usersPlaceholder = "f.eks. 250" +viewEula = "Læs den" volEstimated = "Estimeret ud fra {{count}} brugere (~2.000 PDFs hver, inklusive automatisering). Rediger, hvis du ved bedre." volManual = "Bruger dit tal. Genberegn ud fra teamstørrelsen når som helst." volNoUsers = "Ikke sikker? Indtast din teamstørrelse, så estimerer vi det." @@ -7645,55 +8527,55 @@ volumePlaceholder = "f.eks. 1.000.000" years_one = "{{count}} år" years_other = "{{count}} år" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumenter" -done = "Færdig" -here = "Du er her" -hide = "Skjul" -optional = "Valgfrit" -paidAddon = "Betalt tilføjelse" -show = "Vis" -subtitle = "Alt, hvad du har brug for på hvert trin af rejsen, vises efterhånden som aftalen bevæger sig gennem den." -supportingSubtitle = "SOC 2, sikkerhedsgennemgange, skatteformularer og mere, klar når dit sikkerheds- eller indkøbsteam spørger. Nogle har et engangsgebyr." -supportingTitle = "Understøtter din evaluering" +[portal.procurement.documents] +agreement = "Enterprise-aftale" +agreementSub = "MSA, Order Form og DPA i én signatur." +download = "Download" +eula = "EULA & Commercial Terms" +eulaSub = "Softwaren og de kommercielle self-serve-vilkår." +invoice = "Faktura" +invoiceSub = "Din første abonnementsfaktura." +laterInvoice = "Tilgængelig efter du accepterer" +laterQuote = "Tilgængelig når dit tilbud er udstedt" +quote = "Tilbud" +quoteSub = "Dit specificerede enterprise-tilbud." +sla = "SLA-bilag" +slaSub = "Supportniveauer og svartidsmål." +subprocessors = "Underdatabehandlere" +subprocessorsSub = "Tredjeparter, der behandler data på dine vegne." +subtitle = "Din aftaledokumentation, tilgængelig når som helst." title = "Dokumenter" -upcoming = "Kommende" +view = "Vis" [portal.procurement.error] title = "Noget gik galt" [portal.procurement.hero] -company = "Din enterprise-aftale" +barAria = "Fase {{current}} af {{total}}" ctaAgreement = "Gennemgå og underskriv aftale" -ctaLive = "Du er live" -ctaPayment = "Tilføj betaling" +ctaExploring = "Opsæt din prøveperiode" ctaQuote = "Gennemgå dit tilbud" +ctaReviewQuote = "Gennemgå tilbud" ctaTrial = "Byg dit tilbud" +documents = "Dokumenter" eyebrow = "Enterprise-indkøb" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Inviter teammedlemmer" -keyDocs = "Nøgledokumenter" -nextStep = "Næste trin: {{action}}" -notStarted = "Ikke startet" +licenseKey = "Licensnøgle" +liveSub = "Din organisation er provisioneret, og din licens er aktiv." +liveTitle = "Du er live på Stirling Enterprise" +next = "Næste: {{stage}}" open = "Åbn indkøb" scheduleCall = "Planlæg et opkald" -setup1Sub = "Inviter dine teammedlemmer" -setup1Title = "Udrul PDF Editor" -setup2Sub = "Slå behandling til på tværs af editorer og andre kilder" -setup2Title = "Forbind PDF Processor" -setup3Sub = "Slå Sikkerhed, Compliance, Routing eller Opbevaring til, når du har brug for dem" -setup3Title = "Tilføj anbefalede politikker" +sentenceAgreement = "gennemgå og underskrive aftalen" +sentenceLive = "provisionering og udrulning" +sentencePayment = "købsordre og betaling" +sentenceQuote = "gennemgå og acceptere dit tilbud" +sentenceTrial = "opsætte dit prøvearbejdsområde" [portal.procurement.journey] daysLeft_one = "{{count}} dag tilbage" daysLeft_other = "{{count}} dage tilbage" -engineerLabel = "Din solutions engineer" -eyebrow = "Din udrulning" -live = "Du er live på Stirling Enterprise" -nextStep = "Næste trin: {{action}}" -subtitle = "Din solutions engineer er med på hvert trin. Én næste handling ad gangen; den fulde tjekliste er nedenfor." -title = "Fra prøveperiode til live, én guidet vej" -trialTitle = "Enterprise-prøveperiode" [portal.procurement.journeySteps.agreement] blurb = "Én underskrift dækker MSA, order form, EULA og DPA." @@ -7727,6 +8609,9 @@ downloadError = "Kunne ikke generere offline licensfilen endnu — prøv igen om downloadOffline = "Download offline licens (.lic)" hint = "Indsæt denne nøgle i en self-hosted instans for at aktivere den, eller gem den til dine optegnelser. Opbevar den sikkert." label = "Din licensnøgle" +subtitle = "Aktivér en selvhostet instans med denne nøgle, eller gem den til dine optegnelser." +title = "Din licensnøgle" +trialFileHint = "Dette er din prøvelicensfil. Når din aftale er på plads, skal du komme tilbage og downloade den igen — i modsætning til den online licensnøgle opdateres .lic-filen ikke af sig selv." [portal.procurement.link] cta = "Tilknyt konto" @@ -7739,65 +8624,62 @@ description = "Dit abonnement er aktivt, og din licens er udstedt. Dit team er k eyebrow = "Live" title = "Du er live på Stirling Enterprise" -[portal.procurement.locked] -description = "Prøvenøgler, committed-volume-tilbud, aftalen med én underskrift, betaling og din dokumentoversigt ligger alle her, når du starter en enterprise-evaluering." -eyebrow = "Kun Enterprise" -talkToSales = "Tal med salg" -title = "Indkøbssporet åbner med Enterprise" - [portal.procurement.milestone] -accept = "Accepter og fortsæt" -description = "Download PDF'en for at dele den med dit team, kom tilbage for at acceptere, når du er klar, eller foretag ændringer." -download = "Download PDF" downloadError = "Kunne ikke downloade tilbuds-PDF'en endnu — prøv igen om lidt." -edit = "Rediger tilbud" -eyebrow = "Tilbud {{number}}" -perYear = " / år" -preparedFor = "Udarbejdet til {{company}}" -tcv = "{{value}} samlet kontraktværdi" -title = "Dit tilbud er klar" - -[portal.procurement.modal] -cancel = "Annuller" -chooseFile = "Vælg fil" -close = "Luk" -downloadBody = "Din download begynder om kort tid." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Ingen fil valgt" -payBody = "Betal din bindende kontrakt med kort eller bankoverførsel via Stripe. Dit workspace klargøres, så snart betalingen er gået igennem." -payCta = "Fortsæt til Stripe" -payTitle = "Bekræft betaling" -requestBodyFree = "Vi genererer dette efter behov. Bekræft, og din solutions engineer sender det snart videre." -requestBodyPaid = "Dette er en betalt tilføjelse. Bekræft, og din solutions engineer vurderer omfanget og sender papirarbejdet." -requestCta = "Anmod" -requestTitle = "Anmod om dette dokument" -signBody = "Åbner Stirling Enterprise Agreement til e-signatur: Én underskrift dækker MSA, order form, EULA og DPA. Vi medunderskriver automatisk, og du går videre til betaling." -signCta = "Åbn til underskrift" -signTitle = "Gennemgå og underskriv din aftale" -uploadBody = "Send os din PO, så fakturerer vi mod den på dine vilkår. Træk PDF'en ind, eller vælg en fil nedenfor." -uploadCta = "Upload indkøbsordre" -uploadTitle = "Upload din indkøbsordre" [portal.procurement.payment] description = "Dit tilbud er accepteret, og din licens er allerede aktiv — dit team kan starte med det samme. Betal den første faktura, når du er klar; du kan betale eller downloade den her, ingen e-mail nødvendig." +downloadAgreement = "Download signeret aftale" downloadInvoice = "Download faktura" -simulate = "Simuler modtaget betaling (demo)" +eyebrow = "Betaling" title = "Abonnement oprettet" viewInvoice = "Se og betal faktura" +[portal.procurement.review] +acceptCta = "Accepter tilbud" +annual = "Årligt gebyr (år 1)" +downloadCta = "Download tilbud" +poNumber = "Købsordre: {{po}}" +renewal = "Fornys til {{amount}}/år efter perioden (+{{pct}}% CPI)" +tcv = "{{years}}-års periode, betalt fuldt ud på forhånd: {{tcv}}" +validUntil = "Gyldig indtil {{date}}" + [portal.procurement.schedule] fallback = "Kunne ikke indlæse planlæggeren." fallbackLink = "Åbn planlægning i en ny fane" +loading = "Indlæser planlægger..." subtitle = "Din solutions engineer guider dit team gennem udrulningen. Vælg et tidspunkt, der passer dig." title = "Planlæg et opkald" -[portal.procurement.status] -action = "Handling krævet" -available = "Tilgængelig" -complete = "Fuldført" -pending = "Afventer" -request = "På anmodning" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Forseglet, ingen udgående" +back = "Tilbage" +businessName = "Virksomhedsnavn" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Administreret, hurtigste start" +continue = "Fortsæt" +deployment = "Hvor skal det udrulles?" +eula = "Jeg accepterer Stirling EULA & Commercial Terms." +fullName = "Fulde navn" +fullNamePlaceholder = "Dit navn" +invites = "Inviter teammedlemmer · valgfrit" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Planlæg et opkald" +seats = "Personer, der tester" +seatsPlaceholder = "f.eks. 15" +selfhost = "Selvhostet" +selfhostSub = "Docker eller Kubernetes" +start = "Start prøveperiode" +stepOf = "Trin {{n}} af {{total}}" +subtitle = "Gratis i 14 dage, intet kort kræves." +subtitleDetails = "Et par oplysninger til dit tilbud og din aftale." +talkFirst = "Foretrækker du at tale først?" +title = "Opsæt din prøveperiode" +viewEula = "Læs den" +workEmail = "Arbejdsemail" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Forlængelse tilføjer 7 dage og underretter din solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Maksimum nået" subtitle = "Din gratis prøveperiode løber til {{date}}. Intet kort kræves." title = "Enterprise-prøveperiode" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume-priser, organisationsdækkende SSO + SCIM + RBAC, 90 dages uforanderlig audit og en dedikeret SE." -homeCta = "Start prøveperiode →" -homeHeadline = "Behandl millioner af PDFs." - -[portal.recentActivity] -title = "Seneste aktivitet" -viewAll = "Vis alle" - -[portal.recentActivity.empty] -description = "Pipeline-kørsler, udrulninger og agenthændelser vises her." -title = "Intet her endnu" - -[portal.search] -ariaLabel = "Søg" -placeholder = "Søg i Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Hurtighandlinger vises her, når de er tilgængelige." -noActionsTitle = "Ingen hurtighandlinger" -noMatches = "Ingen matches for \"{{query}}\"" -noMatchesDescription = "Prøv et andet søgeord, eller gennemse kataloget." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Kontotilknytning" -[portal.shell.header] -accountFallback = "Konto" -accountMenu = "Kontomenu" -darkMode = "Mørk tilstand" -lightMode = "Lys tilstand" -search = "Søg" -searchPlaceholder = "Søg…" -signOut = "Log ud" -switchToDark = "Skift til mørkt tema" -switchToLight = "Skift til lyst tema" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Tilknyt Stirling-konto" -planEditor = "Editor-abonnement" -planEnterprise = "Enterprise-abonnement" -planProcessor = "Processor-abonnement" primaryNav = "Primær navigation" switchApp = "Skift app" +[portal.shell.topbar] +closeNav = "Luk navigation" +openNav = "Åbn navigation" +search = "Søg" + [portal.sources] subtitle = "Genanvendelige inputforbindelser, der sender dokumenter ind i Stirling. Konfigurer en forbindelse én gang, og referer derefter til den fra et vilkårligt antal politikker. Klik på en række for dens konfiguration og hvilke politikker, der bruger den." title = "Kilder" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Forbind kilde" +[portal.sources.builder] +back = "Tilbage til kilder" +backToTypes = "Alle kildetyper" +cancel = "Annuller" +chooseHint = "Vælg, hvor dokumenter kommer fra. Nedtonede connectors er på vej." +comingSoon = "Kommer snart" +comingSoonHeading = "Kommer snart" +create = "Opret kilde" +createTitle = "Forbind en kilde" +delete = "Slet" +editTitle = "Rediger kilde" +enabled = "Aktiveret" +save = "Gem ændringer" + +[portal.sources.builder.folderAccess] +description = "Mappeautomatiseringer kan kun bruge mapper, som en administrator har tilladt. Tilføj den under indstillinger for Mappeadgang, og prøv derefter igen." +openSettings = "Indstillinger for Mappeadgang" +title = "Denne mappe er ikke tilladt" + [portal.sources.delete] body = "Slet \"{{name}}\"? Dette kan ikke fortrydes. Politikker, der refererer til den, skal opdateres." cancel = "Annuller" confirm = "Slet" title = "Slet kilde?" -[portal.sources.detail] -closeAriaLabel = "Luk detaljer" -delete = "Slet kilde" -docs24h = "Sidste 24 t." -docs30d = "Sidste 30 dage" -docsTotal = "Set i alt" -docsTrend = "Dokumenter over de sidste 30 dage" -documents = "Dokumenter" -edit = "Rediger" -notReferenced = "Ikke refereret af nogen politik, så den er sikker at slette." -pause = "Sæt på pause" -resume = "Genoptag" -subtitle = "{{type}} · {{status}}" -usedBy = "Brugt af" - -[portal.sources.empty] -description = "Forbind en mappe (og snart cloud storage), så dine politikker har et sted at hente dokumenter fra." -title = "Ingen kilder forbundet endnu" - [portal.sources.kpi] inUse = "I brug" total = "Forbindelser" unused = "Ubrugte" +[portal.sources.networkFields.connection] +helperText = "Den gemte forbindelse med serveradressen og legitimationsoplysninger. Genbruges af hver kilde, der refererer til den." +label = "Forbindelse" + +[portal.sources.networkFields.directory] +helperText = "Mappe på serveren, der skal poll'es, relativt til login-hjemmet eller share-roden. Lad være tom for roden." +label = "Mappe" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume fjerner hver fil fra serveren, når hver politik har behandlet den." +label = "Læsningstilstand" + +[portal.sources.networkFields.mode.options] +consume = "Consume: behandl hver fil én gang" +snapshot = "Snapshot: genlæs mappen ved hver kørsel" + +[portal.sources.networkFields.recursive] +label = "Mappedybde" + +[portal.sources.networkFields.recursive.options] +all = "Inkluder undermapper" +top = "Kun øverste niveau" + [portal.sources.status] active = "Aktiv" disabled = "Deaktiveret" unused = "Ubrugt" [portal.sources.table] +documents = "Dokumenter" source = "Kilde" status = "Status" +type = "Type" usedBy = "Politikker" +[portal.sources.types.box] +description = "Hent dokumenter fra Box-mapper, som dit team allerede deler." +label = "Box" + +[portal.sources.types.dropbox] +description = "Hent dokumenter fra en delt Dropbox-mappe." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenter, som dit team har behandlet i editoren, på tværs af politik- og AI-kørsler." label = "Editor" +noPolicies = "Ingen politikker er kørt fra editoren endnu." + +[portal.sources.types.email] +description = "Gør vedhæftninger sendt til en dedikeret mailboks til dokumenter." +label = "Email-indbakke" [portal.sources.types.folder] description = "Overvåg en mappe på serveren for nye dokumenter." @@ -7943,25 +8829,80 @@ label = "Mappedybde" all = "Medtag undermapper" top = "Kun øverste niveau" +[portal.sources.types.ftp] +description = "Poll en FTP- eller FTPS-servermappe for nye dokumenter." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Hent dokumenter fra delte Google Drive-mapper." +label = "Google Drive" + +[portal.sources.types.network] +description = "Overvåg et SMB-netværksshare for nye dokumenter." +label = "Netværksdrev" + +[portal.sources.types.onedrive] +description = "Overvåg OneDrive-mapper for nye dokumenter." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Hent dokumenter fra en Amazon S3- eller S3-kompatibel bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Den gemte forbindelse, der indeholder bucket og legitimationsoplysninger. Genbruges af hver kilde og pipeline-output, der refererer til den." +label = "Forbindelse" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume fjerner hvert objekt fra bucket'en, når hver politik har behandlet det." +label = "Læsningstilstand" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: behandl hvert objekt én gang" +snapshot = "Snapshot: genlæs bucket'en ved hver kørsel" + +[portal.sources.types.s3.fields.prefix] +helperText = "Kun objekter, hvis nøgler starter med dette præfiks, behandles." +label = "Nøglepræfiks" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Poll en SFTP-dropmappe for nye dokumenter." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Hent dokumenter fra SharePoint-dokumentbiblioteker." +label = "SharePoint" + [portal.sources.types.unknown] label = "Kilde" +[portal.sources.types.webhook] +createNote = "En leverings-URL og signing secret genereres, når du opretter denne webhook. Afsendere POST'er dokumenter til den URL, og de refererende politikker kører ved ankomst." +description = "Modtag dokumenter via signeret HTTP POST fra ethvert eksternt system." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Leverings-URL" +secretNote = "Signing secret vises én gang, når webhooken oprettes. Genskab kilden for at rotere den." + +[portal.sources.types.webhook.reveal] +copy = "Kopiér" +done = "Færdig" +secret = "Signing secret" +secretHelp = "Signer hver leverings rå body med denne nøgle (HMAC-SHA256), og send den som X-Stirling-Signature header." +secretWarning = "Kopiér signing secret nu. Af hensyn til din sikkerhed vises den kun én gang og kan ikke hentes senere." +title = "Webhook oprettet" +url = "Leverings-URL" +usage = "POST hvert dokument som den rå request body til leverings-URL'en med en binær indholdstype (application/pdf eller application/octet-stream). Refererende politikker kører automatisk ved ankomst." + [portal.sources.wizard] -back = "Tilbage" -cancel = "Annuller" -continue = "Fortsæt" -editTitle = "Rediger kilde" name = "Navn" namePlaceholder = "f.eks. Skadesmodtagelse" -save = "Gem ændringer" -subtitle = "Trin {{current}} af {{total}} · {{label}}" -title = "Forbind en kilde" type = "Type" -[portal.sources.wizard.steps] -chooseType = "Vælg type" -configure = "Konfigurer" -review = "Gennemgå og forbind" +[portal.stepModal] +close = "Luk" [portal.usage] managePayment = "Administrer betaling" @@ -8021,16 +8962,9 @@ summary = "Ejer et team — administrerer dets medlemmers ressourcer og delte ko 2 = "Portaladgang via standardpolitikken" 3 = "Alt, hvad medlem kan gøre" -[portal.welcome] -ariaLabel = "Velkommen til Stirling PDF" -badge = "Open-source" -installEditor = "Installer Editor" -inviteTeammates = "Inviter teammedlemmer" -openInBrowser = "Åbn i browser" -perks = "Gratis for evigt · Kan hostes selv" -subtitle = "Verdens mest sikre PDF Editor er gratis for teams i alle størrelser. Inkluderer 60+ PDF-handlinger og SSO." -title = "Velkommen til" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Ubegrænset" [printFile] title = "Udskriv Fil" @@ -8613,6 +9547,14 @@ text = "Rotér dine PDF-sider med eller mod uret i intervaller af 90 grader. All [rotate.tooltip.header] title = "Oversigt over rotationsindstillinger" +[ruler] +clearAll = "Ryd alle" +hideAllLabels = "Skjul alle labels" +hideSmallLabels = "Skjul små labels" +physicalValues = "Fysisk" +scaled = "Skaleret" +showAllLabels = "Vis alle labels" + [sanitize] desc = "Fjern potentielt skadelige elementer fra PDF-filer." sanitizationResults = "Saniteringsresultater" @@ -8654,31 +9596,45 @@ label = "Fjern XMP-metadata" [sanitize.steps] settings = "Indstillinger" -[sanitizePdf] -tags = "rens,sikker,sikker,fjern-trusler" - [sanitizePDF] title = "Rens PDF" +[sanitizePdf] +tags = "rens,sikker,sikker,fjern-trusler" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Justér sidestørrelse" +[scaleSettings] +activeScale = "Aktiv skala" +apply = "Anvend skala" +applyCalibration = "Anvend kalibrering" +calculatedScale = "Beregnet skala" +calibrate = "Kalibrer" +calibrating = "Kalibrerer" +calibrationDistanceRequired = "Angiv en virkelig afstand større end nul" +calibrationInvalid = "Kan ikke beregne målestok ud fra denne måling" +calibrationPaperDistance = "Målt sideafstand: {{distance}}" +calibrationTitle = "Kalibrer målestok" +calibrationTooltip = "Mål en kendt afstand for automatisk at beregne målestokken" +customScale = "Brugerdefineret målestok" +noneSet = "Ingen brugerdefineret målestok angivet" +presets = "Forudindstillede målestokke" +ratio = "Målestoksforhold" +ratioHelp = "Forhold: 1 sideenhed = X virkelige enheder" +ratioPlaceholder = "f.eks. 100" +ratioPositive = "Forholdet skal være større end nul" +ratioRequired = "Forhold er påkrævet" +realDistance = "Virkelig afstand" +realDistancePlaceholder = "f.eks. 5" +reset = "Nulstil til standarder" +unit = "Enhed" +unitInvalid = "Ugyldig enhed: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Vinkeltærskel:" -10 = "Indstiller størrelsen på kanten, der tilføjes og fjernes for at forhindre hvide kanter i outputtet (standard: 1)." -2 = "Indstiller den minimale absolutte vinkel, der kræves for at billedet roteres (standard: 10)." -3 = "Tolerancen:" -4 = "Bestemmer området for farvevariation omkring den estimerede baggrundsfarve (standard: 30)." -5 = "Minimum Areal:" -6 = "Indstiller den minimale arealtærskel for et foto (standard: 10000)." -7 = "Minimum Kontur Areal:" -8 = "Indstiller den minimale kontur arealtærskel for et foto" -9 = "Kantstørrelse:" - [scannerImageSplit] submit = "Udtræk billedscanninger" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Hvad dette gør" whatThisDoesDesc = "Finder og udtrækker automatisk hvert foto fra en scannet side eller et sammensat billede—ingen manuel beskæring." whenToUse = "Hvornår det bruges" +[ScannerImageSplit.selectText] +1 = "Vinkeltærskel:" +10 = "Indstiller størrelsen på kanten, der tilføjes og fjernes for at forhindre hvide kanter i outputtet (standard: 1)." +2 = "Indstiller den minimale absolutte vinkel, der kræves for at billedet roteres (standard: 10)." +3 = "Tolerancen:" +4 = "Bestemmer området for farvevariation omkring den estimerede baggrundsfarve (standard: 30)." +5 = "Minimum Areal:" +6 = "Indstiller den minimale arealtærskel for et foto (standard: 10000)." +7 = "Minimum Kontur Areal:" +8 = "Indstiller den minimale kontur arealtærskel for et foto" +9 = "Kantstørrelse:" + [search] noResults = "Ingen resultater fundet" placeholder = "Indtast søgeord..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Din Stirling-PDF-server er offline, og \"{{endpoint}} [session] expired = "Din sesions tid har udløbet. Genlad siden og prøv igen." -[sessionManagement.tooltip] -header = "Administration af underskriftssessioner" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nye deltagere tilføjes i slutningen af underskriftsrækkefølgen" -bullet2 = "Kan ikke tilføje deltagere, efter sessionen er færdiggjort" -bullet3 = "Hver deltager modtager en meddelelse, når det er deres tur" -description = "Du kan tilføje flere deltagere til en aktiv session når som helst før færdiggørelse." -title = "Tilføjelse af deltagere" - -[sessionManagement.tooltip.finalization] -bullet1 = "Fuld færdiggørelse: Alle deltagere har underskrevet" -bullet2 = "Delvis færdiggørelse: Nogle deltagere har ikke underskrevet endnu" -bullet3 = "Ikke-underskrevne deltagere udelukkes fra det endelige dokument" -bullet4 = "Når den er færdiggjort, kan du indlæse den underskrevne PDF i aktive filer" -description = "Færdiggørelse kombinerer alle underskrifter i én underskrevet PDF. Denne handling kan ikke fortrydes." -title = "Færdiggørelse af session" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Kan ikke fjerne deltagere, der allerede har underskrevet" -bullet2 = "Fjernede deltagere modtager ikke længere meddelelser" -bullet3 = "Underskriftsrækkefølge justeres automatisk" -description = "Deltagere kan fjernes fra sessioner, før de underskriver." -title = "Fjernelse af deltagere" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Hver underskrift anvendes sekventielt på PDF'en" -bullet2 = "Senere underskrivere kan se tidligere underskrifter" -bullet3 = "Afgørende for godkendelsesarbejdsgange og juridiske beviskæder" -description = "Den rækkefølge, du angiver, når du opretter sessionen, bestemmer, hvem der underskriver først." -title = "Underskriftsrækkefølge" - [settings] close = "Luk" +[settings.ai] +documents = "Dokumenter & RAG" +general = "Generelt" +limits = "Grænser & ydeevne" +models = "Modeller & udbydere" +title = "AI" + [settings.configuration] advanced = "Avanceret" database = "Database" endpoints = "Endpoints" features = "Funktioner" +folderAccess = "Mappeadgang" mcp = "MCP Server" storageSharing = "Fillagring og deling" systemSettings = "Systemindstillinger" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Standard PDF-editor" defaultPdfEditorActive = "Stirling PDF er din standard PDF-editor" defaultPdfEditorChecking = "Tjekker..." defaultPdfEditorInactive = "En anden app er sat som standard" +defaultPdfEditorRemind = "Mind mig om at angive som standard" +defaultPdfEditorRemindDescription = "Vis et banner, når Stirling PDF ikke er din standard PDF-applikation." defaultPdfEditorSet = "Allerede standard" defaultStartupView = "Standardvisning ved start" defaultStartupViewDescription = "Vælg hvilken fane der er aktiv i venstre kolonne, når appen starter" @@ -8817,9 +9763,18 @@ languageDescription = "Vælg visningssprog" logout = "Log ud" setAsDefault = "Sæt som standard" theme = "Tema" +themeAccent = "Accentfarve" +themeAccentCustom = "Brugerdefineret farve" +themeAccentCustomHint = "Brugerdefineret (standardindstillinger anbefales)" +themeAccentDark = "Accentfarve for mørk tilstand" +themeAccentDefault = "Standard" +themeAccentDefaultHint = "Standardtema (anbefales)" +themeAccentDescription = "Knapper, links og fremhævninger følger den, og den toner appen subtilt. Lys og mørk har hver sin. System bruger den, der er aktiv." +themeAccentLight = "Accentfarve for lys tilstand" themeDark = "Mørkt" themeDescription = "Skift mellem lyst og mørkt tema" themeLight = "Lyst" +themeReset = "Gendan tema til standard" themeSystem = "System" title = "Generelt" user = "Bruger" @@ -8833,6 +9788,12 @@ intro = "Aktivér brugergodkendelse, teamstyring og arbejdsområdefunktioner for learnMore = "Læs mere i dokumentationen" title = "For systemadministratorer" +[settings.general.loginLanding] +description = "Vælg hvilken app der åbnes, når du logger ind på Stirling Cloud." +editor = "Editor" +processor = "Processor" +title = "Efter login" + [settings.general.mode] fullscreen = "Fuldskærm" sidebar = "Sidepanel" @@ -9013,9 +9974,6 @@ title = "Politikker & Privatliv" [settings.preferences] title = "Præferencer" -[settings.search] -placeholder = "Søg i indstillingssider..." - [settings.security] description = "Opdater din adgangskode for at holde din konto sikker." title = "Sikkerhed" @@ -9041,6 +9999,11 @@ security = "Sikkerhed" telegram = "Telegram" title = "Sikkerhed og autentificering" +[settings.signOut] +confirm = "Er du sikker på, at du vil logge ud?" +submit = "Log ud" +title = "Log ud" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Sidste side" maintainRatio = "Skift bevarelse af billedforhold" next = "Næste side" noSavedSigs = "Ingen Gemte Signaturer Fundet" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personlige Signaturer" previous = "Forrige side" redo = "Gentag" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Fjernelse af baggrund mislykkedes" hint = "Upload et PNG- eller JPG-billede af din underskrift" label = "Upload billede af underskrift" placeholder = "Vælg billedfil" +previewAlt = "Current image signature" processing = "Behandler billede..." removeBackground = "Fjern hvid baggrund (gør gennemsigtig)" @@ -9207,6 +10172,17 @@ saved = "Vælg en gemt signatur ovenfor, og klik derefter hvor som helst på PDF text = "Når du har indtastet dit navn ovenfor, klik hvor som helst på PDF'en for at placere din underskrift." title = "Sådan tilføjer du underskrift" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Flyt underskrift" pause = "Sæt placering på pause" @@ -9653,10 +10629,30 @@ title = "Upload til server" updateButton = "Opdater på server" uploadButton = "Upload til server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klik her" +fallback = "Hvis det ikke åbnede automatisk," +opening = "Åbner Swagger UI i en ny fane..." tags = "api,documentation,swagger,endpoints,development" title = "API-dokumentation" @@ -9764,12 +10760,39 @@ selectFilesHint = "Vælg filer i Aktive filer for at køre dette værktøj" singleFileScope = "Anvendes kun på: {{fileName}}" viewerMode = "Skift til filredigeringsværktøjet for at vælge flere filer." +[toolFormat] +ANY = "enhver fil" +CBR = "et tegneseriearkiv (CBR)" +CBZ = "et tegneseriearkiv (CBZ)" +CSV = "en CSV-fil" +EBOOK = "en e-bog" +EMAIL = "en e-mail" +EXCEL = "et regneark" +HTML = "en webside" +IMAGE = "billeder" +JAVASCRIPT = "en JavaScript-fil" +JSON = "en JSON-fil" +MARKDOWN = "en Markdown-fil" +NONE = "ingen fil" +PCL = "en PCL-printfil" +PDF = "en PDF" +PDF_ENCRYPTED = "en adgangskodebeskyttet PDF" +POSTSCRIPT = "en PostScript-fil" +PPT = "en præsentation" +TEXT = "en tekstfil" +VIDEO = "en video" +WORD = "et Word-dokument" +XML = "en XML-fil" +XPS = "et XPS-dokument" +ZIP = "et ZIP-arkiv" + [toolPanel] alpha = "Alpha" backToAllTools = "Tilbage til alle værktøjer" collapse = "Sammenfold panel" expand = "Udvid panel" goBack = "Gå tilbage" +pdfTools = "PDF-værktøjer" placeholder = "Vælg et værktøj for at komme i gang" premiumFeature = "Premium-funktion:" toolsHeader = "Værktøjer" @@ -9806,11 +10829,6 @@ noToolsFound = "Ingen værktøjer fundet" quickAccess = "HURTIG ADGANG" searchPlaceholder = "Søg i værktøjer..." -[toolPicker.categories] -advancedTools = "Avancerede værktøjer" -recommendedTools = "Anbefalede værktøjer" -standardTools = "Standardværktøjer" - [toolPicker.subcategories] advancedFormatting = "Avanceret formatering" automation = "Automatisering" @@ -9824,6 +10842,9 @@ removal = "Fjernelse" signing = "Signering" verification = "Verifikation" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Ingen værktøjer fundet" noTools = "Ingen værktøjer tilgængelige" @@ -9950,28 +10971,53 @@ title = "Brugere" you = "(dig)" [users.action] +cancelInvite = "Annuller invitation" deleteTeam = "Slet team" disableMfa = "Nulstil MFA" move = "Flyt til team" reinstate = "Genindsæt" remove = "Fjern fra org" +removeTeam = "Fjern fra team" rename = "Omdøb team" resetPw = "Nulstil adgangskode" suspend = "Suspender" unlock = "Lås konto op" +[users.activity] +daysAgo = "{{count}} d siden" +hoursAgo = "{{count}} t siden" +justNow = "Lige nu" +minutesAgo = "{{count}} min siden" +monthsAgo_one = "{{count}} måned siden" +monthsAgo_other = "{{count}} måneder siden" +never = "Aldrig" +weeksAgo_one = "{{count}} uge siden" +weeksAgo_other = "{{count}} uger siden" +yearsAgo_one = "{{count}} år siden" +yearsAgo_other = "{{count}} år siden" + [users.cap] addProcessor = "+ Processor" approver = "Godkender politik" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Annuller invitationen til {{email}}? De vil ikke kunne deltage med det aktuelle link." +cancelInviteTitle = "Annuller invitation" deleteTeamBody = "Slet {{name}}-teamet? Teamet skal først være tomt - flyt dets medlemmer til et andet team, og det må ikke stadig eje integrationskonfigurationer." deleteTeamTitle = "Slet team" disableMfaBody = "Fjern {{name}}s MFA-tilmelding? De sætter den op igen ved næste login, hvis det kræves." disableMfaTitle = "Nulstil MFA" removeBody = "Fjern {{name}} permanent fra organisationen? Dette kan ikke fortrydes." +removeTeamBody = "Fjern {{name}} fra teamet? De beholder deres konto, men mister adgang til dette teams ressourcer." removeTitle = "Fjern medlem" [users.empty] @@ -9982,10 +11028,8 @@ title = "Ingen medlemmer endnu" addToTeam = "Føj til team" guestCount = "{{count}} gæst" guests = "Gæster" -guestsDesc = "Eksterne samarbejdspartnere, begrænset til det, du har delt. Kun Editor." ledBy = "ledet af {{owner}}" org = "Organisation" -orgDesc = "Ejere med organisationsdækkende autoritet og politikgodkendelse" owners = "{{count}} ejer" team = "{{name}}-team" teamMeta = "{{count}} personer" @@ -10022,6 +11066,18 @@ username = "Brugernavn" usernameError = "Brugernavnet skal være på mindst 3 tegn" usernamePlaceholder = "jsmith" +[users.invites] +by = "Inviteret af {{who}}" +cancel = "Annuller" +expiresInDays_one = "Udløber om {{count}} dag" +expiresInDays_other = "Udløber om {{count}} dage" +expiresToday = "Udløber i dag" +title = "Afventende invitationer" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Noget gik galt ved kontakt til backend, eller du har ikke adgang. Prøv igen." title = "Kunne ikke indlæse medlemmer" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Søg i vedhæftninger" title = "Vedhæftninger" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bogmærketitel" +bookmarkTitleRequired = "Bogmærketitel er påkrævet" closeSidebar = "Luk sidepanelet for bogmærker" collapseAll = "Skjul alle bogmærker" +empty = "No bookmarks in this document" expandAll = "Udvid alle bogmærker" +searchPlaceholder = "Søg i bogmærker" +title = "Bookmarks" [viewer.comments] addComment = "Tilføj kommentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Find i dokument" moreActions = "Flere handlinger" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Side {{page}}" placingHint = "Klik på en side for at placere… (annuller)" removeCommentOnly = "Fjern kun kommentar" saveReply = "Gem svar" +searchPlaceholder = "Search comments" title = "Kommentarer" typeComment = "Kommentar" typeInsertText = "Indsæt tekst" @@ -10262,6 +11325,7 @@ unsavedDesc = "Du har ikke-gemte ændringer" closeSidebar = "Luk sidepanelet for lag" hideAll = "Skjul alle lag" showAll = "Vis alle lag" +title = "Layers" [viewer.link] delete = "Slet link" @@ -10605,30 +11669,6 @@ title = "Vandmærketekst" image = "Billede" text = "Tekst" -[wetSignature.tooltip] -header = "Metoder til oprettelse af underskrift" - -[wetSignature.tooltip.draw] -bullet1 = "Tilpas pennens farve og tykkelse" -bullet2 = "Ryd og tegn igen, indtil du er tilfreds" -bullet3 = "Fungerer på touch-enheder (tablets, telefoner)" -description = "Opret en håndskrevet underskrift med din mus eller touchscreen. Bedst til personlige, autentiske underskrifter." -title = "Tegn underskrift" - -[wetSignature.tooltip.type] -bullet1 = "Vælg mellem flere skrifttyper" -bullet2 = "Tilpas tekststørrelse og farve" -bullet3 = "Perfekt til standardiserede underskrifter" -description = "Generér en underskrift ud fra indtastet tekst. Hurtig og ensartet, velegnet til forretningsdokumenter." -title = "Skriv underskrift" - -[wetSignature.tooltip.upload] -bullet1 = "Understøtter PNG, JPG og andre billedformater" -bullet2 = "Gennemsigtige baggrunde anbefales for bedste resultat" -bullet3 = "Billedet ændres i størrelse, så det passer til underskriftsområdet" -description = "Upload et på forhånd oprettet underskriftsbillede. Ideelt, hvis du har en scannet underskrift eller firmalogo." -title = "Upload underskriftsbillede" - [workbenchBar] activeFiles = "Aktive filer" annotations = "Anmærkninger" @@ -10642,6 +11682,7 @@ exitRedaction = "Afslut maskeringstilstand" exportAll = "Eksporter PDF" exportSelected = "Eksporter valgte sider" formFill = "Udfyld formular" +hideToolbar = "Hide toolbar" multiTool = "Multiværktøj" panMode = "Panoreringstilstand" print = "Udskriv PDF" @@ -10652,6 +11693,7 @@ redact = "Maskér" rotateLeft = "Roter til venstre" rotateRight = "Roter til højre" ruler = "Lineal / Måling" +rulerSettings = "Målestoksindstillinger" save = "Gem" saveAll = "Gem alle" saveAs = "Gem som" @@ -10661,6 +11703,7 @@ selectAll = "Vælg alle" selectByNumber = "Vælg efter sidetal" selectLanguage = "Vælg sprog" share = "Del" +showToolbar = "Show toolbar" toggleAnnotations = "Slå anmærkninger til/fra" toggleAttachments = "Slå vedhæftede filer til/fra" toggleBookmarks = "Slå bogmærker til/fra" diff --git a/frontend/editor/public/locales/de-DE/translation.toml b/frontend/editor/public/locales/de-DE/translation.toml index 3736b2adae..d4de81d3ee 100644 --- a/frontend/editor/public/locales/de-DE/translation.toml +++ b/frontend/editor/public/locales/de-DE/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alphabet" apply = "Anwenden" applyAndContinue = "Anwenden & Fortfahren" -areYouSure = "Möchten Sie die Seite wirklich verlassen?" back = "Zurück" black = "Schwarz" blue = "Blau" @@ -46,10 +45,8 @@ filesSelected = "Dateien ausgewählt" font = "Schriftart" fontSizeTooltip = "Größe des Seitenzahl-Texts in Punkten. Größere Werte erzeugen größeren Text." fontTypeTooltip = "Schriftfamilie für die Seitenzahlen. Wählen Sie passend zum Dokumentstil." -genericSubmit = "Absenden" green = "Grün" help = "Hilfe" -imgPrompt = "Wählen Sie ein Bild" incorrectPasswordMessage = "Das Passwort ist falsch." info = "Informationen" invalidUndoData = "Rückgängig nicht möglich: ungültige Vorgangsdaten" @@ -62,7 +59,6 @@ loginAgreementTitle = "Anmeldevereinbarung" logOut = "Abmelden" marginTooltip = "Abstand zwischen der Seitenzahl und dem Seitenrand." moreOptions = "Weitere Optionen" -multiPdfPrompt = "PDFs auswählen (2+)" never = "Nie" no = "Nein" noFileSelected = "Keine Datei ausgewählt. Bitte laden Sie eine hoch." @@ -80,7 +76,6 @@ pageOrderPrompt = "Seitenreihenfolge (Geben Sie eine durch Komma getrennte Liste pages = "Seiten" pageSelectionPrompt = "Benutzerdefinierte Seitenauswahl (Geben Sie eine durch Kommas getrennte Liste von Seitenzahlen 1,5,6 oder Funktionen wie 2n+1 ein):" password = "Passwort" -pdfPrompt = "PDF(s) auswählen" pin = "Datei anheften (nach Tool-Ausführung aktiv halten)" pro = "Pro" processingComplete = "Ihre Datei ist bereit." @@ -110,6 +105,7 @@ undoSuccess = "Vorgang erfolgreich rückgängig gemacht" unknown = "Unbekannt" unpin = "Datei lösen (nach Tool-Ausführung ersetzen)" unsavedChanges = "Sie haben ungespeicherte Änderungen an Ihrer PDF. Was möchten Sie tun?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Ungespeicherte Änderungen" unsupported = "Nicht unterstützt" username = "Benutzername" @@ -692,6 +688,165 @@ manualLinks = "Manuelle Downloads: Links anklicken und die Dateien in den tessda noLanguages = "Keine tessdata-Sprachen im konfigurierten Verzeichnis gefunden." permissionNotice = "Der tessdata-Pfad ist nicht beschreibbar. Downloads werden im Browser geöffnet; bitte die .traineddata-Dateien manuell in den tessdata-Ordner speichern." +[admin.settings.ai.documents] +description = "Konfigurieren Sie das Embedding-Modell und die Retrieval-Einstellungen, die zum Beantworten von Fragen zu Dokumenten verwendet werden. Wird beim Speichern auf die KI-Engine angewendet." +title = "Dokumente & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Leer lassen, um die eigene Umgebungs-Anmeldeinformation der Engine zu verwenden. Gilt für selbst gehostete Single-Engine-Bereitstellungen." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Gespeichert – leer lassen, um den aktuellen Schlüssel beizubehalten" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL des OpenAI-kompatiblen / Ollama-Embeddings-Endpunkts, z. B. http://ollama:11434/v1. Muss auf einen vertrauenswürdigen internen Endpunkt verweisen (SSRF-sensibel)." +label = "Embedding-Base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Name des Embedding-Modells. Freitext; Vorschläge sind nur Hinweise." +label = "Embedding-Modell" + +[admin.settings.ai.documents.embeddingProvider] +description = "Anbieter, der zum Umwandeln von Dokumenttext in Vektor-Embeddings verwendet wird." +label = "Embedding-Anbieter" + +[admin.settings.ai.documents.maxSearches] +description = "Maximale Anzahl von Retrieval-Suchen, die der Agent pro Anfrage ausführen darf." +label = "Max. Suchen" + +[admin.settings.ai.documents.reindexNote] +body = "Eine Änderung des Embedding-Modells wird sofort wirksam, aber Dokumente, die mit dem vorherigen Modell indexiert wurden, müssen neu indexiert werden, damit die Suche korrekte Ergebnisse liefert." +title = "Neuindexierung erforderlich" + +[admin.settings.ai.documents.saved] +reindexNote = "Wenn Sie das Embedding-Modell geändert haben, indexieren Sie vorhandene Dokumente neu, damit die Suche das neue Modell verwendet." + +[admin.settings.ai.documents.topK] +description = "Anzahl der relevantesten Chunks, die pro Suche abgerufen werden." +label = "Top K" + +[admin.settings.ai.general] +description = "Verbinden Sie Stirling mit der Python-KI-Engine und wählen Sie aus, welche KI-Funktionen verfügbar sind. Änderungen werden nach einem Neustart wirksam." +title = "KI-Engine" + +[admin.settings.ai.general.capabilities] +description = "Schalten Sie einzelne KI-Funktionen ein oder aus. Deaktivierte Funktionen werden in der App ausgeblendet." +title = "Funktionen" + +[admin.settings.ai.general.enabled] +description = "Hauptschalter. Wenn ausgeschaltet, sind keine KI-Tools, Agenten oder Engine-Aufrufe verfügbar." +label = "KI aktivieren" + +[admin.settings.ai.general.features.chat] +description = "Konversationsassistent für die Arbeit mit PDFs." +label = "Chat-Assistent" + +[admin.settings.ai.general.features.classify] +description = "Dokumente automatisch nach Typ oder Inhalt kategorisieren." +label = "Dokumentklassifizierung" + +[admin.settings.ai.general.features.createPdf] +description = "Eine neue PDF (z. B. aus HTML) über einen KI-Agenten erstellen." +label = "PDF aus Prompt erstellen" + +[admin.settings.ai.general.features.documentQuestions] +description = "Fragen stellen und Antworten erhalten, die auf einem hochgeladenen Dokument basieren." +label = "Dokumentfragen" + +[admin.settings.ai.general.features.mathAuditor] +description = "Dokumente auf mathematische und numerische Fehler prüfen." +label = "Mathematik-Prüfer" + +[admin.settings.ai.general.features.pdfComment] +description = "KI-erstellte Prüfkommentare und Anmerkungen zu einer PDF hinzufügen." +label = "PDF-Kommentaragent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout für umfangreichere Agentenoperationen wie Dokumenterstellung." +label = "Timeout für lange Vorgänge (Sekunden)" + +[admin.settings.ai.general.note] +body = "Die KI-Engine läuft als separater Dienst. Ihr gemeinsames Secret" +body2 = "wird über eine Container-Umgebungsvariable festgelegt. Anbieter-API keys können auf diesen Seiten eingegeben oder als Engine-Umgebungsvariablen bereitgestellt werden; hier eingegebene Schlüssel werden beim Speichern an die Engine übertragen." +title = "Über die KI-Engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout für gestreamte Chat-Antworten (Token für Token)." +label = "Stream-Timeout (Sekunden)" + +[admin.settings.ai.general.test] +button = "Verbindung testen" +failBody = "Die KI-Engine hat nicht geantwortet. Prüfen Sie die URL, ob der Engine-Container läuft und ob KI aktiviert ist (nach dem Aktivieren ist ein Neustart erforderlich)." +failTitle = "KI-Engine nicht erreichbar" +okBody = "Die KI-Engine hat auf einen Health Check geantwortet." +okTitle = "KI-Engine erreichbar" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout für Standard-KI-Anfragen an die Engine." +label = "Anfrage-Timeout (Sekunden)" + +[admin.settings.ai.general.url] +description = "Interne URL der Python-KI-Engine, z. B. http://stirling-pdf-engine:5001." +label = "KI-Engine-URL" + +[admin.settings.ai.limits] +description = "Schutzgrenzen dafür, wie viel Arbeit KI-Anfragen ausführen dürfen und wie viele gleichzeitig laufen. Wird beim Speichern auf die KI-Engine angewendet." +title = "Limits & Leistung" + +[admin.settings.ai.limits.maxCharacters] +description = "Schutzgrenze: KI-Anfragen ablehnen, deren extrahierter Text diese Länge überschreitet." +label = "Max. Zeichen pro Anfrage" + +[admin.settings.ai.limits.maxPages] +description = "Schutzgrenze: KI-Anfragen mit mehr als dieser Anzahl von PDF-Seiten ablehnen." +label = "Max. Seiten pro Anfrage" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximale Anzahl gleichzeitiger laufender Modellaufrufe in der gesamten Engine." +label = "Max. Modell-Parallelität" + +[admin.settings.ai.models] +description = "Wählen Sie den LLM-Anbieter und die Smart/Fast-Modelle aus, die die KI-Engine verwendet. Wird beim Speichern auf die KI-Engine angewendet." +title = "Modelle & Anbieter" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Leer lassen, um die eigene Umgebungs-Anmeldeinformation der Engine zu verwenden. Gilt für selbst gehostete Single-Engine-Bereitstellungen." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Gespeichert – leer lassen, um den aktuellen Schlüssel beizubehalten" + +[admin.settings.ai.models.baseUrl] +description = "Base URL des OpenAI-kompatiblen / Ollama-Endpunkts, z. B. http://ollama:11434/v1." +label = "Anbieter-Base URL" +warning = "Die Base URL muss auf einen vertrauenswürdigen internen Endpunkt verweisen. Die Engine führt serverseitige Anfragen daran aus, daher ist ein nicht vertrauenswürdiger Wert SSRF-sensibel." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximale Ausgabe-Token für das Fast-Modell." +label = "Max. Token für Fast-Modell" + +[admin.settings.ai.models.fastModel] +description = "Günstigeres, schnelleres Modell für leichte Aufgaben. Freitext; Vorschläge sind nur Hinweise." +label = "Fast-Modell" + +[admin.settings.ai.models.provider] +description = "Mit welchem LLM-Anbieter die Engine kommuniziert." +label = "Anbieter" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximale Ausgabe-Token für das Smart-Modell." +label = "Max. Token für Smart-Modell" + +[admin.settings.ai.models.smartModel] +description = "Leistungsstarkes Modell für komplexes Schlussfolgern. Freitext; Vorschläge sind nur Hinweise." +label = "Smart-Modell" + +[admin.settings.ai.saved] +body = "Änderungen werden automatisch an die KI-Engine übertragen." +bodyNoPush = "Einstellungen gespeichert. Sie werden angewendet, wenn die KI-Engine das nächste Mal ihre Konfiguration übernimmt." +title = "KI-Einstellungen gespeichert" + [admin.settings.badge] clickToUpgrade = "Klicken Sie, um Plandetails anzuzeigen" @@ -939,6 +1094,25 @@ label = "Beim Start neu erzeugen" description = "Anzahl der Tage, für die das Zertifikat gültig ist" label = "Zertifikatsgültigkeit (Tage)" +[admin.settings.folderAccess] +description = "Verzeichnisse, aus denen Ordnerquellen und Ordnerausgaben lesen und in die sie schreiben dürfen. Dies ist eine Sicherheitsgrenze: Automatisierungen können niemals auf einen Serverpfad außerhalb dieser Liste verweisen." +securityNote = "Leer lassen, um Ordnerquellen und -ausgaben vollständig zu deaktivieren. Das eigene Konfigurationsverzeichnis von Stirling ist immer tabu." +title = "Ordnerzugriff" + +[admin.settings.folderAccess.implied] +description = "Diese von Stirling verwalteten Verzeichnisse sind immer erlaubt und können hier nicht geändert werden." +serverStorage = "Server-Dateispeicher" +title = "Immer erlaubt" +watchedFolder = "Überwachter Pipeline-Ordner" + +[admin.settings.folderAccess.roots] +add = "Hinzufügen" +empty = "Keine Ordner erlaubt. Ordnerquellen und -ausgaben sind derzeit deaktiviert." +hint = "Geben Sie absolute Pfade ein, zum Beispiel /data/inbox." +label = "Erlaubte Ordner-Stammverzeichnisse" +placeholder = "/data/inbox" +remove = "Ordner-Stammverzeichnis entfernen" + [admin.settings.general] description = "Systemweite Anwendungseinstellungen einschließlich Branding und Standardverhalten konfigurieren." system = "System" @@ -1705,6 +1879,9 @@ width = "Width" [app] description = "Die kostenlose Adobe Acrobat Alternative (über 10 Millionen Downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "In PDF/A-3b konvertieren" convertToPdfA3bDescription = "Erstellt ein Archiv-PDF mit eingebetteten Anhängen" @@ -1997,6 +2174,73 @@ secureWorkflowDesc = "Sichert PDF-Dokumente durch Entfernen potenziell schädlic description = "Dieses Tool benennt PDF-Dateien automatisch basierend auf ihrem Inhalt um. Es analysiert das Dokument, um den geeignetsten Titel aus dem Text zu finden." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatisch drehen" +tags = "automatisch drehen,Ausrichtung korrigieren,auf dem Kopf,seitlich,begradigen,ausrichten,Ausrichtung erkennen,Drehung korrigieren,automatisch ausrichten,gescannt,OSD" + +[autoRotate.confidenceThreshold] +title = "Konfidenzschwelle" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Nur OCR" +text = "Nur Text" +title = "Erkennungsmethode" + +[autoRotate.error] +failed = "Beim automatischen Drehen der PDF ist ein Fehler aufgetreten." + +[autoRotate.inferUndetected] +title = "Seiten mit niedriger Konfidenz passend drehen" + +[autoRotate.report] +applied = "{{degrees}}° im Uhrzeigersinn" +confidenceValue = "Konfidenz {{value}}" +noChange = "Keine Änderung" +pageLabel = "Seite {{number}}" +summary = "{{rotated}} von {{total}} Seiten gedreht, {{unchanged}} unverändert belassen" +title = "Erkennungsbericht" + +[autoRotate.report.method] +inferred = "Angepasst" +none = "Übersprungen" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Unterhalb der Schwelle" +blankPage = "Seite ist leer" +inferredFromDocument = "Wie die anderen Seiten" +noDominantDirection = "Gemischte Textrichtungen" +osdFailed = "Kein lesbarer Text gefunden" +osdNoVerdict = "Ausrichtung konnte nicht bestimmt werden" +tesseractUnavailable = "OCR nicht installiert" +tooFewGlyphs = "Zu wenig Text" + +[autoRotate.results] +title = "Ergebnisse der automatischen Drehung" + +[autoRotate.settings] +title = "Einstellungen" + +[autoRotate.tooltip.confidenceThreshold] +text = "Wie sicher die Prüfung sein muss, bevor eine Seite gedreht wird. Senken Sie den Wert, um mehr Seiten zu drehen, erhöhen Sie ihn, um mehr Seiten unverändert zu lassen." +title = "Konfidenzschwelle" + +[autoRotate.tooltip.description] +text = "Ermittelt, wie herum jede Seite ausgerichtet sein sollte, und dreht sie richtig. Seiten, die bereits aufrecht sind, bleiben unverändert." + +[autoRotate.tooltip.detectionMode] +text = "Auto liest zuerst den Text auf der Seite und betrachtet die Seite dann als Bild, wenn das nicht ausreicht. Nur Text überspringt die Bildprüfung, was für Dokumente, die nie gescannt wurden, am schnellsten ist. Nur OCR behandelt jede Seite als Bild." +title = "Erkennungsmethode" + +[autoRotate.tooltip.header] +title = "Automatisch drehen" + +[autoRotate.tooltip.inferUndetected] +text = "Leere oder fast leere Seiten können nicht eigenständig geprüft werden. Wenn jede lesbare Seite dieselbe Drehung benötigt, werden diese Seiten passend dazu gedreht." +title = "Seiten mit niedriger Konfidenz passend drehen" + [autoSizeSplitPDF] tags = "pdf,teilen,dokument,organisation" @@ -2199,33 +2443,6 @@ keywords = "Schlüsselwörter: odd, even" numbers = "Zahlen/Bereiche: 5, 10-20" progressions = "Progressionen: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Zertifikatstypen" - -[certificateChoice.tooltip.organization] -bullet1 = "Von Systemadministratoren verwaltet" -bullet2 = "Für berechtigte Benutzer freigegeben" -bullet3 = "Repräsentiert die Unternehmensidentität, nicht die eines Einzelnen" -bullet4 = "Am besten geeignet für: Offizielle Dokumente, Team-Signaturen" -description = "Ein gemeinsames Zertifikat Ihrer Organisation. Wird für unternehmensweite Signierberechtigungen verwendet." -title = "Organisationszertifikat" - -[certificateChoice.tooltip.personal] -bullet1 = "Bei der ersten Nutzung automatisch erzeugt" -bullet2 = "An Ihr Benutzerkonto gebunden" -bullet3 = "Kann nicht mit anderen Benutzern geteilt werden" -bullet4 = "Am besten geeignet für: Persönliche Dokumente, individuelle Verantwortlichkeit" -description = "Ein automatisch generiertes Zertifikat, das Ihrem Benutzerkonto eindeutig zugeordnet ist. Geeignet für individuelle Signaturen." -title = "Persönliches Zertifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Erfordert P12/PFX-Datei und Passwort" -bullet2 = "Kann von externen Zertifizierungsstellen ausgestellt werden" -bullet3 = "Höheres Vertrauensniveau für juristische Dokumente" -bullet4 = "Am besten geeignet für: Rechtsverbindliche Verträge, externe Validierung" -description = "Verwenden Sie Ihre eigene PKCS#12-Zertifikatsdatei. Bietet vollständige Kontrolle über die Zertifikateigenschaften." -title = "Eigenes P12 hochladen" - [certSign] awaitingSignatures = "Warten auf Signaturen" chooseCertificate = "Zertifikatdatei auswählen" @@ -2552,34 +2769,6 @@ server = "Server" stepTitle = "Zertifikatsquelle" upload = "Hochladen" -[certSign.tooltip.header] -title = "Über die Signatur-Verwaltung" - -[certSign.tooltip.overview] -bullet1 = "Vorhandene Signaturen und deren Gültigkeit prüfen" -bullet2 = "Detaillierte Informationen über Signierer und Zertifikate anzeigen" -bullet3 = "Neue digitale Signaturen hinzufügen, um Ihre Dokumente zu sichern" -bullet4 = "Mehrere Dateien unterstützt mit einfacher Navigation" -text = "Dieses Tool ermöglicht es Ihnen zu prüfen, ob Ihre PDFs digital signiert sind, und neue digitale Signaturen hinzuzufügen. Digitale Signaturen beweisen, wer ein Dokument erstellt oder genehmigt hat und zeigen, ob es seit der Signierung geändert wurde." -title = "Was kann dieses Tool?" - -[certSign.tooltip.signing] -bullet1 = "Unterstützt PEM, PKCS12, JKS und Server-Zertifikatformate" -bullet2 = "Option, Signatur auf der PDF zu zeigen oder zu verstecken" -bullet3 = "Grund, Ort und Signierername hinzufügen" -bullet4 = "Wählen Sie, auf welcher Seite sichtbare Signaturen platziert werden sollen" -bullet5 = "Server-Zertifikat für einfache 'Mit Stirling-PDF signieren'-Option verwenden" -text = "Um eine PDF zu signieren, benötigen Sie ein digitales Zertifikat (wie PEM, PKCS12 oder JKS). Sie können wählen, ob Sie die Signatur auf dem Dokument sichtbar machen oder nur für die Sicherheit unsichtbar belassen." -title = "Signaturen hinzufügen" - -[certSign.tooltip.validation] -bullet1 = "Zeigt, ob Signaturen gültig oder ungültig sind" -bullet2 = "Zeigt Signiererinformationen und Signierdatum an" -bullet3 = "Prüft, ob das Dokument nach der Signierung geändert wurde" -bullet4 = "Kann benutzerdefinierte Zertifikate für die Verifizierung verwenden" -text = "Wenn Sie Signaturen prüfen, zeigt Ihnen das Tool, ob sie gültig sind, wer das Dokument signiert hat, wann es signiert wurde und ob das Dokument seit der Signierung geändert wurde." -title = "Signaturen überprüfen" - [changeCreds] changeUsername = "Aktualisieren Sie Ihren Benutzernamen. Nach dem Update werden Sie abgemeldet." credsUpdated = "Konto aktualisiert" @@ -2747,12 +2936,16 @@ text = "Um diese Berechtigungen unveränderlich zu machen, verwenden Sie das Pas [chat.actions] copy = "Nachricht kopieren" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling-Agent-Optionen" +assistant = "AI Assistant" clearChat = "Chat löschen" [chat.input] @@ -2815,28 +3008,35 @@ tooltip = "Dieser Vorgang verwendet Ihre Cloud-Credits" tooltip = "Farbe vom Bildschirm aufnehmen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Farbe wählen" [common] +actions = "Actions" back = "Zurück" cancel = "Abbrechen" close = "Schließen" +codeSample = "Code sample" collapse = "Einklappen" confirm = "Bestätigen" continue = "Weiter" copied = "Kopiert!" copy = "Kopieren" done = "Fertig" +enterValue = "Enter value" error = "Fehler" expand = "Ausklappen" loading = "Laden..." next = "Weiter" +open = "Open" preview = "Vorschau" previous = "Zurück" refresh = "Aktualisieren" remaining = "Verbleibend" retry = "Wiederholen" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Auswahl löschen" @@ -2999,6 +3199,7 @@ title = "Kompressions-Methode" [compress.settings] desiredSize = "Gewünschte Dateigröße" desiredSizePlaceholder = "Größe eingeben" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Komprimierung ist eine einfache Möglichkeit, Ihre Dateigröße zu reduzieren. Wählen Sie Dateigröße, um eine Zielgröße einzugeben und wir passen die Qualität für Sie an. Wählen Sie Qualität, um die Komprimierungsstärke manuell festzulegen." @@ -3201,6 +3402,24 @@ pdfOptions = "PDF-Optionen" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Wählen Sie zuerst ein Quellformat aus" settings = "Einstellungen" single = "Einzeln" @@ -3275,24 +3494,6 @@ description = "Diese Cookies sind für das ordnungsgemäße Funktionieren der We posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Erweitertes Monitoring" -allInOneWorkspace = "All-in-one PDF-Arbeitsbereich (Viewer, Tools & Agent)" -apiSandbox = "API-Sandbox" -dedicatedSupportSlas = "Dedizierter Support & SLAs" -fasterThroughput = "10x schnellerer Durchsatz" -fullyPrivateFiles = "Vollständig private Dateien" -largeFileProcessing = "Verarbeitung großer Dateien" -orgWideAccess = "Organisationsweite Zugriffskontrollen" -premiumAiModels = "Premium-KI-Modelle" -prioritySupport = "Priorisierter Support" -privateDocCloud = "Private Dokumenten-Cloud" -ragFineTuning = "RAG + Fine-Tuning" -secureApiAccess = "Sicherer API-Zugriff" -standardThroughput = "Standard-Durchsatz" -unlimitedApiAccess = "Unbegrenzter API-Zugriff" -unlimitedSeats = "Unbegrenzte Plätze" - [crop] autoCrop = "Auto-crop whitespace" noFileSelected = "Wählen Sie eine PDF-Datei aus, um mit dem Zuschneiden zu beginnen" @@ -3357,6 +3558,7 @@ message = "Standard-PDF-Handler konnte nicht festgelegt werden" title = "Fehler" [defaultApp.prompt] +dontRemind = "Nicht erneut erinnern" message = "Stellen Sie Stirling PDF als Standardanwendung zum Öffnen von PDF-Dateien ein." [defaultApp.settingsOpened] @@ -3396,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Suchen..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Bearbeiten" @@ -3655,11 +3861,13 @@ shareSelected = "Auswahl freigeben" sharing = "Freigabe" showAll = "Alle anzeigen" showHistory = "Verlauf anzeigen" +sortBy = "Sort files" sortByDate = "Nach Datum sortieren" sortByName = "Nach Name sortieren" sortBySize = "Nach Größe sortieren" storage = "Speicher" storageState = "Speicher" +storageUsed = "Storage used" synced = "Synchronisiert" title = "PDF-Dateien hochladen" toolChain = "Angewendete Werkzeuge" @@ -3705,13 +3913,17 @@ addFiles = "Dateien hinzufügen" addingFiles = "Dateien werden hinzugefügt…" collapse = "Seitenleiste einklappen" customizeGroups = "Gruppen anpassen" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Öffnen Sie Dateien, um zu beginnen" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Seitenleiste ausklappen" -files = "Dateien" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive ist nicht konfiguriert" leaveMyFiles = "Meine Dateien verlassen" +library = "PDF-Bibliothek" myFiles = "Meine Dateien" noFiles = "Noch keine Dateien" openFileManager = "Dateimanager öffnen" @@ -3719,37 +3931,36 @@ openFromComputer = "Vom Computer öffnen" openSettings = "Einstellungen öffnen" other = "Andere" recent = "Zuletzt verwendet" -search = "Suchen" -searchPlaceholder = "Dateien suchen..." viewAll = "Alle {{count}} Dateien anzeigen" [fileSidebar.fileItem] closeViewer = "Viewer schließen" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Im Viewer öffnen" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Hinzufügen" -addLabel = "Label hinzufügen…" -categoryIconAria = "Symbol für {{name}} auswählen" -createCategory = "Kategorie erstellen" -delete = "Kategorie löschen" done = "Fertig" -hide = "Kategorie ausblenden" -newCategory = "Neuer Kategoriename…" -removeLabel = "{{name}} entfernen" -rename = "Umbenennen" reset = "Auf Standardwerte zurücksetzen" -search = "Labels suchen…" -show = "Kategorie anzeigen" subtitle = "Gruppieren Sie Ihre Dateien in übergeordnete Kategorien. Fügen Sie vorhandene oder neue Labels zu einer Kategorie hinzu, benennen Sie sie um oder erstellen Sie eine eigene. Dateien, die in keiner Ihrer Kategorien sind, erscheinen unter „Andere“." title = "Seitenleisten-Kategorien" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Zum Arbeitsbereich hinzufügen" addToWorkspaceCount = "{{count}} zum Arbeitsbereich hinzufügen" @@ -3760,7 +3971,6 @@ backToMyFiles = "Zurück zu Meine Dateien" breadcrumbs = "Ordnerpfad" cancel = "Abbrechen" classification = "Klassifizierung" -clearSearch = "Suche löschen" clearSelection = "Auswahl aufheben" closeDetails = "Details schließen" create = "Erstellen" @@ -3792,6 +4002,7 @@ downloadAll = "Alle herunterladen" downloadVersion = "Diese Version herunterladen" dropOverlay = "Dateien zum Hochladen hier ablegen" dropOverlaySub = "Dateien starten in Lokal. Verwenden Sie 'Verschieben nach' oder 'In die Cloud speichern', um sie in einem Ordner zu organisieren." +duplicate = "Duplicate" file = "Datei" fileInfo = "File info" fileMenu = "Dateiaktionen" @@ -3819,13 +4030,12 @@ refresh = "Vom Server aktualisieren" remove = "Löschen" removeVersion = "Diese Version entfernen" rename = "Umbenennen" +renamed = "Umbenannt" renameFolder = "Ordner umbenennen" resizeFolderTree = "Ordnerbaum skalieren (Pfeiltasten, Umschalt für größere Schritte; Doppelklick zum automatischen Anpassen)" save = "Speichern" saveToServer = "Auf dem Server speichern" saveToServerDisabledHint = "Speichern auf dem Server ist auf diesem Server nicht aktiviert. Bitten Sie Ihren Administrator, es zu aktivieren." -search = "Suchen" -searchPlaceholder = "Diesen Ordner & Unterordner durchsuchen" selectAll = "Alle auswählen" selectAllHint = "Klicken, um alles auszuwählen. Tipp: Halten Sie Strg (oder Cmd), um Dateien einzeln hinzuzufügen, Umschalt zum Auswählen eines Bereichs." selectedCount = "{{count}} ausgewählt" @@ -3885,6 +4095,10 @@ title = "Noch keine Cloud-Dateien" hint = "Ohne Hochladen gespeicherte Dateien bleiben hier. Legen Sie eine Datei ab, um eine hinzuzufügen." title = "Keine nur lokalen Dateien" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Dateien, die Sie öffnen oder bearbeiten, erscheinen hier." title = "Noch nichts geändert" @@ -3904,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Ordner konnte nicht gelöscht werden." deleteFolderFailedDetail = "Ordner konnte nicht gelöscht werden: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Ordner-Erscheinungsbild konnte nicht aktualisiert werden." folderAppearanceFailedDetail = "Ordner-Erscheinungsbild konnte nicht aktualisiert werden: {{message}}" moveFilesFailed = "Dateien konnten nicht verschoben werden." @@ -3921,9 +4137,7 @@ count = "Dateien" folder = "Ordner" labels = "Labels" modified = "Geändert" -name = "Name" size = "Größe" -toolHistoryAtVersion = "Kumulative Toolkette" totalSize = "Gesamtgröße" type = "Typ" versionHistory = "Versionsverlauf" @@ -3956,6 +4170,11 @@ localHint = "Nur in diesem Browser gespeichert" shared = "Geteilt" sharedHint = "Über Link mit Ihnen geteilt" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Älteste zuerst" modifiedDesc = "Neueste zuerst" @@ -4011,6 +4230,7 @@ noFilesInStorageOpen = "Keine Dateien im Speicher verfügbar. Öffnen Sie zuerst open = "Öffnen" openFile = "Datei öffnen" openFiles = "Dateien öffnen" +selectFile = "Select {{name}}" selectFromStorage = "Aus Speicher auswählen" upload = "Hochladen" uploadFile = "Datei hochladen" @@ -4084,10 +4304,15 @@ issues = "GitHub" [formFill] allSaved = "Alles gespeichert" +analyzingFields = "Analysiere Formularfelder..." extractCsvError = "CSV konnte nicht extrahiert werden" extractXlsxError = "XLSX konnte nicht extrahiert werden" +filled = "ausgefüllt" flattenAfterFilling = "Nach dem Ausfüllen ebnen" -requiredAbbreviation = "erf." +noFields = "Keine ausfüllbaren Formularfelder gefunden." +placeholderEnter = "Eingabe:" +placeholderSelect = "Auswahl:" +requiredAbbreviation = "erf" requiredFieldsError = "Bitte füllen Sie alle Pflichtfelder aus" rescanFields = "Felder erneut scannen" rescanFormFields = "Formularfelder erneut scannen" @@ -4327,6 +4552,11 @@ desc = "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschr tags = "auto-erkennung,kopfzeilen-basiert,organisieren,umbenennen" title = "PDF automatisch umbenennen" +[home.autoRotate] +desc = "Erkennt die Ausrichtung jeder Seite und dreht sie automatisch aufrecht." +tags = "automatisch drehen,Ausrichtung korrigieren,auf dem Kopf,seitlich,begradigen,ausrichten,Ausrichtung erkennen,Drehung korrigieren,automatisch ausrichten,gescannt,OSD" +title = "Automatisch drehen" + [home.autoSizeSplitPDF] tags = "auto,teilen,größe" title = "Teilen nach Größe/Anzahl" @@ -4416,8 +4646,8 @@ tags = "vereinfachen,entfernen,interaktiv" title = "Abflachen" [home.formFill] -desc = "Fill PDF form fields interactively with a visual editor" -title = "Fill Form" +desc = "PDF-Formularfelder interaktiv mit visuellem Editor ausfüllen" +title = "Formular ausfüllen" [home.getPdfInfo] desc = "Erfasst alle möglichen Informationen in einer PDF" @@ -4626,15 +4856,12 @@ hint = "Laden Sie eine PNG-, JPG-, SVG- oder andere Bilddatei hoch, um sie im PD label = "Bild hochladen" placeholder = "Bilddatei auswählen" -[imageToPdf] -tags = "konvertierung,img,jpg,bild,foto" - [imageToPDF] header = "Bild zu PDF" title = "Bild zu PDF" -[infoBanner] -dismiss = "Ausblenden" +[imageToPdf] +tags = "konvertierung,img,jpg,bild,foto" [invite] acceptError = "Konto konnte nicht erstellt werden" @@ -4665,11 +4892,9 @@ welcomeTitle = "Sie wurden eingeladen!" [landing] addFiles = "Dateien hinzufügen" -heroSubtitle = "Ziehen Sie eine PDF hierher oder fügen Sie eine vorhandene hinzu, um zu beginnen." mobileUpload = "Von Mobilgerät hochladen" openFromComputer = "Vom Computer öffnen" uploadFromComputer = "Vom Computer hochladen" -workbenchEmptyStateHero = "PDF irgendwo ablegen" [language] direction = "ltr" @@ -4709,7 +4934,6 @@ enterUsername = "Benutzernamen eingeben" failedToSignIn = "Anmeldung mit {{provider}} fehlgeschlagen: {{message}}" forgotPassword = "Passwort vergessen?" loggingIn = "Anmeldung läuft..." -logIn = "Anmelden" login = "Anmelden" magicLinkSent = "Magic Link wurde an {{email}} gesendet! Prüfen Sie Ihre E-Mails und klicken Sie auf den Link zur Anmeldung." mfaCode = "Authentifizierungscode" @@ -4733,26 +4957,10 @@ signInWith = "Anmelden mit" title = "Anmelden" unexpectedError = "Unerwarteter Fehler: {{message}}" updatePassword = "Passwort aktualisieren" -useEmailInstead = "Mit E-Mail anmelden" useMagicLink = "Stattdessen Magic Link verwenden" username = "Benutzername" youAreLoggedIn = "Sie sind angemeldet!" -[login.slides.edit] -alt = "PDFs bearbeiten" -subtitle = "Mit über einem Dutzend Tools zum Schwärzen, Signieren, Lesen und Bearbeiten von PDFs finden Sie sicher, was Sie suchen." -title = "PDFs bearbeiten, um gewünschte Informationen anzuzeigen/zu schützen" - -[login.slides.overview] -alt = "Stirling PDF Übersicht" -subtitle = "Eine Privacy-First-Cloud-Suite für PDFs, mit der Sie Dokumente konvertieren, signieren, schwärzen und verwalten – plus 50+ weitere leistungsstarke Tools." -title = "Ihr One-Stop-Shop für alle PDF-Anforderungen." - -[login.slides.secure] -alt = "PDFs schützen" -subtitle = "Fügen Sie Passwörter hinzu, schwärzen Sie Inhalte und verwalten Sie Zertifikate mühelos." -title = "Schützen Sie sensible Informationen in Ihren PDFs" - [margin] large = "Groß" medium = "Mittel" @@ -4836,6 +5044,34 @@ uploadSuccess = "Upload Successful!" uploadSuccessMessage = "Ihre Bilder wurden übertragen." validating = "Validating session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scannen Sie, um Fotos hochzuladen. Bilder werden automatisch in PDF konvertiert." descriptionNoConvert = "Scannen Sie, um Fotos von Ihrem Mobilgerät hochzuladen." @@ -4854,8 +5090,13 @@ title = "Von Mobilgerät hochladen" tags = "Multi-Tool,Mehrfachoperation,UI,Klicken und Ziehen,Frontend,clientseitig" title = "PDF-Multitool" -[navbar] -search = "Suche" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Die Authentifizierung war nicht erfolgreich. Sie können dieses Fenster schließen und es erneut versuchen." @@ -4984,6 +5225,7 @@ activeFiles = "Die Ansicht Aktive Dateien zeigt alle PDFs, die allTools = "Dies ist das Panel Alle Tools, in dem Sie alle verfügbaren PDF-Tools durchsuchen und auswählen können." close = "Schließen" cropSettings = "Nachdem wir die Datei ausgewählt haben, die wir zuschneiden möchten, können wir das Tool Zuschneiden konfigurieren, um den Bereich auszuwählen, auf den das PDF zugeschnitten werden soll." +dialogLabel = "Onboarding" fileCheckbox = "Durch Klicken auf eine der Dateien wählen Sie diese zur Verarbeitung aus. Sie können mehrere Dateien für Batch-Operationen auswählen." fileReplacement = "Die geänderte Datei ersetzt automatisch die Originaldatei im Arbeitsbereich, sodass Sie sie einfach mit weiteren Tools verarbeiten können." filesButton = "Mit der Schaltfläche Dateien in der Schnellzugriffsleiste können Sie PDFs hochladen, um die Tools darauf anzuwenden." @@ -4994,6 +5236,7 @@ pinButton = "Mit der Schaltfläche Anheften können Sie dafür results = "Nachdem das Tool fertig ist, zeigt der Schritt Überprüfen in diesem Bereich eine Vorschau der Ergebnisse und ermöglicht es Ihnen, den Vorgang rückgängig zu machen oder die Datei herunterzuladen." runButton = "Sobald das Tool konfiguriert ist, können Sie mit dieser Schaltfläche das Tool auf alle ausgewählten PDFs ausführen." selectCropTool = "Wählen wir das Tool Zuschneiden aus, um zu zeigen, wie eines der Tools verwendet wird." +stepOf = "Schritt {{current}} von {{total}}" toolInterface = "Dies ist die Oberfläche des Tools Zuschneiden. Wie Sie sehen, ist hier noch nicht viel zu sehen, da wir noch keine PDF-Dateien hinzugefügt haben." workbench = "Dies ist der Arbeitsbereich – der Hauptbereich, in dem Sie Ihre PDFs ansehen und bearbeiten." wrapUp = "Alles fertig! Sie haben die Hauptbereiche der App und deren Verwendung kennengelernt. Klicken Sie jederzeit auf die Schaltfläche Hilfe, um diese Tour erneut zu sehen." @@ -5006,6 +5249,26 @@ showMeAround = "Rundgang starten" skipForNow = "Für jetzt überspringen" skipTheTour = "Rundgang überspringen" +[onboarding.checklist] +dismiss = "Ausblenden" +title = "Stirling PDF einrichten" + +[onboarding.checklist.downloadDesktop] +description = "Stirling nativ auf Ihrem Gerät ausführen" +title = "Stirling für Desktop herunterladen" + +[onboarding.checklist.inviteTeam] +description = "Mit Ihrem Team zusammenarbeiten" +title = "Teammitglieder einladen" + +[onboarding.checklist.shareAnalytics] +description = "Helfen Sie, Stirling zu verbessern" +title = "Anonyme Nutzungsdaten teilen" + +[onboarding.checklist.takeTour] +description = "In einer kurzen Einführung sehen, wie Stirling funktioniert" +title = "Tour starten" + [onboarding.desktopInstall] body = "Stirling funktioniert am besten als Desktop-App. Sie können es offline nutzen, schneller auf Dokumente zugreifen und lokal auf Ihrem Computer bearbeiten." selectOs = "Betriebssystem auswählen" @@ -5037,6 +5300,11 @@ adminTitle = "Admin-Übersicht" userBody = "Laden Sie Teammitglieder ein, weisen Sie Rollen zu und halten Sie Ihre Dokumente in einem sicheren Arbeitsbereich organisiert. Aktivieren Sie den Login-Modus, wenn Sie über die Einzelnutzung hinaus wachsen möchten." userTitle = "Plan-Übersicht" +[onboarding.processorIntro] +body = "Stirling führt jetzt Richtlinien aus — automatisierte Regeln, die jedes Dokument beim Eintreffen klassifizieren, schützen und verarbeiten. Richten Sie sie ein und überwachen Sie Ausführungen im Processor." +cta = "Processor ansehen" +title = "Stirling Processor ansehen" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5394,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5433,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Vorausbezahltes Jahr" + +[payg.prepaid.banner] +exhausted = "Ihre vorausbezahlte Kapazität ist aufgebraucht; die verbrauchsabhängige Abrechnung wurde fortgesetzt. Laden Sie auf, um den Rabatt zu behalten." +expiring_one = "Ihr vorausbezahltes Jahr endet in {{count}} Tag. Ungenutzte Kapazität verfällt — laden Sie auf, um ein neues Jahr zu starten." +expiring_other = "Ihr vorausbezahltes Jahr endet in {{count}} Tagen. Ungenutzte Kapazität verfällt — laden Sie auf, um ein neues Jahr zu starten." +low = "Sie haben nur noch {{pct}}% Ihrer vorausbezahlten Kapazität. Laden Sie jederzeit auf, damit die Verarbeitung ohne Unterbrechung weiterläuft." + +[payg.prepaid.card] +subtitle = "Wird vor der verbrauchsabhängigen Abrechnung und außerhalb Ihres Ausgabenlimits verwendet." +title = "Vorausbezahlte Kapazität" + +[payg.prepaid.meter] +capSuffix = "von {{total}} vorausbezahlten Credits" +expires = "Läuft ab am {{date}}" + +[payg.prepaid.state] +exhausted = "Aufgebraucht" +healthy = "Reichlich übrig" +low = "Wird knapp" + [payg.role] leader = "Team owner" member = "Member" @@ -5451,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5779,6 +6071,11 @@ header = "PDF zu PDF/A" tags = "archiv,langfristig,standard,konvertierung,speicherung,aufbewahrung" title = "PDF zu PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "druck,standard,konvertierung,produktion,vorstufe,archiv" title = "PDF in PDF/X" @@ -5854,6 +6151,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Wählen Sie den Plan, der Ihren Anforderungen entspricht" title = "Verfügbare Pläne" @@ -5969,49 +6267,12 @@ keyDescription = "Fügen Sie den Lizenzschlüssel aus Ihrer E-Mail ein" success = "Lizenz aktiviert!" successMessage = "Ihre Lizenz wurde erfolgreich aktiviert. Sie können dieses Fenster nun schließen." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "durchgesetzt" -enforcing = "Wird durchgesetzt..." -failed = "Durchsetzung fehlgeschlagen" outputsUnavailable = "Richtlinienergebnisse stehen nicht mehr zum Download bereit." partialOutputsUnavailable = "Einige Richtlinienergebnisse stehen nicht mehr zum Download bereit." -retrying = "Ausgelastet, versuche es erneut..." runNotFound = "Der Durchsetzungsdurchlauf konnte nicht mehr gefunden werden." -step = "Schritt {{current}}/{{total}}" timedOut = "Zeitüberschreitung bei der Durchsetzung, bevor der Durchlauf abgeschlossen werden konnte." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Schließen" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Wende {{names}} an" applyingProgress = "Wende {{names}} an ({{done}} von {{total}})" @@ -6021,61 +6282,16 @@ failureBody = "{{failures}} von {{total}} Datei(en) konnten nicht verarbeitet we failureTitle = "Ohne vollständige Durchsetzung exportiert" printPolicyAppliedBody = "Dieses PDF wurde aktualisiert, um eine Richtlinie zu erfüllen. Prüfen Sie die Änderungen und drucken Sie anschließend erneut." printPolicyAppliedTitle = "Richtlinie vor dem Drucken angewendet" -queued = "+{{count}} in Warteschlange" successTitle = "{{names}} angewendet" summaryMore = "{{first}}, {{second}} und {{more}} weitere" summaryTwo = "{{first}} und {{second}}" -[policies.enforcement.triggerVerb] -convert = "Durchsetzen vor dem Konvertieren" -default = "Durchsetzen" -export = "Durchsetzen vor dem Export" -input = "Durchsetzen beim Import" -print = "Durchsetzen vor dem Drucken" - -[policies.field] -accessLog = "Zugriffsprotokoll" -archiveAfter = "Archivieren nach" -auditTrail = "Audit-Trail" -belowThreshold = "Unter Schwellenwert" -destination = "Ziel" -frameworks = "Frameworks" -immutableHold = "Unveränderliche Sperre" -keepFor = "Aufbewahren für" -minConfidence = "Min. Konfidenz" -notify = "Bei Weiterleitung benachrichtigen" -onViolation = "Bei Nichteinhaltung" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Hinzufügen" -addPlaceholder = "Label hinzufügen…" -customNote = "Für Ihr Team angepasst." -defaultNote = "Verwendet den integrierten Standard, der mit Ihrem Team geteilt wird." -duplicate = "\"{{name}}\" existiert bereits." -edit = "Labels bearbeiten" -empty = "Noch keine Labels." -export = "JSON exportieren" -iconAria = "Symbol für {{name}} auswählen" -import = "JSON importieren" -importError = "Diese Datei konnte nicht importiert werden." -managedNote = "Team-Labels werden von Ihrer Teamleitung verwaltet." -modalSubtitle = "Mit Ihrem gesamten Team geteilt. Der Klassifizierer wählt die Labels aus, die zu jedem Dokument passen." -modalTitle = "Klassifizierungslabels" -pickIcon = "Symbol {{label}} verwenden" -removeAria = "{{name}} entfernen" -resetToDefault = "Auf Standard zurücksetzen" -saveForTeam = "Für Team speichern" -saving = "Wird gespeichert…" -sectionLabel = "Klassifizierungslabels" -startFromScratch = "Von Grund auf neu beginnen" -teamCount = "Team-Labels" -tooLong = "Labels dürfen höchstens {{max}} Zeichen lang sein." -ungrouped = "Nicht gruppiert" -view = "Labels anzeigen" +categoryCount = "Kategorien" +hideCategory = "Kategorie ausblenden" +labelCount = "Labels" +sharedNote = "Diese Labels sind integriert und werden von Ihrem gesamten Team gemeinsam genutzt." +showCategory = "Kategorie anzeigen" [policies.pii] account = "Kontonummern (beschriftet)" @@ -6088,93 +6304,10 @@ placeholder = "Select PII types" routing = "US-Routingnummern (ABA)" ssn = "Sozialversicherungsnummern" -[policies.settings] -noneExport = "Derzeit werden beim Export keine Richtlinien ausgeführt." -noneUpload = "Derzeit werden beim Upload keine Richtlinien ausgeführt." -onExport = "Beim Export" -onUpload = "Beim Upload" -reorderHandle = "Ziehen, um neu anzuordnen" -runOrderDesc = "Wenn mehrere Richtlinien durch denselben Auslöser ausgeführt werden, laufen sie in dieser Reihenfolge — jede auf der Ausgabe der vorherigen Richtlinie. Ziehen Sie, um neu anzuordnen." -title = "Richtlinieneinstellungen" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Richtlinienoptionen" -policySettings = "Richtlinieneinstellungen" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Fehlgeschlagene Richtlinien erneut versuchen ({{count}})" -rowProgress = "{{completed}} von {{total}} Dateien verarbeitet" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Was ist eine Richtlinie?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Findet automatisch sensible Daten – wie Sozialversicherungs- und Kartennummern – und schwärzt sie, sodass sie im Dokument nicht gelesen werden können." -sanitize = "Entfernt verstecktes JavaScript aus der Datei, sodass beim Öffnen nichts automatisch ausgeführt wird." -watermark = "Bringt einen sichtbaren Vermerk (z. B. \"Vertraulich\") über jede Seite auf." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Schließen" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} wird durchgesetzt..." badgeRan = "Richtlinie {{name}} wurde für diese Datei ausgeführt" +badgeRunning = "{{name}} läuft..." blockingAction = "{{action}} blockiert, während die Richtlinie durchgesetzt wird. Bitte warten..." dismiss = "Overlay ausblenden" enforcingTitle = "Richtlinie wird durchgesetzt..." @@ -6183,6 +6316,7 @@ viewAnyway = "Datei anzeigen (Richtlinie wird noch durchgesetzt)" [policyOption] autoRedactPhi = "PHI automatisch schwärzen" blockExport = "Export blockieren" +ccpa = "CCPA" contracts = "Verträge" documents = "Dokumente" fedramp = "FedRAMP" @@ -6301,86 +6435,6 @@ free = "Editor-Tarif" subscribed = "Processor-Tarif" unlinked = "Nicht verknüpft" -[portal.agentBuilder] -bootstrapFromDocument = "Aus Dokument initialisieren" -sectionsAriaLabel = "Agent-Builder-Abschnitte" -selectorAriaLabel = "Agents" -subtitle = "Entwerfen, testen und veröffentlichen Sie KI-Agenten, die Ihre Dokumente klassifizieren, Inhalte extrahieren und weiterleiten. Definieren Sie Szenarien, begrenzen Sie den Tool-Zugriff, führen Sie ein Golden Set aus und veröffentlichen Sie eine Version." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Abbrechen" -dropzoneText = "Beispieldokument auswählen (PDF oder Bild)" -lead = "Legen Sie ein Beispieldokument ab, und wir schlagen Szenarien sowie ein Extraktionsschema vor, das Sie verfeinern können. Nichts wird veröffentlicht, bevor Sie es geprüft haben." -submit = "Agent initialisieren" -subtitle = "Einen neuen Agent aus einer repräsentativen Datei erstellen" -title = "Aus einem Dokument initialisieren" - -[portal.agentBuilder.empty] -description = "Initialisieren Sie einen Agent aus einem Beispieldokument, um seine Szenarien und sein Extraktionsschema anzulegen, und verfeinern und veröffentlichen Sie ihn anschließend." -title = "Noch keine Agents" - -[portal.agentBuilder.evals] -casesPassing = "Bestandene Fälle" -columnCase = "Eval-Fall" -columnLatency = "Latenz" -columnResult = "Ergebnis" -fail = "fehlgeschlagen" -goldenSetPassRate = "Golden-Set-Erfolgsrate" -latencyMs = "{{ms}} ms" -notRun = "nicht ausgeführt" -pass = "bestanden" -passRate = "Erfolgsrate" -runEvals = "Evals ausführen" - -[portal.agentBuilder.evals.empty] -description = "Evals machen aus Ihren Szenarien ein wiederholbares Golden Set. Führen Sie ein Upgrade durch, um die Erfolgsrate über die Zeit zu erfassen und Veröffentlichungen davon abhängig zu machen." -title = "Noch kein Golden Set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "über Golden Sets hinweg" -activeAgents = "Aktive Agents" -avgPassRate = "Durchschn. Eval-Erfolgsrate" -fleetWide = "flottenweit" -latestPublished = "Zuletzt veröffentlicht" -scenarios = "Szenarien" -testCases = "Testfälle" -totalDescription_one = "{{count}} insgesamt" -totalDescription_other = "{{count}} insgesamt" - -[portal.agentBuilder.scenarios] -add = "Hinzufügen" -addScenario = "Szenario hinzufügen" -enable = "Aktivieren" -expectationLabel = "Erwartetes Verhalten" -expectationPlaceholder = "Was der Agent tun soll" -inEval = "in Eval" -mute = "Stummschalten" -muted = "stummgeschaltet" -nameLabel = "Name" -namePlaceholder = "z. B. Compliance-Eskalation" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Szenarien" -tools = "Tools" -versions = "Versionen" - -[portal.agentBuilder.tools] -broadAccess = "Breiter Zugriff" -deniedHint = "Ausgewählte Tools sind blockiert. Alles andere bleibt aufrufbar." -deniedTools = "Abgelehnte Tools" -governanceGate = "Tool-Governance ist im Enterprise-Tarif verfügbar." -restricted = "Eingeschränkt" -restrictedAccess = "Eingeschränkter Tool-Zugriff" -restrictedDescription = "Alle Tools außer den unten abgelehnten zulassen." - -[portal.agentBuilder.versions] -current = "aktuell" -historyGate = "Vollständiger Versionsverlauf und Rollback sind im Enterprise-Tarif verfügbar." -publish = "Veröffentlichen" -rollBack = "Zurücksetzen" - [portal.assistant] close = "Assistent schließen" error = "Der Assistent konnte nicht erreicht werden." @@ -6397,9 +6451,21 @@ typing = "Schreibt" loading = "Wird geladen" redirectingToEditor = "Weiterleitung zum Editor..." +[portal.billing.activation] +maybeLater = "Vielleicht später" +title = "Processor einschalten" + +[portal.billing.activation.payg] +sub = "Legen Sie ein monatliches Limit fest. Zahlen Sie für das, was Sie ausführen." +title = "Pay-as-you-go" + +[portal.billing.activation.prepay] +badge = "2 Monate kostenlos" +sub = "Eine Rechnung, Karte oder Banküberweisung." +title = "Ein Jahr im Voraus bezahlen" + [portal.billing.checkout] noClientSecret = "Edge-Funktion hat kein client_secret zurückgegeben." -subtitle = "Fügen Sie eine Karte hinzu, um nach Ihrem kostenlosen Editor-Tarif-Guthaben fortzufahren. Stripe erledigt den Rest." title = "Processor-Tarif aktivieren" [portal.billing.checkout.activationSlow] @@ -6407,6 +6473,22 @@ body = "Ihre Zahlung war erfolgreich, aber die Aktivierung dauert etwas länger close = "Schließen" title = "Fast geschafft" +[portal.billing.checkout.cap] +amountAria = "Monatliches Ausgabenlimit" +back = "Zurück" +continue = "Weiter zur Zahlung" +error = "Ihr Ausgabenlimit konnte nicht festgelegt werden" +finePrint = "Rechnungen werden monatlich erstellt. Jederzeit kündbar." +noLimit = "Kein Limit" +note = "Ihnen wird nie mehr als Ihr Limit berechnet. Die Verarbeitung pausiert einfach." +perMonth = "/ Monat" +title = "Ausgabenlimit festlegen" + +[portal.billing.checkout.card] +fields = "Kartennummer · MM / JJ · CVC · ZIP" +label = "Kartendetails" +note = "Kartendetails werden von Stripe erfasst. Stirling speichert niemals PAN oder CVC." + [portal.billing.checkout.error] title = "Checkout konnte nicht gestartet werden" @@ -6415,10 +6497,9 @@ body = "Ihre Zahlung wurde verarbeitet. Wir aktivieren die nutzungsbasierte Vera hint = "Bitte lassen Sie dieses Fenster geöffnet." title = "Ihr Processor-Tarif wird aktiviert..." -[portal.billing.checkout.notConfigured] -bodyAfter = "in der Portal-Env fest, um den In-App-Checkout zu aktivieren." -bodyBefore = "Legen Sie" -title = "Stripe nicht konfiguriert" +[portal.billing.checkout.pay] +limit = "Monatliches Ausgabenlimit {{amount}}/Monat. Die Verarbeitung pausiert am Limit." +limitNoCap = "Kein Ausgabenlimit. Die Verarbeitung pausiert nicht. Rechnungen werden monatlich erstellt." [portal.billing.enterpriseUpsell] cta = "Enterprise-Angebot erstellen" @@ -6441,10 +6522,12 @@ currentPlan = "Aktueller Tarif" freeForever = "Für immer kostenlos" noTeamResolved = "Für Ihr Wallet wurde noch kein Team ermittelt — aktualisieren Sie und versuchen Sie es erneut." ownerOnly = "Nur der Teambesitzer kann den Processor-Tarif aktivieren." +payInvoice = "Rechnung bezahlen, um abzuschließen" planName = "Editor" ssoIncluded = "SSO enthalten" switchOnProcessor = "Processor aktivieren →" unlimitedUsers = "Unbegrenzte Benutzer" +viewQuote = "Angebot ansehen" [portal.billing.invoices] columnAmount = "Betrag" @@ -6497,7 +6580,111 @@ segmentApiDesc = "Direkte API-Anfragen" segmentApiLabel = "API" segmentAutomationDesc = "Automatisierungen & Pipelines" segmentAutomationLabel = "Automatisierung" +sizeMultiplier = "{{formatted}} PDFs verwendeten einen Größenmultiplikator" +summary = "{{unique}} eindeutig · {{units}} Zählereinheiten · {{avg}} Durchschnitt pro PDF" +summaryNoRate = "{{unique}} eindeutig · {{units}} Zählereinheiten" unit = "nutzungsbasierte PDFs" +unitsPending_one = "{{units}} Zählereinheit wartet auf Synchronisierung von verknüpften Instanzen" +unitsPending_other = "{{units}} Zählereinheiten warten auf Synchronisierung von verknüpften Instanzen" + +[portal.billing.prepaid] +capSuffix = "von {{total}} vorausbezahlten Credits" +expires = "Läuft ab am {{date}}" +eyebrow = "Vorausbezahlte Kapazität" +note = "Wird vor der verbrauchsabhängigen Abrechnung und außerhalb Ihres Ausgabenlimits verwendet." +topUp = "Aufladen" + +[portal.billing.prepaid.buy] +back = "Zurück" +calcTitle = "Jahreszahlung berechnen" +cancel = "Abbrechen" +cancelPurchase = "Kauf abbrechen" +close = "Schließen" +continueToPayment = "Weiter zur Zahlung" +finalise = "Abschließen" +invoiceErrorTitle = "Die Rechnung konnte nicht erstellt werden" +loadingTitle = "Ihr Angebot wird geladen" +payTitle = "Für Ihr Jahr bezahlen" +step = "Schritt {{current}} von {{total}}" + +[portal.billing.prepaid.calc] +change = "Ändern" +deployRow = "Bereitstellung" +done = "Fertig" +enterpriseHint = "Dies ist Enterprise-Maßstab. Bei diesem Volumen sind Enterprise-Tarife besser als jeder Self-Service-Rabatt. Preisbindung, Konditionen und ein Angebot in wenigen Minuten." +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} Richtlinien" +handlesLabel = "Ihr Processor" +handlesValue = "verarbeitet {{volume}} Credits / Monat" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} Richtlinien" +poolCaption = "Ein Pool mit {{credits}} Credits für das Jahr. Starke Monate leihen sich von schwachen Monaten." +priceLabel = "Ihr Jahr · 12 Monate zum Preis von 10" +rateUnknown = "Wir zeigen den genauen Preis beim Checkout an." +savings = "Sie sparen {{amount}} · 2 Monate kostenlos." +sizingRow = "Größe" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Benutzer insgesamt" + +[portal.billing.prepaid.consent] +label = "Ich verstehe, dass die Verarbeitung automatisch zum standardmäßigen verbrauchsabhängigen Pay-as-you-go-Tarif fortgesetzt wird (bis zu meinem Ausgabenlimit), wenn meine vorausbezahlte Kapazität aufgebraucht ist oder nach 12 Monaten abläuft, sofern ich nicht kündige, und dass ich jederzeit über das Abrechnungsportal kündigen kann." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Von Stirling verwaltet. In wenigen Minuten live." +cloudValue = "Stirling Cloud · Verwaltet" +selfhost = "Selbst gehostet" +selfhostDesc = "Läuft auf privater Infrastruktur. Gleicher Tarif." +selfhostValue = "Selbst gehostet · Private Infrastruktur" + +[portal.billing.prepaid.offer] +cta = "Angebot prüfen" +subtitle = "Bezahlen Sie ein Jahr PDF-Verarbeitung im Voraus und erhalten Sie zwei Monate kostenlos, verwendet vor der verbrauchsabhängigen Abrechnung, außerhalb Ihres Ausgabenlimits." +title = "12 Monate zum Preis von 10 erhalten" + +[portal.billing.prepaid.pay] +accountName = "Name des Kontoinhabers" +accountNamePlaceholder = "z. B. Jane Smith" +companyName = "Firmenname (optional)" +companyNamePlaceholder = "z. B. Acme Inc." +downloadInvoice = "Rechnung herunterladen" +payOnline = "Online bezahlen" +poNumber = "Bestellnummer (optional)" +poNumberPlaceholder = "z. B. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Erweitert" +advancedDesc = "Pipelines treiben den Großteil Ihrer Verarbeitung an." +none = "Keine" +noneDesc = "Noch keine Pipelines aktiv. Sie können sie jederzeit einschalten." +standard = "Standard" +standardDesc = "Einige Pipelines verarbeiten eingehende PDFs erneut." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Klassifizierung, Standard, plus Teilen" +governed = "Gesteuert" +governedDesc = "Fügt Sicherheit und Routing hinzu" +regulated = "Reguliert" +regulatedDesc = "Jede Kategorie, inkl. Compliance, Aufbewahrung, Ingestion" + +[portal.billing.prepaid.review] +download = "Angebot herunterladen (PDF)" +downloading = "Wird vorbereitet…" +share = "zur Genehmigung teilen" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "Dateien unter 25 MB, keine Datengebühren" +heavy = "Schwer" +heavyDesc = "Gescannt oder bildlastig, regelmäßig 50 MB+" +standard = "Standard" +standardDesc = "Überwiegend klein, einige Scans über 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Aufgebraucht" +healthy = "Reichlich übrig" +low = "Wird knapp" [portal.billing.spendLimit] adjustLimit = "Limit anpassen" @@ -6526,6 +6713,8 @@ label = "Voraussichtlich überschritten." [portal.billing.spendThisMonth] eyebrow = "Ausgaben diesen Monat" +freeRemaining_one = "{{formatted}} kostenlose PDF verbleibend" +freeRemaining_other = "{{formatted}} kostenlose PDFs verbleibend" processed_one = "{{formattedCount}} PDF verarbeitet." processed_other = "{{formattedCount}} PDFs verarbeitet." processedWithRate_one = "{{formattedCount}} PDF verarbeitet, zu je {{rate}}." @@ -6542,6 +6731,7 @@ reachedTitle = "Monatliches Ausgabenlimit erreicht" title = "Stripe-Portal konnte nicht geöffnet werden" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "von {{allowance}} kostenlosen PDFs genutzt" capSuffix_other = "von {{allowance}} kostenlosen PDFs genutzt" eyebrow = "Processor-Testversion" @@ -6566,6 +6756,376 @@ signature = "Signatur" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Neue Verbindung" +createTitleFor = "{{name}} verbinden" +delete = "Löschen" +edit = "Bearbeiten" +editTitle = "Verbindung bearbeiten" +unsupportedType = "Dieser Verbindungstyp kann hier nicht bearbeitet werden." + +[portal.connections.categories.advanced] +description = "Für einen Dienst, den wir noch nicht unterstützen." +label = "Erweitert" + +[portal.connections.categories.audit] +description = "Aufzeichnen, was mit einem Dokument passiert ist, dort, wo Ihre Prüfer bereits nachsehen." +label = "Audit- & Compliance-Protokollierung" + +[portal.connections.categories.notify] +description = "Ein anderes System benachrichtigen, dass etwas passiert ist." +label = "Benachrichtigung & Workflow" + +[portal.connections.categories.security] +description = "Ein Dokument scannen oder labeln, bevor es weitergeleitet wird." +label = "Sicherheit & Klassifizierung" + +[portal.connections.categories.signing] +description = "Ein Dokument zum Signieren oder Beglaubigen versenden." +label = "E-Signatur & Beglaubigung" + +[portal.connections.categories.storage] +description = "Wo verarbeitete Dokumente landen – ein Bucket, ein Laufwerk, ein Ticket oder eine Seite." +label = "Datei & Anhang" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling speichert dies verschlüsselt und zeigt es nie wieder an." +label = "Access Token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Schritte, die diese Verbindung verwenden, können nur Pfade unter dieser URL aufrufen, niemals einen anderen Host." +label = "Server-URL" + +[portal.connections.commonFields.email] +label = "E-Mail" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Passwort" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Benutzername" + +[portal.connections.commonFields.webhookUrl] +helperText = "Fügen Sie die URL ein, die Ihnen der Dienst gegeben hat. Nur Pfade darunter können aufgerufen werden." +label = "Webhook URL" + +[portal.connections.fields] +name = "Verbindungsname" +namePlaceholder = "z. B. Produktion" + +[portal.connections.picker] +cancel = "Abbrechen" +createNew = "Neue Verbindung..." +placeholder = "Verbindung auswählen" +save = "Verbindung speichern" + +[portal.connections.picker2] +back = "Integration ändern" +heading = "Was möchten Sie verbinden?" +noResultsBody = "Wir unterstützen es noch nicht – erzählen Sie uns davon, und wir prüfen, ob wir es hinzufügen können." +noResultsTitle = "Nichts passt zu \"{{query}}\"" +searchPlaceholder = "Integrationen suchen – versuchen Sie \"sign\", \"ocr\" oder einen Produktnamen" + +[portal.connections.types.api] +description = "Beliebige HTTP API aus einer Richtlinie aufrufen. Sie wählen URL, Authentifizierung und Payload." +label = "Benutzerdefinierte API" + +[portal.connections.types.api.fields.authType] +label = "Authentifizierung" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Benutzername und Passwort" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token in einem benutzerdefinierten Header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Keine" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Für einen Token anmelden" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Schritte, die diese Verbindung verwenden, können nur Pfade unter dieser URL aufrufen, niemals einen anderen Host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header-Name" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Anmeldepfad" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Passwort" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Wenn die API mit einem Download-Link antwortet, dürfen nur diese Hosts abgerufen werden. Mit Kommas trennen. Leer lassen, um nur den eigenen Host der Base URL zu erlauben." +label = "Erlaubte Ergebnis-Hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Token als diesen Header senden" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Der Antwort-Header, in dem der Anmeldeaufruf den Token zurückgibt, z. B. X-Auth-Token." +label = "Token-Antwort-Header" + +[portal.connections.types.api.fields.username] +label = "Benutzername" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Dokumente auf einem von Ihnen betriebenen Server auf Malware scannen – nichts verlässt Ihr Netzwerk." +label = "ClamAV (selbst gehostet)" + +[portal.connections.types.cloudmersive] +description = "Viren- und Inhalts-Scanning für Dokumente in einer Richtlinie." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Dokumente blockieren, die Makros, ausführbare Dateien, Skripte oder eingebettete Objekte enthalten – nicht nur bekannte Malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Verarbeitete Dokumente an eine Confluence-Seite anhängen." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Dokumente zur elektronischen Signatur und Beglaubigung an ConsignO Cloud (Notarius) senden." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Ihre ConsignO Cloud-Umgebung, einschließlich /api/v1." +label = "Server-URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Passwort" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Nur erforderlich, wenn Ihre Organisation Tenants verwendet." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Benutzername" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Eine Nachricht an Discord posten, wenn eine Richtlinie ein Dokument verarbeitet." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Jedes Mal ein Audit-Ereignis in Elasticsearch indexieren, wenn eine Richtlinie ein Dokument verarbeitet." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Einen FTP- oder FTPS-Server auf neue Dokumente abfragen." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Verbindungsmodus" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiv" +passive = "Passiv (empfohlen)" + +[portal.connections.types.ftp.fields.security] +helperText = "Einfaches FTP sendet Anmeldedaten und Dateien unverschlüsselt; wählen Sie FTPS, wenn der Server es unterstützt." +label = "Verschlüsselung" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explizit / AUTH TLS)" +implicit = "FTPS (implizit)" +none = "Keine (einfaches FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Eine Nachricht in einen Google Chat-Bereich posten, wenn eine Richtlinie ausgeführt wird." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Verarbeitete Dokumente an ein Jira-Issue anhängen." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Ein Dokument oder eine Benachrichtigung per E-Mail senden, wenn eine Richtlinie ausgeführt wird." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Verarbeitete Dokumente in Nextcloud ablegen." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App-Passwort" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Personenbezogene Daten in Dokumenttext mit einem von Ihnen betriebenen Presidio-Server erkennen." +label = "Presidio (selbst gehostet)" + +[portal.connections.types.purview] +description = "Microsoft Purview-Vertraulichkeitslabels auf PDFs lesen und anwenden." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Optional. Nur erforderlich, um Labels nach Name auszuwählen; Labeling funktioniert auch ohne." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Wird auf jedes Label als SiteId gestempelt, damit Purview-fähige Tools wissen, welche Organisation es angewendet hat." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Ein S3-Bucket, aus dem Quellen lesen und in den Pipeline-Ausgaben schreiben können." +label = "S3-Speicher" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Für AWS leer lassen. Für MinIO oder einen anderen S3-kompatiblen Speicher festlegen." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Ein Dokument oder eine Benachrichtigung per E-Mail senden, wenn eine Richtlinie ausgeführt wird." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Einen SFTP (SSH)-Server auf neue Dokumente abfragen." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optional. Der SHA-256-Hostschlüssel-Fingerabdruck des Servers (von `ssh-keyscan host | ssh-keygen -lf -`). Wenn festgelegt, wird nur ein Server akzeptiert, der diesen Schlüssel präsentiert; wenn leer, wird der beim ersten Verbinden gesehene Schlüssel gepinnt und spätere Änderungen werden abgelehnt." +label = "Hostschlüssel-Fingerabdruck" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Schlüssel-Passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Leer lassen, wenn Sie sich stattdessen mit einem privaten Schlüssel authentifizieren." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optional. Fügen Sie einen OpenSSH-Privatschlüssel ein, um sich mit einem Schlüssel statt mit einem Passwort zu authentifizieren." +label = "Privater Schlüssel" + +[portal.connections.types.slack] +description = "Eine Nachricht an Slack posten, wenn eine Richtlinie ein Dokument verarbeitet." +label = "Slack" + +[portal.connections.types.smb] +description = "Eine SMB / CIFS-Netzwerkfreigabe überwachen (Windows oder Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Netzwerkfreigabe" + +[portal.connections.types.smb.fields.domain] +helperText = "Optional. Die Windows-Domain oder Arbeitsgruppe, falls Ihr Server eine verwendet." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Freigabe" +placeholder = "Dokumente" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Jedes Mal ein Audit-Ereignis an Splunk senden, wenn eine Richtlinie ein Dokument verarbeitet." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Ein Audit-Ereignis an einen Sumo Logic HTTP Collector senden." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Eine Nachricht in einen Teams-Kanal posten, wenn eine Richtlinie ausgeführt wird." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Jedes Dokument oder nur die Details an eine von Ihnen gewählte URL posten." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Einen Zap oder ein Make-Szenario aus einer Richtlinie auslösen." +label = "Zapier & Make" + +[portal.docs] +browse = "Dokumentation durchsuchen" +viewSource = "Quelle auf GitHub ansehen" + [portal.docs.authentication] codeCaption = "jede Anfrage" eyebrow = "ERSTE SCHRITTE" @@ -6656,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Gebündelte, benannte Fähigkeiten, die Ihr Agent als einzelnes Tool aufruft. Jeder Skill ist eine deterministische Operationskette mit angehängten Evals." title = "Agent-Skills" +[portal.docs.toc] +title = "Auf dieser Seite" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API-REFERENZ" @@ -6729,6 +7292,8 @@ type = "Typ" user = "Benutzer" [portal.documents.queue.empty] +connectSource = "Quelle verbinden" +createPipeline = "Pipeline erstellen" description = "Wenn Quellen Dokumente in Ihre Pipelines einspeisen, erscheinen sie hier zur Überprüfung." title = "Keine Dokumente in der Warteschlange" @@ -6744,11 +7309,11 @@ editorAction = "Editor" empty = "Keine Dokumente entsprechen diesem Filter." rowActions = "Zeilenaktionen" sensitiveLabel = "Sensibel" -sensitiveTitle = "Sensibel — Zugriff erforderlich" [portal.documents.table.columns] action = "Pipeline / Aktion" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Status" time = "Zeit" @@ -6764,6 +7329,7 @@ host = "Host" lastSeen = "Zuletzt gesehen" region = "Region" status = "Status" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6838,13 +7404,121 @@ description = "In dieser Ansicht ist ein unerwarteter Fehler aufgetreten. Versuc retry = "Erneut versuchen" title = "Auf dieser Seite ist etwas schiefgelaufen" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Zurück" +body = "Laden Sie die Desktop-App herunter oder hosten Sie den Server selbst." +done = "Fertig" +guide = "{{name}}-Anleitung öffnen" +openInBrowser = "Stattdessen im Browser öffnen" +sectionDesktop = "Desktop-App" +sectionSelfHosted = "Selbst gehostet" +title = "Stirling PDF Editor installieren" + +[portal.home.download.docker] +detailBody = "Führen Sie das offizielle Image auf Ihrem Server aus." +note = "Öffnet auf :8080. Standard-Login: admin / stirling." +tagline = "Server in einem Container ausführen" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installieren Sie das offizielle Helm-Chart." +note = "Fügen Sie ein PVC für /configs hinzu, um Einstellungen dauerhaft zu speichern." +tagline = "Mit Helm in einem Cluster bereitstellen" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native Builds für Ihre Distribution." +guideBtn = "Linux-Installationsoptionen anzeigen" +note = "AppImage-, .deb- und .rpm-Builds sind für gängige Distributionen verfügbar." +tagline = "Native Desktop-App für Ihre Distribution" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Oder über Homebrew" +detailBody = "Führen Sie den Installer aus oder verwenden Sie Homebrew." +downloadBtn = "Für macOS herunterladen" +tagline = "Native App für Apple Silicon und Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Führen Sie die .jar auf einem Bare-Metal-Server aus." +downloadBtn = ".jar herunterladen" +note = "Erfordert Java 21+. Öffnet auf :8080; Standard-Login admin / stirling." +runLabel = "Dann ausführen" +tagline = "Die .jar auf einem Bare-Metal-Server ausführen" +title = "Manuelle Servereinrichtung" + +[portal.home.download.windows] +altLabel = "Oder über winget" +detailBody = "Führen Sie den Installer aus oder verwenden Sie einen Paketmanager." +downloadBtn = "Installer herunterladen" +tagline = "Native App für Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktiv" -invite = "Teammitglieder einladen" +activeOfDeployed = "{{active}} von {{total}} diesen Monat aktiv" name = "Stirling PDF Editor" open = "Im Browser öffnen" updated = "aktualisiert {{time}}" +[portal.home.editor.deploy] +finish = "Bereitstellung abschließen" +options = "Bereitstellungsoptionen anzeigen" +start = "Editor bereitstellen" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Selbst gehostet · Docker" @@ -6855,49 +7529,6 @@ afternoon = "Guten Tag" evening = "Guten Abend" morning = "Guten Morgen" -[portal.home.onboarding] -dismiss = "Einrichtung ausblenden" -notStarted = "Nicht gestartet" -progress = "{{done}} von {{total}} erledigt" -title = "Einrichtung abschließen" - -[portal.home.onboarding.enterprise] -body = "Organisationsweites SSO + SCIM + RBAC, feste Volumenpreise und Air-gapped-Bereitstellung." -cta = "Testversion starten" -ctaQuote = "Angebot anfordern" -lead = "Für 250+ Mitarbeitende." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Stellen Sie die Desktop-App bereit, dauerhaft kostenlos." -title = "PDF Editor herunterladen" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktiv · {{recommended}} empfohlen" -chip = "{{active}} aktiv" -title = "Richtlinien aktivieren" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} verbunden · jedes PDF wird dort gesteuert, wo es landet" -chip = "{{connected}} verbunden" -title = "Quellen verbinden" - -[portal.home.quickActions] -subtitle = "Wichtigste Aufgaben für heute" -title = "Schnellaktionen" - -[portal.home.quickActions.buildPipeline] -blurb = "3-Schritt-Composer über der typisierten Op-Bibliothek" -title = "Pipeline erstellen" - -[portal.home.quickActions.connectSource] -blurb = "S3, Agents, Webhooks, überwachte Ordner" -title = "Quelle verbinden" - -[portal.home.quickActions.issueApiKey] -blurb = "Eingeschränkter Schlüssel mit Ratenlimits und IP-Allowlist" -title = "API-Schlüssel ausstellen" - [portal.infrastructure] manageEditorDeployment = "Editor-Bereitstellung verwalten" sectionsAriaLabel = "Infrastrukturbereiche" @@ -6910,13 +7541,9 @@ heading = "API-Schlüssel" subheading = "Eingeschränkte Zugangsdaten mit Ratenlimits pro Schlüssel, Berechtigungen und IP-Allowlists." [portal.infrastructure.apiKeys.card] -allowedIps = "Erlaubte IPs" -anyIp = "Beliebige IP (keine Allowlist)" created = "Erstellt" lastUsed = "Zuletzt verwendet" -permissions = "Berechtigungen" -rateLimit = "Ratenlimit" -rateLimitValue = "{{value}} Anfragen/min" +revoke = "Schlüssel widerrufen" usageMonth = "Nutzung diesen Monat" usageToday = "Nutzung heute" @@ -6924,6 +7551,15 @@ usageToday = "Nutzung heute" description = "Erstellen Sie einen eingeschränkten Schlüssel, um die Stirling API aufzurufen." title = "Noch keine API-Schlüssel" +[portal.infrastructure.apiKeys.error] +load = "Ihre API-Schlüssel konnten nicht geladen werden. Bitte versuchen Sie es erneut." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" widerrufen? Jede Integration, die diesen Schlüssel noch verwendet, erhält sofort 401-Fehler. Dies kann nicht rückgängig gemacht werden." +cancel = "Abbrechen" +confirm = "Schlüssel widerrufen" +title = "API-Schlüssel widerrufen" + [portal.infrastructure.audit] filterAriaLabel = "Audit-Ereignisse nach Kategorie filtern" heading = "Audit-Protokolle" @@ -6967,6 +7603,7 @@ all = "Alle" auth = "Auth" config = "Konfiguration" elevation = "Berechtigungserweiterung" +policy = "Richtlinie" processing = "Verarbeitung" security = "Sicherheit" @@ -6976,19 +7613,30 @@ title = "Sie haben keinen Zugriff" [portal.infrastructure.audit.metrics] config = "Konfiguration" -elevation = "Berechtigungserweiterung" +policy = "Richtlinie" processing = "Verarbeitung" totalEvents = "Ereignisse gesamt · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Konfiguration" +elevation = "Erhöhung" +policy = "Richtlinie" +processing = "Verarbeitung" +security = "Sicherheit" + +[portal.infrastructure.auditStatusLabel] +danger = "Fehler" +info = "Info" +success = "Erfolg" +warning = "Warnung" + [portal.infrastructure.createKey] cancel = "Abbrechen" createKey = "Schlüssel erstellen" done = "Fertig" -ipAllowlistHelper = "Durch Kommas getrennte CIDR-Bereiche. Leer lassen, um beliebige IPs zu erlauben." -ipAllowlistLabel = "IP-Allowlist" keyNameLabel = "Schlüsselname" keyNamePlaceholder = "z. B. Produktion · Aufnahme" -permissionsLabel = "Berechtigungen" secretKeyCaption = "Geheimer Schlüssel" secretWarning = "Speichern Sie dies in einem Secrets-Manager. Stirling speichert immer nur einen Hash — es gibt keine Möglichkeit, ihn später wiederherzustellen." subtitle = "Beschränken Sie den Schlüssel auf das Minimum, das er benötigt. Sie können ihn jederzeit rotieren oder widerrufen." @@ -6996,207 +7644,9 @@ subtitleCreated = "Kopieren Sie dieses Secret jetzt — es wird nicht erneut ang title = "API-Schlüssel erstellen" titleCreated = "Schlüssel erstellt" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Bereitgestellt von" -environment = "Umgebung" -product = "Produkt" -status = "Status" -version = "Version" -when = "Wann" - -[portal.infrastructure.deployments.recent] -heading = "Letzte Bereitstellungen" -subheading = "Die neuesten Rollouts über Produkte und Umgebungen hinweg." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanzen" -latency = "Latenz" -load = "Last" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Durchsatz" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regionen" -subheading = "Live-Zustand für jede bereitgestellte Stirling-Region — Latenz, Last und Rollout-Version." - -[portal.infrastructure.deployments.regions.empty] -description = "Bereitgestellte Regionen erscheinen hier, sobald Ihr Arbeitsbereich bereitgestellt ist." -title = "Keine Regionen bereitgestellt" - -[portal.infrastructure.models] -heading = "Modelle" -msValue = "{{value}} ms" -subheading = "Der Modellkatalog und das Routing, die die Dokumentverarbeitung in Ihrem Arbeitsbereich antreiben." - -[portal.infrastructure.models.byom] -description = "Registrieren Sie ein On-Prem- oder selbst gehostetes Modell und pinnen Sie es an eine Region für datenresidenzgebundene Verarbeitung." -title = "Eigenes Modell verwenden" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Verwaltete Modelle, die Ihrem Arbeitsbereich zur Verfügung stehen, mit Live-Latenz und Kosten." -subEnterprise = "Verwaltete, eigene und On-Prem-Modelle — mit verfügbarem Pinning pro Region." - -[portal.infrastructure.models.catalogue.empty] -description = "Modelle im Katalog Ihres Arbeitsbereichs erscheinen hier." -title = "Keine Modelle verfügbar" - -[portal.infrastructure.models.columns] -cost = "Kosten" -latency = "Latenz" -load = "Last" -model = "Modell" -status = "Status" -type = "Typ" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "Aktive Modelle" -avgLatency = "Durchschn. Latenz" -included = "Enthalten" -monthlySpend = "Monatliche Modellausgaben" - -[portal.infrastructure.models.routing] -empty = "Keine Routing-Regeln konfiguriert." -heading = "Routing-Regeln" -sub = "Welches Modell welche Operation verarbeitet. Die Standardeinstellung gilt, wenn keine spezifischere Regel passt." -subLocked = "Leiten Sie Operationen an bestimmte Modelle weiter — verfügbar in kostenpflichtigen Plänen." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgraden Sie auf Pro, um zu steuern, welches Modell welche Operation und welchen Dokumenttyp verarbeitet." -title = "Modell-Routing ist eine kostenpflichtige Funktion" - -[portal.infrastructure.models.routingColumns] -default = "Standard" -docType = "Dokumenttyp" -modelForAria = "Modell für {{operation}}" -operation = "Operation" -routedTo = "Weitergeleitet an" - -[portal.infrastructure.security.access.byok] -description = "Stellen Sie einen Schlüssel aus Ihrem eigenen KMS bereit. Stirling verschlüsselt damit, kann aber weiterhin lesen." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Schlüssel verlassen niemals Ihr KMS. Stirling hält nur Ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling verwaltet Verschlüsselungsschlüssel. Am einfachsten — kein Schlüsselbetrieb auf Ihrer Seite." -label = "Von Stirling gehaltene Schlüssel" - -[portal.infrastructure.security.accessPolicy] -heading = "Dokumentzugriffsrichtlinie" -subheading = "Steuert, wer verarbeitete ruhende Dokumente entschlüsseln kann." - -[portal.infrastructure.security.attestations] -heading = "Compliance-Attestierungen" -noReport = "Kein Bericht verfügbar" -subheading = "Audit-Status nach Framework, mit Berichten zu attestierten Kontrollen." -viewReport = "Bericht anzeigen →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestierungen und Zertifizierungen für die Stirling-Plattform." - -[portal.infrastructure.security.empty] -description = "Die Sicherheitskonfiguration Ihres Arbeitsbereichs erscheint hier." -title = "Sicherheitsstatus nicht verfügbar" - -[portal.infrastructure.security.hyokBanner] -description = "Mit HYOK verlassen Verschlüsselungsschlüssel niemals Ihr KMS. Stirling speichert und verarbeitet nur Ciphertext, den Sie jederzeit widerrufen können." -title = "Stirling kann Ihre Dokumente nicht entschlüsseln" - -[portal.infrastructure.security.ipAllowlist] -empty = "Keine IP-Bereiche konfiguriert — alle IPs erlaubt." -heading = "IP-Allowlist" -sub = "API-Zugriff ist auf diese CIDR-Bereiche beschränkt." -subLocked = "Beschränken Sie API-Zugriff auf bekannte IP-Bereiche — verfügbar in kostenpflichtigen Plänen." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgraden Sie auf Pro, um API-Zugriff auf bestimmte Netzwerke zu beschränken." -title = "IP-Allowlisting ist eine kostenpflichtige Funktion" - -[portal.infrastructure.security.ipColumns] -added = "Hinzugefügt" -addedBy = "Hinzugefügt von" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithmus" -heading = "Verwaltung von Verschlüsselungsschlüsseln" -keyId = "Schlüsselkennung" -lastRotated = "Zuletzt rotiert" -rotateKey = "Schlüssel rotieren" -rotationPolicy = "Rotationsrichtlinie" -subheading = "Verwahrung der Schlüssel, die ruhende Dokumente verschlüsseln — wer entschlüsseln kann und wie Schlüssel rotieren." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) und hold-your-own-key (HYOK) sind in Enterprise verfügbar. Upgraden Sie, um Schlüssel aus Ihrem eigenen KMS bereitzustellen." -title = "Schlüssel werden in Ihrem Plan von Stirling verwaltet" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asien-Pazifik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR-Datengrenze" -label = "Europäische Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Vereinigte Staaten" - -[portal.infrastructure.security.residencyHeader] -heading = "Datenresidenz" -subheading = "Wo Dokumente gespeichert und verarbeitet werden." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} genutzt" - -[portal.infrastructure.storage.empty] -description = "Verbundener Speicher und Nutzung erscheinen hier." -title = "Kein Speicher konfiguriert" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktiv" -activeRange = "0–{{value}} T" -archived = "Archiviert" -coldStorage = "Cold Storage" -deleted = "Gelöscht" -never = "nie" -purged = "endgültig gelöscht" - -[portal.infrastructure.storage.providers] -connect = "Verbinden" -connected = "Verbunden" -heading = "Verbundene Anbieter" -subheading = "Wohin verarbeitete Artefakte geschrieben werden." - -[portal.infrastructure.storage.retention] -heading = "Aufbewahrung" -subheading = "Wie lange Artefakte vor der Lifecycle-Löschung aufbewahrt werden." -windowLabel = "Standard-Aufbewahrungsfenster" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} Tag" -days_other = "{{count}} Tage" -never = "Nie löschen" - -[portal.infrastructure.storage.totalUsage] -heading = "Gesamtnutzung" -progressLabel = "Genutzter Speicher" -subheading = "Speicherverbrauch über alle verbundenen Anbieter hinweg." +revoked = "Widerrufen" [portal.infrastructure.tabs] apiKeys = "API-Schlüssel" @@ -7206,6 +7656,49 @@ models = "Modelle" security = "Sicherheit" storage = "Speicher" +[portal.integrations] +availableHeading = "Verfügbar" +comingSoonHeading = "Demnächst" +connect = "Verbinden" +connectionCount_one = "{{count}} Verbindung" +connectionCount_other = "{{count}} Verbindungen" +customApi = "Benutzerdefinierte API" +searchPlaceholder = "Integrationen suchen" +subtitle = "Alles, womit Stirling kommunizieren kann. Anmeldedaten werden hier einmal erstellt und anschließend von Quellen, Richtlinien und Pipelines referenziert." +title = "Integrationen" +typedName = "Name von {{tool}}" + +[portal.integrations.filters] +all = "Alle" +audit = "Audit" +connected = "Verbunden" +notify = "Benachrichtigen" +security = "Sicherheit" +signing = "Signieren" +storage = "Speicher" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Verbunden" + +[portal.integrations.table] +integration = "Integration" +worksWith = "Funktioniert mit" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Richtlinien" +sources = "Quellen" + +[portal.legal] +draft = "{{label}} · Entwurf" +loadError = "Dieses Dokument konnte nicht geladen werden. Bitte versuchen Sie es erneut." +loading = "Wird geladen…" +title = "Rechtsdokument" + [portal.nav] agent-builder = "Agent Builder" components = "Komponenten" @@ -7214,33 +7707,17 @@ documents = "Dokumente" editor = "Editor" home = "Startseite" infrastructure = "Infrastruktur" +integrations = "Integrationen" pipelines = "Pipelines" policies = "Richtlinien" procurement = "Beschaffung" -settings = "Einstellungen" sources = "Quellen" usage = "Nutzung & Abrechnung" users = "Benutzer" -[portal.notifications] -markAllRead = "Alle als gelesen markieren" -title = "Benachrichtigungen" -viewAll = "Alle anzeigen" - -[portal.notifications.ariaLabel] -none = "Benachrichtigungen, keine ungelesenen" -unread_one = "Benachrichtigungen, {{count}} ungelesen" -unread_other = "Benachrichtigungen, {{count}} ungelesen" - -[portal.notifications.count] -allRead = "alle gelesen" -loading = "wird geladen" -new_one = "{{count}} neu" -new_other = "{{count}} neu" - -[portal.notifications.empty] -description = "Keine neuen Benachrichtigungen." -title = "Sie sind auf dem neuesten Stand" +[portal.nav.section] +platform = "PDF-Plattform" +processor = "PDF-Prozessor" [portal.pipelines] subtitle = "Jede automatisierte Dokument-Pipeline im Backend: eine geordnete Kette von Operationen über eine Reihe von Quellen, ausgeführt durch einen Trigger. Klicken Sie auf eine Zeile, um ihre Schritte und Quellen anzuzeigen." @@ -7250,54 +7727,87 @@ title = "Pipelines" newPipeline = "Neue Pipeline" [portal.pipelines.builder] -addStep = "Tool hinzufügen" +activate = "Activate" back = "Zurück zu Pipelines" +cannotFollow = "Kann {{produced}} nicht übernehmen" +chooseAccount = "Konto auswählen" +chooseDestination = "Ziel auswählen" +chooseOperation = "Auswählen, was dieser Schritt tut" +chooseSource = "Quelle auswählen" discard = "Änderungen verwerfen" -enabled = "Aktiviert" +inputs = "Eingabe" +inputSource = "Eingabequelle" +inputTrigger = "Trigger" keepEditing = "Weiter bearbeiten" -needsUpload = "Benötigt eine hochgeladene Datei" +moreActions = "More actions" +needsConfiguring = "Einrichtung erforderlich" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Keine Tools entsprechen Ihrer Suche." -pipelineSettings = "Pipeline-Einstellungen" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Tools suchen" -selectToolBody = "Fügen Sie ein Tool hinzu, um Ihre Pipeline zu erstellen." -selectToolTitle = "Noch keine Tools" -toolSettings = "Tool-Einstellungen" +sendToSystem = "An ein anderes System senden" +stepsIncompatible = "Diese Schritte können nicht mit dem ausgeführt werden, was der vorherige Schritt erzeugt: {{tools}}." +stepsNeedSetup = "Diese Schritte müssen vor dem Speichern noch eingerichtet werden: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Nicht erkannte Operation, unverändert beibehalten." unsavedBody = "Sie haben ungespeicherte Änderungen. Speichern Sie sie vor dem Verlassen oder verwerfen Sie sie?" unsavedTitle = "Ungespeicherte Änderungen" -uploadUnsupported = "Hochgeladene Dateien werden in Pipelines noch nicht unterstützt, daher können diese Schritte nicht gespeichert werden: {{tools}}." usesDefaults = "Läuft mit Standardeinstellungen" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombiniert jede Datei aus dem vorherigen Schritt" +fan-out = "Wird einmal pro Datei aus dem vorherigen Schritt ausgeführt" +format-mismatch = "Benötigt {{accepts}}, aber der vorherige Schritt erzeugt {{produced}}" +output-uncertain = "Wird möglicherweise nicht ausgeführt: Die Ausgabe des vorherigen Schritts hängt davon ab, wie er eingerichtet ist" +source-mismatch = "Benötigt {{accepts}}, aber die Eingabe dieser Pipeline ist {{produced}}" +undeclared-operation = "Kann nicht prüfen, was dieser Schritt akzeptiert" [portal.pipelines.composer] addTool = "Tool hinzufügen" -cancel = "Abbrechen" -chainEmpty = "Fügen Sie ein Tool hinzu, um mit dem Erstellen Ihrer Pipeline zu beginnen." create = "Pipeline erstellen" -directory = "Ausgabeordner" -directoryHelp = "Absoluter Pfad auf dem Server. Muss innerhalb der konfigurierten erlaubten Ordner liegen." +createPaused = "Create paused" editingUnsupported = "Die Anzeige dieser Tool-Parameter zur Bearbeitung wird noch nicht unterstützt." -moveDown = "Nach unten verschieben" -moveUp = "Nach oben verschieben" +editSource = "Edit source" name = "Name" namePlaceholder = "z. B. Schwärzungsprüfung" -noSources = "Noch keine Quellen verbunden. Die Pipeline kann weiterhin auf Dateien ausgeführt werden, die ihr direkt bereitgestellt werden." noToolSettings = "Dieses Tool hat keine konfigurierbaren Einstellungen." -operations_one = "Operation ({{count}})" -operations_other = "Operationen ({{count}})" output = "Ausgabe" -removeStep = "Operation entfernen" save = "Änderungen speichern" scheduleEvery = "Ausführen alle" -sources = "Quellen" -sourcesLoading = "Quellen werden geladen..." trigger = "Trigger" triggerManual = "Nur manuell" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "Tage" hours = "Stunden" minutes = "Minuten" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" löschen? Dies kann nicht rückgängig gemacht werden." cancel = "Abbrechen" @@ -7305,28 +7815,62 @@ confirm = "Löschen" title = "Pipeline löschen?" [portal.pipelines.detail] +clearHistory = "Verlauf löschen" delete = "Pipeline löschen" run = "Jetzt ausführen" [portal.pipelines.empty] action = "Pipeline erstellen" +connectSource = "Quelle verbinden" description = "Erstellen Sie Ihre erste Pipeline: Wählen Sie die Quellen aus, über die sie läuft, verketten Sie die Operationen und wählen Sie, wohin die Ausgabe geht." title = "Noch keine Pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktiv" paused = "Pausiert" total = "Pipelines" -[portal.pipelines.output] -folder = "In Ordner schreiben" -inline = "Dateien zurückgeben" - [portal.pipelines.run] +allProcessed_one = "Nichts auszuführen: Das {{count}} Dokument der Quelle wurde bereits verarbeitet." +allProcessed_other = "Nichts auszuführen: Alle {{count}} Dokumente in den Quellen wurden bereits verarbeitet." completed_one = "Ausführung abgeschlossen." completed_other = "Alle {{count}} Ausführungen abgeschlossen." empty = "Nichts auszuführen: Die Quellen hatten keine zu verarbeitenden Dokumente." failed = "Ausführung fehlgeschlagen: {{error}}" +inFlight = "Nichts Neues auszuführen: Dokumente werden noch aus einem früheren Lauf verarbeitet." +parked_one = "Nichts auszuführen: {{count}} Dokument ist zuvor fehlgeschlagen und geparkt. Beheben Sie die Ursache und löschen Sie dann den Verlauf, um es erneut zu versuchen." +parked_other = "Nichts auszuführen: {{count}} Dokumente sind zuvor fehlgeschlagen und geparkt. Beheben Sie die Ursache und löschen Sie dann den Verlauf, um sie erneut zu versuchen." running = "Ausführung gestartet; noch in Bearbeitung." timeout = "Die Ausführung dauert länger als erwartet; sie kann im Hintergrund noch abgeschlossen werden." @@ -7339,6 +7883,7 @@ name = "Pipeline" sources = "Quellen" status = "Status" steps = "Schritte" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Ordnerüberwachung" @@ -7346,12 +7891,18 @@ manual = "Manuell" schedule = "Geplant" [portal.policies] +defaultName = "{{category}}-Richtlinie" subtitle = "Dauerhafte Automatisierungen, die eine Tool-Pipeline auf jedes Dokument anwenden. Jede Richtlinie wird beim Hochladen oder Exportieren ausgelöst, führt ihre Tool-Kette aus und speichert die erzwungene Version neben dem Original." title = "Richtlinien" [portal.policies.card] comingSoon = "Upgrade auf Enterprise" notSetUp = "Nicht eingerichtet" +requiresAiEngine = "Erfordert AI-Engine" + +[portal.policies.categories.classification] +desc = "Erkennt den Typ jedes Dokuments beim Hochladen und markiert seine Metadaten für Ablage und Suche." +label = "Klassifizierung" [portal.policies.categories.compliance] desc = "Erzwingen Sie HIPAA-, GDPR-, SOC 2- oder FedRAMP-Anforderungen für jedes Dokument." @@ -7376,6 +7927,13 @@ label = "Sicherheit" [portal.policies.config] scopeAll = "Alle Dokumente" +[portal.policies.config.classification] +summary = "Klassifiziert jedes hochgeladene Dokument anhand der Labels Ihres Teams und markiert seine Metadaten." + +[portal.policies.config.classification.rules] +0 = "Klassifizieren" +1 = "Metadaten markieren" + [portal.policies.config.compliance] summary = "Validiert Dokumente anhand regulatorischer Frameworks, bevor sie das System verlassen." @@ -7403,6 +7961,29 @@ minConfidence = "Min. Konfidenz" 2 = "Benennen" 3 = "Normalisieren" +[portal.policies.config.purview] +label = "Microsoft Purview-Sensitivitätslabel anwenden" + +[portal.policies.config.purview.fields] +connection = "Purview-Mandant" +labelId = "Label-ID" +labelIdHelp = "Die GUID des Labels aus dem Purview-Portal. Das Anwenden eines Labels schreibt seine Metadaten in die PDF; es kann nicht verschlüsseln, dafür ist der Microsoft-Client erforderlich." +labelName = "Label-Name (optional)" +labelNameHelp = "Wird neben der ID aufgezeichnet, damit Personen und Tools das Label erkennen können." + +[portal.policies.config.purview.fields.method] +help = "Purview unterscheidet ein von einer Richtlinie angewendetes Label von einem Label, das eine Person ausgewählt hat." +label = "Wie das Label aufgezeichnet wird" +privileged = "Als von einer Person ausgewählt" +standard = "Als automatisch angewendet" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview-Label lesen" + +[portal.policies.config.purviewRead.fields] +connection = "Purview-Mandant" +connectionHelp = "Liest das Label, das das Dokument bereits trägt, damit spätere Schritte darauf reagieren können." + [portal.policies.config.retention] summary = "Erzwingt, wie lange Dokumente aufbewahrt werden, wann sie archiviert und wann sie gelöscht werden." @@ -7470,6 +8051,7 @@ title = "Noch keine Aktivität" [portal.policies.endpoints] addWatermark = "Wasserzeichen" autoRedact = "PII schwärzen" +classifyAndLabel = "Klassifizieren" compressPdf = "Komprimieren" flatten = "Reduzieren" ocrPdf = "OCR" @@ -7480,6 +8062,201 @@ description = "Ihre Richtlinien sind gespeichert und werden angezeigt, sobald di retry = "Erneut versuchen" title = "Backend nicht verfügbar" +[portal.policies.operations] +change = "Ändern, was dieser Schritt tut" +noResults = "Kein Schritt passt dazu. Versuchen Sie es mit einem Produktnamen oder mit \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Schritte suchen – versuchen Sie \"scan\", \"attach\" oder einen Produktnamen" + +[portal.policies.operations.bodyMode] +binary = "Rohdaten der Datei" +json = "In einer JSON-Payload" +multipart = "Als Datei-Upload (multipart)" + +[portal.policies.operations.clamavScan] +description = "Ihr eigener ClamAV-Server prüft das Dokument; nichts verlässt Ihr Netzwerk." +label = "Auf Viren scannen (selbst gehostet)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Lehnt Dokumente ab, die Makros, ausführbare Dateien, Skripte oder eingebettete Objekte enthalten." +label = "Aktive Inhalte blockieren" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive prüft das Dokument, und der Lauf stoppt, wenn es nicht sauber ist." +label = "Auf Viren scannen" + +[portal.policies.operations.confluenceAttach] +description = "Legt das verarbeitete Dokument auf einer Seite ab." +label = "An eine Confluence-Seite anhängen" + +[portal.policies.operations.consignoSubmit] +description = "Übermittelt das Dokument zum Signieren an ConsignO Cloud." +label = "Zur Signatur senden (ConsignO)" +note = "Übermittelt das Dokument. Das Abrufen der signierten Kopie wird noch nicht unterstützt, da ein Schritt kein Ergebnis an einen späteren Schritt weitergeben kann." + +[portal.policies.operations.customApiCall] +description = "Erstellen Sie den Aufruf selbst: Pfad, Methode, Body und Antwortverarbeitung." +label = "Benutzerdefinierte API aufrufen" + +[portal.policies.operations.discordNotify] +description = "Informiert einen Kanal, dass die Richtlinie ein Dokument verarbeitet hat." +label = "Nachricht in Discord posten" + +[portal.policies.operations.elasticIndex] +description = "Zeichnet auf, was die Richtlinie getan hat, als durchsuchbares Dokument." +label = "Audit-Ereignis in Elasticsearch indexieren" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart eignet sich für Upload-APIs; JSON eignet sich für APIs, die die Datei in einer Payload erwarten; Binär sendet die Rohdaten." +label = "So wird das Dokument gesendet" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Ein JSON-Body, der unverändert gesendet wird. {{document.base64}} enthält die Datei; {{document.*}} und {{run.*}} werden pro Dokument ausgefüllt." +label = "JSON-Body" + +[portal.policies.operations.fields.connection] +label = "Konto" + +[portal.policies.operations.fields.domain] +label = "Mailgun-Domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Das Formularfeld, unter dem die API das Dokument erwartet." +label = "Name des Dateifelds" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Von" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Ein JSON-Objekt mit Header-Namen/Wert-Paaren. Die Anmeldedaten der Verbindung werden für Sie hinzugefügt." +label = "Header" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue-Schlüssel" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Wird als Nachrichtentext gesendet. {{document.*}} und {{run.*}} werden pro Dokument ausgefüllt." +label = "Nachricht" + +[portal.policies.operations.fields.method] +label = "Methode" + +[portal.policies.operations.fields.pageId] +label = "Seiten-ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Wird an die Basis-URL der Verbindung angehängt. Er kann niemals einen anderen Host erreichen." +label = "Pfad" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Wohin im Drive geschrieben werden soll. {{document.filename}} wird pro Dokument ausgefüllt." +label = "Zielpfad" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Bericht lässt das Dokument unverändert. Ersetzen tauscht es gegen das aus, was zurückkommt." +label = "Was mit der Antwort geschehen soll" + +[portal.policies.operations.fields.signerEmail] +label = "E-Mail des Unterzeichners" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Betreff" +placeholder = "Verarbeitet: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analysiert diesen Text. Verwenden Sie {{document.title}} oder fügen Sie den Text ein, den Sie prüfen möchten." +label = "Zu prüfender Text" + +[portal.policies.operations.fields.to] +label = "An" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud-Benutzername" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informiert einen Space, dass die Richtlinie ein Dokument verarbeitet hat." +label = "Nachricht in Google Chat posten" + +[portal.policies.operations.jiraAttach] +description = "Legt das verarbeitete Dokument in einem Issue ab." +label = "An einen Jira-Issue anhängen" + +[portal.policies.operations.mailgunEmail] +description = "Sendet das verarbeitete Dokument als Anhang." +label = "Dokument per E-Mail senden (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Schreibt das verarbeitete Dokument in einen Ordner." +label = "In Nextcloud hochladen" + +[portal.policies.operations.presidioAnalyze] +description = "Ihr eigener Presidio-Server meldet die personenbezogenen Daten, die er findet." +label = "Personenbezogene Daten erkennen" +note = "Presidio liest Text, keine Dateien. Geben Sie den Text an, den Sie prüfen möchten — das Dokument selbst wird nicht hochgeladen." + +[portal.policies.operations.responseMode] +replace = "Dokument durch die Antwort ersetzen" +report = "Dokument behalten, Antwort aufzeichnen" + +[portal.policies.operations.sendgridEmail] +description = "Sendet das verarbeitete Dokument als Anhang." +label = "Dokument per E-Mail senden (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informiert einen Kanal, dass die Richtlinie ein Dokument verarbeitet hat." +label = "Nachricht in Slack posten" + +[portal.policies.operations.splunkEvent] +description = "Zeichnet auf, was die Richtlinie getan hat, dort, wo Ihre Auditoren bereits nachsehen." +label = "Audit-Ereignis an Splunk senden" + +[portal.policies.operations.sumologicEvent] +description = "Zeichnet auf, was die Richtlinie getan hat, an einen HTTP-Collector." +label = "Audit-Ereignis an Sumo Logic senden" + +[portal.policies.operations.teamsNotify] +description = "Informiert einen Kanal, dass die Richtlinie ein Dokument verarbeitet hat." +label = "Nachricht in Teams posten" + +[portal.policies.operations.webhookPost] +description = "Sendet das Dokument selbst an eine URL Ihrer Wahl." +label = "Dokument an einen Webhook posten" + +[portal.policies.operations.zapierNotify] +description = "Übergibt die Details des Laufs an Ihre Automatisierung." +label = "Zap oder Make-Szenario auslösen" + [portal.policies.stats] activeFor = "Aktiv" dataProcessed = "Verarbeitete Daten" @@ -7505,6 +8282,13 @@ label = "Dokumente erzwungen" description = "Konfiguriert, aber nicht aktiv" label = "Pausiert" +[portal.policies.table] +appliesTo = "Gilt für" +docs = "Durchgesetzte Dokumente" +enforces = "Setzt durch" +policy = "Richtlinie" +status = "Status" + [portal.policies.wizard.actions] back = "Zurück" cancel = "Abbrechen" @@ -7512,6 +8296,10 @@ continue = "Weiter" enablePolicy = "Richtlinie aktivieren" saveChanges = "Änderungen speichern" +[portal.policies.wizard.capability.classify] +desc = "Erkennt den Dokumenttyp anhand der Labels Ihres Teams und markiert ihn, damit Ablage und Suche nach Kategorie erfolgen." +label = "Dokument klassifizieren" + [portal.policies.wizard.capability.compress] desc = "Komprimiert das Dokument auf eine kleinere Dateigröße." label = "Dateigröße reduzieren" @@ -7532,10 +8320,18 @@ label = "Vertrauliche Informationen schwärzen" desc = "Entfernt verstecktes JavaScript, damit beim Öffnen des Dokuments nichts automatisch ausgeführt werden kann." label = "Aktive Inhalte entfernen" +[portal.policies.wizard.capability.timestampPdf] +desc = "Beweist mithilfe einer unabhängigen Zeitstempelstelle, dass das Dokument zu einem bestimmten Zeitpunkt in genau dieser Form existierte. Es wird nur ein Hash gesendet – das Dokument verlässt niemals Ihren Server." +label = "Vertrauenswürdigen Zeitstempel hinzufügen" + [portal.policies.wizard.capability.watermark] desc = "Stempelt eine sichtbare Markierung (z. B. „Vertraulich“) auf jede Seite." label = "Wasserzeichen anwenden" +[portal.policies.wizard.classification] +description = "Jedes hochgeladene Dokument wird anhand der integrierten Labels klassifiziert und mit den passenden Typen markiert. Der Label-Satz wird in Ihrem gesamten Team geteilt." +labelsHeading = "Klassifizierungslabels" + [portal.policies.wizard.errors] noTools = "Aktivieren Sie zuerst mindestens ein Tool im Workflow." saveFailed = "Die Richtlinie konnte nicht gespeichert werden. Bitte versuchen Sie es erneut." @@ -7580,73 +8376,118 @@ setUp = "{{category}}-Richtlinie einrichten" [portal.policies.wizard.workflow] description = "Wählen Sie aus, was diese Richtlinie mit jedem Dokument macht, das sie verarbeitet." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktiv" -noRule = "Noch keine Regel erzwungen" -subtitle = "Ständige Automatisierungen, die jedes Dokument durchläuft, unabhängig davon, welche Pipeline es verarbeitet." -title = "Was für Ihre PDFs ausgeführt wird" - [portal.policySummary.action] -comingSoon = "Auf Enterprise upgraden" -configure = "Konfigurieren" setUp = "Einrichten" -[portal.policySummary.column] -activeRule = "Aktive Regel" -policy = "Richtlinie" -status = "Status" +[portal.processorFlow] +footnote = "Zählungen beziehen sich auf die letzten 24 Stunden. Die Flussgeschwindigkeit dient der Veranschaulichung." +liveBadge = "Live" +stats = "{{connected}} verbunden · {{processed}} PDFs verarbeitet" +title = "PDF-Prozessor" -[portal.policySummary.empty] -description = "Sobald Richtlinien konfiguriert sind, erscheinen die Kategorien hier." -title = "Noch keine Richtlinien" +[portal.processorFlow.lens] +ariaLabel = "Ansichtsmodus" +flow = "Fluss" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktiv" -off = "Aus" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Fehlgeschlagen" +heading = "Ergebnisse" +success = "Ausgeliefert" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktiv" +count = "{{n}} · 24 h" +heading = "Richtlinien" +setUp = "Einrichten" soon = "Bald" -[portal.processingStatus] -managePlan = "Plan verwalten" -volumeSuffix = "Verarbeitete PDFs · letzte 30 Tage" +[portal.processorFlow.sankey] +waist = "Richtlinien · {{n}} aktiv" + +[portal.processorFlow.sankey.empty] +description = "Verbinden Sie eine Quelle und schalten Sie eine Richtlinie ein, um zu sehen, wie Dokumente durch den Prozessor fließen." +title = "Noch kein Fluss" + +[portal.processorFlow.sources] +comingSoonTag = "Verbinden" +editor = "Stirling PDF Editor" +heading = "Quellen" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-Mail-Eingang" [portal.procurement] -reset = "Beschaffung zurücksetzen (Demo)" subtitle = "Lassen Sie Ihr Team evaluieren, vertraglich binden und einrichten. Jedes Dokument an einem Ort." title = "Beschaffung" -[portal.procurement.action] -download = "Download" -pay = "Jetzt bezahlen" -request = "Anfordern" -sign = "Prüfen & unterschreiben" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Zustimmen & abonnieren" +confidential = "Vertraulich" confirm = "Ich habe die Stirling Enterprise Agreement gelesen und stimme ihr zu." -eyebrow = "Vereinbarung" -intro = "Eine kombinierte Vereinbarung deckt Ihren Deal ab: Master Service Agreement, Bestellformular, EULA und Data Processing Agreement. Prüfen Sie sie und stimmen Sie dann zu, um das Angebot als verbindliches Abonnement anzunehmen." -title = "Ihre Enterprise-Vereinbarung prüfen" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Bestellformular, EULA und DPA, zusammengefasst in einer Signatur." +download = "Herunterladen" +downloadDraftError = "Die Vereinbarungs-PDF konnte nicht erstellt werden – der Dokument-Renderer ist nicht verfügbar. Bitte versuchen Sie es in Kürze erneut oder kontaktieren Sie den Support." +downloadError = "Die signierte Vereinbarung konnte nicht heruntergeladen werden. Bitte versuchen Sie es erneut." +legalName = "Name der juristischen Person" +legalNamePlaceholder = "Die juristische Person, die unterschreiben wird" +loadError = "Die Vereinbarung konnte nicht geladen werden. Bitte versuchen Sie es erneut." +loading = "Vereinbarung wird geladen..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Änderungen anfordern" +scrollHint = "Zum Prüfen scrollen" +signatory = "Name des Unterzeichners" +signatoryPlaceholder = "Vollständiger Name" +signatoryTitle = "Titel des Unterzeichners" +signatoryTitlePlaceholder = "z. B. General Counsel" +signError = "Ihre Signatur konnte nicht erfasst werden. Bitte versuchen Sie es erneut." [portal.procurement.builder] addons = "Add-ons" +addressLine1 = "Adresszeile 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresszeile 2" +addressLine2Placeholder = "Suite, Etage, Gebäude" back = "Zurück" businessName = "Unternehmensname" businessNamePlaceholder = "Ihr Unternehmen" +city = "Stadt" +cityPlaceholder = "San Francisco" +completeRequired = "Bitte füllen Sie die Pflichtfelder (mit * markiert) mit einer gültigen E-Mail-Adresse aus, bevor Sie das Angebot erstellen." +contactEmail = "Kontakt-E-Mail" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktname" +contactNamePlaceholder = "Jane Doe" continue = "Weiter" -country = "Land" -countryEuro = "Eurozone (EUR €)" -countryUK = "Vereinigtes Königreich (GBP £)" -countryUS = "Vereinigte Staaten (USD $)" +done = "Fertig" eula = "Ich habe die Stirling Enterprise EULA gelesen und stimme ihr zu. Sie regelt die Vereinbarung, die aus diesem Angebot generiert wird." generate = "Angebot erstellen" -included = "Enthalten" indemnification = "IP-Freistellung" indemnificationSub = "Wir verteidigen qualifizierende IP-Ansprüche gemäß der EULA" -offlineLicense = "Offline-/Air-Gapped-Lizenz" -offlineLicenseSub = "Eine herunterladbare Lizenzdatei für eine air-gapped, selbst gehostete Instanz" +paperEyebrow = "Enterprise-Angebot" +paperFor = "Erstellt für" +pdfSize = "PDF-Größe" +poNumber = "Bestellnummer" +poNumberPlaceholder = "Optional" +postalCode = "Postleitzahl" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} Richtlinien" +posture_essentials = "Essentials" +posture_essentialsSub = "Klassifizierung + Freigabe — die Standardwerte" +posture_governed = "Governed" +posture_governedSub = "Fügt Sicherheit und Routing hinzu" +posture_regulated = "Regulated" +posture_regulatedSub = "Jede Kategorie — Compliance, Aufbewahrung, Ingestion" qbr = "Quartalsweise Geschäftsbesprechungen" qbrSub = "Ihr SE prüft Nutzung und Roadmap jedes Quartal" +region = "Bundesland / Region" +regionPlaceholder = "Kalifornien" running = "{{annual}} / Jahr · {{years}} Jahre {{tcv}}" s1Sub = "Ihr Team und die PDFs, die Sie voraussichtlich jedes Jahr verarbeiten." s1Title = "Wie viel werden Sie verarbeiten?" @@ -7655,6 +8496,12 @@ s2Title = "Verpflichtung und Service" s3Sub = "Für das Angebot und die Vereinbarung, die daraus generiert wird." s3Title = "Ihre Angaben" serviceLevel = "Service-Level" +size_compact = "Kompakt" +size_compactSub = "Hauptsächlich Text, unter 1 MB" +size_heavy = "Schwer" +size_heavySub = "Gescannt oder bildlastig, 5 MB+" +size_standard = "Standard" +size_standardSub = "Gemischter Text und Bilder, 1 bis 5 MB" slDedicated = "Dediziert" slDedicatedSub = "4 Geschäftsstunden · dedizierter Account-Manager · +30 %" slPriority = "Priority" @@ -7662,6 +8509,8 @@ slPrioritySub = "Gleicher Geschäftstag · benannter CSM · +15 %" slStandard = "Standard" slStandardSub = "Nächster Geschäftstag · geteilter CSM · enthalten" stepOf = "Schritt {{n}} von {{total}}" +taxId = "USt-IdNr. / Steuernummer" +taxIdPlaceholder = "Optional" term = "Laufzeit" termDiscount = "{{pct}} % Rabatt für mehrjährige Verpflichtung angewendet" title = "Ihr Angebot erstellen" @@ -7669,6 +8518,7 @@ training = "Onboarding & Schulung" trainingSub = "Live-Sitzungen, damit Ihr Team starten kann" users = "Benutzer insgesamt" usersPlaceholder = "z. B. 250" +viewEula = "Lesen" volEstimated = "Geschätzt anhand von {{count}} Benutzern (~2.000 PDFs pro Person, einschließlich Automatisierung). Bearbeiten Sie dies, wenn Sie es genauer wissen." volManual = "Ihre Angabe wird verwendet. Sie können jederzeit anhand Ihrer Teamgröße neu schätzen." volNoUsers = "Nicht sicher? Geben Sie Ihre Teamgröße ein, und wir schätzen sie." @@ -7677,55 +8527,55 @@ volumePlaceholder = "z. B. 1.000.000" years_one = "{{count}} Jahr" years_other = "{{count}} Jahre" -[portal.procurement.docs] -count_one = "{{count}} Dokument" -count_other = "{{count}} Dokumente" -done = "Erledigt" -here = "Sie sind hier" -hide = "Ausblenden" -optional = "Optional" -paidAddon = "Kostenpflichtiges Add-on" -show = "Anzeigen" -subtitle = "Alles, was Sie in jedem Schritt benötigen, erscheint passend zum Fortschritt des Deals." -supportingSubtitle = "SOC 2, Sicherheitsprüfungen, Steuerformulare und mehr, bereit, wenn Ihr Sicherheits- oder Beschaffungsteam danach fragt. Einige sind mit einer einmaligen Gebühr verbunden." -supportingTitle = "Unterstützung Ihrer Evaluation" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Bestellformular und DPA in einer Signatur." +download = "Herunterladen" +eula = "EULA & Commercial Terms" +eulaSub = "Die Software- und Self-Service-Geschäftsbedingungen." +invoice = "Rechnung" +invoiceSub = "Ihre erste Abonnementrechnung." +laterInvoice = "Verfügbar, nachdem Sie akzeptiert haben" +laterQuote = "Verfügbar, sobald Ihr Angebot ausgestellt wurde" +quote = "Angebot" +quoteSub = "Ihr aufgeschlüsseltes Enterprise-Angebot." +sla = "SLA-Anlage" +slaSub = "Supportstufen und Reaktionsziele." +subprocessors = "Unterauftragsverarbeiter" +subprocessorsSub = "Dritte, die Daten in Ihrem Auftrag verarbeiten." +subtitle = "Ihre Vertragsunterlagen, jederzeit verfügbar." title = "Dokumente" -upcoming = "Bevorstehend" +view = "Anzeigen" [portal.procurement.error] title = "Etwas ist schiefgelaufen" [portal.procurement.hero] -company = "Ihr Enterprise-Deal" +barAria = "Phase {{current}} von {{total}}" ctaAgreement = "Vereinbarung prüfen & unterschreiben" -ctaLive = "Sie sind live" -ctaPayment = "Zahlung hinzufügen" +ctaExploring = "Testversion einrichten" ctaQuote = "Ihr Angebot prüfen" +ctaReviewQuote = "Angebot prüfen" ctaTrial = "Ihr Angebot erstellen" +documents = "Dokumente" eyebrow = "Enterprise-Beschaffung" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Teammitglieder einladen" -keyDocs = "Wichtige Dokumente" -nextStep = "Nächster Schritt: {{action}}" -notStarted = "Nicht gestartet" +licenseKey = "Lizenzschlüssel" +liveSub = "Ihre Organisation ist bereitgestellt und Ihre Lizenz ist aktiv." +liveTitle = "Sie sind live auf Stirling Enterprise" +next = "Weiter: {{stage}}" open = "Beschaffung öffnen" scheduleCall = "Anruf planen" -setup1Sub = "Laden Sie Ihre Teammitglieder ein" -setup1Title = "PDF Editor bereitstellen" -setup2Sub = "Aktivieren Sie die Verarbeitung über Editoren und andere Quellen hinweg" -setup2Title = "PDF Processor verbinden" -setup3Sub = "Aktivieren Sie Sicherheit, Compliance, Routing oder Aufbewahrung, wenn Sie sie benötigen" -setup3Title = "Empfohlene Richtlinien hinzufügen" +sentenceAgreement = "die Vereinbarung prüfen und unterschreiben" +sentenceLive = "Bereitstellung und Rollout" +sentencePayment = "Bestellung und Zahlung" +sentenceQuote = "Ihr Angebot prüfen und akzeptieren" +sentenceTrial = "Ihren Test-Workspace einrichten" [portal.procurement.journey] daysLeft_one = "Noch {{count}} Tag" daysLeft_other = "Noch {{count}} Tage" -engineerLabel = "Ihr Solutions Engineer" -eyebrow = "Ihr Rollout" -live = "Sie sind live auf Stirling Enterprise" -nextStep = "Nächster Schritt: {{action}}" -subtitle = "Ihr Solutions Engineer ist bei jedem Schritt dabei. Immer nur die nächste Aktion; die vollständige Checkliste finden Sie unten." -title = "Vom Test bis zum Live-Betrieb, ein geführter Weg" -trialTitle = "Enterprise-Testversion" [portal.procurement.journeySteps.agreement] blurb = "Eine Unterschrift deckt MSA, Bestellformular, EULA und DPA ab." @@ -7759,6 +8609,9 @@ downloadError = "Die Offline-Lizenzdatei konnte noch nicht generiert werden — downloadOffline = "Offline-Lizenz herunterladen (.lic)" hint = "Fügen Sie diesen Schlüssel in eine selbst gehostete Instanz ein, um sie zu aktivieren, oder bewahren Sie ihn für Ihre Unterlagen auf. Bewahren Sie ihn sicher auf." label = "Ihr Lizenzschlüssel" +subtitle = "Aktivieren Sie mit diesem Schlüssel eine selbst gehostete Instanz oder bewahren Sie ihn für Ihre Unterlagen auf." +title = "Ihr Lizenzschlüssel" +trialFileHint = "Dies ist Ihre Testlizenzdatei. Sobald Ihre Vereinbarung abgeschlossen ist, kommen Sie zurück und laden Sie sie erneut herunter — im Gegensatz zum Online-Lizenzschlüssel aktualisiert sich die .lic-Datei nicht von selbst." [portal.procurement.link] cta = "Konto verknüpfen" @@ -7771,65 +8624,62 @@ description = "Ihr Abonnement ist aktiv und Ihre Lizenz wurde ausgestellt. Ihr T eyebrow = "Live" title = "Sie sind live auf Stirling Enterprise" -[portal.procurement.locked] -description = "Testschlüssel, Angebote mit verbindlichem Volumen, die Ein-Signatur-Vereinbarung, Zahlung und Ihr Dokumentenregister befinden sich alle hier, sobald Sie eine Enterprise-Evaluation starten." -eyebrow = "Nur Enterprise" -talkToSales = "Mit Vertrieb sprechen" -title = "Der Beschaffungspfad öffnet sich mit Enterprise" - [portal.procurement.milestone] -accept = "Akzeptieren & fortfahren" -description = "Laden Sie das PDF herunter, um es mit Ihrem Team zu teilen, kommen Sie zurück, um es zu akzeptieren, wenn Sie bereit sind, oder nehmen Sie Änderungen vor." -download = "PDF herunterladen" downloadError = "Das Angebots-PDF konnte noch nicht heruntergeladen werden — bitte versuchen Sie es gleich erneut." -edit = "Angebot bearbeiten" -eyebrow = "Angebot {{number}}" -perYear = " / Jahr" -preparedFor = "Erstellt für {{company}}" -tcv = "{{value}} Gesamtvertragswert" -title = "Ihr Angebot ist bereit" - -[portal.procurement.modal] -cancel = "Abbrechen" -chooseFile = "Datei auswählen" -close = "Schließen" -downloadBody = "Ihr Download beginnt in Kürze." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Keine Datei ausgewählt" -payBody = "Bezahlen Sie Ihren verbindlichen Vertrag per Karte oder Banküberweisung über Stripe. Ihr Workspace wird bereitgestellt, sobald die Zahlung eingegangen ist." -payCta = "Weiter zu Stripe" -payTitle = "Zahlung bestätigen" -requestBodyFree = "Wir generieren dies auf Anfrage. Bestätigen Sie, und Ihr Solutions Engineer sendet es Ihnen in Kürze zu." -requestBodyPaid = "Dies ist ein kostenpflichtiges Add-on. Bestätigen Sie, und Ihr Solutions Engineer wird den Umfang festlegen und die Unterlagen senden." -requestCta = "Anfordern" -requestTitle = "Dieses Dokument anfordern" -signBody = "Öffnet die Stirling Enterprise Agreement zur elektronischen Signatur: Eine Unterschrift deckt MSA, Bestellformular, EULA und DPA ab. Wir gegenzeichnen automatisch, und Sie fahren mit der Zahlung fort." -signCta = "Zur Unterschrift öffnen" -signTitle = "Ihre Vereinbarung prüfen und unterschreiben" -uploadBody = "Senden Sie uns Ihre Bestellung, und wir stellen sie Ihnen zu Ihren Bedingungen in Rechnung. Ziehen Sie das PDF hinein oder wählen Sie unten eine Datei aus." -uploadCta = "Bestellung hochladen" -uploadTitle = "Ihre Bestellung hochladen" [portal.procurement.payment] description = "Ihr Angebot wurde akzeptiert und Ihre Lizenz ist bereits aktiv — Ihr Team kann sofort starten. Bezahlen Sie die erste Rechnung, wenn Sie bereit sind; Sie können sie hier bezahlen oder herunterladen, keine E-Mail erforderlich." +downloadAgreement = "Signierte Vereinbarung herunterladen" downloadInvoice = "Rechnung herunterladen" -simulate = "Zahlungseingang simulieren (Demo)" +eyebrow = "Zahlung" title = "Abonnement erstellt" viewInvoice = "Rechnung anzeigen & bezahlen" +[portal.procurement.review] +acceptCta = "Angebot akzeptieren" +annual = "Jahresgebühr (Jahr 1)" +downloadCta = "Angebot herunterladen" +poNumber = "Bestellung: {{po}}" +renewal = "Verlängert sich nach der Laufzeit zu {{amount}}/Jahr (+{{pct}} % VPI)" +tcv = "{{years}}-Jahres-Laufzeit, vollständig im Voraus bezahlt: {{tcv}}" +validUntil = "Gültig bis {{date}}" + [portal.procurement.schedule] fallback = "Der Planer konnte nicht geladen werden." fallbackLink = "Terminplanung in einem neuen Tab öffnen" +loading = "Planer wird geladen..." subtitle = "Ihr Solutions Engineer führt Ihr Team durch den Rollout. Wählen Sie eine passende Zeit aus." title = "Anruf planen" -[portal.procurement.status] -action = "Aktion erforderlich" -available = "Verfügbar" -complete = "Abgeschlossen" -pending = "Ausstehend" -request = "Auf Anfrage" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Abgeschlossen, keine ausgehenden Verbindungen" +back = "Zurück" +businessName = "Firmenname" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Verwaltet, schnellster Start" +continue = "Weiter" +deployment = "Wo soll es bereitgestellt werden?" +eula = "Ich stimme der Stirling EULA & Commercial Terms zu." +fullName = "Vollständiger Name" +fullNamePlaceholder = "Ihr Name" +invites = "Teammitglieder einladen · optional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Anruf planen" +seats = "Testende Personen" +seatsPlaceholder = "z. B. 15" +selfhost = "Selbst gehostet" +selfhostSub = "Docker oder Kubernetes" +start = "Testversion starten" +stepOf = "Schritt {{n}} von {{total}}" +subtitle = "14 Tage kostenlos, keine Karte erforderlich." +subtitleDetails = "Ein paar Details für Ihr Angebot und Ihre Vereinbarung." +talkFirst = "Möchten Sie lieber zuerst sprechen?" +title = "Testversion einrichten" +viewEula = "Lesen" +workEmail = "Geschäftliche E-Mail" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Durch Verlängern werden 7 Tage hinzugefügt und Ihr Solutions Engineer wird benachrichtigt." @@ -7840,109 +8690,113 @@ maxed = "Ausgeschöpft" subtitle = "Ihre kostenlose Testversion läuft bis {{date}}. Keine Karte erforderlich." title = "Enterprise-Testversion" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Preise für verbindliches Volumen, organisationsweites SSO + SCIM + RBAC, 90 Tage unveränderliches Audit und ein dedizierter SE." -homeCta = "Testversion starten →" -homeHeadline = "Verarbeiten Sie Millionen von PDFs." - -[portal.recentActivity] -title = "Letzte Aktivität" -viewAll = "Alle anzeigen" - -[portal.recentActivity.empty] -description = "Pipeline-Läufe, Deployments und Agent-Ereignisse werden hier angezeigt." -title = "Hier ist noch nichts" - -[portal.search] -ariaLabel = "Suchen" -placeholder = "Stirling durchsuchen — Endpoints, Pipelines, Dokumente…" - -[portal.search.empty] -noActionsDescription = "Schnellaktionen werden hier angezeigt, sobald sie verfügbar sind." -noActionsTitle = "Keine Schnellaktionen" -noMatches = "Keine Treffer für „{{query}}“" -noMatchesDescription = "Versuchen Sie ein anderes Schlüsselwort oder durchsuchen Sie den Katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Kontoverknüpfung" -[portal.shell.header] -accountFallback = "Konto" -accountMenu = "Kontomenü" -darkMode = "Dunkelmodus" -lightMode = "Hellmodus" -search = "Suchen" -searchPlaceholder = "Suchen…" -signOut = "Abmelden" -switchToDark = "Zum dunklen Theme wechseln" -switchToLight = "Zum hellen Theme wechseln" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} Dokumente" linkAccount = "Stirling-Konto verknüpfen" -planEditor = "Editor-Plan" -planEnterprise = "Enterprise-Plan" -planProcessor = "Processor-Plan" primaryNav = "Primäre Navigation" switchApp = "App wechseln" +[portal.shell.topbar] +closeNav = "Navigation schließen" +openNav = "Navigation öffnen" +search = "Suchen" + [portal.sources] subtitle = "Wiederverwendbare Eingabeverbindungen, die Dokumente in Stirling einspeisen. Konfigurieren Sie eine Verbindung einmal und referenzieren Sie sie dann in beliebig vielen Richtlinien. Klicken Sie auf eine Zeile, um ihre Konfiguration und die Richtlinien anzuzeigen, die sie verwenden." title = "Quellen" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Quelle verbinden" +[portal.sources.builder] +back = "Zurück zu Quellen" +backToTypes = "Alle Quellentypen" +cancel = "Abbrechen" +chooseHint = "Wählen Sie aus, woher Dokumente kommen. Ausgegraute Connectors sind in Vorbereitung." +comingSoon = "Demnächst" +comingSoonHeading = "Demnächst" +create = "Quelle erstellen" +createTitle = "Quelle verbinden" +delete = "Löschen" +editTitle = "Quelle bearbeiten" +enabled = "Aktiviert" +save = "Änderungen speichern" + +[portal.sources.builder.folderAccess] +description = "Ordnerautomatisierungen können nur Ordner verwenden, die ein Administrator zugelassen hat. Fügen Sie ihn in den Einstellungen für Ordnerzugriff hinzu und versuchen Sie es dann erneut." +openSettings = "Einstellungen für Ordnerzugriff" +title = "Dieser Ordner ist nicht erlaubt" + [portal.sources.delete] body = "„{{name}}“ löschen? Dies kann nicht rückgängig gemacht werden. Richtlinien, die sie referenzieren, müssten aktualisiert werden." cancel = "Abbrechen" confirm = "Löschen" title = "Quelle löschen?" -[portal.sources.detail] -closeAriaLabel = "Details schließen" -delete = "Quelle löschen" -docs24h = "Letzte 24 Std." -docs30d = "Letzte 30 Tage" -docsTotal = "Insgesamt gesehen" -docsTrend = "Dokumente in den letzten 30 Tagen" -documents = "Dokumente" -edit = "Bearbeiten" -notReferenced = "Von keiner Richtlinie referenziert, daher kann sie sicher gelöscht werden." -pause = "Pausieren" -resume = "Fortsetzen" -subtitle = "{{type}} · {{status}}" -usedBy = "Verwendet von" - -[portal.sources.empty] -description = "Verbinden Sie einen Ordner (und bald Cloud-Speicher), damit Ihre Richtlinien einen Ort haben, von dem sie Dokumente abrufen können." -title = "Noch keine Quellen verbunden" - [portal.sources.kpi] inUse = "In Verwendung" total = "Verbindungen" unused = "Unbenutzt" +[portal.sources.networkFields.connection] +helperText = "Die gespeicherte Verbindung mit Serveradresse und Anmeldedaten. Wird von jeder Quelle wiederverwendet, die sie referenziert." +label = "Verbindung" + +[portal.sources.networkFields.directory] +helperText = "Ordner auf dem Server, der abgefragt werden soll, relativ zum Login-Home oder zum Stamm der Freigabe. Für den Stamm leer lassen." +label = "Ordner" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Verbrauchen entfernt jede Datei vom Server, sobald jede Richtlinie sie verarbeitet hat." +label = "Lesemodus" + +[portal.sources.networkFields.mode.options] +consume = "Verbrauchen: Jede Datei einmal verarbeiten" +snapshot = "Snapshot: Ordner bei jedem Lauf erneut lesen" + +[portal.sources.networkFields.recursive] +label = "Ordnertiefe" + +[portal.sources.networkFields.recursive.options] +all = "Unterordner einschließen" +top = "Nur oberste Ebene" + [portal.sources.status] active = "Aktiv" disabled = "Deaktiviert" unused = "Unbenutzt" [portal.sources.table] +documents = "Dokumente" source = "Quelle" status = "Status" +type = "Type" usedBy = "Richtlinien" +[portal.sources.types.box] +description = "Dokumente aus Box-Ordnern abrufen, die Ihr Team bereits teilt." +label = "Box" + +[portal.sources.types.dropbox] +description = "Dokumente aus einem freigegebenen Dropbox-Ordner abrufen." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumente, die Ihr Team im Editor verarbeitet hat, über Richtlinien- und AI-Läufe hinweg." label = "Editor" +noPolicies = "Noch keine Richtlinien aus dem Editor ausgeführt." + +[portal.sources.types.email] +description = "Anhänge, die an ein dediziertes Postfach gesendet werden, in Dokumente umwandeln." +label = "E-Mail-Posteingang" [portal.sources.types.folder] description = "Überwacht ein Verzeichnis auf dem Server auf neue Dokumente." @@ -7975,25 +8829,80 @@ label = "Ordnertiefe" all = "Unterordner einschließen" top = "Nur oberste Ebene" +[portal.sources.types.ftp] +description = "Einen FTP- oder FTPS-Serverordner auf neue Dokumente abfragen." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Dokumente aus freigegebenen Google Drive-Ordnern abrufen." +label = "Google Drive" + +[portal.sources.types.network] +description = "Eine SMB-Netzwerkfreigabe auf neue Dokumente überwachen." +label = "Netzlaufwerk" + +[portal.sources.types.onedrive] +description = "OneDrive-Ordner auf neue Dokumente überwachen." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Dokumente aus einem Amazon S3- oder S3-kompatiblen Bucket abrufen." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Die gespeicherte Verbindung mit Bucket und Anmeldedaten. Wird von jeder Quelle und Pipeline-Ausgabe wiederverwendet, die sie referenziert." +label = "Verbindung" + +[portal.sources.types.s3.fields.mode] +helperText = "Verbrauchen entfernt jedes Objekt aus dem Bucket, sobald jede Richtlinie es verarbeitet hat." +label = "Lesemodus" + +[portal.sources.types.s3.fields.mode.options] +consume = "Verbrauchen: Jedes Objekt einmal verarbeiten" +snapshot = "Snapshot: Bucket bei jedem Lauf erneut lesen" + +[portal.sources.types.s3.fields.prefix] +helperText = "Nur Objekte, deren Schlüssel mit diesem Präfix beginnen, werden verarbeitet." +label = "Schlüsselpräfix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Einen SFTP-Drop-Ordner auf neue Dokumente abfragen." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Dokumente aus SharePoint-Dokumentbibliotheken abrufen." +label = "SharePoint" + [portal.sources.types.unknown] label = "Quelle" +[portal.sources.types.webhook] +createNote = "Eine Zustell-URL und ein Signaturgeheimnis werden generiert, wenn Sie diesen Webhook erstellen. Absender senden Dokumente per POST an diese URL, und die referenzierenden Richtlinien werden bei Ankunft ausgeführt." +description = "Dokumente per signiertem HTTP POST von einem beliebigen externen System empfangen." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Zustell-URL" +secretNote = "Das Signaturgeheimnis wird einmal angezeigt, wenn der Webhook erstellt wird. Erstellen Sie die Quelle neu, um es zu rotieren." + +[portal.sources.types.webhook.reveal] +copy = "Kopieren" +done = "Fertig" +secret = "Signaturgeheimnis" +secretHelp = "Signieren Sie den Roh-Body jeder Zustellung mit diesem Schlüssel (HMAC-SHA256) und senden Sie ihn als X-Stirling-Signature-Header." +secretWarning = "Kopieren Sie das Signaturgeheimnis jetzt. Zu Ihrer Sicherheit wird es nur einmal angezeigt und kann später nicht abgerufen werden." +title = "Webhook erstellt" +url = "Zustell-URL" +usage = "Senden Sie jedes Dokument per POST als rohen Request-Body an die Zustell-URL mit einem binären Content-Type (application/pdf oder application/octet-stream). Referenzierende Richtlinien werden bei Ankunft automatisch ausgeführt." + [portal.sources.wizard] -back = "Zurück" -cancel = "Abbrechen" -continue = "Weiter" -editTitle = "Quelle bearbeiten" name = "Name" namePlaceholder = "z. B. Schadenaufnahme" -save = "Änderungen speichern" -subtitle = "Schritt {{current}} von {{total}} · {{label}}" -title = "Quelle verbinden" type = "Typ" -[portal.sources.wizard.steps] -chooseType = "Typ auswählen" -configure = "Konfigurieren" -review = "Prüfen & verbinden" +[portal.stepModal] +close = "Schließen" [portal.usage] managePayment = "Zahlung verwalten" @@ -8053,16 +8962,9 @@ summary = "Besitzt ein Team — verwaltet die Ressourcen seiner Mitglieder und f 2 = "Portalzugriff über die Standardrichtlinie" 3 = "Alles, was Member tun kann" -[portal.welcome] -ariaLabel = "Willkommen bei Stirling PDF" -badge = "Open-source" -installEditor = "Editor installieren" -inviteTeammates = "Teammitglieder einladen" -openInBrowser = "Im Browser öffnen" -perks = "Für immer kostenlos · Selbst hostbar" -subtitle = "Der weltweit sicherste PDF Editor ist kostenlos für Teams jeder Größe. Enthält 60+ PDF-Operationen und SSO." -title = "Willkommen bei" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Unbegrenzt" [printFile] title = "Datei drucken" @@ -8645,6 +9547,14 @@ text = "Drehen Sie Ihre PDF-Seiten im oder gegen den Uhrzeigersinn in 90-Grad-Sc [rotate.tooltip.header] title = "Übersicht der Rotationseinstellungen" +[ruler] +clearAll = "Alle löschen" +hideAllLabels = "Alle Beschriftungen ausblenden" +hideSmallLabels = "Kleine Beschriftungen ausblenden" +physicalValues = "Physisch" +scaled = "Skaliert" +showAllLabels = "Alle Beschriftungen anzeigen" + [sanitize] desc = "Potentiell schädliche Elemente aus PDF-Dateien entfernen." sanitizationResults = "Bereinigungsergebnisse" @@ -8686,31 +9596,45 @@ label = "XMP-Metadaten entfernen" [sanitize.steps] settings = "Einstellungen" -[sanitizePdf] -tags = "sauber,sicher,sicher,bedrohungen entfernen" - [sanitizePDF] title = "PDF Bereinigen" +[sanitizePdf] +tags = "sauber,sicher,sicher,bedrohungen entfernen" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Seitengröße anpassen" +[scaleSettings] +activeScale = "Aktiver Maßstab" +apply = "Maßstab anwenden" +applyCalibration = "Kalibrierung anwenden" +calculatedScale = "Berechneter Maßstab" +calibrate = "Kalibrieren" +calibrating = "Kalibrieren" +calibrationDistanceRequired = "Geben Sie eine reale Entfernung größer als null ein" +calibrationInvalid = "Maßstab kann aus dieser Messung nicht berechnet werden" +calibrationPaperDistance = "Gemessene Seitenentfernung: {{distance}}" +calibrationTitle = "Maßstab kalibrieren" +calibrationTooltip = "Messen Sie eine bekannte Entfernung, um den Maßstab automatisch zu berechnen" +customScale = "Benutzerdefinierter Maßstab" +noneSet = "Kein benutzerdefinierter Maßstab festgelegt" +presets = "Voreingestellte Maßstäbe" +ratio = "Maßstabsverhältnis" +ratioHelp = "Verhältnis: 1 Seiteneinheit = X reale Einheiten" +ratioPlaceholder = "z. B. 100" +ratioPositive = "Verhältnis muss größer als null sein" +ratioRequired = "Verhältnis ist erforderlich" +realDistance = "Reale Entfernung" +realDistancePlaceholder = "z. B. 5" +reset = "Auf Standardwerte zurücksetzen" +unit = "Einheit" +unitInvalid = "Ungültige Einheit: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Winkelschwelle:" -10 = "Legt die Größe des hinzugefügten und entfernten Randes fest, um weiße Ränder in der Ausgabe zu verhindern (Standard: 1)." -2 = "Legt den minimalen absoluten Winkel fest, der erforderlich ist, damit das Bild gedreht werden kann (Standard: 10)." -3 = "Toleranz:" -4 = "Bestimmt den Bereich der Farbvariation um die geschätzte Hintergrundfarbe herum (Standard: 30)." -5 = "Mindestbereich:" -6 = "Legt den minimalen Bereichsschwellenwert für ein Foto fest (Standard: 10000)." -7 = "Minimaler Konturbereich:" -8 = "Legt den minimalen Konturbereichsschwellenwert für ein Foto fest" -9 = "Randgröße:" - [scannerImageSplit] submit = "Bild-Scans extrahieren" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8742,6 +9666,18 @@ whatThisDoes = "Was dies tut" whatThisDoesDesc = "Findet und extrahiert automatisch jedes Foto von einer gescannten Seite oder einem zusammengesetzten Bild - kein manuelles Zuschneiden erforderlich." whenToUse = "Wann zu verwenden" +[ScannerImageSplit.selectText] +1 = "Winkelschwelle:" +10 = "Legt die Größe des hinzugefügten und entfernten Randes fest, um weiße Ränder in der Ausgabe zu verhindern (Standard: 1)." +2 = "Legt den minimalen absoluten Winkel fest, der erforderlich ist, damit das Bild gedreht werden kann (Standard: 10)." +3 = "Toleranz:" +4 = "Bestimmt den Bereich der Farbvariation um die geschätzte Hintergrundfarbe herum (Standard: 30)." +5 = "Mindestbereich:" +6 = "Legt den minimalen Bereichsschwellenwert für ein Foto fest (Standard: 10000)." +7 = "Minimaler Konturbereich:" +8 = "Legt den minimalen Konturbereichsschwellenwert für ein Foto fest" +9 = "Randgröße:" + [search] noResults = "Keine Ergebnisse gefunden" placeholder = "Suchbegriff eingeben..." @@ -8759,46 +9695,22 @@ toolNotAvailableLocally = "Ihr Stirling-PDF-Server ist offline und „{{endpoint [session] expired = "Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut." -[sessionManagement.tooltip] -header = "Signiersitzungen verwalten" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Neue Teilnehmer werden am Ende der Signierreihenfolge hinzugefügt" -bullet2 = "Teilnehmer können nach der Finalisierung nicht mehr hinzugefügt werden" -bullet3 = "Jeder Teilnehmer erhält eine Benachrichtigung, wenn er an der Reihe ist" -description = "Sie können einer aktiven Sitzung jederzeit vor der Finalisierung weitere Teilnehmer hinzufügen." -title = "Teilnehmer hinzufügen" - -[sessionManagement.tooltip.finalization] -bullet1 = "Vollständige Finalisierung: Alle Teilnehmer haben signiert" -bullet2 = "Teilweise Finalisierung: Einige Teilnehmer haben noch nicht signiert" -bullet3 = "Nicht signierte Teilnehmer werden aus dem endgültigen Dokument ausgeschlossen" -bullet4 = "Nach der Finalisierung können Sie das signierte PDF in aktive Dateien laden" -description = "Die Finalisierung kombiniert alle Signaturen zu einem einzigen signierten PDF. Diese Aktion kann nicht rückgängig gemacht werden." -title = "Sitzungsfinalisierung" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Teilnehmer, die bereits signiert haben, können nicht entfernt werden" -bullet2 = "Entfernte Teilnehmer erhalten keine Benachrichtigungen mehr" -bullet3 = "Die Signierreihenfolge passt sich automatisch an" -description = "Teilnehmer können aus Sitzungen entfernt werden, bevor sie signieren." -title = "Teilnehmer entfernen" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Jede Signatur wird nacheinander auf das PDF angewendet" -bullet2 = "Spätere Unterzeichner können frühere Signaturen sehen" -bullet3 = "Wichtig für Genehmigungs-Workflows und rechtliche Nachweisketten" -description = "Die von Ihnen bei der Erstellung der Sitzung festgelegte Reihenfolge bestimmt, wer zuerst signiert." -title = "Signaturreihenfolge" - [settings] close = "Schließen" +[settings.ai] +documents = "Dokumente & RAG" +general = "Allgemein" +limits = "Limits & Performance" +models = "Modelle & Anbieter" +title = "AI" + [settings.configuration] advanced = "Erweitert" database = "Datenbank" endpoints = "Endpunkte" features = "Funktionen" +folderAccess = "Ordnerzugriff" mcp = "MCP Server" storageSharing = "Dateispeicher & Freigabe" systemSettings = "Systemeinstellungen" @@ -8832,6 +9744,8 @@ defaultPdfEditor = "Standard-PDF-Editor" defaultPdfEditorActive = "Stirling PDF ist Ihr Standard-PDF-Editor" defaultPdfEditorChecking = "Prüfen..." defaultPdfEditorInactive = "Eine andere Anwendung ist als Standard festgelegt" +defaultPdfEditorRemind = "Mich daran erinnern, als Standard festzulegen" +defaultPdfEditorRemindDescription = "Banner anzeigen, wenn Stirling PDF nicht Ihre Standard-PDF-Anwendung ist." defaultPdfEditorSet = "Bereits Standard" defaultStartupView = "Standardansicht beim Start" defaultStartupViewDescription = "Wählen Sie, welche Registerkarte in der linken Spalte aktiv ist, wenn die App startet" @@ -8849,9 +9763,18 @@ languageDescription = "Anzeigesprache auswählen" logout = "Abmelden" setAsDefault = "Als Standard festlegen" theme = "Design" +themeAccent = "Akzentfarbe" +themeAccentCustom = "Benutzerdefinierte Farbe" +themeAccentCustomHint = "Benutzerdefiniert (Standardwerte empfohlen)" +themeAccentDark = "Akzentfarbe für den Dunkelmodus" +themeAccentDefault = "Standard" +themeAccentDefaultHint = "Standard-Theme (empfohlen)" +themeAccentDescription = "Schaltflächen, Links und Hervorhebungen folgen ihr, und sie färbt die App dezent ein. Hell und Dunkel haben jeweils ihre eigene. System verwendet die jeweils aktive." +themeAccentLight = "Akzentfarbe für den Hellmodus" themeDark = "Dunkel" themeDescription = "Zwischen hellem und dunklem Modus wechseln" themeLight = "Hell" +themeReset = "Theme auf Standard zurücksetzen" themeSystem = "System" title = "Allgemein" user = "Benutzer" @@ -8865,6 +9788,12 @@ intro = "Aktivieren Sie Benutzer-Authentifizierung, Teamverwaltung und Arbeitsbe learnMore = "Mehr in der Dokumentation erfahren" title = "Für Systemadministratoren" +[settings.general.loginLanding] +description = "Wählen Sie, welche App geöffnet wird, wenn Sie sich bei Stirling Cloud anmelden." +editor = "Editor" +processor = "Processor" +title = "Nach der Anmeldung" + [settings.general.mode] fullscreen = "Vollbild" sidebar = "Seitenleiste" @@ -8925,7 +9854,7 @@ capturing = "Tasten drücken… (Esc zum Abbrechen)" change = "Kürzel ändern" customBadge = "Benutzerdefiniert" defaultLabel = "Standard: {{shortcut}}" -description = "Tastenkürzel für schnellen Tool-Zugriff anpassen. Klicken Sie auf \"Shortcut ändern\" und drücken Sie eine neue Tastenkombination. Drücken Sie Esc, um abzubrechen." +description = "Tastenkürzel für schnellen Tool-Zugriff anpassen. Klicken Sie auf \"Kürzel ändern\" und drücken Sie eine neue Tastenkombination. Drücken Sie Esc, um abzubrechen." errorConflict = "Kürzel wird bereits von {{tool}} verwendet." none = "Nicht zugewiesen" noShortcut = "Kein Kürzel festgelegt" @@ -9045,9 +9974,6 @@ title = "Richtlinien & Datenschutz" [settings.preferences] title = "Einstellungen" -[settings.search] -placeholder = "Einstellungen durchsuchen..." - [settings.security] description = "Aktualisieren Sie Ihr Passwort, um Ihr Konto zu schützen." title = "Sicherheit" @@ -9193,6 +10119,7 @@ last = "Letzte Seite" maintainRatio = "Seitenverhältnis beibehalten ein-/ausschalten" next = "Nächste Seite" noSavedSigs = "Es wurden keine gespeicherten Signaturen gefunden" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Persönliche Signaturen" previous = "Vorherige Seite" redo = "Wiederherstellen" @@ -9231,6 +10158,7 @@ backgroundRemovalFailedTitle = "Hintergrund konnte nicht entfernt werden" hint = "Laden Sie ein PNG- oder JPG-Bild Ihrer Unterschrift hoch" label = "Unterschriftsbild hochladen" placeholder = "Bilddatei auswählen" +previewAlt = "Current image signature" processing = "Bild wird verarbeitet..." removeBackground = "Weißen Hintergrund entfernen (transparent machen)" @@ -9244,6 +10172,17 @@ saved = "Wählen Sie oben eine gespeicherte Unterschrift und klicken Sie dann in text = "Nachdem Sie oben Ihren Namen eingegeben haben, klicken Sie irgendwo auf das PDF, um Ihre Signatur zu platzieren." title = "So fügen Sie eine Unterschrift hinzu" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Signatur bewegen" pause = "Platzieren pausieren" @@ -9690,6 +10629,23 @@ title = "Auf Server hochladen" updateButton = "Auf Server aktualisieren" uploadButton = "Auf Server hochladen" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" @@ -9804,12 +10760,39 @@ selectFilesHint = "Wählen Sie Dateien in Aktive Dateien aus, um dieses Tool aus singleFileScope = "Wird nur angewendet auf: {{fileName}}" viewerMode = "Zum Datei-Editor wechseln, um mehrere Dateien auszuwählen." +[toolFormat] +ANY = "beliebige Datei" +CBR = "ein Comic-Archiv (CBR)" +CBZ = "ein Comic-Archiv (CBZ)" +CSV = "eine CSV-Datei" +EBOOK = "ein E-Book" +EMAIL = "eine E-Mail" +EXCEL = "eine Tabellenkalkulation" +HTML = "eine Webseite" +IMAGE = "Bilder" +JAVASCRIPT = "eine JavaScript-Datei" +JSON = "eine JSON-Datei" +MARKDOWN = "eine Markdown-Datei" +NONE = "keine Datei" +PCL = "eine PCL-Druckdatei" +PDF = "eine PDF" +PDF_ENCRYPTED = "eine passwortgeschützte PDF" +POSTSCRIPT = "eine PostScript-Datei" +PPT = "eine Präsentation" +TEXT = "eine Textdatei" +VIDEO = "ein Video" +WORD = "ein Word-Dokument" +XML = "eine XML-Datei" +XPS = "ein XPS-Dokument" +ZIP = "ein ZIP-Archiv" + [toolPanel] alpha = "Alpha" backToAllTools = "Zurück zu allen Tools" collapse = "Bereich einklappen" expand = "Bereich ausklappen" goBack = "Zurück" +pdfTools = "PDF-Tools" placeholder = "Wählen Sie ein Tool, um zu starten" premiumFeature = "Premium-Funktion:" toolsHeader = "Tools" @@ -9846,11 +10829,6 @@ noToolsFound = "Keine Werkzeuge gefunden" quickAccess = "SCHNELLZUGRIFF" searchPlaceholder = "Werkzeuge suchen..." -[toolPicker.categories] -advancedTools = "Erweiterte Werkzeuge" -recommendedTools = "Empfohlene Werkzeuge" -standardTools = "Standard-Werkzeuge" - [toolPicker.subcategories] advancedFormatting = "Erweiterte Formatierung" automation = "Automatisierung" @@ -9864,6 +10842,9 @@ removal = "Entfernung" signing = "Signierung" verification = "Verifizierung" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Keine Tools gefunden" noTools = "Keine Tools verfügbar" @@ -9990,28 +10971,53 @@ title = "Benutzer" you = "(Sie)" [users.action] +cancelInvite = "Einladung zurückziehen" deleteTeam = "Team löschen" disableMfa = "MFA zurücksetzen" move = "In Team verschieben" reinstate = "Wiederherstellen" remove = "Aus Org entfernen" +removeTeam = "Aus dem Team entfernen" rename = "Team umbenennen" resetPw = "Passwort zurücksetzen" suspend = "Suspendieren" unlock = "Konto entsperren" +[users.activity] +daysAgo = "vor {{count}} T." +hoursAgo = "vor {{count}} Std." +justNow = "Gerade eben" +minutesAgo = "vor {{count}} Min." +monthsAgo_one = "vor {{count}} Monat" +monthsAgo_other = "vor {{count}} Monaten" +never = "Nie" +weeksAgo_one = "vor {{count}} Woche" +weeksAgo_other = "vor {{count}} Wochen" +yearsAgo_one = "vor {{count}} Jahr" +yearsAgo_other = "vor {{count}} Jahren" + [users.cap] addProcessor = "+ Processor" approver = "Genehmigt Richtlinie" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Einladung an {{email}} zurückziehen? Diese Person kann dem Team mit dem aktuellen Link nicht mehr beitreten." +cancelInviteTitle = "Einladung zurückziehen" deleteTeamBody = "Das Team {{name}} löschen? Das Team muss zuerst leer sein - verschieben Sie seine Mitglieder in ein anderes Team, und es darf keine Integrationskonfigurationen mehr besitzen." deleteTeamTitle = "Team löschen" disableMfaBody = "MFA-Registrierung von {{name}} entfernen? Sie richten sie beim nächsten Login erneut ein, falls erforderlich." disableMfaTitle = "MFA zurücksetzen" removeBody = "{{name}} dauerhaft aus der Organisation entfernen? Dies kann nicht rückgängig gemacht werden." +removeTeamBody = "{{name}} aus dem Team entfernen? Das Konto bleibt erhalten, aber der Zugriff auf die Ressourcen dieses Teams geht verloren." removeTitle = "Mitglied entfernen" [users.empty] @@ -10022,10 +11028,8 @@ title = "Noch keine Mitglieder" addToTeam = "Zum Team hinzufügen" guestCount = "{{count}} Gast" guests = "Gäste" -guestsDesc = "Externe Mitwirkende, beschränkt auf das, was Sie geteilt haben. Nur Editor." ledBy = "geleitet von {{owner}}" org = "Organisation" -orgDesc = "Inhaber mit organisationsweiter Befugnis und Richtliniengenehmigung" owners = "{{count}} Inhaber" team = "Team {{name}}" teamMeta = "{{count}} Personen" @@ -10062,6 +11066,18 @@ username = "Benutzername" usernameError = "Der Benutzername muss mindestens 3 Zeichen lang sein" usernamePlaceholder = "jsmith" +[users.invites] +by = "Eingeladen von {{who}}" +cancel = "Zurückziehen" +expiresInDays_one = "Läuft in {{count}} Tag ab" +expiresInDays_other = "Läuft in {{count}} Tagen ab" +expiresToday = "Läuft heute ab" +title = "Ausstehende Einladungen" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Beim Zugriff auf das Backend ist etwas schiefgelaufen, oder Sie haben keinen Zugriff. Versuchen Sie es erneut." title = "Mitglieder konnten nicht geladen werden" @@ -10251,12 +11267,15 @@ searchPlaceholder = "Search attachments" title = "Attachments" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Lesezeichentitel" bookmarkTitleRequired = "Ein Lesezeichentitel ist erforderlich" closeSidebar = "Seitenleiste für Lesezeichen schließen" collapseAll = "Alle Lesezeichen einklappen" +empty = "No bookmarks in this document" expandAll = "Alle Lesezeichen ausklappen" searchPlaceholder = "Lesezeichen durchsuchen" +title = "Bookmarks" [viewer.comments] addComment = "Kommentar hinzufügen" @@ -10278,10 +11297,12 @@ locateAnnotation = "Im Dokument anzeigen" moreActions = "Weitere Aktionen" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Seite {{page}}" placingHint = "Zum Platzieren auf eine Seite klicken… (Abbrechen)" removeCommentOnly = "Nur Kommentar entfernen" saveReply = "Antwort speichern" +searchPlaceholder = "Search comments" title = "Kommentare" typeComment = "Kommentar" typeInsertText = "Text einfügen" @@ -10292,18 +11313,19 @@ viewComment = "Kommentar anzeigen" noFileProvided = "Fehler: Dem Viewer wurde keine Datei übergeben" [viewer.formBar] -apply = "Apply Changes" -dismiss = "Dismiss" -download = "Download PDF" -hasFieldsDesc = "This PDF contains fillable fields" -title = "Form Fields" -unsavedBadge = "Unsaved" -unsavedDesc = "You have unsaved changes" +apply = "Änderungen anwenden" +dismiss = "Verwerfen" +download = "PDF herunterladen" +hasFieldsDesc = "Dieses PDF enthält ausfüllbare Felder" +title = "Formularfelder" +unsavedBadge = "Ungespeichert" +unsavedDesc = "Sie haben ungespeicherte Änderungen" [viewer.layers] closeSidebar = "Seitenleiste für Ebenen schließen" hideAll = "Alle Ebenen ausblenden" showAll = "Alle Ebenen anzeigen" +title = "Layers" [viewer.link] delete = "Link löschen" @@ -10647,30 +11669,6 @@ title = "Wasserzeichen-Text" image = "Bild" text = "Text" -[wetSignature.tooltip] -header = "Methoden zur Signaturerstellung" - -[wetSignature.tooltip.draw] -bullet1 = "Stiftfarbe und -stärke anpassen" -bullet2 = "Löschen und neu zeichnen, bis Sie zufrieden sind" -bullet3 = "Funktioniert auf Touch-Geräten (Tablets, Smartphones)" -description = "Erstellen Sie eine handschriftliche Signatur mit Ihrer Maus oder dem Touchscreen. Am besten für persönliche, authentische Signaturen." -title = "Signatur zeichnen" - -[wetSignature.tooltip.type] -bullet1 = "Aus mehreren Schriftarten wählen" -bullet2 = "Textgröße und -farbe anpassen" -bullet3 = "Ideal für standardisierte Signaturen" -description = "Erzeugen Sie eine Signatur aus eingegebenem Text. Schnell und einheitlich, geeignet für Geschäftsdokumente." -title = "Signatur eingeben" - -[wetSignature.tooltip.upload] -bullet1 = "Unterstützt PNG, JPG und andere Bildformate" -bullet2 = "Transparente Hintergründe für beste Ergebnisse empfohlen" -bullet3 = "Das Bild wird an den Signaturbereich angepasst" -description = "Laden Sie ein bereits erstelltes Signaturbild hoch. Ideal, wenn Sie eine eingescannte Unterschrift oder ein Firmenlogo haben." -title = "Signaturbild hochladen" - [workbenchBar] activeFiles = "Aktive Dateien" annotations = "Anmerkungen" @@ -10684,6 +11682,7 @@ exitRedaction = "Schwärzungsmodus beenden" exportAll = "PDF exportieren" exportSelected = "Ausgewählte Seiten exportieren" formFill = "Formular ausfüllen" +hideToolbar = "Hide toolbar" multiTool = "Multitool" panMode = "Verschiebemodus" print = "PDF drucken" @@ -10694,6 +11693,7 @@ redact = "Schwärzen" rotateLeft = "Nach links drehen" rotateRight = "Nach rechts drehen" ruler = "Lineal / Messen" +rulerSettings = "Skaleneinstellungen" save = "Speichern" saveAll = "Alle speichern" saveAs = "Speichern unter" @@ -10703,6 +11703,7 @@ selectAll = "Alle auswählen" selectByNumber = "Nach Seitenzahlen auswählen" selectLanguage = "Sprache auswählen" share = "Teilen" +showToolbar = "Show toolbar" toggleAnnotations = "Anmerkungen ein-/ausblenden" toggleAttachments = "Anhänge ein-/ausblenden" toggleBookmarks = "Lesezeichen ein-/ausblenden" diff --git a/frontend/editor/public/locales/el-GR/translation.toml b/frontend/editor/public/locales/el-GR/translation.toml index 06c35bc059..4c380b9447 100644 --- a/frontend/editor/public/locales/el-GR/translation.toml +++ b/frontend/editor/public/locales/el-GR/translation.toml @@ -1,7 +1,6 @@ alphabet = "Αλφάβητο" apply = "Εφαρμογή" applyAndContinue = "Αποθήκευση & Έξοδος" -areYouSure = "Είστε βέβαιοι ότι θέλετε να αποχωρήσετε;" back = "Πίσω" black = "Μαύρο" blue = "Μπλε" @@ -46,10 +45,8 @@ filesSelected = "αρχεία επιλεγμένα" font = "Γραμματοσειρά" fontSizeTooltip = "Μέγεθος του κειμένου του αριθμού σελίδας σε σημεία. Μεγαλύτεροι αριθμοί δημιουργούν μεγαλύτερο κείμενο." fontTypeTooltip = "Οικογένεια γραμματοσειράς για τους αριθμούς σελίδων. Επιλέξτε σύμφωνα με το στυλ του εγγράφου σας." -genericSubmit = "Υποβολή" green = "Πράσινο" help = "Βοήθεια" -imgPrompt = "Επιλέξτε εικόνα(ες)" incorrectPasswordMessage = "Ο τρέχων κωδικός είναι λανθασμένος." info = "Πληροφορίες" invalidUndoData = "Αδύνατη η αναίρεση: μη έγκυρα δεδομένα ενέργειας" @@ -62,7 +59,6 @@ loginAgreementTitle = "Συμφωνία σύνδεσης" logOut = "Αποσύνδεση" marginTooltip = "Απόσταση μεταξύ του αριθμού σελίδας και του άκρου της σελίδας." moreOptions = "Περισσότερες επιλογές" -multiPdfPrompt = "Επιλέξτε PDFs (2+)" never = "Ποτέ" no = "Όχι" noFileSelected = "Δεν επιλέχθηκε αρχείο. Παρακαλώ ανεβάστε ένα." @@ -80,7 +76,6 @@ pageOrderPrompt = "Προσαρμοσμένη σειρά σελίδων (Εισ pages = "Σελίδες" pageSelectionPrompt = "Προσαρμοσμένη επιλογή σελίδων (Εισάγετε μια λίστα αριθμών σελίδων χωρισμένη με κόμματα 1,5,6 ή συναρτήσεις όπως 2n+1):" password = "Κωδικός" -pdfPrompt = "Επιλέξτε PDF(s)" pin = "Καρφίτσωμα αρχείου (παραμένει ενεργό μετά την εκτέλεση εργαλείου)" pro = "Pro" processingComplete = "Το αρχείο σας είναι έτοιμο." @@ -110,6 +105,7 @@ undoSuccess = "Η ενέργεια αναιρέθηκε με επιτυχία" unknown = "Άγνωστο" unpin = "Ξεκαρφίτσωμα αρχείου (αντικατάσταση μετά την εκτέλεση εργαλείου)" unsavedChanges = "Έχετε μη αποθηκευμένες αλλαγές στο PDF σας." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Μη αποθηκευμένες αλλαγές" unsupported = "Μη υποστηριζόμενο" username = "Όνομα χρήστη" @@ -356,6 +352,7 @@ text = "Αυτά τα δικαιώματα ελέγχουν τι μπορούν title = "Αλλαγή δικαιωμάτων" [addStamp] +preview = "Προεπισκόπηση σφραγίδας" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Χειροκίνητες λήψεις: κάντε κλικ στο noLanguages = "Δεν βρέθηκαν γλώσσες tessdata στον ρυθμισμένο κατάλογο." permissionNotice = "Η διαδρομή tessdata δεν είναι εγγράψιμη. Οι λήψεις θα ανοίγουν στο πρόγραμμα περιήγησης. Αποθηκεύστε χειροκίνητα τα αρχεία .traineddata στον φάκελο tessdata." +[admin.settings.ai.documents] +description = "Ρυθμίστε το μοντέλο embedding και τις ρυθμίσεις ανάκτησης που χρησιμοποιούνται για απαντήσεις σε ερωτήσεις πάνω σε έγγραφα. Εφαρμόζεται στη μηχανή AI όταν αποθηκευτεί." +title = "Έγγραφα & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Αφήστε το κενό για να χρησιμοποιηθεί το διαπιστευτήριο περιβάλλοντος της ίδιας της μηχανής. Ισχύει για αυτοφιλοξενούμενες αναπτύξεις μίας μηχανής." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Αποθηκεύτηκε - αφήστε κενό για να διατηρήσετε το τρέχον key" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Βασικό URL του endpoint embeddings συμβατού με OpenAI / Ollama, π.χ. http://ollama:11434/v1. Πρέπει να δείχνει σε αξιόπιστο εσωτερικό endpoint (ευαίσθητο σε SSRF)." +label = "Βασικό URL embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Όνομα μοντέλου embedding. Ελεύθερο κείμενο· οι προτάσεις είναι μόνο υποδείξεις." +label = "Μοντέλο embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Πάροχος που χρησιμοποιείται για τη μετατροπή κειμένου εγγράφων σε vector embeddings." +label = "Πάροχος embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Μέγιστος αριθμός αναζητήσεων ανάκτησης που μπορεί να εκτελέσει ο agent ανά αίτημα." +label = "Μέγιστες αναζητήσεις" + +[admin.settings.ai.documents.reindexNote] +body = "Η αλλαγή του μοντέλου embedding τίθεται σε ισχύ αμέσως, αλλά τα έγγραφα που έχουν ευρετηριαστεί με το προηγούμενο μοντέλο πρέπει να ευρετηριαστούν ξανά ώστε η αναζήτηση να επιστρέφει σωστά αποτελέσματα." +title = "Απαιτείται επανευρετηρίαση" + +[admin.settings.ai.documents.saved] +reindexNote = "Αν αλλάξατε το μοντέλο embedding, επανευρετηριάστε τα υπάρχοντα έγγραφα ώστε η αναζήτηση να χρησιμοποιεί το νέο μοντέλο." + +[admin.settings.ai.documents.topK] +description = "Αριθμός των πιο σχετικών τμημάτων που ανακτώνται ανά αναζήτηση." +label = "Top K" + +[admin.settings.ai.general] +description = "Συνδέστε το Stirling με τη μηχανή Python AI και επιλέξτε ποιες δυνατότητες AI είναι διαθέσιμες. Οι αλλαγές εφαρμόζονται με επανεκκίνηση." +title = "Μηχανή AI" + +[admin.settings.ai.general.capabilities] +description = "Ενεργοποιήστε ή απενεργοποιήστε μεμονωμένες λειτουργίες AI. Οι απενεργοποιημένες λειτουργίες αποκρύπτονται στην εφαρμογή." +title = "Δυνατότητες" + +[admin.settings.ai.general.enabled] +description = "Κεντρικός διακόπτης. Όταν είναι απενεργοποιημένος, δεν είναι διαθέσιμα εργαλεία AI, agents ή κλήσεις μηχανής." +label = "Ενεργοποίηση AI" + +[admin.settings.ai.general.features.chat] +description = "Βοηθός συνομιλίας για εργασία με PDFs." +label = "Βοηθός συνομιλίας" + +[admin.settings.ai.general.features.classify] +description = "Αυτόματη κατηγοριοποίηση εγγράφων κατά τύπο ή περιεχόμενο." +label = "Ταξινόμηση εγγράφων" + +[admin.settings.ai.general.features.createPdf] +description = "Δημιουργία νέου PDF (π.χ. από HTML) μέσω AI agent." +label = "Δημιουργία PDF από prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Κάντε ερωτήσεις και λάβετε απαντήσεις βασισμένες σε ένα μεταφορτωμένο έγγραφο." +label = "Ερωτήσεις εγγράφου" + +[admin.settings.ai.general.features.mathAuditor] +description = "Έλεγχος εγγράφων για μαθηματικά και αριθμητικά σφάλματα." +label = "Ελεγκτής μαθηματικών" + +[admin.settings.ai.general.features.pdfComment] +description = "Προσθήκη σχολίων αξιολόγησης και επισημάνσεων γραμμένων από AI σε ένα PDF." +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Χρονικό όριο για πιο βαριές λειτουργίες agent, όπως η δημιουργία εγγράφων." +label = "Χρονικό όριο μακράς διάρκειας (δευτερόλεπτα)" + +[admin.settings.ai.general.note] +body = "Η μηχανή AI εκτελείται ως ξεχωριστή υπηρεσία. Το κοινό μυστικό της" +body2 = "ορίζεται μέσω μεταβλητής περιβάλλοντος container. Τα Provider API keys μπορούν να εισαχθούν σε αυτές τις σελίδες ή να δοθούν ως μεταβλητές περιβάλλοντος της μηχανής· τα keys που εισάγονται εδώ προωθούνται στη μηχανή όταν αποθηκευτούν." +title = "Σχετικά με τη μηχανή AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Χρονικό όριο για streaming απαντήσεις συνομιλίας (token προς token)." +label = "Χρονικό όριο stream (δευτερόλεπτα)" + +[admin.settings.ai.general.test] +button = "Δοκιμή σύνδεσης" +failBody = "Η μηχανή AI δεν ανταποκρίθηκε. Ελέγξτε το URL, ότι το container της μηχανής εκτελείται και ότι το AI είναι ενεργοποιημένο (απαιτείται επανεκκίνηση μετά την ενεργοποίηση)." +failTitle = "Η μηχανή AI δεν είναι προσβάσιμη" +okBody = "Η μηχανή AI ανταποκρίθηκε σε έλεγχο υγείας." +okTitle = "Η μηχανή AI είναι προσβάσιμη" + +[admin.settings.ai.general.timeoutSeconds] +description = "Χρονικό όριο για τυπικά αιτήματα AI προς τη μηχανή." +label = "Χρονικό όριο αιτήματος (δευτερόλεπτα)" + +[admin.settings.ai.general.url] +description = "Εσωτερικό URL της μηχανής Python AI, π.χ. http://stirling-pdf-engine:5001." +label = "URL μηχανής AI" + +[admin.settings.ai.limits] +description = "Προστατευτικά όρια για το πόση εργασία μπορούν να κάνουν τα αιτήματα AI και πόσα εκτελούνται ταυτόχρονα. Εφαρμόζονται στη μηχανή AI όταν αποθηκευτούν." +title = "Όρια & Απόδοση" + +[admin.settings.ai.limits.maxCharacters] +description = "Προστατευτικό όριο: απόρριψη αιτημάτων AI των οποίων το εξαγόμενο κείμενο υπερβαίνει αυτό το μήκος." +label = "Μέγιστοι χαρακτήρες ανά αίτημα" + +[admin.settings.ai.limits.maxPages] +description = "Προστατευτικό όριο: απόρριψη αιτημάτων AI πάνω από τόσες σελίδες PDF." +label = "Μέγιστες σελίδες ανά αίτημα" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Μέγιστες ταυτόχρονες εκκρεμείς κλήσεις μοντέλου σε όλη τη μηχανή." +label = "Μέγιστη ταυτόχρονη εκτέλεση μοντέλου" + +[admin.settings.ai.models] +description = "Επιλέξτε τον πάροχο LLM και τα smart/fast μοντέλα που χρησιμοποιεί η μηχανή AI. Εφαρμόζεται στη μηχανή AI όταν αποθηκευτεί." +title = "Μοντέλα & Πάροχοι" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Αφήστε το κενό για να χρησιμοποιηθεί το διαπιστευτήριο περιβάλλοντος της ίδιας της μηχανής. Ισχύει για αυτοφιλοξενούμενες αναπτύξεις μίας μηχανής." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Αποθηκεύτηκε - αφήστε κενό για να διατηρήσετε το τρέχον key" + +[admin.settings.ai.models.baseUrl] +description = "Βασικό URL του endpoint συμβατού με OpenAI / Ollama, π.χ. http://ollama:11434/v1." +label = "Βασικό URL παρόχου" +warning = "Το βασικό URL πρέπει να δείχνει σε αξιόπιστο εσωτερικό endpoint. Η μηχανή θα κάνει server-side αιτήματα προς αυτό, επομένως μια μη αξιόπιστη τιμή είναι ευαίσθητη σε SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Μέγιστος αριθμός output tokens για το fast μοντέλο." +label = "Μέγιστα tokens fast μοντέλου" + +[admin.settings.ai.models.fastModel] +description = "Φθηνότερο, ταχύτερο μοντέλο για ελαφριές εργασίες. Ελεύθερο κείμενο· οι προτάσεις είναι μόνο υποδείξεις." +label = "Fast μοντέλο" + +[admin.settings.ai.models.provider] +description = "Με ποιον πάροχο LLM επικοινωνεί η μηχανή." +label = "Πάροχος" + +[admin.settings.ai.models.smartMaxTokens] +description = "Μέγιστος αριθμός output tokens για το smart μοντέλο." +label = "Μέγιστα tokens smart μοντέλου" + +[admin.settings.ai.models.smartModel] +description = "Μοντέλο υψηλών δυνατοτήτων για σύνθετη συλλογιστική. Ελεύθερο κείμενο· οι προτάσεις είναι μόνο υποδείξεις." +label = "Smart μοντέλο" + +[admin.settings.ai.saved] +body = "Οι αλλαγές προωθούνται αυτόματα στη μηχανή AI." +bodyNoPush = "Οι ρυθμίσεις αποθηκεύτηκαν. Θα εφαρμοστούν την επόμενη φορά που η μηχανή AI θα λάβει τη διαμόρφωσή της." +title = "Οι ρυθμίσεις AI αποθηκεύτηκαν" + [admin.settings.badge] clickToUpgrade = "Κάντε κλικ για προβολή λεπτομερειών πλάνου" @@ -938,6 +1094,25 @@ label = "Επαναδημιουργία κατά την εκκίνηση" description = "Αριθμός ημερών που θα ισχύει το πιστοποιητικό" label = "Ισχύς πιστοποιητικού (ημέρες)" +[admin.settings.folderAccess] +description = "Κατάλογοι από τους οποίους οι πηγές φακέλων και οι έξοδοι φακέλων επιτρέπεται να διαβάζουν και στους οποίους επιτρέπεται να γράφουν. Αυτό είναι όριο ασφαλείας: οι αυτοματισμοί δεν μπορούν ποτέ να δείχνουν σε διαδρομή server εκτός αυτής της λίστας." +securityNote = "Αφήστε το κενό για να απενεργοποιήσετε πλήρως τις πηγές φακέλων και τις εξόδους. Ο κατάλογος διαμόρφωσης του Stirling είναι πάντα εκτός ορίων." +title = "Πρόσβαση σε φακέλους" + +[admin.settings.folderAccess.implied] +description = "Αυτοί οι κατάλογοι που διαχειρίζεται το Stirling επιτρέπονται πάντα και δεν μπορούν να αλλάξουν εδώ." +serverStorage = "Αποθήκευση αρχείων server" +title = "Πάντα επιτρεπόμενα" +watchedFolder = "Παρακολουθούμενος φάκελος pipeline" + +[admin.settings.folderAccess.roots] +add = "Προσθήκη" +empty = "Δεν επιτρέπονται φάκελοι. Οι πηγές φακέλων και οι έξοδοι είναι προς το παρόν απενεργοποιημένες." +hint = "Εισαγάγετε απόλυτες διαδρομές, για παράδειγμα /data/inbox." +label = "Επιτρεπόμενες ρίζες φακέλων" +placeholder = "/data/inbox" +remove = "Αφαίρεση ρίζας φακέλου" + [admin.settings.general] description = "Ρυθμίστε καθολικές ρυθμίσεις εφαρμογής, όπως επωνυμία και προεπιλεγμένη συμπεριφορά." system = "Σύστημα" @@ -1704,6 +1879,9 @@ width = "Πλάτος" [app] description = "Η δωρεάν εναλλακτική του Adobe Acrobat (10M+ λήψεις)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Μετατροπή σε PDF/A-3b" convertToPdfA3bDescription = "Δημιουργεί αρχειακό PDF με ενσωματωμένα συνημμένα" @@ -1842,6 +2020,15 @@ export = "Εξαγωγή" accessDenied = "Απαγορεύεται η πρόσβαση" insufficientPermissions = "Δεν έχετε άδεια για αυτή την ενέργεια." +[auth.callback] +completing = "Ολοκλήρωση ελέγχου ταυτότητας" +invalidToken = "Η σύνδεση OAuth απέτυχε - μη έγκυρο token." +missingToken = "Η σύνδεση OAuth απέτυχε - δεν λήφθηκε token." +oauthFailed = "Η σύνδεση OAuth απέτυχε. Δοκιμάστε ξανά." +pleaseWait = "Περιμένετε όσο ολοκληρώνουμε τη σύνδεσή σας." +signedOut = "Έχετε αποσυνδεθεί. Συνδεθείτε ξανά." +windowMayClose = "Μπορείτε να κλείσετε αυτό το παράθυρο μόλις ολοκληρωθεί." + [auth.displayName] guest = "Επισκέπτης" user = "Χρήστης" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Ασφαλίζει έγγραφα PDF αφαιρώντας description = "Αυτό το εργαλείο θα μετονομάζει αυτόματα αρχεία PDF με βάση το περιεχόμενό τους. Αναλύει το έγγραφο για να βρει τον καταλληλότερο τίτλο από το κείμενο." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Αυτόματη περιστροφή" +tags = "αυτόματη περιστροφή,διόρθωση προσανατολισμού,ανάποδα,πλάγια,ίσιωμα,προσανατολισμός,ανίχνευση προσανατολισμού,διόρθωση περιστροφής,αυτόματος προσανατολισμός,σαρωμένο,OSD" + +[autoRotate.confidenceThreshold] +title = "Όριο εμπιστοσύνης" + +[autoRotate.detectionMode] +auto = "Αυτόματα" +osd = "Μόνο OCR" +text = "Μόνο κείμενο" +title = "Μέθοδος ανίχνευσης" + +[autoRotate.error] +failed = "Παρουσιάστηκε σφάλμα κατά την αυτόματη περιστροφή του PDF." + +[autoRotate.inferUndetected] +title = "Περιστροφή σελίδων χαμηλής εμπιστοσύνης ώστε να ταιριάζουν" + +[autoRotate.report] +applied = "{{degrees}}° δεξιόστροφα" +confidenceValue = "Εμπιστοσύνη {{value}}" +noChange = "Καμία αλλαγή" +pageLabel = "Σελίδα {{number}}" +summary = "Περιστράφηκαν {{rotated}} από {{total}} σελίδες, {{unchanged}} έμειναν ως έχουν" +title = "Αναφορά ανίχνευσης" + +[autoRotate.report.method] +inferred = "Αντιστοιχίστηκε" +none = "Παραλείφθηκε" +osd = "OCR" +text = "Κείμενο" + +[autoRotate.report.note] +belowThreshold = "Κάτω από το όριο" +blankPage = "Η σελίδα είναι κενή" +inferredFromDocument = "Ίδια με τις άλλες σελίδες" +noDominantDirection = "Μικτές κατευθύνσεις κειμένου" +osdFailed = "Δεν βρέθηκε αναγνώσιμο κείμενο" +osdNoVerdict = "Δεν ήταν δυνατό να προσδιοριστεί ο σωστός προσανατολισμός" +tesseractUnavailable = "Το OCR δεν είναι εγκατεστημένο" +tooFewGlyphs = "Πολύ λίγο κείμενο" + +[autoRotate.results] +title = "Αποτελέσματα αυτόματης περιστροφής" + +[autoRotate.settings] +title = "Ρυθμίσεις" + +[autoRotate.tooltip.confidenceThreshold] +text = "Πόσο βέβαιος πρέπει να είναι ο έλεγχος πριν περιστραφεί μια σελίδα. Μειώστε το για να περιστρέφονται περισσότερες σελίδες, αυξήστε το για να μένουν περισσότερες σελίδες ανέπαφες." +title = "Όριο εμπιστοσύνης" + +[autoRotate.tooltip.description] +text = "Υπολογίζει ποιος πρέπει να είναι ο σωστός προσανατολισμός κάθε σελίδας και τη γυρίζει σωστά. Οι σελίδες που είναι ήδη όρθιες μένουν ως έχουν." + +[autoRotate.tooltip.detectionMode] +text = "Το Αυτόματα διαβάζει πρώτα το κείμενο στη σελίδα και μετά εξετάζει τη σελίδα ως εικόνα αν αυτό δεν είναι αρκετό. Το Μόνο κείμενο παραλείπει τον έλεγχο εικόνας, που είναι ταχύτερο για έγγραφα που δεν σαρώθηκαν ποτέ. Το Μόνο OCR αντιμετωπίζει κάθε σελίδα ως εικόνα." +title = "Μέθοδος ανίχνευσης" + +[autoRotate.tooltip.header] +title = "Αυτόματη περιστροφή" + +[autoRotate.tooltip.inferUndetected] +text = "Οι κενές ή σχεδόν κενές σελίδες δεν μπορούν να ελεγχθούν από μόνες τους. Όταν κάθε σελίδα που μπορούσε να διαβαστεί χρειάζεται την ίδια περιστροφή, αυτές οι σελίδες περιστρέφονται ώστε να ταιριάζουν." +title = "Περιστροφή σελίδων χαμηλής εμπιστοσύνης ώστε να ταιριάζουν" + [autoSizeSplitPDF] tags = "pdf,διαχωρισμός,έγγραφο,οργάνωση" @@ -2189,33 +2443,6 @@ keywords = "Λέξεις-κλειδιά: odd, even" numbers = "Αριθμοί/εύρη: 5, 10-20" progressions = "Πρόοδοι: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Τύποι πιστοποιητικών" - -[certificateChoice.tooltip.organization] -bullet1 = "Διαχειρίζεται από διαχειριστές συστήματος" -bullet2 = "Κοινόχρηστο μεταξύ εξουσιοδοτημένων χρηστών" -bullet3 = "Αντιπροσωπεύει την ταυτότητα της εταιρείας, όχι του ατόμου" -bullet4 = "Κατάλληλο για: Επίσημα έγγραφα, ομαδικές υπογραφές" -description = "Ένα κοινόχρηστο πιστοποιητικό που παρέχεται από τον οργανισμό σας. Χρησιμοποιείται για εταιρική εξουσιοδότηση υπογραφών." -title = "Πιστοποιητικό οργανισμού" - -[certificateChoice.tooltip.personal] -bullet1 = "Δημιουργείται αυτόματα κατά την πρώτη χρήση" -bullet2 = "Συνδέεται με τον λογαριασμό χρήστη σας" -bullet3 = "Δεν μπορεί να κοινοποιηθεί σε άλλους χρήστες" -bullet4 = "Κατάλληλο για: Προσωπικά έγγραφα, ατομική υπευθυνότητα" -description = "Ένα αυτόματα δημιουργημένο πιστοποιητικό, μοναδικό για τον λογαριασμό χρήστη σας. Κατάλληλο για ατομικές υπογραφές." -title = "Προσωπικό πιστοποιητικό" - -[certificateChoice.tooltip.upload] -bullet1 = "Απαιτεί αρχείο P12/PFX και κωδικό πρόσβασης" -bullet2 = "Μπορεί να εκδοθεί από εξωτερικές Αρχές Πιστοποίησης" -bullet3 = "Υψηλότερο επίπεδο εμπιστοσύνης για νομικά έγγραφα" -bullet4 = "Κατάλληλο για: Νομικά δεσμευτικά συμβόλαια, εξωτερική επικύρωση" -description = "Χρησιμοποιήστε το δικό σας αρχείο πιστοποιητικού PKCS#12. Παρέχει πλήρη έλεγχο στις ιδιότητες του πιστοποιητικού." -title = "Μεταφόρτωση προσαρμοσμένου P12" - [certSign] awaitingSignatures = "Αναμονή υπογραφών" chooseCertificate = "Επιλέξτε αρχείο πιστοποιητικού" @@ -2542,34 +2769,6 @@ server = "Διακομιστής" stepTitle = "Πηγή πιστοποιητικού" upload = "Μεταφόρτωση" -[certSign.tooltip.header] -title = "Σχετικά με τη διαχείριση υπογραφών" - -[certSign.tooltip.overview] -bullet1 = "Ελέγξτε υπάρχουσες υπογραφές και την εγκυρότητά τους" -bullet2 = "Προβάλετε λεπτομερείς πληροφορίες για υπογράφοντες και πιστοποιητικά" -bullet3 = "Προσθέστε νέες ψηφιακές υπογραφές για να ασφαλίσετε τα έγγραφά σας" -bullet4 = "Υποστήριξη πολλών αρχείων με εύκολη πλοήγηση" -text = "Αυτό το εργαλείο σάς επιτρέπει να ελέγξετε αν τα PDF σας είναι ψηφιακά υπογεγραμμένα και να προσθέσετε νέες ψηφιακές υπογραφές. Οι ψηφιακές υπογραφές αποδεικνύουν ποιος δημιούργησε ή ενέκρινε ένα έγγραφο και αν έχει αλλάξει από την υπογραφή." -title = "Τι μπορεί να κάνει αυτό το εργαλείο;" - -[certSign.tooltip.signing] -bullet1 = "Υποστηρίζει μορφές PEM, PKCS12, JKS και πιστοποιητικό διακομιστή" -bullet2 = "Επιλογή εμφάνισης ή απόκρυψης της υπογραφής στο PDF" -bullet3 = "Προσθέστε λόγο, τοποθεσία και όνομα υπογράφοντος" -bullet4 = "Επιλέξτε σε ποια σελίδα θα τοποθετηθούν οι ορατές υπογραφές" -bullet5 = "Χρήση πιστοποιητικού διακομιστή για την απλή επιλογή 'Sign with Stirling-PDF'" -text = "Για να υπογράψετε ένα PDF, χρειάζεστε ένα ψηφιακό πιστοποιητικό (όπως PEM, PKCS12 ή JKS). Μπορείτε να επιλέξετε να κάνετε την υπογραφή ορατή στο έγγραφο ή να την κρατήσετε αόρατη μόνο για λόγους ασφάλειας." -title = "Προσθήκη υπογραφών" - -[certSign.tooltip.validation] -bullet1 = "Εμφανίζει αν οι υπογραφές είναι έγκυρες ή άκυρες" -bullet2 = "Προβάλλει πληροφορίες υπογράφοντος και ημερομηνία υπογραφής" -bullet3 = "Ελέγχει αν το έγγραφο τροποποιήθηκε μετά την υπογραφή" -bullet4 = "Μπορεί να χρησιμοποιήσει προσαρμοσμένα πιστοποιητικά για επαλήθευση" -text = "Όταν ελέγχετε υπογραφές, το εργαλείο σας λέει αν είναι έγκυρες, ποιος υπέγραψε το έγγραφο, πότε υπογράφηκε και αν το έγγραφο έχει αλλάξει από τότε που υπογράφηκε." -title = "Έλεγχος υπογραφών" - [changeCreds] changeUsername = "Ενημερώστε το όνομα χρήστη σας. Θα αποσυνδεθείτε μετά την ενημέρωση." credsUpdated = "Ο λογαριασμός ενημερώθηκε" @@ -2737,12 +2936,16 @@ text = "Για να καταστούν αυτά τα δικαιώματα μη [chat.actions] copy = "Αντιγραφή μηνύματος" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Επιλογές πράκτορα Stirling" +assistant = "AI Assistant" clearChat = "Καθαρισμός συνομιλίας" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Αυτή η λειτουργία θα χρησιμοποιήσει τ tooltip = "Επιλογή χρώματος από την οθόνη" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Επιλέξτε χρώμα" [common] +actions = "Actions" back = "Πίσω" cancel = "Άκυρο" close = "Κλείσιμο" +codeSample = "Code sample" collapse = "Σύμπτυξη" confirm = "Επιβεβαίωση" continue = "Συνέχεια" copied = "Αντιγράφηκε!" copy = "Αντιγραφή" done = "Τέλος" +enterValue = "Enter value" error = "Σφάλμα" expand = "Ανάπτυξη" loading = "Φόρτωση..." next = "Επόμενο" +open = "Open" preview = "Προεπισκόπηση" previous = "Προηγούμενο" refresh = "Ανανέωση" +remaining = "Απομένουν" retry = "Επανάληψη" save = "Αποθήκευση" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Καθαρισμός επιλεγμένων" @@ -2988,6 +3199,7 @@ title = "Μέθοδος συμπίεσης" [compress.settings] desiredSize = "Επιθυμητό μέγεθος αρχείου" desiredSizePlaceholder = "Εισαγάγετε μέγεθος" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Η συμπίεση είναι ένας εύκολος τρόπος να μειώσετε το μέγεθος του αρχείου σας. Επιλέξτε Μέγεθος αρχείου για να εισαγάγετε έναν στόχο και να προσαρμόσουμε την ποιότητα για εσάς. Επιλέξτε Ποιότητα για να ρυθμίσετε χειροκίνητα την ισχύ συμπίεσης." @@ -3190,6 +3402,24 @@ pdfOptions = "Επιλογές PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Επιλέξτε πρώτα μορφή προέλευσης" settings = "Ρυθμίσεις" single = "Μονό" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Προηγμένη παρακολούθηση" -allInOneWorkspace = "Ενιαίος χώρος εργασίας PDF (προβολέας, εργαλεία & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Αφοσιωμένη υποστήριξη & SLA" -fasterThroughput = "10x ταχύτερος ρυθμός επεξεργασίας" -fullyPrivateFiles = "Πλήρως ιδιωτικά αρχεία" -largeFileProcessing = "Επεξεργασία μεγάλων αρχείων" -orgWideAccess = "Έλεγχοι πρόσβασης σε όλο τον οργανισμό" -premiumAiModels = "Premium μοντέλα AI" -prioritySupport = "Υποστήριξη προτεραιότητας" -privateDocCloud = "Ιδιωτικό Document Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Ασφαλής πρόσβαση API" -standardThroughput = "Τυπικός ρυθμός επεξεργασίας" -unlimitedApiAccess = "Απεριόριστη πρόσβαση API" -unlimitedSeats = "Απεριόριστες θέσεις" - [crop] autoCrop = "Αυτόματη περικοπή κενού" noFileSelected = "Επιλέξτε ένα αρχείο PDF για να ξεκινήσετε την περικοπή" @@ -3346,6 +3558,7 @@ message = "Αποτυχία ορισμού προεπιλεγμένου χειρ title = "Σφάλμα" [defaultApp.prompt] +dontRemind = "Να μην μου το υπενθυμίσεις ξανά" message = "Κάντε το Stirling PDF την προεπιλεγμένη εφαρμογή για άνοιγμα αρχείων PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Αναζήτηση..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Επεξεργασία" @@ -3644,11 +3861,13 @@ shareSelected = "Κοινή χρήση επιλεγμένων" sharing = "Κοινή χρήση" showAll = "Προβολή όλων" showHistory = "Εμφάνιση ιστορικού" +sortBy = "Sort files" sortByDate = "Ταξινόμηση κατά ημερομηνία" sortByName = "Ταξινόμηση κατά όνομα" sortBySize = "Ταξινόμηση κατά μέγεθος" storage = "Αποθήκευση" storageState = "Αποθήκευση" +storageUsed = "Storage used" synced = "Συγχρονισμένο" title = "Μεταφόρτωση αρχείων PDF" toolChain = "Εργαλεία που εφαρμόστηκαν" @@ -3694,13 +3913,17 @@ addFiles = "Προσθήκη αρχείων" addingFiles = "Προσθήκη αρχείων…" collapse = "Σύμπτυξη πλευρικής στήλης" customizeGroups = "Προσαρμογή ομάδων" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Ανοίξτε αρχεία για να ξεκινήσετε" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Ανάπτυξη πλευρικής στήλης" -files = "Αρχεία" googleDrive = "Google Drive" googleDriveDisabled = "Το Google Drive δεν έχει ρυθμιστεί" leaveMyFiles = "Έξοδος από Τα αρχεία μου" +library = "Βιβλιοθήκη PDF" myFiles = "Τα αρχεία μου" noFiles = "Ακόμη δεν υπάρχουν αρχεία" openFileManager = "Άνοιγμα διαχείρισης αρχείων" @@ -3708,37 +3931,36 @@ openFromComputer = "Άνοιγμα από υπολογιστή" openSettings = "Άνοιγμα ρυθμίσεων" other = "Άλλα" recent = "Πρόσφατα" -search = "Αναζήτηση" -searchPlaceholder = "Αναζήτηση αρχείων..." viewAll = "Προβολή όλων των {{count}} αρχείων" [fileSidebar.fileItem] closeViewer = "Κλείσιμο προβολέα" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Άνοιγμα στον προβολέα" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Προσθήκη" -addLabel = "Προσθήκη ετικέτας…" -categoryIconAria = "Επιλέξτε εικονίδιο για {{name}}" -createCategory = "Δημιουργία κατηγορίας" -delete = "Διαγραφή κατηγορίας" done = "Τέλος" -hide = "Απόκρυψη κατηγορίας" -newCategory = "Νέο όνομα κατηγορίας…" -removeLabel = "Αφαίρεση {{name}}" -rename = "Μετονομασία" reset = "Επαναφορά προεπιλογών" -search = "Αναζήτηση ετικετών…" -show = "Εμφάνιση κατηγορίας" subtitle = "Ομαδοποιήστε τα αρχεία σας σε γονικές κατηγορίες. Προσθέστε υπάρχουσες ή νέες ετικέτες σε μια κατηγορία, μετονομάστε την ή δημιουργήστε τη δική σας. Τα αρχεία που δεν ανήκουν σε καμία από τις κατηγορίες σας εμφανίζονται στην ενότητα «Άλλα»." title = "Κατηγορίες πλαϊνής γραμμής" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Προσθήκη σε χώρο εργασίας" addToWorkspaceCount = "Προσθήκη {{count}} σε χώρο εργασίας" @@ -3749,7 +3971,6 @@ backToMyFiles = "Πίσω στα Αρχεία μου" breadcrumbs = "Διαδρομή φακέλου" cancel = "Άκυρο" classification = "Ταξινόμηση" -clearSearch = "Καθαρισμός αναζήτησης" clearSelection = "Εκκαθάριση επιλογής" closeDetails = "Κλείσιμο λεπτομερειών" create = "Δημιουργία" @@ -3781,6 +4002,7 @@ downloadAll = "Λήψη όλων" downloadVersion = "Λήψη αυτής της έκδοσης" dropOverlay = "Αποθέστε αρχεία για μεταφόρτωση" dropOverlaySub = "Τα αρχεία ξεκινούν στο Local. Χρησιμοποιήστε 'Μετακίνηση σε' ή 'Αποθήκευση στο cloud' για να τα οργανώσετε σε φάκελο." +duplicate = "Duplicate" file = "Αρχείο" fileInfo = "File info" fileMenu = "Ενέργειες αρχείου" @@ -3808,13 +4030,12 @@ refresh = "Ανανέωση από τον διακομιστή" remove = "Διαγραφή" removeVersion = "Αφαίρεση αυτής της έκδοσης" rename = "Μετονομασία" +renamed = "Μετονομάστηκε" renameFolder = "Μετονομασία φακέλου" resizeFolderTree = "Αλλαγή μεγέθους δέντρου φακέλων (πλήκτρα βελών, Shift για μεγαλύτερα βήματα· διπλό κλικ για αυτόματη προσαρμογή)" save = "Αποθήκευση" saveToServer = "Αποθήκευση στον διακομιστή" saveToServerDisabledHint = "Η αποθήκευση στον διακομιστή δεν είναι ενεργοποιημένη σε αυτόν τον διακομιστή. Ζητήστε από τον διαχειριστή σας να την ενεργοποιήσει." -search = "Αναζήτηση" -searchPlaceholder = "Αναζήτηση σε αυτόν τον φάκελο & υποφακέλους" selectAll = "Επιλογή όλων" selectAllHint = "Κάντε κλικ για επιλογή όλων. Συμβουλή: κρατήστε πατημένο το Ctrl (ή Cmd) για να προσθέτετε αρχεία ένα-ένα, Shift για επιλογή εύρους." selectedCount = "{{count}} επιλεγμένα" @@ -3874,6 +4095,10 @@ title = "Δεν υπάρχουν ακόμη αρχεία cloud" hint = "Αρχεία που αποθηκεύτηκαν χωρίς μεταφόρτωση μένουν εδώ. Αποθέστε ένα αρχείο για προσθήκη." title = "Δεν υπάρχουν μόνο-τοπικά αρχεία" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Αρχεία που ανοίγετε ή επεξεργάζεστε θα εμφανιστούν εδώ." title = "Δεν έχει τροποποιηθεί ακόμα τίποτα" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Δεν ήταν δυνατή η διαγραφή φακέλου." deleteFolderFailedDetail = "Δεν ήταν δυνατή η διαγραφή φακέλου: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Δεν ήταν δυνατή η ενημέρωση εμφάνισης φακέλου." folderAppearanceFailedDetail = "Δεν ήταν δυνατή η ενημέρωση εμφάνισης φακέλου: {{message}}" moveFilesFailed = "Δεν ήταν δυνατή η μετακίνηση αρχείων." @@ -3910,9 +4137,7 @@ count = "Αρχεία" folder = "Φάκελος" labels = "Ετικέτες" modified = "Τροποποιήθηκε" -name = "Όνομα" size = "Μέγεθος" -toolHistoryAtVersion = "Σωρευτική αλυσίδα εργαλείων" totalSize = "Συνολικό μέγεθος" type = "Τύπος" versionHistory = "Πορεία εκδόσεων" @@ -3945,6 +4170,11 @@ localHint = "Αποθηκευμένο μόνο σε αυτό το πρόγραμ shared = "Κοινοποιημένο" sharedHint = "Κοινοποιήθηκε σε εσάς μέσω συνδέσμου" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Παλαιότερα πρώτα" modifiedDesc = "Πιο πρόσφατα πρώτα" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Δεν υπάρχουν αρχεία στην αποθή open = "Άνοιγμα" openFile = "Άνοιγμα αρχείου" openFiles = "Άνοιγμα αρχείων" +selectFile = "Select {{name}}" selectFromStorage = "Επιλογή από αποθήκευση" upload = "Μεταφόρτωση" uploadFile = "Μεταφόρτωση αρχείου" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Όλα αποθηκεύτηκαν" +analyzingFields = "Analysing form fields..." +extractCsvError = "Απέτυχε η εξαγωγή CSV" +extractXlsxError = "Απέτυχε η εξαγωγή XLSX" +filled = "filled" flattenAfterFilling = "Επιπεδοποίηση μετά τη συμπλήρωση" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "απαιτ." +requiredFieldsError = "Συμπληρώστε όλα τα απαιτούμενα πεδία" rescanFields = "Επανασάρωση πεδίων" rescanFormFields = "Επανασάρωση πεδίων φόρμας" save = "Αποθήκευση" +saveShortcut = "Ctrl+S για αποθήκευση" +unsavedChanges = "Μη αποθηκευμένες αλλαγές" [formFill.sidebar] close = "Κλείσιμο πλευρικής γραμμής" @@ -4309,6 +4552,11 @@ desc = "Μετονομάζει αυτόματα ένα αρχείο PDF βάσε tags = "αυτόματος εντοπισμός,βασισμένο σε κεφαλίδα,οργάνωση,επαναονομασία" title = "Αυτόματη μετονομασία PDF" +[home.autoRotate] +desc = "Ανίχνευση του προσανατολισμού κάθε σελίδας και αυτόματη περιστροφή της σε όρθια θέση." +tags = "αυτόματη περιστροφή,διόρθωση προσανατολισμού,ανάποδα,πλάγια,ίσιωμα,προσανατολισμός,ανίχνευση προσανατολισμού,διόρθωση περιστροφής,αυτόματος προσανατολισμός,σαρωμένο,OSD" +title = "Αυτόματη περιστροφή" + [home.autoSizeSplitPDF] tags = "αυτόματο,διάσπαση,μέγεθος" title = "Αυτόματο σπάσιμο με μέγεθος/σελ." @@ -4608,15 +4856,12 @@ hint = "Μεταφορτώστε ένα PNG, JPG, SVG ή άλλο αρχείο label = "Μεταφόρτωση εικόνας" placeholder = "Επιλέξτε αρχείο εικόνας" -[imageToPdf] -tags = "μετατροπή,εικόνα,jpg,φωτογραφία" - [imageToPDF] header = "Εικόνα σε PDF" title = "Εικόνα σε PDF" -[infoBanner] -dismiss = "Απόρριψη" +[imageToPdf] +tags = "μετατροπή,εικόνα,jpg,φωτογραφία" [invite] acceptError = "Αποτυχία δημιουργίας λογαριασμού" @@ -4647,11 +4892,9 @@ welcomeTitle = "Έχετε προσκληθεί!" [landing] addFiles = "Προσθήκη αρχείων" -heroSubtitle = "Σύρετε εδώ ή προσθέστε ένα υπάρχον PDF για να ξεκινήσετε." mobileUpload = "Μεταφόρτωση από κινητό" openFromComputer = "Άνοιγμα από τον υπολογιστή" uploadFromComputer = "Μεταφόρτωση από υπολογιστή" -workbenchEmptyStateHero = "Αφήστε ένα PDF οπουδήποτε" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Εισαγάγετε όνομα χρήστη" failedToSignIn = "Αποτυχία σύνδεσης με {{provider}}: {{message}}" forgotPassword = "Ξεχάσατε τον κωδικό σας;" loggingIn = "Γίνεται σύνδεση..." -logIn = "Σύνδεση" login = "Σύνδεση" magicLinkSent = "Στάλθηκε μαγικός σύνδεσμος στο {{email}}! Ελέγξτε το email σας και κάντε κλικ στον σύνδεσμο για να συνδεθείτε." mfaCode = "Κωδικός ελέγχου ταυτότητας" @@ -4715,26 +4957,10 @@ signInWith = "Σύνδεση με" title = "Σύνδεση" unexpectedError = "Απρόσμενο σφάλμα: {{message}}" updatePassword = "Ενημέρωση κωδικού πρόσβασης" -useEmailInstead = "Σύνδεση με email" useMagicLink = "Χρησιμοποιήστε μαγικό σύνδεσμο" username = "Όνομα χρήστη" youAreLoggedIn = "Έχετε συνδεθεί!" -[login.slides.edit] -alt = "Επεξεργασία PDF" -subtitle = "Με πάνω από δώδεκα εργαλεία για απόκρυψη, υπογραφή, ανάγνωση και χειρισμό PDF, θα βρείτε σίγουρα αυτό που ψάχνετε." -title = "Επεξεργαστείτε PDF για να προβάλετε/προστατεύσετε τις πληροφορίες που θέλετε" - -[login.slides.overview] -alt = "Επισκόπηση του Stirling PDF" -subtitle = "Σουίτα cloud με προτεραιότητα το απόρρητο για PDF που σας επιτρέπει να μετατρέπετε, να υπογράφετε, να αποκρύπτετε και να διαχειρίζεστε έγγραφα, μαζί με 50+ ακόμη ισχυρά εργαλεία." -title = "Ό,τι χρειάζεστε για τα PDF σε ένα σημείο." - -[login.slides.secure] -alt = "Ασφάλεια PDF" -subtitle = "Προσθέστε κωδικούς, αποκρύψτε περιεχόμενο και διαχειριστείτε πιστοποιητικά με ευκολία." -title = "Προστατέψτε ευαίσθητες πληροφορίες στα PDF σας" - [margin] large = "Μεγάλο" medium = "Μεσαίο" @@ -4818,6 +5044,34 @@ uploadSuccess = "Η μεταφόρτωση ολοκληρώθηκε!" uploadSuccessMessage = "Οι εικόνες σας μεταφέρθηκαν." validating = "Γίνεται επικύρωση συνεδρίας..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Σαρώστε για μεταφόρτωση φωτογραφιών. Οι εικόνες μετατρέπονται αυτόματα σε PDF." descriptionNoConvert = "Σαρώστε για μεταφόρτωση φωτογραφιών από την κινητή συσκευή σας." @@ -4836,8 +5090,13 @@ title = "Μεταφόρτωση από κινητό" tags = "Πολυεργαλείο,Πολλαπλές λειτουργίες,UI,κλικ και σύρσιμο,frontend,πλευρά πελάτη,διαδραστικό,διαχειρίσιμο,μετακίνηση,διαγραφή,μετανάστευση,διαίρεση" title = "Πολυεργαλείο PDF" -[navbar] -search = "Αναζήτηση" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Η ταυτοποίηση δεν ήταν επιτυχής. Μπορείτε να κλείσετε αυτό το παράθυρο και να δοκιμάσετε ξανά." @@ -4966,6 +5225,7 @@ activeFiles = "Η προβολή Ενεργά αρχεία εμ allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Κλείσιμο" cropSettings = "Τώρα που επιλέξαμε το αρχείο που θέλουμε να περικόψουμε, μπορούμε να ρυθμίσουμε το εργαλείο Περικοπή για να ορίσουμε την περιοχή στην οποία θέλουμε να περικοπεί το PDF." +dialogLabel = "Onboarding" fileCheckbox = "Κάνοντας κλικ σε ένα από τα αρχεία το επιλέγετε για επεξεργασία. Μπορείτε να επιλέξετε πολλαπλά αρχεία για ομαδικές ενέργειες." fileReplacement = "Το τροποποιημένο αρχείο θα αντικαταστήσει αυτόματα το πρωτότυπο στην Επιφάνεια εργασίας, ώστε να μπορείτε εύκολα να το περάσετε από περισσότερα εργαλεία." filesButton = "Το κουμπί Αρχεία στη Γραμμή Γρήγορης Πρόσβασης σάς επιτρέπει να ανεβάσετε PDF για χρήση με τα εργαλεία." @@ -4976,6 +5236,7 @@ pinButton = "Μπορείτε να χρησιμοποιήσετε το κουμ results = "Αφού ολοκληρωθεί η εκτέλεση του εργαλείου, το βήμα Ανασκόπηση θα εμφανίσει προεπισκόπηση των αποτελεσμάτων σε αυτό το πάνελ και θα σας επιτρέψει να αναιρέσετε την ενέργεια ή να κατεβάσετε το αρχείο." runButton = "Αφού ρυθμιστεί το εργαλείο, αυτό το κουμπί σάς επιτρέπει να το εκτελέσετε σε όλα τα επιλεγμένα PDF." selectCropTool = "Ας επιλέξουμε το εργαλείο Περικοπή για να δείξουμε πώς να χρησιμοποιείτε ένα από τα εργαλεία." +stepOf = "Βήμα {{current}} από {{total}}" toolInterface = "Αυτή είναι η διεπαφή του εργαλείου Περικοπή. Όπως βλέπετε, δεν υπάρχει ακόμη πολλά, επειδή δεν έχουμε προσθέσει αρχεία PDF για εργασία." workbench = "Αυτή είναι η Επιφάνεια εργασίας - ο κεντρικός χώρος όπου βλέπετε και επεξεργάζεστε τα PDF σας." wrapUp = "Είστε έτοιμοι! Μάθατε για τις κύριες περιοχές της εφαρμογής και πώς να τις χρησιμοποιείτε. Κάντε κλικ στο κουμπί Βοήθεια οποτεδήποτε για να δείτε ξανά αυτή την περιήγηση." @@ -4988,6 +5249,26 @@ showMeAround = "Ξεναγήστε με" skipForNow = "Παράλειψη προς το παρόν" skipTheTour = "Παράλειψη ξενάγησης" +[onboarding.checklist] +dismiss = "Παράβλεψη" +title = "Ρύθμιση του Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Εκτελέστε το Stirling εγγενώς στον υπολογιστή σας" +title = "Λήψη του Stirling για Desktop" + +[onboarding.checklist.inviteTeam] +description = "Συνεργαστείτε με την ομάδα σας" +title = "Προσκαλέστε μέλη ομάδας" + +[onboarding.checklist.shareAnalytics] +description = "Βοηθήστε στη βελτίωση του Stirling" +title = "Κοινή χρήση ανώνυμων δεδομένων χρήσης" + +[onboarding.checklist.takeTour] +description = "Δείτε πώς λειτουργεί το Stirling σε μια γρήγορη περιήγηση" +title = "Κάντε την περιήγηση" + [onboarding.desktopInstall] body = "Το Stirling λειτουργεί καλύτερα ως εφαρμογή για υπολογιστή. Μπορείτε να το χρησιμοποιείτε offline, να έχετε ταχύτερη πρόσβαση στα έγγραφα και να κάνετε επεξεργασίες τοπικά στον υπολογιστή σας." selectOs = "Επιλέξτε λειτουργικό σύστημα" @@ -4996,7 +5277,21 @@ titleWithOs = "Λήψη για {{osLabel}}" [onboarding.mfa] authenticationCode = "Κωδικός ελέγχου ταυτότητας" +description = "Ασφαλίστε τον λογαριασμό σας συνδέοντας μια εφαρμογή ελέγχου ταυτότητας. Σαρώστε τον QR code ή εισαγάγετε το setup key και μετά επιβεβαιώστε τον 6ψήφιο κωδικό για να ολοκληρώσετε." +enable = "Ενεργοποίηση MFA" +enableError = "Δεν είναι δυνατή η ενεργοποίηση του ελέγχου ταυτότητας δύο παραγόντων. Ελέγξτε τον κωδικό και δοκιμάστε ξανά." +enterCode = "Εισαγάγετε τον 6ψήφιο κωδικό από την εφαρμογή σας." +enterCodeError = "Εισαγάγετε τον κωδικό ελέγχου ταυτότητας για να συνεχίσετε." +logout = "Αποσύνδεση" +openAuthenticator = "Ανοίξτε το Google Authenticator, Authy ή 1Password." qrCodeLoading = "Δημιουργία του QR κωδικού σας…" +regenerateQrCode = "Αναδημιουργία QR code" +scanQrCode = "Σαρώστε τον QR code ή εισαγάγετε το setup key παρακάτω." +setupError = "Δεν είναι δυνατή η έναρξη ρύθμισης δύο παραγόντων. Δοκιμάστε ξανά." +setupKey = "Setup key (χειροκίνητη εισαγωγή)" +stepByStep = "Βήμα προς βήμα" +success = "Το MFA έχει ενεργοποιηθεί. Μπορείτε τώρα να συνεχίσετε." +title = "Ρύθμιση ελέγχου ταυτότητας πολλαπλών παραγόντων" [onboarding.planOverview] adminBodyLoginDisabled = "Μόλις ενεργοποιήσετε τη λειτουργία σύνδεσης, μπορείτε να διαχειρίζεστε χρήστες, να ρυθμίζετε επιλογές και να παρακολουθείτε την υγεία του διακομιστή. Τα πρώτα {{freeTierLimit}} άτομα στον διακομιστή σας χρησιμοποιούν το Stirling δωρεάν." @@ -5005,6 +5300,11 @@ adminTitle = "Επισκόπηση διαχειριστή" userBody = "Προσκαλέστε συνεργάτες, αναθέστε ρόλους και κρατήστε τα έγγραφα οργανωμένα σε έναν ασφαλή χώρο εργασίας. Ενεργοποιήστε τη λειτουργία σύνδεσης όταν είστε έτοιμοι να ξεπεράσετε τη μονοπρόσωπη χρήση." userTitle = "Επισκόπηση πλάνου" +[onboarding.processorIntro] +body = "Το Stirling εκτελεί πλέον Πολιτικές — αυτοματοποιημένους κανόνες που ταξινομούν, ασφαλίζουν και επεξεργάζονται κάθε έγγραφο όταν φτάνει. Ρυθμίστε τις και παρακολουθήστε τις εκτελέσεις στον Processor." +cta = "Δείτε τον Processor" +title = "Δείτε τον Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Προπληρωμένο έτος" + +[payg.prepaid.banner] +exhausted = "Η προπληρωμένη χωρητικότητά σας εξαντλήθηκε· η χρέωση βάσει χρήσης συνεχίστηκε. Συμπληρώστε για να διατηρήσετε την έκπτωση." +expiring_one = "Το προπληρωμένο έτος σας λήγει σε {{count}} ημέρα. Η αχρησιμοποίητη χωρητικότητα λήγει — συμπληρώστε για να ξεκινήσετε ένα νέο έτος." +expiring_other = "Το προπληρωμένο έτος σας λήγει σε {{count}} ημέρες. Η αχρησιμοποίητη χωρητικότητα λήγει — συμπληρώστε για να ξεκινήσετε ένα νέο έτος." +low = "Έχετε πέσει στο {{pct}}% της προπληρωμένης χωρητικότητάς σας. Συμπληρώστε οποιαδήποτε στιγμή για να διατηρήσετε την επεξεργασία χωρίς διακοπές." + +[payg.prepaid.card] +subtitle = "Χρησιμοποιείται πριν από τη χρέωση βάσει χρήσης και εκτός του ορίου δαπανών σας." +title = "Προπληρωμένη χωρητικότητα" + +[payg.prepaid.meter] +capSuffix = "από {{total}} προπληρωμένες πιστώσεις" +expires = "Λήγει στις {{date}}" + +[payg.prepaid.state] +exhausted = "Εξαντλήθηκε" +healthy = "Απομένουν πολλά" +low = "Εξαντλείται" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF σε PDF/A" tags = "αρχείο,μακροχρόνιο,πρότυπο,μετατροπή,αποθήκευση,διατήρηση" title = "PDF σε PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "εκτύπωση,πρότυπο,μετατροπή,παραγωγή,προεκτύπωση,αρχειοθέτηση" title = "PDF σε PDF/X" @@ -5822,6 +6151,7 @@ small = "500 μονάδες" xsmall = "100 μονάδες" [plan.availablePlans] +currency = "Billing currency" subtitle = "Επιλέξτε το πλάνο που ταιριάζει στις ανάγκες σας" title = "Διαθέσιμα πλάνα" @@ -5937,49 +6267,12 @@ keyDescription = "Επικολλήστε το κλειδί άδειας από success = "Η άδεια ενεργοποιήθηκε!" successMessage = "Η άδειά σας ενεργοποιήθηκε με επιτυχία. Μπορείτε τώρα να κλείσετε αυτό το παράθυρο." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "επιβλήθηκε" -enforcing = "Γίνεται επιβολή..." -failed = "Η επιβολή απέτυχε" outputsUnavailable = "Τα αποτελέσματα πολιτικής δεν είναι πλέον διαθέσιμα για λήψη." partialOutputsUnavailable = "Ορισμένα αποτελέσματα πολιτικής δεν είναι πλέον διαθέσιμα για λήψη." -retrying = "Απασχολημένο, επαναπροσπάθεια..." runNotFound = "Δεν ήταν πλέον δυνατή η εύρεση της εκτέλεσης επιβολής." -step = "βήμα {{current}}/{{total}}" timedOut = "Η επιβολή έληξε λόγω χρονικού ορίου πριν ολοκληρωθεί η εκτέλεση." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Κλείσιμο" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Εφαρμογή {{names}}" applyingProgress = "Εφαρμογή {{names}} ({{done}} από {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} από {{total}} αρχείο(α) δεν ήταν δ failureTitle = "Εξαγωγή χωρίς πλήρη επιβολή" printPolicyAppliedBody = "Αυτό το PDF ενημερώθηκε για να συμμορφωθεί με μια πολιτική. Ελέγξτε τις αλλαγές και εκτυπώστε ξανά." printPolicyAppliedTitle = "Εφαρμόστηκε πολιτική πριν από την εκτύπωση" -queued = "+{{count}} στην ουρά" successTitle = "{{names}} εφαρμόστηκαν" summaryMore = "{{first}}, {{second}} και {{more}} ακόμη" summaryTwo = "{{first}} και {{second}}" -[policies.enforcement.triggerVerb] -convert = "Επιβολή πριν από τη μετατροπή" -default = "Επιβολή" -export = "Επιβολή πριν από την εξαγωγή" -input = "Επιβολή κατά την εισαγωγή" -print = "Επιβολή πριν από την εκτύπωση" - -[policies.field] -accessLog = "Αρχείο καταγραφής πρόσβασης" -archiveAfter = "Αρχειοθέτηση μετά από" -auditTrail = "Ίχνος ελέγχου" -belowThreshold = "Κάτω από το όριο" -destination = "Προορισμός" -frameworks = "Πλαίσια" -immutableHold = "Αμετάβλητη διατήρηση" -keepFor = "Διατήρηση για" -minConfidence = "Ελάχιστη εμπιστοσύνη" -notify = "Ειδοποίηση κατά τη δρομολόγηση" -onViolation = "Όταν δεν συμμορφώνεται" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Προσθήκη" -addPlaceholder = "Προσθήκη ετικέτας…" -customNote = "Προσαρμοσμένο για την ομάδα σας." -defaultNote = "Χρήση της ενσωματωμένης προεπιλογής, κοινόχρηστης με την ομάδα σας." -duplicate = "\"{{name}}\" υπάρχει ήδη." -edit = "Επεξεργασία ετικετών" -empty = "Δεν υπάρχουν ακόμα ετικέτες." -export = "Εξαγωγή JSON" -iconAria = "Επιλέξτε εικονίδιο για {{name}}" -import = "Εισαγωγή JSON" -importError = "Δεν ήταν δυνατή η εισαγωγή αυτού του αρχείου." -managedNote = "Οι ετικέτες ομάδας διαχειρίζονται από τον επικεφαλής της ομάδας σας." -modalSubtitle = "Κοινόχρηστο με ολόκληρη την ομάδα σας. Ο ταξινομητής επιλέγει τις ετικέτες που ταιριάζουν σε κάθε έγγραφο." -modalTitle = "Ετικέτες ταξινόμησης" -pickIcon = "Χρήση εικονιδίου {{label}}" -removeAria = "Αφαίρεση {{name}}" -resetToDefault = "Επαναφορά στην προεπιλογή" -saveForTeam = "Αποθήκευση για την ομάδα" -saving = "Αποθήκευση…" -sectionLabel = "Ετικέτες ταξινόμησης" -startFromScratch = "Έναρξη από την αρχή" -teamCount = "ετικέτες ομάδας" -tooLong = "Οι ετικέτες μπορούν να έχουν το πολύ {{max}} χαρακτήρες." -ungrouped = "Μη ομαδοποιημένα" -view = "Προβολή ετικετών" +categoryCount = "κατηγορίες" +hideCategory = "Απόκρυψη κατηγορίας" +labelCount = "ετικέτες" +sharedNote = "Αυτές οι ετικέτες είναι ενσωματωμένες και κοινές σε όλη την ομάδα σας." +showCategory = "Εμφάνιση κατηγορίας" [policies.pii] account = "Αριθμοί λογαριασμών (με ετικέτα)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Αριθμοί δρομολόγησης ΗΠΑ (ABA)" ssn = "Αριθμοί κοινωνικής ασφάλισης" -[policies.settings] -noneExport = "Καμία πολιτική δεν εκτελείται επί του παρόντος κατά την εξαγωγή." -noneUpload = "Καμία πολιτική δεν εκτελείται επί του παρόντος κατά τη μεταφόρτωση." -onExport = "Κατά την εξαγωγή" -onUpload = "Κατά τη μεταφόρτωση" -reorderHandle = "Σύρετε για αναδιάταξη" -runOrderDesc = "Όταν περισσότερες από μία πολιτικές εκτελούνται στο ίδιο έναυσμα, εκτελούνται με αυτήν τη σειρά — καθεμία στο αποτέλεσμα της προηγούμενης πολιτικής. Σύρετε για αναδιάταξη." -title = "Ρυθμίσεις πολιτικής" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Επιλογές πολιτικής" -policySettings = "Ρυθμίσεις πολιτικής" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Επανάληψη αποτυχημένων πολιτικών ({{count}})" -rowProgress = "{{completed}} από {{total}} αρχεία επεξεργάστηκαν" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Τι είναι μια πολιτική;" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Εντοπίζει αυτόματα και μαυρίζει ευαίσθητα στοιχεία — όπως αριθμούς κοινωνικής ασφάλισης και καρτών — ώστε να μην μπορούν να διαβαστούν στο έγγραφο." -sanitize = "Αφαιρεί κρυφό JavaScript από το αρχείο, ώστε να μην εκτελείται αυτόματα τίποτα όταν κάποιος το ανοίγει." -watermark = "Τοποθετεί ορατή σήμανση (π.χ. \"Εμπιστευτικό\") σε κάθε σελίδα." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Κλείσιμο" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} σε επιβολή..." badgeRan = "Η πολιτική {{name}} εκτελέστηκε σε αυτό το αρχείο" +badgeRunning = "{{name}} εκτελείται..." blockingAction = "Η ενέργεια {{action}} αποκλείστηκε κατά την επιβολή πολιτικής, περιμένετε..." dismiss = "Απόρριψη επικάλυψης" enforcingTitle = "Επιβολή πολιτικής..." @@ -6151,6 +6316,7 @@ viewAnyway = "Προβολή αρχείου (η πολιτική εξακολο [policyOption] autoRedactPhi = "Αυτόματη απόκρυψη PHI" blockExport = "Αποκλεισμός εξαγωγής" +ccpa = "CCPA" contracts = "Συμβάσεις" documents = "Έγγραφα" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Πλάνο Editor" subscribed = "Πλάνο Processor" unlinked = "Μη συνδεδεμένο" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap από έγγραφο" -sectionsAriaLabel = "Ενότητες δημιουργού πρακτόρων" -selectorAriaLabel = "Πράκτορες" -subtitle = "Σχεδιάστε, δοκιμάστε και διαθέστε τους πράκτορες AI που ταξινομούν, εξάγουν από και δρομολογούν τα έγγραφά σας. Ορίστε σενάρια, περιορίστε την πρόσβαση σε εργαλεία, εκτελέστε ένα golden set και δημοσιεύστε μια έκδοση." -title = "Δημιουργός πρακτόρων" - -[portal.agentBuilder.bootstrap] -cancel = "Ακύρωση" -dropzoneText = "Επιλέξτε ένα δείγμα εγγράφου (PDF ή εικόνα)" -lead = "Αφήστε ένα δείγμα εγγράφου και θα προτείνουμε σενάρια και ένα σχήμα εξαγωγής που μπορείτε να βελτιώσετε. Τίποτα δεν δημοσιεύεται μέχρι να το ελέγξετε." -submit = "Bootstrap πράκτορα" -subtitle = "Ξεκινήστε έναν νέο πράκτορα από ένα αντιπροσωπευτικό αρχείο" -title = "Bootstrap από έγγραφο" - -[portal.agentBuilder.empty] -description = "Κάντε bootstrap ενός πράκτορα από ένα δείγμα εγγράφου για να ξεκινήσετε τα σενάρια και το σχήμα εξαγωγής του και, στη συνέχεια, βελτιώστε και δημοσιεύστε." -title = "Δεν υπάρχουν ακόμα πράκτορες" - -[portal.agentBuilder.evals] -casesPassing = "Περιπτώσεις που περνούν" -columnCase = "Περίπτωση eval" -columnLatency = "Καθυστέρηση" -columnResult = "Αποτέλεσμα" -fail = "αποτυχία" -goldenSetPassRate = "Ποσοστό επιτυχίας golden-set" -latencyMs = "{{ms}} ms" -notRun = "δεν εκτελέστηκε" -pass = "επιτυχία" -passRate = "Ποσοστό επιτυχίας" -runEvals = "Εκτέλεση evals" - -[portal.agentBuilder.evals.empty] -description = "Τα evals μετατρέπουν τα σενάριά σας σε ένα επαναλήψιμο golden set. Αναβαθμίστε για να καταγράφετε το ποσοστό επιτυχίας με την πάροδο του χρόνου και να ελέγχετε τις δημοσιεύσεις βάσει αυτού." -title = "Δεν υπάρχει ακόμα golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "σε όλα τα golden sets" -activeAgents = "Ενεργοί πράκτορες" -avgPassRate = "Μέσο ποσοστό επιτυχίας eval" -fleetWide = "σε όλο τον στόλο" -latestPublished = "Τελευταία δημοσιευμένη" -scenarios = "Σενάρια" -testCases = "περιπτώσεις δοκιμών" -totalDescription_one = "{{count}} συνολικά" -totalDescription_other = "{{count}} συνολικά" - -[portal.agentBuilder.scenarios] -add = "Προσθήκη" -addScenario = "Προσθήκη σεναρίου" -enable = "Ενεργοποίηση" -expectationLabel = "Αναμενόμενη συμπεριφορά" -expectationPlaceholder = "Τι πρέπει να κάνει ο πράκτορας" -inEval = "σε eval" -mute = "Σίγαση" -muted = "σε σίγαση" -nameLabel = "Όνομα" -namePlaceholder = "π.χ. Κλιμάκωση συμμόρφωσης" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Σενάρια" -tools = "Εργαλεία" -versions = "Εκδόσεις" - -[portal.agentBuilder.tools] -broadAccess = "Ευρεία πρόσβαση" -deniedHint = "Τα επιλεγμένα εργαλεία είναι αποκλεισμένα. Όλα τα υπόλοιπα παραμένουν διαθέσιμα για κλήση." -deniedTools = "Αποκλεισμένα εργαλεία" -governanceGate = "Η διακυβέρνηση εργαλείων είναι διαθέσιμη στο πλάνο Enterprise." -restricted = "Περιορισμένο" -restrictedAccess = "Περιορισμένη πρόσβαση σε εργαλεία" -restrictedDescription = "Επιτρέψτε κάθε εργαλείο εκτός από αυτά που απορρίπτετε παρακάτω." - -[portal.agentBuilder.versions] -current = "τρέχουσα" -historyGate = "Το πλήρες ιστορικό εκδόσεων και η επαναφορά είναι διαθέσιμα στο πλάνο Enterprise." -publish = "Δημοσίευση" -rollBack = "Επαναφορά" - [portal.assistant] close = "Κλείσιμο βοηθού" error = "Δεν ήταν δυνατή η επικοινωνία με τον βοηθό." @@ -6365,9 +6451,21 @@ typing = "Πληκτρολογεί" loading = "Φόρτωση" redirectingToEditor = "Ανακατεύθυνση στον editor..." +[portal.billing.activation] +maybeLater = "Ίσως αργότερα" +title = "Ενεργοποίηση του Processor" + +[portal.billing.activation.payg] +sub = "Ορίστε μηνιαίο όριο. Πληρώνετε για ό,τι εκτελείτε." +title = "Πληρωμή ανά χρήση" + +[portal.billing.activation.prepay] +badge = "2 μήνες δωρεάν" +sub = "Ένα τιμολόγιο, κάρτα ή τραπεζικό έμβασμα." +title = "Προπληρώστε ένα έτος" + [portal.billing.checkout] noClientSecret = "Η edge function δεν επέστρεψε client_secret." -subtitle = "Προσθέστε μια κάρτα για να συνεχίσετε πέρα από τη δωρεάν παροχή του πλάνου Editor. Το Stripe αναλαμβάνει τα υπόλοιπα." title = "Ενεργοποίηση του πλάνου Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Η πληρωμή σας πέτυχε, αλλά η ενεργοποίη close = "Κλείσιμο" title = "Σχεδόν έτοιμο" +[portal.billing.checkout.cap] +amountAria = "Μηνιαίο όριο δαπανών" +back = "Πίσω" +continue = "Συνέχεια στην πληρωμή" +error = "Δεν ήταν δυνατό να οριστεί το όριο δαπανών σας" +finePrint = "Τα τιμολόγια εκδίδονται μηνιαία. Ακύρωση οποιαδήποτε στιγμή." +noLimit = "Χωρίς όριο" +note = "Δεν χρεώνεστε ποτέ πέρα από το όριό σας. Η επεξεργασία απλώς παύει." +perMonth = "/ μήνα" +title = "Ορίστε το όριο δαπανών σας" + +[portal.billing.checkout.card] +fields = "Αριθμός κάρτας · MM / YY · CVC · ZIP" +label = "Στοιχεία κάρτας" +note = "Τα στοιχεία κάρτας συλλέγονται από το Stripe. Το Stirling δεν αποθηκεύει ποτέ PAN ή CVC." + [portal.billing.checkout.error] title = "Δεν ήταν δυνατή η έναρξη του checkout" @@ -6383,10 +6497,9 @@ body = "Η πληρωμή σας ολοκληρώθηκε. Ενεργοποιο hint = "Κρατήστε αυτό το παράθυρο ανοιχτό." title = "Ενεργοποίηση του πλάνου Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "στο portal env για να ενεργοποιήσετε το checkout εντός εφαρμογής." -bodyBefore = "Ορίστε" -title = "Το Stripe δεν έχει διαμορφωθεί" +[portal.billing.checkout.pay] +limit = "Μηνιαίο όριο δαπανών {{amount}}/μήνα. Η επεξεργασία παύει στο όριο." +limitNoCap = "Χωρίς όριο δαπανών. Η επεξεργασία δεν θα παύει. Τα τιμολόγια εκδίδονται μηνιαία." [portal.billing.enterpriseUpsell] cta = "Δημιουργήστε την προσφορά σας για Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Τρέχον πλάνο" freeForever = "Δωρεάν για πάντα" noTeamResolved = "Δεν έχει επιλυθεί ακόμα ομάδα στο πορτοφόλι σας — ανανεώστε και δοκιμάστε ξανά." ownerOnly = "Μόνο ο κάτοχος της ομάδας μπορεί να ενεργοποιήσει το πλάνο Processor." +payInvoice = "Πληρώστε το τιμολόγιο για ολοκλήρωση" planName = "Editor" ssoIncluded = "SSO περιλαμβάνεται" switchOnProcessor = "Ενεργοποίηση Processor →" unlimitedUsers = "Απεριόριστοι χρήστες" +viewQuote = "Προβολή προσφοράς" [portal.billing.invoices] columnAmount = "Ποσό" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Άμεσα API αιτήματα" segmentApiLabel = "API" segmentAutomationDesc = "Αυτοματισμοί και pipelines" segmentAutomationLabel = "Αυτοματισμός" +sizeMultiplier = "{{formatted}} PDFs χρησιμοποίησαν πολλαπλασιαστή μεγέθους" +summary = "{{unique}} μοναδικά · {{units}} μονάδες μέτρησης · {{avg}} μέσος όρος ανά PDF" +summaryNoRate = "{{unique}} μοναδικά · {{units}} μονάδες μέτρησης" unit = "μετρημένα PDFs" +unitsPending_one = "{{units}} μονάδα μέτρησης εκκρεμεί για συγχρονισμό από συνδεδεμένες παρουσίες" +unitsPending_other = "{{units}} μονάδες μέτρησης εκκρεμούν για συγχρονισμό από συνδεδεμένες παρουσίες" + +[portal.billing.prepaid] +capSuffix = "από {{total}} προπληρωμένες πιστώσεις" +expires = "Λήγει στις {{date}}" +eyebrow = "Προπληρωμένη χωρητικότητα" +note = "Χρησιμοποιείται πριν από τη χρέωση βάσει χρήσης και εκτός του ορίου δαπανών σας." +topUp = "Συμπλήρωση" + +[portal.billing.prepaid.buy] +back = "Πίσω" +calcTitle = "Υπολογίστε την ετήσια πληρωμή σας" +cancel = "Ακύρωση" +cancelPurchase = "Ακύρωση αγοράς" +close = "Κλείσιμο" +continueToPayment = "Συνέχεια στην πληρωμή" +finalise = "Οριστικοποίηση" +invoiceErrorTitle = "Δεν ήταν δυνατή η δημιουργία του τιμολογίου" +loadingTitle = "Φόρτωση της προσφοράς σας" +payTitle = "Πληρώστε για το έτος σας" +step = "Βήμα {{current}} από {{total}}" + +[portal.billing.prepaid.calc] +change = "Αλλαγή" +deployRow = "Ανάπτυξη" +done = "Τέλος" +enterpriseHint = "Αυτό είναι επίπεδο enterprise. Σε αυτόν τον όγκο, οι enterprise τιμές ξεπερνούν οποιαδήποτε έκπτωση self-serve. Κλείδωμα τιμής, όροι και προσφορά σε λίγα λεπτά." +governanceRow = "Διακυβέρνηση" +governanceValue = "{{label}} · {{count}} πολιτικές" +handlesLabel = "Ο Processor σας" +handlesValue = "διαχειρίζεται {{volume}} πιστώσεις / μήνα" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} πολιτικές" +poolCaption = "Μία δεξαμενή {{credits}} πιστώσεων για το έτος. Οι βαριοί μήνες δανείζονται από τους ελαφριούς." +priceLabel = "Το έτος σας · 12 μήνες στην τιμή των 10" +rateUnknown = "Θα εμφανίσουμε την ακριβή τιμή στο checkout." +savings = "Εξοικονομείτε {{amount}} · 2 μήνες δωρεάν." +sizingRow = "Μέγεθος" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Σύνολο χρηστών" + +[portal.billing.prepaid.consent] +label = "Κατανοώ ότι όταν η προπληρωμένη χωρητικότητά μου εξαντληθεί ή λήξει μετά από 12 μήνες, η επεξεργασία συνεχίζεται αυτόματα με την τυπική μετρημένη τιμή πληρωμής ανά χρήση (έως το όριο δαπανών μου) εκτός αν ακυρώσω, και ότι μπορώ να ακυρώσω οποιαδήποτε στιγμή από την πύλη χρεώσεων." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Διαχειρίζεται από το Stirling. Ζωντανό σε λίγα λεπτά." +cloudValue = "Stirling Cloud · Διαχειριζόμενο" +selfhost = "Self-hosted" +selfhostDesc = "Εκτελείται σε ιδιωτική υποδομή. Ίδια τιμή." +selfhostValue = "Self-hosted · Ιδιωτική υποδομή" + +[portal.billing.prepaid.offer] +cta = "Έλεγχος προσφοράς" +subtitle = "Προπληρώστε ένα έτος επεξεργασίας PDF και λάβετε δύο μήνες δωρεάν, που χρησιμοποιούνται πριν από τη χρέωση βάσει χρήσης, εκτός του ορίου δαπανών σας." +title = "Πάρτε 12 μήνες στην τιμή των 10" + +[portal.billing.prepaid.pay] +accountName = "Όνομα κατόχου λογαριασμού" +accountNamePlaceholder = "π.χ. Jane Smith" +companyName = "Όνομα εταιρείας (προαιρετικό)" +companyNamePlaceholder = "π.χ. Acme Inc." +downloadInvoice = "Λήψη τιμολογίου" +payOnline = "Πληρωμή online" +poNumber = "Αριθμός PO (προαιρετικό)" +poNumberPlaceholder = "π.χ. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Προχωρημένο" +advancedDesc = "Τα pipelines οδηγούν το μεγαλύτερο μέρος της επεξεργασίας σας." +none = "Κανένα" +noneDesc = "Δεν εκτελείτε pipelines ακόμη. Ενεργοποιήστε τα οποιαδήποτε στιγμή." +standard = "Τυπικό" +standardDesc = "Μερικά pipelines επανεπεξεργάζονται τα εισερχόμενα PDFs." + +[portal.billing.prepaid.posture] +essentials = "Βασικά" +essentialsDesc = "Ταξινόμηση, η προεπιλογή, συν Κοινή χρήση" +governed = "Με διακυβέρνηση" +governedDesc = "Προσθέτει Ασφάλεια και Δρομολόγηση" +regulated = "Ρυθμιζόμενο" +regulatedDesc = "Κάθε κατηγορία, συμπ. Συμμόρφωση, Διατήρηση, Ingestion" + +[portal.billing.prepaid.review] +download = "Λήψη προσφοράς (PDF)" +downloading = "Προετοιμασία…" +share = "κοινή χρήση για έγκριση" + +[portal.billing.prepaid.size] +compact = "Συμπαγές" +compactDesc = "Αρχεία κάτω από 25 MB, χωρίς χρεώσεις δεδομένων" +heavy = "Βαρύ" +heavyDesc = "Σαρωμένα ή με πολλές εικόνες, συνήθως 50 MB+" +standard = "Τυπικό" +standardDesc = "Κυρίως μικρά, μερικές σαρώσεις πάνω από 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Εξαντλήθηκε" +healthy = "Απομένουν πολλά" +low = "Εξαντλείται" [portal.billing.spendLimit] adjustLimit = "Προσαρμογή ορίου" @@ -6494,6 +6713,8 @@ label = "Προβλέπεται υπέρβαση." [portal.billing.spendThisMonth] eyebrow = "Δαπάνη αυτόν τον μήνα" +freeRemaining_one = "Απομένει {{formatted}} δωρεάν PDF" +freeRemaining_other = "Απομένουν {{formatted}} δωρεάν PDFs" processed_one = "{{formattedCount}} PDF επεξεργάστηκε." processed_other = "{{formattedCount}} PDFs επεξεργάστηκαν." processedWithRate_one = "{{formattedCount}} PDF επεξεργάστηκε, με {{rate}} το καθένα." @@ -6510,6 +6731,7 @@ reachedTitle = "Το μηνιαίο όριο δαπανών επιτεύχθηκ title = "Δεν ήταν δυνατό το άνοιγμα του portal του Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "από {{allowance}} δωρεάν PDFs χρησιμοποιήθηκε" capSuffix_other = "από {{allowance}} δωρεάν PDFs χρησιμοποιήθηκαν" eyebrow = "Δοκιμή Processor" @@ -6534,6 +6756,376 @@ signature = "υπογραφή" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Νέα σύνδεση" +createTitleFor = "Σύνδεση {{name}}" +delete = "Διαγραφή" +edit = "Επεξεργασία" +editTitle = "Επεξεργασία σύνδεσης" +unsupportedType = "Αυτός ο τύπος σύνδεσης δεν μπορεί να επεξεργαστεί εδώ." + +[portal.connections.categories.advanced] +description = "Για μια υπηρεσία που δεν υποστηρίζουμε ακόμη." +label = "Προχωρημένα" + +[portal.connections.categories.audit] +description = "Καταγράψτε τι συνέβη σε ένα έγγραφο, εκεί όπου ήδη κοιτούν οι ελεγκτές σας." +label = "Καταγραφή ελέγχου & συμμόρφωσης" + +[portal.connections.categories.notify] +description = "Ενημερώστε ένα άλλο σύστημα ότι κάτι συνέβη." +label = "Ειδοποίηση & ροή εργασίας" + +[portal.connections.categories.security] +description = "Σαρώστε ή επισημάνετε ένα έγγραφο πριν προχωρήσει." +label = "Ασφάλεια & ταξινόμηση" + +[portal.connections.categories.signing] +description = "Στείλτε ένα έγγραφο για υπογραφή ή συμβολαιογραφική επικύρωση." +label = "Ηλεκτρονική υπογραφή & συμβολαιογραφική επικύρωση" + +[portal.connections.categories.storage] +description = "Πού καταλήγουν τα επεξεργασμένα έγγραφα - ένα bucket, ένα drive, ένα ticket ή μια σελίδα." +label = "Αρχείο & επισύναψη" + +[portal.connections.commonFields.accessToken] +helperText = "Το Stirling το αποθηκεύει κρυπτογραφημένο και δεν το εμφανίζει ποτέ ξανά." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Τα βήματα που χρησιμοποιούν αυτή τη σύνδεση μπορούν να καλούν μόνο διαδρομές κάτω από αυτό το URL, ποτέ άλλο host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Κωδικός πρόσβασης" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Όνομα χρήστη" + +[portal.connections.commonFields.webhookUrl] +helperText = "Επικολλήστε το URL που σας έδωσε η υπηρεσία. Μόνο διαδρομές κάτω από αυτό μπορούν να καλούνται." +label = "Webhook URL" + +[portal.connections.fields] +name = "Όνομα σύνδεσης" +namePlaceholder = "π.χ. Παραγωγή" + +[portal.connections.picker] +cancel = "Ακύρωση" +createNew = "Νέα σύνδεση..." +placeholder = "Επιλέξτε σύνδεση" +save = "Αποθήκευση σύνδεσης" + +[portal.connections.picker2] +back = "Αλλαγή ενσωμάτωσης" +heading = "Τι θέλετε να συνδέσετε;" +noResultsBody = "Δεν το υποστηρίζουμε ακόμη - πείτε μας γι' αυτό και θα εξετάσουμε την προσθήκη του." +noResultsTitle = "Δεν ταιριάζει τίποτα με \"{{query}}\"" +searchPlaceholder = "Αναζήτηση ενσωματώσεων - δοκιμάστε \"sign\", \"ocr\" ή ένα όνομα προϊόντος" + +[portal.connections.types.api] +description = "Καλέστε οποιοδήποτε HTTP API από μια πολιτική. Επιλέγετε το URL, τον έλεγχο ταυτότητας και το payload." +label = "Προσαρμοσμένο API" + +[portal.connections.types.api.fields.authType] +label = "Έλεγχος ταυτότητας" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Όνομα χρήστη και κωδικός πρόσβασης" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token σε προσαρμοσμένο header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Κανένα" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Σύνδεση για token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Τα βήματα που χρησιμοποιούν αυτή τη σύνδεση μπορούν να καλούν μόνο διαδρομές κάτω από αυτό το URL, ποτέ άλλο host." +label = "Βασικό URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Όνομα header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Διαδρομή σύνδεσης" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Κωδικός πρόσβασης" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Αν το API απαντήσει με σύνδεσμο λήψης, μόνο αυτοί οι hosts μπορούν να ανακτηθούν. Διαχωρίστε με κόμματα. Αφήστε κενό για να επιτρέπεται μόνο ο ίδιος host του βασικού URL." +label = "Επιτρεπόμενοι hosts αποτελεσμάτων" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Στείλτε το token ως αυτό το header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Το response header στο οποίο η κλήση σύνδεσης επιστρέφει το token, π.χ. X-Auth-Token." +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "Όνομα χρήστη" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Σάρωση εγγράφων για malware σε server που εκτελείτε - τίποτα δεν φεύγει από το δίκτυό σας." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Σάρωση ιών και περιεχομένου για έγγραφα σε μια πολιτική." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Αποκλεισμός εγγράφων που περιέχουν macros, εκτελέσιμα, scripts ή ενσωματωμένα αντικείμενα - όχι μόνο γνωστό malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Επισύναψη επεξεργασμένων εγγράφων σε σελίδα Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Στείλτε έγγραφα στο ConsignO Cloud (Notarius) για ηλεκτρονική υπογραφή και συμβολαιογραφική επικύρωση." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Το περιβάλλον ConsignO Cloud σας, συμπεριλαμβανομένου του /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Κωδικός πρόσβασης" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Απαιτείται μόνο αν ο οργανισμός σας χρησιμοποιεί tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Όνομα χρήστη" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Δημοσίευση μηνύματος στο Discord όταν μια πολιτική επεξεργάζεται ένα έγγραφο." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Ευρετηρίαση audit event στο Elasticsearch κάθε φορά που μια πολιτική χειρίζεται ένα έγγραφο." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Έλεγχος ενός FTP ή FTPS server για νέα έγγραφα." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Λειτουργία σύνδεσης" + +[portal.connections.types.ftp.fields.passive.options] +active = "Ενεργή" +passive = "Παθητική (συνιστάται)" + +[portal.connections.types.ftp.fields.security] +helperText = "Το απλό FTP στέλνει διαπιστευτήρια και αρχεία χωρίς κρυπτογράφηση· επιλέξτε FTPS αν το υποστηρίζει ο server." +label = "Κρυπτογράφηση" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Κανένα (απλό FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Δημοσίευση μηνύματος σε χώρο Google Chat όταν εκτελείται μια πολιτική." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Επισύναψη επεξεργασμένων εγγράφων σε ζήτημα Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Αποστολή email με έγγραφο ή ειδοποίηση όταν εκτελείται μια πολιτική." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Αρχειοθέτηση επεξεργασμένων εγγράφων στο Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Κωδικός πρόσβασης εφαρμογής" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Ανίχνευση προσωπικών δεδομένων σε κείμενο εγγράφου χρησιμοποιώντας έναν Presidio server που εκτελείτε." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Ανάγνωση και εφαρμογή ετικετών ευαισθησίας Microsoft Purview σε PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Προαιρετικό. Απαιτείται μόνο για επιλογή ετικετών με βάση το όνομα· η επισήμανση λειτουργεί χωρίς αυτό." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Σφραγίζεται σε κάθε ετικέτα ως SiteId, ώστε τα εργαλεία που γνωρίζουν το Purview να ξέρουν ποιος οργανισμός την εφάρμοσε." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Ένα S3 bucket από το οποίο οι πηγές μπορούν να διαβάζουν και στο οποίο οι έξοδοι pipeline μπορούν να γράφουν." +label = "Αποθήκευση S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Αφήστε κενό για AWS. Ορίστε το για MinIO ή άλλο αποθηκευτικό χώρο συμβατό με S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Αποστολή email με έγγραφο ή ειδοποίηση όταν εκτελείται μια πολιτική." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Έλεγχος ενός SFTP (SSH) server για νέα έγγραφα." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Προαιρετικό. Το SHA-256 host key fingerprint του server (από `ssh-keyscan host | ssh-keygen -lf -`). Όταν οριστεί, γίνεται αποδεκτός μόνο server που παρουσιάζει αυτό το key· όταν είναι κενό, το key που εμφανίζεται στην πρώτη σύνδεση καρφιτσώνεται και μεταγενέστερες αλλαγές απορρίπτονται." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Αφήστε κενό αν κάνετε έλεγχο ταυτότητας με private key αντί για κωδικό πρόσβασης." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Προαιρετικό. Επικολλήστε ένα OpenSSH private key για έλεγχο ταυτότητας με key αντί για κωδικό πρόσβασης." +label = "Private key" + +[portal.connections.types.slack] +description = "Δημοσίευση μηνύματος στο Slack όταν μια πολιτική επεξεργάζεται ένα έγγραφο." +label = "Slack" + +[portal.connections.types.smb] +description = "Παρακολούθηση ενός SMB / CIFS network share (Windows ή Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Προαιρετικό. Το Windows domain ή workgroup, αν ο server σας χρησιμοποιεί κάποιο." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Αποστολή audit event στο Splunk κάθε φορά που μια πολιτική χειρίζεται ένα έγγραφο." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Αποστολή audit event σε Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Δημοσίευση μηνύματος σε κανάλι Teams όταν εκτελείται μια πολιτική." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Δημοσίευση κάθε εγγράφου, ή μόνο των λεπτομερειών, σε URL που επιλέγετε." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Ενεργοποίηση Zap ή Make scenario από μια πολιτική." +label = "Zapier & Make" + +[portal.docs] +browse = "Περιήγηση στα docs" +viewSource = "Προβολή πηγαίου κώδικα στο GitHub" + [portal.docs.authentication] codeCaption = "κάθε αίτημα" eyebrow = "ΞΕΚΙΝΩΝΤΑΣ" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Συσκευασμένες, ονομασμένες δυνατότητες που ο πράκτοράς σας καλεί ως ένα ενιαίο εργαλείο. Κάθε skill είναι μια ντετερμινιστική αλυσίδα λειτουργιών με συνδεδεμένα evals." title = "Agent skills" +[portal.docs.toc] +title = "Σε αυτήν τη σελίδα" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Τύπος" user = "Χρήστης" [portal.documents.queue.empty] +connectSource = "Σύνδεση πηγής" +createPipeline = "Δημιουργία pipeline" description = "Καθώς οι πηγές τροφοδοτούν έγγραφα στις ροές εργασίας σας, θα εμφανίζονται εδώ για έλεγχο." title = "Δεν υπάρχουν έγγραφα στην ουρά" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Κανένα έγγραφο δεν αντιστοιχεί σε αυτό το φίλτρο." rowActions = "Ενέργειες γραμμής" sensitiveLabel = "Ευαίσθητο" -sensitiveTitle = "Ευαίσθητο — απαιτείται πρόσβαση" [portal.documents.table.columns] action = "Ροή εργασίας / Ενέργεια" document = "Έγγραφο" +labels = "Labels" product = "Προϊόν" status = "Κατάσταση" time = "Ώρα" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Τελευταία εμφάνιση" region = "Περιοχή" status = "Κατάσταση" +target = "Target" version = "Έκδοση" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Αυτή η προβολή αντιμετώπισε ένα απρ retry = "Δοκιμάστε ξανά" title = "Κάτι πήγε στραβά σε αυτήν τη σελίδα" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Πίσω" +body = "Κατεβάστε την εφαρμογή desktop ή φιλοξενήστε μόνοι σας τον server." +done = "Τέλος" +guide = "Άνοιγμα του οδηγού {{name}}" +openInBrowser = "Άνοιγμα στο browser αντί γι' αυτό" +sectionDesktop = "Εφαρμογή desktop" +sectionSelfHosted = "Self-hosted" +title = "Εγκατάσταση του Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Εκτελέστε την επίσημη image στον server σας." +note = "Ανοίγει στο :8080. Προεπιλεγμένη σύνδεση: admin / stirling." +tagline = "Εκτελέστε τον server σε container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Εγκαταστήστε το επίσημο Helm chart." +note = "Προσθέστε ένα PVC για το /configs ώστε να διατηρούνται οι ρυθμίσεις." +tagline = "Αναπτύξτε σε cluster με Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native builds για τη διανομή σας." +guideBtn = "Προβολή επιλογών εγκατάστασης Linux" +note = "Διατίθενται AppImage, .deb και .rpm builds για κοινές διανομές." +tagline = "Native εφαρμογή desktop για τη διανομή σας" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ή μέσω Homebrew" +detailBody = "Εκτελέστε το πρόγραμμα εγκατάστασης ή χρησιμοποιήστε Homebrew." +downloadBtn = "Λήψη για macOS" +tagline = "Native εφαρμογή για Apple silicon και Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Εκτελέστε το .jar σε bare metal server." +downloadBtn = "Λήψη του .jar" +note = "Απαιτεί Java 21+. Ανοίγει στο :8080· προεπιλεγμένη σύνδεση admin / stirling." +runLabel = "Έπειτα εκτελέστε το" +tagline = "Εκτελέστε το .jar σε bare metal server" +title = "Χειροκίνητη ρύθμιση server" + +[portal.home.download.windows] +altLabel = "Ή μέσω winget" +detailBody = "Εκτελέστε το πρόγραμμα εγκατάστασης ή χρησιμοποιήστε package manager." +downloadBtn = "Λήψη προγράμματος εγκατάστασης" +tagline = "Native εφαρμογή για Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} ενεργοί" -invite = "Προσκαλέστε συνεργάτες" +activeOfDeployed = "{{active}} από {{total}} ενεργά αυτόν τον μήνα" name = "Stirling PDF Editor" open = "Άνοιγμα σε browser" updated = "ενημερώθηκε {{time}}" +[portal.home.editor.deploy] +finish = "Ολοκλήρωση ανάπτυξης" +options = "Προβολή επιλογών ανάπτυξης" +start = "Ανάπτυξη του Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Καλό απόγευμα" evening = "Καλό βράδυ" morning = "Καλημέρα" -[portal.home.onboarding] -dismiss = "Παράβλεψη ρύθμισης" -notStarted = "Δεν ξεκίνησε" -progress = "{{done}} από {{total}} ολοκληρώθηκαν" -title = "Ολοκληρώστε τη ρύθμιση" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC σε επίπεδο οργανισμού, δεσμευμένη τιμολόγηση όγκου και air-gapped ανάπτυξη." -cta = "Έναρξη δοκιμής" -ctaQuote = "Λήψη προσφοράς" -lead = "Για 250+ εργαζομένους." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Αναπτύξτε την εφαρμογή desktop, δωρεάν για πάντα." -title = "Λήψη του PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} ενεργές · {{recommended}} προτεινόμενες" -chip = "{{active}} ενεργές" -title = "Ενεργοποίηση πολιτικών" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} συνδεδεμένες · κάθε PDF ελεγχόμενο όπου κι αν καταλήγει" -chip = "{{connected}} συνδεδεμένες" -title = "Συνδέστε τις πηγές σας" - -[portal.home.quickActions] -subtitle = "Κορυφαίες εργασίες για σήμερα" -title = "Γρήγορες ενέργειες" - -[portal.home.quickActions.buildPipeline] -blurb = "Συνθέτης 3 βημάτων πάνω στη βιβλιοθήκη typed op" -title = "Δημιουργία ροής εργασίας" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, παρακολουθούμενοι φάκελοι" -title = "Σύνδεση πηγής" - -[portal.home.quickActions.issueApiKey] -blurb = "Scoped key με όρια ρυθμού και IP allowlist" -title = "Έκδοση API key" - [portal.infrastructure] manageEditorDeployment = "Διαχείριση ανάπτυξης Editor" sectionsAriaLabel = "Ενότητες υποδομής" @@ -6878,13 +7541,9 @@ heading = "API keys" subheading = "Scoped διαπιστευτήρια με όρια ρυθμού ανά key, δικαιώματα και IP allowlists." [portal.infrastructure.apiKeys.card] -allowedIps = "Επιτρεπόμενες IPs" -anyIp = "Οποιαδήποτε IP (χωρίς allowlist)" created = "Δημιουργήθηκε" lastUsed = "Τελευταία χρήση" -permissions = "Δικαιώματα" -rateLimit = "Όριο ρυθμού" -rateLimitValue = "{{value}} αιτ./λεπτό" +revoke = "Ανάκληση κλειδιού" usageMonth = "Χρήση αυτόν τον μήνα" usageToday = "Χρήση σήμερα" @@ -6892,6 +7551,15 @@ usageToday = "Χρήση σήμερα" description = "Δημιουργήστε ένα scoped key για να αρχίσετε να καλείτε το Stirling API." title = "Δεν υπάρχουν API keys ακόμα" +[portal.infrastructure.apiKeys.error] +load = "Δεν ήταν δυνατή η φόρτωση των API κλειδιών σας. Δοκιμάστε ξανά." + +[portal.infrastructure.apiKeys.revoke] +body = "Να ανακληθεί το \"{{name}}\"; Οποιαδήποτε ενσωμάτωση εξακολουθεί να χρησιμοποιεί αυτό το κλειδί θα αρχίσει αμέσως να λαμβάνει σφάλματα 401. Αυτό δεν μπορεί να αναιρεθεί." +cancel = "Ακύρωση" +confirm = "Ανάκληση κλειδιού" +title = "Ανάκληση API κλειδιού" + [portal.infrastructure.audit] filterAriaLabel = "Φιλτράρισμα συμβάντων ελέγχου κατά κατηγορία" heading = "Αρχεία καταγραφής ελέγχου" @@ -6935,6 +7603,7 @@ all = "Όλα" auth = "Auth" config = "Config" elevation = "Αναβάθμιση πρόσβασης" +policy = "Πολιτική" processing = "Επεξεργασία" security = "Ασφάλεια" @@ -6944,19 +7613,30 @@ title = "Δεν έχετε πρόσβαση" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Αναβάθμιση πρόσβασης" +policy = "Πολιτική" processing = "Επεξεργασία" totalEvents = "Συνολικά συμβάντα · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Έλεγχος ταυτότητας" +config = "Ρύθμιση" +elevation = "Αναβάθμιση δικαιωμάτων" +policy = "Πολιτική" +processing = "Επεξεργασία" +security = "Ασφάλεια" + +[portal.infrastructure.auditStatusLabel] +danger = "Σφάλμα" +info = "Πληροφορίες" +success = "Επιτυχία" +warning = "Προειδοποίηση" + [portal.infrastructure.createKey] cancel = "Ακύρωση" createKey = "Δημιουργία key" done = "Τέλος" -ipAllowlistHelper = "CIDR ranges χωρισμένα με κόμματα. Αφήστε κενό για να επιτρέψετε οποιαδήποτε IP." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Όνομα key" keyNamePlaceholder = "π.χ. Production · ingest" -permissionsLabel = "Δικαιώματα" secretKeyCaption = "Secret key" secretWarning = "Αποθηκεύστε το σε secrets manager. Το Stirling αποθηκεύει πάντα μόνο hash — δεν υπάρχει τρόπος ανάκτησής του αργότερα." subtitle = "Περιορίστε το key στο ελάχιστο που χρειάζεται. Μπορείτε να το περιστρέψετε ή να το ανακαλέσετε ανά πάσα στιγμή." @@ -6964,207 +7644,9 @@ subtitleCreated = "Αντιγράψτε αυτό το secret τώρα — δεν title = "Δημιουργία API key" titleCreated = "Το key δημιουργήθηκε" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/λεπτό" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Αναπτύχθηκε από" -environment = "Περιβάλλον" -product = "Προϊόν" -status = "Κατάσταση" -version = "Έκδοση" -when = "Πότε" - -[portal.infrastructure.deployments.recent] -heading = "Πρόσφατες αναπτύξεις" -subheading = "Τα πιο πρόσφατα rollouts σε προϊόντα και περιβάλλοντα." - -[portal.infrastructure.deployments.regionColumns] -instances = "Παρουσίες" -latency = "Καθυστέρηση" -load = "Φόρτος" -p99 = "P99" -region = "Περιοχή" -status = "Κατάσταση" -throughput = "Throughput" -uptime = "Uptime" -version = "Έκδοση" - -[portal.infrastructure.deployments.regions] -heading = "Περιοχές" -subheading = "Ζωντανή υγεία για κάθε αναπτυγμένη περιοχή Stirling — καθυστέρηση, φόρτος και έκδοση rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "Οι αναπτυγμένες περιοχές εμφανίζονται εδώ μόλις γίνει provision ο χώρος εργασίας σας." -title = "Δεν έχουν αναπτυχθεί περιοχές" - -[portal.infrastructure.models] -heading = "Μοντέλα" -msValue = "{{value}} ms" -subheading = "Ο κατάλογος μοντέλων και η δρομολόγηση που τροφοδοτούν την επεξεργασία εγγράφων σε όλο τον χώρο εργασίας σας." - -[portal.infrastructure.models.byom] -description = "Καταχωρίστε ένα on-prem ή self-hosted μοντέλο και καρφιτσώστε το σε μια περιοχή για επεξεργασία με δέσμευση παραμονής δεδομένων." -title = "Φέρτε το δικό σας μοντέλο" - -[portal.infrastructure.models.catalogue] -heading = "Κατάλογος" -sub = "Managed μοντέλα διαθέσιμα στον χώρο εργασίας σας, με ζωντανή καθυστέρηση και κόστος." -subEnterprise = "Managed, bring-your-own και on-prem μοντέλα — με διαθέσιμο καρφίτσωμα ανά περιοχή." - -[portal.infrastructure.models.catalogue.empty] -description = "Τα μοντέλα στον κατάλογο του χώρου εργασίας σας εμφανίζονται εδώ." -title = "Δεν υπάρχουν διαθέσιμα μοντέλα" - -[portal.infrastructure.models.columns] -cost = "Κόστος" -latency = "Καθυστέρηση" -load = "Φόρτος" -model = "Μοντέλο" -status = "Κατάσταση" -type = "Τύπος" -version = "Έκδοση" - -[portal.infrastructure.models.metrics] -activeModels = "Ενεργά μοντέλα" -avgLatency = "Μέση καθυστέρηση" -included = "Περιλαμβάνεται" -monthlySpend = "Μηνιαία δαπάνη μοντέλων" - -[portal.infrastructure.models.routing] -empty = "Δεν έχουν διαμορφωθεί κανόνες δρομολόγησης." -heading = "Κανόνες δρομολόγησης" -sub = "Ποιο μοντέλο χειρίζεται κάθε λειτουργία. Η προεπιλογή εφαρμόζεται όταν δεν ταιριάζει πιο συγκεκριμένος κανόνας." -subLocked = "Δρομολογήστε λειτουργίες σε συγκεκριμένα μοντέλα — διαθέσιμο σε πληρωμένα πλάνα." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Αναβαθμίστε σε Pro για να ελέγχετε ποιο μοντέλο χειρίζεται κάθε λειτουργία και τύπο εγγράφου." -title = "Η δρομολόγηση μοντέλων είναι πληρωμένη λειτουργία" - -[portal.infrastructure.models.routingColumns] -default = "Προεπιλογή" -docType = "Τύπος εγγράφου" -modelForAria = "Μοντέλο για {{operation}}" -operation = "Λειτουργία" -routedTo = "Δρομολογείται σε" - -[portal.infrastructure.security.access.byok] -description = "Παρέχετε ένα key από το δικό σας KMS. Το Stirling κρυπτογραφεί με αυτό αλλά μπορεί ακόμα να διαβάζει." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Τα keys δεν φεύγουν ποτέ από το KMS σας. Το Stirling κρατά μόνο ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Το Stirling διαχειρίζεται τα encryption keys. Το απλούστερο — μηδενικές εργασίες key από την πλευρά σας." -label = "Keys που κρατούνται από το Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Πολιτική πρόσβασης εγγράφων" -subheading = "Ελέγχει ποιος μπορεί να αποκρυπτογραφήσει επεξεργασμένα έγγραφα σε κατάσταση αδράνειας." - -[portal.infrastructure.security.attestations] -heading = "Βεβαιώσεις συμμόρφωσης" -noReport = "Δεν υπάρχει διαθέσιμη αναφορά" -subheading = "Στάση ελέγχου ανά πλαίσιο, με διαθέσιμες αναφορές για βεβαιωμένους ελέγχους." -viewReport = "Προβολή αναφοράς →" - -[portal.infrastructure.security.compliance] -heading = "Συμμόρφωση" -subheading = "Βεβαιώσεις και πιστοποιήσεις που καλύπτουν την πλατφόρμα Stirling." - -[portal.infrastructure.security.empty] -description = "Η διαμόρφωση ασφαλείας του χώρου εργασίας σας θα εμφανίζεται εδώ." -title = "Η στάση ασφαλείας δεν είναι διαθέσιμη" - -[portal.infrastructure.security.hyokBanner] -description = "Με HYOK, τα encryption keys δεν φεύγουν ποτέ από το KMS σας. Το Stirling αποθηκεύει και επεξεργάζεται μόνο ciphertext που μπορείτε να ανακαλέσετε ανά πάσα στιγμή." -title = "Το Stirling δεν μπορεί να αποκρυπτογραφήσει τα έγγραφά σας" - -[portal.infrastructure.security.ipAllowlist] -empty = "Δεν έχουν διαμορφωθεί IP ranges — επιτρέπονται όλες οι IPs." -heading = "IP allowlist" -sub = "Η πρόσβαση API περιορίζεται σε αυτά τα CIDR ranges." -subLocked = "Περιορίστε την πρόσβαση API σε γνωστά IP ranges — διαθέσιμο σε πληρωμένα πλάνα." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Αναβαθμίστε σε Pro για να περιορίσετε την πρόσβαση API σε συγκεκριμένα δίκτυα." -title = "Το IP allowlisting είναι πληρωμένη λειτουργία" - -[portal.infrastructure.security.ipColumns] -added = "Προστέθηκε" -addedBy = "Προστέθηκε από" -cidr = "CIDR" -label = "Ετικέτα" - -[portal.infrastructure.security.keyManagement] -algorithm = "Αλγόριθμος" -heading = "Διαχείριση encryption keys" -keyId = "Αναγνωριστικό key" -lastRotated = "Τελευταία περιστροφή" -rotateKey = "Περιστροφή key" -rotationPolicy = "Πολιτική περιστροφής" -subheading = "Φύλαξη των keys που κρυπτογραφούν έγγραφα σε κατάσταση αδράνειας — ποιος μπορεί να αποκρυπτογραφήσει και πώς περιστρέφονται τα keys." - -[portal.infrastructure.security.managedBanner] -description = "Η φύλαξη bring-your-own-key (BYOK) και hold-your-own-key (HYOK) είναι διαθέσιμη στο Enterprise. Αναβαθμίστε για να παρέχετε keys από το δικό σας KMS." -title = "Τα keys διαχειρίζονται από το Stirling στο πλάνο σας" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Ασία-Ειρηνικός" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "Ευρωπαϊκή Ένωση" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Ηνωμένες Πολιτείες" - -[portal.infrastructure.security.residencyHeader] -heading = "Παραμονή δεδομένων" -subheading = "Πού αποθηκεύονται και επεξεργάζονται τα έγγραφα." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} χρησιμοποιήθηκε" - -[portal.infrastructure.storage.empty] -description = "Η συνδεδεμένη αποθήκευση και η χρήση εμφανίζονται εδώ." -title = "Δεν έχει διαμορφωθεί αποθήκευση" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Ενεργό" -activeRange = "0–{{value}}d" -archived = "Αρχειοθετημένο" -coldStorage = "cold storage" -deleted = "Διαγραμμένο" -never = "ποτέ" -purged = "εκκαθαρίστηκε" - -[portal.infrastructure.storage.providers] -connect = "Σύνδεση" -connected = "Συνδεδεμένο" -heading = "Συνδεδεμένοι πάροχοι" -subheading = "Πού γράφονται τα επεξεργασμένα artifacts." - -[portal.infrastructure.storage.retention] -heading = "Διατήρηση" -subheading = "Πόσο καιρό διατηρούνται τα artifacts πριν από τη διαγραφή κύκλου ζωής." -windowLabel = "Προεπιλεγμένο παράθυρο διατήρησης" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} ημέρα" -days_other = "{{count}} ημέρες" -never = "Να μην διαγράφεται ποτέ" - -[portal.infrastructure.storage.totalUsage] -heading = "Συνολική χρήση" -progressLabel = "Χρησιμοποιημένη αποθήκευση" -subheading = "Αποθήκευση που καταναλώνεται σε όλους τους συνδεδεμένους παρόχους." +revoked = "Ανακλημένο" [portal.infrastructure.tabs] apiKeys = "API Keys" @@ -7174,6 +7656,49 @@ models = "Μοντέλα" security = "Ασφάλεια" storage = "Αποθήκευση" +[portal.integrations] +availableHeading = "Διαθέσιμες" +comingSoonHeading = "Προσεχώς" +connect = "Σύνδεση" +connectionCount_one = "{{count}} σύνδεση" +connectionCount_other = "{{count}} συνδέσεις" +customApi = "Προσαρμοσμένο API" +searchPlaceholder = "Αναζήτηση ενσωματώσεων" +subtitle = "Όλα με τα οποία μπορεί να επικοινωνήσει το Stirling. Τα διαπιστευτήρια δημιουργούνται μία φορά εδώ και στη συνέχεια αναφέρονται από πηγές, πολιτικές και pipelines." +title = "Ενσωματώσεις" +typedName = "Όνομα {{tool}}" + +[portal.integrations.filters] +all = "Όλα" +audit = "Έλεγχος" +connected = "Συνδεδεμένες" +notify = "Ειδοποίηση" +security = "Ασφάλεια" +signing = "Υπογραφή" +storage = "Αποθήκευση" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Συνδεδεμένο" + +[portal.integrations.table] +integration = "Ενσωμάτωση" +worksWith = "Λειτουργεί με" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Πολιτικές" +sources = "Πηγές" + +[portal.legal] +draft = "{{label}} · πρόχειρο" +loadError = "Δεν ήταν δυνατή η φόρτωση αυτού του εγγράφου. Δοκιμάστε ξανά." +loading = "Φόρτωση…" +title = "Νομικό έγγραφο" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7182,33 +7707,17 @@ documents = "Έγγραφα" editor = "Editor" home = "Αρχική" infrastructure = "Υποδομή" +integrations = "Ενσωματώσεις" pipelines = "Ροές εργασίας" policies = "Πολιτικές" procurement = "Προμήθειες" -settings = "Ρυθμίσεις" sources = "Πηγές" usage = "Χρήση & Χρέωση" users = "Χρήστες" -[portal.notifications] -markAllRead = "Σήμανση όλων ως αναγνωσμένων" -title = "Ειδοποιήσεις" -viewAll = "Προβολή όλων" - -[portal.notifications.ariaLabel] -none = "Ειδοποιήσεις, καμία μη αναγνωσμένη" -unread_one = "Ειδοποιήσεις, {{count}} μη αναγνωσμένη" -unread_other = "Ειδοποιήσεις, {{count}} μη αναγνωσμένες" - -[portal.notifications.count] -allRead = "όλες αναγνωσμένες" -loading = "φόρτωση" -new_one = "{{count}} νέα" -new_other = "{{count}} νέες" - -[portal.notifications.empty] -description = "Δεν υπάρχουν νέες ειδοποιήσεις." -title = "Είστε ενήμεροι" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Κάθε αυτοματοποιημένη ροή εργασίας εγγράφων στο backend: μια διατεταγμένη αλυσίδα λειτουργιών πάνω σε ένα σύνολο πηγών, που εκτελείται με trigger. Κάντε κλικ σε μια γραμμή για τα βήματα και τις πηγές της." @@ -7218,54 +7727,87 @@ title = "Ροές εργασίας" newPipeline = "Νέα ροή εργασίας" [portal.pipelines.builder] -addStep = "Προσθήκη εργαλείου" +activate = "Activate" back = "Πίσω στις ροές εργασίας" +cannotFollow = "Δεν μπορεί να δεχτεί {{produced}}" +chooseAccount = "Επιλέξτε λογαριασμό" +chooseDestination = "Επιλέξτε προορισμό" +chooseOperation = "Επιλέξτε τι κάνει αυτό το βήμα" +chooseSource = "Επιλέξτε πηγή" discard = "Απόρριψη αλλαγών" -enabled = "Ενεργό" +inputs = "Είσοδος" +inputSource = "Πηγή εισόδου" +inputTrigger = "Trigger" keepEditing = "Συνέχιση επεξεργασίας" -needsUpload = "Χρειάζεται μεταφορτωμένο αρχείο" +moreActions = "More actions" +needsConfiguring = "Χρειάζεται ρύθμιση" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Κανένα εργαλείο δεν ταιριάζει στην αναζήτησή σας." -pipelineSettings = "Ρυθμίσεις ροής εργασίας" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Αναζήτηση εργαλείων" -selectToolBody = "Προσθέστε ένα εργαλείο για να δημιουργήσετε τη ροή εργασίας σας." -selectToolTitle = "Δεν υπάρχουν εργαλεία ακόμα" -toolSettings = "Ρυθμίσεις εργαλείου" +sendToSystem = "Αποστολή σε άλλο σύστημα" +stepsIncompatible = "Αυτά τα βήματα δεν μπορούν να εκτελεστούν σε αυτό που παράγει το προηγούμενο βήμα τους: {{tools}}." +stepsNeedSetup = "Αυτά τα βήματα χρειάζονται ακόμα ρύθμιση πριν από την αποθήκευση: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Μη αναγνωρισμένη λειτουργία, διατηρήθηκε ως έχει." unsavedBody = "Έχετε μη αποθηκευμένες αλλαγές. Αποθηκεύστε τις πριν φύγετε ή απορρίψτε τις;" unsavedTitle = "Μη αποθηκευμένες αλλαγές" -uploadUnsupported = "Τα μεταφορτωμένα αρχεία δεν υποστηρίζονται ακόμα στις ροές εργασίας, επομένως αυτά τα βήματα δεν μπορούν να αποθηκευτούν: {{tools}}." usesDefaults = "Εκτελείται με προεπιλεγμένες ρυθμίσεις" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Συνδυάζει κάθε αρχείο από το προηγούμενο βήμα" +fan-out = "Εκτελείται μία φορά ανά αρχείο από το προηγούμενο βήμα" +format-mismatch = "Χρειάζεται {{accepts}}, αλλά το προηγούμενο βήμα παράγει {{produced}}" +output-uncertain = "Ενδέχεται να μην εκτελεστεί: η έξοδος του προηγούμενου βήματος εξαρτάται από τον τρόπο ρύθμισής του" +source-mismatch = "Χρειάζεται {{accepts}}, αλλά η είσοδος αυτού του pipeline είναι {{produced}}" +undeclared-operation = "Δεν μπορεί να ελεγχθεί τι δέχεται αυτό το βήμα" [portal.pipelines.composer] addTool = "Προσθήκη εργαλείου" -cancel = "Ακύρωση" -chainEmpty = "Προσθέστε ένα εργαλείο για να αρχίσετε να δημιουργείτε τη ροή εργασίας σας." create = "Δημιουργία ροής εργασίας" -directory = "Φάκελος εξόδου" -directoryHelp = "Απόλυτη διαδρομή στον server. Πρέπει να βρίσκεται μέσα στους διαμορφωμένους επιτρεπόμενους φακέλους." +createPaused = "Create paused" editingUnsupported = "Η εμφάνιση αυτών των παραμέτρων εργαλείου για επεξεργασία δεν υποστηρίζεται ακόμα." -moveDown = "Μετακίνηση κάτω" -moveUp = "Μετακίνηση πάνω" +editSource = "Edit source" name = "Όνομα" namePlaceholder = "π.χ. Redaction sweep" -noSources = "Δεν έχουν συνδεθεί πηγές ακόμα. Η ροή εργασίας μπορεί ακόμα να εκτελείται σε αρχεία που παρέχονται απευθείας σε αυτήν." noToolSettings = "Αυτό το εργαλείο δεν έχει διαμορφώσιμες ρυθμίσεις." -operations_one = "Λειτουργία ({{count}})" -operations_other = "Λειτουργίες ({{count}})" output = "Έξοδος" -removeStep = "Αφαίρεση λειτουργίας" save = "Αποθήκευση αλλαγών" scheduleEvery = "Εκτέλεση κάθε" -sources = "Πηγές" -sourcesLoading = "Φόρτωση πηγών..." trigger = "Trigger" triggerManual = "Μόνο χειροκίνητα" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "ημέρες" hours = "ώρες" minutes = "λεπτά" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Διαγραφή του \"{{name}}\"; Αυτό δεν μπορεί να αναιρεθεί." cancel = "Ακύρωση" @@ -7273,28 +7815,62 @@ confirm = "Διαγραφή" title = "Διαγραφή ροής εργασίας;" [portal.pipelines.detail] +clearHistory = "Εκκαθάριση ιστορικού" delete = "Διαγραφή ροής εργασίας" run = "Εκτέλεση τώρα" [portal.pipelines.empty] action = "Δημιουργία ροής εργασίας" +connectSource = "Σύνδεση πηγής" description = "Δημιουργήστε την πρώτη σας ροή εργασίας: επιλέξτε τις πηγές πάνω στις οποίες εκτελείται, συνδέστε τις λειτουργίες σε αλυσίδα και επιλέξτε πού πηγαίνει η έξοδος." title = "Δεν υπάρχουν ροές εργασίας ακόμα" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Ενεργές" paused = "Σε παύση" total = "Ροές εργασίας" -[portal.pipelines.output] -folder = "Εγγραφή σε φάκελο" -inline = "Επιστροφή αρχείων" - [portal.pipelines.run] +allProcessed_one = "Δεν υπάρχει τίποτα για εκτέλεση: το {{count}} έγγραφο της πηγής έχει ήδη υποβληθεί σε επεξεργασία." +allProcessed_other = "Δεν υπάρχει τίποτα για εκτέλεση: όλα τα {{count}} έγγραφα στις πηγές έχουν ήδη υποβληθεί σε επεξεργασία." completed_one = "Η εκτέλεση ολοκληρώθηκε." completed_other = "Και οι {{count}} εκτελέσεις ολοκληρώθηκαν." empty = "Δεν υπάρχει τίποτα για εκτέλεση: οι πηγές δεν είχαν έγγραφα για επεξεργασία." failed = "Η εκτέλεση απέτυχε: {{error}}" +inFlight = "Δεν υπάρχει κάτι νέο για εκτέλεση: τα έγγραφα εξακολουθούν να επεξεργάζονται από προηγούμενη εκτέλεση." +parked_one = "Δεν υπάρχει τίποτα για εκτέλεση: {{count}} έγγραφο απέτυχε προηγουμένως και έχει τεθεί σε αναμονή. Διορθώστε την αιτία και έπειτα εκκαθαρίστε το ιστορικό για να το δοκιμάσετε ξανά." +parked_other = "Δεν υπάρχει τίποτα για εκτέλεση: {{count}} έγγραφα απέτυχαν προηγουμένως και έχουν τεθεί σε αναμονή. Διορθώστε την αιτία και έπειτα εκκαθαρίστε το ιστορικό για να τα δοκιμάσετε ξανά." running = "Η εκτέλεση ξεκίνησε· βρίσκεται ακόμα σε εξέλιξη." timeout = "Η εκτέλεση διαρκεί περισσότερο από το αναμενόμενο· μπορεί ακόμα να ολοκληρωθεί στο παρασκήνιο." @@ -7307,6 +7883,7 @@ name = "Ροή εργασίας" sources = "Πηγές" status = "Κατάσταση" steps = "Βήματα" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Παρακολούθηση φακέλου" @@ -7314,12 +7891,18 @@ manual = "Χειροκίνητο" schedule = "Προγραμματισμένο" [portal.policies] +defaultName = "Πολιτική {{category}}" subtitle = "Μόνιμοι αυτοματισμοί που επιβάλλουν μια ροή εργασίας εργαλείων σε κάθε έγγραφο. Κάθε πολιτική ενεργοποιείται κατά τη μεταφόρτωση ή την εξαγωγή, εκτελεί την αλυσίδα εργαλείων της και αποθηκεύει την επιβεβλημένη έκδοση μαζί με το πρωτότυπο." title = "Πολιτικές" [portal.policies.card] comingSoon = "Αναβάθμιση σε Enterprise" notSetUp = "Δεν έχει ρυθμιστεί" +requiresAiEngine = "Απαιτεί AI engine" + +[portal.policies.categories.classification] +desc = "Αναγνωρίστε τον τύπο κάθε εγγράφου κατά τη μεταφόρτωση και προσθέστε ετικέτες στα μεταδεδομένα του για αρχειοθέτηση και αναζήτηση." +label = "Ταξινόμηση" [portal.policies.categories.compliance] desc = "Επιβάλλετε απαιτήσεις HIPAA, GDPR, SOC 2 ή FedRAMP σε κάθε έγγραφο." @@ -7344,6 +7927,13 @@ label = "Ασφάλεια" [portal.policies.config] scopeAll = "Όλα τα έγγραφα" +[portal.policies.config.classification] +summary = "Ταξινομεί κάθε μεταφορτωμένο έγγραφο με βάση τις ετικέτες της ομάδας σας και προσθέτει ετικέτες στα μεταδεδομένα του." + +[portal.policies.config.classification.rules] +0 = "Ταξινόμηση" +1 = "Προσθήκη ετικέτας στα μεταδεδομένα" + [portal.policies.config.compliance] summary = "Επικυρώνει έγγραφα έναντι κανονιστικών πλαισίων πριν φύγουν από το σύστημα." @@ -7371,6 +7961,29 @@ minConfidence = "Ελάχιστη βεβαιότητα" 2 = "Ονομασία" 3 = "Κανονικοποίηση" +[portal.policies.config.purview] +label = "Εφαρμογή ετικέτας ευαισθησίας Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "Το GUID της ετικέτας, από το portal Purview. Η εφαρμογή ετικέτας γράφει τα μεταδεδομένα της στο PDF· δεν μπορεί να κρυπτογραφήσει, κάτι που απαιτεί τον Microsoft client." +labelName = "Όνομα ετικέτας (προαιρετικό)" +labelNameHelp = "Καταγράφεται μαζί με το ID ώστε οι χρήστες και τα εργαλεία να μπορούν να αναγνωρίζουν την ετικέτα." + +[portal.policies.config.purview.fields.method] +help = "Το Purview διακρίνει μια ετικέτα που εφαρμόστηκε από πολιτική από μία που επέλεξε κάποιος χρήστης." +label = "Πώς καταγράφεται η ετικέτα" +privileged = "Ως επιλεγμένη από χρήστη" +standard = "Ως εφαρμοσμένη αυτόματα" + +[portal.policies.config.purviewRead] +label = "Ανάγνωση ετικέτας Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Διαβάζει την ετικέτα που φέρει ήδη το έγγραφο, ώστε τα επόμενα βήματα να μπορούν να ενεργήσουν βάσει αυτής." + [portal.policies.config.retention] summary = "Επιβάλλει πόσο καιρό διατηρούνται τα έγγραφα, πότε αρχειοθετούνται και πότε διαγράφονται." @@ -7438,6 +8051,7 @@ title = "Δεν υπάρχει ακόμη δραστηριότητα" [portal.policies.endpoints] addWatermark = "Υδατογράφημα" autoRedact = "Απόκρυψη PII" +classifyAndLabel = "Ταξινόμηση" compressPdf = "Συμπίεση" flatten = "Επιπεδοποίηση" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Οι πολιτικές σας αποθηκεύτηκαν και retry = "Επανάληψη" title = "Το Backend δεν είναι διαθέσιμο" +[portal.policies.operations] +change = "Αλλαγή του τι κάνει αυτό το βήμα" +noResults = "Κανένα βήμα δεν ταιριάζει με αυτό. Δοκιμάστε ένα όνομα προϊόντος ή \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Αναζήτηση βημάτων - δοκιμάστε \"scan\", \"attach\" ή ένα όνομα προϊόντος" + +[portal.policies.operations.bodyMode] +binary = "Ακατέργαστα bytes αρχείου" +json = "Μέσα σε JSON payload" +multipart = "Ως μεταφόρτωση αρχείου (multipart)" + +[portal.policies.operations.clamavScan] +description = "Ο δικός σας ClamAV server ελέγχει το έγγραφο· τίποτα δεν φεύγει από το δίκτυό σας." +label = "Σάρωση για ιούς (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Απορρίπτει έγγραφα που περιέχουν macros, εκτελέσιμα, scripts ή ενσωματωμένα αντικείμενα." +label = "Αποκλεισμός ενεργού περιεχομένου" + +[portal.policies.operations.cloudmersiveScan] +description = "Το Cloudmersive ελέγχει το έγγραφο και η εκτέλεση σταματά αν δεν είναι καθαρό." +label = "Σάρωση για ιούς" + +[portal.policies.operations.confluenceAttach] +description = "Αρχειοθετεί το επεξεργασμένο έγγραφο σε μια σελίδα." +label = "Επισύναψη σε σελίδα Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Υποβάλλει το έγγραφο στο ConsignO Cloud για υπογραφή." +label = "Αποστολή για υπογραφή (ConsignO)" +note = "Υποβάλλει το έγγραφο. Η ανάκτηση του υπογεγραμμένου αντιγράφου δεν υποστηρίζεται ακόμα, επειδή ένα βήμα δεν μπορεί να περάσει αποτέλεσμα σε επόμενο βήμα." + +[portal.policies.operations.customApiCall] +description = "Συντάξτε την κλήση μόνοι σας: path, method, body και διαχείριση απόκρισης." +label = "Κλήση προσαρμοσμένου API" + +[portal.policies.operations.discordNotify] +description = "Ενημερώνει ένα κανάλι ότι η πολιτική χειρίστηκε ένα έγγραφο." +label = "Δημοσίευση μηνύματος στο Discord" + +[portal.policies.operations.elasticIndex] +description = "Καταγράφει αυτό που έκανε η πολιτική ως έγγραφο με δυνατότητα αναζήτησης." +label = "Ευρετηρίαση συμβάντος ελέγχου στο Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Το multipart ταιριάζει σε upload APIs· το JSON ταιριάζει σε APIs που θέλουν το αρχείο μέσα σε payload· το binary στέλνει τα ακατέργαστα bytes." +label = "Πώς να σταλεί το έγγραφο" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Ένα JSON body που αποστέλλεται ως έχει. Το {{document.base64}} μεταφέρει το αρχείο· τα {{document.*}} και {{run.*}} συμπληρώνονται ανά έγγραφο." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Λογαριασμός" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Το πεδίο φόρμας κάτω από το οποίο το API αναμένει το έγγραφο." +label = "Όνομα πεδίου αρχείου" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Από" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Ένα JSON object με ζεύγη ονόματος/τιμής header. Το διαπιστευτήριο της σύνδεσης προστίθεται για εσάς." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Κλειδί ζητήματος" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Αποστέλλεται ως σώμα μηνύματος. Τα {{document.*}} και {{run.*}} συμπληρώνονται ανά έγγραφο." +label = "Μήνυμα" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Προστίθεται στο base URL της σύνδεσης. Δεν μπορεί ποτέ να φτάσει σε άλλον host." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Πού στο drive να γραφτεί. Το {{document.filename}} συμπληρώνεται ανά έγγραφο." +label = "Διαδρομή προορισμού" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Η αναφορά αφήνει το έγγραφο ανέπαφο. Η αντικατάσταση το ανταλλάσσει με ό,τι επιστρέφεται." +label = "Τι να γίνει με την απάντηση" + +[portal.policies.operations.fields.signerEmail] +label = "Email υπογράφοντος" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Θέμα" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Το Presidio αναλύει αυτό το κείμενο. Χρησιμοποιήστε {{document.title}} ή επικολλήστε το κείμενο που θέλετε να ελεγχθεί." +label = "Κείμενο προς έλεγχο" + +[portal.policies.operations.fields.to] +label = "Προς" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Όνομα χρήστη Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Ενημερώνει έναν χώρο ότι η πολιτική χειρίστηκε ένα έγγραφο." +label = "Δημοσίευση μηνύματος στο Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Αρχειοθετεί το επεξεργασμένο έγγραφο σε ένα ζήτημα." +label = "Επισύναψη σε ζήτημα Jira" + +[portal.policies.operations.mailgunEmail] +description = "Στέλνει το επεξεργασμένο έγγραφο ως συνημμένο." +label = "Αποστολή του εγγράφου με email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Γράφει το επεξεργασμένο έγγραφο σε έναν φάκελο." +label = "Μεταφόρτωση στο Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Ο δικός σας Presidio server αναφέρει τα προσωπικά δεδομένα που βρίσκει." +label = "Εντοπισμός προσωπικών δεδομένων" +note = "Το Presidio διαβάζει κείμενο, όχι αρχεία. Δώστε του το κείμενο που θέλετε να ελεγχθεί — το ίδιο το έγγραφο δεν μεταφορτώνεται." + +[portal.policies.operations.responseMode] +replace = "Αντικατάσταση του εγγράφου με την απάντηση" +report = "Διατήρηση του εγγράφου, καταγραφή της απάντησης" + +[portal.policies.operations.sendgridEmail] +description = "Στέλνει το επεξεργασμένο έγγραφο ως συνημμένο." +label = "Αποστολή του εγγράφου με email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Ενημερώνει ένα κανάλι ότι η πολιτική χειρίστηκε ένα έγγραφο." +label = "Δημοσίευση μηνύματος στο Slack" + +[portal.policies.operations.splunkEvent] +description = "Καταγράφει αυτό που έκανε η πολιτική, εκεί όπου ήδη αναζητούν οι ελεγκτές σας." +label = "Αποστολή συμβάντος ελέγχου στο Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Καταγράφει αυτό που έκανε η πολιτική σε έναν HTTP collector." +label = "Αποστολή συμβάντος ελέγχου στο Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Ενημερώνει ένα κανάλι ότι η πολιτική χειρίστηκε ένα έγγραφο." +label = "Δημοσίευση μηνύματος στο Teams" + +[portal.policies.operations.webhookPost] +description = "Στέλνει το ίδιο το έγγραφο σε ένα URL που επιλέγετε." +label = "Δημοσίευση του εγγράφου σε webhook" + +[portal.policies.operations.zapierNotify] +description = "Παραδίδει τις λεπτομέρειες της εκτέλεσης στον αυτοματισμό σας." +label = "Trigger ενός Zap ή Make scenario" + [portal.policies.stats] activeFor = "Ενεργό" dataProcessed = "Δεδομένα που επεξεργάστηκαν" @@ -7473,6 +8282,13 @@ label = "Έγγραφα που επιβλήθηκαν" description = "Διαμορφωμένες αλλά δεν ενεργοποιούνται" label = "Σε παύση" +[portal.policies.table] +appliesTo = "Ισχύει για" +docs = "Έγγραφα επιβληθέντα" +enforces = "Επιβάλλει" +policy = "Πολιτική" +status = "Κατάσταση" + [portal.policies.wizard.actions] back = "Πίσω" cancel = "Ακύρωση" @@ -7480,6 +8296,10 @@ continue = "Συνέχεια" enablePolicy = "Ενεργοποίηση πολιτικής" saveChanges = "Αποθήκευση αλλαγών" +[portal.policies.wizard.capability.classify] +desc = "Αναγνωρίζει τον τύπο του εγγράφου από τις ετικέτες της ομάδας σας και το επισημαίνει, ώστε να αρχειοθετείται και να αναζητείται ανά κατηγορία." +label = "Ταξινόμηση του εγγράφου" + [portal.policies.wizard.capability.compress] desc = "Συμπιέζει το έγγραφο σε μικρότερο μέγεθος αρχείου." label = "Μείωση μεγέθους αρχείου" @@ -7500,10 +8320,18 @@ label = "Απόκρυψη ευαίσθητων πληροφοριών" desc = "Αφαιρεί κρυφό JavaScript ώστε τίποτα να μην μπορεί να εκτελεστεί αυτόματα όταν ανοίγει το έγγραφο." label = "Αφαίρεση ενεργού περιεχομένου" +[portal.policies.wizard.capability.timestampPdf] +desc = "Αποδεικνύει ότι το έγγραφο υπήρχε σε αυτήν ακριβώς τη μορφή σε μια συγκεκριμένη χρονική στιγμή, χρησιμοποιώντας ανεξάρτητη αρχή χρονοσήμανσης. Αποστέλλεται μόνο ένα hash - το έγγραφο δεν φεύγει ποτέ από τον server σας." +label = "Προσθήκη αξιόπιστης χρονοσήμανσης" + [portal.policies.wizard.capability.watermark] desc = "Τοποθετεί ένα ορατό σημάδι (π.χ. «Εμπιστευτικό») σε κάθε σελίδα." label = "Εφαρμογή υδατογραφήματος" +[portal.policies.wizard.classification] +description = "Κάθε μεταφορτωμένο έγγραφο ταξινομείται με βάση τις ενσωματωμένες ετικέτες και επισημαίνεται με τους τύπους που ταιριάζουν. Το σύνολο ετικετών είναι κοινό για όλη την ομάδα σας." +labelsHeading = "Ετικέτες ταξινόμησης" + [portal.policies.wizard.errors] noTools = "Ενεργοποιήστε πρώτα τουλάχιστον ένα εργαλείο στη ροή εργασίας." saveFailed = "Δεν ήταν δυνατή η αποθήκευση της πολιτικής. Δοκιμάστε ξανά." @@ -7548,73 +8376,118 @@ setUp = "Ρύθμιση πολιτικής {{category}}" [portal.policies.wizard.workflow] description = "Επιλέξτε τι κάνει αυτή η πολιτική σε κάθε έγγραφο που επεξεργάζεται." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} ενεργές" -noRule = "Δεν έχει επιβληθεί ακόμη κανόνας" -subtitle = "Μόνιμοι αυτοματισμοί από τους οποίους περνά κάθε έγγραφο, ανεξάρτητα από το ποιο pipeline το χειρίζεται." -title = "Τι εκτελείται στα PDF σας" - [portal.policySummary.action] -comingSoon = "Αναβάθμιση σε Enterprise" -configure = "Διαμόρφωση" setUp = "Ρύθμιση" -[portal.policySummary.column] -activeRule = "Ενεργός κανόνας" -policy = "Πολιτική" -status = "Κατάσταση" +[portal.processorFlow] +footnote = "Οι μετρήσεις αφορούν τις τελευταίες 24 ώρες. Η ταχύτητα ροής είναι ενδεικτική." +liveBadge = "Live" +stats = "{{connected}} συνδεδεμένα · {{processed}} PDF επεξεργασμένα" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Μόλις διαμορφωθούν οι πολιτικές, οι κατηγορίες θα εμφανιστούν εδώ." -title = "Δεν υπάρχουν ακόμη πολιτικές" +[portal.processorFlow.lens] +ariaLabel = "Λειτουργία προβολής" +flow = "Ροή" +sankey = "Sankey" -[portal.policySummary.state] -active = "Ενεργό" -off = "Ανενεργό" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Απέτυχαν" +heading = "Αποτελέσματα" +success = "Παραδόθηκαν" + +[portal.processorFlow.policies] +activeCount = "{{n}} ενεργές" +count = "{{n}} · 24h" +heading = "Πολιτικές" +setUp = "Ρύθμιση" soon = "Σύντομα" -[portal.processingStatus] -managePlan = "Διαχείριση πλάνου" -volumeSuffix = "PDF που επεξεργάστηκαν · τελευταίες 30 ημέρες" +[portal.processorFlow.sankey] +waist = "Πολιτικές · {{n}} ενεργές" + +[portal.processorFlow.sankey.empty] +description = "Συνδέστε μια πηγή και ενεργοποιήστε μια πολιτική για να παρακολουθήσετε τα έγγραφα να ρέουν μέσω του processor." +title = "Δεν υπάρχει ροή ακόμα" + +[portal.processorFlow.sources] +comingSoonTag = "Σύνδεση" +editor = "Stirling PDF Editor" +heading = "Πηγές" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Λήψη email" [portal.procurement] -reset = "Επαναφορά προμηθειών (demo)" subtitle = "Αξιολογήστε, συμβασιοποιήστε και εντάξτε την ομάδα σας. Κάθε έγγραφο σε ένα μέρος." title = "Προμήθειες" -[portal.procurement.action] -download = "Λήψη" -pay = "Πληρωμή τώρα" -request = "Αίτημα" -sign = "Έλεγχος & υπογραφή" -upload = "Μεταφόρτωση" - [portal.procurement.agreement] agreeCta = "Συμφωνία & συνδρομή" +confidential = "Εμπιστευτικό" confirm = "Έχω διαβάσει και συμφωνώ με τη Stirling Enterprise Agreement." -eyebrow = "Σύμβαση" -intro = "Μία συνδυασμένη σύμβαση καλύπτει τη συμφωνία σας: Master Service Agreement, Order Form, EULA και Data Processing Agreement. Ελέγξτε τη και μετά συμφωνήστε για να αποδεχτείτε την προσφορά σε δεσμευτική συνδρομή." -title = "Ελέγξτε την εταιρική σας σύμβαση" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA και DPA, συνδυασμένα σε μία υπογραφή." +download = "Λήψη" +downloadDraftError = "Δεν ήταν δυνατή η δημιουργία του PDF συμφωνίας - ο renderer εγγράφων δεν είναι διαθέσιμος. Δοκιμάστε ξανά σύντομα ή επικοινωνήστε με την υποστήριξη." +downloadError = "Δεν ήταν δυνατή η λήψη της υπογεγραμμένης συμφωνίας. Δοκιμάστε ξανά." +legalName = "Όνομα νομικής οντότητας" +legalNamePlaceholder = "Η νομική οντότητα που θα υπογράψει" +loadError = "Δεν ήταν δυνατή η φόρτωση της συμφωνίας. Δοκιμάστε ξανά." +loading = "Φόρτωση της συμφωνίας..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Αίτημα αλλαγών" +scrollHint = "Κάντε κύλιση για έλεγχο" +signatory = "Όνομα υπογράφοντος" +signatoryPlaceholder = "Πλήρες όνομα" +signatoryTitle = "Τίτλος υπογράφοντος" +signatoryTitlePlaceholder = "π.χ. General Counsel" +signError = "Δεν ήταν δυνατή η καταγραφή της υπογραφής σας. Δοκιμάστε ξανά." [portal.procurement.builder] addons = "Πρόσθετα" +addressLine1 = "Γραμμή διεύθυνσης 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Γραμμή διεύθυνσης 2" +addressLine2Placeholder = "Σουίτα, όροφος, κτίριο" back = "Πίσω" businessName = "Επωνυμία επιχείρησης" businessNamePlaceholder = "Η εταιρεία σας" +city = "Πόλη" +cityPlaceholder = "San Francisco" +completeRequired = "Συμπληρώστε τα απαιτούμενα πεδία (σημειωμένα με *) με έγκυρο email πριν δημιουργήσετε την προσφορά." +contactEmail = "Email επικοινωνίας" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Όνομα επικοινωνίας" +contactNamePlaceholder = "Jane Doe" continue = "Συνέχεια" -country = "Χώρα" -countryEuro = "Ευρωζώνη (EUR €)" -countryUK = "Ηνωμένο Βασίλειο (GBP £)" -countryUS = "Ηνωμένες Πολιτείες (USD $)" +done = "Τέλος" eula = "Έχω διαβάσει και συμφωνώ με το Stirling Enterprise EULA. Διέπει τη σύμβαση που δημιουργείται από αυτήν την προσφορά." generate = "Δημιουργία προσφοράς" -included = "Περιλαμβάνεται" indemnification = "Αποζημίωση IP" indemnificationSub = "Υπερασπιζόμαστε επιλέξιμες αξιώσεις IP, σύμφωνα με το EULA" -offlineLicense = "Offline / air-gapped άδεια" -offlineLicenseSub = "Ένα αρχείο άδειας με δυνατότητα λήψης για μια air-gapped self-hosted παρουσία" +paperEyebrow = "Enterprise προσφορά" +paperFor = "Προετοιμάστηκε για" +pdfSize = "Μέγεθος PDF" +poNumber = "Αριθμός PO" +poNumberPlaceholder = "Προαιρετικό" +postalCode = "Ταχυδρομικός κώδικας" +postalCodePlaceholder = "94105" +posture = "Διακυβέρνηση" +posture_count = "~{{count}} πολιτικές" +posture_essentials = "Βασικά" +posture_essentialsSub = "Ταξινόμηση + Κοινή χρήση — οι προεπιλογές" +posture_governed = "Με διακυβέρνηση" +posture_governedSub = "Προσθέτει Ασφάλεια και Δρομολόγηση" +posture_regulated = "Ρυθμιζόμενο" +posture_regulatedSub = "Κάθε κατηγορία — Συμμόρφωση, Διατήρηση, Ingestion" qbr = "Τριμηνιαίες επιχειρηματικές ανασκοπήσεις" qbrSub = "Ο SE σας ανασκοπεί τη χρήση και τον οδικό χάρτη κάθε τρίμηνο" +region = "Πολιτεία / περιοχή" +regionPlaceholder = "California" running = "{{annual}} / έτος · {{years}} έτη {{tcv}}" s1Sub = "Η ομάδα σας και τα PDF που αναμένετε να εκτελείτε κάθε χρόνο." s1Title = "Πόσο θα επεξεργάζεστε;" @@ -7623,6 +8496,12 @@ s2Title = "Δέσμευση και υπηρεσία" s3Sub = "Για την προσφορά και τη σύμβαση που δημιουργεί." s3Title = "Τα στοιχεία σας" serviceLevel = "Επίπεδο υπηρεσίας" +size_compact = "Συμπαγές" +size_compactSub = "Κυρίως κείμενο, κάτω από 1 MB" +size_heavy = "Βαρύ" +size_heavySub = "Σαρωμένο ή με πολλές εικόνες, 5 MB+" +size_standard = "Standard" +size_standardSub = "Μικτό κείμενο και εικόνες, 1 έως 5 MB" slDedicated = "Αποκλειστικό" slDedicatedSub = "4 εργάσιμες ώρες · αποκλειστικός διαχειριστής λογαριασμού · +30%" slPriority = "Προτεραιότητα" @@ -7630,6 +8509,8 @@ slPrioritySub = "Ίδια εργάσιμη ημέρα · ονομαστικός slStandard = "Τυπικό" slStandardSub = "Επόμενη εργάσιμη ημέρα · κοινόχρηστος CSM · περιλαμβάνεται" stepOf = "Βήμα {{n}} από {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Προαιρετικό" term = "Διάρκεια" termDiscount = "Εφαρμόστηκε έκπτωση {{pct}}% για πολυετή δέσμευση" title = "Δημιουργήστε την προσφορά σας" @@ -7637,6 +8518,7 @@ training = "Onboarding & εκπαίδευση" trainingSub = "Ζωντανές συνεδρίες για να ξεκινήσει η ομάδα σας" users = "Σύνολο χρηστών" usersPlaceholder = "π.χ. 250" +viewEula = "Διαβάστε το" volEstimated = "Εκτιμήθηκε από {{count}} χρήστες (~2.000 PDF ο καθένας, συμπεριλαμβανομένου του αυτοματισμού). Επεξεργαστείτε το αν γνωρίζετε καλύτερα." volManual = "Χρήση του αριθμού σας. Επανεκτιμήστε από το μέγεθος της ομάδας σας οποιαδήποτε στιγμή." volNoUsers = "Δεν είστε σίγουροι; Εισαγάγετε το μέγεθος της ομάδας σας και θα το εκτιμήσουμε." @@ -7645,55 +8527,55 @@ volumePlaceholder = "π.χ. 1.000.000" years_one = "{{count}} έτος" years_other = "{{count}} έτη" -[portal.procurement.docs] -count_one = "{{count}} έγγραφο" -count_other = "{{count}} έγγραφα" -done = "Ολοκληρώθηκε" -here = "Είστε εδώ" -hide = "Απόκρυψη" -optional = "Προαιρετικό" -paidAddon = "Πληρωμένο πρόσθετο" -show = "Εμφάνιση" -subtitle = "Όλα όσα χρειάζεστε σε κάθε βήμα της διαδρομής, εμφανίζονται καθώς η συμφωνία προχωρά." -supportingSubtitle = "SOC 2, έλεγχοι ασφαλείας, φορολογικά έντυπα και άλλα, έτοιμα όταν τα ζητήσει η ομάδα ασφάλειας ή προμηθειών σας. Ορισμένα έχουν εφάπαξ χρέωση." -supportingTitle = "Υποστήριξη της αξιολόγησής σας" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form και DPA σε μία υπογραφή." +download = "Λήψη" +eula = "EULA & Εμπορικοί Όροι" +eulaSub = "Οι εμπορικοί όροι λογισμικού και self-serve." +invoice = "Τιμολόγιο" +invoiceSub = "Το πρώτο τιμολόγιο συνδρομής σας." +laterInvoice = "Διαθέσιμο αφού αποδεχτείτε" +laterQuote = "Διαθέσιμο μόλις εκδοθεί η προσφορά σας" +quote = "Προσφορά" +quoteSub = "Η αναλυτική enterprise προσφορά σας." +sla = "SLA Exhibit" +slaSub = "Επίπεδα υποστήριξης και στόχοι απόκρισης." +subprocessors = "Subprocessors" +subprocessorsSub = "Τρίτα μέρη που επεξεργάζονται δεδομένα για λογαριασμό σας." +subtitle = "Τα έγγραφα της συμφωνίας σας, διαθέσιμα ανά πάσα στιγμή." title = "Έγγραφα" -upcoming = "Προσεχώς" +view = "Προβολή" [portal.procurement.error] title = "Κάτι πήγε στραβά" [portal.procurement.hero] -company = "Η εταιρική σας συμφωνία" +barAria = "Στάδιο {{current}} από {{total}}" ctaAgreement = "Έλεγχος & υπογραφή σύμβασης" -ctaLive = "Είστε live" -ctaPayment = "Προσθήκη πληρωμής" +ctaExploring = "Ρύθμιση της δοκιμής σας" ctaQuote = "Ελέγξτε την προσφορά σας" +ctaReviewQuote = "Έλεγχος προσφοράς" ctaTrial = "Δημιουργήστε την προσφορά σας" +documents = "Έγγραφα" eyebrow = "Enterprise προμήθειες" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Πρόσκληση συμπαικτών" -keyDocs = "Βασικά έγγραφα" -nextStep = "Επόμενο βήμα: {{action}}" -notStarted = "Δεν ξεκίνησε" +licenseKey = "Κλειδί άδειας" +liveSub = "Ο οργανισμός σας έχει provisioned και η άδειά σας είναι ενεργή." +liveTitle = "Είστε live στο Stirling Enterprise" +next = "Επόμενο: {{stage}}" open = "Άνοιγμα προμηθειών" scheduleCall = "Προγραμματισμός κλήσης" -setup1Sub = "Προσκαλέστε τους συμπαίκτες σας" -setup1Title = "Αναπτύξτε το PDF Editor" -setup2Sub = "Ενεργοποιήστε την επεξεργασία σε editors και άλλες πηγές" -setup2Title = "Συνδέστε το PDF Processor" -setup3Sub = "Ενεργοποιήστε Security, Compliance, Routing ή Retention όταν τα χρειαστείτε" -setup3Title = "Προσθήκη προτεινόμενων πολιτικών" +sentenceAgreement = "ελέγξτε και υπογράψτε τη συμφωνία" +sentenceLive = "provisioning και rollout" +sentencePayment = "purchase order και πληρωμή" +sentenceQuote = "ελέγξτε και αποδεχτείτε την προσφορά σας" +sentenceTrial = "ρυθμίστε τον δοκιμαστικό χώρο εργασίας σας" [portal.procurement.journey] daysLeft_one = "Απομένει {{count}} ημέρα" daysLeft_other = "Απομένουν {{count}} ημέρες" -engineerLabel = "Ο μηχανικός λύσεών σας" -eyebrow = "Η διάθεσή σας" -live = "Είστε live στο Stirling Enterprise" -nextStep = "Επόμενο βήμα: {{action}}" -subtitle = "Ο μηχανικός λύσεών σας είναι σε κάθε βήμα. Μία επόμενη ενέργεια τη φορά· η πλήρης λίστα ελέγχου είναι παρακάτω." -title = "Από τη δοκιμή στο live, μία καθοδηγούμενη διαδρομή" -trialTitle = "Enterprise δοκιμή" [portal.procurement.journeySteps.agreement] blurb = "Μία υπογραφή καλύπτει MSA, φόρμα παραγγελίας, EULA και DPA." @@ -7727,6 +8609,9 @@ downloadError = "Δεν ήταν δυνατή η δημιουργία του off downloadOffline = "Λήψη offline άδειας (.lic)" hint = "Επικολλήστε αυτό το κλειδί σε μια self-hosted παρουσία για να την ενεργοποιήσετε ή κρατήστε το για τα αρχεία σας. Φυλάξτε το με ασφάλεια." label = "Το κλειδί άδειάς σας" +subtitle = "Ενεργοποιήστε μια self-hosted εγκατάσταση με αυτό το κλειδί ή κρατήστε το για τα αρχεία σας." +title = "Το κλειδί άδειάς σας" +trialFileHint = "Αυτό είναι το αρχείο δοκιμαστικής άδειάς σας. Μόλις τεθεί σε ισχύ η συμφωνία σας, επιστρέψτε και κατεβάστε το ξανά — σε αντίθεση με το online κλειδί άδειας, το αρχείο .lic δεν θα ενημερωθεί μόνο του." [portal.procurement.link] cta = "Σύνδεση λογαριασμού" @@ -7739,65 +8624,62 @@ description = "Η συνδρομή σας είναι ενεργή και η άδ eyebrow = "Live" title = "Είστε live στο Stirling Enterprise" -[portal.procurement.locked] -description = "Τα δοκιμαστικά κλειδιά, οι προσφορές δεσμευμένου όγκου, η σύμβαση μίας υπογραφής, η πληρωμή και το καθολικό εγγράφων σας βρίσκονται όλα εδώ μόλις ξεκινήσετε μια Enterprise αξιολόγηση." -eyebrow = "Μόνο Enterprise" -talkToSales = "Μιλήστε με τις πωλήσεις" -title = "Η διαδρομή προμηθειών ανοίγει με το Enterprise" - [portal.procurement.milestone] -accept = "Αποδοχή & συνέχεια" -description = "Κατεβάστε το PDF για να το μοιραστείτε με την ομάδα σας, επιστρέψτε για αποδοχή όταν είστε έτοιμοι ή κάντε αλλαγές." -download = "Λήψη PDF" downloadError = "Δεν ήταν δυνατή η λήψη του PDF προσφοράς ακόμη — δοκιμάστε ξανά σε λίγο." -edit = "Επεξεργασία προσφοράς" -eyebrow = "Προσφορά {{number}}" -perYear = " / έτος" -preparedFor = "Ετοιμάστηκε για {{company}}" -tcv = "{{value}} συνολική αξία σύμβασης" -title = "Η προσφορά σας είναι έτοιμη" - -[portal.procurement.modal] -cancel = "Ακύρωση" -chooseFile = "Επιλογή αρχείου" -close = "Κλείσιμο" -downloadBody = "Η λήψη σας θα ξεκινήσει σύντομα." -downloadCta = "Λήψη" -downloadTitle = "Λήψη" -noFile = "Δεν επιλέχθηκε αρχείο" -payBody = "Πληρώστε τη δεσμευμένη σύμβασή σας με κάρτα ή τραπεζική μεταφορά μέσω Stripe. Ο χώρος εργασίας σας παρέχεται μόλις εκκαθαριστεί η πληρωμή." -payCta = "Συνέχεια στο Stripe" -payTitle = "Επιβεβαίωση πληρωμής" -requestBodyFree = "Το δημιουργούμε κατόπιν αιτήματος. Επιβεβαιώστε και ο μηχανικός λύσεών σας θα σας το στείλει σύντομα." -requestBodyPaid = "Αυτό είναι πληρωμένο πρόσθετο. Επιβεβαιώστε και ο μηχανικός λύσεών σας θα το οριοθετήσει και θα στείλει τα έγγραφα." -requestCta = "Αίτημα" -requestTitle = "Αίτημα για αυτό το έγγραφο" -signBody = "Ανοίγει τη Stirling Enterprise Agreement για ηλεκτρονική υπογραφή: μία υπογραφή καλύπτει το MSA, τη φόρμα παραγγελίας, το EULA και το DPA. Συνυπογράφουμε αυτόματα και προχωράτε στην πληρωμή." -signCta = "Άνοιγμα για υπογραφή" -signTitle = "Ελέγξτε και υπογράψτε τη σύμβασή σας" -uploadBody = "Στείλτε μας το PO σας και θα εκδώσουμε τιμολόγιο βάσει αυτού με τους όρους σας. Σύρετε το PDF ή επιλέξτε ένα αρχείο παρακάτω." -uploadCta = "Μεταφόρτωση εντολής αγοράς" -uploadTitle = "Μεταφόρτωση της εντολής αγοράς σας" [portal.procurement.payment] description = "Η προσφορά σας έγινε αποδεκτή και η άδειά σας είναι ήδη ενεργή — η ομάδα σας μπορεί να ξεκινήσει αμέσως. Πληρώστε το πρώτο τιμολόγιο όταν είστε έτοιμοι· μπορείτε να το πληρώσετε ή να το κατεβάσετε εδώ, χωρίς να χρειάζεται email." +downloadAgreement = "Λήψη υπογεγραμμένης συμφωνίας" downloadInvoice = "Λήψη τιμολογίου" -simulate = "Προσομοίωση λήψης πληρωμής (demo)" +eyebrow = "Πληρωμή" title = "Η συνδρομή δημιουργήθηκε" viewInvoice = "Προβολή & πληρωμή τιμολογίου" +[portal.procurement.review] +acceptCta = "Αποδοχή προσφοράς" +annual = "Ετήσια χρέωση (έτος 1)" +downloadCta = "Λήψη προσφοράς" +poNumber = "Purchase order: {{po}}" +renewal = "Ανανεώνεται στα {{amount}}/έτος μετά τη διάρκεια (+{{pct}}% CPI)" +tcv = "Διάρκεια {{years}} ετών, πλήρως προπληρωμένη: {{tcv}}" +validUntil = "Ισχύει έως {{date}}" + [portal.procurement.schedule] fallback = "Δεν ήταν δυνατή η φόρτωση του scheduler." fallbackLink = "Άνοιγμα προγραμματισμού σε νέα καρτέλα" +loading = "Φόρτωση scheduler..." subtitle = "Ο μηχανικός λύσεών σας θα καθοδηγήσει την ομάδα σας στη διάθεση. Επιλέξτε μια ώρα που σας βολεύει." title = "Προγραμματισμός κλήσης" -[portal.procurement.status] -action = "Απαιτείται ενέργεια" -available = "Διαθέσιμο" -complete = "Ολοκληρωμένο" -pending = "Σε εκκρεμότητα" -request = "Κατόπιν αιτήματος" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Σφραγισμένο, χωρίς outbound" +back = "Πίσω" +businessName = "Επωνυμία επιχείρησης" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, ταχύτερη εκκίνηση" +continue = "Συνέχεια" +deployment = "Πού πρέπει να αναπτυχθεί;" +eula = "Συμφωνώ με το Stirling EULA & Commercial Terms." +fullName = "Πλήρες όνομα" +fullNamePlaceholder = "Το όνομά σας" +invites = "Προσκαλέστε teammates · προαιρετικό" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Προγραμματισμός κλήσης" +seats = "Άτομα που δοκιμάζουν" +seatsPlaceholder = "π.χ. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker ή Kubernetes" +start = "Έναρξη δοκιμής" +stepOf = "Βήμα {{n}} από {{total}}" +subtitle = "Δωρεάν για 14 ημέρες, χωρίς κάρτα." +subtitleDetails = "Μερικά στοιχεία για την προσφορά και τη συμφωνία σας." +talkFirst = "Προτιμάτε να μιλήσουμε πρώτα;" +title = "Ρύθμιση της δοκιμής σας" +viewEula = "Διαβάστε το" +workEmail = "Επαγγελματικό email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Η επέκταση προσθέτει 7 ημέρες και ειδοποιεί τον μηχανικό λύσεών σας." @@ -7808,109 +8690,113 @@ maxed = "Εξαντλήθηκε" subtitle = "Η δωρεάν δοκιμή σας διαρκεί έως {{date}}. Δεν απαιτείται κάρτα." title = "Enterprise δοκιμή" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Τιμολόγηση δεσμευμένου όγκου, SSO + SCIM + RBAC σε όλο τον οργανισμό, αμετάβλητος έλεγχος 90 ημερών και αποκλειστικός SE." -homeCta = "Έναρξη δοκιμής →" -homeHeadline = "Επεξεργαστείτε εκατομμύρια PDF." - -[portal.recentActivity] -title = "Πρόσφατη δραστηριότητα" -viewAll = "Προβολή όλων" - -[portal.recentActivity.empty] -description = "Εκτελέσεις pipeline, αναπτύξεις και συμβάντα agent θα εμφανιστούν εδώ." -title = "Δεν υπάρχει ακόμη τίποτα εδώ" - -[portal.search] -ariaLabel = "Αναζήτηση" -placeholder = "Αναζήτηση στο Stirling — endpoints, pipelines, έγγραφα…" - -[portal.search.empty] -noActionsDescription = "Οι γρήγορες ενέργειες θα εμφανιστούν εδώ μόλις γίνουν διαθέσιμες." -noActionsTitle = "Δεν υπάρχουν γρήγορες ενέργειες" -noMatches = "Δεν υπάρχουν αντιστοιχίες για \"{{query}}\"" -noMatchesDescription = "Δοκιμάστε διαφορετική λέξη-κλειδί ή περιηγηθείτε στον κατάλογο." - [portal.settings.groups] admin = "Διαχειριστής" [portal.settings.sections] account-link = "Σύνδεση λογαριασμού" -[portal.shell.header] -accountFallback = "Λογαριασμός" -accountMenu = "Μενού λογαριασμού" -darkMode = "Σκοτεινή λειτουργία" -lightMode = "Φωτεινή λειτουργία" -search = "Αναζήτηση" -searchPlaceholder = "Αναζήτηση…" -signOut = "Αποσύνδεση" -switchToDark = "Μετάβαση σε σκοτεινό θέμα" -switchToLight = "Μετάβαση σε φωτεινό θέμα" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} έγγραφα" linkAccount = "Σύνδεση λογαριασμού Stirling" -planEditor = "Πλάνο Editor" -planEnterprise = "Πλάνο Enterprise" -planProcessor = "Πλάνο Processor" primaryNav = "Κύρια πλοήγηση" switchApp = "Αλλαγή εφαρμογής" +[portal.shell.topbar] +closeNav = "Κλείσιμο πλοήγησης" +openNav = "Άνοιγμα πλοήγησης" +search = "Αναζήτηση" + [portal.sources] subtitle = "Επαναχρησιμοποιήσιμες συνδέσεις εισόδου που τροφοδοτούν έγγραφα στο Stirling. Διαμορφώστε μια σύνδεση μία φορά και μετά αναφέρετέ τη από οποιονδήποτε αριθμό πολιτικών. Κάντε κλικ σε μια γραμμή για τη διαμόρφωσή της και για το ποιες πολιτικές τη χρησιμοποιούν." title = "Πηγές" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Σύνδεση πηγής" +[portal.sources.builder] +back = "Πίσω στις πηγές" +backToTypes = "Όλοι οι τύποι πηγών" +cancel = "Ακύρωση" +chooseHint = "Επιλέξτε από πού προέρχονται τα έγγραφα. Οι γκριζαρισμένοι connectors έρχονται σύντομα." +comingSoon = "Προσεχώς" +comingSoonHeading = "Προσεχώς" +create = "Δημιουργία πηγής" +createTitle = "Σύνδεση πηγής" +delete = "Διαγραφή" +editTitle = "Επεξεργασία πηγής" +enabled = "Ενεργοποιημένο" +save = "Αποθήκευση αλλαγών" + +[portal.sources.builder.folderAccess] +description = "Οι αυτοματισμοί φακέλων μπορούν να χρησιμοποιούν μόνο φακέλους που έχει επιτρέψει ένας διαχειριστής. Προσθέστε τον στις ρυθμίσεις Folder Access και δοκιμάστε ξανά." +openSettings = "Ρυθμίσεις Folder Access" +title = "Αυτός ο φάκελος δεν επιτρέπεται" + [portal.sources.delete] body = "Διαγραφή του \"{{name}}\"; Αυτό δεν μπορεί να αναιρεθεί. Οι πολιτικές που το αναφέρουν θα πρέπει να ενημερωθούν." cancel = "Ακύρωση" confirm = "Διαγραφή" title = "Διαγραφή πηγής;" -[portal.sources.detail] -closeAriaLabel = "Κλείσιμο λεπτομερειών" -delete = "Διαγραφή πηγής" -docs24h = "Τελευταίες 24 ώρες" -docs30d = "Τελευταίες 30 ημέρες" -docsTotal = "Σύνολο που εντοπίστηκαν" -docsTrend = "Έγγραφα τις τελευταίες 30 ημέρες" -documents = "Έγγραφα" -edit = "Επεξεργασία" -notReferenced = "Δεν αναφέρεται από καμία πολιτική, οπότε είναι ασφαλές να διαγραφεί." -pause = "Παύση" -resume = "Συνέχιση" -subtitle = "{{type}} · {{status}}" -usedBy = "Χρησιμοποιείται από" - -[portal.sources.empty] -description = "Συνδέστε έναν φάκελο (και, σύντομα, cloud storage) ώστε οι πολιτικές σας να έχουν από πού να αντλούν έγγραφα." -title = "Δεν έχουν συνδεθεί ακόμη πηγές" - [portal.sources.kpi] inUse = "Σε χρήση" total = "Συνδέσεις" unused = "Αχρησιμοποίητες" +[portal.sources.networkFields.connection] +helperText = "Η αποθηκευμένη σύνδεση με τη διεύθυνση server και τα διαπιστευτήρια. Επαναχρησιμοποιείται από κάθε πηγή που την αναφέρει." +label = "Σύνδεση" + +[portal.sources.networkFields.directory] +helperText = "Φάκελος στον server για polling, σχετικός με το αρχικό home σύνδεσης ή τη ρίζα του share. Αφήστε κενό για τη ρίζα." +label = "Φάκελος" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Η κατανάλωση αφαιρεί κάθε αρχείο από τον server μόλις κάθε πολιτική το έχει επεξεργαστεί." +label = "Λειτουργία ανάγνωσης" + +[portal.sources.networkFields.mode.options] +consume = "Κατανάλωση: επεξεργασία κάθε αρχείου μία φορά" +snapshot = "Snapshot: επανάγνωση του φακέλου σε κάθε εκτέλεση" + +[portal.sources.networkFields.recursive] +label = "Βάθος φακέλου" + +[portal.sources.networkFields.recursive.options] +all = "Συμπερίληψη υποφακέλων" +top = "Μόνο ανώτερο επίπεδο" + [portal.sources.status] active = "Ενεργό" disabled = "Απενεργοποιημένο" unused = "Αχρησιμοποίητο" [portal.sources.table] +documents = "Έγγραφα" source = "Πηγή" status = "Κατάσταση" +type = "Type" usedBy = "Πολιτικές" +[portal.sources.types.box] +description = "Τραβήξτε έγγραφα από φακέλους Box που ήδη μοιράζεται η ομάδα σας." +label = "Box" + +[portal.sources.types.dropbox] +description = "Τραβήξτε έγγραφα από κοινόχρηστο φάκελο Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Έγγραφα που έχει επεξεργαστεί η ομάδα σας στον editor, σε εκτελέσεις πολιτικών και AI." label = "Editor" +noPolicies = "Δεν έχουν εκτελεστεί ακόμα πολιτικές από τον editor." + +[portal.sources.types.email] +description = "Μετατρέψτε συνημμένα που αποστέλλονται σε dedicated mailbox σε έγγραφα." +label = "Email inbox" [portal.sources.types.folder] description = "Παρακολουθήστε έναν κατάλογο στον server για νέα έγγραφα." @@ -7943,25 +8829,80 @@ label = "Βάθος φακέλου" all = "Συμπερίληψη υποφακέλων" top = "Μόνο ανώτατο επίπεδο" +[portal.sources.types.ftp] +description = "Κάντε polling σε φάκελο FTP ή FTPS server για νέα έγγραφα." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Τραβήξτε έγγραφα από κοινόχρηστους φακέλους Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Παρακολουθήστε ένα SMB network share για νέα έγγραφα." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "Παρακολουθήστε φακέλους OneDrive για νέα έγγραφα." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Τραβήξτε έγγραφα από Amazon S3 ή S3-compatible bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Η αποθηκευμένη σύνδεση που περιέχει το bucket και τα διαπιστευτήρια. Επαναχρησιμοποιείται από κάθε πηγή και έξοδο pipeline που την αναφέρει." +label = "Σύνδεση" + +[portal.sources.types.s3.fields.mode] +helperText = "Η κατανάλωση αφαιρεί κάθε object από το bucket μόλις κάθε πολιτική το έχει επεξεργαστεί." +label = "Λειτουργία ανάγνωσης" + +[portal.sources.types.s3.fields.mode.options] +consume = "Κατανάλωση: επεξεργασία κάθε object μία φορά" +snapshot = "Snapshot: επανάγνωση του bucket σε κάθε εκτέλεση" + +[portal.sources.types.s3.fields.prefix] +helperText = "Επεξεργάζονται μόνο objects των οποίων τα keys ξεκινούν με αυτό το prefix." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Κάντε polling σε SFTP drop folder για νέα έγγραφα." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Τραβήξτε έγγραφα από βιβλιοθήκες εγγράφων SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Πηγή" +[portal.sources.types.webhook] +createNote = "Ένα delivery URL και signing secret δημιουργούνται όταν δημιουργείτε αυτό το webhook. Οι αποστολείς κάνουν POST έγγραφα σε αυτό το URL και οι πολιτικές που το αναφέρουν εκτελούνται κατά την άφιξη." +description = "Λάβετε έγγραφα μέσω υπογεγραμμένου HTTP POST από οποιοδήποτε εξωτερικό σύστημα." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Το signing secret εμφανίζεται μία φορά, όταν δημιουργείται το webhook. Δημιουργήστε ξανά την πηγή για να το αλλάξετε." + +[portal.sources.types.webhook.reveal] +copy = "Αντιγραφή" +done = "Τέλος" +secret = "Signing secret" +secretHelp = "Υπογράψτε το raw body κάθε παράδοσης με αυτό το κλειδί (HMAC-SHA256) και στείλτε το ως X-Stirling-Signature header." +secretWarning = "Αντιγράψτε τώρα το signing secret. Για την ασφάλειά σας εμφανίζεται μόνο μία φορά και δεν μπορεί να ανακτηθεί αργότερα." +title = "Το webhook δημιουργήθηκε" +url = "Delivery URL" +usage = "Κάντε POST κάθε έγγραφο ως raw request body στο delivery URL με binary content type (application/pdf ή application/octet-stream). Οι πολιτικές που το αναφέρουν εκτελούνται αυτόματα κατά την άφιξη." + [portal.sources.wizard] -back = "Πίσω" -cancel = "Ακύρωση" -continue = "Συνέχεια" -editTitle = "Επεξεργασία πηγής" name = "Όνομα" namePlaceholder = "π.χ. Εισαγωγή αιτήσεων" -save = "Αποθήκευση αλλαγών" -subtitle = "Βήμα {{current}} από {{total}} · {{label}}" -title = "Σύνδεση πηγής" type = "Τύπος" -[portal.sources.wizard.steps] -chooseType = "Επιλογή τύπου" -configure = "Διαμόρφωση" -review = "Έλεγχος & σύνδεση" +[portal.stepModal] +close = "Κλείσιμο" [portal.usage] managePayment = "Διαχείριση πληρωμής" @@ -8021,16 +8962,9 @@ summary = "Κατέχει μια ομάδα — διαχειρίζεται το 2 = "Πρόσβαση στο portal μέσω της προεπιλεγμένης πολιτικής" 3 = "Όλα όσα μπορεί να κάνει το Μέλος" -[portal.welcome] -ariaLabel = "Καλώς ήρθατε στο Stirling PDF" -badge = "Ανοικτού κώδικα" -installEditor = "Εγκατάσταση του Editor" -inviteTeammates = "Πρόσκληση συμπαικτών" -openInBrowser = "Άνοιγμα στο πρόγραμμα περιήγησης" -perks = "Δωρεάν για πάντα · Self-hostable" -subtitle = "Ο πιο ασφαλής PDF Editor στον κόσμο είναι δωρεάν για ομάδες κάθε μεγέθους. Περιλαμβάνει 60+ λειτουργίες PDF και SSO." -title = "Καλώς ήρθατε στο" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Απεριόριστα" [printFile] title = "Εκτύπωση αρχείου" @@ -8613,6 +9547,14 @@ text = "Περιστρέψτε τις σελίδες του PDF σας δεξι [rotate.tooltip.header] title = "Επισκόπηση ρυθμίσεων περιστροφής" +[ruler] +clearAll = "Εκκαθάριση όλων" +hideAllLabels = "Απόκρυψη όλων των ετικετών" +hideSmallLabels = "Απόκρυψη μικρών ετικετών" +physicalValues = "Φυσικές" +scaled = "Σε κλίμακα" +showAllLabels = "Εμφάνιση όλων των ετικετών" + [sanitize] desc = "Αφαιρέστε πιθανά επιβλαβή στοιχεία από αρχεία PDF." sanitizationResults = "Αποτελέσματα καθαρισμού" @@ -8654,31 +9596,45 @@ label = "Αφαίρεση μεταδεδομένων XMP" [sanitize.steps] settings = "Ρυθμίσεις" -[sanitizePdf] -tags = "καθαρισμός,ασφάλεια,ασφαλές,αφαίρεση-απειλών" - [sanitizePDF] title = "Εξυγίανση PDF" +[sanitizePdf] +tags = "καθαρισμός,ασφάλεια,ασφαλές,αφαίρεση-απειλών" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Προσαρμογή κλίμακας σελίδας" +[scaleSettings] +activeScale = "Ενεργή κλίμακα" +apply = "Εφαρμογή κλίμακας" +applyCalibration = "Εφαρμογή βαθμονόμησης" +calculatedScale = "Υπολογισμένη κλίμακα" +calibrate = "Βαθμονόμηση" +calibrating = "Γίνεται βαθμονόμηση" +calibrationDistanceRequired = "Εισαγάγετε μια πραγματική απόσταση μεγαλύτερη από το μηδέν" +calibrationInvalid = "Δεν είναι δυνατός ο υπολογισμός της κλίμακας από αυτή τη μέτρηση" +calibrationPaperDistance = "Μετρημένη απόσταση σελίδας: {{distance}}" +calibrationTitle = "Βαθμονόμηση κλίμακας" +calibrationTooltip = "Μετρήστε μια γνωστή απόσταση για να υπολογιστεί αυτόματα η κλίμακα" +customScale = "Προσαρμοσμένη κλίμακα" +noneSet = "Δεν έχει οριστεί προσαρμοσμένη κλίμακα" +presets = "Προκαθορισμένες κλίμακες" +ratio = "Αναλογία κλίμακας" +ratioHelp = "Αναλογία: 1 μονάδα σελίδας = X πραγματικές μονάδες" +ratioPlaceholder = "π.χ., 100" +ratioPositive = "Η αναλογία πρέπει να είναι μεγαλύτερη από το μηδέν" +ratioRequired = "Η αναλογία απαιτείται" +realDistance = "Πραγματική απόσταση" +realDistancePlaceholder = "π.χ., 5" +reset = "Επαναφορά στις προεπιλογές" +unit = "Μονάδα" +unitInvalid = "Μη έγκυρη μονάδα: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Όριο γωνίας:" -10 = "Ορίζει το μέγεθος του περιγράμματος που προστίθεται και αφαιρείται για την αποφυγή λευκών περιγραμμάτων στο αποτέλεσμα (προεπιλογή: 1)." -2 = "Ορίζει την ελάχιστη απόλυτη γωνία που απαιτείται για την περιστροφή της εικόνας (προεπιλογή: 10)." -3 = "Ανοχή:" -4 = "Καθορίζει το εύρος της χρωματικής διακύμανσης γύρω από το εκτιμώμενο χρώμα φόντου (προεπιλογή: 30)." -5 = "Ελάχιστη περιοχή:" -6 = "Ορίζει το ελάχιστο όριο περιοχής για μια φωτογραφία (προεπιλογή: 10000)." -7 = "Ελάχιστη περιοχή περιγράμματος:" -8 = "Ορίζει το ελάχιστο όριο περιοχής περιγράμματος για μια φωτογραφία" -9 = "Μέγεθος περιγράμματος:" - [scannerImageSplit] submit = "Εξαγωγή σαρώσεων εικόνων" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Τι κάνει αυτό" whatThisDoesDesc = "Εντοπίζει και εξάγει αυτόματα κάθε φωτογραφία από μια σαρωμένη σελίδα ή σύνθετη εικόνα—χωρίς χειροκίνητο κόψιμο." whenToUse = "Πότε να το χρησιμοποιείτε" +[ScannerImageSplit.selectText] +1 = "Όριο γωνίας:" +10 = "Ορίζει το μέγεθος του περιγράμματος που προστίθεται και αφαιρείται για την αποφυγή λευκών περιγραμμάτων στο αποτέλεσμα (προεπιλογή: 1)." +2 = "Ορίζει την ελάχιστη απόλυτη γωνία που απαιτείται για την περιστροφή της εικόνας (προεπιλογή: 10)." +3 = "Ανοχή:" +4 = "Καθορίζει το εύρος της χρωματικής διακύμανσης γύρω από το εκτιμώμενο χρώμα φόντου (προεπιλογή: 30)." +5 = "Ελάχιστη περιοχή:" +6 = "Ορίζει το ελάχιστο όριο περιοχής για μια φωτογραφία (προεπιλογή: 10000)." +7 = "Ελάχιστη περιοχή περιγράμματος:" +8 = "Ορίζει το ελάχιστο όριο περιοχής περιγράμματος για μια φωτογραφία" +9 = "Μέγεθος περιγράμματος:" + [search] noResults = "Δεν βρέθηκαν αποτελέσματα" placeholder = "Εισαγάγετε όρο αναζήτησης..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Ο διακομιστής σας Stirling-PDF είν [session] expired = "Η συνεδρία σας έληξε. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά." -[sessionManagement.tooltip] -header = "Διαχείριση συνεδριών υπογραφής" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Νέοι συμμετέχοντες προστίθενται στο τέλος της σειράς υπογραφών" -bullet2 = "Δεν μπορούν να προστεθούν συμμετέχοντες μετά την οριστικοποίηση της συνεδρίας" -bullet3 = "Κάθε συμμετέχων λαμβάνει ειδοποίηση όταν είναι η σειρά του" -description = "Μπορείτε να προσθέσετε περισσότερους συμμετέχοντες σε μια ενεργή συνεδρία οποτεδήποτε πριν από την οριστικοποίηση." -title = "Προσθήκη συμμετεχόντων" - -[sessionManagement.tooltip.finalization] -bullet1 = "Πλήρης οριστικοποίηση: Όλοι οι συμμετέχοντες έχουν υπογράψει" -bullet2 = "Μερική οριστικοποίηση: Ορισμένοι συμμετέχοντες δεν έχουν υπογράψει ακόμα" -bullet3 = "Οι μη υπογεγραμμένοι συμμετέχοντες θα εξαιρεθούν από το τελικό έγγραφο" -bullet4 = "Μόλις οριστικοποιηθεί, μπορείτε να φορτώσετε το υπογεγραμμένο PDF στα ενεργά αρχεία" -description = "Η οριστικοποίηση συνδυάζει όλες τις υπογραφές σε ένα ενιαίο υπογεγραμμένο PDF. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." -title = "Οριστικοποίηση συνεδρίας" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Δεν μπορούν να αφαιρεθούν συμμετέχοντες που έχουν ήδη υπογράψει" -bullet2 = "Οι αφαιρεθέντες συμμετέχοντες δεν λαμβάνουν πλέον ειδοποιήσεις" -bullet3 = "Η σειρά υπογραφών προσαρμόζεται αυτόματα" -description = "Οι συμμετέχοντες μπορούν να αφαιρεθούν από τις συνεδρίες πριν υπογράψουν." -title = "Αφαίρεση συμμετεχόντων" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Κάθε υπογραφή εφαρμόζεται διαδοχικά στο PDF" -bullet2 = "Οι μεταγενέστεροι υπογράφοντες μπορούν να δουν προηγούμενες υπογραφές" -bullet3 = "Κρίσιμο για ροές έγκρισης και νομικές αλυσίδες φύλαξης" -description = "Η σειρά που ορίζετε κατά τη δημιουργία της συνεδρίας καθορίζει ποιος υπογράφει πρώτος." -title = "Σειρά υπογραφών" - [settings] close = "Κλείσιμο" +[settings.ai] +documents = "Έγγραφα & RAG" +general = "Γενικά" +limits = "Όρια & Απόδοση" +models = "Μοντέλα & Πάροχοι" +title = "AI" + [settings.configuration] advanced = "Προχωρημένα" database = "Βάση δεδομένων" endpoints = "Endpoints" features = "Δυνατότητες" +folderAccess = "Πρόσβαση σε φακέλους" mcp = "MCP Server" storageSharing = "Αποθήκευση Αρχείων & Κοινή Χρήση" systemSettings = "Ρυθμίσεις συστήματος" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Προεπιλεγμένος επεξεργαστής PDF" defaultPdfEditorActive = "Το Stirling PDF είναι ο προεπιλεγμένος επεξεργαστής PDF" defaultPdfEditorChecking = "Έλεγχος..." defaultPdfEditorInactive = "Έχει οριστεί άλλη εφαρμογή ως προεπιλογή" +defaultPdfEditorRemind = "Υπενθύμιση να ορίσω ως προεπιλογή" +defaultPdfEditorRemindDescription = "Εμφάνιση banner όταν το Stirling PDF δεν είναι η προεπιλεγμένη εφαρμογή PDF σας." defaultPdfEditorSet = "Ήδη προεπιλογή" defaultStartupView = "Προεπιλεγμένη προβολή κατά την εκκίνηση" defaultStartupViewDescription = "Επιλέξτε ποια καρτέλα θα είναι ενεργή στην αριστερή στήλη κατά την εκκίνηση της εφαρμογής" @@ -8817,9 +9763,18 @@ languageDescription = "Επιλέξτε τη γλώσσα εμφάνισης" logout = "Αποσύνδεση" setAsDefault = "Ορισμός ως προεπιλογή" theme = "Θέμα" +themeAccent = "Χρώμα έμφασης" +themeAccentCustom = "Προσαρμοσμένο χρώμα" +themeAccentCustomHint = "Προσαρμοσμένο (συνιστώνται οι προεπιλογές)" +themeAccentDark = "Χρώμα έμφασης σκοτεινής λειτουργίας" +themeAccentDefault = "Προεπιλογή" +themeAccentDefaultHint = "Προεπιλεγμένο θέμα (συνιστάται)" +themeAccentDescription = "Τα κουμπιά, οι σύνδεσμοι και οι επισημάνσεις το ακολουθούν, και χρωματίζει διακριτικά την εφαρμογή. Η φωτεινή και η σκοτεινή λειτουργία έχουν η καθεμία το δικό της. Το σύστημα χρησιμοποιεί όποιο είναι ενεργό." +themeAccentLight = "Χρώμα έμφασης φωτεινής λειτουργίας" themeDark = "Σκούρο" themeDescription = "Εναλλαγή μεταξύ φωτεινής και σκοτεινής λειτουργίας" themeLight = "Φωτεινό" +themeReset = "Επαναφορά θέματος στην προεπιλογή" themeSystem = "System" title = "Γενικά" user = "Χρήστης" @@ -8833,6 +9788,12 @@ intro = "Ενεργοποιήστε τον έλεγχο ταυτότητας χ learnMore = "Μάθετε περισσότερα στην τεκμηρίωση" title = "Για διαχειριστές συστήματος" +[settings.general.loginLanding] +description = "Επιλέξτε ποια εφαρμογή ανοίγει όταν συνδέεστε στο Stirling Cloud." +editor = "Πρόγραμμα επεξεργασίας" +processor = "Επεξεργαστής" +title = "Μετά τη σύνδεση" + [settings.general.mode] fullscreen = "Πλήρης οθόνη" sidebar = "Πλευρική γραμμή" @@ -9013,9 +9974,6 @@ title = "Πολιτικές & Απόρρητο" [settings.preferences] title = "Προτιμήσεις" -[settings.search] -placeholder = "Αναζήτηση στις σελίδες ρυθμίσεων..." - [settings.security] description = "Ενημερώστε τον κωδικό σας για να διατηρήσετε τον λογαριασμό σας ασφαλή." title = "Ασφάλεια" @@ -9041,6 +9999,11 @@ security = "Ασφάλεια" telegram = "Telegram" title = "Ασφάλεια & Αυθεντικοποίηση" +[settings.signOut] +confirm = "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε;" +submit = "Αποσύνδεση" +title = "Αποσύνδεση" + [settings.team] title = "Ομάδα" @@ -9156,6 +10119,7 @@ last = "Τελευταία σελίδα" maintainRatio = "Εναλλαγή διατήρησης αναλογίας διαστάσεων" next = "Επόμενη σελίδα" noSavedSigs = "Δεν βρέθηκαν αποθηκευμένες υπογραφές" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Προσωπικές υπογραφές" previous = "Προηγούμενη σελίδα" redo = "Επανάληψη" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Αποτυχία αφαίρεσης φόντου hint = "Μεταφορτώστε εικόνα PNG ή JPG της υπογραφής σας" label = "Μεταφορτώστε εικόνα υπογραφής" placeholder = "Επιλέξτε αρχείο εικόνας" +previewAlt = "Current image signature" processing = "Επεξεργασία εικόνας..." removeBackground = "Αφαίρεση λευκού φόντου (διαφάνεια)" @@ -9207,6 +10172,17 @@ saved = "Επιλέξτε μια αποθηκευμένη υπογραφή πα text = "Αφού εισαγάγετε το όνομά σας παραπάνω, κάντε κλικ οπουδήποτε στο PDF για να τοποθετήσετε την υπογραφή σας." title = "Πώς να προσθέσετε υπογραφή" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Μετακίνηση υπογραφής" pause = "Παύση τοποθέτησης" @@ -9653,10 +10629,30 @@ title = "Μεταφόρτωση στον διακομιστή" updateButton = "Ενημέρωση στον διακομιστή" uploadButton = "Μεταφόρτωση στον διακομιστή" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "κάντε κλικ εδώ" +fallback = "Αν δεν άνοιξε αυτόματα," +opening = "Άνοιγμα του Swagger UI σε νέα καρτέλα..." tags = "api,documentation,swagger,endpoints,development" title = "Τεκμηρίωση API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Επιλέξτε αρχεία στα Ενεργά Αρχεί singleFileScope = "Εφαρμογή μόνο σε: {{fileName}}" viewerMode = "Μεταβείτε στον επεξεργαστή αρχείων για να επιλέξετε πολλά αρχεία." +[toolFormat] +ANY = "οποιοδήποτε αρχείο" +CBR = "ένα αρχείο κόμικ (CBR)" +CBZ = "ένα αρχείο κόμικ (CBZ)" +CSV = "ένα αρχείο CSV" +EBOOK = "ένα ηλεκτρονικό βιβλίο" +EMAIL = "ένα email" +EXCEL = "ένα υπολογιστικό φύλλο" +HTML = "μια ιστοσελίδα" +IMAGE = "εικόνες" +JAVASCRIPT = "ένα αρχείο JavaScript" +JSON = "ένα αρχείο JSON" +MARKDOWN = "ένα αρχείο Markdown" +NONE = "κανένα αρχείο" +PCL = "ένα αρχείο εκτύπωσης PCL" +PDF = "ένα PDF" +PDF_ENCRYPTED = "ένα PDF προστατευμένο με κωδικό πρόσβασης" +POSTSCRIPT = "ένα αρχείο PostScript" +PPT = "μια παρουσίαση" +TEXT = "ένα αρχείο κειμένου" +VIDEO = "ένα βίντεο" +WORD = "ένα έγγραφο Word" +XML = "ένα αρχείο XML" +XPS = "ένα έγγραφο XPS" +ZIP = "ένα αρχείο ZIP" + [toolPanel] alpha = "Άλφα" backToAllTools = "Πίσω σε όλα τα εργαλεία" collapse = "Σύμπτυξη πάνελ" expand = "Ανάπτυξη πάνελ" goBack = "Επιστροφή" +pdfTools = "Εργαλεία PDF" placeholder = "Επιλέξτε ένα εργαλείο για να ξεκινήσετε" premiumFeature = "Premium λειτουργία:" toolsHeader = "Εργαλεία" @@ -9806,11 +10829,6 @@ noToolsFound = "Δεν βρέθηκαν εργαλεία" quickAccess = "ΓΡΗΓΟΡΗ ΠΡΟΣΒΑΣΗ" searchPlaceholder = "Αναζήτηση εργαλείων..." -[toolPicker.categories] -advancedTools = "Προχωρημένα εργαλεία" -recommendedTools = "Προτεινόμενα εργαλεία" -standardTools = "Τυπικά εργαλεία" - [toolPicker.subcategories] advancedFormatting = "Προχωρημένη μορφοποίηση" automation = "Αυτοματοποίηση" @@ -9824,6 +10842,9 @@ removal = "Αφαίρεση" signing = "Υπογραφή" verification = "Επαλήθευση" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Δεν βρέθηκαν εργαλεία" noTools = "Δεν υπάρχουν διαθέσιμα εργαλεία" @@ -9950,28 +10971,53 @@ title = "Χρήστες" you = "(εσείς)" [users.action] +cancelInvite = "Ακύρωση πρόσκλησης" deleteTeam = "Διαγραφή ομάδας" disableMfa = "Επαναφορά MFA" move = "Μετακίνηση σε ομάδα" reinstate = "Επαναφορά" remove = "Αφαίρεση από τον οργανισμό" +removeTeam = "Αφαίρεση από την ομάδα" rename = "Μετονομασία ομάδας" resetPw = "Επαναφορά κωδικού πρόσβασης" suspend = "Αναστολή" unlock = "Ξεκλείδωμα λογαριασμού" +[users.activity] +daysAgo = "{{count}}ημ. πριν" +hoursAgo = "{{count}}ώ. πριν" +justNow = "Μόλις τώρα" +minutesAgo = "{{count}}λ. πριν" +monthsAgo_one = "πριν από {{count}} μήνα" +monthsAgo_other = "πριν από {{count}} μήνες" +never = "Ποτέ" +weeksAgo_one = "πριν από {{count}} εβδομάδα" +weeksAgo_other = "πριν από {{count}} εβδομάδες" +yearsAgo_one = "πριν από {{count}} έτος" +yearsAgo_other = "πριν από {{count}} έτη" + [users.cap] addProcessor = "+ Processor" approver = "Εγκρίνει πολιτική" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Να ακυρωθεί η πρόσκληση προς {{email}}; Δεν θα μπορούν να συμμετάσχουν με τον τρέχοντα σύνδεσμο." +cancelInviteTitle = "Ακύρωση πρόσκλησης" deleteTeamBody = "Διαγραφή της ομάδας {{name}}; Η ομάδα πρέπει πρώτα να είναι άδεια - μετακινήστε τα μέλη της σε άλλη ομάδα και δεν μπορεί ακόμη να κατέχει διαμορφώσεις integration." deleteTeamTitle = "Διαγραφή ομάδας" disableMfaBody = "Αφαίρεση της εγγραφής MFA του {{name}}; Θα τη ρυθμίσουν ξανά στην επόμενη σύνδεση, αν απαιτείται." disableMfaTitle = "Επαναφορά MFA" removeBody = "Οριστική αφαίρεση του {{name}} από τον οργανισμό; Αυτό δεν μπορεί να αναιρεθεί." +removeTeamBody = "Να αφαιρεθεί ο/η {{name}} από την ομάδα; Θα διατηρήσει τον λογαριασμό του/της, αλλά θα χάσει την πρόσβαση στους πόρους αυτής της ομάδας." removeTitle = "Αφαίρεση μέλους" [users.empty] @@ -9982,10 +11028,8 @@ title = "Δεν υπάρχουν ακόμη μέλη" addToTeam = "Προσθήκη σε ομάδα" guestCount = "{{count}} επισκέπτης" guests = "Επισκέπτες" -guestsDesc = "Εξωτερικοί συνεργάτες, περιορισμένοι σε όσα μοιραστήκατε. Μόνο Editor." ledBy = "με επικεφαλής {{owner}}" org = "Οργανισμός" -orgDesc = "Κάτοχοι με εξουσία σε όλο τον οργανισμό και έγκριση πολιτικών" owners = "{{count}} κάτοχος" team = "Ομάδα {{name}}" teamMeta = "{{count}} άτομα" @@ -10022,6 +11066,18 @@ username = "Όνομα χρήστη" usernameError = "Το όνομα χρήστη πρέπει να έχει τουλάχιστον 3 χαρακτήρες" usernamePlaceholder = "jsmith" +[users.invites] +by = "Προσκλήθηκε από {{who}}" +cancel = "Ακύρωση" +expiresInDays_one = "Λήγει σε {{count}} ημέρα" +expiresInDays_other = "Λήγει σε {{count}} ημέρες" +expiresToday = "Λήγει σήμερα" +title = "Εκκρεμείς προσκλήσεις" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Κάτι πήγε στραβά κατά την επικοινωνία με το backend ή δεν έχετε πρόσβαση. Δοκιμάστε ξανά." title = "Δεν ήταν δυνατή η φόρτωση των μελών" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Αναζήτηση συνημμένων" title = "Συνημμένα" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Τίτλος σελιδοδείκτη" +bookmarkTitleRequired = "Απαιτείται τίτλος σελιδοδείκτη" closeSidebar = "Κλείσιμο πλαϊνής γραμμής σελιδοδεικτών" collapseAll = "Σύμπτυξη όλων των σελιδοδεικτών" +empty = "No bookmarks in this document" expandAll = "Ανάπτυξη όλων των σελιδοδεικτών" +searchPlaceholder = "Αναζήτηση σελιδοδεικτών" +title = "Bookmarks" [viewer.comments] addComment = "Προσθήκη σχολίου" @@ -10236,10 +11297,12 @@ locateAnnotation = "Εντοπισμός στο έγγραφο" moreActions = "Περισσότερες ενέργειες" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Σελίδα {{page}}" placingHint = "Κάντε κλικ σε μια σελίδα για τοποθέτηση… (ακύρωση)" removeCommentOnly = "Κατάργηση μόνο του σχολίου" saveReply = "Αποθήκευση απάντησης" +searchPlaceholder = "Search comments" title = "Σχόλια" typeComment = "Σχόλιο" typeInsertText = "Εισαγωγή κειμένου" @@ -10262,6 +11325,7 @@ unsavedDesc = "Έχετε μη αποθηκευμένες αλλαγές" closeSidebar = "Κλείσιμο πλαϊνής γραμμής επιπέδων" hideAll = "Απόκρυψη όλων των επιπέδων" showAll = "Εμφάνιση όλων των επιπέδων" +title = "Layers" [viewer.link] delete = "Διαγραφή συνδέσμου" @@ -10605,30 +11669,6 @@ title = "Κείμενο υδατογραφήματος" image = "Εικόνα" text = "Κείμενο" -[wetSignature.tooltip] -header = "Μέθοδοι δημιουργίας υπογραφής" - -[wetSignature.tooltip.draw] -bullet1 = "Προσαρμόστε το χρώμα και το πάχος της πένας" -bullet2 = "Καθαρίστε και ξανασχεδιάστε μέχρι να είστε ικανοποιημένοι" -bullet3 = "Λειτουργεί σε συσκευές αφής (tablet, τηλέφωνα)" -description = "Δημιουργήστε χειρόγραφη υπογραφή με το ποντίκι ή την οθόνη αφής. Ιδανική για προσωπικές, αυθεντικές υπογραφές." -title = "Σχεδίαση υπογραφής" - -[wetSignature.tooltip.type] -bullet1 = "Επιλέξτε ανάμεσα σε πολλές γραμματοσειρές" -bullet2 = "Προσαρμόστε μέγεθος και χρώμα κειμένου" -bullet3 = "Ιδανικό για τυποποιημένες υπογραφές" -description = "Δημιουργήστε υπογραφή από πληκτρολογημένο κείμενο. Γρήγορη και συνεπής, κατάλληλη για επαγγελματικά έγγραφα." -title = "Πληκτρολόγηση υπογραφής" - -[wetSignature.tooltip.upload] -bullet1 = "Υποστηρίζει PNG, JPG και άλλες μορφές εικόνας" -bullet2 = "Συνιστώνται διαφανή φόντα για καλύτερα αποτελέσματα" -bullet3 = "Η εικόνα θα αλλάξει μέγεθος ώστε να ταιριάζει στην περιοχή υπογραφής" -description = "Μεταφορτώστε μια προδημιουργημένη εικόνα υπογραφής. Ιδανική αν έχετε σαρωμένη υπογραφή ή εταιρικό λογότυπο." -title = "Μεταφόρτωση εικόνας υπογραφής" - [workbenchBar] activeFiles = "Ενεργά αρχεία" annotations = "Σχολιασμοί" @@ -10642,6 +11682,7 @@ exitRedaction = "Έξοδος από λειτουργία απόκρυψης" exportAll = "Εξαγωγή PDF" exportSelected = "Εξαγωγή επιλεγμένων σελίδων" formFill = "Συμπλήρωση φόρμας" +hideToolbar = "Hide toolbar" multiTool = "Πολυεργαλείο" panMode = "Λειτουργία μετακίνησης" print = "Εκτύπωση PDF" @@ -10652,6 +11693,7 @@ redact = "Απόκρυψη" rotateLeft = "Περιστροφή αριστερά" rotateRight = "Περιστροφή δεξιά" ruler = "Χάρακας / Μέτρηση" +rulerSettings = "Ρυθμίσεις κλίμακας" save = "Αποθήκευση" saveAll = "Αποθήκευση όλων" saveAs = "Αποθήκευση ως" @@ -10661,6 +11703,7 @@ selectAll = "Επιλογή όλων" selectByNumber = "Επιλογή βάσει αριθμών σελίδων" selectLanguage = "Επιλέξτε γλώσσα" share = "Κοινή χρήση" +showToolbar = "Show toolbar" toggleAnnotations = "Εναλλαγή ορατότητας σχολιασμών" toggleAttachments = "Εναλλαγή συνημμένων" toggleBookmarks = "Εναλλαγή σελιδοδεικτών" diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml index fba28f6872..3f47ffe965 100644 --- a/frontend/editor/public/locales/en-GB/translation.toml +++ b/frontend/editor/public/locales/en-GB/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alphabet" apply = "Apply" applyAndContinue = "Save & Leave" -areYouSure = "Are you sure you want to leave?" back = "Back" black = "Black" blue = "Blue" @@ -46,10 +45,8 @@ filesSelected = "{{count}} files" font = "Font" fontSizeTooltip = "Size of the page number text in points. Larger numbers create bigger text." fontTypeTooltip = "Font family for the page numbers. Choose based on your document style." -genericSubmit = "Submit" green = "Green" help = "Help" -imgPrompt = "Select Image(s)" incorrectPasswordMessage = "Current password is incorrect." info = "Info" invalidUndoData = "Cannot undo: invalid operation data" @@ -62,7 +59,6 @@ loginAgreementTitle = "Login Agreement" logOut = "Log out" marginTooltip = "Distance between the page number and the edge of the page." moreOptions = "More Options" -multiPdfPrompt = "Select PDFs (2+)" never = "Never" no = "No" noFileSelected = "No file loaded. Please upload one." @@ -80,7 +76,6 @@ pageOrderPrompt = "Custom Page Order (Enter a comma-separated list of page numbe pages = "Pages" pageSelectionPrompt = "Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1)" password = "Password" -pdfPrompt = "Select PDF(s)" pin = "Pin File (keep active after tool run)" pro = "Pro" processingComplete = "Your file is ready." @@ -110,6 +105,7 @@ undoSuccess = "Operation undone successfully" unknown = "Unknown" unpin = "Unpin File (replace after tool run)" unsavedChanges = "You have unsaved changes to your PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Unsaved Changes" unsupported = "Unsupported" username = "Username" @@ -356,6 +352,7 @@ text = "These permissions control what users can do with the PDF. Most effective title = "Change Permissions" [addStamp] +preview = "Preview stamp" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manual downloads: click the links and place the files into the te noLanguages = "No tessdata languages found in the configured directory." permissionNotice = "The tessdata path is not writable. Downloads will be opened in the browser; please save the .traineddata files manually into the tessdata folder." +[admin.settings.ai.documents] +description = "Configure the embedding model and retrieval settings used to answer questions over documents. Applied to the AI engine when saved." +title = "Documents & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Leave blank to use the engine's own environment credential. Applies to self-hosted single-engine deployments." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Saved - leave blank to keep the current key" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL of the OpenAI-compatible / Ollama embeddings endpoint, e.g. http://ollama:11434/v1. Must point at a trusted internal endpoint (SSRF-sensitive)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding model name. Free text; suggestions are hints only." +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provider used to turn document text into vector embeddings." +label = "Embedding provider" + +[admin.settings.ai.documents.maxSearches] +description = "Maximum number of retrieval searches the agent may run per request." +label = "Max searches" + +[admin.settings.ai.documents.reindexNote] +body = "Changing the embedding model takes effect immediately, but documents indexed with the previous model must be re-indexed for search to return correct results." +title = "Re-index required" + +[admin.settings.ai.documents.saved] +reindexNote = "If you changed the embedding model, re-index existing documents so search uses the new model." + +[admin.settings.ai.documents.topK] +description = "Number of most relevant chunks retrieved per search." +label = "Top K" + +[admin.settings.ai.general] +description = "Connect Stirling to the Python AI engine and choose which AI capabilities are exposed. Changes apply on restart." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Turn individual AI features on or off. Disabled features are hidden in the app." +title = "Capabilities" + +[admin.settings.ai.general.enabled] +description = "Master switch. When off, no AI tools, agents, or engine calls are available." +label = "Enable AI" + +[admin.settings.ai.general.features.chat] +description = "Conversational assistant for working with PDFs." +label = "Chat assistant" + +[admin.settings.ai.general.features.classify] +description = "Automatically categorise documents by type or content." +label = "Document classification" + +[admin.settings.ai.general.features.createPdf] +description = "Generate a new PDF (e.g. from HTML) via an AI agent." +label = "Create PDF from prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Ask questions and get answers grounded in an uploaded document." +label = "Document questions" + +[admin.settings.ai.general.features.mathAuditor] +description = "Review documents for mathematical and numerical errors." +label = "Maths auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "Add AI-authored review comments and annotations to a PDF." +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout for heavier agent operations such as document generation." +label = "Long-running timeout (seconds)" + +[admin.settings.ai.general.note] +body = "The AI engine runs as a separate service. Its shared secret" +body2 = "is set via a container environment variable. Provider API keys can be entered on these pages or supplied as engine environment variables; keys entered here are pushed to the engine when saved." +title = "About the AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout for streamed (token-by-token) chat responses." +label = "Stream timeout (seconds)" + +[admin.settings.ai.general.test] +button = "Test connection" +failBody = "The AI engine did not respond. Check the URL, that the engine container is running, and that AI is enabled (a restart is needed after enabling)." +failTitle = "AI engine unreachable" +okBody = "The AI engine responded to a health check." +okTitle = "AI engine reachable" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout for standard AI requests to the engine." +label = "Request timeout (seconds)" + +[admin.settings.ai.general.url] +description = "Internal URL of the Python AI engine, e.g. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Guardrails for how much work AI requests may do and how many run concurrently. Applied to the AI engine when saved." +title = "Limits & Performance" + +[admin.settings.ai.limits.maxCharacters] +description = "Guardrail: reject AI requests whose extracted text exceeds this length." +label = "Max characters per request" + +[admin.settings.ai.limits.maxPages] +description = "Guardrail: reject AI requests over this many PDF pages." +label = "Max pages per request" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximum simultaneous in-flight model calls across the whole engine." +label = "Model max concurrency" + +[admin.settings.ai.models] +description = "Choose the LLM provider and the smart/fast models the AI engine uses. Applied to the AI engine when saved." +title = "Models & Providers" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Leave blank to use the engine's own environment credential. Applies to self-hosted single-engine deployments." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Saved - leave blank to keep the current key" + +[admin.settings.ai.models.baseUrl] +description = "Base URL of the OpenAI-compatible / Ollama endpoint, e.g. http://ollama:11434/v1." +label = "Provider base URL" +warning = "The base URL must point at a trusted internal endpoint. The engine will make server-side requests to it, so an untrusted value is SSRF-sensitive." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximum output tokens for the fast model." +label = "Fast model max tokens" + +[admin.settings.ai.models.fastModel] +description = "Cheaper, faster model for lightweight tasks. Free text; suggestions are hints only." +label = "Fast model" + +[admin.settings.ai.models.provider] +description = "Which LLM provider the engine talks to." +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximum output tokens for the smart model." +label = "Smart model max tokens" + +[admin.settings.ai.models.smartModel] +description = "High-capability model for complex reasoning. Free text; suggestions are hints only." +label = "Smart model" + +[admin.settings.ai.saved] +body = "Changes are pushed to the AI engine automatically." +bodyNoPush = "Settings saved. They will apply the next time the AI engine picks up its configuration." +title = "AI settings saved" + [admin.settings.badge] clickToUpgrade = "Click to view plan details" @@ -938,6 +1094,25 @@ label = "Regenerate on Startup" description = "Number of days the certificate will be valid" label = "Certificate Validity (days)" +[admin.settings.folderAccess] +description = "Directories that folder sources and folder outputs are allowed to read from and write to. This is a security boundary: automations can never be pointed at a server path outside this list." +securityNote = "Leave this empty to disable folder sources and outputs entirely. Stirling's own configuration directory is always off-limits." +title = "Folder Access" + +[admin.settings.folderAccess.implied] +description = "These Stirling-managed directories are always permitted and can't be changed here." +serverStorage = "Server file storage" +title = "Always allowed" +watchedFolder = "Pipeline watched folder" + +[admin.settings.folderAccess.roots] +add = "Add" +empty = "No folders allowed. Folder sources and outputs are currently disabled." +hint = "Enter absolute paths, for example /data/inbox." +label = "Allowed folder roots" +placeholder = "/data/inbox" +remove = "Remove folder root" + [admin.settings.general] description = "Configure system-wide application settings including branding and default behaviour." system = "System" @@ -1704,6 +1879,9 @@ width = "Width" [app] description = "The Free Adobe Acrobat alternative (10M+ Downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convert to PDF/A-3b" convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments" @@ -1842,6 +2020,15 @@ export = "Export" accessDenied = "Access Denied" insufficientPermissions = "You do not have permission to perform this action." +[auth.callback] +completing = "Completing authentication" +invalidToken = "OAuth login failed - invalid token." +missingToken = "OAuth login failed - no token received." +oauthFailed = "OAuth login failed. Please try again." +pleaseWait = "Please wait while we finish signing you in." +signedOut = "You have been signed out. Please sign in again." +windowMayClose = "You can close this window once it completes." + [auth.displayName] guest = "Guest" user = "User" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Secures PDF documents by removing potentially malicious co description = "This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text." tags = "auto-detect,header-based,organise,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Auto Rotate" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Confidence threshold" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "OCR only" +text = "Text only" +title = "Detection method" + +[autoRotate.error] +failed = "An error occurred while auto-rotating the PDF." + +[autoRotate.inferUndetected] +title = "Rotate low-confidence pages to match" + +[autoRotate.report] +applied = "{{degrees}}° CW" +confidenceValue = "Confidence {{value}}" +noChange = "No change" +pageLabel = "Page {{number}}" +summary = "{{rotated}} of {{total}} pages rotated, {{unchanged}} left as they are" +title = "Detection report" + +[autoRotate.report.method] +inferred = "Matched" +none = "Skipped" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Below threshold" +blankPage = "Page is blank" +inferredFromDocument = "Same as the other pages" +noDominantDirection = "Mixed text directions" +osdFailed = "No readable text found" +osdNoVerdict = "Could not tell which way up" +tesseractUnavailable = "OCR not installed" +tooFewGlyphs = "Too little text" + +[autoRotate.results] +title = "Auto Rotate Results" + +[autoRotate.settings] +title = "Settings" + +[autoRotate.tooltip.confidenceThreshold] +text = "How certain the check has to be before a page is turned. Lower it to rotate more pages, raise it to leave more pages untouched." +title = "Confidence threshold" + +[autoRotate.tooltip.description] +text = "Works out which way up each page should be and turns it the right way round. Pages that are already upright are left alone." + +[autoRotate.tooltip.detectionMode] +text = "Auto reads the text on the page first, then looks at the page as an image if that is not enough. Text only skips the image check, which is quickest for documents that were never scanned. OCR only treats every page as an image." +title = "Detection method" + +[autoRotate.tooltip.header] +title = "Auto Rotate" + +[autoRotate.tooltip.inferUndetected] +text = "Blank or nearly blank pages cannot be checked on their own. When every page that could be read needs the same turn, these pages are turned to match them." +title = "Rotate low-confidence pages to match" + [autoSizeSplitPDF] tags = "pdf,split,document,organisation" @@ -2189,33 +2443,6 @@ keywords = "Keywords: odd, even" numbers = "Numbers/ranges: 5, 10-20" progressions = "Progressions: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Certificate Types" - -[certificateChoice.tooltip.organization] -bullet1 = "Managed by system administrators" -bullet2 = "Shared across authorised users" -bullet3 = "Represents company identity, not individual" -bullet4 = "Best for: Official documents, team signatures" -description = "A shared certificate provided by your organisation. Used for company-wide signing authority." -title = "Organisation Certificate" - -[certificateChoice.tooltip.personal] -bullet1 = "Generated automatically on first use" -bullet2 = "Tied to your user account" -bullet3 = "Cannot be shared with other users" -bullet4 = "Best for: Personal documents, individual accountability" -description = "An auto-generated certificate unique to your user account. Suitable for individual signatures." -title = "Personal Certificate" - -[certificateChoice.tooltip.upload] -bullet1 = "Requires P12/PFX file and password" -bullet2 = "Can be issued by external Certificate Authorities" -bullet3 = "Higher trust level for legal documents" -bullet4 = "Best for: Legally binding contracts, external validation" -description = "Use your own PKCS#12 certificate file. Provides full control over certificate properties." -title = "Upload Custom P12" - [certSign] awaitingSignatures = "Awaiting signatures" chooseCertificate = "Choose Certificate File" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Certificate source" upload = "Upload" -[certSign.tooltip.header] -title = "About Managing Signatures" - -[certSign.tooltip.overview] -bullet1 = "Check existing signatures and their validity" -bullet2 = "View detailed information about signers and certificates" -bullet3 = "Add new digital signatures to secure your documents" -bullet4 = "Multiple files supported with easy navigation" -text = "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing." -title = "What can this tool do?" - -[certSign.tooltip.signing] -bullet1 = "Supports PEM, PKCS12, JKS, and server certificate formats" -bullet2 = "Option to show or hide signature on the PDF" -bullet3 = "Add reason, location, and signer name" -bullet4 = "Choose which page to place visible signatures" -bullet5 = "Use server certificate for simple 'Sign with Stirling-PDF' option" -text = "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only." -title = "Adding Signatures" - -[certSign.tooltip.validation] -bullet1 = "Shows if signatures are valid or invalid" -bullet2 = "Displays signer information and signing date" -bullet3 = "Checks if the document was modified after signing" -bullet4 = "Can use custom certificates for verification" -text = "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing." -title = "Checking Signatures" - [changeCreds] changeUsername = "Update your username. You will be logged out after updating." credsUpdated = "Account updated" @@ -2737,12 +2936,16 @@ text = "To make these permissions unchangeable, use the Add Password tool to set [chat.actions] copy = "Copy message" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling agent options" +assistant = "AI Assistant" clearChat = "Clear chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Runs in the cloud (included, no extra charge)" tooltip = "Pick colour from screen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Choose colour" [common] +actions = "Actions" back = "Back" cancel = "Cancel" close = "Close" +codeSample = "Code sample" collapse = "Collapse" confirm = "Confirm" continue = "Continue" copied = "Copied!" copy = "Copy" done = "Done" +enterValue = "Enter value" error = "Error" expand = "Expand" loading = "Loading..." next = "Next" +open = "Open" preview = "Preview" previous = "Previous" refresh = "Refresh" +remaining = "Remaining" retry = "Retry" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Clear selected" @@ -2988,6 +3199,7 @@ title = "Compression Method" [compress.settings] desiredSize = "Desired File Size" desiredSizePlaceholder = "Enter size" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Compression is an easy way to reduce your file size. Pick File Size to enter a target size and have us adjust quality for you. Pick Quality to set compression strength manually." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF Options" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Choose a source format first" settings = "Settings" single = "Single" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Advanced monitoring" -allInOneWorkspace = "All-in-one PDF workspace (viewer, tools & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Dedicated support & SLAs" -fasterThroughput = "10x faster throughput" -fullyPrivateFiles = "Fully private files" -largeFileProcessing = "Large file processing" -orgWideAccess = "Org-wide access controls" -premiumAiModels = "Premium AI models" -prioritySupport = "Priority support" -privateDocCloud = "Private Document Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Secure API access" -standardThroughput = "Standard throughput" -unlimitedApiAccess = "Unlimited API access" -unlimitedSeats = "Unlimited seats" - [crop] autoCrop = "Auto-crop whitespace" noFileSelected = "Add a PDF file to begin cropping" @@ -3346,6 +3558,7 @@ message = "Failed to set default PDF handler" title = "Error" [defaultApp.prompt] +dontRemind = "Don't remind me again" message = "Make Stirling PDF your default application for opening PDF files." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Search..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Edit" @@ -3644,11 +3861,13 @@ shareSelected = "Share Files" sharing = "Sharing" showAll = "Show All" showHistory = "Show History" +sortBy = "Sort files" sortByDate = "Sort by Date" sortByName = "Sort by Name" sortBySize = "Sort by Size" storage = "Storage" storageState = "Storage" +storageUsed = "Storage used" synced = "Synced" title = "Upload PDF Files" toolChain = "Tools Applied" @@ -3694,13 +3913,17 @@ addFiles = "Add files" addingFiles = "Adding files…" collapse = "Collapse sidebar" customizeGroups = "Customise groups" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Open files to get started" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expand sidebar" -files = "Files" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive is not configured" leaveMyFiles = "Leave My Files" +library = "PDF Library" myFiles = "My Files" noFiles = "No files yet" openFileManager = "Browse all files & folders" @@ -3708,37 +3931,36 @@ openFromComputer = "Open from computer" openSettings = "Open settings" other = "Other" recent = "Recent" -search = "Search" -searchPlaceholder = "Search files..." viewAll = "View all {{count}} files" [fileSidebar.fileItem] closeViewer = "Close viewer" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Open in viewer" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Add" -addLabel = "Add a label…" -categoryIconAria = "Choose an icon for {{name}}" -createCategory = "Create category" -delete = "Delete category" done = "Done" -hide = "Hide category" -newCategory = "New category name…" -removeLabel = "Remove {{name}}" -rename = "Rename" reset = "Reset to defaults" -search = "Search labels…" -show = "Show category" subtitle = "Group your files into parent categories. Add existing or new labels to a category, rename it, or create your own. Files in none of your categories appear under “Other”." title = "Sidebar categories" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Add to workspace" addToWorkspaceCount = "Add {{count}} to workspace" @@ -3749,7 +3971,6 @@ backToMyFiles = "Back to My Files" breadcrumbs = "Folder path" cancel = "Cancel" classification = "Classification" -clearSearch = "Clear search" clearSelection = "Clear selection" closeDetails = "Close details" create = "Create" @@ -3781,6 +4002,7 @@ downloadAll = "Download all" downloadVersion = "Download this version" dropOverlay = "Drop files to upload" dropOverlaySub = "Files start in Local. Use 'Move to' or 'Save to cloud' to organise them into a folder." +duplicate = "Duplicate" file = "File" fileInfo = "File info" fileMenu = "File actions" @@ -3808,13 +4030,12 @@ refresh = "Refresh from server" remove = "Delete" removeVersion = "Remove this version" rename = "Rename" +renamed = "Renamed" renameFolder = "Rename folder" resizeFolderTree = "Resize folder tree (arrow keys, Shift for bigger steps; double-click to auto-fit)" save = "Save" saveToServer = "Save to server" saveToServerDisabledHint = "Saving to the server isn't enabled on this server. Ask your admin to enable it." -search = "Search" -searchPlaceholder = "Search this folder & subfolders" selectAll = "Select all" selectAllHint = "Click to select all. Tip: hold Ctrl (or Cmd) to add files one at a time, Shift to select a range." selectedCount = "{{count}} selected" @@ -3874,6 +4095,10 @@ title = "No cloud files yet" hint = "Files saved without uploading stay here. Drop a file to add one." title = "No local-only files" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Files you open or edit will appear here." title = "Nothing modified yet" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Could not delete folder." deleteFolderFailedDetail = "Could not delete folder: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Could not update folder appearance." folderAppearanceFailedDetail = "Could not update folder appearance: {{message}}" moveFilesFailed = "Could not move files." @@ -3910,9 +4137,7 @@ count = "Files" folder = "Folder" labels = "Labels" modified = "Modified" -name = "Name" size = "Size" -toolHistoryAtVersion = "Cumulative tool chain" totalSize = "Total size" type = "Type" versionHistory = "Version journey" @@ -3945,6 +4170,11 @@ localHint = "Only stored in this browser" shared = "Shared" sharedHint = "Shared with you via link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Oldest first" modifiedDesc = "Recent first" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "No files available in storage. Open some files first." open = "Open" openFile = "Open File" openFiles = "Open Files" +selectFile = "Select {{name}}" selectFromStorage = "Select from Storage" upload = "Upload" uploadFile = "Upload File" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "All saved" +analyzingFields = "Analysing form fields..." +extractCsvError = "Failed to extract CSV" +extractXlsxError = "Failed to extract XLSX" +filled = "filled" flattenAfterFilling = "Flatten after filling" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "req" +requiredFieldsError = "Please fill in all required fields" rescanFields = "Re-scan fields" rescanFormFields = "Re-scan form fields" save = "Save" +saveShortcut = "Ctrl+S to save" +unsavedChanges = "Unsaved changes" [formFill.sidebar] close = "Close sidebar" @@ -4309,6 +4552,11 @@ desc = "Auto renames a PDF file based on its detected header" tags = "auto-detect,header-based,organise,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "Auto Rename PDF File" +[home.autoRotate] +desc = "Detect each page's orientation and rotate it upright automatically." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Auto Rotate" + [home.autoSizeSplitPDF] tags = "auto,split,size" title = "Auto Split by Size/Count" @@ -4608,15 +4856,12 @@ hint = "Upload a PNG, JPG, SVG, or other image file to place on the PDF. SVG fil label = "Upload Image" placeholder = "Select image file" -[imageToPdf] -tags = "conversion,img,jpg,picture,photo" - [imageToPDF] header = "Image to PDF" title = "Image to PDF" -[infoBanner] -dismiss = "Dismiss" +[imageToPdf] +tags = "conversion,img,jpg,picture,photo" [invite] acceptError = "Failed to create account" @@ -4647,11 +4892,9 @@ welcomeTitle = "You've been invited!" [landing] addFiles = "Add Files" -heroSubtitle = "Drop in or add an existing PDF to get started." mobileUpload = "Upload from Mobile" openFromComputer = "Open from computer" uploadFromComputer = "Upload from computer" -workbenchEmptyStateHero = "Drop a PDF anywhere" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Enter username" failedToSignIn = "Failed to sign in with {{provider}}: {{message}}" forgotPassword = "Forgot your password?" loggingIn = "Logging In..." -logIn = "Log In" login = "Login" magicLinkSent = "Magic link sent to {{email}}! Check your email and click the link to sign in." mfaCode = "Authentication Code" @@ -4715,26 +4957,10 @@ signInWith = "Sign in with" title = "Sign in" unexpectedError = "Unexpected error: {{message}}" updatePassword = "Update password" -useEmailInstead = "Login with email" useMagicLink = "Use magic link instead" username = "Username" youAreLoggedIn = "You are logged in!" -[login.slides.edit] -alt = "Edit PDFs" -subtitle = "With over a dozen tools to help you redact, sign, read and manipulate PDFs, you will be sure to find what you are looking for." -title = "Edit PDFs to display/secure the information you want" - -[login.slides.overview] -alt = "Stirling PDF overview" -subtitle = "A privacy-first cloud suite for PDFs that lets you convert, sign, redact, and manage documents, along with 50+ other powerful tools." -title = "Your one-stop-shop for all your PDF needs." - -[login.slides.secure] -alt = "Secure PDFs" -subtitle = "Add passwords, redact content, and manage certificates with ease." -title = "Protect sensitive information in your PDFs" - [margin] large = "Large" medium = "Medium" @@ -4818,6 +5044,34 @@ uploadSuccess = "Upload Successful!" uploadSuccessMessage = "Your images have been transferred." validating = "Validating session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan to upload photos. Images auto-convert to PDF." descriptionNoConvert = "Scan to upload photos from your mobile device." @@ -4836,8 +5090,13 @@ title = "Upload from Mobile" tags = "Multi Tool,Multi operation,UI,click drag,front end,client side,interactive,intractable,move,delete,migrate,divide" title = "PDF Multi Tool" -[navbar] -search = "Search" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Authentication was not successful. You can close this window and try again." @@ -4966,6 +5225,7 @@ activeFiles = "The Active Files view shows all of the PDFs you allTools = "This is the Tools panel, where you can browse and select from all available PDF tools." close = "Close" cropSettings = "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to." +dialogLabel = "Onboarding" fileCheckbox = "Clicking one of the files selects it for processing. You can select multiple files for batch operations." fileReplacement = "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools." filesButton = "The Files button on the Quick Access bar allows you to upload PDFs to use the tools on." @@ -4976,6 +5236,7 @@ pinButton = "You can use the Pin button if you'd rather your fi results = "After the tool has finished running, the Review step will show a preview of the results in this panel, and allow you to undo the operation or download the file. " runButton = "Once the tool has been configured, this button allows you to run the tool on all the selected PDFs." selectCropTool = "Let's select the Crop tool to demonstrate how to use one of the tools." +stepOf = "Step {{current}} of {{total}}" toolInterface = "This is the Crop tool interface. As you can see, there's not much there because we haven't added any PDF files to work with yet." workbench = "This is the Workbench - the main area where you view and edit your PDFs." wrapUp = "You're all set! You can replay this tour anytime - just open Settings and find it here in the Tours section under Help." @@ -4988,6 +5249,26 @@ showMeAround = "Show me around" skipForNow = "Skip for now" skipTheTour = "Skip the tour" +[onboarding.checklist] +dismiss = "Dismiss" +title = "Set up Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Run Stirling natively on your machine" +title = "Download Stirling for Desktop" + +[onboarding.checklist.inviteTeam] +description = "Collaborate with your team" +title = "Invite team members" + +[onboarding.checklist.shareAnalytics] +description = "Help improve Stirling" +title = "Share anonymous usage data" + +[onboarding.checklist.takeTour] +description = "See how Stirling works in a quick walkthrough" +title = "Take the tour" + [onboarding.desktopInstall] body = "Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer." selectOs = "Select operating system" @@ -4996,7 +5277,21 @@ titleWithOs = "Download for {{osLabel}}" [onboarding.mfa] authenticationCode = "Authentication code" +description = "Secure your account by linking an authenticator app. Scan the QR code or enter the setup key, then confirm the 6-digit code to finish." +enable = "Enable MFA" +enableError = "Unable to enable two-factor authentication. Check the code and try again." +enterCode = "Enter the 6-digit code from your app." +enterCodeError = "Enter the authentication code to continue." +logout = "Log out" +openAuthenticator = "Open Google Authenticator, Authy or 1Password." qrCodeLoading = "Generating your QR code…" +regenerateQrCode = "Regenerate QR code" +scanQrCode = "Scan the QR code or enter the setup key below." +setupError = "Unable to start two-factor setup. Please try again." +setupKey = "Setup key (manual entry)" +stepByStep = "Step-by-step" +success = "MFA has been enabled. You can now continue." +title = "Multi-Factor Authentication Setup" [onboarding.planOverview] adminBodyLoginDisabled = "Once you enable login mode, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge." @@ -5005,6 +5300,11 @@ adminTitle = "Admin Overview" userBody = "Invite teammates, assign roles, and keep your documents organised in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use." userTitle = "Plan Overview" +[onboarding.processorIntro] +body = "Stirling now runs Policies — automated rules that classify, secure and process every document as it arrives. Set them up and monitor runs in the Processor." +cta = "Check out the Processor" +title = "Check out the Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs used" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Prepaid year" + +[payg.prepaid.banner] +exhausted = "Your prepaid capacity is used up; metered billing has resumed. Top up to keep the discount." +expiring_one = "Your prepaid year ends in {{count}} day. Unused capacity expires — top up to start a fresh year." +expiring_other = "Your prepaid year ends in {{count}} days. Unused capacity expires — top up to start a fresh year." +low = "You're down to {{pct}}% of your prepaid capacity. Top up any time to keep processing uninterrupted." + +[payg.prepaid.card] +subtitle = "Used before metered billing and outside your spend cap." +title = "Prepaid capacity" + +[payg.prepaid.meter] +capSuffix = "of {{total}} prepaid credits" +expires = "Expires {{date}}" + +[payg.prepaid.state] +exhausted = "Used up" +healthy = "Plenty left" +low = "Running low" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF To PDF/A" tags = "archive,long-term,standard,conversion,storage,preservation" title = "PDF To PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "print,standard,conversion,production,prepress,archive" title = "PDF To PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Choose the plan that fits your needs" title = "Available Plans" @@ -5937,49 +6267,12 @@ keyDescription = "Paste the licence key from your email" success = "Licence Activated!" successMessage = "Your licence has been successfully activated. You can now close this window." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "enforced" -enforcing = "Enforcing..." -failed = "Enforcement failed" outputsUnavailable = "Policy outputs are no longer available to download." partialOutputsUnavailable = "Some policy outputs are no longer available to download." -retrying = "Busy, retrying..." runNotFound = "The enforcement run could no longer be found." -step = "step {{current}}/{{total}}" timedOut = "Enforcement timed out before the run could finish." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Close" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Applying {{names}}" applyingProgress = "Applying {{names}} ({{done}} of {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} of {{total}} file(s) couldn't be processed and were failureTitle = "Exported without full enforcement" printPolicyAppliedBody = "This PDF was updated to meet a policy. Review the changes, then print again." printPolicyAppliedTitle = "Policy applied before printing" -queued = "+{{count}} queued" successTitle = "{{names}} applied" summaryMore = "{{first}}, {{second}} and {{more}} more" summaryTwo = "{{first}} and {{second}}" -[policies.enforcement.triggerVerb] -convert = "Enforcing before convert" -default = "Enforcing" -export = "Enforcing before export" -input = "Enforcing on import" -print = "Enforcing before print" - -[policies.field] -accessLog = "Access log" -archiveAfter = "Archive after" -auditTrail = "Audit trail" -belowThreshold = "Below threshold" -destination = "Destination" -frameworks = "Frameworks" -immutableHold = "Immutable hold" -keepFor = "Keep for" -minConfidence = "Min confidence" -notify = "Notify on route" -onViolation = "When non-compliant" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Add" -addPlaceholder = "Add a label…" -customNote = "Customised for your team." -defaultNote = "Using the built-in default, shared with your team." -duplicate = "\"{{name}}\" already exists." -edit = "Edit labels" -empty = "No labels yet." -export = "Export JSON" -iconAria = "Choose an icon for {{name}}" -import = "Import JSON" -importError = "Couldn't import that file." -managedNote = "Team labels are managed by your team leader." -modalSubtitle = "Shared with your whole team. The classifier picks the labels that fit each document." -modalTitle = "Classification labels" -pickIcon = "Use {{label}} icon" -removeAria = "Remove {{name}}" -resetToDefault = "Reset to default" -saveForTeam = "Save for team" -saving = "Saving…" -sectionLabel = "Classification labels" -startFromScratch = "Start from scratch" -teamCount = "team labels" -tooLong = "Labels can be at most {{max}} characters." -ungrouped = "Ungrouped" -view = "View labels" +categoryCount = "categories" +hideCategory = "Hide category" +labelCount = "labels" +sharedNote = "These labels are built in and shared across your whole team." +showCategory = "Show category" [policies.pii] account = "Account numbers (labelled)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US routing numbers (ABA)" ssn = "Social Security numbers" -[policies.settings] -noneExport = "No policies currently run on export." -noneUpload = "No policies currently run on upload." -onExport = "On export" -onUpload = "On upload" -reorderHandle = "Drag to reorder" -runOrderDesc = "When more than one policy runs on the same trigger, they run in this order — each on the previous policy's output. Drag to reorder." -title = "Policy settings" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Policy options" -policySettings = "Policy settings" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Retry failed policies ({{count}})" -rowProgress = "{{completed}} of {{total}} files processed" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "What is a policy?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatically finds and blacks out sensitive details — like Social Security and card numbers — so they can't be read in the document." -sanitize = "Removes hidden JavaScript from the file, so nothing can run automatically when someone opens it." -watermark = "Stamps a visible mark (e.g. \"Confidential\") across every page." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Close" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} enforcing..." badgeRan = "{{name}} policy ran on this file" +badgeRunning = "{{name}} running..." blockingAction = "{{action}} blocked while enforcing policy, please wait..." dismiss = "Dismiss overlay" enforcingTitle = "Enforcing policy..." @@ -6151,6 +6316,7 @@ viewAnyway = "View file (policy still enforcing)" [policyOption] autoRedactPhi = "Auto-redact PHI" blockExport = "Block export" +ccpa = "CCPA" contracts = "Contracts" documents = "Documents" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor plan" subscribed = "Processor plan" unlinked = "Not linked" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap from document" -sectionsAriaLabel = "Agent builder sections" -selectorAriaLabel = "Agents" -subtitle = "Design, test and ship the AI agents that classify, extract from and route your documents. Define scenarios, fence tool access, run a golden set, and publish a version." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Cancel" -dropzoneText = "Choose a sample document (PDF or image)" -lead = "Drop a sample document and we'll propose scenarios and an extraction schema you can refine. Nothing is published until you review it." -submit = "Bootstrap agent" -subtitle = "Seed a new agent from one representative file" -title = "Bootstrap from a document" - -[portal.agentBuilder.empty] -description = "Bootstrap an agent from a sample document to seed its scenarios and extraction schema, then refine and publish." -title = "No agents yet" - -[portal.agentBuilder.evals] -casesPassing = "Cases passing" -columnCase = "Eval case" -columnLatency = "Latency" -columnResult = "Result" -fail = "fail" -goldenSetPassRate = "Golden-set pass rate" -latencyMs = "{{ms}} ms" -notRun = "not run" -pass = "pass" -passRate = "Pass rate" -runEvals = "Run evals" - -[portal.agentBuilder.evals.empty] -description = "Evals turn your scenarios into a repeatable golden set. Upgrade to capture pass-rate over time and gate publishes on it." -title = "No golden set yet" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "across golden sets" -activeAgents = "Active agents" -avgPassRate = "Avg eval pass rate" -fleetWide = "fleet-wide" -latestPublished = "Latest published" -scenarios = "Scenarios" -testCases = "test cases" -totalDescription_one = "{{count}} total" -totalDescription_other = "{{count}} total" - -[portal.agentBuilder.scenarios] -add = "Add" -addScenario = "Add scenario" -enable = "Enable" -expectationLabel = "Expected behaviour" -expectationPlaceholder = "What the agent should do" -inEval = "in eval" -mute = "Mute" -muted = "muted" -nameLabel = "Name" -namePlaceholder = "e.g. Compliance escalation" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarios" -tools = "Tools" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "Broad access" -deniedHint = "Selected tools are blocked. Everything else stays callable." -deniedTools = "Denied tools" -governanceGate = "Tool governance is available on the Enterprise plan." -restricted = "Restricted" -restrictedAccess = "Restricted tool access" -restrictedDescription = "Allow every tool except the ones you deny below." - -[portal.agentBuilder.versions] -current = "current" -historyGate = "Full version history and rollback are available on the Enterprise plan." -publish = "Publish" -rollBack = "Roll back" - [portal.assistant] close = "Close assistant" error = "Couldn't reach the assistant." @@ -6365,9 +6451,21 @@ typing = "Typing" loading = "Loading" redirectingToEditor = "Redirecting to the editor..." +[portal.billing.activation] +maybeLater = "Maybe later" +title = "Switch on the Processor" + +[portal.billing.activation.payg] +sub = "Set a monthly limit. Pay for what you run." +title = "Pay as you go" + +[portal.billing.activation.prepay] +badge = "2 months free" +sub = "One invoice, card or bank transfer." +title = "Prepay for a year" + [portal.billing.checkout] noClientSecret = "Edge function returned no client_secret." -subtitle = "Add a card to keep going past your free Editor-plan grant. Stripe handles the rest." title = "Turn on the Processor plan" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Your payment succeeded, but activation is taking a little longer than us close = "Close" title = "Almost there" +[portal.billing.checkout.cap] +amountAria = "Monthly spend limit" +back = "Back" +continue = "Continue to payment" +error = "Couldn't set your spend limit" +finePrint = "Invoices are posted monthly. Cancel anytime." +noLimit = "No limit" +note = "You're never billed past your limit. Processing just pauses." +perMonth = "/ mo" +title = "Set your spend limit" + +[portal.billing.checkout.card] +fields = "Card number · MM / YY · CVC · ZIP" +label = "Card details" +note = "Card details collected by Stripe. Stirling never stores PAN or CVC." + [portal.billing.checkout.error] title = "Couldn't start checkout" @@ -6383,10 +6497,9 @@ body = "Your payment went through. We're switching on metered processing across hint = "Please keep this window open." title = "Activating your Processor plan..." -[portal.billing.checkout.notConfigured] -bodyAfter = "in the portal env to enable in-app checkout." -bodyBefore = "Set" -title = "Stripe not configured" +[portal.billing.checkout.pay] +limit = "Monthly spend limit {{amount}}/mo. Processing pauses at the limit." +limitNoCap = "No spend limit. Processing won't pause. Invoices are posted monthly." [portal.billing.enterpriseUpsell] cta = "Build your Enterprise quote" @@ -6409,10 +6522,12 @@ currentPlan = "Current plan" freeForever = "Free forever" noTeamResolved = "No team is resolved on your wallet yet — refresh and try again." ownerOnly = "Only the team owner can switch on the Processor plan." +payInvoice = "Pay invoice to complete" planName = "Editor" ssoIncluded = "SSO included" switchOnProcessor = "Switch on the Processor →" unlimitedUsers = "Unlimited users" +viewQuote = "View quote" [portal.billing.invoices] columnAmount = "Amount" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direct API requests" segmentApiLabel = "API" segmentAutomationDesc = "Automations & pipelines" segmentAutomationLabel = "Automation" +sizeMultiplier = "{{formatted}} PDFs used a size multiplier" +summary = "{{unique}} unique · {{units}} meter units · {{avg}} avg per PDF" +summaryNoRate = "{{unique}} unique · {{units}} meter units" unit = "metered PDFs" +unitsPending_one = "{{units}} meter unit pending sync from linked instances" +unitsPending_other = "{{units}} meter units pending sync from linked instances" + +[portal.billing.prepaid] +capSuffix = "of {{total}} prepaid credits" +expires = "Expires {{date}}" +eyebrow = "Prepaid capacity" +note = "Used before metered billing and outside your spend limit." +topUp = "Top up" + +[portal.billing.prepaid.buy] +back = "Back" +calcTitle = "Calculate your annual payment" +cancel = "Cancel" +cancelPurchase = "Cancel purchase" +close = "Close" +continueToPayment = "Continue to payment" +finalise = "Finalise" +invoiceErrorTitle = "Couldn't generate the invoice" +loadingTitle = "Loading your quote" +payTitle = "Pay for your year" +step = "Step {{current}} of {{total}}" + +[portal.billing.prepaid.calc] +change = "Change" +deployRow = "Deployment" +done = "Done" +enterpriseHint = "This is enterprise scale. At this volume, enterprise rates beat any self-service discount. Rate lock, terms and a quote in minutes." +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} policies" +handlesLabel = "Your Processor" +handlesValue = "handles {{volume}} credits / mo" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policies" +poolCaption = "One pool of {{credits}} credits for the year. Heavy months borrow from light ones." +priceLabel = "Your year · 12 months for the price of 10" +rateUnknown = "We'll show the exact price at checkout." +savings = "You save {{amount}} · 2 months free." +sizingRow = "Sizing" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Total users" + +[portal.billing.prepaid.consent] +label = "I understand that when my prepaid capacity is used up or expires after 12 months, processing automatically continues at the standard metered pay-as-you-go rate (up to my spend limit) unless I cancel, and that I can cancel anytime from the billing portal." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Managed by Stirling. Live in minutes." +cloudValue = "Stirling Cloud · Managed" +selfhost = "Self-hosted" +selfhostDesc = "Runs on private infrastructure. Same rate." +selfhostValue = "Self-hosted · Private infrastructure" + +[portal.billing.prepaid.offer] +cta = "Review offer" +subtitle = "Prepay for a year of PDF processing and get two months free, used before metered billing, outside your spend limit." +title = "Get 12 months for the price of 10" + +[portal.billing.prepaid.pay] +accountName = "Account holder name" +accountNamePlaceholder = "e.g. Jane Smith" +companyName = "Company name (optional)" +companyNamePlaceholder = "e.g. Acme Inc." +downloadInvoice = "Download invoice" +payOnline = "Pay online" +poNumber = "PO number (optional)" +poNumberPlaceholder = "e.g. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Advanced" +advancedDesc = "Pipelines drive most of your processing." +none = "None" +noneDesc = "Not running pipelines yet. Turn them on any time." +standard = "Standard" +standardDesc = "A few pipelines re-process arriving PDFs." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Classification, the default, plus Sharing" +governed = "Governed" +governedDesc = "Adds Security and Routing" +regulated = "Regulated" +regulatedDesc = "Every category, incl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Download quote (PDF)" +downloading = "Preparing…" +share = "share for approval" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "Files under 25 MB, no data charges" +heavy = "Heavy" +heavyDesc = "Scanned or image-heavy, routinely 50 MB+" +standard = "Standard" +standardDesc = "Mostly small, some scans past 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Used up" +healthy = "Plenty left" +low = "Running low" [portal.billing.spendLimit] adjustLimit = "Adjust limit" @@ -6494,6 +6713,8 @@ label = "Projected to exceed." [portal.billing.spendThisMonth] eyebrow = "Spend this month" +freeRemaining_one = "{{formatted}} free PDF remaining" +freeRemaining_other = "{{formatted}} free PDFs remaining" processed_one = "{{formattedCount}} PDF processed." processed_other = "{{formattedCount}} PDFs processed." processedWithRate_one = "{{formattedCount}} PDF processed, at {{rate}} each." @@ -6510,6 +6731,7 @@ reachedTitle = "Monthly spend limit reached" title = "Couldn't open Stripe portal" [portal.billing.walletMeter] +barAria = "Free PDFs used" capSuffix_one = "of {{allowance}} free PDFs used" capSuffix_other = "of {{allowance}} free PDFs used" eyebrow = "Processor trial" @@ -6534,6 +6756,372 @@ signature = "signature" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "New connection" +createTitleFor = "Connect {{name}}" +delete = "Delete" +edit = "Edit" +editTitle = "Edit connection" +unsupportedType = "This connection type cannot be edited here." + +[portal.connections.categories.advanced] +description = "For a service we do not support yet." +label = "Advanced" + +[portal.connections.categories.audit] +description = "Record what happened to a document, where your auditors already look." +label = "Audit & compliance logging" + +[portal.connections.categories.notify] +description = "Tell another system that something happened." +label = "Notify & workflow" + +[portal.connections.categories.security] +description = "Scan or label a document before it moves on." +label = "Security & classification" + +[portal.connections.categories.signing] +description = "Send a document out to be signed or notarised." +label = "E-signature & notarisation" + +[portal.connections.categories.storage] +description = "Where processed documents end up - a bucket, a drive, a ticket or a page." +label = "File & attach" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling stores this encrypted and never shows it again." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Steps using this connection can only call paths under this URL, never another host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "Paste the URL the service gave you. Only paths under it can be called." +label = "Webhook URL" + +[portal.connections.fields] +name = "Connection name" +namePlaceholder = "e.g. Production" + +[portal.connections.picker] +cancel = "Cancel" +createNew = "New connection..." +placeholder = "Select a connection" +save = "Save connection" + +[portal.connections.picker2] +back = "Change integration" +heading = "What do you want to connect?" +noResultsBody = "We do not support it yet - tell us about it and we will look at adding it." +noResultsTitle = "Nothing matches \"{{query}}\"" +searchPlaceholder = "Search integrations - try \"sign\", \"ocr\" or a product name" + +[portal.connections.types.api] +description = "Call any HTTP API from a policy. You choose the URL, authentication and payload." +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "Authentication" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username and password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token in a custom header" + +[portal.connections.types.api.fields.authType.options.none] +label = "None" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Log in for a token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Steps using this connection can only call paths under this URL, never another host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header name" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Login path" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "If the API replies with a download link, only these hosts may be fetched. Separate with commas. Leave blank to allow only the base URL's own host." +label = "Allowed result hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Send the token as this header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "The response header the login call returns the token in, e.g. X-Auth-Token." +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scan documents for malware on a server you run - nothing leaves your network." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Virus and content scanning for documents in a policy." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Block documents containing macros, executables, scripts or embedded objects - not just known malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Attach processed documents to a Confluence page." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Send documents to ConsignO Cloud (Notarius) for electronic signature and notarisation." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Your ConsignO Cloud environment, including /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Only needed if your organisation uses tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Post a message to Discord when a policy processes a document." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Index an audit event in Elasticsearch each time a policy handles a document." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Poll an FTP or FTPS server for new documents." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Connection mode" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (recommended)" + +[portal.connections.types.ftp.fields.security] +helperText = "Plain FTP sends credentials and files unencrypted; choose FTPS if the server supports it." +label = "Encryption" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "None (plain FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Post a message to a Google Chat space when a policy runs." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Attach processed documents to a Jira issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Email a document or a notification when a policy runs." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "File processed documents into Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detect personal data in document text using a Presidio server you run." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Read and apply Microsoft Purview sensitivity labels on PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Optional. Only needed to pick labels by name; labelling works without it." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stamped onto every label as its SiteId, so Purview-aware tools know which organisation applied it." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "An S3 bucket that sources can read from and pipeline outputs can write to." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Leave blank for AWS. Set this for MinIO or another S3-compatible store." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Email a document or a notification when a policy runs." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Poll an SFTP (SSH) server for new documents." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optional. The server's SHA-256 host key fingerprint (from `ssh-keyscan host | ssh-keygen -lf -`). When set, only a server presenting this key is accepted; when blank, the key seen on first connect is pinned and later changes are refused." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Leave blank if you authenticate with a private key instead." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optional. Paste an OpenSSH private key to authenticate with a key instead of a password." +label = "Private key" + +[portal.connections.types.slack] +description = "Post a message to Slack when a policy processes a document." +label = "Slack" + +[portal.connections.types.smb] +description = "Watch an SMB / CIFS network share (Windows or Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Optional. The Windows domain or workgroup, if your server uses one." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Send an audit event to Splunk each time a policy handles a document." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Send an audit event to a Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Post a message to a Teams channel when a policy runs." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Post each document, or just the details, to a URL you choose." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Trigger a Zap or Make scenario from a policy." +label = "Zapier & Make" + [portal.docs] browse = "Browse docs" viewSource = "View source on GitHub" @@ -6623,11 +7211,6 @@ title = "Official SDKs" beta = "Beta" deprecated = "Deprecated" -[portal.docs.search] -empty = "No matching docs" -placeholder = "Search docs" -results = "{{count}} results" - [portal.docs.skills] eyebrow = "SKILLS" lead = "Bundled, named capabilities your agent invokes as a single tool. Each skill is a deterministic op chain with evals attached." @@ -6709,6 +7292,8 @@ type = "Type" user = "User" [portal.documents.queue.empty] +connectSource = "Connect a source" +createPipeline = "Create a pipeline" description = "As sources feed documents into your pipelines they'll appear here for review." title = "No documents in the queue" @@ -6724,11 +7309,11 @@ editorAction = "Editor" empty = "No documents match this filter." rowActions = "Row actions" sensitiveLabel = "Sensitive" -sensitiveTitle = "Sensitive — access required" [portal.documents.table.columns] action = "Pipeline / Action" document = "Document" +labels = "Labels" product = "Product" status = "Status" time = "Time" @@ -6744,6 +7329,7 @@ host = "Host" lastSeen = "Last seen" region = "Region" status = "Status" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6818,13 +7404,121 @@ description = "This view hit an unexpected error. Try again, or pick another sec retry = "Try again" title = "Something went wrong on this page" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Back" +body = "Download the desktop app or self-host the server." +done = "Done" +guide = "Open the {{name}} guide" +openInBrowser = "Open in browser instead" +sectionDesktop = "Desktop app" +sectionSelfHosted = "Self-hosted" +title = "Install the Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Run the official image on your server." +note = "Opens at :8080. Default login: admin / stirling." +tagline = "Run the server in a container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Install the official Helm chart." +note = "Add a PVC for /configs to persist settings." +tagline = "Deploy to a cluster with Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native builds for your distribution." +guideBtn = "View Linux install options" +note = "AppImage, .deb and .rpm builds are available for common distros." +tagline = "Native desktop app for your distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Or via Homebrew" +detailBody = "Run the installer, or use Homebrew." +downloadBtn = "Download for macOS" +tagline = "Native app for Apple silicon and Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Run the .jar on a bare-metal server." +downloadBtn = "Download the .jar" +note = "Requires Java 21+. Opens at :8080; default login admin / stirling." +runLabel = "Then run it" +tagline = "Run the .jar on a bare-metal server" +title = "Manual server set-up" + +[portal.home.download.windows] +altLabel = "Or via winget" +detailBody = "Run the installer, or use a package manager." +downloadBtn = "Download installer" +tagline = "Native app for Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} active" -invite = "Invite teammates" +activeOfDeployed = "{{active}} of {{total}} active this month" name = "Stirling PDF Editor" open = "Open in browser" updated = "updated {{time}}" +[portal.home.editor.deploy] +finish = "Finish deployment" +options = "View deployment options" +start = "Deploy the Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6835,49 +7529,6 @@ afternoon = "Good afternoon" evening = "Good evening" morning = "Good morning" -[portal.home.onboarding] -dismiss = "Dismiss setup" -notStarted = "Not started" -progress = "{{done}} of {{total}} done" -title = "Finish setting up" - -[portal.home.onboarding.enterprise] -body = "Org-wide SSO + SCIM + RBAC, committed volume pricing, and air-gapped deployment." -cta = "Start Trial" -ctaQuote = "Get Quote" -lead = "For 250+ employees." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Deploy the desktop app, free forever." -title = "Download the PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} active · {{recommended}} recommended" -chip = "{{active}} active" -title = "Turn on policies" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} connected · every PDF governed where it lands" -chip = "{{connected}} connected" -title = "Connect your sources" - -[portal.home.quickActions] -subtitle = "Top tasks for today" -title = "Quick actions" - -[portal.home.quickActions.buildPipeline] -blurb = "3-step composer over the typed op library" -title = "Build a pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, watched folders" -title = "Connect a source" - -[portal.home.quickActions.issueApiKey] -blurb = "Scoped key with rate limits and IP allowlist" -title = "Issue an API key" - [portal.infrastructure] manageEditorDeployment = "Manage Editor deployment" sectionsAriaLabel = "Infrastructure sections" @@ -6952,6 +7603,7 @@ all = "All" auth = "Auth" config = "Config" elevation = "Elevation" +policy = "Policy" processing = "Processing" security = "Security" @@ -6961,10 +7613,24 @@ title = "You don't have access" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Elevation" +policy = "Policy" processing = "Processing" totalEvents = "Total events · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Config" +elevation = "Elevation" +policy = "Policy" +processing = "Processing" +security = "Security" + +[portal.infrastructure.auditStatusLabel] +danger = "Error" +info = "Info" +success = "Success" +warning = "Warning" + [portal.infrastructure.createKey] cancel = "Cancel" createKey = "Create key" @@ -6978,207 +7644,9 @@ subtitleCreated = "Copy this secret now — it won't be shown again." title = "Create API key" titleCreated = "Key created" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Deployed by" -environment = "Environment" -product = "Product" -status = "Status" -version = "Version" -when = "When" - -[portal.infrastructure.deployments.recent] -heading = "Recent deployments" -subheading = "The latest rollouts across products and environments." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instances" -latency = "Latency" -load = "Load" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Throughput" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "Live health for every deployed Stirling region — latency, load, and rollout version." - -[portal.infrastructure.deployments.regions.empty] -description = "Deployed regions appear here once your workspace is provisioned." -title = "No regions deployed" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "The model catalogue and routing that powers document processing across your workspace." - -[portal.infrastructure.models.byom] -description = "Register an on-prem or self-hosted model and pin it to a region for data-residency-bound processing." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "Managed models available to your workspace, with live latency and cost." -subEnterprise = "Managed, bring-your-own, and on-prem models — with per-region pinning available." - -[portal.infrastructure.models.catalogue.empty] -description = "Models in your workspace's catalogue appear here." -title = "No models available" - -[portal.infrastructure.models.columns] -cost = "Cost" -latency = "Latency" -load = "Load" -model = "Model" -status = "Status" -type = "Type" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "Active models" -avgLatency = "Avg latency" -included = "Included" -monthlySpend = "Monthly model spend" - -[portal.infrastructure.models.routing] -empty = "No routing rules configured." -heading = "Routing rules" -sub = "Which model handles each operation. The default applies when no narrower rule matches." -subLocked = "Route operations to specific models — available on paid plans." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgrade to Pro to control which model handles each operation and document type." -title = "Model routing is a paid feature" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "Document type" -modelForAria = "Model for {{operation}}" -operation = "Operation" -routedTo = "Routed to" - -[portal.infrastructure.security.access.byok] -description = "Supply a key from your own KMS. Stirling encrypts with it but can still read." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Keys never leave your KMS. Stirling holds only ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling manages encryption keys. Simplest — zero key ops on your side." -label = "Stirling-held keys" - -[portal.infrastructure.security.accessPolicy] -heading = "Document access policy" -subheading = "Controls who can decrypt processed documents at rest." - -[portal.infrastructure.security.attestations] -heading = "Compliance attestations" -noReport = "No report available" -subheading = "Framework-by-framework audit posture, with reports available on attested controls." -viewReport = "View report →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestations and certifications covering the Stirling platform." - -[portal.infrastructure.security.empty] -description = "Your workspace's security configuration will appear here." -title = "Security posture unavailable" - -[portal.infrastructure.security.hyokBanner] -description = "With HYOK, encryption keys never leave your KMS. Stirling stores and processes only ciphertext you can revoke at any time." -title = "Stirling cannot decrypt your documents" - -[portal.infrastructure.security.ipAllowlist] -empty = "No IP ranges configured — all IPs allowed." -heading = "IP allowlist" -sub = "API access is restricted to these CIDR ranges." -subLocked = "Restrict API access to known IP ranges — available on paid plans." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgrade to Pro to restrict API access to specific networks." -title = "IP allowlisting is a paid feature" - -[portal.infrastructure.security.ipColumns] -added = "Added" -addedBy = "Added by" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Encryption key management" -keyId = "Key identifier" -lastRotated = "Last rotated" -rotateKey = "Rotate key" -rotationPolicy = "Rotation policy" -subheading = "Custody of the keys that encrypt documents at rest — who can decrypt, and how keys rotate." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) and hold-your-own-key (HYOK) custody are available on Enterprise. Upgrade to supply keys from your own KMS." -title = "Keys are managed by Stirling on your plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "European Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "United States" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "Where documents are stored and processed." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} used" - -[portal.infrastructure.storage.empty] -description = "Connected storage and usage appear here." -title = "No storage configured" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Active" -activeRange = "0–{{value}}d" -archived = "Archived" -coldStorage = "cold storage" -deleted = "Deleted" -never = "never" -purged = "purged" - -[portal.infrastructure.storage.providers] -connect = "Connect" -connected = "Connected" -heading = "Connected providers" -subheading = "Where processed artefacts are written." - -[portal.infrastructure.storage.retention] -heading = "Retention" -subheading = "How long artefacts are kept before lifecycle deletion." -windowLabel = "Default retention window" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} day" -days_other = "{{count}} days" -never = "Never delete" - -[portal.infrastructure.storage.totalUsage] -heading = "Total usage" -progressLabel = "Storage used" -subheading = "Storage consumed across all connected providers." +revoked = "Revoked" [portal.infrastructure.tabs] apiKeys = "API Keys" @@ -7188,6 +7656,49 @@ models = "Models" security = "Security" storage = "Storage" +[portal.integrations] +availableHeading = "Available" +comingSoonHeading = "Coming soon" +connect = "Connect" +connectionCount_one = "{{count}} connection" +connectionCount_other = "{{count}} connections" +customApi = "Custom API" +searchPlaceholder = "Search integrations" +subtitle = "Everything Stirling can talk to. Credentials are created once here, then referenced by sources, policies and pipelines." +title = "Integrations" +typedName = "{{tool}} name" + +[portal.integrations.filters] +all = "All" +audit = "Audit" +connected = "Connected" +notify = "Notify" +security = "Security" +signing = "Signing" +storage = "Storage" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Connected" + +[portal.integrations.table] +integration = "Integration" +worksWith = "Works with" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Policies" +sources = "Sources" + +[portal.legal] +draft = "{{label}} · draft" +loadError = "Could not load this document. Please try again." +loading = "Loading…" +title = "Legal document" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7196,10 +7707,10 @@ documents = "Documents" editor = "Editor" home = "Home" infrastructure = "Infrastructure" +integrations = "Integrations" pipelines = "Pipelines" policies = "Policies" procurement = "Procurement" -settings = "Settings" sources = "Sources" usage = "Usage & Billing" users = "Users" @@ -7208,26 +7719,6 @@ users = "Users" platform = "PDF Platform" processor = "PDF Processor" -[portal.notifications] -markAllRead = "Mark all read" -title = "Notifications" -viewAll = "View all" - -[portal.notifications.ariaLabel] -none = "Notifications, no unread" -unread_one = "Notifications, {{count}} unread" -unread_other = "Notifications, {{count}} unread" - -[portal.notifications.count] -allRead = "all read" -loading = "loading" -new_one = "{{count}} new" -new_other = "{{count}} new" - -[portal.notifications.empty] -description = "No new notifications." -title = "You're all caught up" - [portal.pipelines] subtitle = "Every automated document pipeline on the backend: an ordered chain of operations over a set of sources, run on a trigger. Click a row for its steps and sources." title = "Pipelines" @@ -7236,54 +7727,87 @@ title = "Pipelines" newPipeline = "New pipeline" [portal.pipelines.builder] -addStep = "Add tool" +activate = "Activate" back = "Back to pipelines" +cannotFollow = "Can't take {{produced}}" +chooseAccount = "Choose an account" +chooseDestination = "Choose a destination" +chooseOperation = "Choose what this step does" +chooseSource = "Choose a source" discard = "Discard changes" -enabled = "Enabled" +inputs = "Input" +inputSource = "Input source" +inputTrigger = "Trigger" keepEditing = "Keep editing" -needsUpload = "Needs an uploaded file" +moreActions = "More actions" +needsConfiguring = "Needs setting up" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "No tools match your search." -pipelineSettings = "Pipeline settings" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Search tools" -selectToolBody = "Add a tool to build your pipeline." -selectToolTitle = "No tools yet" -toolSettings = "Tool settings" +sendToSystem = "Send to another system" +stepsIncompatible = "These steps can't run on what their prior step produces: {{tools}}." +stepsNeedSetup = "These steps still need setting up before saving: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Unrecognised operation, kept as-is." unsavedBody = "You have unsaved changes. Save them before leaving, or discard them?" unsavedTitle = "Unsaved changes" -uploadUnsupported = "Uploaded files aren't supported in pipelines yet, so these steps can't be saved: {{tools}}." usesDefaults = "Runs with default settings" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combines every file from the previous step" +fan-out = "Runs once per file from the previous step" +format-mismatch = "Needs {{accepts}}, but the previous step produces {{produced}}" +output-uncertain = "May not run: the previous step's output depends on how it's set up" +source-mismatch = "Needs {{accepts}}, but this pipeline's input is {{produced}}" +undeclared-operation = "Can't check what this step accepts" [portal.pipelines.composer] addTool = "Add tool" -cancel = "Cancel" -chainEmpty = "Add a tool to start building your pipeline." create = "Create pipeline" -directory = "Output folder" -directoryHelp = "Absolute path on the server. Must be within the configured allowed folders." +createPaused = "Create paused" editingUnsupported = "Displaying these tool params for editing is not supported yet." -moveDown = "Move down" -moveUp = "Move up" +editSource = "Edit source" name = "Name" namePlaceholder = "e.g. Redaction sweep" -noSources = "No sources connected yet. The pipeline can still run on files supplied to it directly." noToolSettings = "This tool has no configurable settings." -operations_one = "Operation ({{count}})" -operations_other = "Operations ({{count}})" output = "Output" -removeStep = "Remove operation" save = "Save changes" scheduleEvery = "Run every" -sources = "Sources" -sourcesLoading = "Loading sources..." trigger = "Trigger" triggerManual = "Manual only" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "days" hours = "hours" minutes = "minutes" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Delete \"{{name}}\"? This can't be undone." cancel = "Cancel" @@ -7291,28 +7815,62 @@ confirm = "Delete" title = "Delete pipeline?" [portal.pipelines.detail] +clearHistory = "Clear history" delete = "Delete pipeline" run = "Run now" [portal.pipelines.empty] action = "Create a pipeline" +connectSource = "Connect a source" description = "Create your first pipeline: pick the sources it runs over, chain the operations, and choose where output goes." title = "No pipelines yet" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Active" paused = "Paused" total = "Pipelines" -[portal.pipelines.output] -folder = "Write to folder" -inline = "Return files" - [portal.pipelines.run] +allProcessed_one = "Nothing to run: the source's {{count}} document has already been processed." +allProcessed_other = "Nothing to run: all {{count}} documents in the sources have already been processed." completed_one = "Run completed." completed_other = "All {{count}} runs completed." empty = "Nothing to run: the sources had no documents to process." failed = "Run failed: {{error}}" +inFlight = "Nothing new to run: documents are still being processed from an earlier run." +parked_one = "Nothing to run: {{count}} document failed previously and is parked. Fix the cause, then clear history to retry it." +parked_other = "Nothing to run: {{count}} documents failed previously and are parked. Fix the cause, then clear history to retry them." running = "Run started; still in progress." timeout = "Run is taking longer than expected; it may still finish in the background." @@ -7325,6 +7883,7 @@ name = "Pipeline" sources = "Sources" status = "Status" steps = "Steps" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Folder watch" @@ -7332,12 +7891,18 @@ manual = "Manual" schedule = "Scheduled" [portal.policies] +defaultName = "{{category}} Policy" subtitle = "Standing automations that enforce a tool pipeline on every document. Each policy fires on upload or export, runs its tool chain, and saves the enforced version alongside the original." title = "Policies" [portal.policies.card] comingSoon = "Upgrade to Enterprise" notSetUp = "Not set up" +requiresAiEngine = "Requires AI engine" + +[portal.policies.categories.classification] +desc = "Identify each document's type on upload and tag its metadata for filing and search." +label = "Classification" [portal.policies.categories.compliance] desc = "Enforce HIPAA, GDPR, SOC 2, or FedRAMP requirements on every document." @@ -7362,6 +7927,13 @@ label = "Security" [portal.policies.config] scopeAll = "All documents" +[portal.policies.config.classification] +summary = "Classifies every uploaded document against your team's labels and tags its metadata." + +[portal.policies.config.classification.rules] +0 = "Classify" +1 = "Tag metadata" + [portal.policies.config.compliance] summary = "Validates documents against regulatory frameworks before they leave the system." @@ -7389,6 +7961,29 @@ minConfidence = "Min confidence" 2 = "Name" 3 = "Normalise" +[portal.policies.config.purview] +label = "Apply a Microsoft Purview sensitivity label" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "The label's GUID, from the Purview portal. Applying a label writes its metadata onto the PDF; it cannot encrypt, which needs the Microsoft client." +labelName = "Label name (optional)" +labelNameHelp = "Recorded alongside the ID so people and tools can recognise the label." + +[portal.policies.config.purview.fields.method] +help = "Purview distinguishes a label a policy applied from one a person chose." +label = "How the label is recorded" +privileged = "As chosen by a person" +standard = "As applied automatically" + +[portal.policies.config.purviewRead] +label = "Read a Microsoft Purview label" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Reads the label the document already carries, so later steps can act on it." + [portal.policies.config.retention] summary = "Enforces how long documents are kept, when to archive, and when to delete." @@ -7456,6 +8051,7 @@ title = "No activity yet" [portal.policies.endpoints] addWatermark = "Watermark" autoRedact = "Redact PII" +classifyAndLabel = "Classify" compressPdf = "Compress" flatten = "Flatten" ocrPdf = "OCR" @@ -7466,6 +8062,201 @@ description = "Your policies are saved and will appear once the connection is re retry = "Retry" title = "Backend unavailable" +[portal.policies.operations] +change = "Change what this step does" +noResults = "No step matches that. Try a product name, or \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Search steps - try \"scan\", \"attach\" or a product name" + +[portal.policies.operations.bodyMode] +binary = "Raw file bytes" +json = "Inside a JSON payload" +multipart = "As a file upload (multipart)" + +[portal.policies.operations.clamavScan] +description = "Your own ClamAV server checks the document; nothing leaves your network." +label = "Scan for viruses (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rejects documents carrying macros, executables, scripts or embedded objects." +label = "Block active content" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive checks the document and the run stops if it is not clean." +label = "Scan for viruses" + +[portal.policies.operations.confluenceAttach] +description = "Files the processed document onto a page." +label = "Attach to a Confluence page" + +[portal.policies.operations.consignoSubmit] +description = "Submits the document to ConsignO Cloud for signing." +label = "Send for signature (ConsignO)" +note = "Submits the document. Retrieving the signed copy is not yet supported, because a step cannot pass a result to a later step." + +[portal.policies.operations.customApiCall] +description = "Author the call yourself: path, method, body and response handling." +label = "Call a custom API" + +[portal.policies.operations.discordNotify] +description = "Tells a channel that the policy handled a document." +label = "Post a message to Discord" + +[portal.policies.operations.elasticIndex] +description = "Records what the policy did as a searchable document." +label = "Index an audit event in Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart suits upload APIs; JSON suits APIs that want the file inside a payload; binary sends the raw bytes." +label = "How to send the document" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "A JSON body sent as-is. {{document.base64}} carries the file; {{document.*}} and {{run.*}} are filled in per document." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Account" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "The form field the API expects the document under." +label = "File field name" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "From" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "A JSON object of header name/value pairs. The connection's credential is added for you." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Sent as the message body. {{document.*}} and {{run.*}} are filled in per document." +label = "Message" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Appended to the connection's base URL. It can never reach another host." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Where in the drive to write it. {{document.filename}} is filled in per document." +label = "Destination path" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report leaves the document untouched. Replace swaps it for whatever comes back." +label = "What to do with the reply" + +[portal.policies.operations.fields.signerEmail] +label = "Signer's email" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Subject" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyses this text. Use {{document.title}} or paste the text you want checked." +label = "Text to check" + +[portal.policies.operations.fields.to] +label = "To" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud username" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Tells a space that the policy handled a document." +label = "Post a message to Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Files the processed document onto an issue." +label = "Attach to a Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "Sends the processed document as an attachment." +label = "Email the document (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Writes the processed document into a folder." +label = "Upload to Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Your own Presidio server reports the personal data it finds." +label = "Detect personal data" +note = "Presidio reads text, not files. Give it the text you want checked — the document itself is not uploaded." + +[portal.policies.operations.responseMode] +replace = "Replace the document with the reply" +report = "Keep the document, record the reply" + +[portal.policies.operations.sendgridEmail] +description = "Sends the processed document as an attachment." +label = "Email the document (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Tells a channel that the policy handled a document." +label = "Post a message to Slack" + +[portal.policies.operations.splunkEvent] +description = "Records what the policy did, where your auditors already look." +label = "Send an audit event to Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Records what the policy did to an HTTP collector." +label = "Send an audit event to Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Tells a channel that the policy handled a document." +label = "Post a message to Teams" + +[portal.policies.operations.webhookPost] +description = "Sends the document itself to a URL you choose." +label = "Post the document to a webhook" + +[portal.policies.operations.zapierNotify] +description = "Hands the run's details to your automation." +label = "Trigger a Zap or Make scenario" + [portal.policies.stats] activeFor = "Active" dataProcessed = "Data processed" @@ -7491,6 +8282,13 @@ label = "Docs enforced" description = "Configured but not running" label = "Paused" +[portal.policies.table] +appliesTo = "Applies to" +docs = "Docs enforced" +enforces = "Enforces" +policy = "Policy" +status = "Status" + [portal.policies.wizard.actions] back = "Back" cancel = "Cancel" @@ -7498,6 +8296,10 @@ continue = "Continue" enablePolicy = "Enable policy" saveChanges = "Save changes" +[portal.policies.wizard.capability.classify] +desc = "Identifies the document's type from your team's labels and tags it, so it files and searches by category." +label = "Classify the document" + [portal.policies.wizard.capability.compress] desc = "Compresses the document to a smaller file size." label = "Reduce file size" @@ -7518,10 +8320,18 @@ label = "Redact sensitive information" desc = "Removes hidden JavaScript so nothing can run automatically when the document is opened." label = "Strip active content" +[portal.policies.wizard.capability.timestampPdf] +desc = "Proves the document existed in this exact form at a point in time, using an independent timestamp authority. Only a hash is sent - the document never leaves your server." +label = "Add a trusted timestamp" + [portal.policies.wizard.capability.watermark] desc = "Stamps a visible mark (e.g. “Confidential”) across every page." label = "Apply a watermark" +[portal.policies.wizard.classification] +description = "Every uploaded document is classified against the built-in labels and tagged with the types that fit. The label set is shared across your whole team." +labelsHeading = "Classification labels" + [portal.policies.wizard.errors] noTools = "Enable at least one tool in the workflow first." saveFailed = "Couldn't save the policy. Please try again." @@ -7566,73 +8376,118 @@ setUp = "Set up {{category}} policy" [portal.policies.wizard.workflow] description = "Choose what this policy does to every document it processes." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} active" -noRule = "No rule enforced yet" -subtitle = "Standing automations every document passes through, regardless of which pipeline handles it." -title = "What runs on your PDFs" - [portal.policySummary.action] -comingSoon = "Upgrade to Enterprise" -configure = "Configure" setUp = "Set up" -[portal.policySummary.column] -activeRule = "Active rule" -policy = "Policy" -status = "Status" +[portal.processorFlow] +footnote = "Counts are over the last 24 hours. Flow speed is illustrative." +liveBadge = "Live" +stats = "{{connected}} connected · {{processed}} PDFs processed" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Once policies are configured, the categories appear here." -title = "No policies yet" +[portal.processorFlow.lens] +ariaLabel = "View mode" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "Active" -off = "Off" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Failed" +heading = "Outcomes" +success = "Delivered" + +[portal.processorFlow.policies] +activeCount = "{{n}} active" +count = "{{n}} · 24h" +heading = "Policies" +setUp = "Set up" soon = "Soon" -[portal.processingStatus] -managePlan = "Manage plan" -volumeSuffix = "PDFs processed · last 30 days" +[portal.processorFlow.sankey] +waist = "Policies · {{n}} active" + +[portal.processorFlow.sankey.empty] +description = "Connect a source and switch on a policy to watch documents flow through the processor." +title = "No flow yet" + +[portal.processorFlow.sources] +comingSoonTag = "Connect" +editor = "Stirling PDF Editor" +heading = "Sources" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email intake" [portal.procurement] -reset = "Reset procurement (demo)" subtitle = "Get your team evaluated, contracted and onboarded. Every document in one place." title = "Procurement" -[portal.procurement.action] -download = "Download" -pay = "Pay now" -request = "Request" -sign = "Review & sign" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Agree & subscribe" +confidential = "Confidential" confirm = "I have read and agree to the Stirling Enterprise Agreement." -eyebrow = "Agreement" -intro = "One combined agreement covers your deal: Master Service Agreement, Order Form, EULA and Data Processing Agreement. Review it, then agree to accept the quote into a committed subscription." -title = "Review your enterprise agreement" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA and DPA, combined into one signature." +download = "Download" +downloadDraftError = "Could not generate the agreement PDF - the document renderer is unavailable. Please try again shortly or contact support." +downloadError = "Could not download the signed agreement. Please try again." +legalName = "Legal entity name" +legalNamePlaceholder = "The legal entity that will sign" +loadError = "Could not load the agreement. Please try again." +loading = "Loading the agreement..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Request changes" +scrollHint = "Scroll to review" +signatory = "Signatory name" +signatoryPlaceholder = "Full name" +signatoryTitle = "Signatory title" +signatoryTitlePlaceholder = "e.g. General Counsel" +signError = "Could not record your signature. Please try again." [portal.procurement.builder] addons = "Add-ons" +addressLine1 = "Address line 1" +addressLine1Placeholder = "10 High Street" +addressLine2 = "Address line 2" +addressLine2Placeholder = "Suite, floor, building" back = "Back" businessName = "Business name" businessNamePlaceholder = "Your company" +city = "Town or city" +cityPlaceholder = "London" +completeRequired = "Please complete the required fields (marked *) with a valid email before generating the quote." +contactEmail = "Contact email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Contact name" +contactNamePlaceholder = "Jane Doe" continue = "Continue" -country = "Country" -countryEuro = "Eurozone (EUR €)" -countryUK = "United Kingdom (GBP £)" -countryUS = "United States (USD $)" +done = "Done" eula = "I have read and agree to the Stirling Enterprise EULA. It governs the agreement generated from this quote." generate = "Generate quote" -included = "Included" indemnification = "IP indemnification" indemnificationSub = "We defend qualifying IP claims, as per the EULA" -offlineLicense = "Offline / air-gapped licence" -offlineLicenseSub = "A downloadable licence file for an air-gapped self-hosted instance" +paperEyebrow = "Enterprise quote" +paperFor = "Prepared for" +pdfSize = "PDF size" +poNumber = "PO number" +poNumberPlaceholder = "Optional" +postalCode = "Postcode" +postalCodePlaceholder = "SW1A 1AA" +posture = "Governance" +posture_count = "~{{count}} policies" +posture_essentials = "Essentials" +posture_essentialsSub = "Classification + Sharing — the defaults" +posture_governed = "Governed" +posture_governedSub = "Adds Security and Routing" +posture_regulated = "Regulated" +posture_regulatedSub = "Every category — Compliance, Retention, Ingestion" qbr = "Quarterly business reviews" qbrSub = "Your SE reviews usage and roadmap each quarter" +region = "County / region" +regionPlaceholder = "Greater London" running = "{{annual}} / yr · {{years}}-yr {{tcv}}" s1Sub = "Your team, and the PDFs you expect to run each year." s1Title = "How much will you process?" @@ -7641,6 +8496,12 @@ s2Title = "Commitment and service" s3Sub = "For the quote and the agreement it generates." s3Title = "Your details" serviceLevel = "Service level" +size_compact = "Compact" +size_compactSub = "Mostly text, under 1 MB" +size_heavy = "Heavy" +size_heavySub = "Scanned or image-heavy, 5 MB+" +size_standard = "Standard" +size_standardSub = "Mixed text and images, 1 to 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 business hours · dedicated account manager · +30%" slPriority = "Priority" @@ -7648,6 +8509,8 @@ slPrioritySub = "Same business day · named CSM · +15%" slStandard = "Standard" slStandardSub = "Next business day · shared CSM · included" stepOf = "Step {{n}} of {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Optional" term = "Term" termDiscount = "{{pct}}% multi-year commitment discount applied" title = "Build your quote" @@ -7655,6 +8518,7 @@ training = "Onboarding & training" trainingSub = "Live sessions to get your team running" users = "Total users" usersPlaceholder = "e.g. 250" +viewEula = "Read it" volEstimated = "Estimated from {{count}} users (~2,000 PDFs each, including automation). Edit if you know better." volManual = "Using your figure. Re-estimate from your team size any time." volNoUsers = "Not sure? Enter your team size and we'll estimate it." @@ -7663,55 +8527,55 @@ volumePlaceholder = "e.g. 1,000,000" years_one = "{{count}} year" years_other = "{{count}} years" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Done" -here = "You're here" -hide = "Hide" -optional = "Optional" -paidAddon = "Paid add-on" -show = "Show" -subtitle = "Everything you need at each step of the journey, surfaced as the deal moves through it." -supportingSubtitle = "SOC 2, security reviews, tax forms and more, ready when your security or procurement team asks. Some carry a one-time fee." -supportingTitle = "Supporting your evaluation" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form and DPA in one signature." +download = "Download" +eula = "EULA & Commercial Terms" +eulaSub = "The software and self-serve commercial terms." +invoice = "Invoice" +invoiceSub = "Your first subscription invoice." +laterInvoice = "Available after you accept" +laterQuote = "Available once your quote is issued" +quote = "Quote" +quoteSub = "Your itemised enterprise quote." +sla = "SLA Exhibit" +slaSub = "Support tiers and response targets." +subprocessors = "Subprocessors" +subprocessorsSub = "Third parties that process data on your behalf." +subtitle = "Your deal paperwork, available any time." title = "Documents" -upcoming = "Upcoming" +view = "View" [portal.procurement.error] title = "Something went wrong" [portal.procurement.hero] -company = "Your enterprise deal" +barAria = "Stage {{current}} of {{total}}" ctaAgreement = "Review & sign agreement" -ctaLive = "You're live" -ctaPayment = "Add payment" +ctaExploring = "Set up your trial" ctaQuote = "Review your quote" +ctaReviewQuote = "Review quote" ctaTrial = "Build your quote" +documents = "Documents" eyebrow = "Enterprise procurement" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invite teammates" -keyDocs = "Key documents" -nextStep = "Next step: {{action}}" -notStarted = "Not started" +licenseKey = "Licence key" +liveSub = "Your organisation is provisioned and your licence is active." +liveTitle = "You are live on Stirling Enterprise" +next = "Next: {{stage}}" open = "Open procurement" scheduleCall = "Schedule a call" -setup1Sub = "Invite your teammates" -setup1Title = "Deploy the PDF Editor" -setup2Sub = "Turn on processing across editors and other sources" -setup2Title = "Connect the PDF Processor" -setup3Sub = "Turn on Security, Compliance, Routing or Retention when you need them" -setup3Title = "Add recommended policies" +sentenceAgreement = "review and sign the agreement" +sentenceLive = "provisioning and rollout" +sentencePayment = "purchase order and payment" +sentenceQuote = "review and accept your quote" +sentenceTrial = "set up your trial workspace" [portal.procurement.journey] daysLeft_one = "{{count}} day left" daysLeft_other = "{{count}} days left" -engineerLabel = "Your solutions engineer" -eyebrow = "Your rollout" -live = "You're live on Stirling Enterprise" -nextStep = "Next step: {{action}}" -subtitle = "Your solutions engineer is on every step. One next action at a time; the full checklist is below." -title = "From trial to live, one guided path" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "One signature covers MSA, order form, EULA and DPA." @@ -7745,6 +8609,9 @@ downloadError = "Could not generate the offline licence file just yet — please downloadOffline = "Download offline licence (.lic)" hint = "Paste this key into a self-hosted instance to activate it, or keep it for your records. Keep it safe." label = "Your licence key" +subtitle = "Activate a self-hosted instance with this key, or keep it for your records." +title = "Your licence key" +trialFileHint = "This is your trial licence file. Once your agreement is in place, come back and download it again — unlike the online licence key, the .lic file won't update on its own." [portal.procurement.link] cta = "Link account" @@ -7757,65 +8624,62 @@ description = "Your subscription is active and your licence is issued. Your team eyebrow = "Live" title = "You're live on Stirling Enterprise" -[portal.procurement.locked] -description = "Trial keys, committed-volume quotes, the one-signature agreement, payment and your document ledger all live here once you start an enterprise evaluation." -eyebrow = "Enterprise only" -talkToSales = "Talk to sales" -title = "The procurement track opens with Enterprise" - [portal.procurement.milestone] -accept = "Accept & continue" -description = "Download the PDF to share it with your team, come back to accept when you're ready or make changes." -download = "Download PDF" downloadError = "Could not download the quote PDF just yet — please try again in a moment." -edit = "Edit quote" -eyebrow = "Quote {{number}}" -perYear = " / yr" -preparedFor = "Prepared for {{company}}" -tcv = "{{value}} total contract value" -title = "Your quote is ready" - -[portal.procurement.modal] -cancel = "Cancel" -chooseFile = "Choose file" -close = "Close" -downloadBody = "Your download will begin shortly." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "No file selected" -payBody = "Pay your committed contract by card or bank transfer through Stripe. Your workspace provisions as soon as payment clears." -payCta = "Continue to Stripe" -payTitle = "Confirm payment" -requestBodyFree = "We generate this on demand. Confirm and your solutions engineer will send it across shortly." -requestBodyPaid = "This is a paid add-on. Confirm and your solutions engineer will scope it and send the paperwork." -requestCta = "Request" -requestTitle = "Request this document" -signBody = "Opens the Stirling Enterprise Agreement for e-signature: one signature covers the MSA, order form, EULA and DPA. We countersign automatically and you advance to payment." -signCta = "Open for signature" -signTitle = "Review and sign your agreement" -uploadBody = "Send us your PO and we invoice against it on your terms. Drag in the PDF or pick a file below." -uploadCta = "Upload purchase order" -uploadTitle = "Upload your purchase order" [portal.procurement.payment] description = "Your quote is accepted and your licence is already active — your team can start right away. Pay the first invoice when you're ready; you can pay or download it here, no email needed." +downloadAgreement = "Download signed agreement" downloadInvoice = "Download invoice" -simulate = "Simulate payment received (demo)" +eyebrow = "Payment" title = "Subscription created" viewInvoice = "View & pay invoice" +[portal.procurement.review] +acceptCta = "Accept quote" +annual = "Annual fee (year 1)" +downloadCta = "Download quote" +poNumber = "Purchase order: {{po}}" +renewal = "Renews at {{amount}}/year after the term (+{{pct}}% CPI)" +tcv = "{{years}}-year term, paid in full up front: {{tcv}}" +validUntil = "Valid until {{date}}" + [portal.procurement.schedule] fallback = "Couldn't load the scheduler." fallbackLink = "Open scheduling in a new tab" +loading = "Loading scheduler..." subtitle = "Your solutions engineer will walk your team through the rollout. Pick a time that suits you." title = "Schedule a call" -[portal.procurement.status] -action = "Action needed" -available = "Available" -complete = "Complete" -pending = "Pending" -request = "On request" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sealed, no outbound" +back = "Back" +businessName = "Business name" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, fastest start" +continue = "Continue" +deployment = "Where should it deploy?" +eula = "I agree to the Stirling EULA & Commercial Terms." +fullName = "Full name" +fullNamePlaceholder = "Your name" +invites = "Invite teammates · optional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Schedule a call" +seats = "People testing" +seatsPlaceholder = "e.g. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker or Kubernetes" +start = "Start trial" +stepOf = "Step {{n}} of {{total}}" +subtitle = "Free for 14 days, no card required." +subtitleDetails = "A few details for your quote and agreement." +talkFirst = "Prefer to talk first?" +title = "Set up your trial" +viewEula = "Read it" +workEmail = "Work email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Extending adds 7 days and notifies your solutions engineer." @@ -7826,109 +8690,113 @@ maxed = "Maxed out" subtitle = "Your free trial runs through {{date}}. No card required." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutable audit and a dedicated SE." -homeCta = "Start Trial →" -homeHeadline = "Process millions of PDFs." - -[portal.recentActivity] -title = "Recent activity" -viewAll = "View all" - -[portal.recentActivity.empty] -description = "Pipeline runs, deploys and agent events will appear here." -title = "Nothing here yet" - -[portal.search] -ariaLabel = "Search" -placeholder = "Search Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Quick actions will appear here once they're available." -noActionsTitle = "No quick actions" -noMatches = "No matches for \"{{query}}\"" -noMatchesDescription = "Try a different keyword or browse the catalogue." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Account link" -[portal.shell.header] -accountFallback = "Account" -accountMenu = "Account menu" -darkMode = "Dark mode" -lightMode = "Light mode" -search = "Search" -searchPlaceholder = "Search…" -signOut = "Sign out" -switchToDark = "Switch to dark theme" -switchToLight = "Switch to light theme" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Link Stirling account" -planEditor = "Editor plan" -planEnterprise = "Enterprise plan" -planProcessor = "Processor plan" primaryNav = "Primary navigation" switchApp = "Switch app" +[portal.shell.topbar] +closeNav = "Close navigation" +openNav = "Open navigation" +search = "Search" + [portal.sources] subtitle = "Reusable input connections that feed documents into Stirling. Configure a connection once, then reference it from any number of policies. Click a row for its config and which policies use it." title = "Sources" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Connect source" +[portal.sources.builder] +back = "Back to sources" +backToTypes = "All source types" +cancel = "Cancel" +chooseHint = "Choose where documents come from. Greyed-out connectors are on the way." +comingSoon = "Coming soon" +comingSoonHeading = "Coming soon" +create = "Create source" +createTitle = "Connect a source" +delete = "Delete" +editTitle = "Edit source" +enabled = "Enabled" +save = "Save changes" + +[portal.sources.builder.folderAccess] +description = "Folder automations can only use folders an administrator has allowed. Add it under Folder Access settings, then try again." +openSettings = "Folder Access settings" +title = "This folder isn't allowed" + [portal.sources.delete] body = "Delete \"{{name}}\"? This can't be undone. Policies that reference it would need to be updated." cancel = "Cancel" confirm = "Delete" title = "Delete source?" -[portal.sources.detail] -closeAriaLabel = "Close detail" -delete = "Delete source" -docs24h = "Last 24h" -docs30d = "Last 30 days" -docsTotal = "Total seen" -docsTrend = "Documents over the last 30 days" -documents = "Documents" -edit = "Edit" -notReferenced = "Not referenced by any policy, so it's safe to delete." -pause = "Pause" -resume = "Resume" -subtitle = "{{type}} · {{status}}" -usedBy = "Used by" - -[portal.sources.empty] -description = "Connect a folder (and, soon, cloud storage) so your policies have somewhere to pull documents from." -title = "No sources connected yet" - [portal.sources.kpi] inUse = "In use" total = "Connections" unused = "Unused" +[portal.sources.networkFields.connection] +helperText = "The stored connection with the server address and credentials. Reused by every source that references it." +label = "Connection" + +[portal.sources.networkFields.directory] +helperText = "Folder on the server to poll, relative to the login home or share root. Leave blank for the root." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume removes each file from the server once every policy has processed it." +label = "Read mode" + +[portal.sources.networkFields.mode.options] +consume = "Consume: process each file once" +snapshot = "Snapshot: re-read the folder every run" + +[portal.sources.networkFields.recursive] +label = "Folder depth" + +[portal.sources.networkFields.recursive.options] +all = "Include subfolders" +top = "Top level only" + [portal.sources.status] active = "Active" disabled = "Disabled" unused = "Unused" [portal.sources.table] +documents = "Documents" source = "Source" status = "Status" +type = "Type" usedBy = "Policies" +[portal.sources.types.box] +description = "Pull documents from Box folders your team already shares." +label = "Box" + +[portal.sources.types.dropbox] +description = "Pull documents from a shared Dropbox folder." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documents your team has processed in the editor, across policy and AI runs." label = "Editor" +noPolicies = "No policies run from the editor yet." + +[portal.sources.types.email] +description = "Turn attachments sent to a dedicated mailbox into documents." +label = "Email inbox" [portal.sources.types.folder] description = "Watch a directory on the server for new documents." @@ -7961,25 +8829,80 @@ label = "Folder depth" all = "Include subfolders" top = "Top level only" +[portal.sources.types.ftp] +description = "Poll an FTP or FTPS server folder for new documents." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Pull documents from shared Google Drive folders." +label = "Google Drive" + +[portal.sources.types.network] +description = "Watch an SMB network share for new documents." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "Watch OneDrive folders for new documents." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Pull documents from an Amazon S3 or S3-compatible bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "The stored connection holding the bucket and credentials. Reused by every source and pipeline output that references it." +label = "Connection" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume removes each object from the bucket once every policy has processed it." +label = "Read mode" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: process each object once" +snapshot = "Snapshot: re-read the bucket every run" + +[portal.sources.types.s3.fields.prefix] +helperText = "Only objects whose keys start with this prefix are processed." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Poll an SFTP drop folder for new documents." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Pull documents from SharePoint document libraries." +label = "SharePoint" + [portal.sources.types.unknown] label = "Source" +[portal.sources.types.webhook] +createNote = "A delivery URL and signing secret are generated when you create this webhook. Senders POST documents to that URL and the referencing policies run on arrival." +description = "Receive documents by signed HTTP POST from any external system." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "The signing secret is shown once, when the webhook is created. Recreate the source to roll it." + +[portal.sources.types.webhook.reveal] +copy = "Copy" +done = "Done" +secret = "Signing secret" +secretHelp = "Sign each delivery's raw body with this key (HMAC-SHA256) and send it as the X-Stirling-Signature header." +secretWarning = "Copy the signing secret now. For your security it is shown only once and cannot be retrieved later." +title = "Webhook created" +url = "Delivery URL" +usage = "POST each document as the raw request body to the delivery URL with a binary content type (application/pdf or application/octet-stream). Referencing policies run automatically on arrival." + [portal.sources.wizard] -back = "Back" -cancel = "Cancel" -continue = "Continue" -editTitle = "Edit source" name = "Name" namePlaceholder = "e.g. Claims intake" -save = "Save changes" -subtitle = "Step {{current}} of {{total}} · {{label}}" -title = "Connect a source" type = "Type" -[portal.sources.wizard.steps] -chooseType = "Choose type" -configure = "Configure" -review = "Review & connect" +[portal.stepModal] +close = "Close" [portal.usage] managePayment = "Manage payment" @@ -8039,16 +8962,9 @@ summary = "Owns a team — manages its members' resources and shared configs." 2 = "Portal access via the default policy" 3 = "Everything Member can do" -[portal.welcome] -ariaLabel = "Welcome to Stirling PDF" -badge = "Open-source" -installEditor = "Install the Editor" -inviteTeammates = "Invite teammates" -openInBrowser = "Open in browser" -perks = "Free forever · Self-hostable" -subtitle = "The world's most secure PDF Editor is free for teams of all sizes. Includes 60+ PDF operations and SSO." -title = "Welcome to" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Unlimited" [printFile] title = "Print File" @@ -8631,6 +9547,14 @@ text = "Rotate your PDF pages clockwise or anticlockwise in 90-degree increments [rotate.tooltip.header] title = "Rotate Settings Overview" +[ruler] +clearAll = "Clear all" +hideAllLabels = "Hide all labels" +hideSmallLabels = "Hide small labels" +physicalValues = "Physical" +scaled = "Scaled" +showAllLabels = "Show all labels" + [sanitize] desc = "Remove potentially harmful elements from PDF files." sanitizationResults = "Sanitisation Results" @@ -8672,31 +9596,45 @@ label = "Remove XMP Metadata" [sanitize.steps] settings = "Settings" -[sanitizePdf] -tags = "clean,secure,safe,remove-threats" - [sanitizePDF] title = "Sanitise PDF" +[sanitizePdf] +tags = "clean,secure,safe,remove-threats" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Adjust page-scale" +[scaleSettings] +activeScale = "Active Scale" +apply = "Apply Scale" +applyCalibration = "Apply Calibration" +calculatedScale = "Calculated scale" +calibrate = "Calibrate" +calibrating = "Calibrating" +calibrationDistanceRequired = "Enter a real-world distance greater than zero" +calibrationInvalid = "Unable to calculate scale from this measurement" +calibrationPaperDistance = "Measured page distance: {{distance}}" +calibrationTitle = "Calibrate Scale" +calibrationTooltip = "Measure a known distance to calculate the scale automatically" +customScale = "Custom Scale" +noneSet = "No custom scale set" +presets = "Preset Scales" +ratio = "Scale Ratio" +ratioHelp = "Ratio: 1 page unit = X real-world units" +ratioPlaceholder = "e.g., 100" +ratioPositive = "Ratio must be greater than zero" +ratioRequired = "Ratio is required" +realDistance = "Real-world distance" +realDistancePlaceholder = "e.g., 5" +reset = "Reset to Defaults" +unit = "Unit" +unitInvalid = "Invalid unit: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Angle Threshold:" -10 = "Extra padding (in pixels) around each saved photo so edges aren't cut." -2 = "Tilt (in degrees) needed before we auto-straighten a photo." -3 = "Tolerance:" -4 = "How closely a colour must match the page background to count as background. Higher = looser, lower = stricter." -5 = "Minimum Area:" -6 = "Smallest photo size (in pixels²) we'll keep to avoid tiny fragments." -7 = "Minimum Contour Area:" -8 = "Smallest edge/shape we consider when finding photos (filters dust and specks)." -9 = "Border Size:" - [scannerImageSplit] submit = "Extract Image Scans" tags = "separate,auto-detect,scans,multi-photo,organise" @@ -8728,6 +9666,18 @@ whatThisDoes = "What this does" whatThisDoesDesc = "Automatically finds and extracts each photo from a scanned page or composite image-no manual cropping." whenToUse = "When to use" +[ScannerImageSplit.selectText] +1 = "Angle Threshold:" +10 = "Extra padding (in pixels) around each saved photo so edges aren't cut." +2 = "Tilt (in degrees) needed before we auto-straighten a photo." +3 = "Tolerance:" +4 = "How closely a colour must match the page background to count as background. Higher = looser, lower = stricter." +5 = "Minimum Area:" +6 = "Smallest photo size (in pixels²) we'll keep to avoid tiny fragments." +7 = "Minimum Contour Area:" +8 = "Smallest edge/shape we consider when finding photos (filters dust and specks)." +9 = "Border Size:" + [search] noResults = "No results found" placeholder = "Enter search term..." @@ -8745,46 +9695,22 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint} [session] expired = "Your session has expired. Please refresh the page and try again." -[sessionManagement.tooltip] -header = "Managing Signing Sessions" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "New participants added to the end of signing order" -bullet2 = "Cannot add participants after session is finalised" -bullet3 = "Each participant receives a notification when it's their turn" -description = "You can add more participants to an active session at any time before finalisation." -title = "Adding Participants" - -[sessionManagement.tooltip.finalization] -bullet1 = "Full finalisation: All participants have signed" -bullet2 = "Partial finalisation: Some participants haven't signed yet" -bullet3 = "Unsigned participants will be excluded from the final document" -bullet4 = "Once finalised, you can load the signed PDF into active files" -description = "Finalisation combines all signatures into a single signed PDF. This action cannot be undone." -title = "Session Finalisation" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Cannot remove participants who have already signed" -bullet2 = "Removed participants no longer receive notifications" -bullet3 = "Signing order adjusts automatically" -description = "Participants can be removed from sessions before they sign." -title = "Removing Participants" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Each signature is applied sequentially to the PDF" -bullet2 = "Later signers can see earlier signatures" -bullet3 = "Critical for approval workflows and legal chains of custody" -description = "The order you specify when creating the session determines who signs first." -title = "Signature Order" - [settings] close = "Close" +[settings.ai] +documents = "Documents & RAG" +general = "General" +limits = "Limits & Performance" +models = "Models & Providers" +title = "AI" + [settings.configuration] advanced = "Advanced" database = "Database" endpoints = "Endpoints" features = "Features" +folderAccess = "Folder Access" mcp = "MCP Server" storageSharing = "File Storage & Sharing" systemSettings = "System Settings" @@ -8818,6 +9744,8 @@ defaultPdfEditor = "Default PDF editor" defaultPdfEditorActive = "Stirling PDF is your default PDF editor" defaultPdfEditorChecking = "Checking..." defaultPdfEditorInactive = "Another application is set as default" +defaultPdfEditorRemind = "Remind me to set as default" +defaultPdfEditorRemindDescription = "Show a banner when Stirling PDF is not your default PDF application." defaultPdfEditorSet = "Already Default" defaultStartupView = "Default view on launch" defaultStartupViewDescription = "Choose which view is active when the app starts" @@ -8835,9 +9763,18 @@ languageDescription = "Choose the display language" logout = "Log out" setAsDefault = "Set as Default" theme = "Theme" +themeAccent = "Accent colour" +themeAccentCustom = "Custom colour" +themeAccentCustomHint = "Custom (defaults recommended)" +themeAccentDark = "Dark mode accent colour" +themeAccentDefault = "Default" +themeAccentDefaultHint = "Default theme (recommended)" +themeAccentDescription = "Buttons, links and highlights follow it, and it subtly tints the app. Light and dark each have their own. System uses whichever is active." +themeAccentLight = "Light mode accent colour" themeDark = "Dark" themeDescription = "Switch between light and dark mode" themeLight = "Light" +themeReset = "Restore theme to default" themeSystem = "System" title = "General" user = "User" @@ -8851,6 +9788,12 @@ intro = "Enable user authentication, team management, and workspace features for learnMore = "Learn more in documentation" title = "For System Administrators" +[settings.general.loginLanding] +description = "Choose which app opens when you sign in to Stirling Cloud." +editor = "Editor" +processor = "Processor" +title = "After signing in" + [settings.general.mode] fullscreen = "Fullscreen" sidebar = "Sidebar" @@ -9031,9 +9974,6 @@ title = "Policies & Privacy" [settings.preferences] title = "Preferences" -[settings.search] -placeholder = "Search settings pages..." - [settings.security] description = "Update your password to keep your account secure." title = "Security" @@ -9059,6 +9999,11 @@ security = "Security" telegram = "Telegram" title = "Security & Authentication" +[settings.signOut] +confirm = "Are you sure you want to sign out?" +submit = "Sign out" +title = "Sign out" + [settings.team] title = "Team" @@ -9174,6 +10119,7 @@ last = "Last page" maintainRatio = "Toggle maintain aspect ratio" next = "Next page" noSavedSigs = "No saved signatures found" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personal Signatures" previous = "Previous page" redo = "Redo" @@ -9212,6 +10158,7 @@ backgroundRemovalFailedTitle = "Background removal failed" hint = "Upload a PNG or JPG image of your signature" label = "Upload signature image" placeholder = "Select image file" +previewAlt = "Current image signature" processing = "Processing image..." removeBackground = "Remove white background (make transparent)" @@ -9225,6 +10172,17 @@ saved = "Select a saved signature above, then click anywhere on the PDF to place text = "After entering your name above, click anywhere on the PDF to place your signature." title = "How to add signature" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Move Signature" pause = "Pause placement" @@ -9671,10 +10629,30 @@ title = "Upload to Server" updateButton = "Update on Server" uploadButton = "Upload to Server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "click here" +fallback = "If it didn't open automatically," +opening = "Opening Swagger UI in a new tab..." tags = "api,documentation,swagger,endpoints,development" title = "API Documentation" @@ -9782,12 +10760,39 @@ selectFilesHint = "Select files in Active Files to run this tool" singleFileScope = "Only applying to: {{fileName}}" viewerMode = "Switch to the file editor to add multiple files." +[toolFormat] +ANY = "any file" +CBR = "a comic book archive (CBR)" +CBZ = "a comic book archive (CBZ)" +CSV = "a CSV file" +EBOOK = "an ebook" +EMAIL = "an email" +EXCEL = "a spreadsheet" +HTML = "a web page" +IMAGE = "images" +JAVASCRIPT = "a JavaScript file" +JSON = "a JSON file" +MARKDOWN = "a Markdown file" +NONE = "no file" +PCL = "a PCL print file" +PDF = "a PDF" +PDF_ENCRYPTED = "a password-protected PDF" +POSTSCRIPT = "a PostScript file" +PPT = "a presentation" +TEXT = "a text file" +VIDEO = "a video" +WORD = "a Word document" +XML = "an XML file" +XPS = "an XPS document" +ZIP = "a ZIP archive" + [toolPanel] alpha = "Alpha" backToAllTools = "Back to all tools" collapse = "Collapse panel" expand = "Expand panel" goBack = "Go back" +pdfTools = "PDF Tools" placeholder = "Choose a tool to get started" premiumFeature = "Premium feature:" toolsHeader = "Tools" @@ -9824,11 +10829,6 @@ noToolsFound = "No tools found" quickAccess = "QUICK ACCESS" searchPlaceholder = "Search tools..." -[toolPicker.categories] -advancedTools = "Advanced Tools" -recommendedTools = "Recommended Tools" -standardTools = "Standard Tools" - [toolPicker.subcategories] advancedFormatting = "Advanced Formatting" automation = "Automation" @@ -9842,6 +10842,9 @@ removal = "Removal" signing = "Signing" verification = "Verification" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "No tools found" noTools = "No tools available" @@ -9968,28 +10971,53 @@ title = "Users" you = "(you)" [users.action] +cancelInvite = "Cancel invitation" deleteTeam = "Delete team" disableMfa = "Reset MFA" move = "Move to team" reinstate = "Reinstate" remove = "Remove from org" +removeTeam = "Remove from team" rename = "Rename team" resetPw = "Reset password" suspend = "Suspend" unlock = "Unlock account" +[users.activity] +daysAgo = "{{count}}d ago" +hoursAgo = "{{count}}h ago" +justNow = "Just now" +minutesAgo = "{{count}}m ago" +monthsAgo_one = "{{count}} month ago" +monthsAgo_other = "{{count}} months ago" +never = "Never" +weeksAgo_one = "{{count}} week ago" +weeksAgo_other = "{{count}} weeks ago" +yearsAgo_one = "{{count}} year ago" +yearsAgo_other = "{{count}} years ago" + [users.cap] addProcessor = "+ Processor" approver = "Approves policy" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Cancel the invitation to {{email}}? They won't be able to join with the current link." +cancelInviteTitle = "Cancel invitation" deleteTeamBody = "Delete the {{name}} team? The team must be empty first - move its members to another team, and it can't still own any integration configs." deleteTeamTitle = "Delete team" disableMfaBody = "Remove {{name}}'s MFA enrolment? They'll set it up again on next login if required." disableMfaTitle = "Reset MFA" removeBody = "Permanently remove {{name}} from the organisation? This cannot be undone." +removeTeamBody = "Remove {{name}} from the team? They keep their account but lose access to this team's resources." removeTitle = "Remove member" [users.empty] @@ -10000,10 +11028,8 @@ title = "No members yet" addToTeam = "Add to team" guestCount = "{{count}} guest" guests = "Guests" -guestsDesc = "External collaborators, scoped to what you shared. Editor only." ledBy = "led by {{owner}}" org = "Organisation" -orgDesc = "Owners with org-wide authority and policy approval" owners = "{{count}} owner" team = "{{name}} team" teamMeta = "{{count}} people" @@ -10040,6 +11066,18 @@ username = "Username" usernameError = "Username must be at least 3 characters" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invited by {{who}}" +cancel = "Cancel" +expiresInDays_one = "Expires in {{count}} day" +expiresInDays_other = "Expires in {{count}} days" +expiresToday = "Expires today" +title = "Pending invitations" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Something went wrong reaching the back end, or you don't have access. Try again." title = "Couldn't load members" @@ -10229,10 +11267,15 @@ searchPlaceholder = "Search attachments" title = "Attachments" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bookmark title" +bookmarkTitleRequired = "Bookmark title is required" closeSidebar = "Close bookmarks sidebar" collapseAll = "Collapse all bookmarks" +empty = "No bookmarks in this document" expandAll = "Expand all bookmarks" +searchPlaceholder = "Search bookmarks" +title = "Bookmarks" [viewer.comments] addComment = "Add comment" @@ -10254,10 +11297,12 @@ locateAnnotation = "Locate in document" moreActions = "More actions" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Page {{page}}" placingHint = "Click a page to place… (cancel)" removeCommentOnly = "Remove comment only" saveReply = "Save reply" +searchPlaceholder = "Search comments" title = "Comments" typeComment = "Comment" typeInsertText = "Insert Text" @@ -10280,6 +11325,7 @@ unsavedDesc = "You have unsaved changes" closeSidebar = "Close layers sidebar" hideAll = "Hide all layers" showAll = "Show all layers" +title = "Layers" [viewer.link] delete = "Delete link" @@ -10623,30 +11669,6 @@ title = "Watermark Text" image = "Image" text = "Text" -[wetSignature.tooltip] -header = "Signature Creation Methods" - -[wetSignature.tooltip.draw] -bullet1 = "Customise pen colour and thickness" -bullet2 = "Clear and redraw until satisfied" -bullet3 = "Works on touch devices (tablets, phones)" -description = "Create a handwritten signature using your mouse or touchscreen. Best for personal, authentic signatures." -title = "Draw Signature" - -[wetSignature.tooltip.type] -bullet1 = "Choose from multiple fonts" -bullet2 = "Customise text size and colour" -bullet3 = "Perfect for standardised signatures" -description = "Generate a signature from typed text. Fast and consistent, suitable for business documents." -title = "Type Signature" - -[wetSignature.tooltip.upload] -bullet1 = "Supports PNG, JPG, and other image formats" -bullet2 = "Transparent backgrounds recommended for best results" -bullet3 = "Image will be resized to fit signature area" -description = "Upload a pre-created signature image. Ideal if you have a scanned signature or company logo." -title = "Upload Signature Image" - [workbenchBar] activeFiles = "Active Files" annotations = "Annotations" @@ -10660,6 +11682,7 @@ exitRedaction = "Exit Redaction Mode" exportAll = "Export PDF" exportSelected = "Export Selected Pages" formFill = "Fill Form" +hideToolbar = "Hide toolbar" multiTool = "Multi-Tool" panMode = "Pan Mode" print = "Print PDF" @@ -10670,6 +11693,7 @@ redact = "Redact" rotateLeft = "Rotate Left" rotateRight = "Rotate Right" ruler = "Ruler / Measure" +rulerSettings = "Scale Settings" save = "Save" saveAll = "Save All" saveAs = "Save As" @@ -10679,6 +11703,7 @@ selectAll = "Select All" selectByNumber = "Select by Page Numbers" selectLanguage = "Select language" share = "Share" +showToolbar = "Show toolbar" toggleAnnotations = "Toggle Annotations Visibility" toggleAttachments = "Toggle Attachments" toggleBookmarks = "Toggle Bookmarks" diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 48677633c4..d2eda294b2 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alphabet" apply = "Apply" applyAndContinue = "Save & Leave" -areYouSure = "Are you sure you want to leave?" back = "Back" black = "Black" blue = "Blue" @@ -46,10 +45,8 @@ filesSelected = "{{count}} files" font = "Font" fontSizeTooltip = "Size of the page number text in points. Larger numbers create bigger text." fontTypeTooltip = "Font family for the page numbers. Choose based on your document style." -genericSubmit = "Submit" green = "Green" help = "Help" -imgPrompt = "Select Image(s)" incorrectPasswordMessage = "Current password is incorrect." info = "Info" invalidUndoData = "Cannot undo: invalid operation data" @@ -62,7 +59,6 @@ loginAgreementTitle = "Login Agreement" logOut = "Log out" marginTooltip = "Distance between the page number and the edge of the page." moreOptions = "More Options" -multiPdfPrompt = "Select PDFs (2+)" never = "Never" no = "No" noFileSelected = "No file loaded. Please upload one." @@ -80,7 +76,6 @@ pageOrderPrompt = "Custom Page Order (Enter a comma-separated list of page numbe pages = "Pages" pageSelectionPrompt = "Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1)" password = "Password" -pdfPrompt = "Select PDF(s)" pin = "Pin File (keep active after tool run)" pro = "Pro" processingComplete = "Your file is ready." @@ -110,6 +105,7 @@ undoSuccess = "Operation undone successfully" unknown = "Unknown" unpin = "Unpin File (replace after tool run)" unsavedChanges = "You have unsaved changes to your PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Unsaved Changes" unsupported = "Unsupported" username = "Username" @@ -692,7 +688,6 @@ manualLinks = "Manual downloads: click the links and place the files into the te noLanguages = "No tessdata languages found in the configured directory." permissionNotice = "The tessdata path is not writable. Downloads will be opened in the browser; please save the .traineddata files manually into the tessdata folder." -# AI engine admin settings (AI nav group) [admin.settings.ai.documents] description = "Configure the embedding model and retrieval settings used to answer questions over documents. Applied to the AI engine when saved." title = "Documents & RAG" @@ -1884,6 +1879,9 @@ width = "Width" [app] description = "The Free Adobe Acrobat alternative (10M+ Downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convert to PDF/A-3b" convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments" @@ -2176,6 +2174,73 @@ secureWorkflowDesc = "Secures PDF documents by removing potentially malicious co description = "This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Auto Rotate" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Confidence threshold" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "OCR only" +text = "Text only" +title = "Detection method" + +[autoRotate.error] +failed = "An error occurred while auto-rotating the PDF." + +[autoRotate.inferUndetected] +title = "Rotate low confidence pages to match" + +[autoRotate.report] +applied = "{{degrees}}° CW" +confidenceValue = "Confidence {{value}}" +noChange = "No change" +pageLabel = "Page {{number}}" +summary = "{{rotated}} of {{total}} pages rotated, {{unchanged}} left as they are" +title = "Detection report" + +[autoRotate.report.method] +inferred = "Matched" +none = "Skipped" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Below threshold" +blankPage = "Page is blank" +inferredFromDocument = "Same as the other pages" +noDominantDirection = "Mixed text directions" +osdFailed = "No readable text found" +osdNoVerdict = "Could not tell which way up" +tesseractUnavailable = "OCR not installed" +tooFewGlyphs = "Too little text" + +[autoRotate.results] +title = "Auto Rotate Results" + +[autoRotate.settings] +title = "Settings" + +[autoRotate.tooltip.confidenceThreshold] +text = "How certain the check has to be before a page is turned. Lower it to rotate more pages, raise it to leave more pages untouched." +title = "Confidence threshold" + +[autoRotate.tooltip.description] +text = "Works out which way up each page should be and turns it the right way round. Pages that are already upright are left alone." + +[autoRotate.tooltip.detectionMode] +text = "Auto reads the text on the page first, then looks at the page as an image if that is not enough. Text only skips the image check, which is quickest for documents that were never scanned. OCR only treats every page as an image." +title = "Detection method" + +[autoRotate.tooltip.header] +title = "Auto Rotate" + +[autoRotate.tooltip.inferUndetected] +text = "Blank or nearly blank pages cannot be checked on their own. When every page that could be read needs the same turn, these pages are turned to match them." +title = "Rotate low confidence pages to match" + [autoSizeSplitPDF] tags = "pdf,split,document,organization" @@ -2378,33 +2443,6 @@ keywords = "Keywords: odd, even" numbers = "Numbers/ranges: 5, 10-20" progressions = "Progressions: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Certificate Types" - -[certificateChoice.tooltip.organization] -bullet1 = "Managed by system administrators" -bullet2 = "Shared across authorized users" -bullet3 = "Represents company identity, not individual" -bullet4 = "Best for: Official documents, team signatures" -description = "A shared certificate provided by your organization. Used for company-wide signing authority." -title = "Organization Certificate" - -[certificateChoice.tooltip.personal] -bullet1 = "Generated automatically on first use" -bullet2 = "Tied to your user account" -bullet3 = "Cannot be shared with other users" -bullet4 = "Best for: Personal documents, individual accountability" -description = "An auto-generated certificate unique to your user account. Suitable for individual signatures." -title = "Personal Certificate" - -[certificateChoice.tooltip.upload] -bullet1 = "Requires P12/PFX file and password" -bullet2 = "Can be issued by external Certificate Authorities" -bullet3 = "Higher trust level for legal documents" -bullet4 = "Best for: Legally binding contracts, external validation" -description = "Use your own PKCS#12 certificate file. Provides full control over certificate properties." -title = "Upload Custom P12" - [certSign] awaitingSignatures = "Awaiting signatures" chooseCertificate = "Choose Certificate File" @@ -2731,34 +2769,6 @@ server = "Server" stepTitle = "Certificate source" upload = "Upload" -[certSign.tooltip.header] -title = "About Managing Signatures" - -[certSign.tooltip.overview] -bullet1 = "Check existing signatures and their validity" -bullet2 = "View detailed information about signers and certificates" -bullet3 = "Add new digital signatures to secure your documents" -bullet4 = "Multiple files supported with easy navigation" -text = "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing." -title = "What can this tool do?" - -[certSign.tooltip.signing] -bullet1 = "Supports PEM, PKCS12, JKS, and server certificate formats" -bullet2 = "Option to show or hide signature on the PDF" -bullet3 = "Add reason, location, and signer name" -bullet4 = "Choose which page to place visible signatures" -bullet5 = "Use server certificate for simple 'Sign with Stirling-PDF' option" -text = "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only." -title = "Adding Signatures" - -[certSign.tooltip.validation] -bullet1 = "Shows if signatures are valid or invalid" -bullet2 = "Displays signer information and signing date" -bullet3 = "Checks if the document was modified after signing" -bullet4 = "Can use custom certificates for verification" -text = "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing." -title = "Checking Signatures" - [changeCreds] changeUsername = "Update your username. You will be logged out after updating." credsUpdated = "Account updated" @@ -2926,12 +2936,16 @@ text = "To make these permissions unchangeable, use the Add Password tool to set [chat.actions] copy = "Copy message" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling agent options" +assistant = "AI Assistant" clearChat = "Clear chat" [chat.input] @@ -2994,28 +3008,35 @@ tooltip = "This operation will use your cloud credits" tooltip = "Pick color from screen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Choose color" [common] +actions = "Actions" back = "Back" cancel = "Cancel" close = "Close" +codeSample = "Code sample" collapse = "Collapse" confirm = "Confirm" continue = "Continue" copied = "Copied!" copy = "Copy" done = "Done" +enterValue = "Enter value" error = "Error" expand = "Expand" loading = "Loading..." next = "Next" +open = "Open" preview = "Preview" previous = "Previous" refresh = "Refresh" remaining = "Remaining" retry = "Retry" save = "Save" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Clear selected" @@ -3178,6 +3199,7 @@ title = "Compression Method" [compress.settings] desiredSize = "Desired File Size" desiredSizePlaceholder = "Enter size" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Compression is an easy way to reduce your file size. Pick File Size to enter a target size and have us adjust quality for you. Pick Quality to set compression strength manually." @@ -3380,6 +3402,24 @@ pdfOptions = "PDF Options" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Choose a source format first" settings = "Settings" single = "Single" @@ -3454,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Advanced monitoring" -allInOneWorkspace = "All-in-one PDF workspace (viewer, tools & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Dedicated support & SLAs" -fasterThroughput = "10x faster throughput" -fullyPrivateFiles = "Fully private files" -largeFileProcessing = "Large file processing" -orgWideAccess = "Org-wide access controls" -premiumAiModels = "Premium AI models" -prioritySupport = "Priority support" -privateDocCloud = "Private Document Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Secure API access" -standardThroughput = "Standard throughput" -unlimitedApiAccess = "Unlimited API access" -unlimitedSeats = "Unlimited seats" - [crop] autoCrop = "Auto-crop whitespace" noFileSelected = "Add a PDF file to begin cropping" @@ -3536,6 +3558,7 @@ message = "Failed to set default PDF handler" title = "Error" [defaultApp.prompt] +dontRemind = "Don't remind me again" message = "Make Stirling PDF your default application for opening PDF files." [defaultApp.settingsOpened] @@ -3575,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Search..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Edit" @@ -3834,11 +3861,13 @@ shareSelected = "Share Files" sharing = "Sharing" showAll = "Show All" showHistory = "Show History" +sortBy = "Sort files" sortByDate = "Sort by Date" sortByName = "Sort by Name" sortBySize = "Sort by Size" storage = "Storage" storageState = "Storage" +storageUsed = "Storage used" synced = "Synced" title = "Upload PDF Files" toolChain = "Tools Applied" @@ -3884,13 +3913,17 @@ addFiles = "Add files" addingFiles = "Adding files…" collapse = "Collapse sidebar" customizeGroups = "Customize groups" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Open files to get started" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expand sidebar" -files = "Files" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive is not configured" leaveMyFiles = "Leave My Files" +library = "PDF Library" myFiles = "My Files" noFiles = "No files yet" openFileManager = "Browse all files & folders" @@ -3898,15 +3931,17 @@ openFromComputer = "Open from computer" openSettings = "Open settings" other = "Other" recent = "Recent" -search = "Search" -searchPlaceholder = "Search files..." viewAll = "View all {{count}} files" [fileSidebar.fileItem] closeViewer = "Close viewer" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Open in viewer" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" @@ -3918,6 +3953,14 @@ reset = "Show all" subtitle = "Show or hide categories in the files sidebar." title = "Sidebar categories" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Add to workspace" addToWorkspaceCount = "Add {{count}} to workspace" @@ -3926,9 +3969,9 @@ back = "Back" backToFolder = "Back to {{folder}}" backToMyFiles = "Back to My Files" breadcrumbs = "Folder path" +bulkActions = "Actions" cancel = "Cancel" classification = "Classification" -clearSearch = "Clear search" clearSelection = "Clear selection" closeDetails = "Close details" create = "Create" @@ -3960,6 +4003,7 @@ downloadAll = "Download all" downloadVersion = "Download this version" dropOverlay = "Drop files to upload" dropOverlaySub = "Files start in Local. Use 'Move to' or 'Save to cloud' to organize them into a folder." +duplicate = "Duplicate" file = "File" fileInfo = "File info" fileMenu = "File actions" @@ -3987,13 +4031,12 @@ refresh = "Refresh from server" remove = "Delete" removeVersion = "Remove this version" rename = "Rename" +renamed = "Renamed" renameFolder = "Rename folder" resizeFolderTree = "Resize folder tree (arrow keys, Shift for bigger steps; double-click to auto-fit)" save = "Save" saveToServer = "Save to server" saveToServerDisabledHint = "Saving to the server isn't enabled on this server. Ask your admin to enable it." -search = "Search" -searchPlaceholder = "Search this folder & subfolders" selectAll = "Select all" selectAllHint = "Click to select all. Tip: hold Ctrl (or Cmd) to add files one at a time, Shift to select a range." selectedCount = "{{count}} selected" @@ -4053,6 +4096,10 @@ title = "No cloud files yet" hint = "Files saved without uploading stay here. Drop a file to add one." title = "No local-only files" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Files you open or edit will appear here." title = "Nothing modified yet" @@ -4072,6 +4119,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Could not delete folder." deleteFolderFailedDetail = "Could not delete folder: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Could not update folder appearance." folderAppearanceFailedDetail = "Could not update folder appearance: {{message}}" moveFilesFailed = "Could not move files." @@ -4089,13 +4138,16 @@ count = "Files" folder = "Folder" labels = "Labels" modified = "Modified" -name = "Name" size = "Size" -toolHistoryAtVersion = "Cumulative tool chain" totalSize = "Total size" type = "Type" versionHistory = "Version journey" +[filesPage.filters] +activeCount = "{{count}} filters active" +clearAll = "Clear filters" +label = "Filters" + [filesPage.folderName] cancel = "Cancel" error = "Could not save folder. Try again." @@ -4124,7 +4176,13 @@ localHint = "Only stored in this browser" shared = "Shared" sharedHint = "Shared with you via link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] +label = "Sort files" modifiedAsc = "Oldest first" modifiedDesc = "Recent first" nameAsc = "Name A→Z" @@ -4179,6 +4237,7 @@ noFilesInStorageOpen = "No files available in storage. Open some files first." open = "Open" openFile = "Open File" openFiles = "Open Files" +selectFile = "Select {{name}}" selectFromStorage = "Select from Storage" upload = "Upload" uploadFile = "Upload File" @@ -4252,9 +4311,14 @@ issues = "GitHub" [formFill] allSaved = "All saved" +analyzingFields = "Analysing form fields..." extractCsvError = "Failed to extract CSV" extractXlsxError = "Failed to extract XLSX" +filled = "filled" flattenAfterFilling = "Flatten after filling" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" requiredAbbreviation = "req" requiredFieldsError = "Please fill in all required fields" rescanFields = "Re-scan fields" @@ -4495,6 +4559,11 @@ desc = "Auto renames a PDF file based on its detected header" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "Auto Rename PDF File" +[home.autoRotate] +desc = "Detect each page's orientation and rotate it upright automatically." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Auto Rotate" + [home.autoSizeSplitPDF] tags = "auto,split,size" title = "Auto Split by Size/Count" @@ -4801,9 +4870,6 @@ title = "Image to PDF" [imageToPdf] tags = "conversion,img,jpg,picture,photo" -[infoBanner] -dismiss = "Dismiss" - [invite] acceptError = "Failed to create account" accountFor = "Creating account for" @@ -4835,8 +4901,7 @@ welcomeTitle = "You've been invited!" addFiles = "Add Files" mobileUpload = "Upload from Mobile" openFromComputer = "Open from computer" -uploadFromComputer = "Upload from computer" -workbenchEmptyStateHero = "Drop a PDF anywhere" +uploadFromComputer = "Browse files" [language] direction = "ltr" @@ -4876,12 +4941,10 @@ enterUsername = "Enter username" failedToSignIn = "Failed to sign in with {{provider}}: {{message}}" forgotPassword = "Forgot your password?" loggingIn = "Logging In..." -logIn = "Log In" login = "Login" magicLinkSent = "Magic link sent to {{email}}! Check your email and click the link to sign in." mfaCode = "Authentication Code" mfaRequired = "Two-factor code required" -noAccount = "Don't have an account?" or = "Or" password = "Password" passwordChangedSuccess = "Password changed successfully! Please sign in with your new password." @@ -4901,7 +4964,6 @@ signInWith = "Sign in with" title = "Sign in" unexpectedError = "Unexpected error: {{message}}" updatePassword = "Update password" -useEmailInstead = "Login with email" useMagicLink = "Use magic link instead" username = "Username" youAreLoggedIn = "You are logged in!" @@ -4989,6 +5051,34 @@ uploadSuccess = "Upload Successful!" uploadSuccessMessage = "Your images have been transferred." validating = "Validating session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan to upload photos. Images auto-convert to PDF." descriptionNoConvert = "Scan to upload photos from your mobile device." @@ -5007,8 +5097,13 @@ title = "Upload from Mobile" tags = "Multi Tool,Multi operation,UI,click drag,front end,client side,interactive,intractable,move,delete,migrate,divide" title = "PDF Multi Tool" -[navbar] -search = "Search" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Authentication was not successful. You can close this window and try again." @@ -5137,6 +5232,7 @@ activeFiles = "The Active Files view shows all of the PDFs you allTools = "This is the Tools panel, where you can browse and select from all available PDF tools." close = "Close" cropSettings = "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to." +dialogLabel = "Onboarding" fileCheckbox = "Files on the workbench are selected for processing. You can select multiple files for batch operations using the left files sidebar." fileReplacement = "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools." filesButton = "The Files button on the Quick Access bar allows you to upload PDFs to use the tools on." @@ -5573,7 +5669,8 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] -capSuffix = "/ {{limit}} free PDFs" +barAria = "Free PDFs remaining" +capSuffix = "of {{limit}} free PDFs left" metaCategories = "Automation · AI · API requests" [payg.free.member] @@ -5652,6 +5749,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5980,6 +6078,11 @@ header = "PDF To PDF/A" tags = "archive,long-term,standard,conversion,storage,preservation" title = "PDF To PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "print,standard,conversion,production,prepress,archive" title = "PDF To PDF/X" @@ -6055,6 +6158,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Choose the plan that fits your needs" title = "Available Plans" @@ -6210,6 +6314,7 @@ ssn = "Social Security numbers" [policy] badgeEnforcing = "{{name}} enforcing..." badgeRan = "{{name}} policy ran on this file" +badgeRunning = "{{name}} running..." blockingAction = "{{action}} blocked while enforcing policy, please wait..." dismiss = "Dismiss overlay" enforcingTitle = "Enforcing policy..." @@ -6633,11 +6738,12 @@ reachedTitle = "Monthly spend limit reached" title = "Couldn't open Stripe portal" [portal.billing.walletMeter] -capSuffix_one = "of {{allowance}} free PDFs used" -capSuffix_other = "of {{allowance}} free PDFs used" +barAria = "Free PDFs remaining" +capSuffix_one = "of {{allowance}} free PDF left" +capSuffix_other = "of {{allowance}} free PDFs left" eyebrow = "Processor trial" -statusLabel_one = "{{remaining}} left" -statusLabel_other = "{{remaining}} left" +statusLabel_one = "{{used}} used" +statusLabel_other = "{{used}} used" sub = "Use the PDF Editor for free. Pay to process PDFs automatically." title_one = "Process {{allowance}} PDFs free" title_other = "Process {{allowance}} PDFs free" @@ -6706,9 +6812,15 @@ label = "Server URL" [portal.connections.commonFields.email] label = "Email" +[portal.connections.commonFields.host] +label = "Host" + [portal.connections.commonFields.password] label = "Password" +[portal.connections.commonFields.port] +label = "Port" + [portal.connections.commonFields.username] label = "Username" @@ -6841,6 +6953,27 @@ baseUrlPlaceholder = "https://your-cluster:9200" description = "Index an audit event in Elasticsearch each time a policy handles a document." label = "Elasticsearch" +[portal.connections.types.ftp] +description = "Poll an FTP or FTPS server for new documents." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Connection mode" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (recommended)" + +[portal.connections.types.ftp.fields.security] +helperText = "Plain FTP sends credentials and files unencrypted; choose FTPS if the server supports it." +label = "Encryption" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "None (plain FTP)" + [portal.connections.types.googlechat] baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." description = "Post a message to a Google Chat space when a policy runs." @@ -6855,6 +6988,32 @@ label = "Jira" description = "Email a document or a notification when a policy runs." label = "Mailgun" +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + [portal.connections.types.nextcloud] baseUrlPlaceholder = "https://your-server/remote.php/dav" description = "File processed documents into Nextcloud." @@ -6908,10 +7067,43 @@ label = "Secret access key" description = "Email a document or a notification when a policy runs." label = "SendGrid" +[portal.connections.types.sftp] +description = "Poll an SFTP (SSH) server for new documents." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optional. The server's SHA-256 host key fingerprint (from `ssh-keyscan host | ssh-keygen -lf -`). When set, only a server presenting this key is accepted; when blank, the key seen on first connect is pinned and later changes are refused." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Leave blank if you authenticate with a private key instead." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optional. Paste an OpenSSH private key to authenticate with a key instead of a password." +label = "Private key" + [portal.connections.types.slack] description = "Post a message to Slack when a policy processes a document." label = "Slack" +[portal.connections.types.smb] +description = "Watch an SMB / CIFS network share (Windows or Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Optional. The Windows domain or workgroup, if your server uses one." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + [portal.connections.types.splunk] baseUrlPlaceholder = "https://your-splunk:8088" description = "Send an audit event to Splunk each time a policy handles a document." @@ -7026,11 +7218,6 @@ title = "Official SDKs" beta = "Beta" deprecated = "Deprecated" -[portal.docs.search] -empty = "No matching docs" -placeholder = "Search docs" -results = "{{count}} results" - [portal.docs.skills] eyebrow = "SKILLS" lead = "Bundled, named capabilities your agent invokes as a single tool. Each skill is a deterministic op chain with evals attached." @@ -7129,11 +7316,11 @@ editorAction = "Editor" empty = "No documents match this filter." rowActions = "Row actions" sensitiveLabel = "Sensitive" -sensitiveTitle = "Sensitive — access required" [portal.documents.table.columns] action = "Pipeline / Action" document = "Document" +labels = "Labels" product = "Product" status = "Status" time = "Time" @@ -7149,6 +7336,7 @@ host = "Host" lastSeen = "Last seen" region = "Region" status = "Status" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -7223,6 +7411,56 @@ description = "This view hit an unexpected error. Try again, or pick another sec retry = "Try again" title = "Something went wrong on this page" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + [portal.home.download] back = "Back" body = "Download the desktop app or self-host the server." @@ -7278,13 +7516,16 @@ tagline = "Native app for Microsoft Windows" title = "Windows" [portal.home.editor] -activeUsers = "{{n}} active" -install = "Install the editor" -invite = "Invite teammates" -name = "Stirling PDF Editor" +activeOfDeployed = "{{active}} of {{total}} active this month" +name = "PDF Editor" open = "Open in browser" updated = "updated {{time}}" +[portal.home.editor.deploy] +finish = "Finish deployment" +options = "View deployment options" +start = "Deploy the Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -7295,32 +7536,12 @@ afternoon = "Good afternoon" evening = "Good evening" morning = "Good morning" -[portal.home.onboarding.enterprise] -body = "Org-wide SSO + SCIM + RBAC, committed volume pricing, and air-gapped deployment." -cta = "Start Trial" -ctaQuote = "Get Quote" -lead = "For 250+ employees." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Install the desktop app or self-host" -title = "Download the editor" - -[portal.home.onboarding.steps.invite] -blurb = "Bring your team into the secure workspace" -title = "Invite teammates" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} active · {{recommended}} recommended" -title = "Confirm your policies" - [portal.infrastructure] manageEditorDeployment = "Manage Editor deployment" sectionsAriaLabel = "Infrastructure sections" -subtitle = "Deployments, credentials, security posture, storage, and the audit trail for your Stirling workspace." +subtitle = "API credentials and the audit trail for your Stirling workspace." title = "Infrastructure" -# Fixed-enum label maps rendered via t(MAP[value]) in the infrastructure tabs. [portal.infrastructure.apiKeys] createKey = "Create key" heading = "API keys" @@ -7346,11 +7567,6 @@ cancel = "Cancel" confirm = "Revoke key" title = "Revoke API key" -[portal.infrastructure.attestationLabel] -attested = "Attested" -inScope = "In scope" -notApplicable = "N/A" - [portal.infrastructure.audit] filterAriaLabel = "Filter audit events by category" heading = "Audit logs" @@ -7422,11 +7638,6 @@ info = "Info" success = "Success" warning = "Warning" -[portal.infrastructure.certLabel] -certified = "Certified" -inProgress = "In progress" -notStarted = "Not started" - [portal.infrastructure.createKey] cancel = "Cancel" createKey = "Create key" @@ -7440,238 +7651,10 @@ subtitleCreated = "Copy this secret now — it won't be shown again." title = "Create API key" titleCreated = "Key created" -[portal.infrastructure.deployLabel] -live = "Live" -queued = "Queued" -rolledBack = "Rolled back" -rolling = "Rolling out" - -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Deployed by" -environment = "Environment" -product = "Product" -status = "Status" -version = "Version" -when = "When" - -[portal.infrastructure.deployments.recent] -heading = "Recent deployments" -subheading = "The latest rollouts across products and environments." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instances" -latency = "Latency" -load = "Load" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Throughput" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "Live health for every deployed Stirling region — latency, load, and rollout version." - -[portal.infrastructure.deployments.regions.empty] -description = "Deployed regions appear here once your workspace is provisioned." -title = "No regions deployed" - [portal.infrastructure.keyLabel] active = "Active" revoked = "Revoked" -[portal.infrastructure.modelLabel] -active = "Active" -degraded = "Degraded" -disabled = "Disabled" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "The model catalogue and routing that powers document processing across your workspace." - -[portal.infrastructure.models.byom] -description = "Register an on-prem or self-hosted model and pin it to a region for data-residency-bound processing." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "Managed models available to your workspace, with live latency and cost." -subEnterprise = "Managed, bring-your-own, and on-prem models — with per-region pinning available." - -[portal.infrastructure.models.catalogue.empty] -description = "Models in your workspace's catalogue appear here." -title = "No models available" - -[portal.infrastructure.models.columns] -cost = "Cost" -latency = "Latency" -load = "Load" -model = "Model" -status = "Status" -type = "Type" -version = "Version" - -[portal.infrastructure.models.cost] -perCall = "{{price}}/call" -perThousand = "{{price}}/1k" - -[portal.infrastructure.models.metrics] -activeModels = "Active models" -avgLatency = "Avg latency" -included = "Included" -monthlySpend = "Monthly model spend" - -[portal.infrastructure.models.routing] -empty = "No routing rules configured." -heading = "Routing rules" -sub = "Which model handles each operation. The default applies when no narrower rule matches." -subLocked = "Route operations to specific models — available on paid plans." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgrade to Pro to control which model handles each operation and document type." -title = "Model routing is a paid feature" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "Document type" -modelForAria = "Model for {{operation}}" -operation = "Operation" -routedTo = "Routed to" - -[portal.infrastructure.modelTypeLabel] -classification = "Classification" -extraction = "Extraction" -llm = "LLM" -ocr = "OCR" - -[portal.infrastructure.regionLabel] -degraded = "Degraded" -down = "Down" -healthy = "Healthy" - -[portal.infrastructure.security.access.byok] -description = "Supply a key from your own KMS. Stirling encrypts with it but can still read." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Keys never leave your KMS. Stirling holds only ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling manages encryption keys. Simplest — zero key ops on your side." -label = "Stirling-held keys" - -[portal.infrastructure.security.accessPolicy] -heading = "Document access policy" -subheading = "Controls who can decrypt processed documents at rest." - -[portal.infrastructure.security.attestations] -heading = "Compliance attestations" -noReport = "No report available" -subheading = "Framework-by-framework audit posture, with reports available on attested controls." -viewReport = "View report →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestations and certifications covering the Stirling platform." - -[portal.infrastructure.security.empty] -description = "Your workspace's security configuration will appear here." -title = "Security posture unavailable" - -[portal.infrastructure.security.hyokBanner] -description = "With HYOK, encryption keys never leave your KMS. Stirling stores and processes only ciphertext you can revoke at any time." -title = "Stirling cannot decrypt your documents" - -[portal.infrastructure.security.ipAllowlist] -empty = "No IP ranges configured — all IPs allowed." -heading = "IP allowlist" -sub = "API access is restricted to these CIDR ranges." -subLocked = "Restrict API access to known IP ranges — available on paid plans." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgrade to Pro to restrict API access to specific networks." -title = "IP allowlisting is a paid feature" - -[portal.infrastructure.security.ipColumns] -added = "Added" -addedBy = "Added by" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Encryption key management" -keyId = "Key identifier" -lastRotated = "Last rotated" -rotateKey = "Rotate key" -rotationPolicy = "Rotation policy" -subheading = "Custody of the keys that encrypt documents at rest — who can decrypt, and how keys rotate." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) and hold-your-own-key (HYOK) custody are available on Enterprise. Upgrade to supply keys from your own KMS." -title = "Keys are managed by Stirling on your plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "European Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "United States" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "Where documents are stored and processed." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} used" - -[portal.infrastructure.storage.empty] -description = "Connected storage and usage appear here." -title = "No storage configured" - -[portal.infrastructure.storage.lifecycle] -active = "Active" -activeRange = "0–{{value}}d" -archived = "Archived" -coldStorage = "cold storage" -deleted = "Deleted" -never = "never" -purged = "purged" - -[portal.infrastructure.storage.providers] -connect = "Connect" -connected = "Connected" -heading = "Connected providers" -subheading = "Where processed artifacts are written." - -[portal.infrastructure.storage.retention] -heading = "Retention" -subheading = "How long artifacts are kept before lifecycle deletion." -windowLabel = "Default retention window" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} day" -days_other = "{{count}} days" -never = "Never delete" - -[portal.infrastructure.storage.totalUsage] -heading = "Total usage" -progressLabel = "Storage used" -subheading = "Storage consumed across all connected providers." - [portal.infrastructure.tabs] apiKeys = "API Keys" audit = "Audit Logs" @@ -7681,11 +7664,9 @@ security = "Security" storage = "Storage" [portal.integrations] -addAnother = "Add another" availableHeading = "Available" comingSoonHeading = "Coming soon" connect = "Connect" -connectedHeading = "Connected" connectionCount_one = "{{count}} connection" connectionCount_other = "{{count}} connections" customApi = "Custom API" @@ -7703,6 +7684,10 @@ security = "Security" signing = "Signing" storage = "Storage" +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + [portal.integrations.status] connected = "Connected" @@ -7715,6 +7700,12 @@ pipelines = "Pipelines" policies = "Policies" sources = "Sources" +[portal.legal] +draft = "{{label}} · draft" +loadError = "Could not load this document. Please try again." +loading = "Loading…" +title = "Legal document" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7727,7 +7718,6 @@ integrations = "Integrations" pipelines = "Pipelines" policies = "Policies" procurement = "Procurement" -settings = "Settings" sources = "Sources" usage = "Usage & Billing" users = "Users" @@ -7744,59 +7734,87 @@ title = "Pipelines" newPipeline = "New pipeline" [portal.pipelines.builder] -addStep = "Add tool" +activate = "Activate" back = "Back to pipelines" +cannotFollow = "Can't take {{produced}}" chooseAccount = "Choose an account" +chooseDestination = "Choose a destination" chooseOperation = "Choose what this step does" +chooseSource = "Choose a source" discard = "Discard changes" -enabled = "Enabled" +inputs = "Input" +inputSource = "Input source" +inputTrigger = "Trigger" keepEditing = "Keep editing" -needsUpload = "Needs an uploaded file" +moreActions = "More actions" +needsConfiguring = "Needs setting up" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "No tools match your search." -pipelineSettings = "Pipeline settings" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Search tools" -selectToolBody = "Add a tool to build your pipeline." -selectToolTitle = "No tools yet" sendToSystem = "Send to another system" -stepsNeedSetup = "These steps still need an operation and an account chosen before saving: {{tools}}." -toolSettings = "Tool settings" +stepsIncompatible = "These steps can't run on what their prior step produces: {{tools}}." +stepsNeedSetup = "These steps still need setting up before saving: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Unrecognized operation, kept as-is." unsavedBody = "You have unsaved changes. Save them before leaving, or discard them?" unsavedTitle = "Unsaved changes" -uploadUnsupported = "Uploaded files aren't supported in pipelines yet, so these steps can't be saved: {{tools}}." usesDefaults = "Runs with default settings" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combines every incoming file" +fan-out = "Runs once per incoming file" +format-mismatch = "Sends {{produced}}, needs {{accepts}}" +output-uncertain = "May not run: output depends on setup" +source-mismatch = "Input is {{produced}}, needs {{accepts}}" +undeclared-operation = "Can't check what this step accepts" [portal.pipelines.composer] -addTool = "Add tool" -cancel = "Cancel" -chainEmpty = "Add a tool to start building your pipeline." +addTool = "Add a tool" create = "Create pipeline" -directory = "Output folder" -directoryHelp = "Absolute path on the server. Must be within the configured allowed folders." +createPaused = "Create paused" editingUnsupported = "Displaying these tool params for editing is not supported yet." -moveDown = "Move down" -moveUp = "Move up" +editSource = "Edit source" name = "Name" -namePlaceholder = "e.g. Redaction sweep" -noSources = "No sources connected yet. The pipeline can still run on files supplied to it directly." +namePlaceholder = "Pipeline name" noToolSettings = "This tool has no configurable settings." -operations_one = "Operation ({{count}})" -operations_other = "Operations ({{count}})" -output = "Output" -removeStep = "Remove operation" -s3PrefixHelp = "Outputs are uploaded under this key prefix." +output = "Destination" save = "Save changes" scheduleEvery = "Run every" -sources = "Sources" -sourcesLoading = "Loading sources..." trigger = "Trigger" triggerManual = "Manual only" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "days" hours = "hours" minutes = "minutes" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Delete \"{{name}}\"? This can't be undone." cancel = "Cancel" @@ -7804,7 +7822,7 @@ confirm = "Delete" title = "Delete pipeline?" [portal.pipelines.detail] -clearHistory = "Clear history" +clearHistory = "Process ignored files in source" delete = "Delete pipeline" run = "Run now" @@ -7814,16 +7832,42 @@ connectSource = "Connect a source" description = "Create your first pipeline: pick the sources it runs over, chain the operations, and choose where output goes." title = "No pipelines yet" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Active" paused = "Paused" total = "Pipelines" -[portal.pipelines.output] -folder = "Write to folder" -inline = "Return files" -s3 = "Write to Amazon S3" - [portal.pipelines.run] allProcessed_one = "Nothing to run: the source's {{count}} document has already been processed." allProcessed_other = "Nothing to run: all {{count}} documents in the sources have already been processed." @@ -7831,10 +7875,9 @@ completed_one = "Run completed." completed_other = "All {{count}} runs completed." empty = "Nothing to run: the sources had no documents to process." failed = "Run failed: {{error}}" -historyCleared = "History cleared. The next run reprocesses everything currently in the sources." inFlight = "Nothing new to run: documents are still being processed from an earlier run." -parked_one = "Nothing to run: {{count}} document failed previously and is parked. Fix the cause, then clear history to retry it." -parked_other = "Nothing to run: {{count}} documents failed previously and are parked. Fix the cause, then clear history to retry them." +parked_one = "Nothing to run: {{count}} document failed previously and is parked. Fix the cause, then reprocess the source to retry it." +parked_other = "Nothing to run: {{count}} documents failed previously and are parked. Fix the cause, then reprocess the source to retry them." running = "Run started; still in progress." timeout = "Run is taking longer than expected; it may still finish in the background." @@ -7847,6 +7890,7 @@ name = "Pipeline" sources = "Sources" status = "Status" steps = "Steps" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Folder watch" @@ -8161,6 +8205,24 @@ label = "Attach to a Jira issue" description = "Sends the processed document as an attachment." label = "Email the document (Mailgun)" +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + [portal.policies.operations.nextcloudUpload] description = "Writes the processed document into a folder." label = "Upload to Nextcloud" @@ -8370,19 +8432,27 @@ email = "Email intake" subtitle = "Get your team evaluated, contracted, and onboarded. Every document in one place." title = "Procurement" -[portal.procurement.action] -download = "Download" -pay = "Pay now" -request = "Request" -sign = "Review & sign" -upload = "Upload" - [portal.procurement.agreement] -agreeCta = "Agree & subscribe" -confirm = "I have read and agree to the Stirling Enterprise Agreement." -eyebrow = "Agreement" -intro = "One combined agreement covers your deal: Master Service Agreement, Order Form, EULA, and Data Processing Agreement. Review it, then agree to accept the quote into a committed subscription." -title = "Review your enterprise agreement" +agreeCta = "Sign agreement" +confidential = "Confidential" +confirm = "I have read and agree to this Agreement, and I represent that I am authorized to sign it on behalf of the organization named above." +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA and DPA, combined into one signature." +download = "Download" +downloadDraftError = "Could not generate the agreement PDF - the document renderer is unavailable. Please try again shortly or contact support." +downloadError = "Could not download the signed agreement. Please try again." +legalName = "Legal entity name" +legalNamePlaceholder = "The legal entity that will sign" +loadError = "Could not load the agreement. Please try again." +loading = "Loading the agreement..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Request changes" +scrollHint = "Scroll to review" +signatory = "Signatory name" +signatoryPlaceholder = "Full name" +signatoryTitle = "Signatory title" +signatoryTitlePlaceholder = "e.g. General Counsel" +signError = "Could not record your signature. Please try again." [portal.procurement.builder] addons = "Add-ons" @@ -8395,16 +8465,19 @@ businessName = "Business name" businessNamePlaceholder = "Your company" city = "City" cityPlaceholder = "San Francisco" +completeRequired = "Please complete the required fields (marked *) with a valid email before generating the quote." contactEmail = "Contact email" contactEmailPlaceholder = "jane@acme.com" contactName = "Contact name" contactNamePlaceholder = "Jane Doe" continue = "Continue" +done = "Done" eula = "I have read and agree to the Stirling Enterprise EULA. It governs the agreement generated from this quote." generate = "Generate quote" -included = "Included" -indemnification = "IP indemnification" -indemnificationSub = "We defend qualifying IP claims, per the EULA" +indemnification = "Enhanced IP Protection" +indemnificationSub = "Extends our IP defense to patent claims. Baseline copyright, trademark and trade-secret indemnification is included free." +paperEyebrow = "Enterprise quote" +paperFor = "Prepared for" pdfSize = "PDF size" poNumber = "PO number" poNumberPlaceholder = "Optional" @@ -8424,13 +8497,10 @@ region = "State / region" regionPlaceholder = "California" running = "{{annual}} / yr · {{years}}-yr {{tcv}}" s1Sub = "Your team, and the PDFs you expect to run each year." -# Step 1 — volume s1Title = "How much will you process?" s2Sub = "Longer terms discount the rate; your service level sets support." -# Step 2 — commitment & service s2Title = "Commitment and service" s3Sub = "For the quote and the agreement it generates." -# Step 3 — details s3Title = "Your details" serviceLevel = "Service level" size_compact = "Compact" @@ -8455,6 +8525,7 @@ training = "Onboarding & training" trainingSub = "Live sessions to get your team running" users = "Total users" usersPlaceholder = "e.g. 250" +viewEula = "Read it" volEstimated = "Estimated from {{count}} users (~2,000 PDFs each, including automation). Edit if you know better." volManual = "Using your figure. Re-estimate from your team size any time." volNoUsers = "Not sure? Enter your team size and we'll estimate it." @@ -8463,54 +8534,55 @@ volumePlaceholder = "e.g. 1,000,000" years_one = "{{count}} year" years_other = "{{count}} years" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Done" -here = "You're here" -hide = "Hide" -optional = "Optional" -paidAddon = "Paid add-on" -show = "Show" -subtitle = "Everything you need at each step of the journey, surfaced as the deal moves through it." -supportingSubtitle = "SOC 2, security reviews, tax forms and more, ready when your security or procurement team asks. Some carry a one-time fee." -supportingTitle = "Supporting your evaluation" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form and DPA in one signature." +download = "Download" +eula = "EULA & Commercial Terms" +eulaSub = "The software and self-serve commercial terms." +invoice = "Invoice" +invoiceSub = "Your first subscription invoice." +laterInvoice = "Available after you accept" +laterQuote = "Available once your quote is issued" +quote = "Quote" +quoteSub = "Your itemised enterprise quote." +sla = "SLA Exhibit" +slaSub = "Support tiers and response targets." +subprocessors = "Subprocessors" +subprocessorsSub = "Third parties that process data on your behalf." +subtitle = "Your deal paperwork, available any time." title = "Documents" -upcoming = "Upcoming" +view = "View" [portal.procurement.error] title = "Something went wrong" [portal.procurement.hero] -company = "Your enterprise deal" -ctaLive = "You're live" -ctaPayment = "Add payment" +barAria = "Stage {{current}} of {{total}}" +ctaAgreement = "Review & sign agreement" +ctaExploring = "Set up your trial" ctaQuote = "Review your quote" +ctaReviewQuote = "Review quote" ctaTrial = "Build your quote" -eyebrow = "Enterprise procurement" +documents = "Documents" +eyebrow = "Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invite teammates" licenseKey = "Licence key" -nextStep = "Next step: {{action}}" -notStarted = "Not started" +liveSub = "Your organization is provisioned and your licence is active." +liveTitle = "You are live on Stirling Enterprise" +next = "Next: {{stage}}" open = "Open procurement" scheduleCall = "Schedule a call" -setup1Sub = "Invite your teammates" -setup1Title = "Deploy the PDF Editor" -setup2Sub = "Turn on processing across editors and other sources" -setup2Title = "Connect the PDF Processor" -setup3Sub = "Turn on Security, Compliance, Routing, or Retention when you need them" -setup3Title = "Add recommended policies" +sentenceAgreement = "review and sign the agreement" +sentenceLive = "provisioning and rollout" +sentencePayment = "purchase order and payment" +sentenceQuote = "review and accept your quote" +sentenceTrial = "set up your trial workspace" [portal.procurement.journey] daysLeft_one = "{{count}} day left" daysLeft_other = "{{count}} days left" -engineerLabel = "Your solutions engineer" -eyebrow = "Your rollout" -live = "You're live on Stirling Enterprise" -nextStep = "Next step: {{action}}" -subtitle = "Your solutions engineer is on every step. One next action at a time; the full checklist is below." -title = "From trial to live, one guided path" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "One signature covers MSA, order form, EULA and DPA." @@ -8559,45 +8631,26 @@ description = "Your subscription is active and your licence is issued. Your team eyebrow = "Live" title = "You're live on Stirling Enterprise" -[portal.procurement.locked] -description = "Trial keys, committed-volume quotes, the one-signature agreement, payment, and your document ledger all live here once you start an enterprise evaluation." -eyebrow = "Enterprise only" -talkToSales = "Talk to sales" -title = "The procurement track opens with Enterprise" - [portal.procurement.milestone] -download = "Download PDF" downloadError = "Could not download the quote PDF just yet — please try again in a moment." -edit = "Edit quote" - -[portal.procurement.modal] -cancel = "Cancel" -chooseFile = "Choose file" -close = "Close" -downloadBody = "Your download will begin shortly." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "No file selected" -payBody = "Pay your committed contract by card or bank transfer through Stripe. Your workspace provisions as soon as payment clears." -payCta = "Continue to Stripe" -payTitle = "Confirm payment" -requestBodyFree = "We generate this on demand. Confirm and your solutions engineer will send it across shortly." -requestBodyPaid = "This is a paid add-on. Confirm and your solutions engineer will scope it and send the paperwork." -requestCta = "Request" -requestTitle = "Request this document" -signBody = "Opens the Stirling Enterprise Agreement for e-signature: one signature covers the MSA, order form, EULA and DPA. We countersign automatically and you advance to payment." -signCta = "Open for signature" -signTitle = "Review and sign your agreement" -uploadBody = "Send us your PO and we invoice against it on your terms. Drag in the PDF or pick a file below." -uploadCta = "Upload purchase order" -uploadTitle = "Upload your purchase order" [portal.procurement.payment] description = "Your quote is accepted and your licence is already active — your team can start right away. Pay the first invoice when you're ready; you can pay or download it here, no email needed." +downloadAgreement = "Download signed agreement" downloadInvoice = "Download invoice" +eyebrow = "Payment" title = "Subscription created" viewInvoice = "View & pay invoice" +[portal.procurement.review] +acceptCta = "Accept quote" +annual = "Annual fee (year 1)" +downloadCta = "Download quote" +poNumber = "Purchase order: {{po}}" +renewal = "Renews at {{amount}}/year after the term (+{{pct}}% CPI)" +tcv = "{{years}}-year term, paid in full up front: {{tcv}}" +validUntil = "Valid until {{date}}" + [portal.procurement.schedule] fallback = "Couldn't load the scheduler." fallbackLink = "Open scheduling in a new tab" @@ -8607,25 +8660,33 @@ title = "Schedule a call" [portal.procurement.setup] airgap = "Air-gapped" -airgapSub = "Fully offline, isolated network. Includes a downloadable licence file." -cloud = "Cloud" -cloudSub = "Fully managed by Stirling. Nothing for you to run." -deployment = "Where will you run Stirling?" -seats = "Team size" -seatsHint = "Roughly how many people will use it. You can refine this when you build your quote." -seatsPlaceholder = "e.g. 250" +airgapSub = "Sealed, no outbound" +back = "Back" +businessName = "Business name" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, fastest start" +continue = "Continue" +deployment = "Where should it deploy?" +eula = "I agree to the Stirling EULA & Commercial Terms." +fullName = "Full name" +fullNamePlaceholder = "Your name" +invites = "Invite teammates · optional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Schedule a call" +seats = "People testing" +seatsPlaceholder = "e.g. 15" selfhost = "Self-hosted" -selfhostSub = "Run it in your own cloud or data centre." +selfhostSub = "Docker or Kubernetes" start = "Start trial" -subtitle = "Tell us how you plan to run Stirling so we can tailor your trial and quote. No card required." +stepOf = "Step {{n}} of {{total}}" +subtitle = "Free for 14 days, no card required." +subtitleDetails = "A few details for your quote and agreement." +talkFirst = "Prefer to talk first?" title = "Set up your trial" - -[portal.procurement.status] -action = "Action needed" -available = "Available" -complete = "Complete" -pending = "Pending" -request = "On request" +viewEula = "Read it" +workEmail = "Work email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Extending adds 7 days and notifies your solutions engineer." @@ -8636,22 +8697,6 @@ maxed = "Maxed out" subtitle = "Your free trial runs through {{date}}. No card required." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutable audit, and a dedicated SE." -homeCta = "Start Trial →" -homeHeadline = "Process millions of PDFs." - -[portal.search] -ariaLabel = "Search" -placeholder = "Search Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Quick actions will appear here once they're available." -noActionsTitle = "No quick actions" -noMatches = "No matches for \"{{query}}\"" -noMatchesDescription = "Try a different keyword or browse the catalogue." - [portal.settings.groups] admin = "Admin" @@ -8661,7 +8706,6 @@ account-link = "Account link" [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" linkAccount = "Link Stirling account" primaryNav = "Primary navigation" switchApp = "Switch app" @@ -8703,15 +8747,35 @@ cancel = "Cancel" confirm = "Delete" title = "Delete source?" -[portal.sources.empty] -description = "Connect a folder (and, soon, cloud storage) so your policies have somewhere to pull documents from." -title = "No sources connected yet" - [portal.sources.kpi] inUse = "In use" total = "Connections" unused = "Unused" +[portal.sources.networkFields.connection] +helperText = "The stored connection with the server address and credentials. Reused by every source that references it." +label = "Connection" + +[portal.sources.networkFields.directory] +helperText = "Folder on the server to poll, relative to the login home or share root. Leave blank for the root." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume removes each file from the server once every policy has processed it." +label = "Read mode" + +[portal.sources.networkFields.mode.options] +consume = "Consume: process each file once" +snapshot = "Snapshot: re-read the folder every run" + +[portal.sources.networkFields.recursive] +label = "Folder depth" + +[portal.sources.networkFields.recursive.options] +all = "Include subfolders" +top = "Top level only" + [portal.sources.status] active = "Active" disabled = "Disabled" @@ -8721,6 +8785,7 @@ unused = "Unused" documents = "Documents" source = "Source" status = "Status" +type = "Type" usedBy = "Policies" [portal.sources.types.box] @@ -8771,6 +8836,10 @@ label = "Folder depth" all = "Include subfolders" top = "Top level only" +[portal.sources.types.ftp] +description = "Poll an FTP or FTPS server folder for new documents." +label = "FTP" + [portal.sources.types.googledrive] description = "Pull documents from shared Google Drive folders." label = "Google Drive" @@ -8839,10 +8908,8 @@ name = "Name" namePlaceholder = "e.g. Claims intake" type = "Type" -[portal.tier] -enterprise = "Enterprise plan" -free = "Editor plan" -pro = "Processor plan" +[portal.stepModal] +close = "Close" [portal.usage] managePayment = "Manage Payment" @@ -8906,14 +8973,6 @@ summary = "Owns a team — manages its members' resources and shared configs." limited = "{{used}} / {{limit}}" unlimited = "{{used}} · Unlimited" -[portal.welcome] -ariaLabel = "Welcome to Stirling PDF" -install = "Install the editor" -invite = "Invite teammates" -openInBrowser = "Open in browser" -productName = "PDF Editor" -stats = "30M downloads · 60+ PDF operations · Free forever" - [printFile] title = "Print File" @@ -9495,6 +9554,14 @@ text = "Rotate your PDF pages clockwise or counterclockwise in 90-degree increme [rotate.tooltip.header] title = "Rotate Settings Overview" +[ruler] +clearAll = "Clear all" +hideAllLabels = "Hide all labels" +hideSmallLabels = "Hide small labels" +physicalValues = "Physical" +scaled = "Scaled" +showAllLabels = "Show all labels" + [sanitize] desc = "Remove potentially harmful elements from PDF files." sanitizationResults = "Sanitization Results" @@ -9546,6 +9613,32 @@ tags = "clean,secure,safe,remove-threats" tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Adjust page-scale" +[scaleSettings] +activeScale = "Active Scale" +apply = "Apply Scale" +applyCalibration = "Apply Calibration" +calculatedScale = "Calculated scale" +calibrate = "Calibrate" +calibrating = "Calibrating" +calibrationDistanceRequired = "Enter a real-world distance greater than zero" +calibrationInvalid = "Unable to calculate scale from this measurement" +calibrationPaperDistance = "Measured page distance: {{distance}}" +calibrationTitle = "Calibrate Scale" +calibrationTooltip = "Measure a known distance to calculate the scale automatically" +customScale = "Custom Scale" +noneSet = "No custom scale set" +presets = "Preset Scales" +ratio = "Scale Ratio" +ratioHelp = "Ratio: 1 page unit = X real-world units" +ratioPlaceholder = "e.g., 100" +ratioPositive = "Ratio must be greater than zero" +ratioRequired = "Ratio is required" +realDistance = "Real-world distance" +realDistancePlaceholder = "e.g., 5" +reset = "Reset to Defaults" +unit = "Unit" +unitInvalid = "Invalid unit: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" @@ -9609,40 +9702,10 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint} [session] expired = "Your session has expired. Please refresh the page and try again." -[sessionManagement.tooltip] -header = "Managing Signing Sessions" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "New participants added to the end of signing order" -bullet2 = "Cannot add participants after session is finalized" -bullet3 = "Each participant receives a notification when it's their turn" -description = "You can add more participants to an active session at any time before finalization." -title = "Adding Participants" - -[sessionManagement.tooltip.finalization] -bullet1 = "Full finalization: All participants have signed" -bullet2 = "Partial finalization: Some participants haven't signed yet" -bullet3 = "Unsigned participants will be excluded from the final document" -bullet4 = "Once finalized, you can load the signed PDF into active files" -description = "Finalization combines all signatures into a single signed PDF. This action cannot be undone." -title = "Session Finalization" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Cannot remove participants who have already signed" -bullet2 = "Removed participants no longer receive notifications" -bullet3 = "Signing order adjusts automatically" -description = "Participants can be removed from sessions before they sign." -title = "Removing Participants" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Each signature is applied sequentially to the PDF" -bullet2 = "Later signers can see earlier signatures" -bullet3 = "Critical for approval workflows and legal chains of custody" -description = "The order you specify when creating the session determines who signs first." -title = "Signature Order" - [settings] +backToSections = "All settings" close = "Close" +title = "Settings" [settings.ai] documents = "Documents & RAG" @@ -9690,6 +9753,8 @@ defaultPdfEditor = "Default PDF editor" defaultPdfEditorActive = "Stirling PDF is your default PDF editor" defaultPdfEditorChecking = "Checking..." defaultPdfEditorInactive = "Another application is set as default" +defaultPdfEditorRemind = "Remind me to set as default" +defaultPdfEditorRemindDescription = "Show a banner when Stirling PDF is not your default PDF application." defaultPdfEditorSet = "Already Default" defaultStartupView = "Default view on launch" defaultStartupViewDescription = "Choose which view is active when the app starts" @@ -9918,9 +9983,6 @@ title = "Policies & Privacy" [settings.preferences] title = "Preferences" -[settings.search] -placeholder = "Search settings pages..." - [settings.security] description = "Update your password to keep your account secure." title = "Security" @@ -10066,6 +10128,7 @@ last = "Last page" maintainRatio = "Toggle maintain aspect ratio" next = "Next page" noSavedSigs = "No saved signatures found" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personal Signatures" previous = "Previous page" redo = "Redo" @@ -10104,6 +10167,7 @@ backgroundRemovalFailedTitle = "Background removal failed" hint = "Upload a PNG or JPG image of your signature" label = "Upload signature image" placeholder = "Select image file" +previewAlt = "Current image signature" processing = "Processing image..." removeBackground = "Remove white background (make transparent)" @@ -10117,6 +10181,17 @@ saved = "Select a saved signature above, then click anywhere on the PDF to place text = "After entering your name above, click anywhere on the PDF to place your signature." title = "How to add signature" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Move Signature" pause = "Pause placement" @@ -10563,6 +10638,24 @@ title = "Upload to Server" updateButton = "Update on Server" uploadButton = "Upload to Server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +placeholderShort = "Search" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" @@ -10677,12 +10770,39 @@ selectFilesHint = "Select files in Active Files to run this tool" singleFileScope = "Only applying to: {{fileName}}" viewerMode = "Switch to the file editor to add multiple files." +[toolFormat] +ANY = "any file" +CBR = "a comic book archive (CBR)" +CBZ = "a comic book archive (CBZ)" +CSV = "a CSV file" +EBOOK = "an ebook" +EMAIL = "an email" +EXCEL = "a spreadsheet" +HTML = "a web page" +IMAGE = "images" +JAVASCRIPT = "a JavaScript file" +JSON = "a JSON file" +MARKDOWN = "a Markdown file" +NONE = "no file" +PCL = "a PCL print file" +PDF = "a PDF" +PDF_ENCRYPTED = "a password-protected PDF" +POSTSCRIPT = "a PostScript file" +PPT = "a presentation" +TEXT = "a text file" +VIDEO = "a video" +WORD = "a Word document" +XML = "an XML file" +XPS = "an XPS document" +ZIP = "a ZIP archive" + [toolPanel] alpha = "Alpha" backToAllTools = "Back to all tools" collapse = "Collapse panel" expand = "Expand panel" goBack = "Go back" +pdfTools = "PDF Tools" placeholder = "Choose a tool to get started" premiumFeature = "Premium feature:" toolsHeader = "Tools" @@ -10719,11 +10839,6 @@ noToolsFound = "No tools found" quickAccess = "QUICK ACCESS" searchPlaceholder = "Search tools..." -[toolPicker.categories] -advancedTools = "Advanced Tools" -recommendedTools = "Recommended Tools" -standardTools = "Standard Tools" - [toolPicker.subcategories] advancedFormatting = "Advanced Formatting" automation = "Automation" @@ -10737,6 +10852,9 @@ removal = "Removal" signing = "Signing" verification = "Verification" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "No tools found" noTools = "No tools available" @@ -10894,6 +11012,13 @@ approver = "Approves policy" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] cancelInviteBody = "Cancel the invitation to {{email}}? They won't be able to join with the current link." cancelInviteTitle = "Cancel invitation" @@ -10913,10 +11038,8 @@ title = "No members yet" addToTeam = "Add to team" guestCount = "{{count}} guest" guests = "Guests" -guestsDesc = "External collaborators, scoped to what you shared. Editor only." ledBy = "led by {{owner}}" org = "Organization" -orgDesc = "Owners with org-wide authority and policy approval" owners = "{{count}} owner" team = "{{name}} team" teamMeta = "{{count}} people" @@ -10956,13 +11079,15 @@ usernamePlaceholder = "jsmith" [users.invites] by = "Invited by {{who}}" cancel = "Cancel" -count = "{{count}} pending" -desc = "Invited people who haven't joined yet. They hold a seat until they accept." expiresInDays_one = "Expires in {{count}} day" expiresInDays_other = "Expires in {{count}} days" expiresToday = "Expires today" title = "Pending invitations" +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Something went wrong reaching the backend, or you don't have access. Try again." title = "Couldn't load members" @@ -11152,12 +11277,15 @@ searchPlaceholder = "Search attachments" title = "Attachments" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bookmark title" bookmarkTitleRequired = "Bookmark title is required" closeSidebar = "Close bookmarks sidebar" collapseAll = "Collapse all bookmarks" +empty = "No bookmarks in this document" expandAll = "Expand all bookmarks" searchPlaceholder = "Search bookmarks" +title = "Bookmarks" [viewer.comments] addComment = "Add comment" @@ -11179,10 +11307,12 @@ locateAnnotation = "Locate in document" moreActions = "More actions" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Page {{page}}" placingHint = "Click a page to place… (cancel)" removeCommentOnly = "Remove comment only" saveReply = "Save reply" +searchPlaceholder = "Search comments" title = "Comments" typeComment = "Comment" typeInsertText = "Insert Text" @@ -11205,6 +11335,7 @@ unsavedDesc = "You have unsaved changes" closeSidebar = "Close layers sidebar" hideAll = "Hide all layers" showAll = "Show all layers" +title = "Layers" [viewer.link] delete = "Delete link" @@ -11214,8 +11345,11 @@ columnDefault = "Column {{index}}" convertToPdf = "Convert to PDF" csvStats = "{{rows}} rows · {{columns}} columns · {{size}}" emptyFile = "Empty file" +htmlHidePreview = "Hide preview" htmlPreview = "HTML preview" +htmlPreviewMobileHidden = "HTML pages are laid out for desktop widths, so the preview is off by default here." htmlPreviewWarning = "HTML preview - external resources may not load · {{size}}" +htmlShowPreview = "Show preview anyway" invalidJson = "Invalid JSON - showing raw content" lineNumbers = "Line numbers" loading = "Loading..." @@ -11548,30 +11682,6 @@ title = "Watermark Text" image = "Image" text = "Text" -[wetSignature.tooltip] -header = "Signature Creation Methods" - -[wetSignature.tooltip.draw] -bullet1 = "Customize pen color and thickness" -bullet2 = "Clear and redraw until satisfied" -bullet3 = "Works on touch devices (tablets, phones)" -description = "Create a handwritten signature using your mouse or touchscreen. Best for personal, authentic signatures." -title = "Draw Signature" - -[wetSignature.tooltip.type] -bullet1 = "Choose from multiple fonts" -bullet2 = "Customize text size and color" -bullet3 = "Perfect for standardized signatures" -description = "Generate a signature from typed text. Fast and consistent, suitable for business documents." -title = "Type Signature" - -[wetSignature.tooltip.upload] -bullet1 = "Supports PNG, JPG, and other image formats" -bullet2 = "Transparent backgrounds recommended for best results" -bullet3 = "Image will be resized to fit signature area" -description = "Upload a pre-created signature image. Ideal if you have a scanned signature or company logo." -title = "Upload Signature Image" - [workbenchBar] activeFiles = "Active Files" annotations = "Annotations" @@ -11585,6 +11695,8 @@ exitRedaction = "Exit Redaction Mode" exportAll = "Export PDF" exportSelected = "Export Selected Pages" formFill = "Fill Form" +hideToolbar = "Hide toolbar" +moreActions = "More actions" multiTool = "Multi-Tool" panMode = "Pan Mode" print = "Print PDF" @@ -11595,6 +11707,7 @@ redact = "Redact" rotateLeft = "Rotate Left" rotateRight = "Rotate Right" ruler = "Ruler / Measure" +rulerSettings = "Scale Settings" save = "Save" saveAll = "Save All" saveAs = "Save As" @@ -11604,6 +11717,9 @@ selectAll = "Select All" selectByNumber = "Select by Page Numbers" selectLanguage = "Select language" share = "Share" +showAllTools = "Show all tools" +showFewerTools = "Collapse toolbar" +showToolbar = "Show toolbar" toggleAnnotations = "Toggle Annotations Visibility" toggleAttachments = "Toggle Attachments" toggleBookmarks = "Toggle Bookmarks" diff --git a/frontend/editor/public/locales/es-ES/translation.toml b/frontend/editor/public/locales/es-ES/translation.toml index 19d4cd6fcd..bc1300bceb 100644 --- a/frontend/editor/public/locales/es-ES/translation.toml +++ b/frontend/editor/public/locales/es-ES/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabeto" apply = "Aplicar" applyAndContinue = "Guardar y salir" -areYouSure = "¿Seguro que quieres salir?" back = "Atrás" black = "Negro" blue = "Azul" @@ -46,10 +45,8 @@ filesSelected = "{{count}} archivos seleccionados" font = "Fuente" fontSizeTooltip = "Tamaño del texto del número de página en puntos. Números mayores generan texto más grande." fontTypeTooltip = "Familia de fuente para los números de página. Elige según el estilo de tu documento." -genericSubmit = "Procesar" green = "Verde" help = "Ayuda" -imgPrompt = "Seleccionar Imagen(es)" incorrectPasswordMessage = "La contraseña actual no es correcta." info = "Información" invalidUndoData = "No se puede deshacer: datos de la operación no válidos" @@ -62,7 +59,6 @@ loginAgreementTitle = "Acuerdo de acceso" logOut = "Cerrar sesión" marginTooltip = "Distancia entre el número de página y el borde de la página." moreOptions = "Más Opciones" -multiPdfPrompt = "Seleccionar PDFs (2+)" never = "Nunca" no = "No" noFileSelected = "No has seleccionado ningún archivo. Por favor, carga uno." @@ -80,7 +76,6 @@ pageOrderPrompt = "Orden personalizado de páginas (Introduce una lista de núme pages = "Páginas" pageSelectionPrompt = "Selección personalizada de páginas (Introduce una lista de números de página separados por comas 1,5,6 o funciones como 2n+1):" password = "Contraseña" -pdfPrompt = "Seleccionar PDF(s)" pin = "Fijar archivo (mantener activo tras ejecutar la herramienta)" pro = "Pro" processingComplete = "Tu archivo está listo." @@ -110,6 +105,7 @@ undoSuccess = "Operación deshecha correctamente" unknown = "Desconocido" unpin = "Desfijar archivo (reemplazar tras ejecutar la herramienta)" unsavedChanges = "Tienes cambios sin guardar en tu PDF. ¿Qué te gustaría hacer?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Cambios sin guardar" unsupported = "No compatible" username = "Nombre de usuario" @@ -356,6 +352,7 @@ text = "Estos permisos controlan lo que los usuarios pueden hacer con el PDF. So title = "Cambiar permisos" [addStamp] +preview = "Vista previa del sello" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Descargas manuales: haz clic en los enlaces y coloca los archivos noLanguages = "No se han encontrado idiomas de tessdata en el directorio configurado." permissionNotice = "La ruta de tessdata no tiene permisos de escritura. Las descargas se abrirán en el navegador; guarda los archivos .traineddata manualmente en la carpeta tessdata." +[admin.settings.ai.documents] +description = "Configura el modelo de embedding y los ajustes de recuperación utilizados para responder preguntas sobre documentos. Se aplica al motor de AI al guardar." +title = "Documentos y RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Déjalo en blanco para usar la credencial de entorno propia del motor. Se aplica a despliegues autoalojados de un solo motor." +generic = "Clave de API de embedding" +openai = "Clave de API de OpenAI" +setPlaceholder = "Guardada: déjalo en blanco para mantener la clave actual" +voyageai = "Clave de API de VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL base del endpoint de embeddings compatible con OpenAI / Ollama, p. ej. http://ollama:11434/v1. Debe apuntar a un endpoint interno de confianza (sensible a SSRF)." +label = "URL base de embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Nombre del modelo de embedding. Texto libre; las sugerencias son solo orientativas." +label = "Modelo de embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Proveedor utilizado para convertir el texto del documento en embeddings vectoriales." +label = "Proveedor de embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Número máximo de búsquedas de recuperación que el agente puede ejecutar por solicitud." +label = "Máx. búsquedas" + +[admin.settings.ai.documents.reindexNote] +body = "Cambiar el modelo de embedding surte efecto inmediatamente, pero los documentos indexados con el modelo anterior deben reindexarse para que la búsqueda devuelva resultados correctos." +title = "Reindexación requerida" + +[admin.settings.ai.documents.saved] +reindexNote = "Si has cambiado el modelo de embedding, reindexa los documentos existentes para que la búsqueda use el nuevo modelo." + +[admin.settings.ai.documents.topK] +description = "Número de fragmentos más relevantes recuperados por búsqueda." +label = "Top K" + +[admin.settings.ai.general] +description = "Conecta Stirling al motor de AI de Python y elige qué capacidades de AI se exponen. Los cambios se aplican al reiniciar." +title = "Motor de AI" + +[admin.settings.ai.general.capabilities] +description = "Activa o desactiva funciones individuales de AI. Las funciones desactivadas se ocultan en la aplicación." +title = "Capacidades" + +[admin.settings.ai.general.enabled] +description = "Interruptor principal. Cuando está desactivado, no hay herramientas, agentes ni llamadas al motor de AI disponibles." +label = "Activar AI" + +[admin.settings.ai.general.features.chat] +description = "Asistente conversacional para trabajar con PDFs." +label = "Asistente de chat" + +[admin.settings.ai.general.features.classify] +description = "Categoriza documentos automáticamente por tipo o contenido." +label = "Clasificación de documentos" + +[admin.settings.ai.general.features.createPdf] +description = "Genera un nuevo PDF (p. ej., desde HTML) mediante un agente de AI." +label = "Crear PDF desde una petición" + +[admin.settings.ai.general.features.documentQuestions] +description = "Haz preguntas y obtén respuestas basadas en un documento subido." +label = "Preguntas sobre documentos" + +[admin.settings.ai.general.features.mathAuditor] +description = "Revisa documentos en busca de errores matemáticos y numéricos." +label = "Auditor matemático" + +[admin.settings.ai.general.features.pdfComment] +description = "Añade comentarios de revisión y anotaciones redactados por AI a un PDF." +label = "Agente de comentarios de PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Tiempo de espera para operaciones de agente más pesadas, como la generación de documentos." +label = "Tiempo de espera de larga duración (segundos)" + +[admin.settings.ai.general.note] +body = "El motor de AI se ejecuta como un servicio independiente. Su secreto compartido" +body2 = "se establece mediante una variable de entorno del contenedor. Las claves de API de proveedores pueden introducirse en estas páginas o suministrarse como variables de entorno del motor; las claves introducidas aquí se envían al motor al guardar." +title = "Acerca del motor de AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Tiempo de espera para respuestas de chat transmitidas (token a token)." +label = "Tiempo de espera de stream (segundos)" + +[admin.settings.ai.general.test] +button = "Probar conexión" +failBody = "El motor de AI no ha respondido. Comprueba la URL, que el contenedor del motor esté en ejecución y que la AI esté activada (se necesita reiniciar después de activarla)." +failTitle = "Motor de AI inaccesible" +okBody = "El motor de AI ha respondido a una comprobación de estado." +okTitle = "Motor de AI accesible" + +[admin.settings.ai.general.timeoutSeconds] +description = "Tiempo de espera para solicitudes estándar de AI al motor." +label = "Tiempo de espera de solicitud (segundos)" + +[admin.settings.ai.general.url] +description = "URL interna del motor de AI de Python, p. ej. http://stirling-pdf-engine:5001." +label = "URL del motor de AI" + +[admin.settings.ai.limits] +description = "Límites de seguridad para cuánto trabajo pueden realizar las solicitudes de AI y cuántas se ejecutan simultáneamente. Se aplica al motor de AI al guardar." +title = "Límites y rendimiento" + +[admin.settings.ai.limits.maxCharacters] +description = "Límite de seguridad: rechaza solicitudes de AI cuyo texto extraído supere esta longitud." +label = "Máx. caracteres por solicitud" + +[admin.settings.ai.limits.maxPages] +description = "Límite de seguridad: rechaza solicitudes de AI que superen este número de páginas PDF." +label = "Máx. páginas por solicitud" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Número máximo de llamadas simultáneas al modelo en curso en todo el motor." +label = "Concurrencia máx. del modelo" + +[admin.settings.ai.models] +description = "Elige el proveedor de LLM y los modelos inteligente/rápido que usa el motor de AI. Se aplica al motor de AI al guardar." +title = "Modelos y proveedores" + +[admin.settings.ai.models.apiKey] +anthropic = "Clave de API de Anthropic" +description = "Déjalo en blanco para usar la credencial de entorno propia del motor. Se aplica a despliegues autoalojados de un solo motor." +generic = "Clave de API" +openai = "Clave de API de OpenAI" +setPlaceholder = "Guardada: déjalo en blanco para mantener la clave actual" + +[admin.settings.ai.models.baseUrl] +description = "URL base del endpoint compatible con OpenAI / Ollama, p. ej. http://ollama:11434/v1." +label = "URL base del proveedor" +warning = "La URL base debe apuntar a un endpoint interno de confianza. El motor realizará solicitudes del lado del servidor a ella, por lo que un valor no fiable es sensible a SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Tokens de salida máximos para el modelo rápido." +label = "Máx. tokens del modelo rápido" + +[admin.settings.ai.models.fastModel] +description = "Modelo más barato y rápido para tareas ligeras. Texto libre; las sugerencias son solo orientativas." +label = "Modelo rápido" + +[admin.settings.ai.models.provider] +description = "Proveedor de LLM con el que se comunica el motor." +label = "Proveedor" + +[admin.settings.ai.models.smartMaxTokens] +description = "Tokens de salida máximos para el modelo inteligente." +label = "Máx. tokens del modelo inteligente" + +[admin.settings.ai.models.smartModel] +description = "Modelo de alta capacidad para razonamiento complejo. Texto libre; las sugerencias son solo orientativas." +label = "Modelo inteligente" + +[admin.settings.ai.saved] +body = "Los cambios se envían automáticamente al motor de AI." +bodyNoPush = "Ajustes guardados. Se aplicarán la próxima vez que el motor de AI recoja su configuración." +title = "Ajustes de AI guardados" + [admin.settings.badge] clickToUpgrade = "Haz clic para ver los detalles del plan" @@ -938,6 +1094,25 @@ label = "Regenerar al inicio" description = "Número de días que el certificado será válido" label = "Validez del certificado (días)" +[admin.settings.folderAccess] +description = "Directorios desde los que las fuentes de carpeta y las salidas de carpeta pueden leer y en los que pueden escribir. Esto es un límite de seguridad: las automatizaciones nunca pueden apuntar a una ruta del servidor fuera de esta lista." +securityNote = "Déjalo vacío para desactivar por completo las fuentes y salidas de carpeta. El directorio de configuración propio de Stirling siempre está fuera de los límites." +title = "Acceso a carpetas" + +[admin.settings.folderAccess.implied] +description = "Estos directorios gestionados por Stirling siempre están permitidos y no se pueden cambiar aquí." +serverStorage = "Almacenamiento de archivos del servidor" +title = "Siempre permitido" +watchedFolder = "Carpeta vigilada del pipeline" + +[admin.settings.folderAccess.roots] +add = "Añadir" +empty = "No hay carpetas permitidas. Las fuentes y salidas de carpeta están desactivadas actualmente." +hint = "Introduce rutas absolutas, por ejemplo /data/inbox." +label = "Raíces de carpeta permitidas" +placeholder = "/data/inbox" +remove = "Eliminar raíz de carpeta" + [admin.settings.general] description = "Configura la configuración global de la aplicación, incluida la marca y el comportamiento predeterminado." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Ancho" [app] description = "La alternativa gratuita a Adobe Acrobat (más de 10 millones de descargas)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convertir a PDF/A-3b" convertToPdfA3bDescription = "Crea un PDF para archivo con adjuntos incrustados" @@ -1842,6 +2020,15 @@ export = "Exportar" accessDenied = "Acceso denegado" insufficientPermissions = "No tienes permiso para realizar esta acción." +[auth.callback] +completing = "Completando autenticación" +invalidToken = "Error de inicio de sesión con OAuth: token no válido." +missingToken = "Error de inicio de sesión con OAuth: no se ha recibido ningún token." +oauthFailed = "Error de inicio de sesión con OAuth. Inténtalo de nuevo." +pleaseWait = "Espera mientras terminamos de iniciar tu sesión." +signedOut = "Se ha cerrado tu sesión. Inicia sesión de nuevo." +windowMayClose = "Puedes cerrar esta ventana cuando se complete." + [auth.displayName] guest = "Invitado" user = "Usuario" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Asegura documentos PDF eliminando contenido potencialmente description = "Esta herramienta renombrará automáticamente los archivos PDF en función de su contenido. Analiza el documento para encontrar el título más adecuado a partir del texto." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rotar automáticamente" +tags = "rotar automáticamente,corregir orientación,boca abajo,de lado,enderezar,orientar,detectar orientación,corregir rotación,orientar automáticamente,escaneado,OSD" + +[autoRotate.confidenceThreshold] +title = "Umbral de confianza" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Solo OCR" +text = "Solo texto" +title = "Método de detección" + +[autoRotate.error] +failed = "Se ha producido un error al rotar automáticamente el PDF." + +[autoRotate.inferUndetected] +title = "Rotar páginas de baja confianza para que coincidan" + +[autoRotate.report] +applied = "{{degrees}}° horario" +confidenceValue = "Confianza {{value}}" +noChange = "Sin cambios" +pageLabel = "Página {{number}}" +summary = "{{rotated}} de {{total}} páginas rotadas, {{unchanged}} se dejaron como estaban" +title = "Informe de detección" + +[autoRotate.report.method] +inferred = "Coincidencia" +none = "Omitida" +osd = "OCR" +text = "Texto" + +[autoRotate.report.note] +belowThreshold = "Por debajo del umbral" +blankPage = "La página está en blanco" +inferredFromDocument = "Igual que las otras páginas" +noDominantDirection = "Direcciones de texto mixtas" +osdFailed = "No se ha encontrado texto legible" +osdNoVerdict = "No se pudo determinar la orientación" +tesseractUnavailable = "OCR no instalado" +tooFewGlyphs = "Muy poco texto" + +[autoRotate.results] +title = "Resultados de rotación automática" + +[autoRotate.settings] +title = "Ajustes" + +[autoRotate.tooltip.confidenceThreshold] +text = "Qué grado de certeza debe tener la comprobación antes de girar una página. Bájalo para rotar más páginas; súbelo para dejar más páginas intactas." +title = "Umbral de confianza" + +[autoRotate.tooltip.description] +text = "Determina en qué orientación debe estar cada página y la gira correctamente. Las páginas que ya están derechas se dejan igual." + +[autoRotate.tooltip.detectionMode] +text = "Auto lee primero el texto de la página y luego analiza la página como imagen si no es suficiente. Solo texto omite la comprobación de imagen, lo más rápido para documentos que nunca se escanearon. Solo OCR trata cada página como una imagen." +title = "Método de detección" + +[autoRotate.tooltip.header] +title = "Rotar automáticamente" + +[autoRotate.tooltip.inferUndetected] +text = "Las páginas en blanco o casi en blanco no se pueden comprobar por sí solas. Cuando todas las páginas que pudieron leerse necesitan el mismo giro, estas páginas se giran para coincidir con ellas." +title = "Rotar páginas de baja confianza para que coincidan" + [autoSizeSplitPDF] tags = "pdf,dividir,documento,organización" @@ -2189,33 +2443,6 @@ keywords = "Palabras clave: odd, even" numbers = "Números/rangos: 5, 10-20" progressions = "Progresiones: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipos de certificado" - -[certificateChoice.tooltip.organization] -bullet1 = "Gestionado por administradores del sistema" -bullet2 = "Compartido entre usuarios autorizados" -bullet3 = "Representa la identidad de la empresa, no la individual" -bullet4 = "Ideal para: Documentos oficiales, firmas de equipo" -description = "Un certificado compartido proporcionado por tu organización. Se usa para la autoridad de firma a nivel de empresa." -title = "Certificado de la organización" - -[certificateChoice.tooltip.personal] -bullet1 = "Se genera automáticamente en el primer uso" -bullet2 = "Vinculado a tu cuenta de usuario" -bullet3 = "No se puede compartir con otros usuarios" -bullet4 = "Ideal para: Documentos personales, responsabilidad individual" -description = "Un certificado autogenerado único para tu cuenta de usuario. Adecuado para firmas individuales." -title = "Certificado personal" - -[certificateChoice.tooltip.upload] -bullet1 = "Requiere archivo P12/PFX y contraseña" -bullet2 = "Puede ser emitido por Autoridades de Certificación externas" -bullet3 = "Mayor nivel de confianza para documentos legales" -bullet4 = "Ideal para: Contratos legalmente vinculantes, validación externa" -description = "Usa tu propio archivo de certificado PKCS#12. Proporciona control total sobre las propiedades del certificado." -title = "Cargar P12 personalizado" - [certSign] awaitingSignatures = "A la espera de firmas" chooseCertificate = "Elegir archivo de certificado" @@ -2542,34 +2769,6 @@ server = "Servidor" stepTitle = "Origen del certificado" upload = "Subir" -[certSign.tooltip.header] -title = "Acerca de la gestión de firmas" - -[certSign.tooltip.overview] -bullet1 = "Verificar firmas existentes y su validez" -bullet2 = "Ver información detallada sobre firmantes y certificados" -bullet3 = "Añadir nuevas firmas digitales para asegurar tus documentos" -bullet4 = "Múltiples archivos compatibles con navegación fácil" -text = "Esta herramienta te permite verificar si tus PDF están firmados digitalmente y añadir nuevas firmas digitales. Las firmas digitales demuestran quién creó o aprobó un documento y muestran si se ha cambiado desde la firma." -title = "¿Qué puede hacer esta herramienta?" - -[certSign.tooltip.signing] -bullet1 = "Compatible con formatos PEM, PKCS12, JKS y certificado de servidor" -bullet2 = "Opción de mostrar u ocultar firma en el PDF" -bullet3 = "Añadir motivo, ubicación y nombre del firmante" -bullet4 = "Elegir en qué página colocar firmas visibles" -bullet5 = "Usar certificado de servidor para la opción simple 'Firmar con Stirling-PDF'" -text = "Para firmar un PDF, necesita un certificado digital (como PEM, PKCS12 o JKS). Puede elegir hacer visible la firma en el documento o mantenerla invisible solo para seguridad." -title = "Añadir firmas" - -[certSign.tooltip.validation] -bullet1 = "Muestra si las firmas son válidas o inválidas" -bullet2 = "Muestra información del firmante y fecha de firma" -bullet3 = "Verifica si el documento fue modificado después de firmar" -bullet4 = "Puede usar certificados personalizados para verificación" -text = "Cuando verificas firmas, la herramienta te indica si son válidas, quién firmó el documento, cuándo se firmó y si el documento se ha cambiado desde la firma." -title = "Verificar firmas" - [changeCreds] changeUsername = "Actualiza tu nombre de usuario. Se cerrará tu sesión tras actualizar." credsUpdated = "Cuenta actualizada" @@ -2737,12 +2936,16 @@ text = "Para hacer estos permisos inmutables, usa la herramienta Agregar Contras [chat.actions] copy = "Copiar mensaje" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opciones del agente Stirling" +assistant = "AI Assistant" clearChat = "Borrar chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Esta operación usará tus créditos en la nube" tooltip = "Tomar color de la pantalla" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Elija el color" [common] +actions = "Actions" back = "Atrás" cancel = "Cancelar" close = "Cerrar" +codeSample = "Code sample" collapse = "Contraer" confirm = "Confirmar" continue = "Continuar" copied = "¡Copiado!" copy = "Copiar" done = "Hecho" +enterValue = "Enter value" error = "Error" expand = "Expandir" loading = "Cargando..." next = "Siguiente" +open = "Open" preview = "Vista previa" previous = "Anterior" refresh = "Actualizar" +remaining = "Restante" retry = "Reintentar" save = "Guardar" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Borrar selección" @@ -2988,6 +3199,7 @@ title = "Método de Compresión" [compress.settings] desiredSize = "Tamaño de archivo deseado" desiredSizePlaceholder = "Introduce el tamaño" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "La compresión es una forma fácil de reducir el tamaño de su archivo. Elija Tamaño de Archivo para ingresar un tamaño objetivo y nosotros ajustaremos la calidad por usted. Elija Calidad para establecer la intensidad de compresión manualmente." @@ -3190,6 +3402,24 @@ pdfOptions = "Opciones de PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Seleccione primero un formato de origen" settings = "Configuración" single = "Individual" @@ -3264,24 +3494,6 @@ description = "Estas cookies son esenciales para que el sitio web funcione corre posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Supervisión avanzada" -allInOneWorkspace = "Espacio de trabajo PDF todo en uno (visor, herramientas y agente)" -apiSandbox = "Entorno de pruebas de API" -dedicatedSupportSlas = "Soporte dedicado y SLA" -fasterThroughput = "10x más rendimiento" -fullyPrivateFiles = "Archivos totalmente privados" -largeFileProcessing = "Procesamiento de archivos grandes" -orgWideAccess = "Controles de acceso para toda la organización" -premiumAiModels = "Modelos de IA premium" -prioritySupport = "Soporte prioritario" -privateDocCloud = "Nube de documentos privada" -ragFineTuning = "RAG + ajuste fino" -secureApiAccess = "Acceso seguro a la API" -standardThroughput = "Rendimiento estándar" -unlimitedApiAccess = "Acceso ilimitado a la API" -unlimitedSeats = "Usuarios ilimitados" - [crop] autoCrop = "Recortar automáticamente espacios en blanco" noFileSelected = "Seleccione un archivo PDF para comenzar a recortar" @@ -3346,6 +3558,7 @@ message = "No se pudo establecer el gestor de PDF predeterminado" title = "Error" [defaultApp.prompt] +dontRemind = "No volver a recordármelo" message = "Haga que Stirling PDF sea su aplicación predeterminada para abrir archivos PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Buscar..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Editar" @@ -3644,11 +3861,13 @@ shareSelected = "Compartir seleccionados" sharing = "Uso compartido" showAll = "Mostrar todo" showHistory = "Mostrar Historial" +sortBy = "Sort files" sortByDate = "Ordenar por fecha" sortByName = "Ordenar por nombre" sortBySize = "Ordenar por tamaño" storage = "Almacenamiento" storageState = "Almacenamiento" +storageUsed = "Storage used" synced = "Sincronizado" title = "Cargar Archivos PDF" toolChain = "Herramientas Aplicadas" @@ -3694,13 +3913,17 @@ addFiles = "Añadir archivos" addingFiles = "Añadiendo archivos…" collapse = "Contraer barra lateral" customizeGroups = "Personalizar grupos" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Abre archivos para empezar" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expandir barra lateral" -files = "Archivos" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive no está configurado" leaveMyFiles = "Salir de Mis archivos" +library = "Biblioteca de PDF" myFiles = "Mis archivos" noFiles = "Aún no hay archivos" openFileManager = "Abrir gestor de archivos" @@ -3708,37 +3931,36 @@ openFromComputer = "Abrir desde el ordenador" openSettings = "Abrir configuración" other = "Otros" recent = "Recientes" -search = "Buscar" -searchPlaceholder = "Buscar archivos..." viewAll = "Ver los {{count}} archivos" [fileSidebar.fileItem] closeViewer = "Cerrar visor" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Abrir en el visor" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Añadir" -addLabel = "Añadir una etiqueta…" -categoryIconAria = "Elegir un icono para {{name}}" -createCategory = "Crear categoría" -delete = "Eliminar categoría" done = "Hecho" -hide = "Ocultar categoría" -newCategory = "Nombre de nueva categoría…" -removeLabel = "Eliminar {{name}}" -rename = "Cambiar nombre" reset = "Restablecer valores predeterminados" -search = "Buscar etiquetas…" -show = "Mostrar categoría" subtitle = "Agrupa tus archivos en categorías principales. Añade etiquetas existentes o nuevas a una categoría, cámbiale el nombre o crea la tuya propia. Los archivos que no estén en ninguna de tus categorías aparecen en “Otros”." title = "Categorías de la barra lateral" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Añadir al espacio de trabajo" addToWorkspaceCount = "Añadir {{count}} al espacio de trabajo" @@ -3749,7 +3971,6 @@ backToMyFiles = "Volver a Mis archivos" breadcrumbs = "Ruta de carpeta" cancel = "Cancelar" classification = "Clasificación" -clearSearch = "Borrar búsqueda" clearSelection = "Borrar selección" closeDetails = "Cerrar detalles" create = "Crear" @@ -3781,6 +4002,7 @@ downloadAll = "Descargar todo" downloadVersion = "Descargar esta versión" dropOverlay = "Suelta archivos para subirlos" dropOverlaySub = "Los archivos empiezan en Local. Usa 'Mover a' o 'Guardar en la nube' para organizarlos en una carpeta." +duplicate = "Duplicate" file = "Archivo" fileInfo = "File info" fileMenu = "Acciones de archivo" @@ -3808,13 +4030,12 @@ refresh = "Actualizar desde el servidor" remove = "Eliminar" removeVersion = "Eliminar esta versión" rename = "Renombrar" +renamed = "Renombrado" renameFolder = "Renombrar carpeta" resizeFolderTree = "Redimensionar árbol de carpetas (teclas de flecha, Mayús para pasos más grandes; doble clic para ajustar automáticamente)" save = "Guardar" saveToServer = "Guardar en el servidor" saveToServerDisabledHint = "Guardar en el servidor no está habilitado en este servidor. Pide a tu administrador que lo habilite." -search = "Buscar" -searchPlaceholder = "Buscar en esta carpeta y subcarpetas" selectAll = "Seleccionar todo" selectAllHint = "Haz clic para seleccionar todo. Consejo: mantén pulsado Ctrl (o Cmd) para añadir archivos uno a uno, Mayús para seleccionar un rango." selectedCount = "{{count}} seleccionados" @@ -3874,6 +4095,10 @@ title = "Aún no hay archivos en la nube" hint = "Los archivos guardados sin subir se quedan aquí. Suelta un archivo para añadir uno." title = "No hay archivos solo locales" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Los archivos que abras o edites aparecerán aquí." title = "Nada modificado aún" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "No se pudo eliminar la carpeta." deleteFolderFailedDetail = "No se pudo eliminar la carpeta: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "No se pudo actualizar la apariencia de la carpeta." folderAppearanceFailedDetail = "No se pudo actualizar la apariencia de la carpeta: {{message}}" moveFilesFailed = "No se pudieron mover los archivos." @@ -3910,9 +4137,7 @@ count = "Archivos" folder = "Carpeta" labels = "Etiquetas" modified = "Modificado" -name = "Nombre" size = "Tamaño" -toolHistoryAtVersion = "Cadena de herramientas acumulada" totalSize = "Tamaño total" type = "Tipo" versionHistory = "Evolución de versiones" @@ -3945,6 +4170,11 @@ localHint = "Solo almacenado en este navegador" shared = "Compartido" sharedHint = "Compartido contigo mediante enlace" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Más antiguos primero" modifiedDesc = "Más recientes primero" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "No hay archivos disponibles en el almacenamiento. Abra a open = "Abrir" openFile = "Abrir archivo" openFiles = "Abrir archivos" +selectFile = "Select {{name}}" selectFromStorage = "Seleccionar del Almacenamiento" upload = "Cargar" uploadFile = "Cargar Archivo" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Todo guardado" +analyzingFields = "Analysing form fields..." +extractCsvError = "No se pudo extraer CSV" +extractXlsxError = "No se pudo extraer XLSX" +filled = "filled" flattenAfterFilling = "Aplanar después de rellenar" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "oblig." +requiredFieldsError = "Rellena todos los campos obligatorios" rescanFields = "Volver a escanear campos" rescanFormFields = "Volver a escanear campos del formulario" save = "Guardar" +saveShortcut = "Ctrl+S para guardar" +unsavedChanges = "Cambios sin guardar" [formFill.sidebar] close = "Cerrar barra lateral" @@ -4309,6 +4552,11 @@ desc = "Renombra automáticamente un archivo PDF basándose en su encabezado det tags = "auto-detectar,basado-en-encabezado,organizar,reetiquetar" title = "Renombrar Automáticamente Archivo PDF" +[home.autoRotate] +desc = "Detecta la orientación de cada página y la gira automáticamente para dejarla derecha." +tags = "rotar automáticamente,corregir orientación,boca abajo,de lado,enderezar,orientar,detectar orientación,corregir rotación,orientar automáticamente,escaneado,OSD" +title = "Rotar automáticamente" + [home.autoSizeSplitPDF] tags = "auto,dividir,tamaño" title = "Auto dividir por tamaño/conteo" @@ -4608,15 +4856,12 @@ hint = "Sube un PNG, JPG, SVG u otro archivo de imagen para colocarlo en el PDF. label = "Subir imagen" placeholder = "Seleccionar archivo de imagen" -[imageToPdf] -tags = "conversión,img,jpg,imagen,fotografía" - [imageToPDF] header = "Imagen a PDF" title = "Imagen a PDF" -[infoBanner] -dismiss = "Descartar" +[imageToPdf] +tags = "conversión,img,jpg,imagen,fotografía" [invite] acceptError = "No se pudo crear la cuenta" @@ -4647,11 +4892,9 @@ welcomeTitle = "¡Has sido invitado!" [landing] addFiles = "Agregar Archivos" -heroSubtitle = "Suelte o añada un PDF existente para empezar." mobileUpload = "Subir desde el móvil" openFromComputer = "Abrir desde el ordenador" uploadFromComputer = "Cargar desde el ordenador" -workbenchEmptyStateHero = "Suelta un PDF en cualquier lugar" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Introduce el nombre de usuario" failedToSignIn = "Error al iniciar sesión con {{provider}}: {{message}}" forgotPassword = "¿Has olvidado tu contraseña?" loggingIn = "Iniciando sesión..." -logIn = "Iniciar sesión" login = "Iniciar sesión" magicLinkSent = "¡Enlace mágico enviado a {{email}}! Revisa tu correo electrónico y haz clic en el enlace para iniciar sesión." mfaCode = "Código de autenticación" @@ -4715,26 +4957,10 @@ signInWith = "Iniciar sesión con" title = "Iniciar sesión" unexpectedError = "Error inesperado: {{message}}" updatePassword = "Actualizar contraseña" -useEmailInstead = "Iniciar sesión con correo electrónico" useMagicLink = "Usar enlace mágico en su lugar" username = "Nombre de usuario" youAreLoggedIn = "¡Has iniciado sesión!" -[login.slides.edit] -alt = "Editar PDFs" -subtitle = "Con más de una docena de herramientas para ocultar, firmar, leer y manipular PDFs, seguro que encuentra lo que busca." -title = "Edite PDFs para mostrar/proteger la información que quiera" - -[login.slides.overview] -alt = "Vista general de Stirling PDF" -subtitle = "Una suite en la nube centrada en la privacidad para PDF que le permite convertir, firmar, ocultar y gestionar documentos, junto con 50+ herramientas potentes." -title = "Su solución integral para todas sus necesidades de PDF." - -[login.slides.secure] -alt = "Proteger PDFs" -subtitle = "Añada contraseñas, oculte contenido y gestione certificados con facilidad." -title = "Proteja la información sensible en sus PDFs" - [margin] large = "Grande" medium = "Mediano" @@ -4818,6 +5044,34 @@ uploadSuccess = "¡Subida correcta!" uploadSuccessMessage = "Tus imágenes han sido transferidas." validating = "Validando la sesión..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Escanea para subir fotos. Las imágenes se convierten automáticamente a PDF." descriptionNoConvert = "Escanea para subir fotos desde tu dispositivo móvil." @@ -4836,8 +5090,13 @@ title = "Subir desde el móvil" tags = "Multi-herramienta,Multi-operación,Interfaz de usuario,Arrastrar con un click,front end,lado del cliente" title = "Multi-herramienta PDF" -[navbar] -search = "Buscar" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "La autenticación no se realizó correctamente. Puede cerrar esta ventana e intentarlo de nuevo." @@ -4966,6 +5225,7 @@ activeFiles = "La vista de Archivos activos muestra todos los P allTools = "Este es el panel de Todas las herramientas, donde puedes buscar y seleccionar entre todas las herramientas PDF disponibles." close = "Cerrar" cropSettings = "Ahora que hemos seleccionado el archivo que queremos recortar, podemos configurar la herramienta Recortar para elegir el área a la que queremos recortar el PDF." +dialogLabel = "Onboarding" fileCheckbox = "Al hacer clic en uno de los archivos lo seleccionas para procesarlo. Puedes seleccionar varios archivos para operaciones por lotes." fileReplacement = "El archivo modificado reemplazará automáticamente al original en el Workbench, lo que te permitirá pasarlo fácilmente por más herramientas." filesButton = "El botón Archivos en la barra de acceso rápido te permite subir PDFs sobre los que usar las herramientas." @@ -4976,6 +5236,7 @@ pinButton = "Puedes usar el botón Fijar si prefieres que tus a results = "Después de que la herramienta haya terminado, el paso de Revisión mostrará una vista previa de los resultados en este panel y te permitirá deshacer la operación o descargar el archivo. " runButton = "Una vez configurada la herramienta, este botón te permite ejecutar la herramienta en todos los PDFs seleccionados." selectCropTool = "Seleccionemos la herramienta Recortar para mostrar cómo usar una de las herramientas." +stepOf = "Paso {{current}} de {{total}}" toolInterface = "Esta es la interfaz de la herramienta Recortar. Como puedes ver, no hay mucho porque aún no hemos añadido ningún archivo PDF con el que trabajar." workbench = "Este es el Workbench - el área principal donde ves y editas tus PDFs." wrapUp = "¡Todo listo! Has aprendido sobre las áreas principales de la aplicación y cómo usarlas. Haz clic en el botón Ayuda cuando quieras para ver este recorrido de nuevo." @@ -4988,6 +5249,26 @@ showMeAround = "Muéstreme el recorrido" skipForNow = "Omitir por ahora" skipTheTour = "Saltar el recorrido" +[onboarding.checklist] +dismiss = "Descartar" +title = "Configurar Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Ejecuta Stirling de forma nativa en tu equipo" +title = "Descargar Stirling para escritorio" + +[onboarding.checklist.inviteTeam] +description = "Colabora con tu equipo" +title = "Invitar a miembros del equipo" + +[onboarding.checklist.shareAnalytics] +description = "Ayuda a mejorar Stirling" +title = "Compartir datos de uso anónimos" + +[onboarding.checklist.takeTour] +description = "Descubre cómo funciona Stirling con una visita rápida" +title = "Hacer la visita guiada" + [onboarding.desktopInstall] body = "Stirling funciona mejor como aplicación de escritorio. Puede usarla sin conexión, acceder a los documentos más rápido y editar localmente en su ordenador." selectOs = "Seleccionar sistema operativo" @@ -4996,7 +5277,21 @@ titleWithOs = "Descargar para {{osLabel}}" [onboarding.mfa] authenticationCode = "Código de autenticación" +description = "Protege tu cuenta vinculando una aplicación de autenticación. Escanea el código QR o introduce la clave de configuración y, a continuación, confirma el código de 6 dígitos para finalizar." +enable = "Activar MFA" +enableError = "No se ha podido activar la autenticación de dos factores. Comprueba el código e inténtalo de nuevo." +enterCode = "Introduce el código de 6 dígitos de tu aplicación." +enterCodeError = "Introduce el código de autenticación para continuar." +logout = "Cerrar sesión" +openAuthenticator = "Abre Google Authenticator, Authy o 1Password." qrCodeLoading = "Generando tu código QR…" +regenerateQrCode = "Regenerar código QR" +scanQrCode = "Escanea el código QR o introduce la clave de configuración a continuación." +setupError = "No se ha podido iniciar la configuración de dos factores. Inténtalo de nuevo." +setupKey = "Clave de configuración (entrada manual)" +stepByStep = "Paso a paso" +success = "MFA se ha activado. Ya puedes continuar." +title = "Configuración de autenticación multifactor" [onboarding.planOverview] adminBodyLoginDisabled = "Cuando habilite el modo de inicio de sesión, podrá gestionar usuarios, configurar ajustes y supervisar la salud del servidor. Las primeras {{freeTierLimit}} personas de su servidor pueden usar Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Resumen de administrador" userBody = "Invite a sus compañeros, asigne roles y mantenga sus documentos organizados en un espacio de trabajo seguro. Active el modo de inicio de sesión cuando esté listo para pasar del uso individual." userTitle = "Resumen del plan" +[onboarding.processorIntro] +body = "Stirling ahora ejecuta Políticas: reglas automatizadas que clasifican, protegen y procesan cada documento cuando llega. Configúralas y supervisa las ejecuciones en el Procesador." +cta = "Ver el Procesador" +title = "Descubre el Procesador de Stirling" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Año prepago" + +[payg.prepaid.banner] +exhausted = "Tu capacidad prepago se ha agotado; se ha reanudado la facturación medida. Recarga para mantener el descuento." +expiring_one = "Tu año prepago termina en {{count}} día. La capacidad no utilizada caduca; recarga para iniciar un año nuevo." +expiring_other = "Tu año prepago termina en {{count}} días. La capacidad no utilizada caduca; recarga para iniciar un año nuevo." +low = "Te queda el {{pct}}% de tu capacidad prepago. Recarga en cualquier momento para mantener el procesamiento sin interrupciones." + +[payg.prepaid.card] +subtitle = "Se usa antes de la facturación medida y fuera de tu límite de gasto." +title = "Capacidad prepago" + +[payg.prepaid.meter] +capSuffix = "de {{total}} créditos prepago" +expires = "Caduca el {{date}}" + +[payg.prepaid.state] +exhausted = "Agotada" +healthy = "Queda bastante" +low = "Queda poco" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF a PDF/A" tags = "archivo,largo plazo,estándar,conversión,almacenamiento,conservación" title = "PDF a PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "impresión,estándar,conversión,producción,preimpresión,archivo" title = "PDF a PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Créditos" xsmall = "100 Créditos" [plan.availablePlans] +currency = "Billing currency" subtitle = "Elige el plan que se ajuste a tus necesidades" title = "Planes disponibles" @@ -5937,49 +6267,12 @@ keyDescription = "Pega la clave de licencia de tu correo" success = "¡Licencia activada!" successMessage = "Tu licencia se ha activado correctamente. Ya puedes cerrar esta ventana." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "aplicado" -enforcing = "Aplicando..." -failed = "Error en la aplicación" outputsUnavailable = "Los resultados de la política ya no están disponibles para descargar." partialOutputsUnavailable = "Algunos resultados de la política ya no están disponibles para descargar." -retrying = "Ocupado, reintentando..." runNotFound = "No se pudo encontrar la ejecución de la aplicación." -step = "paso {{current}}/{{total}}" timedOut = "La aplicación agotó el tiempo de espera antes de que la ejecución pudiera finalizar." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Cerrar" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Aplicando {{names}}" applyingProgress = "Aplicando {{names}} ({{done}} de {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} de {{total}} archivo(s) no se pudieron procesar y se failureTitle = "Exportado sin aplicación completa" printPolicyAppliedBody = "Este PDF se actualizó para cumplir una política. Revisa los cambios y vuelve a imprimir." printPolicyAppliedTitle = "Política aplicada antes de imprimir" -queued = "+{{count}} en cola" successTitle = "{{names}} aplicadas" summaryMore = "{{first}}, {{second}} y {{more}} más" summaryTwo = "{{first}} y {{second}}" -[policies.enforcement.triggerVerb] -convert = "Aplicando antes de convertir" -default = "Aplicando" -export = "Aplicando antes de exportar" -input = "Aplicando al importar" -print = "Aplicando antes de imprimir" - -[policies.field] -accessLog = "Registro de acceso" -archiveAfter = "Archivar después de" -auditTrail = "Pista de auditoría" -belowThreshold = "Por debajo del umbral" -destination = "Destino" -frameworks = "Marcos" -immutableHold = "Retención inmutable" -keepFor = "Conservar durante" -minConfidence = "Confianza mín." -notify = "Notificar al enrutar" -onViolation = "Cuando no cumple" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Añadir" -addPlaceholder = "Añadir una etiqueta…" -customNote = "Personalizado para tu equipo." -defaultNote = "Usando el valor predeterminado integrado, compartido con tu equipo." -duplicate = "\"{{name}}\" ya existe." -edit = "Editar etiquetas" -empty = "Aún no hay etiquetas." -export = "Exportar JSON" -iconAria = "Elegir un icono para {{name}}" -import = "Importar JSON" -importError = "No se ha podido importar ese archivo." -managedNote = "Las etiquetas de equipo las gestiona el líder de tu equipo." -modalSubtitle = "Compartido con todo tu equipo. El clasificador elige las etiquetas que encajan con cada documento." -modalTitle = "Etiquetas de clasificación" -pickIcon = "Usar el icono de {{label}}" -removeAria = "Eliminar {{name}}" -resetToDefault = "Restablecer al valor predeterminado" -saveForTeam = "Guardar para el equipo" -saving = "Guardando…" -sectionLabel = "Etiquetas de clasificación" -startFromScratch = "Empezar desde cero" -teamCount = "etiquetas de equipo" -tooLong = "Las etiquetas pueden tener como máximo {{max}} caracteres." -ungrouped = "Sin agrupar" -view = "Ver etiquetas" +categoryCount = "categorías" +hideCategory = "Ocultar categoría" +labelCount = "etiquetas" +sharedNote = "Estas etiquetas están integradas y se comparten con todo tu equipo." +showCategory = "Mostrar categoría" [policies.pii] account = "Números de cuenta (etiquetados)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Números de enrutamiento de EE. UU. (ABA)" ssn = "Números de la Seguridad Social" -[policies.settings] -noneExport = "Actualmente no se ejecuta ninguna política al exportar." -noneUpload = "Actualmente no se ejecuta ninguna política al subir." -onExport = "Al exportar" -onUpload = "Al subir" -reorderHandle = "Arrastrar para reordenar" -runOrderDesc = "Cuando se ejecuta más de una política en el mismo activador, se ejecutan en este orden: cada una sobre la salida de la política anterior. Arrastra para reordenar." -title = "Configuración de políticas" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opciones de política" -policySettings = "Configuración de políticas" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Reintentar políticas fallidas ({{count}})" -rowProgress = "{{completed}} de {{total}} archivos procesados" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "¿Qué es una política?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Encuentra y oculta automáticamente detalles sensibles —como números de la Seguridad Social y de tarjetas— para que no puedan leerse en el documento." -sanitize = "Elimina JavaScript oculto del archivo, para que nada se ejecute automáticamente cuando alguien lo abra." -watermark = "Estampa una marca visible (p. ej., \"Confidential\") en cada página." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Cerrar" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "Aplicando {{name}}..." badgeRan = "La política {{name}} se ejecutó en este archivo" +badgeRunning = "{{name}} en ejecución..." blockingAction = "{{action}} bloqueada mientras se aplica la política, espera..." dismiss = "Descartar superposición" enforcingTitle = "Aplicando política..." @@ -6151,6 +6316,7 @@ viewAnyway = "Ver archivo (la política aún se está aplicando)" [policyOption] autoRedactPhi = "Censurar PHI automáticamente" blockExport = "Bloquear exportación" +ccpa = "CCPA" contracts = "Contratos" documents = "Documentos" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plan Editor" subscribed = "Plan Processor" unlinked = "No vinculada" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap desde documento" -sectionsAriaLabel = "Secciones del creador de agentes" -selectorAriaLabel = "Agentes" -subtitle = "Diseña, prueba y lanza los agentes de IA que clasifican, extraen y enrutan tus documentos. Define escenarios, delimita el acceso a herramientas, ejecuta un conjunto de referencia y publica una versión." -title = "Creador de agentes" - -[portal.agentBuilder.bootstrap] -cancel = "Cancelar" -dropzoneText = "Elegir un documento de muestra (PDF o imagen)" -lead = "Suelta un documento de muestra y propondremos escenarios y un esquema de extracción que podrás perfeccionar. No se publica nada hasta que lo revises." -submit = "Hacer bootstrap del agente" -subtitle = "Inicializa un nuevo agente a partir de un archivo representativo" -title = "Bootstrap desde un documento" - -[portal.agentBuilder.empty] -description = "Haz bootstrap de un agente desde un documento de muestra para inicializar sus escenarios y su esquema de extracción; luego perfila y publica." -title = "Aún no hay agentes" - -[portal.agentBuilder.evals] -casesPassing = "Casos aprobados" -columnCase = "Caso de evaluación" -columnLatency = "Latencia" -columnResult = "Resultado" -fail = "fallo" -goldenSetPassRate = "Tasa de aprobación del conjunto de referencia" -latencyMs = "{{ms}} ms" -notRun = "no ejecutado" -pass = "aprobado" -passRate = "Tasa de aprobación" -runEvals = "Ejecutar evaluaciones" - -[portal.agentBuilder.evals.empty] -description = "Las evaluaciones convierten tus escenarios en un conjunto de referencia repetible. Actualiza para capturar la tasa de aprobación a lo largo del tiempo y condicionar las publicaciones a ella." -title = "Aún no hay conjunto de referencia" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "entre conjuntos de referencia" -activeAgents = "Agentes activos" -avgPassRate = "Tasa media de aprobación de evaluaciones" -fleetWide = "en toda la flota" -latestPublished = "Última publicada" -scenarios = "Escenarios" -testCases = "casos de prueba" -totalDescription_one = "{{count}} en total" -totalDescription_other = "{{count}} en total" - -[portal.agentBuilder.scenarios] -add = "Añadir" -addScenario = "Añadir escenario" -enable = "Habilitar" -expectationLabel = "Comportamiento esperado" -expectationPlaceholder = "Qué debe hacer el agente" -inEval = "en evaluación" -mute = "Silenciar" -muted = "silenciado" -nameLabel = "Nombre" -namePlaceholder = "p. ej., Escalada de cumplimiento" - -[portal.agentBuilder.tabs] -evals = "Evaluaciones" -scenarios = "Escenarios" -tools = "Herramientas" -versions = "Versiones" - -[portal.agentBuilder.tools] -broadAccess = "Acceso amplio" -deniedHint = "Las herramientas seleccionadas están bloqueadas. Todo lo demás sigue siendo invocable." -deniedTools = "Herramientas denegadas" -governanceGate = "La gobernanza de herramientas está disponible en el plan Enterprise." -restricted = "Restringido" -restrictedAccess = "Acceso restringido a herramientas" -restrictedDescription = "Permite todas las herramientas excepto las que deniegues abajo." - -[portal.agentBuilder.versions] -current = "actual" -historyGate = "El historial completo de versiones y la reversión están disponibles en el plan Enterprise." -publish = "Publicar" -rollBack = "Revertir" - [portal.assistant] close = "Cerrar asistente" error = "No se ha podido contactar con el asistente." @@ -6365,9 +6451,21 @@ typing = "Escribiendo" loading = "Cargando" redirectingToEditor = "Redirigiendo al editor..." +[portal.billing.activation] +maybeLater = "Quizá más tarde" +title = "Activar el Procesador" + +[portal.billing.activation.payg] +sub = "Establece un límite mensual. Paga por lo que ejecutes." +title = "Pago por uso" + +[portal.billing.activation.prepay] +badge = "2 meses gratis" +sub = "Una factura, tarjeta o transferencia bancaria." +title = "Prepaga un año" + [portal.billing.checkout] noClientSecret = "La función Edge no devolvió client_secret." -subtitle = "Añade una tarjeta para continuar tras tu concesión gratuita del plan Editor. Stripe se encarga del resto." title = "Activar el plan Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Tu pago se ha realizado correctamente, pero la activación está tardand close = "Cerrar" title = "Ya casi está" +[portal.billing.checkout.cap] +amountAria = "Límite de gasto mensual" +back = "Atrás" +continue = "Continuar al pago" +error = "No se pudo establecer tu límite de gasto" +finePrint = "Las facturas se emiten mensualmente. Cancela en cualquier momento." +noLimit = "Sin límite" +note = "Nunca se te facturará por encima de tu límite. El procesamiento simplemente se pausa." +perMonth = "/ mes" +title = "Establece tu límite de gasto" + +[portal.billing.checkout.card] +fields = "Número de tarjeta · MM / AA · CVC · ZIP" +label = "Datos de la tarjeta" +note = "Datos de tarjeta recopilados por Stripe. Stirling nunca almacena PAN ni CVC." + [portal.billing.checkout.error] title = "No se ha podido iniciar el pago" @@ -6383,10 +6497,9 @@ body = "Tu pago se ha procesado. Estamos activando el procesamiento medido en tu hint = "Mantén esta ventana abierta." title = "Activando tu plan Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "en el env del portal para habilitar el pago en la aplicación." -bodyBefore = "Configura" -title = "Stripe no configurado" +[portal.billing.checkout.pay] +limit = "Límite de gasto mensual {{amount}}/mes. El procesamiento se pausa al alcanzar el límite." +limitNoCap = "Sin límite de gasto. El procesamiento no se pausará. Las facturas se emiten mensualmente." [portal.billing.enterpriseUpsell] cta = "Crear tu presupuesto Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plan actual" freeForever = "Gratis para siempre" noTeamResolved = "Aún no se ha resuelto ningún equipo en tu cartera; actualiza e inténtalo de nuevo." ownerOnly = "Solo el propietario del equipo puede activar el plan Processor." +payInvoice = "Pagar factura para completar" planName = "Editor" ssoIncluded = "SSO incluido" switchOnProcessor = "Activar Processor →" unlimitedUsers = "Usuarios ilimitados" +viewQuote = "Ver presupuesto" [portal.billing.invoices] columnAmount = "Importe" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Solicitudes directas de API" segmentApiLabel = "API" segmentAutomationDesc = "Automatizaciones y pipelines" segmentAutomationLabel = "Automatización" +sizeMultiplier = "{{formatted}} PDFs usaron un multiplicador de tamaño" +summary = "{{unique}} únicos · {{units}} unidades de contador · {{avg}} media por PDF" +summaryNoRate = "{{unique}} únicos · {{units}} unidades de contador" unit = "PDFs medidos" +unitsPending_one = "{{units}} unidad de contador pendiente de sincronización desde instancias vinculadas" +unitsPending_other = "{{units}} unidades de contador pendientes de sincronización desde instancias vinculadas" + +[portal.billing.prepaid] +capSuffix = "de {{total}} créditos prepago" +expires = "Caduca el {{date}}" +eyebrow = "Capacidad prepago" +note = "Se usa antes de la facturación medida y fuera de tu límite de gasto." +topUp = "Recargar" + +[portal.billing.prepaid.buy] +back = "Atrás" +calcTitle = "Calcula tu pago anual" +cancel = "Cancelar" +cancelPurchase = "Cancelar compra" +close = "Cerrar" +continueToPayment = "Continuar al pago" +finalise = "Finalizar" +invoiceErrorTitle = "No se pudo generar la factura" +loadingTitle = "Cargando tu presupuesto" +payTitle = "Paga tu año" +step = "Paso {{current}} de {{total}}" + +[portal.billing.prepaid.calc] +change = "Cambiar" +deployRow = "Despliegue" +done = "Hecho" +enterpriseHint = "Esto es escala empresarial. Con este volumen, las tarifas empresariales superan cualquier descuento de autoservicio. Precio fijo, condiciones y presupuesto en minutos." +governanceRow = "Gobernanza" +governanceValue = "{{label}} · {{count}} políticas" +handlesLabel = "Tu Procesador" +handlesValue = "gestiona {{volume}} créditos / mes" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} políticas" +poolCaption = "Un fondo de {{credits}} créditos para el año. Los meses de mayor uso toman prestado de los meses de menor uso." +priceLabel = "Tu año · 12 meses por el precio de 10" +rateUnknown = "Mostraremos el precio exacto al pagar." +savings = "Ahorras {{amount}} · 2 meses gratis." +sizingRow = "Dimensionamiento" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Usuarios totales" + +[portal.billing.prepaid.consent] +label = "Entiendo que, cuando mi capacidad prepago se agote o caduque después de 12 meses, el procesamiento continuará automáticamente a la tarifa estándar medida de pago por uso (hasta mi límite de gasto) salvo que cancele, y que puedo cancelar en cualquier momento desde el portal de facturación." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gestionado por Stirling. En funcionamiento en minutos." +cloudValue = "Stirling Cloud · Gestionado" +selfhost = "Autoalojado" +selfhostDesc = "Se ejecuta en infraestructura privada. Misma tarifa." +selfhostValue = "Autoalojado · Infraestructura privada" + +[portal.billing.prepaid.offer] +cta = "Revisar oferta" +subtitle = "Prepaga un año de procesamiento de PDF y obtén dos meses gratis, usados antes de la facturación medida, fuera de tu límite de gasto." +title = "Consigue 12 meses por el precio de 10" + +[portal.billing.prepaid.pay] +accountName = "Nombre del titular de la cuenta" +accountNamePlaceholder = "p. ej. Jane Smith" +companyName = "Nombre de la empresa (opcional)" +companyNamePlaceholder = "p. ej. Acme Inc." +downloadInvoice = "Descargar factura" +payOnline = "Pagar online" +poNumber = "Número de pedido (opcional)" +poNumberPlaceholder = "p. ej. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avanzado" +advancedDesc = "Los pipelines impulsan la mayor parte de tu procesamiento." +none = "Ninguno" +noneDesc = "Aún no ejecutas pipelines. Actívalos en cualquier momento." +standard = "Estándar" +standardDesc = "Algunos pipelines reprocesan los PDFs entrantes." + +[portal.billing.prepaid.posture] +essentials = "Esenciales" +essentialsDesc = "Clasificación, la predeterminada, más uso compartido" +governed = "Gobernado" +governedDesc = "Añade seguridad y enrutamiento" +regulated = "Regulado" +regulatedDesc = "Todas las categorías, incl. cumplimiento, retención e ingesta" + +[portal.billing.prepaid.review] +download = "Descargar presupuesto (PDF)" +downloading = "Preparando…" +share = "compartir para aprobación" + +[portal.billing.prepaid.size] +compact = "Compacto" +compactDesc = "Archivos de menos de 25 MB, sin cargos por datos" +heavy = "Pesado" +heavyDesc = "Escaneados o con muchas imágenes, normalmente 50 MB+" +standard = "Estándar" +standardDesc = "Principalmente pequeños, algunos escaneos por encima de 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Agotada" +healthy = "Queda bastante" +low = "Queda poco" [portal.billing.spendLimit] adjustLimit = "Ajustar límite" @@ -6494,6 +6713,8 @@ label = "Proyectado para superar." [portal.billing.spendThisMonth] eyebrow = "Gasto este mes" +freeRemaining_one = "Queda {{formatted}} PDF gratis" +freeRemaining_other = "Quedan {{formatted}} PDFs gratis" processed_one = "{{formattedCount}} PDF procesado." processed_other = "{{formattedCount}} PDFs procesados." processedWithRate_one = "{{formattedCount}} PDF procesado, a {{rate}} cada uno." @@ -6510,6 +6731,7 @@ reachedTitle = "Límite de gasto mensual alcanzado" title = "No se ha podido abrir el portal de Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "de {{allowance}} PDFs gratuitos usados" capSuffix_other = "de {{allowance}} PDFs gratuitos usados" eyebrow = "Prueba de Processor" @@ -6534,6 +6756,376 @@ signature = "firma" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nueva conexión" +createTitleFor = "Conectar {{name}}" +delete = "Eliminar" +edit = "Editar" +editTitle = "Editar conexión" +unsupportedType = "Este tipo de conexión no se puede editar aquí." + +[portal.connections.categories.advanced] +description = "Para un servicio que aún no admitimos." +label = "Avanzado" + +[portal.connections.categories.audit] +description = "Registra lo que le ocurrió a un documento, donde tus auditores ya miran." +label = "Registro de auditoría y cumplimiento" + +[portal.connections.categories.notify] +description = "Informa a otro sistema de que ha ocurrido algo." +label = "Notificación y flujo de trabajo" + +[portal.connections.categories.security] +description = "Escanea o etiqueta un documento antes de que avance." +label = "Seguridad y clasificación" + +[portal.connections.categories.signing] +description = "Envía un documento para que se firme o notarice." +label = "Firma electrónica y notarización" + +[portal.connections.categories.storage] +description = "Dónde acaban los documentos procesados: un bucket, una unidad, un ticket o una página." +label = "Archivo y adjuntar" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling lo almacena cifrado y nunca vuelve a mostrarlo." +label = "Token de acceso" + +[portal.connections.commonFields.apiKey] +label = "Clave de API" + +[portal.connections.commonFields.apiToken] +label = "Token de API" + +[portal.connections.commonFields.baseUrl] +helperText = "Los pasos que usan esta conexión solo pueden llamar a rutas bajo esta URL, nunca a otro host." +label = "URL del servidor" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Contraseña" + +[portal.connections.commonFields.port] +label = "Puerto" + +[portal.connections.commonFields.username] +label = "Nombre de usuario" + +[portal.connections.commonFields.webhookUrl] +helperText = "Pega la URL que te proporcionó el servicio. Solo se pueden llamar rutas bajo ella." +label = "Webhook URL" + +[portal.connections.fields] +name = "Nombre de la conexión" +namePlaceholder = "p. ej. Producción" + +[portal.connections.picker] +cancel = "Cancelar" +createNew = "Nueva conexión..." +placeholder = "Selecciona una conexión" +save = "Guardar conexión" + +[portal.connections.picker2] +back = "Cambiar integración" +heading = "¿Qué quieres conectar?" +noResultsBody = "Aún no lo admitimos; cuéntanoslo y estudiaremos añadirlo." +noResultsTitle = "Nada coincide con \"{{query}}\"" +searchPlaceholder = "Buscar integraciones: prueba \"sign\", \"ocr\" o un nombre de producto" + +[portal.connections.types.api] +description = "Llama a cualquier HTTP API desde una política. Tú eliges la URL, la autenticación y la carga útil." +label = "API personalizada" + +[portal.connections.types.api.fields.authType] +label = "Autenticación" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nombre de usuario y contraseña" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token en una cabecera personalizada" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ninguna" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Iniciar sesión para obtener un token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Los pasos que usan esta conexión solo pueden llamar a rutas bajo esta URL, nunca a otro host." +label = "URL base" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nombre de la cabecera" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Ruta de inicio de sesión" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Contraseña" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Si la API responde con un enlace de descarga, solo se podrán obtener estos hosts. Sepáralos con comas. Déjalo en blanco para permitir solo el host propio de la URL base." +label = "Hosts de resultado permitidos" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Enviar el token como esta cabecera" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "La cabecera de respuesta en la que la llamada de inicio de sesión devuelve el token, p. ej. X-Auth-Token." +label = "Cabecera de respuesta del token" + +[portal.connections.types.api.fields.username] +label = "Nombre de usuario" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Escanea documentos en busca de malware en un servidor que tú ejecutas; nada sale de tu red." +label = "ClamAV (autoalojado)" + +[portal.connections.types.cloudmersive] +description = "Escaneo de virus y contenido para documentos en una política." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloquea documentos que contengan macros, ejecutables, scripts u objetos incrustados, no solo malware conocido." +label = "Escaneo avanzado de Cloudmersive" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Adjunta documentos procesados a una página de Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Envía documentos a ConsignO Cloud (Notarius) para firma electrónica y notarización." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Tu entorno de ConsignO Cloud, incluido /api/v1." +label = "URL del servidor" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Clave de API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Secreto de API" + +[portal.connections.types.consigno.fields.password] +label = "Contraseña" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Solo necesario si tu organización usa tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nombre de usuario" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publica un mensaje en Discord cuando una política procese un documento." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexa un evento de auditoría en Elasticsearch cada vez que una política gestiona un documento." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Consulta un servidor FTP o FTPS en busca de nuevos documentos." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Modo de conexión" + +[portal.connections.types.ftp.fields.passive.options] +active = "Activo" +passive = "Pasivo (recomendado)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP sin cifrar envía credenciales y archivos sin cifrado; elige FTPS si el servidor lo admite." +label = "Cifrado" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explícito / AUTH TLS)" +implicit = "FTPS (implícito)" +none = "Ninguno (FTP sin cifrar)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publica un mensaje en un espacio de Google Chat cuando se ejecute una política." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Adjunta documentos procesados a una incidencia de Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Envía por email un documento o una notificación cuando se ejecute una política." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Archiva documentos procesados en Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Contraseña de la aplicación" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detecta datos personales en el texto de documentos usando un servidor Presidio que tú ejecutas." +label = "Presidio (autoalojado)" + +[portal.connections.types.purview] +description = "Lee y aplica etiquetas de sensibilidad de Microsoft Purview en PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcional. Solo necesario para elegir etiquetas por nombre; el etiquetado funciona sin él." +label = "ID de aplicación (cliente)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Secreto de cliente" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Se estampa en cada etiqueta como su SiteId, para que las herramientas compatibles con Purview sepan qué organización la aplicó." +label = "ID de directorio (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Un bucket S3 desde el que las fuentes pueden leer y en el que las salidas de pipeline pueden escribir." +label = "Almacenamiento S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID de clave de acceso" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Déjalo en blanco para AWS. Establécelo para MinIO u otro almacén compatible con S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Región" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Clave de acceso secreta" + +[portal.connections.types.sendgrid] +description = "Envía por email un documento o una notificación cuando se ejecute una política." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Consulta un servidor SFTP (SSH) en busca de nuevos documentos." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcional. La huella de clave de host SHA-256 del servidor (de `ssh-keyscan host | ssh-keygen -lf -`). Cuando se establece, solo se acepta un servidor que presente esta clave; cuando está en blanco, la clave vista en la primera conexión queda fijada y se rechazan cambios posteriores." +label = "Huella de clave de host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frase de contraseña de la clave" + +[portal.connections.types.sftp.fields.password] +helperText = "Déjalo en blanco si te autenticas con una clave privada en su lugar." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcional. Pega una clave privada OpenSSH para autenticarte con una clave en lugar de una contraseña." +label = "Clave privada" + +[portal.connections.types.slack] +description = "Publica un mensaje en Slack cuando una política procese un documento." +label = "Slack" + +[portal.connections.types.smb] +description = "Vigila un recurso compartido de red SMB / CIFS (Windows o Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Recurso compartido de red" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcional. El dominio o grupo de trabajo de Windows, si tu servidor usa uno." +label = "Dominio" + +[portal.connections.types.smb.fields.share] +label = "Recurso compartido" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Envía un evento de auditoría a Splunk cada vez que una política gestiona un documento." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Envía un evento de auditoría a un recopilador HTTP de Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publica un mensaje en un canal de Teams cuando se ejecute una política." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publica cada documento, o solo los detalles, en una URL que elijas." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Activa un escenario de Zap o Make desde una política." +label = "Zapier y Make" + +[portal.docs] +browse = "Explorar documentación" +viewSource = "Ver código fuente en GitHub" + [portal.docs.authentication] codeCaption = "cada solicitud" eyebrow = "PRIMEROS PASOS" @@ -6624,6 +7216,9 @@ eyebrow = "HABILIDADES" lead = "Capacidades incluidas y con nombre que tu agente invoca como una sola herramienta. Cada habilidad es una cadena de operaciones determinista con evaluaciones adjuntas." title = "Habilidades de agente" +[portal.docs.toc] +title = "En esta página" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERENCIA DE API" @@ -6697,6 +7292,8 @@ type = "Tipo" user = "Usuario" [portal.documents.queue.empty] +connectSource = "Conectar una fuente" +createPipeline = "Crear un pipeline" description = "A medida que los orígenes envíen documentos a tus pipelines, aparecerán aquí para su revisión." title = "No hay documentos en la cola" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Ningún documento coincide con este filtro." rowActions = "Acciones de fila" sensitiveLabel = "Sensible" -sensitiveTitle = "Sensible — se requiere acceso" [portal.documents.table.columns] action = "Pipeline / Acción" document = "Documento" +labels = "Labels" product = "Producto" status = "Estado" time = "Hora" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Última vez visto" region = "Región" status = "Estado" +target = "Target" version = "Versión" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Esta vista ha encontrado un error inesperado. Inténtalo de nuevo retry = "Intentarlo de nuevo" title = "Algo ha salido mal en esta página" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Atrás" +body = "Descarga la aplicación de escritorio o autoaloja el servidor." +done = "Hecho" +guide = "Abrir la guía de {{name}}" +openInBrowser = "Abrir en el navegador en su lugar" +sectionDesktop = "Aplicación de escritorio" +sectionSelfHosted = "Autoalojado" +title = "Instalar el editor de Stirling PDF" + +[portal.home.download.docker] +detailBody = "Ejecuta la imagen oficial en tu servidor." +note = "Se abre en :8080. Inicio de sesión predeterminado: admin / stirling." +tagline = "Ejecuta el servidor en un contenedor" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instala el chart oficial de Helm." +note = "Añade un PVC para /configs para conservar la configuración." +tagline = "Despliega en un clúster con Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Compilaciones nativas para tu distribución." +guideBtn = "Ver opciones de instalación para Linux" +note = "Hay compilaciones AppImage, .deb y .rpm disponibles para distribuciones comunes." +tagline = "Aplicación de escritorio nativa para tu distribución" +title = "Linux" + +[portal.home.download.mac] +altLabel = "O mediante Homebrew" +detailBody = "Ejecuta el instalador o usa Homebrew." +downloadBtn = "Descargar para macOS" +tagline = "Aplicación nativa para Apple silicon e Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Ejecuta el .jar en un servidor físico." +downloadBtn = "Descargar el .jar" +note = "Requiere Java 21+. Se abre en :8080; inicio de sesión predeterminado admin / stirling." +runLabel = "Después ejecútalo" +tagline = "Ejecuta el .jar en un servidor físico" +title = "Configuración manual del servidor" + +[portal.home.download.windows] +altLabel = "O mediante winget" +detailBody = "Ejecuta el instalador o usa un gestor de paquetes." +downloadBtn = "Descargar instalador" +tagline = "Aplicación nativa para Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} activos" -invite = "Invitar a compañeros" +activeOfDeployed = "{{active}} de {{total}} activos este mes" name = "Stirling PDF Editor" open = "Abrir en el navegador" updated = "actualizado {{time}}" +[portal.home.editor.deploy] +finish = "Finalizar despliegue" +options = "Ver opciones de despliegue" +start = "Desplegar el Editor" + [portal.home.editor.target] cloud = "Cloud gestionado" docker = "Autoalojado · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Buenas tardes" evening = "Buenas noches" morning = "Buenos días" -[portal.home.onboarding] -dismiss = "Descartar configuración" -notStarted = "No iniciado" -progress = "{{done}} de {{total}} completado" -title = "Terminar la configuración" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC para toda la organización, precios por volumen comprometido y despliegue aislado de red." -cta = "Iniciar prueba" -ctaQuote = "Solicitar presupuesto" -lead = "Para más de 250 empleados." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Despliega la aplicación de escritorio, gratis para siempre." -title = "Descargar el PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} activas · {{recommended}} recomendadas" -chip = "{{active}} activas" -title = "Activar políticas" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} conectados · cada PDF gobernado donde llega" -chip = "{{connected}} conectados" -title = "Conectar tus orígenes" - -[portal.home.quickActions] -subtitle = "Tareas principales para hoy" -title = "Acciones rápidas" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositor de 3 pasos sobre la biblioteca de operaciones tipadas" -title = "Crear un pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agentes, webhooks, carpetas supervisadas" -title = "Conectar un origen" - -[portal.home.quickActions.issueApiKey] -blurb = "Clave con alcance, límites de tasa y lista de permitidos IP" -title = "Emitir una clave API" - [portal.infrastructure] manageEditorDeployment = "Gestionar despliegue del Editor" sectionsAriaLabel = "Secciones de infraestructura" @@ -6878,13 +7541,9 @@ heading = "Claves API" subheading = "Credenciales con alcance, límites de tasa por clave, permisos y listas de IP permitidas." [portal.infrastructure.apiKeys.card] -allowedIps = "IP permitidas" -anyIp = "Cualquier IP (sin lista de permitidos)" created = "Creada" lastUsed = "Último uso" -permissions = "Permisos" -rateLimit = "Límite de tasa" -rateLimitValue = "{{value}} req/min" +revoke = "Revocar clave" usageMonth = "Uso este mes" usageToday = "Uso hoy" @@ -6892,6 +7551,15 @@ usageToday = "Uso hoy" description = "Crea una clave con alcance para empezar a llamar a Stirling API." title = "Aún no hay claves API" +[portal.infrastructure.apiKeys.error] +load = "No se han podido cargar tus claves de API. Inténtalo de nuevo." + +[portal.infrastructure.apiKeys.revoke] +body = "¿Revocar \"{{name}}\"? Cualquier integración que siga usando esta clave empezará a recibir errores 401 inmediatamente. Esta acción no se puede deshacer." +cancel = "Cancelar" +confirm = "Revocar clave" +title = "Revocar clave de API" + [portal.infrastructure.audit] filterAriaLabel = "Filtrar eventos de auditoría por categoría" heading = "Registros de auditoría" @@ -6935,6 +7603,7 @@ all = "Todos" auth = "Autenticación" config = "Configuración" elevation = "Elevación" +policy = "Política" processing = "Procesamiento" security = "Seguridad" @@ -6944,19 +7613,30 @@ title = "No tienes acceso" [portal.infrastructure.audit.metrics] config = "Configuración" -elevation = "Elevación" +policy = "Política" processing = "Procesamiento" totalEvents = "Eventos totales · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autenticación" +config = "Configuración" +elevation = "Elevación" +policy = "Política" +processing = "Procesamiento" +security = "Seguridad" + +[portal.infrastructure.auditStatusLabel] +danger = "Error" +info = "Información" +success = "Correcto" +warning = "Advertencia" + [portal.infrastructure.createKey] cancel = "Cancelar" createKey = "Crear clave" done = "Hecho" -ipAllowlistHelper = "Rangos CIDR separados por comas. Déjalo en blanco para permitir cualquier IP." -ipAllowlistLabel = "Lista de IP permitidas" keyNameLabel = "Nombre de clave" keyNamePlaceholder = "p. ej. Producción · ingesta" -permissionsLabel = "Permisos" secretKeyCaption = "Clave secreta" secretWarning = "Guárdala en un gestor de secretos. Stirling solo almacena un hash — no hay forma de recuperarla más tarde." subtitle = "Limita la clave a lo mínimo que necesita. Puedes rotarla o revocarla en cualquier momento." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copia este secreto ahora — no se volverá a mostrar." title = "Crear clave API" titleCreated = "Clave creada" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Desplegado por" -environment = "Entorno" -product = "Producto" -status = "Estado" -version = "Versión" -when = "Cuándo" - -[portal.infrastructure.deployments.recent] -heading = "Despliegues recientes" -subheading = "Los últimos despliegues en productos y entornos." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instancias" -latency = "Latencia" -load = "Carga" -p99 = "P99" -region = "Región" -status = "Estado" -throughput = "Rendimiento" -uptime = "Tiempo de actividad" -version = "Versión" - -[portal.infrastructure.deployments.regions] -heading = "Regiones" -subheading = "Estado en vivo de cada región Stirling desplegada — latencia, carga y versión de despliegue." - -[portal.infrastructure.deployments.regions.empty] -description = "Las regiones desplegadas aparecerán aquí cuando se aprovisione tu espacio de trabajo." -title = "No hay regiones desplegadas" - -[portal.infrastructure.models] -heading = "Modelos" -msValue = "{{value}} ms" -subheading = "El catálogo de modelos y el enrutamiento que impulsa el procesamiento de documentos en todo tu espacio de trabajo." - -[portal.infrastructure.models.byom] -description = "Registra un modelo on-prem o autoalojado y fíjalo a una región para procesamiento sujeto a residencia de datos." -title = "Trae tu propio modelo" - -[portal.infrastructure.models.catalogue] -heading = "Catálogo" -sub = "Modelos gestionados disponibles para tu espacio de trabajo, con latencia y coste en vivo." -subEnterprise = "Modelos gestionados, propios y on-prem — con fijación por región disponible." - -[portal.infrastructure.models.catalogue.empty] -description = "Los modelos del catálogo de tu espacio de trabajo aparecen aquí." -title = "No hay modelos disponibles" - -[portal.infrastructure.models.columns] -cost = "Coste" -latency = "Latencia" -load = "Carga" -model = "Modelo" -status = "Estado" -type = "Tipo" -version = "Versión" - -[portal.infrastructure.models.metrics] -activeModels = "Modelos activos" -avgLatency = "Latencia media" -included = "Incluido" -monthlySpend = "Gasto mensual en modelos" - -[portal.infrastructure.models.routing] -empty = "No hay reglas de enrutamiento configuradas." -heading = "Reglas de enrutamiento" -sub = "Qué modelo gestiona cada operación. El valor predeterminado se aplica cuando no coincide ninguna regla más específica." -subLocked = "Dirige operaciones a modelos específicos — disponible en planes de pago." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Mejora a Pro para controlar qué modelo gestiona cada operación y tipo de documento." -title = "El enrutamiento de modelos es una función de pago" - -[portal.infrastructure.models.routingColumns] -default = "Predeterminado" -docType = "Tipo de documento" -modelForAria = "Modelo para {{operation}}" -operation = "Operación" -routedTo = "Enrutado a" - -[portal.infrastructure.security.access.byok] -description = "Proporciona una clave de tu propio KMS. Stirling cifra con ella, pero aún puede leer." -label = "Trae tu propia clave (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Las claves nunca salen de tu KMS. Stirling solo conserva texto cifrado." -label = "Conserva tu propia clave (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling gestiona las claves de cifrado. Lo más sencillo — cero operaciones de claves por tu parte." -label = "Claves conservadas por Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Política de acceso a documentos" -subheading = "Controla quién puede descifrar documentos procesados en reposo." - -[portal.infrastructure.security.attestations] -heading = "Atestaciones de cumplimiento" -noReport = "No hay informe disponible" -subheading = "Postura de auditoría marco por marco, con informes disponibles sobre controles atestados." -viewReport = "Ver informe →" - -[portal.infrastructure.security.compliance] -heading = "Cumplimiento" -subheading = "Atestaciones y certificaciones que cubren la plataforma Stirling." - -[portal.infrastructure.security.empty] -description = "La configuración de seguridad de tu espacio de trabajo aparecerá aquí." -title = "Postura de seguridad no disponible" - -[portal.infrastructure.security.hyokBanner] -description = "Con HYOK, las claves de cifrado nunca salen de tu KMS. Stirling almacena y procesa solo texto cifrado que puedes revocar en cualquier momento." -title = "Stirling no puede descifrar tus documentos" - -[portal.infrastructure.security.ipAllowlist] -empty = "No hay rangos IP configurados — todas las IP están permitidas." -heading = "Lista de IP permitidas" -sub = "El acceso a API está restringido a estos rangos CIDR." -subLocked = "Restringe el acceso a API a rangos IP conocidos — disponible en planes de pago." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Mejora a Pro para restringir el acceso a API a redes específicas." -title = "La lista de IP permitidas es una función de pago" - -[portal.infrastructure.security.ipColumns] -added = "Añadido" -addedBy = "Añadido por" -cidr = "CIDR" -label = "Etiqueta" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmo" -heading = "Gestión de claves de cifrado" -keyId = "Identificador de clave" -lastRotated = "Última rotación" -rotateKey = "Rotar clave" -rotationPolicy = "Política de rotación" -subheading = "Custodia de las claves que cifran documentos en reposo — quién puede descifrar y cómo rotan las claves." - -[portal.infrastructure.security.managedBanner] -description = "La custodia trae-tu-propia-clave (BYOK) y conserva-tu-propia-clave (HYOK) está disponible en Enterprise. Mejora para proporcionar claves desde tu propio KMS." -title = "Las claves las gestiona Stirling en tu plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia-Pacífico" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · límite de datos GDPR" -label = "Unión Europea" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Estados Unidos" - -[portal.infrastructure.security.residencyHeader] -heading = "Residencia de datos" -subheading = "Dónde se almacenan y procesan los documentos." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} usado" - -[portal.infrastructure.storage.empty] -description = "El almacenamiento conectado y el uso aparecerán aquí." -title = "No hay almacenamiento configurado" - -[portal.infrastructure.storage.lifecycle] -active = "Activo" -activeRange = "0–{{value}} d" -archived = "Archivado" -coldStorage = "almacenamiento en frío" -deleted = "Eliminado" -never = "nunca" -purged = "purgado" - -[portal.infrastructure.storage.providers] -connect = "Conectar" -connected = "Conectado" -heading = "Proveedores conectados" -subheading = "Dónde se escriben los artefactos procesados." - -[portal.infrastructure.storage.retention] -heading = "Retención" -subheading = "Cuánto tiempo se conservan los artefactos antes de la eliminación del ciclo de vida." -windowLabel = "Ventana de retención predeterminada" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} día" -days_other = "{{count}} días" -never = "No eliminar nunca" - -[portal.infrastructure.storage.totalUsage] -heading = "Uso total" -progressLabel = "Almacenamiento usado" -subheading = "Almacenamiento consumido en todos los proveedores conectados." +[portal.infrastructure.keyLabel] +active = "Activa" +revoked = "Revocada" [portal.infrastructure.tabs] apiKeys = "Claves API" @@ -7174,6 +7656,49 @@ models = "Modelos" security = "Seguridad" storage = "Almacenamiento" +[portal.integrations] +availableHeading = "Disponibles" +comingSoonHeading = "Próximamente" +connect = "Conectar" +connectionCount_one = "{{count}} conexión" +connectionCount_other = "{{count}} conexiones" +customApi = "API personalizada" +searchPlaceholder = "Buscar integraciones" +subtitle = "Todo con lo que Stirling puede comunicarse. Las credenciales se crean una vez aquí y luego se referencian desde orígenes, políticas y canalizaciones." +title = "Integraciones" +typedName = "Nombre de {{tool}}" + +[portal.integrations.filters] +all = "Todas" +audit = "Auditoría" +connected = "Conectadas" +notify = "Notificación" +security = "Seguridad" +signing = "Firma" +storage = "Almacenamiento" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Conectada" + +[portal.integrations.table] +integration = "Integración" +worksWith = "Funciona con" + +[portal.integrations.worksWith] +pipelines = "Canalizaciones" +policies = "Políticas" +sources = "Orígenes" + +[portal.legal] +draft = "{{label}} · borrador" +loadError = "No se ha podido cargar este documento. Inténtalo de nuevo." +loading = "Cargando…" +title = "Documento legal" + [portal.nav] agent-builder = "Constructor de agentes" components = "Componentes" @@ -7182,33 +7707,17 @@ documents = "Documentos" editor = "Editor" home = "Inicio" infrastructure = "Infraestructura" +integrations = "Integraciones" pipelines = "Pipelines" policies = "Políticas" procurement = "Compras" -settings = "Configuración" sources = "Orígenes" usage = "Uso y facturación" users = "Usuarios" -[portal.notifications] -markAllRead = "Marcar todo como leído" -title = "Notificaciones" -viewAll = "Ver todo" - -[portal.notifications.ariaLabel] -none = "Notificaciones, ninguna sin leer" -unread_one = "Notificaciones, {{count}} sin leer" -unread_other = "Notificaciones, {{count}} sin leer" - -[portal.notifications.count] -allRead = "todo leído" -loading = "cargando" -new_one = "{{count}} nueva" -new_other = "{{count}} nuevas" - -[portal.notifications.empty] -description = "No hay notificaciones nuevas." -title = "Estás al día" +[portal.nav.section] +platform = "Plataforma PDF" +processor = "Procesador PDF" [portal.pipelines] subtitle = "Cada pipeline automatizado de documentos en el backend: una cadena ordenada de operaciones sobre un conjunto de orígenes, ejecutada con un desencadenador. Haz clic en una fila para ver sus pasos y orígenes." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Nuevo pipeline" [portal.pipelines.builder] -addStep = "Añadir herramienta" +activate = "Activate" back = "Volver a pipelines" +cannotFollow = "No puede tomar {{produced}}" +chooseAccount = "Elige una cuenta" +chooseDestination = "Elige un destino" +chooseOperation = "Elige qué hace este paso" +chooseSource = "Elige un origen" discard = "Descartar cambios" -enabled = "Activado" +inputs = "Entrada" +inputSource = "Origen de entrada" +inputTrigger = "Disparador" keepEditing = "Seguir editando" -needsUpload = "Necesita un archivo subido" +moreActions = "More actions" +needsConfiguring = "Necesita configuración" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Ninguna herramienta coincide con tu búsqueda." -pipelineSettings = "Configuración del pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Buscar herramientas" -selectToolBody = "Añade una herramienta para crear tu pipeline." -selectToolTitle = "Aún no hay herramientas" -toolSettings = "Configuración de la herramienta" +sendToSystem = "Enviar a otro sistema" +stepsIncompatible = "Estos pasos no se pueden ejecutar sobre lo que produce su paso anterior: {{tools}}." +stepsNeedSetup = "Estos pasos aún necesitan configuración antes de guardar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operación no reconocida, conservada tal cual." unsavedBody = "Tienes cambios sin guardar. ¿Guardarlos antes de salir o descartarlos?" unsavedTitle = "Cambios sin guardar" -uploadUnsupported = "Los archivos subidos aún no son compatibles con pipelines, por lo que estos pasos no se pueden guardar: {{tools}}." usesDefaults = "Se ejecuta con la configuración predeterminada" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combina todos los archivos del paso anterior" +fan-out = "Se ejecuta una vez por archivo del paso anterior" +format-mismatch = "Necesita {{accepts}}, pero el paso anterior produce {{produced}}" +output-uncertain = "Puede que no se ejecute: la salida del paso anterior depende de cómo esté configurado" +source-mismatch = "Necesita {{accepts}}, pero la entrada de esta canalización es {{produced}}" +undeclared-operation = "No se puede comprobar qué acepta este paso" [portal.pipelines.composer] addTool = "Añadir herramienta" -cancel = "Cancelar" -chainEmpty = "Añade una herramienta para empezar a crear tu pipeline." create = "Crear pipeline" -directory = "Carpeta de salida" -directoryHelp = "Ruta absoluta en el servidor. Debe estar dentro de las carpetas permitidas configuradas." +createPaused = "Create paused" editingUnsupported = "Aún no se admite mostrar estos parámetros de herramienta para editarlos." -moveDown = "Mover abajo" -moveUp = "Mover arriba" +editSource = "Edit source" name = "Nombre" namePlaceholder = "p. ej. Barrido de censura" -noSources = "Aún no hay orígenes conectados. El pipeline aún puede ejecutarse con archivos suministrados directamente." noToolSettings = "Esta herramienta no tiene ajustes configurables." -operations_one = "Operación ({{count}})" -operations_other = "Operaciones ({{count}})" output = "Salida" -removeStep = "Eliminar operación" save = "Guardar cambios" scheduleEvery = "Ejecutar cada" -sources = "Orígenes" -sourcesLoading = "Cargando orígenes..." trigger = "Desencadenador" triggerManual = "Solo manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "días" hours = "horas" minutes = "minutos" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "¿Eliminar \"{{name}}\"? Esta acción no se puede deshacer." cancel = "Cancelar" @@ -7273,28 +7815,62 @@ confirm = "Eliminar" title = "¿Eliminar pipeline?" [portal.pipelines.detail] +clearHistory = "Borrar historial" delete = "Eliminar pipeline" run = "Ejecutar ahora" [portal.pipelines.empty] action = "Crear un pipeline" +connectSource = "Conectar un origen" description = "Crea tu primer pipeline: elige los orígenes sobre los que se ejecuta, encadena las operaciones y elige dónde va la salida." title = "Aún no hay pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Activos" paused = "Pausados" total = "Pipelines" -[portal.pipelines.output] -folder = "Escribir en carpeta" -inline = "Devolver archivos" - [portal.pipelines.run] +allProcessed_one = "Nada que ejecutar: el {{count}} documento del origen ya se ha procesado." +allProcessed_other = "Nada que ejecutar: los {{count}} documentos de los orígenes ya se han procesado." completed_one = "Ejecución completada." completed_other = "Las {{count}} ejecuciones se han completado." empty = "Nada que ejecutar: los orígenes no tenían documentos que procesar." failed = "La ejecución ha fallado: {{error}}" +inFlight = "Nada nuevo que ejecutar: todavía se están procesando documentos de una ejecución anterior." +parked_one = "Nada que ejecutar: {{count}} documento falló anteriormente y está aparcado. Corrige la causa y luego borra el historial para reintentarlo." +parked_other = "Nada que ejecutar: {{count}} documentos fallaron anteriormente y están aparcados. Corrige la causa y luego borra el historial para reintentarlos." running = "Ejecución iniciada; aún en curso." timeout = "La ejecución está tardando más de lo esperado; puede que aún termine en segundo plano." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Orígenes" status = "Estado" steps = "Pasos" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Supervisión de carpeta" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Programado" [portal.policies] +defaultName = "Política de {{category}}" subtitle = "Automatizaciones permanentes que aplican un pipeline de herramientas a cada documento. Cada política se activa al subir o exportar, ejecuta su cadena de herramientas y guarda la versión aplicada junto al original." title = "Políticas" [portal.policies.card] comingSoon = "Mejorar a Enterprise" notSetUp = "No configurado" +requiresAiEngine = "Requiere motor de IA" + +[portal.policies.categories.classification] +desc = "Identifica el tipo de cada documento al subirlo y etiqueta sus metadatos para el archivado y la búsqueda." +label = "Clasificación" [portal.policies.categories.compliance] desc = "Aplica requisitos HIPAA, GDPR, SOC 2 o FedRAMP a cada documento." @@ -7344,6 +7927,13 @@ label = "Seguridad" [portal.policies.config] scopeAll = "Todos los documentos" +[portal.policies.config.classification] +summary = "Clasifica cada documento subido según las etiquetas de tu equipo y etiqueta sus metadatos." + +[portal.policies.config.classification.rules] +0 = "Clasificar" +1 = "Etiquetar metadatos" + [portal.policies.config.compliance] summary = "Valida documentos frente a marcos normativos antes de que salgan del sistema." @@ -7371,6 +7961,29 @@ minConfidence = "Confianza mínima" 2 = "Nombrar" 3 = "Normalizar" +[portal.policies.config.purview] +label = "Aplicar una etiqueta de sensibilidad de Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Inquilino de Purview" +labelId = "ID de etiqueta" +labelIdHelp = "El GUID de la etiqueta, desde el portal de Purview. Al aplicar una etiqueta, se escriben sus metadatos en el PDF; no puede cifrar, ya que eso requiere el cliente de Microsoft." +labelName = "Nombre de etiqueta (opcional)" +labelNameHelp = "Se registra junto al ID para que las personas y las herramientas puedan reconocer la etiqueta." + +[portal.policies.config.purview.fields.method] +help = "Purview distingue una etiqueta aplicada por una política de una elegida por una persona." +label = "Cómo se registra la etiqueta" +privileged = "Como elegida por una persona" +standard = "Como aplicada automáticamente" + +[portal.policies.config.purviewRead] +label = "Leer una etiqueta de Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Inquilino de Purview" +connectionHelp = "Lee la etiqueta que el documento ya lleva, para que los pasos posteriores puedan actuar sobre ella." + [portal.policies.config.retention] summary = "Aplica cuánto tiempo se conservan los documentos, cuándo archivarlos y cuándo eliminarlos." @@ -7438,6 +8051,7 @@ title = "Aún no hay actividad" [portal.policies.endpoints] addWatermark = "Marca de agua" autoRedact = "Censurar PII" +classifyAndLabel = "Clasificar" compressPdf = "Comprimir" flatten = "Aplanar" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Tus políticas están guardadas y aparecerán cuando se restablez retry = "Reintentar" title = "Backend no disponible" +[portal.policies.operations] +change = "Cambiar lo que hace este paso" +noResults = "Ningún paso coincide con eso. Prueba con un nombre de producto, o \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Buscar pasos: prueba con \"scan\", \"attach\" o un nombre de producto" + +[portal.policies.operations.bodyMode] +binary = "Bytes de archivo sin procesar" +json = "Dentro de una carga útil JSON" +multipart = "Como subida de archivo (multipart)" + +[portal.policies.operations.clamavScan] +description = "Tu propio servidor ClamAV comprueba el documento; nada sale de tu red." +label = "Buscar virus (autoalojado)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rechaza documentos que contengan macros, ejecutables, scripts u objetos incrustados." +label = "Bloquear contenido activo" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive comprueba el documento y la ejecución se detiene si no está limpio." +label = "Buscar virus" + +[portal.policies.operations.confluenceAttach] +description = "Archiva el documento procesado en una página." +label = "Adjuntar a una página de Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Envía el documento a ConsignO Cloud para su firma." +label = "Enviar para firma (ConsignO)" +note = "Envía el documento. La recuperación de la copia firmada aún no es compatible, porque un paso no puede pasar un resultado a un paso posterior." + +[portal.policies.operations.customApiCall] +description = "Crea tú mismo la llamada: ruta, método, cuerpo y gestión de la respuesta." +label = "Llamar a una API personalizada" + +[portal.policies.operations.discordNotify] +description = "Informa a un canal de que la política ha gestionado un documento." +label = "Publicar un mensaje en Discord" + +[portal.policies.operations.elasticIndex] +description = "Registra lo que hizo la política como un documento consultable." +label = "Indexar un evento de auditoría en Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart es adecuado para API de subida; JSON es adecuado para API que quieren el archivo dentro de una carga útil; binario envía los bytes sin procesar." +label = "Cómo enviar el documento" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Un cuerpo JSON enviado tal cual. {{document.base64}} contiene el archivo; {{document.*}} y {{run.*}} se rellenan por documento." +label = "Cuerpo JSON" + +[portal.policies.operations.fields.connection] +label = "Cuenta" + +[portal.policies.operations.fields.domain] +label = "Dominio de Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "El campo del formulario en el que la API espera el documento." +label = "Nombre del campo de archivo" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Un objeto JSON de pares nombre/valor de encabezado. La credencial de la conexión se añade por ti." +label = "Encabezados" + +[portal.policies.operations.fields.index] +label = "Índice" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Clave de incidencia" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Se envía como cuerpo del mensaje. {{document.*}} y {{run.*}} se rellenan por documento." +label = "Mensaje" + +[portal.policies.operations.fields.method] +label = "Método" + +[portal.policies.operations.fields.pageId] +label = "ID de página" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Se añade a la URL base de la conexión. Nunca puede llegar a otro host." +label = "Ruta" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Dónde escribirlo en la unidad. {{document.filename}} se rellena por documento." +label = "Ruta de destino" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Informe deja el documento intacto. Reemplazar lo cambia por lo que se devuelva." +label = "Qué hacer con la respuesta" + +[portal.policies.operations.fields.signerEmail] +label = "Correo electrónico del firmante" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Asunto" +placeholder = "Procesado: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analiza este texto. Usa {{document.title}} o pega el texto que quieras comprobar." +label = "Texto que comprobar" + +[portal.policies.operations.fields.to] +label = "Para" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nombre de usuario de Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informa a un espacio de que la política ha gestionado un documento." +label = "Publicar un mensaje en Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Archiva el documento procesado en una incidencia." +label = "Adjuntar a una incidencia de Jira" + +[portal.policies.operations.mailgunEmail] +description = "Envía el documento procesado como adjunto." +label = "Enviar el documento por correo electrónico (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Escribe el documento procesado en una carpeta." +label = "Subir a Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Tu propio servidor Presidio informa de los datos personales que encuentra." +label = "Detectar datos personales" +note = "Presidio lee texto, no archivos. Proporciónale el texto que quieras comprobar; el documento en sí no se sube." + +[portal.policies.operations.responseMode] +replace = "Reemplazar el documento por la respuesta" +report = "Conservar el documento, registrar la respuesta" + +[portal.policies.operations.sendgridEmail] +description = "Envía el documento procesado como adjunto." +label = "Enviar el documento por correo electrónico (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informa a un canal de que la política ha gestionado un documento." +label = "Publicar un mensaje en Slack" + +[portal.policies.operations.splunkEvent] +description = "Registra lo que hizo la política donde tus auditores ya consultan." +label = "Enviar un evento de auditoría a Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registra lo que hizo la política en un recopilador HTTP." +label = "Enviar un evento de auditoría a Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informa a un canal de que la política ha gestionado un documento." +label = "Publicar un mensaje en Teams" + +[portal.policies.operations.webhookPost] +description = "Envía el propio documento a una URL que elijas." +label = "Publicar el documento en un webhook" + +[portal.policies.operations.zapierNotify] +description = "Entrega los detalles de la ejecución a tu automatización." +label = "Activar un Zap o un escenario de Make" + [portal.policies.stats] activeFor = "Activa" dataProcessed = "Datos procesados" @@ -7473,6 +8282,13 @@ label = "Docs aplicados" description = "Configuradas pero sin activarse" label = "Pausadas" +[portal.policies.table] +appliesTo = "Se aplica a" +docs = "Docs aplicados" +enforces = "Aplica" +policy = "Política" +status = "Estado" + [portal.policies.wizard.actions] back = "Atrás" cancel = "Cancelar" @@ -7480,6 +8296,10 @@ continue = "Continuar" enablePolicy = "Activar política" saveChanges = "Guardar cambios" +[portal.policies.wizard.capability.classify] +desc = "Identifica el tipo del documento a partir de las etiquetas de tu equipo y lo etiqueta, para que se archive y se busque por categoría." +label = "Clasificar el documento" + [portal.policies.wizard.capability.compress] desc = "Comprime el documento para reducir el tamaño del archivo." label = "Reducir tamaño del archivo" @@ -7500,10 +8320,18 @@ label = "Censurar información confidencial" desc = "Elimina JavaScript oculto para que nada pueda ejecutarse automáticamente al abrir el documento." label = "Quitar contenido activo" +[portal.policies.wizard.capability.timestampPdf] +desc = "Demuestra que el documento existía con esta forma exacta en un momento determinado, usando una autoridad de sellado de tiempo independiente. Solo se envía un hash: el documento nunca sale de tu servidor." +label = "Añadir un sello de tiempo de confianza" + [portal.policies.wizard.capability.watermark] desc = "Estampa una marca visible (p. ej., “Confidencial”) en cada página." label = "Aplicar una marca de agua" +[portal.policies.wizard.classification] +description = "Cada documento subido se clasifica según las etiquetas integradas y se etiqueta con los tipos que encajan. El conjunto de etiquetas se comparte en todo tu equipo." +labelsHeading = "Etiquetas de clasificación" + [portal.policies.wizard.errors] noTools = "Activa al menos una herramienta en el flujo de trabajo primero." saveFailed = "No se ha podido guardar la política. Inténtalo de nuevo." @@ -7548,73 +8376,118 @@ setUp = "Configurar política de {{category}}" [portal.policies.wizard.workflow] description = "Elige qué hace esta política con cada documento que procesa." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} activas" -noRule = "Aún no se aplica ninguna regla" -subtitle = "Automatizaciones permanentes por las que pasa cada documento, independientemente de qué pipeline lo gestione." -title = "Qué se ejecuta en tus PDF" - [portal.policySummary.action] -comingSoon = "Actualizar a Enterprise" -configure = "Configurar" setUp = "Configurar" -[portal.policySummary.column] -activeRule = "Regla activa" -policy = "Política" -status = "Estado" +[portal.processorFlow] +footnote = "Los recuentos corresponden a las últimas 24 horas. La velocidad del flujo es ilustrativa." +liveBadge = "En vivo" +stats = "{{connected}} conectados · {{processed}} PDFs procesados" +title = "Procesador PDF" -[portal.policySummary.empty] -description = "Cuando se configuren políticas, las categorías aparecerán aquí." -title = "Aún no hay políticas" +[portal.processorFlow.lens] +ariaLabel = "Modo de vista" +flow = "Flujo" +sankey = "Sankey" -[portal.policySummary.state] -active = "Activa" -off = "Desactivada" -soon = "Próximamente" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Fallidos" +heading = "Resultados" +success = "Entregados" -[portal.processingStatus] -managePlan = "Gestionar plan" -volumeSuffix = "PDF procesados · últimos 30 días" +[portal.processorFlow.policies] +activeCount = "{{n}} activas" +count = "{{n}} · 24 h" +heading = "Políticas" +setUp = "Configurar" +soon = "Pronto" + +[portal.processorFlow.sankey] +waist = "Políticas · {{n}} activas" + +[portal.processorFlow.sankey.empty] +description = "Conecta un origen y activa una política para ver cómo los documentos fluyen por el procesador." +title = "Aún no hay flujo" + +[portal.processorFlow.sources] +comingSoonTag = "Conectar" +editor = "Editor de Stirling PDF" +heading = "Orígenes" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Entrada de correo electrónico" [portal.procurement] -reset = "Restablecer compras (demo)" subtitle = "Evalúa, contrata e incorpora a tu equipo. Cada documento en un solo lugar." title = "Compras" -[portal.procurement.action] -download = "Descargar" -pay = "Pagar ahora" -request = "Solicitar" -sign = "Revisar y firmar" -upload = "Subir" - [portal.procurement.agreement] agreeCta = "Aceptar y suscribirse" +confidential = "Confidencial" confirm = "He leído y acepto el Acuerdo de Stirling Enterprise." -eyebrow = "Acuerdo" -intro = "Un acuerdo combinado cubre tu operación: Acuerdo Marco de Servicios, Formulario de pedido, EULA y Acuerdo de tratamiento de datos. Revísalo y luego acéptalo para convertir el presupuesto en una suscripción comprometida." -title = "Revisa tu acuerdo Enterprise" +docName = "Acuerdo Enterprise de Stirling" +docSub = "MSA, formulario de pedido, EULA y DPA, combinados en una sola firma." +download = "Descargar" +downloadDraftError = "No se ha podido generar el PDF del acuerdo: el renderizador de documentos no está disponible. Inténtalo de nuevo en breve o contacta con soporte." +downloadError = "No se ha podido descargar el acuerdo firmado. Inténtalo de nuevo." +legalName = "Nombre de la entidad legal" +legalNamePlaceholder = "La entidad legal que firmará" +loadError = "No se ha podido cargar el acuerdo. Inténtalo de nuevo." +loading = "Cargando el acuerdo..." +ref = "Ref. {{ref}} · {{version}}" +requestChanges = "Solicitar cambios" +scrollHint = "Desplázate para revisar" +signatory = "Nombre del firmante" +signatoryPlaceholder = "Nombre completo" +signatoryTitle = "Cargo del firmante" +signatoryTitlePlaceholder = "p. ej., asesor jurídico general" +signError = "No se ha podido registrar tu firma. Inténtalo de nuevo." [portal.procurement.builder] addons = "Complementos" +addressLine1 = "Dirección, línea 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Dirección, línea 2" +addressLine2Placeholder = "Suite, planta, edificio" back = "Atrás" businessName = "Nombre de la empresa" businessNamePlaceholder = "Tu empresa" +city = "Ciudad" +cityPlaceholder = "San Francisco" +completeRequired = "Completa los campos obligatorios (marcados con *) con un correo electrónico válido antes de generar el presupuesto." +contactEmail = "Correo electrónico de contacto" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nombre de contacto" +contactNamePlaceholder = "Jane Doe" continue = "Continuar" -country = "País" -countryEuro = "Eurozona (EUR €)" -countryUK = "Reino Unido (GBP £)" -countryUS = "Estados Unidos (USD $)" +done = "Hecho" eula = "He leído y acepto el EULA de Stirling Enterprise. Rige el acuerdo generado a partir de este presupuesto." generate = "Generar presupuesto" -included = "Incluido" indemnification = "Indemnización de IP" indemnificationSub = "Defendemos reclamaciones de IP que cumplan los requisitos, según el EULA" -offlineLicense = "Licencia sin conexión / aislada" -offlineLicenseSub = "Un archivo de licencia descargable para una instancia self-hosted aislada" +paperEyebrow = "Presupuesto Enterprise" +paperFor = "Preparado para" +pdfSize = "Tamaño del PDF" +poNumber = "Número de pedido" +poNumberPlaceholder = "Opcional" +postalCode = "Código postal" +postalCodePlaceholder = "94105" +posture = "Gobernanza" +posture_count = "~{{count}} políticas" +posture_essentials = "Essentials" +posture_essentialsSub = "Clasificación + Uso compartido: los valores predeterminados" +posture_governed = "Governed" +posture_governedSub = "Añade Seguridad y Enrutamiento" +posture_regulated = "Regulated" +posture_regulatedSub = "Todas las categorías: Cumplimiento, Retención, Ingesta" qbr = "Revisiones trimestrales de negocio" qbrSub = "Tu SE revisa el uso y la hoja de ruta cada trimestre" +region = "Estado / región" +regionPlaceholder = "California" running = "{{annual}} / año · {{years}} años {{tcv}}" s1Sub = "Tu equipo y los PDF que esperas procesar cada año." s1Title = "¿Cuánto vas a procesar?" @@ -7623,6 +8496,12 @@ s2Title = "Compromiso y servicio" s3Sub = "Para el presupuesto y el acuerdo que genera." s3Title = "Tus datos" serviceLevel = "Nivel de servicio" +size_compact = "Compacto" +size_compactSub = "Principalmente texto, menos de 1 MB" +size_heavy = "Pesado" +size_heavySub = "Escaneado o con muchas imágenes, 5 MB+" +size_standard = "Estándar" +size_standardSub = "Texto e imágenes mezclados, de 1 a 5 MB" slDedicated = "Dedicado" slDedicatedSub = "4 horas laborables · gestor de cuenta dedicado · +30%" slPriority = "Prioritario" @@ -7630,6 +8509,8 @@ slPrioritySub = "Mismo día laborable · CSM asignado · +15%" slStandard = "Estándar" slStandardSub = "Siguiente día laborable · CSM compartido · incluido" stepOf = "Paso {{n}} de {{total}}" +taxId = "IVA / ID fiscal" +taxIdPlaceholder = "Opcional" term = "Plazo" termDiscount = "Descuento del {{pct}}% por compromiso plurianual aplicado" title = "Crea tu presupuesto" @@ -7637,6 +8518,7 @@ training = "Onboarding y formación" trainingSub = "Sesiones en directo para poner en marcha a tu equipo" users = "Usuarios totales" usersPlaceholder = "p. ej., 250" +viewEula = "Leerlo" volEstimated = "Estimado a partir de {{count}} usuarios (~2000 PDF cada uno, incluida la automatización). Edítalo si tienes un dato mejor." volManual = "Usando tu cifra. Puedes volver a estimarla a partir del tamaño de tu equipo en cualquier momento." volNoUsers = "¿No lo sabes? Introduce el tamaño de tu equipo y lo estimaremos." @@ -7645,55 +8527,55 @@ volumePlaceholder = "p. ej., 1.000.000" years_one = "{{count}} año" years_other = "{{count}} años" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Hecho" -here = "Estás aquí" -hide = "Ocultar" -optional = "Opcional" -paidAddon = "Complemento de pago" -show = "Mostrar" -subtitle = "Todo lo que necesitas en cada paso del recorrido, mostrado a medida que la operación avanza." -supportingSubtitle = "SOC 2, revisiones de seguridad, formularios fiscales y más, listos cuando tu equipo de seguridad o compras los solicite. Algunos tienen una tarifa única." -supportingTitle = "Apoyo a tu evaluación" +[portal.procurement.documents] +agreement = "Acuerdo Enterprise" +agreementSub = "MSA, formulario de pedido y DPA en una sola firma." +download = "Descargar" +eula = "EULA y condiciones comerciales" +eulaSub = "El software y las condiciones comerciales de autoservicio." +invoice = "Factura" +invoiceSub = "La factura de tu primera suscripción." +laterInvoice = "Disponible después de aceptar" +laterQuote = "Disponible una vez emitido tu presupuesto" +quote = "Presupuesto" +quoteSub = "Tu presupuesto Enterprise desglosado." +sla = "Anexo SLA" +slaSub = "Niveles de soporte y objetivos de respuesta." +subprocessors = "Subencargados del tratamiento" +subprocessorsSub = "Terceros que procesan datos en tu nombre." +subtitle = "La documentación de tu acuerdo, disponible en cualquier momento." title = "Documentos" -upcoming = "Próximos" +view = "Ver" [portal.procurement.error] title = "Algo ha ido mal" [portal.procurement.hero] -company = "Tu operación Enterprise" +barAria = "Etapa {{current}} de {{total}}" ctaAgreement = "Revisar y firmar acuerdo" -ctaLive = "Ya estás en directo" -ctaPayment = "Añadir pago" +ctaExploring = "Configura tu prueba" ctaQuote = "Revisar tu presupuesto" +ctaReviewQuote = "Revisar presupuesto" ctaTrial = "Crear tu presupuesto" +documents = "Documentos" eyebrow = "Compras Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invitar a compañeros" -keyDocs = "Documentos clave" -nextStep = "Siguiente paso: {{action}}" -notStarted = "No iniciado" +licenseKey = "Clave de licencia" +liveSub = "Tu organización está aprovisionada y tu licencia está activa." +liveTitle = "Ya estás en vivo en Stirling Enterprise" +next = "Siguiente: {{stage}}" open = "Abrir compras" scheduleCall = "Programar una llamada" -setup1Sub = "Invita a tus compañeros" -setup1Title = "Implementa el Editor de PDF" -setup2Sub = "Activa el procesamiento en editores y otras fuentes" -setup2Title = "Conecta el Procesador de PDF" -setup3Sub = "Activa Seguridad, Cumplimiento, Enrutamiento o Retención cuando los necesites" -setup3Title = "Añadir políticas recomendadas" +sentenceAgreement = "revisar y firmar el acuerdo" +sentenceLive = "aprovisionamiento y despliegue" +sentencePayment = "orden de compra y pago" +sentenceQuote = "revisar y aceptar tu presupuesto" +sentenceTrial = "configurar tu espacio de trabajo de prueba" [portal.procurement.journey] daysLeft_one = "Queda {{count}} día" daysLeft_other = "Quedan {{count}} días" -engineerLabel = "Tu ingeniero de soluciones" -eyebrow = "Tu despliegue" -live = "Ya estás en directo en Stirling Enterprise" -nextStep = "Siguiente paso: {{action}}" -subtitle = "Tu ingeniero de soluciones está en cada paso. Una acción siguiente cada vez; la lista completa está abajo." -title = "De la prueba a la puesta en marcha, un camino guiado" -trialTitle = "Prueba Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Una firma cubre MSA, formulario de pedido, EULA y DPA." @@ -7727,6 +8609,9 @@ downloadError = "Aún no se ha podido generar el archivo de licencia sin conexi downloadOffline = "Descargar licencia sin conexión (.lic)" hint = "Pega esta clave en una instancia self-hosted para activarla, o guárdala para tus registros. Mantenla a salvo." label = "Tu clave de licencia" +subtitle = "Activa una instancia autoalojada con esta clave o guárdala para tus registros." +title = "Tu clave de licencia" +trialFileHint = "Este es tu archivo de licencia de prueba. Una vez que el acuerdo esté en vigor, vuelve y descárgalo de nuevo; a diferencia de la clave de licencia online, el archivo .lic no se actualizará por sí solo." [portal.procurement.link] cta = "Vincular cuenta" @@ -7739,65 +8624,62 @@ description = "Tu suscripción está activa y tu licencia se ha emitido. Tu equi eyebrow = "En directo" title = "Ya estás en directo en Stirling Enterprise" -[portal.procurement.locked] -description = "Las claves de prueba, los presupuestos por volumen comprometido, el acuerdo de una sola firma, el pago y tu libro de documentos estarán aquí cuando inicies una evaluación Enterprise." -eyebrow = "Solo Enterprise" -talkToSales = "Hablar con ventas" -title = "El flujo de compras se abre con Enterprise" - [portal.procurement.milestone] -accept = "Aceptar y continuar" -description = "Descarga el PDF para compartirlo con tu equipo, vuelve para aceptarlo cuando estés listo o haz cambios." -download = "Descargar PDF" downloadError = "Aún no se ha podido descargar el PDF del presupuesto — inténtalo de nuevo en un momento." -edit = "Editar presupuesto" -eyebrow = "Presupuesto {{number}}" -perYear = " / año" -preparedFor = "Preparado para {{company}}" -tcv = "Valor total del contrato {{value}}" -title = "Tu presupuesto está listo" - -[portal.procurement.modal] -cancel = "Cancelar" -chooseFile = "Elegir archivo" -close = "Cerrar" -downloadBody = "Tu descarga comenzará en breve." -downloadCta = "Descargar" -downloadTitle = "Descargar" -noFile = "No se ha seleccionado ningún archivo" -payBody = "Paga tu contrato comprometido con tarjeta o transferencia bancaria a través de Stripe. Tu espacio de trabajo se provisiona en cuanto se confirme el pago." -payCta = "Continuar a Stripe" -payTitle = "Confirmar pago" -requestBodyFree = "Lo generamos bajo demanda. Confirma y tu ingeniero de soluciones lo enviará en breve." -requestBodyPaid = "Este es un complemento de pago. Confirma y tu ingeniero de soluciones lo dimensionará y enviará la documentación." -requestCta = "Solicitar" -requestTitle = "Solicitar este documento" -signBody = "Abre el Acuerdo de Stirling Enterprise para firma electrónica: una firma cubre el MSA, el formulario de pedido, el EULA y el DPA. Lo contrafirmamos automáticamente y avanzas al pago." -signCta = "Abrir para firmar" -signTitle = "Revisa y firma tu acuerdo" -uploadBody = "Envíanos tu PO y facturaremos contra él según tus condiciones. Arrastra el PDF o elige un archivo abajo." -uploadCta = "Subir pedido de compra" -uploadTitle = "Sube tu pedido de compra" [portal.procurement.payment] description = "Tu presupuesto está aceptado y tu licencia ya está activa — tu equipo puede empezar de inmediato. Paga la primera factura cuando estés listo; puedes pagarla o descargarla aquí, sin necesidad de email." +downloadAgreement = "Descargar acuerdo firmado" downloadInvoice = "Descargar factura" -simulate = "Simular pago recibido (demo)" +eyebrow = "Pago" title = "Suscripción creada" viewInvoice = "Ver y pagar factura" +[portal.procurement.review] +acceptCta = "Aceptar presupuesto" +annual = "Cuota anual (año 1)" +downloadCta = "Descargar presupuesto" +poNumber = "Orden de compra: {{po}}" +renewal = "Se renueva a {{amount}}/año después del plazo (+{{pct}}% IPC)" +tcv = "Plazo de {{years}} años, pagado íntegramente por adelantado: {{tcv}}" +validUntil = "Válido hasta {{date}}" + [portal.procurement.schedule] fallback = "No se ha podido cargar el planificador." fallbackLink = "Abrir planificación en una pestaña nueva" +loading = "Cargando planificador..." subtitle = "Tu ingeniero de soluciones guiará a tu equipo durante el despliegue. Elige una hora que te venga bien." title = "Programar una llamada" -[portal.procurement.status] -action = "Acción necesaria" -available = "Disponible" -complete = "Completado" -pending = "Pendiente" -request = "Bajo solicitud" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sellado, sin salida" +back = "Atrás" +businessName = "Nombre de la empresa" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gestionado, inicio más rápido" +continue = "Continuar" +deployment = "¿Dónde debe desplegarse?" +eula = "Acepto el EULA y las condiciones comerciales de Stirling." +fullName = "Nombre completo" +fullNamePlaceholder = "Tu nombre" +invites = "Invitar compañeros · opcional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Programar una llamada" +seats = "Personas que prueban" +seatsPlaceholder = "p. ej., 15" +selfhost = "Autoalojado" +selfhostSub = "Docker o Kubernetes" +start = "Iniciar prueba" +stepOf = "Paso {{n}} de {{total}}" +subtitle = "Gratis durante 14 días, sin tarjeta." +subtitleDetails = "Algunos datos para tu presupuesto y acuerdo." +talkFirst = "¿Prefieres hablar primero?" +title = "Configura tu prueba" +viewEula = "Leerlo" +workEmail = "Correo electrónico de trabajo" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Ampliar añade 7 días y avisa a tu ingeniero de soluciones." @@ -7808,109 +8690,113 @@ maxed = "Agotadas" subtitle = "Tu prueba gratuita se extiende hasta {{date}}. No se requiere tarjeta." title = "Prueba Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Precios por volumen comprometido, SSO + SCIM + RBAC para toda la organización, auditoría inmutable de 90 días y un SE dedicado." -homeCta = "Iniciar prueba →" -homeHeadline = "Procesa millones de PDF." - -[portal.recentActivity] -title = "Actividad reciente" -viewAll = "Ver todo" - -[portal.recentActivity.empty] -description = "Las ejecuciones de pipelines, despliegues y eventos de agentes aparecerán aquí." -title = "Aún no hay nada aquí" - -[portal.search] -ariaLabel = "Buscar" -placeholder = "Buscar en Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Las acciones rápidas aparecerán aquí cuando estén disponibles." -noActionsTitle = "Sin acciones rápidas" -noMatches = "No hay coincidencias para \"{{query}}\"" -noMatchesDescription = "Prueba con otra palabra clave o explora el catálogo." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Vinculación de cuenta" -[portal.shell.header] -accountFallback = "Cuenta" -accountMenu = "Menú de cuenta" -darkMode = "Modo oscuro" -lightMode = "Modo claro" -search = "Buscar" -searchPlaceholder = "Buscar…" -signOut = "Cerrar sesión" -switchToDark = "Cambiar al tema oscuro" -switchToLight = "Cambiar al tema claro" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Procesador" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Vincular cuenta de Stirling" -planEditor = "Plan Editor" -planEnterprise = "Plan Enterprise" -planProcessor = "Plan Processor" primaryNav = "Navegación principal" switchApp = "Cambiar aplicación" +[portal.shell.topbar] +closeNav = "Cerrar navegación" +openNav = "Abrir navegación" +search = "Buscar" + [portal.sources] subtitle = "Conexiones de entrada reutilizables que alimentan documentos en Stirling. Configura una conexión una vez y luego haz referencia a ella desde cualquier número de políticas. Haz clic en una fila para ver su configuración y qué políticas la usan." title = "Fuentes" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Conectar fuente" +[portal.sources.builder] +back = "Volver a orígenes" +backToTypes = "Todos los tipos de origen" +cancel = "Cancelar" +chooseHint = "Elige de dónde proceden los documentos. Los conectores atenuados están en camino." +comingSoon = "Próximamente" +comingSoonHeading = "Próximamente" +create = "Crear origen" +createTitle = "Conectar un origen" +delete = "Eliminar" +editTitle = "Editar origen" +enabled = "Habilitado" +save = "Guardar cambios" + +[portal.sources.builder.folderAccess] +description = "Las automatizaciones de carpetas solo pueden usar carpetas que un administrador haya permitido. Añádela en la configuración de Acceso a carpetas y vuelve a intentarlo." +openSettings = "Configuración de Acceso a carpetas" +title = "Esta carpeta no está permitida" + [portal.sources.delete] body = "¿Eliminar \"{{name}}\"? Esto no se puede deshacer. Las políticas que la referencian tendrían que actualizarse." cancel = "Cancelar" confirm = "Eliminar" title = "¿Eliminar fuente?" -[portal.sources.detail] -closeAriaLabel = "Cerrar detalle" -delete = "Eliminar fuente" -docs24h = "Últimas 24 h" -docs30d = "Últimos 30 días" -docsTotal = "Total vistos" -docsTrend = "Documentos durante los últimos 30 días" -documents = "Documentos" -edit = "Editar" -notReferenced = "No está referenciada por ninguna política, por lo que es seguro eliminarla." -pause = "Pausar" -resume = "Reanudar" -subtitle = "{{type}} · {{status}}" -usedBy = "Usada por" - -[portal.sources.empty] -description = "Conecta una carpeta (y, pronto, almacenamiento en la nube) para que tus políticas tengan de dónde extraer documentos." -title = "Aún no hay fuentes conectadas" - [portal.sources.kpi] inUse = "En uso" total = "Conexiones" unused = "Sin usar" +[portal.sources.networkFields.connection] +helperText = "La conexión almacenada con la dirección del servidor y las credenciales. La reutiliza cada origen que la referencia." +label = "Conexión" + +[portal.sources.networkFields.directory] +helperText = "Carpeta en el servidor que sondear, relativa al inicio de sesión o a la raíz del recurso compartido. Déjala en blanco para la raíz." +label = "Carpeta" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consumir elimina cada archivo del servidor una vez que cada política lo ha procesado." +label = "Modo de lectura" + +[portal.sources.networkFields.mode.options] +consume = "Consumir: procesar cada archivo una vez" +snapshot = "Instantánea: volver a leer la carpeta en cada ejecución" + +[portal.sources.networkFields.recursive] +label = "Profundidad de carpeta" + +[portal.sources.networkFields.recursive.options] +all = "Incluir subcarpetas" +top = "Solo nivel superior" + [portal.sources.status] active = "Activa" disabled = "Desactivada" unused = "Sin usar" [portal.sources.table] +documents = "Documentos" source = "Fuente" status = "Estado" +type = "Type" usedBy = "Políticas" +[portal.sources.types.box] +description = "Extrae documentos de carpetas de Box que tu equipo ya comparte." +label = "Box" + +[portal.sources.types.dropbox] +description = "Extrae documentos de una carpeta compartida de Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documentos que tu equipo ha procesado en el editor, en ejecuciones de políticas e IA." label = "Editor" +noPolicies = "Aún no se ha ejecutado ninguna política desde el editor." + +[portal.sources.types.email] +description = "Convierte los adjuntos enviados a un buzón dedicado en documentos." +label = "Bandeja de entrada de correo electrónico" [portal.sources.types.folder] description = "Vigila un directorio en el servidor para detectar documentos nuevos." @@ -7943,25 +8829,80 @@ label = "Profundidad de carpeta" all = "Incluir subcarpetas" top = "Solo nivel superior" +[portal.sources.types.ftp] +description = "Sondea una carpeta de servidor FTP o FTPS en busca de documentos nuevos." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Extrae documentos de carpetas compartidas de Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Vigila un recurso compartido de red SMB en busca de documentos nuevos." +label = "Unidad de red" + +[portal.sources.types.onedrive] +description = "Vigila carpetas de OneDrive en busca de documentos nuevos." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Extrae documentos de un bucket de Amazon S3 o compatible con S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "La conexión almacenada que contiene el bucket y las credenciales. La reutilizan todos los orígenes y salidas de canalización que la referencian." +label = "Conexión" + +[portal.sources.types.s3.fields.mode] +helperText = "Consumir elimina cada objeto del bucket una vez que cada política lo ha procesado." +label = "Modo de lectura" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consumir: procesar cada objeto una vez" +snapshot = "Instantánea: volver a leer el bucket en cada ejecución" + +[portal.sources.types.s3.fields.prefix] +helperText = "Solo se procesan los objetos cuyas claves empiezan por este prefijo." +label = "Prefijo de clave" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Sondea una carpeta de entrega SFTP en busca de documentos nuevos." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Extrae documentos de bibliotecas de documentos de SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Fuente" +[portal.sources.types.webhook] +createNote = "Se generan una URL de entrega y un secreto de firma al crear este webhook. Los remitentes hacen POST de documentos a esa URL y las políticas que lo referencian se ejecutan al llegar." +description = "Recibe documentos mediante HTTP POST firmado desde cualquier sistema externo." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de entrega" +secretNote = "El secreto de firma se muestra una vez, al crear el webhook. Vuelve a crear el origen para rotarlo." + +[portal.sources.types.webhook.reveal] +copy = "Copiar" +done = "Hecho" +secret = "Secreto de firma" +secretHelp = "Firma el cuerpo sin procesar de cada entrega con esta clave (HMAC-SHA256) y envíalo como encabezado X-Stirling-Signature." +secretWarning = "Copia ahora el secreto de firma. Por tu seguridad, se muestra solo una vez y no se puede recuperar más tarde." +title = "Webhook creado" +url = "URL de entrega" +usage = "Haz POST de cada documento como cuerpo de solicitud sin procesar a la URL de entrega con un tipo de contenido binario (application/pdf o application/octet-stream). Las políticas que lo referencian se ejecutan automáticamente al llegar." + [portal.sources.wizard] -back = "Atrás" -cancel = "Cancelar" -continue = "Continuar" -editTitle = "Editar fuente" name = "Nombre" namePlaceholder = "p. ej., Entrada de reclamaciones" -save = "Guardar cambios" -subtitle = "Paso {{current}} de {{total}} · {{label}}" -title = "Conectar una fuente" type = "Tipo" -[portal.sources.wizard.steps] -chooseType = "Elegir tipo" -configure = "Configurar" -review = "Revisar y conectar" +[portal.stepModal] +close = "Cerrar" [portal.usage] managePayment = "Gestionar pago" @@ -8021,16 +8962,9 @@ summary = "Posee un equipo — gestiona los recursos de sus miembros y las confi 2 = "Acceso al portal mediante la política predeterminada" 3 = "Todo lo que puede hacer un miembro" -[portal.welcome] -ariaLabel = "Bienvenido a Stirling PDF" -badge = "Open-source" -installEditor = "Instalar el Editor" -inviteTeammates = "Invitar a compañeros" -openInBrowser = "Abrir en el navegador" -perks = "Gratis para siempre · Self-hostable" -subtitle = "El Editor de PDF más seguro del mundo es gratis para equipos de todos los tamaños. Incluye más de 60 operaciones PDF y SSO." -title = "Bienvenido a" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Ilimitado" [printFile] title = "Imprimir archivo" @@ -8613,6 +9547,14 @@ text = "Rote las páginas de su PDF en sentido horario o antihorario en incremen [rotate.tooltip.header] title = "Resumen de Configuración de Rotación" +[ruler] +clearAll = "Borrar todo" +hideAllLabels = "Ocultar todas las etiquetas" +hideSmallLabels = "Ocultar etiquetas pequeñas" +physicalValues = "Físico" +scaled = "Escalado" +showAllLabels = "Mostrar todas las etiquetas" + [sanitize] desc = "Eliminar elementos potencialmente dañinos de archivos PDF." sanitizationResults = "Resultados de Desinfección" @@ -8654,31 +9596,45 @@ label = "Eliminar metadatos XMP" [sanitize.steps] settings = "Configuración" -[sanitizePdf] -tags = "limpiar,asegurar,seguro,quitar amenazas" - [sanitizePDF] title = "Limpiar archivo PDF" +[sanitizePdf] +tags = "limpiar,asegurar,seguro,quitar amenazas" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Ajustar escala de la página" +[scaleSettings] +activeScale = "Escala activa" +apply = "Aplicar escala" +applyCalibration = "Aplicar calibración" +calculatedScale = "Escala calculada" +calibrate = "Calibrar" +calibrating = "Calibrando" +calibrationDistanceRequired = "Introduce una distancia real mayor que cero" +calibrationInvalid = "No se puede calcular la escala a partir de esta medida" +calibrationPaperDistance = "Distancia medida en la página: {{distance}}" +calibrationTitle = "Calibrar escala" +calibrationTooltip = "Mide una distancia conocida para calcular la escala automáticamente" +customScale = "Escala personalizada" +noneSet = "No se ha establecido ninguna escala personalizada" +presets = "Escalas predefinidas" +ratio = "Relación de escala" +ratioHelp = "Relación: 1 unidad de página = X unidades reales" +ratioPlaceholder = "p. ej., 100" +ratioPositive = "La relación debe ser mayor que cero" +ratioRequired = "La relación es obligatoria" +realDistance = "Distancia real" +realDistancePlaceholder = "p. ej., 5" +reset = "Restablecer valores predeterminados" +unit = "Unidad" +unitInvalid = "Unidad no válida: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Umbral de ángulo:" -10 = "Establece el tamaño del borde agregado y eliminado para evitar bordes blancos en la salida (predeterminado: 1)." -2 = "Establecer el ángulo absoluto mínimo requerido para rotar la imagen (predeterminado: 10)." -3 = "Tolerancia:" -4 = "Determinar el rango de variación de color alrededor del color de fondo estimado (predeterminado: 30)." -5 = "Área mínima:" -6 = "Establecer el umbral mínimo de área para una foto (predeterminado: 10000)." -7 = "Área mínima de contorno:" -8 = "Establecer el umbral mínimo del área de contorno para una foto" -9 = "Tamaño del borde:" - [scannerImageSplit] submit = "Extraer Escaneos de Imágenes" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Qué hace esto" whatThisDoesDesc = "Encuentra y extrae automáticamente cada foto de una página escaneada o imagen compuesta, sin recorte manual." whenToUse = "Cuándo usar" +[ScannerImageSplit.selectText] +1 = "Umbral de ángulo:" +10 = "Establece el tamaño del borde agregado y eliminado para evitar bordes blancos en la salida (predeterminado: 1)." +2 = "Establecer el ángulo absoluto mínimo requerido para rotar la imagen (predeterminado: 10)." +3 = "Tolerancia:" +4 = "Determinar el rango de variación de color alrededor del color de fondo estimado (predeterminado: 30)." +5 = "Área mínima:" +6 = "Establecer el umbral mínimo de área para una foto (predeterminado: 10000)." +7 = "Área mínima de contorno:" +8 = "Establecer el umbral mínimo del área de contorno para una foto" +9 = "Tamaño del borde:" + [search] noResults = "No se encontraron resultados" placeholder = "Ingrese término de búsqueda..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Tu servidor Stirling-PDF está sin conexión y \"{{en [session] expired = "Su sesión ha caducado. Actualice la página e inténtelo de nuevo." -[sessionManagement.tooltip] -header = "Gestión de sesiones de firma" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Los nuevos participantes se añaden al final del orden de firma" -bullet2 = "No se pueden añadir participantes después de finalizar la sesión" -bullet3 = "Cada participante recibe una notificación cuando sea su turno" -description = "Puedes añadir más participantes a una sesión activa en cualquier momento antes de la finalización." -title = "Añadir participantes" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalización completa: Todos los participantes han firmado" -bullet2 = "Finalización parcial: Algunos participantes aún no han firmado" -bullet3 = "Los participantes sin firmar serán excluidos del documento final" -bullet4 = "Una vez finalizada, puedes cargar el PDF firmado en archivos activos" -description = "La finalización combina todas las firmas en un único PDF firmado. Esta acción no se puede deshacer." -title = "Finalización de la sesión" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "No se pueden eliminar participantes que ya hayan firmado" -bullet2 = "Los participantes eliminados ya no recibirán notificaciones" -bullet3 = "El orden de firma se ajusta automáticamente" -description = "Se puede eliminar a los participantes de las sesiones antes de que firmen." -title = "Eliminar participantes" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Cada firma se aplica secuencialmente al PDF" -bullet2 = "Los firmantes posteriores pueden ver las firmas anteriores" -bullet3 = "Crucial para flujos de aprobación y cadenas de custodia legales" -description = "El orden que especifiques al crear la sesión determina quién firma primero." -title = "Orden de firmas" - [settings] close = "Cerrar" +[settings.ai] +documents = "Documentos y RAG" +general = "General" +limits = "Límites y rendimiento" +models = "Modelos y proveedores" +title = "IA" + [settings.configuration] advanced = "Avanzado" database = "Base de datos" endpoints = "Endpoints" features = "Funciones" +folderAccess = "Acceso a carpetas" mcp = "MCP Server" storageSharing = "Almacenamiento y uso compartido de archivos" systemSettings = "Ajustes del sistema" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Editor de PDF predeterminado" defaultPdfEditorActive = "Stirling PDF es su editor de PDF predeterminado" defaultPdfEditorChecking = "Comprobando..." defaultPdfEditorInactive = "Otra aplicación está establecida como predeterminada" +defaultPdfEditorRemind = "Recordarme que lo establezca como predeterminado" +defaultPdfEditorRemindDescription = "Mostrar un banner cuando Stirling PDF no sea tu aplicación PDF predeterminada." defaultPdfEditorSet = "Ya es predeterminado" defaultStartupView = "Vista predeterminada al iniciar" defaultStartupViewDescription = "Elija qué pestaña estará activa en la columna izquierda cuando se inicie la aplicación" @@ -8817,9 +9763,18 @@ languageDescription = "Elige el idioma de la interfaz" logout = "Cerrar sesión" setAsDefault = "Establecer como predeterminado" theme = "Tema" +themeAccent = "Color de acento" +themeAccentCustom = "Color personalizado" +themeAccentCustomHint = "Personalizado (se recomiendan los valores predeterminados)" +themeAccentDark = "Color de acento del modo oscuro" +themeAccentDefault = "Predeterminado" +themeAccentDefaultHint = "Tema predeterminado (recomendado)" +themeAccentDescription = "Los botones, enlaces y resaltados lo siguen, y tiñe sutilmente la aplicación. Los modos claro y oscuro tienen cada uno el suyo. El sistema utiliza el que esté activo." +themeAccentLight = "Color de acento del modo claro" themeDark = "Oscuro" themeDescription = "Cambia entre el modo claro y oscuro" themeLight = "Claro" +themeReset = "Restaurar tema predeterminado" themeSystem = "System" title = "General" user = "Usuario" @@ -8833,6 +9788,12 @@ intro = "Activa la autenticación de usuarios, la gestión de equipos y las func learnMore = "Más información en la documentación" title = "Para administradores del sistema" +[settings.general.loginLanding] +description = "Elige qué aplicación se abre cuando inicias sesión en Stirling Cloud." +editor = "Editor" +processor = "Procesador" +title = "Después de iniciar sesión" + [settings.general.mode] fullscreen = "Pantalla completa" sidebar = "Barra lateral" @@ -9013,9 +9974,6 @@ title = "Políticas y privacidad" [settings.preferences] title = "Preferencias" -[settings.search] -placeholder = "Buscar en las páginas de ajustes..." - [settings.security] description = "Actualiza tu contraseña para mantener tu cuenta segura." title = "Seguridad" @@ -9041,6 +9999,11 @@ security = "Seguridad" telegram = "Telegram" title = "Seguridad y autenticación" +[settings.signOut] +confirm = "¿Seguro que quieres cerrar sesión?" +submit = "Cerrar sesión" +title = "Cerrar sesión" + [settings.team] title = "Equipo" @@ -9156,6 +10119,7 @@ last = "Última página" maintainRatio = "Activar/desactivar la relación de aspecto" next = "Siguiente página" noSavedSigs = "No se encontraron firmas guardadas" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Firmas Personales" previous = "Página anterior" redo = "Rehacer" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Error al eliminar el fondo" hint = "Cargue una imagen PNG o JPG de su firma" label = "Cargar imagen de firma" placeholder = "Seleccionar archivo de imagen" +previewAlt = "Current image signature" processing = "Procesando imagen..." removeBackground = "Eliminar fondo blanco (hacer transparente)" @@ -9207,6 +10172,17 @@ saved = "Seleccione una firma guardada arriba y haga clic en cualquier parte del text = "Tras introducir tu nombre arriba, haz clic en cualquier parte del PDF para colocar tu firma." title = "Cómo añadir firma" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Mover firma" pause = "Pausar colocación" @@ -9653,10 +10629,30 @@ title = "Subir al servidor" updateButton = "Actualizar en el servidor" uploadButton = "Subir al servidor" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "haz clic aquí" +fallback = "Si no se ha abierto automáticamente," +opening = "Abriendo Swagger UI en una nueva pestaña..." tags = "api,documentación,swagger,endpoints,desarrollo" title = "Documentación de API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Seleccione archivos en Archivos activos para ejecutar esta he singleFileScope = "Solo se aplica a: {{fileName}}" viewerMode = "Cambie al editor de archivos para seleccionar varios archivos." +[toolFormat] +ANY = "cualquier archivo" +CBR = "un archivo de cómic (CBR)" +CBZ = "un archivo de cómic (CBZ)" +CSV = "un archivo CSV" +EBOOK = "un libro electrónico" +EMAIL = "un correo electrónico" +EXCEL = "una hoja de cálculo" +HTML = "una página web" +IMAGE = "imágenes" +JAVASCRIPT = "un archivo JavaScript" +JSON = "un archivo JSON" +MARKDOWN = "un archivo Markdown" +NONE = "ningún archivo" +PCL = "un archivo de impresión PCL" +PDF = "un PDF" +PDF_ENCRYPTED = "un PDF protegido con contraseña" +POSTSCRIPT = "un archivo PostScript" +PPT = "una presentación" +TEXT = "un archivo de texto" +VIDEO = "un vídeo" +WORD = "un documento Word" +XML = "un archivo XML" +XPS = "un documento XPS" +ZIP = "un archivo ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Volver a todas las herramientas" collapse = "Contraer panel" expand = "Expandir panel" goBack = "Volver" +pdfTools = "Herramientas PDF" placeholder = "Elige una herramienta para empezar" premiumFeature = "Función Premium:" toolsHeader = "Herramientas" @@ -9806,11 +10829,6 @@ noToolsFound = "No se encontraron herramientas" quickAccess = "ACCESO RÁPIDO" searchPlaceholder = "Buscar herramientas..." -[toolPicker.categories] -advancedTools = "Herramientas Avanzadas" -recommendedTools = "Herramientas Recomendadas" -standardTools = "Herramientas Estándar" - [toolPicker.subcategories] advancedFormatting = "Formato Avanzado" automation = "Automatización" @@ -9824,6 +10842,9 @@ removal = "Eliminación" signing = "Firma" verification = "Verificación" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "No se encontraron herramientas" noTools = "No hay herramientas disponibles" @@ -9950,28 +10971,53 @@ title = "Usuarios" you = "(tú)" [users.action] +cancelInvite = "Cancelar invitación" deleteTeam = "Eliminar equipo" disableMfa = "Restablecer MFA" move = "Mover a equipo" reinstate = "Readmitir" remove = "Eliminar de la org" +removeTeam = "Quitar del equipo" rename = "Cambiar nombre del equipo" resetPw = "Restablecer contraseña" suspend = "Suspender" unlock = "Desbloquear cuenta" +[users.activity] +daysAgo = "hace {{count}} d" +hoursAgo = "hace {{count}} h" +justNow = "Ahora mismo" +minutesAgo = "hace {{count}} min" +monthsAgo_one = "hace {{count}} mes" +monthsAgo_other = "hace {{count}} meses" +never = "Nunca" +weeksAgo_one = "hace {{count}} semana" +weeksAgo_other = "hace {{count}} semanas" +yearsAgo_one = "hace {{count}} año" +yearsAgo_other = "hace {{count}} años" + [users.cap] addProcessor = "+ Processor" approver = "Aprueba política" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "¿Cancelar la invitación a {{email}}? No podrá unirse con el enlace actual." +cancelInviteTitle = "Cancelar invitación" deleteTeamBody = "¿Eliminar el equipo {{name}}? El equipo debe estar vacío primero: mueve sus miembros a otro equipo y no puede seguir siendo propietario de ninguna configuración de integración." deleteTeamTitle = "Eliminar equipo" disableMfaBody = "¿Eliminar la inscripción MFA de {{name}}? La configurará de nuevo en el próximo inicio de sesión si es necesario." disableMfaTitle = "Restablecer MFA" removeBody = "¿Eliminar permanentemente a {{name}} de la organización? Esto no se puede deshacer." +removeTeamBody = "¿Quitar a {{name}} del equipo? Mantendrá su cuenta, pero perderá el acceso a los recursos de este equipo." removeTitle = "Eliminar miembro" [users.empty] @@ -9982,10 +11028,8 @@ title = "Aún no hay miembros" addToTeam = "Añadir al equipo" guestCount = "{{count}} invitado" guests = "Invitados" -guestsDesc = "Colaboradores externos, limitados a lo que hayas compartido. Solo Editor." ledBy = "dirigido por {{owner}}" org = "Organización" -orgDesc = "Propietarios con autoridad en toda la org y aprobación de políticas" owners = "{{count}} propietario" team = "Equipo {{name}}" teamMeta = "{{count}} personas" @@ -10022,6 +11066,18 @@ username = "Nombre de usuario" usernameError = "El nombre de usuario debe tener al menos 3 caracteres" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invitado por {{who}}" +cancel = "Cancelar" +expiresInDays_one = "Caduca en {{count}} día" +expiresInDays_other = "Caduca en {{count}} días" +expiresToday = "Caduca hoy" +title = "Invitaciones pendientes" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Algo ha fallado al contactar con el backend, o no tienes acceso. Inténtalo de nuevo." title = "No se han podido cargar los miembros" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Buscar adjuntos" title = "Adjuntos" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Título del marcador" +bookmarkTitleRequired = "El título del marcador es obligatorio" closeSidebar = "Cerrar barra lateral de marcadores" collapseAll = "Contraer todos los marcadores" +empty = "No bookmarks in this document" expandAll = "Expandir todos los marcadores" +searchPlaceholder = "Buscar marcadores" +title = "Bookmarks" [viewer.comments] addComment = "Añadir comentario" @@ -10236,10 +11297,12 @@ locateAnnotation = "Localizar en el documento" moreActions = "Más acciones" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Página {{page}}" placingHint = "Haz clic en una página para colocar… (cancelar)" removeCommentOnly = "Quitar solo el comentario" saveReply = "Guardar respuesta" +searchPlaceholder = "Search comments" title = "Comentarios" typeComment = "Comentario" typeInsertText = "Insertar texto" @@ -10262,6 +11325,7 @@ unsavedDesc = "Tienes cambios sin guardar" closeSidebar = "Cerrar barra lateral de capas" hideAll = "Ocultar todas las capas" showAll = "Mostrar todas las capas" +title = "Layers" [viewer.link] delete = "Eliminar enlace" @@ -10605,30 +11669,6 @@ title = "Texto de Marca de Agua" image = "Imagen" text = "Texto" -[wetSignature.tooltip] -header = "Métodos de creación de firma" - -[wetSignature.tooltip.draw] -bullet1 = "Personaliza el color y el grosor del trazo" -bullet2 = "Borra y vuelve a dibujar hasta que quedes conforme" -bullet3 = "Funciona en dispositivos táctiles (tabletas, teléfonos)" -description = "Crea una firma manuscrita con el ratón o la pantalla táctil. Ideal para firmas personales y auténticas." -title = "Dibujar firma" - -[wetSignature.tooltip.type] -bullet1 = "Elige entre varias tipografías" -bullet2 = "Personaliza el tamaño y el color del texto" -bullet3 = "Perfecto para firmas estandarizadas" -description = "Genera una firma a partir de texto escrito. Rápida y coherente, adecuada para documentos empresariales." -title = "Escribir firma" - -[wetSignature.tooltip.upload] -bullet1 = "Compatible con PNG, JPG y otros formatos de imagen" -bullet2 = "Se recomiendan fondos transparentes para mejores resultados" -bullet3 = "La imagen se ajustará para encajar en el área de la firma" -description = "Sube una imagen de firma ya creada. Ideal si tienes una firma escaneada o el logotipo de la empresa." -title = "Subir imagen de la firma" - [workbenchBar] activeFiles = "Archivos activos" annotations = "Anotaciones" @@ -10642,6 +11682,7 @@ exitRedaction = "Salir del modo de redacción" exportAll = "Exportar PDF" exportSelected = "Exportar páginas seleccionadas" formFill = "Rellenar formulario" +hideToolbar = "Hide toolbar" multiTool = "Multiherramienta" panMode = "Modo desplazar" print = "Imprimir PDF" @@ -10652,6 +11693,7 @@ redact = "Redactar" rotateLeft = "Girar a la izquierda" rotateRight = "Girar a la derecha" ruler = "Regla / Medir" +rulerSettings = "Ajustes de escala" save = "Guardar" saveAll = "Guardar todo" saveAs = "Guardar como" @@ -10661,6 +11703,7 @@ selectAll = "Seleccionar todo" selectByNumber = "Seleccionar por números de página" selectLanguage = "Seleccionar idioma" share = "Compartir" +showToolbar = "Show toolbar" toggleAnnotations = "Alternar visibilidad de las anotaciones" toggleAttachments = "Alternar adjuntos" toggleBookmarks = "Alternar marcadores" diff --git a/frontend/editor/public/locales/eu-ES/translation.toml b/frontend/editor/public/locales/eu-ES/translation.toml index 704f1fb43c..4b41940aae 100644 --- a/frontend/editor/public/locales/eu-ES/translation.toml +++ b/frontend/editor/public/locales/eu-ES/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabetoa" apply = "Aplikatu" applyAndContinue = "Gorde eta irten" -areYouSure = "Ziur zaude irten nahi duzula?" back = "Atzera" black = "Beltza" blue = "Urdina" @@ -46,10 +45,8 @@ filesSelected = "Hautatutako fitxategiak" font = "Letra-tipoa" fontSizeTooltip = "Orri-zenbakiaren testuaren tamaina puntutan. Zenbaki handiagoek testu handiagoa sortzen dute." fontTypeTooltip = "Orri-zenbakientzako letra-familia. Aukeratu zure dokumentuaren estiloaren arabera." -genericSubmit = "Bidali" green = "Berdea" help = "Laguntza" -imgPrompt = "Hautatu Irudia(k)" incorrectPasswordMessage = "Uneko pasahitza ez da zuzena." info = "Info" invalidUndoData = "Ezin da desegin: eragiketaren datuak baliogabeak dira" @@ -62,7 +59,6 @@ loginAgreementTitle = "Saio-hasierako hitzarmena" logOut = "Saioa itxi" marginTooltip = "Orri-zenbakiaren eta orrialdearen ertzen arteko distantzia." moreOptions = "Aukera gehiago" -multiPdfPrompt = "Hautatu PDFak (2+)" never = "Inoiz ez" no = "Ez" noFileSelected = "Ez da fitxategirik hautatu. Mesedez, igo bat." @@ -80,7 +76,6 @@ pageOrderPrompt = "Orrialdeen ordena (sartu komaz bereizitako orrialde-zenbakien pages = "Orrialdeak" pageSelectionPrompt = "Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1) :" password = "Pasahitza" -pdfPrompt = "Hautatu PDFa(k)" pin = "Ainguratu fitxategia (aktibo mantendu tresna exekutatu ondoren)" pro = "Pro" processingComplete = "Zure fitxategia prest dago." @@ -110,6 +105,7 @@ undoSuccess = "Eragiketa ongi desegin da" unknown = "Ezezaguna" unpin = "Kendu aingura fitxategiari (ordeztu tresna exekutatu ondoren)" unsavedChanges = "Gorde gabeko aldaketak dituzu zure PDFan." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Gorde gabeko aldaketak" unsupported = "Ez da onartzen" username = "Erabiltzaile izena" @@ -356,6 +352,7 @@ text = "Baimen hauek erabiltzaileek PDFarekin egin dezaketena kontrolatzen dute. title = "Aldatu baimenak" [addStamp] +preview = "Zigiluaren aurrebista" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Deskarga manualak: egin klik esteketan eta jarri fitxategiak tess noLanguages = "Ez da tessdata hizkuntzarik aurkitu konfiguratutako direktorioan." permissionNotice = "tessdata bidea ez da idazgarria. Deskargak nabigatzailean irekiko dira; gorde .traineddata fitxategiak eskuz tessdata direktorioan." +[admin.settings.ai.documents] +description = "Konfiguratu dokumentuei buruzko galderei erantzuteko erabiltzen diren embedding eredua eta berreskuratze-ezarpenak. Gordetzean AI motorrari aplikatzen zaio." +title = "Dokumentuak eta RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Utzi hutsik motorraren inguruneko kredentziala erabiltzeko. Motor bakarreko self-hosted inplementazioei aplikatzen zaie." +generic = "Embedding API gakoa" +openai = "OpenAI API gakoa" +setPlaceholder = "Gordeta - utzi hutsik uneko gakoa mantentzeko" +voyageai = "VoyageAI API gakoa" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI-rekin bateragarria den / Ollama embeddings endpoint-aren oinarrizko URL, adib. http://ollama:11434/v1. Fidagarria den barne-endpoint batera apuntatu behar du (SSRFarekiko sentikorra)." +label = "Embedding oinarrizko URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding ereduaren izena. Testu librea; iradokizunak pistak baino ez dira." +label = "Embedding eredua" + +[admin.settings.ai.documents.embeddingProvider] +description = "Dokumentuaren testua bektore-embedding bihurtzeko erabiltzen den hornitzailea." +label = "Embedding hornitzailea" + +[admin.settings.ai.documents.maxSearches] +description = "Agenteak eskaera bakoitzeko exekuta ditzakeen berreskuratze-bilaketen gehieneko kopurua." +label = "Gehieneko bilaketak" + +[admin.settings.ai.documents.reindexNote] +body = "Embedding eredua aldatzeak berehala du eragina, baina aurreko ereduarekin indexatutako dokumentuak berriro indexatu behar dira bilaketak emaitza zuzenak itzul ditzan." +title = "Berrindexatzea beharrezkoa da" + +[admin.settings.ai.documents.saved] +reindexNote = "Embedding eredua aldatu baduzu, berrindexatu dauden dokumentuak bilaketak eredu berria erabil dezan." + +[admin.settings.ai.documents.topK] +description = "Bilaketa bakoitzean berreskuratzen diren zatirik garrantzitsuenen kopurua." +label = "Top K" + +[admin.settings.ai.general] +description = "Konektatu Stirling Python AI motorrarekin eta aukeratu zein AI gaitasun erakutsiko diren. Aldaketak berrabiaraztean aplikatzen dira." +title = "AI motorra" + +[admin.settings.ai.general.capabilities] +description = "Aktibatu edo desaktibatu AI eginbide bakoitza. Desaktibatutako eginbideak aplikazioan ezkutatzen dira." +title = "Gaitasunak" + +[admin.settings.ai.general.enabled] +description = "Etengailu nagusia. Desaktibatuta dagoenean, ez dago AI tresnarik, agenterik edo motor-deirik erabilgarri." +label = "Gaitu AI" + +[admin.settings.ai.general.features.chat] +description = "PDFekin lan egiteko elkarrizketa-laguntzailea." +label = "Txat-laguntzailea" + +[admin.settings.ai.general.features.classify] +description = "Sailkatu dokumentuak automatikoki motaren edo edukiaren arabera." +label = "Dokumentuen sailkapena" + +[admin.settings.ai.general.features.createPdf] +description = "Sortu PDF berri bat (adib. HTMLtik) AI agente baten bidez." +label = "Sortu PDF gonbitetik" + +[admin.settings.ai.general.features.documentQuestions] +description = "Egin galderak eta jaso igotako dokumentu batean oinarritutako erantzunak." +label = "Dokumentu-galderak" + +[admin.settings.ai.general.features.mathAuditor] +description = "Berrikusi dokumentuak akats matematiko eta numerikoak aurkitzeko." +label = "Matematika-auditorea" + +[admin.settings.ai.general.features.pdfComment] +description = "Gehitu AI-k idatzitako berrikuspen-iruzkinak eta oharrak PDF bati." +label = "PDF iruzkin-agentea" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Agente-eragiketa astunagoetarako denbora-muga, hala nola dokumentuen sorkuntza." +label = "Iraupen luzeko denbora-muga (segundoak)" + +[admin.settings.ai.general.note] +body = "AI motorra aparteko zerbitzu gisa exekutatzen da. Bere sekretu partekatua" +body2 = "edukiontziaren ingurune-aldagai baten bidez ezartzen da. Hornitzaileen API gakoak orri hauetan sar daitezke edo motorraren ingurune-aldagai gisa eman; hemen sartutako gakoak motorra bidaltzen dira gordetzean." +title = "AI motorrari buruz" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Streaming bidezko (tokenetik tokenera) txat-erantzunetarako denbora-muga." +label = "Stream denbora-muga (segundoak)" + +[admin.settings.ai.general.test] +button = "Probatu konexioa" +failBody = "AI motorrak ez du erantzun. Egiaztatu URL, motorraren edukiontzia martxan dagoela eta AI gaituta dagoela (gaitu ondoren berrabiaraztea behar da)." +failTitle = "AI motorra ezin da atzitu" +okBody = "AI motorrak osasun-egiaztapen bati erantzun dio." +okTitle = "AI motorra atzigarria da" + +[admin.settings.ai.general.timeoutSeconds] +description = "Motorra bidaltzen diren AI eskaera estandarretarako denbora-muga." +label = "Eskaeraren denbora-muga (segundoak)" + +[admin.settings.ai.general.url] +description = "Python AI motorraren barne-URL, adib. http://stirling-pdf-engine:5001." +label = "AI motorraren URL" + +[admin.settings.ai.limits] +description = "AI eskaerek zenbat lan egin dezaketen eta zenbat exekuta daitezkeen aldi berean mugatzeko babesak. Gordetzean AI motorrari aplikatzen zaio." +title = "Mugak eta errendimendua" + +[admin.settings.ai.limits.maxCharacters] +description = "Babesa: baztertu erauzitako testuak luzera hau gainditzen duten AI eskaerak." +label = "Gehieneko karaktereak eskaera bakoitzeko" + +[admin.settings.ai.limits.maxPages] +description = "Babesa: baztertu PDF orri kopuru hau gainditzen duten AI eskaerak." +label = "Gehieneko orriak eskaera bakoitzeko" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Motor osoan aldi berean martxan egon daitezkeen eredu-deien gehieneko kopurua." +label = "Ereduaren gehieneko aldiberekotasuna" + +[admin.settings.ai.models] +description = "Aukeratu AI motorrak erabiltzen dituen LLM hornitzailea eta eredu adimentsu/azkarrak. Gordetzean AI motorrari aplikatzen zaio." +title = "Ereduak eta hornitzaileak" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API gakoa" +description = "Utzi hutsik motorraren inguruneko kredentziala erabiltzeko. Motor bakarreko self-hosted inplementazioei aplikatzen zaie." +generic = "API gakoa" +openai = "OpenAI API gakoa" +setPlaceholder = "Gordeta - utzi hutsik uneko gakoa mantentzeko" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI-rekin bateragarria den / Ollama endpoint-aren oinarrizko URL, adib. http://ollama:11434/v1." +label = "Hornitzailearen oinarrizko URL" +warning = "Oinarrizko URLak fidagarria den barne-endpoint batera apuntatu behar du. Motorrak zerbitzariaren aldetik eskaerak egingo dizkio; beraz, balio ez-fidagarria SSRFarekiko sentikorra da." + +[admin.settings.ai.models.fastMaxTokens] +description = "Eredu azkarraren irteerako token gehieneko kopurua." +label = "Eredu azkarraren gehieneko tokenak" + +[admin.settings.ai.models.fastModel] +description = "Zeregin arinetarako eredu merkeagoa eta azkarragoa. Testu librea; iradokizunak pistak baino ez dira." +label = "Eredu azkarra" + +[admin.settings.ai.models.provider] +description = "Motorrak zein LLM hornitzailerekin komunikatzen den." +label = "Hornitzailea" + +[admin.settings.ai.models.smartMaxTokens] +description = "Eredu adimentsuaren irteerako token gehieneko kopurua." +label = "Eredu adimentsuaren gehieneko tokenak" + +[admin.settings.ai.models.smartModel] +description = "Arrazonamendu konplexurako gaitasun handiko eredua. Testu librea; iradokizunak pistak baino ez dira." +label = "Eredu adimentsua" + +[admin.settings.ai.saved] +body = "Aldaketak automatikoki bidaltzen zaizkio AI motorrari." +bodyNoPush = "Ezarpenak gordeta. AI motorrak bere konfigurazioa hurrengoan hartzen duenean aplikatuko dira." +title = "AI ezarpenak gordeta" + [admin.settings.badge] clickToUpgrade = "Egin klik planaren xehetasunak ikusteko" @@ -938,6 +1094,25 @@ label = "Abiokoan birsortu" description = "Ziurtagiria balioz egongo den egun kopurua" label = "Ziurtagiriaren balio-epea (egunak)" +[admin.settings.folderAccess] +description = "Karpeta-iturriek eta karpeta-irteerek irakurri eta idatz ditzaketen direktorioak. Hau segurtasun-muga bat da: automatizazioak ezin dira inoiz zerrenda honetatik kanpoko zerbitzari-bide batera zuzendu." +securityNote = "Utzi hau hutsik karpeta-iturriak eta irteerak erabat desgaitzeko. Stirling-en konfigurazio-direktorioa beti dago mugaz kanpo." +title = "Karpeta-sarbidea" + +[admin.settings.folderAccess.implied] +description = "Stirling-ek kudeatutako direktorio hauek beti baimenduta daude eta ezin dira hemen aldatu." +serverStorage = "Zerbitzariko fitxategi-biltegia" +title = "Beti baimenduta" +watchedFolder = "Pipelinearen zaindutako karpeta" + +[admin.settings.folderAccess.roots] +add = "Gehitu" +empty = "Ez dago karpetarik baimenduta. Karpeta-iturriak eta irteerak desgaituta daude une honetan." +hint = "Sartu bide absolutuak, adibidez /data/inbox." +label = "Baimendutako karpeta-erroak" +placeholder = "/data/inbox" +remove = "Kendu karpeta-erroa" + [admin.settings.general] description = "Aplikazioaren sistema mailako ezarpenak konfiguratu, marka eta portaera lehenetsia barne." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Zabalera" [app] description = "Adobe Acrobat-en doako alternatiba (10M+ deskarga)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Bihurtu PDF/A-3b formatura" convertToPdfA3bDescription = "Eranskinak txertatuta dituen artxibatzeko PDF bat sortzen du" @@ -1842,6 +2020,15 @@ export = "Esportatu" accessDenied = "Sarbidea ukatuta" insufficientPermissions = "Ez duzu ekintza hau egiteko baimenik." +[auth.callback] +completing = "Autentifikazioa osatzen" +invalidToken = "OAuth saioa hasteak huts egin du - token baliogabea." +missingToken = "OAuth saioa hasteak huts egin du - ez da tokenik jaso." +oauthFailed = "OAuth saioa hasteak huts egin du. Saiatu berriro." +pleaseWait = "Itxaron saioa hasten amaitzen dugun bitartean." +signedOut = "Saioa amaitu duzu. Hasi saioa berriro." +windowMayClose = "Leiho hau itxi dezakezu osatzen denean." + [auth.displayName] guest = "Gonbidatua" user = "Erabiltzailea" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "PDF dokumentuak seguru bihurtzen ditu eduki gaizto potentz description = "Tresna honek PDF fitxategiak automatikoki birizendatuko ditu haien edukian oinarrituta. Dokumentua aztertzen du testutik titulu egokiena aurkitzeko." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Biratu automatikoki" +tags = "biratu automatikoki,konpondu orientazioa,buruz behera,alboz,zuzen jarri,orientatu,detektatu orientazioa,zuzendu biraketa,orientatu automatikoki,eskaneatua,OSD" + +[autoRotate.confidenceThreshold] +title = "Konfiantza-atalasea" + +[autoRotate.detectionMode] +auto = "Automatikoa" +osd = "OCR bakarrik" +text = "Testua bakarrik" +title = "Detekzio-metodoa" + +[autoRotate.error] +failed = "Errore bat gertatu da PDFa automatikoki biratzean." + +[autoRotate.inferUndetected] +title = "Biratu konfiantza txikiko orriak bat etor daitezen" + +[autoRotate.report] +applied = "{{degrees}}° erlojuaren noranzkoan" +confidenceValue = "Konfiantza {{value}}" +noChange = "Aldaketarik ez" +pageLabel = "{{number}}. orria" +summary = "{{total}} orritik {{rotated}} biratu dira, {{unchanged}} bere horretan utzi dira" +title = "Detekzio-txostena" + +[autoRotate.report.method] +inferred = "Bat etorria" +none = "Saltatuta" +osd = "OCR" +text = "Testua" + +[autoRotate.report.note] +belowThreshold = "Atalasearen azpitik" +blankPage = "Orria hutsik dago" +inferredFromDocument = "Beste orrien berdina" +noDominantDirection = "Testu-norabide nahasiak" +osdFailed = "Ez da testu irakurgarririk aurkitu" +osdNoVerdict = "Ezin izan da jakin zein den goiko aldea" +tesseractUnavailable = "OCR ez dago instalatuta" +tooFewGlyphs = "Testu gutxiegi" + +[autoRotate.results] +title = "Biraketa automatikoaren emaitzak" + +[autoRotate.settings] +title = "Ezarpenak" + +[autoRotate.tooltip.confidenceThreshold] +text = "Egiaztapenak zenbaterainoko ziurtasuna izan behar duen orri bat biratu aurretik. Jaitsi orri gehiago biratzeko; igo orri gehiago ukitu gabe uzteko." +title = "Konfiantza-atalasea" + +[autoRotate.tooltip.description] +text = "Orri bakoitzaren goiko aldea zein den kalkulatzen du eta norabide egokira biratzen du. Dagoeneko tente dauden orriak bere horretan uzten dira." + +[autoRotate.tooltip.detectionMode] +text = "Automatikoak lehenik orriko testua irakurtzen du, eta hori nahikoa ez bada, orria irudi gisa aztertzen du. Testua bakarrik aukerak irudi-egiaztapena saltatzen du, eta azkarrena da inoiz eskaneatu ez diren dokumentuetarako. OCR bakarrik aukerak orri guztiak irudi gisa tratatzen ditu." +title = "Detekzio-metodoa" + +[autoRotate.tooltip.header] +title = "Biratu automatikoki" + +[autoRotate.tooltip.inferUndetected] +text = "Orri hutsak edo ia hutsak ezin dira bere kabuz egiaztatu. Irakur daitezkeen orri guztiek biraketa bera behar dutenean, orri hauek haiekin bat etortzeko biratzen dira." +title = "Biratu konfiantza txikiko orriak bat etor daitezen" + [autoSizeSplitPDF] tags = "pdf,banatu,dokumentua,antolaketa" @@ -2189,33 +2443,6 @@ keywords = "Gako‑hitzak: odd, even" numbers = "Zenbakiak/barrutiak: 5, 10-20" progressions = "Progresioak: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Ziurtagiri motak" - -[certificateChoice.tooltip.organization] -bullet1 = "Sistema-administratzaileek kudeatua" -bullet2 = "Baimendutako erabiltzaileen artean partekatua" -bullet3 = "Enpresaren nortasuna ordezkatzen du, ez pertsonarena" -bullet4 = "Egokiena: dokumentu ofizialak, talde-sinadurak" -description = "Zure erakundeak emandako ziurtagiri partekatua. Enpresa-mailako sinadura-agintaritzarako erabiltzen da." -title = "Erakundeko ziurtagiria" - -[certificateChoice.tooltip.personal] -bullet1 = "Lehen erabileran automatikoki sortua" -bullet2 = "Zure erabiltzaile-kontuari lotua" -bullet3 = "Ezin da beste erabiltzaileekin partekatu" -bullet4 = "Egokiena: dokumentu pertsonalak, erantzukizun indibiduala" -description = "Zure erabiltzaile-kontuarentzat berez sortutako ziurtagiri bakarra. Sinadura indibidualetarako egokia." -title = "Ziurtagiri pertsonala" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX fitxategia eta pasahitza behar ditu" -bullet2 = "Kanpoko Ziurtagiri Agintariek eman dezakete" -bullet3 = "Fidagarritasun-maila handiagoa agiri juridikoetarako" -bullet4 = "Egokiena: legez lotesleak diren kontratuak, kanpo-balidazioa" -description = "Erabili zure PKCS#12 ziurtagiri-fitxategia. Ziurtagiriaren propietateen gaineko kontrol osoa ematen du." -title = "Igo P12 pertsonalizatua" - [certSign] awaitingSignatures = "Sinaduren zain" chooseCertificate = "Aukeratu ziurtagiri-fitxategia" @@ -2542,34 +2769,6 @@ server = "Zerbitzaria" stepTitle = "Ziurtagiriaren iturburua" upload = "Igo" -[certSign.tooltip.header] -title = "Sinadurak kudeatzeari buruz" - -[certSign.tooltip.overview] -bullet1 = "Egiaztatu dauden sinadurak eta haien baliozkotasuna" -bullet2 = "Ikusi sinatzaileen eta ziurtagirien informazio zehatza" -bullet3 = "Gehitu sinadura digital berriak zure dokumentuak babesteko" -bullet4 = "Hainbat fitxategi onartzen dira nabigazio errazaarekin" -text = "Tresna honek zure PDFak digitalki sinatuta dauden egiaztatzen eta sinadura digital berriak gehitzen uzten dizu. Sinadura digitalek nor sortu edo onartu duen frogatzen dute eta sinatu zenetik aldaketarik egon den erakusten dute." -title = "Zer egin dezake tresna honek?" - -[certSign.tooltip.signing] -bullet1 = "Onartzen ditu PEM, PKCS12, JKS eta zerbitzariaren ziurtagiri formatuak" -bullet2 = "Aukera sinadura PDFan erakusteko edo ezkutatzeko" -bullet3 = "Gehitu arrazoia, kokapena eta sinatzailearen izena" -bullet4 = "Aukeratu zein orritan jarri sinadura ikusgarriak" -bullet5 = "Erabili zerbitzariaren ziurtagiria 'Sinatu Stirling-PDF-rekin' aukera errazarentzat" -text = "PDF bat sinatzeko, ziurtagiri digital bat behar duzu (PEM, PKCS12 edo JKS modukoa). Sinadura dokumentuan ikusgai egitea edo segurtasunerako ikusezin mantentzea aukera dezakezu." -title = "Sinadurak gehitzea" - -[certSign.tooltip.validation] -bullet1 = "Erakusten du sinadurak baliozkoak edo baliogabeak diren" -bullet2 = "Bistaratzen du sinatzailearen informazioa eta sinadura-data" -bullet3 = "Egiaztatzen du dokumentua sinatu ondoren aldatu den ala ez" -bullet4 = "Egiaztapenerako ziurtagiri pertsonalizatuak erabil ditzake" -text = "Sinadurak egiaztatzean, tresnak baliozkoak diren edo ez, nork sinatu duen, noiz sinatu zen, eta dokumentua sinatu ondoren aldatu den ala ez esaten dizu." -title = "Sinadurak egiaztatzea" - [changeCreds] changeUsername = "Eguneratu zure erabiltzaile-izena. Eguneratu ondoren saioa itxiko da." credsUpdated = "Kontua eguneratu da" @@ -2737,12 +2936,16 @@ text = "Baimen hauek aldaezinak bihurtzeko, erabili Gehitu Pasahitza tresna jabe [chat.actions] copy = "Kopiatu mezua" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling agentearen aukerak" +assistant = "AI Assistant" clearChat = "Garbitu txata" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Eragiketa honek zure hodeiko kredituak erabiliko ditu" tooltip = "Hautatu kolorea pantailatik" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Aukeratu kolorea" [common] +actions = "Actions" back = "Atzera" cancel = "Utzi" close = "Itxi" +codeSample = "Code sample" collapse = "Tolestu" confirm = "Berretsi" continue = "Jarraitu" copied = "Kopiatuta!" copy = "Kopiatu" done = "Eginda" +enterValue = "Enter value" error = "Errorea" expand = "Zabaldu" loading = "Kargatzen..." next = "Hurrengoa" +open = "Open" preview = "Aurrebista" previous = "Aurrekoa" refresh = "Freskatu" +remaining = "Geratzen dena" retry = "Saiatu berriro" save = "Gorde" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Garbitu hautatutakoa" @@ -2988,6 +3199,7 @@ title = "Konpresio-metodoa" [compress.settings] desiredSize = "Nahi den fitxategi-tamaina" desiredSizePlaceholder = "Sartu tamaina" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Konpresioa modu erraza da fitxategi-tamaina murrizteko. Aukeratu Fitxategiaren tamaina helburu tamaina bat sartzeko eta guk kalitatea doitzeko. Aukeratu Kalitatea konpresio-indarra eskuz ezartzeko." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF aukerak" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Lehenik hautatu iturburu formatua" settings = "Ezarpenak" single = "Bakarra" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitorizazio aurreratua" -allInOneWorkspace = "PDF lanerako ingurune osoa (ikusgailua, tresnak eta agentea)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Laguntza dedikatua eta SLAak" -fasterThroughput = "10x azkarragoa" -fullyPrivateFiles = "Fitxategi erabat pribatuak" -largeFileProcessing = "Fitxategi handien prozesamendua" -orgWideAccess = "Erakunde osoko sarbide-kontrolak" -premiumAiModels = "Premium IA ereduak" -prioritySupport = "Lehentasunezko laguntza" -privateDocCloud = "Dokumentu-hodei pribatua" -ragFineTuning = "RAG + fintzea" -secureApiAccess = "API sarbide segurua" -standardThroughput = "Errendimendu estandarra" -unlimitedApiAccess = "API sarbide mugagabea" -unlimitedSeats = "Eserleku mugagabeak" - [crop] autoCrop = "Moztu automatikoki zuriuneak" noFileSelected = "Hautatu PDF fitxategi bat mozten hasteko" @@ -3346,6 +3558,7 @@ message = "Ezin izan da PDF kudeatzaile lehenetsia ezarri" title = "Errorea" [defaultApp.prompt] +dontRemind = "Ez gogorarazi berriro" message = "Egin Stirling PDF PDF fitxategiak irekitzeko zure aplikazio lehenetsia." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Bilatu..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Editatu" @@ -3644,11 +3861,13 @@ shareSelected = "Partekatu hautatutakoak" sharing = "Partekatzea" showAll = "Erakutsi denak" showHistory = "Historia erakutsi" +sortBy = "Sort files" sortByDate = "Ordenatu dataren arabera" sortByName = "Ordenatu izenaren arabera" sortBySize = "Ordenatu tamainaren arabera" storage = "Biltegia" storageState = "Biltegiratzea" +storageUsed = "Storage used" synced = "Sinkronizatuta" title = "PDF fitxategiak igo" toolChain = "Aplikatutako tresnak" @@ -3694,13 +3913,17 @@ addFiles = "Gehitu fitxategiak" addingFiles = "Fitxategiak gehitzen…" collapse = "Tolestu alboko barra" customizeGroups = "Pertsonalizatu taldeak" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Ireki fitxategiak hasteko" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Zabaldu alboko barra" -files = "Fitxategiak" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive ez dago konfiguratuta" leaveMyFiles = "Irten Nire Fitxategietatik" +library = "PDF liburutegia" myFiles = "Nire Fitxategiak" noFiles = "Oraindik ez dago fitxategirik" openFileManager = "Ireki fitxategi-kudeatzailea" @@ -3708,37 +3931,36 @@ openFromComputer = "Ireki ordenagailutik" openSettings = "Ireki ezarpenak" other = "Besteak" recent = "Azkenak" -search = "Bilatu" -searchPlaceholder = "Bilatu fitxategiak..." viewAll = "Ikusi {{count}} fitxategi guztiak" [fileSidebar.fileItem] closeViewer = "Itxi ikustailea" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Ireki ikustailean" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Gehitu" -addLabel = "Gehitu etiketa bat…" -categoryIconAria = "Aukeratu ikono bat {{name}}(r)entzat" -createCategory = "Sortu kategoria" -delete = "Ezabatu kategoria" done = "Eginda" -hide = "Ezkutatu kategoria" -newCategory = "Kategoria-izen berria…" -removeLabel = "Kendu {{name}}" -rename = "Aldatu izena" reset = "Berrezarri lehenetsietara" -search = "Bilatu etiketak…" -show = "Erakutsi kategoria" subtitle = "Taldekatu zure fitxategiak kategoria nagusietan. Gehitu kategoria bati lehendik dauden edo etiketa berriak, aldatu izena edo sortu zurea. Zure kategorietako batean ez dauden fitxategiak “Besteak” atalean agertuko dira." title = "Alboko barrako kategoriak" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Gehitu lan-eremura" addToWorkspaceCount = "Gehitu {{count}} lan-eremura" @@ -3749,7 +3971,6 @@ backToMyFiles = "Itzuli Nire Fitxategietara" breadcrumbs = "Karpetaren bidea" cancel = "Utzi" classification = "Sailkapena" -clearSearch = "Garbitu bilaketa" clearSelection = "Garbitu hautapena" closeDetails = "Itxi xehetasunak" create = "Sortu" @@ -3781,6 +4002,7 @@ downloadAll = "Deskargatu dena" downloadVersion = "Deskargatu bertsio hau" dropOverlay = "Jaregin fitxategiak igotzeko" dropOverlaySub = "Fitxategiak Tokikoan hasten dira. Erabili 'Eraman hona' edo 'Gorde hodeian' karpeta batean antolatzeko." +duplicate = "Duplicate" file = "Fitxategia" fileInfo = "File info" fileMenu = "Fitxategi-ekintzak" @@ -3808,13 +4030,12 @@ refresh = "Freskatu zerbitzaritik" remove = "Ezabatu" removeVersion = "Kendu bertsio hau" rename = "Aldatu izena" +renamed = "Berrizendatua" renameFolder = "Aldatu karpetaren izena" resizeFolderTree = "Aldatu karpeta-zuhaitzaren tamaina (gezi-teklak, Shift pauso handiagoetarako; klik bikoitza doitzeko)" save = "Gorde" saveToServer = "Gorde zerbitzarian" saveToServerDisabledHint = "Zerbitzarian gordetzea ez dago gaituta zerbitzari honetan. Eskatu administratzaileari gaitzeko." -search = "Bilatu" -searchPlaceholder = "Bilatu karpeta honetan eta azpikarpetetan" selectAll = "Hautatu dena" selectAllHint = "Egin klik dena hautatzeko. Aholkua: eutsi Ctrl (edo Cmd) tekla fitxategiak banaka gehitzeko, Shift tarte bat hautatzeko." selectedCount = "{{count}} hautatuta" @@ -3874,6 +4095,10 @@ title = "Oraindik ez dago hodeiko fitxategirik" hint = "Igo gabe gordetako fitxategiak hemen geratzen dira. Jaregin fitxategi bat gehitzeko." title = "Ez dago tokiko soilik diren fitxategirik" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Irekiko edo editatuko dituzun fitxategiak hemen agertuko dira." title = "Oraindik ez da ezer aldatu" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Ezin izan da karpeta ezabatu." deleteFolderFailedDetail = "Ezin izan da karpeta ezabatu: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Ezin izan da karpetaren itxura eguneratu." folderAppearanceFailedDetail = "Ezin izan da karpetaren itxura eguneratu: {{message}}" moveFilesFailed = "Ezin izan dira fitxategiak mugitu." @@ -3910,9 +4137,7 @@ count = "Fitxategiak" folder = "Karpeta" labels = "Etiketak" modified = "Aldatuta" -name = "Izena" size = "Tamaina" -toolHistoryAtVersion = "Tresna-kate metatua" totalSize = "Tamaina osoa" type = "Mota" versionHistory = "Bertsioen ibilbidea" @@ -3945,6 +4170,11 @@ localHint = "Nabigatzaile honetan soilik gordeta" shared = "Partekatuak" sharedHint = "Zurekin estekaz partekatuta" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Zaharrenak lehenik" modifiedDesc = "Berrienak lehenik" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Ez dago fitxategirik biltegian. Ireki lehenik fitxategi open = "Ireki" openFile = "Fitxategia ireki" openFiles = "Fitxategiak ireki" +selectFile = "Select {{name}}" selectFromStorage = "Biltegitik hautatu" upload = "Igo" uploadFile = "Fitxategia igo" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Dena gordeta" +analyzingFields = "Analysing form fields..." +extractCsvError = "Ezin izan da CSV erauzi" +extractXlsxError = "Ezin izan da XLSX erauzi" +filled = "filled" flattenAfterFilling = "Lautu betetze ondoren" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "behar." +requiredFieldsError = "Bete beharrezko eremu guztiak" rescanFields = "Eremuak berriro eskaneatu" rescanFormFields = "Inprimaki-eremuak berriro eskaneatu" save = "Gorde" +saveShortcut = "Ctrl+S gordetzeko" +unsavedChanges = "Gorde gabeko aldaketak" [formFill.sidebar] close = "Itxi alboko barra" @@ -4309,6 +4552,11 @@ desc = "PDF fitxategi bat automatikoki berrizendatzen du detektatutako goiburuar tags = "auto-detektatu,goiburuetan oinarritua,antolatu,berrerrotulatu" title = "Automatikoki berrizendatu PDF" +[home.autoRotate] +desc = "Detektatu orri bakoitzaren orientazioa eta biratu automatikoki tente jartzeko." +tags = "biratu automatikoki,konpondu orientazioa,buruz behera,alboz,zuzen jarri,orientatu,detektatu orientazioa,zuzendu biraketa,orientatu automatikoki,eskaneatua,OSD" +title = "Biratu automatikoki" + [home.autoSizeSplitPDF] tags = "auto,banatu,tamaina" title = "Autom. zatitu tamaina/kop." @@ -4608,15 +4856,12 @@ hint = "Igo PNG, JPG, SVG edo bestelako irudi-fitxategi bat PDFan kokatzeko. SVG label = "Igo irudia" placeholder = "Hautatu irudi-fitxategia" -[imageToPdf] -tags = "conversion,img,jpg,picture,photo,psd,photoshop" - [imageToPDF] header = "Irudia PDF bihurtu" title = "Irudia PDF bihurtu" -[infoBanner] -dismiss = "Baztertu" +[imageToPdf] +tags = "conversion,img,jpg,picture,photo,psd,photoshop" [invite] acceptError = "Ezin izan da kontua sortu" @@ -4647,11 +4892,9 @@ welcomeTitle = "Gonbidapena jaso duzu!" [landing] addFiles = "Gehitu fitxategiak" -heroSubtitle = "Jaregin edo gehitu dagoen PDF bat hasteko." mobileUpload = "Kargatu mugikorretik" openFromComputer = "Ireki ordenagailutik" uploadFromComputer = "Igo ordenagailutik" -workbenchEmptyStateHero = "Jaregin PDF bat edonon" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Sartu erabiltzaile-izena" failedToSignIn = "Ezin izan da saioa hasi {{provider}}: {{message}}" forgotPassword = "Ahaztu duzu pasahitza?" loggingIn = "Saioa hasten..." -logIn = "Hasi saioa" login = "Saioa hasi" magicLinkSent = "Esteka magikoa bidali zaio {{email}} helbidera! Egiaztatu zure e-posta eta egin klik estekan saioa hasteko." mfaCode = "Autentifikazio-kodea" @@ -4715,26 +4957,10 @@ signInWith = "Hasi saioa honekin" title = "Saioa hasi" unexpectedError = "Ustekabeko errorea: {{message}}" updatePassword = "Pasahitza eguneratu" -useEmailInstead = "Hasi saioa e-postarekin" useMagicLink = "Erabili esteka magikoa" username = "Erabiltzaile-izena" youAreLoggedIn = "Saioa hasita daukazu!" -[login.slides.edit] -alt = "PDFak editatu" -subtitle = "Dozenaka tresnarekin PDFak zentsuratu, sinatu, irakurri eta manipulatzeko, bilatzen ari zarena aurkituko duzu." -title = "Editatu PDFak nahi duzun informazioa bistaratu edo babesteko" - -[login.slides.overview] -alt = "Stirling PDF ikuspegi orokorra" -subtitle = "Pribatutasuna lehenesten duen hodeiko suitea, PDFak bihurtu, sinatu, zentsuratu eta kudeatzeko, eta 50+ tresna indartsu gehiagorekin." -title = "Zure PDF behar guztietarako gune bakarra." - -[login.slides.secure] -alt = "PDFak segurtatu" -subtitle = "Gehitu pasahitzak, zentsuratu edukia eta kudeatu ziurtagiriak erraz." -title = "Babestu informazio sentikorra zure PDFetan" - [margin] large = "Handia" medium = "Ertaina" @@ -4818,6 +5044,34 @@ uploadSuccess = "Kargaketa arrakastatsua!" uploadSuccessMessage = "Zure irudiak transferitu dira." validating = "Saioa balioztatzen..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Eskaneatu argazkiak igotzeko. Irudiak automatikoki PDFra bihurtzen dira." descriptionNoConvert = "Eskaneatu argazkiak zure gailu mugikorretik igotzeko." @@ -4836,8 +5090,13 @@ title = "Kargatu mugikorretik" tags = "Multi Tool,Multi operation,UI,click drag,front end,client side" title = "PDF erabilera anitzeko tresna" -[navbar] -search = "Bilatu" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentifikazioa ez da ongi burutu. Leiho hau itxi eta saiatu berriro." @@ -4966,6 +5225,7 @@ activeFiles = "Fitxategi Aktiboak ikuspegiak tresnan kargatuta allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Itxi" cropSettings = "Orain moztu nahi dugun fitxategia hautatu dugunez, Moztu tresna konfigura dezakegu PDFa moztu nahi dugun eremua aukeratzeko." +dialogLabel = "Onboarding" fileCheckbox = "Fitxategietako bat sakatzean, prozesatzeko hautatzen da. Hainbat fitxategi hauta ditzakezu batch eragiketetarako." fileReplacement = "Aldatutako fitxategiak automatikoki ordezkatuko du jatorrizkoa Lan-mahaian, errazago beste tresna batzuekin berriz prozesatzeko." filesButton = "Sarbide azkarreko barrako Fitxategiak botoiak PDFak igotzea ahalbidetzen dizu tresnekin erabiltzeko." @@ -4976,6 +5236,7 @@ pinButton = "Finkatu botoia erabil dezakezu fitxategiak aktibo results = "Tresnak exekutatzea amaitutakoan, Berrikusi urratsak emaitzen aurrebista erakutsiko du panel honetan, eta eragiketa desegiteko edo fitxategia deskargatzeko aukera emango dizu. " runButton = "Tresna konfiguratu ondoren, botoi honek hautatutako PDF guztietan tresna exekutatzeko aukera ematen du." selectCropTool = "Hauta dezagun Moztu tresna tresnetako bat nola erabili erakusteko." +stepOf = "{{current}}/{{total}} urratsa" toolInterface = "Hau da Moztu tresnaren interfazea. Ikusten duzunez, ez dago asko oraindik, lantzeko PDF fitxategirik ez dugulako gehitu." workbench = "Hau da Lan-mahaia - zure PDFak ikusi eta editatzeko eremu nagusia." wrapUp = "Dena prest! Aplikazioaren eremu nagusiak eta nola erabili ikasi dituzu. Egin klik Laguntza botoian bira hau berriro ikusteko nahi duzunean." @@ -4988,6 +5249,26 @@ showMeAround = "Erakutsi ingurunea" skipForNow = "Utzi oraingoz" skipTheTour = "Utzi bisita gidatua" +[onboarding.checklist] +dismiss = "Baztertu" +title = "Konfiguratu Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Exekutatu Stirling modu natiboan zure makinan" +title = "Deskargatu Stirling mahaigainerako" + +[onboarding.checklist.inviteTeam] +description = "Kolaboratu zure taldearekin" +title = "Gonbidatu taldeko kideak" + +[onboarding.checklist.shareAnalytics] +description = "Lagundu Stirling hobetzen" +title = "Partekatu erabilera-datu anonimoak" + +[onboarding.checklist.takeTour] +description = "Ikusi Stirling nola dabilen ibilbide azkar batean" +title = "Egin ibilbidea" + [onboarding.desktopInstall] body = "Stirling hobekien funtzionatzen du mahaigaineko aplikazio gisa. Offline erabil dezakezu, dokumentuak azkarrago atzitu, eta editatu tokian bertan zure ordenagailuan." selectOs = "Hautatu sistema eragilea" @@ -4996,7 +5277,21 @@ titleWithOs = "Deskargatu {{osLabel}}-rako" [onboarding.mfa] authenticationCode = "Autentifikazio-kodea" +description = "Babestu zure kontua autentifikazio-aplikazio bat lotuz. Eskaneatu QR kodea edo sartu konfigurazio-gakoa, eta ondoren berretsi 6 digituko kodea amaitzeko." +enable = "Gaitu MFA" +enableError = "Ezin da bi faktoreko autentifikazioa gaitu. Egiaztatu kodea eta saiatu berriro." +enterCode = "Sartu zure aplikazioko 6 digituko kodea." +enterCodeError = "Sartu autentifikazio-kodea jarraitzeko." +logout = "Amaitu saioa" +openAuthenticator = "Ireki Google Authenticator, Authy edo 1Password." qrCodeLoading = "Zure QR kodea sortzen…" +regenerateQrCode = "Birsortu QR kodea" +scanQrCode = "Eskaneatu QR kodea edo sartu beheko konfigurazio-gakoa." +setupError = "Ezin da bi faktoreko konfigurazioa hasi. Saiatu berriro." +setupKey = "Konfigurazio-gakoa (eskuzko sarrera)" +stepByStep = "Urratsez urrats" +success = "MFA gaituta dago. Orain jarrai dezakezu." +title = "Faktore anitzeko autentifikazioaren konfigurazioa" [onboarding.planOverview] adminBodyLoginDisabled = "Saio-hasiera modua gaitzen duzunean, erabiltzaileak kudeatu, ezarpenak konfiguratu eta zerbitzariaren osasuna monitorizatu ahal izango dituzu. Zure zerbitzariko lehen {{freeTierLimit}} pertsonek doan erabil dezakete Stirling." @@ -5005,6 +5300,11 @@ adminTitle = "Admin ikuspegi orokorra" userBody = "Gonbidatu taldekideak, esleitu rolak eta mantendu dokumentuak antolatuta lan-espazio seguruan. Gaitu saio-hasiera modua bakarkako erabileratik harago hazteko prest zaudenean." userTitle = "Planaren ikuspegi orokorra" +[onboarding.processorIntro] +body = "Stirling-ek orain Politikak exekutatzen ditu — dokumentu bakoitza iristen denean sailkatu, babestu eta prozesatzen duten arau automatizatuak. Konfiguratu eta monitorizatu exekuzioak Processor-en." +cta = "Ikusi Processor" +title = "Ikusi Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Aurrez ordaindutako urtea" + +[payg.prepaid.banner] +exhausted = "Zure aurrez ordaindutako edukiera agortu da; erabileraren araberako fakturazioa berriro hasi da. Kargatu gehiago deskontua mantentzeko." +expiring_one = "Zure aurrez ordaindutako urtea {{count}} egun barru amaituko da. Erabili gabeko edukiera iraungiko da — kargatu gehiago urte berri bat hasteko." +expiring_other = "Zure aurrez ordaindutako urtea {{count}} egun barru amaituko da. Erabili gabeko edukiera iraungiko da — kargatu gehiago urte berri bat hasteko." +low = "Zure aurrez ordaindutako edukieraren %{{pct}} geratzen zaizu. Kargatu gehiago edonoiz prozesamendua eten gabe mantentzeko." + +[payg.prepaid.card] +subtitle = "Erabileraren araberako fakturazioaren aurretik eta zure gastu-mugatik kanpo erabilia." +title = "Aurrez ordaindutako edukiera" + +[payg.prepaid.meter] +capSuffix = "{{total}} aurrez ordaindutako kreditutik" +expires = "Iraungitze-data: {{date}}" + +[payg.prepaid.state] +exhausted = "Agortuta" +healthy = "Asko geratzen da" +low = "Gutxi geratzen da" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDFa PDF/A bihurtu" tags = "artxibo,epe-luze,estandar,bihurketa,biltegiratzea,kontserbazioa" title = "PDFa PDF/A bihurtu" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "inprimaketa,estandarra,bihurketa,produkzioa,aurre-inprimaketa,artxiboa" title = "PDFtik PDF/Xera" @@ -5822,6 +6151,7 @@ small = "500 kreditu" xsmall = "100 kreditu" [plan.availablePlans] +currency = "Billing currency" subtitle = "Aukeratu zure beharretara egokitzen den plana" title = "Plan eskuragarriak" @@ -5937,49 +6267,12 @@ keyDescription = "Itsatsi lizentzia-gakoa zure postatik" success = "Lizentzia aktibatuta!" successMessage = "Zure lizentzia ongi aktibatu da. Orain leiho hau itxi dezakezu." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "betearazita" -enforcing = "Betearazten..." -failed = "Betearazteak huts egin du" outputsUnavailable = "Politikaren emaitzak ez daude jada deskargatzeko eskuragarri." partialOutputsUnavailable = "Politikaren emaitza batzuk ez daude jada deskargatzeko eskuragarri." -retrying = "Lanpetuta, berriz saiatzen..." runNotFound = "Betearazte-exekuzioa ezin da jada aurkitu." -step = "urratsa {{current}}/{{total}}" timedOut = "Betearazteak denbora-muga gainditu du exekuzioa amaitu aurretik." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Itxi" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} aplikatzen" applyingProgress = "{{names}} aplikatzen ({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} fitxategitatik {{failures}} ezin izan dira prozesatu et failureTitle = "Betearazpen osorik gabe esportatuta" printPolicyAppliedBody = "PDF hau politika bat betetzeko eguneratu da. Aldaketak berrikusi eta, ondoren, inprimatu berriro." printPolicyAppliedTitle = "Inprimatu aurretik politika aplikatuta" -queued = "+{{count}} ilaran" successTitle = "{{names}} aplikatuta" summaryMore = "{{first}}, {{second}} eta {{more}} gehiago" summaryTwo = "{{first}} eta {{second}}" -[policies.enforcement.triggerVerb] -convert = "Bihurtu aurretik betearazten" -default = "Betearazten" -export = "Esportatu aurretik betearazten" -input = "Inportatzean betearazten" -print = "Inprimatu aurretik betearazten" - -[policies.field] -accessLog = "Sarbide-erregistroa" -archiveAfter = "Artxibatu honen ondoren" -auditTrail = "Auditoria-arrastoa" -belowThreshold = "Atalasearen azpitik" -destination = "Helmuga" -frameworks = "Esparruak" -immutableHold = "Eutsi aldaezina" -keepFor = "Mantendu honetarako" -minConfidence = "Gutxieneko konfiantza" -notify = "Jakinarazi bideratzean" -onViolation = "Betetzen ez denean" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Gehitu" -addPlaceholder = "Gehitu etiketa bat…" -customNote = "Zure talderako pertsonalizatua." -defaultNote = "Lehenetsi integratua erabiltzen, zure taldearekin partekatua." -duplicate = "\"{{name}}\" lehendik badago." -edit = "Editatu etiketak" -empty = "Oraindik ez dago etiketarik." -export = "Esportatu JSON" -iconAria = "Aukeratu ikono bat {{name}}(r)entzat" -import = "Inportatu JSON" -importError = "Ezin izan da fitxategi hori inportatu." -managedNote = "Taldeko etiketak zure talde-buruak kudeatzen ditu." -modalSubtitle = "Zure talde osoarekin partekatua. Sailkatzaileak dokumentu bakoitzari dagozkion etiketak aukeratzen ditu." -modalTitle = "Sailkapen-etiketak" -pickIcon = "Erabili {{label}} ikonoa" -removeAria = "Kendu {{name}}" -resetToDefault = "Berrezarri lehenetsira" -saveForTeam = "Gorde taldearentzat" -saving = "Gordetzen…" -sectionLabel = "Sailkapen-etiketak" -startFromScratch = "Hasi hutsetik" -teamCount = "taldeko etiketak" -tooLong = "Etiketek gehienez {{max}} karaktere izan ditzakete." -ungrouped = "Taldekatu gabe" -view = "Ikusi etiketak" +categoryCount = "kategoriak" +hideCategory = "Ezkutatu kategoria" +labelCount = "etiketak" +sharedNote = "Etiketa hauek barnean datoz eta zure talde osoan partekatzen dira." +showCategory = "Erakutsi kategoria" [policies.pii] account = "Kontu-zenbakiak (etiketatuak)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "AEBetako bideratze-zenbakiak (ABA)" ssn = "Gizarte Segurantzako zenbakiak" -[policies.settings] -noneExport = "Une honetan ez da politikarik exekutatzen esportatzean." -noneUpload = "Une honetan ez da politikarik exekutatzen kargatzean." -onExport = "Esportatzean" -onUpload = "Kargatzean" -reorderHandle = "Arrastatu berrordenatzeko" -runOrderDesc = "Politika bat baino gehiago abiarazle berean exekutatzen denean, ordena honetan exekutatzen dira — bakoitza aurreko politikaren irteeran. Arrastatu berrordenatzeko." -title = "Politika-ezarpenak" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Politika-aukerak" -policySettings = "Politika-ezarpenak" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Saiatu berriro huts egindako politikak ({{count}})" -rowProgress = "{{completed}} / {{total}} fitxategi prozesatuta" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Zer da politika bat?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatikoki aurkitzen eta beltzez estaltzen ditu datu sentikorrak — adibidez, Gizarte Segurantzako eta txartel-zenbakiak —, dokumentuan irakurri ez daitezen." -sanitize = "Fitxategitik ezkutuko JavaScript kentzen du, norbaitek irekitzean ezer ez dadin automatikoki exekutatu." -watermark = "Orri bakoitzean marka ikusgai bat jartzen du (adib., \"Confidential\")." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Itxi" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} betearazten..." badgeRan = "{{name}} politika fitxategi honetan exekutatu da" +badgeRunning = "{{name}} exekutatzen..." blockingAction = "{{action}} blokeatuta politika betearazten den bitartean; itxaron, mesedez..." dismiss = "Baztertu gainjartzea" enforcingTitle = "Politika betearazten..." @@ -6151,6 +6316,7 @@ viewAnyway = "Ikusi fitxategia (politika oraindik betearazten ari da)" [policyOption] autoRedactPhi = "Automatikoki ezkutatu PHI" blockExport = "Blokeatu esportazioa" +ccpa = "CCPA" contracts = "Kontratuak" documents = "Dokumentuak" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor plana" subscribed = "Processor plana" unlinked = "Lotu gabe" -[portal.agentBuilder] -bootstrapFromDocument = "Hasieratu dokumentutik" -sectionsAriaLabel = "Agente-eraikitzailearen atalak" -selectorAriaLabel = "Agenteak" -subtitle = "Diseinatu, probatu eta argitaratu dokumentuak sailkatu, haietatik erauzi eta bideratzen dituzten AI agenteak. Definitu agertokiak, mugatu tresnen sarbidea, exekutatu urrezko multzo bat eta argitaratu bertsio bat." -title = "Agente-eraikitzailea" - -[portal.agentBuilder.bootstrap] -cancel = "Utzi" -dropzoneText = "Aukeratu lagin-dokumentu bat (PDF edo irudia)" -lead = "Jaregin lagin-dokumentu bat eta findu dezakezun agertokiak eta erauzketa-eskema bat proposatuko dizkizugu. Ez da ezer argitaratuko zuk berrikusi arte." -submit = "Hasieratu agentea" -subtitle = "Sortu agente berri baten oinarria fitxategi adierazgarri batetik" -title = "Hasieratu dokumentu batetik" - -[portal.agentBuilder.empty] -description = "Hasieratu agente bat lagin-dokumentu batetik bere agertokiak eta erauzketa-eskema sortzeko; gero, findu eta argitaratu." -title = "Oraindik ez dago agenterik" - -[portal.agentBuilder.evals] -casesPassing = "Gainditzen duten kasuak" -columnCase = "Ebaluazio-kasua" -columnLatency = "Latentzia" -columnResult = "Emaitza" -fail = "huts egin" -goldenSetPassRate = "Urrezko multzoaren gainditze-tasa" -latencyMs = "{{ms}} ms" -notRun = "exekutatu gabe" -pass = "gainditu" -passRate = "Gainditze-tasa" -runEvals = "Exekutatu ebaluazioak" - -[portal.agentBuilder.evals.empty] -description = "Ebaluazioek zure agertokiak urrezko multzo errepikagarri bihurtzen dituzte. Eguneratu plana denboran zehar gainditze-tasa jasotzeko eta argitalpenak horren arabera baldintzatzeko." -title = "Oraindik ez dago urrezko multzorik" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "urrezko multzoetan zehar" -activeAgents = "Agente aktiboak" -avgPassRate = "Batez besteko ebaluazioen gainditze-tasa" -fleetWide = "flota osoan" -latestPublished = "Azken argitaratua" -scenarios = "Agertokiak" -testCases = "proba-kasuak" -totalDescription_one = "{{count}} guztira" -totalDescription_other = "{{count}} guztira" - -[portal.agentBuilder.scenarios] -add = "Gehitu" -addScenario = "Gehitu agertokia" -enable = "Gaitu" -expectationLabel = "Espero den portaera" -expectationPlaceholder = "Agenteak egin beharko lukeena" -inEval = "ebaluazioan" -mute = "Isilarazi" -muted = "isilarazita" -nameLabel = "Izena" -namePlaceholder = "adib. Betetze-eskalatzea" - -[portal.agentBuilder.tabs] -evals = "Ebaluazioak" -scenarios = "Agertokiak" -tools = "Tresnak" -versions = "Bertsioak" - -[portal.agentBuilder.tools] -broadAccess = "Sarbide zabala" -deniedHint = "Hautatutako tresnak blokeatuta daude. Gainerako guztia deigarria izaten jarraitzen du." -deniedTools = "Ukatutako tresnak" -governanceGate = "Tresnen gobernantza Enterprise planean dago erabilgarri." -restricted = "Murriztua" -restrictedAccess = "Tresnen sarbide murriztua" -restrictedDescription = "Baimendu tresna guztiak, behean ukatzen dituzunak izan ezik." - -[portal.agentBuilder.versions] -current = "uneko" -historyGate = "Bertsio-historia osoa eta atzera egitea Enterprise planean daude erabilgarri." -publish = "Argitaratu" -rollBack = "Itzuli atzera" - [portal.assistant] close = "Itxi laguntzailea" error = "Ezin izan da laguntzailearekin konektatu." @@ -6365,9 +6451,21 @@ typing = "Idazten" loading = "Kargatzen" redirectingToEditor = "Editorera birbideratzen..." +[portal.billing.activation] +maybeLater = "Agian geroago" +title = "Aktibatu Processor" + +[portal.billing.activation.payg] +sub = "Ezarri hileko muga. Ordaindu exekutatzen duzuna." +title = "Erabili ahala ordaindu" + +[portal.billing.activation.prepay] +badge = "2 hilabete doan" +sub = "Faktura bakarra, txartela edo banku-transferentzia." +title = "Ordaindu urte bat aurrez" + [portal.billing.checkout] noClientSecret = "Edge funtzioak ez du client_secret itzuli." -subtitle = "Gehitu txartel bat doako Editor planaren baimenetik harago jarraitzeko. Stripe-k gainerakoa kudeatzen du." title = "Aktibatu Processor plana" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Ordainketa ondo egin da, baina aktibazioa ohikoa baino pixka bat gehiago close = "Itxi" title = "Ia prest" +[portal.billing.checkout.cap] +amountAria = "Hileko gastu-muga" +back = "Atzera" +continue = "Jarraitu ordainketara" +error = "Ezin izan da zure gastu-muga ezarri" +finePrint = "Fakturak hilero igortzen dira. Ezeztatu edonoiz." +noLimit = "Mugarik ez" +note = "Ez zaizu inoiz zure mugatik gora fakturatuko. Prozesamendua pausatu besterik ez da egingo." +perMonth = "/ hil" +title = "Ezarri zure gastu-muga" + +[portal.billing.checkout.card] +fields = "Txartel-zenbakia · HH / UU · CVC · ZIP" +label = "Txartelaren xehetasunak" +note = "Txartelaren xehetasunak Stripe-k biltzen ditu. Stirling-ek ez du inoiz PAN edo CVC gordetzen." + [portal.billing.checkout.error] title = "Ezin izan da ordainketa-prozesua hasi" @@ -6383,10 +6497,9 @@ body = "Zure ordainketa gauzatu da. Neurtutako prozesamendua aktibatzen ari gara hint = "Mantendu leiho hau irekita, mesedez." title = "Zure Processor plana aktibatzen..." -[portal.billing.checkout.notConfigured] -bodyAfter = "portaleko env-en aplikazio barruko ordainketa gaitzeko." -bodyBefore = "Ezarri" -title = "Stripe ez dago konfiguratuta" +[portal.billing.checkout.pay] +limit = "Hileko gastu-muga {{amount}}/hil. Prozesamendua mugan pausatuko da." +limitNoCap = "Gastu-mugarik ez. Prozesamendua ez da pausatuko. Fakturak hilero igortzen dira." [portal.billing.enterpriseUpsell] cta = "Sortu zure Enterprise aurrekontua" @@ -6409,10 +6522,12 @@ currentPlan = "Uneko plana" freeForever = "Doan betiko" noTeamResolved = "Zure zorroan oraindik ez da talderik ebatzi — freskatu eta saiatu berriro." ownerOnly = "Taldearen jabeak bakarrik aktiba dezake Processor plana." +payInvoice = "Ordaindu faktura osatzeko" planName = "Editor" ssoIncluded = "SSO barne" switchOnProcessor = "Aktibatu Processor →" unlimitedUsers = "Erabiltzaile mugagabeak" +viewQuote = "Ikusi aurrekontua" [portal.billing.invoices] columnAmount = "Zenbatekoa" @@ -6465,7 +6580,111 @@ segmentApiDesc = "API eskaera zuzenak" segmentApiLabel = "API" segmentAutomationDesc = "Automatizazioak eta pipeline-ak" segmentAutomationLabel = "Automatizazioa" +sizeMultiplier = "{{formatted}} PDFek tamaina-biderkatzailea erabili dute" +summary = "{{unique}} bakar · {{units}} neurgailu-unitate · {{avg}} batez beste PDF bakoitzeko" +summaryNoRate = "{{unique}} bakar · {{units}} neurgailu-unitate" unit = "neurtutako PDFak" +unitsPending_one = "{{units}} neurgailu-unitate sinkronizatu gabe dago lotutako instantzietatik" +unitsPending_other = "{{units}} neurgailu-unitate sinkronizatu gabe daude lotutako instantzietatik" + +[portal.billing.prepaid] +capSuffix = "{{total}} aurrez ordaindutako kreditutik" +expires = "Iraungitze-data: {{date}}" +eyebrow = "Aurrez ordaindutako edukiera" +note = "Erabileraren araberako fakturazioaren aurretik eta zure gastu-mugatik kanpo erabilia." +topUp = "Kargatu gehiago" + +[portal.billing.prepaid.buy] +back = "Atzera" +calcTitle = "Kalkulatu zure urteko ordainketa" +cancel = "Utzi" +cancelPurchase = "Ezeztatu erosketa" +close = "Itxi" +continueToPayment = "Jarraitu ordainketara" +finalise = "Amaitu" +invoiceErrorTitle = "Ezin izan da faktura sortu" +loadingTitle = "Zure aurrekontua kargatzen" +payTitle = "Ordaindu zure urtea" +step = "{{current}}/{{total}} urratsa" + +[portal.billing.prepaid.calc] +change = "Aldatu" +deployRow = "Inplementazioa" +done = "Eginda" +enterpriseHint = "Hau enpresa-mailako eskala da. Bolumen honetan, enpresa-tarifek autoservizioko edozein deskontu gainditzen dute. Tarifa-blokeoa, baldintzak eta aurrekontua minutu gutxitan." +governanceRow = "Gobernantza" +governanceValue = "{{label}} · {{count}} politika" +handlesLabel = "Zure Processor" +handlesValue = "{{volume}} kreditu / hil kudeatzen ditu" +pipelinesLabel = "Pipelineak" +policiesMeta = "{{count}} politika" +poolCaption = "Urterako {{credits}} kredituko multzo bakarra. Hilabete astunek arinetatik mailegatzen dute." +priceLabel = "Zure urtea · 12 hilabete 10en prezioan" +rateUnknown = "Prezio zehatza checkout-ean erakutsiko dugu." +savings = "{{amount}} aurrezten duzu · 2 hilabete doan." +sizingRow = "Tamainaketa" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Erabiltzaileak guztira" + +[portal.billing.prepaid.consent] +label = "Ulertzen dut nire aurrez ordaindutako edukiera agortzen denean edo 12 hilabeteren ondoren iraungitzen denean, prozesamenduak automatikoki jarraituko duela erabileraren araberako tarifa estandar neurtuan (nire gastu-mugaraino), ezeztatzen ez badut, eta edonoiz ezezta dezakedala fakturazio-ataritik." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling-ek kudeatua. Martxan minutu gutxitan." +cloudValue = "Stirling Cloud · Kudeatua" +selfhost = "Self-hosted" +selfhostDesc = "Azpiegitura pribatuan exekutatzen da. Tarifa bera." +selfhostValue = "Self-hosted · Azpiegitura pribatua" + +[portal.billing.prepaid.offer] +cta = "Berrikusi eskaintza" +subtitle = "Ordaindu aurrez PDF prozesamenduko urte bat eta lortu bi hilabete doan, erabileraren araberako fakturazioaren aurretik eta zure gastu-mugatik kanpo erabilita." +title = "Lortu 12 hilabete 10en prezioan" + +[portal.billing.prepaid.pay] +accountName = "Kontuaren titularraren izena" +accountNamePlaceholder = "adib. Jane Smith" +companyName = "Enpresaren izena (aukerakoa)" +companyNamePlaceholder = "adib. Acme Inc." +downloadInvoice = "Deskargatu faktura" +payOnline = "Ordaindu online" +poNumber = "PO zenbakia (aukerakoa)" +poNumberPlaceholder = "adib. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Aurreratua" +advancedDesc = "Pipelineek zure prozesamenduaren zatirik handiena gidatzen dute." +none = "Bat ere ez" +noneDesc = "Oraindik ez duzu pipelinerik exekutatzen. Aktibatu edonoiz." +standard = "Estandarra" +standardDesc = "Pipeline gutxi batzuek iristen diren PDFak berriro prozesatzen dituzte." + +[portal.billing.prepaid.posture] +essentials = "Oinarrizkoak" +essentialsDesc = "Sailkapena, lehenetsia, gehi Partekatzea" +governed = "Gobernatua" +governedDesc = "Segurtasuna eta bideratzea gehitzen ditu" +regulated = "Arautua" +regulatedDesc = "Kategoria guztiak, barne Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Deskargatu aurrekontua (PDF)" +downloading = "Prestatzen…" +share = "partekatu onarpenerako" + +[portal.billing.prepaid.size] +compact = "Trinkoa" +compactDesc = "25 MB baino gutxiagoko fitxategiak, datu-kargurik gabe" +heavy = "Astuna" +heavyDesc = "Eskaneatuak edo irudi askokoak, normalean 50 MB+" +standard = "Estandarra" +standardDesc = "Gehienbat txikiak, eskaneatu batzuk 25 MBtik gorakoak" + +[portal.billing.prepaid.state] +exhausted = "Agortuta" +healthy = "Asko geratzen da" +low = "Gutxi geratzen da" [portal.billing.spendLimit] adjustLimit = "Doitu muga" @@ -6494,6 +6713,8 @@ label = "Gainditzea aurreikusten da." [portal.billing.spendThisMonth] eyebrow = "Hilabete honetako gastua" +freeRemaining_one = "Doako {{formatted}} PDF geratzen da" +freeRemaining_other = "Doako {{formatted}} PDF geratzen dira" processed_one = "{{formattedCount}} PDF prozesatuta." processed_other = "{{formattedCount}} PDF prozesatuta." processedWithRate_one = "{{formattedCount}} PDF prozesatuta, bakoitza {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "Hileko gastu-mugara iritsi da" title = "Ezin izan da Stripe ataria ireki" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} doako PDFetatik erabilita" capSuffix_other = "{{allowance}} doako PDFetatik erabilita" eyebrow = "Processor proba" @@ -6534,6 +6756,376 @@ signature = "sinadura" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Konexio berria" +createTitleFor = "Konektatu {{name}}" +delete = "Ezabatu" +edit = "Editatu" +editTitle = "Editatu konexioa" +unsupportedType = "Konexio mota hau ezin da hemen editatu." + +[portal.connections.categories.advanced] +description = "Oraindik onartzen ez dugun zerbitzu baterako." +label = "Aurreratua" + +[portal.connections.categories.audit] +description = "Erregistratu dokumentu bati zer gertatu zaion, zure auditoreek dagoeneko begiratzen duten tokian." +label = "Auditoria eta betetze-erregistroa" + +[portal.connections.categories.notify] +description = "Jakinarazi beste sistema bati zerbait gertatu dela." +label = "Jakinarazpena eta lan-fluxua" + +[portal.connections.categories.security] +description = "Eskaneatu edo etiketatu dokumentu bat aurrera mugitu aurretik." +label = "Segurtasuna eta sailkapena" + +[portal.connections.categories.signing] +description = "Bidali dokumentu bat sinatzeko edo notario bidez egiaztatzeko." +label = "Sinadura elektronikoa eta notarizazioa" + +[portal.connections.categories.storage] +description = "Prozesatutako dokumentuak non amaitzen diren - bucket bat, drive bat, ticket bat edo orri bat." +label = "Fitxategia eta erantsi" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling-ek hau zifratuta gordetzen du eta ez du berriro erakusten." +label = "Sarbide-tokena" + +[portal.connections.commonFields.apiKey] +label = "API gakoa" + +[portal.connections.commonFields.apiToken] +label = "API tokena" + +[portal.connections.commonFields.baseUrl] +helperText = "Konexio hau erabiltzen duten urratsek URL honen azpiko bideak soilik dei ditzakete, inoiz ez beste host bat." +label = "Zerbitzariaren URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Pasahitza" + +[portal.connections.commonFields.port] +label = "Ataka" + +[portal.connections.commonFields.username] +label = "Erabiltzaile-izena" + +[portal.connections.commonFields.webhookUrl] +helperText = "Itsatsi zerbitzuak eman dizun URL. Haren azpiko bideak soilik dei daitezke." +label = "Webhook URL" + +[portal.connections.fields] +name = "Konexioaren izena" +namePlaceholder = "adib. Produkzioa" + +[portal.connections.picker] +cancel = "Utzi" +createNew = "Konexio berria..." +placeholder = "Hautatu konexio bat" +save = "Gorde konexioa" + +[portal.connections.picker2] +back = "Aldatu integrazioa" +heading = "Zer konektatu nahi duzu?" +noResultsBody = "Oraindik ez dugu onartzen - esan iezaguzu eta gehitzea aztertuko dugu." +noResultsTitle = "Ez dago bat datorren ezer: \"{{query}}\"" +searchPlaceholder = "Bilatu integrazioak - saiatu \"sign\", \"ocr\" edo produktu-izen batekin" + +[portal.connections.types.api] +description = "Deitu edozein HTTP API politika batetik. Zuk aukeratzen dituzu URL, autentifikazioa eta payload-a." +label = "API pertsonalizatua" + +[portal.connections.types.api.fields.authType] +label = "Autentifikazioa" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Erabiltzaile-izena eta pasahitza" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer tokena" + +[portal.connections.types.api.fields.authType.options.header] +label = "Tokena goiburu pertsonalizatu batean" + +[portal.connections.types.api.fields.authType.options.none] +label = "Bat ere ez" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Hasi saioa token bat lortzeko" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Konexio hau erabiltzen duten urratsek URL honen azpiko bideak soilik dei ditzakete, inoiz ez beste host bat." +label = "Oinarrizko URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Goiburuaren izena" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Saioa hasteko bidea" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Pasahitza" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "APIak deskarga-esteka batekin erantzuten badu, host hauek bakarrik eskuratu ahal izango dira. Bereizi komaz. Utzi hutsik oinarrizko URLaren host propioa bakarrik baimentzeko." +label = "Baimendutako emaitza-hostak" + +[portal.connections.types.api.fields.token] +label = "Tokena" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Bidali tokena goiburu honekin" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Saio-hasierako deiak tokena itzultzen duen erantzun-goiburua, adib. X-Auth-Token." +label = "Token-erantzunaren goiburua" + +[portal.connections.types.api.fields.username] +label = "Erabiltzaile-izena" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Eskaneatu dokumentuak malware bila zuk exekutatzen duzun zerbitzari batean - ezer ez da zure saretik ateratzen." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Dokumentuen birus- eta eduki-eskaneatzea politika batean." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokeatu makroak, exekutagarriak, scriptak edo txertatutako objektuak dituzten dokumentuak - ez bakarrik malware ezaguna." +label = "Cloudmersive eskaneatze aurreratua" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Erantsi prozesatutako dokumentuak Confluence orri bati." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Bidali dokumentuak ConsignO Cloud-era (Notarius) sinadura elektronikorako eta notarizaziorako." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Zure ConsignO Cloud ingurunea, /api/v1 barne." +label = "Zerbitzariaren URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API gakoa" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API sekretua" + +[portal.connections.types.consigno.fields.password] +label = "Pasahitza" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Zure erakundeak maizterrak erabiltzen baditu bakarrik behar da." +label = "Maizterra" + +[portal.connections.types.consigno.fields.username] +label = "Erabiltzaile-izena" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Bidali mezu bat Discord-era politika batek dokumentu bat prozesatzen duenean." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexatu auditoria-gertaera bat Elasticsearch-en politika batek dokumentu bat kudeatzen duen bakoitzean." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Kontsultatu FTP edo FTPS zerbitzari bat dokumentu berrien bila." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Konexio modua" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiboa" +passive = "Pasiboa (gomendatua)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP arruntak kredentzialak eta fitxategiak zifratu gabe bidaltzen ditu; aukeratu FTPS zerbitzariak onartzen badu." +label = "Zifratzea" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (esplizitua / AUTH TLS)" +implicit = "FTPS (inplizitua)" +none = "Bat ere ez (FTP arrunta)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Bidali mezu bat Google Chat espazio batera politika bat exekutatzen denean." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Erantsi prozesatutako dokumentuak Jira issue bati." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Bidali dokumentu bat edo jakinarazpen bat email bidez politika bat exekutatzen denean." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Gorde prozesatutako dokumentuak Nextcloud-en." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Aplikazioaren pasahitza" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detektatu datu pertsonalak dokumentuaren testuan zuk exekutatzen duzun Presidio zerbitzari bat erabiliz." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Irakurri eta aplikatu Microsoft Purview sentsibilitate-etiketak PDFetan." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Aukerakoa. Etiketak izenaren arabera hautatzeko bakarrik behar da; etiketatzeak hori gabe funtzionatzen du." +label = "Aplikazioaren (bezeroaren) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Bezero-sekretua" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Etiketa guztietan SiteId gisa zigilatzen da, Purview-rekin bateragarriak diren tresnek zein erakundek aplikatu duen jakin dezaten." +label = "Direktorioaren (maizterraren) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Iturriek irakurri eta pipeline-irteerek idatzi dezaketen S3 bucket bat." +label = "S3 biltegia" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Sarbide-gakoaren ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Utzi hutsik AWS-erako. Ezarri hau MinIO edo S3-rekin bateragarria den beste biltegi baterako." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Eskualdea" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Sarbide-gako sekretua" + +[portal.connections.types.sendgrid] +description = "Bidali dokumentu bat edo jakinarazpen bat email bidez politika bat exekutatzen denean." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Kontsultatu SFTP (SSH) zerbitzari bat dokumentu berrien bila." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Aukerakoa. Zerbitzariaren SHA-256 host-gakoaren fingerprint-a (`ssh-keyscan host | ssh-keygen -lf -` bidez). Ezarrita dagoenean, gako hau aurkezten duen zerbitzaria bakarrik onartzen da; hutsik dagoenean, lehen konexioan ikusitako gakoa finkatzen da eta geroagoko aldaketak ukatzen dira." +label = "Host-gakoaren fingerprint-a" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Gakoaren pasaesaldia" + +[portal.connections.types.sftp.fields.password] +helperText = "Utzi hutsik pasahitzaren ordez gako pribatu batekin autentifikatzen bazara." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Aukerakoa. Itsatsi OpenSSH gako pribatu bat pasahitzaren ordez gako batekin autentifikatzeko." +label = "Gako pribatua" + +[portal.connections.types.slack] +description = "Bidali mezu bat Slack-era politika batek dokumentu bat prozesatzen duenean." +label = "Slack" + +[portal.connections.types.smb] +description = "Monitorizatu SMB / CIFS sare-partekatze bat (Windows edo Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Sare-partekatzea" + +[portal.connections.types.smb.fields.domain] +helperText = "Aukerakoa. Windows domeinua edo lan-taldea, zure zerbitzariak bat erabiltzen badu." +label = "Domeinua" + +[portal.connections.types.smb.fields.share] +label = "Partekatzea" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Bidali auditoria-gertaera bat Splunk-era politika batek dokumentu bat kudeatzen duen bakoitzean." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Bidali auditoria-gertaera bat Sumo Logic HTTP collector batera." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Bidali mezu bat Teams kanal batera politika bat exekutatzen denean." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Bidali dokumentu bakoitza, edo xehetasunak bakarrik, zuk aukeratutako URL batera." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Abiarazi Zap edo Make eszenario bat politika batetik." +label = "Zapier eta Make" + +[portal.docs] +browse = "Arakatu dokumentazioa" +viewSource = "Ikusi iturburua GitHub-en" + [portal.docs.authentication] codeCaption = "eskaera bakoitza" eyebrow = "HASTEN" @@ -6624,6 +7216,9 @@ eyebrow = "TREBETASUNAK" lead = "Agenteak tresna bakar gisa deitzen dituen gaitasun integratu eta izendatuak. Trebetasun bakoitza ebaluazioak lotuta dituen eragiketa-kate determinista bat da." title = "Agente-trebetasunak" +[portal.docs.toc] +title = "Orri honetan" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API ERREFERENTZIA" @@ -6697,6 +7292,8 @@ type = "Mota" user = "Erabiltzailea" [portal.documents.queue.empty] +connectSource = "Konektatu iturri bat" +createPipeline = "Sortu pipeline bat" description = "Iturburuek dokumentuak zure pipelineetara bidali ahala, hemen agertuko dira berrikusteko." title = "Ez dago dokumenturik ilaran" @@ -6712,11 +7309,11 @@ editorAction = "Editorea" empty = "Ez dago iragazki honekin bat datorren dokumenturik." rowActions = "Errenkadako ekintzak" sensitiveLabel = "Sentikorra" -sensitiveTitle = "Sentikorra — sarbidea behar da" [portal.documents.table.columns] action = "Pipelinea / Ekintza" document = "Dokumentua" +labels = "Labels" product = "Produktua" status = "Egoera" time = "Ordua" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Azken aldiz ikusita" region = "Eskualdea" status = "Egoera" +target = "Target" version = "Bertsioa" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Ikuspegi honek ustekabeko errore bat izan du. Saiatu berriro, edo retry = "Saiatu berriro" title = "Zerbait gaizki atera da orri honetan" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Atzera" +body = "Deskargatu mahaigaineko aplikazioa edo egin zerbitzaria self-hosted." +done = "Eginda" +guide = "Ireki {{name}} gida" +openInBrowser = "Ireki arakatzailean ordez" +sectionDesktop = "Mahaigaineko aplikazioa" +sectionSelfHosted = "Self-hosted" +title = "Instalatu Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Exekutatu irudi ofiziala zure zerbitzarian." +note = "Irekitzen da :8080 atakan. Saio-hasiera lehenetsia: admin / stirling." +tagline = "Exekutatu zerbitzaria edukiontzi batean" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instalatu Helm chart ofiziala." +note = "Gehitu PVC bat /configs-erako ezarpenak mantentzeko." +tagline = "Zabaldu kluster batean Helm erabiliz" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Zure banaketarako build natiboak." +guideBtn = "Ikusi Linux instalazio-aukerak" +note = "AppImage, .deb eta .rpm build-ak eskuragarri daude ohiko distroetarako." +tagline = "Zure distrorako mahaigaineko aplikazio natiboa" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Edo Homebrew bidez" +detailBody = "Exekutatu instalatzailea, edo erabili Homebrew." +downloadBtn = "Deskargatu macOS-erako" +tagline = "Apple silicon eta Intel-erako aplikazio natiboa" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Exekutatu .jar bare metal zerbitzari batean." +downloadBtn = "Deskargatu .jar" +note = "Java 21+ behar du. :8080-n irekitzen da; saio-hasiera lehenetsia admin / stirling." +runLabel = "Ondoren exekutatu" +tagline = "Exekutatu .jar bare metal zerbitzari batean" +title = "Eskuzko zerbitzari-konfigurazioa" + +[portal.home.download.windows] +altLabel = "Edo winget bidez" +detailBody = "Exekutatu instalatzailea, edo erabili pakete-kudeatzaile bat." +downloadBtn = "Deskargatu instalatzailea" +tagline = "Microsoft Windows-erako aplikazio natiboa" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktibo" -invite = "Gonbidatu taldekideak" +activeOfDeployed = "{{active}} / {{total}} aktibo hilabete honetan" name = "Stirling PDF Editor" open = "Ireki arakatzailean" updated = "eguneratua {{time}}" +[portal.home.editor.deploy] +finish = "Amaitu zabalkundea" +options = "Ikusi zabalkunde-aukerak" +start = "Zabaldu editorea" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Norberak ostatatua · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Arratsalde on" evening = "Gabon" morning = "Egun on" -[portal.home.onboarding] -dismiss = "Baztertu konfigurazioa" -notStarted = "Hasi gabe" -progress = "{{done}} / {{total}} eginda" -title = "Amaitu konfigurazioa" - -[portal.home.onboarding.enterprise] -body = "Erakunde osoko SSO + SCIM + RBAC, konprometitutako bolumen-prezioak eta sarez isolatutako hedapena." -cta = "Hasi proba" -ctaQuote = "Eskatu aurrekontua" -lead = "250+ langilerentzat." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Hedatu mahaigaineko aplikazioa, betiko doan." -title = "Deskargatu PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktibo · {{recommended}} gomendatu" -chip = "{{active}} aktibo" -title = "Aktibatu gidalerroak" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} konektatuta · PDF bakoitza gobernatuta iristen den tokian" -chip = "{{connected}} konektatuta" -title = "Konektatu zure iturburuak" - -[portal.home.quickActions] -subtitle = "Gaurko zeregin nagusiak" -title = "Ekintza azkarrak" - -[portal.home.quickActions.buildPipeline] -blurb = "3 urratseko konposatzailea idatzitako eragiketa-liburutegiaren gainean" -title = "Eraiki pipeline bat" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenteak, webhooks, zaindutako karpetak" -title = "Konektatu iturburu bat" - -[portal.home.quickActions.issueApiKey] -blurb = "Esparru mugatuko gakoa, tasa-mugekin eta IP baimen-zerrendarekin" -title = "Jaulki API gako bat" - [portal.infrastructure] manageEditorDeployment = "Kudeatu Editorearen hedapena" sectionsAriaLabel = "Azpiegitura-atalak" @@ -6878,13 +7541,9 @@ heading = "API gakoak" subheading = "Esparru mugatuko kredentzialak, gako bakoitzeko tasa-mugekin, baimenekin eta IP baimen-zerrendekin." [portal.infrastructure.apiKeys.card] -allowedIps = "Baimendutako IPak" -anyIp = "Edozein IP (baimen-zerrendarik gabe)" created = "Sortuta" lastUsed = "Azken erabilera" -permissions = "Baimenak" -rateLimit = "Tasa-muga" -rateLimitValue = "{{value}} eskaera/min" +revoke = "Ezeztatu gakoa" usageMonth = "Hilabete honetako erabilera" usageToday = "Gaurko erabilera" @@ -6892,6 +7551,15 @@ usageToday = "Gaurko erabilera" description = "Sortu esparru mugatuko gako bat Stirling API deitzen hasteko." title = "Oraindik ez dago API gakorik" +[portal.infrastructure.apiKeys.error] +load = "Ezin izan dira zure API gakoak kargatu. Saiatu berriro." + +[portal.infrastructure.apiKeys.revoke] +body = "Ezeztatu \"{{name}}\"? Gako hau oraindik erabiltzen duen edozein integrazio berehala hasiko da 401 erroreak jasotzen. Ezin da desegin." +cancel = "Utzi" +confirm = "Ezeztatu gakoa" +title = "Ezeztatu API gakoa" + [portal.infrastructure.audit] filterAriaLabel = "Iragazi auditoria-gertaerak kategoriaren arabera" heading = "Auditoria-erregistroak" @@ -6935,6 +7603,7 @@ all = "Guztiak" auth = "Auth" config = "Konfig." elevation = "Sarbide-igoera" +policy = "Politika" processing = "Prozesamendua" security = "Segurtasuna" @@ -6944,19 +7613,30 @@ title = "Ez duzu sarbiderik" [portal.infrastructure.audit.metrics] config = "Konfig." -elevation = "Sarbide-igoera" +policy = "Politika" processing = "Prozesamendua" totalEvents = "Gertaerak guztira · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Autentifikazioa" +config = "Konfigurazioa" +elevation = "Goratzea" +policy = "Politika" +processing = "Prozesatzea" +security = "Segurtasuna" + +[portal.infrastructure.auditStatusLabel] +danger = "Errorea" +info = "Informazioa" +success = "Arrakasta" +warning = "Abisua" + [portal.infrastructure.createKey] cancel = "Utzi" createKey = "Sortu gakoa" done = "Eginda" -ipAllowlistHelper = "Komaz bereizitako CIDR barrutiak. Utzi hutsik edozein IP baimentzeko." -ipAllowlistLabel = "IP baimen-zerrenda" keyNameLabel = "Gakoaren izena" keyNamePlaceholder = "adib. Ekoizpena · ingestioa" -permissionsLabel = "Baimenak" secretKeyCaption = "Gako sekretua" secretWarning = "Gorde hau sekretuen kudeatzaile batean. Stirling-ek hash bat baino ez du gordetzen — ez dago gero berreskuratzeko modurik." subtitle = "Mugatu gakoa behar duen gutxienekora. Edonoiz biratu edo baliogabetu dezakezu." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopiatu sekretu hau orain — ez da berriro erakutsiko." title = "Sortu API gakoa" titleCreated = "Gakoa sortuta" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Hedatu duena" -environment = "Ingurunea" -product = "Produktua" -status = "Egoera" -version = "Bertsioa" -when = "Noiz" - -[portal.infrastructure.deployments.recent] -heading = "Azken hedapenak" -subheading = "Produktu eta inguruneetako azken zabalkundeak." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instantziak" -latency = "Latentzia" -load = "Karga" -p99 = "P99" -region = "Eskualdea" -status = "Egoera" -throughput = "Errendimendua" -uptime = "Erabilgarritasun-denbora" -version = "Bertsioa" - -[portal.infrastructure.deployments.regions] -heading = "Eskualdeak" -subheading = "Hedatutako Stirling eskualde bakoitzaren zuzeneko osasuna — latentzia, karga eta zabalkunde-bertsioa." - -[portal.infrastructure.deployments.regions.empty] -description = "Hedatutako eskualdeak hemen agertuko dira zure lan-eremua hornitu ondoren." -title = "Ez dago eskualderik hedatuta" - -[portal.infrastructure.models] -heading = "Modeloak" -msValue = "{{value}} ms" -subheading = "Zure lan-eremuan dokumentuen prozesamendua elikatzen duten modelo-katalogoa eta bideratzea." - -[portal.infrastructure.models.byom] -description = "Erregistratu on-prem edo norberak ostatatutako modelo bat eta lotu eskualde bati datu-egoitzak mugatutako prozesamendurako." -title = "Ekarri zure modeloa" - -[portal.infrastructure.models.catalogue] -heading = "Katalogoa" -sub = "Zure lan-eremurako eskuragarri dauden kudeatutako modeloak, zuzeneko latentziarekin eta kostuarekin." -subEnterprise = "Kudeatutako, norberak ekarritako eta on-prem modeloak — eskualdeka lotzeko aukerarekin." - -[portal.infrastructure.models.catalogue.empty] -description = "Zure lan-eremuko katalogoko modeloak hemen agertuko dira." -title = "Ez dago modelorik erabilgarri" - -[portal.infrastructure.models.columns] -cost = "Kostua" -latency = "Latentzia" -load = "Karga" -model = "Modeloa" -status = "Egoera" -type = "Mota" -version = "Bertsioa" - -[portal.infrastructure.models.metrics] -activeModels = "Modelo aktiboak" -avgLatency = "Batez besteko latentzia" -included = "Barne" -monthlySpend = "Hileko modelo-gastua" - -[portal.infrastructure.models.routing] -empty = "Ez dago bideratze-arau konfiguraturik." -heading = "Bideratze-arauak" -sub = "Zein modelok kudeatzen duen eragiketa bakoitza. Lehenetsia aplikatzen da arau zehatzagorik bat ez datorrenean." -subLocked = "Bideratu eragiketak modelo zehatzetara — ordaindutako planetan eskuragarri." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Berritu Pro planera eragiketa eta dokumentu mota bakoitza zein modelok kudeatzen duen kontrolatzeko." -title = "Modeloen bideratzea ordaindutako eginbidea da" - -[portal.infrastructure.models.routingColumns] -default = "Lehenetsia" -docType = "Dokumentu mota" -modelForAria = "{{operation}} eragiketarako modeloa" -operation = "Eragiketa" -routedTo = "Hona bideratua" - -[portal.infrastructure.security.access.byok] -description = "Eman zure KMS propioaren gako bat. Stirling-ek harekin zifratzen du, baina oraindik irakur dezake." -label = "Ekarri zure gakoa (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Gakoak ez dira inoiz zure KMS-tik irteten. Stirling-ek zifratu-testua baino ez du gordetzen." -label = "Gorde zure gakoa (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling-ek zifratze-gakoak kudeatzen ditu. Errazena — zure aldetik gako-eragiketarik gabe." -label = "Stirling-ek gordetako gakoak" - -[portal.infrastructure.security.accessPolicy] -heading = "Dokumentuetarako sarbide-gidalerroa" -subheading = "Atsedenean dauden prozesatutako dokumentuak nork deszifratu ditzakeen kontrolatzen du." - -[portal.infrastructure.security.attestations] -heading = "Betetze-egiaztapenak" -noReport = "Ez dago txostenik eskuragarri" -subheading = "Esparruz esparruko auditoria-jarrera, egiaztatutako kontroletan txostenak eskuragarri direla." -viewReport = "Ikusi txostena →" - -[portal.infrastructure.security.compliance] -heading = "Betetzea" -subheading = "Stirling plataforma estaltzen duten egiaztapenak eta ziurtagiriak." - -[portal.infrastructure.security.empty] -description = "Zure lan-eremuko segurtasun-konfigurazioa hemen agertuko da." -title = "Segurtasun-jarrera ez dago eskuragarri" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK erabiliz, zifratze-gakoak ez dira inoiz zure KMS-tik irteten. Stirling-ek edozein unetan baliogabetu dezakezun zifratu-testua baino ez du gordetzen eta prozesatzen." -title = "Stirling-ek ezin ditu zure dokumentuak deszifratu" - -[portal.infrastructure.security.ipAllowlist] -empty = "Ez dago IP barrutirik konfiguratuta — IP guztiak baimenduta daude." -heading = "IP baimen-zerrenda" -sub = "API sarbidea CIDR barruti hauetara mugatuta dago." -subLocked = "Mugatu API sarbidea IP barruti ezagunetara — ordaindutako planetan eskuragarri." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Berritu Pro planera API sarbidea sare zehatzetara mugatzeko." -title = "IP baimen-zerrendak ordaindutako eginbidea dira" - -[portal.infrastructure.security.ipColumns] -added = "Gehituta" -addedBy = "Gehitu duena" -cidr = "CIDR" -label = "Etiketa" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmoa" -heading = "Zifratze-gakoen kudeaketa" -keyId = "Gako-identifikatzailea" -lastRotated = "Azken biraketa" -rotateKey = "Biratu gakoa" -rotationPolicy = "Biraketa-gidalerroa" -subheading = "Atsedenean dokumentuak zifratzen dituzten gakoen zaintza — nork deszifratu dezakeen eta gakoak nola biratzen diren." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) eta hold-your-own-key (HYOK) zaintza Enterprise planean daude eskuragarri. Berritu zure KMS propioko gakoak emateko." -title = "Zure planean, gakoak Stirling-ek kudeatzen ditu" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pazifikoa" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR datu-muga" -label = "Europar Batasuna" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Estatu Batuak" - -[portal.infrastructure.security.residencyHeader] -heading = "Datu-egoitza" -subheading = "Dokumentuak non gordetzen eta prozesatzen diren." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} erabilita" - -[portal.infrastructure.storage.empty] -description = "Konektatutako biltegiratzea eta erabilera hemen agertuko dira." -title = "Ez dago biltegiratzerik konfiguratuta" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktibo" -activeRange = "0–{{value}}d" -archived = "Artxibatuta" -coldStorage = "biltegiratze hotza" -deleted = "Ezabatuta" -never = "inoiz ez" -purged = "garbituta" - -[portal.infrastructure.storage.providers] -connect = "Konektatu" -connected = "Konektatuta" -heading = "Konektatutako hornitzaileak" -subheading = "Prozesatutako artefaktuak non idazten diren." - -[portal.infrastructure.storage.retention] -heading = "Atxikipena" -subheading = "Artefaktuak bizi-zikloko ezabaketaren aurretik zenbat denboraz mantentzen diren." -windowLabel = "Atxikipen-leiho lehenetsia" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} egun" -days_other = "{{count}} egun" -never = "Ez ezabatu inoiz" - -[portal.infrastructure.storage.totalUsage] -heading = "Erabilera guztira" -progressLabel = "Erabilitako biltegiratzea" -subheading = "Konektatutako hornitzaile guztietan kontsumitutako biltegiratzea." +revoked = "Ezeztatuta" [portal.infrastructure.tabs] apiKeys = "API gakoak" @@ -7174,6 +7656,49 @@ models = "Modeloak" security = "Segurtasuna" storage = "Biltegiratzea" +[portal.integrations] +availableHeading = "Eskuragarri" +comingSoonHeading = "Laster" +connect = "Konektatu" +connectionCount_one = "{{count}} konexio" +connectionCount_other = "{{count}} konexio" +customApi = "API pertsonalizatua" +searchPlaceholder = "Bilatu integrazioak" +subtitle = "Stirling-ek komunikatu dezakeen guztia. Kredentzialak behin sortzen dira hemen, eta gero iturburu, politika eta pipeline-ek erreferentziatzen dituzte." +title = "Integrazioak" +typedName = "{{tool}} izena" + +[portal.integrations.filters] +all = "Guztiak" +audit = "Auditoria" +connected = "Konektatuta" +notify = "Jakinarazi" +security = "Segurtasuna" +signing = "Sinatzea" +storage = "Biltegiratzea" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Konektatuta" + +[portal.integrations.table] +integration = "Integrazioa" +worksWith = "Honekin funtzionatzen du" + +[portal.integrations.worksWith] +pipelines = "Pipeline-ak" +policies = "Politikak" +sources = "Iturburuak" + +[portal.legal] +draft = "{{label}} · zirriborroa" +loadError = "Ezin izan da dokumentu hau kargatu. Saiatu berriro." +loading = "Kargatzen…" +title = "Dokumentu juridikoa" + [portal.nav] agent-builder = "Agente-eraikitzailea" components = "Osagaiak" @@ -7182,33 +7707,17 @@ documents = "Dokumentuak" editor = "Editorea" home = "Hasiera" infrastructure = "Azpiegitura" +integrations = "Integrazioak" pipelines = "Pipelineak" policies = "Gidalerroak" procurement = "Erosketak" -settings = "Ezarpenak" sources = "Iturburuak" usage = "Erabilera eta fakturazioa" users = "Erabiltzaileak" -[portal.notifications] -markAllRead = "Markatu guztiak irakurrita" -title = "Jakinarazpenak" -viewAll = "Ikusi guztiak" - -[portal.notifications.ariaLabel] -none = "Jakinarazpenak, irakurri gabekorik ez" -unread_one = "Jakinarazpenak, {{count}} irakurri gabe" -unread_other = "Jakinarazpenak, {{count}} irakurri gabe" - -[portal.notifications.count] -allRead = "guztiak irakurrita" -loading = "kargatzen" -new_one = "{{count}} berri" -new_other = "{{count}} berri" - -[portal.notifications.empty] -description = "Ez dago jakinarazpen berririk." -title = "Dena eguneratuta duzu" +[portal.nav.section] +platform = "PDF plataforma" +processor = "PDF prozesadorea" [portal.pipelines] subtitle = "Backend-eko dokumentu-pipeline automatizatu guztiak: iturburu multzo baten gaineko eragiketa-kate ordenatua, abiarazle batek exekutatua. Egin klik errenkada batean haren urratsak eta iturburuak ikusteko." @@ -7218,54 +7727,87 @@ title = "Pipelineak" newPipeline = "Pipeline berria" [portal.pipelines.builder] -addStep = "Gehitu tresna" +activate = "Activate" back = "Itzuli pipelineetara" +cannotFollow = "Ezin du {{produced}} hartu" +chooseAccount = "Aukeratu kontu bat" +chooseDestination = "Aukeratu helmuga bat" +chooseOperation = "Aukeratu urrats honek zer egiten duen" +chooseSource = "Aukeratu iturburu bat" discard = "Baztertu aldaketak" -enabled = "Gaituta" +inputs = "Sarrera" +inputSource = "Sarrera-iturburua" +inputTrigger = "Abiarazlea" keepEditing = "Jarraitu editatzen" -needsUpload = "Kargatutako fitxategi bat behar du" +moreActions = "More actions" +needsConfiguring = "Konfiguratu behar da" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Ez dago zure bilaketarekin bat datorren tresnarik." -pipelineSettings = "Pipelinearen ezarpenak" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Bilatu tresnak" -selectToolBody = "Gehitu tresna bat zure pipelinea eraikitzeko." -selectToolTitle = "Oraindik ez dago tresnarik" -toolSettings = "Tresnaren ezarpenak" +sendToSystem = "Bidali beste sistema batera" +stepsIncompatible = "Urrats hauek ezin dira exekutatu aurreko urratsak sortzen duenarekin: {{tools}}." +stepsNeedSetup = "Urrats hauek oraindik konfiguratu behar dira gorde aurretik: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Eragiketa ezezaguna, bere horretan mantendu da." unsavedBody = "Gorde gabeko aldaketak dituzu. Gorde irten aurretik, edo baztertu?" unsavedTitle = "Gorde gabeko aldaketak" -uploadUnsupported = "Kargatutako fitxategiak oraindik ez dira onartzen pipelineetan; beraz, urrats hauek ezin dira gorde: {{tools}}." usesDefaults = "Ezarpen lehenetsiekin exekutatzen da" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Aurreko urratseko fitxategi guztiak konbinatzen ditu" +fan-out = "Aurreko urratseko fitxategi bakoitzeko behin exekutatzen da" +format-mismatch = "{{accepts}} behar du, baina aurreko urratsak {{produced}} sortzen du" +output-uncertain = "Baliteke ez exekutatzea: aurreko urratsaren irteera konfigurazioaren araberakoa da" +source-mismatch = "{{accepts}} behar du, baina pipeline honen sarrera {{produced}} da" +undeclared-operation = "Ezin da egiaztatu urrats honek zer onartzen duen" [portal.pipelines.composer] addTool = "Gehitu tresna" -cancel = "Utzi" -chainEmpty = "Gehitu tresna bat zure pipelinea eraikitzen hasteko." create = "Sortu pipelinea" -directory = "Irteera-karpeta" -directoryHelp = "Zerbitzariko bide absolutua. Konfiguratutako baimendutako karpeten barruan egon behar du." +createPaused = "Create paused" editingUnsupported = "Tresna-parametro hauek editatzeko bistaratzea oraindik ez da onartzen." -moveDown = "Mugitu behera" -moveUp = "Mugitu gora" +editSource = "Edit source" name = "Izena" namePlaceholder = "adib. Erredakzio-ekorketa" -noSources = "Oraindik ez dago iturbururik konektatuta. Pipelinea zuzenean emandako fitxategietan exekuta daiteke hala ere." noToolSettings = "Tresna honek ez du ezarpen konfiguragarririk." -operations_one = "Eragiketa ({{count}})" -operations_other = "Eragiketak ({{count}})" output = "Irteera" -removeStep = "Kendu eragiketa" save = "Gorde aldaketak" scheduleEvery = "Exekutatu behin" -sources = "Iturburuak" -sourcesLoading = "Iturburuak kargatzen..." trigger = "Abiarazlea" triggerManual = "Eskuz bakarrik" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "egun" hours = "ordu" minutes = "minutu" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Ezabatu \"{{name}}\"? Hau ezin da desegin." cancel = "Utzi" @@ -7273,28 +7815,62 @@ confirm = "Ezabatu" title = "Ezabatu pipelinea?" [portal.pipelines.detail] +clearHistory = "Garbitu historia" delete = "Ezabatu pipelinea" run = "Exekutatu orain" [portal.pipelines.empty] action = "Sortu pipeline bat" +connectSource = "Konektatu iturburu bat" description = "Sortu zure lehen pipelinea: aukeratu zein iturburutan exekutatuko den, kateatu eragiketak eta aukeratu irteera nora joango den." title = "Oraindik ez dago pipelinerik" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktibo" paused = "Pausatuta" total = "Pipelineak" -[portal.pipelines.output] -folder = "Idatzi karpetara" -inline = "Itzuli fitxategiak" - [portal.pipelines.run] +allProcessed_one = "Ez dago exekutatzeko ezer: iturburuko {{count}} dokumentua dagoeneko prozesatu da." +allProcessed_other = "Ez dago exekutatzeko ezer: iturburuetako {{count}} dokumentu guztiak dagoeneko prozesatu dira." completed_one = "Exekuzioa osatu da." completed_other = "{{count}} exekuzio guztiak osatu dira." empty = "Ez dago exekutatzeko ezer: iturburuek ez zuten prozesatzeko dokumenturik." failed = "Exekuzioak huts egin du: {{error}}" +inFlight = "Ez dago exekutatzeko ezer berririk: dokumentuak oraindik aurreko exekuzio batetik prozesatzen ari dira." +parked_one = "Ez dago exekutatzeko ezer: {{count}} dokumentuk huts egin zuen lehenago eta aparkatuta dago. Konpondu arrazoia, eta gero garbitu historia berriro saiatzeko." +parked_other = "Ez dago exekutatzeko ezer: {{count}} dokumentuk huts egin zuten lehenago eta aparkatuta daude. Konpondu arrazoia, eta gero garbitu historia berriro saiatzeko." running = "Exekuzioa hasi da; oraindik martxan dago." timeout = "Exekuzioa espero baino gehiago luzatzen ari da; baliteke atzeko planoan amaitzea." @@ -7307,6 +7883,7 @@ name = "Pipelinea" sources = "Iturburuak" status = "Egoera" steps = "Urratsak" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Karpetaren zaintza" @@ -7314,12 +7891,18 @@ manual = "Eskuzkoa" schedule = "Programatua" [portal.policies] +defaultName = "{{category}} politika" subtitle = "Dokumentu bakoitzean tresna-pipeline bat betearazten duten automatizazio iraunkorrak. Gidalerro bakoitza kargatzean edo esportatzean aktibatzen da, bere tresna-katea exekutatzen du eta betearazitako bertsioa jatorrizkoaren alboan gordetzen du." title = "Gidalerroak" [portal.policies.card] comingSoon = "Berritu Enterprise planera" notSetUp = "Konfiguratu gabe" +requiresAiEngine = "AI motorra behar du" + +[portal.policies.categories.classification] +desc = "Identifikatu dokumentu bakoitzaren mota igotzean eta etiketatu haren metadatuak artxibatzeko eta bilatzeko." +label = "Sailkapena" [portal.policies.categories.compliance] desc = "Betearazi HIPAA, GDPR, SOC 2 edo FedRAMP eskakizunak dokumentu bakoitzean." @@ -7344,6 +7927,13 @@ label = "Segurtasuna" [portal.policies.config] scopeAll = "Dokumentu guztiak" +[portal.policies.config.classification] +summary = "Igotako dokumentu guztiak zure taldearen etiketa multzoaren arabera sailkatzen ditu eta haien metadatuak etiketatzen ditu." + +[portal.policies.config.classification.rules] +0 = "Sailkatu" +1 = "Etiketatu metadatuak" + [portal.policies.config.compliance] summary = "Dokumentuak arau-esparruen arabera baliozkotzen ditu sistematik irten aurretik." @@ -7371,6 +7961,29 @@ minConfidence = "Gutxieneko konfiantza" 2 = "Izena" 3 = "Normalizatu" +[portal.policies.config.purview] +label = "Aplikatu Microsoft Purview sentikortasun-etiketa bat" + +[portal.policies.config.purview.fields] +connection = "Purview tenant-a" +labelId = "Etiketa ID" +labelIdHelp = "Etiketaren GUID-a, Purview ataritik. Etiketa bat aplikatzeak haren metadatuak PDF-ean idazten ditu; ezin du zifratu, horretarako Microsoft bezeroa behar da." +labelName = "Etiketaren izena (aukerakoa)" +labelNameHelp = "ID-arekin batera erregistratzen da, pertsonek eta tresnek etiketa ezagut dezaten." + +[portal.policies.config.purview.fields.method] +help = "Purview-ek bereizten du politika batek aplikatutako etiketa eta pertsona batek aukeratutakoa." +label = "Etiketa nola erregistratzen den" +privileged = "Pertsona batek aukeratu bezala" +standard = "Automatikoki aplikatu bezala" + +[portal.policies.config.purviewRead] +label = "Irakurri Microsoft Purview etiketa bat" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant-a" +connectionHelp = "Dokumentuak dagoeneko daraman etiketa irakurtzen du, ondorengo urratsek haren arabera jardun dezaten." + [portal.policies.config.retention] summary = "Dokumentuak zenbat denboraz mantentzen diren, noiz artxibatu eta noiz ezabatu betearazten du." @@ -7438,6 +8051,7 @@ title = "Oraindik ez dago jarduerarik" [portal.policies.endpoints] addWatermark = "Ur-marka" autoRedact = "PII erredaktatu" +classifyAndLabel = "Sailkatu" compressPdf = "Konprimitu" flatten = "Lautu" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Zure politikak gordeta daude eta konexioa leheneratzen denean age retry = "Saiatu berriro" title = "Backend ez dago erabilgarri" +[portal.policies.operations] +change = "Aldatu urrats honek zer egiten duen" +noResults = "Ez dago horrekin bat datorren urratsik. Saiatu produktu-izen batekin, edo \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Bilatu urratsak - saiatu \"scan\", \"attach\" edo produktu-izen batekin" + +[portal.policies.operations.bodyMode] +binary = "Fitxategi-byte gordinak" +json = "JSON karga baten barruan" +multipart = "Fitxategi-igoera gisa (multipart)" + +[portal.policies.operations.clamavScan] +description = "Zure ClamAV zerbitzari propioak dokumentua egiaztatzen du; ezer ez da zure saretik ateratzen." +label = "Eskaneatu birusen bila (autoostatatua)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Makroak, exekutagarriak, script-ak edo txertatutako objektuak dituzten dokumentuak baztertzen ditu." +label = "Blokeatu eduki aktiboa" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive-k dokumentua egiaztatzen du eta exekuzioa gelditzen da garbi ez badago." +label = "Eskaneatu birusen bila" + +[portal.policies.operations.confluenceAttach] +description = "Prozesatutako dokumentua orri batean artxibatzen du." +label = "Erantsi Confluence orri bati" + +[portal.policies.operations.consignoSubmit] +description = "Dokumentua ConsignO Cloud-era bidaltzen du sinatzeko." +label = "Bidali sinatzeko (ConsignO)" +note = "Dokumentua bidaltzen du. Sinatutako kopia eskuratzea oraindik ez dago onartuta, urrats batek ezin diolako emaitzarik pasa ondorengo urrats bati." + +[portal.policies.operations.customApiCall] +description = "Sortu deia zuk zeuk: path, method, body eta erantzunaren kudeaketa." +label = "Deitu API pertsonalizatu bati" + +[portal.policies.operations.discordNotify] +description = "Kanal bati jakinarazten dio politikak dokumentu bat kudeatu duela." +label = "Argitaratu mezu bat Discord-en" + +[portal.policies.operations.elasticIndex] +description = "Politikak egindakoa dokumentu bilagarri gisa erregistratzen du." +label = "Indexatu auditoria-gertaera bat Elasticsearch-en" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart egokia da igoera-API-etarako; JSON egokia da fitxategia karga baten barruan nahi duten API-etarako; binary-k byte gordinak bidaltzen ditu." +label = "Dokumentua nola bidali" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Bere horretan bidalitako JSON body bat. {{document.base64}} fitxategia darama; {{document.*}} eta {{run.*}} dokumentu bakoitzeko betetzen dira." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Kontua" + +[portal.policies.operations.fields.domain] +label = "Mailgun domeinua" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API-ak dokumentua jasotzea espero duen formulario-eremua." +label = "Fitxategi-eremuaren izena" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Nork" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Goiburukoen izen/balio bikoteen JSON objektu bat. Konexioaren kredentziala zuretzat gehitzen da." +label = "Goiburukoak" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Mezuaren gorputz gisa bidaltzen da. {{document.*}} eta {{run.*}} dokumentu bakoitzeko betetzen dira." +label = "Mezua" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Orri ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Konexioaren oinarrizko URL-ari eransten zaio. Ezin da inoiz beste host batera iritsi." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Unitatean non idatziko den. {{document.filename}} dokumentu bakoitzeko betetzen da." +label = "Helmuga-path" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Txostenak dokumentua aldatu gabe uzten du. Ordezteak itzultzen denarekin trukatzen du." +label = "Zer egin erantzunarekin" + +[portal.policies.operations.fields.signerEmail] +label = "Sinatzailearen emaila" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Gaia" +placeholder = "Prozesatuta: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio-k testu hau aztertzen du. Erabili {{document.title}} edo itsatsi egiaztatu nahi duzun testua." +label = "Egiaztatu beharreko testua" + +[portal.policies.operations.fields.to] +label = "Nori" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud erabiltzaile-izena" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Espazio bati jakinarazten dio politikak dokumentu bat kudeatu duela." +label = "Argitaratu mezu bat Google Chat-en" + +[portal.policies.operations.jiraAttach] +description = "Prozesatutako dokumentua issue bati eransten dio." +label = "Erantsi Jira issue bati" + +[portal.policies.operations.mailgunEmail] +description = "Prozesatutako dokumentua eranskin gisa bidaltzen du." +label = "Bidali dokumentua email bidez (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Prozesatutako dokumentua karpeta batean idazten du." +label = "Igo Nextcloud-era" + +[portal.policies.operations.presidioAnalyze] +description = "Zure Presidio zerbitzari propioak aurkitzen dituen datu pertsonalak jakinarazten ditu." +label = "Detektatu datu pertsonalak" +note = "Presidio-k testua irakurtzen du, ez fitxategiak. Eman egiaztatu nahi duzun testua — dokumentua bera ez da igotzen." + +[portal.policies.operations.responseMode] +replace = "Ordeztu dokumentua erantzunarekin" +report = "Mantendu dokumentua, erregistratu erantzuna" + +[portal.policies.operations.sendgridEmail] +description = "Prozesatutako dokumentua eranskin gisa bidaltzen du." +label = "Bidali dokumentua email bidez (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Kanal bati jakinarazten dio politikak dokumentu bat kudeatu duela." +label = "Argitaratu mezu bat Slack-en" + +[portal.policies.operations.splunkEvent] +description = "Politikak egindakoa erregistratzen du, zure auditoreek dagoeneko begiratzen duten tokian." +label = "Bidali auditoria-gertaera bat Splunk-era" + +[portal.policies.operations.sumologicEvent] +description = "Politikak HTTP collector bati egindakoa erregistratzen du." +label = "Bidali auditoria-gertaera bat Sumo Logic-era" + +[portal.policies.operations.teamsNotify] +description = "Kanal bati jakinarazten dio politikak dokumentu bat kudeatu duela." +label = "Argitaratu mezu bat Teams-en" + +[portal.policies.operations.webhookPost] +description = "Dokumentua bera zuk aukeratutako URL batera bidaltzen du." +label = "Argitaratu dokumentua webhook batean" + +[portal.policies.operations.zapierNotify] +description = "Exekuzioaren xehetasunak zure automatizazioari ematen dizkio." +label = "Abiarazi Zap edo Make eszenario bat" + [portal.policies.stats] activeFor = "Aktibo" dataProcessed = "Prozesatutako datuak" @@ -7473,6 +8282,13 @@ label = "Betearazitako dok." description = "Konfiguratuta baina ez da abiarazten" label = "Pausatuta" +[portal.policies.table] +appliesTo = "Honi aplikatzen zaio" +docs = "Betearazitako dokumentuak" +enforces = "Betearazten du" +policy = "Politika" +status = "Egoera" + [portal.policies.wizard.actions] back = "Atzera" cancel = "Utzi" @@ -7480,6 +8296,10 @@ continue = "Jarraitu" enablePolicy = "Gaitu politika" saveChanges = "Gorde aldaketak" +[portal.policies.wizard.capability.classify] +desc = "Dokumentuaren mota identifikatzen du zure taldearen etiketetatik eta etiketatu egiten du, kategoriaren arabera artxibatu eta bilatu dadin." +label = "Sailkatu dokumentua" + [portal.policies.wizard.capability.compress] desc = "Dokumentua konprimitu fitxategi-tamaina txikiagoa izan dezan." label = "Murriztu fitxategiaren tamaina" @@ -7500,10 +8320,18 @@ label = "Erredaktatu informazio sentikorra" desc = "Ezkutuko JavaScript kentzen du, dokumentua irekitzean ezer automatikoki exekuta ez dadin." label = "Kendu eduki aktiboa" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dokumentua une jakin batean forma zehatz honetan existitzen zela frogatzen du, denbora-zigilu autoritate independente bat erabiliz. Hash bat bakarrik bidaltzen da - dokumentua ez da inoiz zure zerbitzaritik ateratzen." +label = "Gehitu denbora-zigilu fidagarri bat" + [portal.policies.wizard.capability.watermark] desc = "Marka ikusgarri bat jartzen du (adib. “Konfidentziala”) orri guztietan." label = "Aplikatu ur-marka" +[portal.policies.wizard.classification] +description = "Igotako dokumentu guztiak barneko etiketen arabera sailkatzen dira eta egokitzen zaizkien motekin etiketatzen dira. Etiketa multzoa zure talde osoan partekatzen da." +labelsHeading = "Sailkapen-etiketak" + [portal.policies.wizard.errors] noTools = "Gaitu gutxienez tresna bat lan-fluxuan lehenik." saveFailed = "Ezin izan da politika gorde. Saiatu berriro." @@ -7548,73 +8376,118 @@ setUp = "Konfiguratu {{category}} politika" [portal.policies.wizard.workflow] description = "Aukeratu politika honek zer egiten dion prozesatzen duen dokumentu bakoitzari." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktibo" -noRule = "Oraindik ez da araurik betearazi" -subtitle = "Dokumentu bakoitzak zeharkatzen dituen automatizazio iraunkorrak, hura zein pipeline-k kudeatzen duen kontuan hartu gabe." -title = "Zer exekutatzen den zure PDFetan" - [portal.policySummary.action] -comingSoon = "Eguneratu Enterprise-ra" -configure = "Konfiguratu" setUp = "Konfiguratu" -[portal.policySummary.column] -activeRule = "Arau aktiboa" -policy = "Politika" -status = "Egoera" +[portal.processorFlow] +footnote = "Kontaketak azken 24 orduetakoak dira. Fluxuaren abiadura adierazgarria da." +liveBadge = "Zuzenean" +stats = "{{connected}} konektatuta · {{processed}} PDF prozesatuta" +title = "PDF prozesadorea" -[portal.policySummary.empty] -description = "Politikak konfiguratu ondoren, kategoriak hemen agertuko dira." -title = "Oraindik ez dago politikarik" +[portal.processorFlow.lens] +ariaLabel = "Ikuspegi modua" +flow = "Fluxua" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktibo" -off = "Desaktibatuta" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Huts eginda" +heading = "Emaitzak" +success = "Entregatuta" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktibo" +count = "{{n}} · 24h" +heading = "Politikak" +setUp = "Konfiguratu" soon = "Laster" -[portal.processingStatus] -managePlan = "Kudeatu plana" -volumeSuffix = "Prozesatutako PDFak · azken 30 egunak" +[portal.processorFlow.sankey] +waist = "Politikak · {{n}} aktibo" + +[portal.processorFlow.sankey.empty] +description = "Konektatu iturburu bat eta aktibatu politika bat dokumentuak prozesadorean zehar nola dabiltzan ikusteko." +title = "Oraindik ez dago fluxurik" + +[portal.processorFlow.sources] +comingSoonTag = "Konektatu" +editor = "Stirling PDF editorea" +heading = "Iturburuak" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email sarrera" [portal.procurement] -reset = "Berrezarri erosketa (demo)" subtitle = "Lortu zure taldea ebaluatzea, kontratatzea eta martxan jartzea. Dokumentu guztiak leku bakarrean." title = "Erosketa" -[portal.procurement.action] -download = "Deskargatu" -pay = "Ordaindu orain" -request = "Eskatu" -sign = "Berrikusi eta sinatu" -upload = "Kargatu" - [portal.procurement.agreement] agreeCta = "Adostu eta harpidetu" +confidential = "Konfidentziala" confirm = "Stirling Enterprise Agreement irakurri dut eta ados nago." -eyebrow = "Akordioa" -intro = "Akordio bateratu batek zure tratua estaltzen du: Master Service Agreement, Order Form, EULA eta Data Processing Agreement. Berrikusi, eta gero adostu aurrekontua harpidetza konprometitu gisa onartzeko." -title = "Berrikusi zure enpresa-akordioa" +docName = "Stirling Enterprise akordioa" +docSub = "MSA, Order Form, EULA eta DPA, sinadura bakarrean konbinatuta." +download = "Deskargatu" +downloadDraftError = "Ezin izan da akordioaren PDF-a sortu - dokumentu-errendatzailea ez dago erabilgarri. Saiatu berriro laster edo jarri harremanetan laguntzarekin." +downloadError = "Ezin izan da sinatutako akordioa deskargatu. Saiatu berriro." +legalName = "Entitate juridikoaren izena" +legalNamePlaceholder = "Sinatuko duen entitate juridikoa" +loadError = "Ezin izan da akordioa kargatu. Saiatu berriro." +loading = "Akordioa kargatzen..." +ref = "Erref. {{ref}} · {{version}}" +requestChanges = "Eskatu aldaketak" +scrollHint = "Korritu berrikusteko" +signatory = "Sinatzailearen izena" +signatoryPlaceholder = "Izen-abizenak" +signatoryTitle = "Sinatzailearen kargua" +signatoryTitlePlaceholder = "adib. Aholkulari juridiko nagusia" +signError = "Ezin izan da zure sinadura erregistratu. Saiatu berriro." [portal.procurement.builder] addons = "Gehigarriak" +addressLine1 = "Helbidearen 1. lerroa" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Helbidearen 2. lerroa" +addressLine2Placeholder = "Suitea, solairua, eraikina" back = "Atzera" businessName = "Enpresaren izena" businessNamePlaceholder = "Zure enpresa" +city = "Hiria" +cityPlaceholder = "San Francisco" +completeRequired = "Osatu derrigorrezko eremuak (* markatuta) baliozko email batekin aurrekontua sortu aurretik." +contactEmail = "Harremanetarako emaila" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Harremanetarako izena" +contactNamePlaceholder = "Jane Doe" continue = "Jarraitu" -country = "Herrialdea" -countryEuro = "Eurogunea (EUR €)" -countryUK = "Erresuma Batua (GBP £)" -countryUS = "Estatu Batuak (USD $)" +done = "Eginda" eula = "Stirling Enterprise EULA irakurri dut eta ados nago. Aurrekontu honetatik sortutako akordioa arautzen du." generate = "Sortu aurrekontua" -included = "Barne" indemnification = "IP kalte-ordaina" indemnificationSub = "EULAren arabera, IP erreklamazio kalifikatuak defendatzen ditugu" -offlineLicense = "Offline / air-gapped lizentzia" -offlineLicenseSub = "Air-gapped self-hosted instantzia baterako deskarga daitekeen lizentzia-fitxategia" +paperEyebrow = "Enterprise aurrekontua" +paperFor = "Honentzat prestatua" +pdfSize = "PDF tamaina" +poNumber = "PO zenbakia" +poNumberPlaceholder = "Aukerakoa" +postalCode = "Posta-kodea" +postalCodePlaceholder = "94105" +posture = "Gobernantza" +posture_count = "~{{count}} politika" +posture_essentials = "Oinarrizkoak" +posture_essentialsSub = "Sailkapena + Partekatzea — lehenetsiak" +posture_governed = "Gobernatua" +posture_governedSub = "Segurtasuna eta bideratzea gehitzen ditu" +posture_regulated = "Arautua" +posture_regulatedSub = "Kategoria guztiak — Betetzea, Atxikipena, Ingestioa" qbr = "Hiruhileko negozio-berrikuspenak" qbrSub = "Zure SEk erabilera eta roadmap-a berrikusten ditu hiruhileko bakoitzean" +region = "Estatua / eskualdea" +regionPlaceholder = "California" running = "{{annual}} / urte · {{years}} urteko {{tcv}}" s1Sub = "Zure taldea eta urtero exekutatzea espero dituzun PDFak." s1Title = "Zenbat prozesatuko duzu?" @@ -7623,6 +8496,12 @@ s2Title = "Konpromisoa eta zerbitzua" s3Sub = "Aurrekonturako eta horrek sortzen duen akordiorako." s3Title = "Zure datuak" serviceLevel = "Zerbitzu-maila" +size_compact = "Trinkoa" +size_compactSub = "Gehienbat testua, 1 MB baino gutxiago" +size_heavy = "Astuna" +size_heavySub = "Eskaneatua edo irudi askokoa, 5 MB+" +size_standard = "Standard" +size_standardSub = "Testu eta irudi mistoak, 1 eta 5 MB artean" slDedicated = "Dedikatua" slDedicatedSub = "4 lanordu · kontu-kudeatzaile dedikatua · +30%" slPriority = "Lehentasunezkoa" @@ -7630,6 +8509,8 @@ slPrioritySub = "Lanegun berean · izendatutako CSM · +15%" slStandard = "Estandarra" slStandardSub = "Hurrengo laneguna · partekatutako CSM · barne" stepOf = "{{total}} urratsetatik {{n}}." +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Aukerakoa" term = "Epea" termDiscount = "{{pct}}% urte anitzeko konpromiso-deskontua aplikatuta" title = "Eraiki zure aurrekontua" @@ -7637,6 +8518,7 @@ training = "Onboarding eta prestakuntza" trainingSub = "Zuzeneko saioak zure taldea martxan jartzeko" users = "Erabiltzaileak guztira" usersPlaceholder = "adib. 250" +viewEula = "Irakurri" volEstimated = "{{count}} erabiltzaileren arabera kalkulatua (~2.000 PDF bakoitzeko, automatizazioa barne). Editatu hobeto badakizu." volManual = "Zure kopurua erabiltzen. Berriro kalkulatu zure taldearen tamainaren arabera nahi duzunean." volNoUsers = "Ez zaude ziur? Sartu zure taldearen tamaina eta guk kalkulatuko dugu." @@ -7645,55 +8527,55 @@ volumePlaceholder = "adib. 1.000.000" years_one = "{{count}} urte" years_other = "{{count}} urte" -[portal.procurement.docs] -count_one = "{{count}} dok." -count_other = "{{count}} dok." -done = "Eginda" -here = "Hemen zaude" -hide = "Ezkutatu" -optional = "Aukerakoa" -paidAddon = "Ordainpeko gehigarria" -show = "Erakutsi" -subtitle = "Ibilbideko urrats bakoitzean behar duzun guztia, tratua aurrera doan heinean agertuta." -supportingSubtitle = "SOC 2, segurtasun-berrikuspenak, zerga-inprimakiak eta gehiago, zure segurtasun- edo erosketa-taldeak eskatzen duenean prest. Batzuek behin bakarrik ordaintzeko kuota dute." -supportingTitle = "Zure ebaluazioari euskarria ematen" +[portal.procurement.documents] +agreement = "Enterprise akordioa" +agreementSub = "MSA, Order Form eta DPA sinadura bakarrean." +download = "Deskargatu" +eula = "EULA eta merkataritza-baldintzak" +eulaSub = "Softwarea eta autozerbitzuko merkataritza-baldintzak." +invoice = "Faktura" +invoiceSub = "Zure lehen harpidetza-faktura." +laterInvoice = "Onartu ondoren eskuragarri" +laterQuote = "Zure aurrekontua jaulki ondoren eskuragarri" +quote = "Aurrekontua" +quoteSub = "Zure enterprise aurrekontu xehatua." +sla = "SLA eranskina" +slaSub = "Laguntza-mailak eta erantzun-helburuak." +subprocessors = "Azpiprozesadoreak" +subprocessorsSub = "Zure izenean datuak prozesatzen dituzten hirugarrenak." +subtitle = "Zure akordioaren dokumentazioa, edozein unetan eskuragarri." title = "Dokumentuak" -upcoming = "Datozenak" +view = "Ikusi" [portal.procurement.error] title = "Zerbait gaizki joan da" [portal.procurement.hero] -company = "Zure enpresa-tratua" +barAria = "{{current}}. etapa {{total}}(e)tik" ctaAgreement = "Berrikusi eta sinatu akordioa" -ctaLive = "Martxan zaude" -ctaPayment = "Gehitu ordainketa" +ctaExploring = "Konfiguratu zure proba" ctaQuote = "Berrikusi zure aurrekontua" +ctaReviewQuote = "Berrikusi aurrekontua" ctaTrial = "Eraiki zure aurrekontua" +documents = "Dokumentuak" eyebrow = "Enterprise erosketa" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Gonbidatu taldekideak" -keyDocs = "Dokumentu nagusiak" -nextStep = "Hurrengo urratsa: {{action}}" -notStarted = "Hasi gabe" +licenseKey = "Lizentzia-gakoa" +liveSub = "Zure erakundea hornituta dago eta zure lizentzia aktibo dago." +liveTitle = "Stirling Enterprise martxan duzu" +next = "Hurrengoa: {{stage}}" open = "Ireki erosketa" scheduleCall = "Programatu dei bat" -setup1Sub = "Gonbidatu zure taldekideak" -setup1Title = "Hedatu PDF Editor" -setup2Sub = "Aktibatu prozesamendua editoreetan eta beste iturburu batzuetan" -setup2Title = "Konektatu PDF Processor" -setup3Sub = "Aktibatu Security, Compliance, Routing edo Retention behar dituzunean" -setup3Title = "Gehitu gomendatutako politikak" +sentenceAgreement = "berrikusi eta sinatu akordioa" +sentenceLive = "hornidura eta zabalkundea" +sentencePayment = "erosketa-agindua eta ordainketa" +sentenceQuote = "berrikusi eta onartu zure aurrekontua" +sentenceTrial = "konfiguratu zure proba-laneko gunea" [portal.procurement.journey] daysLeft_one = "{{count}} egun geratzen da" daysLeft_other = "{{count}} egun geratzen dira" -engineerLabel = "Zure solutions engineer" -eyebrow = "Zure hedapena" -live = "Stirling Enterprise-n martxan zaude" -nextStep = "Hurrengo urratsa: {{action}}" -subtitle = "Zure solutions engineer urrats guztietan dago. Hurrengo ekintza bat aldi berean; kontrol-zerrenda osoa behean dago." -title = "Probalditik martxan egotera, bide gidatu bakarra" -trialTitle = "Enterprise probaldia" [portal.procurement.journeySteps.agreement] blurb = "Sinadura batek MSA, order form, EULA eta DPA estaltzen ditu." @@ -7727,6 +8609,9 @@ downloadError = "Oraindik ezin izan da offline lizentzia-fitxategia sortu — sa downloadOffline = "Deskargatu offline lizentzia (.lic)" hint = "Itsatsi gako hau self-hosted instantzia batean aktibatzeko, edo gorde zure erregistroetarako. Gorde seguru." label = "Zure lizentzia-gakoa" +subtitle = "Aktibatu autoostatatutako instantzia bat gako honekin, edo gorde zure erregistroetarako." +title = "Zure lizentzia-gakoa" +trialFileHint = "Hau zure proba-lizentziaren fitxategia da. Zure akordioa indarrean dagoenean, itzuli eta deskargatu berriro — online lizentzia-gakoa ez bezala, .lic fitxategia ez da berez eguneratuko." [portal.procurement.link] cta = "Lotu kontua" @@ -7739,65 +8624,62 @@ description = "Zure harpidetza aktibo dago eta zure lizentzia jaulkita dago. Zur eyebrow = "Martxan" title = "Stirling Enterprise-n martxan zaude" -[portal.procurement.locked] -description = "Probaldi-gakoak, bolumen konprometituaren aurrekontuak, sinadura bakarreko akordioa, ordainketa eta zure dokumentu-liburua hemen egongo dira enterprise ebaluazioa hasten duzunean." -eyebrow = "Enterprise soilik" -talkToSales = "Hitz egin salmentekin" -title = "Erosketa-bidea Enterprise-rekin irekitzen da" - [portal.procurement.milestone] -accept = "Onartu eta jarraitu" -description = "Deskargatu PDFa zure taldearekin partekatzeko, itzuli prest zaudenean onartzera edo egin aldaketak." -download = "Deskargatu PDF" downloadError = "Oraindik ezin izan da aurrekontuaren PDFa deskargatu — saiatu berriro une batean." -edit = "Editatu aurrekontua" -eyebrow = "{{number}} aurrekontua" -perYear = " / urte" -preparedFor = "{{company}}-rentzat prestatua" -tcv = "{{value}} kontratuaren balio osoa" -title = "Zure aurrekontua prest dago" - -[portal.procurement.modal] -cancel = "Utzi" -chooseFile = "Aukeratu fitxategia" -close = "Itxi" -downloadBody = "Zure deskarga laster hasiko da." -downloadCta = "Deskargatu" -downloadTitle = "Deskargatu" -noFile = "Ez da fitxategirik hautatu" -payBody = "Ordaindu zure kontratu konprometitua txartelarekin edo banku-transferentziaz Stripe bidez. Zure lan-eremua ordainketa garbitu bezain laster hornituko da." -payCta = "Jarraitu Stripe-ra" -payTitle = "Berretsi ordainketa" -requestBodyFree = "Eskariaren arabera sortzen dugu. Berretsi eta zure solutions engineer-ek laster bidaliko dizu." -requestBodyPaid = "Hau ordainpeko gehigarria da. Berretsi eta zure solutions engineer-ek irismena zehaztu eta dokumentazioa bidaliko du." -requestCta = "Eskatu" -requestTitle = "Eskatu dokumentu hau" -signBody = "Stirling Enterprise Agreement irekitzen du e-sinadurarako: sinadura batek MSA, order form, EULA eta DPA estaltzen ditu. Guk automatikoki kontrassinatu eta ordainketara aurreratzen zara." -signCta = "Ireki sinatzeko" -signTitle = "Berrikusi eta sinatu zure akordioa" -uploadBody = "Bidali zure PO eta haren kontra fakturatuko dizugu zure baldintzetan. Arrastatu PDFa edo aukeratu fitxategi bat behean." -uploadCta = "Kargatu erosketa-agindua" -uploadTitle = "Kargatu zure erosketa-agindua" [portal.procurement.payment] description = "Zure aurrekontua onartuta dago eta zure lizentzia dagoeneko aktibo dago — zure taldea berehala has daiteke. Ordaindu lehen faktura prest zaudenean; hemen ordaindu edo deskarga dezakezu, emailik behar gabe." +downloadAgreement = "Deskargatu sinatutako akordioa" downloadInvoice = "Deskargatu faktura" -simulate = "Simulatu ordainketa jasota (demo)" +eyebrow = "Ordainketa" title = "Harpidetza sortuta" viewInvoice = "Ikusi eta ordaindu faktura" +[portal.procurement.review] +acceptCta = "Onartu aurrekontua" +annual = "Urteko kuota (1. urtea)" +downloadCta = "Deskargatu aurrekontua" +poNumber = "Erosketa-agindua: {{po}}" +renewal = "{{amount}}/urtean berritzen da epearen ondoren (+{{pct}}% CPI)" +tcv = "{{years}} urteko epea, osorik aurrez ordaindua: {{tcv}}" +validUntil = "Baliozkoa {{date}} arte" + [portal.procurement.schedule] fallback = "Ezin izan da programatzailea kargatu." fallbackLink = "Ireki programazioa fitxa berri batean" +loading = "Programatzailea kargatzen..." subtitle = "Zure solutions engineer-ek hedapenean zehar gidatuko du zure taldea. Aukeratu ondo datorkizun ordua." title = "Programatu dei bat" -[portal.procurement.status] -action = "Ekintza behar da" -available = "Erabilgarri" -complete = "Osatuta" -pending = "Zain" -request = "Eskatuta" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Zigilatua, irteerarik gabe" +back = "Atzera" +businessName = "Enpresaren izena" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Kudeatua, hasiera azkarrena" +continue = "Jarraitu" +deployment = "Non zabaldu behar da?" +eula = "Stirling EULA eta merkataritza-baldintzak onartzen ditut." +fullName = "Izen-abizenak" +fullNamePlaceholder = "Zure izena" +invites = "Gonbidatu taldekideak · aukerakoa" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Programatu dei bat" +seats = "Probatzen ari diren pertsonak" +seatsPlaceholder = "adib. 15" +selfhost = "Autoostatatua" +selfhostSub = "Docker edo Kubernetes" +start = "Hasi proba" +stepOf = "{{n}}. urratsa {{total}}(e)tik" +subtitle = "Doan 14 egunez, txartelik gabe." +subtitleDetails = "Xehetasun batzuk zure aurrekontu eta akordiorako." +talkFirst = "Nahiago duzu lehenik hitz egin?" +title = "Konfiguratu zure proba" +viewEula = "Irakurri" +workEmail = "Laneko emaila" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Luzatzeak 7 egun gehitzen ditu eta zure solutions engineer-i jakinarazten dio." @@ -7808,109 +8690,113 @@ maxed = "Mugara iritsita" subtitle = "Zure doako probaldia {{date}} arte doa. Ez da txartelik behar." title = "Enterprise probaldia" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Bolumen konprometituaren prezioak, erakunde osorako SSO + SCIM + RBAC, 90 eguneko auditoretza aldaezina eta SE dedikatua." -homeCta = "Hasi probaldia →" -homeHeadline = "Prozesatu milioika PDF." - -[portal.recentActivity] -title = "Azken jarduera" -viewAll = "Ikusi dena" - -[portal.recentActivity.empty] -description = "Pipeline exekuzioak, hedapenak eta agente-gertaerak hemen agertuko dira." -title = "Oraindik ez dago ezer hemen" - -[portal.search] -ariaLabel = "Bilatu" -placeholder = "Bilatu Stirling — endpointak, pipelineak, dokumentuak…" - -[portal.search.empty] -noActionsDescription = "Ekintza azkarrak hemen agertuko dira erabilgarri daudenean." -noActionsTitle = "Ez dago ekintza azkarrik" -noMatches = "Ez dago bat-etortzerik \"{{query}}\"-rentzat" -noMatchesDescription = "Saiatu beste gako-hitz batekin edo arakatu katalogoa." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Kontu-lotura" -[portal.shell.header] -accountFallback = "Kontua" -accountMenu = "Kontu-menua" -darkMode = "Modu iluna" -lightMode = "Modu argia" -search = "Bilatu" -searchPlaceholder = "Bilatu…" -signOut = "Amaitu saioa" -switchToDark = "Aldatu gai ilunera" -switchToLight = "Aldatu gai argira" - [portal.shell.sidebar] appEditor = "Editorea" appProcessor = "Prozesadorea" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dok." linkAccount = "Lotu Stirling kontua" -planEditor = "Editor plana" -planEnterprise = "Enterprise plana" -planProcessor = "Processor plana" primaryNav = "Nabigazio nagusia" switchApp = "Aldatu aplikazioa" +[portal.shell.topbar] +closeNav = "Itxi nabigazioa" +openNav = "Ireki nabigazioa" +search = "Bilatu" + [portal.sources] subtitle = "Stirling-era dokumentuak elikatzen dituzten sarrera-konexio berrerabilgarriak. Konfiguratu konexio bat behin, eta erreferentziatu nahi adina politikatan. Egin klik errenkada batean haren konfigurazioa eta zein politikek erabiltzen duten ikusteko." title = "Iturburuak" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Konektatu iturburua" +[portal.sources.builder] +back = "Itzuli iturburuetara" +backToTypes = "Iturburu mota guztiak" +cancel = "Utzi" +chooseHint = "Aukeratu dokumentuak nondik datozen. Grisez dauden konektoreak bidean daude." +comingSoon = "Laster" +comingSoonHeading = "Laster" +create = "Sortu iturburua" +createTitle = "Konektatu iturburu bat" +delete = "Ezabatu" +editTitle = "Editatu iturburua" +enabled = "Gaituta" +save = "Gorde aldaketak" + +[portal.sources.builder.folderAccess] +description = "Karpeta-automatizazioek administratzaileak baimendutako karpetak bakarrik erabil ditzakete. Gehitu Folder Access ezarpenetan, eta saiatu berriro." +openSettings = "Folder Access ezarpenak" +title = "Karpeta hau ez dago baimenduta" + [portal.sources.delete] body = "Ezabatu \"{{name}}\"? Hau ezin da desegin. Hura erreferentziatzen duten politikak eguneratu beharko lirateke." cancel = "Utzi" confirm = "Ezabatu" title = "Ezabatu iturburua?" -[portal.sources.detail] -closeAriaLabel = "Itxi xehetasuna" -delete = "Ezabatu iturburua" -docs24h = "Azken 24 h" -docs30d = "Azken 30 egunak" -docsTotal = "Ikusitakoak guztira" -docsTrend = "Azken 30 egunetako dokumentuak" -documents = "Dokumentuak" -edit = "Editatu" -notReferenced = "Ez du politikak erreferentziatzen; beraz, seguru ezaba daiteke." -pause = "Pausatu" -resume = "Berrekin" -subtitle = "{{type}} · {{status}}" -usedBy = "Honek erabilia" - -[portal.sources.empty] -description = "Konektatu karpeta bat (eta, laster, hodeiko biltegiratzea), zure politikek dokumentuak nondik hartu izan dezaten." -title = "Oraindik ez dago iturbururik konektatuta" - [portal.sources.kpi] inUse = "Erabiltzen" total = "Konexioak" unused = "Erabili gabe" +[portal.sources.networkFields.connection] +helperText = "Zerbitzariaren helbidea eta kredentzialak dituen gordetako konexioa. Erreferentzia egiten dion iturburu bakoitzak berrerabiltzen du." +label = "Konexioa" + +[portal.sources.networkFields.directory] +helperText = "Zerbitzarian kontsultatu beharreko karpeta, saio-hasierako home-arekiko edo partekatze-erroarekiko erlatiboa. Utzi hutsik errorako." +label = "Karpeta" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Kontsumitzeak fitxategi bakoitza zerbitzaritik kentzen du politika guztiek prozesatu ondoren." +label = "Irakurketa modua" + +[portal.sources.networkFields.mode.options] +consume = "Kontsumitu: prozesatu fitxategi bakoitza behin" +snapshot = "Snapshot: berrirakurri karpeta exekuzio bakoitzean" + +[portal.sources.networkFields.recursive] +label = "Karpetaren sakonera" + +[portal.sources.networkFields.recursive.options] +all = "Sartu azpikarpetak" +top = "Goiko maila bakarrik" + [portal.sources.status] active = "Aktibo" disabled = "Desgaituta" unused = "Erabili gabe" [portal.sources.table] +documents = "Dokumentuak" source = "Iturburua" status = "Egoera" +type = "Type" usedBy = "Politikak" +[portal.sources.types.box] +description = "Ekarri dokumentuak zure taldeak dagoeneko partekatzen dituen Box karpetetatik." +label = "Box" + +[portal.sources.types.dropbox] +description = "Ekarri dokumentuak partekatutako Dropbox karpeta batetik." +label = "Dropbox" + [portal.sources.types.editor] +description = "Zure taldeak editorean prozesatutako dokumentuak, politika eta AI exekuzioetan zehar." label = "Editorea" +noPolicies = "Oraindik ez da politikarik exekutatu editoretik." + +[portal.sources.types.email] +description = "Bihurtu dedikatutako postontzi batera bidalitako eranskinak dokumentu." +label = "Email sarrera-ontzia" [portal.sources.types.folder] description = "Begiratu zerbitzariko direktorio bat dokumentu berriak detektatzeko." @@ -7943,25 +8829,80 @@ label = "Karpetaren sakonera" all = "Sartu azpikarpetak" top = "Goiko maila soilik" +[portal.sources.types.ftp] +description = "Kontsultatu FTP edo FTPS zerbitzari-karpeta bat dokumentu berrien bila." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Ekarri dokumentuak partekatutako Google Drive karpetetatik." +label = "Google Drive" + +[portal.sources.types.network] +description = "Zaindu SMB sare-partekatze bat dokumentu berrien bila." +label = "Sare-unitatea" + +[portal.sources.types.onedrive] +description = "Zaindu OneDrive karpetak dokumentu berrien bila." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Ekarri dokumentuak Amazon S3 edo S3 bateragarri den bucket batetik." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Bucket-a eta kredentzialak dituen gordetako konexioa. Erreferentzia egiten dion iturburu eta pipeline irteera bakoitzak berrerabiltzen du." +label = "Konexioa" + +[portal.sources.types.s3.fields.mode] +helperText = "Kontsumitzeak objektu bakoitza bucket-etik kentzen du politika guztiek prozesatu ondoren." +label = "Irakurketa modua" + +[portal.sources.types.s3.fields.mode.options] +consume = "Kontsumitu: prozesatu objektu bakoitza behin" +snapshot = "Snapshot: berrirakurri bucket-a exekuzio bakoitzean" + +[portal.sources.types.s3.fields.prefix] +helperText = "Aurrizki honekin hasten diren gakoak dituzten objektuak bakarrik prozesatzen dira." +label = "Gako-aurrizkia" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Kontsultatu SFTP drop karpeta bat dokumentu berrien bila." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Ekarri dokumentuak SharePoint dokumentu-liburutegietatik." +label = "SharePoint" + [portal.sources.types.unknown] label = "Iturburua" +[portal.sources.types.webhook] +createNote = "Bidalketa-URL bat eta sinadura-sekretu bat sortzen dira webhook hau sortzen duzunean. Bidaltzaileek dokumentuak URL horretara POST bidez bidaltzen dituzte eta erreferentziatzen duten politikak iristean exekutatzen dira." +description = "Jaso dokumentuak sinatutako HTTP POST bidez edozein kanpoko sistematik." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Bidalketa-URL" +secretNote = "Sinadura-sekretua behin erakusten da, webhook-a sortzen denean. Iturburua berriro sortu aldatzeko." + +[portal.sources.types.webhook.reveal] +copy = "Kopiatu" +done = "Eginda" +secret = "Sinadura-sekretua" +secretHelp = "Sinatu bidalketa bakoitzaren gorputz gordina gako honekin (HMAC-SHA256) eta bidali X-Stirling-Signature goiburu gisa." +secretWarning = "Kopiatu sinadura-sekretua orain. Zure segurtasunerako behin bakarrik erakusten da eta ezin da gero berreskuratu." +title = "Webhook sortuta" +url = "Bidalketa-URL" +usage = "Bidali dokumentu bakoitza POST bidez eskaeraren gorputz gordin gisa bidalketa-URL-ra eduki mota bitar batekin (application/pdf edo application/octet-stream). Erreferentzia egiten duten politikak automatikoki exekutatzen dira iristean." + [portal.sources.wizard] -back = "Atzera" -cancel = "Utzi" -continue = "Jarraitu" -editTitle = "Editatu iturburua" name = "Izena" namePlaceholder = "adib. Erreklamazioen sarrera" -save = "Gorde aldaketak" -subtitle = "{{total}} urratsetatik {{current}} · {{label}}" -title = "Konektatu iturburu bat" type = "Mota" -[portal.sources.wizard.steps] -chooseType = "Aukeratu mota" -configure = "Konfiguratu" -review = "Berrikusi eta konektatu" +[portal.stepModal] +close = "Itxi" [portal.usage] managePayment = "Kudeatu ordainketa" @@ -8021,16 +8962,9 @@ summary = "Talde baten jabea da — kideen baliabideak eta partekatutako konfigu 2 = "Atarirako sarbidea politika lehenetsiaren bidez" 3 = "Member-ek egin dezakeen guztia" -[portal.welcome] -ariaLabel = "Ongi etorri Stirling PDF-ra" -badge = "Open-source" -installEditor = "Instalatu editorea" -inviteTeammates = "Gonbidatu taldekideak" -openInBrowser = "Ireki arakatzailean" -perks = "Doan betiko · Self-hostable" -subtitle = "Munduko PDF Editor seguruena doakoa da tamaina guztietako taldeentzat. 60+ PDF eragiketa eta SSO barne." -title = "Ongi etorri" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Mugagabea" [printFile] title = "Inprimatu fitxategia" @@ -8613,6 +9547,14 @@ text = "Biratu zure PDF orrialdeak erlojuaren noranzkoan edo kontrakoan 90 gradu [rotate.tooltip.header] title = "Biraketa ezarpenen ikuspegi orokorra" +[ruler] +clearAll = "Garbitu dena" +hideAllLabels = "Ezkutatu etiketa guztiak" +hideSmallLabels = "Ezkutatu etiketa txikiak" +physicalValues = "Fisikoa" +scaled = "Eskalatua" +showAllLabels = "Erakutsi etiketa guztiak" + [sanitize] desc = "Kaltegarriak izan daitezkeen elementuak kendu PDF fitxategietatik." sanitizationResults = "Saneatze-emaitzak" @@ -8654,31 +9596,45 @@ label = "XMP metadatuak kendu" [sanitize.steps] settings = "Ezarpenak" -[sanitizePdf] -tags = "garbitu,seguru,segurtasuna,mehatxuak-kendu" - [sanitizePDF] title = "PDF-a desinfektatu" +[sanitizePdf] +tags = "garbitu,seguru,segurtasuna,mehatxuak-kendu" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Doitu orrialdearen eskala" +[scaleSettings] +activeScale = "Eskala aktiboa" +apply = "Aplikatu eskala" +applyCalibration = "Aplikatu kalibrazioa" +calculatedScale = "Kalkulatutako eskala" +calibrate = "Kalibratu" +calibrating = "Kalibratzen" +calibrationDistanceRequired = "Sartu zero baino handiagoa den mundu errealeko distantzia bat" +calibrationInvalid = "Ezin da eskala kalkulatu neurketa honetatik" +calibrationPaperDistance = "Neurtutako orrialdeko distantzia: {{distance}}" +calibrationTitle = "Kalibratu eskala" +calibrationTooltip = "Neurtu distantzia ezagun bat eskala automatikoki kalkulatzeko" +customScale = "Eskala pertsonalizatua" +noneSet = "Ez da eskala pertsonalizaturik ezarri" +presets = "Aurrez ezarritako eskalak" +ratio = "Eskala-ratioa" +ratioHelp = "Ratioa: orrialde-unitate 1 = mundu errealeko X unitate" +ratioPlaceholder = "adib., 100" +ratioPositive = "Ratioak zero baino handiagoa izan behar du" +ratioRequired = "Ratioa beharrezkoa da" +realDistance = "Mundu errealeko distantzia" +realDistancePlaceholder = "adib., 5" +reset = "Berrezarri lehenetsietara" +unit = "Unitatea" +unitInvalid = "Unitate baliogabea: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Angeluaren gutxieneko balioa:" -10 = "Ezarri gehitutako eta ezabatutako ertzaren tamaina irteeran ertz zuriak saihesteko (lehenetsia: 1)." -2 = "Ezarri eskatutako gutxieneko angelu absolutua irudia biratzeko (lehenetsia: 10)." -3 = "Tolerantzia:" -4 = "Ezarri kalkulatutako atzeko kolorearen inguruko kolorearen aldakuntza tartea (lehenetsia: 30)." -5 = "Gutxieneko area:" -6 = "Ezarri arearen gutxieneko balioa argazki batentzat (lehenetsia: 10000)." -7 = "Inguruko area gutxienekoa:" -8 = "Ezarri inguruko arearen gutxieneko balioa argazki batentzat" -9 = "Ertzaren tamaina:" - [scannerImageSplit] submit = "Irudi-eskaneoak erauzi" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Zer egiten duen" whatThisDoesDesc = "Automatikoki aurkitu eta erauzten du orri eskaneatu edo irudi konposatu bateko argazki bakoitza—eskuzko mozketarik gabe." whenToUse = "Noiz erabili" +[ScannerImageSplit.selectText] +1 = "Angeluaren gutxieneko balioa:" +10 = "Ezarri gehitutako eta ezabatutako ertzaren tamaina irteeran ertz zuriak saihesteko (lehenetsia: 1)." +2 = "Ezarri eskatutako gutxieneko angelu absolutua irudia biratzeko (lehenetsia: 10)." +3 = "Tolerantzia:" +4 = "Ezarri kalkulatutako atzeko kolorearen inguruko kolorearen aldakuntza tartea (lehenetsia: 30)." +5 = "Gutxieneko area:" +6 = "Ezarri arearen gutxieneko balioa argazki batentzat (lehenetsia: 10000)." +7 = "Inguruko area gutxienekoa:" +8 = "Ezarri inguruko arearen gutxieneko balioa argazki batentzat" +9 = "Ertzaren tamaina:" + [search] noResults = "Ez da emaitzarik aurkitu" placeholder = "Idatzi bilaketa-terminoa..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Zure Stirling-PDF zerbitzaria lineaz kanpo dago eta \ [session] expired = "Zure saioa iraungi da. Freskatu orria eta saiatu berriro." -[sessionManagement.tooltip] -header = "Sinatze-saioen kudeaketa" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Parte-hartzaile berriak sinadura-ordenaren amaieran gehitzen dira" -bullet2 = "Ezin dira parte-hartzaileak gehitu saioa amaitu ondoren" -bullet3 = "Parte-hartzaile bakoitzak jakinarazpena jasotzen du bere txanda denean" -description = "Saio aktibo batera parte-hartzaile gehiago gehi ditzakezu edozein unetan amaitu aurretik." -title = "Parte-hartzaileak gehitzea" - -[sessionManagement.tooltip.finalization] -bullet1 = "Amaiera osoa: Parte-hartzaile guztiek sinatu dute" -bullet2 = "Amaiera partziala: Parte-hartzaile batzuek oraindik ez dute sinatu" -bullet3 = "Sinatu gabe dauden parte-hartzaileak ez dira azken dokumentuan sartuko" -bullet4 = "Behin amaituta, sinatutako PDFa fitxategi aktiboetan kargatu dezakezu" -description = "Amaitzeak sinadura guztiak PDF sinatu bakarrean konbinatzen ditu. Ekintza hau ezin da desegin." -title = "Saioaren amaiera" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Ezin dira sinatu duten parte-hartzaileak kendu" -bullet2 = "Kendutako parte-hartzaileek ez dute gehiago jakinarazpenik jasoko" -bullet3 = "Sinadura-ordena automatikoki doitzen da" -description = "Parte-hartzaileak ken daitezke sinatu aurretik." -title = "Parte-hartzaileak kentzea" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Sinadura bakoitza sekuentzialki aplikatzen da PDFari" -bullet2 = "Beranduago sinatzen dutenek aurreko sinadurak ikus ditzakete" -bullet3 = "Funtsezkoa da onarpen-fluxuetarako eta zaintza-kate legaletarako" -description = "Saioa sortzean zehazten duzun ordenak zein sinatuko duen lehenik ezartzen du." -title = "Sinadura-ordena" - [settings] close = "Itxi" +[settings.ai] +documents = "Dokumentuak eta RAG" +general = "Orokorra" +limits = "Mugak eta errendimendua" +models = "Ereduak eta hornitzaileak" +title = "AI" + [settings.configuration] advanced = "Aurreratua" database = "Datu-basea" endpoints = "Endpoints" features = "Eginbideak" +folderAccess = "Karpetetarako sarbidea" mcp = "MCP Server" storageSharing = "Fitxategi biltegiratzea eta partekatzea" systemSettings = "Sistemaren ezarpenak" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "PDF editore lehenetsia" defaultPdfEditorActive = "Stirling PDF zure PDF editore lehenetsia da" defaultPdfEditorChecking = "Egiaztatzen..." defaultPdfEditorInactive = "Beste aplikazio bat dago lehenetsi gisa" +defaultPdfEditorRemind = "Gogorarazi lehenetsi gisa ezartzea" +defaultPdfEditorRemindDescription = "Erakutsi banner bat Stirling PDF zure PDF aplikazio lehenetsia ez denean." defaultPdfEditorSet = "Dagoeneko lehenetsia" defaultStartupView = "Abioan ikuspegi lehenetsia" defaultStartupViewDescription = "Aukeratu aplikazioa abiatzean ezkerreko zutabeko zein fitxa egongo den aktibo" @@ -8817,9 +9763,18 @@ languageDescription = "Aukeratu bistaratze-hizkuntza" logout = "Amaitu saioa" setAsDefault = "Ezarri lehenetsia" theme = "Gaia" +themeAccent = "Azentu-kolorea" +themeAccentCustom = "Kolore pertsonalizatua" +themeAccentCustomHint = "Pertsonalizatua (lehenetsiak gomendatzen dira)" +themeAccentDark = "Modu iluneko azentu-kolorea" +themeAccentDefault = "Lehenetsia" +themeAccentDefaultHint = "Gai lehenetsia (gomendatua)" +themeAccentDescription = "Botoiek, estekek eta nabarmentzeek hari jarraitzen diote, eta aplikazioa sotilki tindatzen du. Modu argiak eta ilunak bakoitzak berea dute. Sistemak aktibo dagoena erabiltzen du." +themeAccentLight = "Modu argiko azentu-kolorea" themeDark = "Iluna" themeDescription = "Aldatu modu argi eta ilunaren artean" themeLight = "Argia" +themeReset = "Berrezarri gaia lehenetsira" themeSystem = "System" title = "Orokorra" user = "Erabiltzailea" @@ -8833,6 +9788,12 @@ intro = "Gaitu erabiltzaileen autentifikazioa, talde-kudeaketa eta lan-eremuaren learnMore = "Ikasi gehiago dokumentazioan" title = "Sistemako administratzaileentzat" +[settings.general.loginLanding] +description = "Aukeratu zein aplikazio irekitzen den Stirling Cloud-en saioa hasten duzunean." +editor = "Editorea" +processor = "Prozesadorea" +title = "Saioa hasi ondoren" + [settings.general.mode] fullscreen = "Pantaila osoa" sidebar = "Alboko barra" @@ -9013,9 +9974,6 @@ title = "Politikak eta Pribatutasuna" [settings.preferences] title = "Hobespenak" -[settings.search] -placeholder = "Bilatu ezarpen-orrietan..." - [settings.security] description = "Eguneratu pasahitza kontua seguru mantentzeko." title = "Segurtasuna" @@ -9041,6 +9999,11 @@ security = "Segurtasuna" telegram = "Telegram" title = "Segurtasuna eta Autentifikazioa" +[settings.signOut] +confirm = "Ziur saioa amaitu nahi duzula?" +submit = "Amaitu saioa" +title = "Amaitu saioa" + [settings.team] title = "Taldea" @@ -9156,6 +10119,7 @@ last = "Azken orria" maintainRatio = "Aldatu aspektu-erlazioa mantentzea" next = "Hurrengo orria" noSavedSigs = "Ez da gordetako sinadurarik aurkitu" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Sinadura pertsonalak" previous = "Aurreko orria" redo = "Berregin" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Atzeko planoa kentzeak huts egin du" hint = "Igo zure sinaduraren PNG edo JPG irudia" label = "Igo sinaduraren irudia" placeholder = "Hautatu irudi-fitxategia" +previewAlt = "Current image signature" processing = "Irudia prozesatzen..." removeBackground = "Kendu atzeko plano zuria (gardendu)" @@ -9207,6 +10172,17 @@ saved = "Hautatu goiko gordetako sinadura, ondoren egin klik PDFan edozein lekut text = "Goian zure izena sartu ondoren, egin klik PDFn edonon zure sinadura kokatzeko." title = "Nola gehitu sinadura" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Mugitu sinadura" pause = "Kokatzea pausatu" @@ -9653,10 +10629,30 @@ title = "Igo zerbitzarira" updateButton = "Eguneratu zerbitzarian" uploadButton = "Igo zerbitzarira" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "egin klik hemen" +fallback = "Automatikoki ireki ez bada," +opening = "Swagger UI fitxa berri batean irekitzen..." tags = "api,dokumentazioa,swagger,endpointak,garapena" title = "API Dokumentazioa" @@ -9764,12 +10760,39 @@ selectFilesHint = "Tresna hau exekutatzeko, hautatu fitxategiak Fitxategi aktibo singleFileScope = "Honetara soilik aplikatzen da: {{fileName}}" viewerMode = "Aldatu fitxategi editoreara hainbat fitxategi hautatzeko." +[toolFormat] +ANY = "edozein fitxategi" +CBR = "komiki-artxibo bat (CBR)" +CBZ = "komiki-artxibo bat (CBZ)" +CSV = "CSV fitxategi bat" +EBOOK = "liburu elektroniko bat" +EMAIL = "mezu elektroniko bat" +EXCEL = "kalkulu-orri bat" +HTML = "web-orri bat" +IMAGE = "irudiak" +JAVASCRIPT = "JavaScript fitxategi bat" +JSON = "JSON fitxategi bat" +MARKDOWN = "Markdown fitxategi bat" +NONE = "fitxategirik ez" +PCL = "PCL inprimatze-fitxategi bat" +PDF = "PDF bat" +PDF_ENCRYPTED = "pasahitzez babestutako PDF bat" +POSTSCRIPT = "PostScript fitxategi bat" +PPT = "aurkezpen bat" +TEXT = "testu-fitxategi bat" +VIDEO = "bideo bat" +WORD = "Word dokumentu bat" +XML = "XML fitxategi bat" +XPS = "XPS dokumentu bat" +ZIP = "ZIP artxibo bat" + [toolPanel] alpha = "Alpha" backToAllTools = "Itzuli tresna guztietara" collapse = "Tolestu panela" expand = "Zabaldu panela" goBack = "Itzuli atzera" +pdfTools = "PDF tresnak" placeholder = "Aukeratu tresna bat hasteko" premiumFeature = "Premium ezaugarria:" toolsHeader = "Tresnak" @@ -9806,11 +10829,6 @@ noToolsFound = "Ez da tresnarik aurkitu" quickAccess = "SARBIDE AZKARRA" searchPlaceholder = "Bilatu tresnak..." -[toolPicker.categories] -advancedTools = "Tresna aurreratuak" -recommendedTools = "Gomendatutako tresnak" -standardTools = "Tresna estandarrak" - [toolPicker.subcategories] advancedFormatting = "Formateatze aurreratua" automation = "Automatizazioa" @@ -9824,6 +10842,9 @@ removal = "Kentzea" signing = "Sinatzea" verification = "Egiaztapena" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Ez da tresnarik aurkitu" noTools = "Ez dago tresnarik eskuragarri" @@ -9950,28 +10971,53 @@ title = "Erabiltzaileak" you = "(zu)" [users.action] +cancelInvite = "Utzi gonbidapena bertan behera" deleteTeam = "Ezabatu taldea" disableMfa = "Berrezarri MFA" move = "Mugitu taldera" reinstate = "Berrezarri" remove = "Kendu erakundetik" +removeTeam = "Kendu taldetik" rename = "Aldatu taldearen izena" resetPw = "Berrezarri pasahitza" suspend = "Eten" unlock = "Desblokeatu kontua" +[users.activity] +daysAgo = "Duela {{count}} egun" +hoursAgo = "Duela {{count}} ordu" +justNow = "Oraintxe bertan" +minutesAgo = "Duela {{count}} min" +monthsAgo_one = "Duela {{count}} hilabete" +monthsAgo_other = "Duela {{count}} hilabete" +never = "Inoiz ez" +weeksAgo_one = "Duela {{count}} aste" +weeksAgo_other = "Duela {{count}} aste" +yearsAgo_one = "Duela {{count}} urte" +yearsAgo_other = "Duela {{count}} urte" + [users.cap] addProcessor = "+ Processor" approver = "Politika onartzen du" editor = "Editorea" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}} helbiderako gonbidapena bertan behera utzi? Ezingo dute uneko estekarekin bat egin." +cancelInviteTitle = "Utzi gonbidapena bertan behera" deleteTeamBody = "Ezabatu {{name}} taldea? Taldeak hutsik egon behar du lehenik - mugitu kideak beste talde batera, eta ezin du integrazio-konfiguraziorik oraindik bere jabetzan eduki." deleteTeamTitle = "Ezabatu taldea" disableMfaBody = "Kendu {{name}}-ren MFA erregistroa? Hurrengo saio-hasieran berriro konfiguratuko du, beharrezkoa bada." disableMfaTitle = "Berrezarri MFA" removeBody = "Betiko kendu {{name}} erakundetik? Hau ezin da desegin." +removeTeamBody = "{{name}} taldetik kendu? Kontua mantenduko dute, baina talde honen baliabideetarako sarbidea galduko dute." removeTitle = "Kendu kidea" [users.empty] @@ -9982,10 +11028,8 @@ title = "Oraindik ez dago kiderik" addToTeam = "Gehitu taldera" guestCount = "{{count}} gonbidatu" guests = "Gonbidatuak" -guestsDesc = "Kanpoko kolaboratzaileak, partekatu duzunera mugatuak. Editorea soilik." ledBy = "{{owner}}-k gidatua" org = "Erakundea" -orgDesc = "Erakunde osoko agintea eta politika-onarpena dituzten jabeak" owners = "{{count}} jabe" team = "{{name}} taldea" teamMeta = "{{count}} pertsona" @@ -10022,6 +11066,18 @@ username = "Erabiltzaile-izena" usernameError = "Erabiltzaile-izenak gutxienez 3 karaktere izan behar ditu" usernamePlaceholder = "jsmith" +[users.invites] +by = "{{who}} erabiltzaileak gonbidatua" +cancel = "Utzi bertan behera" +expiresInDays_one = "{{count}} egun barru iraungiko da" +expiresInDays_other = "{{count}} egun barru iraungiko da" +expiresToday = "Gaur iraungiko da" +title = "Zain dauden gonbidapenak" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Zerbait gaizki joan da backend-era konektatzean, edo ez duzu sarbiderik. Saiatu berriro." title = "Ezin izan dira kideak kargatu" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Bilatu eranskinak" title = "Eranskinak" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Laster-markaren izenburua" +bookmarkTitleRequired = "Laster-markaren izenburua beharrezkoa da" closeSidebar = "Itxi laster-marken alboko barra" collapseAll = "Tolestu laster-marka guztiak" +empty = "No bookmarks in this document" expandAll = "Zabaldu laster-marka guztiak" +searchPlaceholder = "Bilatu laster-markak" +title = "Bookmarks" [viewer.comments] addComment = "Gehitu iruzkina" @@ -10236,10 +11297,12 @@ locateAnnotation = "Kokatu dokumentuan" moreActions = "Ekintza gehiago" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Orrialdea {{page}}" placingHint = "Egin klik orri batean kokatzeko… (utzi)" removeCommentOnly = "Kendu iruzkina bakarrik" saveReply = "Gorde erantzuna" +searchPlaceholder = "Search comments" title = "Iruzkinak" typeComment = "Iruzkina" typeInsertText = "Testua txertatu" @@ -10262,6 +11325,7 @@ unsavedDesc = "Gorde gabeko aldaketak dituzu" closeSidebar = "Itxi geruzen alboko barra" hideAll = "Ezkutatu geruza guztiak" showAll = "Erakutsi geruza guztiak" +title = "Layers" [viewer.link] delete = "Ezabatu esteka" @@ -10605,30 +11669,6 @@ title = "Ur‑markaren testua" image = "Irudia" text = "Testua" -[wetSignature.tooltip] -header = "Sinadura sortzeko metodoak" - -[wetSignature.tooltip.draw] -bullet1 = "Pertsonalizatu arkatzaren kolorea eta lodiera" -bullet2 = "Garbitu eta berriz marraztu gustura geratu arte" -bullet3 = "Ukipen-gailuetan funtzionatzen du (tabletak, telefonuak)" -description = "Sortu eskuz idatzitako sinadura saguarekin edo ukipen-pantailarekin. Pertsonal eta egiazko sinaduretarako egokiena." -title = "Marraztu sinadura" - -[wetSignature.tooltip.type] -bullet1 = "Aukeratu letra-tipo anitzen artetik" -bullet2 = "Pertsonalizatu testuaren tamaina eta kolorea" -bullet3 = "Estandarizatutako sinaduretarako aproposa" -description = "Sortu sinadura idatzitako testutik. Azkarra eta koherentea, enpresa-dokumentuetarako egokia." -title = "Idatzi sinadura" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG eta bestelako irudi-formatuak onartzen ditu" -bullet2 = "Atzeko plano gardenak gomendatzen dira emaitza onenak lortzeko" -bullet3 = "Irudia sinadura-eremura egokitzeko tamainaz aldatuko da" -description = "Igo aldez aurretik sortutako sinadura-irudia. Ideala sinadura eskaneatua edo enpresako logotipoa baduzu." -title = "Igo sinadura-irudia" - [workbenchBar] activeFiles = "Fitxategi aktiboak" annotations = "Anotazioak" @@ -10642,6 +11682,7 @@ exitRedaction = "Irten zentsura modutik" exportAll = "Esportatu PDFa" exportSelected = "Esportatu hautatutako orriak" formFill = "Bete inprimakia" +hideToolbar = "Hide toolbar" multiTool = "Multi-tresna" panMode = "Pan modua" print = "Inprimatu PDFa" @@ -10652,6 +11693,7 @@ redact = "Zentsuratu" rotateLeft = "Biratu ezkerrera" rotateRight = "Biratu eskuinera" ruler = "Erregela / Neurtu" +rulerSettings = "Eskala-ezarpenak" save = "Gorde" saveAll = "Gorde dena" saveAs = "Gorde honela" @@ -10661,6 +11703,7 @@ selectAll = "Hautatu dena" selectByNumber = "Hautatu orri-zenbakien arabera" selectLanguage = "Hautatu hizkuntza" share = "Partekatu" +showToolbar = "Show toolbar" toggleAnnotations = "Txandakatu anotazioen ikusgarritasuna" toggleAttachments = "Txandakatu eranskinak" toggleBookmarks = "Txandakatu laster-markak" diff --git a/frontend/editor/public/locales/fa-IR/translation.toml b/frontend/editor/public/locales/fa-IR/translation.toml index b7519bc879..8dabf9598d 100644 --- a/frontend/editor/public/locales/fa-IR/translation.toml +++ b/frontend/editor/public/locales/fa-IR/translation.toml @@ -1,7 +1,6 @@ alphabet = "حروف الفبا" apply = "اعمال" applyAndContinue = "ذخیره و خروج" -areYouSure = "مطمئنید می‌خواهید خارج شوید؟" back = "بازگشت" black = "سیاه" blue = "آبی" @@ -46,10 +45,8 @@ filesSelected = "فایل‌ها انتخاب شدند" font = "فونت" fontSizeTooltip = "اندازه متن شماره صفحه بر حسب پوینت. اعداد بزرگ‌تر متن بزرگ‌تری ایجاد می‌کنند." fontTypeTooltip = "خانواده فونت برای شماره صفحات. بر اساس سبک سند خود انتخاب کنید." -genericSubmit = "ارسال" green = "سبز" help = "راهنما" -imgPrompt = "انتخاب تصویر(ها)" incorrectPasswordMessage = "رمز عبور فعلی نادرست است." info = "اطلاعات" invalidUndoData = "امکان واگردانی نیست: داده‌های عملیات نامعتبر است" @@ -62,7 +59,6 @@ loginAgreementTitle = "توافق‌نامهٔ ورود" logOut = "خروج از حساب" marginTooltip = "فاصله بین شماره صفحه و لبه صفحه." moreOptions = "گزینه‌های بیشتر" -multiPdfPrompt = "انتخاب فایل‌های PDF (دو یا بیشتر)" never = "هرگز" no = "خیر" noFileSelected = "هیچ فایلی انتخاب نشده است. لطفاً یک فایل بارگذاری کنید." @@ -80,7 +76,6 @@ pageOrderPrompt = "ترتیب صفحات سفارشی (یک لیست از شما pages = "صفحات" pageSelectionPrompt = "انتخاب صفحات سفارشی (یک لیست از شماره صفحات به صورت جدا شده با کاما وارد کنید مانند 1,5,6 یا از توابعی مانند 2n+1 استفاده کنید):" password = "رمز عبور" -pdfPrompt = "انتخاب فایل(های) PDF" pin = "سنجاق کردن فایل (پس از اجرای ابزار فعال بماند)" pro = "نسخه حرفه‌ای" processingComplete = "فایل شما آماده است." @@ -110,6 +105,7 @@ undoSuccess = "عملیات با موفقیت واگردانی شد" unknown = "نامعلوم" unpin = "برداشتن سنجاق فایل (پس از اجرای ابزار جایگزین شود)" unsavedChanges = "شما تغییرات ذخیره‌نشده‌ای در PDF دارید." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "تغییرات ذخیره‌نشده" unsupported = "پشتیبانی نمی‌شود" username = "نام کاربری" @@ -356,6 +352,7 @@ text = "این مجوزها کنترل می‌کنند کاربران چه کا title = "تغییر مجوزها" [addStamp] +preview = "پیش‌نمایش مهر" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "دانلود دستی: روی پیوندها کلیک کنید و noLanguages = "هیچ زبان tessdata در پوشهٔ پیکربندی‌شده یافت نشد." permissionNotice = "مسیر tessdata قابل نوشتن نیست. دانلودها در مرورگر باز می‌شوند؛ لطفاً فایل‌های .traineddata را به‌صورت دستی در پوشهٔ tessdata ذخیره کنید." +[admin.settings.ai.documents] +description = "مدل embedding و تنظیمات بازیابی مورد استفاده برای پاسخ به پرسش‌ها درباره اسناد را پیکربندی کنید. پس از ذخیره، روی موتور AI اعمال می‌شود." +title = "اسناد و RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "برای استفاده از اعتبارنامه محیطی خود موتور، خالی بگذارید. برای استقرارهای خودمیزبان تک‌موتوره اعمال می‌شود." +generic = "کلید API embedding" +openai = "کلید OpenAI API" +setPlaceholder = "ذخیره شده - برای نگه داشتن کلید فعلی خالی بگذارید" +voyageai = "کلید VoyageAI API" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL پایه endpoint embedding سازگار با OpenAI / Ollama، مانند http://ollama:11434/v1. باید به یک endpoint داخلی قابل اعتماد اشاره کند (حساس به SSRF)." +label = "URL پایه embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "نام مدل embedding. متن آزاد؛ پیشنهادها فقط راهنما هستند." +label = "مدل embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "ارائه‌دهنده‌ای که برای تبدیل متن سند به embeddingهای برداری استفاده می‌شود." +label = "ارائه‌دهنده embedding" + +[admin.settings.ai.documents.maxSearches] +description = "حداکثر تعداد جست‌وجوهای بازیابی که عامل می‌تواند در هر درخواست اجرا کند." +label = "حداکثر جست‌وجوها" + +[admin.settings.ai.documents.reindexNote] +body = "تغییر مدل embedding بلافاصله اعمال می‌شود، اما اسنادی که با مدل قبلی ایندکس شده‌اند باید دوباره ایندکس شوند تا جست‌وجو نتایج درست برگرداند." +title = "ایندکس‌سازی دوباره لازم است" + +[admin.settings.ai.documents.saved] +reindexNote = "اگر مدل embedding را تغییر داده‌اید، اسناد موجود را دوباره ایندکس کنید تا جست‌وجو از مدل جدید استفاده کند." + +[admin.settings.ai.documents.topK] +description = "تعداد مرتبط‌ترین بخش‌های بازیابی‌شده در هر جست‌وجو." +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling را به موتور Python AI متصل کنید و قابلیت‌های AI قابل ارائه را انتخاب کنید. تغییرات پس از راه‌اندازی مجدد اعمال می‌شوند." +title = "موتور AI" + +[admin.settings.ai.general.capabilities] +description = "ویژگی‌های جداگانه AI را روشن یا خاموش کنید. ویژگی‌های غیرفعال در برنامه پنهان می‌شوند." +title = "قابلیت‌ها" + +[admin.settings.ai.general.enabled] +description = "کلید اصلی. وقتی خاموش باشد، هیچ ابزار، عامل یا فراخوانی موتور AI در دسترس نیست." +label = "فعال‌سازی AI" + +[admin.settings.ai.general.features.chat] +description = "دستیار گفت‌وگویی برای کار با PDFها." +label = "دستیار چت" + +[admin.settings.ai.general.features.classify] +description = "اسناد را به‌طور خودکار بر اساس نوع یا محتوا دسته‌بندی کنید." +label = "دسته‌بندی سند" + +[admin.settings.ai.general.features.createPdf] +description = "ایجاد یک PDF جدید (مثلاً از HTML) از طریق یک عامل AI." +label = "ایجاد PDF از prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "پرسش بپرسید و پاسخ‌هایی دریافت کنید که بر اساس یک سند بارگذاری‌شده هستند." +label = "پرسش‌های سند" + +[admin.settings.ai.general.features.mathAuditor] +description = "اسناد را برای خطاهای ریاضی و عددی بررسی کنید." +label = "بازرس ریاضی" + +[admin.settings.ai.general.features.pdfComment] +description = "افزودن دیدگاه‌های بازبینی و یادداشت‌های نوشته‌شده توسط AI به یک PDF." +label = "عامل دیدگاه PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "مهلت زمانی برای عملیات سنگین‌تر عامل، مانند ایجاد سند." +label = "مهلت زمانی عملیات طولانی‌مدت (ثانیه)" + +[admin.settings.ai.general.note] +body = "موتور AI به‌عنوان یک سرویس جداگانه اجرا می‌شود. راز مشترک آن" +body2 = "از طریق یک متغیر محیطی کانتینر تنظیم می‌شود. کلیدهای API ارائه‌دهنده را می‌توان در این صفحات وارد کرد یا به‌عنوان متغیرهای محیطی موتور ارائه داد؛ کلیدهای واردشده در اینجا هنگام ذخیره به موتور ارسال می‌شوند." +title = "درباره موتور AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "مهلت زمانی برای پاسخ‌های چت جریانی (توکن‌به‌توکن)." +label = "مهلت زمانی جریان (ثانیه)" + +[admin.settings.ai.general.test] +button = "آزمایش اتصال" +failBody = "موتور AI پاسخ نداد. URL را بررسی کنید، مطمئن شوید کانتینر موتور در حال اجراست و AI فعال است (پس از فعال‌سازی، راه‌اندازی مجدد لازم است)." +failTitle = "موتور AI در دسترس نیست" +okBody = "موتور AI به بررسی سلامت پاسخ داد." +okTitle = "موتور AI در دسترس است" + +[admin.settings.ai.general.timeoutSeconds] +description = "مهلت زمانی برای درخواست‌های استاندارد AI به موتور." +label = "مهلت زمانی درخواست (ثانیه)" + +[admin.settings.ai.general.url] +description = "URL داخلی موتور Python AI، مانند http://stirling-pdf-engine:5001." +label = "URL موتور AI" + +[admin.settings.ai.limits] +description = "محافظ‌هایی برای میزان کاری که درخواست‌های AI می‌توانند انجام دهند و تعداد اجرای هم‌زمان آن‌ها. پس از ذخیره، روی موتور AI اعمال می‌شود." +title = "محدودیت‌ها و عملکرد" + +[admin.settings.ai.limits.maxCharacters] +description = "محافظ: درخواست‌های AI را که متن استخراج‌شده آن‌ها از این طول بیشتر است رد کنید." +label = "حداکثر نویسه در هر درخواست" + +[admin.settings.ai.limits.maxPages] +description = "محافظ: درخواست‌های AI با بیش از این تعداد صفحه PDF را رد کنید." +label = "حداکثر صفحات در هر درخواست" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "حداکثر فراخوانی‌های هم‌زمان مدل در حال اجرا در کل موتور." +label = "حداکثر هم‌زمانی مدل" + +[admin.settings.ai.models] +description = "ارائه‌دهنده LLM و مدل‌های هوشمند/سریع مورد استفاده موتور AI را انتخاب کنید. پس از ذخیره، روی موتور AI اعمال می‌شود." +title = "مدل‌ها و ارائه‌دهندگان" + +[admin.settings.ai.models.apiKey] +anthropic = "کلید Anthropic API" +description = "برای استفاده از اعتبارنامه محیطی خود موتور، خالی بگذارید. برای استقرارهای خودمیزبان تک‌موتوره اعمال می‌شود." +generic = "کلید API" +openai = "کلید OpenAI API" +setPlaceholder = "ذخیره شده - برای نگه داشتن کلید فعلی خالی بگذارید" + +[admin.settings.ai.models.baseUrl] +description = "URL پایه endpoint سازگار با OpenAI / Ollama، مانند http://ollama:11434/v1." +label = "URL پایه ارائه‌دهنده" +warning = "URL پایه باید به یک endpoint داخلی قابل اعتماد اشاره کند. موتور درخواست‌های سمت سرور را به آن ارسال می‌کند، بنابراین مقدار غیرقابل اعتماد حساس به SSRF است." + +[admin.settings.ai.models.fastMaxTokens] +description = "حداکثر توکن‌های خروجی برای مدل سریع." +label = "حداکثر توکن‌های مدل سریع" + +[admin.settings.ai.models.fastModel] +description = "مدل ارزان‌تر و سریع‌تر برای کارهای سبک. متن آزاد؛ پیشنهادها فقط راهنما هستند." +label = "مدل سریع" + +[admin.settings.ai.models.provider] +description = "موتور با کدام ارائه‌دهنده LLM ارتباط برقرار می‌کند." +label = "ارائه‌دهنده" + +[admin.settings.ai.models.smartMaxTokens] +description = "حداکثر توکن‌های خروجی برای مدل هوشمند." +label = "حداکثر توکن‌های مدل هوشمند" + +[admin.settings.ai.models.smartModel] +description = "مدل با قابلیت بالا برای استدلال پیچیده. متن آزاد؛ پیشنهادها فقط راهنما هستند." +label = "مدل هوشمند" + +[admin.settings.ai.saved] +body = "تغییرات به‌طور خودکار به موتور AI ارسال می‌شوند." +bodyNoPush = "تنظیمات ذخیره شد. دفعه بعد که موتور AI پیکربندی خود را دریافت کند، اعمال می‌شوند." +title = "تنظیمات AI ذخیره شد" + [admin.settings.badge] clickToUpgrade = "برای مشاهده جزئیات طرح کلیک کنید" @@ -938,6 +1094,25 @@ label = "تولید مجدد هنگام راه‌اندازی" description = "تعداد روزهای اعتبار گواهی" label = "اعتبار گواهی (روز)" +[admin.settings.folderAccess] +description = "دایرکتوری‌هایی که منابع پوشه و خروجی‌های پوشه اجازه خواندن از آن‌ها و نوشتن در آن‌ها را دارند. این یک مرز امنیتی است: اتوماسیون‌ها هرگز نمی‌توانند به مسیر سروری خارج از این فهرست اشاره کنند." +securityNote = "برای غیرفعال‌سازی کامل منابع و خروجی‌های پوشه، این را خالی بگذارید. دایرکتوری پیکربندی خود Stirling همیشه ممنوع است." +title = "دسترسی پوشه" + +[admin.settings.folderAccess.implied] +description = "این دایرکتوری‌های مدیریت‌شده توسط Stirling همیشه مجاز هستند و اینجا قابل تغییر نیستند." +serverStorage = "ذخیره‌سازی فایل سرور" +title = "همیشه مجاز" +watchedFolder = "پوشه تحت‌نظر پایپ‌لاین" + +[admin.settings.folderAccess.roots] +add = "افزودن" +empty = "هیچ پوشه‌ای مجاز نیست. منابع و خروجی‌های پوشه در حال حاضر غیرفعال هستند." +hint = "مسیرهای مطلق را وارد کنید، برای مثال /data/inbox." +label = "ریشه‌های پوشه مجاز" +placeholder = "/data/inbox" +remove = "حذف ریشه پوشه" + [admin.settings.general] description = "پیکربندی تنظیمات سراسری برنامه شامل برندینگ و رفتار پیش‌فرض." system = "سیستم" @@ -1704,6 +1879,9 @@ width = "عرض" [app] description = "جایگزین رایگان Adobe Acrobat (10M+ دانلود)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "تبدیل به PDF/A-3b" convertToPdfA3bDescription = "یک PDF آرشیوی با پیوست‌های توکار ایجاد می‌کند" @@ -1842,6 +2020,15 @@ export = "برون‌بری" accessDenied = "دسترسی رد شد" insufficientPermissions = "شما مجوز انجام این اقدام را ندارید." +[auth.callback] +completing = "در حال تکمیل احراز هویت" +invalidToken = "ورود OAuth ناموفق بود - توکن نامعتبر است." +missingToken = "ورود OAuth ناموفق بود - توکنی دریافت نشد." +oauthFailed = "ورود OAuth ناموفق بود. لطفاً دوباره تلاش کنید." +pleaseWait = "لطفاً صبر کنید تا ورود شما را تکمیل کنیم." +signedOut = "از سیستم خارج شده‌اید. لطفاً دوباره وارد شوید." +windowMayClose = "پس از تکمیل، می‌توانید این پنجره را ببندید." + [auth.displayName] guest = "مهمان" user = "کاربر" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "اسناد PDF را با حذف محتوای بالقوه description = "این ابزار فایل‌های PDF را بر اساس محتوایشان به‌صورت خودکار تغییر نام می‌دهد. سند را تحلیل می‌کند تا مناسب‌ترین عنوان را از متن بیابد." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "چرخش خودکار" +tags = "چرخش خودکار،اصلاح جهت،وارونه،افقی،صاف کردن،جهت‌دهی،تشخیص جهت،تصحیح چرخش،جهت‌دهی خودکار،اسکن‌شده،OSD" + +[autoRotate.confidenceThreshold] +title = "آستانه اطمینان" + +[autoRotate.detectionMode] +auto = "خودکار" +osd = "فقط OCR" +text = "فقط متن" +title = "روش تشخیص" + +[autoRotate.error] +failed = "هنگام چرخش خودکار PDF خطایی رخ داد." + +[autoRotate.inferUndetected] +title = "چرخاندن صفحات با اطمینان پایین برای همسان‌سازی" + +[autoRotate.report] +applied = "{{degrees}}° ساعتگرد" +confidenceValue = "اطمینان {{value}}" +noChange = "بدون تغییر" +pageLabel = "صفحه {{number}}" +summary = "{{rotated}} از {{total}} صفحه چرخانده شد، {{unchanged}} بدون تغییر باقی ماند" +title = "گزارش تشخیص" + +[autoRotate.report.method] +inferred = "همسان‌سازی شد" +none = "نادیده گرفته شد" +osd = "OCR" +text = "متن" + +[autoRotate.report.note] +belowThreshold = "کمتر از آستانه" +blankPage = "صفحه خالی است" +inferredFromDocument = "همانند سایر صفحات" +noDominantDirection = "جهت‌های متنی ترکیبی" +osdFailed = "متن قابل خواندن یافت نشد" +osdNoVerdict = "مشخص نشد کدام سمت بالا است" +tesseractUnavailable = "OCR نصب نیست" +tooFewGlyphs = "متن بسیار کم است" + +[autoRotate.results] +title = "نتایج چرخش خودکار" + +[autoRotate.settings] +title = "تنظیمات" + +[autoRotate.tooltip.confidenceThreshold] +text = "این بررسی باید تا چه حد مطمئن باشد تا صفحه چرخانده شود. برای چرخاندن صفحات بیشتر آن را کاهش دهید، و برای دست‌نخورده ماندن صفحات بیشتر آن را افزایش دهید." +title = "آستانه اطمینان" + +[autoRotate.tooltip.description] +text = "تشخیص می‌دهد هر صفحه باید به کدام سمت باشد و آن را به جهت درست می‌چرخاند. صفحاتی که از قبل درست هستند، بدون تغییر باقی می‌مانند." + +[autoRotate.tooltip.detectionMode] +text = "خودکار ابتدا متن روی صفحه را می‌خواند، سپس اگر کافی نباشد صفحه را به‌عنوان تصویر بررسی می‌کند. فقط متن بررسی تصویر را رد می‌کند که برای اسنادی که هرگز اسکن نشده‌اند سریع‌تر است. فقط OCR هر صفحه را به‌عنوان تصویر در نظر می‌گیرد." +title = "روش تشخیص" + +[autoRotate.tooltip.header] +title = "چرخش خودکار" + +[autoRotate.tooltip.inferUndetected] +text = "صفحات خالی یا تقریباً خالی به‌تنهایی قابل بررسی نیستند. وقتی همه صفحاتی که قابل خواندن بوده‌اند به یک چرخش یکسان نیاز داشته باشند، این صفحات نیز برای همسان‌سازی با آن‌ها چرخانده می‌شوند." +title = "چرخاندن صفحات با اطمینان پایین برای همسان‌سازی" + [autoSizeSplitPDF] tags = "PDF، تقسیم، سند، سازماندهی" @@ -2189,33 +2443,6 @@ keywords = "کلیدواژه‌ها: odd, even" numbers = "اعداد/بازه‌ها: 5, 10-20" progressions = "پیشروی‌ها: 3n، 4n+1" -[certificateChoice.tooltip] -header = "انواع گواهی" - -[certificateChoice.tooltip.organization] -bullet1 = "مدیریت‌شده توسط مدیران سیستم" -bullet2 = "مشترک بین کاربران مجاز" -bullet3 = "نمایانگر هویت شرکت، نه فرد" -bullet4 = "مناسب برای: اسناد رسمی، امضاهای تیمی" -description = "یک گواهی مشترک که توسط سازمان شما ارائه می‌شود. برای اختیار امضای سراسری شرکت استفاده می‌شود." -title = "گواهی سازمان" - -[certificateChoice.tooltip.personal] -bullet1 = "در اولین استفاده به‌طور خودکار ایجاد می‌شود" -bullet2 = "به حساب کاربری شما متصل است" -bullet3 = "قابل اشتراک با دیگر کاربران نیست" -bullet4 = "مناسب برای: اسناد شخصی، پاسخ‌گویی فردی" -description = "یک گواهی منحصربه‌فرد که به‌طور خودکار برای حساب کاربری شما ایجاد می‌شود. مناسب برای امضاهای فردی." -title = "گواهی شخصی" - -[certificateChoice.tooltip.upload] -bullet1 = "نیازمند فایل و گذرواژه P12/PFX" -bullet2 = "می‌تواند توسط مراجع صدور گواهی خارجی صادر شود" -bullet3 = "سطح اعتماد بالاتر برای اسناد حقوقی" -bullet4 = "مناسب برای: قراردادهای الزام‌آور قانونی، اعتبارسنجی خارجی" -description = "از فایل گواهی PKCS#12 خود استفاده کنید. کنترل کامل روی ویژگی‌های گواهی را فراهم می‌کند." -title = "بارگذاری P12 سفارشی" - [certSign] awaitingSignatures = "در انتظار امضاها" chooseCertificate = "انتخاب فایل گواهی" @@ -2542,34 +2769,6 @@ server = "سرور" stepTitle = "منبع گواهی" upload = "بارگذاری" -[certSign.tooltip.header] -title = "درباره مدیریت امضاها" - -[certSign.tooltip.overview] -bullet1 = "بررسی امضاهای موجود و اعتبار آن‌ها" -bullet2 = "مشاهده اطلاعات دقیق درباره امضاکنندگان و گواهی‌ها" -bullet3 = "افزودن امضاهای دیجیتال جدید برای ایمن‌سازی اسناد شما" -bullet4 = "پشتیبانی از چندین فایل با پیمایش آسان" -text = "این ابزار به شما امکان می‌دهد بررسی کنید آیا PDFهای شما به‌صورت دیجیتال امضا شده‌اند و امضاهای دیجیتال جدید اضافه کنید. امضاهای دیجیتال ثابت می‌کنند چه کسی سند را ایجاد یا تأیید کرده است و نشان می‌دهند از زمان امضا تغییر کرده یا نه." -title = "این ابزار چه می‌کند؟" - -[certSign.tooltip.signing] -bullet1 = "پشتیبانی از فرمت‌های PEM، PKCS12، JKS و گواهی سرور" -bullet2 = "گزینه‌ای برای نمایش یا پنهان کردن امضا روی PDF" -bullet3 = "افزودن دلیل، موقعیت و نام امضاکننده" -bullet4 = "انتخاب صفحه‌ای که امضای قابل‌نمایش روی آن قرار می‌گیرد" -bullet5 = "استفاده از گواهی سرور برای گزینه ساده 'Sign with Stirling-PDF'" -text = "برای امضای یک PDF، به یک گواهی دیجیتال (مانند PEM، PKCS12 یا JKS) نیاز دارید. می‌توانید امضا را روی سند قابل‌نمایش کنید یا آن را فقط برای امنیت نامرئی نگه دارید." -title = "افزودن امضاها" - -[certSign.tooltip.validation] -bullet1 = "نمایش معتبر یا نامعتبر بودن امضاها" -bullet2 = "نمایش اطلاعات امضاکننده و تاریخ امضا" -bullet3 = "بررسی اینکه آیا سند پس از امضا ویرایش شده است یا خیر" -bullet4 = "امکان استفاده از گواهی‌های سفارشی برای اعتبارسنجی" -text = "وقتی امضاها را بررسی می‌کنید، ابزار به شما می‌گوید آیا معتبرند، چه کسی سند را امضا کرده، چه زمانی امضا شده و اینکه آیا سند از زمان امضا تغییر کرده است یا نه." -title = "بررسی امضاها" - [changeCreds] changeUsername = "نام کاربری خود را به‌روزرسانی کنید. پس از به‌روزرسانی از حساب خارج خواهید شد." credsUpdated = "حساب به‌روزرسانی شد" @@ -2737,12 +2936,16 @@ text = "برای غیرقابل‌تغییر کردن این مجوزها، از [chat.actions] copy = "کپی پیام" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "گزینه‌های عامل Stirling" +assistant = "AI Assistant" clearChat = "پاک‌کردن چت" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "این عملیات از اعتبارهای ابری شما استف tooltip = "برداشت رنگ از صفحه" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "انتخاب رنگ" [common] +actions = "Actions" back = "بازگشت" cancel = "انصراف" close = "بستن" +codeSample = "Code sample" collapse = "جمع کردن" confirm = "تأیید" continue = "ادامه" copied = "کپی شد!" copy = "کپی" done = "انجام شد" +enterValue = "Enter value" error = "خطا" expand = "گسترش دادن" loading = "در حال بارگذاری..." next = "بعدی" +open = "Open" preview = "پیش‌نمایش" previous = "قبلی" refresh = "تازه‌سازی" +remaining = "باقی‌مانده" retry = "تلاش مجدد" save = "ذخیره" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "پاک کردن انتخاب‌شده‌ها" @@ -2988,6 +3199,7 @@ title = "روش فشرده‌سازی" [compress.settings] desiredSize = "حجم فایل موردنظر" desiredSizePlaceholder = "اندازه را وارد کنید" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "فشرده‌سازی راهی آسان برای کاهش اندازه فایل است. «اندازه فایل» را انتخاب کنید تا اندازه هدف را وارد کنید و ما کیفیت را برای شما تنظیم کنیم. «کیفیت» را انتخاب کنید تا قدرت فشرده‌سازی را به‌صورت دستی تنظیم کنید." @@ -3190,6 +3402,24 @@ pdfOptions = "گزینه‌های PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "ابتدا قالب مبدأ را انتخاب کنید" settings = "تنظیمات" single = "تکی" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "پایش پیشرفته" -allInOneWorkspace = "فضای کاری یکپارچه PDF (نمایشگر، ابزارها و عامل)" -apiSandbox = "محیط آزمایشی API" -dedicatedSupportSlas = "پشتیبانی اختصاصی و SLAها" -fasterThroughput = "توان عبوری ۱۰ برابری" -fullyPrivateFiles = "فایل‌ها کاملاً خصوصی" -largeFileProcessing = "پردازش فایل‌های بزرگ" -orgWideAccess = "کنترل‌های دسترسی در سطح سازمان" -premiumAiModels = "مدل‌های هوش مصنوعی پریمیوم" -prioritySupport = "پشتیبانی اولویت‌دار" -privateDocCloud = "ابر اسناد خصوصی" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "دسترسی امن API" -standardThroughput = "توان عبوری استاندارد" -unlimitedApiAccess = "دسترسی نامحدود به API" -unlimitedSeats = "کاربران نامحدود" - [crop] autoCrop = "برش خودکار فضاهای خالی" noFileSelected = "برای شروع برش یک فایل PDF انتخاب کنید" @@ -3346,6 +3558,7 @@ message = "تنظیم برنامه پیش‌فرض PDF ناموفق بود" title = "خطا" [defaultApp.prompt] +dontRemind = "دیگر به من یادآوری نکن" message = "Stirling PDF را برنامه پیش‌فرض خود برای باز کردن فایل‌های PDF کنید." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "جستجو..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "ویرایش" @@ -3644,11 +3861,13 @@ shareSelected = "اشتراک‌گذاری موارد انتخاب‌شده" sharing = "اشتراک‌گذاری" showAll = "نمایش همه" showHistory = "نمایش تاریخچه" +sortBy = "Sort files" sortByDate = "مرتب‌سازی بر اساس تاریخ" sortByName = "مرتب‌سازی بر اساس نام" sortBySize = "مرتب‌سازی بر اساس حجم" storage = "فضای ذخیره" storageState = "ذخیره‌سازی" +storageUsed = "Storage used" synced = "همگام شد" title = "بارگذاری فایل‌های PDF" toolChain = "ابزارهای اعمال‌شده" @@ -3694,13 +3913,17 @@ addFiles = "افزودن فایل‌ها" addingFiles = "در حال افزودن فایل‌ها…" collapse = "جمع کردن نوار کناری" customizeGroups = "سفارشی‌سازی گروه‌ها" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "برای شروع فایل‌ها را باز کنید" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "گسترش نوار کناری" -files = "فایل‌ها" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive پیکربندی نشده است" leaveMyFiles = "خروج از فایل‌های من" +library = "کتابخانه PDF" myFiles = "فایل‌های من" noFiles = "هنوز فایلی نیست" openFileManager = "باز کردن مدیریت فایل" @@ -3708,37 +3931,36 @@ openFromComputer = "باز کردن از رایانه" openSettings = "باز کردن تنظیمات" other = "سایر" recent = "اخیر" -search = "جستجو" -searchPlaceholder = "جستجوی فایل‌ها..." viewAll = "مشاهده همه {{count}} فایل" [fileSidebar.fileItem] closeViewer = "بستن نمایشگر" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "باز کردن در نمایشگر" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "افزودن" -addLabel = "افزودن برچسب…" -categoryIconAria = "انتخاب آیکون برای {{name}}" -createCategory = "ایجاد دسته" -delete = "حذف دسته" done = "انجام شد" -hide = "پنهان کردن دسته" -newCategory = "نام دسته جدید…" -removeLabel = "حذف {{name}}" -rename = "تغییر نام" reset = "بازنشانی به پیش‌فرض‌ها" -search = "جستجوی برچسب‌ها…" -show = "نمایش دسته" subtitle = "فایل‌های خود را در دسته‌های والد گروه‌بندی کنید. برچسب‌های موجود یا جدید را به یک دسته اضافه کنید، نام آن را تغییر دهید، یا دسته خودتان را بسازید. فایل‌هایی که در هیچ‌یک از دسته‌های شما نیستند زیر «سایر» نمایش داده می‌شوند." title = "دسته‌های نوار کناری" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "افزودن به فضای کاری" addToWorkspaceCount = "افزودن {{count}} مورد به فضای کاری" @@ -3749,7 +3971,6 @@ backToMyFiles = "بازگشت به فایل‌های من" breadcrumbs = "مسیر پوشه" cancel = "لغو" classification = "طبقه‌بندی" -clearSearch = "پاک‌کردن جستجو" clearSelection = "پاک‌کردن انتخاب" closeDetails = "بستن جزئیات" create = "ایجاد" @@ -3781,6 +4002,7 @@ downloadAll = "دانلود همه" downloadVersion = "دانلود این نسخه" dropOverlay = "فایل‌ها را برای بارگذاری رها کنید" dropOverlaySub = "فایل‌ها در «محلی» شروع می‌شوند. برای سازمان‌دهی آن‌ها در یک پوشه از 'انتقال به' یا 'ذخیره در ابر' استفاده کنید." +duplicate = "Duplicate" file = "فایل" fileInfo = "File info" fileMenu = "اقدامات فایل" @@ -3808,13 +4030,12 @@ refresh = "نوسازی از سرور" remove = "حذف" removeVersion = "حذف این نسخه" rename = "تغییر نام" +renamed = "تغییر نام داده شد" renameFolder = "تغییر نام پوشه" resizeFolderTree = "تغییر اندازهٔ درخت پوشه‌ها (کلیدهای جهت، Shift برای گام‌های بزرگ‌تر؛ دوبار کلیک برای برازش خودکار)" save = "ذخیره" saveToServer = "ذخیره در سرور" saveToServerDisabledHint = "ذخیره در سرور روی این سرور فعال نیست. از مدیر خود بخواهید آن را فعال کند." -search = "جستجو" -searchPlaceholder = "جستجوی این پوشه و زیرپوشه‌ها" selectAll = "انتخاب همه" selectAllHint = "برای انتخاب همه کلیک کنید. نکته: برای افزودن فایل‌ها به‌صورت تکی Ctrl (یا Cmd) را نگه دارید، و برای انتخاب یک بازه از Shift استفاده کنید." selectedCount = "{{count}} مورد انتخاب‌شده" @@ -3874,6 +4095,10 @@ title = "هنوز فایلی در ابر نیست" hint = "فایل‌هایی که بدون بارگذاری ذخیره می‌کنید اینجا می‌مانند. برای افزودن، یک فایل را رها کنید." title = "هیچ فایل صرفاً محلی نیست" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "فایل‌هایی که باز یا ویرایش می‌کنید اینجا نمایش داده می‌شوند." title = "هنوز موردی تغییر نکرده است" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "امکان حذف پوشه نبود." deleteFolderFailedDetail = "امکان حذف پوشه نبود: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "امکان به‌روزرسانی ظاهر پوشه نبود." folderAppearanceFailedDetail = "امکان به‌روزرسانی ظاهر پوشه نبود: {{message}}" moveFilesFailed = "امکان جابه‌جایی فایل‌ها نبود." @@ -3910,9 +4137,7 @@ count = "فایل‌ها" folder = "پوشه" labels = "برچسب‌ها" modified = "تغییر یافته" -name = "نام" size = "حجم" -toolHistoryAtVersion = "زنجیرهٔ ابزار تجمیعی" totalSize = "حجم کل" type = "نوع" versionHistory = "سفر نسخه" @@ -3945,6 +4170,11 @@ localHint = "فقط در این مرورگر ذخیره شده است" shared = "اشتراکی" sharedHint = "از طریق لینک با شما به اشتراک گذاشته شده" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "قدیمی‌ترین ابتدا" modifiedDesc = "جدیدترین ابتدا" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "فایلی در فضای ذخیره‌سازی موجو open = "باز کردن" openFile = "باز کردن فایل" openFiles = "باز کردن فایل‌ها" +selectFile = "Select {{name}}" selectFromStorage = "انتخاب از فضای ذخیره" upload = "بارگذاری" uploadFile = "بارگذاری فایل" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "همه ذخیره شد" +analyzingFields = "Analysing form fields..." +extractCsvError = "استخراج CSV ناموفق بود" +extractXlsxError = "استخراج XLSX ناموفق بود" +filled = "filled" flattenAfterFilling = "مسطح‌سازی پس از پرکردن" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "الزامی" +requiredFieldsError = "لطفاً همه فیلدهای الزامی را پر کنید" rescanFields = "اسکن دوبارهٔ فیلدها" rescanFormFields = "اسکن دوبارهٔ فیلدهای فرم" save = "ذخیره" +saveShortcut = "Ctrl+S برای ذخیره" +unsavedChanges = "تغییرات ذخیره‌نشده" [formFill.sidebar] close = "بستن نوار کناری" @@ -4309,6 +4552,11 @@ desc = "به‌طور خودکار نام فایل PDF را بر اساس سرب tags = "تشخیص خودکار,بر پایه سربرگ,سازمان‌دهی,نام‌گذاری مجدد" title = "تغییر نام خودکار فایل PDF" +[home.autoRotate] +desc = "جهت هر صفحه را تشخیص دهید و آن را به‌طور خودکار راست‌قامت بچرخانید." +tags = "چرخش خودکار،اصلاح جهت،وارونه،افقی،صاف کردن،جهت‌دهی،تشخیص جهت،تصحیح چرخش،جهت‌دهی خودکار،اسکن‌شده،OSD" +title = "چرخش خودکار" + [home.autoSizeSplitPDF] tags = "خودکار,تفکیک,اندازه" title = "تقسیم خودکار بر اساس اندازه/تعداد" @@ -4608,15 +4856,12 @@ hint = "یک فایل تصویر PNG، JPG، SVG یا تصویر دیگر را label = "بارگذاری تصویر" placeholder = "انتخاب فایل تصویر" -[imageToPdf] -tags = "تبدیل،عکس،jpg،تصویر،عکس" - [imageToPDF] header = "تبدیل تصویر به PDF" title = "تبدیل تصویر به PDF" -[infoBanner] -dismiss = "بستن" +[imageToPdf] +tags = "تبدیل،عکس،jpg،تصویر،عکس" [invite] acceptError = "ایجاد حساب ناموفق بود" @@ -4647,11 +4892,9 @@ welcomeTitle = "شما دعوت شده‌اید!" [landing] addFiles = "افزودن فایل‌ها" -heroSubtitle = "برای شروع یک PDF موجود را بکشید و رها کنید یا اضافه کنید." mobileUpload = "آپلود از موبایل" openFromComputer = "باز کردن از رایانه" uploadFromComputer = "بارگذاری از رایانه" -workbenchEmptyStateHero = "یک PDF را هرجا رها کنید" [language] direction = "rtl" @@ -4691,7 +4934,6 @@ enterUsername = "نام کاربری را وارد کنید" failedToSignIn = "ورود با {{provider}} ناموفق بود: {{message}}" forgotPassword = "گذرواژه خود را فراموش کرده‌اید؟" loggingIn = "در حال ورود..." -logIn = "ورود" login = "ورود" magicLinkSent = "لینک جادویی به {{email}} ارسال شد! ایمیل خود را بررسی کرده و برای ورود روی لینک کلیک کنید." mfaCode = "کد احراز هویت" @@ -4715,26 +4957,10 @@ signInWith = "ورود با" title = "ورود" unexpectedError = "خطای غیرمنتظره: {{message}}" updatePassword = "به‌روزرسانی گذرواژه" -useEmailInstead = "ورود با ایمیل" useMagicLink = "به‌جای آن از لینک جادویی استفاده کنید" username = "نام کاربری" youAreLoggedIn = "وارد شده‌اید!" -[login.slides.edit] -alt = "ویرایش PDFها" -subtitle = "با بیش از دوازده ابزار برای پنهان‌سازی، امضا، خواندن و دستکاری PDFها، مطمئناً آنچه می‌خواهید را پیدا می‌کنید." -title = "ویرایش PDFها برای نمایش/ایمن‌سازی اطلاعات موردنظر شما" - -[login.slides.overview] -alt = "نمای کلی Stirling PDF" -subtitle = "یک مجموعه ابری با اولویت حریم خصوصی برای PDFها که امکان تبدیل، امضا، پنهان‌سازی و مدیریت اسناد را به‌همراه 50+ ابزار قدرتمند دیگر فراهم می‌کند." -title = "همه نیازهای PDF شما در یک‌جا." - -[login.slides.secure] -alt = "ایمن‌سازی PDFها" -subtitle = "به‌سادگی گذرواژه اضافه کنید، محتوا را پنهان‌سازی کنید و گواهی‌ها را مدیریت کنید." -title = "از اطلاعات حساس در PDFهای خود محافظت کنید" - [margin] large = "بزرگ" medium = "متوسط" @@ -4818,6 +5044,34 @@ uploadSuccess = "آپلود موفق!" uploadSuccessMessage = "تصاویر شما منتقل شدند." validating = "در حال اعتبارسنجی نشست..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "برای آپلود عکس‌ها اسکن کنید. تصاویر به‌صورت خودکار به PDF تبدیل می‌شوند." descriptionNoConvert = "برای آپلود عکس‌ها از دستگاه موبایل خود اسکن کنید." @@ -4836,8 +5090,13 @@ title = "آپلود از موبایل" tags = "ابزار چندگانه،عملیات چندگانه،واسط کاربری،کلیک و کشیدن،فرانت‌اند،کاربردی،قابل تعامل،جابجایی،حذف،تقسیم" title = "ابزار چندگانه PDF" -[navbar] -search = "جستجو" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "احراز هویت موفق نبود. می‌توانید این پنجره را ببندید و دوباره تلاش کنید." @@ -4966,6 +5225,7 @@ activeFiles = "نمای Active Files همه PDFهایی را ک allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "بستن" cropSettings = "حالا که فایلی را که می‌خواهیم برش دهیم انتخاب کرده‌ایم، می‌توانیم ابزار Crop را پیکربندی کنیم تا ناحیه‌ای را که می‌خواهیم PDF به آن برش داده شود انتخاب کنیم." +dialogLabel = "Onboarding" fileCheckbox = "با کلیک روی یکی از فایل‌ها، آن برای پردازش انتخاب می‌شود. می‌توانید چندین فایل را برای عملیات دسته‌ای انتخاب کنید." fileReplacement = "فایل تغییر‌یافته به‌طور خودکار در Workbench جایگزین فایل اصلی می‌شود و امکان می‌دهد به‌راحتی آن را از ابزارهای بیشتری عبور دهید." filesButton = "دکمه Files در نوار دسترسی سریع به شما امکان می‌دهد PDFها را برای استفاده در ابزارها بارگذاری کنید." @@ -4976,6 +5236,7 @@ pinButton = "اگر می‌خواهید فایل‌ها پس از اجرای ا results = "پس از پایان اجرای ابزار، گام Review پیش‌نمایشی از نتایج را در این پنل نشان می‌دهد و به شما اجازه می‌دهد عملیات را واگردانی یا فایل را دانلود کنید." runButton = "پس از پیکربندی ابزار، این دکمه اجازه می‌دهد ابزار را روی همه PDFهای انتخاب‌شده اجرا کنید." selectCropTool = "بیایید ابزار Crop را انتخاب کنیم تا نحوه استفاده از یکی از ابزارها را نشان دهیم." +stepOf = "مرحله {{current}} از {{total}}" toolInterface = "این رابط ابزار Crop است. همان‌طور که می‌بینید فعلاً چیز زیادی آن‌جا نیست چون هنوز فایل‌های PDF برای کار اضافه نکرده‌ایم." workbench = "این Workbench است - ناحیه اصلی که در آن PDFهای خود را مشاهده و ویرایش می‌کنید." wrapUp = "همه‌چیز آماده است! شما با نواحی اصلی برنامه و نحوه استفاده از آن‌ها آشنا شدید. هر زمان خواستید برای مشاهده دوباره این تور روی دکمه راهنما کلیک کنید." @@ -4988,6 +5249,26 @@ showMeAround = "راهنمایی کن" skipForNow = "فعلاً رد کن" skipTheTour = "از تور بگذر" +[onboarding.checklist] +dismiss = "رد کردن" +title = "راه‌اندازی Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Stirling را به‌صورت بومی روی دستگاه خود اجرا کنید" +title = "دانلود Stirling برای دسکتاپ" + +[onboarding.checklist.inviteTeam] +description = "با تیم خود همکاری کنید" +title = "دعوت از اعضای تیم" + +[onboarding.checklist.shareAnalytics] +description = "به بهبود Stirling کمک کنید" +title = "اشتراک‌گذاری داده‌های استفاده ناشناس" + +[onboarding.checklist.takeTour] +description = "در یک مرور سریع ببینید Stirling چگونه کار می‌کند" +title = "شروع تور" + [onboarding.desktopInstall] body = "Stirling به‌صورت برنامه دسکتاپ بهترین عملکرد را دارد. می‌توانید آفلاین از آن استفاده کنید، سریع‌تر به اسناد دسترسی داشته باشید و ویرایش‌ها را به‌صورت محلی روی رایانه انجام دهید." selectOs = "انتخاب سیستم‌عامل" @@ -4996,7 +5277,21 @@ titleWithOs = "دانلود برای {{osLabel}}" [onboarding.mfa] authenticationCode = "کد احراز هویت" +description = "با اتصال یک برنامه authenticator، حساب خود را ایمن کنید. QR code را اسکن کنید یا کلید راه‌اندازی را وارد کنید، سپس برای پایان، کد ۶ رقمی را تأیید کنید." +enable = "فعال‌سازی MFA" +enableError = "فعال‌سازی احراز هویت دومرحله‌ای ممکن نیست. کد را بررسی کنید و دوباره تلاش کنید." +enterCode = "کد ۶ رقمی برنامه خود را وارد کنید." +enterCodeError = "برای ادامه، کد احراز هویت را وارد کنید." +logout = "خروج" +openAuthenticator = "Google Authenticator، Authy یا 1Password را باز کنید." qrCodeLoading = "در حال تولید کد QR شما…" +regenerateQrCode = "تولید دوباره QR code" +scanQrCode = "QR code را اسکن کنید یا کلید راه‌اندازی زیر را وارد کنید." +setupError = "شروع راه‌اندازی احراز هویت دومرحله‌ای ممکن نیست. لطفاً دوباره تلاش کنید." +setupKey = "کلید راه‌اندازی (ورود دستی)" +stepByStep = "گام‌به‌گام" +success = "MFA فعال شده است. اکنون می‌توانید ادامه دهید." +title = "راه‌اندازی احراز هویت چندعاملی" [onboarding.planOverview] adminBodyLoginDisabled = "وقتی حالت لاگین را فعال کنید، می‌توانید کاربران را مدیریت کنید، تنظیمات را پیکربندی کنید و سلامت سرور را پایش کنید. نخستین {{freeTierLimit}} نفر روی سرور شما به‌صورت رایگان از Stirling استفاده می‌کنند." @@ -5005,6 +5300,11 @@ adminTitle = "نمای کلی ادمین" userBody = "هم‌تیمی‌ها را دعوت کنید، نقش‌ها را اختصاص دهید و اسناد را در یک فضای کار امن منسجم نگه دارید. هر زمان آماده رشد فراتر از استفاده فردی بودید، حالت لاگین را فعال کنید." userTitle = "نمای کلی پلن" +[onboarding.processorIntro] +body = "Stirling اکنون خط‌مشی‌ها را اجرا می‌کند — قوانین خودکاری که هر سند را هنگام ورود دسته‌بندی، ایمن و پردازش می‌کنند. آن‌ها را تنظیم کنید و اجراها را در پردازشگر پایش کنید." +cta = "مشاهده پردازشگر" +title = "پردازشگر Stirling را بررسی کنید" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "سال پیش‌پرداخت‌شده" + +[payg.prepaid.banner] +exhausted = "ظرفیت پیش‌پرداخت شما مصرف شده است؛ صورت‌حساب بر اساس مصرف از سر گرفته شده است. برای حفظ تخفیف، شارژ کنید." +expiring_one = "سال پیش‌پرداخت شما تا {{count}} روز دیگر پایان می‌یابد. ظرفیت استفاده‌نشده منقضی می‌شود — برای شروع یک سال تازه، شارژ کنید." +expiring_other = "سال پیش‌پرداخت شما تا {{count}} روز دیگر پایان می‌یابد. ظرفیت استفاده‌نشده منقضی می‌شود — برای شروع یک سال تازه، شارژ کنید." +low = "شما به {{pct}}% از ظرفیت پیش‌پرداخت خود رسیده‌اید. هر زمان شارژ کنید تا پردازش بدون وقفه ادامه یابد." + +[payg.prepaid.card] +subtitle = "قبل از صورت‌حساب بر اساس مصرف و خارج از سقف هزینه شما استفاده می‌شود." +title = "ظرفیت پیش‌پرداخت" + +[payg.prepaid.meter] +capSuffix = "از {{total}} اعتبار پیش‌پرداخت" +expires = "انقضا {{date}}" + +[payg.prepaid.state] +exhausted = "مصرف شده" +healthy = "مقدار زیادی باقی مانده" +low = "رو به اتمام" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF به PDF/A" tags = "آرشیو،ذخیره‌سازی بلندمدت،استاندارد،تبدیل،ذخیره‌سازی،حفظ" title = "PDF به PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "چاپ,استاندارد,تبدیل,تولید,پیش‌ازچاپ,آرشیو" title = "تبدیل PDF به PDF/X" @@ -5822,6 +6151,7 @@ small = "500 اعتبار" xsmall = "100 اعتبار" [plan.availablePlans] +currency = "Billing currency" subtitle = "طرحی را انتخاب کنید که با نیازهای شما سازگار است" title = "طرح‌های موجود" @@ -5937,49 +6267,12 @@ keyDescription = "کلید لایسنس موجود در ایمیل خود را success = "لایسنس فعال شد!" successMessage = "لایسنس شما با موفقیت فعال شد. اکنون می‌توانید این پنجره را ببندید." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "اعمال شد" -enforcing = "در حال اعمال..." -failed = "اعمال ناموفق بود" outputsUnavailable = "خروجی‌های خط‌مشی دیگر برای دانلود در دسترس نیستند." partialOutputsUnavailable = "برخی از خروجی‌های خط‌مشی دیگر برای دانلود در دسترس نیستند." -retrying = "مشغول است، تلاش مجدد..." runNotFound = "اجرای اعمال دیگر یافت نمی‌شود." -step = "مرحله {{current}}/{{total}}" timedOut = "مهلت اعمال پیش از پایان اجرا به اتمام رسید." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "بستن" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "در حال اعمال {{names}}" applyingProgress = "در حال اعمال {{names}} ({{done}} از {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} از {{total}} فایل پردازش نشدند و failureTitle = "صدور بدون اعمال کامل" printPolicyAppliedBody = "این PDF برای برآورده‌کردن یک خط‌مشی به‌روزرسانی شد. تغییرات را بررسی کرده و سپس دوباره چاپ کنید." printPolicyAppliedTitle = "خط‌مشی پیش از چاپ اعمال شد" -queued = "+{{count}} در صف" successTitle = "{{names}} اعمال شد" summaryMore = "{{first}}، {{second}} و {{more}} مورد دیگر" summaryTwo = "{{first}} و {{second}}" -[policies.enforcement.triggerVerb] -convert = "اعمال پیش از تبدیل" -default = "در حال اعمال" -export = "اعمال پیش از صدور" -input = "اعمال هنگام وارد کردن" -print = "اعمال پیش از چاپ" - -[policies.field] -accessLog = "گزارش دسترسی" -archiveAfter = "بایگانی پس از" -auditTrail = "ردیابی حسابرسی" -belowThreshold = "زیر آستانه" -destination = "مقصد" -frameworks = "چارچوب‌ها" -immutableHold = "نگهداری تغییرناپذیر" -keepFor = "نگهداری برای" -minConfidence = "حداقل اطمینان" -notify = "اعلان هنگام مسیریابی" -onViolation = "هنگام عدم انطباق" -webhookUrl = "URL Webhook" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "افزودن" -addPlaceholder = "افزودن برچسب…" -customNote = "برای تیم شما سفارشی شده است." -defaultNote = "استفاده از پیش‌فرض داخلی، مشترک با تیم شما." -duplicate = "\"{{name}}\" از قبل وجود دارد." -edit = "ویرایش برچسب‌ها" -empty = "هنوز برچسبی وجود ندارد." -export = "خروجی JSON" -iconAria = "انتخاب آیکون برای {{name}}" -import = "وارد کردن JSON" -importError = "امکان وارد کردن آن فایل وجود نداشت." -managedNote = "برچسب‌های تیم توسط رهبر تیم شما مدیریت می‌شوند." -modalSubtitle = "با کل تیم شما مشترک است. طبقه‌بند برچسب‌هایی را انتخاب می‌کند که با هر سند مطابقت دارند." -modalTitle = "برچسب‌های طبقه‌بندی" -pickIcon = "استفاده از آیکون {{label}}" -removeAria = "حذف {{name}}" -resetToDefault = "بازنشانی به پیش‌فرض" -saveForTeam = "ذخیره برای تیم" -saving = "در حال ذخیره…" -sectionLabel = "برچسب‌های طبقه‌بندی" -startFromScratch = "شروع از ابتدا" -teamCount = "برچسب‌های تیم" -tooLong = "برچسب‌ها حداکثر می‌توانند {{max}} نویسه باشند." -ungrouped = "گروه‌بندی‌نشده" -view = "مشاهده برچسب‌ها" +categoryCount = "دسته‌ها" +hideCategory = "پنهان کردن دسته" +labelCount = "برچسب‌ها" +sharedNote = "این برچسب‌ها داخلی هستند و در سراسر تیم شما مشترک‌اند." +showCategory = "نمایش دسته" [policies.pii] account = "شماره‌های حساب (برچسب‌دار)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "شماره‌های مسیریابی بانکی ایالات متحده (ABA)" ssn = "شماره‌های تأمین اجتماعی" -[policies.settings] -noneExport = "در حال حاضر هیچ خط‌مشی‌ای هنگام خروجی اجرا نمی‌شود." -noneUpload = "در حال حاضر هیچ خط‌مشی‌ای هنگام بارگذاری اجرا نمی‌شود." -onExport = "هنگام خروجی" -onUpload = "هنگام بارگذاری" -reorderHandle = "برای تغییر ترتیب بکشید" -runOrderDesc = "وقتی بیش از یک خط‌مشی روی یک ماشه اجرا می‌شود، به این ترتیب اجرا می‌شوند — هرکدام روی خروجی خط‌مشی قبلی. برای تغییر ترتیب بکشید." -title = "تنظیمات خط‌مشی" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "گزینه‌های خط‌مشی" -policySettings = "تنظیمات خط‌مشی" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "تلاش دوباره برای خط‌مشی‌های ناموفق ({{count}})" -rowProgress = "{{completed}} از {{total}} فایل پردازش شد" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "خط‌مشی چیست؟" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "به‌طور خودکار جزئیات حساس — مانند شماره‌های تأمین اجتماعی و کارت — را پیدا کرده و سیاه می‌کند تا در سند قابل خواندن نباشند." -sanitize = "کدهای JavaScript پنهان را از فایل حذف می‌کند تا هنگام باز کردن فایل، هیچ‌چیز به‌صورت خودکار اجرا نشود." -watermark = "یک نشان قابل مشاهده (مثلاً \"Confidential\") را روی هر صفحه درج می‌کند." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "בستن" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} در حال اعمال..." badgeRan = "خط‌مشی {{name}} روی این فایل اجرا شد" +badgeRunning = "{{name}} در حال اجرا..." blockingAction = "{{action}} هنگام اعمال خط‌مشی مسدود شد، لطفاً صبر کنید..." dismiss = "بستن پوشش" enforcingTitle = "در حال اعمال خط‌مشی..." @@ -6151,6 +6316,7 @@ viewAnyway = "مشاهده فایل (خط‌مشی همچنان در حال اع [policyOption] autoRedactPhi = "حذف خودکار PHI" blockExport = "مسدود کردن خروجی" +ccpa = "CCPA" contracts = "قراردادها" documents = "اسناد" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "طرح Editor" subscribed = "طرح Processor" unlinked = "پیوند داده نشده" -[portal.agentBuilder] -bootstrapFromDocument = "راه‌اندازی اولیه از سند" -sectionsAriaLabel = "بخش‌های سازنده عامل" -selectorAriaLabel = "عامل‌ها" -subtitle = "عامل‌های AI را طراحی، آزمایش و منتشر کنید که اسناد شما را طبقه‌بندی می‌کنند، از آن‌ها استخراج می‌کنند و مسیریابی می‌کنند. سناریوها را تعریف کنید، دسترسی ابزار را محدود کنید، یک مجموعه طلایی اجرا کنید و یک نسخه منتشر کنید." -title = "سازنده عامل" - -[portal.agentBuilder.bootstrap] -cancel = "لغو" -dropzoneText = "یک سند نمونه انتخاب کنید (PDF یا تصویر)" -lead = "یک سند نمونه رها کنید تا سناریوها و یک شِمای استخراج پیشنهاد کنیم که بتوانید اصلاحش کنید. تا زمانی که آن را بازبینی نکنید چیزی منتشر نمی‌شود." -submit = "راه‌اندازی اولیه عامل" -subtitle = "آغاز یک عامل جدید از یک فایل نماینده" -title = "راه‌اندازی اولیه از یک سند" - -[portal.agentBuilder.empty] -description = "یک عامل را از یک سند نمونه راه‌اندازی اولیه کنید تا سناریوها و شِمای استخراج آن آغاز شود، سپس اصلاح و منتشر کنید." -title = "هنوز عاملی وجود ندارد" - -[portal.agentBuilder.evals] -casesPassing = "موارد موفق" -columnCase = "مورد ارزیابی" -columnLatency = "تأخیر" -columnResult = "نتیجه" -fail = "ناموفق" -goldenSetPassRate = "نرخ موفقیت مجموعه طلایی" -latencyMs = "{{ms}} ms" -notRun = "اجرا نشده" -pass = "موفق" -passRate = "نرخ موفقیت" -runEvals = "اجرای ارزیابی‌ها" - -[portal.agentBuilder.evals.empty] -description = "ارزیابی‌ها سناریوهای شما را به یک مجموعه طلایی تکرارپذیر تبدیل می‌کنند. ارتقا دهید تا نرخ موفقیت را در طول زمان ثبت کنید و انتشارها را بر اساس آن کنترل کنید." -title = "هنوز مجموعه طلایی وجود ندارد" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "در سراسر مجموعه‌های طلایی" -activeAgents = "عامل‌های فعال" -avgPassRate = "میانگین نرخ موفقیت ارزیابی" -fleetWide = "در کل ناوگان" -latestPublished = "آخرین منتشرشده" -scenarios = "سناریوها" -testCases = "موارد آزمون" -totalDescription_one = "در مجموع {{count}}" -totalDescription_other = "در مجموع {{count}}" - -[portal.agentBuilder.scenarios] -add = "افزودن" -addScenario = "افزودن سناریو" -enable = "فعال‌سازی" -expectationLabel = "رفتار مورد انتظار" -expectationPlaceholder = "عامل باید چه کاری انجام دهد" -inEval = "در ارزیابی" -mute = "بی‌صدا کردن" -muted = "بی‌صدا شده" -nameLabel = "نام" -namePlaceholder = "مثلاً تشدید انطباق" - -[portal.agentBuilder.tabs] -evals = "ارزیابی‌ها" -scenarios = "سناریوها" -tools = "ابزارها" -versions = "نسخه‌ها" - -[portal.agentBuilder.tools] -broadAccess = "دسترسی گسترده" -deniedHint = "ابزارهای انتخاب‌شده مسدود هستند. همه موارد دیگر همچنان قابل فراخوانی می‌مانند." -deniedTools = "ابزارهای ردشده" -governanceGate = "حاکمیت ابزار در طرح Enterprise در دسترس است." -restricted = "محدود" -restrictedAccess = "دسترسی محدود به ابزار" -restrictedDescription = "همه ابزارها را به‌جز مواردی که در زیر رد می‌کنید مجاز کنید." - -[portal.agentBuilder.versions] -current = "فعلی" -historyGate = "تاریخچه کامل نسخه‌ها و بازگشت به عقب در طرح Enterprise در دسترس است." -publish = "انتشار" -rollBack = "بازگشت به عقب" - [portal.assistant] close = "بستن دستیار" error = "امکان دسترسی به دستیار وجود نداشت." @@ -6365,9 +6451,21 @@ typing = "در حال تایپ" loading = "در حال بارگیری" redirectingToEditor = "در حال هدایت به ویرایشگر..." +[portal.billing.activation] +maybeLater = "شاید بعداً" +title = "روشن کردن پردازشگر" + +[portal.billing.activation.payg] +sub = "یک محدودیت ماهانه تعیین کنید. برای آنچه اجرا می‌کنید پرداخت کنید." +title = "پرداخت به‌ازای مصرف" + +[portal.billing.activation.prepay] +badge = "۲ ماه رایگان" +sub = "یک فاکتور، کارت یا انتقال بانکی." +title = "پیش‌پرداخت یک سال" + [portal.billing.checkout] noClientSecret = "تابع Edge مقدار client_secret برنگرداند." -subtitle = "برای ادامه پس از اعتبار رایگان طرح Editor، یک کارت اضافه کنید. Stripe بقیه کارها را انجام می‌دهد." title = "فعال کردن طرح Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "پرداخت شما موفق بود، اما فعال‌سازی کمی close = "بستن" title = "تقریباً تمام شد" +[portal.billing.checkout.cap] +amountAria = "محدودیت هزینه ماهانه" +back = "بازگشت" +continue = "ادامه به پرداخت" +error = "امکان تنظیم محدودیت هزینه شما نبود" +finePrint = "فاکتورها ماهانه صادر می‌شوند. هر زمان لغو کنید." +noLimit = "بدون محدودیت" +note = "هرگز بیش از محدودیت خود صورتحساب نمی‌شوید. پردازش فقط متوقف می‌شود." +perMonth = "/ ماه" +title = "محدودیت هزینه خود را تنظیم کنید" + +[portal.billing.checkout.card] +fields = "شماره کارت · MM / YY · CVC · ZIP" +label = "جزئیات کارت" +note = "جزئیات کارت توسط Stripe جمع‌آوری می‌شود. Stirling هرگز PAN یا CVC را ذخیره نمی‌کند." + [portal.billing.checkout.error] title = "امکان شروع پرداخت وجود نداشت" @@ -6383,10 +6497,9 @@ body = "پرداخت شما انجام شد. ما در حال فعال‌ساز hint = "لطفاً این پنجره را باز نگه دارید." title = "در حال فعال‌سازی طرح Processor شما..." -[portal.billing.checkout.notConfigured] -bodyAfter = "در env پورتال تنظیم کنید تا پرداخت درون‌برنامه‌ای فعال شود." -bodyBefore = "تنظیم کنید" -title = "Stripe پیکربندی نشده است" +[portal.billing.checkout.pay] +limit = "محدودیت هزینه ماهانه {{amount}}/ماه. پردازش در حد محدودیت متوقف می‌شود." +limitNoCap = "بدون محدودیت هزینه. پردازش متوقف نمی‌شود. فاکتورها ماهانه صادر می‌شوند." [portal.billing.enterpriseUpsell] cta = "پیشنهاد قیمت Enterprise خود را بسازید" @@ -6409,10 +6522,12 @@ currentPlan = "طرح فعلی" freeForever = "برای همیشه رایگان" noTeamResolved = "هنوز هیچ تیمی در کیف پول شما شناسایی نشده است — تازه‌سازی کنید و دوباره تلاش کنید." ownerOnly = "فقط مالک تیم می‌تواند طرح Processor را فعال کند." +payInvoice = "پرداخت فاکتور برای تکمیل" planName = "Editor" ssoIncluded = "SSO شامل است" switchOnProcessor = "فعال کردن Processor →" unlimitedUsers = "کاربران نامحدود" +viewQuote = "مشاهده پیش‌فاکتور" [portal.billing.invoices] columnAmount = "مبلغ" @@ -6465,7 +6580,111 @@ segmentApiDesc = "درخواست‌های مستقیم API" segmentApiLabel = "API" segmentAutomationDesc = "اتوماسیون‌ها و خط لوله‌ها" segmentAutomationLabel = "اتوماسیون" +sizeMultiplier = "{{formatted}} PDF از ضریب اندازه استفاده کردند" +summary = "{{unique}} یکتا · {{units}} واحد کنتور · میانگین {{avg}} برای هر PDF" +summaryNoRate = "{{unique}} یکتا · {{units}} واحد کنتور" unit = "PDFهای اندازه‌گیری‌شده" +unitsPending_one = "{{units}} واحد کنتور در انتظار همگام‌سازی از نمونه‌های متصل" +unitsPending_other = "{{units}} واحد کنتور در انتظار همگام‌سازی از نمونه‌های متصل" + +[portal.billing.prepaid] +capSuffix = "از {{total}} اعتبار پیش‌پرداخت" +expires = "انقضا {{date}}" +eyebrow = "ظرفیت پیش‌پرداخت" +note = "قبل از صورت‌حساب بر اساس مصرف و خارج از محدودیت هزینه شما استفاده می‌شود." +topUp = "شارژ" + +[portal.billing.prepaid.buy] +back = "بازگشت" +calcTitle = "محاسبه پرداخت سالانه شما" +cancel = "لغو" +cancelPurchase = "لغو خرید" +close = "بستن" +continueToPayment = "ادامه به پرداخت" +finalise = "نهایی‌سازی" +invoiceErrorTitle = "امکان ایجاد فاکتور نبود" +loadingTitle = "در حال بارگذاری پیش‌فاکتور شما" +payTitle = "پرداخت برای سال شما" +step = "مرحله {{current}} از {{total}}" + +[portal.billing.prepaid.calc] +change = "تغییر" +deployRow = "استقرار" +done = "انجام شد" +enterpriseHint = "این در مقیاس سازمانی است. در این حجم، نرخ‌های سازمانی از هر تخفیف سلف‌سرویس بهتر هستند. قفل نرخ، شرایط و پیش‌فاکتور در چند دقیقه." +governanceRow = "حاکمیت" +governanceValue = "{{label}} · {{count}} خط‌مشی" +handlesLabel = "پردازشگر شما" +handlesValue = "{{volume}} اعتبار / ماه را پردازش می‌کند" +pipelinesLabel = "پایپ‌لاین‌ها" +policiesMeta = "{{count}} خط‌مشی" +poolCaption = "یک مخزن از {{credits}} اعتبار برای سال. ماه‌های سنگین از ماه‌های سبک قرض می‌گیرند." +priceLabel = "سال شما · ۱۲ ماه به قیمت ۱۰ ماه" +rateUnknown = "قیمت دقیق را در پرداخت نشان می‌دهیم." +savings = "شما {{amount}} صرفه‌جویی می‌کنید · ۲ ماه رایگان." +sizingRow = "اندازه‌بندی" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "کل کاربران" + +[portal.billing.prepaid.consent] +label = "می‌دانم که وقتی ظرفیت پیش‌پرداخت من مصرف شود یا پس از ۱۲ ماه منقضی شود، پردازش به‌طور خودکار با نرخ استاندارد پرداخت به‌ازای مصرف بر اساس کنتور ادامه می‌یابد (تا سقف محدودیت هزینه من) مگر اینکه لغو کنم، و می‌توانم هر زمان از پورتال صورت‌حساب لغو کنم." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "مدیریت‌شده توسط Stirling. فعال در چند دقیقه." +cloudValue = "Stirling Cloud · مدیریت‌شده" +selfhost = "خودمیزبان" +selfhostDesc = "روی زیرساخت خصوصی اجرا می‌شود. همان نرخ." +selfhostValue = "خودمیزبان · زیرساخت خصوصی" + +[portal.billing.prepaid.offer] +cta = "بررسی پیشنهاد" +subtitle = "یک سال پردازش PDF را پیش‌پرداخت کنید و دو ماه رایگان دریافت کنید؛ پیش از صورت‌حساب بر اساس مصرف و خارج از محدودیت هزینه شما استفاده می‌شود." +title = "۱۲ ماه را به قیمت ۱۰ ماه دریافت کنید" + +[portal.billing.prepaid.pay] +accountName = "نام صاحب حساب" +accountNamePlaceholder = "مثلاً Jane Smith" +companyName = "نام شرکت (اختیاری)" +companyNamePlaceholder = "مثلاً Acme Inc." +downloadInvoice = "دانلود فاکتور" +payOnline = "پرداخت آنلاین" +poNumber = "شماره PO (اختیاری)" +poNumberPlaceholder = "مثلاً PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "پیشرفته" +advancedDesc = "پایپ‌لاین‌ها بیشتر پردازش شما را انجام می‌دهند." +none = "هیچ‌کدام" +noneDesc = "هنوز پایپ‌لاین‌ها اجرا نمی‌شوند. هر زمان آن‌ها را روشن کنید." +standard = "استاندارد" +standardDesc = "چند پایپ‌لاین PDFهای ورودی را دوباره پردازش می‌کنند." + +[portal.billing.prepaid.posture] +essentials = "ضروریات" +essentialsDesc = "دسته‌بندی، حالت پیش‌فرض، به‌علاوه اشتراک‌گذاری" +governed = "تحت حاکمیت" +governedDesc = "امنیت و مسیریابی را اضافه می‌کند" +regulated = "تحت مقررات" +regulatedDesc = "هر دسته، شامل Compliance، Retention، Ingestion" + +[portal.billing.prepaid.review] +download = "دانلود پیش‌فاکتور (PDF)" +downloading = "در حال آماده‌سازی…" +share = "اشتراک‌گذاری برای تأیید" + +[portal.billing.prepaid.size] +compact = "فشرده" +compactDesc = "فایل‌های کمتر از ۲۵ MB، بدون هزینه داده" +heavy = "سنگین" +heavyDesc = "اسکن‌شده یا پر از تصویر، معمولاً ۵۰ MB+" +standard = "استاندارد" +standardDesc = "عمدتاً کوچک، برخی اسکن‌ها بیش از ۲۵ MB" + +[portal.billing.prepaid.state] +exhausted = "مصرف شده" +healthy = "مقدار زیادی باقی مانده" +low = "رو به اتمام" [portal.billing.spendLimit] adjustLimit = "تنظیم حد" @@ -6494,6 +6713,8 @@ label = "پیش‌بینی می‌شود از حد عبور کند." [portal.billing.spendThisMonth] eyebrow = "هزینه این ماه" +freeRemaining_one = "{{formatted}} PDF رایگان باقی مانده" +freeRemaining_other = "{{formatted}} PDF رایگان باقی مانده" processed_one = "{{formattedCount}} PDF پردازش شد." processed_other = "{{formattedCount}} PDF پردازش شد." processedWithRate_one = "{{formattedCount}} PDF پردازش شد، هرکدام با {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "حد هزینه ماهانه رسیده است" title = "امکان باز کردن پورتال Stripe وجود نداشت" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "از {{allowance}} PDF رایگان استفاده شده" capSuffix_other = "از {{allowance}} PDF رایگان استفاده شده" eyebrow = "آزمایشی Processor" @@ -6534,6 +6756,376 @@ signature = "امضا" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "اتصال جدید" +createTitleFor = "اتصال {{name}}" +delete = "حذف" +edit = "ویرایش" +editTitle = "ویرایش اتصال" +unsupportedType = "این نوع اتصال از اینجا قابل ویرایش نیست." + +[portal.connections.categories.advanced] +description = "برای سرویسی که هنوز پشتیبانی نمی‌کنیم." +label = "پیشرفته" + +[portal.connections.categories.audit] +description = "ثبت کنید چه اتفاقی برای یک سند افتاده است، همان‌جایی که حسابرسان شما از قبل بررسی می‌کنند." +label = "ثبت ممیزی و انطباق" + +[portal.connections.categories.notify] +description = "به سیستم دیگری اطلاع دهید که اتفاقی افتاده است." +label = "اعلان و گردش‌کار" + +[portal.connections.categories.security] +description = "قبل از انتقال سند، آن را اسکن یا برچسب‌گذاری کنید." +label = "امنیت و دسته‌بندی" + +[portal.connections.categories.signing] +description = "سند را برای امضا یا ثبت رسمی ارسال کنید." +label = "امضای الکترونیکی و ثبت رسمی" + +[portal.connections.categories.storage] +description = "جایی که اسناد پردازش‌شده در آن قرار می‌گیرند - یک bucket، یک drive، یک ticket یا یک page." +label = "فایل و پیوست" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling این را رمزگذاری‌شده ذخیره می‌کند و دیگر هرگز نشان نمی‌دهد." +label = "توکن دسترسی" + +[portal.connections.commonFields.apiKey] +label = "کلید API" + +[portal.connections.commonFields.apiToken] +label = "توکن API" + +[portal.connections.commonFields.baseUrl] +helperText = "مراحلی که از این اتصال استفاده می‌کنند فقط می‌توانند مسیرهای زیر این URL را فراخوانی کنند، نه میزبان دیگری را." +label = "URL سرور" + +[portal.connections.commonFields.email] +label = "ایمیل" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "رمز عبور" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "نام کاربری" + +[portal.connections.commonFields.webhookUrl] +helperText = "URL ارائه‌شده توسط سرویس را جای‌گذاری کنید. فقط مسیرهای زیر آن قابل فراخوانی هستند." +label = "Webhook URL" + +[portal.connections.fields] +name = "نام اتصال" +namePlaceholder = "مثلاً Production" + +[portal.connections.picker] +cancel = "لغو" +createNew = "اتصال جدید..." +placeholder = "یک اتصال انتخاب کنید" +save = "ذخیره اتصال" + +[portal.connections.picker2] +back = "تغییر یکپارچه‌سازی" +heading = "می‌خواهید چه چیزی را متصل کنید؟" +noResultsBody = "هنوز از آن پشتیبانی نمی‌کنیم - درباره آن به ما بگویید تا افزودنش را بررسی کنیم." +noResultsTitle = "هیچ موردی با «{{query}}» مطابقت ندارد" +searchPlaceholder = "جست‌وجوی یکپارچه‌سازی‌ها - «sign»، «ocr» یا نام محصول را امتحان کنید" + +[portal.connections.types.api] +description = "هر HTTP API را از یک خط‌مشی فراخوانی کنید. شما URL، احراز هویت و payload را انتخاب می‌کنید." +label = "API سفارشی" + +[portal.connections.types.api.fields.authType] +label = "احراز هویت" + +[portal.connections.types.api.fields.authType.options.basic] +label = "نام کاربری و رمز عبور" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "توکن در header سفارشی" + +[portal.connections.types.api.fields.authType.options.none] +label = "هیچ‌کدام" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "ورود برای دریافت توکن" + +[portal.connections.types.api.fields.baseUrl] +helperText = "مراحلی که از این اتصال استفاده می‌کنند فقط می‌توانند مسیرهای زیر این URL را فراخوانی کنند، نه میزبان دیگری را." +label = "URL پایه" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "نام header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "مسیر ورود" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "رمز عبور" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "اگر API با یک لینک دانلود پاسخ دهد، فقط این hostها می‌توانند دریافت شوند. با کاما جدا کنید. برای اجازه فقط به host خود URL پایه، خالی بگذارید." +label = "hostهای مجاز نتیجه" + +[portal.connections.types.api.fields.token] +label = "توکن" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "ارسال توکن به‌عنوان این header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "header پاسخی که فراخوانی ورود توکن را در آن برمی‌گرداند، مانند X-Auth-Token." +label = "header پاسخ توکن" + +[portal.connections.types.api.fields.username] +label = "نام کاربری" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "اسناد را برای بدافزار روی سروری که خودتان اجرا می‌کنید اسکن کنید - هیچ‌چیز از شبکه شما خارج نمی‌شود." +label = "ClamAV (خودمیزبان)" + +[portal.connections.types.cloudmersive] +description = "اسکن ویروس و محتوا برای اسناد در یک خط‌مشی." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "اسنادی را که شامل ماکروها، فایل‌های اجرایی، اسکریپت‌ها یا اشیای جاسازی‌شده هستند مسدود کنید - نه فقط بدافزارهای شناخته‌شده." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "اسناد پردازش‌شده را به یک صفحه Confluence پیوست کنید." +label = "Confluence" + +[portal.connections.types.consigno] +description = "اسناد را برای امضای الکترونیکی و ثبت رسمی به ConsignO Cloud (Notarius) ارسال کنید." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "محیط ConsignO Cloud شما، شامل /api/v1." +label = "URL سرور" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "کلید API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "راز API" + +[portal.connections.types.consigno.fields.password] +label = "رمز عبور" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "فقط اگر سازمان شما از tenantها استفاده می‌کند لازم است." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "نام کاربری" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "وقتی یک خط‌مشی سندی را پردازش می‌کند، پیامی در Discord ارسال کنید." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "هر بار که یک خط‌مشی سندی را مدیریت می‌کند، یک رویداد ممیزی را در Elasticsearch ایندکس کنید." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "یک سرور FTP یا FTPS را برای اسناد جدید پایش کنید." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "حالت اتصال" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (توصیه‌شده)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP ساده اعتبارنامه‌ها و فایل‌ها را بدون رمزگذاری ارسال می‌کند؛ اگر سرور پشتیبانی می‌کند، FTPS را انتخاب کنید." +label = "رمزگذاری" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "هیچ‌کدام (FTP ساده)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "وقتی یک خط‌مشی اجرا می‌شود، پیامی به فضای Google Chat ارسال کنید." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "اسناد پردازش‌شده را به یک issue در Jira پیوست کنید." +label = "Jira" + +[portal.connections.types.mailgun] +description = "وقتی یک خط‌مشی اجرا می‌شود، سند یا اعلان را ایمیل کنید." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "اسناد پردازش‌شده را در Nextcloud قرار دهید." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "رمز عبور برنامه" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "با استفاده از یک سرور Presidio که خودتان اجرا می‌کنید، داده‌های شخصی را در متن سند تشخیص دهید." +label = "Presidio (خودمیزبان)" + +[portal.connections.types.purview] +description = "برچسب‌های حساسیت Microsoft Purview را روی PDFها بخوانید و اعمال کنید." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "اختیاری. فقط برای انتخاب برچسب‌ها با نام لازم است؛ برچسب‌گذاری بدون آن هم کار می‌کند." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "راز client" + +[portal.connections.types.purview.fields.tenantId] +helperText = "روی هر برچسب به‌عنوان SiteId آن درج می‌شود تا ابزارهای آگاه از Purview بدانند کدام سازمان آن را اعمال کرده است." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "یک bucket در S3 که منابع می‌توانند از آن بخوانند و خروجی‌های پایپ‌لاین می‌توانند در آن بنویسند." +label = "ذخیره‌سازی S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "برای AWS خالی بگذارید. برای MinIO یا ذخیره‌ساز سازگار با S3 دیگر، این را تنظیم کنید." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "وقتی یک خط‌مشی اجرا می‌شود، سند یا اعلان را ایمیل کنید." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "یک سرور SFTP (SSH) را برای اسناد جدید پایش کنید." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "اختیاری. اثرانگشت کلید host سرور با SHA-256 (از `ssh-keyscan host | ssh-keygen -lf -`). وقتی تنظیم شود، فقط سروری که این کلید را ارائه دهد پذیرفته می‌شود؛ وقتی خالی باشد، کلیدی که در اولین اتصال دیده می‌شود pin می‌شود و تغییرات بعدی رد می‌شوند." +label = "اثر انگشت کلید host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "عبارت عبور کلید" + +[portal.connections.types.sftp.fields.password] +helperText = "اگر به‌جای آن با کلید خصوصی احراز هویت می‌کنید، خالی بگذارید." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "اختیاری. برای احراز هویت با کلید به‌جای رمز عبور، یک کلید خصوصی OpenSSH را جای‌گذاری کنید." +label = "کلید خصوصی" + +[portal.connections.types.slack] +description = "وقتی یک خط‌مشی سندی را پردازش می‌کند، پیامی در Slack ارسال کنید." +label = "Slack" + +[portal.connections.types.smb] +description = "یک SMB / CIFS network share را زیر نظر بگیرید (Windows یا Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / اشتراک شبکه" + +[portal.connections.types.smb.fields.domain] +helperText = "اختیاری. اگر سرور شما از آن استفاده می‌کند، domain یا workgroup ویندوز." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "هر بار که یک خط‌مشی سندی را مدیریت می‌کند، یک رویداد ممیزی به Splunk ارسال کنید." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "یک رویداد ممیزی را به یک HTTP collector در Sumo Logic ارسال کنید." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "وقتی یک خط‌مشی اجرا می‌شود، پیامی به کانال Teams ارسال کنید." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "هر سند، یا فقط جزئیات را به URL انتخابی خود ارسال کنید." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "یک سناریوی Zap یا Make را از یک خط‌مشی فعال کنید." +label = "Zapier و Make" + +[portal.docs] +browse = "مرور مستندات" +viewSource = "مشاهده منبع در GitHub" + [portal.docs.authentication] codeCaption = "هر درخواست" eyebrow = "شروع به کار" @@ -6624,6 +7216,9 @@ eyebrow = "مهارت‌ها" lead = "قابلیت‌های بسته‌بندی‌شده و نام‌دار که عامل شما به‌عنوان یک ابزار واحد فراخوانی می‌کند. هر مهارت یک زنجیره عملیات قطعی با ارزیابی‌های پیوست‌شده است." title = "مهارت‌های عامل" +[portal.docs.toc] +title = "در این صفحه" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "مرجع API" @@ -6697,6 +7292,8 @@ type = "نوع" user = "کاربر" [portal.documents.queue.empty] +connectSource = "اتصال یک منبع" +createPipeline = "ایجاد یک پایپ‌لاین" description = "وقتی منابع اسناد را وارد پایپ‌لاین‌های شما کنند، برای بازبینی اینجا نمایش داده می‌شوند." title = "هیچ سندی در صف نیست" @@ -6712,11 +7309,11 @@ editorAction = "ویرایشگر" empty = "هیچ سندی با این فیلتر مطابقت ندارد." rowActions = "اقدامات ردیف" sensitiveLabel = "حساس" -sensitiveTitle = "حساس — دسترسی لازم است" [portal.documents.table.columns] action = "Pipeline / اقدام" document = "سند" +labels = "Labels" product = "محصول" status = "وضعیت" time = "زمان" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "آخرین مشاهده" region = "Region" status = "وضعیت" +target = "Target" version = "نسخه" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "این نما با خطای غیرمنتظره‌ای روبه‌ retry = "دوباره تلاش کنید" title = "در این صفحه مشکلی پیش آمد" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "بازگشت" +body = "برنامه دسکتاپ را دانلود کنید یا سرور را خودمیزبان کنید." +done = "انجام شد" +guide = "باز کردن راهنمای {{name}}" +openInBrowser = "به‌جای آن در مرورگر باز کنید" +sectionDesktop = "برنامه دسکتاپ" +sectionSelfHosted = "خودمیزبان" +title = "نصب ویرایشگر Stirling PDF" + +[portal.home.download.docker] +detailBody = "image رسمی را روی سرور خود اجرا کنید." +note = "در :8080 باز می‌شود. ورود پیش‌فرض: admin / stirling." +tagline = "اجرای سرور در یک کانتینر" +title = "Docker" +variantFat = "کامل" +variantLite = "فوق‌سبک" +variantStandard = "استاندارد" + +[portal.home.download.kubernetes] +detailBody = "نمودار رسمی Helm را نصب کنید." +note = "برای /configs یک PVC اضافه کنید تا تنظیمات پایدار بمانند." +tagline = "استقرار در یک کلاستر با Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "بیلدهای بومی برای توزیع شما." +guideBtn = "مشاهده گزینه‌های نصب Linux" +note = "بیلدهای AppImage، .deb و .rpm برای توزیع‌های رایج در دسترس هستند." +tagline = "برنامه دسکتاپ بومی برای توزیع شما" +title = "Linux" + +[portal.home.download.mac] +altLabel = "یا از طریق Homebrew" +detailBody = "نصب‌کننده را اجرا کنید، یا از Homebrew استفاده کنید." +downloadBtn = "دانلود برای macOS" +tagline = "برنامه بومی برای Apple silicon و Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "فایل .jar را روی یک سرور bare metal اجرا کنید." +downloadBtn = "دانلود .jar" +note = "به Java 21+ نیاز دارد. در :8080 باز می‌شود؛ ورود پیش‌فرض admin / stirling است." +runLabel = "سپس آن را اجرا کنید" +tagline = "اجرای .jar روی یک سرور bare metal" +title = "راه‌اندازی دستی سرور" + +[portal.home.download.windows] +altLabel = "یا از طریق winget" +detailBody = "نصب‌کننده را اجرا کنید، یا از یک مدیر بسته استفاده کنید." +downloadBtn = "دانلود نصب‌کننده" +tagline = "برنامه بومی برای Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} فعال" -invite = "دعوت از هم‌تیمی‌ها" +activeOfDeployed = "{{active}} از {{total}} در این ماه فعال" name = "Stirling PDF Editor" open = "باز کردن در مرورگر" updated = "به‌روزرسانی‌شده {{time}}" +[portal.home.editor.deploy] +finish = "پایان استقرار" +options = "مشاهده گزینه‌های استقرار" +start = "استقرار ویرایشگر" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "خودمیزبان · Docker" @@ -6823,49 +7529,6 @@ afternoon = "عصر بخیر" evening = "شب بخیر" morning = "صبح بخیر" -[portal.home.onboarding] -dismiss = "رد کردن راه‌اندازی" -notStarted = "شروع‌نشده" -progress = "{{done}} از {{total}} انجام شد" -title = "تکمیل راه‌اندازی" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC در سطح سازمان، قیمت‌گذاری حجمی تعهدی، و استقرار air-gapped." -cta = "شروع دوره آزمایشی" -ctaQuote = "دریافت پیش‌فاکتور" -lead = "برای بیش از ۲۵۰ کارمند." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "برنامه دسکتاپ را مستقر کنید، برای همیشه رایگان." -title = "دانلود PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} فعال · {{recommended}} پیشنهادی" -chip = "{{active}} فعال" -title = "فعال کردن سیاست‌ها" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} متصل · هر PDF در محل ورود تحت حاکمیت است" -chip = "{{connected}} متصل" -title = "اتصال منابع" - -[portal.home.quickActions] -subtitle = "کارهای اصلی امروز" -title = "اقدامات سریع" - -[portal.home.quickActions.buildPipeline] -blurb = "سازنده ۳ مرحله‌ای روی کتابخانه عملیات تایپ‌شده" -title = "ساخت یک پایپ‌لاین" - -[portal.home.quickActions.connectSource] -blurb = "S3، agentها، webhookها، پوشه‌های پایش‌شده" -title = "اتصال یک منبع" - -[portal.home.quickActions.issueApiKey] -blurb = "کلید محدودشده با rate limit و IP allowlist" -title = "صدور کلید API" - [portal.infrastructure] manageEditorDeployment = "مدیریت استقرار ویرایشگر" sectionsAriaLabel = "بخش‌های زیرساخت" @@ -6878,13 +7541,9 @@ heading = "کلیدهای API" subheading = "اعتبارنامه‌های محدودشده با rate limit، مجوزها و IP allowlist برای هر کلید." [portal.infrastructure.apiKeys.card] -allowedIps = "IPهای مجاز" -anyIp = "هر IP (بدون allowlist)" created = "ایجادشده" lastUsed = "آخرین استفاده" -permissions = "مجوزها" -rateLimit = "Rate limit" -rateLimitValue = "{{value}} درخواست/دقیقه" +revoke = "لغو کلید" usageMonth = "مصرف این ماه" usageToday = "مصرف امروز" @@ -6892,6 +7551,15 @@ usageToday = "مصرف امروز" description = "برای شروع فراخوانی Stirling API یک کلید محدودشده ایجاد کنید." title = "هنوز کلید API وجود ندارد" +[portal.infrastructure.apiKeys.error] +load = "بارگذاری کلیدهای API شما ممکن نشد. لطفاً دوباره تلاش کنید." + +[portal.infrastructure.apiKeys.revoke] +body = "کلید \"{{name}}\" لغو شود؟ هر یکپارچه‌سازی که هنوز از این کلید استفاده می‌کند، بلافاصله خطاهای 401 دریافت خواهد کرد. این کار قابل بازگشت نیست." +cancel = "لغو" +confirm = "لغو کلید" +title = "لغو کلید API" + [portal.infrastructure.audit] filterAriaLabel = "فیلتر کردن رویدادهای حسابرسی بر اساس دسته" heading = "گزارش‌های حسابرسی" @@ -6935,6 +7603,7 @@ all = "همه" auth = "احراز هویت" config = "پیکربندی" elevation = "ارتقای دسترسی" +policy = "خط‌مشی" processing = "پردازش" security = "امنیت" @@ -6944,19 +7613,30 @@ title = "شما دسترسی ندارید" [portal.infrastructure.audit.metrics] config = "پیکربندی" -elevation = "ارتقای دسترسی" +policy = "خط‌مشی" processing = "پردازش" totalEvents = "کل رویدادها · ۲۴ ساعت" +[portal.infrastructure.auditCatLabel] +auth = "احراز هویت" +config = "پیکربندی" +elevation = "ارتقا" +policy = "خط‌مشی" +processing = "پردازش" +security = "امنیت" + +[portal.infrastructure.auditStatusLabel] +danger = "خطا" +info = "اطلاعات" +success = "موفقیت" +warning = "هشدار" + [portal.infrastructure.createKey] cancel = "لغو" createKey = "ایجاد کلید" done = "انجام شد" -ipAllowlistHelper = "محدوده‌های CIDR جداشده با ویرگول. برای مجاز بودن هر IP خالی بگذارید." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "نام کلید" keyNamePlaceholder = "مثلاً Production · ingest" -permissionsLabel = "مجوزها" secretKeyCaption = "کلید محرمانه" secretWarning = "این را در یک secrets manager ذخیره کنید. Stirling فقط یک hash ذخیره می‌کند — هیچ راهی برای بازیابی آن در آینده وجود ندارد." subtitle = "دامنه کلید را به حداقل نیاز آن محدود کنید. هر زمان می‌توانید آن را بچرخانید یا لغو کنید." @@ -6964,207 +7644,9 @@ subtitleCreated = "این secret را اکنون کپی کنید — دوبار title = "ایجاد کلید API" titleCreated = "کلید ایجاد شد" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/دقیقه" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "مستقرشده توسط" -environment = "محیط" -product = "محصول" -status = "وضعیت" -version = "نسخه" -when = "زمان" - -[portal.infrastructure.deployments.recent] -heading = "استقرارهای اخیر" -subheading = "آخرین rolloutها در محصولات و محیط‌ها." - -[portal.infrastructure.deployments.regionColumns] -instances = "نمونه‌ها" -latency = "Latency" -load = "بار" -p99 = "P99" -region = "Region" -status = "وضعیت" -throughput = "Throughput" -uptime = "Uptime" -version = "نسخه" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "سلامت زنده برای هر Stirling region مستقرشده — latency، بار و نسخه rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "پس از تأمین فضای کاری شما، regionهای مستقرشده اینجا نمایش داده می‌شوند." -title = "هیچ regionی مستقر نشده است" - -[portal.infrastructure.models] -heading = "مدل‌ها" -msValue = "{{value}} ms" -subheading = "کاتالوگ مدل و routing که پردازش سند را در سراسر فضای کاری شما توانمند می‌کند." - -[portal.infrastructure.models.byom] -description = "یک مدل on-prem یا خودمیزبان ثبت کنید و آن را برای پردازش محدود به data residency به یک region پین کنید." -title = "مدل خودتان را بیاورید" - -[portal.infrastructure.models.catalogue] -heading = "کاتالوگ" -sub = "مدل‌های مدیریت‌شده در دسترس فضای کاری شما، با latency و هزینه زنده." -subEnterprise = "مدل‌های مدیریت‌شده، bring-your-own و on-prem — با امکان پین کردن برای هر region." - -[portal.infrastructure.models.catalogue.empty] -description = "مدل‌های موجود در کاتالوگ فضای کاری شما اینجا نمایش داده می‌شوند." -title = "هیچ مدلی در دسترس نیست" - -[portal.infrastructure.models.columns] -cost = "هزینه" -latency = "Latency" -load = "بار" -model = "مدل" -status = "وضعیت" -type = "نوع" -version = "نسخه" - -[portal.infrastructure.models.metrics] -activeModels = "مدل‌های فعال" -avgLatency = "میانگین latency" -included = "شامل‌شده" -monthlySpend = "هزینه ماهانه مدل" - -[portal.infrastructure.models.routing] -empty = "هیچ قانون routing پیکربندی نشده است." -heading = "قوانین routing" -sub = "کدام مدل هر عملیات را مدیریت می‌کند. حالت پیش‌فرض زمانی اعمال می‌شود که هیچ قانون محدودتری مطابقت نداشته باشد." -subLocked = "هدایت عملیات به مدل‌های مشخص — در طرح‌های پولی در دسترس است." - -[portal.infrastructure.models.routing.lockedBanner] -description = "برای کنترل اینکه کدام مدل هر عملیات و نوع سند را مدیریت کند، به Pro ارتقا دهید." -title = "Model routing یک قابلیت پولی است" - -[portal.infrastructure.models.routingColumns] -default = "پیش‌فرض" -docType = "نوع سند" -modelForAria = "مدل برای {{operation}}" -operation = "عملیات" -routedTo = "هدایت‌شده به" - -[portal.infrastructure.security.access.byok] -description = "یک کلید از KMS خودتان ارائه دهید. Stirling با آن رمزنگاری می‌کند اما همچنان می‌تواند بخواند." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "کلیدها هرگز KMS شما را ترک نمی‌کنند. Stirling فقط ciphertext را نگه می‌دارد." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling کلیدهای رمزنگاری را مدیریت می‌کند. ساده‌ترین حالت — بدون عملیات کلید از سمت شما." -label = "کلیدهای نگه‌داری‌شده توسط Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "سیاست دسترسی سند" -subheading = "کنترل می‌کند چه کسانی می‌توانند اسناد پردازش‌شده در حالت at rest را رمزگشایی کنند." - -[portal.infrastructure.security.attestations] -heading = "گواهی‌های انطباق" -noReport = "هیچ گزارشی در دسترس نیست" -subheading = "وضعیت حسابرسی چارچوب‌به‌چارچوب، با گزارش‌های در دسترس برای کنترل‌های گواهی‌شده." -viewReport = "مشاهده گزارش →" - -[portal.infrastructure.security.compliance] -heading = "انطباق" -subheading = "گواهی‌ها و تأییدیه‌هایی که پلتفرم Stirling را پوشش می‌دهند." - -[portal.infrastructure.security.empty] -description = "پیکربندی امنیتی فضای کاری شما اینجا نمایش داده می‌شود." -title = "وضعیت امنیتی در دسترس نیست" - -[portal.infrastructure.security.hyokBanner] -description = "با HYOK، کلیدهای رمزنگاری هرگز KMS شما را ترک نمی‌کنند. Stirling فقط ciphertext را ذخیره و پردازش می‌کند که می‌توانید هر زمان لغو کنید." -title = "Stirling نمی‌تواند اسناد شما را رمزگشایی کند" - -[portal.infrastructure.security.ipAllowlist] -empty = "هیچ محدوده IP پیکربندی نشده است — همه IPها مجاز هستند." -heading = "IP allowlist" -sub = "دسترسی API به این محدوده‌های CIDR محدود شده است." -subLocked = "محدود کردن دسترسی API به محدوده‌های IP شناخته‌شده — در طرح‌های پولی در دسترس است." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "برای محدود کردن دسترسی API به شبکه‌های مشخص، به Pro ارتقا دهید." -title = "IP allowlisting یک قابلیت پولی است" - -[portal.infrastructure.security.ipColumns] -added = "افزوده‌شده" -addedBy = "افزوده‌شده توسط" -cidr = "CIDR" -label = "برچسب" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "مدیریت کلید رمزنگاری" -keyId = "شناسه کلید" -lastRotated = "آخرین چرخش" -rotateKey = "چرخش کلید" -rotationPolicy = "سیاست چرخش" -subheading = "نگه‌داری کلیدهایی که اسناد at rest را رمزنگاری می‌کنند — چه کسی می‌تواند رمزگشایی کند و کلیدها چگونه می‌چرخند." - -[portal.infrastructure.security.managedBanner] -description = "نگه‌داری bring-your-own-key (BYOK) و hold-your-own-key (HYOK) در Enterprise در دسترس است. برای ارائه کلیدها از KMS خودتان ارتقا دهید." -title = "در طرح شما، کلیدها توسط Stirling مدیریت می‌شوند" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "آسیا-اقیانوسیه" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · مرز داده GDPR" -label = "اتحادیه اروپا" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "ایالات متحده" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "محل ذخیره و پردازش اسناد." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} استفاده‌شده" - -[portal.infrastructure.storage.empty] -description = "ذخیره‌سازی متصل و مصرف اینجا نمایش داده می‌شود." -title = "هیچ ذخیره‌سازی پیکربندی نشده است" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "فعال" -activeRange = "0–{{value}} روز" -archived = "آرشیوشده" -coldStorage = "ذخیره‌سازی سرد" -deleted = "حذف‌شده" -never = "هرگز" -purged = "پاک‌سازی‌شده" - -[portal.infrastructure.storage.providers] -connect = "اتصال" -connected = "متصل" -heading = "ارائه‌دهندگان متصل" -subheading = "محل نوشتن artifactهای پردازش‌شده." - -[portal.infrastructure.storage.retention] -heading = "نگه‌داری" -subheading = "مدت نگه‌داری artifactها پیش از حذف چرخه عمر." -windowLabel = "بازه نگه‌داری پیش‌فرض" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} روز" -days_other = "{{count}} روز" -never = "هرگز حذف نکن" - -[portal.infrastructure.storage.totalUsage] -heading = "مصرف کل" -progressLabel = "ذخیره‌سازی استفاده‌شده" -subheading = "ذخیره‌سازی مصرف‌شده در همه ارائه‌دهندگان متصل." +revoked = "لغوشده" [portal.infrastructure.tabs] apiKeys = "کلیدهای API" @@ -7174,6 +7656,49 @@ models = "مدل‌ها" security = "امنیت" storage = "ذخیره‌سازی" +[portal.integrations] +availableHeading = "در دسترس" +comingSoonHeading = "به‌زودی" +connect = "متصل کردن" +connectionCount_one = "{{count}} اتصال" +connectionCount_other = "{{count}} اتصال" +customApi = "API سفارشی" +searchPlaceholder = "جستجوی یکپارچه‌سازی‌ها" +subtitle = "هر چیزی که Stirling می‌تواند با آن ارتباط برقرار کند. اعتبارنامه‌ها یک‌بار اینجا ساخته می‌شوند، سپس توسط منابع، خط‌مشی‌ها و Pipelineها ارجاع داده می‌شوند." +title = "یکپارچه‌سازی‌ها" +typedName = "نام {{tool}}" + +[portal.integrations.filters] +all = "همه" +audit = "ممیزی" +connected = "متصل‌شده" +notify = "اعلان" +security = "امنیت" +signing = "امضا" +storage = "ذخیره‌سازی" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "متصل‌شده" + +[portal.integrations.table] +integration = "یکپارچه‌سازی" +worksWith = "کار می‌کند با" + +[portal.integrations.worksWith] +pipelines = "Pipelineها" +policies = "خط‌مشی‌ها" +sources = "منابع" + +[portal.legal] +draft = "{{label}} · پیش‌نویس" +loadError = "بارگذاری این سند ممکن نشد. لطفاً دوباره تلاش کنید." +loading = "در حال بارگذاری…" +title = "سند حقوقی" + [portal.nav] agent-builder = "سازنده Agent" components = "کامپوننت‌ها" @@ -7182,33 +7707,17 @@ documents = "اسناد" editor = "ویرایشگر" home = "خانه" infrastructure = "زیرساخت" +integrations = "یکپارچه‌سازی‌ها" pipelines = "پایپ‌لاین‌ها" policies = "سیاست‌ها" procurement = "تدارکات" -settings = "تنظیمات" sources = "منابع" usage = "مصرف و صورت‌حساب" users = "کاربران" -[portal.notifications] -markAllRead = "علامت‌گذاری همه به‌عنوان خوانده‌شده" -title = "اعلان‌ها" -viewAll = "مشاهده همه" - -[portal.notifications.ariaLabel] -none = "اعلان‌ها، خوانده‌نشده‌ای نیست" -unread_one = "اعلان‌ها، {{count}} خوانده‌نشده" -unread_other = "اعلان‌ها، {{count}} خوانده‌نشده" - -[portal.notifications.count] -allRead = "همه خوانده‌شده" -loading = "در حال بارگذاری" -new_one = "{{count}} جدید" -new_other = "{{count}} جدید" - -[portal.notifications.empty] -description = "اعلان جدیدی وجود ندارد." -title = "همه چیز به‌روز است" +[portal.nav.section] +platform = "پلتفرم PDF" +processor = "پردازشگر PDF" [portal.pipelines] subtitle = "هر پایپ‌لاین سند خودکار در backend: زنجیره‌ای مرتب از عملیات روی مجموعه‌ای از منابع که با یک trigger اجرا می‌شود. برای دیدن مراحل و منابع، روی یک ردیف کلیک کنید." @@ -7218,54 +7727,87 @@ title = "پایپ‌لاین‌ها" newPipeline = "پایپ‌لاین جدید" [portal.pipelines.builder] -addStep = "افزودن ابزار" +activate = "Activate" back = "بازگشت به پایپ‌لاین‌ها" +cannotFollow = "نمی‌تواند {{produced}} را بگیرد" +chooseAccount = "یک حساب انتخاب کنید" +chooseDestination = "یک مقصد انتخاب کنید" +chooseOperation = "انتخاب کنید این مرحله چه کاری انجام دهد" +chooseSource = "یک منبع انتخاب کنید" discard = "دور انداختن تغییرات" -enabled = "فعال" +inputs = "ورودی" +inputSource = "منبع ورودی" +inputTrigger = "محرک" keepEditing = "ادامه ویرایش" -needsUpload = "به یک فایل آپلودشده نیاز دارد" +moreActions = "More actions" +needsConfiguring = "نیاز به راه‌اندازی دارد" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "هیچ ابزاری با جستجوی شما مطابقت ندارد." -pipelineSettings = "تنظیمات پایپ‌لاین" +pause = "Pause" +rename = "Rename pipeline" searchTools = "جستجوی ابزارها" -selectToolBody = "برای ساخت پایپ‌لاین خود، یک ابزار اضافه کنید." -selectToolTitle = "هنوز ابزاری وجود ندارد" -toolSettings = "تنظیمات ابزار" +sendToSystem = "ارسال به سیستم دیگر" +stepsIncompatible = "این مراحل نمی‌توانند روی خروجی مرحله قبلی خود اجرا شوند: {{tools}}." +stepsNeedSetup = "این مراحل پیش از ذخیره هنوز به راه‌اندازی نیاز دارند: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "عملیات ناشناخته، بدون تغییر نگه داشته شد." unsavedBody = "تغییرات ذخیره‌نشده دارید. پیش از خروج آن‌ها را ذخیره می‌کنید یا دور می‌اندازید؟" unsavedTitle = "تغییرات ذخیره‌نشده" -uploadUnsupported = "فایل‌های آپلودشده هنوز در پایپ‌لاین‌ها پشتیبانی نمی‌شوند، بنابراین این مراحل قابل ذخیره نیستند: {{tools}}." usesDefaults = "با تنظیمات پیش‌فرض اجرا می‌شود" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "همه فایل‌های مرحله قبلی را ترکیب می‌کند" +fan-out = "برای هر فایل از مرحله قبلی یک‌بار اجرا می‌شود" +format-mismatch = "به {{accepts}} نیاز دارد، اما مرحله قبلی {{produced}} تولید می‌کند" +output-uncertain = "ممکن است اجرا نشود: خروجی مرحله قبلی به نحوه راه‌اندازی آن بستگی دارد" +source-mismatch = "به {{accepts}} نیاز دارد، اما ورودی این Pipeline برابر {{produced}} است" +undeclared-operation = "نمی‌توان بررسی کرد این مرحله چه چیزی را می‌پذیرد" [portal.pipelines.composer] addTool = "افزودن ابزار" -cancel = "لغو" -chainEmpty = "برای شروع ساخت پایپ‌لاین خود، یک ابزار اضافه کنید." create = "ایجاد پایپ‌لاین" -directory = "پوشه خروجی" -directoryHelp = "مسیر مطلق روی سرور. باید داخل پوشه‌های مجاز پیکربندی‌شده باشد." +createPaused = "Create paused" editingUnsupported = "نمایش این params ابزار برای ویرایش هنوز پشتیبانی نمی‌شود." -moveDown = "انتقال به پایین" -moveUp = "انتقال به بالا" +editSource = "Edit source" name = "نام" namePlaceholder = "مثلاً Redaction sweep" -noSources = "هنوز هیچ منبعی متصل نیست. پایپ‌لاین همچنان می‌تواند روی فایل‌هایی که مستقیماً به آن داده می‌شوند اجرا شود." noToolSettings = "این ابزار تنظیمات قابل پیکربندی ندارد." -operations_one = "عملیات ({{count}})" -operations_other = "عملیات ({{count}})" output = "خروجی" -removeStep = "حذف عملیات" save = "ذخیره تغییرات" scheduleEvery = "اجرا هر" -sources = "منابع" -sourcesLoading = "در حال بارگذاری منابع..." trigger = "Trigger" triggerManual = "فقط دستی" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "روز" hours = "ساعت" minutes = "دقیقه" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "«{{name}}» حذف شود؟ این کار قابل بازگشت نیست." cancel = "لغو" @@ -7273,28 +7815,62 @@ confirm = "حذف" title = "پایپ‌لاین حذف شود؟" [portal.pipelines.detail] +clearHistory = "پاک کردن تاریخچه" delete = "حذف پایپ‌لاین" run = "اکنون اجرا کن" [portal.pipelines.empty] action = "ایجاد یک پایپ‌لاین" +connectSource = "اتصال یک منبع" description = "اولین پایپ‌لاین خود را ایجاد کنید: منابعی را که روی آن‌ها اجرا می‌شود انتخاب کنید، عملیات را زنجیره کنید و محل خروجی را انتخاب کنید." title = "هنوز پایپ‌لاینی وجود ندارد" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "فعال" paused = "متوقف‌شده" total = "پایپ‌لاین‌ها" -[portal.pipelines.output] -folder = "نوشتن در پوشه" -inline = "بازگرداندن فایل‌ها" - [portal.pipelines.run] +allProcessed_one = "چیزی برای اجرا نیست: {{count}} سند منبع قبلاً پردازش شده است." +allProcessed_other = "چیزی برای اجرا نیست: همه {{count}} سند در منابع قبلاً پردازش شده‌اند." completed_one = "اجرا کامل شد." completed_other = "همه {{count}} اجرا کامل شد." empty = "چیزی برای اجرا نیست: منابع سندی برای پردازش نداشتند." failed = "اجرا ناموفق بود: {{error}}" +inFlight = "مورد جدیدی برای اجرا نیست: اسناد هنوز از اجرای قبلی در حال پردازش هستند." +parked_one = "چیزی برای اجرا نیست: {{count}} سند قبلاً ناموفق بوده و کنار گذاشته شده است. علت را رفع کنید، سپس برای تلاش مجدد تاریخچه را پاک کنید." +parked_other = "چیزی برای اجرا نیست: {{count}} سند قبلاً ناموفق بوده‌اند و کنار گذاشته شده‌اند. علت را رفع کنید، سپس برای تلاش مجدد تاریخچه را پاک کنید." running = "اجرا شروع شد؛ هنوز در حال پیشرفت است." timeout = "اجرا بیشتر از حد انتظار طول می‌کشد؛ ممکن است همچنان در پس‌زمینه کامل شود." @@ -7307,6 +7883,7 @@ name = "پایپ‌لاین" sources = "منابع" status = "وضعیت" steps = "مراحل" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "پایش پوشه" @@ -7314,12 +7891,18 @@ manual = "دستی" schedule = "زمان‌بندی‌شده" [portal.policies] +defaultName = "خط‌مشی {{category}}" subtitle = "اتوماسیون‌های دائمی که یک پایپ‌لاین ابزار را روی هر سند اعمال می‌کنند. هر سیاست هنگام آپلود یا export اجرا می‌شود، زنجیره ابزار خود را اجرا می‌کند و نسخه اعمال‌شده را کنار نسخه اصلی ذخیره می‌کند." title = "سیاست‌ها" [portal.policies.card] comingSoon = "ارتقا به Enterprise" notSetUp = "راه‌اندازی نشده" +requiresAiEngine = "به موتور AI نیاز دارد" + +[portal.policies.categories.classification] +desc = "نوع هر سند را هنگام آپلود شناسایی کنید و فراداده آن را برای بایگانی و جستجو برچسب‌گذاری کنید." +label = "طبقه‌بندی" [portal.policies.categories.compliance] desc = "الزامات HIPAA، GDPR، SOC 2 یا FedRAMP را روی هر سند اعمال کنید." @@ -7344,6 +7927,13 @@ label = "امنیت" [portal.policies.config] scopeAll = "همه اسناد" +[portal.policies.config.classification] +summary = "هر سند آپلودشده را بر اساس برچسب‌های تیم شما طبقه‌بندی می‌کند و فراداده آن را برچسب‌گذاری می‌کند." + +[portal.policies.config.classification.rules] +0 = "طبقه‌بندی" +1 = "برچسب‌گذاری فراداده" + [portal.policies.config.compliance] summary = "اسناد را پیش از خروج از سیستم در برابر چارچوب‌های مقرراتی اعتبارسنجی می‌کند." @@ -7371,6 +7961,29 @@ minConfidence = "حداقل اطمینان" 2 = "نام‌گذاری" 3 = "نرمال‌سازی" +[portal.policies.config.purview] +label = "اعمال برچسب حساسیت Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "مستأجر Purview" +labelId = "شناسه برچسب" +labelIdHelp = "GUID برچسب، از پرتال Purview. اعمال یک برچسب، فراداده آن را روی PDF می‌نویسد؛ نمی‌تواند رمزگذاری کند، که به کلاینت Microsoft نیاز دارد." +labelName = "نام برچسب (اختیاری)" +labelNameHelp = "در کنار شناسه ثبت می‌شود تا افراد و ابزارها بتوانند برچسب را تشخیص دهند." + +[portal.policies.config.purview.fields.method] +help = "Purview بین برچسبی که یک خط‌مشی اعمال کرده و برچسبی که یک شخص انتخاب کرده تفاوت قائل می‌شود." +label = "نحوه ثبت برچسب" +privileged = "همان‌طور که توسط یک شخص انتخاب شده" +standard = "همان‌طور که به‌صورت خودکار اعمال شده" + +[portal.policies.config.purviewRead] +label = "خواندن برچسب Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "مستأجر Purview" +connectionHelp = "برچسبی را می‌خواند که سند از قبل همراه دارد، تا مراحل بعدی بتوانند بر اساس آن عمل کنند." + [portal.policies.config.retention] summary = "مدت نگه‌داری اسناد، زمان آرشیو و زمان حذف را اعمال می‌کند." @@ -7438,6 +8051,7 @@ title = "هنوز فعالیتی وجود ندارد" [portal.policies.endpoints] addWatermark = "واترمارک" autoRedact = "حذف PII" +classifyAndLabel = "طبقه‌بندی" compressPdf = "فشرده‌سازی" flatten = "تخت کردن" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "خط‌مشی‌های شما ذخیره شده‌اند و پس retry = "تلاش دوباره" title = "Backend در دسترس نیست" +[portal.policies.operations] +change = "تغییر کاری که این مرحله انجام می‌دهد" +noResults = "هیچ مرحله‌ای با آن مطابقت ندارد. یک نام محصول، یا \"scan\"، \"notify\"، \"attach\" را امتحان کنید." +searchPlaceholder = "جستجوی مراحل - \"scan\"، \"attach\" یا نام محصول را امتحان کنید" + +[portal.policies.operations.bodyMode] +binary = "بایت‌های خام فایل" +json = "داخل یک payload از نوع JSON" +multipart = "به‌عنوان آپلود فایل (multipart)" + +[portal.policies.operations.clamavScan] +description = "سرور ClamAV خودتان سند را بررسی می‌کند؛ چیزی از شبکه شما خارج نمی‌شود." +label = "اسکن برای ویروس‌ها (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "اسناد دارای ماکرو، فایل اجرایی، اسکریپت یا اشیای تعبیه‌شده را رد می‌کند." +label = "مسدود کردن محتوای فعال" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive سند را بررسی می‌کند و اگر پاک نباشد، اجرا متوقف می‌شود." +label = "اسکن برای ویروس‌ها" + +[portal.policies.operations.confluenceAttach] +description = "سند پردازش‌شده را روی یک صفحه بایگانی می‌کند." +label = "پیوست به یک صفحه Confluence" + +[portal.policies.operations.consignoSubmit] +description = "سند را برای امضا به ConsignO Cloud ارسال می‌کند." +label = "ارسال برای امضا (ConsignO)" +note = "سند را ارسال می‌کند. دریافت نسخه امضاشده هنوز پشتیبانی نمی‌شود، چون یک مرحله نمی‌تواند نتیجه را به مرحله بعدی منتقل کند." + +[portal.policies.operations.customApiCall] +description = "فراخوانی را خودتان بسازید: مسیر، روش، بدنه و مدیریت پاسخ." +label = "فراخوانی یک API سفارشی" + +[portal.policies.operations.discordNotify] +description = "به یک کانال اطلاع می‌دهد که خط‌مشی یک سند را رسیدگی کرده است." +label = "ارسال پیام به Discord" + +[portal.policies.operations.elasticIndex] +description = "آنچه خط‌مشی انجام داده را به‌عنوان یک سند قابل جستجو ثبت می‌کند." +label = "ایندکس کردن یک رویداد ممیزی در Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart برای APIهای آپلود مناسب است؛ JSON برای APIهایی مناسب است که فایل را داخل یک payload می‌خواهند؛ binary بایت‌های خام را ارسال می‌کند." +label = "نحوه ارسال سند" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "یک بدنه JSON که همان‌طور که هست ارسال می‌شود. {{document.base64}} فایل را حمل می‌کند؛ {{document.*}} و {{run.*}} برای هر سند پر می‌شوند." +label = "بدنه JSON" + +[portal.policies.operations.fields.connection] +label = "حساب" + +[portal.policies.operations.fields.domain] +label = "دامنه Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "فیلد فرمی که API انتظار دارد سند زیر آن باشد." +label = "نام فیلد فایل" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "از" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "یک شیء JSON از جفت‌های نام/مقدار header. اعتبارنامه اتصال برای شما اضافه می‌شود." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "کلید issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "به‌عنوان بدنه پیام ارسال می‌شود. {{document.*}} و {{run.*}} برای هر سند پر می‌شوند." +label = "پیام" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "شناسه صفحه" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "به URL پایه اتصال افزوده می‌شود. هرگز نمی‌تواند به میزبان دیگری برسد." +label = "مسیر" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "محل نوشتن آن در drive. {{document.filename}} برای هر سند پر می‌شود." +label = "مسیر مقصد" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report سند را دست‌نخورده می‌گذارد. Replace آن را با هرچه برمی‌گردد جایگزین می‌کند." +label = "با پاسخ چه کار شود" + +[portal.policies.operations.fields.signerEmail] +label = "ایمیل امضاکننده" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "موضوع" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio این متن را تحلیل می‌کند. از {{document.title}} استفاده کنید یا متنی را که می‌خواهید بررسی شود جای‌گذاری کنید." +label = "متن برای بررسی" + +[portal.policies.operations.fields.to] +label = "به" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "نام کاربری Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "به یک فضا اطلاع می‌دهد که خط‌مشی یک سند را رسیدگی کرده است." +label = "ارسال پیام به Google Chat" + +[portal.policies.operations.jiraAttach] +description = "سند پردازش‌شده را روی یک issue بایگانی می‌کند." +label = "پیوست به یک issue در Jira" + +[portal.policies.operations.mailgunEmail] +description = "سند پردازش‌شده را به‌عنوان پیوست ارسال می‌کند." +label = "ایمیل کردن سند (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "سند پردازش‌شده را داخل یک پوشه می‌نویسد." +label = "آپلود به Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "سرور Presidio خودتان داده‌های شخصی‌ای را که پیدا می‌کند گزارش می‌دهد." +label = "تشخیص داده‌های شخصی" +note = "Presidio متن را می‌خواند، نه فایل‌ها را. متنی را که می‌خواهید بررسی شود به آن بدهید — خود سند آپلود نمی‌شود." + +[portal.policies.operations.responseMode] +replace = "جایگزینی سند با پاسخ" +report = "نگه داشتن سند، ثبت پاسخ" + +[portal.policies.operations.sendgridEmail] +description = "سند پردازش‌شده را به‌عنوان پیوست ارسال می‌کند." +label = "ایمیل کردن سند (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "به یک کانال اطلاع می‌دهد که خط‌مشی یک سند را رسیدگی کرده است." +label = "ارسال پیام به Slack" + +[portal.policies.operations.splunkEvent] +description = "آنچه خط‌مشی انجام داده را در جایی ثبت می‌کند که ممیزان شما از قبل بررسی می‌کنند." +label = "ارسال رویداد ممیزی به Splunk" + +[portal.policies.operations.sumologicEvent] +description = "آنچه خط‌مشی انجام داده را در یک گردآورنده HTTP ثبت می‌کند." +label = "ارسال رویداد ممیزی به Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "به یک کانال اطلاع می‌دهد که خط‌مشی یک سند را رسیدگی کرده است." +label = "ارسال پیام به Teams" + +[portal.policies.operations.webhookPost] +description = "خود سند را به URL انتخابی شما ارسال می‌کند." +label = "ارسال سند به یک webhook" + +[portal.policies.operations.zapierNotify] +description = "جزئیات اجرا را به اتوماسیون شما می‌دهد." +label = "راه‌اندازی یک Zap یا سناریوی Make" + [portal.policies.stats] activeFor = "فعال" dataProcessed = "داده پردازش‌شده" @@ -7473,6 +8282,13 @@ label = "اسناد اعمال‌شده" description = "پیکربندی شده اما اجرا نمی‌شود" label = "متوقف" +[portal.policies.table] +appliesTo = "اعمال می‌شود به" +docs = "اسناد اعمال‌شده" +enforces = "اعمال می‌کند" +policy = "خط‌مشی" +status = "وضعیت" + [portal.policies.wizard.actions] back = "بازگشت" cancel = "لغو" @@ -7480,6 +8296,10 @@ continue = "ادامه" enablePolicy = "فعال کردن خط‌مشی" saveChanges = "ذخیره تغییرات" +[portal.policies.wizard.capability.classify] +desc = "نوع سند را از برچسب‌های تیم شما شناسایی و آن را برچسب‌گذاری می‌کند، تا بر اساس دسته‌بندی بایگانی و جستجو شود." +label = "طبقه‌بندی سند" + [portal.policies.wizard.capability.compress] desc = "سند را به اندازه فایل کوچک‌تر فشرده می‌کند." label = "کاهش اندازه فایل" @@ -7500,10 +8320,18 @@ label = "حذف اطلاعات حساس" desc = "JavaScript پنهان را حذف می‌کند تا هنگام باز شدن سند چیزی به‌صورت خودکار اجرا نشود." label = "حذف محتوای فعال" +[portal.policies.wizard.capability.timestampPdf] +desc = "با استفاده از یک مرجع timestamp مستقل ثابت می‌کند سند در این شکل دقیق، در یک نقطه زمانی وجود داشته است. فقط یک hash ارسال می‌شود - سند هرگز سرور شما را ترک نمی‌کند." +label = "افزودن timestamp مورد اعتماد" + [portal.policies.wizard.capability.watermark] desc = "یک نشان قابل مشاهده (مثلاً «محرمانه») را روی همه صفحات درج می‌کند." label = "اعمال واترمارک" +[portal.policies.wizard.classification] +description = "هر سند آپلودشده بر اساس برچسب‌های داخلی طبقه‌بندی و با انواع مناسب برچسب‌گذاری می‌شود. مجموعه برچسب‌ها در سراسر تیم شما مشترک است." +labelsHeading = "برچسب‌های طبقه‌بندی" + [portal.policies.wizard.errors] noTools = "ابتدا حداقل یک ابزار را در گردش کار فعال کنید." saveFailed = "خط‌مشی ذخیره نشد. لطفاً دوباره تلاش کنید." @@ -7548,73 +8376,118 @@ setUp = "راه‌اندازی خط‌مشی {{category}}" [portal.policies.wizard.workflow] description = "انتخاب کنید این خط‌مشی با هر سندی که پردازش می‌کند چه کاری انجام دهد." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} فعال" -noRule = "هنوز هیچ قاعده‌ای اعمال نشده است" -subtitle = "اتوماسیون‌های دائمی که هر سند از آن‌ها عبور می‌کند، فارغ از اینکه کدام پایپ‌لاین آن را مدیریت می‌کند." -title = "آنچه روی PDFهای شما اجرا می‌شود" - [portal.policySummary.action] -comingSoon = "ارتقا به Enterprise" -configure = "پیکربندی" setUp = "راه‌اندازی" -[portal.policySummary.column] -activeRule = "قاعده فعال" -policy = "خط‌مشی" -status = "وضعیت" +[portal.processorFlow] +footnote = "شمارش‌ها مربوط به 24 ساعت گذشته هستند. سرعت جریان نمایشی است." +liveBadge = "زنده" +stats = "{{connected}} متصل · {{processed}} PDF پردازش‌شده" +title = "پردازشگر PDF" -[portal.policySummary.empty] -description = "پس از پیکربندی خط‌مشی‌ها، دسته‌ها اینجا نمایش داده می‌شوند." -title = "هنوز خط‌مشی‌ای وجود ندارد" +[portal.processorFlow.lens] +ariaLabel = "حالت نمایش" +flow = "جریان" +sankey = "Sankey" -[portal.policySummary.state] -active = "فعال" -off = "خاموش" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "ناموفق" +heading = "نتایج" +success = "تحویل‌شده" + +[portal.processorFlow.policies] +activeCount = "{{n}} فعال" +count = "{{n}} · 24h" +heading = "خط‌مشی‌ها" +setUp = "راه‌اندازی" soon = "به‌زودی" -[portal.processingStatus] -managePlan = "مدیریت طرح" -volumeSuffix = "PDFهای پردازش‌شده · ۳۰ روز گذشته" +[portal.processorFlow.sankey] +waist = "خط‌مشی‌ها · {{n}} فعال" + +[portal.processorFlow.sankey.empty] +description = "یک منبع متصل کنید و یک خط‌مشی را روشن کنید تا جریان اسناد در پردازشگر را ببینید." +title = "هنوز جریانی وجود ندارد" + +[portal.processorFlow.sources] +comingSoonTag = "اتصال" +editor = "ویرایشگر Stirling PDF" +heading = "منابع" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "دریافت Email" [portal.procurement] -reset = "بازنشانی خرید سازمانی (دمو)" subtitle = "تیم خود را ارزیابی، قراردادبندی و راه‌اندازی کنید. همه اسناد در یک مکان." title = "خرید سازمانی" -[portal.procurement.action] -download = "دانلود" -pay = "اکنون پرداخت کنید" -request = "درخواست" -sign = "بررسی و امضا" -upload = "آپلود" - [portal.procurement.agreement] agreeCta = "موافقت و اشتراک" +confidential = "محرمانه" confirm = "من توافق‌نامه Stirling Enterprise را خوانده‌ام و با آن موافقم." -eyebrow = "توافق‌نامه" -intro = "یک توافق‌نامه ترکیبی معامله شما را پوشش می‌دهد: Master Service Agreement، Order Form، EULA و Data Processing Agreement. آن را بررسی کنید، سپس موافقت کنید تا پیشنهاد قیمت را به‌عنوان یک اشتراک متعهد بپذیرید." -title = "توافق‌نامه Enterprise خود را بررسی کنید" +docName = "توافق‌نامه سازمانی Stirling" +docSub = "MSA، Order Form، EULA و DPA، ترکیب‌شده در یک امضا." +download = "دانلود" +downloadDraftError = "تولید PDF توافق‌نامه ممکن نشد - renderer سند در دسترس نیست. لطفاً کمی بعد دوباره تلاش کنید یا با پشتیبانی تماس بگیرید." +downloadError = "دانلود توافق‌نامه امضاشده ممکن نشد. لطفاً دوباره تلاش کنید." +legalName = "نام شخصیت حقوقی" +legalNamePlaceholder = "شخصیت حقوقی که امضا خواهد کرد" +loadError = "بارگذاری توافق‌نامه ممکن نشد. لطفاً دوباره تلاش کنید." +loading = "در حال بارگذاری توافق‌نامه..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "درخواست تغییرات" +scrollHint = "برای بازبینی پیمایش کنید" +signatory = "نام امضاکننده" +signatoryPlaceholder = "نام کامل" +signatoryTitle = "عنوان امضاکننده" +signatoryTitlePlaceholder = "مثلاً مشاور حقوقی ارشد" +signError = "ثبت امضای شما ممکن نشد. لطفاً دوباره تلاش کنید." [portal.procurement.builder] addons = "افزونه‌ها" +addressLine1 = "خط آدرس 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "خط آدرس 2" +addressLine2Placeholder = "سوئیت، طبقه، ساختمان" back = "بازگشت" businessName = "نام کسب‌وکار" businessNamePlaceholder = "شرکت شما" +city = "شهر" +cityPlaceholder = "San Francisco" +completeRequired = "لطفاً پیش از تولید پیش‌فاکتور، فیلدهای الزامی (مشخص‌شده با *) را با یک ایمیل معتبر تکمیل کنید." +contactEmail = "ایمیل تماس" +contactEmailPlaceholder = "jane@acme.com" +contactName = "نام تماس" +contactNamePlaceholder = "Jane Doe" continue = "ادامه" -country = "کشور" -countryEuro = "منطقه یورو (EUR €)" -countryUK = "بریتانیا (GBP £)" -countryUS = "ایالات متحده (USD $)" +done = "انجام شد" eula = "من EULA مربوط به Stirling Enterprise را خوانده‌ام و با آن موافقم. این سند بر توافق‌نامه‌ای که از این پیشنهاد قیمت ایجاد می‌شود حاکم است." generate = "ایجاد پیشنهاد قیمت" -included = "شامل" indemnification = "غرامت IP" indemnificationSub = "طبق EULA، از ادعاهای واجد شرایط IP دفاع می‌کنیم" -offlineLicense = "مجوز آفلاین / air-gapped" -offlineLicenseSub = "یک فایل مجوز قابل دانلود برای یک نمونه self-hosted و air-gapped" +paperEyebrow = "پیش‌فاکتور سازمانی" +paperFor = "تهیه‌شده برای" +pdfSize = "اندازه PDF" +poNumber = "شماره PO" +poNumberPlaceholder = "اختیاری" +postalCode = "کد پستی" +postalCodePlaceholder = "94105" +posture = "حاکمیت" +posture_count = "حدود {{count}} خط‌مشی" +posture_essentials = "ضروریات" +posture_essentialsSub = "طبقه‌بندی + اشتراک‌گذاری — پیش‌فرض‌ها" +posture_governed = "حاکمیت‌یافته" +posture_governedSub = "امنیت و مسیریابی را اضافه می‌کند" +posture_regulated = "تنظیم‌شده" +posture_regulatedSub = "هر دسته — Compliance، Retention، Ingestion" qbr = "بازبینی‌های فصلی کسب‌وکار" qbrSub = "SE شما هر فصل استفاده و نقشه راه را بررسی می‌کند" +region = "ایالت / منطقه" +regionPlaceholder = "California" running = "{{annual}} / سال · {{years}} ساله {{tcv}}" s1Sub = "تیم شما و PDFهایی که انتظار دارید هر سال اجرا شوند." s1Title = "چه مقدار پردازش خواهید کرد؟" @@ -7623,6 +8496,12 @@ s2Title = "تعهد و سرویس" s3Sub = "برای پیشنهاد قیمت و توافق‌نامه‌ای که ایجاد می‌کند." s3Title = "جزئیات شما" serviceLevel = "سطح سرویس" +size_compact = "فشرده" +size_compactSub = "بیشتر متن، کمتر از 1 MB" +size_heavy = "سنگین" +size_heavySub = "اسکن‌شده یا پرتصویر، 5 MB+" +size_standard = "استاندارد" +size_standardSub = "متن و تصاویر ترکیبی، 1 تا 5 MB" slDedicated = "اختصاصی" slDedicatedSub = "۴ ساعت کاری · مدیر حساب اختصاصی · +۳۰٪" slPriority = "اولویت‌دار" @@ -7630,6 +8509,8 @@ slPrioritySub = "همان روز کاری · CSM مشخص · +۱۵٪" slStandard = "استاندارد" slStandardSub = "روز کاری بعد · CSM مشترک · شامل" stepOf = "مرحله {{n}} از {{total}}" +taxId = "VAT / شناسه مالیاتی" +taxIdPlaceholder = "اختیاری" term = "دوره" termDiscount = "{{pct}}٪ تخفیف تعهد چندساله اعمال شد" title = "ساخت پیشنهاد قیمت" @@ -7637,6 +8518,7 @@ training = "راه‌اندازی و آموزش" trainingSub = "جلسات زنده برای شروع کار تیم شما" users = "کل کاربران" usersPlaceholder = "مثلاً 250" +viewEula = "خواندن آن" volEstimated = "برآورد شده از {{count}} کاربر (حدود ۲٬۰۰۰ PDF برای هر نفر، شامل اتوماسیون). اگر عدد دقیق‌تری می‌دانید ویرایش کنید." volManual = "از عدد شما استفاده می‌شود. هر زمان می‌توانید بر اساس اندازه تیم دوباره برآورد کنید." volNoUsers = "مطمئن نیستید؟ اندازه تیم خود را وارد کنید تا برایتان برآورد کنیم." @@ -7645,55 +8527,55 @@ volumePlaceholder = "مثلاً 1,000,000" years_one = "{{count}} سال" years_other = "{{count}} سال" -[portal.procurement.docs] -count_one = "{{count}} سند" -count_other = "{{count}} سند" -done = "انجام شد" -here = "اینجا هستید" -hide = "پنهان کردن" -optional = "اختیاری" -paidAddon = "افزونه پولی" -show = "نمایش" -subtitle = "هر چیزی که در هر مرحله از مسیر نیاز دارید، هم‌زمان با پیشرفت معامله نمایش داده می‌شود." -supportingSubtitle = "SOC 2، بازبینی‌های امنیتی، فرم‌های مالیاتی و موارد بیشتر، آماده زمانی که تیم امنیت یا خرید شما درخواست کند. برخی هزینه یک‌باره دارند." -supportingTitle = "پشتیبانی از ارزیابی شما" +[portal.procurement.documents] +agreement = "توافق‌نامه سازمانی" +agreementSub = "MSA، Order Form و DPA در یک امضا." +download = "دانلود" +eula = "EULA و شرایط تجاری" +eulaSub = "نرم‌افزار و شرایط تجاری self-serve." +invoice = "فاکتور" +invoiceSub = "اولین فاکتور اشتراک شما." +laterInvoice = "پس از پذیرش شما در دسترس است" +laterQuote = "پس از صدور پیش‌فاکتور شما در دسترس است" +quote = "پیش‌فاکتور" +quoteSub = "پیش‌فاکتور سازمانی ریزاقلام شما." +sla = "پیوست SLA" +slaSub = "سطوح پشتیبانی و اهداف زمان پاسخ." +subprocessors = "پردازشگران فرعی" +subprocessorsSub = "اشخاص ثالثی که داده‌ها را از طرف شما پردازش می‌کنند." +subtitle = "مدارک معامله شما، در هر زمان در دسترس." title = "اسناد" -upcoming = "پیش‌رو" +view = "مشاهده" [portal.procurement.error] title = "مشکلی پیش آمد" [portal.procurement.hero] -company = "معامله Enterprise شما" +barAria = "مرحله {{current}} از {{total}}" ctaAgreement = "بررسی و امضای توافق‌نامه" -ctaLive = "شما فعال هستید" -ctaPayment = "افزودن پرداخت" +ctaExploring = "راه‌اندازی نسخه آزمایشی شما" ctaQuote = "بررسی پیشنهاد قیمت" +ctaReviewQuote = "بازبینی پیش‌فاکتور" ctaTrial = "ساخت پیشنهاد قیمت" +documents = "اسناد" eyebrow = "خرید سازمانی Enterprise" +eyebrowCompany = "{{company}} سازمانی" inviteTeammates = "دعوت از هم‌تیمی‌ها" -keyDocs = "اسناد کلیدی" -nextStep = "مرحله بعد: {{action}}" -notStarted = "شروع نشده" +licenseKey = "کلید مجوز" +liveSub = "سازمان شما آماده‌سازی شده و مجوز شما فعال است." +liveTitle = "شما روی Stirling Enterprise فعال هستید" +next = "بعدی: {{stage}}" open = "باز کردن خرید سازمانی" scheduleCall = "زمان‌بندی تماس" -setup1Sub = "هم‌تیمی‌های خود را دعوت کنید" -setup1Title = "استقرار ویرایشگر PDF" -setup2Sub = "پردازش را در ویرایشگرها و منابع دیگر فعال کنید" -setup2Title = "اتصال پردازشگر PDF" -setup3Sub = "در صورت نیاز، Security، Compliance، Routing یا Retention را فعال کنید" -setup3Title = "افزودن خط‌مشی‌های پیشنهادی" +sentenceAgreement = "بازبینی و امضای توافق‌نامه" +sentenceLive = "آماده‌سازی و عرضه" +sentencePayment = "سفارش خرید و پرداخت" +sentenceQuote = "بازبینی و پذیرش پیش‌فاکتور شما" +sentenceTrial = "راه‌اندازی فضای کاری آزمایشی شما" [portal.procurement.journey] daysLeft_one = "{{count}} روز باقی مانده" daysLeft_other = "{{count}} روز باقی مانده" -engineerLabel = "مهندس راهکارهای شما" -eyebrow = "استقرار شما" -live = "شما در Stirling Enterprise فعال هستید" -nextStep = "مرحله بعد: {{action}}" -subtitle = "مهندس راهکارهای شما در همه مراحل همراه است. هر بار یک اقدام بعدی؛ فهرست کامل در پایین آمده است." -title = "از دوره آزمایشی تا اجرا، یک مسیر هدایت‌شده" -trialTitle = "دوره آزمایشی Enterprise" [portal.procurement.journeySteps.agreement] blurb = "یک امضا MSA، فرم سفارش، EULA و DPA را پوشش می‌دهد." @@ -7727,6 +8609,9 @@ downloadError = "هنوز امکان ایجاد فایل مجوز آفلاین downloadOffline = "دانلود مجوز آفلاین (.lic)" hint = "این کلید را برای فعال‌سازی در یک نمونه self-hosted وارد کنید، یا برای سوابق خود نگه دارید. آن را امن نگه دارید." label = "کلید مجوز شما" +subtitle = "یک نمونه self-hosted را با این کلید فعال کنید، یا آن را برای سوابق خود نگه دارید." +title = "کلید مجوز شما" +trialFileHint = "این فایل مجوز آزمایشی شماست. وقتی توافق‌نامه شما برقرار شد، برگردید و دوباره آن را دانلود کنید — برخلاف کلید مجوز آنلاین، فایل .lic به‌خودی‌خود به‌روزرسانی نمی‌شود." [portal.procurement.link] cta = "پیوند دادن حساب" @@ -7739,65 +8624,62 @@ description = "اشتراک شما فعال است و مجوز شما صادر eyebrow = "فعال" title = "شما در Stirling Enterprise فعال هستید" -[portal.procurement.locked] -description = "کلیدهای آزمایشی، پیشنهادهای قیمت با حجم متعهد، توافق‌نامه تک‌امضایی، پرداخت و دفتر اسناد شما پس از شروع ارزیابی Enterprise همه اینجا قرار می‌گیرند." -eyebrow = "فقط Enterprise" -talkToSales = "صحبت با فروش" -title = "مسیر خرید سازمانی با Enterprise باز می‌شود" - [portal.procurement.milestone] -accept = "پذیرش و ادامه" -description = "PDF را دانلود کنید تا با تیم خود به اشتراک بگذارید، وقتی آماده بودید برای پذیرش برگردید، یا تغییرات ایجاد کنید." -download = "دانلود PDF" downloadError = "هنوز امکان دانلود PDF پیشنهاد قیمت وجود ندارد — لطفاً چند لحظه دیگر دوباره تلاش کنید." -edit = "ویرایش پیشنهاد قیمت" -eyebrow = "پیشنهاد قیمت {{number}}" -perYear = " / سال" -preparedFor = "تهیه‌شده برای {{company}}" -tcv = "{{value}} ارزش کل قرارداد" -title = "پیشنهاد قیمت شما آماده است" - -[portal.procurement.modal] -cancel = "لغو" -chooseFile = "انتخاب فایل" -close = "بستن" -downloadBody = "دانلود شما به‌زودی آغاز می‌شود." -downloadCta = "دانلود" -downloadTitle = "دانلود" -noFile = "هیچ فایلی انتخاب نشده است" -payBody = "قرارداد متعهد خود را از طریق Stripe با کارت یا انتقال بانکی پرداخت کنید. فضای کاری شما به‌محض تسویه پرداخت آماده می‌شود." -payCta = "ادامه به Stripe" -payTitle = "تأیید پرداخت" -requestBodyFree = "ما این مورد را بنا به درخواست ایجاد می‌کنیم. تأیید کنید تا مهندس راهکارهای شما به‌زودی آن را ارسال کند." -requestBodyPaid = "این یک افزونه پولی است. تأیید کنید تا مهندس راهکارهای شما آن را برآورد کرده و مدارک را ارسال کند." -requestCta = "درخواست" -requestTitle = "درخواست این سند" -signBody = "توافق‌نامه Stirling Enterprise را برای امضای الکترونیکی باز می‌کند: یک امضا MSA، فرم سفارش، EULA و DPA را پوشش می‌دهد. ما به‌صورت خودکار امضای متقابل می‌کنیم و شما به مرحله پرداخت می‌روید." -signCta = "باز کردن برای امضا" -signTitle = "توافق‌نامه خود را بررسی و امضا کنید" -uploadBody = "PO خود را برای ما بفرستید و ما مطابق شرایط شما فاکتور صادر می‌کنیم. PDF را بکشید و رها کنید یا از پایین یک فایل انتخاب کنید." -uploadCta = "آپلود سفارش خرید" -uploadTitle = "آپلود سفارش خرید شما" [portal.procurement.payment] description = "پیشنهاد قیمت شما پذیرفته شده و مجوز شما از قبل فعال است — تیم شما می‌تواند بلافاصله شروع کند. هر زمان آماده بودید اولین فاکتور را پرداخت کنید؛ می‌توانید آن را اینجا پرداخت یا دانلود کنید، بدون نیاز به ایمیل." +downloadAgreement = "دانلود توافق‌نامه امضاشده" downloadInvoice = "دانلود فاکتور" -simulate = "شبیه‌سازی دریافت پرداخت (دمو)" +eyebrow = "پرداخت" title = "اشتراک ایجاد شد" viewInvoice = "مشاهده و پرداخت فاکتور" +[portal.procurement.review] +acceptCta = "پذیرش پیش‌فاکتور" +annual = "هزینه سالانه (سال 1)" +downloadCta = "دانلود پیش‌فاکتور" +poNumber = "سفارش خرید: {{po}}" +renewal = "پس از دوره با {{amount}}/سال تمدید می‌شود (+{{pct}}% CPI)" +tcv = "دوره {{years}} ساله، پرداخت کامل از پیش: {{tcv}}" +validUntil = "معتبر تا {{date}}" + [portal.procurement.schedule] fallback = "زمان‌بند بارگیری نشد." fallbackLink = "باز کردن زمان‌بندی در برگه جدید" +loading = "در حال بارگذاری زمان‌بند..." subtitle = "مهندس راهکارهای شما تیم‌تان را در طول استقرار راهنمایی می‌کند. زمانی را انتخاب کنید که برای شما مناسب است." title = "زمان‌بندی تماس" -[portal.procurement.status] -action = "اقدام لازم است" -available = "در دسترس" -complete = "کامل" -pending = "در انتظار" -request = "در صورت درخواست" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "مهروموم‌شده، بدون خروجی" +back = "بازگشت" +businessName = "نام کسب‌وکار" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "مدیریت‌شده، سریع‌ترین شروع" +continue = "ادامه" +deployment = "کجا باید مستقر شود؟" +eula = "با Stirling EULA و شرایط تجاری موافقم." +fullName = "نام کامل" +fullNamePlaceholder = "نام شما" +invites = "دعوت از هم‌تیمی‌ها · اختیاری" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "زمان‌بندی تماس" +seats = "افراد آزمایش‌کننده" +seatsPlaceholder = "مثلاً 15" +selfhost = "Self-hosted" +selfhostSub = "Docker یا Kubernetes" +start = "شروع نسخه آزمایشی" +stepOf = "مرحله {{n}} از {{total}}" +subtitle = "14 روز رایگان، بدون نیاز به کارت." +subtitleDetails = "چند جزئیات برای پیش‌فاکتور و توافق‌نامه شما." +talkFirst = "ترجیح می‌دهید ابتدا صحبت کنید؟" +title = "راه‌اندازی نسخه آزمایشی شما" +viewEula = "خواندن آن" +workEmail = "ایمیل کاری" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "تمدید، ۷ روز اضافه می‌کند و به مهندس راهکارهای شما اطلاع می‌دهد." @@ -7808,109 +8690,113 @@ maxed = "به سقف رسیده" subtitle = "دوره آزمایشی رایگان شما تا {{date}} ادامه دارد. کارت لازم نیست." title = "دوره آزمایشی Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "قیمت‌گذاری حجم متعهد، SSO + SCIM + RBAC در سطح سازمان، ممیزی تغییرناپذیر ۹۰ روزه و SE اختصاصی." -homeCta = "شروع دوره آزمایشی →" -homeHeadline = "میلیون‌ها PDF را پردازش کنید." - -[portal.recentActivity] -title = "فعالیت اخیر" -viewAll = "مشاهده همه" - -[portal.recentActivity.empty] -description = "اجرای پایپ‌لاین‌ها، استقرارها و رویدادهای عامل اینجا نمایش داده می‌شوند." -title = "هنوز چیزی اینجا نیست" - -[portal.search] -ariaLabel = "جستجو" -placeholder = "جستجو در Stirling — endpoints، پایپ‌لاین‌ها، مستندات…" - -[portal.search.empty] -noActionsDescription = "وقتی اقدامات سریع در دسترس باشند، اینجا نمایش داده می‌شوند." -noActionsTitle = "اقدام سریعی وجود ندارد" -noMatches = "هیچ نتیجه‌ای برای «{{query}}» یافت نشد" -noMatchesDescription = "یک کلیدواژه دیگر امتحان کنید یا کاتالوگ را مرور کنید." - [portal.settings.groups] admin = "مدیر" [portal.settings.sections] account-link = "پیوند حساب" -[portal.shell.header] -accountFallback = "حساب" -accountMenu = "منوی حساب" -darkMode = "حالت تاریک" -lightMode = "حالت روشن" -search = "جستجو" -searchPlaceholder = "جستجو…" -signOut = "خروج" -switchToDark = "تغییر به پوسته تاریک" -switchToLight = "تغییر به پوسته روشن" - [portal.shell.sidebar] appEditor = "ویرایشگر" appProcessor = "پردازشگر" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} سند" linkAccount = "پیوند حساب Stirling" -planEditor = "طرح ویرایشگر" -planEnterprise = "طرح Enterprise" -planProcessor = "طرح پردازشگر" primaryNav = "ناوبری اصلی" switchApp = "تغییر برنامه" +[portal.shell.topbar] +closeNav = "بستن ناوبری" +openNav = "باز کردن ناوبری" +search = "جستجو" + [portal.sources] subtitle = "اتصالات ورودی قابل استفاده مجدد که اسناد را به Stirling وارد می‌کنند. یک اتصال را یک‌بار پیکربندی کنید، سپس از آن در هر تعداد خط‌مشی استفاده کنید. برای پیکربندی و دیدن خط‌مشی‌هایی که از آن استفاده می‌کنند، روی یک ردیف کلیک کنید." title = "منابع" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "اتصال منبع" +[portal.sources.builder] +back = "بازگشت به منابع" +backToTypes = "همه انواع منبع" +cancel = "لغو" +chooseHint = "انتخاب کنید اسناد از کجا می‌آیند. اتصال‌دهنده‌های خاکستری‌شده در راه هستند." +comingSoon = "به‌زودی" +comingSoonHeading = "به‌زودی" +create = "ایجاد منبع" +createTitle = "اتصال یک منبع" +delete = "حذف" +editTitle = "ویرایش منبع" +enabled = "فعال" +save = "ذخیره تغییرات" + +[portal.sources.builder.folderAccess] +description = "اتوماسیون‌های پوشه فقط می‌توانند از پوشه‌هایی استفاده کنند که یک مدیر مجاز کرده است. آن را در تنظیمات دسترسی پوشه اضافه کنید، سپس دوباره تلاش کنید." +openSettings = "تنظیمات دسترسی پوشه" +title = "این پوشه مجاز نیست" + [portal.sources.delete] body = "«{{name}}» حذف شود؟ این کار قابل بازگشت نیست. خط‌مشی‌هایی که به آن ارجاع می‌دهند باید به‌روزرسانی شوند." cancel = "لغو" confirm = "حذف" title = "منبع حذف شود؟" -[portal.sources.detail] -closeAriaLabel = "بستن جزئیات" -delete = "حذف منبع" -docs24h = "۲۴ ساعت گذشته" -docs30d = "۳۰ روز گذشته" -docsTotal = "کل مشاهده‌شده" -docsTrend = "اسناد در ۳۰ روز گذشته" -documents = "اسناد" -edit = "ویرایش" -notReferenced = "هیچ خط‌مشی‌ای به آن ارجاع نمی‌دهد، بنابراین حذف آن امن است." -pause = "مکث" -resume = "ادامه" -subtitle = "{{type}} · {{status}}" -usedBy = "استفاده‌شده توسط" - -[portal.sources.empty] -description = "یک پوشه (و به‌زودی فضای ذخیره‌سازی ابری) را متصل کنید تا خط‌مشی‌های شما جایی برای دریافت اسناد داشته باشند." -title = "هنوز هیچ منبعی متصل نشده است" - [portal.sources.kpi] inUse = "در حال استفاده" total = "اتصالات" unused = "استفاده‌نشده" +[portal.sources.networkFields.connection] +helperText = "اتصال ذخیره‌شده با آدرس سرور و اعتبارنامه‌ها. توسط هر منبعی که به آن ارجاع می‌دهد دوباره استفاده می‌شود." +label = "اتصال" + +[portal.sources.networkFields.directory] +helperText = "پوشه‌ای روی سرور برای poll کردن، نسبت به خانه ورود یا ریشه share. برای ریشه خالی بگذارید." +label = "پوشه" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume هر فایل را پس از اینکه هر خط‌مشی آن را پردازش کرد، از سرور حذف می‌کند." +label = "حالت خواندن" + +[portal.sources.networkFields.mode.options] +consume = "Consume: پردازش هر فایل فقط یک‌بار" +snapshot = "Snapshot: خواندن دوباره پوشه در هر اجرا" + +[portal.sources.networkFields.recursive] +label = "عمق پوشه" + +[portal.sources.networkFields.recursive.options] +all = "شامل زیرپوشه‌ها" +top = "فقط سطح بالا" + [portal.sources.status] active = "فعال" disabled = "غیرفعال" unused = "استفاده‌نشده" [portal.sources.table] +documents = "اسناد" source = "منبع" status = "وضعیت" +type = "Type" usedBy = "خط‌مشی‌ها" +[portal.sources.types.box] +description = "اسناد را از پوشه‌های Box که تیم شما از قبل به اشتراک گذاشته است بیرون بکشید." +label = "Box" + +[portal.sources.types.dropbox] +description = "اسناد را از یک پوشه مشترک Dropbox بیرون بکشید." +label = "Dropbox" + [portal.sources.types.editor] +description = "اسنادی که تیم شما در ویرایشگر پردازش کرده است، در اجراهای خط‌مشی و AI." label = "ویرایشگر" +noPolicies = "هنوز هیچ خط‌مشی‌ای از ویرایشگر اجرا نشده است." + +[portal.sources.types.email] +description = "پیوست‌های ارسال‌شده به یک mailbox اختصاصی را به سند تبدیل کنید." +label = "صندوق ورودی Email" [portal.sources.types.folder] description = "یک دایرکتوری روی سرور را برای اسناد جدید پایش کنید." @@ -7943,25 +8829,80 @@ label = "عمق پوشه" all = "شامل زیرپوشه‌ها" top = "فقط سطح بالا" +[portal.sources.types.ftp] +description = "یک پوشه سرور FTP یا FTPS را برای اسناد جدید poll کنید." +label = "FTP" + +[portal.sources.types.googledrive] +description = "اسناد را از پوشه‌های مشترک Google Drive بیرون بکشید." +label = "Google Drive" + +[portal.sources.types.network] +description = "یک SMB network share را برای اسناد جدید پایش کنید." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "پوشه‌های OneDrive را برای اسناد جدید پایش کنید." +label = "OneDrive" + +[portal.sources.types.s3] +description = "اسناد را از یک bucket در Amazon S3 یا سازگار با S3 بیرون بکشید." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "اتصال ذخیره‌شده‌ای که bucket و اعتبارنامه‌ها را نگه می‌دارد. توسط هر منبع و خروجی Pipeline که به آن ارجاع می‌دهد دوباره استفاده می‌شود." +label = "اتصال" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume هر object را پس از اینکه هر خط‌مشی آن را پردازش کرد، از bucket حذف می‌کند." +label = "حالت خواندن" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: پردازش هر object فقط یک‌بار" +snapshot = "Snapshot: خواندن دوباره bucket در هر اجرا" + +[portal.sources.types.s3.fields.prefix] +helperText = "فقط objectهایی که key آن‌ها با این prefix شروع می‌شود پردازش می‌شوند." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "یک پوشه drop در SFTP را برای اسناد جدید poll کنید." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "اسناد را از کتابخانه‌های سند SharePoint بیرون بکشید." +label = "SharePoint" + [portal.sources.types.unknown] label = "منبع" +[portal.sources.types.webhook] +createNote = "هنگام ایجاد این webhook، یک URL تحویل و secret امضا تولید می‌شود. فرستنده‌ها اسناد را با POST به آن URL ارسال می‌کنند و خط‌مشی‌های ارجاع‌دهنده هنگام ورود اجرا می‌شوند." +description = "دریافت اسناد با HTTP POST امضاشده از هر سیستم خارجی." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL تحویل" +secretNote = "secret امضا هنگام ایجاد webhook فقط یک‌بار نمایش داده می‌شود. برای تغییر آن، منبع را دوباره بسازید." + +[portal.sources.types.webhook.reveal] +copy = "کپی" +done = "انجام شد" +secret = "secret امضا" +secretHelp = "بدنه خام هر تحویل را با این کلید امضا کنید (HMAC-SHA256) و آن را به‌عنوان header با نام X-Stirling-Signature ارسال کنید." +secretWarning = "secret امضا را همین حالا کپی کنید. برای امنیت شما فقط یک‌بار نمایش داده می‌شود و بعداً قابل بازیابی نیست." +title = "Webhook ایجاد شد" +url = "URL تحویل" +usage = "هر سند را به‌عنوان بدنه خام درخواست با POST به URL تحویل ارسال کنید، با نوع محتوای binary (application/pdf یا application/octet-stream). خط‌مشی‌های ارجاع‌دهنده هنگام ورود به‌صورت خودکار اجرا می‌شوند." + [portal.sources.wizard] -back = "بازگشت" -cancel = "لغو" -continue = "ادامه" -editTitle = "ویرایش منبع" name = "نام" namePlaceholder = "مثلاً دریافت ادعاها" -save = "ذخیره تغییرات" -subtitle = "مرحله {{current}} از {{total}} · {{label}}" -title = "اتصال یک منبع" type = "نوع" -[portal.sources.wizard.steps] -chooseType = "انتخاب نوع" -configure = "پیکربندی" -review = "بررسی و اتصال" +[portal.stepModal] +close = "بستن" [portal.usage] managePayment = "مدیریت پرداخت" @@ -8021,16 +8962,9 @@ summary = "مالک یک تیم است — منابع اعضا و پیکربند 2 = "دسترسی پورتال از طریق خط‌مشی پیش‌فرض" 3 = "هر کاری که عضو می‌تواند انجام دهد" -[portal.welcome] -ariaLabel = "به Stirling PDF خوش آمدید" -badge = "Open-source" -installEditor = "نصب ویرایشگر" -inviteTeammates = "دعوت از هم‌تیمی‌ها" -openInBrowser = "باز کردن در مرورگر" -perks = "همیشه رایگان · قابل self-host" -subtitle = "امن‌ترین ویرایشگر PDF جهان برای تیم‌ها با هر اندازه‌ای رایگان است. شامل بیش از ۶۰ عملیات PDF و SSO." -title = "خوش آمدید به" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · نامحدود" [printFile] title = "چاپ فایل" @@ -8613,6 +9547,14 @@ text = "صفحات PDF خود را در گام‌های ۹۰ درجه‌ای ب [rotate.tooltip.header] title = "نمای کلی تنظیمات چرخش" +[ruler] +clearAll = "پاک کردن همه" +hideAllLabels = "پنهان کردن همه برچسب‌ها" +hideSmallLabels = "پنهان کردن برچسب‌های کوچک" +physicalValues = "فیزیکی" +scaled = "مقیاس‌شده" +showAllLabels = "نمایش همه برچسب‌ها" + [sanitize] desc = "حذف عناصر بالقوه مضر از فایل‌های PDF." sanitizationResults = "نتایج پاک‌سازی" @@ -8654,31 +9596,45 @@ label = "حذف فراداده XMP" [sanitize.steps] settings = "تنظیمات" -[sanitizePdf] -tags = "پاکسازی،امن،ایمن،حذف تهدیدها" - [sanitizePDF] title = "پاکسازی PDF" +[sanitizePdf] +tags = "پاکسازی،امن،ایمن،حذف تهدیدها" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "تنظیم مقیاس صفحه" +[scaleSettings] +activeScale = "مقیاس فعال" +apply = "اعمال مقیاس" +applyCalibration = "اعمال کالیبراسیون" +calculatedScale = "مقیاس محاسبه‌شده" +calibrate = "کالیبره کردن" +calibrating = "در حال کالیبره‌کردن" +calibrationDistanceRequired = "یک فاصله واقعی بزرگ‌تر از صفر وارد کنید" +calibrationInvalid = "محاسبه مقیاس از این اندازه‌گیری ممکن نیست" +calibrationPaperDistance = "فاصله اندازه‌گیری‌شده در صفحه: {{distance}}" +calibrationTitle = "کالیبره‌کردن مقیاس" +calibrationTooltip = "برای محاسبه خودکار مقیاس، یک فاصله معلوم را اندازه‌گیری کنید" +customScale = "مقیاس سفارشی" +noneSet = "هیچ مقیاس سفارشی تنظیم نشده است" +presets = "مقیاس‌های از پیش تنظیم‌شده" +ratio = "نسبت مقیاس" +ratioHelp = "نسبت: 1 واحد صفحه = X واحد واقعی" +ratioPlaceholder = "مثلاً 100" +ratioPositive = "نسبت باید بزرگ‌تر از صفر باشد" +ratioRequired = "نسبت الزامی است" +realDistance = "فاصله واقعی" +realDistancePlaceholder = "مثلاً 5" +reset = "بازنشانی به پیش‌فرض‌ها" +unit = "واحد" +unitInvalid = "واحد نامعتبر: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "آستانه زاویه:" -10 = "اندازه حاشیه اضافه‌شده و حذف‌شده برای جلوگیری از حاشیه‌های سفید در خروجی را تنظیم می‌کند (پیش‌فرض: ۱)." -2 = "حداقل زاویه مطلق مورد نیاز برای چرخاندن تصویر را تنظیم می‌کند (پیش‌فرض: ۱۰)." -3 = "تحمل‌پذیری:" -4 = "دامنه تغییر رنگ در اطراف رنگ پس‌زمینه تخمین‌زده‌شده را تعیین می‌کند (پیش‌فرض: ۳۰)." -5 = "حداقل ناحیه:" -6 = "آستانه حداقل ناحیه برای یک عکس را تنظیم می‌کند (پیش‌فرض: ۱۰۰۰۰)." -7 = "حداقل ناحیه کانتور:" -8 = "آستانه حداقل ناحیه کانتور برای یک عکس را تنظیم می‌کند" -9 = "اندازه حاشیه:" - [scannerImageSplit] submit = "استخراج اسکن‌های تصویر" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "این ابزار چه می‌کند" whatThisDoesDesc = "به‌صورت خودکار هر عکس را از یک صفحه اسکن‌شده یا تصویر مرکب پیدا و استخراج می‌کند—بدون برش دستی." whenToUse = "زمان استفاده" +[ScannerImageSplit.selectText] +1 = "آستانه زاویه:" +10 = "اندازه حاشیه اضافه‌شده و حذف‌شده برای جلوگیری از حاشیه‌های سفید در خروجی را تنظیم می‌کند (پیش‌فرض: ۱)." +2 = "حداقل زاویه مطلق مورد نیاز برای چرخاندن تصویر را تنظیم می‌کند (پیش‌فرض: ۱۰)." +3 = "تحمل‌پذیری:" +4 = "دامنه تغییر رنگ در اطراف رنگ پس‌زمینه تخمین‌زده‌شده را تعیین می‌کند (پیش‌فرض: ۳۰)." +5 = "حداقل ناحیه:" +6 = "آستانه حداقل ناحیه برای یک عکس را تنظیم می‌کند (پیش‌فرض: ۱۰۰۰۰)." +7 = "حداقل ناحیه کانتور:" +8 = "آستانه حداقل ناحیه کانتور برای یک عکس را تنظیم می‌کند" +9 = "اندازه حاشیه:" + [search] noResults = "هیچ نتیجه‌ای یافت نشد" placeholder = "عبارت جستجو را وارد کنید..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "سرور Stirling-PDF شما آفلاین است و \ [session] expired = "نشست شما به پایان رسیده است. لطفاً صفحه را تازه‌سازی کرده و دوباره تلاش کنید." -[sessionManagement.tooltip] -header = "مدیریت نشست‌های امضا" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "شرکت‌کنندگان جدید به انتهای ترتیب امضا اضافه می‌شوند" -bullet2 = "پس از نهایی‌سازی نشست، نمی‌توان شرکت‌کننده اضافه کرد" -bullet3 = "هر شرکت‌کننده وقتی نوبتش شود اعلان دریافت می‌کند" -description = "می‌توانید هر زمان پیش از نهایی‌سازی، شرکت‌کنندگان بیشتری به یک نشست فعال اضافه کنید." -title = "افزودن شرکت‌کنندگان" - -[sessionManagement.tooltip.finalization] -bullet1 = "نهایی‌سازی کامل: همه شرکت‌کنندگان امضا کرده‌اند" -bullet2 = "نهایی‌سازی جزئی: برخی شرکت‌کنندگان هنوز امضا نکرده‌اند" -bullet3 = "شرکت‌کنندگان امضانشده از سند نهایی حذف می‌شوند" -bullet4 = "پس از نهایی‌سازی، می‌توانید PDF امضاشده را در فایل‌های فعال بارگذاری کنید" -description = "نهایی‌سازی، همه امضاها را در یک PDF امضاشده واحد ترکیب می‌کند. این اقدام قابل بازگشت نیست." -title = "نهایی‌سازی نشست" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "نمی‌توان شرکت‌کنندگانی را که قبلاً امضا کرده‌اند حذف کرد" -bullet2 = "شرکت‌کنندگان حذف‌شده دیگر اعلان دریافت نخواهند کرد" -bullet3 = "ترتیب امضا به‌صورت خودکار تنظیم می‌شود" -description = "پیش از امضا می‌توان شرکت‌کنندگان را از نشست‌ها حذف کرد." -title = "حذف شرکت‌کنندگان" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "هر امضا به‌صورت ترتیبی روی PDF اعمال می‌شود" -bullet2 = "امضاکنندگان بعدی می‌توانند امضاهای قبلی را ببینند" -bullet3 = "برای فرایندهای تأیید و زنجیره‌های حفظ و انتقال قانونی حیاتی است" -description = "ترتیبی که هنگام ایجاد نشست مشخص می‌کنید، تعیین می‌کند چه کسی ابتدا امضا کند." -title = "ترتیب امضا" - [settings] close = "بستن" +[settings.ai] +documents = "اسناد و RAG" +general = "عمومی" +limits = "محدودیت‌ها و عملکرد" +models = "مدل‌ها و ارائه‌دهندگان" +title = "هوش مصنوعی" + [settings.configuration] advanced = "پیشرفته" database = "پایگاه داده" endpoints = "نقاط پایانی" features = "قابلیت‌ها" +folderAccess = "دسترسی به پوشه" mcp = "MCP Server" storageSharing = "ذخیره‌سازی فایل و اشتراک‌گذاری" systemSettings = "تنظیمات سیستم" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "ویرایشگر پیش‌فرض PDF" defaultPdfEditorActive = "Stirling PDF ویرایشگر پیش‌فرض PDF شماست" defaultPdfEditorChecking = "در حال بررسی..." defaultPdfEditorInactive = "برنامه دیگری به‌عنوان پیش‌فرض تنظیم شده است" +defaultPdfEditorRemind = "یادآوری برای تنظیم به‌عنوان پیش‌فرض" +defaultPdfEditorRemindDescription = "وقتی Stirling PDF برنامه پیش‌فرض PDF شما نیست، یک بنر نمایش داده شود." defaultPdfEditorSet = "از پیش پیش‌فرض است" defaultStartupView = "نمای پیش‌فرض هنگام اجرا" defaultStartupViewDescription = "انتخاب کنید هنگام شروع برنامه کدام زبانه در ستون چپ فعال باشد" @@ -8817,9 +9763,18 @@ languageDescription = "زبان نمایش را انتخاب کنید" logout = "خروج" setAsDefault = "تنظیم به‌عنوان پیش‌فرض" theme = "تم" +themeAccent = "رنگ تأکیدی" +themeAccentCustom = "رنگ سفارشی" +themeAccentCustomHint = "سفارشی (پیش‌فرض‌ها توصیه می‌شوند)" +themeAccentDark = "رنگ تأکیدی حالت تاریک" +themeAccentDefault = "پیش‌فرض" +themeAccentDefaultHint = "تم پیش‌فرض (توصیه می‌شود)" +themeAccentDescription = "دکمه‌ها، لینک‌ها و برجسته‌سازی‌ها از آن پیروی می‌کنند و به‌طور نامحسوس به برنامه رنگ می‌دهند. حالت روشن و تاریک هرکدام رنگ مخصوص خود را دارند. سیستم از هرکدام که فعال باشد استفاده می‌کند." +themeAccentLight = "رنگ تأکیدی حالت روشن" themeDark = "تیره" themeDescription = "میان حالت روشن و تیره جابه‌جا شوید" themeLight = "روشن" +themeReset = "بازگرداندن تم به پیش‌فرض" themeSystem = "System" title = "عمومی" user = "کاربر" @@ -8833,6 +9788,12 @@ intro = "احراز هویت کاربر، مدیریت تیم و قابلیت‌ learnMore = "بیشتر در مستندات بیاموزید" title = "برای مدیران سیستم" +[settings.general.loginLanding] +description = "انتخاب کنید هنگام ورود به Stirling Cloud کدام برنامه باز شود." +editor = "ویرایشگر" +processor = "پردازشگر" +title = "پس از ورود" + [settings.general.mode] fullscreen = "تمام‌صفحه" sidebar = "نوار کناری" @@ -9013,9 +9974,6 @@ title = "سیاست‌ها و حریم خصوصی" [settings.preferences] title = "ترجیحات" -[settings.search] -placeholder = "جستجوی صفحات تنظیمات..." - [settings.security] description = "رمز عبور خود را به‌روزرسانی کنید تا حسابتان ایمن بماند." title = "امنیت" @@ -9041,6 +9999,11 @@ security = "امنیت" telegram = "Telegram" title = "امنیت و احراز هویت" +[settings.signOut] +confirm = "مطمئن هستید که می‌خواهید خارج شوید؟" +submit = "خروج" +title = "خروج" + [settings.team] title = "تیم" @@ -9156,6 +10119,7 @@ last = "صفحه آخر" maintainRatio = "تغییر حفظ نسبت ابعاد" next = "صفحه بعدی" noSavedSigs = "هیچ امضای ذخیره‌شده‌ای یافت نشد" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "امضاهای شخصی" previous = "صفحه قبلی" redo = "انجام مجدد" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "حذف پس‌زمینه ناموفق بود" hint = "یک تصویر PNG یا JPG از امضای خود بارگذاری کنید" label = "بارگذاری تصویر امضا" placeholder = "انتخاب فایل تصویر" +previewAlt = "Current image signature" processing = "در حال پردازش تصویر..." removeBackground = "حذف پس‌زمینه سفید (شفاف‌سازی)" @@ -9207,6 +10172,17 @@ saved = "یک امضای ذخیره‌شده را انتخاب کنید، سپس text = "پس از وارد کردن نام خود در بالا، هرجای PDF کلیک کنید تا امضای شما قرار گیرد." title = "چگونه امضا اضافه کنیم" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "جابجایی امضا" pause = "مکث جانمایی" @@ -9653,10 +10629,30 @@ title = "بارگذاری در سرور" updateButton = "به‌روزرسانی روی سرور" uploadButton = "بارگذاری در سرور" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "اینجا کلیک کنید" +fallback = "اگر به‌صورت خودکار باز نشد،" +opening = "در حال باز کردن Swagger UI در یک تب جدید..." tags = "api,مستندات,swagger,پایانه‌ها,توسعه" title = "مستندات API" @@ -9764,12 +10760,39 @@ selectFilesHint = "برای اجرای این ابزار، پرونده‌ها singleFileScope = "فقط اعمال می‌شود روی: {{fileName}}" viewerMode = "برای انتخاب چندین پرونده به ویرایشگر پرونده بروید." +[toolFormat] +ANY = "هر فایل" +CBR = "یک آرشیو کتاب کمیک (CBR)" +CBZ = "یک آرشیو کتاب کمیک (CBZ)" +CSV = "یک فایل CSV" +EBOOK = "یک کتاب الکترونیکی" +EMAIL = "یک ایمیل" +EXCEL = "یک صفحه گسترده" +HTML = "یک صفحه وب" +IMAGE = "تصاویر" +JAVASCRIPT = "یک فایل JavaScript" +JSON = "یک فایل JSON" +MARKDOWN = "یک فایل Markdown" +NONE = "بدون فایل" +PCL = "یک فایل چاپ PCL" +PDF = "یک PDF" +PDF_ENCRYPTED = "یک PDF محافظت‌شده با گذرواژه" +POSTSCRIPT = "یک فایل PostScript" +PPT = "یک ارائه" +TEXT = "یک فایل متنی" +VIDEO = "یک ویدیو" +WORD = "یک سند Word" +XML = "یک فایل XML" +XPS = "یک سند XPS" +ZIP = "یک آرشیو ZIP" + [toolPanel] alpha = "آلفا" backToAllTools = "بازگشت به همهٔ ابزارها" collapse = "جمع کردن پنل" expand = "گسترش پنل" goBack = "بازگشت" +pdfTools = "ابزارهای PDF" placeholder = "برای شروع یک ابزار انتخاب کنید" premiumFeature = "ویژگی پرمیوم:" toolsHeader = "ابزارها" @@ -9806,11 +10829,6 @@ noToolsFound = "ابزاری یافت نشد" quickAccess = "دسترسی سریع" searchPlaceholder = "جستجوی ابزارها..." -[toolPicker.categories] -advancedTools = "ابزارهای پیشرفته" -recommendedTools = "ابزارهای پیشنهادی" -standardTools = "ابزارهای استاندارد" - [toolPicker.subcategories] advancedFormatting = "قالب‌بندی پیشرفته" automation = "اتوماسیون" @@ -9824,6 +10842,9 @@ removal = "حذف" signing = "امضا" verification = "تأیید" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "هیچ ابزاری یافت نشد" noTools = "هیچ ابزاری موجود نیست" @@ -9950,28 +10971,53 @@ title = "کاربران" you = "(شما)" [users.action] +cancelInvite = "لغو دعوت" deleteTeam = "حذف تیم" disableMfa = "بازنشانی MFA" move = "انتقال به تیم" reinstate = "بازگردانی" remove = "حذف از سازمان" +removeTeam = "حذف از تیم" rename = "تغییر نام تیم" resetPw = "بازنشانی رمز عبور" suspend = "تعلیق" unlock = "باز کردن قفل حساب" +[users.activity] +daysAgo = "{{count}} روز پیش" +hoursAgo = "{{count}} ساعت پیش" +justNow = "همین حالا" +minutesAgo = "{{count}} دقیقه پیش" +monthsAgo_one = "{{count}} ماه پیش" +monthsAgo_other = "{{count}} ماه پیش" +never = "هرگز" +weeksAgo_one = "{{count}} هفته پیش" +weeksAgo_other = "{{count}} هفته پیش" +yearsAgo_one = "{{count}} سال پیش" +yearsAgo_other = "{{count}} سال پیش" + [users.cap] addProcessor = "+ پردازشگر" approver = "تأیید خط‌مشی" editor = "ویرایشگر" processor = "پردازشگر" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "دعوت به {{email}} لغو شود؟ آن‌ها دیگر نمی‌توانند با لینک فعلی بپیوندند." +cancelInviteTitle = "لغو دعوت" deleteTeamBody = "تیم {{name}} حذف شود؟ ابتدا تیم باید خالی باشد - اعضای آن را به تیم دیگری منتقل کنید و نباید همچنان مالک هیچ پیکربندی یکپارچه‌سازی باشد." deleteTeamTitle = "حذف تیم" disableMfaBody = "ثبت‌نام MFA برای {{name}} حذف شود؟ اگر لازم باشد، در ورود بعدی دوباره آن را راه‌اندازی می‌کنند." disableMfaTitle = "بازنشانی MFA" removeBody = "{{name}} برای همیشه از سازمان حذف شود؟ این کار قابل بازگشت نیست." +removeTeamBody = "{{name}} از تیم حذف شود؟ حساب کاربری خود را حفظ می‌کنند اما دسترسی به منابع این تیم را از دست می‌دهند." removeTitle = "حذف عضو" [users.empty] @@ -9982,10 +11028,8 @@ title = "هنوز عضوی وجود ندارد" addToTeam = "افزودن به تیم" guestCount = "{{count}} مهمان" guests = "مهمانان" -guestsDesc = "همکاران خارجی، محدود به آنچه به اشتراک گذاشته‌اید. فقط ویرایشگر." ledBy = "به رهبری {{owner}}" org = "سازمان" -orgDesc = "مالکانی با اختیار در سراسر سازمان و تأیید خط‌مشی" owners = "{{count}} مالک" team = "تیم {{name}}" teamMeta = "{{count}} نفر" @@ -10022,6 +11066,18 @@ username = "نام کاربری" usernameError = "نام کاربری باید حداقل ۳ نویسه باشد" usernamePlaceholder = "jsmith" +[users.invites] +by = "دعوت‌شده توسط {{who}}" +cancel = "لغو" +expiresInDays_one = "{{count}} روز دیگر منقضی می‌شود" +expiresInDays_other = "{{count}} روز دیگر منقضی می‌شود" +expiresToday = "امروز منقضی می‌شود" +title = "دعوت‌های در انتظار" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "در برقراری ارتباط با بک‌اند مشکلی پیش آمد، یا شما دسترسی ندارید. دوباره تلاش کنید." title = "امکان بارگیری اعضا نبود" @@ -10211,10 +11267,15 @@ searchPlaceholder = "جستجوی پیوست‌ها" title = "پیوست‌ها" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "عنوان نشانک" +bookmarkTitleRequired = "عنوان نشانک الزامی است" closeSidebar = "بستن نوار کناری نشانک‌ها" collapseAll = "جمع کردن همه نشانک‌ها" +empty = "No bookmarks in this document" expandAll = "باز کردن همه نشانک‌ها" +searchPlaceholder = "جستجوی نشانک‌ها" +title = "Bookmarks" [viewer.comments] addComment = "افزودن نظر" @@ -10236,10 +11297,12 @@ locateAnnotation = "یافتن در سند" moreActions = "عملیات بیشتر" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "صفحه {{page}}" placingHint = "برای قرار دادن، روی یک صفحه کلیک کنید… (لغو)" removeCommentOnly = "فقط حذف نظر" saveReply = "ذخیره پاسخ" +searchPlaceholder = "Search comments" title = "نظرات" typeComment = "نظر" typeInsertText = "درج متن" @@ -10262,6 +11325,7 @@ unsavedDesc = "تغییرات ذخیره‌نشده دارید" closeSidebar = "بستن نوار کناری لایه‌ها" hideAll = "پنهان کردن همه لایه‌ها" showAll = "نمایش همه لایه‌ها" +title = "Layers" [viewer.link] delete = "حذف لینک" @@ -10605,30 +11669,6 @@ title = "متن واترمارک" image = "تصویر" text = "متن" -[wetSignature.tooltip] -header = "روش‌های ایجاد امضا" - -[wetSignature.tooltip.draw] -bullet1 = "سفارشی‌سازی رنگ و ضخامت قلم" -bullet2 = "پاک کنید و تا رسیدن به نتیجه مطلوب دوباره رسم کنید" -bullet3 = "روی دستگاه‌های لمسی (تبلت‌ها، گوشی‌ها) کار می‌کند" -description = "با استفاده از ماوس یا صفحه‌نمایش لمسی یک امضای دست‌نویس بسازید. بهترین گزینه برای امضاهای شخصی و اصیل." -title = "رسم امضا" - -[wetSignature.tooltip.type] -bullet1 = "انتخاب از میان چندین فونت" -bullet2 = "سفارشی‌سازی اندازه و رنگ متن" -bullet3 = "مناسب برای امضاهای استاندارد" -description = "از متن تایپ‌شده امضا تولید کنید. سریع و یکنواخت، مناسب اسناد تجاری." -title = "تایپ امضا" - -[wetSignature.tooltip.upload] -bullet1 = "از PNG، JPG و سایر فرمت‌های تصویری پشتیبانی می‌کند" -bullet2 = "برای بهترین نتیجه پس‌زمینه‌های شفاف توصیه می‌شود" -bullet3 = "تصویر برای تناسب با ناحیه امضا تغییر اندازه می‌یابد" -description = "یک تصویر امضای ازپیش‌ساخته را بارگذاری کنید. ایده‌آل اگر امضای اسکن‌شده یا نشان شرکت دارید." -title = "بارگذاری تصویر امضا" - [workbenchBar] activeFiles = "فایل‌های فعال" annotations = "حاشیه‌نویسی‌ها" @@ -10642,6 +11682,7 @@ exitRedaction = "خروج از حالت پنهان‌سازی" exportAll = "خروجی گرفتن از PDF" exportSelected = "خروجی گرفتن از صفحات منتخب" formFill = "پر کردن فرم" +hideToolbar = "Hide toolbar" multiTool = "ابزار چندگانه" panMode = "حالت جابه‌جایی" print = "چاپ PDF" @@ -10652,6 +11693,7 @@ redact = "پنهان‌سازی" rotateLeft = "چرخش به چپ" rotateRight = "چرخش به راست" ruler = "خط‌کش / اندازه‌گیری" +rulerSettings = "تنظیمات مقیاس" save = "ذخیره" saveAll = "ذخیره همه" saveAs = "ذخیره به‌عنوان" @@ -10661,6 +11703,7 @@ selectAll = "انتخاب همه" selectByNumber = "انتخاب بر اساس شماره‌های صفحه" selectLanguage = "انتخاب زبان" share = "اشتراک‌گذاری" +showToolbar = "Show toolbar" toggleAnnotations = "نمایش/مخفی کردن حاشیه‌نویسی‌ها" toggleAttachments = "نمایش/مخفی کردن پیوست‌ها" toggleBookmarks = "نمایش/مخفی کردن نشانک‌ها" diff --git a/frontend/editor/public/locales/fr-FR/translation.toml b/frontend/editor/public/locales/fr-FR/translation.toml index 0c17c3b375..8734944054 100644 --- a/frontend/editor/public/locales/fr-FR/translation.toml +++ b/frontend/editor/public/locales/fr-FR/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alphabet" apply = "Appliquer" applyAndContinue = "Appliquer et continuer" -areYouSure = "Voulez-vous vraiment quitter ?" back = "Retour" black = "Noir" blue = "Bleu" @@ -46,10 +45,8 @@ filesSelected = "fichiers sélectionnés" font = "Police" fontSizeTooltip = "Taille du texte du numéro de page en points. Une valeur plus élevée crée un texte plus grand." fontTypeTooltip = "Famille de police pour les numéros de page. Choisissez selon le style de votre document." -genericSubmit = "Envoyer" green = "Vert" help = "Aide" -imgPrompt = "Choisir une image" incorrectPasswordMessage = "Le mot de passe actuel est incorrect." info = "Informations" invalidUndoData = "Impossible d’annuler : données d’opération invalides" @@ -62,7 +59,6 @@ loginAgreementTitle = "Accord de connexion" logOut = "Se déconnecter" marginTooltip = "Distance entre le numéro de page et le bord de la page." moreOptions = "Plus d’options" -multiPdfPrompt = "Sélectionnez les PDF" never = "Jamais" no = "Non" noFileSelected = "Aucun fichier sélectionné. Veuillez en télécharger un." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordre des pages (entrez une liste de numéros de page sépar pages = "Pages" pageSelectionPrompt = "Sélection des pages (entrez une liste de numéros de page séparés par des virgules ou des fonctions telles que 2n+1) :" password = "Mot de passe" -pdfPrompt = "Sélectionnez le(s) PDF" pin = "Épingler le fichier (le garder actif après l’exécution de l’outil)" pro = "Pro" processingComplete = "Votre fichier est prêt." @@ -110,6 +105,7 @@ undoSuccess = "Opération annulée avec succès" unknown = "Inconnu" unpin = "Désépingler le fichier (remplacer après l’exécution de l’outil)" unsavedChanges = "Vous avez des modifications non enregistrées sur votre PDF. Que souhaitez-vous faire ?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Modifications non enregistrées" unsupported = "Non pris en charge" username = "Nom d'utilisateur" @@ -356,6 +352,7 @@ text = "Ces permissions contrôlent ce que les utilisateurs peuvent faire avec l title = "Modifier les permissions" [addStamp] +preview = "Aperçu du tampon" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Téléchargements manuels : cliquez sur les liens et placez les f noLanguages = "Aucune langue tessdata trouvée dans le répertoire configuré." permissionNotice = "Le chemin tessdata n’est pas accessible en écriture. Les téléchargements seront ouverts dans le navigateur ; veuillez enregistrer manuellement les fichiers .traineddata dans le dossier tessdata." +[admin.settings.ai.documents] +description = "Configurez le modèle d’embedding et les paramètres de récupération utilisés pour répondre aux questions sur les documents. Appliqué au moteur IA lors de l’enregistrement." +title = "Documents et RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Laissez vide pour utiliser l’identifiant d’environnement propre au moteur. S’applique aux déploiements auto-hébergés à moteur unique." +generic = "Clé API d’embedding" +openai = "Clé API OpenAI" +setPlaceholder = "Enregistrée - laissez vide pour conserver la clé actuelle" +voyageai = "Clé API VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL de base du endpoint d’embeddings compatible OpenAI / Ollama, p. ex. http://ollama:11434/v1. Doit pointer vers un endpoint interne de confiance (sensible au SSRF)." +label = "URL de base d’embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Nom du modèle d’embedding. Texte libre ; les suggestions sont uniquement indicatives." +label = "Modèle d’embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Fournisseur utilisé pour convertir le texte des documents en embeddings vectoriels." +label = "Fournisseur d’embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Nombre maximal de recherches de récupération que l’agent peut exécuter par requête." +label = "Recherches max" + +[admin.settings.ai.documents.reindexNote] +body = "La modification du modèle d’embedding prend effet immédiatement, mais les documents indexés avec le modèle précédent doivent être réindexés pour que la recherche renvoie des résultats corrects." +title = "Réindexation requise" + +[admin.settings.ai.documents.saved] +reindexNote = "Si vous avez modifié le modèle d’embedding, réindexez les documents existants afin que la recherche utilise le nouveau modèle." + +[admin.settings.ai.documents.topK] +description = "Nombre de segments les plus pertinents récupérés par recherche." +label = "Top K" + +[admin.settings.ai.general] +description = "Connectez Stirling au moteur IA Python et choisissez les fonctionnalités IA exposées. Les modifications s’appliquent au redémarrage." +title = "Moteur IA" + +[admin.settings.ai.general.capabilities] +description = "Activez ou désactivez les fonctionnalités IA individuellement. Les fonctionnalités désactivées sont masquées dans l’application." +title = "Fonctionnalités" + +[admin.settings.ai.general.enabled] +description = "Interrupteur principal. Lorsqu’il est désactivé, aucun outil, agent ni appel au moteur IA n’est disponible." +label = "Activer l’IA" + +[admin.settings.ai.general.features.chat] +description = "Assistant conversationnel pour travailler avec des PDFs." +label = "Assistant de chat" + +[admin.settings.ai.general.features.classify] +description = "Catégoriser automatiquement les documents par type ou contenu." +label = "Classification des documents" + +[admin.settings.ai.general.features.createPdf] +description = "Générer un nouveau PDF (p. ex. depuis HTML) via un agent IA." +label = "Créer un PDF à partir d’une invite" + +[admin.settings.ai.general.features.documentQuestions] +description = "Posez des questions et obtenez des réponses basées sur un document téléversé." +label = "Questions sur le document" + +[admin.settings.ai.general.features.mathAuditor] +description = "Examiner les documents pour détecter les erreurs mathématiques et numériques." +label = "Auditeur mathématique" + +[admin.settings.ai.general.features.pdfComment] +description = "Ajouter des commentaires de révision et des annotations rédigés par l’IA à un PDF." +label = "Agent de commentaires PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Délai d’expiration pour les opérations d’agent plus lourdes, comme la génération de documents." +label = "Délai d’expiration des opérations longues (secondes)" + +[admin.settings.ai.general.note] +body = "Le moteur IA s’exécute comme un service séparé. Son secret partagé" +body2 = "est défini via une variable d’environnement du conteneur. Les clés API des fournisseurs peuvent être saisies sur ces pages ou fournies comme variables d’environnement du moteur ; les clés saisies ici sont envoyées au moteur lors de l’enregistrement." +title = "À propos du moteur IA" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Délai d’expiration pour les réponses de chat diffusées (token par token)." +label = "Délai d’expiration du flux (secondes)" + +[admin.settings.ai.general.test] +button = "Tester la connexion" +failBody = "Le moteur IA n’a pas répondu. Vérifiez l’URL, que le conteneur du moteur est en cours d’exécution et que l’IA est activée (un redémarrage est nécessaire après l’activation)." +failTitle = "Moteur IA inaccessible" +okBody = "Le moteur IA a répondu à une vérification d’état." +okTitle = "Moteur IA accessible" + +[admin.settings.ai.general.timeoutSeconds] +description = "Délai d’expiration pour les requêtes IA standard vers le moteur." +label = "Délai d’expiration des requêtes (secondes)" + +[admin.settings.ai.general.url] +description = "URL interne du moteur IA Python, p. ex. http://stirling-pdf-engine:5001." +label = "URL du moteur IA" + +[admin.settings.ai.limits] +description = "Garde-fous concernant la quantité de travail que les requêtes IA peuvent effectuer et le nombre pouvant s’exécuter simultanément. Appliqué au moteur IA lors de l’enregistrement." +title = "Limites et performances" + +[admin.settings.ai.limits.maxCharacters] +description = "Garde-fou : rejeter les requêtes IA dont le texte extrait dépasse cette longueur." +label = "Caractères max par requête" + +[admin.settings.ai.limits.maxPages] +description = "Garde-fou : rejeter les requêtes IA dépassant ce nombre de pages PDF." +label = "Pages max par requête" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Nombre maximal d’appels de modèle simultanés en cours sur l’ensemble du moteur." +label = "Concurrence max du modèle" + +[admin.settings.ai.models] +description = "Choisissez le fournisseur LLM ainsi que les modèles intelligent/rapide utilisés par le moteur IA. Appliqué au moteur IA lors de l’enregistrement." +title = "Modèles et fournisseurs" + +[admin.settings.ai.models.apiKey] +anthropic = "Clé API Anthropic" +description = "Laissez vide pour utiliser l’identifiant d’environnement propre au moteur. S’applique aux déploiements auto-hébergés à moteur unique." +generic = "Clé API" +openai = "Clé API OpenAI" +setPlaceholder = "Enregistrée - laissez vide pour conserver la clé actuelle" + +[admin.settings.ai.models.baseUrl] +description = "URL de base du endpoint compatible OpenAI / Ollama, p. ex. http://ollama:11434/v1." +label = "URL de base du fournisseur" +warning = "L’URL de base doit pointer vers un endpoint interne de confiance. Le moteur effectuera des requêtes côté serveur vers celle-ci ; une valeur non fiable est donc sensible au SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Nombre maximal de tokens de sortie pour le modèle rapide." +label = "Tokens max du modèle rapide" + +[admin.settings.ai.models.fastModel] +description = "Modèle moins coûteux et plus rapide pour les tâches légères. Texte libre ; les suggestions sont uniquement indicatives." +label = "Modèle rapide" + +[admin.settings.ai.models.provider] +description = "Fournisseur LLM avec lequel le moteur communique." +label = "Fournisseur" + +[admin.settings.ai.models.smartMaxTokens] +description = "Nombre maximal de tokens de sortie pour le modèle intelligent." +label = "Tokens max du modèle intelligent" + +[admin.settings.ai.models.smartModel] +description = "Modèle à hautes capacités pour les raisonnements complexes. Texte libre ; les suggestions sont uniquement indicatives." +label = "Modèle intelligent" + +[admin.settings.ai.saved] +body = "Les modifications sont envoyées automatiquement au moteur IA." +bodyNoPush = "Paramètres enregistrés. Ils s’appliqueront la prochaine fois que le moteur IA récupérera sa configuration." +title = "Paramètres IA enregistrés" + [admin.settings.badge] clickToUpgrade = "Cliquez pour voir les détails de l’abonnement" @@ -938,6 +1094,25 @@ label = "Régénérer au démarrage" description = "Nombre de jours pendant lesquels le certificat sera valide" label = "Validité du certificat (jours)" +[admin.settings.folderAccess] +description = "Répertoires depuis lesquels les sources de dossiers et sorties de dossiers sont autorisées à lire et vers lesquels elles peuvent écrire. Il s’agit d’une limite de sécurité : les automatisations ne peuvent jamais être dirigées vers un chemin serveur en dehors de cette liste." +securityNote = "Laissez vide pour désactiver entièrement les sources et sorties de dossiers. Le répertoire de configuration propre à Stirling est toujours interdit." +title = "Accès aux dossiers" + +[admin.settings.folderAccess.implied] +description = "Ces répertoires gérés par Stirling sont toujours autorisés et ne peuvent pas être modifiés ici." +serverStorage = "Stockage de fichiers serveur" +title = "Toujours autorisés" +watchedFolder = "Dossier surveillé du pipeline" + +[admin.settings.folderAccess.roots] +add = "Ajouter" +empty = "Aucun dossier autorisé. Les sources et sorties de dossiers sont actuellement désactivées." +hint = "Saisissez des chemins absolus, par exemple /data/inbox." +label = "Racines de dossiers autorisées" +placeholder = "/data/inbox" +remove = "Supprimer la racine de dossier" + [admin.settings.general] description = "Configurer les paramètres applicatifs à l’échelle du système, y compris l’image de marque et le comportement par défaut." system = "Système" @@ -1704,6 +1879,9 @@ width = "Largeur" [app] description = "L’alternative gratuite à Adobe Acrobat (10M+ téléchargements)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convertir en PDF/A-3b" convertToPdfA3bDescription = "Crée un PDF d’archivage avec pièces jointes intégrées" @@ -1842,6 +2020,15 @@ export = "Exporter" accessDenied = "Accès refusé" insufficientPermissions = "Vous n'avez pas l'autorisation d'effectuer cette action." +[auth.callback] +completing = "Finalisation de l’authentification" +invalidToken = "Connexion OAuth échouée - token invalide." +missingToken = "Connexion OAuth échouée - aucun token reçu." +oauthFailed = "Connexion OAuth échouée. Veuillez réessayer." +pleaseWait = "Veuillez patienter pendant que nous terminons votre connexion." +signedOut = "Vous avez été déconnecté. Veuillez vous reconnecter." +windowMayClose = "Vous pouvez fermer cette fenêtre une fois l’opération terminée." + [auth.displayName] guest = "Invité" user = "Utilisateur" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Sécurise les documents PDF en supprimant les contenus pot description = "Cet outil renommera automatiquement les fichiers PDF en fonction de leur contenu. Il analyse le document pour trouver le titre le plus approprié à partir du texte." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rotation automatique" +tags = "rotation automatique,corriger orientation,à l’envers,de côté,redresser,orienter,détecter orientation,corriger rotation,orientation automatique,numérisé,OSD" + +[autoRotate.confidenceThreshold] +title = "Seuil de confiance" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "OCR uniquement" +text = "Texte uniquement" +title = "Méthode de détection" + +[autoRotate.error] +failed = "Une erreur s’est produite lors de la rotation automatique du PDF." + +[autoRotate.inferUndetected] +title = "Faire correspondre les pages à faible confiance" + +[autoRotate.report] +applied = "{{degrees}}° horaire" +confidenceValue = "Confiance {{value}}" +noChange = "Aucun changement" +pageLabel = "Page {{number}}" +summary = "{{rotated}} page(s) sur {{total}} pivotée(s), {{unchanged}} laissée(s) telle(s) quelle(s)" +title = "Rapport de détection" + +[autoRotate.report.method] +inferred = "Correspondance" +none = "Ignorée" +osd = "OCR" +text = "Texte" + +[autoRotate.report.note] +belowThreshold = "Sous le seuil" +blankPage = "La page est vide" +inferredFromDocument = "Identique aux autres pages" +noDominantDirection = "Directions de texte mixtes" +osdFailed = "Aucun texte lisible trouvé" +osdNoVerdict = "Impossible de déterminer le sens" +tesseractUnavailable = "OCR non installé" +tooFewGlyphs = "Trop peu de texte" + +[autoRotate.results] +title = "Résultats de la rotation automatique" + +[autoRotate.settings] +title = "Paramètres" + +[autoRotate.tooltip.confidenceThreshold] +text = "Niveau de certitude requis avant qu’une page soit tournée. Diminuez-le pour faire pivoter plus de pages, augmentez-le pour laisser plus de pages intactes." +title = "Seuil de confiance" + +[autoRotate.tooltip.description] +text = "Détermine le sens dans lequel chaque page doit être orientée et la pivote correctement. Les pages déjà à l’endroit restent inchangées." + +[autoRotate.tooltip.detectionMode] +text = "Auto lit d’abord le texte sur la page, puis examine la page comme une image si cela ne suffit pas. Texte uniquement ignore la vérification d’image, ce qui est le plus rapide pour les documents qui n’ont jamais été numérisés. OCR uniquement traite chaque page comme une image." +title = "Méthode de détection" + +[autoRotate.tooltip.header] +title = "Rotation automatique" + +[autoRotate.tooltip.inferUndetected] +text = "Les pages vides ou presque vides ne peuvent pas être vérifiées seules. Lorsque toutes les pages lisibles nécessitent la même rotation, ces pages sont pivotées pour correspondre." +title = "Faire correspondre les pages à faible confiance" + [autoSizeSplitPDF] tags = "pdf, découpage, document, organisation" @@ -2189,33 +2443,6 @@ keywords = "Mots-clés : impair, pair" numbers = "Nombres/plages : 5, 10-20" progressions = "Progressions : 3n, 4n+1" -[certificateChoice.tooltip] -header = "Types de certificats" - -[certificateChoice.tooltip.organization] -bullet1 = "Géré par les administrateurs système" -bullet2 = "Partagé entre les utilisateurs autorisés" -bullet3 = "Représente l’identité de l’entreprise, pas celle d’un individu" -bullet4 = "Idéal pour : documents officiels, signatures d’équipe" -description = "Un certificat partagé fourni par votre organisation. Utilisé pour l’autorité de signature à l’échelle de l’entreprise." -title = "Certificat d’organisation" - -[certificateChoice.tooltip.personal] -bullet1 = "Généré automatiquement lors de la première utilisation" -bullet2 = "Lié à votre compte utilisateur" -bullet3 = "Ne peut pas être partagé avec d’autres utilisateurs" -bullet4 = "Idéal pour : documents personnels, responsabilité individuelle" -description = "Un certificat généré automatiquement et propre à votre compte utilisateur. Adapté aux signatures individuelles." -title = "Certificat personnel" - -[certificateChoice.tooltip.upload] -bullet1 = "Nécessite un fichier P12/PFX et un mot de passe" -bullet2 = "Peut être délivré par des autorités de certification externes" -bullet3 = "Niveau de confiance supérieur pour les documents juridiques" -bullet4 = "Idéal pour : contrats juridiquement contraignants, validation externe" -description = "Utilisez votre propre fichier de certificat PKCS#12. Offre un contrôle total des propriétés du certificat." -title = "Téléverser un P12 personnalisé" - [certSign] awaitingSignatures = "En attente de signatures" chooseCertificate = "Choisir le fichier de certificat" @@ -2542,34 +2769,6 @@ server = "Serveur" stepTitle = "Source du certificat" upload = "Téléverser" -[certSign.tooltip.header] -title = "À propos de la gestion des signatures" - -[certSign.tooltip.overview] -bullet1 = "Vérifiez les signatures existantes et leur validité" -bullet2 = "Affichez des informations détaillées sur les signataires et certificats" -bullet3 = "Ajoutez de nouvelles signatures numériques pour sécuriser vos documents" -bullet4 = "Plusieurs fichiers pris en charge avec navigation facilitée" -text = "Cet outil vous permet de vérifier si vos PDF sont signés numériquement et d’ajouter de nouvelles signatures. Les signatures numériques prouvent l’auteur ou l’approbateur d’un document et indiquent s’il a été modifié depuis la signature." -title = "Que peut faire cet outil ?" - -[certSign.tooltip.signing] -bullet1 = "Prend en charge les formats PEM, PKCS12, JKS et le certificat serveur" -bullet2 = "Option pour afficher ou masquer la signature sur le PDF" -bullet3 = "Ajoutez la raison, le lieu et le nom du signataire" -bullet4 = "Choisissez la page où placer les signatures visibles" -bullet5 = "Utilisez le certificat serveur pour l’option simple « Signer avec Stirling-PDF »" -text = "Pour signer un PDF, vous avez besoin d’un certificat numérique (comme PEM, PKCS12 ou JKS). Vous pouvez choisir d’afficher la signature sur le document ou de la garder invisible pour la sécurité uniquement." -title = "Ajout de signatures" - -[certSign.tooltip.validation] -bullet1 = "Indique si les signatures sont valides ou invalides" -bullet2 = "Affiche les informations sur le signataire et la date de signature" -bullet3 = "Vérifie si le document a été modifié après la signature" -bullet4 = "Peut utiliser des certificats personnalisés pour la vérification" -text = "Lors de la vérification des signatures, l’outil indique si elles sont valides, qui a signé le document, quand il a été signé et s’il a été modifié depuis." -title = "Vérification des signatures" - [changeCreds] changeUsername = "Mettre à jour votre nom d’utilisateur. Vous serez déconnecté après la mise à jour." credsUpdated = "Compte mis à jour" @@ -2737,12 +2936,16 @@ text = "Pour rendre ces permissions non modifiables, utilisez l’outil Ajouter [chat.actions] copy = "Copier le message" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Options de l’agent Stirling" +assistant = "AI Assistant" clearChat = "Effacer la discussion" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Cette opération utilisera vos crédits cloud" tooltip = "Prélever une couleur à l’écran" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Choisir la couleur" [common] +actions = "Actions" back = "Retour" cancel = "Annuler" close = "Fermer" +codeSample = "Code sample" collapse = "Réduire" confirm = "Confirmer" continue = "Continuer" copied = "Copié !" copy = "Copier" done = "Terminé" +enterValue = "Enter value" error = "Erreur" expand = "Développer" loading = "Chargement…" next = "Suivant" +open = "Open" preview = "Aperçu" previous = "Précédent" refresh = "Actualiser" +remaining = "Restant" retry = "Réessayer" save = "Enregistrer" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Effacer la sélection" @@ -2988,6 +3199,7 @@ title = "Méthode de compression" [compress.settings] desiredSize = "Taille de fichier souhaitée" desiredSizePlaceholder = "Saisir la taille" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "La compression est un moyen simple de réduire la taille de votre fichier. Choisissez Taille de fichier pour saisir une taille cible et nous ajusterons la qualité pour vous. Choisissez Qualité pour régler manuellement la force de compression." @@ -3190,6 +3402,24 @@ pdfOptions = "Options PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Sélectionnez d’abord un format source" settings = "Paramètres" single = "Unique" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Surveillance avancée" -allInOneWorkspace = "Espace de travail PDF tout-en-un (visionneuse, outils et agent)" -apiSandbox = "Bac à sable API" -dedicatedSupportSlas = "Support dédié et SLA" -fasterThroughput = "Débit 10x plus rapide" -fullyPrivateFiles = "Fichiers entièrement privés" -largeFileProcessing = "Traitement de gros fichiers" -orgWideAccess = "Contrôles d’accès à l’échelle de l’organisation" -premiumAiModels = "Modèles IA premium" -prioritySupport = "Support prioritaire" -privateDocCloud = "Cloud de documents privé" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Accès API sécurisé" -standardThroughput = "Débit standard" -unlimitedApiAccess = "Accès API illimité" -unlimitedSeats = "Postes illimités" - [crop] autoCrop = "Recadrer automatiquement les marges blanches" noFileSelected = "Sélectionnez un fichier PDF pour commencer le recadrage" @@ -3346,6 +3558,7 @@ message = "Échec de la définition du gestionnaire PDF par défaut" title = "Erreur" [defaultApp.prompt] +dontRemind = "Ne plus me le rappeler" message = "Définissez Stirling PDF comme application par défaut pour ouvrir les fichiers PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Rechercher..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Modifier" @@ -3644,11 +3861,13 @@ shareSelected = "Partager la sélection" sharing = "Partage" showAll = "Tout afficher" showHistory = "Afficher l’historique" +sortBy = "Sort files" sortByDate = "Trier par date" sortByName = "Trier par nom" sortBySize = "Trier par taille" storage = "Stockage" storageState = "Stockage" +storageUsed = "Storage used" synced = "Synchronisé" title = "Téléverser des fichiers PDF" toolChain = "Outils appliqués" @@ -3694,13 +3913,17 @@ addFiles = "Ajouter des fichiers" addingFiles = "Ajout de fichiers…" collapse = "Réduire la barre latérale" customizeGroups = "Personnaliser les groupes" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Ouvrez des fichiers pour commencer" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Développer la barre latérale" -files = "Fichiers" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive n’est pas configuré" leaveMyFiles = "Quitter Mes fichiers" +library = "Bibliothèque PDF" myFiles = "Mes fichiers" noFiles = "Aucun fichier pour le moment" openFileManager = "Ouvrir le gestionnaire de fichiers" @@ -3708,37 +3931,36 @@ openFromComputer = "Ouvrir depuis l’ordinateur" openSettings = "Ouvrir les paramètres" other = "Autre" recent = "Récent" -search = "Rechercher" -searchPlaceholder = "Rechercher des fichiers..." viewAll = "Voir les {{count}} fichiers" [fileSidebar.fileItem] closeViewer = "Fermer la visionneuse" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Ouvrir dans la visionneuse" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Ajouter" -addLabel = "Ajouter une étiquette…" -categoryIconAria = "Choisir une icône pour {{name}}" -createCategory = "Créer une catégorie" -delete = "Supprimer la catégorie" done = "Terminé" -hide = "Masquer la catégorie" -newCategory = "Nom de la nouvelle catégorie…" -removeLabel = "Supprimer {{name}}" -rename = "Renommer" reset = "Réinitialiser aux valeurs par défaut" -search = "Rechercher des étiquettes…" -show = "Afficher la catégorie" subtitle = "Regroupez vos fichiers dans des catégories parentes. Ajoutez des étiquettes existantes ou nouvelles à une catégorie, renommez-la ou créez la vôtre. Les fichiers qui ne figurent dans aucune de vos catégories apparaissent sous « Autre »." title = "Catégories de la barre latérale" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Ajouter à l’espace de travail" addToWorkspaceCount = "Ajouter {{count}} à l’espace de travail" @@ -3749,7 +3971,6 @@ backToMyFiles = "Retour à Mes fichiers" breadcrumbs = "Chemin du dossier" cancel = "Annuler" classification = "Classification" -clearSearch = "Effacer la recherche" clearSelection = "Effacer la sélection" closeDetails = "Fermer les détails" create = "Créer" @@ -3781,6 +4002,7 @@ downloadAll = "Tout télécharger" downloadVersion = "Télécharger cette version" dropOverlay = "Déposez les fichiers pour téléverser" dropOverlaySub = "Les fichiers commencent en Local. Utilisez « Déplacer vers » ou « Enregistrer dans le cloud » pour les organiser dans un dossier." +duplicate = "Duplicate" file = "Fichier" fileInfo = "File info" fileMenu = "Actions sur le fichier" @@ -3808,13 +4030,12 @@ refresh = "Actualiser depuis le serveur" remove = "Supprimer" removeVersion = "Supprimer cette version" rename = "Renommer" +renamed = "Renommé" renameFolder = "Renommer le dossier" resizeFolderTree = "Redimensionner l’arborescence des dossiers (touches fléchées, Maj pour des pas plus grands ; double-clic pour ajuster automatiquement)" save = "Enregistrer" saveToServer = "Enregistrer sur le serveur" saveToServerDisabledHint = "L’enregistrement sur le serveur n’est pas activé sur ce serveur. Demandez à votre administrateur de l’activer." -search = "Rechercher" -searchPlaceholder = "Rechercher dans ce dossier et les sous-dossiers" selectAll = "Tout sélectionner" selectAllHint = "Cliquez pour tout sélectionner. Astuce : maintenez Ctrl (ou Cmd) pour ajouter des fichiers un par un, Maj pour sélectionner une plage." selectedCount = "{{count}} sélectionné(s)" @@ -3874,6 +4095,10 @@ title = "Pas encore de fichiers cloud" hint = "Les fichiers enregistrés sans téléversement restent ici. Déposez un fichier pour en ajouter un." title = "Aucun fichier local uniquement" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Les fichiers que vous ouvrez ou modifiez apparaîtront ici." title = "Aucune modification pour l’instant" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Impossible de supprimer le dossier." deleteFolderFailedDetail = "Impossible de supprimer le dossier : {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Impossible de mettre à jour l’apparence du dossier." folderAppearanceFailedDetail = "Impossible de mettre à jour l’apparence du dossier : {{message}}" moveFilesFailed = "Impossible de déplacer les fichiers." @@ -3910,9 +4137,7 @@ count = "Fichiers" folder = "Dossier" labels = "Étiquettes" modified = "Modifié" -name = "Nom" size = "Taille" -toolHistoryAtVersion = "Chaîne d’outils cumulative" totalSize = "Taille totale" type = "Type" versionHistory = "Parcours de version" @@ -3945,6 +4170,11 @@ localHint = "Stocké uniquement dans ce navigateur" shared = "Partagé" sharedHint = "Partagé avec vous via un lien" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Plus ancien d’abord" modifiedDesc = "Plus récent d’abord" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Aucun fichier disponible dans le stockage. Ouvrez d’ab open = "Ouvrir" openFile = "Ouvrir un fichier" openFiles = "Ouvrir des fichiers" +selectFile = "Select {{name}}" selectFromStorage = "Sélectionner depuis le stockage" upload = "Téléverser" uploadFile = "Téléverser un fichier" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tout est enregistré" +analyzingFields = "Analysing form fields..." +extractCsvError = "Échec de l’extraction du CSV" +extractXlsxError = "Échec de l’extraction du XLSX" +filled = "filled" flattenAfterFilling = "Aplatir après remplissage" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl." +requiredFieldsError = "Veuillez remplir tous les champs obligatoires" rescanFields = "Réanalyser les champs" rescanFormFields = "Réanalyser les champs du formulaire" save = "Enregistrer" +saveShortcut = "Ctrl+S pour enregistrer" +unsavedChanges = "Modifications non enregistrées" [formFill.sidebar] close = "Fermer la barre latérale" @@ -4309,6 +4552,11 @@ desc = "Renomme automatiquement un fichier PDF sur la base de son en-tête déte tags = "détection automatique,basé-en-tête,organiser,renommer" title = "Renommer PDF auto" +[home.autoRotate] +desc = "Détectez l’orientation de chaque page et remettez-la automatiquement à l’endroit." +tags = "rotation automatique,corriger orientation,à l’envers,de côté,redresser,orienter,détecter orientation,corriger rotation,orientation automatique,numérisé,OSD" +title = "Rotation automatique" + [home.autoSizeSplitPDF] tags = "auto,diviser,taille" title = "Scinder auto taille/pages" @@ -4608,15 +4856,12 @@ hint = "Téléversez une image PNG, JPG, SVG ou autre à placer sur le PDF. Les label = "Téléverser une image" placeholder = "Sélectionner un fichier image" -[imageToPdf] -tags = "pdf,conversion,img,jpg,image,photo" - [imageToPDF] header = "Image en PDF" title = "Image en PDF" -[infoBanner] -dismiss = "Fermer" +[imageToPdf] +tags = "pdf,conversion,img,jpg,image,photo" [invite] acceptError = "Échec de la création du compte" @@ -4647,11 +4892,9 @@ welcomeTitle = "Vous avez été invité !" [landing] addFiles = "Ajouter des fichiers" -heroSubtitle = "Déposez ou ajoutez un PDF existant pour commencer." mobileUpload = "Téléverser depuis un mobile" openFromComputer = "Ouvrir depuis l’ordinateur" uploadFromComputer = "Téléverser depuis l’ordinateur" -workbenchEmptyStateHero = "Déposez un PDF n’importe où" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Saisissez le nom d’utilisateur" failedToSignIn = "Échec de connexion avec {{provider}} : {{message}}" forgotPassword = "Mot de passe oublié ?" loggingIn = "Connexion…" -logIn = "Se connecter" login = "Se connecter" magicLinkSent = "Lien magique envoyé à {{email}} ! Consultez votre e-mail et cliquez sur le lien pour vous connecter." mfaCode = "Code d'authentification" @@ -4715,26 +4957,10 @@ signInWith = "Se connecter avec" title = "Connexion" unexpectedError = "Erreur inattendue : {{message}}" updatePassword = "Mise à jour du mot de passe" -useEmailInstead = "Se connecter avec l’e‑mail" useMagicLink = "Utiliser plutôt le lien magique" username = "Nom d’utilisateur" youAreLoggedIn = "Vous êtes connecté !" -[login.slides.edit] -alt = "Modifier des PDF" -subtitle = "Avec plus d’une douzaine d’outils pour caviarder, signer, lire et manipuler des PDF, vous trouverez forcément ce que vous cherchez." -title = "Modifiez des PDF pour afficher/sécuriser les informations que vous souhaitez" - -[login.slides.overview] -alt = "Aperçu de Stirling PDF" -subtitle = "Une suite cloud axée sur la confidentialité pour les PDF, qui vous permet de convertir, signer, caviarder et gérer des documents, avec plus de 50 autres outils puissants." -title = "Votre solution tout-en-un pour tous vos besoins PDF." - -[login.slides.secure] -alt = "Sécuriser des PDF" -subtitle = "Ajoutez des mots de passe, caviardez du contenu et gérez des certificats en toute simplicité." -title = "Protégez les informations sensibles de vos PDF" - [margin] large = "Grande" medium = "Moyenne" @@ -4818,6 +5044,34 @@ uploadSuccess = "Téléversement réussi !" uploadSuccessMessage = "Vos images ont été transférées." validating = "Validation de la session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scannez pour téléverser des photos. Les images se convertissent automatiquement en PDF." descriptionNoConvert = "Scannez pour téléverser des photos depuis votre appareil mobile." @@ -4836,8 +5090,13 @@ title = "Téléverser depuis un mobile" tags = "outil multifonction,opération multifonction,interface utilisateur,glisser déposer,front-end,client side,interactif,intransigeant,déplacer,multi tool" title = "Outil multifonction PDF" -[navbar] -search = "Rechercher" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "L'authentification n'a pas abouti. Vous pouvez fermer cette fenêtre et réessayer." @@ -4966,6 +5225,7 @@ activeFiles = "La vue Fichiers actifs affiche tous les PDF char allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Fermer" cropSettings = "Maintenant que nous avons sélectionné le fichier que nous voulons rogner, nous pouvons configurer l’outil Rogner pour définir la zone de rognage du PDF." +dialogLabel = "Onboarding" fileCheckbox = "Cliquer sur l’un des fichiers le sélectionne pour le traitement. Vous pouvez en sélectionner plusieurs pour des opérations par lots." fileReplacement = "Le fichier modifié remplacera automatiquement le fichier d’origine dans le Workbench, ce qui vous permettra de le faire passer facilement par d’autres outils." filesButton = "Le bouton Fichiers de la barre d’accès rapide vous permet de téléverser des PDF à utiliser avec les outils." @@ -4976,6 +5236,7 @@ pinButton = "Vous pouvez utiliser le bouton Épingler si vous p results = "Après l’exécution de l’outil, l’étape de Revue affichera un aperçu des résultats dans ce panneau et vous permettra d’annuler l’opération ou de télécharger le fichier." runButton = "Une fois l’outil configuré, ce bouton vous permet d’exécuter l’outil sur tous les PDF sélectionnés." selectCropTool = "Sélectionnons l’outil Rogner pour montrer comment utiliser l’un des outils." +stepOf = "Étape {{current}} sur {{total}}" toolInterface = "Voici l’interface de l’outil Rogner. Comme vous pouvez le voir, il n’y a pas grand-chose pour l’instant car nous n’avons pas encore ajouté de fichiers PDF sur lesquels travailler." workbench = "Voici le Workbench - la zone principale où vous visualisez et modifiez vos PDF." wrapUp = "Tout est prêt ! Vous avez appris les principales zones de l’application et comment les utiliser. Cliquez sur le bouton Aide quand vous le souhaitez pour revoir cette visite." @@ -4988,6 +5249,26 @@ showMeAround = "Faites-moi faire le tour" skipForNow = "Passer pour l’instant" skipTheTour = "Passer la visite" +[onboarding.checklist] +dismiss = "Ignorer" +title = "Configurer Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Exécutez Stirling nativement sur votre machine" +title = "Télécharger Stirling pour ordinateur" + +[onboarding.checklist.inviteTeam] +description = "Collaborez avec votre équipe" +title = "Inviter des membres de l’équipe" + +[onboarding.checklist.shareAnalytics] +description = "Aidez à améliorer Stirling" +title = "Partager des données d’utilisation anonymes" + +[onboarding.checklist.takeTour] +description = "Découvrez le fonctionnement de Stirling avec une visite rapide" +title = "Faire la visite guidée" + [onboarding.desktopInstall] body = "Stirling fonctionne mieux en application de bureau. Vous pouvez l’utiliser hors ligne, accéder plus rapidement aux documents et effectuer des modifications en local sur votre ordinateur." selectOs = "Sélectionner le système d’exploitation" @@ -4996,7 +5277,21 @@ titleWithOs = "Télécharger pour {{osLabel}}" [onboarding.mfa] authenticationCode = "Code d’authentification" +description = "Sécurisez votre compte en associant une application d’authentification. Scannez le code QR ou saisissez la clé de configuration, puis confirmez le code à 6 chiffres pour terminer." +enable = "Activer la MFA" +enableError = "Impossible d’activer l’authentification à deux facteurs. Vérifiez le code et réessayez." +enterCode = "Saisissez le code à 6 chiffres de votre application." +enterCodeError = "Saisissez le code d’authentification pour continuer." +logout = "Déconnexion" +openAuthenticator = "Ouvrez Google Authenticator, Authy ou 1Password." qrCodeLoading = "Génération de votre code QR…" +regenerateQrCode = "Regénérer le code QR" +scanQrCode = "Scannez le code QR ou saisissez la clé de configuration ci-dessous." +setupError = "Impossible de démarrer la configuration à deux facteurs. Veuillez réessayer." +setupKey = "Clé de configuration (saisie manuelle)" +stepByStep = "Étape par étape" +success = "La MFA a été activée. Vous pouvez maintenant continuer." +title = "Configuration de l’authentification multifacteur" [onboarding.planOverview] adminBodyLoginDisabled = "Une fois le mode connexion activé, vous pourrez gérer les utilisateurs, configurer les paramètres et surveiller la santé du serveur. Les {{freeTierLimit}} premières personnes sur votre serveur utilisent Stirling gratuitement." @@ -5005,6 +5300,11 @@ adminTitle = "Aperçu admin" userBody = "Invitez des coéquipiers, assignez des rôles et gardez vos documents organisés dans un espace de travail sécurisé. Activez le mode connexion quand vous êtes prêt à aller au‑delà d’un usage solo." userTitle = "Aperçu du plan" +[onboarding.processorIntro] +body = "Stirling exécute désormais des politiques — des règles automatisées qui classent, sécurisent et traitent chaque document à son arrivée. Configurez-les et surveillez les exécutions dans le Processor." +cta = "Découvrir le Processor" +title = "Découvrez le Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Année prépayée" + +[payg.prepaid.banner] +exhausted = "Votre capacité prépayée est épuisée ; la facturation à l’usage a repris. Rechargez pour conserver la remise." +expiring_one = "Votre année prépayée se termine dans {{count}} jour. La capacité inutilisée expire — rechargez pour démarrer une nouvelle année." +expiring_other = "Votre année prépayée se termine dans {{count}} jours. La capacité inutilisée expire — rechargez pour démarrer une nouvelle année." +low = "Il ne vous reste plus que {{pct}} % de votre capacité prépayée. Rechargez à tout moment pour continuer le traitement sans interruption." + +[payg.prepaid.card] +subtitle = "Utilisée avant la facturation à l’usage et en dehors de votre plafond de dépenses." +title = "Capacité prépayée" + +[payg.prepaid.meter] +capSuffix = "sur {{total}} crédits prépayés" +expires = "Expire le {{date}}" + +[payg.prepaid.state] +exhausted = "Épuisée" +healthy = "Il en reste beaucoup" +low = "Bientôt épuisée" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF en PDF/A" tags = "convertion,archive,long-term,standard,conversion,storage,préservation,preservation" title = "PDF en PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "impression,standard,conversion,production,prépresse,archive" title = "PDF vers PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Choisissez le forfait adapté à vos besoins" title = "Forfaits disponibles" @@ -5937,49 +6267,12 @@ keyDescription = "Collez la clé de licence depuis votre e-mail" success = "Licence activée !" successMessage = "Votre licence a été activée avec succès. Vous pouvez maintenant fermer cette fenêtre." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "application effectuée" -enforcing = "Application en cours..." -failed = "Échec de l'application" outputsUnavailable = "Les résultats des politiques ne sont plus disponibles au téléchargement." partialOutputsUnavailable = "Certains résultats des politiques ne sont plus disponibles au téléchargement." -retrying = "Occupé, nouvelle tentative..." runNotFound = "L'exécution de l'application est introuvable." -step = "étape {{current}}/{{total}}" timedOut = "Délai d'application dépassé avant la fin de l'exécution." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Fermer" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Application de {{names}}" applyingProgress = "Application de {{names}} ({{done}} sur {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} fichier(s) sur {{total}} n'ont pas pu être traités failureTitle = "Exporté sans application complète" printPolicyAppliedBody = "Ce PDF a été mis à jour pour respecter une politique. Vérifiez les modifications, puis réimprimez." printPolicyAppliedTitle = "Politique appliquée avant l'impression" -queued = "+{{count}} en file d'attente" successTitle = "{{names}} appliquées" summaryMore = "{{first}}, {{second}} et {{more}} de plus" summaryTwo = "{{first}} et {{second}}" -[policies.enforcement.triggerVerb] -convert = "Application avant conversion" -default = "Application" -export = "Application avant exportation" -input = "Application lors de l'importation" -print = "Application avant impression" - -[policies.field] -accessLog = "Journal d’accès" -archiveAfter = "Archiver après" -auditTrail = "Piste d’audit" -belowThreshold = "Sous le seuil" -destination = "Destination" -frameworks = "Frameworks" -immutableHold = "Conservation immuable" -keepFor = "Conserver pendant" -minConfidence = "Confiance min." -notify = "Notifier lors du routage" -onViolation = "En cas de non-conformité" -webhookUrl = "URL du webhook" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Ajouter" -addPlaceholder = "Ajouter une étiquette…" -customNote = "Personnalisé pour votre équipe." -defaultNote = "Utilisation du réglage intégré par défaut, partagé avec votre équipe." -duplicate = "\"{{name}}\" existe déjà." -edit = "Modifier les étiquettes" -empty = "Aucune étiquette pour le moment." -export = "Exporter JSON" -iconAria = "Choisir une icône pour {{name}}" -import = "Importer JSON" -importError = "Impossible d’importer ce fichier." -managedNote = "Les étiquettes d’équipe sont gérées par votre responsable d’équipe." -modalSubtitle = "Partagées avec toute votre équipe. Le classificateur choisit les étiquettes qui correspondent à chaque document." -modalTitle = "Étiquettes de classification" -pickIcon = "Utiliser l’icône {{label}}" -removeAria = "Supprimer {{name}}" -resetToDefault = "Réinitialiser par défaut" -saveForTeam = "Enregistrer pour l’équipe" -saving = "Enregistrement…" -sectionLabel = "Étiquettes de classification" -startFromScratch = "Repartir de zéro" -teamCount = "étiquettes d’équipe" -tooLong = "Les étiquettes peuvent contenir au maximum {{max}} caractères." -ungrouped = "Non groupé" -view = "Voir les étiquettes" +categoryCount = "catégories" +hideCategory = "Masquer la catégorie" +labelCount = "libellés" +sharedNote = "Ces libellés sont intégrés et partagés avec toute votre équipe." +showCategory = "Afficher la catégorie" [policies.pii] account = "Numéros de compte (étiquetés)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Numéros d'acheminement américains (ABA)" ssn = "Numéros de sécurité sociale" -[policies.settings] -noneExport = "Aucune politique n’est actuellement exécutée lors de l’exportation." -noneUpload = "Aucune politique n’est actuellement exécutée lors de l’importation." -onExport = "Lors de l’exportation" -onUpload = "Lors de l’importation" -reorderHandle = "Faire glisser pour réorganiser" -runOrderDesc = "Lorsque plusieurs politiques s’exécutent sur le même déclencheur, elles s’exécutent dans cet ordre — chacune sur la sortie de la politique précédente. Faites glisser pour réorganiser." -title = "Paramètres des politiques" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Options des politiques" -policySettings = "Paramètres des politiques" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Réessayer les politiques en échec ({{count}})" -rowProgress = "{{completed}} fichiers sur {{total}} traités" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Qu’est-ce qu’une politique ?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Détecte automatiquement et noircit les informations sensibles — comme les numéros de sécurité sociale et de carte — afin qu'elles ne puissent pas être lues dans le document." -sanitize = "Supprime le JavaScript caché du fichier, afin que rien ne puisse s'exécuter automatiquement à l'ouverture." -watermark = "Appose une marque visible (p. ex. \"Confidentiel\") sur chaque page." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Fermer" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} en cours d’application..." badgeRan = "La politique {{name}} a été exécutée sur ce fichier" +badgeRunning = "{{name}} en cours d’exécution..." blockingAction = "{{action}} bloquée pendant l’application de la politique, veuillez patienter..." dismiss = "Fermer la superposition" enforcingTitle = "Application de la politique..." @@ -6151,6 +6316,7 @@ viewAnyway = "Voir le fichier (politique toujours en cours d’application)" [policyOption] autoRedactPhi = "Masquer automatiquement les PHI" blockExport = "Bloquer l’exportation" +ccpa = "CCPA" contracts = "Contrats" documents = "Documents" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plan Editor" subscribed = "Plan Processor" unlinked = "Non lié" -[portal.agentBuilder] -bootstrapFromDocument = "Initialiser à partir d’un document" -sectionsAriaLabel = "Sections du créateur d’agents" -selectorAriaLabel = "Agents" -subtitle = "Concevez, testez et déployez les agents AI qui classifient, extraient et acheminent vos documents. Définissez des scénarios, encadrez l’accès aux outils, exécutez un jeu de référence et publiez une version." -title = "Créateur d’agents" - -[portal.agentBuilder.bootstrap] -cancel = "Annuler" -dropzoneText = "Choisir un document d’exemple (PDF ou image)" -lead = "Déposez un document d’exemple et nous proposerons des scénarios et un schéma d’extraction que vous pourrez affiner. Rien n’est publié tant que vous ne l’avez pas examiné." -submit = "Initialiser l’agent" -subtitle = "Créer un nouvel agent à partir d’un fichier représentatif" -title = "Initialiser à partir d’un document" - -[portal.agentBuilder.empty] -description = "Initialisez un agent à partir d’un document d’exemple pour créer ses scénarios et son schéma d’extraction, puis affinez-le et publiez-le." -title = "Aucun agent pour le moment" - -[portal.agentBuilder.evals] -casesPassing = "Cas réussis" -columnCase = "Cas d’évaluation" -columnLatency = "Latence" -columnResult = "Résultat" -fail = "échec" -goldenSetPassRate = "Taux de réussite du jeu de référence" -latencyMs = "{{ms}} ms" -notRun = "non exécuté" -pass = "réussite" -passRate = "Taux de réussite" -runEvals = "Exécuter les évaluations" - -[portal.agentBuilder.evals.empty] -description = "Les évaluations transforment vos scénarios en un jeu de référence reproductible. Passez à un niveau supérieur pour capturer le taux de réussite dans le temps et conditionner les publications à celui-ci." -title = "Aucun jeu de référence pour le moment" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "sur les jeux de référence" -activeAgents = "Agents actifs" -avgPassRate = "Taux de réussite moyen des évaluations" -fleetWide = "sur toute la flotte" -latestPublished = "Dernière publication" -scenarios = "Scénarios" -testCases = "cas de test" -totalDescription_one = "{{count}} au total" -totalDescription_other = "{{count}} au total" - -[portal.agentBuilder.scenarios] -add = "Ajouter" -addScenario = "Ajouter un scénario" -enable = "Activer" -expectationLabel = "Comportement attendu" -expectationPlaceholder = "Ce que l’agent doit faire" -inEval = "en évaluation" -mute = "Désactiver" -muted = "désactivé" -nameLabel = "Nom" -namePlaceholder = "p. ex. Escalade de conformité" - -[portal.agentBuilder.tabs] -evals = "Évaluations" -scenarios = "Scénarios" -tools = "Outils" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "Accès étendu" -deniedHint = "Les outils sélectionnés sont bloqués. Tout le reste reste appelable." -deniedTools = "Outils refusés" -governanceGate = "La gouvernance des outils est disponible avec le plan Enterprise." -restricted = "Restreint" -restrictedAccess = "Accès restreint aux outils" -restrictedDescription = "Autoriser tous les outils sauf ceux que vous refusez ci-dessous." - -[portal.agentBuilder.versions] -current = "actuelle" -historyGate = "L’historique complet des versions et la restauration sont disponibles avec le plan Enterprise." -publish = "Publier" -rollBack = "Restaurer" - [portal.assistant] close = "Fermer l’assistant" error = "Impossible de joindre l’assistant." @@ -6365,9 +6451,21 @@ typing = "Saisie en cours" loading = "Chargement" redirectingToEditor = "Redirection vers l’éditeur..." +[portal.billing.activation] +maybeLater = "Peut-être plus tard" +title = "Activer le Processor" + +[portal.billing.activation.payg] +sub = "Définissez une limite mensuelle. Payez ce que vous exécutez." +title = "Paiement à l’usage" + +[portal.billing.activation.prepay] +badge = "2 mois gratuits" +sub = "Une seule facture, par carte ou virement bancaire." +title = "Prépayez une année" + [portal.billing.checkout] noClientSecret = "La fonction Edge n’a renvoyé aucun client_secret." -subtitle = "Ajoutez une carte pour continuer après votre allocation gratuite du plan Editor. Stripe s’occupe du reste." title = "Activer le plan Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Votre paiement a réussi, mais l’activation prend un peu plus de temps close = "Fermer" title = "Presque terminé" +[portal.billing.checkout.cap] +amountAria = "Limite de dépenses mensuelle" +back = "Retour" +continue = "Continuer vers le paiement" +error = "Impossible de définir votre limite de dépenses" +finePrint = "Factures émises mensuellement. Annulation possible à tout moment." +noLimit = "Aucune limite" +note = "Vous n’êtes jamais facturé au-delà de votre limite. Le traitement est simplement mis en pause." +perMonth = "/ mois" +title = "Définissez votre limite de dépenses" + +[portal.billing.checkout.card] +fields = "Numéro de carte · MM / AA · CVC · ZIP" +label = "Détails de la carte" +note = "Détails de la carte collectés par Stripe. Stirling ne stocke jamais le PAN ni le CVC." + [portal.billing.checkout.error] title = "Impossible de démarrer le paiement" @@ -6383,10 +6497,9 @@ body = "Votre paiement est passé. Nous activons le traitement mesuré sur vos i hint = "Veuillez garder cette fenêtre ouverte." title = "Activation de votre plan Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "dans l’environnement du portail pour activer le paiement dans l’application." -bodyBefore = "Définir" -title = "Stripe non configuré" +[portal.billing.checkout.pay] +limit = "Limite de dépenses mensuelle {{amount}}/mois. Le traitement s’interrompt à la limite." +limitNoCap = "Aucune limite de dépenses. Le traitement ne s’interrompra pas. Les factures sont émises mensuellement." [portal.billing.enterpriseUpsell] cta = "Créer votre devis Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plan actuel" freeForever = "Gratuit pour toujours" noTeamResolved = "Aucune équipe n’est encore résolue sur votre portefeuille — actualisez et réessayez." ownerOnly = "Seul le propriétaire de l’équipe peut activer le plan Processor." +payInvoice = "Payer la facture pour terminer" planName = "Editor" ssoIncluded = "SSO inclus" switchOnProcessor = "Activer Processor →" unlimitedUsers = "Utilisateurs illimités" +viewQuote = "Voir le devis" [portal.billing.invoices] columnAmount = "Montant" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Requêtes API directes" segmentApiLabel = "API" segmentAutomationDesc = "Automatisations et pipelines" segmentAutomationLabel = "Automatisation" +sizeMultiplier = "{{formatted}} PDFs ont utilisé un multiplicateur de taille" +summary = "{{unique}} uniques · {{units}} unités de compteur · {{avg}} moy. par PDF" +summaryNoRate = "{{unique}} uniques · {{units}} unités de compteur" unit = "PDF mesurés" +unitsPending_one = "{{units}} unité de compteur en attente de synchronisation depuis les instances liées" +unitsPending_other = "{{units}} unités de compteur en attente de synchronisation depuis les instances liées" + +[portal.billing.prepaid] +capSuffix = "sur {{total}} crédits prépayés" +expires = "Expire le {{date}}" +eyebrow = "Capacité prépayée" +note = "Utilisée avant la facturation à l’usage et en dehors de votre limite de dépenses." +topUp = "Recharger" + +[portal.billing.prepaid.buy] +back = "Retour" +calcTitle = "Calculez votre paiement annuel" +cancel = "Annuler" +cancelPurchase = "Annuler l’achat" +close = "Fermer" +continueToPayment = "Continuer vers le paiement" +finalise = "Finaliser" +invoiceErrorTitle = "Impossible de générer la facture" +loadingTitle = "Chargement de votre devis" +payTitle = "Payez votre année" +step = "Étape {{current}} sur {{total}}" + +[portal.billing.prepaid.calc] +change = "Modifier" +deployRow = "Déploiement" +done = "Terminé" +enterpriseHint = "Il s’agit d’une échelle entreprise. À ce volume, les tarifs entreprise surpassent toute remise en libre-service. Tarif verrouillé, conditions et devis en quelques minutes." +governanceRow = "Gouvernance" +governanceValue = "{{label}} · {{count}} politiques" +handlesLabel = "Votre Processor" +handlesValue = "gère {{volume}} crédits / mois" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} politiques" +poolCaption = "Une réserve de {{credits}} crédits pour l’année. Les mois chargés empruntent aux mois plus légers." +priceLabel = "Votre année · 12 mois pour le prix de 10" +rateUnknown = "Nous afficherons le prix exact lors du paiement." +savings = "Vous économisez {{amount}} · 2 mois gratuits." +sizingRow = "Dimensionnement" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Utilisateurs au total" + +[portal.billing.prepaid.consent] +label = "Je comprends que lorsque ma capacité prépayée est épuisée ou expire après 12 mois, le traitement continue automatiquement au tarif standard de paiement à l’usage (jusqu’à ma limite de dépenses), sauf si j’annule, et que je peux annuler à tout moment depuis le portail de facturation." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Géré par Stirling. Opérationnel en quelques minutes." +cloudValue = "Stirling Cloud · Géré" +selfhost = "Auto-hébergé" +selfhostDesc = "Fonctionne sur une infrastructure privée. Même tarif." +selfhostValue = "Auto-hébergé · Infrastructure privée" + +[portal.billing.prepaid.offer] +cta = "Examiner l’offre" +subtitle = "Prépayez une année de traitement PDF et obtenez deux mois gratuits, utilisés avant la facturation à l’usage, en dehors de votre limite de dépenses." +title = "Obtenez 12 mois pour le prix de 10" + +[portal.billing.prepaid.pay] +accountName = "Nom du titulaire du compte" +accountNamePlaceholder = "p. ex. Jane Smith" +companyName = "Nom de l’entreprise (facultatif)" +companyNamePlaceholder = "p. ex. Acme Inc." +downloadInvoice = "Télécharger la facture" +payOnline = "Payer en ligne" +poNumber = "Numéro de commande (facultatif)" +poNumberPlaceholder = "p. ex. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avancé" +advancedDesc = "Les pipelines assurent l’essentiel de votre traitement." +none = "Aucun" +noneDesc = "Aucun pipeline en cours pour l’instant. Activez-les à tout moment." +standard = "Standard" +standardDesc = "Quelques pipelines retraitent les PDFs entrants." + +[portal.billing.prepaid.posture] +essentials = "Essentiels" +essentialsDesc = "Classification, par défaut, plus Partage" +governed = "Gouverné" +governedDesc = "Ajoute Sécurité et Routage" +regulated = "Réglementé" +regulatedDesc = "Toutes les catégories, incl. Conformité, Conservation, Ingestion" + +[portal.billing.prepaid.review] +download = "Télécharger le devis (PDF)" +downloading = "Préparation…" +share = "partager pour approbation" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "Fichiers de moins de 25 MB, sans frais de données" +heavy = "Lourd" +heavyDesc = "Numérisés ou riches en images, régulièrement 50 MB+" +standard = "Standard" +standardDesc = "Principalement petits, quelques scans au-delà de 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Épuisée" +healthy = "Il en reste beaucoup" +low = "Bientôt épuisée" [portal.billing.spendLimit] adjustLimit = "Ajuster la limite" @@ -6494,6 +6713,8 @@ label = "Dépassement prévu." [portal.billing.spendThisMonth] eyebrow = "Dépenses ce mois-ci" +freeRemaining_one = "{{formatted}} PDF gratuit restant" +freeRemaining_other = "{{formatted}} PDFs gratuits restants" processed_one = "{{formattedCount}} PDF traité." processed_other = "{{formattedCount}} PDF traités." processedWithRate_one = "{{formattedCount}} PDF traité, à {{rate}} chacun." @@ -6510,6 +6731,7 @@ reachedTitle = "Limite de dépenses mensuelle atteinte" title = "Impossible d’ouvrir le portail Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "sur {{allowance}} PDF gratuits utilisés" capSuffix_other = "sur {{allowance}} PDF gratuits utilisés" eyebrow = "Essai Processor" @@ -6534,6 +6756,376 @@ signature = "signature" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nouvelle connexion" +createTitleFor = "Connecter {{name}}" +delete = "Supprimer" +edit = "Modifier" +editTitle = "Modifier la connexion" +unsupportedType = "Ce type de connexion ne peut pas être modifié ici." + +[portal.connections.categories.advanced] +description = "Pour un service que nous ne prenons pas encore en charge." +label = "Avancé" + +[portal.connections.categories.audit] +description = "Enregistrez ce qui est arrivé à un document, là où vos auditeurs consultent déjà." +label = "Journalisation d’audit et de conformité" + +[portal.connections.categories.notify] +description = "Informer un autre système qu’un événement s’est produit." +label = "Notification et workflow" + +[portal.connections.categories.security] +description = "Analyser ou étiqueter un document avant qu’il ne poursuive son chemin." +label = "Sécurité et classification" + +[portal.connections.categories.signing] +description = "Envoyer un document pour signature ou notarisation." +label = "Signature électronique et notarisation" + +[portal.connections.categories.storage] +description = "Où les documents traités aboutissent - un bucket, un drive, un ticket ou une page." +label = "Fichier et pièce jointe" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling stocke ceci chiffré et ne l’affiche plus jamais." +label = "Token d’accès" + +[portal.connections.commonFields.apiKey] +label = "Clé API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Les étapes utilisant cette connexion ne peuvent appeler que des chemins sous cette URL, jamais un autre hôte." +label = "URL du serveur" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Hôte" + +[portal.connections.commonFields.password] +label = "Mot de passe" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Nom d’utilisateur" + +[portal.connections.commonFields.webhookUrl] +helperText = "Collez l’URL fournie par le service. Seuls les chemins sous celle-ci peuvent être appelés." +label = "URL Webhook" + +[portal.connections.fields] +name = "Nom de la connexion" +namePlaceholder = "p. ex. Production" + +[portal.connections.picker] +cancel = "Annuler" +createNew = "Nouvelle connexion..." +placeholder = "Sélectionner une connexion" +save = "Enregistrer la connexion" + +[portal.connections.picker2] +back = "Changer d’intégration" +heading = "Que voulez-vous connecter ?" +noResultsBody = "Nous ne le prenons pas encore en charge - dites-nous de quoi il s’agit et nous envisagerons de l’ajouter." +noResultsTitle = "Aucun résultat ne correspond à \"{{query}}\"" +searchPlaceholder = "Rechercher des intégrations - essayez \"sign\", \"ocr\" ou un nom de produit" + +[portal.connections.types.api] +description = "Appelez n’importe quelle API HTTP depuis une politique. Vous choisissez l’URL, l’authentification et la charge utile." +label = "API personnalisée" + +[portal.connections.types.api.fields.authType] +label = "Authentification" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nom d’utilisateur et mot de passe" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Token Bearer" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token dans un en-tête personnalisé" + +[portal.connections.types.api.fields.authType.options.none] +label = "Aucune" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Se connecter pour obtenir un token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Les étapes utilisant cette connexion ne peuvent appeler que des chemins sous cette URL, jamais un autre hôte." +label = "URL de base" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nom de l’en-tête" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Chemin de connexion" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Mot de passe" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Si l’API répond avec un lien de téléchargement, seuls ces hôtes peuvent être récupérés. Séparez-les par des virgules. Laissez vide pour autoriser uniquement l’hôte propre à l’URL de base." +label = "Hôtes de résultat autorisés" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Envoyer le token avec cet en-tête" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "L’en-tête de réponse dans lequel l’appel de connexion renvoie le token, p. ex. X-Auth-Token." +label = "En-tête de réponse du token" + +[portal.connections.types.api.fields.username] +label = "Nom d’utilisateur" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Analysez les documents à la recherche de malwares sur un serveur que vous exploitez - rien ne quitte votre réseau." +label = "ClamAV (auto-hébergé)" + +[portal.connections.types.cloudmersive] +description = "Analyse antivirus et de contenu pour les documents dans une politique." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloquez les documents contenant des macros, exécutables, scripts ou objets intégrés - pas seulement les malwares connus." +label = "Analyse avancée Cloudmersive" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Joindre des documents traités à une page Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Envoyez des documents à ConsignO Cloud (Notarius) pour signature électronique et notarisation." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Votre environnement ConsignO Cloud, incluant /api/v1." +label = "URL du serveur" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Clé API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Secret API" + +[portal.connections.types.consigno.fields.password] +label = "Mot de passe" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Nécessaire uniquement si votre organisation utilise des tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nom d’utilisateur" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publier un message sur Discord lorsqu’une politique traite un document." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexer un événement d’audit dans Elasticsearch chaque fois qu’une politique traite un document." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Interroger un serveur FTP ou FTPS pour récupérer de nouveaux documents." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Mode de connexion" + +[portal.connections.types.ftp.fields.passive.options] +active = "Actif" +passive = "Passif (recommandé)" + +[portal.connections.types.ftp.fields.security] +helperText = "Le FTP simple envoie les identifiants et les fichiers sans chiffrement ; choisissez FTPS si le serveur le prend en charge." +label = "Chiffrement" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicite / AUTH TLS)" +implicit = "FTPS (implicite)" +none = "Aucun (FTP simple)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publier un message dans un espace Google Chat lorsqu’une politique s’exécute." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Joindre des documents traités à un ticket Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Envoyer un document ou une notification par email lorsqu’une politique s’exécute." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Classer les documents traités dans Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Mot de passe d’application" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Détecter les données personnelles dans le texte des documents à l’aide d’un serveur Presidio que vous exploitez." +label = "Presidio (auto-hébergé)" + +[portal.connections.types.purview] +description = "Lire et appliquer des libellés de sensibilité Microsoft Purview sur les PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Facultatif. Nécessaire uniquement pour choisir les libellés par nom ; l’étiquetage fonctionne sans cela." +label = "ID d’application (client)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Secret client" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Apposé sur chaque libellé comme SiteId, afin que les outils compatibles Purview sachent quelle organisation l’a appliqué." +label = "ID d’annuaire (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Un bucket S3 depuis lequel les sources peuvent lire et vers lequel les sorties de pipeline peuvent écrire." +label = "Stockage S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID de clé d’accès" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Laissez vide pour AWS. Définissez ceci pour MinIO ou un autre stockage compatible S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Région" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Clé d’accès secrète" + +[portal.connections.types.sendgrid] +description = "Envoyer un document ou une notification par email lorsqu’une politique s’exécute." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Interroger un serveur SFTP (SSH) pour récupérer de nouveaux documents." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Facultatif. L’empreinte SHA-256 de la clé d’hôte du serveur (depuis `ssh-keyscan host | ssh-keygen -lf -`). Lorsqu’elle est définie, seul un serveur présentant cette clé est accepté ; lorsqu’elle est vide, la clé vue à la première connexion est épinglée et les changements ultérieurs sont refusés." +label = "Empreinte de clé d’hôte" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Phrase secrète de la clé" + +[portal.connections.types.sftp.fields.password] +helperText = "Laissez vide si vous vous authentifiez avec une clé privée à la place." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Facultatif. Collez une clé privée OpenSSH pour vous authentifier avec une clé plutôt qu’un mot de passe." +label = "Clé privée" + +[portal.connections.types.slack] +description = "Publier un message sur Slack lorsqu’une politique traite un document." +label = "Slack" + +[portal.connections.types.smb] +description = "Surveiller un partage réseau SMB / CIFS (Windows ou Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Partage réseau" + +[portal.connections.types.smb.fields.domain] +helperText = "Facultatif. Le domaine ou groupe de travail Windows, si votre serveur en utilise un." +label = "Domaine" + +[portal.connections.types.smb.fields.share] +label = "Partage" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Envoyer un événement d’audit à Splunk chaque fois qu’une politique traite un document." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Envoyer un événement d’audit à un collecteur HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publier un message dans un canal Teams lorsqu’une politique s’exécute." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publier chaque document, ou seulement les détails, vers une URL de votre choix." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Déclencher un scénario Zap ou Make depuis une politique." +label = "Zapier & Make" + +[portal.docs] +browse = "Parcourir les docs" +viewSource = "Voir la source sur GitHub" + [portal.docs.authentication] codeCaption = "chaque requête" eyebrow = "BIEN DÉMARRER" @@ -6624,6 +7216,9 @@ eyebrow = "COMPÉTENCES" lead = "Capacités nommées et groupées que votre agent invoque comme un seul outil. Chaque compétence est une chaîne d’opérations déterministe avec des évaluations associées." title = "Compétences d’agent" +[portal.docs.toc] +title = "Sur cette page" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "RÉFÉRENCE API" @@ -6697,6 +7292,8 @@ type = "Type" user = "Utilisateur" [portal.documents.queue.empty] +connectSource = "Connecter une source" +createPipeline = "Créer un pipeline" description = "À mesure que les sources alimentent vos pipelines en documents, ils apparaîtront ici pour vérification." title = "Aucun document dans la file d’attente" @@ -6712,11 +7309,11 @@ editorAction = "Éditeur" empty = "Aucun document ne correspond à ce filtre." rowActions = "Actions de ligne" sensitiveLabel = "Sensible" -sensitiveTitle = "Sensible — accès requis" [portal.documents.table.columns] action = "Pipeline / Action" document = "Document" +labels = "Labels" product = "Produit" status = "Statut" time = "Heure" @@ -6732,6 +7329,7 @@ host = "Hôte" lastSeen = "Dernière activité" region = "Région" status = "Statut" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Cette vue a rencontré une erreur inattendue. Réessayez ou chois retry = "Réessayer" title = "Un problème est survenu sur cette page" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Retour" +body = "Téléchargez l’application de bureau ou auto-hébergez le serveur." +done = "Terminé" +guide = "Ouvrir le guide {{name}}" +openInBrowser = "Ouvrir dans le navigateur à la place" +sectionDesktop = "Application de bureau" +sectionSelfHosted = "Auto-hébergé" +title = "Installer l’éditeur Stirling PDF" + +[portal.home.download.docker] +detailBody = "Exécutez l’image officielle sur votre serveur." +note = "S’ouvre sur :8080. Connexion par défaut : admin / stirling." +tagline = "Exécuter le serveur dans un conteneur" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installez le chart Helm officiel." +note = "Ajoutez un PVC pour /configs afin de conserver les paramètres." +tagline = "Déployer sur un cluster avec Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Versions natives pour votre distribution." +guideBtn = "Voir les options d’installation Linux" +note = "Des versions AppImage, .deb et .rpm sont disponibles pour les distributions courantes." +tagline = "Application de bureau native pour votre distribution" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ou via Homebrew" +detailBody = "Exécutez l’installateur ou utilisez Homebrew." +downloadBtn = "Télécharger pour macOS" +tagline = "Application native pour Apple silicon et Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Exécutez le .jar sur un serveur bare metal." +downloadBtn = "Télécharger le .jar" +note = "Nécessite Java 21+. S’ouvre sur :8080 ; identifiant par défaut admin / stirling." +runLabel = "Puis exécutez-le" +tagline = "Exécutez le .jar sur un serveur bare metal" +title = "Configuration manuelle du serveur" + +[portal.home.download.windows] +altLabel = "Ou via winget" +detailBody = "Exécutez l’installateur ou utilisez un gestionnaire de paquets." +downloadBtn = "Télécharger l’installateur" +tagline = "Application native pour Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} actifs" -invite = "Inviter des coéquipiers" +activeOfDeployed = "{{active}} sur {{total}} actifs ce mois-ci" name = "Stirling PDF Editor" open = "Ouvrir dans le navigateur" updated = "mis à jour {{time}}" +[portal.home.editor.deploy] +finish = "Terminer le déploiement" +options = "Voir les options de déploiement" +start = "Déployer l’éditeur" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Auto-hébergé · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Bon après-midi" evening = "Bonsoir" morning = "Bonjour" -[portal.home.onboarding] -dismiss = "Ignorer la configuration" -notStarted = "Non démarré" -progress = "{{done}} sur {{total}} terminés" -title = "Terminer la configuration" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC à l’échelle de l’organisation, tarification au volume engagé et déploiement air-gapped." -cta = "Démarrer l’essai" -ctaQuote = "Obtenir un devis" -lead = "Pour plus de 250 employés." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Déployez l’application de bureau, gratuite pour toujours." -title = "Télécharger l’éditeur PDF" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} actives · {{recommended}} recommandées" -chip = "{{active}} actives" -title = "Activer les politiques" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} connectées · chaque PDF gouverné là où il arrive" -chip = "{{connected}} connectées" -title = "Connecter vos sources" - -[portal.home.quickActions] -subtitle = "Tâches principales pour aujourd’hui" -title = "Actions rapides" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositeur en 3 étapes sur la bibliothèque d’opérations typées" -title = "Créer un pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, dossiers surveillés" -title = "Connecter une source" - -[portal.home.quickActions.issueApiKey] -blurb = "Clé délimitée avec limites de débit et liste d’autorisation IP" -title = "Émettre une clé API" - [portal.infrastructure] manageEditorDeployment = "Gérer le déploiement de l’éditeur" sectionsAriaLabel = "Sections d’infrastructure" @@ -6878,13 +7541,9 @@ heading = "Clés API" subheading = "Identifiants délimités avec limites de débit par clé, permissions et listes d’autorisation IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IP autorisées" -anyIp = "Toute IP (aucune liste d’autorisation)" created = "Créée" lastUsed = "Dernière utilisation" -permissions = "Permissions" -rateLimit = "Limite de débit" -rateLimitValue = "{{value}} req/min" +revoke = "Révoquer la clé" usageMonth = "Utilisation ce mois-ci" usageToday = "Utilisation aujourd’hui" @@ -6892,6 +7551,15 @@ usageToday = "Utilisation aujourd’hui" description = "Créez une clé délimitée pour commencer à appeler l’API Stirling." title = "Aucune clé API pour le moment" +[portal.infrastructure.apiKeys.error] +load = "Impossible de charger vos clés API. Veuillez réessayer." + +[portal.infrastructure.apiKeys.revoke] +body = "Révoquer \"{{name}}\" ? Toute intégration utilisant encore cette clé commencera immédiatement à recevoir des erreurs 401. Cette action est irréversible." +cancel = "Annuler" +confirm = "Révoquer la clé" +title = "Révoquer la clé API" + [portal.infrastructure.audit] filterAriaLabel = "Filtrer les événements d’audit par catégorie" heading = "Journaux d’audit" @@ -6935,6 +7603,7 @@ all = "Tous" auth = "Auth" config = "Config" elevation = "Élévation" +policy = "Stratégie" processing = "Traitement" security = "Sécurité" @@ -6944,19 +7613,30 @@ title = "Vous n’avez pas accès" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Élévation" +policy = "Stratégie" processing = "Traitement" totalEvents = "Événements totaux · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Authentification" +config = "Config" +elevation = "Élévation" +policy = "Stratégie" +processing = "Traitement" +security = "Sécurité" + +[portal.infrastructure.auditStatusLabel] +danger = "Erreur" +info = "Info" +success = "Succès" +warning = "Avertissement" + [portal.infrastructure.createKey] cancel = "Annuler" createKey = "Créer une clé" done = "Terminé" -ipAllowlistHelper = "Plages CIDR séparées par des virgules. Laissez vide pour autoriser toute IP." -ipAllowlistLabel = "Liste d’autorisation IP" keyNameLabel = "Nom de la clé" keyNamePlaceholder = "p. ex. Production · ingestion" -permissionsLabel = "Permissions" secretKeyCaption = "Clé secrète" secretWarning = "Stockez-la dans un gestionnaire de secrets. Stirling ne stocke jamais qu’un hash — il n’y a aucun moyen de la récupérer plus tard." subtitle = "Limitez la clé au minimum nécessaire. Vous pouvez la faire tourner ou la révoquer à tout moment." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copiez ce secret maintenant — il ne sera plus affiché." title = "Créer une clé API" titleCreated = "Clé créée" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Déployé par" -environment = "Environnement" -product = "Produit" -status = "Statut" -version = "Version" -when = "Quand" - -[portal.infrastructure.deployments.recent] -heading = "Déploiements récents" -subheading = "Les derniers déploiements sur les produits et environnements." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instances" -latency = "Latence" -load = "Charge" -p99 = "P99" -region = "Région" -status = "Statut" -throughput = "Débit" -uptime = "Temps de disponibilité" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Régions" -subheading = "Santé en direct de chaque région Stirling déployée — latence, charge et version de déploiement." - -[portal.infrastructure.deployments.regions.empty] -description = "Les régions déployées apparaissent ici une fois votre espace de travail provisionné." -title = "Aucune région déployée" - -[portal.infrastructure.models] -heading = "Modèles" -msValue = "{{value}} ms" -subheading = "Le catalogue de modèles et le routage qui alimentent le traitement des documents dans votre espace de travail." - -[portal.infrastructure.models.byom] -description = "Enregistrez un modèle on-prem ou auto-hébergé et épinglez-le à une région pour un traitement lié à la résidence des données." -title = "Apportez votre propre modèle" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "Modèles gérés disponibles pour votre espace de travail, avec latence et coût en direct." -subEnterprise = "Modèles gérés, apportez-votre-propre-modèle et on-prem — avec épinglage par région disponible." - -[portal.infrastructure.models.catalogue.empty] -description = "Les modèles du catalogue de votre espace de travail apparaissent ici." -title = "Aucun modèle disponible" - -[portal.infrastructure.models.columns] -cost = "Coût" -latency = "Latence" -load = "Charge" -model = "Modèle" -status = "Statut" -type = "Type" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "Modèles actifs" -avgLatency = "Latence moy." -included = "Inclus" -monthlySpend = "Dépense mensuelle des modèles" - -[portal.infrastructure.models.routing] -empty = "Aucune règle de routage configurée." -heading = "Règles de routage" -sub = "Quel modèle gère chaque opération. La valeur par défaut s’applique lorsqu’aucune règle plus précise ne correspond." -subLocked = "Acheminez les opérations vers des modèles spécifiques — disponible sur les plans payants." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Passez à Pro pour contrôler quel modèle gère chaque opération et type de document." -title = "Le routage des modèles est une fonctionnalité payante" - -[portal.infrastructure.models.routingColumns] -default = "Par défaut" -docType = "Type de document" -modelForAria = "Modèle pour {{operation}}" -operation = "Opération" -routedTo = "Acheminé vers" - -[portal.infrastructure.security.access.byok] -description = "Fournissez une clé depuis votre propre KMS. Stirling chiffre avec celle-ci mais peut toujours lire." -label = "Apportez votre propre clé (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Les clés ne quittent jamais votre KMS. Stirling ne détient que du texte chiffré." -label = "Conservez votre propre clé (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling gère les clés de chiffrement. Le plus simple — aucune opération de clé de votre côté." -label = "Clés détenues par Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Politique d’accès aux documents" -subheading = "Contrôle qui peut déchiffrer les documents traités au repos." - -[portal.infrastructure.security.attestations] -heading = "Attestations de conformité" -noReport = "Aucun rapport disponible" -subheading = "Posture d’audit cadre par cadre, avec rapports disponibles sur les contrôles attestés." -viewReport = "Voir le rapport →" - -[portal.infrastructure.security.compliance] -heading = "Conformité" -subheading = "Attestations et certifications couvrant la plateforme Stirling." - -[portal.infrastructure.security.empty] -description = "La configuration de sécurité de votre espace de travail apparaîtra ici." -title = "Posture de sécurité indisponible" - -[portal.infrastructure.security.hyokBanner] -description = "Avec HYOK, les clés de chiffrement ne quittent jamais votre KMS. Stirling stocke et traite uniquement du texte chiffré que vous pouvez révoquer à tout moment." -title = "Stirling ne peut pas déchiffrer vos documents" - -[portal.infrastructure.security.ipAllowlist] -empty = "Aucune plage IP configurée — toutes les IP sont autorisées." -heading = "Liste d’autorisation IP" -sub = "L’accès API est limité à ces plages CIDR." -subLocked = "Limitez l’accès API aux plages IP connues — disponible sur les plans payants." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Passez à Pro pour limiter l’accès API à des réseaux spécifiques." -title = "La liste d’autorisation IP est une fonctionnalité payante" - -[portal.infrastructure.security.ipColumns] -added = "Ajouté" -addedBy = "Ajouté par" -cidr = "CIDR" -label = "Libellé" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithme" -heading = "Gestion des clés de chiffrement" -keyId = "Identifiant de clé" -lastRotated = "Dernière rotation" -rotateKey = "Faire tourner la clé" -rotationPolicy = "Politique de rotation" -subheading = "Garde des clés qui chiffrent les documents au repos — qui peut déchiffrer et comment les clés tournent." - -[portal.infrastructure.security.managedBanner] -description = "La garde apportez-votre-propre-clé (BYOK) et conservez-votre-propre-clé (HYOK) est disponible sur Enterprise. Mettez à niveau pour fournir des clés depuis votre propre KMS." -title = "Les clés sont gérées par Stirling avec votre plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asie-Pacifique" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · limite des données GDPR" -label = "Union européenne" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "États-Unis" - -[portal.infrastructure.security.residencyHeader] -heading = "Résidence des données" -subheading = "Où les documents sont stockés et traités." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} utilisé" - -[portal.infrastructure.storage.empty] -description = "Le stockage connecté et l’utilisation apparaissent ici." -title = "Aucun stockage configuré" - -[portal.infrastructure.storage.lifecycle] -active = "Actif" -activeRange = "0–{{value}}j" -archived = "Archivé" -coldStorage = "stockage froid" -deleted = "Supprimé" -never = "jamais" -purged = "purgé" - -[portal.infrastructure.storage.providers] -connect = "Connecter" -connected = "Connecté" -heading = "Fournisseurs connectés" -subheading = "Où les artefacts traités sont écrits." - -[portal.infrastructure.storage.retention] -heading = "Rétention" -subheading = "Durée de conservation des artefacts avant suppression du cycle de vie." -windowLabel = "Fenêtre de rétention par défaut" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} jour" -days_other = "{{count}} jours" -never = "Ne jamais supprimer" - -[portal.infrastructure.storage.totalUsage] -heading = "Utilisation totale" -progressLabel = "Stockage utilisé" -subheading = "Stockage consommé sur tous les fournisseurs connectés." +[portal.infrastructure.keyLabel] +active = "Active" +revoked = "Révoquée" [portal.infrastructure.tabs] apiKeys = "Clés API" @@ -7174,6 +7656,49 @@ models = "Modèles" security = "Sécurité" storage = "Stockage" +[portal.integrations] +availableHeading = "Disponible" +comingSoonHeading = "Bientôt disponible" +connect = "Connecter" +connectionCount_one = "{{count}} connexion" +connectionCount_other = "{{count}} connexions" +customApi = "API personnalisée" +searchPlaceholder = "Rechercher des intégrations" +subtitle = "Tout ce avec quoi Stirling peut communiquer. Les identifiants sont créés une fois ici, puis référencés par les sources, les stratégies et les pipelines." +title = "Intégrations" +typedName = "Nom de {{tool}}" + +[portal.integrations.filters] +all = "Tous" +audit = "Audit" +connected = "Connecté" +notify = "Notification" +security = "Sécurité" +signing = "Signature" +storage = "Stockage" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Connecté" + +[portal.integrations.table] +integration = "Intégration" +worksWith = "Fonctionne avec" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Stratégies" +sources = "Sources" + +[portal.legal] +draft = "{{label}} · brouillon" +loadError = "Impossible de charger ce document. Veuillez réessayer." +loading = "Chargement…" +title = "Document juridique" + [portal.nav] agent-builder = "Constructeur d’agents" components = "Composants" @@ -7182,33 +7707,17 @@ documents = "Documents" editor = "Éditeur" home = "Accueil" infrastructure = "Infrastructure" +integrations = "Intégrations" pipelines = "Pipelines" policies = "Politiques" procurement = "Achats" -settings = "Paramètres" sources = "Sources" usage = "Utilisation et facturation" users = "Utilisateurs" -[portal.notifications] -markAllRead = "Tout marquer comme lu" -title = "Notifications" -viewAll = "Tout voir" - -[portal.notifications.ariaLabel] -none = "Notifications, aucune non lue" -unread_one = "Notifications, {{count}} non lue" -unread_other = "Notifications, {{count}} non lues" - -[portal.notifications.count] -allRead = "tout lu" -loading = "chargement" -new_one = "{{count}} nouvelle" -new_other = "{{count}} nouvelles" - -[portal.notifications.empty] -description = "Aucune nouvelle notification." -title = "Vous êtes à jour" +[portal.nav.section] +platform = "Plateforme PDF" +processor = "Processeur PDF" [portal.pipelines] subtitle = "Chaque pipeline de documents automatisé sur le backend : une chaîne ordonnée d’opérations sur un ensemble de sources, exécutée sur un déclencheur. Cliquez sur une ligne pour voir ses étapes et sources." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Nouveau pipeline" [portal.pipelines.builder] -addStep = "Ajouter un outil" +activate = "Activate" back = "Retour aux pipelines" +cannotFollow = "Impossible d’accepter {{produced}}" +chooseAccount = "Choisir un compte" +chooseDestination = "Choisir une destination" +chooseOperation = "Choisir l’action de cette étape" +chooseSource = "Choisir une source" discard = "Abandonner les modifications" -enabled = "Activé" +inputs = "Entrée" +inputSource = "Source d’entrée" +inputTrigger = "Déclencheur" keepEditing = "Continuer à modifier" -needsUpload = "Nécessite un fichier téléversé" +moreActions = "More actions" +needsConfiguring = "Configuration requise" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Aucun outil ne correspond à votre recherche." -pipelineSettings = "Paramètres du pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Rechercher des outils" -selectToolBody = "Ajoutez un outil pour construire votre pipeline." -selectToolTitle = "Aucun outil pour le moment" -toolSettings = "Paramètres de l’outil" +sendToSystem = "Envoyer vers un autre système" +stepsIncompatible = "Ces étapes ne peuvent pas s’exécuter sur ce que produit leur étape précédente : {{tools}}." +stepsNeedSetup = "Ces étapes doivent encore être configurées avant l’enregistrement : {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Opération non reconnue, conservée telle quelle." unsavedBody = "Vous avez des modifications non enregistrées. Enregistrez-les avant de quitter ou abandonnez-les ?" unsavedTitle = "Modifications non enregistrées" -uploadUnsupported = "Les fichiers téléversés ne sont pas encore pris en charge dans les pipelines, ces étapes ne peuvent donc pas être enregistrées : {{tools}}." usesDefaults = "S’exécute avec les paramètres par défaut" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combine tous les fichiers de l’étape précédente" +fan-out = "S’exécute une fois par fichier de l’étape précédente" +format-mismatch = "Nécessite {{accepts}}, mais l’étape précédente produit {{produced}}" +output-uncertain = "Peut ne pas s’exécuter : la sortie de l’étape précédente dépend de sa configuration" +source-mismatch = "Nécessite {{accepts}}, mais l’entrée de ce pipeline est {{produced}}" +undeclared-operation = "Impossible de vérifier ce que cette étape accepte" [portal.pipelines.composer] addTool = "Ajouter un outil" -cancel = "Annuler" -chainEmpty = "Ajoutez un outil pour commencer à construire votre pipeline." create = "Créer le pipeline" -directory = "Dossier de sortie" -directoryHelp = "Chemin absolu sur le serveur. Doit se trouver dans les dossiers autorisés configurés." +createPaused = "Create paused" editingUnsupported = "L’affichage de ces paramètres d’outil pour modification n’est pas encore pris en charge." -moveDown = "Déplacer vers le bas" -moveUp = "Déplacer vers le haut" +editSource = "Edit source" name = "Nom" namePlaceholder = "p. ex. Passage de caviardage" -noSources = "Aucune source connectée pour le moment. Le pipeline peut toujours s’exécuter sur des fichiers qui lui sont fournis directement." noToolSettings = "Cet outil n’a aucun paramètre configurable." -operations_one = "Opération ({{count}})" -operations_other = "Opérations ({{count}})" output = "Sortie" -removeStep = "Supprimer l’opération" save = "Enregistrer les modifications" scheduleEvery = "Exécuter tous les" -sources = "Sources" -sourcesLoading = "Chargement des sources..." trigger = "Déclencheur" triggerManual = "Manuel uniquement" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "jours" hours = "heures" minutes = "minutes" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Supprimer \"{{name}}\" ? Cette action est irréversible." cancel = "Annuler" @@ -7273,28 +7815,62 @@ confirm = "Supprimer" title = "Supprimer le pipeline ?" [portal.pipelines.detail] +clearHistory = "Effacer l’historique" delete = "Supprimer le pipeline" run = "Exécuter maintenant" [portal.pipelines.empty] action = "Créer un pipeline" +connectSource = "Connecter une source" description = "Créez votre premier pipeline : choisissez les sources sur lesquelles il s’exécute, enchaînez les opérations et choisissez où va la sortie." title = "Aucun pipeline pour le moment" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Actifs" paused = "En pause" total = "Pipelines" -[portal.pipelines.output] -folder = "Écrire dans un dossier" -inline = "Renvoyer les fichiers" - [portal.pipelines.run] +allProcessed_one = "Rien à exécuter : le {{count}} document de la source a déjà été traité." +allProcessed_other = "Rien à exécuter : les {{count}} documents des sources ont déjà été traités." completed_one = "Exécution terminée." completed_other = "Les {{count}} exécutions sont terminées." empty = "Rien à exécuter : les sources n’avaient aucun document à traiter." failed = "Échec de l’exécution : {{error}}" +inFlight = "Rien de nouveau à exécuter : des documents sont encore en cours de traitement depuis une exécution précédente." +parked_one = "Rien à exécuter : {{count}} document a échoué précédemment et est mis en attente. Corrigez la cause, puis effacez l’historique pour le réessayer." +parked_other = "Rien à exécuter : {{count}} documents ont échoué précédemment et sont mis en attente. Corrigez la cause, puis effacez l’historique pour les réessayer." running = "Exécution démarrée ; toujours en cours." timeout = "L’exécution prend plus de temps que prévu ; elle peut encore se terminer en arrière-plan." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Sources" status = "Statut" steps = "Étapes" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Surveillance de dossier" @@ -7314,12 +7891,18 @@ manual = "Manuel" schedule = "Planifié" [portal.policies] +defaultName = "Stratégie {{category}}" subtitle = "Automatisations permanentes qui appliquent un pipeline d’outils à chaque document. Chaque politique se déclenche lors du téléversement ou de l’exportation, exécute sa chaîne d’outils et enregistre la version appliquée à côté de l’original." title = "Politiques" [portal.policies.card] comingSoon = "Passer à Enterprise" notSetUp = "Non configuré" +requiresAiEngine = "Nécessite le moteur AI" + +[portal.policies.categories.classification] +desc = "Identifiez le type de chaque document lors du téléversement et marquez ses métadonnées pour le classement et la recherche." +label = "Classification" [portal.policies.categories.compliance] desc = "Appliquez les exigences HIPAA, GDPR, SOC 2 ou FedRAMP à chaque document." @@ -7344,6 +7927,13 @@ label = "Sécurité" [portal.policies.config] scopeAll = "Tous les documents" +[portal.policies.config.classification] +summary = "Classe chaque document téléversé selon les étiquettes de votre équipe et marque ses métadonnées." + +[portal.policies.config.classification.rules] +0 = "Classer" +1 = "Marquer les métadonnées" + [portal.policies.config.compliance] summary = "Valide les documents par rapport aux cadres réglementaires avant qu’ils ne quittent le système." @@ -7371,6 +7961,29 @@ minConfidence = "Confiance min." 2 = "Nommer" 3 = "Normaliser" +[portal.policies.config.purview] +label = "Appliquer une étiquette de sensibilité Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant Purview" +labelId = "ID de l’étiquette" +labelIdHelp = "Le GUID de l’étiquette, depuis le portail Purview. L’application d’une étiquette écrit ses métadonnées sur le PDF ; elle ne peut pas chiffrer, ce qui nécessite le client Microsoft." +labelName = "Nom de l’étiquette (facultatif)" +labelNameHelp = "Enregistré avec l’ID afin que les personnes et les outils puissent reconnaître l’étiquette." + +[portal.policies.config.purview.fields.method] +help = "Purview distingue une étiquette appliquée par une stratégie d’une étiquette choisie par une personne." +label = "Mode d’enregistrement de l’étiquette" +privileged = "Comme choisie par une personne" +standard = "Comme appliquée automatiquement" + +[portal.policies.config.purviewRead] +label = "Lire une étiquette Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant Purview" +connectionHelp = "Lit l’étiquette que le document porte déjà, afin que les étapes ultérieures puissent agir en conséquence." + [portal.policies.config.retention] summary = "Applique la durée de conservation des documents, quand les archiver et quand les supprimer." @@ -7438,6 +8051,7 @@ title = "Aucune activité pour le moment" [portal.policies.endpoints] addWatermark = "Filigrane" autoRedact = "Masquer les PII" +classifyAndLabel = "Classer" compressPdf = "Compresser" flatten = "Aplatir" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vos politiques sont enregistrées et apparaîtront une fois la co retry = "Réessayer" title = "Backend indisponible" +[portal.policies.operations] +change = "Changer l’action de cette étape" +noResults = "Aucune étape ne correspond. Essayez un nom de produit, ou \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Rechercher des étapes — essayez \"scan\", \"attach\" ou un nom de produit" + +[portal.policies.operations.bodyMode] +binary = "Octets bruts du fichier" +json = "Dans une charge utile JSON" +multipart = "Comme téléversement de fichier (multipart)" + +[portal.policies.operations.clamavScan] +description = "Votre propre serveur ClamAV vérifie le document ; rien ne quitte votre réseau." +label = "Rechercher des virus (auto-hébergé)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rejette les documents contenant des macros, exécutables, scripts ou objets intégrés." +label = "Bloquer le contenu actif" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive vérifie le document et l’exécution s’arrête s’il n’est pas sans menace." +label = "Rechercher des virus" + +[portal.policies.operations.confluenceAttach] +description = "Classe le document traité sur une page." +label = "Joindre à une page Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Soumet le document à ConsignO Cloud pour signature." +label = "Envoyer pour signature (ConsignO)" +note = "Soumet le document. La récupération de la copie signée n’est pas encore prise en charge, car une étape ne peut pas transmettre un résultat à une étape ultérieure." + +[portal.policies.operations.customApiCall] +description = "Définissez vous-même l’appel : chemin, méthode, corps et traitement de la réponse." +label = "Appeler une API personnalisée" + +[portal.policies.operations.discordNotify] +description = "Informe un canal que la stratégie a traité un document." +label = "Publier un message sur Discord" + +[portal.policies.operations.elasticIndex] +description = "Enregistre ce que la stratégie a fait sous forme de document consultable." +label = "Indexer un événement d’audit dans Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart convient aux API de téléversement ; JSON convient aux API qui veulent le fichier dans une charge utile ; binary envoie les octets bruts." +label = "Comment envoyer le document" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Un corps JSON envoyé tel quel. {{document.base64}} contient le fichier ; {{document.*}} et {{run.*}} sont renseignés pour chaque document." +label = "Corps JSON" + +[portal.policies.operations.fields.connection] +label = "Compte" + +[portal.policies.operations.fields.domain] +label = "Domaine Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Le champ de formulaire sous lequel l’API attend le document." +label = "Nom du champ de fichier" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Un objet JSON de paires nom/valeur d’en-tête. L’identifiant de la connexion est ajouté pour vous." +label = "En-têtes" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Clé du ticket" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Envoyé comme corps du message. {{document.*}} et {{run.*}} sont renseignés pour chaque document." +label = "Message" + +[portal.policies.operations.fields.method] +label = "Méthode" + +[portal.policies.operations.fields.pageId] +label = "ID de page" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Ajouté à l’URL de base de la connexion. Il ne peut jamais atteindre un autre hôte." +label = "Chemin" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Emplacement où l’écrire dans le lecteur. {{document.filename}} est renseigné pour chaque document." +label = "Chemin de destination" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapport laisse le document intact. Remplacer l’échange contre ce qui est renvoyé." +label = "Que faire de la réponse" + +[portal.policies.operations.fields.signerEmail] +label = "E-mail du signataire" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Objet" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyse ce texte. Utilisez {{document.title}} ou collez le texte que vous souhaitez vérifier." +label = "Texte à vérifier" + +[portal.policies.operations.fields.to] +label = "À" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nom d’utilisateur Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informe un espace que la stratégie a traité un document." +label = "Publier un message sur Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Classe le document traité dans un ticket." +label = "Joindre à un ticket Jira" + +[portal.policies.operations.mailgunEmail] +description = "Envoie le document traité en pièce jointe." +label = "Envoyer le document par e-mail (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Écrit le document traité dans un dossier." +label = "Téléverser vers Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Votre propre serveur Presidio signale les données personnelles qu’il trouve." +label = "Détecter les données personnelles" +note = "Presidio lit du texte, pas des fichiers. Fournissez-lui le texte que vous souhaitez vérifier — le document lui-même n’est pas téléversé." + +[portal.policies.operations.responseMode] +replace = "Remplacer le document par la réponse" +report = "Conserver le document, enregistrer la réponse" + +[portal.policies.operations.sendgridEmail] +description = "Envoie le document traité en pièce jointe." +label = "Envoyer le document par e-mail (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informe un canal que la stratégie a traité un document." +label = "Publier un message sur Slack" + +[portal.policies.operations.splunkEvent] +description = "Enregistre ce que la stratégie a fait, là où vos auditeurs consultent déjà." +label = "Envoyer un événement d’audit à Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Enregistre ce que la stratégie a fait dans un collecteur HTTP." +label = "Envoyer un événement d’audit à Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informe un canal que la stratégie a traité un document." +label = "Publier un message sur Teams" + +[portal.policies.operations.webhookPost] +description = "Envoie le document lui-même à l’URL de votre choix." +label = "Publier le document vers un webhook" + +[portal.policies.operations.zapierNotify] +description = "Transmet les détails de l’exécution à votre automatisation." +label = "Déclencher un Zap ou un scénario Make" + [portal.policies.stats] activeFor = "Actif" dataProcessed = "Données traitées" @@ -7473,6 +8282,13 @@ label = "Docs appliqués" description = "Configurées mais non déclenchées" label = "En pause" +[portal.policies.table] +appliesTo = "S’applique à" +docs = "Docs appliqués" +enforces = "Applique" +policy = "Stratégie" +status = "Statut" + [portal.policies.wizard.actions] back = "Retour" cancel = "Annuler" @@ -7480,6 +8296,10 @@ continue = "Continuer" enablePolicy = "Activer la politique" saveChanges = "Enregistrer les modifications" +[portal.policies.wizard.capability.classify] +desc = "Identifie le type du document à partir des étiquettes de votre équipe et le marque, afin qu’il soit classé et recherché par catégorie." +label = "Classer le document" + [portal.policies.wizard.capability.compress] desc = "Compresse le document pour réduire la taille du fichier." label = "Réduire la taille du fichier" @@ -7500,10 +8320,18 @@ label = "Masquer les informations sensibles" desc = "Supprime le JavaScript masqué afin que rien ne puisse s’exécuter automatiquement à l’ouverture du document." label = "Retirer le contenu actif" +[portal.policies.wizard.capability.timestampPdf] +desc = "Prouve que le document existait sous cette forme exacte à un moment donné, à l’aide d’une autorité d’horodatage indépendante. Seul un hash est envoyé - le document ne quitte jamais votre serveur." +label = "Ajouter un horodatage de confiance" + [portal.policies.wizard.capability.watermark] desc = "Appose une marque visible (p. ex. « Confidentiel ») sur chaque page." label = "Appliquer un filigrane" +[portal.policies.wizard.classification] +description = "Chaque document téléversé est classé selon les étiquettes intégrées et marqué avec les types qui conviennent. L’ensemble d’étiquettes est partagé par toute votre équipe." +labelsHeading = "Étiquettes de classification" + [portal.policies.wizard.errors] noTools = "Activez d’abord au moins un outil dans le flux de travail." saveFailed = "Impossible d’enregistrer la politique. Veuillez réessayer." @@ -7548,73 +8376,118 @@ setUp = "Configurer la politique {{category}}" [portal.policies.wizard.workflow] description = "Choisissez ce que cette politique fait à chaque document qu’elle traite." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} actifs" -noRule = "Aucune règle appliquée pour le moment" -subtitle = "Automatisations permanentes par lesquelles passe chaque document, quel que soit le pipeline qui le gère." -title = "Ce qui s’exécute sur vos PDF" - [portal.policySummary.action] -comingSoon = "Passer à Enterprise" -configure = "Configurer" setUp = "Configurer" -[portal.policySummary.column] -activeRule = "Règle active" -policy = "Politique" -status = "Statut" +[portal.processorFlow] +footnote = "Les compteurs portent sur les dernières 24 heures. La vitesse du flux est illustrative." +liveBadge = "En direct" +stats = "{{connected}} connectés · {{processed}} PDF traités" +title = "Processeur PDF" -[portal.policySummary.empty] -description = "Une fois les politiques configurées, les catégories apparaissent ici." -title = "Aucune politique pour le moment" +[portal.processorFlow.lens] +ariaLabel = "Mode d’affichage" +flow = "Flux" +sankey = "Sankey" -[portal.policySummary.state] -active = "Actif" -off = "Désactivé" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Échec" +heading = "Résultats" +success = "Livrés" + +[portal.processorFlow.policies] +activeCount = "{{n}} actives" +count = "{{n}} · 24 h" +heading = "Stratégies" +setUp = "Configurer" soon = "Bientôt" -[portal.processingStatus] -managePlan = "Gérer le plan" -volumeSuffix = "PDF traités · 30 derniers jours" +[portal.processorFlow.sankey] +waist = "Stratégies · {{n}} actives" + +[portal.processorFlow.sankey.empty] +description = "Connectez une source et activez une stratégie pour voir les documents circuler dans le processeur." +title = "Aucun flux pour le moment" + +[portal.processorFlow.sources] +comingSoonTag = "Connecter" +editor = "Éditeur Stirling PDF" +heading = "Sources" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Réception des e-mails" [portal.procurement] -reset = "Réinitialiser les achats (démo)" subtitle = "Faites évaluer, contractualiser et intégrer votre équipe. Chaque document au même endroit." title = "Achats" -[portal.procurement.action] -download = "Télécharger" -pay = "Payer maintenant" -request = "Demander" -sign = "Vérifier et signer" -upload = "Uploader" - [portal.procurement.agreement] agreeCta = "Accepter et s’abonner" +confidential = "Confidentiel" confirm = "J’ai lu et j’accepte le Stirling Enterprise Agreement." -eyebrow = "Contrat" -intro = "Un contrat combiné couvre votre accord : Master Service Agreement, Order Form, EULA et Data Processing Agreement. Relisez-le, puis acceptez pour transformer le devis en abonnement engagé." -title = "Vérifiez votre contrat Enterprise" +docName = "Contrat Stirling Enterprise" +docSub = "MSA, bon de commande, EULA et DPA, combinés en une seule signature." +download = "Télécharger" +downloadDraftError = "Impossible de générer le PDF du contrat - le moteur de rendu des documents est indisponible. Veuillez réessayer sous peu ou contacter le support." +downloadError = "Impossible de télécharger le contrat signé. Veuillez réessayer." +legalName = "Nom de l’entité juridique" +legalNamePlaceholder = "L’entité juridique qui signera" +loadError = "Impossible de charger le contrat. Veuillez réessayer." +loading = "Chargement du contrat..." +ref = "Réf. {{ref}} · {{version}}" +requestChanges = "Demander des modifications" +scrollHint = "Faites défiler pour examiner" +signatory = "Nom du signataire" +signatoryPlaceholder = "Nom complet" +signatoryTitle = "Fonction du signataire" +signatoryTitlePlaceholder = "p. ex. Directeur juridique" +signError = "Impossible d’enregistrer votre signature. Veuillez réessayer." [portal.procurement.builder] addons = "Modules complémentaires" +addressLine1 = "Adresse ligne 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresse ligne 2" +addressLine2Placeholder = "Suite, étage, bâtiment" back = "Retour" businessName = "Nom de l’entreprise" businessNamePlaceholder = "Votre entreprise" +city = "Ville" +cityPlaceholder = "San Francisco" +completeRequired = "Veuillez renseigner les champs obligatoires (marqués *) avec une adresse e-mail valide avant de générer le devis." +contactEmail = "E-mail du contact" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nom du contact" +contactNamePlaceholder = "Jane Doe" continue = "Continuer" -country = "Pays" -countryEuro = "Zone euro (EUR €)" -countryUK = "Royaume-Uni (GBP £)" -countryUS = "États-Unis (USD $)" +done = "Terminé" eula = "J’ai lu et j’accepte le Stirling Enterprise EULA. Il régit le contrat généré à partir de ce devis." generate = "Générer le devis" -included = "Inclus" indemnification = "Indemnisation IP" indemnificationSub = "Nous défendons les réclamations IP éligibles, conformément au EULA" -offlineLicense = "Licence hors ligne / air-gapped" -offlineLicenseSub = "Un fichier de licence téléchargeable pour une instance auto-hébergée air-gapped" +paperEyebrow = "Devis Entreprise" +paperFor = "Préparé pour" +pdfSize = "Taille du PDF" +poNumber = "Numéro de bon de commande" +poNumberPlaceholder = "Facultatif" +postalCode = "Code postal" +postalCodePlaceholder = "94105" +posture = "Gouvernance" +posture_count = "~{{count}} stratégies" +posture_essentials = "Essentiels" +posture_essentialsSub = "Classification + Partage — les valeurs par défaut" +posture_governed = "Gouverné" +posture_governedSub = "Ajoute Sécurité et Routage" +posture_regulated = "Réglementé" +posture_regulatedSub = "Chaque catégorie — Conformité, Conservation, Ingestion" qbr = "Revues d’activité trimestrielles" qbrSub = "Votre SE examine l’utilisation et la feuille de route chaque trimestre" +region = "État / région" +regionPlaceholder = "California" running = "{{annual}} / an · {{years}} an(s) {{tcv}}" s1Sub = "Votre équipe et les PDF que vous prévoyez de traiter chaque année." s1Title = "Quel volume allez-vous traiter ?" @@ -7623,6 +8496,12 @@ s2Title = "Engagement et service" s3Sub = "Pour le devis et le contrat qu’il génère." s3Title = "Vos informations" serviceLevel = "Niveau de service" +size_compact = "Compact" +size_compactSub = "Principalement du texte, moins de 1 MB" +size_heavy = "Lourd" +size_heavySub = "Scanné ou riche en images, 5 MB+" +size_standard = "Standard" +size_standardSub = "Texte et images mixtes, 1 à 5 MB" slDedicated = "Dédié" slDedicatedSub = "4 heures ouvrées · gestionnaire de compte dédié · +30 %" slPriority = "Prioritaire" @@ -7630,6 +8509,8 @@ slPrioritySub = "Même jour ouvré · CSM nommé · +15 %" slStandard = "Standard" slStandardSub = "Jour ouvré suivant · CSM partagé · inclus" stepOf = "Étape {{n}} sur {{total}}" +taxId = "N° de TVA / identifiant fiscal" +taxIdPlaceholder = "Facultatif" term = "Durée" termDiscount = "Remise d’engagement pluriannuel de {{pct}} % appliquée" title = "Créez votre devis" @@ -7637,6 +8518,7 @@ training = "Intégration et formation" trainingSub = "Sessions en direct pour lancer votre équipe" users = "Nombre total d’utilisateurs" usersPlaceholder = "p. ex. 250" +viewEula = "Lire" volEstimated = "Estimé à partir de {{count}} utilisateurs (~2 000 PDF chacun, automatisation incluse). Modifiez si vous avez une meilleure estimation." volManual = "Utilisation de votre chiffre. Réestimez à partir de la taille de votre équipe à tout moment." volNoUsers = "Vous n’êtes pas sûr ? Saisissez la taille de votre équipe et nous l’estimerons." @@ -7645,55 +8527,55 @@ volumePlaceholder = "p. ex. 1 000 000" years_one = "{{count}} an" years_other = "{{count}} ans" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Terminé" -here = "Vous êtes ici" -hide = "Masquer" -optional = "Facultatif" -paidAddon = "Module complémentaire payant" -show = "Afficher" -subtitle = "Tout ce dont vous avez besoin à chaque étape du parcours, mis en avant à mesure que l’accord progresse." -supportingSubtitle = "SOC 2, revues de sécurité, formulaires fiscaux et plus encore, prêts lorsque votre équipe sécurité ou achats les demande. Certains entraînent des frais ponctuels." -supportingTitle = "Soutenir votre évaluation" +[portal.procurement.documents] +agreement = "Contrat Entreprise" +agreementSub = "MSA, bon de commande et DPA en une seule signature." +download = "Télécharger" +eula = "EULA et conditions commerciales" +eulaSub = "Les conditions du logiciel et commerciales en libre-service." +invoice = "Facture" +invoiceSub = "Votre première facture d’abonnement." +laterInvoice = "Disponible après votre acceptation" +laterQuote = "Disponible une fois votre devis émis" +quote = "Devis" +quoteSub = "Votre devis Entreprise détaillé." +sla = "Annexe SLA" +slaSub = "Niveaux de support et objectifs de réponse." +subprocessors = "Sous-traitants" +subprocessorsSub = "Tiers qui traitent des données pour votre compte." +subtitle = "Vos documents contractuels, disponibles à tout moment." title = "Documents" -upcoming = "À venir" +view = "Voir" [portal.procurement.error] title = "Une erreur s’est produite" [portal.procurement.hero] -company = "Votre accord Enterprise" +barAria = "Étape {{current}} sur {{total}}" ctaAgreement = "Vérifier et signer le contrat" -ctaLive = "Vous êtes en ligne" -ctaPayment = "Ajouter un paiement" +ctaExploring = "Configurer votre essai" ctaQuote = "Vérifier votre devis" +ctaReviewQuote = "Examiner le devis" ctaTrial = "Créer votre devis" +documents = "Documents" eyebrow = "Achats Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Inviter des coéquipiers" -keyDocs = "Documents clés" -nextStep = "Étape suivante : {{action}}" -notStarted = "Non commencé" +licenseKey = "Clé de licence" +liveSub = "Votre organisation est provisionnée et votre licence est active." +liveTitle = "Vous êtes en production sur Stirling Enterprise" +next = "Suivant : {{stage}}" open = "Ouvrir les achats" scheduleCall = "Planifier un appel" -setup1Sub = "Invitez vos coéquipiers" -setup1Title = "Déployer le PDF Editor" -setup2Sub = "Activez le traitement dans les éditeurs et autres sources" -setup2Title = "Connecter le PDF Processor" -setup3Sub = "Activez Security, Compliance, Routing ou Retention lorsque vous en avez besoin" -setup3Title = "Ajouter les politiques recommandées" +sentenceAgreement = "examiner et signer le contrat" +sentenceLive = "provisionnement et déploiement" +sentencePayment = "bon de commande et paiement" +sentenceQuote = "examiner et accepter votre devis" +sentenceTrial = "configurer votre espace d’essai" [portal.procurement.journey] daysLeft_one = "{{count}} jour restant" daysLeft_other = "{{count}} jours restants" -engineerLabel = "Votre solutions engineer" -eyebrow = "Votre déploiement" -live = "Vous êtes en ligne sur Stirling Enterprise" -nextStep = "Étape suivante : {{action}}" -subtitle = "Votre solutions engineer est présent à chaque étape. Une action suivante à la fois ; la liste complète est ci-dessous." -title = "De l’essai à la mise en ligne, un parcours guidé" -trialTitle = "Essai Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Une seule signature couvre MSA, order form, EULA et DPA." @@ -7727,6 +8609,9 @@ downloadError = "Impossible de générer le fichier de licence hors ligne pour l downloadOffline = "Télécharger la licence hors ligne (.lic)" hint = "Collez cette clé dans une instance auto-hébergée pour l’activer, ou conservez-la pour vos dossiers. Gardez-la en sécurité." label = "Votre clé de licence" +subtitle = "Activez une instance auto-hébergée avec cette clé, ou conservez-la pour vos dossiers." +title = "Votre clé de licence" +trialFileHint = "Ceci est votre fichier de licence d’essai. Une fois votre contrat en place, revenez le télécharger à nouveau — contrairement à la clé de licence en ligne, le fichier .lic ne se mettra pas à jour tout seul." [portal.procurement.link] cta = "Lier le compte" @@ -7739,65 +8624,62 @@ description = "Votre abonnement est actif et votre licence est émise. Votre éq eyebrow = "En ligne" title = "Vous êtes en ligne sur Stirling Enterprise" -[portal.procurement.locked] -description = "Les clés d’essai, les devis à volume engagé, le contrat à signature unique, le paiement et votre registre de documents se trouvent tous ici une fois que vous démarrez une évaluation Enterprise." -eyebrow = "Enterprise uniquement" -talkToSales = "Contacter l’équipe commerciale" -title = "Le parcours d’achats s’ouvre avec Enterprise" - [portal.procurement.milestone] -accept = "Accepter et continuer" -description = "Téléchargez le PDF pour le partager avec votre équipe, revenez pour accepter lorsque vous serez prêt, ou apportez des modifications." -download = "Télécharger le PDF" downloadError = "Impossible de télécharger le PDF du devis pour le moment — veuillez réessayer dans un instant." -edit = "Modifier le devis" -eyebrow = "Devis {{number}}" -perYear = " / an" -preparedFor = "Préparé pour {{company}}" -tcv = "{{value}} valeur totale du contrat" -title = "Votre devis est prêt" - -[portal.procurement.modal] -cancel = "Annuler" -chooseFile = "Choisir un fichier" -close = "Fermer" -downloadBody = "Votre téléchargement va bientôt commencer." -downloadCta = "Télécharger" -downloadTitle = "Télécharger" -noFile = "Aucun fichier sélectionné" -payBody = "Payez votre contrat engagé par carte ou virement bancaire via Stripe. Votre espace de travail est provisionné dès que le paiement est validé." -payCta = "Continuer vers Stripe" -payTitle = "Confirmer le paiement" -requestBodyFree = "Nous générons cela à la demande. Confirmez et votre solutions engineer vous l’enverra sous peu." -requestBodyPaid = "Il s’agit d’un module complémentaire payant. Confirmez et votre solutions engineer en définira le périmètre et enverra les documents." -requestCta = "Demander" -requestTitle = "Demander ce document" -signBody = "Ouvre le Stirling Enterprise Agreement pour signature électronique : une seule signature couvre le MSA, l’order form, le EULA et le DPA. Nous contresignons automatiquement et vous passez au paiement." -signCta = "Ouvrir pour signature" -signTitle = "Vérifier et signer votre contrat" -uploadBody = "Envoyez-nous votre PO et nous le facturons selon vos conditions. Déposez le PDF ou choisissez un fichier ci-dessous." -uploadCta = "Uploader le bon de commande" -uploadTitle = "Uploader votre bon de commande" [portal.procurement.payment] description = "Votre devis est accepté et votre licence est déjà active — votre équipe peut commencer immédiatement. Payez la première facture lorsque vous êtes prêt ; vous pouvez la payer ou la télécharger ici, sans email." +downloadAgreement = "Télécharger le contrat signé" downloadInvoice = "Télécharger la facture" -simulate = "Simuler la réception du paiement (démo)" +eyebrow = "Paiement" title = "Abonnement créé" viewInvoice = "Voir et payer la facture" +[portal.procurement.review] +acceptCta = "Accepter le devis" +annual = "Frais annuels (année 1)" +downloadCta = "Télécharger le devis" +poNumber = "Bon de commande : {{po}}" +renewal = "Renouvellement à {{amount}}/an après la durée (+{{pct}} % IPC)" +tcv = "Durée de {{years}} ans, payée intégralement d’avance : {{tcv}}" +validUntil = "Valable jusqu’au {{date}}" + [portal.procurement.schedule] fallback = "Impossible de charger le planificateur." fallbackLink = "Ouvrir la planification dans un nouvel onglet" +loading = "Chargement du planificateur..." subtitle = "Votre solutions engineer guidera votre équipe tout au long du déploiement. Choisissez un créneau qui vous convient." title = "Planifier un appel" -[portal.procurement.status] -action = "Action requise" -available = "Disponible" -complete = "Terminé" -pending = "En attente" -request = "Sur demande" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Fermé, sans trafic sortant" +back = "Retour" +businessName = "Nom de l’entreprise" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Géré, démarrage le plus rapide" +continue = "Continuer" +deployment = "Où doit-il être déployé ?" +eula = "J’accepte l’EULA et les conditions commerciales de Stirling." +fullName = "Nom complet" +fullNamePlaceholder = "Votre nom" +invites = "Inviter des coéquipiers · facultatif" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Planifier un appel" +seats = "Personnes testant" +seatsPlaceholder = "p. ex. 15" +selfhost = "Auto-hébergé" +selfhostSub = "Docker ou Kubernetes" +start = "Démarrer l’essai" +stepOf = "Étape {{n}} sur {{total}}" +subtitle = "Gratuit pendant 14 jours, aucune carte requise." +subtitleDetails = "Quelques détails pour votre devis et votre contrat." +talkFirst = "Vous préférez d’abord en parler ?" +title = "Configurer votre essai" +viewEula = "Lire" +workEmail = "E-mail professionnel" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "La prolongation ajoute 7 jours et informe votre solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Limite atteinte" subtitle = "Votre essai gratuit est valable jusqu’au {{date}}. Aucune carte requise." title = "Essai Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Tarification au volume engagé, SSO + SCIM + RBAC à l’échelle de l’organisation, audit immuable de 90 jours et SE dédié." -homeCta = "Démarrer l’essai →" -homeHeadline = "Traitez des millions de PDF." - -[portal.recentActivity] -title = "Activité récente" -viewAll = "Voir tout" - -[portal.recentActivity.empty] -description = "Les exécutions de pipeline, les déploiements et les événements d’agent apparaîtront ici." -title = "Rien ici pour le moment" - -[portal.search] -ariaLabel = "Rechercher" -placeholder = "Rechercher dans Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Les actions rapides apparaîtront ici une fois disponibles." -noActionsTitle = "Aucune action rapide" -noMatches = "Aucune correspondance pour \"{{query}}\"" -noMatchesDescription = "Essayez un autre mot-clé ou parcourez le catalogue." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Lien du compte" -[portal.shell.header] -accountFallback = "Compte" -accountMenu = "Menu du compte" -darkMode = "Mode sombre" -lightMode = "Mode clair" -search = "Rechercher" -searchPlaceholder = "Rechercher…" -signOut = "Se déconnecter" -switchToDark = "Passer au thème sombre" -switchToLight = "Passer au thème clair" - [portal.shell.sidebar] appEditor = "Éditeur" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Lier un compte Stirling" -planEditor = "Plan Editor" -planEnterprise = "Plan Enterprise" -planProcessor = "Plan Processor" primaryNav = "Navigation principale" switchApp = "Changer d’application" +[portal.shell.topbar] +closeNav = "Fermer la navigation" +openNav = "Ouvrir la navigation" +search = "Rechercher" + [portal.sources] subtitle = "Connexions d’entrée réutilisables qui fournissent des documents à Stirling. Configurez une connexion une fois, puis référencez-la depuis autant de politiques que nécessaire. Cliquez sur une ligne pour voir sa configuration et les politiques qui l’utilisent." title = "Sources" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Connecter une source" +[portal.sources.builder] +back = "Retour aux sources" +backToTypes = "Tous les types de sources" +cancel = "Annuler" +chooseHint = "Choisissez d’où viennent les documents. Les connecteurs grisés arrivent bientôt." +comingSoon = "Bientôt disponible" +comingSoonHeading = "Bientôt disponible" +create = "Créer une source" +createTitle = "Connecter une source" +delete = "Supprimer" +editTitle = "Modifier la source" +enabled = "Activé" +save = "Enregistrer les modifications" + +[portal.sources.builder.folderAccess] +description = "Les automatisations de dossiers ne peuvent utiliser que les dossiers autorisés par un administrateur. Ajoutez-le dans les paramètres d’accès aux dossiers, puis réessayez." +openSettings = "Paramètres d’accès aux dossiers" +title = "Ce dossier n’est pas autorisé" + [portal.sources.delete] body = "Supprimer \"{{name}}\" ? Cette action est irréversible. Les politiques qui y font référence devront être mises à jour." cancel = "Annuler" confirm = "Supprimer" title = "Supprimer la source ?" -[portal.sources.detail] -closeAriaLabel = "Fermer le détail" -delete = "Supprimer la source" -docs24h = "Dernières 24 h" -docs30d = "30 derniers jours" -docsTotal = "Total vus" -docsTrend = "Documents sur les 30 derniers jours" -documents = "Documents" -edit = "Modifier" -notReferenced = "Référencée par aucune politique, elle peut donc être supprimée en toute sécurité." -pause = "Mettre en pause" -resume = "Reprendre" -subtitle = "{{type}} · {{status}}" -usedBy = "Utilisé par" - -[portal.sources.empty] -description = "Connectez un dossier (et bientôt un stockage cloud) afin que vos politiques disposent d’un emplacement où récupérer des documents." -title = "Aucune source connectée pour le moment" - [portal.sources.kpi] inUse = "En cours d’utilisation" total = "Connexions" unused = "Inutilisées" +[portal.sources.networkFields.connection] +helperText = "La connexion enregistrée avec l’adresse du serveur et les identifiants. Réutilisée par chaque source qui la référence." +label = "Connexion" + +[portal.sources.networkFields.directory] +helperText = "Dossier du serveur à interroger, relatif au dossier personnel de connexion ou à la racine du partage. Laissez vide pour la racine." +label = "Dossier" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consommer supprime chaque fichier du serveur une fois que chaque stratégie l’a traité." +label = "Mode de lecture" + +[portal.sources.networkFields.mode.options] +consume = "Consommer : traiter chaque fichier une seule fois" +snapshot = "Instantané : relire le dossier à chaque exécution" + +[portal.sources.networkFields.recursive] +label = "Profondeur du dossier" + +[portal.sources.networkFields.recursive.options] +all = "Inclure les sous-dossiers" +top = "Niveau supérieur uniquement" + [portal.sources.status] active = "Actif" disabled = "Désactivé" unused = "Inutilisé" [portal.sources.table] +documents = "Documents" source = "Source" status = "Statut" +type = "Type" usedBy = "Politiques" +[portal.sources.types.box] +description = "Récupérez des documents depuis les dossiers Box que votre équipe partage déjà." +label = "Box" + +[portal.sources.types.dropbox] +description = "Récupérez des documents depuis un dossier Dropbox partagé." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documents que votre équipe a traités dans l’éditeur, à travers les exécutions de stratégies et AI." label = "Éditeur" +noPolicies = "Aucune stratégie exécutée depuis l’éditeur pour le moment." + +[portal.sources.types.email] +description = "Transformez les pièces jointes envoyées à une boîte dédiée en documents." +label = "Boîte de réception e-mail" [portal.sources.types.folder] description = "Surveille un répertoire sur le serveur pour les nouveaux documents." @@ -7943,25 +8829,80 @@ label = "Profondeur du dossier" all = "Inclure les sous-dossiers" top = "Niveau supérieur uniquement" +[portal.sources.types.ftp] +description = "Interrogez un dossier de serveur FTP ou FTPS pour détecter de nouveaux documents." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Récupérez des documents depuis des dossiers Google Drive partagés." +label = "Google Drive" + +[portal.sources.types.network] +description = "Surveillez un partage réseau SMB pour détecter de nouveaux documents." +label = "Lecteur réseau" + +[portal.sources.types.onedrive] +description = "Surveillez des dossiers OneDrive pour détecter de nouveaux documents." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Récupérez des documents depuis un bucket Amazon S3 ou compatible S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "La connexion enregistrée contenant le bucket et les identifiants. Réutilisée par chaque source et sortie de pipeline qui la référence." +label = "Connexion" + +[portal.sources.types.s3.fields.mode] +helperText = "Consommer supprime chaque objet du bucket une fois que chaque stratégie l’a traité." +label = "Mode de lecture" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consommer : traiter chaque objet une seule fois" +snapshot = "Instantané : relire le bucket à chaque exécution" + +[portal.sources.types.s3.fields.prefix] +helperText = "Seuls les objets dont les clés commencent par ce préfixe sont traités." +label = "Préfixe de clé" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Interrogez un dossier de dépôt SFTP pour détecter de nouveaux documents." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Récupérez des documents depuis des bibliothèques de documents SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Source" +[portal.sources.types.webhook] +createNote = "Une URL de livraison et un secret de signature sont générés lorsque vous créez ce webhook. Les expéditeurs envoient les documents en POST à cette URL et les stratégies référencées s’exécutent à l’arrivée." +description = "Recevez des documents via HTTP POST signé depuis n’importe quel système externe." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de livraison" +secretNote = "Le secret de signature est affiché une seule fois, lors de la création du webhook. Recréez la source pour le renouveler." + +[portal.sources.types.webhook.reveal] +copy = "Copier" +done = "Terminé" +secret = "Secret de signature" +secretHelp = "Signez le corps brut de chaque livraison avec cette clé (HMAC-SHA256) et envoyez-le comme en-tête X-Stirling-Signature." +secretWarning = "Copiez le secret de signature maintenant. Pour votre sécurité, il n’est affiché qu’une seule fois et ne pourra pas être récupéré ultérieurement." +title = "Webhook créé" +url = "URL de livraison" +usage = "Envoyez chaque document en POST comme corps brut de la requête vers l’URL de livraison avec un type de contenu binaire (application/pdf ou application/octet-stream). Les stratégies référencées s’exécutent automatiquement à l’arrivée." + [portal.sources.wizard] -back = "Retour" -cancel = "Annuler" -continue = "Continuer" -editTitle = "Modifier la source" name = "Nom" namePlaceholder = "p. ex. Réception des demandes" -save = "Enregistrer les modifications" -subtitle = "Étape {{current}} sur {{total}} · {{label}}" -title = "Connecter une source" type = "Type" -[portal.sources.wizard.steps] -chooseType = "Choisir le type" -configure = "Configurer" -review = "Vérifier et connecter" +[portal.stepModal] +close = "Fermer" [portal.usage] managePayment = "Gérer le paiement" @@ -8021,16 +8962,9 @@ summary = "Possède une équipe — gère les ressources de ses membres et les c 2 = "Accès au portail via la politique par défaut" 3 = "Tout ce qu’un Membre peut faire" -[portal.welcome] -ariaLabel = "Bienvenue dans Stirling PDF" -badge = "Open-source" -installEditor = "Installer l’éditeur" -inviteTeammates = "Inviter des coéquipiers" -openInBrowser = "Ouvrir dans le navigateur" -perks = "Gratuit pour toujours · Auto-hébergeable" -subtitle = "L’éditeur PDF le plus sécurisé au monde est gratuit pour les équipes de toutes tailles. Inclut plus de 60 opérations PDF et SSO." -title = "Bienvenue dans" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Illimité" [printFile] title = "Imprimer le fichier" @@ -8613,6 +9547,14 @@ text = "Faites pivoter les pages de votre PDF dans le sens horaire ou antihorair [rotate.tooltip.header] title = "Aperçu des paramètres de rotation" +[ruler] +clearAll = "Tout effacer" +hideAllLabels = "Masquer toutes les étiquettes" +hideSmallLabels = "Masquer les petites étiquettes" +physicalValues = "Physique" +scaled = "À l’échelle" +showAllLabels = "Afficher toutes les étiquettes" + [sanitize] desc = "Supprimer les éléments potentiellement nuisibles des fichiers PDF." sanitizationResults = "Résultats de l’assainissement" @@ -8654,31 +9596,45 @@ label = "Supprimer les métadonnées XMP" [sanitize.steps] settings = "Paramètres" -[sanitizePdf] -tags = "assainir,sécurisé,clean,secure,safe,remove-threats" - [sanitizePDF] title = "Assainir" +[sanitizePdf] +tags = "assainir,sécurisé,clean,secure,safe,remove-threats" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Ajuster la taille ou l'échelle" +[scaleSettings] +activeScale = "Échelle active" +apply = "Appliquer l’échelle" +applyCalibration = "Appliquer l’étalonnage" +calculatedScale = "Échelle calculée" +calibrate = "Étalonner" +calibrating = "Étalonnage" +calibrationDistanceRequired = "Saisissez une distance réelle supérieure à zéro" +calibrationInvalid = "Impossible de calculer l’échelle à partir de cette mesure" +calibrationPaperDistance = "Distance mesurée sur la page : {{distance}}" +calibrationTitle = "Étalonner l’échelle" +calibrationTooltip = "Mesurez une distance connue pour calculer automatiquement l’échelle" +customScale = "Échelle personnalisée" +noneSet = "Aucune échelle personnalisée définie" +presets = "Échelles prédéfinies" +ratio = "Rapport d’échelle" +ratioHelp = "Rapport : 1 unité de page = X unités réelles" +ratioPlaceholder = "p. ex., 100" +ratioPositive = "Le rapport doit être supérieur à zéro" +ratioRequired = "Le rapport est requis" +realDistance = "Distance réelle" +realDistancePlaceholder = "p. ex., 5" +reset = "Réinitialiser aux valeurs par défaut" +unit = "Unité" +unitInvalid = "Unité non valide : {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Seuil de rotation" -10 = "Définit la taille de la bordure ajoutée et supprimée pour éviter les bordures blanches dans la sortie (par défaut : 1)." -2 = "Définit l'angle absolu minimum requis pour la rotation de l'image (par défaut : 10)." -3 = "Tolérance" -4 = "Détermine la plage de variation de couleur autour de la couleur d'arrière-plan estimée (par défaut : 20)." -5 = "Surface minimale" -6 = "Définit la surface minimale pour une photo (par défaut : 8 000)." -7 = "Surface de contour minimale" -8 = "Définit la surface de contour minimale pour une photo (par défaut : 500)." -9 = "Taille de la bordure" - [scannerImageSplit] submit = "Extraire les photos scannées" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Ce que fait cet outil" whatThisDoesDesc = "Détecte et extrait automatiquement chaque photo d’une page scannée ou d’une image composite — sans recadrage manuel." whenToUse = "Quand l’utiliser" +[ScannerImageSplit.selectText] +1 = "Seuil de rotation" +10 = "Définit la taille de la bordure ajoutée et supprimée pour éviter les bordures blanches dans la sortie (par défaut : 1)." +2 = "Définit l'angle absolu minimum requis pour la rotation de l'image (par défaut : 10)." +3 = "Tolérance" +4 = "Détermine la plage de variation de couleur autour de la couleur d'arrière-plan estimée (par défaut : 20)." +5 = "Surface minimale" +6 = "Définit la surface minimale pour une photo (par défaut : 8 000)." +7 = "Surface de contour minimale" +8 = "Définit la surface de contour minimale pour une photo (par défaut : 500)." +9 = "Taille de la bordure" + [search] noResults = "Aucun résultat trouvé" placeholder = "Saisir un terme de recherche…" @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint} [session] expired = "Votre session a expiré. Veuillez recharger la page et réessayer." -[sessionManagement.tooltip] -header = "Gestion des sessions de signature" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Les nouveaux participants sont ajoutés à la fin de l’ordre de signature" -bullet2 = "Impossible d’ajouter des participants après la finalisation de la session" -bullet3 = "Chaque participant reçoit une notification lorsqu’arrive son tour" -description = "Vous pouvez ajouter d’autres participants à une session active à tout moment avant la finalisation." -title = "Ajout de participants" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalisation complète : Tous les participants ont signé" -bullet2 = "Finalisation partielle : Certains participants n’ont pas encore signé" -bullet3 = "Les participants n’ayant pas signé seront exclus du document final" -bullet4 = "Une fois finalisée, vous pouvez charger le PDF signé dans les fichiers actifs" -description = "La finalisation combine toutes les signatures en un seul PDF signé. Cette action est irréversible." -title = "Finalisation de la session" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Impossible de supprimer les participants ayant déjà signé" -bullet2 = "Les participants supprimés ne reçoivent plus de notifications" -bullet3 = "L’ordre de signature s’ajuste automatiquement" -description = "Les participants peuvent être retirés des sessions avant de signer." -title = "Suppression de participants" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Chaque signature est appliquée séquentiellement au PDF" -bullet2 = "Les signataires suivants peuvent voir les signatures précédentes" -bullet3 = "Critique pour les flux d’approbation et les chaînes de conservation légales" -description = "L’ordre que vous spécifiez lors de la création de la session détermine qui signe en premier." -title = "Ordre des signatures" - [settings] close = "Fermer" +[settings.ai] +documents = "Documents et RAG" +general = "Général" +limits = "Limites et performances" +models = "Modèles et fournisseurs" +title = "IA" + [settings.configuration] advanced = "Avancé" database = "Base de données" endpoints = "Endpoints" features = "Fonctionnalités" +folderAccess = "Accès aux dossiers" mcp = "MCP Server" storageSharing = "Stockage et partage de fichiers" systemSettings = "Paramètres système" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Éditeur PDF par défaut" defaultPdfEditorActive = "Stirling PDF est votre éditeur PDF par défaut" defaultPdfEditorChecking = "Vérification…" defaultPdfEditorInactive = "Une autre application est définie par défaut" +defaultPdfEditorRemind = "Me rappeler de définir par défaut" +defaultPdfEditorRemindDescription = "Afficher une bannière lorsque Stirling PDF n’est pas votre application PDF par défaut." defaultPdfEditorSet = "Déjà par défaut" defaultStartupView = "Vue par défaut au démarrage" defaultStartupViewDescription = "Choisissez quel onglet est actif dans la colonne de gauche au démarrage de l'application" @@ -8817,9 +9763,18 @@ languageDescription = "Choisir la langue d’affichage" logout = "Se déconnecter" setAsDefault = "Définir par défaut" theme = "Thème" +themeAccent = "Couleur d’accentuation" +themeAccentCustom = "Couleur personnalisée" +themeAccentCustomHint = "Personnalisée (valeurs par défaut recommandées)" +themeAccentDark = "Couleur d’accentuation du mode sombre" +themeAccentDefault = "Par défaut" +themeAccentDefaultHint = "Thème par défaut (recommandé)" +themeAccentDescription = "Les boutons, liens et surlignages l’utilisent, et elle teinte subtilement l’application. Les modes clair et sombre ont chacun la leur. Le système utilise celle qui est active." +themeAccentLight = "Couleur d’accentuation du mode clair" themeDark = "Sombre" themeDescription = "Basculer entre le mode clair et le mode sombre" themeLight = "Clair" +themeReset = "Restaurer le thème par défaut" themeSystem = "System" title = "Général" user = "Utilisateur" @@ -8833,6 +9788,12 @@ intro = "Activer l’authentification des utilisateurs, la gestion des équipes learnMore = "En savoir plus dans la documentation" title = "Pour les administrateurs système" +[settings.general.loginLanding] +description = "Choisissez quelle application s’ouvre lorsque vous vous connectez à Stirling Cloud." +editor = "Éditeur" +processor = "Processeur" +title = "Après la connexion" + [settings.general.mode] fullscreen = "Plein écran" sidebar = "Barre latérale" @@ -9013,9 +9974,6 @@ title = "Politiques et confidentialité" [settings.preferences] title = "Préférences" -[settings.search] -placeholder = "Rechercher dans les paramètres…" - [settings.security] description = "Mettez à jour votre mot de passe pour sécuriser votre compte." title = "Sécurité" @@ -9041,6 +9999,11 @@ security = "Sécurité" telegram = "Telegram" title = "Sécurité et authentification" +[settings.signOut] +confirm = "Voulez-vous vraiment vous déconnecter ?" +submit = "Se déconnecter" +title = "Déconnexion" + [settings.team] title = "Équipe" @@ -9156,6 +10119,7 @@ last = "Dernière page" maintainRatio = "Conserver les proportions" next = "Page suivante" noSavedSigs = "Aucun sceau enregistré trouvé" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Sceaux personnels" previous = "Page précédente" redo = "Refaire" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Échec de la suppression du fond" hint = "Téléversez une image PNG ou JPG de votre signature" label = "Téléverser une image de signature" placeholder = "Sélectionner un fichier image" +previewAlt = "Current image signature" processing = "Traitement de l’image..." removeBackground = "Supprimer le fond blanc (le rendre transparent)" @@ -9207,6 +10172,17 @@ saved = "Sélectionnez une signature enregistrée ci-dessus, puis cliquez n’im text = "Après avoir saisi votre nom ci-dessus, cliquez n’importe où sur le PDF pour placer votre signature." title = "Comment ajouter une signature" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Déplacer la signature" pause = "Suspendre le placement" @@ -9653,10 +10629,30 @@ title = "Téléverser sur le serveur" updateButton = "Mettre à jour sur le serveur" uploadButton = "Téléverser sur le serveur" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "cliquez ici" +fallback = "Si cela ne s’est pas ouvert automatiquement," +opening = "Ouverture de Swagger UI dans un nouvel onglet..." tags = "api,documentation,swagger,points de terminaison,développement" title = "Documentation de l’API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Sélectionnez des fichiers dans Fichiers actifs pour exécute singleFileScope = "Application uniquement à : {{fileName}}" viewerMode = "Passez à l'éditeur de fichiers pour sélectionner plusieurs fichiers." +[toolFormat] +ANY = "n’importe quel fichier" +CBR = "une archive de bande dessinée (CBR)" +CBZ = "une archive de bande dessinée (CBZ)" +CSV = "un fichier CSV" +EBOOK = "un livre numérique" +EMAIL = "un e-mail" +EXCEL = "une feuille de calcul" +HTML = "une page web" +IMAGE = "des images" +JAVASCRIPT = "un fichier JavaScript" +JSON = "un fichier JSON" +MARKDOWN = "un fichier Markdown" +NONE = "aucun fichier" +PCL = "un fichier d’impression PCL" +PDF = "un PDF" +PDF_ENCRYPTED = "un PDF protégé par mot de passe" +POSTSCRIPT = "un fichier PostScript" +PPT = "une présentation" +TEXT = "un fichier texte" +VIDEO = "une vidéo" +WORD = "un document Word" +XML = "un fichier XML" +XPS = "un document XPS" +ZIP = "une archive ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Retour à tous les outils" collapse = "Réduire le panneau" expand = "Développer le panneau" goBack = "Revenir en arrière" +pdfTools = "Outils PDF" placeholder = "Choisissez un outil pour commencer" premiumFeature = "Fonctionnalité Premium :" toolsHeader = "Outils" @@ -9806,11 +10829,6 @@ noToolsFound = "Aucun outil trouvé" quickAccess = "ACCÈS RAPIDE" searchPlaceholder = "Rechercher des outils…" -[toolPicker.categories] -advancedTools = "Outils avancés" -recommendedTools = "Outils recommandés" -standardTools = "Outils standards" - [toolPicker.subcategories] advancedFormatting = "Mise en forme avancée" automation = "Automatisation" @@ -9824,6 +10842,9 @@ removal = "Suppression" signing = "Signature" verification = "Vérification" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Aucun outil trouvé" noTools = "Aucun outil disponible" @@ -9950,28 +10971,53 @@ title = "Utilisateurs" you = "(vous)" [users.action] +cancelInvite = "Annuler l’invitation" deleteTeam = "Supprimer l’équipe" disableMfa = "Réinitialiser MFA" move = "Déplacer vers une équipe" reinstate = "Réintégrer" remove = "Supprimer de l’organisation" +removeTeam = "Retirer de l’équipe" rename = "Renommer l’équipe" resetPw = "Réinitialiser le mot de passe" suspend = "Suspendre" unlock = "Déverrouiller le compte" +[users.activity] +daysAgo = "il y a {{count}} j" +hoursAgo = "il y a {{count}} h" +justNow = "À l’instant" +minutesAgo = "il y a {{count}} min" +monthsAgo_one = "il y a {{count}} mois" +monthsAgo_other = "il y a {{count}} mois" +never = "Jamais" +weeksAgo_one = "il y a {{count}} semaine" +weeksAgo_other = "il y a {{count}} semaines" +yearsAgo_one = "il y a {{count}} an" +yearsAgo_other = "il y a {{count}} ans" + [users.cap] addProcessor = "+ Processor" approver = "Approuve la politique" editor = "Éditeur" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Annuler l’invitation envoyée à {{email}} ? Cette personne ne pourra pas rejoindre avec le lien actuel." +cancelInviteTitle = "Annuler l’invitation" deleteTeamBody = "Supprimer l’équipe {{name}} ? L’équipe doit d’abord être vide : déplacez ses membres vers une autre équipe, et elle ne doit plus posséder aucune configuration d’intégration." deleteTeamTitle = "Supprimer l’équipe" disableMfaBody = "Supprimer l’inscription MFA de {{name}} ? Elle sera configurée à nouveau lors de la prochaine connexion si nécessaire." disableMfaTitle = "Réinitialiser MFA" removeBody = "Supprimer définitivement {{name}} de l’organisation ? Cette action est irréversible." +removeTeamBody = "Retirer {{name}} de l’équipe ? Cette personne conserve son compte, mais perd l’accès aux ressources de cette équipe." removeTitle = "Supprimer le membre" [users.empty] @@ -9982,10 +11028,8 @@ title = "Aucun membre pour le moment" addToTeam = "Ajouter à l’équipe" guestCount = "{{count}} invité" guests = "Invités" -guestsDesc = "Collaborateurs externes, limités à ce que vous avez partagé. Éditeur uniquement." ledBy = "dirigée par {{owner}}" org = "Organisation" -orgDesc = "Propriétaires avec autorité à l’échelle de l’organisation et approbation des politiques" owners = "{{count}} propriétaire" team = "Équipe {{name}}" teamMeta = "{{count}} personnes" @@ -10022,6 +11066,18 @@ username = "Nom d’utilisateur" usernameError = "Le nom d’utilisateur doit comporter au moins 3 caractères" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invité par {{who}}" +cancel = "Annuler" +expiresInDays_one = "Expire dans {{count}} jour" +expiresInDays_other = "Expire dans {{count}} jours" +expiresToday = "Expire aujourd’hui" +title = "Invitations en attente" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Une erreur s’est produite lors de l’accès au backend, ou vous n’avez pas accès. Réessayez." title = "Impossible de charger les membres" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Rechercher des pièces jointes" title = "Pièces jointes" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Titre du signet" +bookmarkTitleRequired = "Le titre du signet est requis" closeSidebar = "Fermer le panneau latéral des signets" collapseAll = "Réduire tous les signets" +empty = "No bookmarks in this document" expandAll = "Développer tous les signets" +searchPlaceholder = "Rechercher des signets" +title = "Bookmarks" [viewer.comments] addComment = "Ajouter un commentaire" @@ -10236,10 +11297,12 @@ locateAnnotation = "Chercher dans le document" moreActions = "Plus d'actions" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Page {{page}}" placingHint = "Cliquez sur une page pour placer… (annuler)" removeCommentOnly = "Supprimer seulement le commentaire" saveReply = "Sauvegarder la réponse" +searchPlaceholder = "Search comments" title = "Commentaires" typeComment = "Commentaire" typeInsertText = "Insérer du text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Vous avez des modifications non enregistrées" closeSidebar = "Fermer le panneau latéral des calques" hideAll = "Masquer tous les calques" showAll = "Afficher tous les calques" +title = "Layers" [viewer.link] delete = "Supprimer le lien" @@ -10605,30 +11669,6 @@ title = "Texte du filigrane" image = "Image" text = "Texte" -[wetSignature.tooltip] -header = "Méthodes de création de signature" - -[wetSignature.tooltip.draw] -bullet1 = "Personnaliser la couleur et l’épaisseur du trait" -bullet2 = "Effacer et redessiner jusqu’à ce que vous soyez satisfait" -bullet3 = "Fonctionne sur les appareils tactiles (tablettes, téléphones)" -description = "Créez une signature manuscrite à l’aide de votre souris ou de votre écran tactile. Idéal pour des signatures personnelles et authentiques." -title = "Dessiner la signature" - -[wetSignature.tooltip.type] -bullet1 = "Choisir parmi plusieurs polices" -bullet2 = "Personnaliser la taille et la couleur du texte" -bullet3 = "Parfait pour des signatures standardisées" -description = "Générez une signature à partir d’un texte saisi. Rapide et cohérent, adapté aux documents professionnels." -title = "Saisir la signature" - -[wetSignature.tooltip.upload] -bullet1 = "Prend en charge PNG, JPG et autres formats d’image" -bullet2 = "Arrière-plans transparents recommandés pour de meilleurs résultats" -bullet3 = "L’image sera redimensionnée pour s’adapter à la zone de signature" -description = "Téléversez une image de signature préalablement créée. Idéal si vous avez une signature numérisée ou un logo d’entreprise." -title = "Téléverser une image de signature" - [workbenchBar] activeFiles = "Fichiers actifs" annotations = "Annotations" @@ -10642,6 +11682,7 @@ exitRedaction = "Quitter le mode Rédaction" exportAll = "Exporter le PDF" exportSelected = "Exporter les pages sélectionnées" formFill = "Remplir le formulaire" +hideToolbar = "Hide toolbar" multiTool = "Outil multifonction" panMode = "Mode panoramique" print = "Imprimer le PDF" @@ -10652,6 +11693,7 @@ redact = "Caviarder" rotateLeft = "Pivoter à gauche" rotateRight = "Pivoter à droite" ruler = "Règle / Mesure" +rulerSettings = "Paramètres d’échelle" save = "Enregistrer" saveAll = "Tout enregistrer" saveAs = "Enregistrer sous" @@ -10661,6 +11703,7 @@ selectAll = "Tout sélectionner" selectByNumber = "Sélectionner par numéros de pages" selectLanguage = "Sélectionner la langue" share = "Partager" +showToolbar = "Show toolbar" toggleAnnotations = "Basculer la visibilité des annotations" toggleAttachments = "Basculer les pièces jointes" toggleBookmarks = "Basculer les signets" diff --git a/frontend/editor/public/locales/ga-IE/translation.toml b/frontend/editor/public/locales/ga-IE/translation.toml index 24d4de5d37..718b2c593f 100644 --- a/frontend/editor/public/locales/ga-IE/translation.toml +++ b/frontend/editor/public/locales/ga-IE/translation.toml @@ -1,7 +1,6 @@ alphabet = "Aibítir" apply = "Cuir i bhFeidhm" applyAndContinue = "Sábháil & Fág" -areYouSure = "An bhfuil tú cinnte gur mian leat imeacht?" back = "Siar" black = "Dubh" blue = "Gorm" @@ -46,10 +45,8 @@ filesSelected = "comhaid roghnaithe" font = "Cló" fontSizeTooltip = "Méid téacs na huimhreach leathanaigh i bpointí. Cruthaíonn méideanna níos mó téacs níos mó." fontTypeTooltip = "Clannchló don uimhriú leathanaigh. Roghnaigh bunaithe ar stíl do cháipéis." -genericSubmit = "Cuir isteach" green = "Glas" help = "Cabhrú" -imgPrompt = "Roghnaigh Íomhá(í)" incorrectPasswordMessage = "Tá an pasfhocal reatha mícheart." info = "Eolas" invalidUndoData = "Ní féidir a chealú: sonraí oibríochta neamhbhailí" @@ -62,7 +59,6 @@ loginAgreementTitle = "Comhaontú Logála Isteach" logOut = "Logáil amach" marginTooltip = "Fad idir an uimhir leathanaigh agus imeall na leathanaigh." moreOptions = "Tuilleadh Roghanna" -multiPdfPrompt = "Roghnaigh PDFs (2+)" never = "Riamh" no = "Níl" noFileSelected = "Níl aon chomhad roghnaithe. Uaslódáil ceann, le do thoil." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordú Leathanach Saincheaptha (Iontráil liosta uimhreacha le pages = "Leathanaigh" pageSelectionPrompt = "Roghnú Leathanach Saincheaptha (Iontráil liosta leathanach scartha le camóg d'uimhreacha 1,5,6 nó Feidhmeanna ar nós 2n+1):" password = "Pasfhocal" -pdfPrompt = "Roghnaigh PDF(anna)" pin = "Pionnáil Comhad (coinnigh gníomhach tar éis rith na huirlise)" pro = "Pro" processingComplete = "Tá do chomhad réidh." @@ -110,6 +105,7 @@ undoSuccess = "Oibríocht cealaithe go rathúil" unknown = "Anaithnid" unpin = "Díphionnáil Comhad (cuir ina ionad tar éis rith na huirlise)" unsavedChanges = "Tá athruithe gan sábháil agat ar do PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Athruithe Gan Sábháil" unsupported = "Gan tacaíocht" username = "Ainm úsáideora" @@ -356,6 +352,7 @@ text = "Rialaíonn na ceadanna seo cad is féidir le húsáideoirí a dhéanamh title = "Athrú Ceadanna" [addStamp] +preview = "Réamhamharc ar Stampa" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Íoslódálacha láimhe: cliceáil ar na naisc agus cuir na comha noLanguages = "Níor aimsíodh teangacha tessdata sa chomhadlann chumraithe." permissionNotice = "Níl cosán an tessdata inscríofa. Osclófar íoslódálacha sa bhrabhsálaí; sábháil na comhaid .traineddata de láimh sa chomhadlann tessdata le do thoil." +[admin.settings.ai.documents] +description = "Cumraigh an tsamhail leabaithe agus na socruithe aisghabhála a úsáidtear chun ceisteanna faoi dhoiciméid a fhreagairt. Cuirtear i bhfeidhm ar an inneall AI nuair a shábháiltear iad." +title = "Doiciméid & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Fág bán é chun dintiúr timpeallachta an innill féin a úsáid. Baineann sé le himscaradh féin-óstáilte aon-innill." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Sábháilte - fág bán é chun an eochair reatha a choinneáil" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL an chríochphointe leabaithe OpenAI-comhoiriúnach / Ollama, m.sh. http://ollama:11434/v1. Caithfidh sé pointeáil chuig críochphointe inmheánach iontaofa (íogair do SSRF)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Ainm na samhla leabaithe. Téacs saor; leideanna amháin atá sna moltaí." +label = "Samhail leabaithe" + +[admin.settings.ai.documents.embeddingProvider] +description = "Soláthraí a úsáidtear chun téacs doiciméid a thiontú ina leabuithe veicteora." +label = "Soláthraí leabaithe" + +[admin.settings.ai.documents.maxSearches] +description = "Uaslíon cuardaigh aisghabhála is féidir leis an ngníomhaire a rith in aghaidh an iarratais." +label = "Uasmhéid cuardach" + +[admin.settings.ai.documents.reindexNote] +body = "Tagann athrú ar an tsamhail leabaithe i bhfeidhm láithreach, ach caithfear doiciméid a innéacsaíodh leis an tsamhail roimhe seo a ath-innéacsú chun torthaí cearta a fháil sa chuardach." +title = "Ath-innéacsú riachtanach" + +[admin.settings.ai.documents.saved] +reindexNote = "Má d'athraigh tú an tsamhail leabaithe, ath-innéacsaigh na doiciméid atá ann cheana ionas go n-úsáidfidh an cuardach an tsamhail nua." + +[admin.settings.ai.documents.topK] +description = "Líon na míreanna is ábhartha a aisghabhtar in aghaidh an chuardaigh." +label = "Top K" + +[admin.settings.ai.general] +description = "Ceangail Stirling leis an inneall Python AI agus roghnaigh cé na cumais AI atá le nochtadh. Cuirtear athruithe i bhfeidhm ar atosú." +title = "Inneall AI" + +[admin.settings.ai.general.capabilities] +description = "Cas gnéithe AI aonair air nó as. Bíonn gnéithe díchumasaithe i bhfolach san aip." +title = "Cumais" + +[admin.settings.ai.general.enabled] +description = "Príomhlasc. Nuair atá sé as, níl aon uirlisí AI, gníomhairí ná glaonna innill ar fáil." +label = "Cumasaigh AI" + +[admin.settings.ai.general.features.chat] +description = "Cúntóir comhrá chun obair le PDFs." +label = "Cúntóir comhrá" + +[admin.settings.ai.general.features.classify] +description = "Catagóirigh doiciméid go huathoibríoch de réir cineáil nó ábhair." +label = "Aicmiú doiciméad" + +[admin.settings.ai.general.features.createPdf] +description = "Gin PDF nua (m.sh. ó HTML) trí ghníomhaire AI." +label = "Cruthaigh PDF ó leid" + +[admin.settings.ai.general.features.documentQuestions] +description = "Cuir ceisteanna agus faigh freagraí bunaithe ar dhoiciméad uaslódáilte." +label = "Ceisteanna doiciméid" + +[admin.settings.ai.general.features.mathAuditor] +description = "Athbhreithnigh doiciméid le haghaidh earráidí matamaiticiúla agus uimhriúla." +label = "Iniúchóir matamaitice" + +[admin.settings.ai.general.features.pdfComment] +description = "Cuir nótaí athbhreithnithe agus anótálacha scríofa ag AI le PDF." +label = "Gníomhaire tráchta PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Teorainn ama d'oibríochtaí gníomhaire níos troime amhail giniúint doiciméad." +label = "Teorainn ama fhada (soicindí)" + +[admin.settings.ai.general.note] +body = "Ritheann an t-inneall AI mar sheirbhís ar leith. A rún comhroinnte" +body2 = "socraítear é trí athróg timpeallachta coimeádáin. Is féidir API keys soláthraithe a iontráil ar na leathanaigh seo nó a sholáthar mar athróga timpeallachta innill; brúitear eochracha a iontráiltear anseo chuig an inneall nuair a shábháiltear iad." +title = "Maidir leis an inneall AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Teorainn ama do fhreagraí comhrá sruthaithe (token ar token)." +label = "Teorainn ama srutha (soicindí)" + +[admin.settings.ai.general.test] +button = "Tástáil ceangal" +failBody = "Níor fhreagair an t-inneall AI. Seiceáil an URL, go bhfuil coimeádán an innill ag rith, agus go bhfuil AI cumasaithe (tá atosú ag teastáil tar éis cumasaithe)." +failTitle = "Ní féidir an t-inneall AI a bhaint amach" +okBody = "D'fhreagair an t-inneall AI do sheiceáil sláinte." +okTitle = "Is féidir an t-inneall AI a bhaint amach" + +[admin.settings.ai.general.timeoutSeconds] +description = "Teorainn ama d'iarratais chaighdeánacha AI chuig an inneall." +label = "Teorainn ama iarratais (soicindí)" + +[admin.settings.ai.general.url] +description = "URL inmheánach an innill Python AI, m.sh. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Cosaintí maidir leis an méid oibre is féidir le hiarratais AI a dhéanamh agus cé mhéad atá ag rith ag an am céanna. Cuirtear i bhfeidhm ar an inneall AI nuair a shábháiltear iad." +title = "Teorainneacha & Feidhmíocht" + +[admin.settings.ai.limits.maxCharacters] +description = "Cosaint: diúltaigh d'iarratais AI a bhfuil a dtéacs eastósctha níos faide ná seo." +label = "Uasmhéid carachtar in aghaidh an iarratais" + +[admin.settings.ai.limits.maxPages] +description = "Cosaint: diúltaigh d'iarratais AI os cionn an líon seo de leathanaigh PDF." +label = "Uasmhéid leathanach in aghaidh an iarratais" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Uasmhéid glaonna samhla comhuaineacha ar siúl ar fud an innill ar fad." +label = "Uasmhéid comhthráthachta samhla" + +[admin.settings.ai.models] +description = "Roghnaigh an soláthraí LLM agus na samhlacha cliste/tapa a úsáideann an t-inneall AI. Cuirtear i bhfeidhm ar an inneall AI nuair a shábháiltear iad." +title = "Samhlacha & Soláthraithe" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Fág bán é chun dintiúr timpeallachta an innill féin a úsáid. Baineann sé le himscaradh féin-óstáilte aon-innill." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Sábháilte - fág bán é chun an eochair reatha a choinneáil" + +[admin.settings.ai.models.baseUrl] +description = "Base URL an chríochphointe OpenAI-comhoiriúnach / Ollama, m.sh. http://ollama:11434/v1." +label = "Provider base URL" +warning = "Caithfidh an base URL pointeáil chuig críochphointe inmheánach iontaofa. Déanfaidh an t-inneall iarratais taobh an fhreastalaí chuige, mar sin tá luach neamhiontaofa íogair do SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Uasmhéid token aschuir don tsamhail thapa." +label = "Uasmhéid token don tsamhail thapa" + +[admin.settings.ai.models.fastModel] +description = "Samhail níos saoire agus níos tapúla do thascanna éadroma. Téacs saor; leideanna amháin atá sna moltaí." +label = "Samhail thapa" + +[admin.settings.ai.models.provider] +description = "Cén soláthraí LLM a labhraíonn an t-inneall leis." +label = "Soláthraí" + +[admin.settings.ai.models.smartMaxTokens] +description = "Uasmhéid token aschuir don tsamhail chliste." +label = "Uasmhéid token don tsamhail chliste" + +[admin.settings.ai.models.smartModel] +description = "Samhail ardchumais le haghaidh réasúnaíocht chasta. Téacs saor; leideanna amháin atá sna moltaí." +label = "Samhail chliste" + +[admin.settings.ai.saved] +body = "Brúitear athruithe chuig an inneall AI go huathoibríoch." +bodyNoPush = "Sábháladh na socruithe. Cuirfear i bhfeidhm iad an chéad uair eile a phiocfaidh an t-inneall AI suas a chumraíocht." +title = "Sábháladh socruithe AI" + [admin.settings.badge] clickToUpgrade = "Cliceáil chun sonraí an phlean a fheiceáil" @@ -938,6 +1094,25 @@ label = "Athghin ar Thosach" description = "Líon na laethanta a bheidh an deimhniú bailí" label = "Bailíocht an Deimhnithe (laethanta)" +[admin.settings.folderAccess] +description = "Comhadlanna a bhfuil cead ag foinsí fillteáin agus aschuir fillteáin léamh uathu agus scríobh chucu. Is teorainn slándála í seo: ní féidir uathoibrithe a dhíriú riamh ar chonair freastalaí lasmuigh den liosta seo." +securityNote = "Fág folamh é seo chun foinsí agus aschuir fillteáin a dhíchumasú go hiomlán. Bíonn comhadlann chumraíochta Stirling féin i gcónaí as teorainn." +title = "Rochtain Fillteáin" + +[admin.settings.folderAccess.implied] +description = "Ceadaítear na comhadlanna seo atá á mbainistiú ag Stirling i gcónaí agus ní féidir iad a athrú anseo." +serverStorage = "Stóráil comhad freastalaí" +title = "Ceadaithe i gcónaí" +watchedFolder = "Fillteán faire Pipeline" + +[admin.settings.folderAccess.roots] +add = "Cuir leis" +empty = "Níl aon fhillteáin ceadaithe. Tá foinsí agus aschuir fillteáin díchumasaithe faoi láthair." +hint = "Iontráil conairí iomlána, mar shampla /data/inbox." +label = "Fréamhacha fillteáin ceadaithe" +placeholder = "/data/inbox" +remove = "Bain fréamh fillteáin" + [admin.settings.general] description = "Cumraigh socruithe feidhmchláir ar leibhéal an chórais lena n‑áirítear brandaíocht agus iompar réamhshocraithe." system = "Córas" @@ -1704,6 +1879,9 @@ width = "Leithead" [app] description = "An rogha saor in aisce seachas Adobe Acrobat (10M+ íoslódálacha)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Tiontaigh go PDF/A-3b" convertToPdfA3bDescription = "Cruthaíonn sé PDF cartlannaithe le ceangaltáin leabaithe" @@ -1842,6 +2020,15 @@ export = "Easpórtáil" accessDenied = "Rochtain diúltaithe" insufficientPermissions = "Níl cead agat an gníomh seo a dhéanamh." +[auth.callback] +completing = "Fíordheimhniú á chríochnú" +invalidToken = "Theip ar logáil isteach OAuth - token neamhbhailí." +missingToken = "Theip ar logáil isteach OAuth - ní bhfuarthas token." +oauthFailed = "Theip ar logáil isteach OAuth. Bain triail eile as." +pleaseWait = "Fan le do thoil agus muid ag críochnú do shínithe isteach." +signedOut = "Síníodh amach thú. Sínigh isteach arís le do thoil." +windowMayClose = "Is féidir leat an fhuinneog seo a dhúnadh nuair a bheidh sé críochnaithe." + [auth.displayName] guest = "Aoi" user = "Úsáideoir" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Slánaíonn sé doiciméid PDF trí ábhar dainséarach f description = "Athainmneoidh an uirlis seo comhaid PDF go huathoibríoch bunaithe ar a n-ábhar. Déanann sí an doiciméad a anailísiú chun an teideal is oiriúnaí ón téacs a aimsiú." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rothlaigh go hUathoibríoch" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Tairseach muiníne" + +[autoRotate.detectionMode] +auto = "Uathoibríoch" +osd = "OCR amháin" +text = "Téacs amháin" +title = "Modh braite" + +[autoRotate.error] +failed = "Tharla earráid agus an PDF á rothlú go huathoibríoch." + +[autoRotate.inferUndetected] +title = "Rothlaigh leathanaigh le muinín íseal chun iad a mheaitseáil" + +[autoRotate.report] +applied = "{{degrees}}° deiseal" +confidenceValue = "Muinín {{value}}" +noChange = "Gan athrú" +pageLabel = "Leathanach {{number}}" +summary = "Rothlaíodh {{rotated}} as {{total}} leathanach, fágadh {{unchanged}} mar atá siad" +title = "Tuairisc braite" + +[autoRotate.report.method] +inferred = "Meaitseáilte" +none = "Scipeáilte" +osd = "OCR" +text = "Téacs" + +[autoRotate.report.note] +belowThreshold = "Faoi bhun na tairsí" +blankPage = "Tá an leathanach bán" +inferredFromDocument = "Mar an gcéanna leis na leathanaigh eile" +noDominantDirection = "Treonna téacs measctha" +osdFailed = "Níor aimsíodh téacs inléite" +osdNoVerdict = "Níorbh fhéidir a rá cén taobh atá suas" +tesseractUnavailable = "Níl OCR suiteáilte" +tooFewGlyphs = "Ró-bheagán téacs" + +[autoRotate.results] +title = "Torthaí Rothlaithe Uathoibríoch" + +[autoRotate.settings] +title = "Socruithe" + +[autoRotate.tooltip.confidenceThreshold] +text = "Cé chomh cinnte is gá don tseiceáil a bheith sula rothlaítear leathanach. Ísligh é chun níos mó leathanach a rothlú, ardaigh é chun níos mó leathanach a fhágáil gan teagmháil." +title = "Tairseach muiníne" + +[autoRotate.tooltip.description] +text = "Oibríonn sé amach cén taobh suas ba chóir a bheith ar gach leathanach agus rothlaíonn sé sa treo ceart é. Fágtar leathanaigh atá díreach cheana féin mar atá siad." + +[autoRotate.tooltip.detectionMode] +text = "Léann Uathoibríoch an téacs ar an leathanach ar dtús, ansin breathnaíonn sé ar an leathanach mar íomhá mura leor é sin. Scipeálann Téacs amháin an tseiceáil íomhá, arb é sin an ceann is tapúla do dhoiciméid nár scanadh riamh. Déileálann OCR amháin le gach leathanach mar íomhá." +title = "Modh braite" + +[autoRotate.tooltip.header] +title = "Rothlaigh go hUathoibríoch" + +[autoRotate.tooltip.inferUndetected] +text = "Ní féidir leathanaigh bhána nó beagnach bán a sheiceáil leo féin. Nuair a theastaíonn an rothlú céanna ó gach leathanach a d'fhéadfaí a léamh, rothlaítear na leathanaigh seo chun iad a mheaitseáil." +title = "Rothlaigh leathanaigh le muinín íseal chun iad a mheaitseáil" + [autoSizeSplitPDF] tags = "pdf, scoilt, doiciméad, eagraíocht" @@ -2189,33 +2443,6 @@ keywords = "Eochairfhocail: odd, even" numbers = "Uimhreacha/raonta: 5, 10-20" progressions = "Forásanna: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Cineálacha Teastais" - -[certificateChoice.tooltip.organization] -bullet1 = "Á bhainistiú ag riarthóirí an chórais" -bullet2 = "Roinnte i measc úsáideoirí údaraithe" -bullet3 = "Léiríonn sé aitheantas na cuideachta, ní duine aonair" -bullet4 = "Is fearr do: Doiciméid oifigiúla, sínithe foirne" -description = "Teastas roinnte a sholáthraíonn d’eagraíocht. Úsáidtear le haghaidh údarás sínithe ar fud na cuideachta." -title = "Teastas Eagraíochta" - -[certificateChoice.tooltip.personal] -bullet1 = "Ginte go huathoibríoch ag an gcéad úsáid" -bullet2 = "Ceangailte le do chuntas úsáideora" -bullet3 = "Ní féidir a roinnt le húsáideoirí eile" -bullet4 = "Is fearr do: Doiciméid phearsanta, cuntasacht aonair" -description = "Teastas uathghinte uathúil do do chuntas úsáideora. Oiriúnach do shínithe aonair." -title = "Teastas Pearsanta" - -[certificateChoice.tooltip.upload] -bullet1 = "Éilíonn comhad P12/PFX agus focal faire" -bullet2 = "Is féidir le hÚdaráis Teastais sheachtracha é a eisiúint" -bullet3 = "Leibhéal mhuiníne níos airde do dhoiciméid dhlíthiúla" -bullet4 = "Is fearr do: Conarthaí dlíthiúla ceangailteacha, bailíochtú seachtrach" -description = "Úsáid do chomhad teastais PKCS#12 féin. Soláthraíonn sé smacht iomlán ar airíonna an teastais." -title = "Uasluchtaigh P12 Saincheaptha" - [certSign] awaitingSignatures = "Ag feitheamh le sínithe" chooseCertificate = "Roghnaigh Comhad Teastais" @@ -2542,34 +2769,6 @@ server = "Freastalaí" stepTitle = "Foinse an teastais" upload = "Uaslódáil" -[certSign.tooltip.header] -title = "Maidir le Bainistiú Sínithe" - -[certSign.tooltip.overview] -bullet1 = "Seiceáil sínithe atá ann cheana agus a bhailíocht" -bullet2 = "Féach faisnéis mhionsonraithe faoi shínitheoirí agus theastais" -bullet3 = "Cuir sínithe digiteacha nua leis chun do dhoiciméid a dhaingniú" -bullet4 = "Tacaítear le comhaid iolracha le nascleanúint éasca" -text = "Ligeann an uirlis seo duit a sheiceáil an bhfuil do PDFanna sínithe go digiteach agus sínithe digiteacha nua a chur leis. Dearbhaíonn sínithe digiteacha cé a chruthaigh nó a cheadaigh doiciméad agus taispeánann siad an ndearnadh athrú air ó shíníodh é." -title = "Cad is féidir leis an uirlis seo a dhéanamh?" - -[certSign.tooltip.signing] -bullet1 = "Tacaíonn sé le formáidí PEM, PKCS12, JKS agus teastas an fhreastalaí" -bullet2 = "Rogha an síniú a thaispeáint nó a cheilt ar an PDF" -bullet3 = "Cuir cúis, suíomh, agus ainm an tsíní leis" -bullet4 = "Roghnaigh cén leathanach a gcuirfear sínithe infheicthe air" -bullet5 = "Úsáid teastas an fhreastalaí don rogha shimplí 'Sign with Stirling-PDF'" -text = "Chun PDF a shíniú, teastaíonn teastas digiteach uait (mar PEM, PKCS12, nó JKS). Is féidir leat an síniú a dhéanamh infheicthe ar an doiciméad nó é a choinneáil dofheicthe ar mhaithe le slándáil amháin." -title = "Sínithe a Chur Leis" - -[certSign.tooltip.validation] -bullet1 = "Taispeánann sé an bhfuil sínithe bailí nó neamhbhailí" -bullet2 = "Taispeánann sé faisnéis faoin síní agus an dáta sínithe" -bullet3 = "Seiceálann sé an ndearnadh modhnú ar an doiciméad i ndiaidh a shínithe" -bullet4 = "Is féidir teastais shaincheaptha a úsáid le haghaidh fíoraithe" -text = "Nuair a sheiceálann tú sínithe, deir an uirlis leat an bhfuil siad bailí, cé a shínigh an doiciméad, cathain a síníodh é, agus an ndearnadh aon athrú ar an doiciméad ó síníodh é." -title = "Sínithe á Seiceáil" - [changeCreds] changeUsername = "Nuashonraigh d'ainm úsáideora. Logálfar amach thú tar éis an nuashonraithe." credsUpdated = "Cuntas nuashonraithe" @@ -2737,12 +2936,16 @@ text = "Chun na ceadanna seo a dhéanamh dodhéanta a athrú, úsáid an uirlis [chat.actions] copy = "Cóipeáil teachtaireacht" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Roghanna gníomhaire Stirling" +assistant = "AI Assistant" clearChat = "Glan comhrá" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Úsáidfidh an oibríocht seo do chreidmheasanna scamall" tooltip = "Roghnaigh dath ón scáileán" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Roghnaigh dath" [common] +actions = "Actions" back = "Siar" cancel = "Cealaigh" close = "Dún" +codeSample = "Code sample" collapse = "Fill" confirm = "Deimhnigh" continue = "Lean ar aghaidh" copied = "Cóipeáilte!" copy = "Cóipeáil" done = "Déanta" +enterValue = "Enter value" error = "Earráid" expand = "Leathnaigh" loading = "Á lódáil..." next = "Ar Aghaidh" +open = "Open" preview = "Réamhamharc" previous = "Roimhe Seo" refresh = "Athnuaigh" +remaining = "Fágtha" retry = "Bain triail eile as" save = "Sábháil" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Glan roghnaithe" @@ -2988,6 +3199,7 @@ title = "Modh Comhbhrúite" [compress.settings] desiredSize = "Méid comhaid inmhianaithe" desiredSizePlaceholder = "Iontráil an méid" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Is bealach éasca é comhbhrú chun do mhéid comhaid a laghdú. Roghnaigh Méid Comhaid chun spriocmhéid a iontráil agus ligean dúinn an cháilíocht a choigeartú duitse. Roghnaigh Cáilíocht chun neart an chomhbhrú a shocrú de láimh." @@ -3190,6 +3402,24 @@ pdfOptions = "Roghanna PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Roghnaigh formáid foinse ar dtús" settings = "Socruithe" single = "Aonair" @@ -3264,24 +3494,6 @@ description = "Tá na fianáin seo riachtanach chun go bhfeidhmeoidh an suíomh posthog = "Anailísíocht PostHog" scarf = "Scairf Picteilín" -[credits.modal] -advancedMonitoring = "Monatóireacht ardleibhéil" -allInOneWorkspace = "Spás oibre uile-i-aon PDF (amharcán, uirlisí & gníomhaire)" -apiSandbox = "Bosca gainimh API" -dedicatedSupportSlas = "Tacaíocht thiomnaithe & SLAanna" -fasterThroughput = "Ráta próiseála 10x níos tapúla" -fullyPrivateFiles = "Comhaid go hiomlán príobháideacha" -largeFileProcessing = "Próiseáil comhad mór" -orgWideAccess = "Rialuithe rochtana ar fud na heagraíochta" -premiumAiModels = "Samhlacha AI préimhe" -prioritySupport = "Tacaíocht tosaíochta" -privateDocCloud = "Scamall Doiciméad Príobháideach" -ragFineTuning = "RAG + mionchoigeartú" -secureApiAccess = "Rochtain API shlán" -standardThroughput = "Ráta próiseála caighdeánach" -unlimitedApiAccess = "Rochtain API neamhtheoranta" -unlimitedSeats = "Suíocháin neamhtheoranta" - [crop] autoCrop = "Spás bán a bhearradh go huathoibríoch" noFileSelected = "Roghnaigh comhad PDF chun tosú ar bhearradh" @@ -3346,6 +3558,7 @@ message = "Theip ar shocrú láimhseálaí PDF réamhshocraithe" title = "Earráid" [defaultApp.prompt] +dontRemind = "Ná cuir i gcuimhne dom arís" message = "Déan Stirling PDF mar d’fheidhmchlár réamhshocraithe chun comhaid PDF a oscailt." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Cuardaigh..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Cuir in eagar" @@ -3644,11 +3861,13 @@ shareSelected = "Comhroinn Roghnaithe" sharing = "Comhroinnt" showAll = "Taispeáin Uile" showHistory = "Taispeáin Stair" +sortBy = "Sort files" sortByDate = "Sórtáil de réir Dáta" sortByName = "Sórtáil de réir Ainm" sortBySize = "Sórtáil de réir Méid" storage = "Stóráil" storageState = "Stóráil" +storageUsed = "Storage used" synced = "Sioncrónaithe" title = "Uaslódáil Comhaid PDF" toolChain = "Uirlisí a Cuireadh i bhFeidhm" @@ -3694,13 +3913,17 @@ addFiles = "Cuir comhaid leis" addingFiles = "Comhaid á gcur leis…" collapse = "Fill an barra taoibh" customizeGroups = "Saincheap grúpaí" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Oscail comhaid chun tosú" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Leathnaigh an barra taoibh" -files = "Comhaid" googleDrive = "Google Drive" googleDriveDisabled = "Níl Google Drive cumraithe" leaveMyFiles = "Fág Mo Chomhaid" +library = "Leabharlann PDF" myFiles = "Mo Chomhaid" noFiles = "Níl aon chomhaid go fóill" openFileManager = "Oscail bainisteoir comhad" @@ -3708,37 +3931,36 @@ openFromComputer = "Oscail ón ríomhaire" openSettings = "Oscail socruithe" other = "Eile" recent = "Le déanaí" -search = "Cuardaigh" -searchPlaceholder = "Cuardaigh comhaid..." viewAll = "Amharc ar gach {{count}} comhad" [fileSidebar.fileItem] closeViewer = "Dún an t-amharcán" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Oscail san amharcán" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Cuir leis" -addLabel = "Cuir lipéad leis…" -categoryIconAria = "Roghnaigh deilbhín do {{name}}" -createCategory = "Cruthaigh catagóir" -delete = "Scrios catagóir" done = "Déanta" -hide = "Folaigh catagóir" -newCategory = "Ainm catagóire nua…" -removeLabel = "Bain {{name}}" -rename = "Athainmnigh" reset = "Athshocraigh go réamhshocruithe" -search = "Cuardaigh lipéid…" -show = "Taispeáin catagóir" subtitle = "Cuir do chuid comhad i ngrúpaí mar mháthairchatagóirí. Cuir lipéid atá ann cheana nó lipéid nua le catagóir, athainmnigh í, nó cruthaigh do cheann féin. Taispeántar comhaid nach bhfuil in aon cheann de do chatagóirí faoi “Eile”." title = "Catagóirí an bharra taoibh" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Cuir leis an spás oibre" addToWorkspaceCount = "Cuir {{count}} leis an spás oibre" @@ -3749,7 +3971,6 @@ backToMyFiles = "Ar ais go Mo Chomhaid" breadcrumbs = "Conair an fhillteáin" cancel = "Cealaigh" classification = "Aicmiú" -clearSearch = "Glan an cuardach" clearSelection = "Glan an roghnú" closeDetails = "Dún na sonraí" create = "Cruthaigh" @@ -3781,6 +4002,7 @@ downloadAll = "Íoslódáil gach ceann" downloadVersion = "Íoslódáil an leagan seo" dropOverlay = "Scaoil comhaid chun uaslódáil" dropOverlaySub = "Tosaíonn comhaid san Áitiúil. Úsáid 'Bog go' nó 'Sábháil sa scamall' chun iad a eagrú i bhfillteán." +duplicate = "Duplicate" file = "Comhad" fileInfo = "File info" fileMenu = "Gníomhartha comhaid" @@ -3808,13 +4030,12 @@ refresh = "Athnuaigh ón bhfreastalaí" remove = "Scrios" removeVersion = "Bain an leagan seo" rename = "Athainmnigh" +renamed = "Athainmnithe" renameFolder = "Athainmnigh an fillteán" resizeFolderTree = "Athraigh méid chrann na bhfillteán (eochracha saigheada, Shift le haghaidh céimeanna níos mó; déchliceáil le huath-oiriúnú)" save = "Sábháil" saveToServer = "Sábháil chuig an bhfreastalaí" saveToServerDisabledHint = "Níl sábháil chuig an bhfreastalaí cumasaithe ar an bhfreastalaí seo. Iarr ar do riarthóir é a chumasú." -search = "Cuardaigh" -searchPlaceholder = "Cuardaigh an fillteán seo agus fofhillteáin" selectAll = "Roghnaigh gach rud" selectAllHint = "Cliceáil chun gach rud a roghnú. Leid: coinnigh Ctrl (nó Cmd) síos chun comhaid a chur leis ceann ar cheann, Shift chun raon a roghnú." selectedCount = "Roghnaithe: {{count}}" @@ -3874,6 +4095,10 @@ title = "Níl aon chomhaid scamaill fós" hint = "Fanann comhaid a sábháladh gan uaslódáil anseo. Scaoil comhad chun ceann a chur leis." title = "Níl aon chomhad áitiúil amháin" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Beidh comhaid a osclaíonn nó a chuireann tú in eagar le feiceáil anseo." title = "Níor athraíodh aon rud fós" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Níorbh fhéidir an fillteán a scriosadh." deleteFolderFailedDetail = "Níorbh fhéidir an fillteán a scriosadh: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Níorbh fhéidir cuma an fhillteáin a nuashonrú." folderAppearanceFailedDetail = "Níorbh fhéidir cuma an fhillteáin a nuashonrú: {{message}}" moveFilesFailed = "Níorbh fhéidir comhaid a bhogadh." @@ -3910,9 +4137,7 @@ count = "Comhaid" folder = "Fillteán" labels = "Lipéid" modified = "Athraithe" -name = "Ainm" size = "Méid" -toolHistoryAtVersion = "Slabhra uirlisí carntha" totalSize = "Méid iomlán" type = "Cineál" versionHistory = "Turas an leagain" @@ -3945,6 +4170,11 @@ localHint = "Stóráilte sa bhrabhsálaí seo amháin" shared = "Roinnte" sharedHint = "Roinnte leat trí nasc" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Is sine ar dtús" modifiedDesc = "Is nua ar dtús" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Níl comhaid ar fáil sa stóráil. Oscail roinnt comhad open = "Oscail" openFile = "Oscail Comhad" openFiles = "Oscail Comhaid" +selectFile = "Select {{name}}" selectFromStorage = "Roghnaigh ón Stóráil" upload = "Uaslódáil" uploadFile = "Uaslódáil Comhad" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Sábháilte go léir" +analyzingFields = "Analysing form fields..." +extractCsvError = "Theip ar eastóscadh CSV" +extractXlsxError = "Theip ar eastóscadh XLSX" +filled = "filled" flattenAfterFilling = "Cothromaigh tar éis líonadh" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "riach" +requiredFieldsError = "Líon isteach gach réimse riachtanach le do thoil" rescanFields = "Athscan na réimsí" rescanFormFields = "Athscan réimsí an fhoirme" save = "Sábháil" +saveShortcut = "Ctrl+S chun sábháil" +unsavedChanges = "Athruithe gan sábháil" [formFill.sidebar] close = "Dún an barra taoibh" @@ -4309,6 +4552,11 @@ desc = "Athainmníonn comhad PDF go huathoibríoch bunaithe ar a cheannteideal b tags = "uathbhraite,bunaithe ar cheannteideal,eagraigh,athlipéadaigh" title = "Athainmnigh Comhad PDF go hUathoibríoch" +[home.autoRotate] +desc = "Braith treoshuíomh gach leathanaigh agus rothlaigh go díreach é go huathoibríoch." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Rothlaigh go hUathoibríoch" + [home.autoSizeSplitPDF] tags = "uathoibríoch,roinn,méid" title = "Scoilt auto: méid/líon" @@ -4608,15 +4856,12 @@ hint = "Uaslódáil PNG, JPG, SVG, nó comhad íomhá eile le cur ar an PDF. Tio label = "Uaslódáil Íomhá" placeholder = "Roghnaigh comhad íomhá" -[imageToPdf] -tags = "comhshó, img, jpg, pictiúr, grianghraf" - [imageToPDF] header = "Íomhá go PDF" title = "Íomhá go PDF" -[infoBanner] -dismiss = "Díchuir" +[imageToPdf] +tags = "comhshó, img, jpg, pictiúr, grianghraf" [invite] acceptError = "Theip ar chuntas a chruthú" @@ -4647,11 +4892,9 @@ welcomeTitle = "Tugadh cuireadh duit!" [landing] addFiles = "Cuir Comhaid Leis" -heroSubtitle = "Scaoil isteach nó cuir PDF atá ann cheana leis chun tosú." mobileUpload = "Uaslódáil ó Shoghluaiste" openFromComputer = "Oscail ón ríomhaire" uploadFromComputer = "Uaslódáil ón ríomhaire" -workbenchEmptyStateHero = "Scaoil PDF áit ar bith" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Cuir isteach ainm úsáideora" failedToSignIn = "Theip ar shíniú isteach le {{provider}}: {{message}}" forgotPassword = "An ndearna tú dearmad ar d'fhocal faire?" loggingIn = "Ag Logáil Isteach..." -logIn = "Logáil Isteach" login = "Logáil isteach" magicLinkSent = "Seoladh nasc draíochta chuig {{email}}! Seiceáil do ríomhphost agus cliceáil an nasc chun logáil isteach." mfaCode = "Cód Fíordheimhnithe" @@ -4715,26 +4957,10 @@ signInWith = "Logáil isteach le" title = "Sínigh isteach" unexpectedError = "Earráid gan choinne: {{message}}" updatePassword = "Nuashonraigh focal faire" -useEmailInstead = "Logáil isteach le ríomhphost" useMagicLink = "Úsáid nasc draíochta ina ionad" username = "Ainm úsáideora" youAreLoggedIn = "Tá tú logáilte isteach!" -[login.slides.edit] -alt = "Cuir PDFanna in eagar" -subtitle = "Le breis agus dosaen uirlis chun cabhrú leat PDFanna a dhubhú, a shíniú, a léamh agus a ionramháil, gheobhaidh tú go cinnte an rud atá uait." -title = "Cuir PDFanna in eagar chun an t-eolas is mian leat a thaispeáint nó a dhaingniú" - -[login.slides.overview] -alt = "Forbhreathnú Stirling PDF" -subtitle = "Sraith néal-bhunaithe le príobháideachas ar dtús do PDFanna a ligeann duit tiontú, síniú, dubhú agus bainistiú a dhéanamh ar cháipéisí, móide 50+ uirlis chumhachtach eile." -title = "Do shiopa aon-stad do gach riachtanas PDF." - -[login.slides.secure] -alt = "Daingnigh PDFanna" -subtitle = "Cuir focail fhaire leis, dubhaigh ábhar, agus bainistigh teastais go héasca." -title = "Cosain eolas íogair i do PDFanna" - [margin] large = "Mór" medium = "Meánach" @@ -4818,6 +5044,34 @@ uploadSuccess = "Uaslódáil Rathúil!" uploadSuccessMessage = "Tá do chuid íomhánna aistrithe." validating = "Ag bailíochtú an tseisiúin..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan an cód QR seo le do ghléas soghluaiste chun grianghraif a uaslódáil go díreach chuig an leathanach seo." descriptionNoConvert = "Scanáil chun grianghraif a uaslódáil ó do ghléas soghluaiste." @@ -4836,8 +5090,13 @@ title = "Uaslódáil ó Shoghluaiste" tags = "Il-Uirlis, Iloibríocht, Chomhéadain, cliceáil tarraing, ceann tosaigh, taobh an chliaint, idirghníomhach, intractable, bog" title = "Il-uirlis PDF" -[navbar] -search = "Cuardach" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Níor éirigh leis an bhfíordheimhniú. Is féidir leat an fhuinneog seo a dhúnadh agus triail eile a bhaint as." @@ -4966,6 +5225,7 @@ activeFiles = "Taispeánann an radharc Comhaid Ghníomhacha na allTools = "Seo é an painéal Gach Uirlis, áit ar féidir leat brabhsáil agus roghnú astu na huirlisí PDF uile atá ar fáil." close = "Dún" cropSettings = "Anois go bhfuil an comhad roghnaithe atá uainn a bhearradh, is féidir linn an uirlis Bearrtha a chumrú chun an limistéar a roghnú ar mian linn an PDF a bhearradh chuige." +dialogLabel = "Onboarding" fileCheckbox = "Roghnaítear comhad le próiseáil nuair a chliceálann tú air. Is féidir leat ilchomhaid a roghnú le haghaidh oibríochtaí baisc." fileReplacement = "Cuirfear an comhad athraithe in ionad an bhunchomhaid ar an mBinse Oibre go huathoibríoch, rud a fhágfaidh gur féidir leat é a rith trí níos mó uirlisí go héasca." filesButton = "Ligeann an cnaipe Comhaid ar an mBarra Rochtana Thapa duit PDFanna a uaslódáil chun na huirlisí a úsáid orthu." @@ -4976,6 +5236,7 @@ pinButton = "Is féidir leat an cnaipe Pionnáil a úsáid más results = "Tar éis don uirlis rith, taispeánfaidh an chéim Athbhreithnithe réamhamharc de na torthaí sa phainéal seo, agus ligfidh sé duit an oibríocht a chealú nó an comhad a íoslódáil." runButton = "Nuair a bheidh an uirlis cumraithe, ligfidh an cnaipe seo duit an uirlis a rith ar na PDFanna roghnaithe go léir." selectCropTool = "Roghnaímis an uirlis Bearradh chun a léiriú conas ceann de na huirlisí a úsáid." +stepOf = "Céim {{current}} as {{total}}" toolInterface = "Seo comhéadan na huirlise Bearradh. Mar a fheiceann tú, níl mórán ann fós mar nár chuir muid aon chomhaid PDF leis le hoibriú leo go fóill." workbench = "Seo é an Binse Oibre - an príomhlimistéar ina bhfeiceann agus ina n-eagraíonn tú do PDFanna." wrapUp = "Tá gach rud réidh agat! D'fhoghlaim tú faoin bpríomhlimistéir den aip agus conas iad a úsáid. Cliceáil an cnaipe Cabhair aon uair is mian leat an turas seo a fheiceáil arís." @@ -4988,6 +5249,26 @@ showMeAround = "Taispeáin timpeall dom" skipForNow = "Scipeáil faoi láthair" skipTheTour = "Scipeáil an turas" +[onboarding.checklist] +dismiss = "Ruaig" +title = "Socraigh Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Rith Stirling go dúchasach ar do mheaisín" +title = "Íoslódáil Stirling don Deasc" + +[onboarding.checklist.inviteTeam] +description = "Comhoibrigh le do fhoireann" +title = "Tabhair cuireadh do bhaill foirne" + +[onboarding.checklist.shareAnalytics] +description = "Cuidigh le Stirling a fheabhsú" +title = "Comhroinn sonraí úsáide anaithnide" + +[onboarding.checklist.takeTour] +description = "Féach conas a oibríonn Stirling i dtreoir thapa" +title = "Tóg an turas" + [onboarding.desktopInstall] body = "Oibríonn Stirling is fearr mar aip deisce. Is féidir leat é a úsáid as líne, rochtain níos tapúla a fháil ar dhoiciméid, agus eagarthóireacht a dhéanamh go háitiúil ar do ríomhaire." selectOs = "Roghnaigh córas oibriúcháin" @@ -4996,7 +5277,21 @@ titleWithOs = "Íoslódáil do {{osLabel}}" [onboarding.mfa] authenticationCode = "Cód fíordheimhnithe" +description = "Cosain do chuntas trí aip fíordheimhnitheora a nascadh. Scan an QR code nó iontráil an eochair socraithe, ansin dearbhaigh an cód 6 dhigit chun críochnú." +enable = "Cumasaigh MFA" +enableError = "Ní féidir fíordheimhniú dhá fhachtóir a chumasú. Seiceáil an cód agus bain triail eile as." +enterCode = "Iontráil an cód 6 dhigit ó d'aip." +enterCodeError = "Iontráil an cód fíordheimhnithe chun leanúint ar aghaidh." +logout = "Logáil amach" +openAuthenticator = "Oscail Google Authenticator, Authy, nó 1Password." qrCodeLoading = "Cód QR á ghiniúint…" +regenerateQrCode = "Athghin QR code" +scanQrCode = "Scan an QR code nó iontráil an eochair socraithe thíos." +setupError = "Ní féidir socrú dhá fhachtóir a thosú. Bain triail eile as le do thoil." +setupKey = "Eochair socraithe (iontráil láimhe)" +stepByStep = "Céim ar chéim" +success = "Cumasaíodh MFA. Is féidir leat leanúint ar aghaidh anois." +title = "Socrú Fíordheimhnithe Ilfhachtóra" [onboarding.planOverview] adminBodyLoginDisabled = "Nuair a chumasaíonn tú mód logála isteach, is féidir leat úsáideoirí a bhainistiú, socruithe a chumrú, agus sláinte an fhreastalaí a mheas. Faigheann an chéad {{freeTierLimit}} duine ar do fhreastalaí Stirling saor in aisce." @@ -5005,6 +5300,11 @@ adminTitle = "Forbhreathnú Riarthóra" userBody = "Tabhair cuireadh do chomhoibrithe, sann róil, agus coinnigh do dhoiciméid eagraithe in aon spás oibre slán amháin. Cumasaigh mód logála isteach aon uair is mian leat dul thar úsáid aonair." userTitle = "Forbhreathnú Plean" +[onboarding.processorIntro] +body = "Ritheann Stirling Polasaithe anois — rialacha uathoibrithe a aicmíonn, a dhaingníonn agus a phróiseálann gach doiciméad de réir mar a thagann sé isteach. Socraigh iad agus déan monatóireacht ar rith sa Processor." +cta = "Féach ar an Processor" +title = "Féach ar an Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Bliain réamhíoctha" + +[payg.prepaid.banner] +exhausted = "Tá do chumas réamhíoctha ídithe; tá billeáil mhéadraithe atosaithe. Breisigh chun an lascaine a choinneáil." +expiring_one = "Críochnaíonn do bhliain réamhíoctha i gceann {{count}} lá. Rachaidh cumas neamhúsáidte in éag — breisigh chun bliain úr a thosú." +expiring_other = "Críochnaíonn do bhliain réamhíoctha i gceann {{count}} lá. Rachaidh cumas neamhúsáidte in éag — breisigh chun bliain úr a thosú." +low = "Tá tú síos go {{pct}}% de do chumas réamhíoctha. Breisigh am ar bith chun próiseáil gan bhriseadh a choinneáil." + +[payg.prepaid.card] +subtitle = "Úsáidtear roimh bhilleáil mhéadraithe agus lasmuigh de do theorainn chaiteachais." +title = "Cumas réamhíoctha" + +[payg.prepaid.meter] +capSuffix = "as {{total}} creidmheas réamhíoctha" +expires = "Éagann {{date}}" + +[payg.prepaid.state] +exhausted = "Ídithe" +healthy = "Neart fágtha" +low = "Ag ísliú" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF Go PDF/A" tags = "cartlann, fadtéarmach, caighdeánach, comhshó, stóráil, caomhnú" title = "PDF Go PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "priontáil,caighdeán,tiontú,táirgeadh,réamhphriontáil,cartlann" title = "PDF go PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Creidiúintí" xsmall = "100 Creidiúintí" [plan.availablePlans] +currency = "Billing currency" subtitle = "Roghnaigh an plean a oireann do do riachtanais" title = "Pleananna atá ar fáil" @@ -5937,49 +6267,12 @@ keyDescription = "Greamaigh an eochair cheadúnais ó do ríomhphost" success = "Ceadúnas Gníomhachtaithe!" successMessage = "Tá do cheadúnas gníomhachtaithe go rathúil. Is féidir leat an fhuinneog seo a dhúnadh anois." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "curtha i bhfeidhm" -enforcing = "Á chur i bhfeidhm..." -failed = "Theip ar an bhforfheidhmiú" outputsUnavailable = "Níl aschuir na beartais ar fáil le híoslódáil a thuilleadh." partialOutputsUnavailable = "Níl cuid d'aschuir na beartais ar fáil le híoslódáil a thuilleadh." -retrying = "Gnóthach, ag iarraidh arís..." runNotFound = "Ní raibh an rith forfheidhmithe le fáil a thuilleadh." -step = "céim {{current}}/{{total}}" timedOut = "Chuaigh an forfheidhmiú thar am sular fhéad an rith críochnú." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Dún" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Á chur {{names}} i bhfeidhm" applyingProgress = "Á chur {{names}} i bhfeidhm ({{done}} as {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "Níor próiseáladh {{failures}} as {{total}} comhad agus easpór failureTitle = "Easpórtáladh gan fhorfheidhmiú iomlán" printPolicyAppliedBody = "Nuashonraíodh an PDF seo chun beartas a chomhlíonadh. Athbhreithnigh na hathruithe, ansin priontáil arís." printPolicyAppliedTitle = "Beartas curtha i bhfeidhm roimh phriontáil" -queued = "+{{count}} sa scuaine" successTitle = "Cuireadh {{names}} i bhfeidhm" summaryMore = "{{first}}, {{second}} agus {{more}} eile" summaryTwo = "{{first}} agus {{second}}" -[policies.enforcement.triggerVerb] -convert = "Forfheidhmiú roimh thiontú" -default = "Forfheidhmiú" -export = "Forfheidhmiú roimh easpórtáil" -input = "Forfheidhmiú ar iompórtáil" -print = "Forfheidhmiú roimh phriontáil" - -[policies.field] -accessLog = "Loga rochtana" -archiveAfter = "Cartlannaigh tar éis" -auditTrail = "Rian iniúchta" -belowThreshold = "Faoi bhun na tairsí" -destination = "Ceann scríbe" -frameworks = "Creataí" -immutableHold = "Coinneáil dho-athraithe" -keepFor = "Coinnigh ar feadh" -minConfidence = "Muinín íosta" -notify = "Cuir ar an eolas ar ródú" -onViolation = "Nuair nach bhfuil sé comhlíontach" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Cuir leis" -addPlaceholder = "Cuir lipéad leis…" -customNote = "Saincheaptha do d’fhoireann." -defaultNote = "Ag úsáid an réamhshocraithe ionsuite, roinnte le d’fhoireann." -duplicate = "Tá \"{{name}}\" ann cheana." -edit = "Cuir lipéid in eagar" -empty = "Níl aon lipéid fós." -export = "Easpórtáil JSON" -iconAria = "Roghnaigh deilbhín do {{name}}" -import = "Iompórtáil JSON" -importError = "Níorbh fhéidir an comhad sin a iompórtáil." -managedNote = "Bainistíonn ceannaire d’fhoirne lipéid foirne." -modalSubtitle = "Roinnte le d’fhoireann iomlán. Roghnaíonn an t-aicmitheoir na lipéid a oireann do gach doiciméad." -modalTitle = "Lipéid aicmithe" -pickIcon = "Úsáid deilbhín {{label}}" -removeAria = "Bain {{name}}" -resetToDefault = "Athshocraigh go réamhshocrú" -saveForTeam = "Sábháil don fhoireann" -saving = "Á shábháil…" -sectionLabel = "Lipéid aicmithe" -startFromScratch = "Tosaigh ón tús" -teamCount = "lipéid foirne" -tooLong = "Ní féidir le lipéid a bheith níos faide ná {{max}} carachtar." -ungrouped = "Gan ghrúpáil" -view = "Amharc ar lipéid" +categoryCount = "catagóirí" +hideCategory = "Folaigh catagóir" +labelCount = "lipéid" +sharedNote = "Tá na lipéid seo ionsuite agus comhroinnte ar fud d'fhoirne ar fad." +showCategory = "Taispeáin catagóir" [policies.pii] account = "Uimhreacha cuntais (lipéadaithe)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Uimhreacha ródaithe SAM (ABA)" ssn = "Uimhreacha Slándála Sóisialta" -[policies.settings] -noneExport = "Níl aon pholasaithe ag rith ar easpórtáil faoi láthair." -noneUpload = "Níl aon pholasaithe ag rith ar uaslódáil faoi láthair." -onExport = "Ar easpórtáil" -onUpload = "Ar uaslódáil" -reorderHandle = "Tarraing chun athordú" -runOrderDesc = "Nuair a ritheann níos mó ná polasaí amháin ar an truicear céanna, ritheann siad san ord seo — gach ceann ar aschur an pholasaí roimhe. Tarraing chun athordú." -title = "Socruithe polasaí" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Roghanna polasaí" -policySettings = "Socruithe polasaí" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Atriail polasaithe ar theip orthu ({{count}})" -rowProgress = "{{completed}} as {{total}} comhad próiseáilte" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Cad is polasaí ann?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Aimsíonn agus dubháilann sé sonraí íogaire go huathoibríoch — amhail uimhreacha Slándála Sóisialta agus uimhreacha cárta — ionas nach féidir iad a léamh sa cháipéis." -sanitize = "Baineann sé JavaScript folaithe den chomhad, ionas nach rithfidh aon rud go huathoibríoch nuair a osclaíonn duine é." -watermark = "Stampálann sé marc infheicthe (m.sh. \"Confidential\") trasna gach leathanaigh." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Dún" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} á chur i bhfeidhm..." badgeRan = "Rith polasaí {{name}} ar an gcomhad seo" +badgeRunning = "{{name}} ag rith..." blockingAction = "Cuireadh bac ar {{action}} agus polasaí á chur i bhfeidhm, fan le do thoil..." dismiss = "Dún forleagan" enforcingTitle = "Polasaí á chur i bhfeidhm..." @@ -6151,6 +6316,7 @@ viewAnyway = "Amharc ar chomhad (polasaí fós á chur i bhfeidhm)" [policyOption] autoRedactPhi = "Ceil PHI go huathoibríoch" blockExport = "Blocáil easpórtáil" +ccpa = "CCPA" contracts = "Conarthaí" documents = "Doiciméid" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plean Editor" subscribed = "Plean Processor" unlinked = "Gan nascadh" -[portal.agentBuilder] -bootstrapFromDocument = "Tosaigh ó dhoiciméad" -sectionsAriaLabel = "Rannáin thógálaí gníomhairí" -selectorAriaLabel = "Gníomhairí" -subtitle = "Dear, tástáil agus seol na gníomhairí AI a dhéanann do dhoiciméid a aicmiú, sonraí a bhaint astu agus iad a ródú. Sainigh cásanna, cuir teorainneacha ar rochtain uirlisí, rith tacar órga, agus foilsigh leagan." -title = "Tógálaí Gníomhairí" - -[portal.agentBuilder.bootstrap] -cancel = "Cealaigh" -dropzoneText = "Roghnaigh doiciméad samplach (PDF nó íomhá)" -lead = "Scaoil doiciméad samplach agus molfaidh muid cásanna agus scéimre eastósctha is féidir leat a bheachtú. Ní fhoilsítear aon rud go dtí go ndéanann tú athbhreithniú air." -submit = "Tosaigh gníomhaire" -subtitle = "Cuir tús le gníomhaire nua ó chomhad ionadaíoch amháin" -title = "Tosaigh ó dhoiciméad" - -[portal.agentBuilder.empty] -description = "Tosaigh gníomhaire ó dhoiciméad samplach chun a chásanna agus a scéimre eastósctha a shíolú, ansin é a bheachtú agus a fhoilsiú." -title = "Níl aon ghníomhairí fós" - -[portal.agentBuilder.evals] -casesPassing = "Cásanna ag pasáil" -columnCase = "Cás measúnaithe" -columnLatency = "Moill" -columnResult = "Toradh" -fail = "teip" -goldenSetPassRate = "Ráta pasála an tacair órga" -latencyMs = "{{ms}} ms" -notRun = "níor rith" -pass = "pas" -passRate = "Ráta pasála" -runEvals = "Rith measúnuithe" - -[portal.agentBuilder.evals.empty] -description = "Athraíonn measúnuithe do chásanna ina dtacar órga in-athdhéanta. Uasghrádaigh chun ráta pasála a ghabháil le himeacht ama agus foilseacháin a ghlasáil air." -title = "Níl aon tacar órga fós" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "thar thacair órga" -activeAgents = "Gníomhairí gníomhacha" -avgPassRate = "Meánráta pasála measúnaithe" -fleetWide = "ar fud an chabhlaigh" -latestPublished = "Foilsithe is déanaí" -scenarios = "Cásanna" -testCases = "cásanna tástála" -totalDescription_one = "{{count}} san iomlán" -totalDescription_other = "{{count}} san iomlán" - -[portal.agentBuilder.scenarios] -add = "Cuir leis" -addScenario = "Cuir cás leis" -enable = "Cumasaigh" -expectationLabel = "Iompar a bhfuiltear ag súil leis" -expectationPlaceholder = "Cad ba cheart don ghníomhaire a dhéanamh" -inEval = "i measúnú" -mute = "Balbhaigh" -muted = "balbhaithe" -nameLabel = "Ainm" -namePlaceholder = "m.sh. Ardú comhlíontachta" - -[portal.agentBuilder.tabs] -evals = "Measúnuithe" -scenarios = "Cásanna" -tools = "Uirlisí" -versions = "Leaganacha" - -[portal.agentBuilder.tools] -broadAccess = "Rochtain leathan" -deniedHint = "Tá na huirlisí roghnaithe blocáilte. Fanann gach rud eile inghlaoite." -deniedTools = "Uirlisí diúltaithe" -governanceGate = "Tá rialachas uirlisí ar fáil ar an bplean Enterprise." -restricted = "Srianta" -restrictedAccess = "Rochtain srianta ar uirlisí" -restrictedDescription = "Ceadaigh gach uirlis seachas na cinn a dhiúltaíonn tú thíos." - -[portal.agentBuilder.versions] -current = "reatha" -historyGate = "Tá stair iomlán leaganacha agus filleadh ar leagan ar fáil ar an bplean Enterprise." -publish = "Foilsigh" -rollBack = "Fill ar ais" - [portal.assistant] close = "Dún cúntóir" error = "Níorbh fhéidir an cúntóir a bhaint amach." @@ -6365,9 +6451,21 @@ typing = "Ag clóscríobh" loading = "Á luchtú" redirectingToEditor = "Á atreorú chuig an eagarthóir..." +[portal.billing.activation] +maybeLater = "B'fhéidir níos déanaí" +title = "Cuir an Processor ar siúl" + +[portal.billing.activation.payg] +sub = "Socraigh teorainn mhíosúil. Íoc as an méid a ritheann tú." +title = "Íoc de réir mar a úsáideann tú" + +[portal.billing.activation.prepay] +badge = "2 mhí saor in aisce" +sub = "Sonrasc amháin, cárta nó aistriú bainc." +title = "Réamhíoc bliain" + [portal.billing.checkout] noClientSecret = "D’fhill Edge function gan client_secret." -subtitle = "Cuir cárta leis chun leanúint ar aghaidh thar do dheontas saor in aisce ar phlean Editor. Láimhseálann Stripe an chuid eile." title = "Cuir an plean Processor ar siúl" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "D’éirigh le d’íocaíocht, ach tá an gníomhachtú ag glacadh beag close = "Dún" title = "Beagnach ann" +[portal.billing.checkout.cap] +amountAria = "Teorainn chaiteachais mhíosúil" +back = "Siar" +continue = "Lean ar aghaidh chuig íocaíocht" +error = "Níorbh fhéidir do theorainn chaiteachais a shocrú" +finePrint = "Eisítear sonraisc go míosúil. Cealaigh am ar bith." +noLimit = "Gan teorainn" +note = "Ní ghearrtar bille ort riamh thar do theorainn. Ní dhéanann an phróiseáil ach sos." +perMonth = "/ mí" +title = "Socraigh do theorainn chaiteachais" + +[portal.billing.checkout.card] +fields = "Uimhir chárta · MM / YY · CVC · ZIP" +label = "Sonraí cárta" +note = "Bailíonn Stripe sonraí cárta. Ní stórálann Stirling PAN ná CVC riamh." + [portal.billing.checkout.error] title = "Níorbh fhéidir checkout a thosú" @@ -6383,10 +6497,9 @@ body = "Chuaigh d’íocaíocht tríd. Táimid ag cur próiseáil mhéadraithe a hint = "Coinnigh an fhuinneog seo oscailte, le do thoil." title = "Do phlean Processor á ghníomhachtú..." -[portal.billing.checkout.notConfigured] -bodyAfter = "i portal env chun checkout in-app a chumasú." -bodyBefore = "Socraigh" -title = "Níl Stripe cumraithe" +[portal.billing.checkout.pay] +limit = "Teorainn chaiteachais mhíosúil {{amount}}/mí. Stadann próiseáil ag an teorainn." +limitNoCap = "Gan teorainn chaiteachais. Ní stadfaidh próiseáil. Eisítear sonraisc go míosúil." [portal.billing.enterpriseUpsell] cta = "Tóg do luachan Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plean reatha" freeForever = "Saor in aisce go deo" noTeamResolved = "Níl aon fhoireann réitithe ar do sparán fós — athnuaigh agus bain triail eile as." ownerOnly = "Ní féidir ach le húinéir na foirne an plean Processor a chur ar siúl." +payInvoice = "Íoc sonrasc chun críochnú" planName = "Editor" ssoIncluded = "SSO san áireamh" switchOnProcessor = "Cuir Processor ar siúl →" unlimitedUsers = "Úsáideoirí gan teorainn" +viewQuote = "Féach ar luachan" [portal.billing.invoices] columnAmount = "Méid" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Iarratais dhíreacha API" segmentApiLabel = "API" segmentAutomationDesc = "Uathoibrithe & píblínte" segmentAutomationLabel = "Uathoibriú" +sizeMultiplier = "D'úsáid {{formatted}} PDFs iolraitheoir méide" +summary = "{{unique}} uathúil · {{units}} aonad méadair · {{avg}} meán in aghaidh an PDF" +summaryNoRate = "{{unique}} uathúil · {{units}} aonad méadair" unit = "PDFs méadraithe" +unitsPending_one = "{{units}} aonad méadair ag fanacht le sioncronú ó shamplaí nasctha" +unitsPending_other = "{{units}} aonad méadair ag fanacht le sioncronú ó shamplaí nasctha" + +[portal.billing.prepaid] +capSuffix = "as {{total}} creidmheas réamhíoctha" +expires = "Éagann {{date}}" +eyebrow = "Cumas réamhíoctha" +note = "Úsáidtear roimh bhilleáil mhéadraithe agus lasmuigh de do theorainn chaiteachais." +topUp = "Breisigh" + +[portal.billing.prepaid.buy] +back = "Siar" +calcTitle = "Ríomh d'íocaíocht bhliantúil" +cancel = "Cealaigh" +cancelPurchase = "Cealaigh ceannach" +close = "Dún" +continueToPayment = "Lean ar aghaidh chuig íocaíocht" +finalise = "Críochnaigh" +invoiceErrorTitle = "Níorbh fhéidir an sonrasc a ghiniúint" +loadingTitle = "Do luachan á luchtú" +payTitle = "Íoc as do bhliain" +step = "Céim {{current}} as {{total}}" + +[portal.billing.prepaid.calc] +change = "Athraigh" +deployRow = "Imscaradh" +done = "Déanta" +enterpriseHint = "Is scála fiontraíochta é seo. Ag an méid seo, sáraíonn rátaí fiontraíochta aon lascaine féinseirbhíse. Glasáil ráta, téarmaí, agus luachan i gceann cúpla nóiméad." +governanceRow = "Rialachas" +governanceValue = "{{label}} · {{count}} polasaí" +handlesLabel = "Do Processor" +handlesValue = "láimhseálann {{volume}} creidmheas / mí" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} polasaí" +poolCaption = "Linn amháin de {{credits}} creidmheas don bhliain. Faigheann míonna troma iasacht ó mhíonna éadroma." +priceLabel = "Do bhliain · 12 mhí ar phraghas 10" +rateUnknown = "Taispeánfaimid an praghas cruinn ag an tseiceáil amach." +savings = "Sábhálann tú {{amount}} · 2 mhí saor in aisce." +sizingRow = "Méidiú" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Úsáideoirí iomlána" + +[portal.billing.prepaid.consent] +label = "Tuigim nuair a ídítear mo chumas réamhíoctha nó nuair a théann sé in éag tar éis 12 mhí, go leanann próiseáil ar aghaidh go huathoibríoch ag an ráta caighdeánach méadraithe íoc-mar-a-úsáideann-tú (suas go dtí mo theorainn chaiteachais) mura gcealaím, agus gur féidir liom cealú am ar bith ón tairseach billeála." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Bainistithe ag Stirling. Beo i gceann cúpla nóiméad." +cloudValue = "Stirling Cloud · Bainistithe" +selfhost = "Féin-óstáilte" +selfhostDesc = "Ritheann ar bhonneagar príobháideach. An ráta céanna." +selfhostValue = "Féin-óstáilte · Bonneagar príobháideach" + +[portal.billing.prepaid.offer] +cta = "Athbhreithnigh tairiscint" +subtitle = "Réamhíoc bliain de phróiseáil PDF agus faigh dhá mhí saor in aisce, úsáidte roimh bhilleáil mhéadraithe, lasmuigh de do theorainn chaiteachais." +title = "Faigh 12 mhí ar phraghas 10" + +[portal.billing.prepaid.pay] +accountName = "Ainm shealbhóir an chuntais" +accountNamePlaceholder = "m.sh. Jane Smith" +companyName = "Ainm cuideachta (roghnach)" +companyNamePlaceholder = "m.sh. Acme Inc." +downloadInvoice = "Íoslódáil sonrasc" +payOnline = "Íoc ar líne" +poNumber = "Uimhir PO (roghnach)" +poNumberPlaceholder = "m.sh. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Ardleibhéal" +advancedDesc = "Tiomáineann Pipelines an chuid is mó de do phróiseáil." +none = "Ceann ar bith" +noneDesc = "Níl Pipelines á rith fós. Cuir ar siúl iad am ar bith." +standard = "Caighdeánach" +standardDesc = "Athphróiseálann cúpla Pipeline PDFs a thagann isteach." + +[portal.billing.prepaid.posture] +essentials = "Bunriachtanais" +essentialsDesc = "Aicmiú, an réamhshocrú, móide Comhroinnt" +governed = "Rialaithe" +governedDesc = "Cuireann Slándáil agus Ródú leis" +regulated = "Rialaithe go docht" +regulatedDesc = "Gach catagóir, lena n-áirítear Comhlíonadh, Coinneáil, Ionghabháil" + +[portal.billing.prepaid.review] +download = "Íoslódáil luachan (PDF)" +downloading = "Á ullmhú…" +share = "comhroinn lena cheadú" + +[portal.billing.prepaid.size] +compact = "Dlúth" +compactDesc = "Comhaid faoi 25 MB, gan táillí sonraí" +heavy = "Trom" +heavyDesc = "Scantha nó lán d'íomhánna, 50 MB+ go rialta" +standard = "Caighdeánach" +standardDesc = "Beag den chuid is mó, roinnt scananna thar 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Ídithe" +healthy = "Neart fágtha" +low = "Ag ísliú" [portal.billing.spendLimit] adjustLimit = "Coigeartaigh teorainn" @@ -6494,6 +6713,8 @@ label = "Réamh-mheasta go sárófar í." [portal.billing.spendThisMonth] eyebrow = "Caiteachas an mhí seo" +freeRemaining_one = "{{formatted}} PDF saor in aisce fágtha" +freeRemaining_other = "{{formatted}} PDFs saor in aisce fágtha" processed_one = "{{formattedCount}} PDF próiseáilte." processed_other = "{{formattedCount}} PDFs próiseáilte." processedWithRate_one = "{{formattedCount}} PDF próiseáilte, ar {{rate}} an ceann." @@ -6510,6 +6731,7 @@ reachedTitle = "Sroicheadh teorainn chaiteachais mhíosúil" title = "Níorbh fhéidir portal Stripe a oscailt" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "as {{allowance}} PDF saor in aisce úsáidte" capSuffix_other = "as {{allowance}} PDFs saor in aisce úsáidte" eyebrow = "Triail Processor" @@ -6534,6 +6756,376 @@ signature = "síniú" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Ceangal nua" +createTitleFor = "Ceangail {{name}}" +delete = "Scrios" +edit = "Cuir in eagar" +editTitle = "Cuir ceangal in eagar" +unsupportedType = "Ní féidir an cineál ceangail seo a chur in eagar anseo." + +[portal.connections.categories.advanced] +description = "Do sheirbhís nach dtacaímid léi fós." +label = "Ardleibhéal" + +[portal.connections.categories.audit] +description = "Taifead cad a tharla do dhoiciméad, san áit a mbreathnaíonn d'iniúchóirí cheana féin." +label = "Logáil iniúchta & comhlíonta" + +[portal.connections.categories.notify] +description = "Inis do chóras eile gur tharla rud éigin." +label = "Fógra & sreabhadh oibre" + +[portal.connections.categories.security] +description = "Scan nó lipéadaigh doiciméad sula mbogann sé ar aghaidh." +label = "Slándáil & aicmiú" + +[portal.connections.categories.signing] +description = "Seol doiciméad amach le síniú nó le nótárú." +label = "Ríomhshíniú & nótárú" + +[portal.connections.categories.storage] +description = "Áit a gcríochnaíonn doiciméid phróiseáilte - buicéad, tiomántán, ticéad nó leathanach." +label = "Comhad & ceangail" + +[portal.connections.commonFields.accessToken] +helperText = "Stórálann Stirling é seo criptithe agus ní thaispeánann sé arís é riamh." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Ní féidir le céimeanna a úsáideann an ceangal seo ach conairí faoin URL seo a ghlaoch, riamh óstach eile." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Pasfhocal" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Ainm úsáideora" + +[portal.connections.commonFields.webhookUrl] +helperText = "Greamaigh an URL a thug an tseirbhís duit. Ní féidir ach conairí faoi a ghlaoch." +label = "Webhook URL" + +[portal.connections.fields] +name = "Ainm ceangail" +namePlaceholder = "m.sh. Táirgeadh" + +[portal.connections.picker] +cancel = "Cealaigh" +createNew = "Ceangal nua..." +placeholder = "Roghnaigh ceangal" +save = "Sábháil ceangal" + +[portal.connections.picker2] +back = "Athraigh comhtháthú" +heading = "Cad ba mhaith leat a cheangal?" +noResultsBody = "Ní thacaímid leis fós - inis dúinn faoi agus breathnóimid ar é a chur leis." +noResultsTitle = "Ní mheaitseálann aon rud \"{{query}}\"" +searchPlaceholder = "Cuardaigh comhtháthuithe - bain triail as \"sign\", \"ocr\" nó ainm táirge" + +[portal.connections.types.api] +description = "Glaoigh ar aon HTTP API ó pholasaí. Roghnaíonn tú an URL, fíordheimhniú agus payload." +label = "API saincheaptha" + +[portal.connections.types.api.fields.authType] +label = "Fíordheimhniú" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Ainm úsáideora agus pasfhocal" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token i gceanntásc saincheaptha" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ceann ar bith" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Logáil isteach le haghaidh token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Ní féidir le céimeanna a úsáideann an ceangal seo ach conairí faoin URL seo a ghlaoch, riamh óstach eile." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Ainm ceanntásca" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Conair logála isteach" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Pasfhocal" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Má fhreagraíonn an API le nasc íoslódála, ní féidir ach na hóstach seo a fháil. Dealaigh le camóga. Fág bán é chun óstach an base URL amháin a cheadú." +label = "Óstaigh torthaí ceadaithe" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Seol an token mar an ceanntásc seo" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "An ceanntásc freagartha a fhilleann an glao logála isteach an token ann, m.sh. X-Auth-Token." +label = "Ceanntásc freagartha token" + +[portal.connections.types.api.fields.username] +label = "Ainm úsáideora" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scan doiciméid le haghaidh malware ar fhreastalaí a ritheann tú - ní fhágann aon rud do líonra." +label = "ClamAV (féin-óstáilte)" + +[portal.connections.types.cloudmersive] +description = "Scanadh víreas agus ábhair do dhoiciméid i bpolasaí." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Cuir bac ar dhoiciméid ina bhfuil macros, comhaid inrite, scripts nó réada leabaithe - ní hamháin malware aitheanta." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Ceangail doiciméid phróiseáilte le leathanach Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Seol doiciméid chuig ConsignO Cloud (Notarius) le haghaidh sínithe leictreonaigh agus nótárú." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Do thimpeallacht ConsignO Cloud, lena n-áirítear /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Pasfhocal" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Ní theastaíonn ach má úsáideann d'eagraíocht tionóntaí." +label = "Tionónta" + +[portal.connections.types.consigno.fields.username] +label = "Ainm úsáideora" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Postáil teachtaireacht chuig Discord nuair a phróiseálann polasaí doiciméad." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Innéacsaigh teagmhas iniúchta in Elasticsearch gach uair a láimhseálann polasaí doiciméad." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Fiafraigh de fhreastalaí FTP nó FTPS le haghaidh doiciméid nua." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Modh ceangail" + +[portal.connections.types.ftp.fields.passive.options] +active = "Gníomhach" +passive = "Éighníomhach (molta)" + +[portal.connections.types.ftp.fields.security] +helperText = "Seolann FTP lom dintiúir agus comhaid gan chriptiú; roghnaigh FTPS má thacaíonn an freastalaí leis." +label = "Criptiú" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Ceann ar bith (FTP lom)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Postáil teachtaireacht chuig spás Google Chat nuair a ritheann polasaí." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Ceangail doiciméid phróiseáilte le saincheist Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Seol doiciméad nó fógra ar Email nuair a ritheann polasaí." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Cuir doiciméid phróiseáilte i Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Pasfhocal aipe" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Braith sonraí pearsanta i dtéacs doiciméid ag úsáid freastalaí Presidio a ritheann tú." +label = "Presidio (féin-óstáilte)" + +[portal.connections.types.purview] +description = "Léigh agus cuir lipéid íogaireachta Microsoft Purview i bhfeidhm ar PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Roghnach. Ní theastaíonn ach chun lipéid a roghnú de réir ainm; oibríonn lipéadú gan é." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stampáilte ar gach lipéad mar a SiteId, ionas go mbeidh a fhios ag uirlisí a thuigeann Purview cén eagraíocht a chuir i bhfeidhm é." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Buicéad S3 ar féidir le foinsí léamh uaidh agus le haschuir pipeline scríobh chuige." +label = "Stóráil S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Buicéad" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Fág bán é do AWS. Socraigh é seo do MinIO nó stór eile atá comhoiriúnach le S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Réigiún" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Seol doiciméad nó fógra ar Email nuair a ritheann polasaí." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Fiafraigh de fhreastalaí SFTP (SSH) le haghaidh doiciméid nua." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Roghnach. Méarlorg eochair ósta SHA-256 an fhreastalaí (ó `ssh-keyscan host | ssh-keygen -lf -`). Nuair atá sé socraithe, ní ghlactar ach le freastalaí a chuireann an eochair seo i láthair; nuair atá sé bán, pinntear an eochair a fheictear ar an gcéad cheangal agus diúltaítear d'athruithe ina dhiaidh sin." +label = "Méarlorg eochair ósta" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frása faire eochrach" + +[portal.connections.types.sftp.fields.password] +helperText = "Fág bán é má fhíordheimhníonn tú le heochair phríobháideach ina ionad." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Roghnach. Greamaigh eochair phríobháideach OpenSSH chun fíordheimhniú le heochair in ionad pasfhocail." +label = "Eochair phríobháideach" + +[portal.connections.types.slack] +description = "Postáil teachtaireacht chuig Slack nuair a phróiseálann polasaí doiciméad." +label = "Slack" + +[portal.connections.types.smb] +description = "Déan faire ar chomhroinnt líonra SMB / CIFS (Windows nó Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Comhroinnt líonra" + +[portal.connections.types.smb.fields.domain] +helperText = "Roghnach. An fearann Windows nó grúpa oibre, má úsáideann do fhreastalaí ceann." +label = "Fearann" + +[portal.connections.types.smb.fields.share] +label = "Comhroinnt" +placeholder = "doiciméid" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Seol teagmhas iniúchta chuig Splunk gach uair a láimhseálann polasaí doiciméad." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Seol teagmhas iniúchta chuig bailitheoir HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Postáil teachtaireacht chuig cainéal Teams nuair a ritheann polasaí." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Postáil gach doiciméad, nó na sonraí amháin, chuig URL a roghnaíonn tú." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Spreag Zap nó cás Make ó pholasaí." +label = "Zapier & Make" + +[portal.docs] +browse = "Brabhsáil docs" +viewSource = "Féach ar an bhfoinse ar GitHub" + [portal.docs.authentication] codeCaption = "gach iarratas" eyebrow = "AG TOSÚ" @@ -6624,6 +7216,9 @@ eyebrow = "SCILEANNA" lead = "Cumasachtaí cuachta, ainmnithe a ghlaonn do ghníomhaire mar uirlis aonair. Is slabhra oibríochtaí cinntitheach gach scil, le measúnuithe ceangailte." title = "Scileanna gníomhairí" +[portal.docs.toc] +title = "Ar an leathanach seo" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "TAGAIRT API" @@ -6697,6 +7292,8 @@ type = "Cineál" user = "Úsáideoir" [portal.documents.queue.empty] +connectSource = "Ceangail foinse" +createPipeline = "Cruthaigh pipeline" description = "De réir mar a sheolann foinsí doiciméid isteach i do phíblínte, taispeánfar anseo iad le hathbhreithniú." title = "Níl aon doiciméid sa scuaine" @@ -6712,11 +7309,11 @@ editorAction = "Eagarthóir" empty = "Ní mheaitseálann aon doiciméid an scagaire seo." rowActions = "Gníomhartha ró" sensitiveLabel = "Íogair" -sensitiveTitle = "Íogair — rochtain de dhíth" [portal.documents.table.columns] action = "Píblíne / Gníomh" document = "Doiciméad" +labels = "Labels" product = "Táirge" status = "Stádas" time = "Am" @@ -6732,6 +7329,7 @@ host = "Óstach" lastSeen = "Feicthe go deireanach" region = "Réigiún" status = "Stádas" +target = "Target" version = "Leagan" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Bhuail an radharc seo earráid gan choinne. Bain triail eile as, retry = "Bain triail eile as" title = "Chuaigh rud éigin mícheart ar an leathanach seo" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Siar" +body = "Íoslódáil an aip deisce nó féin-óstáil an freastalaí." +done = "Déanta" +guide = "Oscail treoir {{name}}" +openInBrowser = "Oscail sa bhrabhsálaí ina ionad" +sectionDesktop = "Aip deisce" +sectionSelfHosted = "Féin-óstáilte" +title = "Suiteáil an Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Rith an íomhá oifigiúil ar do fhreastalaí." +note = "Osclaíonn ag :8080. Logáil isteach réamhshocraithe: admin / stirling." +tagline = "Rith an freastalaí i gcoimeádán" +title = "Docker" +variantFat = "Iomlán" +variantLite = "Ultra-Éadrom" +variantStandard = "Caighdeánach" + +[portal.home.download.kubernetes] +detailBody = "Suiteáil an Helm chart oifigiúil." +note = "Cuir PVC leis do /configs chun socruithe a choinneáil." +tagline = "Imscar chuig cnuasach le Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Tógálacha dúchasacha do do dháileadh." +guideBtn = "Féach roghanna suiteála Linux" +note = "Tá tógálacha AppImage, .deb agus .rpm ar fáil do dháiltí coitianta." +tagline = "Aip deisce dhúchasach do do distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Nó trí Homebrew" +detailBody = "Rith an suiteálaí, nó úsáid Homebrew." +downloadBtn = "Íoslódáil do macOS" +tagline = "Aip dhúchasach do Apple silicon agus Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Rith an .jar ar fhreastalaí bare metal." +downloadBtn = "Íoslódáil an .jar" +note = "Teastaíonn Java 21+. Osclaíonn ag :8080; logáil isteach réamhshocraithe admin / stirling." +runLabel = "Ansin rith é" +tagline = "Rith an .jar ar fhreastalaí bare metal" +title = "Socrú freastalaí láimhe" + +[portal.home.download.windows] +altLabel = "Nó trí winget" +detailBody = "Rith an suiteálaí, nó úsáid bainisteoir pacáistí." +downloadBtn = "Íoslódáil suiteálaí" +tagline = "Aip dhúchasach do Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} gníomhach" -invite = "Tabhair cuireadh do chomhghleacaithe" +activeOfDeployed = "{{active}} as {{total}} gníomhach an mhí seo" name = "Stirling PDF Editor" open = "Oscail sa bhrabhsálaí" updated = "nuashonraithe {{time}}" +[portal.home.editor.deploy] +finish = "Críochnaigh imscaradh" +options = "Féach roghanna imscartha" +start = "Imscar an tEagarthóir" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Féin-óstáilte · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Tráthnóna maith" evening = "Oíche mhaith" morning = "Maidin mhaith" -[portal.home.onboarding] -dismiss = "Dún an socrú" -notStarted = "Gan tosú" -progress = "{{done}} as {{total}} déanta" -title = "Críochnaigh an socrú" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC ar fud na heagraíochta, praghsáil toirte tiomanta, agus imscaradh air-gapped." -cta = "Tosaigh Triail" -ctaQuote = "Faigh Luachan" -lead = "Do 250+ fostaí." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Imscar an aip deisce, saor in aisce go deo." -title = "Íoslódáil an PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} gníomhach · {{recommended}} molta" -chip = "{{active}} gníomhach" -title = "Cuir beartais ar siúl" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} ceangailte · gach PDF rialaithe san áit a dtagann sé i dtír" -chip = "{{connected}} ceangailte" -title = "Ceangail do chuid foinsí" - -[portal.home.quickActions] -subtitle = "Príomhthascanna don lá inniu" -title = "Gníomhartha tapa" - -[portal.home.quickActions.buildPipeline] -blurb = "Cumadóir 3 chéim thar an leabharlann typed op" -title = "Tóg píblíne" - -[portal.home.quickActions.connectSource] -blurb = "S3, gníomhairí, webhooks, fillteáin fhaire" -title = "Ceangail foinse" - -[portal.home.quickActions.issueApiKey] -blurb = "Eochair scóipithe le teorainneacha ráta agus liosta ceadaithe IP" -title = "Eisigh eochair API" - [portal.infrastructure] manageEditorDeployment = "Bainistigh imscaradh an Eagarthóra" sectionsAriaLabel = "Rannáin bhonneagair" @@ -6878,13 +7541,9 @@ heading = "Eochracha API" subheading = "Dintiúir scóipithe le teorainneacha ráta in aghaidh na heochrach, ceadanna, agus liostaí ceadaithe IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IPanna ceadaithe" -anyIp = "Aon IP (gan liosta ceadaithe)" created = "Cruthaithe" lastUsed = "Úsáidte go deireanach" -permissions = "Ceadanna" -rateLimit = "Teorainn ráta" -rateLimitValue = "{{value}} iarr/min" +revoke = "Cúlghair eochair" usageMonth = "Úsáid an mhí seo" usageToday = "Úsáid inniu" @@ -6892,6 +7551,15 @@ usageToday = "Úsáid inniu" description = "Cruthaigh eochair scóipithe chun tosú ag glaoch ar an Stirling API." title = "Níl aon eochracha API fós" +[portal.infrastructure.apiKeys.error] +load = "Níorbh fhéidir d'eochracha API a lódáil. Bain triail eile as." + +[portal.infrastructure.apiKeys.revoke] +body = "Cúlghair \"{{name}}\"? Tosóidh aon chomhtháthú atá fós ag úsáid na heochrach seo ag fáil earráidí 401 láithreach. Ní féidir é seo a chur ar ceal." +cancel = "Cealaigh" +confirm = "Cúlghair eochair" +title = "Cúlghair eochair API" + [portal.infrastructure.audit] filterAriaLabel = "Scag imeachtaí iniúchta de réir catagóire" heading = "Logaí iniúchta" @@ -6935,6 +7603,7 @@ all = "Uile" auth = "Auth" config = "Cumraíocht" elevation = "Ardú" +policy = "Polasaí" processing = "Próiseáil" security = "Slándáil" @@ -6944,19 +7613,30 @@ title = "Níl rochtain agat" [portal.infrastructure.audit.metrics] config = "Cumraíocht" -elevation = "Ardú" +policy = "Polasaí" processing = "Próiseáil" totalEvents = "Imeachtaí iomlána · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Fíordheimhniú" +config = "Cumraíocht" +elevation = "Ardú" +policy = "Polasaí" +processing = "Próiseáil" +security = "Slándáil" + +[portal.infrastructure.auditStatusLabel] +danger = "Earráid" +info = "Eolas" +success = "D'éirigh leis" +warning = "Rabhadh" + [portal.infrastructure.createKey] cancel = "Cealaigh" createKey = "Cruthaigh eochair" done = "Déanta" -ipAllowlistHelper = "Raonta CIDR scartha le camóga. Fág bán chun aon IP a cheadú." -ipAllowlistLabel = "Liosta ceadaithe IP" keyNameLabel = "Ainm eochrach" keyNamePlaceholder = "m.sh. Táirgeadh · ionghabháil" -permissionsLabel = "Ceadanna" secretKeyCaption = "Eochair rúnda" secretWarning = "Stóráil é seo i mbainisteoir rúin. Ní stórálann Stirling ach hash riamh — níl aon bhealach ann é a aisghabháil níos déanaí." subtitle = "Scóipigh an eochair chuig an íosmhéid a theastaíonn uaithi. Is féidir leat í a rothlú nó a chúlghairm am ar bith." @@ -6964,207 +7644,9 @@ subtitleCreated = "Cóipeáil an rún seo anois — ní thaispeánfar arís é." title = "Cruthaigh eochair API" titleCreated = "Eochair cruthaithe" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Imscartha ag" -environment = "Timpeallacht" -product = "Táirge" -status = "Stádas" -version = "Leagan" -when = "Cathain" - -[portal.infrastructure.deployments.recent] -heading = "Imscarthaí le déanaí" -subheading = "Na rollouts is déanaí ar fud táirgí agus timpeallachtaí." - -[portal.infrastructure.deployments.regionColumns] -instances = "Insteansanna" -latency = "Foighne" -load = "Ualach" -p99 = "P99" -region = "Réigiún" -status = "Stádas" -throughput = "Tréchur" -uptime = "Aga fónaimh" -version = "Leagan" - -[portal.infrastructure.deployments.regions] -heading = "Réigiúin" -subheading = "Sláinte bheo do gach réigiún Stirling imscartha — foighne, ualach, agus leagan rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "Taispeánfar réigiúin imscartha anseo nuair a bheidh do spás oibre soláthraithe." -title = "Níl aon réigiúin imscartha" - -[portal.infrastructure.models] -heading = "Samhlacha" -msValue = "{{value}} ms" -subheading = "An chatalóg samhlacha agus an ródú a chumhachtaíonn próiseáil doiciméad ar fud do spáis oibre." - -[portal.infrastructure.models.byom] -description = "Cláraigh samhail on-prem nó féin-óstáilte agus pináil í le réigiún le haghaidh próiseála atá faoi cheangal cónaitheachta sonraí." -title = "Tabhair leat do shamhail féin" - -[portal.infrastructure.models.catalogue] -heading = "Catalóg" -sub = "Samhlacha bainistithe atá ar fáil do do spás oibre, le foighne agus costas beo." -subEnterprise = "Samhlacha bainistithe, tabhair-leat-do-cheann-féin, agus on-prem — le pináil in aghaidh an réigiúin ar fáil." - -[portal.infrastructure.models.catalogue.empty] -description = "Taispeánfar samhlacha i gcatalóg do spáis oibre anseo." -title = "Níl aon samhlacha ar fáil" - -[portal.infrastructure.models.columns] -cost = "Costas" -latency = "Foighne" -load = "Ualach" -model = "Samhail" -status = "Stádas" -type = "Cineál" -version = "Leagan" - -[portal.infrastructure.models.metrics] -activeModels = "Samhlacha gníomhacha" -avgLatency = "Meánfhoighne" -included = "San áireamh" -monthlySpend = "Caiteachas míosúil ar shamhlacha" - -[portal.infrastructure.models.routing] -empty = "Níl aon rialacha ródaithe cumraithe." -heading = "Rialacha ródaithe" -sub = "Cén tsamhail a láimhseálann gach oibríocht. Cuirtear an réamhshocrú i bhfeidhm nuair nach meaitseálann riail níos cúinge." -subLocked = "Ródáil oibríochtaí chuig samhlacha ar leith — ar fáil ar phleananna íoctha." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Uasghrádaigh go Pro chun rialú a dhéanamh ar cén tsamhail a láimhseálann gach oibríocht agus cineál doiciméid." -title = "Is gné íoctha é ródú samhlacha" - -[portal.infrastructure.models.routingColumns] -default = "Réamhshocrú" -docType = "Cineál doiciméid" -modelForAria = "Samhail do {{operation}}" -operation = "Oibríocht" -routedTo = "Ródaithe chuig" - -[portal.infrastructure.security.access.byok] -description = "Soláthair eochair ó do KMS féin. Criptíonn Stirling léi ach is féidir leis léamh fós." -label = "Tabhair leat d’eochair féin (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ní fhágann eochracha do KMS riamh. Ní choinníonn Stirling ach ciphertext." -label = "Coinnigh d’eochair féin (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Bainistíonn Stirling eochracha criptithe. Is simplí é — gan aon ops eochrach ar do thaobh." -label = "Eochracha á gcoinneáil ag Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Beartas rochtana doiciméad" -subheading = "Rialaíonn sé cé atá in ann doiciméid phróiseáilte atá ar fos a dhíchriptiú." - -[portal.infrastructure.security.attestations] -heading = "Fianuithe comhlíonta" -noReport = "Níl aon tuairisc ar fáil" -subheading = "Seasamh iniúchta de réir creatlaí, le tuairiscí ar fáil ar rialuithe fianaithe." -viewReport = "Féach ar thuairisc →" - -[portal.infrastructure.security.compliance] -heading = "Comhlíonadh" -subheading = "Fianuithe agus deimhnithe a chlúdaíonn ardán Stirling." - -[portal.infrastructure.security.empty] -description = "Taispeánfar cumraíocht slándála do spáis oibre anseo." -title = "Níl seasamh slándála ar fáil" - -[portal.infrastructure.security.hyokBanner] -description = "Le HYOK, ní fhágann eochracha criptithe do KMS riamh. Ní stórálann ná ní phróiseálann Stirling ach ciphertext ar féidir leat a chúlghairm am ar bith." -title = "Ní féidir le Stirling do dhoiciméid a dhíchriptiú" - -[portal.infrastructure.security.ipAllowlist] -empty = "Níl aon raonta IP cumraithe — ceadaítear gach IP." -heading = "Liosta ceadaithe IP" -sub = "Tá rochtain API srianta do na raonta CIDR seo." -subLocked = "Srian rochtain API do raonta IP aitheanta — ar fáil ar phleananna íoctha." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Uasghrádaigh go Pro chun rochtain API a shrianadh do líonraí ar leith." -title = "Is gné íoctha é liostú ceadaithe IP" - -[portal.infrastructure.security.ipColumns] -added = "Curtha leis" -addedBy = "Curtha leis ag" -cidr = "CIDR" -label = "Lipéad" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algartam" -heading = "Bainistiú eochracha criptithe" -keyId = "Aitheantóir eochrach" -lastRotated = "Rothlaithe go deireanach" -rotateKey = "Rothlaigh eochair" -rotationPolicy = "Beartas rothlaithe" -subheading = "Coimeád na n-eochracha a chriptíonn doiciméid atá ar fos — cé atá in ann díchriptiú, agus conas a rothlaítear eochracha." - -[portal.infrastructure.security.managedBanner] -description = "Tá coimeád tabhair-leat-d’eochair-féin (BYOK) agus coimeád coinnigh-d’eochair-féin (HYOK) ar fáil ar Enterprise. Uasghrádaigh chun eochracha a sholáthar ó do KMS féin." -title = "Tá eochracha á mbainistiú ag Stirling ar do phlean" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · teorainn sonraí GDPR" -label = "An tAontas Eorpach" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Stáit Aontaithe Mheiriceá" - -[portal.infrastructure.security.residencyHeader] -heading = "Cónaitheacht sonraí" -subheading = "An áit a stóráiltear agus a phróiseáiltear doiciméid." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} úsáidte" - -[portal.infrastructure.storage.empty] -description = "Taispeánfar stóráil cheangailte agus úsáid anseo." -title = "Níl aon stóráil cumraithe" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Gníomhach" -activeRange = "0–{{value}}d" -archived = "Cartlannaithe" -coldStorage = "stóráil fhuar" -deleted = "Scriosta" -never = "riamh" -purged = "glanta" - -[portal.infrastructure.storage.providers] -connect = "Ceangail" -connected = "Ceangailte" -heading = "Soláthraithe ceangailte" -subheading = "An áit a scríobhtar déantáin phróiseáilte." - -[portal.infrastructure.storage.retention] -heading = "Coinneáil" -subheading = "Cá fhad a choinnítear déantáin sula scriostar iad de réir saolré." -windowLabel = "Fuinneog choinneála réamhshocraithe" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} lá" -days_other = "{{count}} lá" -never = "Ná scrios riamh" - -[portal.infrastructure.storage.totalUsage] -heading = "Úsáid iomlán" -progressLabel = "Stóráil úsáidte" -subheading = "Stóráil ídithe ar fud gach soláthraí ceangailte." +revoked = "Cúlghairthe" [portal.infrastructure.tabs] apiKeys = "Eochracha API" @@ -7174,6 +7656,49 @@ models = "Samhlacha" security = "Slándáil" storage = "Stóráil" +[portal.integrations] +availableHeading = "Ar fáil" +comingSoonHeading = "Ag teacht go luath" +connect = "Ceangail" +connectionCount_one = "{{count}} nasc" +connectionCount_other = "{{count}} nasc" +customApi = "API saincheaptha" +searchPlaceholder = "Cuardaigh comhtháthuithe" +subtitle = "Gach rud ar féidir le Stirling labhairt leis. Cruthaítear dintiúir uair amháin anseo, ansin tagraíonn foinsí, polasaithe agus píblínte dóibh." +title = "Comhtháthuithe" +typedName = "Ainm {{tool}}" + +[portal.integrations.filters] +all = "Uile" +audit = "Iniúchadh" +connected = "Ceangailte" +notify = "Fógraigh" +security = "Slándáil" +signing = "Síniú" +storage = "Stóráil" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Ceangailte" + +[portal.integrations.table] +integration = "Comhtháthú" +worksWith = "Oibríonn le" + +[portal.integrations.worksWith] +pipelines = "Píblínte" +policies = "Polasaithe" +sources = "Foinsí" + +[portal.legal] +draft = "{{label}} · dréacht" +loadError = "Níorbh fhéidir an doiciméad seo a lódáil. Bain triail eile as." +loading = "Á lódáil…" +title = "Doiciméad dlíthiúil" + [portal.nav] agent-builder = "Tógálaí Gníomhairí" components = "Comhpháirteanna" @@ -7182,33 +7707,17 @@ documents = "Doiciméid" editor = "Eagarthóir" home = "Baile" infrastructure = "Bonneagar" +integrations = "Comhtháthuithe" pipelines = "Píblínte" policies = "Beartais" procurement = "Soláthar" -settings = "Socruithe" sources = "Foinsí" usage = "Úsáid & Billeáil" users = "Úsáideoirí" -[portal.notifications] -markAllRead = "Marcáil gach rud léite" -title = "Fógraí" -viewAll = "Féach ar gach rud" - -[portal.notifications.ariaLabel] -none = "Fógraí, gan aon rud gan léamh" -unread_one = "Fógraí, {{count}} gan léamh" -unread_other = "Fógraí, {{count}} gan léamh" - -[portal.notifications.count] -allRead = "gach rud léite" -loading = "á luchtú" -new_one = "{{count}} nua" -new_other = "{{count}} nua" - -[portal.notifications.empty] -description = "Níl aon fhógraí nua ann." -title = "Tá tú suas chun dáta" +[portal.nav.section] +platform = "Ardán PDF" +processor = "Próiseálaí PDF" [portal.pipelines] subtitle = "Gach píblíne doiciméad uathoibrithe ar an inneall cúil: slabhra ordaithe oibríochtaí thar shraith foinsí, á rith ar thruicear. Cliceáil ró le haghaidh a céimeanna agus a foinsí." @@ -7218,54 +7727,87 @@ title = "Píblínte" newPipeline = "Píblíne nua" [portal.pipelines.builder] -addStep = "Cuir uirlis leis" +activate = "Activate" back = "Ar ais chuig píblínte" +cannotFollow = "Ní féidir {{produced}} a ghlacadh" +chooseAccount = "Roghnaigh cuntas" +chooseDestination = "Roghnaigh ceann scríbe" +chooseOperation = "Roghnaigh cad a dhéanann an chéim seo" +chooseSource = "Roghnaigh foinse" discard = "Caith athruithe amach" -enabled = "Cumasaithe" +inputs = "Ionchur" +inputSource = "Foinse ionchuir" +inputTrigger = "Truicear" keepEditing = "Lean den eagarthóireacht" -needsUpload = "Teastaíonn comhad uaslódáilte" +moreActions = "More actions" +needsConfiguring = "Teastaíonn socrú" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Ní mheaitseálann aon uirlisí do chuardach." -pipelineSettings = "Socruithe píblíne" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Cuardaigh uirlisí" -selectToolBody = "Cuir uirlis leis chun do phíblíne a thógáil." -selectToolTitle = "Níl aon uirlisí fós" -toolSettings = "Socruithe uirlise" +sendToSystem = "Seol chuig córas eile" +stepsIncompatible = "Ní féidir leis na céimeanna seo rith ar an méid a tháirgeann an chéim roimhe seo: {{tools}}." +stepsNeedSetup = "Tá socrú fós ag teastáil ó na céimeanna seo sula sábhálfar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Oibríocht neamhaitheanta, coinnithe mar atá." unsavedBody = "Tá athruithe gan sábháil agat. Sábháil iad sula bhfágann tú, nó caith amach iad?" unsavedTitle = "Athruithe gan sábháil" -uploadUnsupported = "Ní thacaítear le comhaid uaslódáilte i bpíblínte fós, mar sin ní féidir na céimeanna seo a shábháil: {{tools}}." usesDefaults = "Ritheann le socruithe réamhshocraithe" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Comhcheanglaíonn sé gach comhad ón gcéim roimhe seo" +fan-out = "Ritheann sé uair amháin in aghaidh an chomhaid ón gcéim roimhe seo" +format-mismatch = "Teastaíonn {{accepts}}, ach táirgeann an chéim roimhe seo {{produced}}" +output-uncertain = "Seans nach rithfidh sé: braitheann aschur na céime roimhe seo ar an gcaoi a bhfuil sé socraithe" +source-mismatch = "Teastaíonn {{accepts}}, ach is é {{produced}} ionchur na píblíne seo" +undeclared-operation = "Ní féidir a sheiceáil cad a ghlacann an chéim seo" [portal.pipelines.composer] addTool = "Cuir uirlis leis" -cancel = "Cealaigh" -chainEmpty = "Cuir uirlis leis chun tosú ag tógáil do phíblíne." create = "Cruthaigh píblíne" -directory = "Fillteán aschuir" -directoryHelp = "Conair absalóideach ar an bhfreastalaí. Caithfidh sí a bheith laistigh de na fillteáin cheadaithe chumraithe." +createPaused = "Create paused" editingUnsupported = "Ní thacaítear fós le taispeáint na bparaiméadar uirlise seo le haghaidh eagarthóireachta." -moveDown = "Bog síos" -moveUp = "Bog suas" +editSource = "Edit source" name = "Ainm" namePlaceholder = "m.sh. Scuabadh eagarthóireachta" -noSources = "Níl aon fhoinsí ceangailte fós. Is féidir leis an bpíblíne rith fós ar chomhaid a sholáthraítear di go díreach." noToolSettings = "Níl aon socruithe inchumraithe ag an uirlis seo." -operations_one = "Oibríocht ({{count}})" -operations_other = "Oibríochtaí ({{count}})" output = "Aschur" -removeStep = "Bain oibríocht" save = "Sábháil athruithe" scheduleEvery = "Rith gach" -sources = "Foinsí" -sourcesLoading = "Foinsí á luchtú..." trigger = "Truicear" triggerManual = "Láimhe amháin" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "lá" hours = "uair" minutes = "nóiméad" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Scrios \"{{name}}\"? Ní féidir é seo a chealú." cancel = "Cealaigh" @@ -7273,28 +7815,62 @@ confirm = "Scrios" title = "Scrios píblíne?" [portal.pipelines.detail] +clearHistory = "Glan stair" delete = "Scrios píblíne" run = "Rith anois" [portal.pipelines.empty] action = "Cruthaigh píblíne" +connectSource = "Ceangail foinse" description = "Cruthaigh do chéad phíblíne: roghnaigh na foinsí a ritheann sí orthu, slabhraigh na hoibríochtaí, agus roghnaigh cá dtéann an t-aschur." title = "Níl aon phíblínte fós" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Gníomhach" paused = "Ar sos" total = "Píblínte" -[portal.pipelines.output] -folder = "Scríobh chuig fillteán" -inline = "Tabhair comhaid ar ais" - [portal.pipelines.run] +allProcessed_one = "Níl aon rud le rith: próiseáladh {{count}} doiciméad na foinse cheana féin." +allProcessed_other = "Níl aon rud le rith: próiseáladh gach ceann de {{count}} doiciméad sna foinsí cheana féin." completed_one = "Rith críochnaithe." completed_other = "Críochnaíodh gach {{count}} rith." empty = "Níl aon rud le rith: ní raibh aon doiciméid le próiseáil ag na foinsí." failed = "Theip ar an rith: {{error}}" +inFlight = "Níl aon rud nua le rith: tá doiciméid fós á bpróiseáil ó rith níos luaithe." +parked_one = "Níl aon rud le rith: theip ar {{count}} doiciméad roimhe seo agus tá sé páirceáilte. Ceartaigh an chúis, ansin glan an stair chun triail eile a bhaint as." +parked_other = "Níl aon rud le rith: theip ar {{count}} doiciméad roimhe seo agus tá siad páirceáilte. Ceartaigh an chúis, ansin glan an stair chun triail eile a bhaint astu." running = "Tosaíodh an rith; tá sé fós ar siúl." timeout = "Tá an rith ag glacadh níos faide ná mar a bhíothas ag súil leis; d’fhéadfadh sé críochnú sa chúlra fós." @@ -7307,6 +7883,7 @@ name = "Píblíne" sources = "Foinsí" status = "Stádas" steps = "Céimeanna" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Faire fillteáin" @@ -7314,12 +7891,18 @@ manual = "Láimhe" schedule = "Sceidealaithe" [portal.policies] +defaultName = "Polasaí {{category}}" subtitle = "Uathoibrithe seasta a fhorfheidhmíonn píblíne uirlisí ar gach doiciméad. Spreagtar gach beartas ar uaslódáil nó easpórtáil, ritheann sé a shlabhra uirlisí, agus sábhálann sé an leagan forfheidhmithe taobh leis an mbunleagan." title = "Beartais" [portal.policies.card] comingSoon = "Uasghrádaigh go Enterprise" notSetUp = "Gan socrú" +requiresAiEngine = "Teastaíonn inneall AI" + +[portal.policies.categories.classification] +desc = "Aithin cineál gach doiciméid agus é á uaslódáil agus clibeáil a mheiteashonraí le haghaidh comhdaithe agus cuardaigh." +label = "Aicmiú" [portal.policies.categories.compliance] desc = "Forfheidhmigh ceanglais HIPAA, GDPR, SOC 2, nó FedRAMP ar gach doiciméad." @@ -7344,6 +7927,13 @@ label = "Slándáil" [portal.policies.config] scopeAll = "Gach doiciméad" +[portal.policies.config.classification] +summary = "Aicmíonn sé gach doiciméad uaslódáilte de réir lipéid d'fhoirne agus clibeálann sé a mheiteashonraí." + +[portal.policies.config.classification.rules] +0 = "Aicmigh" +1 = "Clibeáil meiteashonraí" + [portal.policies.config.compliance] summary = "Bailíochtaíonn doiciméid i gcoinne creatlaí rialála sula bhfágann siad an córas." @@ -7371,6 +7961,29 @@ minConfidence = "Muinín íosta" 2 = "Ainmnigh" 3 = "Normalú" +[portal.policies.config.purview] +label = "Cuir lipéad íogaireachta Microsoft Purview i bhfeidhm" + +[portal.policies.config.purview.fields] +connection = "Tionónta Purview" +labelId = "ID lipéid" +labelIdHelp = "GUID an lipéid, ón tairseach Purview. Scríobhann cur i bhfeidhm lipéid a mheiteashonraí ar an PDF; ní féidir leis criptiú, rud a dteastaíonn cliant Microsoft uaidh." +labelName = "Ainm lipéid (roghnach)" +labelNameHelp = "Taifeadta in éineacht leis an ID ionas gur féidir le daoine agus uirlisí an lipéad a aithint." + +[portal.policies.config.purview.fields.method] +help = "Déanann Purview idirdhealú idir lipéad a chuir polasaí i bhfeidhm agus ceann a roghnaigh duine." +label = "Conas a thaifeadtar an lipéad" +privileged = "Mar a roghnaigh duine é" +standard = "Mar a cuireadh i bhfeidhm go huathoibríoch é" + +[portal.policies.config.purviewRead] +label = "Léigh lipéad Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tionónta Purview" +connectionHelp = "Léann sé an lipéad atá ar an doiciméad cheana féin, ionas gur féidir le céimeanna níos déanaí gníomhú air." + [portal.policies.config.retention] summary = "Forfheidhmíonn sé cá fhad a choinnítear doiciméid, cathain a chartlannaítear iad, agus cathain a scriostar iad." @@ -7438,6 +8051,7 @@ title = "Níl aon ghníomhaíocht fós" [portal.policies.endpoints] addWatermark = "Comhartha uisce" autoRedact = "Ceil PII" +classifyAndLabel = "Aicmigh" compressPdf = "Comhbhrúigh" flatten = "Leacaigh" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Sábháladh do bheartais agus beidh siad le feiceáil nuair a bhe retry = "Atriail" title = "Níl Backend ar fáil" +[portal.policies.operations] +change = "Athraigh cad a dhéanann an chéim seo" +noResults = "Ní mheaitseálann aon chéim é sin. Bain triail as ainm táirge, nó \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Cuardaigh céimeanna - bain triail as \"scan\", \"attach\" nó ainm táirge" + +[portal.policies.operations.bodyMode] +binary = "Bearta comhaid amh" +json = "Taobh istigh de phálód JSON" +multipart = "Mar uaslódáil comhaid (multipart)" + +[portal.policies.operations.clamavScan] +description = "Seiceálann do fhreastalaí ClamAV féin an doiciméad; ní fhágann aon rud do líonra." +label = "Scan le haghaidh víreas (féin-óstáilte)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Diúltaíonn sé do dhoiciméid a bhfuil macraí, comhaid inrite, scripteanna nó réada leabaithe iontu." +label = "Blocáil ábhar gníomhach" + +[portal.policies.operations.cloudmersiveScan] +description = "Seiceálann Cloudmersive an doiciméad agus stopann an rith mura bhfuil sé glan." +label = "Scan le haghaidh víreas" + +[portal.policies.operations.confluenceAttach] +description = "Comhdaíonn sé an doiciméad próiseáilte ar leathanach." +label = "Ceangail le leathanach Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Cuireann sé an doiciméad faoi bhráid ConsignO Cloud le síniú." +label = "Seol le haghaidh sínithe (ConsignO)" +note = "Cuireann sé an doiciméad isteach. Ní thacaítear fós leis an gcóip shínithe a aisghabháil, toisc nach féidir le céim toradh a chur ar aghaidh chuig céim níos déanaí." + +[portal.policies.operations.customApiCall] +description = "Cum an glao tú féin: cosán, modh, corp agus láimhseáil freagartha." +label = "Glaoigh ar API saincheaptha" + +[portal.policies.operations.discordNotify] +description = "Insíonn sé do chainéal gur láimhseáil an polasaí doiciméad." +label = "Postáil teachtaireacht chuig Discord" + +[portal.policies.operations.elasticIndex] +description = "Taifeadann sé cad a rinne an polasaí mar dhoiciméad inchuardaithe." +label = "Innéacsaigh imeacht iniúchta in Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Oireann multipart do APIanna uaslódála; oireann JSON do APIanna a theastaíonn an comhad uathu taobh istigh de phálód; seolann binary na bearta amha." +label = "Conas an doiciméad a sheoladh" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Corp JSON a sheoltar mar atá. Iompraíonn {{document.base64}} an comhad; líontar {{document.*}} agus {{run.*}} isteach in aghaidh an doiciméid." +label = "Corp JSON" + +[portal.policies.operations.fields.connection] +label = "Cuntas" + +[portal.policies.operations.fields.domain] +label = "Fearann Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "An réimse foirme a bhfuil an API ag súil leis an doiciméad faoi." +label = "Ainm réimse comhaid" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Ó" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Oibiacht JSON de phéirí ainm/luach ceanntáisc. Cuirtear dintiúr an naisc leis duit." +label = "Ceanntásca" + +[portal.policies.operations.fields.index] +label = "Innéacs" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Eochair eisiúna" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Seolta mar chorp na teachtaireachta. Líontar {{document.*}} agus {{run.*}} isteach in aghaidh an doiciméid." +label = "Teachtaireacht" + +[portal.policies.operations.fields.method] +label = "Modh" + +[portal.policies.operations.fields.pageId] +label = "ID leathanaigh" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Ceangailte le bun-URL an naisc. Ní féidir leis óstach eile a bhaint amach riamh." +label = "Cosán" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Cá háit sa tiomántán le scríobh. Líontar {{document.filename}} isteach in aghaidh an doiciméid." +label = "Cosán cinn scríbe" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Fágann tuairisc an doiciméad gan athrú. Cuireann ionadaigh an méid a thagann ar ais ina áit." +label = "Cad atá le déanamh leis an bhfreagra" + +[portal.policies.operations.fields.signerEmail] +label = "Ríomhphost an tsínitheora" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Ábhar" +placeholder = "Próiseáilte: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Déanann Presidio anailís ar an téacs seo. Úsáid {{document.title}} nó greamaigh an téacs is mian leat a sheiceáil." +label = "Téacs le seiceáil" + +[portal.policies.operations.fields.to] +label = "Chuig" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Ainm úsáideora Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Insíonn sé do spás gur láimhseáil an polasaí doiciméad." +label = "Postáil teachtaireacht chuig Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Comhdaíonn sé an doiciméad próiseáilte ar shaincheist." +label = "Ceangail le saincheist Jira" + +[portal.policies.operations.mailgunEmail] +description = "Seolann sé an doiciméad próiseáilte mar cheangaltán." +label = "Seol an doiciméad ar ríomhphost (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Scríobhann sé an doiciméad próiseáilte isteach i bhfillteán." +label = "Uaslódáil chuig Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Tuairiscíonn do fhreastalaí Presidio féin na sonraí pearsanta a aimsíonn sé." +label = "Braith sonraí pearsanta" +note = "Léann Presidio téacs, ní comhaid. Tabhair dó an téacs is mian leat a sheiceáil — ní uaslódáiltear an doiciméad féin." + +[portal.policies.operations.responseMode] +replace = "Cuir an freagra in áit an doiciméid" +report = "Coinnigh an doiciméad, taifead an freagra" + +[portal.policies.operations.sendgridEmail] +description = "Seolann sé an doiciméad próiseáilte mar cheangaltán." +label = "Seol an doiciméad ar ríomhphost (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Insíonn sé do chainéal gur láimhseáil an polasaí doiciméad." +label = "Postáil teachtaireacht chuig Slack" + +[portal.policies.operations.splunkEvent] +description = "Taifeadann sé cad a rinne an polasaí, áit a bhféachann d'iniúchóirí cheana féin." +label = "Seol imeacht iniúchta chuig Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Taifeadann sé cad a rinne an polasaí chuig bailitheoir HTTP." +label = "Seol imeacht iniúchta chuig Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Insíonn sé do chainéal gur láimhseáil an polasaí doiciméad." +label = "Postáil teachtaireacht chuig Teams" + +[portal.policies.operations.webhookPost] +description = "Seolann sé an doiciméad féin chuig URL a roghnaíonn tú." +label = "Postáil an doiciméad chuig webhook" + +[portal.policies.operations.zapierNotify] +description = "Tugann sé sonraí an rite do d'uathoibriú." +label = "Truicear Zap nó cás Make" + [portal.policies.stats] activeFor = "Gníomhach" dataProcessed = "Sonraí próiseáilte" @@ -7473,6 +8282,13 @@ label = "Cáipéisí forfheidhmithe" description = "Cumraithe ach gan lasadh" label = "Ar sos" +[portal.policies.table] +appliesTo = "Baineann le" +docs = "Doiciméid curtha i bhfeidhm" +enforces = "Cuireann i bhfeidhm" +policy = "Polasaí" +status = "Stádas" + [portal.policies.wizard.actions] back = "Ar ais" cancel = "Cealaigh" @@ -7480,6 +8296,10 @@ continue = "Lean ar aghaidh" enablePolicy = "Cumasaigh beartas" saveChanges = "Sábháil athruithe" +[portal.policies.wizard.capability.classify] +desc = "Aithníonn sé cineál an doiciméid ó lipéid d'fhoirne agus clibeálann sé é, ionas go gcomhdaítear agus go gcuardaítear é de réir catagóire." +label = "Aicmigh an doiciméad" + [portal.policies.wizard.capability.compress] desc = "Comhbhrúnn sé an cháipéis go méid comhaid níos lú." label = "Laghdaigh méid an chomhaid" @@ -7500,10 +8320,18 @@ label = "Ceil faisnéis íogair" desc = "Baineann sé JavaScript i bhfolach ionas nach féidir aon rud a rith go huathoibríoch nuair a osclaítear an cháipéis." label = "Bain ábhar gníomhach" +[portal.policies.wizard.capability.timestampPdf] +desc = "Cruthaíonn sé go raibh an doiciméad ann san fhoirm bheacht seo ag pointe ama, ag úsáid údarás stampa ama neamhspleách. Ní sheoltar ach hash - ní fhágann an doiciméad do fhreastalaí riamh." +label = "Cuir stampa ama iontaofa leis" + [portal.policies.wizard.capability.watermark] desc = "Cuireann sé marc infheicthe (m.sh. “Rúnda”) trasna gach leathanaigh." label = "Cuir comhartha uisce i bhfeidhm" +[portal.policies.wizard.classification] +description = "Aicmítear gach doiciméad uaslódáilte de réir na lipéad ionsuite agus clibeáiltear é leis na cineálacha a oireann. Roinntear an tacar lipéad ar fud d'fhoirne ar fad." +labelsHeading = "Lipéid aicmithe" + [portal.policies.wizard.errors] noTools = "Cumasaigh uirlis amháin ar a laghad sa sreabhadh oibre ar dtús." saveFailed = "Níorbh fhéidir an beartas a shábháil. Bain triail eile as." @@ -7548,73 +8376,118 @@ setUp = "Socraigh beartas {{category}}" [portal.policies.wizard.workflow] description = "Roghnaigh cad a dhéanann an beartas seo le gach cáipéis a phróiseálann sé." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} gníomhach" -noRule = "Níl aon riail forfheidhmithe fós" -subtitle = "Uathoibrithe seasta a dtéann gach cáipéis tríothu, beag beann ar an bpíblíne a láimhseálann í." -title = "Cad a ritheann ar do PDFs" - [portal.policySummary.action] -comingSoon = "Uasghrádaigh go Enterprise" -configure = "Cumraigh" setUp = "Socraigh" -[portal.policySummary.column] -activeRule = "Riail ghníomhach" -policy = "Beartas" -status = "Stádas" +[portal.processorFlow] +footnote = "Tá na comhaireamh thar na 24 uair an chloig dheireanacha. Tá luas an tsreafa léiriúcháin." +liveBadge = "Beo" +stats = "{{connected}} ceangailte · {{processed}} PDF próiseáilte" +title = "Próiseálaí PDF" -[portal.policySummary.empty] -description = "Nuair a bheidh beartais cumraithe, beidh na catagóirí le feiceáil anseo." -title = "Níl aon bheartais fós" +[portal.processorFlow.lens] +ariaLabel = "Mód amhairc" +flow = "Sreabhadh" +sankey = "Sankey" -[portal.policySummary.state] -active = "Gníomhach" -off = "As" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Theip" +heading = "Torthaí" +success = "Seachadta" + +[portal.processorFlow.policies] +activeCount = "{{n}} gníomhach" +count = "{{n}} · 24h" +heading = "Polasaithe" +setUp = "Socraigh" soon = "Go luath" -[portal.processingStatus] -managePlan = "Bainistigh plean" -volumeSuffix = "PDFs próiseáilte · 30 lá anuas" +[portal.processorFlow.sankey] +waist = "Polasaithe · {{n}} gníomhach" + +[portal.processorFlow.sankey.empty] +description = "Ceangail foinse agus cuir polasaí ar siúl chun doiciméid a fheiceáil ag sreabhadh tríd an bpróiseálaí." +title = "Níl aon sreabhadh fós" + +[portal.processorFlow.sources] +comingSoonTag = "Ceangail" +editor = "Eagarthóir Stirling PDF" +heading = "Foinsí" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Iontógáil ríomhphoist" [portal.procurement] -reset = "Athshocraigh soláthar (taispeántas)" subtitle = "Faigh do fhoireann measúnaithe, conraithe agus ionduchtaithe. Gach cáipéis in aon áit amháin." title = "Soláthar" -[portal.procurement.action] -download = "Íoslódáil" -pay = "Íoc anois" -request = "Iarr" -sign = "Athbhreithnigh & sínigh" -upload = "Uaslódáil" - [portal.procurement.agreement] agreeCta = "Aontaigh & liostáil" +confidential = "Rúnda" confirm = "Tá Comhaontú Stirling Enterprise léite agam agus aontaím leis." -eyebrow = "Comhaontú" -intro = "Clúdaíonn comhaontú comhcheangailte amháin do mhargadh: Máistir-Chomhaontú Seirbhíse, Foirm Ordaithe, EULA, agus Comhaontú Próiseála Sonraí. Déan athbhreithniú air, ansin aontaigh chun glacadh leis an luachan mar shíntiús tiomanta." -title = "Athbhreithnigh do chomhaontú enterprise" +docName = "Comhaontú Fiontair Stirling" +docSub = "MSA, Foirm Ordaithe, EULA agus DPA, comhcheangailte in aon síniú amháin." +download = "Íoslódáil" +downloadDraftError = "Níorbh fhéidir PDF an chomhaontaithe a ghiniúint - níl rindreálaí an doiciméid ar fáil. Bain triail eile as go gairid nó déan teagmháil le tacaíocht." +downloadError = "Níorbh fhéidir an comhaontú sínithe a íoslódáil. Bain triail eile as." +legalName = "Ainm an eintitis dhlíthiúil" +legalNamePlaceholder = "An t-eintiteas dlíthiúil a shíneoidh" +loadError = "Níorbh fhéidir an comhaontú a lódáil. Bain triail eile as." +loading = "An comhaontú á lódáil..." +ref = "Tag {{ref}} · {{version}}" +requestChanges = "Iarr athruithe" +scrollHint = "Scrollaigh le hathbhreithniú" +signatory = "Ainm an tsínitheora" +signatoryPlaceholder = "Ainm iomlán" +signatoryTitle = "Teideal an tsínitheora" +signatoryTitlePlaceholder = "m.sh. Abhcóide Ginearálta" +signError = "Níorbh fhéidir do shíniú a thaifeadadh. Bain triail eile as." [portal.procurement.builder] addons = "Breiseáin" +addressLine1 = "Seoladh líne 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Seoladh líne 2" +addressLine2Placeholder = "Svuít, urlár, foirgneamh" back = "Ar ais" businessName = "Ainm gnó" businessNamePlaceholder = "Do chuideachta" +city = "Cathair" +cityPlaceholder = "San Francisco" +completeRequired = "Comhlánaigh na réimsí riachtanacha (marcáilte *) le ríomhphost bailí sula ngintear an luachan." +contactEmail = "Ríomhphost teagmhála" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Ainm teagmhála" +contactNamePlaceholder = "Jane Doe" continue = "Lean ar aghaidh" -country = "Tír" -countryEuro = "Limistéar an euro (EUR €)" -countryUK = "An Ríocht Aontaithe (GBP £)" -countryUS = "Stáit Aontaithe Mheiriceá (USD $)" +done = "Déanta" eula = "Tá Stirling Enterprise EULA léite agam agus aontaím leis. Rialaíonn sé an comhaontú a ghintear ón luachan seo." generate = "Gin luachan" -included = "San áireamh" indemnification = "Slánaíocht IP" indemnificationSub = "Cosnaímid éilimh cháilitheacha IP, de réir an EULA" -offlineLicense = "Ceadúnas as líne / air-gapped" -offlineLicenseSub = "Comhad ceadúnais in-íoslódáilte le haghaidh instance self-hosted air-gapped" +paperEyebrow = "Luachan fiontair" +paperFor = "Ullmhaithe do" +pdfSize = "Méid PDF" +poNumber = "Uimhir PO" +poNumberPlaceholder = "Roghnach" +postalCode = "Cód poist" +postalCodePlaceholder = "94105" +posture = "Rialachas" +posture_count = "~{{count}} polasaí" +posture_essentials = "Bunriachtanais" +posture_essentialsSub = "Aicmiú + Comhroinnt — na réamhshocruithe" +posture_governed = "Rialaithe" +posture_governedSub = "Cuireann Slándáil agus Ródú leis" +posture_regulated = "Rialáilte" +posture_regulatedSub = "Gach catagóir — Comhlíonadh, Coinneáil, Iontógáil" qbr = "Athbhreithnithe gnó ráithiúla" qbrSub = "Déanann do SE athbhreithniú ar úsáid agus treochlár gach ráithe" +region = "Stát / réigiún" +regionPlaceholder = "California" running = "{{annual}} / bl · {{years}} bl {{tcv}}" s1Sub = "D’fhoireann, agus na PDFs a bhfuil súil agat a rith gach bliain." s1Title = "Cé mhéad a phróiseálfaidh tú?" @@ -7623,6 +8496,12 @@ s2Title = "Tiomantas agus seirbhís" s3Sub = "Don luachan agus don chomhaontú a ghineann sé." s3Title = "Do shonraí" serviceLevel = "Leibhéal seirbhíse" +size_compact = "Dlúth" +size_compactSub = "Téacs den chuid is mó, faoi 1 MB" +size_heavy = "Trom" +size_heavySub = "Scanta nó lán d'íomhánna, 5 MB+" +size_standard = "Caighdeánach" +size_standardSub = "Téacs agus íomhánna measctha, 1 go 5 MB" slDedicated = "Tiomnaithe" slDedicatedSub = "4 huaire gnó · bainisteoir cuntais tiomnaithe · +30%" slPriority = "Tosaíocht" @@ -7630,6 +8509,8 @@ slPrioritySub = "An lá gnó céanna · CSM ainmnithe · +15%" slStandard = "Caighdeánach" slStandardSub = "An chéad lá gnó eile · CSM comhroinnte · san áireamh" stepOf = "Céim {{n}} as {{total}}" +taxId = "VAT / ID cánach" +taxIdPlaceholder = "Roghnach" term = "Téarma" termDiscount = "Cuireadh lascaine tiomantais ilbhliantúil {{pct}}% i bhfeidhm" title = "Tóg do luachan" @@ -7637,6 +8518,7 @@ training = "Ionduchtú & oiliúint" trainingSub = "Seisiúin bheo chun d’fhoireann a chur ag obair" users = "Úsáideoirí iomlána" usersPlaceholder = "m.sh. 250" +viewEula = "Léigh é" volEstimated = "Measta ó {{count}} úsáideoir (~2,000 PDFs an duine, uathoibriú san áireamh). Cuir in eagar é má tá eolas níos fearr agat." volManual = "Ag úsáid d’fhigiúir. Athmheas ó mhéid d’fhoirne am ar bith." volNoUsers = "Níl tú cinnte? Cuir isteach méid d’fhoirne agus meastóimid é." @@ -7645,55 +8527,55 @@ volumePlaceholder = "m.sh. 1,000,000" years_one = "{{count}} bhliain" years_other = "{{count}} bliana" -[portal.procurement.docs] -count_one = "{{count}} cháipéis" -count_other = "{{count}} cháipéis" -done = "Déanta" -here = "Tá tú anseo" -hide = "Folaigh" -optional = "Roghnach" -paidAddon = "Breiseán íoctha" -show = "Taispeáin" -subtitle = "Gach rud atá uait ag gach céim den turas, á thaispeáint de réir mar a bhogann an margadh tríd." -supportingSubtitle = "SOC 2, athbhreithnithe slándála, foirmeacha cánach agus tuilleadh, réidh nuair a iarrann d’fhoireann slándála nó soláthair iad. Tá táille aonuaire ar chuid acu." -supportingTitle = "Ag tacú le do mheasúnú" -title = "Cáipéisí" -upcoming = "Le teacht" +[portal.procurement.documents] +agreement = "Comhaontú Fiontair" +agreementSub = "MSA, Foirm Ordaithe agus DPA in aon síniú amháin." +download = "Íoslódáil" +eula = "EULA & Téarmaí Tráchtála" +eulaSub = "Téarmaí tráchtála na bogearraí agus féinseirbhíse." +invoice = "Sonrasc" +invoiceSub = "Do chéad sonrasc síntiúis." +laterInvoice = "Ar fáil tar éis duit glacadh leis" +laterQuote = "Ar fáil nuair a eisítear do luachan" +quote = "Luachan" +quoteSub = "Do luachan fiontair mionsonraithe." +sla = "Iarscríbhinn SLA" +slaSub = "Sraitheanna tacaíochta agus spriocanna freagartha." +subprocessors = "Fophróiseálaithe" +subprocessorsSub = "Tríú páirtithe a phróiseálann sonraí ar do shon." +subtitle = "Do pháipéarachas margaidh, ar fáil am ar bith." +title = "Doiciméid" +view = "Féach" [portal.procurement.error] title = "Chuaigh rud éigin mícheart" [portal.procurement.hero] -company = "Do mhargadh enterprise" +barAria = "Céim {{current}} as {{total}}" ctaAgreement = "Athbhreithnigh & sínigh comhaontú" -ctaLive = "Tá tú beo" -ctaPayment = "Cuir íocaíocht leis" +ctaExploring = "Socraigh do thriail" ctaQuote = "Athbhreithnigh do luachan" +ctaReviewQuote = "Athbhreithnigh luachan" ctaTrial = "Tóg do luachan" +documents = "Doiciméid" eyebrow = "Soláthar enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Tabhair cuireadh do chomhpháirtithe foirne" -keyDocs = "Príomhcháipéisí" -nextStep = "An chéad chéim eile: {{action}}" -notStarted = "Gan tosú" +licenseKey = "Eochair cheadúnais" +liveSub = "Tá d'eagraíocht soláthraithe agus tá do cheadúnas gníomhach." +liveTitle = "Tá tú beo ar Stirling Enterprise" +next = "Ar aghaidh: {{stage}}" open = "Oscail soláthar" scheduleCall = "Sceideal glao" -setup1Sub = "Tabhair cuireadh do do chomhpháirtithe foirne" -setup1Title = "Imscar an PDF Editor" -setup2Sub = "Cuir próiseáil ar siúl trasna eagarthóirí agus foinsí eile" -setup2Title = "Ceangail an PDF Processor" -setup3Sub = "Cuir Slándáil, Comhlíonadh, Ródú, nó Coinneáil ar siúl nuair a bhíonn siad uait" -setup3Title = "Cuir beartais mholta leis" +sentenceAgreement = "athbhreithniú agus síniú an chomhaontaithe" +sentenceLive = "soláthar agus rolladh amach" +sentencePayment = "ordú ceannaigh agus íocaíocht" +sentenceQuote = "athbhreithniú agus glacadh le do luachan" +sentenceTrial = "socraigh do spás oibre trialach" [portal.procurement.journey] daysLeft_one = "{{count}} lá fágtha" daysLeft_other = "{{count}} lá fágtha" -engineerLabel = "D’innealtóir réiteach" -eyebrow = "Do chur i bhfeidhm" -live = "Tá tú beo ar Stirling Enterprise" -nextStep = "An chéad chéim eile: {{action}}" -subtitle = "Tá d’innealtóir réiteach ar gach céim. Gníomh amháin eile ag an am; tá an seicliosta iomlán thíos." -title = "Ó thriail go beo, cosán treoraithe amháin" -trialTitle = "Triail Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Clúdaíonn síniú amháin MSA, foirm ordaithe, EULA agus DPA." @@ -7727,6 +8609,9 @@ downloadError = "Níorbh fhéidir an comhad ceadúnais as líne a ghiniúint fó downloadOffline = "Íoslódáil ceadúnas as líne (.lic)" hint = "Greamaigh an eochair seo isteach in instance self-hosted chun é a ghníomhachtú, nó coinnigh í do do thaifid. Coinnigh slán í." label = "D’eochair cheadúnais" +subtitle = "Gníomhachtaigh ásc féin-óstáilte leis an eochair seo, nó coinnigh í do do thaifid." +title = "D'eochair cheadúnais" +trialFileHint = "Seo do chomhad ceadúnais trialach. Nuair a bheidh do chomhaontú i bhfeidhm, tar ar ais agus íoslódáil arís é — murab ionann agus an eochair cheadúnais ar líne, ní nuashonróidh an comhad .lic é féin." [portal.procurement.link] cta = "Nasc cuntas" @@ -7739,65 +8624,62 @@ description = "Tá do shíntiús gníomhach agus tá do cheadúnas eisithe. Tá eyebrow = "Beo" title = "Tá tú beo ar Stirling Enterprise" -[portal.procurement.locked] -description = "Beidh eochracha trialach, luachana toirte tiomanta, an comhaontú aon-sínithe, íocaíocht, agus do mhórleabhar cáipéisí ar fad anseo nuair a thosóidh tú measúnú enterprise." -eyebrow = "Enterprise amháin" -talkToSales = "Labhair le díolacháin" -title = "Osclaíonn rian an tsoláthair le Enterprise" - [portal.procurement.milestone] -accept = "Glac & lean ar aghaidh" -description = "Íoslódáil an PDF chun é a roinnt le d’fhoireann, tar ar ais chun glacadh leis nuair atá tú réidh, nó déan athruithe." -download = "Íoslódáil PDF" downloadError = "Níorbh fhéidir an PDF luachana a íoslódáil fós — bain triail eile as ar ball." -edit = "Cuir luachan in eagar" -eyebrow = "Luachan {{number}}" -perYear = " / bl" -preparedFor = "Ullmhaithe do {{company}}" -tcv = "luach iomlán conartha {{value}}" -title = "Tá do luachan réidh" - -[portal.procurement.modal] -cancel = "Cealaigh" -chooseFile = "Roghnaigh comhad" -close = "Dún" -downloadBody = "Tosóidh d’íoslódáil go gairid." -downloadCta = "Íoslódáil" -downloadTitle = "Íoslódáil" -noFile = "Níl aon chomhad roghnaithe" -payBody = "Íoc do chonradh tiomanta le cárta nó aistriú bainc trí Stripe. Soláthraítear do spás oibre chomh luath agus a ghlantar an íocaíocht." -payCta = "Lean ar aghaidh go Stripe" -payTitle = "Deimhnigh íocaíocht" -requestBodyFree = "Ginimid é seo ar éileamh. Deimhnigh agus seolfaidh d’innealtóir réiteach chugat é go gairid." -requestBodyPaid = "Is breiseán íoctha é seo. Deimhnigh agus déanfaidh d’innealtóir réiteach a raon a shainiú agus an páipéarachas a sheoladh." -requestCta = "Iarr" -requestTitle = "Iarr an cháipéis seo" -signBody = "Osclaíonn sé Comhaontú Stirling Enterprise le haghaidh r-shínithe: clúdaíonn síniú amháin an MSA, foirm ordaithe, EULA agus DPA. Frithshínímid go huathoibríoch agus bogann tú ar aghaidh go híocaíocht." -signCta = "Oscail le síniú" -signTitle = "Athbhreithnigh agus sínigh do chomhaontú" -uploadBody = "Seol do PO chugainn agus eiseoimid sonrasc ina choinne ar do théarmaí. Tarraing isteach an PDF nó roghnaigh comhad thíos." -uploadCta = "Uaslódáil ordú ceannaigh" -uploadTitle = "Uaslódáil d’ordú ceannaigh" [portal.procurement.payment] description = "Glacadh le do luachan agus tá do cheadúnas gníomhach cheana — is féidir le d’fhoireann tosú láithreach. Íoc an chéad sonrasc nuair atá tú réidh; is féidir leat é a íoc nó a íoslódáil anseo, gan ríomhphost de dhíth." +downloadAgreement = "Íoslódáil comhaontú sínithe" downloadInvoice = "Íoslódáil sonrasc" -simulate = "Insamhail íocaíocht faighte (taispeántas)" +eyebrow = "Íocaíocht" title = "Cruthaíodh síntiús" viewInvoice = "Féach & íoc sonrasc" +[portal.procurement.review] +acceptCta = "Glac le luachan" +annual = "Táille bhliantúil (bliain 1)" +downloadCta = "Íoslódáil luachan" +poNumber = "Ordú ceannaigh: {{po}}" +renewal = "Athnuachan ag {{amount}}/bliain tar éis an téarma (+{{pct}}% CPI)" +tcv = "Téarma {{years}} bliana, íoctha ina iomláine roimh ré: {{tcv}}" +validUntil = "Bailí go dtí {{date}}" + [portal.procurement.schedule] fallback = "Níorbh fhéidir an sceidealóir a lódáil." fallbackLink = "Oscail sceidealú i gcluaisín nua" +loading = "Sceidealóir á lódáil..." subtitle = "Treoróidh d’innealtóir réiteach d’fhoireann tríd an gcur i bhfeidhm. Roghnaigh am a oireann duit." title = "Sceideal glao" -[portal.procurement.status] -action = "Gníomh de dhíth" -available = "Ar fáil" -complete = "Críochnaithe" -pending = "Ar feitheamh" -request = "Ar iarratas" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Séalaithe, gan aon aschur" +back = "Siar" +businessName = "Ainm gnó" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Bainistithe, tosú is tapúla" +continue = "Lean ar aghaidh" +deployment = "Cá háit ar cheart é a imscaradh?" +eula = "Aontaím le Stirling EULA & Téarmaí Tráchtála." +fullName = "Ainm iomlán" +fullNamePlaceholder = "D'ainm" +invites = "Tabhair cuireadh do chomhghleacaithe · roghnach" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Sceidealaigh glao" +seats = "Daoine ag tástáil" +seatsPlaceholder = "m.sh. 15" +selfhost = "Féin-óstáilte" +selfhostSub = "Docker nó Kubernetes" +start = "Tosaigh triail" +stepOf = "Céim {{n}} as {{total}}" +subtitle = "Saor in aisce ar feadh 14 lá, níl cárta ag teastáil." +subtitleDetails = "Cúpla sonra do do luachan agus do chomhaontú." +talkFirst = "B'fhearr leat labhairt ar dtús?" +title = "Socraigh do thriail" +viewEula = "Léigh é" +workEmail = "Ríomhphost oibre" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Cuireann síneadh 7 lá leis agus cuireann sé d’innealtóir réiteach ar an eolas." @@ -7808,109 +8690,113 @@ maxed = "Uasmhéadaithe" subtitle = "Maireann do thriail saor in aisce go dtí {{date}}. Níl cárta de dhíth." title = "Triail Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Praghsáil toirte tiomanta, SSO + SCIM + RBAC ar fud na heagraíochta, iniúchadh do-athraithe 90 lá, agus SE tiomnaithe." -homeCta = "Tosaigh Triail →" -homeHeadline = "Próiseáil na milliúin PDFs." - -[portal.recentActivity] -title = "Gníomhaíocht le déanaí" -viewAll = "Féach ar fad" - -[portal.recentActivity.empty] -description = "Beidh ritheanna píblíne, imscarthaí agus teagmhais gníomhairí le feiceáil anseo." -title = "Níl aon rud anseo fós" - -[portal.search] -ariaLabel = "Cuardaigh" -placeholder = "Cuardaigh Stirling — endpoints, píblínte, cáipéisí…" - -[portal.search.empty] -noActionsDescription = "Beidh gníomhartha tapa le feiceáil anseo nuair a bheidh siad ar fáil." -noActionsTitle = "Níl aon ghníomhartha tapa" -noMatches = "Níl aon mheaitseálacha do \"{{query}}\"" -noMatchesDescription = "Bain triail as eochairfhocal eile nó brabhsáil an chatalóg." - [portal.settings.groups] admin = "Riarachán" [portal.settings.sections] account-link = "Nasc cuntais" -[portal.shell.header] -accountFallback = "Cuntas" -accountMenu = "Roghchlár cuntais" -darkMode = "Mód dorcha" -lightMode = "Mód geal" -search = "Cuardaigh" -searchPlaceholder = "Cuardaigh…" -signOut = "Sínigh amach" -switchToDark = "Athraigh go téama dorcha" -switchToLight = "Athraigh go téama geal" - [portal.shell.sidebar] appEditor = "Eagarthóir" appProcessor = "Próiseálaí" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} cháipéis" linkAccount = "Nasc cuntas Stirling" -planEditor = "Plean Eagarthóra" -planEnterprise = "Plean Enterprise" -planProcessor = "Plean Próiseálaí" primaryNav = "Príomh-nascleanúint" switchApp = "Athraigh aip" +[portal.shell.topbar] +closeNav = "Dún nascleanúint" +openNav = "Oscail nascleanúint" +search = "Cuardaigh" + [portal.sources] subtitle = "Naisc ionchuir in-athúsáidte a chuireann cáipéisí isteach i Stirling. Cumraigh ceangal uair amháin, ansin tagair dó ó aon líon beartas. Cliceáil ró dá chumraíocht agus na beartais a úsáideann é." title = "Foinsí" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Ceangail foinse" +[portal.sources.builder] +back = "Ar ais chuig foinsí" +backToTypes = "Gach cineál foinse" +cancel = "Cealaigh" +chooseHint = "Roghnaigh cá as a dtagann doiciméid. Tá nascóirí liathaithe ar an mbealach." +comingSoon = "Ag teacht go luath" +comingSoonHeading = "Ag teacht go luath" +create = "Cruthaigh foinse" +createTitle = "Ceangail foinse" +delete = "Scrios" +editTitle = "Cuir foinse in eagar" +enabled = "Cumasaithe" +save = "Sábháil athruithe" + +[portal.sources.builder.folderAccess] +description = "Ní féidir le huathoibrithe fillteáin ach fillteáin atá ceadaithe ag riarthóir a úsáid. Cuir leis é faoi shocruithe Rochtana Fillteáin, ansin bain triail eile as." +openSettings = "Socruithe Rochtana Fillteáin" +title = "Níl an fillteán seo ceadaithe" + [portal.sources.delete] body = "Scrios \"{{name}}\"? Ní féidir é seo a chealú. Bheadh ar bheartais a thagraíonn dó a bheith nuashonraithe." cancel = "Cealaigh" confirm = "Scrios" title = "Scrios foinse?" -[portal.sources.detail] -closeAriaLabel = "Dún sonraí" -delete = "Scrios foinse" -docs24h = "24h anuas" -docs30d = "30 lá anuas" -docsTotal = "Iomlán feicthe" -docsTrend = "Cáipéisí le 30 lá anuas" -documents = "Cáipéisí" -edit = "Cuir in eagar" -notReferenced = "Ní thagraíonn aon bheartas dó, mar sin tá sé sábháilte é a scriosadh." -pause = "Cuir ar sos" -resume = "Lean ar aghaidh" -subtitle = "{{type}} · {{status}}" -usedBy = "Úsáidte ag" - -[portal.sources.empty] -description = "Ceangail fillteán (agus, go luath, stóráil scamall) ionas go mbeidh áit ag do bheartais le cáipéisí a tharraingt uaithi." -title = "Níl aon fhoinsí ceangailte fós" - [portal.sources.kpi] inUse = "In úsáid" total = "Naisc" unused = "Gan úsáid" +[portal.sources.networkFields.connection] +helperText = "An nasc stóráilte le seoladh an fhreastalaí agus dintiúir. Athúsáidtear é ag gach foinse a thagraíonn dó." +label = "Nasc" + +[portal.sources.networkFields.directory] +helperText = "Fillteán ar an bhfreastalaí le polláil, i gcoibhneas leis an mbaile logála isteach nó fréamh an chomhroinnte. Fág bán don fhréamh." +label = "Fillteán" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Baineann Consume gach comhad den fhreastalaí nuair a bhíonn gach polasaí tar éis é a phróiseáil." +label = "Mód léitheoireachta" + +[portal.sources.networkFields.mode.options] +consume = "Consume: próiseáil gach comhad uair amháin" +snapshot = "Snapshot: athléigh an fillteán gach rith" + +[portal.sources.networkFields.recursive] +label = "Doimhneacht fillteáin" + +[portal.sources.networkFields.recursive.options] +all = "Cuir fofhillteáin san áireamh" +top = "Barrleibhéal amháin" + [portal.sources.status] active = "Gníomhach" disabled = "Díchumasaithe" unused = "Gan úsáid" [portal.sources.table] +documents = "Doiciméid" source = "Foinse" status = "Stádas" +type = "Type" usedBy = "Beartais" +[portal.sources.types.box] +description = "Tarraing doiciméid ó fhillteáin Box atá roinnte ag d'fhoireann cheana féin." +label = "Box" + +[portal.sources.types.dropbox] +description = "Tarraing doiciméid ó fhillteán Dropbox comhroinnte." +label = "Dropbox" + [portal.sources.types.editor] +description = "Doiciméid atá próiseáilte ag d'fhoireann san eagarthóir, thar ritheanna polasaí agus AI." label = "Eagarthóir" +noPolicies = "Níor rith aon pholasaithe ón eagarthóir fós." + +[portal.sources.types.email] +description = "Tiontaigh ceangaltáin a sheoltar chuig bosca ríomhphoist tiomnaithe ina ndoiciméid." +label = "Bosca isteach ríomhphoist" [portal.sources.types.folder] description = "Faire ar chomhadlann ar an bhfreastalaí le haghaidh cáipéisí nua." @@ -7943,25 +8829,80 @@ label = "Doimhneacht fillteáin" all = "Cuir fofhillteáin san áireamh" top = "Barrleibhéal amháin" +[portal.sources.types.ftp] +description = "Polláil fillteán freastalaí FTP nó FTPS le haghaidh doiciméad nua." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Tarraing doiciméid ó fhillteáin Google Drive comhroinnte." +label = "Google Drive" + +[portal.sources.types.network] +description = "Faire ar chomhroinnt líonra SMB le haghaidh doiciméad nua." +label = "Tiomántán líonra" + +[portal.sources.types.onedrive] +description = "Faire ar fhillteáin OneDrive le haghaidh doiciméad nua." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Tarraing doiciméid ó bhuicéad Amazon S3 nó buicéad comhoiriúnach le S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "An nasc stóráilte ina bhfuil an buicéad agus dintiúir. Athúsáidtear é ag gach foinse agus aschur píblíne a thagraíonn dó." +label = "Nasc" + +[portal.sources.types.s3.fields.mode] +helperText = "Baineann Consume gach réad den bhuicéad nuair a bhíonn gach polasaí tar éis é a phróiseáil." +label = "Mód léitheoireachta" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: próiseáil gach réad uair amháin" +snapshot = "Snapshot: athléigh an buicéad gach rith" + +[portal.sources.types.s3.fields.prefix] +helperText = "Ní phróiseáiltear ach réada a dtosaíonn a n-eochracha leis an réimír seo." +label = "Réimír eochrach" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Polláil fillteán titim SFTP le haghaidh doiciméad nua." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Tarraing doiciméid ó leabharlanna doiciméad SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Foinse" +[portal.sources.types.webhook] +createNote = "Gintear URL seachadta agus rún sínithe nuair a chruthaíonn tú an webhook seo. Seolann seoltóirí doiciméid le POST chuig an URL sin agus ritheann na polasaithe tagartha ar theacht dóibh." +description = "Faigh doiciméid trí HTTP POST sínithe ó aon chóras seachtrach." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL seachadta" +secretNote = "Taispeántar an rún sínithe uair amháin, nuair a chruthaítear an webhook. Athchruthaigh an fhoinse chun é a rolladh." + +[portal.sources.types.webhook.reveal] +copy = "Cóipeáil" +done = "Déanta" +secret = "Rún sínithe" +secretHelp = "Sínigh corp amh gach seachadta leis an eochair seo (HMAC-SHA256) agus seol é mar cheanntásc X-Stirling-Signature." +secretWarning = "Cóipeáil an rún sínithe anois. Ar mhaithe le do shlándáil, ní thaispeántar é ach uair amháin agus ní féidir é a aisghabháil níos déanaí." +title = "Webhook cruthaithe" +url = "URL seachadta" +usage = "Déan POST ar gach doiciméad mar chorp amh an iarratais chuig an URL seachadta le cineál ábhair binary (application/pdf nó application/octet-stream). Ritheann polasaithe tagartha go huathoibríoch ar theacht dóibh." + [portal.sources.wizard] -back = "Ar ais" -cancel = "Cealaigh" -continue = "Lean ar aghaidh" -editTitle = "Cuir foinse in eagar" name = "Ainm" namePlaceholder = "m.sh. Glacadh éileamh" -save = "Sábháil athruithe" -subtitle = "Céim {{current}} as {{total}} · {{label}}" -title = "Ceangail foinse" type = "Cineál" -[portal.sources.wizard.steps] -chooseType = "Roghnaigh cineál" -configure = "Cumraigh" -review = "Athbhreithnigh & ceangail" +[portal.stepModal] +close = "Dún" [portal.usage] managePayment = "Bainistigh Íocaíocht" @@ -8021,16 +8962,9 @@ summary = "Is leis/léi foireann — bainistíonn sé/sí acmhainní a ball agus 2 = "Rochtain portal tríd an mbeartas réamhshocraithe" 3 = "Gach rud is féidir le Ball a dhéanamh" -[portal.welcome] -ariaLabel = "Fáilte go Stirling PDF" -badge = "Open-source" -installEditor = "Suiteáil an tEagarthóir" -inviteTeammates = "Tabhair cuireadh do chomhpháirtithe foirne" -openInBrowser = "Oscail sa bhrabhsálaí" -perks = "Saor go deo · Self-hostable" -subtitle = "Tá an PDF Editor is sláine ar domhan saor in aisce d’fhoirne de gach méid. Áirítear 60+ oibríocht PDF agus SSO." -title = "Fáilte go" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Gan teorainn" [printFile] title = "Priontáil Comhad" @@ -8613,6 +9547,14 @@ text = "Rothlaigh leathanaigh do PDF deiseal nó tuathalach i méaduithe 90 céi [rotate.tooltip.header] title = "Forléargas ar Shocruithe Rothlaithe" +[ruler] +clearAll = "Glan uile" +hideAllLabels = "Folaigh gach lipéad" +hideSmallLabels = "Folaigh lipéid bheaga" +physicalValues = "Fisiciúil" +scaled = "Scálaithe" +showAllLabels = "Taispeáin gach lipéad" + [sanitize] desc = "Bain eilimintí a d’fhéadfadh a bheith dochrach as comhaid PDF." sanitizationResults = "Torthaí Sláintiúcháin" @@ -8654,31 +9596,45 @@ label = "Bain Meiteashonraí XMP" [sanitize.steps] settings = "Socruithe" -[sanitizePdf] -tags = "glan, slán, sábháilte, bain bagairtí" - [sanitizePDF] title = "PDF sláintíocht" +[sanitizePdf] +tags = "glan, slán, sábháilte, bain bagairtí" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Coigeartaigh scála an leathanaigh" +[scaleSettings] +activeScale = "Scála Gníomhach" +apply = "Cuir Scála i bhFeidhm" +applyCalibration = "Cuir Calabrú i bhFeidhm" +calculatedScale = "Scála ríofa" +calibrate = "Calabraigh" +calibrating = "Á chalabrú" +calibrationDistanceRequired = "Cuir isteach fad fíorshaoil atá níos mó ná náid" +calibrationInvalid = "Ní féidir scála a ríomh ón tomhas seo" +calibrationPaperDistance = "Fad tomhaiste an leathanaigh: {{distance}}" +calibrationTitle = "Calabraigh Scála" +calibrationTooltip = "Tomhais fad aitheanta chun an scála a ríomh go huathoibríoch" +customScale = "Scála Saincheaptha" +noneSet = "Níl aon scála saincheaptha socraithe" +presets = "Scálaí Réamhshocraithe" +ratio = "Cóimheas Scála" +ratioHelp = "Cóimheas: 1 aonad leathanaigh = X aonad fíorshaoil" +ratioPlaceholder = "m.sh., 100" +ratioPositive = "Caithfidh an cóimheas a bheith níos mó ná náid" +ratioRequired = "Tá cóimheas riachtanach" +realDistance = "Fad fíorshaoil" +realDistancePlaceholder = "m.sh., 5" +reset = "Athshocraigh go Réamhshocruithe" +unit = "Aonad" +unitInvalid = "Aonad neamhbhailí: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Tairseach Uillinn:" -10 = "Socraíonn sé méid na teorann a chuirtear leis agus a bhaintear chun teorainneacha bán a chosc san aschur (réamhshocraithe: 1)." -2 = "Socraíonn sé an uillinn iomlán íosta a theastaíonn chun an íomhá a rothlú (réamhshocraithe: 10)." -3 = "Caoinfhulaingt:" -4 = "Cinneann an raon éagsúlachta dath timpeall an dath cúlra measta (réamhshocraithe: 30)." -5 = "Achar Íosta:" -6 = "Socraíonn sé an tairseach achair íosta le haghaidh grianghraf (réamhshocraithe: 10000)." -7 = "Íos-Limistéar Comhrianta:" -8 = "Socraíonn sé an tairseach íosta achar comhrianta le haghaidh grianghraf" -9 = "Méid na Teorann:" - [scannerImageSplit] submit = "Bain Scananna Íomhá" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Cad a dhéanann sé seo" whatThisDoesDesc = "Aimsíonn agus bainann sé gach grianghraf ó leathanach scanta nó íomhá chomhdhéanta go huathoibríoch—gan barradh láimhe." whenToUse = "Cathain le húsáid" +[ScannerImageSplit.selectText] +1 = "Tairseach Uillinn:" +10 = "Socraíonn sé méid na teorann a chuirtear leis agus a bhaintear chun teorainneacha bán a chosc san aschur (réamhshocraithe: 1)." +2 = "Socraíonn sé an uillinn iomlán íosta a theastaíonn chun an íomhá a rothlú (réamhshocraithe: 10)." +3 = "Caoinfhulaingt:" +4 = "Cinneann an raon éagsúlachta dath timpeall an dath cúlra measta (réamhshocraithe: 30)." +5 = "Achar Íosta:" +6 = "Socraíonn sé an tairseach achair íosta le haghaidh grianghraf (réamhshocraithe: 10000)." +7 = "Íos-Limistéar Comhrianta:" +8 = "Socraíonn sé an tairseach íosta achar comhrianta le haghaidh grianghraf" +9 = "Méid na Teorann:" + [search] noResults = "Níor aimsíodh torthaí" placeholder = "Iontráil téarma cuardaigh..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Tá do fhreastalaí Stirling-PDF as líne agus níl \ [session] expired = "Tá do sheisiún imithe in éag. Athnuaigh an leathanach agus bain triail eile as." -[sessionManagement.tooltip] -header = "Bainistiú Seisiúin Shínithe" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Cuirtear rannpháirtithe nua leis ag deireadh an orduithe sínithe" -bullet2 = "Ní féidir rannpháirtithe a chur leis tar éis an seisiún a bheith críochnaithe" -bullet3 = "Faigheann gach rannpháirtí fógra nuair is é a seal é" -description = "Is féidir leat níos mó rannpháirtithe a chur le seisiún gníomhach am ar bith roimh an gcríochniú." -title = "Rannpháirtithe a Chur Leis" - -[sessionManagement.tooltip.finalization] -bullet1 = "Críochnú iomlán: Tá gach rannpháirtí tar éis síniú" -bullet2 = "Críochnú páirteach: Níl cuid de na rannpháirtithe tar éis síniú fós" -bullet3 = "Ní áireofar rannpháirtithe gan síniú sa doiciméad deiridh" -bullet4 = "Nuair a bheidh sé críochnaithe, is féidir an PDF sínithe a lódáil isteach sna comhaid ghníomhacha" -description = "Comhcheanglaíonn an críochnú gach síniú isteach i PDF sínithe amháin. Ní féidir an gníomh seo a chur ar ceal." -title = "Críochnú an tSeisiúin" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Ní féidir rannpháirtithe a bhaint a bhfuil síniú déanta acu cheana" -bullet2 = "Ní bhfaighidh rannpháirtithe bainte fógraí a thuilleadh" -bullet3 = "Coigeartaítear ord an tsínithe go huathoibríoch" -description = "Is féidir rannpháirtithe a bhaint as seisiúin sula síníonn siad." -title = "Rannpháirtithe a Bhaint" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Cuirtear gach síniú i bhfeidhm ar an PDF go seicheamhach" -bullet2 = "Is féidir le sínitheoirí níos déanaí sínithe níos luaithe a fheiceáil" -bullet3 = "Riachtanach do shreabha oibre formheasa agus do shlabhraí coimeádta dlíthiúla" -description = "Socraíonn an t-ord a shonraíonn tú nuair a chruthaíonn tú an seisiún cé a shíníonn ar dtús." -title = "Ord an tSínithe" - [settings] close = "Dún" +[settings.ai] +documents = "Doiciméid & RAG" +general = "Ginearálta" +limits = "Teorainneacha & Feidhmíocht" +models = "Samhlacha & Soláthraithe" +title = "AI" + [settings.configuration] advanced = "Ardroghanna" database = "Bunachar Sonraí" endpoints = "Deirphointí" features = "Gnéithe" +folderAccess = "Rochtain ar Fhillteáin" mcp = "MCP Server" storageSharing = "Stóráil Comhad agus Comhroinnt" systemSettings = "Socruithe Córais" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Eagarthóir PDF réamhshocraithe" defaultPdfEditorActive = "Is é Stirling PDF d’eagarthóir PDF réamhshocraithe" defaultPdfEditorChecking = "Á sheiceáil..." defaultPdfEditorInactive = "Tá feidhmchlár eile socraithe mar réamhshocrú" +defaultPdfEditorRemind = "Cuir i gcuimhne dom é a shocrú mar réamhshocrú" +defaultPdfEditorRemindDescription = "Taispeáin meirge nuair nach é Stirling PDF d'fheidhmchlár PDF réamhshocraithe." defaultPdfEditorSet = "Réamhshocraithe Cheana" defaultStartupView = "Amharc réamhshocraithe ar thosú" defaultStartupViewDescription = "Roghnaigh cén cluaisín a bheidh gníomhach sa cholún clé nuair a thosaíonn an aip" @@ -8817,9 +9763,18 @@ languageDescription = "Roghnaigh an teanga taispeána" logout = "Logáil amach" setAsDefault = "Socraigh mar Réamhshocraithe" theme = "Téama" +themeAccent = "Dath béime" +themeAccentCustom = "Dath saincheaptha" +themeAccentCustomHint = "Saincheaptha (moltar na réamhshocruithe)" +themeAccentDark = "Dath béime i mód dorcha" +themeAccentDefault = "Réamhshocrú" +themeAccentDefaultHint = "Téama réamhshocraithe (molta)" +themeAccentDescription = "Leanann cnaipí, naisc agus buaicphointí é, agus cuireann sé tint chaolchúiseach ar an aip. Tá ceann ar leith ag an mód éadrom agus ag an mód dorcha. Úsáideann an córas cibé ceann atá gníomhach." +themeAccentLight = "Dath béime i mód éadrom" themeDark = "Dorcha" themeDescription = "Athraigh idir mód geal agus mód dorcha" themeLight = "Geal" +themeReset = "Cuir an téama ar ais chuig an réamhshocrú" themeSystem = "System" title = "Ginearálta" user = "Úsáideoir" @@ -8833,6 +9788,12 @@ intro = "Cumasaigh fíordheimhniú úsáideoirí, bainistíocht foirne, agus gn learnMore = "Tuilleadh eolais sa doiciméadú" title = "Do Riarthóirí Córais" +[settings.general.loginLanding] +description = "Roghnaigh cén aip a osclaítear nuair a shíníonn tú isteach i Stirling Cloud." +editor = "Eagarthóir" +processor = "Próiseálaí" +title = "Tar éis síniú isteach" + [settings.general.mode] fullscreen = "Lánscáileán" sidebar = "Barra taoibh" @@ -9013,9 +9974,6 @@ title = "Polasaithe agus Príobháideachas" [settings.preferences] title = "Sainroghanna" -[settings.search] -placeholder = "Déan cuardach ar leathanaigh socruithe..." - [settings.security] description = "Nuashonraigh do phasfhocal chun do chuntas a choinneáil slán." title = "Slándáil" @@ -9041,6 +9999,11 @@ security = "Slándáil" telegram = "Telegram" title = "Slándáil agus Fíordheimhniú" +[settings.signOut] +confirm = "An bhfuil tú cinnte gur mhaith leat síniú amach?" +submit = "Sínigh amach" +title = "Sínigh amach" + [settings.team] title = "Foireann" @@ -9156,6 +10119,7 @@ last = "An leathanach deiridh" maintainRatio = "Scoránaigh, coinnigh an cóimheas gné" next = "An chéad leathanach eile" noSavedSigs = "Níor aimsíodh aon síniú sábháilte" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Sínithe Pearsanta" previous = "Leathanach roimhe seo" redo = "Athdhéan" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Theip ar bhaint an chúlra" hint = "Uaslódáil íomhá PNG nó JPG de do shíniú" label = "Uaslódáil íomhá sínithe" placeholder = "Roghnaigh comhad íomhá" +previewAlt = "Current image signature" processing = "Íomhá á phróiseáil..." removeBackground = "Bain an cúlra bán (déan trédhearcach)" @@ -9207,6 +10172,17 @@ saved = "Roghnaigh síniú sábháilte thuas, ansin cliceáil áit ar bith ar an text = "Tar éis duit d’ainm a chur isteach thuas, cliceáil áit ar bith ar an PDF chun do shíniú a chur." title = "Conas síniú a chur leis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Bog an Síniú" pause = "Cuir suíniú ar sos" @@ -9653,10 +10629,30 @@ title = "Uaslódáil chuig an bhfreastalaí" updateButton = "Nuashonraigh ar an bhfreastalaí" uploadButton = "Uaslódáil chuig an bhfreastalaí" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "cliceáil anseo" +fallback = "Mura n-osclaíodh é go huathoibríoch," +opening = "Swagger UI á oscailt i gcluaisín nua..." tags = "api,doiciméadú,swagger,críochphointí,forbairt" title = "Doiciméadú API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Roghnaigh comhaid i gComhaid Gníomhacha chun an uirlis seo a singleFileScope = "Á chur i bhfeidhm ar: {{fileName}} amháin" viewerMode = "Athraigh go dtí an t-eagarthóir comhad chun roinnt comhad a roghnú." +[toolFormat] +ANY = "comhad ar bith" +CBR = "cartlann leabhair ghrinn (CBR)" +CBZ = "cartlann leabhair ghrinn (CBZ)" +CSV = "comhad CSV" +EBOOK = "ríomhleabhar" +EMAIL = "ríomhphost" +EXCEL = "scarbhileog" +HTML = "leathanach gréasáin" +IMAGE = "íomhánna" +JAVASCRIPT = "comhad JavaScript" +JSON = "comhad JSON" +MARKDOWN = "comhad Markdown" +NONE = "gan chomhad" +PCL = "comhad priontála PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF cosanta le pasfhocal" +POSTSCRIPT = "comhad PostScript" +PPT = "cur i láthair" +TEXT = "comhad téacs" +VIDEO = "físeán" +WORD = "doiciméad Word" +XML = "comhad XML" +XPS = "doiciméad XPS" +ZIP = "cartlann ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Ar ais go dtí na huirlisí go léir" collapse = "Laghdaigh painéal" expand = "Leathnaigh painéal" goBack = "Téigh ar ais" +pdfTools = "Uirlisí PDF" placeholder = "Roghnaigh uirlis chun tosú" premiumFeature = "Gné Préimhe:" toolsHeader = "Uirlisí" @@ -9806,11 +10829,6 @@ noToolsFound = "Níor aimsíodh uirlisí" quickAccess = "ROCHTAIN THAPA" searchPlaceholder = "Cuardaigh uirlisí..." -[toolPicker.categories] -advancedTools = "Uirlisí Casta" -recommendedTools = "Uirlisí Molta" -standardTools = "Uirlisí Caighdeánacha" - [toolPicker.subcategories] advancedFormatting = "Formáidiú Casta" automation = "Uathoibriú" @@ -9824,6 +10842,9 @@ removal = "Baint" signing = "Síniú" verification = "Fíorú" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Níor aimsíodh uirlisí" noTools = "Níl aon uirlisí ar fáil" @@ -9950,28 +10971,53 @@ title = "Úsáideoirí" you = "(tusa)" [users.action] +cancelInvite = "Cealaigh cuireadh" deleteTeam = "Scrios foireann" disableMfa = "Athshocraigh MFA" move = "Bog chuig foireann" reinstate = "Athbhunaigh" remove = "Bain ón eagraíocht" +removeTeam = "Bain den fhoireann" rename = "Athainmnigh foireann" resetPw = "Athshocraigh pasfhocal" suspend = "Cuir ar fionraí" unlock = "Díghlasáil cuntas" +[users.activity] +daysAgo = "{{count}} lá ó shin" +hoursAgo = "{{count}} uair ó shin" +justNow = "Anois díreach" +minutesAgo = "{{count}} nóim ó shin" +monthsAgo_one = "{{count}} mhí ó shin" +monthsAgo_other = "{{count}} mí ó shin" +never = "Riamh" +weeksAgo_one = "{{count}} seachtain ó shin" +weeksAgo_other = "{{count}} seachtain ó shin" +yearsAgo_one = "{{count}} bhliain ó shin" +yearsAgo_other = "{{count}} bliana ó shin" + [users.cap] addProcessor = "+ Próiseálaí" approver = "Faomhann beartas" editor = "Eagarthóir" processor = "Próiseálaí" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Cealaigh an cuireadh chuig {{email}}? Ní bheidh siad in ann dul isteach leis an nasc reatha." +cancelInviteTitle = "Cealaigh cuireadh" deleteTeamBody = "Scrios an fhoireann {{name}}? Caithfidh an fhoireann a bheith folamh ar dtús - bog a baill chuig foireann eile, agus ní féidir léi aon chumraíochtaí comhtháthaithe a bheith fós ina húinéireacht." deleteTeamTitle = "Scrios foireann" disableMfaBody = "Bain clárú MFA {{name}}? Socróidh siad arís é ar an gcéad logáil isteach eile má tá gá leis." disableMfaTitle = "Athshocraigh MFA" removeBody = "Bain {{name}} go buan ón eagraíocht? Ní féidir é seo a chealú." +removeTeamBody = "Bain {{name}} den fhoireann? Coinneoidh siad a gcuntas ach caillfidh siad rochtain ar acmhainní na foirne seo." removeTitle = "Bain ball" [users.empty] @@ -9982,10 +11028,8 @@ title = "Níl aon bhaill fós" addToTeam = "Cuir le foireann" guestCount = "{{count}} aoi" guests = "Aíonna" -guestsDesc = "Comhoibrithe seachtracha, teoranta don rud a chomhroinn tú. Eagarthóir amháin." ledBy = "faoi stiúir {{owner}}" org = "Eagraíocht" -orgDesc = "Úinéirí le húdarás ar fud na heagraíochta agus faomhadh beartas" owners = "{{count}} úinéir" team = "foireann {{name}}" teamMeta = "{{count}} duine" @@ -10022,6 +11066,18 @@ username = "Ainm úsáideora" usernameError = "Caithfidh an t-ainm úsáideora a bheith 3 charachtar ar a laghad" usernamePlaceholder = "jsmith" +[users.invites] +by = "Cuireadh ó {{who}}" +cancel = "Cealaigh" +expiresInDays_one = "Rachaidh sé in éag i gceann {{count}} lá" +expiresInDays_other = "Rachaidh sé in éag i gceann {{count}} lá" +expiresToday = "Rachaidh sé in éag inniu" +title = "Cuirí ar feitheamh" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Chuaigh rud éigin mícheart agus an backend á bhaint amach, nó níl rochtain agat. Bain triail eile as." title = "Níorbh fhéidir baill a lódáil" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Cuardaigh iatáin" title = "Iatáin" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Teideal an leabharmhairc" +bookmarkTitleRequired = "Tá teideal an leabharmhairc riachtanach" closeSidebar = "Dún barra taoibh na leabharmharcanna" collapseAll = "Laghdaigh gach leabharmharc" +empty = "No bookmarks in this document" expandAll = "Leathnaigh gach leabharmharc" +searchPlaceholder = "Cuardaigh leabharmharcanna" +title = "Bookmarks" [viewer.comments] addComment = "Cuir trácht leis" @@ -10236,10 +11297,12 @@ locateAnnotation = "Aimsigh sa doiciméad" moreActions = "Tuilleadh gníomhartha" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Leathanach {{page}}" placingHint = "Cliceáil ar leathanach chun é a chur… (cealaigh)" removeCommentOnly = "Bain an trácht amháin" saveReply = "Sábháil freagra" +searchPlaceholder = "Search comments" title = "Tráchtanna" typeComment = "Trácht" typeInsertText = "Ionsáigh Téacs" @@ -10262,6 +11325,7 @@ unsavedDesc = "Tá athruithe gan sábháil agat" closeSidebar = "Dún barra taoibh na sraitheanna" hideAll = "Folaigh gach sraith" showAll = "Taispeáin gach sraith" +title = "Layers" [viewer.link] delete = "Scrios nasc" @@ -10605,30 +11669,6 @@ title = "Téacs an Chomhartha Uisce" image = "Íomhá" text = "Téacs" -[wetSignature.tooltip] -header = "Modhanna chun síniú a chruthú" - -[wetSignature.tooltip.draw] -bullet1 = "Saincheap dath agus tiús an pheann" -bullet2 = "Glan agus tarraing arís go dtí go mbeidh tú sásta" -bullet3 = "Oibríonn ar ghléasanna tadhaill (táibléid, fóin)" -description = "Cruthaigh síniú lámhscríofa le do luchóg nó le do scáileán tadhaill. Is fearr é do shínithe pearsanta, barántúla." -title = "Tarraing síniú" - -[wetSignature.tooltip.type] -bullet1 = "Roghnaigh as iliomad clónna" -bullet2 = "Saincheap méid agus dath an téacs" -bullet3 = "Foirfe do shínithe caighdeánaithe" -description = "Gin síniú ó théacs clóscríofa. Tapa agus comhsheasmhach, oiriúnach do cháipéisí gnó." -title = "Clóscríobh síniú" - -[wetSignature.tooltip.upload] -bullet1 = "Tacaíonn sé le PNG, JPG, agus formáidí íomhá eile" -bullet2 = "Moltar cúlraí trédhearcacha chun na torthaí is fearr a bhaint amach" -bullet3 = "Athrófar méid na híomhá chun an limistéar sínithe a oiriúnú" -description = "Uaslódáil íomhá sínithe réamhdhéanta. Oiriúnach má tá síniú scanta nó lógó cuideachta agat." -title = "Uaslódáil íomhá sínithe" - [workbenchBar] activeFiles = "Comhaid ghníomhacha" annotations = "Anótálacha" @@ -10642,6 +11682,7 @@ exitRedaction = "Scoir de mhód ceilte" exportAll = "Easpórtáil PDF" exportSelected = "Easpórtáil na leathanaigh roghnaithe" formFill = "Líon foirm" +hideToolbar = "Hide toolbar" multiTool = "Il-uirlis" panMode = "Mód pánála" print = "Priontáil PDF" @@ -10652,6 +11693,7 @@ redact = "Ceil" rotateLeft = "Rothlaigh go clé" rotateRight = "Rothlaigh go deas" ruler = "Rialóir / Tomhas" +rulerSettings = "Socruithe Scála" save = "Sábháil" saveAll = "Sábháil gach comhad" saveAs = "Sábháil mar" @@ -10661,6 +11703,7 @@ selectAll = "Roghnaigh uile" selectByNumber = "Roghnaigh de réir uimhreacha leathanaigh" selectLanguage = "Roghnaigh teanga" share = "Roinn" +showToolbar = "Show toolbar" toggleAnnotations = "Scoránaigh infheictheacht na n-anótálacha" toggleAttachments = "Scoránaigh iatáin" toggleBookmarks = "Scoránaigh leabharmharcanna" diff --git a/frontend/editor/public/locales/hi-IN/translation.toml b/frontend/editor/public/locales/hi-IN/translation.toml index 6f19aa1c0b..0e122c9980 100644 --- a/frontend/editor/public/locales/hi-IN/translation.toml +++ b/frontend/editor/public/locales/hi-IN/translation.toml @@ -1,7 +1,6 @@ alphabet = "वर्णमाला" apply = "लागू करें" applyAndContinue = "सहेजें और बाहर निकलें" -areYouSure = "क्या आप वाकई छोड़ना चाहते हैं?" back = "वापस" black = "काला" blue = "नीला" @@ -46,10 +45,8 @@ filesSelected = "फ़ाइलें चयनित" font = "फ़ॉन्ट" fontSizeTooltip = "पॉइंट्स में पृष्ठ संख्या के टेक्स्ट का आकार। बड़ी संख्या बड़ा टेक्स्ट बनाती है।" fontTypeTooltip = "पृष्ठ संख्याओं के लिए फ़ॉन्ट परिवार। अपने दस्तावेज़ शैली के आधार पर चुनें।" -genericSubmit = "जमा करें" green = "हरा" help = "सहायता" -imgPrompt = "छवि(यां) चुनें" incorrectPasswordMessage = "वर्तमान पासवर्ड गलत है।" info = "जानकारी" invalidUndoData = "पूर्ववत नहीं कर सकते: अमान्य ऑपरेशन डेटा" @@ -62,7 +59,6 @@ loginAgreementTitle = "लॉगिन समझौता" logOut = "लॉग आउट" marginTooltip = "पृष्ठ संख्या और पृष्ठ की किनारी के बीच की दूरी।" moreOptions = "अधिक विकल्प" -multiPdfPrompt = "पीडीएफ फ़ाइलें चुनें (2+)" never = "कभी नहीं" no = "नहीं" noFileSelected = "कोई फ़ाइल चयनित नहीं है। कृपया एक अपलोड करें।" @@ -80,7 +76,6 @@ pageOrderPrompt = "कस्टम पृष्ठ क्रम (पृष्ठ pages = "पृष्ठ" pageSelectionPrompt = "कस्टम पृष्ठ चयन (पृष्ठ संख्याओं 1,5,6 या 2n+1 जैसे फ़ंक्शन की अल्पविराम से अलग सूची दर्ज करें):" password = "पासवर्ड" -pdfPrompt = "पीडीएफ फ़ाइल(ें) चुनें" pin = "फ़ाइल पिन करें (टूल चलने के बाद सक्रिय रखें)" pro = "प्रो" processingComplete = "आपकी फ़ाइल तैयार है।" @@ -110,6 +105,7 @@ undoSuccess = "ऑपरेशन सफलतापूर्वक पूर् unknown = "अज्ञात" unpin = "फ़ाइल अनपिन करें (टूल चलने के बाद प्रतिस्थापित करें)" unsavedChanges = "आपके PDF में असहेजे परिवर्तन हैं।" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "असहेजे परिवर्तन" unsupported = "असमर्थित" username = "उपयोगकर्ता नाम" @@ -356,6 +352,7 @@ text = "ये अनुमतियाँ तय करती हैं कि title = "अनुमतियां बदलें" [addStamp] +preview = "स्टैम्प का पूर्वावलोकन" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "मैन्युअल डाउनलोड: लिंक प noLanguages = "कॉन्फ़िगर की गई डायरेक्टरी में कोई tessdata भाषा नहीं मिली।" permissionNotice = "tessdata पाथ पर लिखना संभव नहीं है। डाउनलोड ब्राउज़र में खुलेंगे; कृपया .traineddata फ़ाइलें मैन्युअल रूप से tessdata फ़ोल्डर में सहेजें।" +[admin.settings.ai.documents] +description = "दस्तावेज़ों पर प्रश्नों के उत्तर देने के लिए उपयोग किए जाने वाले एम्बेडिंग मॉडल और रिट्रीवल सेटिंग्स कॉन्फ़िगर करें। सहेजने पर AI engine पर लागू होता है।" +title = "दस्तावेज़ और RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "engine के अपने environment credential का उपयोग करने के लिए खाली छोड़ें। स्वयं-होस्टेड single-engine deployments पर लागू होता है।" +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "सहेजा गया - वर्तमान key रखने के लिए खाली छोड़ें" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI-compatible / Ollama embeddings endpoint का Base URL, जैसे http://ollama:11434/v1। किसी विश्वसनीय internal endpoint की ओर इशारा करना चाहिए (SSRF-संवेदनशील)।" +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding model नाम। मुक्त टेक्स्ट; सुझाव केवल संकेत हैं।" +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "दस्तावेज़ टेक्स्ट को vector embeddings में बदलने के लिए उपयोग किया गया provider।" +label = "Embedding provider" + +[admin.settings.ai.documents.maxSearches] +description = "प्रति request agent द्वारा चलाए जा सकने वाले retrieval searches की अधिकतम संख्या।" +label = "अधिकतम खोजें" + +[admin.settings.ai.documents.reindexNote] +body = "Embedding model बदलना तुरंत प्रभावी होता है, लेकिन पिछले model से indexed दस्तावेज़ों को सही खोज परिणाम लौटाने के लिए फिर से index करना होगा।" +title = "Re-index आवश्यक" + +[admin.settings.ai.documents.saved] +reindexNote = "यदि आपने embedding model बदला है, तो मौजूदा दस्तावेज़ों को re-index करें ताकि खोज नया model उपयोग करे।" + +[admin.settings.ai.documents.topK] +description = "प्रति खोज प्राप्त किए गए सबसे प्रासंगिक chunks की संख्या।" +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling को Python AI engine से कनेक्ट करें और चुनें कि कौन-सी AI क्षमताएँ उपलब्ध कराई जाएँ। बदलाव restart पर लागू होते हैं।" +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "व्यक्तिगत AI सुविधाओं को चालू या बंद करें। अक्षम सुविधाएँ app में छिपी रहती हैं।" +title = "क्षमताएँ" + +[admin.settings.ai.general.enabled] +description = "मुख्य switch। बंद होने पर कोई AI tools, agents, या engine calls उपलब्ध नहीं होते।" +label = "AI सक्षम करें" + +[admin.settings.ai.general.features.chat] +description = "PDFs के साथ काम करने के लिए संवादात्मक assistant।" +label = "Chat assistant" + +[admin.settings.ai.general.features.classify] +description = "दस्तावेज़ों को प्रकार या सामग्री के आधार पर स्वचालित रूप से वर्गीकृत करें।" +label = "दस्तावेज़ वर्गीकरण" + +[admin.settings.ai.general.features.createPdf] +description = "AI agent के माध्यम से नया PDF बनाएँ (जैसे HTML से)।" +label = "prompt से PDF बनाएँ" + +[admin.settings.ai.general.features.documentQuestions] +description = "प्रश्न पूछें और uploaded दस्तावेज़ पर आधारित उत्तर पाएँ।" +label = "दस्तावेज़ प्रश्न" + +[admin.settings.ai.general.features.mathAuditor] +description = "गणितीय और संख्यात्मक त्रुटियों के लिए दस्तावेज़ों की समीक्षा करें।" +label = "Math auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "PDF में AI-लिखित समीक्षा टिप्पणियाँ और annotations जोड़ें।" +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "दस्तावेज़ generation जैसे भारी agent operations के लिए timeout।" +label = "लंबे समय तक चलने वाला timeout (seconds)" + +[admin.settings.ai.general.note] +body = "AI engine एक अलग service के रूप में चलता है। इसका shared secret" +body2 = "container environment variable के माध्यम से सेट किया जाता है। Provider API keys इन pages पर दर्ज की जा सकती हैं या engine environment variables के रूप में दी जा सकती हैं; यहाँ दर्ज keys सहेजने पर engine को भेजी जाती हैं।" +title = "AI engine के बारे में" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "streamed (token-by-token) chat responses के लिए timeout।" +label = "Stream timeout (seconds)" + +[admin.settings.ai.general.test] +button = "connection test करें" +failBody = "AI engine ने response नहीं दिया। URL जाँचें, engine container चल रहा है या नहीं जाँचें, और AI सक्षम है या नहीं जाँचें (सक्षम करने के बाद restart आवश्यक है)।" +failTitle = "AI engine पहुँच योग्य नहीं" +okBody = "AI engine ने health check का response दिया।" +okTitle = "AI engine पहुँच योग्य है" + +[admin.settings.ai.general.timeoutSeconds] +description = "engine के लिए standard AI requests का timeout।" +label = "Request timeout (seconds)" + +[admin.settings.ai.general.url] +description = "Python AI engine का internal URL, जैसे http://stirling-pdf-engine:5001।" +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "AI requests कितना काम कर सकती हैं और कितनी साथ-साथ चल सकती हैं, इसके लिए guardrails। सहेजने पर AI engine पर लागू होता है।" +title = "सीमाएँ और प्रदर्शन" + +[admin.settings.ai.limits.maxCharacters] +description = "Guardrail: ऐसे AI requests अस्वीकार करें जिनका extracted text इस लंबाई से अधिक हो।" +label = "प्रति request अधिकतम characters" + +[admin.settings.ai.limits.maxPages] +description = "Guardrail: इतने PDF pages से अधिक वाले AI requests अस्वीकार करें।" +label = "प्रति request अधिकतम pages" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "पूरे engine में एक साथ चल रहे in-flight model calls की अधिकतम संख्या।" +label = "Model max concurrency" + +[admin.settings.ai.models] +description = "LLM provider और AI engine द्वारा उपयोग किए जाने वाले smart/fast models चुनें। सहेजने पर AI engine पर लागू होता है।" +title = "Models और Providers" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "engine के अपने environment credential का उपयोग करने के लिए खाली छोड़ें। स्वयं-होस्टेड single-engine deployments पर लागू होता है।" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "सहेजा गया - वर्तमान key रखने के लिए खाली छोड़ें" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI-compatible / Ollama endpoint का Base URL, जैसे http://ollama:11434/v1।" +label = "Provider base URL" +warning = "base URL किसी विश्वसनीय internal endpoint की ओर इशारा करना चाहिए। engine इसे server-side requests करेगा, इसलिए अविश्वसनीय value SSRF-संवेदनशील है।" + +[admin.settings.ai.models.fastMaxTokens] +description = "fast model के लिए अधिकतम output tokens।" +label = "Fast model max tokens" + +[admin.settings.ai.models.fastModel] +description = "हल्के tasks के लिए सस्ता, तेज़ model। मुक्त टेक्स्ट; सुझाव केवल संकेत हैं।" +label = "Fast model" + +[admin.settings.ai.models.provider] +description = "engine किस LLM provider से बात करता है।" +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "smart model के लिए अधिकतम output tokens।" +label = "Smart model max tokens" + +[admin.settings.ai.models.smartModel] +description = "जटिल reasoning के लिए उच्च-क्षमता वाला model। मुक्त टेक्स्ट; सुझाव केवल संकेत हैं।" +label = "Smart model" + +[admin.settings.ai.saved] +body = "बदलाव AI engine को स्वचालित रूप से भेज दिए जाते हैं।" +bodyNoPush = "Settings सहेजी गईं। अगली बार जब AI engine अपनी configuration लेगा, तब वे लागू होंगी।" +title = "AI settings सहेजी गईं" + [admin.settings.badge] clickToUpgrade = "प्लान विवरण देखने के लिए क्लिक करें" @@ -938,6 +1094,25 @@ label = "स्टार्टअप पर पुनः जेनरेट क description = "सर्टिफ़िकेट वैध रहने के दिनों की संख्या" label = "सर्टिफ़िकेट वैधता (दिन)" +[admin.settings.folderAccess] +description = "वे directories जिनसे folder sources और folder outputs को पढ़ने और लिखने की अनुमति है। यह एक security boundary है: automations को इस list के बाहर किसी server path पर कभी point नहीं किया जा सकता।" +securityNote = "folder sources और outputs को पूरी तरह अक्षम करने के लिए इसे खाली छोड़ें। Stirling की अपनी configuration directory हमेशा प्रतिबंधित रहती है।" +title = "Folder Access" + +[admin.settings.folderAccess.implied] +description = "Stirling-managed ये directories हमेशा अनुमत हैं और यहाँ बदली नहीं जा सकतीं।" +serverStorage = "Server file storage" +title = "हमेशा अनुमत" +watchedFolder = "Pipeline watched folder" + +[admin.settings.folderAccess.roots] +add = "जोड़ें" +empty = "कोई folders अनुमत नहीं। Folder sources और outputs फिलहाल अक्षम हैं।" +hint = "absolute paths दर्ज करें, उदाहरण के लिए /data/inbox।" +label = "Allowed folder roots" +placeholder = "/data/inbox" +remove = "folder root हटाएँ" + [admin.settings.general] description = "ब्रांडिंग और डिफ़ॉल्ट व्यवहार सहित सिस्टम-व्यापी एप्लिकेशन सेटिंग्स कॉन्फ़िगर करें।" system = "सिस्टम" @@ -1704,6 +1879,9 @@ width = "चौड़ाई" [app] description = "मुफ़्त Adobe Acrobat विकल्प (10M+ डाउनलोड)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b में बदलें" convertToPdfA3bDescription = "एम्बेडेड अटैचमेंट्स के साथ एक आर्काइवल PDF बनाता है" @@ -1842,6 +2020,15 @@ export = "एक्सपोर्ट" accessDenied = "पहुँच अस्वीकृत" insufficientPermissions = "आपके पास यह क्रिया करने की अनुमति नहीं है." +[auth.callback] +completing = "authentication पूरा हो रहा है" +invalidToken = "OAuth login विफल - invalid token।" +missingToken = "OAuth login विफल - कोई token प्राप्त नहीं हुआ।" +oauthFailed = "OAuth login विफल। कृपया फिर से प्रयास करें।" +pleaseWait = "कृपया प्रतीक्षा करें, हम आपका sign in पूरा कर रहे हैं।" +signedOut = "आप sign out हो गए हैं। कृपया फिर से sign in करें।" +windowMayClose = "पूरा होने पर आप यह window बंद कर सकते हैं।" + [auth.displayName] guest = "अतिथि" user = "उपयोगकर्ता" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "JavaScript और एम्बेडेड फ़ाइल description = "यह टूल सामग्री के आधार पर PDF फ़ाइलों का नाम स्वतः बदल देगा। यह दस्तावेज़ का विश्लेषण कर पाठ से सबसे उपयुक्त शीर्षक खोजता है।" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "ऑटो रोटेट" +tags = "ऑटो रोटेट,orientation ठीक करें,उल्टा,बगल की ओर,सीधा करें,orient,orientation detect करें,rotation सही करें,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Confidence threshold" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "OCR only" +text = "Text only" +title = "Detection method" + +[autoRotate.error] +failed = "PDF को auto-rotate करते समय error हुआ।" + +[autoRotate.inferUndetected] +title = "कम confidence pages को मिलान के लिए घुमाएँ" + +[autoRotate.report] +applied = "{{degrees}}° CW" +confidenceValue = "Confidence {{value}}" +noChange = "कोई बदलाव नहीं" +pageLabel = "Page {{number}}" +summary = "{{total}} pages में से {{rotated}} घुमाए गए, {{unchanged}} वैसे ही छोड़े गए" +title = "Detection report" + +[autoRotate.report.method] +inferred = "मिलान किया गया" +none = "छोड़ा गया" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "threshold से कम" +blankPage = "Page खाली है" +inferredFromDocument = "अन्य pages जैसा ही" +noDominantDirection = "मिश्रित text directions" +osdFailed = "पढ़ने योग्य text नहीं मिला" +osdNoVerdict = "कौन-सा भाग ऊपर है, पता नहीं चल सका" +tesseractUnavailable = "OCR installed नहीं है" +tooFewGlyphs = "बहुत कम text" + +[autoRotate.results] +title = "Auto Rotate Results" + +[autoRotate.settings] +title = "Settings" + +[autoRotate.tooltip.confidenceThreshold] +text = "page को घुमाने से पहले check को कितना निश्चित होना चाहिए। अधिक pages घुमाने के लिए इसे घटाएँ, अधिक pages को untouched छोड़ने के लिए बढ़ाएँ।" +title = "Confidence threshold" + +[autoRotate.tooltip.description] +text = "यह पता लगाता है कि प्रत्येक page किस दिशा में होना चाहिए और उसे सही दिशा में घुमा देता है। जो pages पहले से सीधे हैं, उन्हें वैसे ही छोड़ दिया जाता है।" + +[autoRotate.tooltip.detectionMode] +text = "Auto पहले page पर text पढ़ता है, फिर यदि वह पर्याप्त न हो तो page को image के रूप में देखता है। Text only image check को छोड़ देता है, जो उन documents के लिए सबसे तेज़ है जिन्हें कभी scan नहीं किया गया था। OCR only हर page को image मानता है।" +title = "Detection method" + +[autoRotate.tooltip.header] +title = "Auto Rotate" + +[autoRotate.tooltip.inferUndetected] +text = "खाली या लगभग खाली pages को अपने-आप check नहीं किया जा सकता। जब पढ़े जा सकने वाले हर page को समान turn की आवश्यकता होती है, तो इन pages को उनसे match करने के लिए घुमा दिया जाता है।" +title = "कम confidence pages को मिलान के लिए घुमाएँ" + [autoSizeSplitPDF] tags = "pdf,विभाजन,दस्तावेज़,व्यवस्था" @@ -2189,33 +2443,6 @@ keywords = "कीवर्ड: odd, even" numbers = "संख्याएँ/रेंज: 5, 10-20" progressions = "प्रोग्रेशन: 3n, 4n+1" -[certificateChoice.tooltip] -header = "प्रमाणपत्र प्रकार" - -[certificateChoice.tooltip.organization] -bullet1 = "सिस्टम प्रशासकों द्वारा प्रबंधित" -bullet2 = "अधिकृत उपयोगकर्ताओं के बीच साझा" -bullet3 = "व्यक्ति नहीं, कंपनी की पहचान दर्शाता है" -bullet4 = "उपयुक्त: आधिकारिक दस्तावेज़, टीम हस्ताक्षर" -description = "आपके संगठन द्वारा प्रदान किया गया एक साझा प्रमाणपत्र। कंपनी-स्तर के हस्ताक्षर अधिकार के लिए उपयोग होता है।" -title = "संगठन प्रमाणपत्र" - -[certificateChoice.tooltip.personal] -bullet1 = "पहली बार उपयोग पर स्वतः जनरेट होता है" -bullet2 = "आपके उपयोगकर्ता खाते से सम्बद्ध" -bullet3 = "अन्य उपयोगकर्ताओं के साथ साझा नहीं किया जा सकता" -bullet4 = "उपयुक्त: व्यक्तिगत दस्तावेज़, व्यक्तिगत उत्तरदायित्व" -description = "आपके उपयोगकर्ता खाते के लिए विशिष्ट स्वतः-जनरेटेड प्रमाणपत्र। व्यक्तिगत हस्ताक्षरों के लिए उपयुक्त।" -title = "व्यक्तिगत प्रमाणपत्र" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX फ़ाइल और पासवर्ड आवश्यक" -bullet2 = "बाहरी Certificate Authorities द्वारा जारी किया जा सकता है" -bullet3 = "कानूनी दस्तावेज़ों के लिए उच्च विश्वसनीयता" -bullet4 = "उपयुक्त: कानूनी रूप से बाध्यकारी अनुबंध, बाहरी सत्यापन" -description = "अपनी PKCS#12 प्रमाणपत्र फ़ाइल का उपयोग करें। प्रमाणपत्र गुणों पर पूर्ण नियंत्रण देता है।" -title = "कस्टम P12 अपलोड करें" - [certSign] awaitingSignatures = "हस्ताक्षरों की प्रतीक्षा" chooseCertificate = "प्रमाणपत्र फ़ाइल चुनें" @@ -2542,34 +2769,6 @@ server = "सर्वर" stepTitle = "प्रमाणपत्र स्रोत" upload = "अपलोड" -[certSign.tooltip.header] -title = "हस्ताक्षरों के प्रबंधन के बारे में" - -[certSign.tooltip.overview] -bullet1 = "मौजूदा हस्ताक्षर और उनकी वैधता जांचें" -bullet2 = "हस्ताक्षरकर्ता और प्रमाणपत्रों के विस्तृत विवरण देखें" -bullet3 = "अपने दस्तावेज़ सुरक्षित करने के लिए नए डिजिटल हस्ताक्षर जोड़ें" -bullet4 = "आसान नेविगेशन के साथ एकाधिक फ़ाइलें समर्थित" -text = "यह टूल आपको जांचने देता है कि आपके PDF डिजिटल रूप से साइन हैं या नहीं और नए डिजिटल हस्ताक्षर जोड़ने देता है। डिजिटल हस्ताक्षर साबित करते हैं कि दस्तावेज़ किसने बनाया या अनुमोदित किया और साइन करने के बाद उसमें बदलाव हुआ या नहीं दिखाते हैं।" -title = "यह टूल क्या कर सकता है?" - -[certSign.tooltip.signing] -bullet1 = "PEM, PKCS12, JKS, और सर्वर प्रमाणपत्र फ़ॉर्मैट का समर्थन" -bullet2 = "PDF पर हस्ताक्षर को दिखाने या छिपाने का विकल्प" -bullet3 = "कारण, स्थान, और हस्ताक्षरकर्ता का नाम जोड़ें" -bullet4 = "दृश्यमान हस्ताक्षर किस पृष्ठ पर रखें, चुनें" -bullet5 = "सरल 'Sign with Stirling-PDF' विकल्प हेतु सर्वर प्रमाणपत्र का उपयोग करें" -text = "PDF पर हस्ताक्षर करने के लिए आपको एक डिजिटल प्रमाणपत्र (जैसे PEM, PKCS12, या JKS) चाहिए। आप हस्ताक्षर को दस्तावेज़ पर दृश्य रूप में दिखाने या केवल सुरक्षा हेतु अदृश्य रखने का विकल्प चुन सकते हैं।" -title = "हस्ताक्षर जोड़ना" - -[certSign.tooltip.validation] -bullet1 = "दिखाता है कि हस्ताक्षर वैध हैं या अवैध" -bullet2 = "हस्ताक्षरकर्ता की जानकारी और हस्ताक्षर की तिथि दिखाता है" -bullet3 = "जाँचता है कि हस्ताक्षर के बाद दस्तावेज़ संशोधित हुआ है या नहीं" -bullet4 = "सत्यापन के लिए कस्टम प्रमाणपत्र का उपयोग कर सकता है" -text = "जब आप हस्ताक्षरों की जाँच करते हैं, तो टूल बताता है कि वे वैध हैं या नहीं, किसने दस्तावेज़ पर हस्ताक्षर किए, कब किए, और हस्ताक्षर के बाद दस्तावेज़ में कोई बदलाव हुआ है या नहीं।" -title = "हस्ताक्षर की जाँच" - [changeCreds] changeUsername = "अपना उपयोगकर्ता नाम अपडेट करें। अपडेट के बाद आप लॉगआउट हो जाएँगे।" credsUpdated = "खाता अपडेट किया गया" @@ -2737,12 +2936,16 @@ text = "इन अनुमतियों को अपरिवर्तनी [chat.actions] copy = "संदेश कॉपी करें" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling एजेंट विकल्प" +assistant = "AI Assistant" clearChat = "चैट साफ़ करें" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "यह ऑपरेशन आपके क्लाउड क्र tooltip = "स्क्रीन से रंग चुनें" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "रंग चुनें" [common] +actions = "Actions" back = "वापस" cancel = "रद्द करें" close = "बंद करें" +codeSample = "Code sample" collapse = "समेटें" confirm = "पुष्टि करें" continue = "जारी रखें" copied = "कॉपी हो गया!" copy = "कॉपी करें" done = "पूर्ण" +enterValue = "Enter value" error = "त्रुटि" expand = "विस्तारित करें" loading = "लोड हो रहा है..." next = "अगला" +open = "Open" preview = "पूर्वावलोकन" previous = "पिछला" refresh = "रीफ़्रेश करें" +remaining = "शेष" retry = "फिर से प्रयास करें" save = "सहेजें" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "चयनित साफ़ करें" @@ -2988,6 +3199,7 @@ title = "संपीड़न विधि" [compress.settings] desiredSize = "वांछित फ़ाइल आकार" desiredSizePlaceholder = "आकार दर्ज करें" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "संपीड़न फ़ाइल आकार कम करने का आसान तरीका है। 'File Size' चुनें और लक्ष्य आकार दर्ज करें ताकि हम आपके लिए गुणवत्ता समायोजित कर दें। 'Quality' चुनें ताकि आप संपीड़न की तीव्रता मैन्युअल रूप से सेट कर सकें।" @@ -3190,6 +3402,24 @@ pdfOptions = "PDF विकल्प" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "पहले एक स्रोत फ़ॉर्मेट चुनें" settings = "सेटिंग्स" single = "एकल" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog एनालिटिक्स" scarf = "Scarf पिक्सेल" -[credits.modal] -advancedMonitoring = "एडवांस्ड मॉनिटरिंग" -allInOneWorkspace = "ऑल-इन-वन PDF वर्कस्पेस (व्यूअर, टूल्स और एजेंट)" -apiSandbox = "API सैंडबॉक्स" -dedicatedSupportSlas = "समर्पित सपोर्ट और SLAs" -fasterThroughput = "10x तेज थ्रूपुट" -fullyPrivateFiles = "पूरी तरह निजी फ़ाइलें" -largeFileProcessing = "बड़ी फ़ाइल प्रोसेसिंग" -orgWideAccess = "संगठन-व्यापी एक्सेस नियंत्रण" -premiumAiModels = "प्रीमियम AI मॉडल" -prioritySupport = "प्राथमिकता सपोर्ट" -privateDocCloud = "प्राइवेट डॉक्युमेंट क्लाउड" -ragFineTuning = "RAG + फाइन-ट्यूनिंग" -secureApiAccess = "सिक्योर API एक्सेस" -standardThroughput = "स्टैंडर्ड थ्रूपुट" -unlimitedApiAccess = "अनलिमिटेड API एक्सेस" -unlimitedSeats = "अनलिमिटेड सीटें" - [crop] autoCrop = "खाली स्थान स्वतः क्रॉप करें" noFileSelected = "क्रॉप शुरू करने के लिए एक PDF फ़ाइल चुनें" @@ -3346,6 +3558,7 @@ message = "डिफ़ॉल्ट PDF हैंडलर सेट नही title = "त्रुटि" [defaultApp.prompt] +dontRemind = "मुझे फिर याद न दिलाएँ" message = "PDF फ़ाइलें खोलने के लिए Stirling PDF को अपनी डिफ़ॉल्ट ऐप बनाएं।" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "खोजें..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "संपादित करें" @@ -3644,11 +3861,13 @@ shareSelected = "चयनित साझा करें" sharing = "शेयरिंग" showAll = "सभी दिखाएँ" showHistory = "इतिहास दिखाएँ" +sortBy = "Sort files" sortByDate = "तिथि के अनुसार क्रमबद्ध करें" sortByName = "नाम के अनुसार क्रमबद्ध करें" sortBySize = "आकार के अनुसार क्रमबद्ध करें" storage = "स्टोरेज" storageState = "भंडारण" +storageUsed = "Storage used" synced = "सिंक किया गया" title = "PDF फ़ाइलें अपलोड करें" toolChain = "लागू किए गए टूल" @@ -3694,13 +3913,17 @@ addFiles = "फ़ाइलें जोड़ें" addingFiles = "फ़ाइलें जोड़ी जा रही हैं…" collapse = "साइडबार समेटें" customizeGroups = "समूह अनुकूलित करें" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "शुरू करने के लिए फ़ाइलें खोलें" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "साइडबार फैलाएँ" -files = "फ़ाइलें" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive कॉन्फ़िगर नहीं है" leaveMyFiles = "मेरी फ़ाइलों से बाहर जाएँ" +library = "PDF Library" myFiles = "मेरी फ़ाइलें" noFiles = "अभी कोई फ़ाइल नहीं" openFileManager = "फ़ाइल प्रबंधक खोलें" @@ -3708,37 +3931,36 @@ openFromComputer = "कंप्यूटर से खोलें" openSettings = "सेटिंग्स खोलें" other = "अन्य" recent = "हाल के" -search = "खोजें" -searchPlaceholder = "फ़ाइलें खोजें..." viewAll = "सभी {{count}} फ़ाइलें देखें" [fileSidebar.fileItem] closeViewer = "व्यूअर बंद करें" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "व्यूअर में खोलें" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "जोड़ें" -addLabel = "एक लेबल जोड़ें…" -categoryIconAria = "{{name}} के लिए एक आइकन चुनें" -createCategory = "श्रेणी बनाएँ" -delete = "श्रेणी हटाएँ" done = "हो गया" -hide = "श्रेणी छिपाएँ" -newCategory = "नई श्रेणी का नाम…" -removeLabel = "{{name}} हटाएँ" -rename = "नाम बदलें" reset = "डिफ़ॉल्ट पर रीसेट करें" -search = "लेबल खोजें…" -show = "श्रेणी दिखाएँ" subtitle = "अपनी फ़ाइलों को मूल श्रेणियों में समूहित करें। किसी श्रेणी में मौजूदा या नए लेबल जोड़ें, उसका नाम बदलें, या अपनी खुद की श्रेणी बनाएँ। आपकी किसी भी श्रेणी में न आने वाली फ़ाइलें “अन्य” के अंतर्गत दिखाई देती हैं।" title = "Sidebar श्रेणियाँ" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "वर्कस्पेस में जोड़ें" addToWorkspaceCount = "{{count}} को वर्कस्पेस में जोड़ें" @@ -3749,7 +3971,6 @@ backToMyFiles = "मेरी फ़ाइलों पर वापस" breadcrumbs = "फ़ोल्डर पथ" cancel = "रद्द करें" classification = "वर्गीकरण" -clearSearch = "खोज साफ़ करें" clearSelection = "चयन साफ़ करें" closeDetails = "विवरण बंद करें" create = "बनाएं" @@ -3781,6 +4002,7 @@ downloadAll = "सभी डाउनलोड करें" downloadVersion = "यह संस्करण डाउनलोड करें" dropOverlay = "अपलोड करने के लिए फ़ाइलें छोड़ें" dropOverlaySub = "फ़ाइलें Local में शुरू होती हैं। उन्हें किसी फ़ोल्डर में व्यवस्थित करने के लिए 'Move to' या 'Save to cloud' का उपयोग करें।" +duplicate = "Duplicate" file = "फ़ाइल" fileInfo = "File info" fileMenu = "फ़ाइल क्रियाएँ" @@ -3808,13 +4030,12 @@ refresh = "सर्वर से रिफ्रेश करें" remove = "हटाएं" removeVersion = "यह संस्करण हटाएं" rename = "नाम बदलें" +renamed = "नाम बदला गया" renameFolder = "फ़ोल्डर का नाम बदलें" resizeFolderTree = "फ़ोल्डर ट्री का आकार बदलें (एरो कुंजियाँ, बड़े कदमों के लिए Shift; ऑटो-फिट के लिए डबल-क्लिक)" save = "सहेजें" saveToServer = "सर्वर पर सहेजें" saveToServerDisabledHint = "इस सर्वर पर सहेजना सक्षम नहीं है। अपने प्रशासक से इसे सक्षम करने के लिए कहें।" -search = "खोज" -searchPlaceholder = "इस फ़ोल्डर और उप-फ़ोल्डरों में खोजें" selectAll = "सभी चुनें" selectAllHint = "सभी चुनने के लिए क्लिक करें। सुझाव: एक-एक करके फ़ाइलें जोड़ने के लिए Ctrl (या Cmd) दबाए रखें, रेंज चुनने के लिए Shift।" selectedCount = "{{count}} चयनित" @@ -3874,6 +4095,10 @@ title = "अभी तक कोई क्लाउड फ़ाइल नही hint = "बिना अपलोड सहेजी गई फ़ाइलें यहाँ रहती हैं। जोड़ने के लिए कोई फ़ाइल छोड़ें।" title = "कोई केवल-लोकल फ़ाइल नहीं" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "जो फ़ाइलें आप खोलते या संपादित करते हैं, वे यहाँ दिखाई देंगी।" title = "अभी तक कुछ संशोधित नहीं" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "फ़ोल्डर हटाया नहीं जा सका।" deleteFolderFailedDetail = "फ़ोल्डर हटाया नहीं जा सका: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "फ़ोल्डर दिखावट अपडेट नहीं हो सकी।" folderAppearanceFailedDetail = "फ़ोल्डर दिखावट अपडेट नहीं हो सकी: {{message}}" moveFilesFailed = "फ़ाइलें ले जाई नहीं जा सकीं।" @@ -3910,9 +4137,7 @@ count = "फ़ाइलें" folder = "फ़ोल्डर" labels = "लेबल" modified = "संशोधित" -name = "नाम" size = "आकार" -toolHistoryAtVersion = "संचयी उपकरण श्रृंखला" totalSize = "कुल आकार" type = "प्रकार" versionHistory = "संस्करण यात्रा" @@ -3945,6 +4170,11 @@ localHint = "केवल इस ब्राउज़र में संग् shared = "शेयर किया गया" sharedHint = "लिंक के माध्यम से आपके साथ साझा" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "सबसे पुराने पहले" modifiedDesc = "हाल के पहले" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "स्टोरेज में कोई फ़ाइ open = "खोलें" openFile = "फ़ाइल खोलें" openFiles = "फ़ाइलें खोलें" +selectFile = "Select {{name}}" selectFromStorage = "स्टोरेज से चुनें" upload = "अपलोड" uploadFile = "फ़ाइल अपलोड करें" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "सब सहेजा गया" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV extract करने में विफल" +extractXlsxError = "XLSX extract करने में विफल" +filled = "filled" flattenAfterFilling = "भरने के बाद समतल करें" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "आव." +requiredFieldsError = "कृपया सभी आवश्यक fields भरें" rescanFields = "फ़ील्ड्स दोबारा स्कैन करें" rescanFormFields = "फॉर्म फ़ील्ड्स दोबारा स्कैन करें" save = "सहेजें" +saveShortcut = "सहेजने के लिए Ctrl+S" +unsavedChanges = "बिना सहेजे बदलाव" [formFill.sidebar] close = "साइडबार बंद करें" @@ -4309,6 +4552,11 @@ desc = "पता चले हेडर के आधार पर PDF फ़ tags = "ऑटो-डिटेक्ट,हेडर-आधारित,संगठित करें,पुन:लेबल करें" title = "PDF फ़ाइल ऑटो रिनेम" +[home.autoRotate] +desc = "प्रत्येक page की orientation detect करें और उसे स्वचालित रूप से सीधा घुमाएँ।" +tags = "ऑटो रोटेट,orientation ठीक करें,उल्टा,बगल की ओर,सीधा करें,orient,orientation detect करें,rotation सही करें,auto orient,scanned,OSD" +title = "Auto Rotate" + [home.autoSizeSplitPDF] tags = "ऑटो,विभाजित करें,आकार" title = "आकार/गिनती से ऑटो बाँटें" @@ -4608,15 +4856,12 @@ hint = "PDF पर रखने के लिए PNG, JPG, SVG, या अन् label = "इमेज अपलोड करें" placeholder = "इमेज फ़ाइल चुनें" -[imageToPdf] -tags = "रूपांतरण,img,jpg,चित्र,फोटो" - [imageToPDF] header = "छवि से PDF" title = "छवि से PDF" -[infoBanner] -dismiss = "खारिज करें" +[imageToPdf] +tags = "रूपांतरण,img,jpg,चित्र,फोटो" [invite] acceptError = "खाता बनाने में विफल" @@ -4647,11 +4892,9 @@ welcomeTitle = "आपको आमंत्रित किया गया ह [landing] addFiles = "फ़ाइलें जोड़ें" -heroSubtitle = "शुरू करने के लिए कोई PDF यहां छोड़ें या मौजूदा PDF जोड़ें।" mobileUpload = "मोबाइल से अपलोड करें" openFromComputer = "कंप्यूटर से खोलें" uploadFromComputer = "कंप्यूटर से अपलोड करें" -workbenchEmptyStateHero = "कहीं भी PDF छोड़ें" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "उपयोगकर्ता नाम दर्ज करे failedToSignIn = "{{provider}} के साथ साइन इन विफल: {{message}}" forgotPassword = "पासवर्ड भूल गए?" loggingIn = "लॉगिन हो रहा है..." -logIn = "लॉग इन" login = "लॉगिन" magicLinkSent = "मैजिक लिंक {{email}} पर भेज दिया गया! साइन इन करने के लिए अपना ईमेल जाँचें और लिंक पर क्लिक करें।" mfaCode = "प्रमाणीकरण कोड" @@ -4715,26 +4957,10 @@ signInWith = "इसके साथ साइन इन करें" title = "साइन इन करें" unexpectedError = "अप्रत्याशित त्रुटि: {{message}}" updatePassword = "पासवर्ड अपडेट करें" -useEmailInstead = "ईमेल से लॉगिन करें" useMagicLink = "इसके बजाय मैजिक लिंक उपयोग करें" username = "उपयोगकर्ता नाम" youAreLoggedIn = "आप लॉग इन हैं!" -[login.slides.edit] -alt = "PDF संपादित करें" -subtitle = "redact, sign, पढ़ने और संशोधित करने में मदद करने वाले दर्जनभर से अधिक टूल्स के साथ, आपको जो चाहिए वह जरूर मिलेगा।" -title = "अपनी मनचाही जानकारी दिखाने/सुरक्षित करने के लिए PDF संपादित करें" - -[login.slides.overview] -alt = "Stirling PDF ओवरव्यू" -subtitle = "प्राइवेसी-फर्स्ट क्लाउड सूट जो PDF को convert, sign, redact और manage करने देता है, साथ ही 50+ शक्तिशाली टूल्स।" -title = "आपकी सभी PDF जरूरतों के लिए एक स्थान" - -[login.slides.secure] -alt = "PDF सुरक्षित करें" -subtitle = "पासवर्ड जोड़ें, कंटेंट redact करें, और सर्टिफिकेट्स आसानी से मैनेज करें।" -title = "अपने PDF में संवेदनशील जानकारी सुरक्षित रखें" - [margin] large = "बड़ा" medium = "मध्यम" @@ -4818,6 +5044,34 @@ uploadSuccess = "अपलोड सफल!" uploadSuccessMessage = "आपकी छवियाँ स्थानांतरित कर दी गई हैं।" validating = "सेशन सत्यापित किया जा रहा है..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "फ़ोटो अपलोड करने के लिए स्कैन करें। छवियाँ स्वतः PDF में बदल जाती हैं।" descriptionNoConvert = "अपने मोबाइल डिवाइस से फ़ोटो अपलोड करने के लिए स्कैन करें।" @@ -4836,8 +5090,13 @@ title = "मोबाइल से अपलोड करें" tags = "मल्टी टूल,मल्टी ऑपरेशन,UI,क्लिक ड्रैग,फ्रंट एंड,क्लाइंट साइड,इंटरैक्टिव,इंटरैक्टेबल,मूव,डिलीट,माइग्रेट,डिवाइड" title = "PDF मल्टी टूल" -[navbar] -search = "खोजें" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "प्रमाणीकरण सफल नहीं हुआ। आप यह विंडो बंद कर फिर से प्रयास कर सकते हैं।" @@ -4966,6 +5225,7 @@ activeFiles = "Active Files दृश्य उन सभी PD allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "बंद करें" cropSettings = "अब जब हमने वह फ़ाइल चुन ली है जिसे क्रॉप करना है, तो हम Crop टूल को कॉन्फ़िगर कर सकते हैं ताकि वह क्षेत्र चुनें जहाँ तक PDF को क्रॉप करना है।" +dialogLabel = "Onboarding" fileCheckbox = "किसी फ़ाइल पर क्लिक करने से वह प्रोसेसिंग के लिए चुनी जाती है। बैच ऑपरेशंस के लिए आप कई फ़ाइलें चुन सकते हैं।" fileReplacement = "संशोधित फ़ाइल Workbench में मूल फ़ाइल को स्वचालित रूप से बदल देगी, जिससे आपको इसे और टूल्स से चलाना आसान हो जाएगा।" filesButton = "क्विक एक्सेस बार पर Files बटन आपको PDF अपलोड करने देता है जिन पर आप टूल्स का उपयोग कर सकते हैं।" @@ -4976,6 +5236,7 @@ pinButton = "यदि आप चाहते हैं कि आपके फ results = "टूल के चलने के बाद, Review चरण इस पैनल में परिणामों का पूर्वावलोकन दिखाएगा, और आपको क्रिया पूर्ववत करने या फ़ाइल डाउनलोड करने देगा। " runButton = "टूल कॉन्फ़िगर हो जाने के बाद, यह बटन सभी चयनित PDF पर टूल चलाने देता है।" selectCropTool = "आइए, Crop टूल चुनते हैं ताकि किसी एक टूल का उपयोग दिखा सकें।" +stepOf = "Step {{current}} of {{total}}" toolInterface = "यह Crop टूल इंटरफ़ेस है। जैसा कि आप देख सकते हैं, यहाँ अभी ज्यादा कुछ नहीं है क्योंकि हमने काम करने के लिए अभी कोई PDF फ़ाइल नहीं जोड़ी है।" workbench = "यह Workbench है - मुख्य क्षेत्र जहाँ आप अपनी PDF देखते और संपादित करते हैं।" wrapUp = "सब सेट! आपने ऐप के मुख्य क्षेत्रों और उनका उपयोग करना सीख लिया है। जब चाहें फिर से यह टूर देखने के लिए Help बटन पर क्लिक करें।" @@ -4988,6 +5249,26 @@ showMeAround = "मुझे दिखाएँ" skipForNow = "अभी छोड़ें" skipTheTour = "टूर छोड़ें" +[onboarding.checklist] +dismiss = "खारिज करें" +title = "Stirling PDF सेट अप करें" + +[onboarding.checklist.downloadDesktop] +description = "Stirling को अपनी मशीन पर natively चलाएँ" +title = "Desktop के लिए Stirling download करें" + +[onboarding.checklist.inviteTeam] +description = "अपनी team के साथ collaborate करें" +title = "team members को invite करें" + +[onboarding.checklist.shareAnalytics] +description = "Stirling को बेहतर बनाने में मदद करें" +title = "anonymous usage data share करें" + +[onboarding.checklist.takeTour] +description = "एक quick walkthrough में देखें कि Stirling कैसे काम करता है" +title = "tour लें" + [onboarding.desktopInstall] body = "Stirling एक डेस्कटॉप ऐप के रूप में सबसे अच्छा काम करता है। आप इसे ऑफ़लाइन उपयोग कर सकते हैं, दस्तावेज़ों तक तेज़ी से पहुँच सकते हैं, और अपने कंप्यूटर पर स्थानीय रूप से संपादन कर सकते हैं।" selectOs = "ऑपरेटिंग सिस्टम चुनें" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}} के लिए डाउनलोड" [onboarding.mfa] authenticationCode = "प्रमाणीकरण कोड" +description = "authenticator app लिंक करके अपना account secure करें। QR code scan करें या setup key दर्ज करें, फिर पूरा करने के लिए 6-digit code confirm करें।" +enable = "MFA सक्षम करें" +enableError = "two-factor authentication सक्षम नहीं कर सके। code जाँचें और फिर प्रयास करें।" +enterCode = "अपने app से 6-digit code दर्ज करें।" +enterCodeError = "जारी रखने के लिए authentication code दर्ज करें।" +logout = "Logout" +openAuthenticator = "Google Authenticator, Authy, या 1Password खोलें।" qrCodeLoading = "आपका QR कोड जनरेट किया जा रहा है…" +regenerateQrCode = "QR code फिर से बनाएँ" +scanQrCode = "QR code scan करें या नीचे setup key दर्ज करें।" +setupError = "two-factor setup शुरू नहीं कर सके। कृपया फिर प्रयास करें।" +setupKey = "Setup key (manual entry)" +stepByStep = "Step-by-step" +success = "MFA सक्षम हो गया है। अब आप जारी रख सकते हैं।" +title = "Multi-Factor Authentication Setup" [onboarding.planOverview] adminBodyLoginDisabled = "एक बार आप लॉगिन मोड सक्षम कर दें, आप उपयोगकर्ताओं को प्रबंधित कर सकते हैं, सेटिंग्स कॉन्फ़िगर कर सकते हैं, और सर्वर स्वास्थ्य मॉनिटर कर सकते हैं। आपके सर्वर पर पहले {{freeTierLimit}} लोग Stirling मुफ्त में उपयोग कर पाएँगे।" @@ -5005,6 +5300,11 @@ adminTitle = "एडमिन अवलोकन" userBody = "टीममेट्स को आमंत्रित करें, भूमिकाएँ असाइन करें, और अपने दस्तावेज़ों को एक सुरक्षित वर्कस्पेस में व्यवस्थित रखें। जब भी आप सोलो उपयोग से आगे बढ़ने के लिए तैयार हों, लॉगिन मोड सक्षम करें।" userTitle = "प्लान अवलोकन" +[onboarding.processorIntro] +body = "Stirling अब Policies चलाता है — automated rules जो हर दस्तावेज़ के आते ही उसे classify, secure, और process करते हैं। इन्हें सेट अप करें और Processor में runs monitor करें।" +cta = "Processor देखें" +title = "Stirling Processor देखें" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Prepaid year" + +[payg.prepaid.banner] +exhausted = "आपकी prepaid capacity उपयोग हो चुकी है; metered billing फिर शुरू हो गई है। discount बनाए रखने के लिए top up करें।" +expiring_one = "आपका prepaid year {{count}} दिन में समाप्त होगा। unused capacity expire हो जाएगी — नया year शुरू करने के लिए top up करें।" +expiring_other = "आपका prepaid year {{count}} दिनों में समाप्त होगा। unused capacity expire हो जाएगी — नया year शुरू करने के लिए top up करें।" +low = "आपकी prepaid capacity का {{pct}}% बचा है। processing निर्बाध रखने के लिए कभी भी top up करें।" + +[payg.prepaid.card] +subtitle = "metered billing से पहले और आपके spend cap के बाहर उपयोग किया जाता है।" +title = "Prepaid capacity" + +[payg.prepaid.meter] +capSuffix = "{{total}} prepaid credits में से" +expires = "{{date}} को expires" + +[payg.prepaid.state] +exhausted = "उपयोग हो गया" +healthy = "काफी बचा है" +low = "कम हो रहा है" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF से PDF/A" tags = "संग्रह,लंबी अवधि,मानक,रूपांतरण,भंडारण,संरक्षण" title = "PDF से PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "print,standard,conversion,production,prepress,archive" title = "PDF से PDF/X" @@ -5822,6 +6151,7 @@ small = "500 क्रेडिट्स" xsmall = "100 क्रेडिट्स" [plan.availablePlans] +currency = "Billing currency" subtitle = "अपनी जरूरतों के अनुसार प्लान चुनें" title = "उपलब्ध प्लान" @@ -5937,49 +6267,12 @@ keyDescription = "ईमेल से मिली लाइसेंस कु success = "लाइसेंस सक्रिय हुआ!" successMessage = "आपका लाइसेंस सफलतापूर्वक सक्रिय हो गया है। अब आप यह विंडो बंद कर सकते हैं।" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "लागू किया गया" -enforcing = "लागू किया जा रहा है..." -failed = "लागू करना विफल रहा" outputsUnavailable = "नीति आउटपुट अब डाउनलोड के लिए उपलब्ध नहीं हैं।" partialOutputsUnavailable = "कुछ नीति आउटपुट अब डाउनलोड के लिए उपलब्ध नहीं हैं।" -retrying = "व्यस्त है, पुनः प्रयास किया जा रहा है..." runNotFound = "लागू करने की प्रक्रिया अब नहीं मिली।" -step = "चरण {{current}}/{{total}}" timedOut = "रन पूरा होने से पहले लागू करने की समय-सीमा समाप्त हो गई।" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "बंद करें" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} लागू किए जा रहे हैं" applyingProgress = "{{names}} लागू किए जा रहे हैं ({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} में से {{failures}} फ़ाइल(ें) failureTitle = "पूर्ण रूप से लागू किए बिना निर्यात किया गया" printPolicyAppliedBody = "इस PDF को किसी नीति के अनुरूप अद्यतन किया गया। बदलावों की समीक्षा करें, फिर पुनः प्रिंट करें।" printPolicyAppliedTitle = "प्रिंट करने से पहले नीति लागू की गई" -queued = "+{{count}} कतार में" successTitle = "{{names}} लागू किए गए" summaryMore = "{{first}}, {{second}} और {{more}} अन्य" summaryTwo = "{{first}} और {{second}}" -[policies.enforcement.triggerVerb] -convert = "रूपांतरण से पहले लागू करना" -default = "लागू करना" -export = "निर्यात से पहले लागू करना" -input = "आयात पर लागू करना" -print = "प्रिंट से पहले लागू करना" - -[policies.field] -accessLog = "एक्सेस लॉग" -archiveAfter = "इसके बाद आर्काइव करें" -auditTrail = "ऑडिट ट्रेल" -belowThreshold = "सीमा से नीचे" -destination = "गंतव्य" -frameworks = "फ़्रेमवर्क" -immutableHold = "अपरिवर्तनीय होल्ड" -keepFor = "इतने समय तक रखें" -minConfidence = "न्यूनतम कॉन्फ़िडेंस" -notify = "रूट पर सूचित करें" -onViolation = "अनुपालन न होने पर" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "जोड़ें" -addPlaceholder = "एक लेबल जोड़ें…" -customNote = "आपकी टीम के लिए अनुकूलित।" -defaultNote = "बिल्ट-इन डिफ़ॉल्ट का उपयोग हो रहा है, जो आपकी टीम के साथ साझा है।" -duplicate = "\"{{name}}\" पहले से मौजूद है।" -edit = "लेबल संपादित करें" -empty = "अभी कोई लेबल नहीं।" -export = "JSON export करें" -iconAria = "{{name}} के लिए एक आइकन चुनें" -import = "JSON import करें" -importError = "वह फ़ाइल import नहीं की जा सकी।" -managedNote = "टीम लेबल आपके टीम लीडर द्वारा प्रबंधित किए जाते हैं।" -modalSubtitle = "आपकी पूरी टीम के साथ साझा। classifier हर दस्तावेज़ के लिए उपयुक्त लेबल चुनता है।" -modalTitle = "वर्गीकरण लेबल" -pickIcon = "{{label}} आइकन का उपयोग करें" -removeAria = "{{name}} हटाएँ" -resetToDefault = "डिफ़ॉल्ट पर रीसेट करें" -saveForTeam = "टीम के लिए सहेजें" -saving = "सहेजा जा रहा है…" -sectionLabel = "वर्गीकरण लेबल" -startFromScratch = "शुरुआत से शुरू करें" -teamCount = "टीम लेबल" -tooLong = "लेबल अधिकतम {{max}} वर्णों के हो सकते हैं।" -ungrouped = "असमूहित" -view = "लेबल देखें" +categoryCount = "categories" +hideCategory = "category छिपाएँ" +labelCount = "labels" +sharedNote = "ये labels built in हैं और आपकी पूरी team में shared हैं।" +showCategory = "category दिखाएँ" [policies.pii] account = "खाता संख्याएँ (लेबल युक्त)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US रूटिंग नंबर (ABA)" ssn = "सोशल सिक्योरिटी नंबर" -[policies.settings] -noneExport = "वर्तमान में export पर कोई policy नहीं चलती।" -noneUpload = "वर्तमान में upload पर कोई policy नहीं चलती।" -onExport = "export पर" -onUpload = "upload पर" -reorderHandle = "क्रम बदलने के लिए खींचें" -runOrderDesc = "जब एक ही trigger पर एक से अधिक policy चलती हैं, तो वे इसी क्रम में चलती हैं — प्रत्येक पिछली policy के output पर। क्रम बदलने के लिए खींचें।" -title = "Policy सेटिंग्स" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Policy विकल्प" -policySettings = "Policy सेटिंग्स" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "विफल policies पुनः प्रयास करें ({{count}})" -rowProgress = "{{total}} में से {{completed}} फ़ाइलें प्रोसेस हुईं" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Policy क्या है?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "संवेदनशील विवरण—जैसे सोशल सिक्योरिटी और कार्ड नंबर—को स्वतः ढूँढकर काला कर देता है, ताकि दस्तावेज़ में उन्हें पढ़ा न जा सके।" -sanitize = "फ़ाइल से छिपा हुआ JavaScript हटाता है, ताकि इसे खोलने पर कुछ भी स्वतः न चले।" -watermark = "प्रत्येक पृष्ठ पर एक दृश्यमान निशान (जैसे \"Confidential\") लगाता है।" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "बंद करें" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} लागू हो रही है..." badgeRan = "इस फ़ाइल पर {{name}} policy चली" +badgeRunning = "{{name}} चल रहा है..." blockingAction = "policy लागू होने के दौरान {{action}} ब्लॉक है, कृपया प्रतीक्षा करें..." dismiss = "ओवरले हटाएँ" enforcingTitle = "policy लागू हो रही है..." @@ -6151,6 +6316,7 @@ viewAnyway = "फ़ाइल देखें (policy अभी भी लाग [policyOption] autoRedactPhi = "PHI को अपने-आप redact करें" blockExport = "export ब्लॉक करें" +ccpa = "CCPA" contracts = "अनुबंध" documents = "दस्तावेज़" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor प्लान" subscribed = "Processor प्लान" unlinked = "लिंक नहीं किया गया" -[portal.agentBuilder] -bootstrapFromDocument = "दस्तावेज़ से bootstrap करें" -sectionsAriaLabel = "Agent builder सेक्शन" -selectorAriaLabel = "Agents" -subtitle = "उन AI agents को डिज़ाइन, टेस्ट और ship करें जो आपके दस्तावेज़ों को classify करते हैं, उनसे extract करते हैं और route करते हैं। scenarios परिभाषित करें, tool access सीमित करें, golden set चलाएँ, और version publish करें।" -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "रद्द करें" -dropzoneText = "एक sample document चुनें (PDF या image)" -lead = "एक sample document छोड़ें और हम scenarios और extraction schema सुझाएँगे जिन्हें आप refine कर सकते हैं। जब तक आप review नहीं करते, कुछ भी publish नहीं होता।" -submit = "Agent bootstrap करें" -subtitle = "एक प्रतिनिधि फ़ाइल से नया agent seed करें" -title = "दस्तावेज़ से bootstrap करें" - -[portal.agentBuilder.empty] -description = "sample document से agent bootstrap करके उसके scenarios और extraction schema seed करें, फिर refine और publish करें।" -title = "अभी कोई agents नहीं" - -[portal.agentBuilder.evals] -casesPassing = "पास हो रहे cases" -columnCase = "Eval case" -columnLatency = "Latency" -columnResult = "परिणाम" -fail = "fail" -goldenSetPassRate = "Golden-set pass rate" -latencyMs = "{{ms}} ms" -notRun = "नहीं चला" -pass = "pass" -passRate = "Pass rate" -runEvals = "evals चलाएँ" - -[portal.agentBuilder.evals.empty] -description = "Evals आपके scenarios को repeatable golden set में बदलते हैं। समय के साथ pass-rate capture करने और publishes को gate करने के लिए upgrade करें।" -title = "अभी कोई golden set नहीं" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "golden sets में" -activeAgents = "सक्रिय agents" -avgPassRate = "औसत eval pass rate" -fleetWide = "fleet-wide" -latestPublished = "नवीनतम प्रकाशित" -scenarios = "Scenarios" -testCases = "test cases" -totalDescription_one = "कुल {{count}}" -totalDescription_other = "कुल {{count}}" - -[portal.agentBuilder.scenarios] -add = "जोड़ें" -addScenario = "Scenario जोड़ें" -enable = "सक्षम करें" -expectationLabel = "अपेक्षित व्यवहार" -expectationPlaceholder = "agent को क्या करना चाहिए" -inEval = "eval में" -mute = "Mute करें" -muted = "muted" -nameLabel = "नाम" -namePlaceholder = "उदा. Compliance escalation" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarios" -tools = "Tools" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "व्यापक access" -deniedHint = "चुने गए tools ब्लॉक हैं। बाकी सब callable रहता है।" -deniedTools = "Denied tools" -governanceGate = "Tool governance Enterprise प्लान पर उपलब्ध है।" -restricted = "Restricted" -restrictedAccess = "Restricted tool access" -restrictedDescription = "नीचे जिन tools को आप deny करते हैं, उन्हें छोड़कर हर tool की अनुमति दें।" - -[portal.agentBuilder.versions] -current = "वर्तमान" -historyGate = "पूरी version history और rollback Enterprise प्लान पर उपलब्ध हैं।" -publish = "Publish करें" -rollBack = "Roll back करें" - [portal.assistant] close = "assistant बंद करें" error = "assistant तक नहीं पहुँचा जा सका।" @@ -6365,9 +6451,21 @@ typing = "टाइप हो रहा है" loading = "लोड हो रहा है" redirectingToEditor = "editor पर redirect किया जा रहा है..." +[portal.billing.activation] +maybeLater = "शायद बाद में" +title = "Processor चालू करें" + +[portal.billing.activation.payg] +sub = "monthly limit सेट करें। जितना चलाएँ, उतना भुगतान करें।" +title = "Pay as you go" + +[portal.billing.activation.prepay] +badge = "2 months free" +sub = "एक invoice, card या bank transfer।" +title = "एक year prepay करें" + [portal.billing.checkout] noClientSecret = "Edge function ने कोई client_secret नहीं लौटाया।" -subtitle = "अपने मुफ़्त Editor-plan grant से आगे बढ़ने के लिए card जोड़ें। बाकी Stripe संभालता है।" title = "Processor प्लान चालू करें" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "आपका payment सफल रहा, लेकिन activation म close = "बंद करें" title = "लगभग हो गया" +[portal.billing.checkout.cap] +amountAria = "Monthly spend limit" +back = "वापस" +continue = "payment पर जारी रखें" +error = "आपकी spend limit सेट नहीं कर सके" +finePrint = "Invoices monthly post होते हैं। कभी भी cancel करें।" +noLimit = "कोई limit नहीं" +note = "आपसे आपकी limit से अधिक कभी bill नहीं किया जाता। Processing बस pause हो जाती है।" +perMonth = "/ mo" +title = "अपनी spend limit सेट करें" + +[portal.billing.checkout.card] +fields = "Card number · MM / YY · CVC · ZIP" +label = "Card details" +note = "Card details Stripe द्वारा collect किए जाते हैं। Stirling कभी PAN या CVC store नहीं करता।" + [portal.billing.checkout.error] title = "checkout शुरू नहीं किया जा सका" @@ -6383,10 +6497,9 @@ body = "आपका payment पूरा हो गया। हम आपक hint = "कृपया यह विंडो खुली रखें।" title = "आपका Processor प्लान सक्रिय किया जा रहा है..." -[portal.billing.checkout.notConfigured] -bodyAfter = "portal env में in-app checkout सक्षम करने के लिए।" -bodyBefore = "सेट करें" -title = "Stripe कॉन्फ़िगर नहीं है" +[portal.billing.checkout.pay] +limit = "Monthly spend limit {{amount}}/mo। Processing limit पर pause हो जाती है।" +limitNoCap = "कोई spend limit नहीं। Processing pause नहीं होगी। Invoices monthly post होते हैं।" [portal.billing.enterpriseUpsell] cta = "अपना Enterprise quote बनाएँ" @@ -6409,10 +6522,12 @@ currentPlan = "वर्तमान प्लान" freeForever = "हमेशा मुफ़्त" noTeamResolved = "आपके wallet पर अभी कोई team resolved नहीं है — refresh करें और फिर कोशिश करें।" ownerOnly = "केवल team owner Processor प्लान चालू कर सकता है।" +payInvoice = "पूरा करने के लिए invoice pay करें" planName = "Editor" ssoIncluded = "SSO शामिल" switchOnProcessor = "Processor चालू करें →" unlimitedUsers = "असीमित users" +viewQuote = "quote देखें" [portal.billing.invoices] columnAmount = "राशि" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direct API requests" segmentApiLabel = "API" segmentAutomationDesc = "Automations और pipelines" segmentAutomationLabel = "Automation" +sizeMultiplier = "{{formatted}} PDFs ने size multiplier उपयोग किया" +summary = "{{unique}} unique · {{units}} meter units · {{avg}} avg per PDF" +summaryNoRate = "{{unique}} unique · {{units}} meter units" unit = "metered PDFs" +unitsPending_one = "linked instances से {{units}} meter unit sync pending है" +unitsPending_other = "linked instances से {{units}} meter units sync pending हैं" + +[portal.billing.prepaid] +capSuffix = "{{total}} prepaid credits में से" +expires = "{{date}} को expires" +eyebrow = "Prepaid capacity" +note = "metered billing से पहले और आपकी spend limit के बाहर उपयोग किया जाता है।" +topUp = "Top up" + +[portal.billing.prepaid.buy] +back = "वापस" +calcTitle = "अपना annual payment calculate करें" +cancel = "Cancel" +cancelPurchase = "purchase cancel करें" +close = "बंद करें" +continueToPayment = "payment पर जारी रखें" +finalise = "Finalise" +invoiceErrorTitle = "invoice generate नहीं कर सके" +loadingTitle = "आपका quote load हो रहा है" +payTitle = "अपने year के लिए pay करें" +step = "Step {{current}} of {{total}}" + +[portal.billing.prepaid.calc] +change = "बदलें" +deployRow = "Deployment" +done = "Done" +enterpriseHint = "यह enterprise scale है। इस volume पर, enterprise rates किसी भी self-serve discount से बेहतर हैं। Rate lock, terms, और minutes में quote।" +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} policies" +handlesLabel = "आपका Processor" +handlesValue = "{{volume}} credits / mo handle करता है" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policies" +poolCaption = "year के लिए {{credits}} credits का एक pool। Heavy months, light months से borrow करते हैं।" +priceLabel = "आपका year · 10 की कीमत में 12 months" +rateUnknown = "checkout पर हम exact price दिखाएँगे।" +savings = "आप {{amount}} बचाते हैं · 2 months free।" +sizingRow = "Sizing" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "कुल users" + +[portal.billing.prepaid.consent] +label = "मैं समझता/समझती हूँ कि जब मेरी prepaid capacity उपयोग हो जाती है या 12 months बाद expire हो जाती है, तो processing automatically standard metered pay-as-you-go rate पर (मेरी spend limit तक) जारी रहती है जब तक मैं cancel न करूँ, और मैं billing portal से कभी भी cancel कर सकता/सकती हूँ।" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling द्वारा managed। minutes में live।" +cloudValue = "Stirling Cloud · Managed" +selfhost = "Self-hosted" +selfhostDesc = "private infrastructure पर चलता है। Same rate।" +selfhostValue = "Self-hosted · Private infrastructure" + +[portal.billing.prepaid.offer] +cta = "offer review करें" +subtitle = "PDF processing का एक year prepay करें और two months free पाएँ, metered billing से पहले और आपकी spend limit के बाहर उपयोग किया जाता है।" +title = "10 की कीमत में 12 months पाएँ" + +[portal.billing.prepaid.pay] +accountName = "Account holder name" +accountNamePlaceholder = "e.g. Jane Smith" +companyName = "Company name (optional)" +companyNamePlaceholder = "e.g. Acme Inc." +downloadInvoice = "invoice download करें" +payOnline = "online pay करें" +poNumber = "PO number (optional)" +poNumberPlaceholder = "e.g. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Advanced" +advancedDesc = "Pipelines आपकी अधिकांश processing चलाते हैं।" +none = "None" +noneDesc = "अभी pipelines नहीं चल रहे। इन्हें कभी भी चालू करें।" +standard = "Standard" +standardDesc = "कुछ pipelines आने वाले PDFs को re-process करते हैं।" + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Classification, default, साथ में Sharing" +governed = "Governed" +governedDesc = "Security और Routing जोड़ता है" +regulated = "Regulated" +regulatedDesc = "हर category, incl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "quote download करें (PDF)" +downloading = "तैयार हो रहा है…" +share = "approval के लिए share करें" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "25 MB से कम files, कोई data charges नहीं" +heavy = "Heavy" +heavyDesc = "Scanned या image-heavy, नियमित रूप से 50 MB+" +standard = "Standard" +standardDesc = "ज़्यादातर छोटी, कुछ scans 25 MB से अधिक" + +[portal.billing.prepaid.state] +exhausted = "उपयोग हो गया" +healthy = "काफी बचा है" +low = "कम हो रहा है" [portal.billing.spendLimit] adjustLimit = "limit समायोजित करें" @@ -6494,6 +6713,8 @@ label = "exceed होने का अनुमान।" [portal.billing.spendThisMonth] eyebrow = "इस माह खर्च" +freeRemaining_one = "{{formatted}} free PDF शेष" +freeRemaining_other = "{{formatted}} free PDFs शेष" processed_one = "{{formattedCount}} PDF processed।" processed_other = "{{formattedCount}} PDFs processed।" processedWithRate_one = "{{formattedCount}} PDF processed, प्रत्येक {{rate}} पर।" @@ -6510,6 +6731,7 @@ reachedTitle = "मासिक spend limit पहुँच गई" title = "Stripe portal नहीं खोला जा सका" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} मुफ़्त PDFs में से उपयोग हुआ" capSuffix_other = "{{allowance}} मुफ़्त PDFs में से उपयोग हुआ" eyebrow = "Processor trial" @@ -6534,6 +6756,376 @@ signature = "signature" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "नया connection" +createTitleFor = "{{name}} connect करें" +delete = "Delete" +edit = "Edit" +editTitle = "connection edit करें" +unsupportedType = "यह connection type यहाँ edit नहीं किया जा सकता।" + +[portal.connections.categories.advanced] +description = "ऐसी service के लिए जिसे हम अभी support नहीं करते।" +label = "Advanced" + +[portal.connections.categories.audit] +description = "किसी document के साथ क्या हुआ, उसे वहाँ record करें जहाँ आपके auditors पहले से देखते हैं।" +label = "Audit और compliance logging" + +[portal.connections.categories.notify] +description = "दूसरे system को बताएँ कि कुछ हुआ है।" +label = "Notify और workflow" + +[portal.connections.categories.security] +description = "document आगे बढ़ने से पहले उसे scan या label करें।" +label = "Security और classification" + +[portal.connections.categories.signing] +description = "document को sign या notarize कराने के लिए बाहर भेजें।" +label = "E-signature और notarization" + +[portal.connections.categories.storage] +description = "processed documents कहाँ पहुँचते हैं - bucket, drive, ticket या page।" +label = "File और attach" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling इसे encrypted store करता है और फिर कभी नहीं दिखाता।" +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "इस connection का उपयोग करने वाले steps केवल इस URL के अंतर्गत paths call कर सकते हैं, किसी अन्य host को कभी नहीं।" +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "service ने जो URL दिया है, उसे paste करें। केवल उसके अंतर्गत paths call किए जा सकते हैं।" +label = "Webhook URL" + +[portal.connections.fields] +name = "Connection name" +namePlaceholder = "e.g. Production" + +[portal.connections.picker] +cancel = "Cancel" +createNew = "नया connection..." +placeholder = "connection चुनें" +save = "connection save करें" + +[portal.connections.picker2] +back = "integration बदलें" +heading = "आप क्या connect करना चाहते हैं?" +noResultsBody = "हम अभी इसे support नहीं करते - हमें इसके बारे में बताएं और हम इसे जोड़ने पर विचार करेंगे।" +noResultsTitle = "\"{{query}}\" से कुछ भी match नहीं हुआ" +searchPlaceholder = "integrations search करें - \"sign\", \"ocr\" या product name आज़माएँ" + +[portal.connections.types.api] +description = "policy से किसी भी HTTP API को call करें। आप URL, authentication और payload चुनते हैं।" +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "Authentication" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username और password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "custom header में token" + +[portal.connections.types.api.fields.authType.options.none] +label = "None" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "token के लिए log in करें" + +[portal.connections.types.api.fields.baseUrl] +helperText = "इस connection का उपयोग करने वाले steps केवल इस URL के अंतर्गत paths call कर सकते हैं, किसी अन्य host को कभी नहीं।" +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header name" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Login path" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "यदि API download link के साथ reply करता है, तो केवल इन hosts को fetch किया जा सकता है। commas से अलग करें। केवल base URL के अपने host को allow करने के लिए खाली छोड़ें।" +label = "Allowed result hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "इस header के रूप में token भेजें" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "response header जिसमें login call token लौटाता है, जैसे X-Auth-Token।" +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "आपके चलाए server पर malware के लिए documents scan करें - आपके network से कुछ बाहर नहीं जाता।" +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "policy में documents के लिए virus और content scanning।" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "macros, executables, scripts या embedded objects वाले documents block करें - केवल ज्ञात malware ही नहीं।" +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "processed documents को Confluence page से attach करें।" +label = "Confluence" + +[portal.connections.types.consigno] +description = "documents को electronic signature और notarization के लिए ConsignO Cloud (Notarius) भेजें।" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "आपका ConsignO Cloud environment, /api/v1 सहित।" +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "केवल तब आवश्यक जब आपकी organisation tenants उपयोग करती है।" +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "policy जब document process करे, तो Discord पर message post करें।" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "हर बार जब policy document handle करे, Elasticsearch में audit event index करें।" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "नए documents के लिए FTP या FTPS server poll करें।" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Connection mode" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (recommended)" + +[portal.connections.types.ftp.fields.security] +helperText = "Plain FTP credentials और files को unencrypted भेजता है; यदि server support करता है तो FTPS चुनें।" +label = "Encryption" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "None (plain FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "policy run होने पर Google Chat space में message post करें।" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "processed documents को Jira issue से attach करें।" +label = "Jira" + +[portal.connections.types.mailgun] +description = "policy run होने पर document या notification email करें।" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "processed documents को Nextcloud में file करें।" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "आपके चलाए Presidio server का उपयोग करके document text में personal data detect करें।" +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "PDFs पर Microsoft Purview sensitivity labels पढ़ें और apply करें।" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Optional। केवल labels को name से चुनने के लिए आवश्यक; labelling इसके बिना भी काम करती है।" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "हर label पर उसके SiteId के रूप में stamp किया जाता है, ताकि Purview-aware tools जानें कि किस organisation ने इसे apply किया।" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "एक S3 bucket जिसे sources पढ़ सकते हैं और pipeline outputs लिख सकते हैं।" +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS के लिए खाली छोड़ें। MinIO या किसी अन्य S3-compatible store के लिए इसे सेट करें।" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "policy run होने पर document या notification email करें।" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "नए documents के लिए SFTP (SSH) server poll करें।" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optional। server का SHA-256 host key fingerprint (`ssh-keyscan host | ssh-keygen -lf -` से)। सेट होने पर, केवल यह key प्रस्तुत करने वाला server स्वीकार किया जाता है; खाली होने पर, first connect पर देखी गई key pinned होती है और बाद के changes अस्वीकार किए जाते हैं।" +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "यदि आप इसके बजाय private key से authenticate करते हैं तो खाली छोड़ें।" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optional। password के बजाय key से authenticate करने के लिए OpenSSH private key paste करें।" +label = "Private key" + +[portal.connections.types.slack] +description = "policy जब document process करे, तो Slack पर message post करें।" +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS network share (Windows या Samba) watch करें।" +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Optional। Windows domain या workgroup, यदि आपका server एक उपयोग करता है।" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "हर बार जब policy document handle करे, Splunk को audit event भेजें।" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Sumo Logic HTTP collector को audit event भेजें।" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "policy run होने पर Teams channel में message post करें।" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "प्रत्येक document, या केवल details, आपके चुने URL पर post करें।" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "policy से Zap या Make scenario trigger करें।" +label = "Zapier और Make" + +[portal.docs] +browse = "docs browse करें" +viewSource = "GitHub पर source देखें" + [portal.docs.authentication] codeCaption = "हर request" eyebrow = "शुरुआत करें" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Bundled, named capabilities जिन्हें आपका agent single tool के रूप में invoke करता है। हर skill deterministic op chain है जिसके साथ evals attached हैं।" title = "Agent skills" +[portal.docs.toc] +title = "इस page पर" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "प्रकार" user = "उपयोगकर्ता" [portal.documents.queue.empty] +connectSource = "source connect करें" +createPipeline = "pipeline बनाएँ" description = "जब स्रोत आपकी पाइपलाइनों में दस्तावेज़ भेजेंगे, तो वे समीक्षा के लिए यहाँ दिखाई देंगे।" title = "क्यू में कोई दस्तावेज़ नहीं" @@ -6712,11 +7309,11 @@ editorAction = "संपादक" empty = "इस फ़िल्टर से कोई दस्तावेज़ मेल नहीं खाते।" rowActions = "पंक्ति कार्रवाइयाँ" sensitiveLabel = "संवेदनशील" -sensitiveTitle = "संवेदनशील — एक्सेस आवश्यक" [portal.documents.table.columns] action = "पाइपलाइन / कार्रवाई" document = "दस्तावेज़" +labels = "Labels" product = "उत्पाद" status = "स्थिति" time = "समय" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "अंतिम बार देखा गया" region = "क्षेत्र" status = "स्थिति" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "इस व्यू में अनपेक्षित त् retry = "फिर से कोशिश करें" title = "इस पेज पर कुछ गलत हो गया" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "वापस" +body = "desktop app download करें या server self-host करें।" +done = "Done" +guide = "{{name}} guide खोलें" +openInBrowser = "इसके बजाय browser में खोलें" +sectionDesktop = "Desktop app" +sectionSelfHosted = "Self-hosted" +title = "Stirling PDF Editor install करें" + +[portal.home.download.docker] +detailBody = "अपने server पर official image चलाएँ।" +note = ":8080 पर खुलता है। Default login: admin / stirling।" +tagline = "server को container में चलाएँ" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "आधिकारिक Helm chart इंस्टॉल करें।" +note = "सेटिंग्स को बनाए रखने के लिए /configs के लिए एक PVC जोड़ें।" +tagline = "Helm के साथ क्लस्टर पर डिप्लॉय करें" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "आपके वितरण के लिए नेटिव builds।" +guideBtn = "Linux इंस्टॉल विकल्प देखें" +note = "सामान्य distros के लिए AppImage, .deb और .rpm builds उपलब्ध हैं।" +tagline = "आपके distro के लिए नेटिव desktop app" +title = "Linux" + +[portal.home.download.mac] +altLabel = "या Homebrew के माध्यम से" +detailBody = "इंस्टॉलर चलाएँ, या Homebrew का उपयोग करें।" +downloadBtn = "macOS के लिए डाउनलोड करें" +tagline = "Apple silicon और Intel के लिए नेटिव app" +title = "macOS" + +[portal.home.download.manual] +detailBody = "bare metal server पर .jar चलाएँ।" +downloadBtn = ".jar डाउनलोड करें" +note = "Java 21+ आवश्यक है। :8080 पर खुलता है; डिफ़ॉल्ट लॉगिन admin / stirling।" +runLabel = "फिर इसे चलाएँ" +tagline = "bare metal server पर .jar चलाएँ" +title = "मैन्युअल server setup" + +[portal.home.download.windows] +altLabel = "या winget के माध्यम से" +detailBody = "इंस्टॉलर चलाएँ, या package manager का उपयोग करें।" +downloadBtn = "इंस्टॉलर डाउनलोड करें" +tagline = "Microsoft Windows के लिए नेटिव app" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} सक्रिय" -invite = "टीममेट्स को आमंत्रित करें" +activeOfDeployed = "इस महीने {{total}} में से {{active}} सक्रिय" name = "Stirling PDF Editor" open = "ब्राउज़र में खोलें" updated = "{{time}} अपडेट किया गया" +[portal.home.editor.deploy] +finish = "डिप्लॉयमेंट समाप्त करें" +options = "डिप्लॉयमेंट विकल्प देखें" +start = "Editor डिप्लॉय करें" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "नमस्कार" evening = "शुभ संध्या" morning = "सुप्रभात" -[portal.home.onboarding] -dismiss = "सेटअप हटाएँ" -notStarted = "शुरू नहीं हुआ" -progress = "{{total}} में से {{done}} पूरा" -title = "सेटअप पूरा करें" - -[portal.home.onboarding.enterprise] -body = "Org-wide SSO + SCIM + RBAC, committed volume pricing, और air-gapped deployment." -cta = "ट्रायल शुरू करें" -ctaQuote = "कोटेशन प्राप्त करें" -lead = "250+ कर्मचारियों के लिए।" -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "डेस्कटॉप ऐप डिप्लॉय करें, हमेशा के लिए मुफ़्त।" -title = "PDF Editor डाउनलोड करें" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} सक्रिय · {{recommended}} अनुशंसित" -chip = "{{active}} सक्रिय" -title = "पॉलिसियाँ चालू करें" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} कनेक्टेड · हर PDF जहाँ भी पहुँचता है, नियंत्रित" -chip = "{{connected}} कनेक्टेड" -title = "अपने स्रोत कनेक्ट करें" - -[portal.home.quickActions] -subtitle = "आज के प्रमुख कार्य" -title = "त्वरित कार्रवाइयाँ" - -[portal.home.quickActions.buildPipeline] -blurb = "typed op library पर 3-step composer" -title = "पाइपलाइन बनाएँ" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, watched folders" -title = "स्रोत कनेक्ट करें" - -[portal.home.quickActions.issueApiKey] -blurb = "rate limits और IP allowlist के साथ scoped key" -title = "API key जारी करें" - [portal.infrastructure] manageEditorDeployment = "Editor डिप्लॉयमेंट प्रबंधित करें" sectionsAriaLabel = "इन्फ्रास्ट्रक्चर अनुभाग" @@ -6878,13 +7541,9 @@ heading = "API keys" subheading = "प्रति-key rate limits, अनुमतियाँ, और IP allowlists के साथ scoped credentials." [portal.infrastructure.apiKeys.card] -allowedIps = "अनुमत IPs" -anyIp = "कोई भी IP (कोई allowlist नहीं)" created = "बनाया गया" lastUsed = "अंतिम उपयोग" -permissions = "अनुमतियाँ" -rateLimit = "Rate limit" -rateLimitValue = "{{value}} req/min" +revoke = "key रद्द करें" usageMonth = "इस महीने का उपयोग" usageToday = "आज का उपयोग" @@ -6892,6 +7551,15 @@ usageToday = "आज का उपयोग" description = "Stirling API कॉल करना शुरू करने के लिए scoped key बनाएँ।" title = "अभी कोई API keys नहीं" +[portal.infrastructure.apiKeys.error] +load = "आपकी API keys लोड नहीं हो सकीं। कृपया फिर से प्रयास करें।" + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" रद्द करें? इस key का अभी भी उपयोग कर रहा कोई भी इंटीग्रेशन तुरंत 401 errors प्राप्त करना शुरू कर देगा। इसे पूर्ववत नहीं किया जा सकता।" +cancel = "रद्द करें" +confirm = "key रद्द करें" +title = "API key रद्द करें" + [portal.infrastructure.audit] filterAriaLabel = "श्रेणी के आधार पर audit events फ़िल्टर करें" heading = "Audit logs" @@ -6935,6 +7603,7 @@ all = "सभी" auth = "Auth" config = "Config" elevation = "अधिकार उन्नयन" +policy = "नीति" processing = "Processing" security = "Security" @@ -6944,19 +7613,30 @@ title = "आपके पास एक्सेस नहीं है" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "अधिकार उन्नयन" +policy = "नीति" processing = "Processing" totalEvents = "कुल events · 24h" +[portal.infrastructure.auditCatLabel] +auth = "प्रमाणीकरण" +config = "कॉन्फ़िग" +elevation = "उन्नयन" +policy = "नीति" +processing = "प्रोसेसिंग" +security = "सुरक्षा" + +[portal.infrastructure.auditStatusLabel] +danger = "त्रुटि" +info = "जानकारी" +success = "सफलता" +warning = "चेतावनी" + [portal.infrastructure.createKey] cancel = "रद्द करें" createKey = "key बनाएँ" done = "पूरा" -ipAllowlistHelper = "कॉमा-सेपरेटेड CIDR ranges. किसी भी IP की अनुमति देने के लिए खाली छोड़ें।" -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Key name" keyNamePlaceholder = "उदा. Production · ingest" -permissionsLabel = "अनुमतियाँ" secretKeyCaption = "Secret key" secretWarning = "इसे secrets manager में स्टोर करें। Stirling केवल hash स्टोर करता है — बाद में इसे पुनर्प्राप्त करने का कोई तरीका नहीं है।" subtitle = "Key को न्यूनतम आवश्यकता तक सीमित रखें। आप किसी भी समय रोटेट या revoke कर सकते हैं।" @@ -6964,207 +7644,9 @@ subtitleCreated = "इस secret को अभी कॉपी करें — title = "API key बनाएँ" titleCreated = "Key बनाई गई" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "डिप्लॉय किया" -environment = "Environment" -product = "उत्पाद" -status = "स्थिति" -version = "Version" -when = "कब" - -[portal.infrastructure.deployments.recent] -heading = "हाल के डिप्लॉयमेंट" -subheading = "Products और environments में नवीनतम rollouts." - -[portal.infrastructure.deployments.regionColumns] -instances = "इंस्टेंस" -latency = "Latency" -load = "Load" -p99 = "P99" -region = "क्षेत्र" -status = "स्थिति" -throughput = "Throughput" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "क्षेत्र" -subheading = "हर डिप्लॉय किए गए Stirling क्षेत्र का live health — latency, load, और rollout version." - -[portal.infrastructure.deployments.regions.empty] -description = "आपका workspace provision होने के बाद डिप्लॉय किए गए क्षेत्र यहाँ दिखाई देंगे।" -title = "कोई क्षेत्र डिप्लॉय नहीं" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "आपके workspace में document processing को power देने वाला model catalogue और routing." - -[portal.infrastructure.models.byom] -description = "On-prem या self-hosted model रजिस्टर करें और data-residency-bound processing के लिए उसे किसी क्षेत्र में pin करें।" -title = "अपना model लाएँ" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "आपके workspace के लिए उपलब्ध managed models, live latency और cost के साथ।" -subEnterprise = "Managed, bring-your-own, और on-prem models — per-region pinning उपलब्ध।" - -[portal.infrastructure.models.catalogue.empty] -description = "आपके workspace के catalogue में models यहाँ दिखाई देंगे।" -title = "कोई models उपलब्ध नहीं" - -[portal.infrastructure.models.columns] -cost = "लागत" -latency = "Latency" -load = "Load" -model = "Model" -status = "स्थिति" -type = "प्रकार" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "सक्रिय models" -avgLatency = "औसत latency" -included = "शामिल" -monthlySpend = "मासिक model खर्च" - -[portal.infrastructure.models.routing] -empty = "कोई routing rules कॉन्फ़िगर नहीं।" -heading = "Routing rules" -sub = "प्रत्येक operation को कौन सा model संभालता है। जब कोई संकीर्ण rule मेल नहीं खाता, तो default लागू होता है।" -subLocked = "Operations को विशिष्ट models तक route करें — paid plans पर उपलब्ध।" - -[portal.infrastructure.models.routing.lockedBanner] -description = "प्रत्येक operation और document type को कौन सा model संभालता है, इसे नियंत्रित करने के लिए Pro में अपग्रेड करें।" -title = "Model routing एक paid feature है" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "दस्तावेज़ प्रकार" -modelForAria = "{{operation}} के लिए model" -operation = "Operation" -routedTo = "Route किया गया" - -[portal.infrastructure.security.access.byok] -description = "अपने KMS से key दें। Stirling इसके साथ encrypt करता है लेकिन अभी भी पढ़ सकता है।" -label = "अपनी key लाएँ (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Keys कभी आपके KMS से बाहर नहीं जातीं। Stirling केवल ciphertext रखता है।" -label = "अपनी key अपने पास रखें (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling encryption keys प्रबंधित करता है। सबसे सरल — आपकी ओर से zero key ops." -label = "Stirling-held keys" - -[portal.infrastructure.security.accessPolicy] -heading = "दस्तावेज़ एक्सेस पॉलिसी" -subheading = "यह नियंत्रित करता है कि rest पर processed documents को कौन decrypt कर सकता है।" - -[portal.infrastructure.security.attestations] -heading = "Compliance attestations" -noReport = "कोई report उपलब्ध नहीं" -subheading = "Framework-by-framework audit posture, attested controls पर reports उपलब्ध।" -viewReport = "Report देखें →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Stirling platform को कवर करने वाले attestations और certifications." - -[portal.infrastructure.security.empty] -description = "आपके workspace की security configuration यहाँ दिखाई देगी।" -title = "Security posture उपलब्ध नहीं" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK के साथ, encryption keys कभी आपके KMS से बाहर नहीं जातीं। Stirling केवल ciphertext स्टोर और process करता है, जिसे आप किसी भी समय revoke कर सकते हैं।" -title = "Stirling आपके दस्तावेज़ decrypt नहीं कर सकता" - -[portal.infrastructure.security.ipAllowlist] -empty = "कोई IP ranges कॉन्फ़िगर नहीं — सभी IPs अनुमत।" -heading = "IP allowlist" -sub = "API access इन CIDR ranges तक सीमित है।" -subLocked = "API access को ज्ञात IP ranges तक सीमित करें — paid plans पर उपलब्ध।" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "API access को विशिष्ट networks तक सीमित करने के लिए Pro में अपग्रेड करें।" -title = "IP allowlisting एक paid feature है" - -[portal.infrastructure.security.ipColumns] -added = "जोड़ा गया" -addedBy = "जोड़ा" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Encryption key management" -keyId = "Key identifier" -lastRotated = "अंतिम बार रोटेट किया गया" -rotateKey = "key रोटेट करें" -rotationPolicy = "Rotation policy" -subheading = "वे keys जो rest पर documents को encrypt करती हैं — कौन decrypt कर सकता है, और keys कैसे rotate होती हैं।" - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) और hold-your-own-key (HYOK) custody Enterprise पर उपलब्ध हैं। अपने KMS से keys देने के लिए अपग्रेड करें।" -title = "आपके plan पर keys Stirling द्वारा managed हैं" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "एशिया प्रशांत" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "यूरोपीय संघ" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "संयुक्त राज्य" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "जहाँ दस्तावेज़ स्टोर और process किए जाते हैं।" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} उपयोग किया गया" - -[portal.infrastructure.storage.empty] -description = "Connected storage और उपयोग यहाँ दिखाई देंगे।" -title = "कोई storage कॉन्फ़िगर नहीं" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "सक्रिय" -activeRange = "0–{{value}}d" -archived = "Archived" -coldStorage = "cold storage" -deleted = "हटाया गया" -never = "कभी नहीं" -purged = "purged" - -[portal.infrastructure.storage.providers] -connect = "कनेक्ट करें" -connected = "कनेक्टेड" -heading = "Connected providers" -subheading = "जहाँ processed artifacts लिखे जाते हैं।" - -[portal.infrastructure.storage.retention] -heading = "Retention" -subheading = "Lifecycle deletion से पहले artifacts कितने समय तक रखे जाते हैं।" -windowLabel = "Default retention window" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} दिन" -days_other = "{{count}} दिन" -never = "कभी न हटाएँ" - -[portal.infrastructure.storage.totalUsage] -heading = "कुल उपयोग" -progressLabel = "उपयोग किया गया storage" -subheading = "सभी connected providers में खपत हुआ storage." +revoked = "रद्द" [portal.infrastructure.tabs] apiKeys = "API Keys" @@ -7174,6 +7656,49 @@ models = "Models" security = "Security" storage = "Storage" +[portal.integrations] +availableHeading = "उपलब्ध" +comingSoonHeading = "जल्द आ रहा है" +connect = "कनेक्ट करें" +connectionCount_one = "{{count}} कनेक्शन" +connectionCount_other = "{{count}} कनेक्शन" +customApi = "कस्टम API" +searchPlaceholder = "इंटीग्रेशन खोजें" +subtitle = "Stirling जिनसे बात कर सकता है, वह सब कुछ। क्रेडेंशियल्स यहाँ एक बार बनाए जाते हैं, फिर स्रोतों, नीतियों और पाइपलाइनों द्वारा संदर्भित किए जाते हैं।" +title = "इंटीग्रेशन" +typedName = "{{tool}} नाम" + +[portal.integrations.filters] +all = "सभी" +audit = "ऑडिट" +connected = "कनेक्टेड" +notify = "सूचित करें" +security = "सुरक्षा" +signing = "हस्ताक्षर" +storage = "स्टोरेज" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "कनेक्टेड" + +[portal.integrations.table] +integration = "इंटीग्रेशन" +worksWith = "इसके साथ काम करता है" + +[portal.integrations.worksWith] +pipelines = "पाइपलाइन" +policies = "नीतियाँ" +sources = "स्रोत" + +[portal.legal] +draft = "{{label}} · draft" +loadError = "यह दस्तावेज़ लोड नहीं हो सका। कृपया फिर से प्रयास करें।" +loading = "लोड हो रहा है…" +title = "कानूनी दस्तावेज़" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7182,33 +7707,17 @@ documents = "दस्तावेज़" editor = "Editor" home = "होम" infrastructure = "इन्फ्रास्ट्रक्चर" +integrations = "इंटीग्रेशन" pipelines = "पाइपलाइन्स" policies = "पॉलिसियाँ" procurement = "Procurement" -settings = "सेटिंग्स" sources = "स्रोत" usage = "Usage & Billing" users = "उपयोगकर्ता" -[portal.notifications] -markAllRead = "सभी को पढ़ा हुआ चिह्नित करें" -title = "सूचनाएँ" -viewAll = "सभी देखें" - -[portal.notifications.ariaLabel] -none = "सूचनाएँ, कोई अपठित नहीं" -unread_one = "सूचनाएँ, {{count}} अपठित" -unread_other = "सूचनाएँ, {{count}} अपठित" - -[portal.notifications.count] -allRead = "सभी पढ़े गए" -loading = "लोड हो रहा है" -new_one = "{{count}} नया" -new_other = "{{count}} नए" - -[portal.notifications.empty] -description = "कोई नई सूचनाएँ नहीं।" -title = "आप पूरी तरह अपडेट हैं" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Backend पर हर automated document pipeline: sources के सेट पर operations की ordered chain, trigger पर चलती है। इसके steps और sources के लिए किसी row पर क्लिक करें।" @@ -7218,54 +7727,87 @@ title = "पाइपलाइन्स" newPipeline = "नई पाइपलाइन" [portal.pipelines.builder] -addStep = "टूल जोड़ें" +activate = "Activate" back = "पाइपलाइन्स पर वापस जाएँ" +cannotFollow = "{{produced}} नहीं ले सकता" +chooseAccount = "एक खाता चुनें" +chooseDestination = "एक गंतव्य चुनें" +chooseOperation = "चुनें कि यह चरण क्या करता है" +chooseSource = "एक स्रोत चुनें" discard = "बदलाव छोड़ें" -enabled = "सक्षम" +inputs = "इनपुट" +inputSource = "इनपुट स्रोत" +inputTrigger = "ट्रिगर" keepEditing = "संपादन जारी रखें" -needsUpload = "अपलोड की गई फ़ाइल आवश्यक" +moreActions = "More actions" +needsConfiguring = "सेटअप आवश्यक है" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "आपकी खोज से कोई टूल मेल नहीं खाते।" -pipelineSettings = "पाइपलाइन सेटिंग्स" +pause = "Pause" +rename = "Rename pipeline" searchTools = "टूल खोजें" -selectToolBody = "अपनी पाइपलाइन बनाने के लिए एक टूल जोड़ें।" -selectToolTitle = "अभी कोई टूल नहीं" -toolSettings = "टूल सेटिंग्स" +sendToSystem = "दूसरे सिस्टम को भेजें" +stepsIncompatible = "ये चरण उनके पिछले चरण द्वारा उत्पन्न चीज़ पर नहीं चल सकते: {{tools}}।" +stepsNeedSetup = "सेव करने से पहले इन चरणों का सेटअप अभी भी आवश्यक है: {{tools}}।" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "अपरिचित operation, जैसा है वैसा रखा गया।" unsavedBody = "आपके पास unsaved बदलाव हैं। बाहर निकलने से पहले उन्हें save करें, या छोड़ दें?" unsavedTitle = "Unsaved बदलाव" -uploadUnsupported = "Uploaded files अभी pipelines में supported नहीं हैं, इसलिए ये steps save नहीं किए जा सकते: {{tools}}." usesDefaults = "Default settings के साथ चलता है" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "पिछले चरण की हर फ़ाइल को जोड़ता है" +fan-out = "पिछले चरण की प्रत्येक फ़ाइल के लिए एक बार चलता है" +format-mismatch = "{{accepts}} आवश्यक है, लेकिन पिछला चरण {{produced}} उत्पन्न करता है" +output-uncertain = "चल नहीं सकता: पिछले चरण का आउटपुट उसके सेटअप पर निर्भर करता है" +source-mismatch = "{{accepts}} आवश्यक है, लेकिन इस पाइपलाइन का इनपुट {{produced}} है" +undeclared-operation = "जाँच नहीं कर सकता कि यह चरण क्या स्वीकार करता है" [portal.pipelines.composer] addTool = "टूल जोड़ें" -cancel = "रद्द करें" -chainEmpty = "अपनी पाइपलाइन बनाना शुरू करने के लिए एक टूल जोड़ें।" create = "पाइपलाइन बनाएँ" -directory = "Output folder" -directoryHelp = "Server पर absolute path. यह configured allowed folders के भीतर होना चाहिए।" +createPaused = "Create paused" editingUnsupported = "Editing के लिए इन tool params को दिखाना अभी supported नहीं है।" -moveDown = "नीचे ले जाएँ" -moveUp = "ऊपर ले जाएँ" +editSource = "Edit source" name = "नाम" namePlaceholder = "उदा. Redaction sweep" -noSources = "अभी कोई स्रोत कनेक्ट नहीं हैं। पाइपलाइन फिर भी सीधे दी गई files पर चल सकती है।" noToolSettings = "इस टूल में कोई configurable settings नहीं हैं।" -operations_one = "Operation ({{count}})" -operations_other = "Operations ({{count}})" output = "Output" -removeStep = "Operation हटाएँ" save = "बदलाव save करें" scheduleEvery = "हर" -sources = "स्रोत" -sourcesLoading = "स्रोत लोड हो रहे हैं..." trigger = "Trigger" triggerManual = "केवल Manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "दिन" hours = "घंटे" minutes = "मिनट" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" हटाएँ? इसे पूर्ववत नहीं किया जा सकता।" cancel = "रद्द करें" @@ -7273,28 +7815,62 @@ confirm = "हटाएँ" title = "पाइपलाइन हटाएँ?" [portal.pipelines.detail] +clearHistory = "इतिहास साफ़ करें" delete = "पाइपलाइन हटाएँ" run = "अभी चलाएँ" [portal.pipelines.empty] action = "पाइपलाइन बनाएँ" +connectSource = "स्रोत कनेक्ट करें" description = "अपनी पहली पाइपलाइन बनाएँ: वे स्रोत चुनें जिन पर यह चलती है, operations को chain करें, और output कहाँ जाए चुनें।" title = "अभी कोई पाइपलाइन नहीं" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "सक्रिय" paused = "रोकी गई" total = "पाइपलाइन्स" -[portal.pipelines.output] -folder = "Folder में लिखें" -inline = "Files लौटाएँ" - [portal.pipelines.run] +allProcessed_one = "चलाने के लिए कुछ नहीं: स्रोत का {{count}} दस्तावेज़ पहले ही प्रोसेस हो चुका है।" +allProcessed_other = "चलाने के लिए कुछ नहीं: स्रोतों में सभी {{count}} दस्तावेज़ पहले ही प्रोसेस हो चुके हैं।" completed_one = "Run पूरा हुआ।" completed_other = "सभी {{count}} runs पूरे हुए।" empty = "चलाने के लिए कुछ नहीं: sources में process करने के लिए कोई documents नहीं थे।" failed = "Run विफल रहा: {{error}}" +inFlight = "चलाने के लिए कुछ नया नहीं: दस्तावेज़ अभी भी पहले के रन से प्रोसेस हो रहे हैं।" +parked_one = "चलाने के लिए कुछ नहीं: {{count}} दस्तावेज़ पहले विफल हुआ और पार्क किया गया है। कारण ठीक करें, फिर इसे फिर से प्रयास करने के लिए इतिहास साफ़ करें।" +parked_other = "चलाने के लिए कुछ नहीं: {{count}} दस्तावेज़ पहले विफल हुए और पार्क किए गए हैं। कारण ठीक करें, फिर इन्हें फिर से प्रयास करने के लिए इतिहास साफ़ करें।" running = "Run शुरू हुआ; अभी भी जारी है।" timeout = "Run अपेक्षा से अधिक समय ले रहा है; यह background में अभी भी पूरा हो सकता है।" @@ -7307,6 +7883,7 @@ name = "पाइपलाइन" sources = "स्रोत" status = "स्थिति" steps = "Steps" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Folder watch" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Scheduled" [portal.policies] +defaultName = "{{category}} नीति" subtitle = "Standing automations जो हर दस्तावेज़ पर tool pipeline लागू करते हैं। प्रत्येक policy upload या export पर चलती है, अपनी tool chain चलाती है, और enforced version को original के साथ save करती है।" title = "पॉलिसियाँ" [portal.policies.card] comingSoon = "Enterprise में अपग्रेड करें" notSetUp = "सेट अप नहीं" +requiresAiEngine = "AI engine आवश्यक है" + +[portal.policies.categories.classification] +desc = "अपलोड पर प्रत्येक दस्तावेज़ का प्रकार पहचानें और फ़ाइलिंग व खोज के लिए उसके metadata को tag करें।" +label = "वर्गीकरण" [portal.policies.categories.compliance] desc = "हर दस्तावेज़ पर HIPAA, GDPR, SOC 2, या FedRAMP आवश्यकताएँ लागू करें।" @@ -7344,6 +7927,13 @@ label = "Security" [portal.policies.config] scopeAll = "सभी दस्तावेज़" +[portal.policies.config.classification] +summary = "आपकी टीम के labels के विरुद्ध हर अपलोड किए गए दस्तावेज़ को वर्गीकृत करता है और उसके metadata को tag करता है।" + +[portal.policies.config.classification.rules] +0 = "वर्गीकृत करें" +1 = "metadata tag करें" + [portal.policies.config.compliance] summary = "System से बाहर जाने से पहले regulatory frameworks के विरुद्ध documents को validate करता है।" @@ -7371,6 +7961,29 @@ minConfidence = "न्यूनतम confidence" 2 = "Name" 3 = "Normalize" +[portal.policies.config.purview] +label = "Microsoft Purview sensitivity label लागू करें" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "Purview portal से label का GUID। label लागू करने से उसका metadata PDF पर लिखा जाता है; यह encrypt नहीं कर सकता, जिसके लिए Microsoft client आवश्यक है।" +labelName = "Label नाम (वैकल्पिक)" +labelNameHelp = "ID के साथ रिकॉर्ड किया जाता है ताकि लोग और tools label को पहचान सकें।" + +[portal.policies.config.purview.fields.method] +help = "Purview किसी नीति द्वारा लागू किए गए label और व्यक्ति द्वारा चुने गए label में अंतर करता है।" +label = "label कैसे रिकॉर्ड किया जाता है" +privileged = "जैसा किसी व्यक्ति ने चुना" +standard = "जैसा स्वतः लागू किया गया" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview label पढ़ें" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "दस्तावेज़ पर पहले से मौजूद label को पढ़ता है, ताकि बाद के चरण उस पर कार्रवाई कर सकें।" + [portal.policies.config.retention] summary = "दस्तावेज़ कितने समय तक रखे जाएँ, कब archive हों, और कब delete हों, इसे लागू करता है।" @@ -7438,6 +8051,7 @@ title = "अभी तक कोई गतिविधि नहीं" [portal.policies.endpoints] addWatermark = "Watermark" autoRedact = "PII संपादित करें" +classifyAndLabel = "वर्गीकृत करें" compressPdf = "Compress" flatten = "Flatten" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "आपकी नीतियां सहेजी गई है retry = "फिर से प्रयास करें" title = "Backend उपलब्ध नहीं" +[portal.policies.operations] +change = "बदलें कि यह चरण क्या करता है" +noResults = "कोई चरण इससे मेल नहीं खाता। product नाम, या \"scan\", \"notify\", \"attach\" आज़माएँ।" +searchPlaceholder = "चरण खोजें - \"scan\", \"attach\" या product नाम आज़माएँ" + +[portal.policies.operations.bodyMode] +binary = "Raw file bytes" +json = "JSON payload के अंदर" +multipart = "file upload (multipart) के रूप में" + +[portal.policies.operations.clamavScan] +description = "आपका अपना ClamAV server दस्तावेज़ जाँचता है; आपके network से कुछ बाहर नहीं जाता।" +label = "वायरस के लिए scan करें (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "macros, executables, scripts या embedded objects वाले दस्तावेज़ों को अस्वीकार करता है।" +label = "active content ब्लॉक करें" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive दस्तावेज़ जाँचता है और यदि यह clean नहीं है तो run रुक जाता है।" +label = "वायरस के लिए scan करें" + +[portal.policies.operations.confluenceAttach] +description = "प्रोसेस किए गए दस्तावेज़ को page पर फ़ाइल करता है।" +label = "Confluence page में संलग्न करें" + +[portal.policies.operations.consignoSubmit] +description = "दस्तावेज़ को signing के लिए ConsignO Cloud में जमा करता है।" +label = "हस्ताक्षर के लिए भेजें (ConsignO)" +note = "दस्तावेज़ जमा करता है। signed copy प्राप्त करना अभी समर्थित नहीं है, क्योंकि कोई चरण परिणाम को बाद के चरण में पास नहीं कर सकता।" + +[portal.policies.operations.customApiCall] +description = "call स्वयं बनाएँ: path, method, body और response handling।" +label = "कस्टम API call करें" + +[portal.policies.operations.discordNotify] +description = "एक channel को बताता है कि नीति ने दस्तावेज़ संभाला।" +label = "Discord पर संदेश पोस्ट करें" + +[portal.policies.operations.elasticIndex] +description = "नीति ने क्या किया, इसे searchable दस्तावेज़ के रूप में रिकॉर्ड करता है।" +label = "Elasticsearch में audit event index करें" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart upload APIs के लिए उपयुक्त है; JSON उन APIs के लिए उपयुक्त है जो payload के अंदर फ़ाइल चाहते हैं; binary raw bytes भेजता है।" +label = "दस्तावेज़ कैसे भेजें" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "एक JSON body जो as-is भेजी जाती है। {{document.base64}} फ़ाइल रखता है; {{document.*}} और {{run.*}} प्रति दस्तावेज़ भरे जाते हैं।" +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "खाता" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "form field जिसके अंतर्गत API दस्तावेज़ की अपेक्षा करता है।" +label = "File field name" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "से" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "header name/value pairs का JSON object। connection का credential आपके लिए जोड़ा जाता है।" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "message body के रूप में भेजा गया। {{document.*}} और {{run.*}} प्रति दस्तावेज़ भरे जाते हैं।" +label = "संदेश" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "connection के base URL में जोड़ा गया। यह कभी दूसरे host तक नहीं पहुँच सकता।" +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "drive में इसे कहाँ लिखना है। {{document.filename}} प्रति दस्तावेज़ भरा जाता है।" +label = "गंतव्य path" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report दस्तावेज़ को अपरिवर्तित छोड़ता है। Replace इसे लौटे हुए उत्तर से बदल देता है।" +label = "reply के साथ क्या करें" + +[portal.policies.operations.fields.signerEmail] +label = "हस्ताक्षरकर्ता का email" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "विषय" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio इस text का विश्लेषण करता है। {{document.title}} का उपयोग करें या वह text paste करें जिसे आप जाँचना चाहते हैं।" +label = "जाँचने के लिए text" + +[portal.policies.operations.fields.to] +label = "को" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud username" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "एक space को बताता है कि नीति ने दस्तावेज़ संभाला।" +label = "Google Chat पर संदेश पोस्ट करें" + +[portal.policies.operations.jiraAttach] +description = "प्रोसेस किए गए दस्तावेज़ को issue पर फ़ाइल करता है।" +label = "Jira issue में संलग्न करें" + +[portal.policies.operations.mailgunEmail] +description = "प्रोसेस किए गए दस्तावेज़ को attachment के रूप में भेजता है।" +label = "दस्तावेज़ email करें (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "प्रोसेस किए गए दस्तावेज़ को folder में लिखता है।" +label = "Nextcloud पर अपलोड करें" + +[portal.policies.operations.presidioAnalyze] +description = "आपका अपना Presidio server उसे मिले personal data की रिपोर्ट करता है।" +label = "personal data का पता लगाएँ" +note = "Presidio text पढ़ता है, files नहीं। उसे वह text दें जिसे आप जाँचना चाहते हैं — दस्तावेज़ स्वयं अपलोड नहीं होता।" + +[portal.policies.operations.responseMode] +replace = "दस्तावेज़ को reply से बदलें" +report = "दस्तावेज़ रखें, reply रिकॉर्ड करें" + +[portal.policies.operations.sendgridEmail] +description = "प्रोसेस किए गए दस्तावेज़ को attachment के रूप में भेजता है।" +label = "दस्तावेज़ email करें (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "एक channel को बताता है कि नीति ने दस्तावेज़ संभाला।" +label = "Slack पर संदेश पोस्ट करें" + +[portal.policies.operations.splunkEvent] +description = "नीति ने क्या किया, इसे वहाँ रिकॉर्ड करता है जहाँ आपके auditors पहले से देखते हैं।" +label = "Splunk को audit event भेजें" + +[portal.policies.operations.sumologicEvent] +description = "नीति ने क्या किया, इसे HTTP collector में रिकॉर्ड करता है।" +label = "Sumo Logic को audit event भेजें" + +[portal.policies.operations.teamsNotify] +description = "एक channel को बताता है कि नीति ने दस्तावेज़ संभाला।" +label = "Teams पर संदेश पोस्ट करें" + +[portal.policies.operations.webhookPost] +description = "दस्तावेज़ स्वयं आपके चुने हुए URL पर भेजता है।" +label = "दस्तावेज़ को webhook पर पोस्ट करें" + +[portal.policies.operations.zapierNotify] +description = "run के विवरण आपके automation को सौंपता है।" +label = "Zap या Make scenario trigger करें" + [portal.policies.stats] activeFor = "सक्रिय" dataProcessed = "प्रोसेस किया गया डेटा" @@ -7473,6 +8282,13 @@ label = "लागू किए गए दस्तावेज़" description = "कॉन्फ़िगर किया गया लेकिन चालू नहीं हो रहा" label = "रोका गया" +[portal.policies.table] +appliesTo = "इस पर लागू" +docs = "लागू दस्तावेज़" +enforces = "लागू करता है" +policy = "नीति" +status = "स्थिति" + [portal.policies.wizard.actions] back = "वापस" cancel = "रद्द करें" @@ -7480,6 +8296,10 @@ continue = "जारी रखें" enablePolicy = "नीति सक्षम करें" saveChanges = "बदलाव सहेजें" +[portal.policies.wizard.capability.classify] +desc = "आपकी टीम के labels से दस्तावेज़ का प्रकार पहचानता है और उसे tag करता है, ताकि वह category के अनुसार फ़ाइल और खोज हो सके।" +label = "दस्तावेज़ वर्गीकृत करें" + [portal.policies.wizard.capability.compress] desc = "दस्तावेज़ को छोटे फ़ाइल आकार में Compress करता है।" label = "फ़ाइल आकार घटाएं" @@ -7500,10 +8320,18 @@ label = "संवेदनशील जानकारी संपादित desc = "छिपा हुआ JavaScript हटाता है ताकि दस्तावेज़ खोले जाने पर कुछ भी अपने आप न चल सके।" label = "सक्रिय सामग्री हटाएं" +[portal.policies.wizard.capability.timestampPdf] +desc = "स्वतंत्र timestamp authority का उपयोग करके साबित करता है कि दस्तावेज़ किसी समय पर इसी सटीक रूप में मौजूद था। केवल hash भेजा जाता है - दस्तावेज़ कभी आपके server से बाहर नहीं जाता।" +label = "trusted timestamp जोड़ें" + [portal.policies.wizard.capability.watermark] desc = "हर पेज पर एक दिखाई देने वाला निशान (जैसे “Confidential”) लगाता है।" label = "Watermark लागू करें" +[portal.policies.wizard.classification] +description = "हर अपलोड किया गया दस्तावेज़ built-in labels के विरुद्ध वर्गीकृत किया जाता है और उपयुक्त प्रकारों से tag किया जाता है। label set आपकी पूरी टीम में साझा होता है।" +labelsHeading = "वर्गीकरण labels" + [portal.policies.wizard.errors] noTools = "पहले वर्कफ़्लो में कम से कम एक टूल सक्षम करें।" saveFailed = "नीति सहेजी नहीं जा सकी। कृपया फिर से प्रयास करें।" @@ -7548,73 +8376,118 @@ setUp = "{{category}} नीति सेट अप करें" [portal.policies.wizard.workflow] description = "चुनें कि यह नीति प्रोसेस किए जाने वाले हर दस्तावेज़ के साथ क्या करती है।" -[portal.policySummary] -activeSummary = "{{active}} / {{total}} सक्रिय" -noRule = "अभी तक कोई नियम लागू नहीं" -subtitle = "स्थायी ऑटोमेशन जिनसे हर दस्तावेज़ गुजरता है, चाहे उसे कौन सी pipeline संभालती हो।" -title = "आपके PDFs पर क्या चलता है" - [portal.policySummary.action] -comingSoon = "Enterprise में अपग्रेड करें" -configure = "कॉन्फ़िगर करें" setUp = "सेट अप करें" -[portal.policySummary.column] -activeRule = "सक्रिय नियम" -policy = "नीति" -status = "स्थिति" +[portal.processorFlow] +footnote = "गिनती पिछले 24 घंटों की है। Flow speed illustrative है।" +liveBadge = "Live" +stats = "{{connected}} कनेक्टेड · {{processed}} PDFs प्रोसेस किए गए" +title = "PDF Processor" -[portal.policySummary.empty] -description = "नीतियां कॉन्फ़िगर होने के बाद, श्रेणियां यहां दिखाई देंगी।" -title = "अभी तक कोई नीति नहीं" +[portal.processorFlow.lens] +ariaLabel = "View mode" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "सक्रिय" -off = "बंद" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "विफल" +heading = "परिणाम" +success = "Delivered" + +[portal.processorFlow.policies] +activeCount = "{{n}} सक्रिय" +count = "{{n}} · 24h" +heading = "नीतियाँ" +setUp = "सेट अप करें" soon = "जल्द" -[portal.processingStatus] -managePlan = "प्लान प्रबंधित करें" -volumeSuffix = "PDFs प्रोसेस किए गए · पिछले 30 दिन" +[portal.processorFlow.sankey] +waist = "नीतियाँ · {{n}} सक्रिय" + +[portal.processorFlow.sankey.empty] +description = "processor से दस्तावेज़ों का flow देखने के लिए एक स्रोत कनेक्ट करें और एक नीति चालू करें।" +title = "अभी कोई flow नहीं" + +[portal.processorFlow.sources] +comingSoonTag = "कनेक्ट करें" +editor = "Stirling PDF Editor" +heading = "स्रोत" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email intake" [portal.procurement] -reset = "Procurement रीसेट करें (demo)" subtitle = "अपनी टीम का मूल्यांकन, अनुबंध और ऑनबोर्डिंग कराएं। हर दस्तावेज़ एक जगह।" title = "Procurement" -[portal.procurement.action] -download = "डाउनलोड" -pay = "अभी भुगतान करें" -request = "अनुरोध करें" -sign = "समीक्षा करें और हस्ताक्षर करें" -upload = "अपलोड" - [portal.procurement.agreement] agreeCta = "सहमत हों और सदस्यता लें" +confidential = "गोपनीय" confirm = "मैंने Stirling Enterprise Agreement पढ़ लिया है और उससे सहमत हूं।" -eyebrow = "Agreement" -intro = "एक संयुक्त agreement आपकी deal को कवर करता है: Master Service Agreement, Order Form, EULA, और Data Processing Agreement। इसकी समीक्षा करें, फिर quote को committed subscription में स्वीकार करने के लिए सहमत हों।" -title = "अपने enterprise agreement की समीक्षा करें" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA और DPA, एक signature में संयुक्त।" +download = "डाउनलोड" +downloadDraftError = "agreement PDF generate नहीं हो सका - document renderer उपलब्ध नहीं है। कृपया थोड़ी देर बाद फिर से प्रयास करें या support से संपर्क करें।" +downloadError = "signed agreement डाउनलोड नहीं हो सका। कृपया फिर से प्रयास करें।" +legalName = "कानूनी entity name" +legalNamePlaceholder = "वह legal entity जो हस्ताक्षर करेगी" +loadError = "agreement लोड नहीं हो सका। कृपया फिर से प्रयास करें।" +loading = "agreement लोड हो रहा है..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "बदलाव का अनुरोध करें" +scrollHint = "समीक्षा के लिए scroll करें" +signatory = "हस्ताक्षरकर्ता का नाम" +signatoryPlaceholder = "पूरा नाम" +signatoryTitle = "हस्ताक्षरकर्ता का title" +signatoryTitlePlaceholder = "जैसे General Counsel" +signError = "आपका signature रिकॉर्ड नहीं हो सका। कृपया फिर से प्रयास करें।" [portal.procurement.builder] addons = "Add-ons" +addressLine1 = "पता पंक्ति 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "पता पंक्ति 2" +addressLine2Placeholder = "Suite, floor, building" back = "वापस" businessName = "व्यवसाय का नाम" businessNamePlaceholder = "आपकी कंपनी" +city = "शहर" +cityPlaceholder = "San Francisco" +completeRequired = "quote generate करने से पहले कृपया आवश्यक fields (चिह्नित *) को valid email के साथ पूरा करें।" +contactEmail = "संपर्क email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "संपर्क नाम" +contactNamePlaceholder = "Jane Doe" continue = "जारी रखें" -country = "देश" -countryEuro = "Eurozone (EUR €)" -countryUK = "United Kingdom (GBP £)" -countryUS = "United States (USD $)" +done = "हो गया" eula = "मैंने Stirling Enterprise EULA पढ़ लिया है और उससे सहमत हूं। यह इस quote से जनरेट किए गए agreement को नियंत्रित करता है।" generate = "Quote जनरेट करें" -included = "शामिल" indemnification = "IP indemnification" indemnificationSub = "EULA के अनुसार, हम योग्य IP दावों का बचाव करते हैं" -offlineLicense = "Offline / air-gapped licence" -offlineLicenseSub = "air-gapped self-hosted instance के लिए डाउनलोड योग्य licence फ़ाइल" +paperEyebrow = "Enterprise quote" +paperFor = "इसके लिए तैयार" +pdfSize = "PDF size" +poNumber = "PO number" +poNumberPlaceholder = "वैकल्पिक" +postalCode = "Postal code" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} नीतियाँ" +posture_essentials = "Essentials" +posture_essentialsSub = "Classification + Sharing — डिफ़ॉल्ट" +posture_governed = "Governed" +posture_governedSub = "Security और Routing जोड़ता है" +posture_regulated = "Regulated" +posture_regulatedSub = "हर category — Compliance, Retention, Ingestion" qbr = "Quarterly business reviews" qbrSub = "आपका SE हर तिमाही उपयोग और roadmap की समीक्षा करता है" +region = "State / region" +regionPlaceholder = "California" running = "{{annual}} / वर्ष · {{years}}-वर्ष {{tcv}}" s1Sub = "आपकी टीम, और वे PDFs जिन्हें आप हर साल चलाने की उम्मीद करते हैं।" s1Title = "आप कितना प्रोसेस करेंगे?" @@ -7623,6 +8496,12 @@ s2Title = "प्रतिबद्धता और सेवा" s3Sub = "Quote और उससे जनरेट होने वाले agreement के लिए।" s3Title = "आपका विवरण" serviceLevel = "Service level" +size_compact = "Compact" +size_compactSub = "अधिकतर text, 1 MB से कम" +size_heavy = "Heavy" +size_heavySub = "Scanned या image-heavy, 5 MB+" +size_standard = "Standard" +size_standardSub = "मिश्रित text और images, 1 से 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 व्यावसायिक घंटे · dedicated account manager · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "उसी व्यावसायिक दिन · named CSM slStandard = "Standard" slStandardSub = "अगला व्यावसायिक दिन · shared CSM · शामिल" stepOf = "चरण {{n}} / {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "वैकल्पिक" term = "अवधि" termDiscount = "{{pct}}% multi-year commitment discount लागू किया गया" title = "अपना quote बनाएं" @@ -7637,6 +8518,7 @@ training = "Onboarding और training" trainingSub = "आपकी टीम को शुरू कराने के लिए live sessions" users = "कुल उपयोगकर्ता" usersPlaceholder = "जैसे 250" +viewEula = "इसे पढ़ें" volEstimated = "{{count}} उपयोगकर्ताओं से अनुमानित (~2,000 PDFs प्रत्येक, automation सहित)। यदि आपको बेहतर पता हो तो संपादित करें।" volManual = "आपके आंकड़े का उपयोग हो रहा है। अपनी टीम के आकार से कभी भी फिर से अनुमान लगाएं।" volNoUsers = "पक्का नहीं? अपनी टीम का आकार दर्ज करें और हम इसका अनुमान लगाएंगे।" @@ -7645,55 +8527,55 @@ volumePlaceholder = "जैसे 1,000,000" years_one = "{{count}} वर्ष" years_other = "{{count}} वर्ष" -[portal.procurement.docs] -count_one = "{{count}} दस्तावेज़" -count_other = "{{count}} दस्तावेज़" -done = "पूर्ण" -here = "आप यहां हैं" -hide = "छिपाएं" -optional = "वैकल्पिक" -paidAddon = "Paid add-on" -show = "दिखाएं" -subtitle = "यात्रा के हर चरण में आपको जो कुछ चाहिए, deal के आगे बढ़ते ही सामने लाया जाता है।" -supportingSubtitle = "SOC 2, सुरक्षा समीक्षाएं, कर फ़ॉर्म और बहुत कुछ, जब आपकी सुरक्षा या procurement टीम मांगे तब तैयार। कुछ पर एक बार का शुल्क लगता है।" -supportingTitle = "आपके मूल्यांकन का समर्थन" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form और DPA एक signature में।" +download = "डाउनलोड" +eula = "EULA & Commercial Terms" +eulaSub = "software और self-serve commercial terms।" +invoice = "Invoice" +invoiceSub = "आपका पहला subscription invoice।" +laterInvoice = "आपके स्वीकार करने के बाद उपलब्ध" +laterQuote = "आपका quote जारी होने पर उपलब्ध" +quote = "Quote" +quoteSub = "आपका itemised enterprise quote।" +sla = "SLA Exhibit" +slaSub = "Support tiers और response targets।" +subprocessors = "Subprocessors" +subprocessorsSub = "Third parties जो आपकी ओर से data process करते हैं।" +subtitle = "आपके deal paperwork, किसी भी समय उपलब्ध।" title = "दस्तावेज़" -upcoming = "आगामी" +view = "देखें" [portal.procurement.error] title = "कुछ गलत हो गया" [portal.procurement.hero] -company = "आपकी enterprise deal" +barAria = "Stage {{current}} of {{total}}" ctaAgreement = "Agreement की समीक्षा करें और हस्ताक्षर करें" -ctaLive = "आप live हैं" -ctaPayment = "भुगतान जोड़ें" +ctaExploring = "अपना trial सेट अप करें" ctaQuote = "अपने quote की समीक्षा करें" +ctaReviewQuote = "quote की समीक्षा करें" ctaTrial = "अपना quote बनाएं" +documents = "दस्तावेज़" eyebrow = "Enterprise procurement" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "टीममेट्स को आमंत्रित करें" -keyDocs = "मुख्य दस्तावेज़" -nextStep = "अगला चरण: {{action}}" -notStarted = "शुरू नहीं हुआ" +licenseKey = "लाइसेंस key" +liveSub = "आपका organization provisioned है और आपका लाइसेंस सक्रिय है।" +liveTitle = "आप Stirling Enterprise पर live हैं" +next = "अगला: {{stage}}" open = "Procurement खोलें" scheduleCall = "कॉल शेड्यूल करें" -setup1Sub = "अपने टीममेट्स को आमंत्रित करें" -setup1Title = "PDF Editor deploy करें" -setup2Sub = "Editors और अन्य स्रोतों में processing चालू करें" -setup2Title = "PDF Processor कनेक्ट करें" -setup3Sub = "जब आवश्यकता हो Security, Compliance, Routing, या Retention चालू करें" -setup3Title = "अनुशंसित नीतियां जोड़ें" +sentenceAgreement = "agreement की समीक्षा करें और हस्ताक्षर करें" +sentenceLive = "provisioning और rollout" +sentencePayment = "purchase order और payment" +sentenceQuote = "अपने quote की समीक्षा करें और स्वीकार करें" +sentenceTrial = "अपना trial workspace सेट अप करें" [portal.procurement.journey] daysLeft_one = "{{count}} दिन बाकी" daysLeft_other = "{{count}} दिन बाकी" -engineerLabel = "आपके solutions engineer" -eyebrow = "आपका rollout" -live = "आप Stirling Enterprise पर live हैं" -nextStep = "अगला चरण: {{action}}" -subtitle = "आपका solutions engineer हर चरण में साथ है। एक समय में एक अगली कार्रवाई; पूरी checklist नीचे है।" -title = "Trial से live तक, एक guided path" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "एक हस्ताक्षर MSA, order form, EULA और DPA को कवर करता है।" @@ -7727,6 +8609,9 @@ downloadError = "Offline licence फ़ाइल अभी जनरेट न downloadOffline = "Offline licence डाउनलोड करें (.lic)" hint = "इसे activate करने के लिए इस key को self-hosted instance में paste करें, या अपने records के लिए रखें। इसे सुरक्षित रखें।" label = "आपकी licence key" +subtitle = "इस key से self-hosted instance सक्रिय करें, या इसे अपने records के लिए रखें।" +title = "आपकी लाइसेंस key" +trialFileHint = "यह आपकी trial licence file है। आपका agreement लागू होने के बाद, वापस आएँ और इसे फिर से डाउनलोड करें — online licence key के विपरीत, .lic file अपने आप update नहीं होगी।" [portal.procurement.link] cta = "Account लिंक करें" @@ -7739,65 +8624,62 @@ description = "आपकी subscription सक्रिय है और आप eyebrow = "Live" title = "आप Stirling Enterprise पर live हैं" -[portal.procurement.locked] -description = "Trial keys, committed-volume quotes, one-signature agreement, payment, और आपका document ledger, enterprise evaluation शुरू करने के बाद सब यहां रहते हैं।" -eyebrow = "केवल Enterprise" -talkToSales = "Sales से बात करें" -title = "Procurement track Enterprise के साथ खुलता है" - [portal.procurement.milestone] -accept = "स्वीकार करें और जारी रखें" -description = "PDF को अपनी टीम के साथ साझा करने के लिए डाउनलोड करें, तैयार होने पर स्वीकार करने के लिए वापस आएं, या बदलाव करें।" -download = "PDF डाउनलोड करें" downloadError = "Quote PDF अभी डाउनलोड नहीं किया जा सका — कृपया कुछ देर बाद फिर से प्रयास करें।" -edit = "Quote संपादित करें" -eyebrow = "Quote {{number}}" -perYear = " / वर्ष" -preparedFor = "{{company}} के लिए तैयार" -tcv = "{{value}} कुल contract value" -title = "आपका quote तैयार है" - -[portal.procurement.modal] -cancel = "रद्द करें" -chooseFile = "फ़ाइल चुनें" -close = "बंद करें" -downloadBody = "आपका डाउनलोड शीघ्र शुरू होगा।" -downloadCta = "डाउनलोड" -downloadTitle = "डाउनलोड" -noFile = "कोई फ़ाइल चयनित नहीं" -payBody = "Stripe के माध्यम से card या bank transfer द्वारा अपना committed contract भुगतान करें। भुगतान clear होते ही आपका workspace provision हो जाएगा।" -payCta = "Stripe पर जारी रखें" -payTitle = "भुगतान की पुष्टि करें" -requestBodyFree = "हम इसे on demand जनरेट करते हैं। पुष्टि करें और आपका solutions engineer इसे शीघ्र भेज देगा।" -requestBodyPaid = "यह paid add-on है। पुष्टि करें और आपका solutions engineer इसका scope तय करके paperwork भेजेगा।" -requestCta = "अनुरोध करें" -requestTitle = "इस दस्तावेज़ का अनुरोध करें" -signBody = "E-signature के लिए Stirling Enterprise Agreement खोलता है: एक हस्ताक्षर MSA, order form, EULA और DPA को कवर करता है। हम अपने आप countersign करते हैं और आप payment तक आगे बढ़ते हैं।" -signCta = "हस्ताक्षर के लिए खोलें" -signTitle = "अपने agreement की समीक्षा करें और हस्ताक्षर करें" -uploadBody = "हमें अपना PO भेजें और हम आपकी शर्तों पर उसके विरुद्ध invoice देंगे। PDF को drag करें या नीचे फ़ाइल चुनें।" -uploadCta = "Purchase order अपलोड करें" -uploadTitle = "अपना purchase order अपलोड करें" [portal.procurement.payment] description = "आपका quote स्वीकार कर लिया गया है और आपका licence पहले से सक्रिय है — आपकी टीम तुरंत शुरू कर सकती है। तैयार होने पर पहला invoice भुगतान करें; आप इसे यहां भुगतान या डाउनलोड कर सकते हैं, email की आवश्यकता नहीं।" +downloadAgreement = "signed agreement डाउनलोड करें" downloadInvoice = "Invoice डाउनलोड करें" -simulate = "प्राप्त भुगतान simulate करें (demo)" +eyebrow = "Payment" title = "Subscription बनाई गई" viewInvoice = "Invoice देखें और भुगतान करें" +[portal.procurement.review] +acceptCta = "quote स्वीकार करें" +annual = "वार्षिक शुल्क (वर्ष 1)" +downloadCta = "quote डाउनलोड करें" +poNumber = "Purchase order: {{po}}" +renewal = "term के बाद {{amount}}/year पर renew होता है (+{{pct}}% CPI)" +tcv = "{{years}}-year term, पूरा upfront paid: {{tcv}}" +validUntil = "{{date}} तक मान्य" + [portal.procurement.schedule] fallback = "Scheduler लोड नहीं हो सका।" fallbackLink = "Scheduling को नए tab में खोलें" +loading = "scheduler लोड हो रहा है..." subtitle = "आपका solutions engineer rollout में आपकी टीम का मार्गदर्शन करेगा। अपने लिए उपयुक्त समय चुनें।" title = "कॉल शेड्यूल करें" -[portal.procurement.status] -action = "कार्रवाई आवश्यक" -available = "उपलब्ध" -complete = "पूर्ण" -pending = "लंबित" -request = "अनुरोध पर" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sealed, no outbound" +back = "वापस" +businessName = "Business name" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, सबसे तेज़ शुरुआत" +continue = "जारी रखें" +deployment = "इसे कहाँ deploy करना चाहिए?" +eula = "मैं Stirling EULA & Commercial Terms से सहमत हूँ।" +fullName = "पूरा नाम" +fullNamePlaceholder = "आपका नाम" +invites = "teammates आमंत्रित करें · वैकल्पिक" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "call schedule करें" +seats = "testing करने वाले लोग" +seatsPlaceholder = "जैसे 15" +selfhost = "Self-hosted" +selfhostSub = "Docker या Kubernetes" +start = "trial शुरू करें" +stepOf = "चरण {{n}} / {{total}}" +subtitle = "14 दिनों के लिए मुफ़्त, card आवश्यक नहीं।" +subtitleDetails = "आपके quote और agreement के लिए कुछ विवरण।" +talkFirst = "पहले बात करना पसंद करेंगे?" +title = "अपना trial सेट अप करें" +viewEula = "इसे पढ़ें" +workEmail = "Work email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "विस्तार करने से 7 दिन जुड़ते हैं और आपके solutions engineer को सूचना मिलती है।" @@ -7808,109 +8690,113 @@ maxed = "अधिकतम हो गया" subtitle = "आपका free trial {{date}} तक चलता है। Card आवश्यक नहीं।" title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutable audit, और dedicated SE।" -homeCta = "Trial शुरू करें →" -homeHeadline = "लाखों PDFs प्रोसेस करें।" - -[portal.recentActivity] -title = "हाल की गतिविधि" -viewAll = "सभी देखें" - -[portal.recentActivity.empty] -description = "Pipeline runs, deploys और agent events यहां दिखाई देंगे।" -title = "अभी यहां कुछ नहीं" - -[portal.search] -ariaLabel = "खोजें" -placeholder = "Stirling खोजें — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "उपलब्ध होने पर quick actions यहां दिखाई देंगी।" -noActionsTitle = "कोई quick actions नहीं" -noMatches = "\"{{query}}\" के लिए कोई मिलान नहीं" -noMatchesDescription = "कोई अलग keyword आज़माएं या catalogue browse करें।" - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Account link" -[portal.shell.header] -accountFallback = "Account" -accountMenu = "Account menu" -darkMode = "Dark mode" -lightMode = "Light mode" -search = "खोजें" -searchPlaceholder = "खोजें…" -signOut = "Sign out" -switchToDark = "Dark theme पर स्विच करें" -switchToLight = "Light theme पर स्विच करें" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Stirling account लिंक करें" -planEditor = "Editor plan" -planEnterprise = "Enterprise plan" -planProcessor = "Processor plan" primaryNav = "Primary navigation" switchApp = "App बदलें" +[portal.shell.topbar] +closeNav = "navigation बंद करें" +openNav = "navigation खोलें" +search = "खोजें" + [portal.sources] subtitle = "पुनः उपयोग योग्य input connections जो दस्तावेज़ों को Stirling में भेजते हैं। एक connection को एक बार कॉन्फ़िगर करें, फिर उसे किसी भी संख्या में policies से reference करें। इसके config और इसे उपयोग करने वाली policies के लिए किसी row पर click करें।" title = "स्रोत" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "स्रोत कनेक्ट करें" +[portal.sources.builder] +back = "स्रोतों पर वापस" +backToTypes = "सभी स्रोत प्रकार" +cancel = "रद्द करें" +chooseHint = "चुनें कि दस्तावेज़ कहाँ से आते हैं। greyed-out connectors आने वाले हैं।" +comingSoon = "जल्द आ रहा है" +comingSoonHeading = "जल्द आ रहा है" +create = "स्रोत बनाएँ" +createTitle = "स्रोत कनेक्ट करें" +delete = "हटाएँ" +editTitle = "स्रोत संपादित करें" +enabled = "सक्षम" +save = "बदलाव सेव करें" + +[portal.sources.builder.folderAccess] +description = "Folder automations केवल उन folders का उपयोग कर सकते हैं जिन्हें administrator ने अनुमति दी है। इसे Folder Access settings में जोड़ें, फिर फिर से प्रयास करें।" +openSettings = "Folder Access settings" +title = "इस folder की अनुमति नहीं है" + [portal.sources.delete] body = "\"{{name}}\" हटाएं? इसे वापस नहीं किया जा सकता। इसे reference करने वाली policies को update करना होगा।" cancel = "रद्द करें" confirm = "हटाएं" title = "स्रोत हटाएं?" -[portal.sources.detail] -closeAriaLabel = "विवरण बंद करें" -delete = "स्रोत हटाएं" -docs24h = "पिछले 24h" -docs30d = "पिछले 30 दिन" -docsTotal = "कुल देखे गए" -docsTrend = "पिछले 30 दिनों में दस्तावेज़" -documents = "दस्तावेज़" -edit = "संपादित करें" -notReferenced = "किसी भी नीति द्वारा reference नहीं किया गया, इसलिए इसे हटाना सुरक्षित है।" -pause = "रोकें" -resume = "फिर शुरू करें" -subtitle = "{{type}} · {{status}}" -usedBy = "इसके द्वारा उपयोग किया गया" - -[portal.sources.empty] -description = "एक folder (और जल्द ही, cloud storage) कनेक्ट करें ताकि आपकी policies के पास दस्तावेज़ खींचने की जगह हो।" -title = "अभी तक कोई स्रोत कनेक्ट नहीं" - [portal.sources.kpi] inUse = "उपयोग में" total = "Connections" unused = "Unused" +[portal.sources.networkFields.connection] +helperText = "server address और credentials के साथ stored connection। इसे संदर्भित करने वाले हर स्रोत द्वारा पुनः उपयोग किया जाता है।" +label = "Connection" + +[portal.sources.networkFields.directory] +helperText = "poll करने के लिए server पर folder, login home या share root के सापेक्ष। root के लिए खाली छोड़ें।" +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume हर policy द्वारा process होने के बाद server से प्रत्येक file हटा देता है।" +label = "Read mode" + +[portal.sources.networkFields.mode.options] +consume = "Consume: प्रत्येक file को एक बार process करें" +snapshot = "Snapshot: हर run में folder फिर से पढ़ें" + +[portal.sources.networkFields.recursive] +label = "Folder depth" + +[portal.sources.networkFields.recursive.options] +all = "subfolders शामिल करें" +top = "केवल top level" + [portal.sources.status] active = "सक्रिय" disabled = "Disabled" unused = "Unused" [portal.sources.table] +documents = "दस्तावेज़" source = "स्रोत" status = "स्थिति" +type = "Type" usedBy = "Policies" +[portal.sources.types.box] +description = "आपकी team द्वारा पहले से shared Box folders से दस्तावेज़ pull करें।" +label = "Box" + +[portal.sources.types.dropbox] +description = "shared Dropbox folder से दस्तावेज़ pull करें।" +label = "Dropbox" + [portal.sources.types.editor] +description = "policy और AI runs में, editor में आपकी team द्वारा process किए गए दस्तावेज़।" label = "Editor" +noPolicies = "editor से अभी तक कोई policies run नहीं हुईं।" + +[portal.sources.types.email] +description = "dedicated mailbox पर भेजे गए attachments को दस्तावेज़ों में बदलें।" +label = "Email inbox" [portal.sources.types.folder] description = "नए दस्तावेज़ों के लिए server पर directory देखें।" @@ -7943,25 +8829,80 @@ label = "Folder depth" all = "Subfolders शामिल करें" top = "केवल top level" +[portal.sources.types.ftp] +description = "नए दस्तावेज़ों के लिए FTP या FTPS server folder poll करें।" +label = "FTP" + +[portal.sources.types.googledrive] +description = "shared Google Drive folders से दस्तावेज़ pull करें।" +label = "Google Drive" + +[portal.sources.types.network] +description = "नए दस्तावेज़ों के लिए SMB network share देखें।" +label = "Network drive" + +[portal.sources.types.onedrive] +description = "नए दस्तावेज़ों के लिए OneDrive folders देखें।" +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 या S3-compatible bucket से दस्तावेज़ pull करें।" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "bucket और credentials रखने वाला stored connection। इसे संदर्भित करने वाले हर source और pipeline output द्वारा पुनः उपयोग किया जाता है।" +label = "Connection" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume हर policy द्वारा process होने के बाद bucket से प्रत्येक object हटा देता है।" +label = "Read mode" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: प्रत्येक object को एक बार process करें" +snapshot = "Snapshot: हर run में bucket फिर से पढ़ें" + +[portal.sources.types.s3.fields.prefix] +helperText = "केवल वे objects process किए जाते हैं जिनकी keys इस prefix से शुरू होती हैं।" +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "नए दस्तावेज़ों के लिए SFTP drop folder poll करें।" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint document libraries से दस्तावेज़ pull करें।" +label = "SharePoint" + [portal.sources.types.unknown] label = "स्रोत" +[portal.sources.types.webhook] +createNote = "जब आप यह webhook बनाते हैं, तो delivery URL और signing secret generate किए जाते हैं। Senders उस URL पर दस्तावेज़ POST करते हैं और referencing policies arrival पर run होती हैं।" +description = "किसी भी external system से signed HTTP POST द्वारा दस्तावेज़ प्राप्त करें।" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "signing secret webhook बनाते समय एक बार दिखाया जाता है। इसे roll करने के लिए source फिर से बनाएँ।" + +[portal.sources.types.webhook.reveal] +copy = "कॉपी करें" +done = "हो गया" +secret = "Signing secret" +secretHelp = "इस key (HMAC-SHA256) से हर delivery के raw body पर sign करें और इसे X-Stirling-Signature header के रूप में भेजें।" +secretWarning = "signing secret अभी कॉपी करें। आपकी सुरक्षा के लिए यह केवल एक बार दिखाया जाता है और बाद में पुनः प्राप्त नहीं किया जा सकता।" +title = "Webhook बनाया गया" +url = "Delivery URL" +usage = "binary content type (application/pdf या application/octet-stream) के साथ प्रत्येक दस्तावेज़ को raw request body के रूप में delivery URL पर POST करें। Referencing policies arrival पर अपने आप run होती हैं।" + [portal.sources.wizard] -back = "वापस" -cancel = "रद्द करें" -continue = "जारी रखें" -editTitle = "स्रोत संपादित करें" name = "नाम" namePlaceholder = "जैसे Claims intake" -save = "बदलाव सहेजें" -subtitle = "चरण {{current}} / {{total}} · {{label}}" -title = "स्रोत कनेक्ट करें" type = "प्रकार" -[portal.sources.wizard.steps] -chooseType = "प्रकार चुनें" -configure = "कॉन्फ़िगर करें" -review = "समीक्षा करें और कनेक्ट करें" +[portal.stepModal] +close = "बंद करें" [portal.usage] managePayment = "भुगतान प्रबंधित करें" @@ -8021,16 +8962,9 @@ summary = "Team का मालिक — उसके members के resources 2 = "Default policy के माध्यम से portal access" 3 = "Member जो कुछ कर सकता है, वह सब" -[portal.welcome] -ariaLabel = "Stirling PDF में आपका स्वागत है" -badge = "Open-source" -installEditor = "Editor install करें" -inviteTeammates = "टीममेट्स को आमंत्रित करें" -openInBrowser = "Browser में खोलें" -perks = "हमेशा मुफ़्त · Self-hostable" -subtitle = "दुनिया का सबसे सुरक्षित PDF Editor सभी आकार की teams के लिए मुफ़्त है। इसमें 60+ PDF operations और SSO शामिल हैं।" -title = "स्वागत है" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Unlimited" [printFile] title = "फ़ाइल प्रिंट करें" @@ -8613,6 +9547,14 @@ text = "अपने PDF पृष्ठों को 90-डिग्री इ [rotate.tooltip.header] title = "रोटेशन सेटिंग्स का अवलोकन" +[ruler] +clearAll = "सभी साफ़ करें" +hideAllLabels = "सभी labels छिपाएँ" +hideSmallLabels = "छोटे labels छिपाएँ" +physicalValues = "Physical" +scaled = "Scaled" +showAllLabels = "सभी labels दिखाएँ" + [sanitize] desc = "PDF फ़ाइलों से संभावित रूप से हानिकारक तत्व हटाएँ।" sanitizationResults = "सैनिटाइज़ेशन परिणाम" @@ -8654,31 +9596,45 @@ label = "XMP मेटाडाटा हटाएँ" [sanitize.steps] settings = "सेटिंग्स" -[sanitizePdf] -tags = "साफ,सुरक्षित,सुरक्षा,खतरे-हटाएं" - [sanitizePDF] title = "PDF सैनिटाइज़ करें" +[sanitizePdf] +tags = "साफ,सुरक्षित,सुरक्षा,खतरे-हटाएं" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "पृष्ठ-स्केल समायोजित करें" +[scaleSettings] +activeScale = "सक्रिय Scale" +apply = "Scale लागू करें" +applyCalibration = "Calibration लागू करें" +calculatedScale = "Calculated scale" +calibrate = "Calibrate" +calibrating = "कैलिब्रेट हो रहा है" +calibrationDistanceRequired = "शून्य से अधिक वास्तविक दूरी दर्ज करें" +calibrationInvalid = "इस माप से पैमाना गणना करने में असमर्थ" +calibrationPaperDistance = "मापी गई पृष्ठ दूरी: {{distance}}" +calibrationTitle = "पैमाना कैलिब्रेट करें" +calibrationTooltip = "पैमाना स्वचालित रूप से गणना करने के लिए कोई ज्ञात दूरी मापें" +customScale = "कस्टम पैमाना" +noneSet = "कोई कस्टम पैमाना सेट नहीं है" +presets = "पूर्वनिर्धारित पैमाने" +ratio = "पैमाना अनुपात" +ratioHelp = "अनुपात: 1 पृष्ठ इकाई = X वास्तविक इकाइयाँ" +ratioPlaceholder = "उदा., 100" +ratioPositive = "अनुपात शून्य से अधिक होना चाहिए" +ratioRequired = "अनुपात आवश्यक है" +realDistance = "वास्तविक दूरी" +realDistancePlaceholder = "उदा., 5" +reset = "डिफ़ॉल्ट पर रीसेट करें" +unit = "इकाई" +unitInvalid = "अमान्य इकाई: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "कोण सीमा:" -10 = "आउटपुट में श्वेत बॉर्डर को रोकने के लिए जोड़े और हटाए गए बॉर्डर का आकार निर्धारित करता है (डिफ़ॉल्ट: 1)।" -2 = "छवि को घुमाने के लिए आवश्यक न्यूनतम सापेक्ष कोण निर्धारित करता है (डिफ़ॉल्ट: 10)।" -3 = "टॉलरेंस:" -4 = "अनुमानित पृष्ठभूमि रंग के आसपास रंग विविधता की सीमा निर्धारित करता है (डिफ़ॉल्ट: 30)।" -5 = "न्यूनतम क्षेत्र:" -6 = "एक फोटो के लिए न्यूनतम क्षेत्र सीमा निर्धारित करता है (डिफ़ॉल्ट: 10000)।" -7 = "न्यूनतम कंटूर क्षेत्र:" -8 = "एक फोटो के लिए न्यूनतम कंटूर क्षेत्र सीमा निर्धारित करता है" -9 = "बॉर्डर आकार:" - [scannerImageSplit] submit = "छवि स्कैन निकालें" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "यह क्या करता है" whatThisDoesDesc = "स्कैन्ड पृष्ठ या समग्र छवि से प्रत्येक फोटो को स्वचालित रूप से ढूंढकर निकालता है—मैन्युअल क्रॉपिंग की आवश्यकता नहीं।" whenToUse = "कब उपयोग करें" +[ScannerImageSplit.selectText] +1 = "कोण सीमा:" +10 = "आउटपुट में श्वेत बॉर्डर को रोकने के लिए जोड़े और हटाए गए बॉर्डर का आकार निर्धारित करता है (डिफ़ॉल्ट: 1)।" +2 = "छवि को घुमाने के लिए आवश्यक न्यूनतम सापेक्ष कोण निर्धारित करता है (डिफ़ॉल्ट: 10)।" +3 = "टॉलरेंस:" +4 = "अनुमानित पृष्ठभूमि रंग के आसपास रंग विविधता की सीमा निर्धारित करता है (डिफ़ॉल्ट: 30)।" +5 = "न्यूनतम क्षेत्र:" +6 = "एक फोटो के लिए न्यूनतम क्षेत्र सीमा निर्धारित करता है (डिफ़ॉल्ट: 10000)।" +7 = "न्यूनतम कंटूर क्षेत्र:" +8 = "एक फोटो के लिए न्यूनतम कंटूर क्षेत्र सीमा निर्धारित करता है" +9 = "बॉर्डर आकार:" + [search] noResults = "कोई परिणाम नहीं मिला" placeholder = "खोज शब्द दर्ज करें..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "आपका Stirling-PDF सर्वर ऑफ़ [session] expired = "आपका सत्र समाप्त हो गया है। कृपया पृष्ठ को रिफ्रेश करें और पुन: प्रयास करें।" -[sessionManagement.tooltip] -header = "हस्ताक्षर सत्र प्रबंधन" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "नए प्रतिभागी हस्ताक्षर क्रम के अंत में जोड़े जाते हैं" -bullet2 = "सत्र के अंतिम रूप के बाद प्रतिभागी नहीं जोड़े जा सकते" -bullet3 = "प्रत्येक प्रतिभागी की बारी आने पर उसे सूचना मिलती है" -description = "आप अंतिम रूप देने से पहले किसी भी समय सक्रिय सत्र में और प्रतिभागी जोड़ सकते हैं।" -title = "प्रतिभागी जोड़ना" - -[sessionManagement.tooltip.finalization] -bullet1 = "पूर्ण अंतिमकरण: सभी प्रतिभागियों ने हस्ताक्षर किए हैं" -bullet2 = "आंशिक अंतिमकरण: कुछ प्रतिभागियों ने अभी हस्ताक्षर नहीं किए" -bullet3 = "बिना हस्ताक्षर वाले प्रतिभागियों को अंतिम दस्तावेज़ से बाहर रखा जाएगा" -bullet4 = "अंतिम रूप देने के बाद, आप हस्ताक्षरित PDF को सक्रिय फ़ाइलों में लोड कर सकते हैं" -description = "अंतिमकरण सभी हस्ताक्षरों को एकल हस्ताक्षरित PDF में संयोजित करता है। इस क्रिया को वापस नहीं लिया जा सकता।" -title = "सत्र अंतिमकरण" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "जिन प्रतिभागियों ने पहले ही हस्ताक्षर कर दिए हैं, उन्हें नहीं हटाया जा सकता" -bullet2 = "हटाए गए प्रतिभागियों को अब सूचनाएँ नहीं मिलेंगी" -bullet3 = "हस्ताक्षर क्रम स्वतः समायोजित होता है" -description = "प्रतिभागियों को हस्ताक्षर से पहले सत्रों से हटाया जा सकता है।" -title = "प्रतिभागियों को हटाना" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "प्रत्येक हस्ताक्षर PDF पर क्रमवार लागू होता है" -bullet2 = "बाद के साइनर पहले के हस्ताक्षर देख सकते हैं" -bullet3 = "अनुमोदन कार्यप्रवाह और कानूनी कस्टडी चेन के लिए महत्वपूर्ण" -description = "सत्र बनाते समय आप जो क्रम निर्दिष्ट करते हैं वह निर्धारित करता है कि पहले कौन हस्ताक्षर करेगा।" -title = "हस्ताक्षर क्रम" - [settings] close = "बंद करें" +[settings.ai] +documents = "दस्तावेज़ और RAG" +general = "सामान्य" +limits = "सीमाएँ और प्रदर्शन" +models = "मॉडल और प्रदाता" +title = "AI" + [settings.configuration] advanced = "एडवांस्ड" database = "डेटाबेस" endpoints = "एंडपॉइंट्स" features = "फ़ीचर्स" +folderAccess = "फ़ोल्डर पहुँच" mcp = "MCP Server" storageSharing = "फ़ाइल भंडारण और शेयरिंग" systemSettings = "सिस्टम सेटिंग्स" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "डिफ़ॉल्ट PDF एडिटर" defaultPdfEditorActive = "Stirling PDF आपका डिफ़ॉल्ट PDF एडिटर है" defaultPdfEditorChecking = "जांच हो रही है..." defaultPdfEditorInactive = "कोई अन्य एप्लिकेशन डिफ़ॉल्ट है" +defaultPdfEditorRemind = "डिफ़ॉल्ट के रूप में सेट करने के लिए मुझे याद दिलाएँ" +defaultPdfEditorRemindDescription = "जब Stirling PDF आपका डिफ़ॉल्ट PDF एप्लिकेशन न हो, तो बैनर दिखाएँ।" defaultPdfEditorSet = "पहले से डिफ़ॉल्ट" defaultStartupView = "लॉन्च पर डिफ़ॉल्ट दृश्य" defaultStartupViewDescription = "ऐप शुरू होने पर बाएँ कॉलम में कौन-सा टैब सक्रिय हो, चुनें" @@ -8817,9 +9763,18 @@ languageDescription = "प्रदर्शन भाषा चुनें" logout = "लॉग आउट" setAsDefault = "डिफ़ॉल्ट बनाएं" theme = "थीम" +themeAccent = "एक्सेंट रंग" +themeAccentCustom = "कस्टम रंग" +themeAccentCustomHint = "कस्टम (डिफ़ॉल्ट अनुशंसित)" +themeAccentDark = "डार्क मोड एक्सेंट रंग" +themeAccentDefault = "डिफ़ॉल्ट" +themeAccentDefaultHint = "डिफ़ॉल्ट थीम (अनुशंसित)" +themeAccentDescription = "बटन, लिंक और हाइलाइट इसके अनुसार होते हैं, और यह ऐप को हल्का रंग देता है। लाइट और डार्क, दोनों के अपने-अपने रंग हैं। सिस्टम जो सक्रिय हो उसका उपयोग करता है।" +themeAccentLight = "लाइट मोड एक्सेंट रंग" themeDark = "डार्क" themeDescription = "लाइट और डार्क मोड के बीच स्विच करें" themeLight = "लाइट" +themeReset = "थीम को डिफ़ॉल्ट पर पुनर्स्थापित करें" themeSystem = "System" title = "सामान्य" user = "उपयोगकर्ता" @@ -8833,6 +9788,12 @@ intro = "अपने संगठन के लिए उपयोगकर् learnMore = "दस्तावेज़ीकरण में और जानें" title = "सिस्टम प्रशासकों के लिए" +[settings.general.loginLanding] +description = "Stirling Cloud में साइन इन करने पर कौन सा ऐप खुले, चुनें।" +editor = "एडिटर" +processor = "प्रोसेसर" +title = "साइन इन करने के बाद" + [settings.general.mode] fullscreen = "फुलस्क्रीन" sidebar = "साइडबार" @@ -9013,9 +9974,6 @@ title = "नीतियाँ और गोपनीयता" [settings.preferences] title = "प्राथमिकताएँ" -[settings.search] -placeholder = "सेटिंग्स पेज खोजें..." - [settings.security] description = "अपने खाते को सुरक्षित रखने के लिए अपना पासवर्ड अपडेट करें।" title = "सुरक्षा" @@ -9041,6 +9999,11 @@ security = "सुरक्षा" telegram = "Telegram" title = "सुरक्षा और प्रमाणीकरण" +[settings.signOut] +confirm = "क्या आप वाकई साइन आउट करना चाहते हैं?" +submit = "साइन आउट" +title = "साइन आउट" + [settings.team] title = "टीम" @@ -9156,6 +10119,7 @@ last = "अंतिम पृष्ठ" maintainRatio = "आनुपातिक अनुपात बनाए रखें टॉगल करें" next = "अगला पृष्ठ" noSavedSigs = "कोई सहेजा गया हस्ताक्षर नहीं मिला" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "व्यक्तिगत हस्ताक्षर" previous = "पिछला पृष्ठ" redo = "फिर करें" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "पृष्ठभूमि हटाना व hint = "अपने हस्ताक्षर की PNG या JPG इमेज अपलोड करें" label = "हस्ताक्षर की इमेज अपलोड करें" placeholder = "इमेज फ़ाइल चुनें" +previewAlt = "Current image signature" processing = "छवि संसाधित हो रही है..." removeBackground = "सफेद पृष्ठभूमि हटाएँ (पारदर्शी बनाएँ)" @@ -9207,6 +10172,17 @@ saved = "ऊपर से एक सहेजा हुआ हस्ताक् text = "ऊपर अपना नाम दर्ज करने के बाद, PDF पर कहीं भी क्लिक करके अपना हस्ताक्षर रखें।" title = "हस्ताक्षर कैसे जोड़ें" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "हस्ताक्षर को स्थानांतरित करें" pause = "प्लेसमेंट रोकें" @@ -9653,10 +10629,30 @@ title = "सर्वर पर अपलोड करें" updateButton = "सर्वर पर अपडेट करें" uploadButton = "सर्वर पर अपलोड करें" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "यहाँ क्लिक करें" +fallback = "यदि यह स्वचालित रूप से नहीं खुला," +opening = "Swagger UI नए टैब में खुल रहा है..." tags = "api,documentation,swagger,endpoints,development" title = "API दस्तावेज़ीकरण" @@ -9764,12 +10760,39 @@ selectFilesHint = "यह उपकरण चलाने के लिए Activ singleFileScope = "केवल इस पर लागू: {{fileName}}" viewerMode = "एकाधिक फ़ाइलें चुनने के लिए फ़ाइल संपादक पर स्विच करें।" +[toolFormat] +ANY = "कोई भी फ़ाइल" +CBR = "कॉमिक बुक आर्काइव (CBR)" +CBZ = "कॉमिक बुक आर्काइव (CBZ)" +CSV = "CSV फ़ाइल" +EBOOK = "ईबुक" +EMAIL = "ईमेल" +EXCEL = "स्प्रेडशीट" +HTML = "वेब पेज" +IMAGE = "छवियाँ" +JAVASCRIPT = "JavaScript फ़ाइल" +JSON = "JSON फ़ाइल" +MARKDOWN = "Markdown फ़ाइल" +NONE = "कोई फ़ाइल नहीं" +PCL = "PCL प्रिंट फ़ाइल" +PDF = "PDF" +PDF_ENCRYPTED = "पासवर्ड-सुरक्षित PDF" +POSTSCRIPT = "PostScript फ़ाइल" +PPT = "प्रस्तुति" +TEXT = "टेक्स्ट फ़ाइल" +VIDEO = "वीडियो" +WORD = "Word दस्तावेज़" +XML = "XML फ़ाइल" +XPS = "XPS दस्तावेज़" +ZIP = "ZIP आर्काइव" + [toolPanel] alpha = "Alpha" backToAllTools = "सभी उपकरणों पर वापस" collapse = "पैनल समेटें" expand = "पैनल फैलाएँ" goBack = "वापस जाएं" +pdfTools = "PDF Tools" placeholder = "शुरू करने के लिए कोई टूल चुनें" premiumFeature = "प्रीमियम फीचर:" toolsHeader = "उपकरण" @@ -9806,11 +10829,6 @@ noToolsFound = "कोई टूल नहीं मिला" quickAccess = "त्वरित पहुँच" searchPlaceholder = "टूल खोजें..." -[toolPicker.categories] -advancedTools = "उन्नत टूल" -recommendedTools = "अनुशंसित टूल" -standardTools = "मानक टूल" - [toolPicker.subcategories] advancedFormatting = "उन्नत फ़ॉर्मेटिंग" automation = "स्वचालन" @@ -9824,6 +10842,9 @@ removal = "हटाना" signing = "हस्ताक्षर" verification = "सत्यापन" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "कोई टूल नहीं मिला" noTools = "कोई टूल उपलब्ध नहीं" @@ -9950,28 +10971,53 @@ title = "उपयोगकर्ता" you = "(आप)" [users.action] +cancelInvite = "आमंत्रण रद्द करें" deleteTeam = "Team हटाएं" disableMfa = "MFA रीसेट करें" move = "Team में ले जाएं" reinstate = "Reinstate" remove = "Org से हटाएं" +removeTeam = "टीम से हटाएँ" rename = "Team का नाम बदलें" resetPw = "Password रीसेट करें" suspend = "Suspend" unlock = "Account unlock करें" +[users.activity] +daysAgo = "{{count}} दिन पहले" +hoursAgo = "{{count}} घंटे पहले" +justNow = "अभी-अभी" +minutesAgo = "{{count}} मिनट पहले" +monthsAgo_one = "{{count}} महीना पहले" +monthsAgo_other = "{{count}} महीने पहले" +never = "कभी नहीं" +weeksAgo_one = "{{count}} सप्ताह पहले" +weeksAgo_other = "{{count}} सप्ताह पहले" +yearsAgo_one = "{{count}} वर्ष पहले" +yearsAgo_other = "{{count}} वर्ष पहले" + [users.cap] addProcessor = "+ Processor" approver = "Policy approve करता है" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}} को भेजा गया आमंत्रण रद्द करें? वे मौजूदा लिंक से शामिल नहीं हो पाएँगे।" +cancelInviteTitle = "आमंत्रण रद्द करें" deleteTeamBody = "{{name}} team हटाएं? पहले team खाली होनी चाहिए - इसके members को दूसरी team में ले जाएं, और इसके पास कोई integration configs नहीं होने चाहिए।" deleteTeamTitle = "Team हटाएं" disableMfaBody = "{{name}} का MFA enrolment हटाएं? यदि आवश्यक हो तो वे अगले login पर इसे फिर से set up करेंगे।" disableMfaTitle = "MFA रीसेट करें" removeBody = "{{name}} को organization से स्थायी रूप से हटाएं? इसे वापस नहीं किया जा सकता।" +removeTeamBody = "{{name}} को टीम से हटाएँ? उनका खाता बना रहेगा, लेकिन वे इस टीम के संसाधनों तक पहुँच खो देंगे।" removeTitle = "Member हटाएं" [users.empty] @@ -9982,10 +11028,8 @@ title = "अभी तक कोई member नहीं" addToTeam = "Team में जोड़ें" guestCount = "{{count}} guest" guests = "Guests" -guestsDesc = "बाहरी collaborators, आपके द्वारा साझा की गई चीज़ों तक सीमित। केवल Editor।" ledBy = "{{owner}} द्वारा नेतृत्व" org = "Organization" -orgDesc = "Org-wide authority और policy approval वाले owners" owners = "{{count}} owner" team = "{{name}} team" teamMeta = "{{count}} लोग" @@ -10022,6 +11066,18 @@ username = "उपयोगकर्ता नाम" usernameError = "उपयोगकर्ता नाम कम से कम 3 वर्णों का होना चाहिए" usernamePlaceholder = "jsmith" +[users.invites] +by = "{{who}} द्वारा आमंत्रित" +cancel = "रद्द करें" +expiresInDays_one = "{{count}} दिन में समाप्त होगा" +expiresInDays_other = "{{count}} दिनों में समाप्त होगा" +expiresToday = "आज समाप्त होगा" +title = "लंबित आमंत्रण" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "backend तक पहुँचने में कुछ गलत हुआ, या आपके पास पहुँच नहीं है। फिर से कोशिश करें।" title = "सदस्यों को लोड नहीं किया जा सका" @@ -10211,10 +11267,15 @@ searchPlaceholder = "संलग्नक खोजें" title = "संलग्नक" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "बुकमार्क शीर्षक" +bookmarkTitleRequired = "बुकमार्क शीर्षक आवश्यक है" closeSidebar = "बुकमार्क sidebar बंद करें" collapseAll = "सभी बुकमार्क संक्षिप्त करें" +empty = "No bookmarks in this document" expandAll = "सभी बुकमार्क विस्तृत करें" +searchPlaceholder = "बुकमार्क खोजें" +title = "Bookmarks" [viewer.comments] addComment = "टिप्पणी जोड़ें" @@ -10236,10 +11297,12 @@ locateAnnotation = "दस्तावेज़ में ढूँढें" moreActions = "और क्रियाएँ" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "पेज {{page}}" placingHint = "रखने के लिए किसी पृष्ठ पर क्लिक करें… (रद्द करें)" removeCommentOnly = "केवल टिप्पणी हटाएँ" saveReply = "उत्तर सहेजें" +searchPlaceholder = "Search comments" title = "टिप्पणियाँ" typeComment = "टिप्पणी" typeInsertText = "पाठ डालें" @@ -10262,6 +11325,7 @@ unsavedDesc = "आपके पास असहेजे परिवर्त closeSidebar = "layers sidebar बंद करें" hideAll = "सभी layers छिपाएँ" showAll = "सभी layers दिखाएँ" +title = "Layers" [viewer.link] delete = "लिंक हटाएं" @@ -10605,30 +11669,6 @@ title = "वॉटरमार्क टेक्स्ट" image = "इमेज" text = "टेक्स्ट" -[wetSignature.tooltip] -header = "हस्ताक्षर बनाने के तरीके" - -[wetSignature.tooltip.draw] -bullet1 = "पेन का रंग और मोटाई अनुकूलित करें" -bullet2 = "संतुष्ट होने तक साफ करें और फिर से बनाएं" -bullet3 = "टच डिवाइस (टैबलेट, फ़ोन) पर काम करता है" -description = "अपने माउस या टचस्क्रीन का उपयोग करके हस्तलिखित हस्ताक्षर बनाएं। व्यक्तिगत और प्रामाणिक हस्ताक्षरों के लिए सर्वोत्तम।" -title = "हस्ताक्षर बनाएं" - -[wetSignature.tooltip.type] -bullet1 = "कई फ़ॉन्ट में से चुनें" -bullet2 = "टेक्स्ट का आकार और रंग अनुकूलित करें" -bullet3 = "मानकीकृत हस्ताक्षरों के लिए उपयुक्त" -description = "टाइप किए गए टेक्स्ट से हस्ताक्षर बनाएं। तेज़ और सुसंगत, व्यावसायिक दस्तावेज़ों के लिए उपयुक्त।" -title = "हस्ताक्षर टाइप करें" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG और अन्य इमेज फ़ॉर्मैट समर्थित हैं" -bullet2 = "बेहतर परिणामों के लिए पारदर्शी पृष्ठभूमि की अनुशंसा की जाती है" -bullet3 = "हस्ताक्षर क्षेत्र में फिट करने के लिए इमेज का आकार बदला जाएगा" -description = "पहले से बनाई गई हस्ताक्षर इमेज अपलोड करें। यदि आपके पास स्कैन किया हुआ हस्ताक्षर या कंपनी लोगो है तो उपयुक्त।" -title = "हस्ताक्षर इमेज अपलोड करें" - [workbenchBar] activeFiles = "सक्रिय फ़ाइलें" annotations = "एनोटेशन" @@ -10642,6 +11682,7 @@ exitRedaction = "रिडैक्शन मोड से बाहर नि exportAll = "PDF निर्यात करें" exportSelected = "चयनित पृष्ठ निर्यात करें" formFill = "फ़ॉर्म भरें" +hideToolbar = "Hide toolbar" multiTool = "मल्टी-टूल" panMode = "पैन मोड" print = "PDF प्रिंट करें" @@ -10652,6 +11693,7 @@ redact = "रिडैक्ट" rotateLeft = "बाएँ घुमाएँ" rotateRight = "दाएँ घुमाएँ" ruler = "रूलर / माप" +rulerSettings = "पैमाना सेटिंग्स" save = "सहेजें" saveAll = "सभी सहेजें" saveAs = "इस रूप में सहेजें" @@ -10661,6 +11703,7 @@ selectAll = "सभी चुनें" selectByNumber = "पृष्ठ संख्याओं से चुनें" selectLanguage = "भाषा चुनें" share = "साझा करें" +showToolbar = "Show toolbar" toggleAnnotations = "एनोटेशन की दृश्यता टॉगल करें" toggleAttachments = "संलग्नक टॉगल करें" toggleBookmarks = "बुकमार्क टॉगल करें" diff --git a/frontend/editor/public/locales/hr-HR/translation.toml b/frontend/editor/public/locales/hr-HR/translation.toml index 82f488b577..0b2aa9b82c 100644 --- a/frontend/editor/public/locales/hr-HR/translation.toml +++ b/frontend/editor/public/locales/hr-HR/translation.toml @@ -1,7 +1,6 @@ alphabet = "Abeceda" apply = "Primijeni" applyAndContinue = "Spremi i izađi" -areYouSure = "Jeste li sigurni da želite izaći?" back = "Natrag" black = "Crno" blue = "Plavo" @@ -46,10 +45,8 @@ filesSelected = "odabrane datoteke" font = "Pismo" fontSizeTooltip = "Veličina teksta broja stranice u točkama. Veći brojevi stvaraju veći tekst." fontTypeTooltip = "Obitelj fonta za brojeve stranica. Odaberite prema stilu dokumenta." -genericSubmit = "Pošalji" green = "Zeleno" help = "Pomoć" -imgPrompt = "Odaberi sliku (slike)" incorrectPasswordMessage = "Kriva zaporka." info = "Informacije" invalidUndoData = "Nije moguće poništiti: nevaljani podaci o radnji" @@ -62,7 +59,6 @@ loginAgreementTitle = "Ugovor o prijavi" logOut = "Odjava" marginTooltip = "Udaljenost između broja stranice i ruba stranice." moreOptions = "Više opcija" -multiPdfPrompt = "Odaberi PDF-ove (2+)" never = "Nikada" no = "Ne" noFileSelected = "Nije odabrana nijedna datoteka. Učitajte jednu." @@ -80,7 +76,6 @@ pageOrderPrompt = "Prilagođeni redoslijed stranica (unesi listu brojeva stranic pages = "Stranice" pageSelectionPrompt = "Prilagođeni odabir stranica (unesi listu brojeva stranica ili funkcija, kao što su 2n+1, razdvojene zarezima) :" password = "Zaporka" -pdfPrompt = "Odaberi PDF(ove)" pin = "Prikvači datoteku (zadrži aktivnu nakon pokretanja alata)" pro = "Pro" processingComplete = "Vaša datoteka je spremna." @@ -110,6 +105,7 @@ undoSuccess = "Radnja je uspješno poništena" unknown = "Nepoznato" unpin = "Otkači datoteku (zamijeni nakon pokretanja alata)" unsavedChanges = "Imate nespremljene promjene u svom PDF-u." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Nespremljene promjene" unsupported = "Nepodržano" username = "Korisničko ime" @@ -356,6 +352,7 @@ text = "Ove dozvole kontroliraju što korisnici mogu raditi s PDF-om. Najdjelotv title = "Promjena dopuštenja" [addStamp] +preview = "Pregled žiga" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ručno preuzimanje: kliknite poveznice i stavite datoteke u mapu noLanguages = "U konfiguriranom direktoriju nisu pronađeni tessdata jezici." permissionNotice = "Putanja do tessdata nije zapisiva. Preuzimanja će se otvoriti u pregledniku; spremite .traineddata datoteke ručno u mapu tessdata." +[admin.settings.ai.documents] +description = "Konfigurirajte model ugradnje i postavke dohvaćanja koje se koriste za odgovaranje na pitanja o dokumentima. Primjenjuje se na AI engine nakon spremanja." +title = "Dokumenti i RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Ostavite prazno za upotrebu vlastite vjerodajnice okruženja enginea. Primjenjuje se na samostalno hostirane implementacije s jednim engineom." +generic = "API ključ za ugradnju" +openai = "OpenAI API ključ" +setPlaceholder = "Spremljeno - ostavite prazno za zadržavanje trenutačnog ključa" +voyageai = "VoyageAI API ključ" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Osnovni URL OpenAI-kompatibilne / Ollama krajnje točke za ugradnje, npr. http://ollama:11434/v1. Mora upućivati na pouzdanu internu krajnju točku (osjetljivo na SSRF)." +label = "Osnovni URL ugradnje" + +[admin.settings.ai.documents.embeddingModel] +description = "Naziv modela ugradnje. Slobodan tekst; prijedlozi su samo smjernice." +label = "Model ugradnje" + +[admin.settings.ai.documents.embeddingProvider] +description = "Pružatelj koji se koristi za pretvaranje teksta dokumenta u vektorske ugradnje." +label = "Pružatelj ugradnje" + +[admin.settings.ai.documents.maxSearches] +description = "Maksimalan broj pretraživanja dohvaćanja koje agent može pokrenuti po zahtjevu." +label = "Maks. pretraživanja" + +[admin.settings.ai.documents.reindexNote] +body = "Promjena modela ugradnje stupa na snagu odmah, ali dokumenti indeksirani prethodnim modelom moraju se ponovno indeksirati kako bi pretraživanje vratilo ispravne rezultate." +title = "Potrebno ponovno indeksiranje" + +[admin.settings.ai.documents.saved] +reindexNote = "Ako ste promijenili model ugradnje, ponovno indeksirajte postojeće dokumente kako bi pretraživanje koristilo novi model." + +[admin.settings.ai.documents.topK] +description = "Broj najrelevantnijih dijelova dohvaćenih po pretraživanju." +label = "Top K" + +[admin.settings.ai.general] +description = "Povežite Stirling s Python AI engineom i odaberite koje su AI mogućnosti dostupne. Promjene se primjenjuju pri ponovnom pokretanju." +title = "AI engine" + +[admin.settings.ai.general.capabilities] +description = "Uključite ili isključite pojedinačne AI značajke. Onemogućene značajke skrivene su u aplikaciji." +title = "Mogućnosti" + +[admin.settings.ai.general.enabled] +description = "Glavni prekidač. Kada je isključen, nisu dostupni AI alati, agenti ni pozivi enginea." +label = "Omogući AI" + +[admin.settings.ai.general.features.chat] +description = "Razgovorni asistent za rad s PDF-ovima." +label = "Asistent za chat" + +[admin.settings.ai.general.features.classify] +description = "Automatski kategorizirajte dokumente prema vrsti ili sadržaju." +label = "Klasifikacija dokumenata" + +[admin.settings.ai.general.features.createPdf] +description = "Generirajte novi PDF (npr. iz HTML-a) putem AI agenta." +label = "Izradi PDF iz upita" + +[admin.settings.ai.general.features.documentQuestions] +description = "Postavljajte pitanja i dobivajte odgovore utemeljene na prenesenom dokumentu." +label = "Pitanja o dokumentu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Pregledajte dokumente radi matematičkih i numeričkih pogrešaka." +label = "Matematički revizor" + +[admin.settings.ai.general.features.pdfComment] +description = "Dodajte AI-generirane komentare pregleda i bilješke u PDF." +label = "Agent za PDF komentare" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Istek vremena za zahtjevnije operacije agenata, poput generiranja dokumenata." +label = "Istek vremena za dugotrajne zadatke (sekunde)" + +[admin.settings.ai.general.note] +body = "AI engine radi kao zasebna usluga. Njegova dijeljena tajna" +body2 = "postavlja se putem varijable okruženja spremnika. API ključevi pružatelja mogu se unijeti na ovim stranicama ili dostaviti kao varijable okruženja enginea; ovdje uneseni ključevi šalju se engineu nakon spremanja." +title = "O AI engineu" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Istek vremena za streamane (token-po-token) odgovore chata." +label = "Istek vremena streama (sekunde)" + +[admin.settings.ai.general.test] +button = "Testiraj vezu" +failBody = "AI engine nije odgovorio. Provjerite URL, radi li spremnik enginea i je li AI omogućen (nakon omogućavanja potrebno je ponovno pokretanje)." +failTitle = "AI engine nije dostupan" +okBody = "AI engine je odgovorio na provjeru stanja." +okTitle = "AI engine je dostupan" + +[admin.settings.ai.general.timeoutSeconds] +description = "Istek vremena za standardne AI zahtjeve prema engineu." +label = "Istek vremena zahtjeva (sekunde)" + +[admin.settings.ai.general.url] +description = "Interni URL Python AI enginea, npr. http://stirling-pdf-engine:5001." +label = "URL AI enginea" + +[admin.settings.ai.limits] +description = "Zaštitna ograničenja za količinu rada koju AI zahtjevi mogu obaviti i koliko ih se može izvršavati istodobno. Primjenjuje se na AI engine nakon spremanja." +title = "Ograničenja i performanse" + +[admin.settings.ai.limits.maxCharacters] +description = "Zaštitno ograničenje: odbij AI zahtjeve čiji izdvojeni tekst premašuje ovu duljinu." +label = "Maks. znakova po zahtjevu" + +[admin.settings.ai.limits.maxPages] +description = "Zaštitno ograničenje: odbij AI zahtjeve s više od ovoliko PDF stranica." +label = "Maks. stranica po zahtjevu" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maksimalan broj istodobnih poziva modela u tijeku preko cijelog enginea." +label = "Maks. istodobnost modela" + +[admin.settings.ai.models] +description = "Odaberite LLM pružatelja i pametne/brze modele koje AI engine koristi. Primjenjuje se na AI engine nakon spremanja." +title = "Modeli i pružatelji" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API ključ" +description = "Ostavite prazno za upotrebu vlastite vjerodajnice okruženja enginea. Primjenjuje se na samostalno hostirane implementacije s jednim engineom." +generic = "API ključ" +openai = "OpenAI API ključ" +setPlaceholder = "Spremljeno - ostavite prazno za zadržavanje trenutačnog ključa" + +[admin.settings.ai.models.baseUrl] +description = "Osnovni URL OpenAI-kompatibilne / Ollama krajnje točke, npr. http://ollama:11434/v1." +label = "Osnovni URL pružatelja" +warning = "Osnovni URL mora upućivati na pouzdanu internu krajnju točku. Engine će slati zahtjeve prema njoj sa strane poslužitelja, pa je nepouzdana vrijednost osjetljiva na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maksimalan broj izlaznih tokena za brzi model." +label = "Maks. tokena brzog modela" + +[admin.settings.ai.models.fastModel] +description = "Jeftiniji, brži model za lagane zadatke. Slobodan tekst; prijedlozi su samo smjernice." +label = "Brzi model" + +[admin.settings.ai.models.provider] +description = "S kojim LLM pružateljem engine komunicira." +label = "Pružatelj" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maksimalan broj izlaznih tokena za pametni model." +label = "Maks. tokena pametnog modela" + +[admin.settings.ai.models.smartModel] +description = "Model visokih mogućnosti za složeno zaključivanje. Slobodan tekst; prijedlozi su samo smjernice." +label = "Pametni model" + +[admin.settings.ai.saved] +body = "Promjene se automatski šalju AI engineu." +bodyNoPush = "Postavke su spremljene. Primijenit će se sljedeći put kada AI engine preuzme svoju konfiguraciju." +title = "AI postavke spremljene" + [admin.settings.badge] clickToUpgrade = "Kliknite za prikaz detalja plana" @@ -938,6 +1094,25 @@ label = "Ponovno generiraj pri pokretanju" description = "Broj dana valjanosti certifikata" label = "Valjanost certifikata (dani)" +[admin.settings.folderAccess] +description = "Direktoriji iz kojih izvori mapa i izlazi mapa smiju čitati i u koje smiju pisati. Ovo je sigurnosna granica: automatizacije se nikada ne mogu usmjeriti na putanju poslužitelja izvan ovog popisa." +securityNote = "Ostavite prazno za potpuno onemogućavanje izvora mapa i izlaza. Stirlingov vlastiti konfiguracijski direktorij uvijek je zabranjen." +title = "Pristup mapama" + +[admin.settings.folderAccess.implied] +description = "Ovi direktoriji kojima upravlja Stirling uvijek su dopušteni i ovdje se ne mogu mijenjati." +serverStorage = "Pohrana datoteka na poslužitelju" +title = "Uvijek dopušteno" +watchedFolder = "Praćena mapa pipelinea" + +[admin.settings.folderAccess.roots] +add = "Dodaj" +empty = "Nijedna mapa nije dopuštena. Izvori mapa i izlazi trenutačno su onemogućeni." +hint = "Unesite apsolutne putanje, na primjer /data/inbox." +label = "Dopušteni korijeni mapa" +placeholder = "/data/inbox" +remove = "Ukloni korijen mape" + [admin.settings.general] description = "Konfigurirajte postavke aplikacije na razini sustava, uključujući brendiranje i zadano ponašanje." system = "Sustav" @@ -1704,6 +1879,9 @@ width = "Širina" [app] description = "Besplatna alternativa Adobe Acrobatu (10M+ preuzimanja)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Pretvori u PDF/A-3b" convertToPdfA3bDescription = "Stvara arhivski PDF s ugrađenim privicima" @@ -1842,6 +2020,15 @@ export = "Izvoz" accessDenied = "Pristup odbijen" insufficientPermissions = "Nemate dopuštenje za ovu radnju." +[auth.callback] +completing = "Dovršavanje autentifikacije" +invalidToken = "OAuth prijava nije uspjela - nevažeći token." +missingToken = "OAuth prijava nije uspjela - token nije primljen." +oauthFailed = "OAuth prijava nije uspjela. Pokušajte ponovno." +pleaseWait = "Pričekajte dok dovršimo vašu prijavu." +signedOut = "Odjavljeni ste. Prijavite se ponovno." +windowMayClose = "Možete zatvoriti ovaj prozor nakon dovršetka." + [auth.displayName] guest = "Gost" user = "Korisnik" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Osigurava PDF dokumente uklanjanjem potencijalno zlonamjer description = "Ovaj će alat automatski preimenovati PDF datoteke na temelju njihova sadržaja. Analizira dokument kako bi iz teksta pronašao najprikladniji naslov." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatski zakreni" +tags = "automatsko zakretanje,popravi orijentaciju,naopako,bočno,poravnaj,orijentiraj,otkrij orijentaciju,ispravi zakretanje,automatski orijentiraj,skenirano,OSD" + +[autoRotate.confidenceThreshold] +title = "Prag pouzdanosti" + +[autoRotate.detectionMode] +auto = "Automatski" +osd = "Samo OCR" +text = "Samo tekst" +title = "Metoda otkrivanja" + +[autoRotate.error] +failed = "Došlo je do pogreške pri automatskom zakretanju PDF-a." + +[autoRotate.inferUndetected] +title = "Zakreni stranice niske pouzdanosti da se podudaraju" + +[autoRotate.report] +applied = "{{degrees}}° u smjeru kazaljke na satu" +confidenceValue = "Pouzdanost {{value}}" +noChange = "Bez promjene" +pageLabel = "Stranica {{number}}" +summary = "Zakrenuto {{rotated}} od {{total}} stranica, {{unchanged}} ostavljeno je kako jest" +title = "Izvješće otkrivanja" + +[autoRotate.report.method] +inferred = "Podudarno" +none = "Preskočeno" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Ispod praga" +blankPage = "Stranica je prazna" +inferredFromDocument = "Isto kao ostale stranice" +noDominantDirection = "Miješani smjerovi teksta" +osdFailed = "Nije pronađen čitljiv tekst" +osdNoVerdict = "Nije bilo moguće odrediti koja je strana gore" +tesseractUnavailable = "OCR nije instaliran" +tooFewGlyphs = "Premalo teksta" + +[autoRotate.results] +title = "Rezultati automatskog zakretanja" + +[autoRotate.settings] +title = "Postavke" + +[autoRotate.tooltip.confidenceThreshold] +text = "Koliko provjera mora biti sigurna prije nego što se stranica zakrene. Smanjite ga za zakretanje više stranica, povećajte ga da više stranica ostane netaknuto." +title = "Prag pouzdanosti" + +[autoRotate.tooltip.description] +text = "Određuje koja strana svake stranice treba biti gore i zakreće je u ispravan položaj. Stranice koje su već uspravne ostaju nepromijenjene." + +[autoRotate.tooltip.detectionMode] +text = "Automatski najprije čita tekst na stranici, zatim pregledava stranicu kao sliku ako to nije dovoljno. Samo tekst preskače provjeru slike, što je najbrže za dokumente koji nikada nisu bili skenirani. Samo OCR tretira svaku stranicu kao sliku." +title = "Metoda otkrivanja" + +[autoRotate.tooltip.header] +title = "Automatski zakreni" + +[autoRotate.tooltip.inferUndetected] +text = "Prazne ili gotovo prazne stranice ne mogu se samostalno provjeriti. Kada svaka stranica koja se mogla pročitati treba isti zakret, te se stranice zakreću da im odgovaraju." +title = "Zakreni stranice niske pouzdanosti da se podudaraju" + [autoSizeSplitPDF] tags = "pdf,podjela,dokumenti,organizacija" @@ -2189,33 +2443,6 @@ keywords = "Ključne riječi: odd, even" numbers = "Brojevi/rasponi: 5, 10-20" progressions = "Progresije: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Vrste certifikata" - -[certificateChoice.tooltip.organization] -bullet1 = "Upravljaju administratori sustava" -bullet2 = "Dijeljeno među ovlaštenim korisnicima" -bullet3 = "Predstavlja identitet tvrtke, ne pojedinca" -bullet4 = "Najbolje za: službene dokumente, timske potpise" -description = "Zajednički certifikat koji pruža vaša organizacija. Koristi se za ovlasti potpisivanja na razini tvrtke." -title = "Certifikat organizacije" - -[certificateChoice.tooltip.personal] -bullet1 = "Automatski se generira pri prvoj upotrebi" -bullet2 = "Povezan s vašim korisničkim računom" -bullet3 = "Ne može se dijeliti s drugim korisnicima" -bullet4 = "Najbolje za: osobne dokumente, individualnu odgovornost" -description = "Automatski generirani certifikat jedinstven za vaš korisnički račun. Primjeren za individualne potpise." -title = "Osobni certifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Zahtijeva P12/PFX datoteku i lozinku" -bullet2 = "Može ga izdati vanjsko certifikacijsko tijelo" -bullet3 = "Viša razina povjerenja za pravne dokumente" -bullet4 = "Najbolje za: pravno obvezujuće ugovore, vanjsku validaciju" -description = "Koristite vlastitu PKCS#12 datoteku certifikata. Pruža potpunu kontrolu nad svojstvima certifikata." -title = "Prenesi prilagođeni P12" - [certSign] awaitingSignatures = "Čeka potpise" chooseCertificate = "Odaberite datoteku certifikata" @@ -2542,34 +2769,6 @@ server = "Poslužitelj" stepTitle = "Izvor certifikata" upload = "Učitaj" -[certSign.tooltip.header] -title = "O upravljanju potpisima" - -[certSign.tooltip.overview] -bullet1 = "Provjerite postojeće potpise i njihovu valjanost" -bullet2 = "Prikažite detaljne informacije o potpisnicima i certifikatima" -bullet3 = "Dodajte nove digitalne potpise za zaštitu vaših dokumenata" -bullet4 = "Podržane višestruke datoteke uz jednostavnu navigaciju" -text = "Ovaj alat omogućuje provjeru je li vaš PDF digitalno potpisan i dodavanje novih digitalnih potpisa. Digitalni potpisi dokazuju tko je stvorio ili odobrio dokument i pokazuju je li mijenjan nakon potpisivanja." -title = "Što ovaj alat može?" - -[certSign.tooltip.signing] -bullet1 = "Podržava PEM, PKCS12, JKS i formate poslužiteljskog certifikata" -bullet2 = "Opcija za prikaz ili skrivanje potpisa na PDF-u" -bullet3 = "Dodajte razlog, lokaciju i ime potpisnika" -bullet4 = "Odaberite na kojoj će se stranici postaviti vidljivi potpisi" -bullet5 = "Koristite poslužiteljski certifikat za jednostavnu opciju 'Sign with Stirling-PDF'" -text = "Za potpis PDF-a potreban je digitalni certifikat (kao PEM, PKCS12 ili JKS). Možete odabrati da potpis bude vidljiv na dokumentu ili nevidljiv samo radi sigurnosti." -title = "Dodavanje potpisa" - -[certSign.tooltip.validation] -bullet1 = "Prikazuje jesu li potpisi valjani ili nevaljani" -bullet2 = "Prikazuje podatke o potpisniku i datum potpisivanja" -bullet3 = "Provjerava je li dokument mijenjan nakon potpisivanja" -bullet4 = "Može koristiti prilagođene certifikate za provjeru" -text = "Pri provjeri potpisa alat javlja jesu li valjani, tko je potpisao dokument, kada je potpisan i je li mijenjan nakon potpisivanja." -title = "Provjera potpisa" - [changeCreds] changeUsername = "Ažurirajte svoje korisničko ime. Bit ćete odjavljeni nakon ažuriranja." credsUpdated = "Račun ažuriran" @@ -2737,12 +2936,16 @@ text = "Kako bi ove dozvole bile nepromjenjive, upotrijebite alat Dodaj lozinku [chat.actions] copy = "Kopiraj poruku" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opcije Stirling agenta" +assistant = "AI Assistant" clearChat = "Obriši chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ova radnja će iskoristiti vaše kredite u oblaku" tooltip = "Odaberi boju sa zaslona" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Odaberite boju" [common] +actions = "Actions" back = "Natrag" cancel = "Odustani" close = "Zatvori" +codeSample = "Code sample" collapse = "Sažmi" confirm = "Potvrdi" continue = "Nastavi" copied = "Kopirano!" copy = "Kopiraj" done = "Gotovo" +enterValue = "Enter value" error = "Pogreška" expand = "Proširi" loading = "Učitavanje..." next = "Sljedeće" +open = "Open" preview = "Pregled" previous = "Prethodno" refresh = "Osvježi" +remaining = "Preostalo" retry = "Pokušaj ponovno" save = "Spremi" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Očisti odabrano" @@ -2988,6 +3199,7 @@ title = "Metoda kompresije" [compress.settings] desiredSize = "Željena veličina datoteke" desiredSizePlaceholder = "Unesite veličinu" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Kompresija je jednostavan način smanjenja veličine datoteke. Odaberite Veličinu datoteke za unos ciljne veličine i da mi prilagodimo kvalitetu umjesto vas. Odaberite Kvaliteta za ručno postavljanje jačine kompresije." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF opcije" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Najprije odaberite izvorni format" settings = "Postavke" single = "Jedan" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Napredno nadziranje" -allInOneWorkspace = "Sve-u-jednom PDF radni prostor (preglednik, alati i agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Namjenska podrška i SLA-ovi" -fasterThroughput = "10x brži protok" -fullyPrivateFiles = "Potpuno privatne datoteke" -largeFileProcessing = "Obrada velikih datoteka" -orgWideAccess = "Organizacijska kontrola pristupa" -premiumAiModels = "Premium AI modeli" -prioritySupport = "Prioritetna podrška" -privateDocCloud = "Privatni oblak dokumenata" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Siguran API pristup" -standardThroughput = "Standardni protok" -unlimitedApiAccess = "Neograničen API pristup" -unlimitedSeats = "Neograničen broj korisnika" - [crop] autoCrop = "Automatski obreži bijeli prostor" noFileSelected = "Odaberite PDF datoteku za početak izrezivanja" @@ -3346,6 +3558,7 @@ message = "Nije uspjelo postaviti zadanu aplikaciju za PDF" title = "Pogreška" [defaultApp.prompt] +dontRemind = "Ne podsjećaj me ponovno" message = "Postavite Stirling PDF kao zadanu aplikaciju za otvaranje PDF datoteka." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Pretraži..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Uredi" @@ -3644,11 +3861,13 @@ shareSelected = "Podijeli odabrano" sharing = "Dijeljenje" showAll = "Prikaži sve" showHistory = "Prikaži povijest" +sortBy = "Sort files" sortByDate = "Sortiraj po datumu" sortByName = "Sortiraj po imenu" sortBySize = "Sortiraj po veličini" storage = "Spremište" storageState = "Pohrana" +storageUsed = "Storage used" synced = "Sinkronizirano" title = "Otpremi PDF datoteke" toolChain = "Primijenjeni alati" @@ -3694,13 +3913,17 @@ addFiles = "Dodaj datoteke" addingFiles = "Dodavanje datoteka…" collapse = "Sažmi bočnu traku" customizeGroups = "Prilagodi grupe" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Otvorite datoteke za početak" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Proširi bočnu traku" -files = "Datoteke" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive nije konfiguriran" leaveMyFiles = "Napusti Moje datoteke" +library = "PDF biblioteka" myFiles = "Moje datoteke" noFiles = "Još nema datoteka" openFileManager = "Otvori upravitelj datoteka" @@ -3708,37 +3931,36 @@ openFromComputer = "Otvori s računala" openSettings = "Otvori postavke" other = "Ostalo" recent = "Nedavno" -search = "Pretraži" -searchPlaceholder = "Pretraži datoteke..." viewAll = "Prikaži svih {{count}} datoteka" [fileSidebar.fileItem] closeViewer = "Zatvori preglednik" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Otvori u pregledniku" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Dodaj" -addLabel = "Dodaj oznaku…" -categoryIconAria = "Odaberite ikonu za {{name}}" -createCategory = "Stvori kategoriju" -delete = "Izbriši kategoriju" done = "Gotovo" -hide = "Sakrij kategoriju" -newCategory = "Naziv nove kategorije…" -removeLabel = "Ukloni {{name}}" -rename = "Preimenuj" reset = "Vrati na zadane vrijednosti" -search = "Pretraži oznake…" -show = "Prikaži kategoriju" subtitle = "Grupirajte svoje datoteke u nadređene kategorije. Dodajte postojeće ili nove oznake u kategoriju, preimenujte je ili stvorite vlastitu. Datoteke koje nisu ni u jednoj od vaših kategorija prikazuju se pod “Ostalo”." title = "Kategorije bočne trake" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Dodaj u radni prostor" addToWorkspaceCount = "Dodaj {{count}} u radni prostor" @@ -3749,7 +3971,6 @@ backToMyFiles = "Natrag na Moje datoteke" breadcrumbs = "Putanja mape" cancel = "Odustani" classification = "Klasifikacija" -clearSearch = "Očisti pretragu" clearSelection = "Poništi odabir" closeDetails = "Zatvori detalje" create = "Stvori" @@ -3781,6 +4002,7 @@ downloadAll = "Preuzmi sve" downloadVersion = "Preuzmi ovu verziju" dropOverlay = "Ispusti datoteke za prijenos" dropOverlaySub = "Datoteke započinju u Lokalno. Upotrijebite 'Premjesti u' ili 'Spremi u oblak' da ih organizirate u mapu." +duplicate = "Duplicate" file = "Datoteka" fileInfo = "File info" fileMenu = "Radnje nad datotekom" @@ -3808,13 +4030,12 @@ refresh = "Osvježi s poslužitelja" remove = "Izbriši" removeVersion = "Ukloni ovu verziju" rename = "Preimenuj" +renamed = "Preimenovano" renameFolder = "Preimenuj mapu" resizeFolderTree = "Promijeni veličinu stabla mapa (strelice, Shift za veće korake; dvoklik za automatsko prilagođavanje)" save = "Spremi" saveToServer = "Spremi na poslužitelj" saveToServerDisabledHint = "Spremanje na poslužitelj nije omogućeno na ovom poslužitelju. Zamolite administratora da to omogući." -search = "Pretraži" -searchPlaceholder = "Pretraži ovu mapu i podmape" selectAll = "Odaberi sve" selectAllHint = "Kliknite za odabir svega. Savjet: držite Ctrl (ili Cmd) za dodavanje datoteka jednu po jednu, Shift za odabir raspona." selectedCount = "{{count}} odabrano" @@ -3874,6 +4095,10 @@ title = "Još nema datoteka u oblaku" hint = "Datoteke spremljene bez prijenosa ostaju ovdje. Ispustite datoteku da biste je dodali." title = "Nema lokalnih datoteka" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Datoteke koje otvorite ili uredite pojavit će se ovdje." title = "Još ništa nije izmijenjeno" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nije moguće izbrisati mapu." deleteFolderFailedDetail = "Nije moguće izbrisati mapu: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nije moguće ažurirati izgled mape." folderAppearanceFailedDetail = "Nije moguće ažurirati izgled mape: {{message}}" moveFilesFailed = "Nije moguće premjestiti datoteke." @@ -3910,9 +4137,7 @@ count = "Datoteke" folder = "Mapa" labels = "Oznake" modified = "Izmijenjeno" -name = "Naziv" size = "Veličina" -toolHistoryAtVersion = "Kumulativni lanac alata" totalSize = "Ukupna veličina" type = "Vrsta" versionHistory = "Povijest verzija" @@ -3945,6 +4170,11 @@ localHint = "Pohranjeno samo u ovom pregledniku" shared = "Dijeljeno" sharedHint = "Podijeljeno s vama putem veze" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Najstarije prvo" modifiedDesc = "Najnovije prvo" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Nema datoteka u pohrani. Najprije otvorite neke datoteke open = "Otvori" openFile = "Otvori datoteku" openFiles = "Otvori datoteke" +selectFile = "Select {{name}}" selectFromStorage = "Odaberite iz spremišta" upload = "Otpremi" uploadFile = "Otpremi datoteku" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Sve spremljeno" +analyzingFields = "Analysing form fields..." +extractCsvError = "Izdvajanje CSV-a nije uspjelo" +extractXlsxError = "Izdvajanje XLSX-a nije uspjelo" +filled = "filled" flattenAfterFilling = "Izravnaj nakon ispunjavanja" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obv" +requiredFieldsError = "Ispunite sva obavezna polja" rescanFields = "Ponovno skeniraj polja" rescanFormFields = "Ponovno skeniraj polja obrasca" save = "Spremi" +saveShortcut = "Ctrl+S za spremanje" +unsavedChanges = "Nespremljene promjene" [formFill.sidebar] close = "Zatvori bočnu traku" @@ -4309,6 +4552,11 @@ desc = "Automatski preimenuje PDF datoteku na temelju detektiranog zaglavlja" tags = "automatski-detekcija,temeljen-na-zaglavlju,organiziraj,preimenuj" title = "Auto preimenuj PDF" +[home.autoRotate] +desc = "Otkrijte orijentaciju svake stranice i automatski je zakrenite uspravno." +tags = "automatsko zakretanje,popravi orijentaciju,naopako,bočno,poravnaj,orijentiraj,otkrij orijentaciju,ispravi zakretanje,automatski orijentiraj,skenirano,OSD" +title = "Automatski zakreni" + [home.autoSizeSplitPDF] tags = "automatski,razdijeli,veličina" title = "Auto dijeli po veličini/broju" @@ -4608,15 +4856,12 @@ hint = "Prenesite PNG, JPG, SVG ili drugu slikovnu datoteku za postavljanje na P label = "Prenesi sliku" placeholder = "Odaberite slikovnu datoteku" -[imageToPdf] -tags = "konverzija,pretvaranje,img,jpg,slika,foto" - [imageToPDF] header = "Slika u PDF" title = "Slika u PDF" -[infoBanner] -dismiss = "Odbaci" +[imageToPdf] +tags = "konverzija,pretvaranje,img,jpg,slika,foto" [invite] acceptError = "Neuspjelo stvaranje računa" @@ -4647,11 +4892,9 @@ welcomeTitle = "Pozvani ste!" [landing] addFiles = "Dodaj datoteke" -heroSubtitle = "Povucite i ispustite ili dodajte postojeći PDF za početak." mobileUpload = "Otpremanje s mobitela" openFromComputer = "Otvori s računala" uploadFromComputer = "Učitaj s računala" -workbenchEmptyStateHero = "Ispustite PDF bilo gdje" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Unesite korisničko ime" failedToSignIn = "Prijava putem {{provider}} nije uspjela: {{message}}" forgotPassword = "Zaboravili ste lozinku?" loggingIn = "Prijavljivanje..." -logIn = "Prijavite se" login = "Prijava" magicLinkSent = "Magic link poslan na {{email}}! Provjerite e-poštu i kliknite poveznicu za prijavu." mfaCode = "Autentikacijski kod" @@ -4715,26 +4957,10 @@ signInWith = "Prijavite se s" title = "Prijavite se" unexpectedError = "Neočekivana pogreška: {{message}}" updatePassword = "Ažuriraj lozinku" -useEmailInstead = "Prijava e-poštom" useMagicLink = "Umjesto toga koristite magic link" username = "Korisničko ime" youAreLoggedIn = "Prijavljeni ste!" -[login.slides.edit] -alt = "Uređivanje PDF-ova" -subtitle = "S više od desetak alata za redaktiranje, potpisivanje, čitanje i obradu PDF-ova, sigurno ćete naći što tražite." -title = "Uredite PDF-ove kako biste prikazali/zaštitili željene informacije" - -[login.slides.overview] -alt = "Pregled Stirling PDF-a" -subtitle = "Cloud paket s fokusom na privatnost za PDF-ove koji omogućuje pretvaranje, potpisivanje, redaktiranje i upravljanje dokumentima, uz 50+ drugih moćnih alata." -title = "Sve za PDF na jednom mjestu." - -[login.slides.secure] -alt = "Zaštita PDF-ova" -subtitle = "Dodajte lozinke, redaktirajte sadržaj i lako upravljajte certifikatima." -title = "Zaštitite osjetljive informacije u svojim PDF-ovima" - [margin] large = "Veliko" medium = "Srednje" @@ -4818,6 +5044,34 @@ uploadSuccess = "Otpremanje uspješno!" uploadSuccessMessage = "Vaše slike su prenesene." validating = "Provjera sesije..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skenirajte za otpremu fotografija. Slike se automatski pretvaraju u PDF." descriptionNoConvert = "Skenirajte za otpremu fotografija s mobilnog uređaja." @@ -4836,8 +5090,13 @@ title = "Otpremanje s mobitela" tags = "Višestruki alat, više operacija, korisničko sučelje, povlačenje klikom, prednji kraj, strana klijenta, interaktivno, nepopravljivo, pomicanje" title = "PDF Višenamjenski alat" -[navbar] -search = "Pretraži" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentikacija nije bila uspješna. Možete zatvoriti ovaj prozor i pokušati ponovno." @@ -4966,6 +5225,7 @@ activeFiles = "Prikaz Aktivne datoteke pokazuje sve PDF-ove koj allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zatvori" cropSettings = "Sada kada smo odabrali datoteku koju želimo obrezati, možemo konfigurirati alat Obrezivanje i odabrati područje na koje želimo obrezati PDF." +dialogLabel = "Onboarding" fileCheckbox = "Klikom na jednu od datoteka odabirete je za obradu. Možete odabrati više datoteka za skupne radnje." fileReplacement = "Izmijenjena datoteka automatski će zamijeniti izvornu datoteku na Radnoj ploči, što vam omogućuje da je lako proslijedite kroz još alata." filesButton = "Gumb Datoteke na traci brzog pristupa omogućuje učitavanje PDF-ova na koje ćete primijeniti alate." @@ -4976,6 +5236,7 @@ pinButton = "Možete koristiti gumb Prikvači ako želite da va results = "Nakon što alat završi s radom, korak Pregled prikazat će pregled rezultata u ovom panelu i omogućiti vam poništavanje radnje ili preuzimanje datoteke." runButton = "Kada je alat konfiguriran, ovim gumbom možete pokrenuti alat na svim odabranim PDF-ovima." selectCropTool = "Odaberimo alat Obrezivanje kako bismo pokazali kako koristiti jedan od alata." +stepOf = "Korak {{current}} od {{total}}" toolInterface = "Ovo je sučelje alata Obrezivanje. Kao što vidite, nema mnogo toga jer još nismo dodali PDF datoteke s kojima bismo radili." workbench = "Ovo je Radna ploča - glavno područje gdje pregledavate i uređujete svoje PDF-ove." wrapUp = "Sve je spremno! Naučili ste o glavnim područjima aplikacije i kako ih koristiti. Kliknite gumb Pomoć kada god želite ponovno vidjeti ovaj obilazak." @@ -4988,6 +5249,26 @@ showMeAround = "Provedi me" skipForNow = "Preskoči zasad" skipTheTour = "Preskoči obilazak" +[onboarding.checklist] +dismiss = "Odbaci" +title = "Postavite Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Pokrenite Stirling izvorno na svojem računalu" +title = "Preuzmi Stirling za Desktop" + +[onboarding.checklist.inviteTeam] +description = "Surađujte sa svojim timom" +title = "Pozovite članove tima" + +[onboarding.checklist.shareAnalytics] +description = "Pomozite poboljšati Stirling" +title = "Dijeli anonimne podatke o upotrebi" + +[onboarding.checklist.takeTour] +description = "Pogledajte kako Stirling radi u kratkom obilasku" +title = "Kreni u obilazak" + [onboarding.desktopInstall] body = "Stirling najbolje radi kao desktop aplikacija. Možete ga koristiti izvan mreže, brže pristupati dokumentima i uređivati lokalno na računalu." selectOs = "Odaberite operacijski sustav" @@ -4996,7 +5277,21 @@ titleWithOs = "Preuzmi za {{osLabel}}" [onboarding.mfa] authenticationCode = "Autentifikacijski kod" +description = "Zaštitite svoj račun povezivanjem aplikacije za autentifikaciju. Skenirajte QR kod ili unesite ključ za postavljanje, zatim potvrdite 6-znamenkasti kod za završetak." +enable = "Omogući MFA" +enableError = "Nije moguće omogućiti dvofaktorsku autentifikaciju. Provjerite kod i pokušajte ponovno." +enterCode = "Unesite 6-znamenkasti kod iz svoje aplikacije." +enterCodeError = "Unesite autentifikacijski kod za nastavak." +logout = "Odjava" +openAuthenticator = "Otvorite Google Authenticator, Authy ili 1Password." qrCodeLoading = "Generiranje vašeg QR koda…" +regenerateQrCode = "Ponovno generiraj QR kod" +scanQrCode = "Skenirajte QR kod ili unesite ključ za postavljanje u nastavku." +setupError = "Nije moguće pokrenuti postavljanje dvofaktorske autentifikacije. Pokušajte ponovno." +setupKey = "Ključ za postavljanje (ručni unos)" +stepByStep = "Korak po korak" +success = "MFA je omogućen. Sada možete nastaviti." +title = "Postavljanje višefaktorske autentifikacije" [onboarding.planOverview] adminBodyLoginDisabled = "Kad omogućite način prijave, moći ćete upravljati korisnicima, konfigurirati postavke i nadzirati zdravlje poslužitelja. Prvih {{freeTierLimit}} osoba na vašem poslužitelju koristi Stirling besplatno." @@ -5005,6 +5300,11 @@ adminTitle = "Pregled za administratore" userBody = "Pozovite suradnike, dodijelite uloge i držite dokumente organiziranima u jednom sigurnom radnom prostoru. Omogućite način prijave kad budete spremni prerasti samostalnu upotrebu." userTitle = "Pregled plana" +[onboarding.processorIntro] +body = "Stirling sada pokreće Pravila — automatizirana pravila koja klasificiraju, štite i obrađuju svaki dokument čim stigne. Postavite ih i pratite izvođenja u Processoru." +cta = "Pogledajte Processor" +title = "Pogledajte Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Pretplaćena godina" + +[payg.prepaid.banner] +exhausted = "Vaš pretplaćeni kapacitet je potrošen; obračun prema potrošnji je nastavljen. Nadoplatite za zadržavanje popusta." +expiring_one = "Vaša pretplaćena godina završava za {{count}} dan. Neiskorišteni kapacitet istječe — nadoplatite za početak nove godine." +expiring_other = "Vaša pretplaćena godina završava za {{count}} dana. Neiskorišteni kapacitet istječe — nadoplatite za početak nove godine." +low = "Preostalo vam je {{pct}}% pretplaćenog kapaciteta. Nadoplatite bilo kada kako bi obrada ostala neprekinuta." + +[payg.prepaid.card] +subtitle = "Koristi se prije obračuna prema potrošnji i izvan vašeg ograničenja potrošnje." +title = "Pretplaćeni kapacitet" + +[payg.prepaid.meter] +capSuffix = "od {{total}} pretplaćenih kredita" +expires = "Istječe {{date}}" + +[payg.prepaid.state] +exhausted = "Potrošeno" +healthy = "Dovoljno preostalo" +low = "Pri kraju" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF u PDF/A" tags = "arhiva,dugoročno,standardno,konverzija,čuvanje,čuvanje" title = "PDF u PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tisak,standard,pretvorba,produkcija,priprema za tisak,arhiva" title = "PDF u PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kredita" xsmall = "100 kredita" [plan.availablePlans] +currency = "Billing currency" subtitle = "Odaberite plan koji odgovara vašim potrebama" title = "Dostupni planovi" @@ -5937,49 +6267,12 @@ keyDescription = "Zalijepite licenčni ključ iz e-poruke" success = "Licenca aktivirana!" successMessage = "Vaša licenca je uspješno aktivirana. Sada možete zatvoriti ovaj prozor." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "provedeno" -enforcing = "Provodi se..." -failed = "Provedba nije uspjela" outputsUnavailable = "Rezultati pravila više nisu dostupni za preuzimanje." partialOutputsUnavailable = "Neki rezultati pravila više nisu dostupni za preuzimanje." -retrying = "Zauzeto, ponovno pokušavam..." runNotFound = "Pokretanje provedbe više nije moguće pronaći." -step = "korak {{current}}/{{total}}" timedOut = "Isteklo je vrijeme prije nego što je provedba mogla završiti." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zatvori" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Primjenjuje se {{names}}" applyingProgress = "Primjenjuje se {{names}} ({{done}} od {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} od {{total}} datoteka nije bilo moguće obraditi te failureTitle = "Izvezeno bez potpune provedbe" printPolicyAppliedBody = "Ovaj PDF je ažuriran kako bi bio u skladu s pravilom. Pregledajte promjene, zatim ponovno ispišite." printPolicyAppliedTitle = "Pravilo primijenjeno prije ispisa" -queued = "+{{count}} na čekanju" successTitle = "{{names}} primijenjeno" summaryMore = "{{first}}, {{second}} i još {{more}}" summaryTwo = "{{first}} i {{second}}" -[policies.enforcement.triggerVerb] -convert = "Provedba prije pretvorbe" -default = "Provedba" -export = "Provedba prije izvoza" -input = "Provedba pri uvozu" -print = "Provedba prije ispisa" - -[policies.field] -accessLog = "Zapisnik pristupa" -archiveAfter = "Arhiviraj nakon" -auditTrail = "Revizijski trag" -belowThreshold = "Ispod praga" -destination = "Odredište" -frameworks = "Okviri" -immutableHold = "Nepromjenjivo zadržavanje" -keepFor = "Zadrži na" -minConfidence = "Min. pouzdanost" -notify = "Obavijesti pri usmjeravanju" -onViolation = "Kada nije usklađeno" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Dodaj" -addPlaceholder = "Dodaj oznaku…" -customNote = "Prilagođeno za vaš tim." -defaultNote = "Koristi se ugrađena zadana vrijednost, dijeljena s vašim timom." -duplicate = "\"{{name}}\" već postoji." -edit = "Uredi oznake" -empty = "Još nema oznaka." -export = "Izvezi JSON" -iconAria = "Odaberite ikonu za {{name}}" -import = "Uvezi JSON" -importError = "Nije moguće uvesti tu datoteku." -managedNote = "Timskim oznakama upravlja voditelj vašeg tima." -modalSubtitle = "Dijeli se s cijelim vašim timom. Klasifikator odabire oznake koje odgovaraju svakom dokumentu." -modalTitle = "Klasifikacijske oznake" -pickIcon = "Upotrijebi ikonu {{label}}" -removeAria = "Ukloni {{name}}" -resetToDefault = "Vrati na zadano" -saveForTeam = "Spremi za tim" -saving = "Spremanje…" -sectionLabel = "Klasifikacijske oznake" -startFromScratch = "Počni od početka" -teamCount = "timske oznake" -tooLong = "Oznake mogu imati najviše {{max}} znakova." -ungrouped = "Negrupirano" -view = "Prikaži oznake" +categoryCount = "kategorije" +hideCategory = "Sakrij kategoriju" +labelCount = "oznake" +sharedNote = "Ove su oznake ugrađene i dijele se u cijelom vašem timu." +showCategory = "Prikaži kategoriju" [policies.pii] account = "Brojevi računa (označeni)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Američki brojevi usmjeravanja (ABA)" ssn = "Brojevi socijalnog osiguranja" -[policies.settings] -noneExport = "Trenutačno se pri izvozu ne pokreću pravila." -noneUpload = "Trenutačno se pri prijenosu ne pokreću pravila." -onExport = "Pri izvozu" -onUpload = "Pri prijenosu" -reorderHandle = "Povucite za promjenu redoslijeda" -runOrderDesc = "Kada se na isti okidač pokreće više od jednog pravila, pokreću se ovim redoslijedom — svako na izlazu prethodnog pravila. Povucite za promjenu redoslijeda." -title = "Postavke pravila" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opcije pravila" -policySettings = "Postavke pravila" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Ponovi neuspjela pravila ({{count}})" -rowProgress = "Obrađeno {{completed}} od {{total}} datoteka" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Što je pravilo?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatski pronalazi i zacrnjuje osjetljive podatke — poput brojeva socijalnog osiguranja i kartica — kako se ne bi mogli čitati u dokumentu." -sanitize = "Uklanja skriveni JavaScript iz datoteke, kako se ništa ne bi automatski pokrenulo pri otvaranju." -watermark = "Otiskuje vidljivu oznaku (npr. \"Povjerljivo\") preko svake stranice." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zatvori" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} se primjenjuje..." badgeRan = "Pravilo {{name}} pokrenuto je na ovoj datoteci" +badgeRunning = "{{name}} se izvodi..." blockingAction = "{{action}} blokirano je tijekom primjene pravila, pričekajte..." dismiss = "Zatvori preklapanje" enforcingTitle = "Primjena pravila..." @@ -6151,6 +6316,7 @@ viewAnyway = "Prikaži datoteku (pravilo se još primjenjuje)" [policyOption] autoRedactPhi = "Automatski redigiraj PHI" blockExport = "Blokiraj izvoz" +ccpa = "CCPA" contracts = "Ugovori" documents = "Dokumenti" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plan Editor" subscribed = "Plan Processor" unlinked = "Nije povezano" -[portal.agentBuilder] -bootstrapFromDocument = "Izradi početni nacrt iz dokumenta" -sectionsAriaLabel = "Odjeljci alata za izradu agenata" -selectorAriaLabel = "Agenti" -subtitle = "Dizajnirajte, testirajte i objavite AI agente koji klasificiraju, izdvajaju iz i usmjeravaju vaše dokumente. Definirajte scenarije, ograničite pristup alatima, pokrenite zlatni skup i objavite verziju." -title = "Alat za izradu agenata" - -[portal.agentBuilder.bootstrap] -cancel = "Odustani" -dropzoneText = "Odaberite ogledni dokument (PDF ili sliku)" -lead = "Ispustite ogledni dokument i predložit ćemo scenarije i shemu izdvajanja koju možete doraditi. Ništa se ne objavljuje dok to ne pregledate." -submit = "Izradi početnog agenta" -subtitle = "Pokrenite novog agenta iz jedne reprezentativne datoteke" -title = "Izradi početni nacrt iz dokumenta" - -[portal.agentBuilder.empty] -description = "Izradite početni nacrt agenta iz oglednog dokumenta kako biste postavili njegove scenarije i shemu izdvajanja, zatim ga doradite i objavite." -title = "Još nema agenata" - -[portal.agentBuilder.evals] -casesPassing = "Prolazni slučajevi" -columnCase = "Evaluacijski slučaj" -columnLatency = "Latencija" -columnResult = "Rezultat" -fail = "neuspjeh" -goldenSetPassRate = "Stopa prolaznosti zlatnog skupa" -latencyMs = "{{ms}} ms" -notRun = "nije pokrenuto" -pass = "prolaz" -passRate = "Stopa prolaznosti" -runEvals = "Pokreni evaluacije" - -[portal.agentBuilder.evals.empty] -description = "Evaluacije pretvaraju vaše scenarije u ponovljiv zlatni skup. Nadogradite kako biste pratili stopu prolaznosti kroz vrijeme i uvjetovali objave njome." -title = "Još nema zlatnog skupa" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "preko zlatnih skupova" -activeAgents = "Aktivni agenti" -avgPassRate = "Prosječna stopa prolaznosti evaluacije" -fleetWide = "na razini flote" -latestPublished = "Najnovije objavljeno" -scenarios = "Scenariji" -testCases = "testni slučajevi" -totalDescription_one = "ukupno {{count}}" -totalDescription_other = "ukupno {{count}}" - -[portal.agentBuilder.scenarios] -add = "Dodaj" -addScenario = "Dodaj scenarij" -enable = "Omogući" -expectationLabel = "Očekivano ponašanje" -expectationPlaceholder = "Što agent treba učiniti" -inEval = "u evaluaciji" -mute = "Utišaj" -muted = "utišano" -nameLabel = "Naziv" -namePlaceholder = "npr. eskalacija usklađenosti" - -[portal.agentBuilder.tabs] -evals = "Evaluacije" -scenarios = "Scenariji" -tools = "Alati" -versions = "Verzije" - -[portal.agentBuilder.tools] -broadAccess = "Širok pristup" -deniedHint = "Odabrani alati su blokirani. Sve ostalo ostaje dostupno za pozivanje." -deniedTools = "Zabranjeni alati" -governanceGate = "Upravljanje alatima dostupno je na planu Enterprise." -restricted = "Ograničeno" -restrictedAccess = "Ograničeni pristup alatima" -restrictedDescription = "Dopustite svaki alat osim onih koje zabranite u nastavku." - -[portal.agentBuilder.versions] -current = "trenutačno" -historyGate = "Puna povijest verzija i vraćanje dostupni su na planu Enterprise." -publish = "Objavi" -rollBack = "Vrati na prethodno" - [portal.assistant] close = "Zatvori asistenta" error = "Nije moguće dohvatiti asistenta." @@ -6365,9 +6451,21 @@ typing = "Tipkanje" loading = "Učitavanje" redirectingToEditor = "Preusmjeravanje na uređivač..." +[portal.billing.activation] +maybeLater = "Možda kasnije" +title = "Uključite Processor" + +[portal.billing.activation.payg] +sub = "Postavite mjesečno ograničenje. Plaćajte ono što pokrenete." +title = "Plaćanje prema potrošnji" + +[portal.billing.activation.prepay] +badge = "2 mjeseca besplatno" +sub = "Jedan račun, kartica ili bankovni prijenos." +title = "Pretplatite godinu unaprijed" + [portal.billing.checkout] noClientSecret = "Edge funkcija nije vratila client_secret." -subtitle = "Dodajte karticu kako biste nastavili nakon besplatne dodjele plana Editor. Stripe će obraditi ostalo." title = "Uključite plan Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Vaše plaćanje je uspjelo, ali aktivacija traje malo dulje nego obično close = "Zatvori" title = "Skoro gotovo" +[portal.billing.checkout.cap] +amountAria = "Mjesečno ograničenje potrošnje" +back = "Natrag" +continue = "Nastavi na plaćanje" +error = "Nije moguće postaviti ograničenje potrošnje" +finePrint = "Računi se izdaju mjesečno. Otkažite bilo kada." +noLimit = "Bez ograničenja" +note = "Nikada vam se neće naplatiti više od vašeg ograničenja. Obrada se samo pauzira." +perMonth = "/ mj." +title = "Postavite ograničenje potrošnje" + +[portal.billing.checkout.card] +fields = "Broj kartice · MM / GG · CVC · ZIP" +label = "Podaci o kartici" +note = "Podatke o kartici prikuplja Stripe. Stirling nikada ne pohranjuje PAN ili CVC." + [portal.billing.checkout.error] title = "Nije moguće pokrenuti checkout" @@ -6383,10 +6497,9 @@ body = "Vaše plaćanje je prošlo. Uključujemo mjereno procesiranje na vašim hint = "Držite ovaj prozor otvorenim." title = "Aktivacija vašeg plana Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "u portal env kako biste omogućili checkout unutar aplikacije." -bodyBefore = "Postavite" -title = "Stripe nije konfiguriran" +[portal.billing.checkout.pay] +limit = "Mjesečno ograničenje potrošnje {{amount}}/mj. Obrada se pauzira na ograničenju." +limitNoCap = "Bez ograničenja potrošnje. Obrada se neće pauzirati. Računi se izdaju mjesečno." [portal.billing.enterpriseUpsell] cta = "Izradite svoju Enterprise ponudu" @@ -6409,10 +6522,12 @@ currentPlan = "Trenutačni plan" freeForever = "Zauvijek besplatno" noTeamResolved = "Na vašem novčaniku još nije razriješen tim — osvježite i pokušajte ponovno." ownerOnly = "Samo vlasnik tima može uključiti plan Processor." +payInvoice = "Plati račun za dovršetak" planName = "Editor" ssoIncluded = "SSO uključen" switchOnProcessor = "Uključi Processor →" unlimitedUsers = "Neograničen broj korisnika" +viewQuote = "Prikaži ponudu" [portal.billing.invoices] columnAmount = "Iznos" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Izravni API zahtjevi" segmentApiLabel = "API" segmentAutomationDesc = "Automatizacije i cjevovodi" segmentAutomationLabel = "Automatizacija" +sizeMultiplier = "{{formatted}} PDF-ova koristilo je množitelj veličine" +summary = "{{unique}} jedinstvenih · {{units}} mjernih jedinica · {{avg}} prosjek po PDF-u" +summaryNoRate = "{{unique}} jedinstvenih · {{units}} mjernih jedinica" unit = "mjereni PDF-ovi" +unitsPending_one = "{{units}} mjerna jedinica čeka sinkronizaciju iz povezanih instanci" +unitsPending_other = "{{units}} mjernih jedinica čeka sinkronizaciju iz povezanih instanci" + +[portal.billing.prepaid] +capSuffix = "od {{total}} pretplaćenih kredita" +expires = "Istječe {{date}}" +eyebrow = "Pretplaćeni kapacitet" +note = "Koristi se prije obračuna prema potrošnji i izvan vašeg ograničenja potrošnje." +topUp = "Nadoplati" + +[portal.billing.prepaid.buy] +back = "Natrag" +calcTitle = "Izračunajte svoje godišnje plaćanje" +cancel = "Odustani" +cancelPurchase = "Odustani od kupnje" +close = "Zatvori" +continueToPayment = "Nastavi na plaćanje" +finalise = "Dovrši" +invoiceErrorTitle = "Nije moguće generirati račun" +loadingTitle = "Učitavanje vaše ponude" +payTitle = "Platite svoju godinu" +step = "Korak {{current}} od {{total}}" + +[portal.billing.prepaid.calc] +change = "Promijeni" +deployRow = "Implementacija" +done = "Gotovo" +enterpriseHint = "Ovo je razina poduzeća. Pri ovom volumenu, enterprise cijene nadmašuju svaki popust za samostalnu kupnju. Zaključavanje cijene, uvjeti i ponuda u nekoliko minuta." +governanceRow = "Upravljanje" +governanceValue = "{{label}} · {{count}} pravila" +handlesLabel = "Vaš Processor" +handlesValue = "obrađuje {{volume}} kredita / mj." +pipelinesLabel = "Pipelineovi" +policiesMeta = "{{count}} pravila" +poolCaption = "Jedan fond od {{credits}} kredita za godinu. Teški mjeseci posuđuju od lakših." +priceLabel = "Vaša godina · 12 mjeseci po cijeni 10" +rateUnknown = "Točnu cijenu prikazat ćemo pri plaćanju." +savings = "Štedite {{amount}} · 2 mjeseca besplatno." +sizingRow = "Veličina" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Ukupno korisnika" + +[portal.billing.prepaid.consent] +label = "Razumijem da, kada se moj pretplaćeni kapacitet potroši ili istekne nakon 12 mjeseci, obrada automatski nastavlja po standardnoj obračunskoj stopi plaćanja prema potrošnji (do mog ograničenja potrošnje) osim ako otkažem, i da mogu otkazati bilo kada iz portala za naplatu." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Upravlja Stirling. Dostupno za nekoliko minuta." +cloudValue = "Stirling Cloud · Upravljano" +selfhost = "Samostalno hostirano" +selfhostDesc = "Radi na privatnoj infrastrukturi. Ista cijena." +selfhostValue = "Samostalno hostirano · Privatna infrastruktura" + +[portal.billing.prepaid.offer] +cta = "Pregledaj ponudu" +subtitle = "Pretplatite godinu obrade PDF-ova i dobijte dva mjeseca besplatno, koristi se prije obračuna prema potrošnji, izvan vašeg ograničenja potrošnje." +title = "Dobijte 12 mjeseci po cijeni 10" + +[portal.billing.prepaid.pay] +accountName = "Ime vlasnika računa" +accountNamePlaceholder = "npr. Jane Smith" +companyName = "Naziv tvrtke (neobavezno)" +companyNamePlaceholder = "npr. Acme Inc." +downloadInvoice = "Preuzmi račun" +payOnline = "Plati online" +poNumber = "Broj narudžbenice (neobavezno)" +poNumberPlaceholder = "npr. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Napredno" +advancedDesc = "Pipelineovi pokreću većinu vaše obrade." +none = "Nijedan" +noneDesc = "Još ne pokrećete pipelineove. Uključite ih bilo kada." +standard = "Standardno" +standardDesc = "Nekoliko pipelineova ponovno obrađuje pristigle PDF-ove." + +[portal.billing.prepaid.posture] +essentials = "Osnovno" +essentialsDesc = "Klasifikacija, zadana, plus dijeljenje" +governed = "Upravljano" +governedDesc = "Dodaje sigurnost i usmjeravanje" +regulated = "Regulirano" +regulatedDesc = "Svaka kategorija, uključujući usklađenost, zadržavanje, unos" + +[portal.billing.prepaid.review] +download = "Preuzmi ponudu (PDF)" +downloading = "Priprema…" +share = "podijeli za odobrenje" + +[portal.billing.prepaid.size] +compact = "Kompaktno" +compactDesc = "Datoteke ispod 25 MB, bez naknada za podatke" +heavy = "Teško" +heavyDesc = "Skenirano ili s mnogo slika, uobičajeno 50 MB+" +standard = "Standardno" +standardDesc = "Uglavnom male, neke skenirane preko 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Potrošeno" +healthy = "Dovoljno preostalo" +low = "Pri kraju" [portal.billing.spendLimit] adjustLimit = "Prilagodi ograničenje" @@ -6494,6 +6713,8 @@ label = "Predviđa se prekoračenje." [portal.billing.spendThisMonth] eyebrow = "Potrošnja ovog mjeseca" +freeRemaining_one = "Preostaje {{formatted}} besplatni PDF" +freeRemaining_other = "Preostaje {{formatted}} besplatnih PDF-ova" processed_one = "Obrađen {{formattedCount}} PDF." processed_other = "Obrađeno {{formattedCount}} PDF-ova." processedWithRate_one = "Obrađen {{formattedCount}} PDF, po {{rate}} svaki." @@ -6510,6 +6731,7 @@ reachedTitle = "Dosegnuto je mjesečno ograničenje potrošnje" title = "Nije moguće otvoriti Stripe portal" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "od {{allowance}} besplatnih PDF-ova iskorišteno" capSuffix_other = "od {{allowance}} besplatnih PDF-ova iskorišteno" eyebrow = "Probno razdoblje za Processor" @@ -6534,6 +6756,376 @@ signature = "potpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nova veza" +createTitleFor = "Poveži {{name}}" +delete = "Izbriši" +edit = "Uredi" +editTitle = "Uredi vezu" +unsupportedType = "Ova vrsta veze ne može se ovdje uređivati." + +[portal.connections.categories.advanced] +description = "Za uslugu koju još ne podržavamo." +label = "Napredno" + +[portal.connections.categories.audit] +description = "Zabilježite što se dogodilo s dokumentom, tamo gdje vaši revizori već gledaju." +label = "Revizija i bilježenje usklađenosti" + +[portal.connections.categories.notify] +description = "Obavijestite drugi sustav da se nešto dogodilo." +label = "Obavijesti i tijek rada" + +[portal.connections.categories.security] +description = "Skenirajte ili označite dokument prije nego što nastavi dalje." +label = "Sigurnost i klasifikacija" + +[portal.connections.categories.signing] +description = "Pošaljite dokument na potpisivanje ili ovjeru." +label = "E-potpis i ovjera" + +[portal.connections.categories.storage] +description = "Gdje obrađeni dokumenti završavaju - bucket, drive, tiket ili stranica." +label = "Datoteke i privitci" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling ovo pohranjuje šifrirano i nikada ga više ne prikazuje." +label = "Pristupni token" + +[portal.connections.commonFields.apiKey] +label = "API ključ" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Koraci koji koriste ovu vezu mogu pozivati samo putanje pod ovim URL-om, nikada drugi host." +label = "URL poslužitelja" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Lozinka" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Korisničko ime" + +[portal.connections.commonFields.webhookUrl] +helperText = "Zalijepite URL koji vam je usluga dala. Mogu se pozivati samo putanje pod njim." +label = "Webhook URL" + +[portal.connections.fields] +name = "Naziv veze" +namePlaceholder = "npr. Produkcija" + +[portal.connections.picker] +cancel = "Odustani" +createNew = "Nova veza..." +placeholder = "Odaberite vezu" +save = "Spremi vezu" + +[portal.connections.picker2] +back = "Promijeni integraciju" +heading = "Što želite povezati?" +noResultsBody = "Još to ne podržavamo - recite nam o tome i razmotrit ćemo dodavanje." +noResultsTitle = "Ništa ne odgovara \"{{query}}\"" +searchPlaceholder = "Pretražite integracije - pokušajte \"sign\", \"ocr\" ili naziv proizvoda" + +[portal.connections.types.api] +description = "Pozovite bilo koji HTTP API iz pravila. Vi odabirete URL, autentifikaciju i payload." +label = "Prilagođeni API" + +[portal.connections.types.api.fields.authType] +label = "Autentifikacija" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Korisničko ime i lozinka" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token u prilagođenom headeru" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ništa" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Prijava za token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Koraci koji koriste ovu vezu mogu pozivati samo putanje pod ovim URL-om, nikada drugi host." +label = "Osnovni URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Naziv headera" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Putanja prijave" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Lozinka" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Ako API odgovori poveznicom za preuzimanje, mogu se dohvatiti samo ovi hostovi. Odvojite zarezima. Ostavite prazno da dopustite samo vlastiti host osnovnog URL-a." +label = "Dopušteni hostovi rezultata" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Pošalji token kao ovaj header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Header odgovora u kojem poziv prijave vraća token, npr. X-Auth-Token." +label = "Header odgovora tokena" + +[portal.connections.types.api.fields.username] +label = "Korisničko ime" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skenirajte dokumente na zlonamjerni softver na poslužitelju koji vi pokrećete - ništa ne napušta vašu mrežu." +label = "ClamAV (samostalno hostiran)" + +[portal.connections.types.cloudmersive] +description = "Skeniranje virusa i sadržaja za dokumente u pravilu." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokirajte dokumente koji sadrže makronaredbe, izvršne datoteke, skripte ili ugrađene objekte - ne samo poznati zlonamjerni softver." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Priložite obrađene dokumente Confluence stranici." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Pošaljite dokumente u ConsignO Cloud (Notarius) za elektronički potpis i ovjeru." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Vaše ConsignO Cloud okruženje, uključujući /api/v1." +label = "URL poslužitelja" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API ključ" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API tajna" + +[portal.connections.types.consigno.fields.password] +label = "Lozinka" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potrebno samo ako vaša organizacija koristi stanare." +label = "Stanar" + +[portal.connections.types.consigno.fields.username] +label = "Korisničko ime" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Objavite poruku na Discord kada pravilo obradi dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indeksirajte revizijski događaj u Elasticsearch svaki put kada pravilo obradi dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Ispitujte FTP ili FTPS poslužitelj za nove dokumente." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Način povezivanja" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktivno" +passive = "Pasivno (preporučeno)" + +[portal.connections.types.ftp.fields.security] +helperText = "Obični FTP šalje vjerodajnice i datoteke nešifrirano; odaberite FTPS ako ga poslužitelj podržava." +label = "Šifriranje" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (eksplicitno / AUTH TLS)" +implicit = "FTPS (implicitno)" +none = "Ništa (obični FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Objavite poruku u Google Chat prostor kada se pravilo pokrene." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Priložite obrađene dokumente Jira issueu." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Pošaljite dokument ili obavijest emailom kada se pravilo pokrene." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Spremite obrađene dokumente u Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Lozinka aplikacije" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Otkrijte osobne podatke u tekstu dokumenta pomoću Presidio poslužitelja koji vi pokrećete." +label = "Presidio (samostalno hostiran)" + +[portal.connections.types.purview] +description = "Čitajte i primijenite Microsoft Purview oznake osjetljivosti na PDF-ove." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Neobavezno. Potrebno samo za odabir oznaka po nazivu; označavanje radi i bez toga." +label = "ID aplikacije (klijenta)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Klijentska tajna" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Utisnuto na svaku oznaku kao njezin SiteId, kako bi alati svjesni Purviewa znali koja ju je organizacija primijenila." +label = "ID direktorija (stanara)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket iz kojeg izvori mogu čitati i u koji izlazi pipelinea mogu pisati." +label = "S3 pohrana" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID pristupnog ključa" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Ostavite prazno za AWS. Postavite ovo za MinIO ili drugu S3-kompatibilnu pohranu." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Regija" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Tajni pristupni ključ" + +[portal.connections.types.sendgrid] +description = "Pošaljite dokument ili obavijest emailom kada se pravilo pokrene." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Ispitujte SFTP (SSH) poslužitelj za nove dokumente." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Neobavezno. SHA-256 otisak host ključa poslužitelja (iz `ssh-keyscan host | ssh-keygen -lf -`). Kada je postavljen, prihvaća se samo poslužitelj koji predstavlja ovaj ključ; kada je prazno, ključ viđen pri prvom povezivanju se prikvačuje i kasnije promjene se odbijaju." +label = "Otisak host ključa" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Lozinka ključa" + +[portal.connections.types.sftp.fields.password] +helperText = "Ostavite prazno ako se autentificirate privatnim ključem umjesto toga." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Neobavezno. Zalijepite OpenSSH privatni ključ za autentifikaciju ključem umjesto lozinkom." +label = "Privatni ključ" + +[portal.connections.types.slack] +description = "Objavite poruku na Slack kada pravilo obradi dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Pratite SMB / CIFS mrežno dijeljenje (Windows ili Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / mrežno dijeljenje" + +[portal.connections.types.smb.fields.domain] +helperText = "Neobavezno. Windows domena ili radna grupa, ako je vaš poslužitelj koristi." +label = "Domena" + +[portal.connections.types.smb.fields.share] +label = "Dijeljenje" +placeholder = "dokumenti" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Pošaljite revizijski događaj u Splunk svaki put kada pravilo obradi dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Pošaljite revizijski događaj u Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Objavite poruku u Teams kanal kada se pravilo pokrene." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Objavite svaki dokument, ili samo pojedinosti, na URL koji odaberete." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Pokrenite Zap ili Make scenarij iz pravila." +label = "Zapier i Make" + +[portal.docs] +browse = "Pregledaj dokumentaciju" +viewSource = "Prikaži izvor na GitHubu" + [portal.docs.authentication] codeCaption = "svaki zahtjev" eyebrow = "POČETAK RADA" @@ -6624,6 +7216,9 @@ eyebrow = "VJEŠTINE" lead = "Ugrađene, imenovane sposobnosti koje vaš agent poziva kao jedan alat. Svaka vještina je deterministički lanac operacija s priloženim evaluacijama." title = "Vještine agenta" +[portal.docs.toc] +title = "Na ovoj stranici" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCA" @@ -6697,6 +7292,8 @@ type = "Vrsta" user = "Korisnik" [portal.documents.queue.empty] +connectSource = "Poveži izvor" +createPipeline = "Izradi pipeline" description = "Kako izvori šalju dokumente u vaše pipelineove, ovdje će se pojaviti za pregled." title = "Nema dokumenata u redu čekanja" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Nijedan dokument ne odgovara ovom filtru." rowActions = "Radnje retka" sensitiveLabel = "Osjetljivo" -sensitiveTitle = "Osjetljivo — potreban je pristup" [portal.documents.table.columns] action = "Pipeline / Radnja" document = "Dokument" +labels = "Labels" product = "Proizvod" status = "Status" time = "Vrijeme" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Zadnji put viđeno" region = "Regija" status = "Status" +target = "Target" version = "Verzija" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Ovaj prikaz naišao je na neočekivanu pogrešku. Pokušajte pono retry = "Pokušaj ponovno" title = "Nešto je pošlo po zlu na ovoj stranici" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Natrag" +body = "Preuzmite desktop aplikaciju ili samostalno hostirajte poslužitelj." +done = "Gotovo" +guide = "Otvori vodič {{name}}" +openInBrowser = "Umjesto toga otvori u pregledniku" +sectionDesktop = "Desktop aplikacija" +sectionSelfHosted = "Samostalno hostirano" +title = "Instalirajte Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Pokrenite službenu sliku na svojem poslužitelju." +note = "Otvara se na :8080. Zadana prijava: admin / stirling." +tagline = "Pokrenite poslužitelj u spremniku" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instalirajte službeni Helm chart." +note = "Dodajte PVC za /configs kako bi se postavke trajno spremile." +tagline = "Implementirajte u cluster pomoću Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Izvorne verzije za vašu distribuciju." +guideBtn = "Prikaži opcije instalacije za Linux" +note = "AppImage, .deb i .rpm verzije dostupne su za uobičajene distribucije." +tagline = "Izvorna desktop aplikacija za vašu distribuciju" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ili putem Homebrew" +detailBody = "Pokrenite instalacijski program ili upotrijebite Homebrew." +downloadBtn = "Preuzmi za macOS" +tagline = "Izvorna aplikacija za Apple silicon i Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Pokrenite .jar na bare metal poslužitelju." +downloadBtn = "Preuzmi .jar" +note = "Zahtijeva Java 21+. Otvara se na :8080; zadana prijava admin / stirling." +runLabel = "Zatim ga pokrenite" +tagline = "Pokrenite .jar na bare metal poslužitelju" +title = "Ručno postavljanje poslužitelja" + +[portal.home.download.windows] +altLabel = "Ili putem winget" +detailBody = "Pokrenite instalacijski program ili upotrijebite upravitelj paketa." +downloadBtn = "Preuzmi instalacijski program" +tagline = "Izvorna aplikacija za Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktivno" -invite = "Pozovi članove tima" +activeOfDeployed = "{{active}} od {{total}} aktivno ovog mjeseca" name = "Stirling PDF Editor" open = "Otvori u pregledniku" updated = "ažurirano {{time}}" +[portal.home.editor.deploy] +finish = "Dovrši implementaciju" +options = "Prikaži opcije implementacije" +start = "Implementiraj Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Samostalno hostano · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dobar dan" evening = "Dobra večer" morning = "Dobro jutro" -[portal.home.onboarding] -dismiss = "Odbaci postavljanje" -notStarted = "Nije započeto" -progress = "{{done}} od {{total}} dovršeno" -title = "Dovršite postavljanje" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC za cijelu organizaciju, ugovorene količinske cijene i implementacija bez mrežne veze." -cta = "Pokreni probno razdoblje" -ctaQuote = "Zatraži ponudu" -lead = "Za 250+ zaposlenika." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Implementirajte desktop aplikaciju, zauvijek besplatno." -title = "Preuzmite PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktivno · {{recommended}} preporučeno" -chip = "{{active}} aktivno" -title = "Uključite pravila" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} povezano · svaki PDF pod pravilima gdje god stigne" -chip = "{{connected}} povezano" -title = "Povežite svoje izvore" - -[portal.home.quickActions] -subtitle = "Najvažniji zadaci za danas" -title = "Brze radnje" - -[portal.home.quickActions.buildPipeline] -blurb = "Sastavljač u 3 koraka preko biblioteke tipiziranih operacija" -title = "Izradi pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooks, nadzirane mape" -title = "Poveži izvor" - -[portal.home.quickActions.issueApiKey] -blurb = "Ključ s opsegom, ograničenjima brzine i IP allowlistom" -title = "Izdaj API ključ" - [portal.infrastructure] manageEditorDeployment = "Upravljaj implementacijom Editora" sectionsAriaLabel = "Odjeljci infrastrukture" @@ -6878,13 +7541,9 @@ heading = "API ključevi" subheading = "Vjerodajnice s opsegom, ograničenjima brzine po ključu, dozvolama i IP allowlistama." [portal.infrastructure.apiKeys.card] -allowedIps = "Dopuštene IP adrese" -anyIp = "Bilo koji IP (bez allowliste)" created = "Izrađeno" lastUsed = "Zadnji put korišteno" -permissions = "Dozvole" -rateLimit = "Ograničenje brzine" -rateLimitValue = "{{value}} zahtj./min" +revoke = "Opozovi ključ" usageMonth = "Korištenje ovog mjeseca" usageToday = "Korištenje danas" @@ -6892,6 +7551,15 @@ usageToday = "Korištenje danas" description = "Izradite ključ s opsegom za početak pozivanja Stirling API." title = "Još nema API ključeva" +[portal.infrastructure.apiKeys.error] +load = "Nije moguće učitati vaše API ključeve. Pokušajte ponovno." + +[portal.infrastructure.apiKeys.revoke] +body = "Opozvati \"{{name}}\"? Svaka integracija koja još koristi ovaj ključ odmah će početi primati pogreške 401. To se ne može poništiti." +cancel = "Odustani" +confirm = "Opozovi ključ" +title = "Opozovi API ključ" + [portal.infrastructure.audit] filterAriaLabel = "Filtriraj revizijske događaje prema kategoriji" heading = "Revizijski zapisi" @@ -6935,6 +7603,7 @@ all = "Sve" auth = "Autentifikacija" config = "Konfiguracija" elevation = "Povišenje ovlasti" +policy = "Pravilo" processing = "Obrada" security = "Sigurnost" @@ -6944,19 +7613,30 @@ title = "Nemate pristup" [portal.infrastructure.audit.metrics] config = "Konfiguracija" -elevation = "Povišenje ovlasti" +policy = "Pravilo" processing = "Obrada" totalEvents = "Ukupno događaja · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autentikacija" +config = "Konfiguracija" +elevation = "Povišenje ovlasti" +policy = "Pravilo" +processing = "Obrada" +security = "Sigurnost" + +[portal.infrastructure.auditStatusLabel] +danger = "Pogreška" +info = "Info" +success = "Uspjeh" +warning = "Upozorenje" + [portal.infrastructure.createKey] cancel = "Odustani" createKey = "Izradi ključ" done = "Gotovo" -ipAllowlistHelper = "CIDR rasponi odvojeni zarezima. Ostavite prazno za dopuštanje bilo koje IP adrese." -ipAllowlistLabel = "IP allowlista" keyNameLabel = "Naziv ključa" keyNamePlaceholder = "npr. Produkcija · unos" -permissionsLabel = "Dozvole" secretKeyCaption = "Tajni ključ" secretWarning = "Pohranite ovo u upravitelj tajni. Stirling uvijek pohranjuje samo hash — kasnije ga nije moguće oporaviti." subtitle = "Ograničite ključ na minimum koji mu je potreban. Možete ga rotirati ili opozvati u bilo kojem trenutku." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopirajte ovu tajnu sada — neće se ponovno prikazati." title = "Izradi API ključ" titleCreated = "Ključ izrađen" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Implementirao" -environment = "Okruženje" -product = "Proizvod" -status = "Status" -version = "Verzija" -when = "Kada" - -[portal.infrastructure.deployments.recent] -heading = "Nedavne implementacije" -subheading = "Najnovija uvođenja kroz proizvode i okruženja." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instance" -latency = "Latencija" -load = "Opterećenje" -p99 = "P99" -region = "Regija" -status = "Status" -throughput = "Propusnost" -uptime = "Vrijeme rada" -version = "Verzija" - -[portal.infrastructure.deployments.regions] -heading = "Regije" -subheading = "Živo zdravlje za svaku implementiranu Stirling regiju — latencija, opterećenje i verzija uvođenja." - -[portal.infrastructure.deployments.regions.empty] -description = "Implementirane regije pojavit će se ovdje nakon što vaš radni prostor bude provisioniran." -title = "Nema implementiranih regija" - -[portal.infrastructure.models] -heading = "Modeli" -msValue = "{{value}} ms" -subheading = "Katalog modela i usmjeravanje koje pokreće obradu dokumenata u vašem radnom prostoru." - -[portal.infrastructure.models.byom] -description = "Registrirajte on-prem ili samostalno hostani model i vežite ga uz regiju za obradu vezanu uz rezidenciju podataka." -title = "Donesite vlastiti model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Upravljani modeli dostupni vašem radnom prostoru, s latencijom i troškom uživo." -subEnterprise = "Upravljani, vlastiti i on-prem modeli — s dostupnim vezivanjem po regiji." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeli u katalogu vašeg radnog prostora pojavit će se ovdje." -title = "Nema dostupnih modela" - -[portal.infrastructure.models.columns] -cost = "Trošak" -latency = "Latencija" -load = "Opterećenje" -model = "Model" -status = "Status" -type = "Vrsta" -version = "Verzija" - -[portal.infrastructure.models.metrics] -activeModels = "Aktivni modeli" -avgLatency = "Prosj. latencija" -included = "Uključeno" -monthlySpend = "Mjesečna potrošnja za modele" - -[portal.infrastructure.models.routing] -empty = "Nema konfiguriranih pravila usmjeravanja." -heading = "Pravila usmjeravanja" -sub = "Koji model obrađuje svaku operaciju. Zadano se primjenjuje kada ne odgovara nijedno uže pravilo." -subLocked = "Usmjerite operacije na određene modele — dostupno na plaćenim planovima." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Nadogradite na Pro kako biste kontrolirali koji model obrađuje svaku operaciju i vrstu dokumenta." -title = "Usmjeravanje modela je plaćena značajka" - -[portal.infrastructure.models.routingColumns] -default = "Zadano" -docType = "Vrsta dokumenta" -modelForAria = "Model za {{operation}}" -operation = "Operacija" -routedTo = "Usmjereno na" - -[portal.infrastructure.security.access.byok] -description = "Dostavite ključ iz vlastitog KMS. Stirling njime šifrira, ali i dalje može čitati." -label = "Koristite vlastiti ključ (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ključevi nikada ne napuštaju vaš KMS. Stirling drži samo šifrirani tekst." -label = "Držite vlastiti ključ (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling upravlja ključevima za šifriranje. Najjednostavnije — bez operacija s ključevima s vaše strane." -label = "Ključevi koje drži Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Pravilo pristupa dokumentima" -subheading = "Kontrolira tko može dešifrirati obrađene dokumente u mirovanju." - -[portal.infrastructure.security.attestations] -heading = "Atestacije usklađenosti" -noReport = "Nema dostupnog izvješća" -subheading = "Revizijski položaj po okvirima, s izvješćima dostupnima za atestirane kontrole." -viewReport = "Prikaži izvješće →" - -[portal.infrastructure.security.compliance] -heading = "Usklađenost" -subheading = "Atestacije i certifikati koji pokrivaju Stirling platformu." - -[portal.infrastructure.security.empty] -description = "Sigurnosna konfiguracija vašeg radnog prostora pojavit će se ovdje." -title = "Sigurnosni položaj nije dostupan" - -[portal.infrastructure.security.hyokBanner] -description = "Uz HYOK, ključevi za šifriranje nikada ne napuštaju vaš KMS. Stirling pohranjuje i obrađuje samo šifrirani tekst koji možete opozvati u bilo kojem trenutku." -title = "Stirling ne može dešifrirati vaše dokumente" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nema konfiguriranih IP raspona — sve IP adrese su dopuštene." -heading = "IP allowlista" -sub = "API pristup ograničen je na ove CIDR raspone." -subLocked = "Ograničite API pristup na poznate IP raspone — dostupno na plaćenim planovima." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Nadogradite na Pro kako biste ograničili API pristup na određene mreže." -title = "IP allowlista je plaćena značajka" - -[portal.infrastructure.security.ipColumns] -added = "Dodano" -addedBy = "Dodao" -cidr = "CIDR" -label = "Oznaka" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritam" -heading = "Upravljanje ključevima za šifriranje" -keyId = "Identifikator ključa" -lastRotated = "Zadnja rotacija" -rotateKey = "Rotiraj ključ" -rotationPolicy = "Pravilo rotacije" -subheading = "Skrbništvo nad ključevima koji šifriraju dokumente u mirovanju — tko može dešifrirati i kako se ključevi rotiraju." - -[portal.infrastructure.security.managedBanner] -description = "Skrbništvo Bring-your-own-key (BYOK) i hold-your-own-key (HYOK) dostupno je na Enterprise planu. Nadogradite kako biste dostavili ključeve iz vlastitog KMS." -title = "Ključevima na vašem planu upravlja Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Azija i Pacifik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR granica podataka" -label = "Europska unija" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Sjedinjene Američke Države" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezidencija podataka" -subheading = "Gdje se dokumenti pohranjuju i obrađuju." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} iskorišteno" - -[portal.infrastructure.storage.empty] -description = "Povezana pohrana i korištenje pojavit će se ovdje." -title = "Nema konfigurirane pohrane" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktivno" -activeRange = "0–{{value}}d" -archived = "Arhivirano" -coldStorage = "hladna pohrana" -deleted = "Izbrisano" -never = "nikad" -purged = "trajno izbrisano" - -[portal.infrastructure.storage.providers] -connect = "Poveži" -connected = "Povezano" -heading = "Povezani pružatelji" -subheading = "Gdje se zapisuju obrađeni artefakti." - -[portal.infrastructure.storage.retention] -heading = "Zadržavanje" -subheading = "Koliko se dugo artefakti čuvaju prije brisanja u životnom ciklusu." -windowLabel = "Zadani prozor zadržavanja" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dan" -days_other = "{{count}} dana" -never = "Nikad ne briši" - -[portal.infrastructure.storage.totalUsage] -heading = "Ukupno korištenje" -progressLabel = "Iskorištena pohrana" -subheading = "Pohrana iskorištena kroz sve povezane pružatelje." +revoked = "Opozvano" [portal.infrastructure.tabs] apiKeys = "API ključevi" @@ -7174,6 +7656,49 @@ models = "Modeli" security = "Sigurnost" storage = "Pohrana" +[portal.integrations] +availableHeading = "Dostupno" +comingSoonHeading = "Uskoro" +connect = "Poveži" +connectionCount_one = "{{count}} veza" +connectionCount_other = "{{count}} veza" +customApi = "Prilagođeni API" +searchPlaceholder = "Pretraži integracije" +subtitle = "Sve s čime Stirling može komunicirati. Vjerodajnice se ovdje stvaraju jednom, a zatim ih koriste izvori, pravila i tijekovi obrade." +title = "Integracije" +typedName = "Naziv za {{tool}}" + +[portal.integrations.filters] +all = "Sve" +audit = "Revizija" +connected = "Povezano" +notify = "Obavijesti" +security = "Sigurnost" +signing = "Potpisivanje" +storage = "Pohrana" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Povezano" + +[portal.integrations.table] +integration = "Integracija" +worksWith = "Radi s" + +[portal.integrations.worksWith] +pipelines = "Tijekovi obrade" +policies = "Pravila" +sources = "Izvori" + +[portal.legal] +draft = "{{label}} · nacrt" +loadError = "Nije moguće učitati ovaj dokument. Pokušajte ponovno." +loading = "Učitavanje…" +title = "Pravni dokument" + [portal.nav] agent-builder = "Agent Builder" components = "Komponente" @@ -7182,33 +7707,17 @@ documents = "Dokumenti" editor = "Editor" home = "Početna" infrastructure = "Infrastruktura" +integrations = "Integracije" pipelines = "Pipelineovi" policies = "Pravila" procurement = "Nabava" -settings = "Postavke" sources = "Izvori" usage = "Korištenje i naplata" users = "Korisnici" -[portal.notifications] -markAllRead = "Označi sve kao pročitano" -title = "Obavijesti" -viewAll = "Prikaži sve" - -[portal.notifications.ariaLabel] -none = "Obavijesti, nema nepročitanih" -unread_one = "Obavijesti, {{count}} nepročitana" -unread_other = "Obavijesti, {{count}} nepročitanih" - -[portal.notifications.count] -allRead = "sve pročitano" -loading = "učitavanje" -new_one = "{{count}} novo" -new_other = "{{count}} novih" - -[portal.notifications.empty] -description = "Nema novih obavijesti." -title = "Sve ste riješili" +[portal.nav.section] +platform = "PDF platforma" +processor = "PDF Processor" [portal.pipelines] subtitle = "Svaki automatizirani pipeline dokumenata na backendu: uređeni lanac operacija nad skupom izvora, pokrenut okidačem. Kliknite redak za njegove korake i izvore." @@ -7218,54 +7727,87 @@ title = "Pipelineovi" newPipeline = "Novi pipeline" [portal.pipelines.builder] -addStep = "Dodaj alat" +activate = "Activate" back = "Natrag na pipelineove" +cannotFollow = "Ne može primiti {{produced}}" +chooseAccount = "Odaberite račun" +chooseDestination = "Odaberite odredište" +chooseOperation = "Odaberite što ovaj korak radi" +chooseSource = "Odaberite izvor" discard = "Odbaci promjene" -enabled = "Omogućeno" +inputs = "Ulaz" +inputSource = "Ulazni izvor" +inputTrigger = "Okidač" keepEditing = "Nastavi uređivati" -needsUpload = "Potrebna je učitana datoteka" +moreActions = "More actions" +needsConfiguring = "Potrebno je postavljanje" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nijedan alat ne odgovara vašem pretraživanju." -pipelineSettings = "Postavke pipelinea" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Pretraži alate" -selectToolBody = "Dodajte alat za izradu pipelinea." -selectToolTitle = "Još nema alata" -toolSettings = "Postavke alata" +sendToSystem = "Pošalji u drugi sustav" +stepsIncompatible = "Ovi se koraci ne mogu pokrenuti na onome što proizvodi njihov prethodni korak: {{tools}}." +stepsNeedSetup = "Ove korake još treba postaviti prije spremanja: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Neprepoznata operacija, zadržana takva kakva jest." unsavedBody = "Imate nespremljene promjene. Spremiti ih prije odlaska ili ih odbaciti?" unsavedTitle = "Nespremljene promjene" -uploadUnsupported = "Učitane datoteke još nisu podržane u pipelineovima, pa se ovi koraci ne mogu spremiti: {{tools}}." usesDefaults = "Pokreće se sa zadanim postavkama" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinira svaku datoteku iz prethodnog koraka" +fan-out = "Pokreće se jednom po datoteci iz prethodnog koraka" +format-mismatch = "Treba {{accepts}}, ali prethodni korak proizvodi {{produced}}" +output-uncertain = "Možda se neće pokrenuti: izlaz prethodnog koraka ovisi o tome kako je postavljen" +source-mismatch = "Treba {{accepts}}, ali ulaz ovog tijeka obrade je {{produced}}" +undeclared-operation = "Nije moguće provjeriti što ovaj korak prihvaća" [portal.pipelines.composer] addTool = "Dodaj alat" -cancel = "Odustani" -chainEmpty = "Dodajte alat za početak izrade pipelinea." create = "Izradi pipeline" -directory = "Izlazna mapa" -directoryHelp = "Apsolutna putanja na serveru. Mora biti unutar konfiguriranih dopuštenih mapa." +createPaused = "Create paused" editingUnsupported = "Prikaz ovih parametara alata za uređivanje još nije podržan." -moveDown = "Pomakni dolje" -moveUp = "Pomakni gore" +editSource = "Edit source" name = "Naziv" namePlaceholder = "npr. Pregled redakcija" -noSources = "Još nema povezanih izvora. Pipeline se i dalje može pokretati na datotekama koje su mu izravno dostavljene." noToolSettings = "Ovaj alat nema konfigurabilnih postavki." -operations_one = "Operacija ({{count}})" -operations_other = "Operacije ({{count}})" output = "Izlaz" -removeStep = "Ukloni operaciju" save = "Spremi promjene" scheduleEvery = "Pokreni svakih" -sources = "Izvori" -sourcesLoading = "Učitavanje izvora..." trigger = "Okidač" triggerManual = "Samo ručno" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dana" hours = "sati" minutes = "minuta" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Izbrisati \"{{name}}\"? Ovo se ne može poništiti." cancel = "Odustani" @@ -7273,28 +7815,62 @@ confirm = "Izbriši" title = "Izbrisati pipeline?" [portal.pipelines.detail] +clearHistory = "Očisti povijest" delete = "Izbriši pipeline" run = "Pokreni sada" [portal.pipelines.empty] action = "Izradi pipeline" +connectSource = "Poveži izvor" description = "Izradite svoj prvi pipeline: odaberite izvore nad kojima se pokreće, povežite operacije i odaberite kamo ide izlaz." title = "Još nema pipelineova" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktivno" paused = "Pauzirano" total = "Pipelineovi" -[portal.pipelines.output] -folder = "Zapiši u mapu" -inline = "Vrati datoteke" - [portal.pipelines.run] +allProcessed_one = "Nema ničega za pokretanje: {{count}} dokument izvora već je obrađen." +allProcessed_other = "Nema ničega za pokretanje: svih {{count}} dokumenata u izvorima već je obrađeno." completed_one = "Pokretanje dovršeno." completed_other = "Svih {{count}} pokretanja je dovršeno." empty = "Nema ništa za pokretanje: izvori nisu imali dokumente za obradu." failed = "Pokretanje nije uspjelo: {{error}}" +inFlight = "Nema ničeg novog za pokretanje: dokumenti se još obrađuju iz ranijeg pokretanja." +parked_one = "Nema ničega za pokretanje: {{count}} dokument prethodno nije uspio i parkiran je. Ispravite uzrok, zatim očistite povijest da biste ga ponovno pokušali obraditi." +parked_other = "Nema ničega za pokretanje: {{count}} dokumenata prethodno nije uspjelo i parkirano je. Ispravite uzrok, zatim očistite povijest da biste ih ponovno pokušali obraditi." running = "Pokretanje je započelo; još je u tijeku." timeout = "Pokretanje traje dulje od očekivanog; možda će se ipak dovršiti u pozadini." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Izvori" status = "Status" steps = "Koraci" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Nadzor mape" @@ -7314,12 +7891,18 @@ manual = "Ručno" schedule = "Zakazano" [portal.policies] +defaultName = "Pravilo za {{category}}" subtitle = "Trajne automatizacije koje primjenjuju pipeline alata na svaki dokument. Svako pravilo pokreće se pri učitavanju ili izvozu, izvršava svoj lanac alata i sprema nametnutu verziju uz izvornik." title = "Pravila" [portal.policies.card] comingSoon = "Nadogradi na Enterprise" notSetUp = "Nije postavljeno" +requiresAiEngine = "Zahtijeva AI engine" + +[portal.policies.categories.classification] +desc = "Prepoznajte vrstu svakog dokumenta pri prijenosu i označite njegove metapodatke za pohranu i pretraživanje." +label = "Klasifikacija" [portal.policies.categories.compliance] desc = "Primijenite HIPAA, GDPR, SOC 2 ili FedRAMP zahtjeve na svaki dokument." @@ -7344,6 +7927,13 @@ label = "Sigurnost" [portal.policies.config] scopeAll = "Svi dokumenti" +[portal.policies.config.classification] +summary = "Klasificira svaki preneseni dokument prema oznakama vašeg tima i označava njegove metapodatke." + +[portal.policies.config.classification.rules] +0 = "Klasificiraj" +1 = "Označi metapodatke" + [portal.policies.config.compliance] summary = "Provjerava dokumente prema regulatornim okvirima prije nego što napuste sustav." @@ -7371,6 +7961,29 @@ minConfidence = "Min. pouzdanost" 2 = "Imenuj" 3 = "Normaliziraj" +[portal.policies.config.purview] +label = "Primijeni Microsoft Purview oznaku osjetljivosti" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "ID oznake" +labelIdHelp = "GUID oznake iz Purview portala. Primjena oznake zapisuje njezine metapodatke u PDF; ne može šifrirati, za što je potreban Microsoft klijent." +labelName = "Naziv oznake (neobavezno)" +labelNameHelp = "Bilježi se uz ID kako bi ljudi i alati mogli prepoznati oznaku." + +[portal.policies.config.purview.fields.method] +help = "Purview razlikuje oznaku koju je primijenilo pravilo od one koju je odabrala osoba." +label = "Kako se oznaka bilježi" +privileged = "Kao da ju je odabrala osoba" +standard = "Kao automatski primijenjena" + +[portal.policies.config.purviewRead] +label = "Pročitaj Microsoft Purview oznaku" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Čita oznaku koju dokument već nosi, kako bi kasniji koraci mogli djelovati prema njoj." + [portal.policies.config.retention] summary = "Primjenjuje koliko se dugo dokumenti čuvaju, kada se arhiviraju i kada se brišu." @@ -7438,6 +8051,7 @@ title = "Još nema aktivnosti" [portal.policies.endpoints] addWatermark = "Vodeni žig" autoRedact = "Redigiraj PII" +classifyAndLabel = "Klasificiraj" compressPdf = "Komprimiraj" flatten = "Izravnaj" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vaša su pravila spremljena i pojavit će se nakon što se veza p retry = "Pokušaj ponovno" title = "Backend nije dostupan" +[portal.policies.operations] +change = "Promijeni što ovaj korak radi" +noResults = "Nijedan korak ne odgovara tome. Pokušajte naziv proizvoda ili \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Pretraži korake - pokušajte \"scan\", \"attach\" ili naziv proizvoda" + +[portal.policies.operations.bodyMode] +binary = "Neobrađeni bajtovi datoteke" +json = "Unutar JSON payload" +multipart = "Kao prijenos datoteke (multipart)" + +[portal.policies.operations.clamavScan] +description = "Vaš vlastiti ClamAV poslužitelj provjerava dokument; ništa ne napušta vašu mrežu." +label = "Skeniraj viruse (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Odbija dokumente koji sadrže makronaredbe, izvršne datoteke, skripte ili ugrađene objekte." +label = "Blokiraj aktivni sadržaj" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive provjerava dokument i pokretanje se zaustavlja ako nije čist." +label = "Skeniraj viruse" + +[portal.policies.operations.confluenceAttach] +description = "Pohranjuje obrađeni dokument na stranicu." +label = "Priloži na Confluence stranicu" + +[portal.policies.operations.consignoSubmit] +description = "Šalje dokument u ConsignO Cloud na potpisivanje." +label = "Pošalji na potpis (ConsignO)" +note = "Šalje dokument. Dohvaćanje potpisane kopije još nije podržano jer korak ne može proslijediti rezultat kasnijem koraku." + +[portal.policies.operations.customApiCall] +description = "Sami definirajte poziv: putanju, metodu, tijelo i obradu odgovora." +label = "Pozovi prilagođeni API" + +[portal.policies.operations.discordNotify] +description = "Obavještava kanal da je pravilo obradilo dokument." +label = "Objavi poruku na Discord" + +[portal.policies.operations.elasticIndex] +description = "Bilježi što je pravilo učinilo kao dokument koji se može pretraživati." +label = "Indeksiraj revizijski događaj u Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart odgovara API-jima za prijenos; JSON odgovara API-jima koji žele datoteku unutar payload; binary šalje neobrađene bajtove." +label = "Kako poslati dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON tijelo šalje se takvo kakvo jest. {{document.base64}} nosi datoteku; {{document.*}} i {{run.*}} popunjavaju se po dokumentu." +label = "JSON tijelo" + +[portal.policies.operations.fields.connection] +label = "Račun" + +[portal.policies.operations.fields.domain] +label = "Mailgun domena" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Polje obrasca pod kojim API očekuje dokument." +label = "Naziv polja datoteke" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON objekt parova naziv/vrijednost zaglavlja. Vjerodajnica veze dodaje se za vas." +label = "Zaglavlja" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ključ zadatka" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Šalje se kao tijelo poruke. {{document.*}} i {{run.*}} popunjavaju se po dokumentu." +label = "Poruka" + +[portal.policies.operations.fields.method] +label = "Metoda" + +[portal.policies.operations.fields.pageId] +label = "ID stranice" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Dodaje se osnovnom URL veze. Nikad ne može dosegnuti drugi host." +label = "Putanja" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Gdje ga zapisati na drive. {{document.filename}} popunjava se po dokumentu." +label = "Odredišna putanja" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Izvješće ostavlja dokument nepromijenjenim. Zamjena ga mijenja onime što se vrati." +label = "Što učiniti s odgovorom" + +[portal.policies.operations.fields.signerEmail] +label = "Email potpisnika" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Predmet" +placeholder = "Obrađeno: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizira ovaj tekst. Upotrijebite {{document.title}} ili zalijepite tekst koji želite provjeriti." +label = "Tekst za provjeru" + +[portal.policies.operations.fields.to] +label = "Za" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud korisničko ime" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Obavještava prostor da je pravilo obradilo dokument." +label = "Objavi poruku na Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Pohranjuje obrađeni dokument na zadatak." +label = "Priloži na Jira zadatak" + +[portal.policies.operations.mailgunEmail] +description = "Šalje obrađeni dokument kao privitak." +label = "Pošalji dokument emailom (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Zapisuje obrađeni dokument u mapu." +label = "Prenesi na Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Vaš vlastiti Presidio poslužitelj izvještava o osobnim podacima koje pronađe." +label = "Otkrij osobne podatke" +note = "Presidio čita tekst, ne datoteke. Dajte mu tekst koji želite provjeriti — sam dokument se ne prenosi." + +[portal.policies.operations.responseMode] +replace = "Zamijeni dokument odgovorom" +report = "Zadrži dokument, zabilježi odgovor" + +[portal.policies.operations.sendgridEmail] +description = "Šalje obrađeni dokument kao privitak." +label = "Pošalji dokument emailom (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Obavještava kanal da je pravilo obradilo dokument." +label = "Objavi poruku na Slack" + +[portal.policies.operations.splunkEvent] +description = "Bilježi što je pravilo učinilo tamo gdje vaši revizori već gledaju." +label = "Pošalji revizijski događaj u Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Bilježi što je pravilo učinilo u HTTP kolektor." +label = "Pošalji revizijski događaj u Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Obavještava kanal da je pravilo obradilo dokument." +label = "Objavi poruku na Teams" + +[portal.policies.operations.webhookPost] +description = "Šalje sam dokument na URL koji odaberete." +label = "Objavi dokument na webhook" + +[portal.policies.operations.zapierNotify] +description = "Predaje detalje pokretanja vašoj automatizaciji." +label = "Pokreni Zap ili Make scenarij" + [portal.policies.stats] activeFor = "Aktivno" dataProcessed = "Obrađeni podaci" @@ -7473,6 +8282,13 @@ label = "Primijenjeni dokumenti" description = "Konfigurirano, ali se ne pokreće" label = "Pauzirano" +[portal.policies.table] +appliesTo = "Primjenjuje se na" +docs = "Dokumenti pod pravilima" +enforces = "Provodi" +policy = "Pravilo" +status = "Status" + [portal.policies.wizard.actions] back = "Natrag" cancel = "Odustani" @@ -7480,6 +8296,10 @@ continue = "Nastavi" enablePolicy = "Omogući pravilo" saveChanges = "Spremi promjene" +[portal.policies.wizard.capability.classify] +desc = "Prepoznaje vrstu dokumenta prema oznakama vašeg tima i označava ga, kako bi se pohranjivao i pretraživao po kategoriji." +label = "Klasificiraj dokument" + [portal.policies.wizard.capability.compress] desc = "Komprimira dokument na manju veličinu datoteke." label = "Smanji veličinu datoteke" @@ -7500,10 +8320,18 @@ label = "Redigiraj osjetljive informacije" desc = "Uklanja skriveni JavaScript kako se ništa ne bi moglo automatski pokrenuti pri otvaranju dokumenta." label = "Ukloni aktivni sadržaj" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dokazuje da je dokument postojao u ovom točnom obliku u određenom trenutku, pomoću neovisnog tijela za vremenske žigove. Šalje se samo hash - dokument nikad ne napušta vaš poslužitelj." +label = "Dodaj pouzdani vremenski žig" + [portal.policies.wizard.capability.watermark] desc = "Utiskuje vidljivu oznaku (npr. “Povjerljivo”) preko svake stranice." label = "Primijeni vodeni žig" +[portal.policies.wizard.classification] +description = "Svaki preneseni dokument klasificira se prema ugrađenim oznakama i označava vrstama koje odgovaraju. Skup oznaka dijeli se s cijelim timom." +labelsHeading = "Klasifikacijske oznake" + [portal.policies.wizard.errors] noTools = "Najprije omogućite barem jedan alat u tijeku rada." saveFailed = "Nije moguće spremiti pravilo. Pokušajte ponovno." @@ -7548,73 +8376,118 @@ setUp = "Postavi pravilo {{category}}" [portal.policies.wizard.workflow] description = "Odaberite što ovo pravilo radi sa svakim dokumentom koji obrađuje." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktivno" -noRule = "Još nije primijenjeno nijedno pravilo" -subtitle = "Stalne automatizacije kroz koje prolazi svaki dokument, bez obzira na to koji ga pipeline obrađuje." -title = "Što se pokreće na vašim PDF-ovima" - [portal.policySummary.action] -comingSoon = "Nadogradi na Enterprise" -configure = "Konfiguriraj" setUp = "Postavi" -[portal.policySummary.column] -activeRule = "Aktivno pravilo" -policy = "Pravilo" -status = "Status" +[portal.processorFlow] +footnote = "Brojevi se odnose na posljednja 24 sata. Brzina toka je ilustrativna." +liveBadge = "Aktivno" +stats = "{{connected}} povezano · {{processed}} PDF-ova obrađeno" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Nakon konfiguriranja pravila, kategorije će se pojaviti ovdje." -title = "Još nema pravila" +[portal.processorFlow.lens] +ariaLabel = "Način prikaza" +flow = "Tok" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktivno" -off = "Isključeno" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Neuspjelo" +heading = "Ishodi" +success = "Isporučeno" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktivno" +count = "{{n}} · 24h" +heading = "Pravila" +setUp = "Postavi" soon = "Uskoro" -[portal.processingStatus] -managePlan = "Upravljaj planom" -volumeSuffix = "Obrađenih PDF-ova · zadnjih 30 dana" +[portal.processorFlow.sankey] +waist = "Pravila · {{n}} aktivno" + +[portal.processorFlow.sankey.empty] +description = "Povežite izvor i uključite pravilo kako biste pratili tok dokumenata kroz processor." +title = "Još nema toka" + +[portal.processorFlow.sources] +comingSoonTag = "Poveži" +editor = "Stirling PDF Editor" +heading = "Izvori" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Ulaz emailom" [portal.procurement] -reset = "Resetiraj nabavu (demo)" subtitle = "Provedite evaluaciju tima, ugovaranje i uvođenje. Svaki dokument na jednom mjestu." title = "Nabava" -[portal.procurement.action] -download = "Preuzmi" -pay = "Plati sada" -request = "Zatraži" -sign = "Pregledaj i potpiši" -upload = "Prenesi" - [portal.procurement.agreement] agreeCta = "Prihvati i pretplati se" +confidential = "Povjerljivo" confirm = "Pročitao/la sam i prihvaćam Stirling Enterprise Agreement." -eyebrow = "Ugovor" -intro = "Jedan objedinjeni ugovor pokriva vašu ponudu: Master Service Agreement, Order Form, EULA i Data Processing Agreement. Pregledajte ga, a zatim prihvatite ponudu kao obvezujuću pretplatu." -title = "Pregledajte svoj enterprise ugovor" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, narudžbenica, EULA i DPA, spojeni u jedan potpis." +download = "Preuzmi" +downloadDraftError = "Nije moguće generirati PDF ugovora - renderer dokumenta nije dostupan. Pokušajte ponovno uskoro ili kontaktirajte podršku." +downloadError = "Nije moguće preuzeti potpisani ugovor. Pokušajte ponovno." +legalName = "Naziv pravnog subjekta" +legalNamePlaceholder = "Pravni subjekt koji će potpisati" +loadError = "Nije moguće učitati ugovor. Pokušajte ponovno." +loading = "Učitavanje ugovora..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Zatraži izmjene" +scrollHint = "Pomaknite se za pregled" +signatory = "Ime potpisnika" +signatoryPlaceholder = "Puno ime" +signatoryTitle = "Titula potpisnika" +signatoryTitlePlaceholder = "npr. glavni pravni savjetnik" +signError = "Nije moguće zabilježiti vaš potpis. Pokušajte ponovno." [portal.procurement.builder] addons = "Dodaci" +addressLine1 = "Adresa, redak 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresa, redak 2" +addressLine2Placeholder = "Apartman, kat, zgrada" back = "Natrag" businessName = "Naziv tvrtke" businessNamePlaceholder = "Vaša tvrtka" +city = "Grad" +cityPlaceholder = "San Francisco" +completeRequired = "Ispunite obavezna polja (označena *) valjanom email adresom prije generiranja ponude." +contactEmail = "Kontakt email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Ime kontakta" +contactNamePlaceholder = "Jane Doe" continue = "Nastavi" -country = "Država" -countryEuro = "Eurozona (EUR €)" -countryUK = "Ujedinjeno Kraljevstvo (GBP £)" -countryUS = "Sjedinjene Američke Države (USD $)" +done = "Gotovo" eula = "Pročitao/la sam i prihvaćam Stirling Enterprise EULA. Njime se uređuje ugovor generiran iz ove ponude." generate = "Generiraj ponudu" -included = "Uključeno" indemnification = "IP obeštećenje" indemnificationSub = "Branimo kvalificirane IP zahtjeve, prema EULA" -offlineLicense = "Offline / air-gapped licencija" -offlineLicenseSub = "Licencijska datoteka za preuzimanje za air-gapped self-hosted instancu" +paperEyebrow = "Enterprise ponuda" +paperFor = "Pripremljeno za" +pdfSize = "Veličina PDF" +poNumber = "Broj narudžbenice" +poNumberPlaceholder = "Neobavezno" +postalCode = "Poštanski broj" +postalCodePlaceholder = "94105" +posture = "Upravljanje" +posture_count = "~{{count}} pravila" +posture_essentials = "Osnove" +posture_essentialsSub = "Klasifikacija + dijeljenje — zadano" +posture_governed = "Upravljano" +posture_governedSub = "Dodaje sigurnost i usmjeravanje" +posture_regulated = "Regulirano" +posture_regulatedSub = "Svaka kategorija — usklađenost, zadržavanje, unos" qbr = "Tromjesečni poslovni pregledi" qbrSub = "Vaš SE pregledava upotrebu i plan razvoja svakog tromjesečja" +region = "Država / regija" +regionPlaceholder = "California" running = "{{annual}} / god. · {{years}} god. {{tcv}}" s1Sub = "Vaš tim i PDF-ovi koje očekujete obraditi svake godine." s1Title = "Koliko ćete obrađivati?" @@ -7623,6 +8496,12 @@ s2Title = "Obveza i usluga" s3Sub = "Za ponudu i ugovor koji ona generira." s3Title = "Vaši podaci" serviceLevel = "Razina usluge" +size_compact = "Kompaktno" +size_compactSub = "Uglavnom tekst, manje od 1 MB" +size_heavy = "Teško" +size_heavySub = "Skenirano ili s mnogo slika, 5 MB+" +size_standard = "Standardno" +size_standardSub = "Miješani tekst i slike, 1 do 5 MB" slDedicated = "Namjenska" slDedicatedSub = "4 radna sata · namjenski account manager · +30%" slPriority = "Prioritetna" @@ -7630,6 +8509,8 @@ slPrioritySub = "Isti radni dan · imenovani CSM · +15%" slStandard = "Standardna" slStandardSub = "Sljedeći radni dan · dijeljeni CSM · uključeno" stepOf = "Korak {{n}} od {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Neobavezno" term = "Rok" termDiscount = "Primijenjen je popust od {{pct}}% za višegodišnju obvezu" title = "Izradite svoju ponudu" @@ -7637,6 +8518,7 @@ training = "Uvođenje i obuka" trainingSub = "Sesije uživo za pokretanje vašeg tima" users = "Ukupno korisnika" usersPlaceholder = "npr. 250" +viewEula = "Pročitaj" volEstimated = "Procijenjeno na temelju {{count}} korisnika (~2.000 PDF-ova po korisniku, uključujući automatizaciju). Uredite ako znate bolje." volManual = "Koristi se vaš broj. U bilo kojem trenutku ponovno procijenite prema veličini tima." volNoUsers = "Niste sigurni? Unesite veličinu tima i procijenit ćemo." @@ -7645,55 +8527,55 @@ volumePlaceholder = "npr. 1.000.000" years_one = "{{count}} godina" years_other = "{{count}} godina" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumenata" -done = "Gotovo" -here = "Ovdje ste" -hide = "Sakrij" -optional = "Neobavezno" -paidAddon = "Plaćeni dodatak" -show = "Prikaži" -subtitle = "Sve što vam treba u svakom koraku putovanja, prikazano kako se posao kreće kroz proces." -supportingSubtitle = "SOC 2, sigurnosne provjere, porezni obrasci i više, spremno kada vaš sigurnosni ili nabavni tim zatraži. Neki nose jednokratnu naknadu." -supportingTitle = "Podrška vašoj evaluaciji" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, narudžbenica i DPA u jednom potpisu." +download = "Preuzmi" +eula = "EULA i komercijalni uvjeti" +eulaSub = "Softver i self-serve komercijalni uvjeti." +invoice = "Račun" +invoiceSub = "Vaš prvi račun za pretplatu." +laterInvoice = "Dostupno nakon prihvaćanja" +laterQuote = "Dostupno nakon izdavanja vaše ponude" +quote = "Ponuda" +quoteSub = "Vaša detaljna enterprise ponuda." +sla = "SLA prilog" +slaSub = "Razine podrške i ciljevi odgovora." +subprocessors = "Podizvršitelji obrade" +subprocessorsSub = "Treće strane koje obrađuju podatke u vaše ime." +subtitle = "Vaša dokumentacija posla, dostupna u bilo kojem trenutku." title = "Dokumenti" -upcoming = "Nadolazeće" +view = "Prikaži" [portal.procurement.error] title = "Nešto je pošlo po zlu" [portal.procurement.hero] -company = "Vaš enterprise posao" +barAria = "Faza {{current}} od {{total}}" ctaAgreement = "Pregledaj i potpiši ugovor" -ctaLive = "Aktivni ste" -ctaPayment = "Dodaj plaćanje" +ctaExploring = "Postavite probno razdoblje" ctaQuote = "Pregledajte svoju ponudu" +ctaReviewQuote = "Pregledaj ponudu" ctaTrial = "Izradite svoju ponudu" +documents = "Dokumenti" eyebrow = "Enterprise nabava" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Pozovi članove tima" -keyDocs = "Ključni dokumenti" -nextStep = "Sljedeći korak: {{action}}" -notStarted = "Nije započeto" +licenseKey = "Licencni ključ" +liveSub = "Vaša je organizacija opskrbljena i vaša je licenca aktivna." +liveTitle = "Aktivni ste na Stirling Enterprise" +next = "Sljedeće: {{stage}}" open = "Otvori nabavu" scheduleCall = "Zakaži poziv" -setup1Sub = "Pozovite članove svojeg tima" -setup1Title = "Postavite PDF Editor" -setup2Sub = "Uključite obradu kroz editore i druge izvore" -setup2Title = "Povežite PDF Processor" -setup3Sub = "Uključite Security, Compliance, Routing ili Retention kada vam zatrebaju" -setup3Title = "Dodajte preporučena pravila" +sentenceAgreement = "pregledajte i potpišite ugovor" +sentenceLive = "opskrba i uvođenje" +sentencePayment = "narudžbenica i plaćanje" +sentenceQuote = "pregledajte i prihvatite ponudu" +sentenceTrial = "postavite probni radni prostor" [portal.procurement.journey] daysLeft_one = "Preostao je {{count}} dan" daysLeft_other = "Preostalo je {{count}} dana" -engineerLabel = "Vaš solutions engineer" -eyebrow = "Vaše uvođenje" -live = "Aktivni ste na Stirling Enterprise" -nextStep = "Sljedeći korak: {{action}}" -subtitle = "Vaš solutions engineer sudjeluje u svakom koraku. Jedna sljedeća radnja odjednom; potpuni popis za provjeru nalazi se u nastavku." -title = "Od probnog razdoblja do produkcije, jedan vođeni put" -trialTitle = "Enterprise probno razdoblje" [portal.procurement.journeySteps.agreement] blurb = "Jedan potpis pokriva MSA, narudžbenicu, EULA i DPA." @@ -7727,6 +8609,9 @@ downloadError = "Još nije moguće generirati offline licencijsku datoteku — p downloadOffline = "Preuzmi offline licenciju (.lic)" hint = "Zalijepite ovaj ključ u self-hosted instancu kako biste je aktivirali ili ga čuvajte za svoju evidenciju. Čuvajte ga na sigurnom." label = "Vaš licencijski ključ" +subtitle = "Aktivirajte self-hosted instancu ovim ključem ili ga sačuvajte za svoju evidenciju." +title = "Vaš licencni ključ" +trialFileHint = "Ovo je vaša datoteka probne licence. Nakon što vaš ugovor bude sklopljen, vratite se i ponovno je preuzmite — za razliku od online licencnog ključa, .lic datoteka neće se sama ažurirati." [portal.procurement.link] cta = "Poveži račun" @@ -7739,65 +8624,62 @@ description = "Vaša je pretplata aktivna i licencija je izdana. Vaš je tim pos eyebrow = "Aktivno" title = "Aktivni ste na Stirling Enterprise" -[portal.procurement.locked] -description = "Probni ključevi, ponude za obvezani volumen, ugovor s jednim potpisom, plaćanje i vaš registar dokumenata nalaze se ovdje nakon što započnete enterprise evaluaciju." -eyebrow = "Samo Enterprise" -talkToSales = "Razgovarajte s prodajom" -title = "Nabavni tijek otvara se uz Enterprise" - [portal.procurement.milestone] -accept = "Prihvati i nastavi" -description = "Preuzmite PDF kako biste ga podijelili s timom, vratite se prihvatiti kada budete spremni ili unesite promjene." -download = "Preuzmi PDF" downloadError = "Još nije moguće preuzeti PDF ponude — pokušajte ponovno za trenutak." -edit = "Uredi ponudu" -eyebrow = "Ponuda {{number}}" -perYear = " / god." -preparedFor = "Pripremljeno za {{company}}" -tcv = "{{value}} ukupna vrijednost ugovora" -title = "Vaša je ponuda spremna" - -[portal.procurement.modal] -cancel = "Odustani" -chooseFile = "Odaberi datoteku" -close = "Zatvori" -downloadBody = "Preuzimanje će uskoro započeti." -downloadCta = "Preuzmi" -downloadTitle = "Preuzmi" -noFile = "Nije odabrana datoteka" -payBody = "Platite svoj obvezani ugovor karticom ili bankovnim prijenosom putem Stripe. Vaš se radni prostor postavlja čim se plaćanje potvrdi." -payCta = "Nastavi na Stripe" -payTitle = "Potvrdi plaćanje" -requestBodyFree = "Generiramo ovo na zahtjev. Potvrdite i vaš solutions engineer uskoro će ga poslati." -requestBodyPaid = "Ovo je plaćeni dodatak. Potvrdite i vaš solutions engineer odredit će opseg i poslati dokumentaciju." -requestCta = "Zatraži" -requestTitle = "Zatraži ovaj dokument" -signBody = "Otvara Stirling Enterprise Agreement za e-potpis: jedan potpis pokriva MSA, narudžbenicu, EULA i DPA. Mi automatski supotpisujemo i prelazite na plaćanje." -signCta = "Otvori za potpis" -signTitle = "Pregledajte i potpišite svoj ugovor" -uploadBody = "Pošaljite nam svoju narudžbenicu i fakturirat ćemo prema vašim uvjetima. Povucite PDF ili odaberite datoteku u nastavku." -uploadCta = "Prenesi narudžbenicu" -uploadTitle = "Prenesite svoju narudžbenicu" [portal.procurement.payment] description = "Vaša je ponuda prihvaćena i licencija je već aktivna — vaš tim može odmah početi. Platite prvi račun kada budete spremni; možete ga platiti ili preuzeti ovdje, bez potrebe za emailom." +downloadAgreement = "Preuzmi potpisani ugovor" downloadInvoice = "Preuzmi račun" -simulate = "Simuliraj primljeno plaćanje (demo)" +eyebrow = "Plaćanje" title = "Pretplata je stvorena" viewInvoice = "Prikaži i plati račun" +[portal.procurement.review] +acceptCta = "Prihvati ponudu" +annual = "Godišnja naknada (1. godina)" +downloadCta = "Preuzmi ponudu" +poNumber = "Narudžbenica: {{po}}" +renewal = "Obnavlja se po {{amount}}/godina nakon razdoblja (+{{pct}}% CPI)" +tcv = "{{years}}-godišnje razdoblje, plaćeno u cijelosti unaprijed: {{tcv}}" +validUntil = "Vrijedi do {{date}}" + [portal.procurement.schedule] fallback = "Nije moguće učitati planer." fallbackLink = "Otvori zakazivanje u novoj kartici" +loading = "Učitavanje planera..." subtitle = "Vaš solutions engineer provest će vaš tim kroz uvođenje. Odaberite termin koji vam odgovara." title = "Zakaži poziv" -[portal.procurement.status] -action = "Potrebna radnja" -available = "Dostupno" -complete = "Dovršeno" -pending = "Na čekanju" -request = "Na zahtjev" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Zapečaćeno, bez izlazne veze" +back = "Natrag" +businessName = "Naziv tvrtke" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Upravljano, najbrži početak" +continue = "Nastavi" +deployment = "Gdje ga treba implementirati?" +eula = "Prihvaćam Stirling EULA i komercijalne uvjete." +fullName = "Puno ime" +fullNamePlaceholder = "Vaše ime" +invites = "Pozovite članove tima · neobavezno" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Zakaži poziv" +seats = "Osobe koje testiraju" +seatsPlaceholder = "npr. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker ili Kubernetes" +start = "Pokreni probno razdoblje" +stepOf = "Korak {{n}} od {{total}}" +subtitle = "Besplatno 14 dana, kartica nije potrebna." +subtitleDetails = "Nekoliko detalja za vašu ponudu i ugovor." +talkFirst = "Želite prvo razgovarati?" +title = "Postavite probno razdoblje" +viewEula = "Pročitaj" +workEmail = "Poslovni email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Produljenjem se dodaje 7 dana i obavještava vaš solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Dosegnut maksimum" subtitle = "Vaše besplatno probno razdoblje traje do {{date}}. Kartica nije potrebna." title = "Enterprise probno razdoblje" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Cijene za obvezani volumen, SSO + SCIM + RBAC na razini organizacije, 90-dnevni nepromjenjivi audit i namjenski SE." -homeCta = "Započni probno razdoblje →" -homeHeadline = "Obradite milijune PDF-ova." - -[portal.recentActivity] -title = "Nedavna aktivnost" -viewAll = "Prikaži sve" - -[portal.recentActivity.empty] -description = "Pokretanja pipelinea, implementacije i događaji agenata pojavit će se ovdje." -title = "Ovdje još nema ničega" - -[portal.search] -ariaLabel = "Pretraživanje" -placeholder = "Pretražite Stirling — endpoints, pipelines, dokumente…" - -[portal.search.empty] -noActionsDescription = "Brze radnje pojavit će se ovdje kada budu dostupne." -noActionsTitle = "Nema brzih radnji" -noMatches = "Nema podudaranja za \"{{query}}\"" -noMatchesDescription = "Pokušajte s drugom ključnom riječi ili pregledajte katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Povezivanje računa" -[portal.shell.header] -accountFallback = "Račun" -accountMenu = "Izbornik računa" -darkMode = "Tamni način" -lightMode = "Svijetli način" -search = "Pretraživanje" -searchPlaceholder = "Pretraži…" -signOut = "Odjavi se" -switchToDark = "Prebaci na tamnu temu" -switchToLight = "Prebaci na svijetlu temu" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumenata" linkAccount = "Poveži Stirling račun" -planEditor = "Editor plan" -planEnterprise = "Enterprise plan" -planProcessor = "Processor plan" primaryNav = "Primarna navigacija" switchApp = "Promijeni aplikaciju" +[portal.shell.topbar] +closeNav = "Zatvori navigaciju" +openNav = "Otvori navigaciju" +search = "Pretraži" + [portal.sources] subtitle = "Ponovno upotrebljive ulazne veze koje dovode dokumente u Stirling. Konfigurirajte vezu jednom, a zatim je referencirajte iz bilo kojeg broja pravila. Kliknite redak za njegovu konfiguraciju i pravila koja ga koriste." title = "Izvori" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Poveži izvor" +[portal.sources.builder] +back = "Natrag na izvore" +backToTypes = "Sve vrste izvora" +cancel = "Odustani" +chooseHint = "Odaberite odakle dokumenti dolaze. Zasivljeni konektori su u pripremi." +comingSoon = "Uskoro" +comingSoonHeading = "Uskoro" +create = "Izradi izvor" +createTitle = "Poveži izvor" +delete = "Izbriši" +editTitle = "Uredi izvor" +enabled = "Omogućeno" +save = "Spremi izmjene" + +[portal.sources.builder.folderAccess] +description = "Automatizacije mapa mogu koristiti samo mape koje je administrator dopustio. Dodajte je u postavkama pristupa mapama, zatim pokušajte ponovno." +openSettings = "Postavke pristupa mapama" +title = "Ova mapa nije dopuštena" + [portal.sources.delete] body = "Izbrisati \"{{name}}\"? To se ne može poništiti. Pravila koja ga referenciraju morat će se ažurirati." cancel = "Odustani" confirm = "Izbriši" title = "Izbrisati izvor?" -[portal.sources.detail] -closeAriaLabel = "Zatvori detalje" -delete = "Izbriši izvor" -docs24h = "Zadnja 24 h" -docs30d = "Zadnjih 30 dana" -docsTotal = "Ukupno viđeno" -docsTrend = "Dokumenti u zadnjih 30 dana" -documents = "Dokumenti" -edit = "Uredi" -notReferenced = "Ne referencira ga nijedno pravilo, pa ga je sigurno izbrisati." -pause = "Pauziraj" -resume = "Nastavi" -subtitle = "{{type}} · {{status}}" -usedBy = "Koristi se u" - -[portal.sources.empty] -description = "Povežite mapu (a uskoro i cloud storage) kako bi vaša pravila imala odakle povlačiti dokumente." -title = "Još nema povezanih izvora" - [portal.sources.kpi] inUse = "U upotrebi" total = "Veze" unused = "Neiskorišteno" +[portal.sources.networkFields.connection] +helperText = "Spremljena veza s adresom poslužitelja i vjerodajnicama. Ponovno je koristi svaki izvor koji se na nju poziva." +label = "Veza" + +[portal.sources.networkFields.directory] +helperText = "Mapa na poslužitelju za provjeru, relativno na početnu mapu prijave ili korijen dijeljenja. Ostavite prazno za korijen." +label = "Mapa" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume uklanja svaku datoteku s poslužitelja nakon što je svako pravilo obradi." +label = "Način čitanja" + +[portal.sources.networkFields.mode.options] +consume = "Consume: obradi svaku datoteku jednom" +snapshot = "Snapshot: ponovno pročitaj mapu pri svakom pokretanju" + +[portal.sources.networkFields.recursive] +label = "Dubina mape" + +[portal.sources.networkFields.recursive.options] +all = "Uključi podmape" +top = "Samo najviša razina" + [portal.sources.status] active = "Aktivno" disabled = "Onemogućeno" unused = "Neiskorišteno" [portal.sources.table] +documents = "Dokumenti" source = "Izvor" status = "Status" +type = "Type" usedBy = "Pravila" +[portal.sources.types.box] +description = "Dohvatite dokumente iz Box mapa koje vaš tim već dijeli." +label = "Box" + +[portal.sources.types.dropbox] +description = "Dohvatite dokumente iz dijeljene Dropbox mape." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenti koje je vaš tim obradio u editoru, kroz pravila i AI pokretanja." label = "Editor" +noPolicies = "Još nema pravila pokrenutih iz editora." + +[portal.sources.types.email] +description = "Pretvorite privitke poslane u namjenski poštanski sandučić u dokumente." +label = "Email sandučić" [portal.sources.types.folder] description = "Prati direktorij na serveru za nove dokumente." @@ -7943,25 +8829,80 @@ label = "Dubina mape" all = "Uključi podmape" top = "Samo najviša razina" +[portal.sources.types.ftp] +description = "Provjerava mapu FTP ili FTPS poslužitelja za nove dokumente." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Dohvatite dokumente iz dijeljenih Google Drive mapa." +label = "Google Drive" + +[portal.sources.types.network] +description = "Pratite SMB mrežno dijeljenje za nove dokumente." +label = "Mrežni drive" + +[portal.sources.types.onedrive] +description = "Pratite OneDrive mape za nove dokumente." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Dohvatite dokumente iz Amazon S3 ili S3-kompatibilnog bucketa." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Spremljena veza koja sadrži bucket i vjerodajnice. Ponovno je koristi svaki izvor i izlaz tijeka obrade koji se na nju poziva." +label = "Veza" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume uklanja svaki objekt iz bucketa nakon što ga svako pravilo obradi." +label = "Način čitanja" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: obradi svaki objekt jednom" +snapshot = "Snapshot: ponovno pročitaj bucket pri svakom pokretanju" + +[portal.sources.types.s3.fields.prefix] +helperText = "Obrađuju se samo objekti čiji ključevi počinju ovim prefiksom." +label = "Prefiks ključa" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Provjerava SFTP drop mapu za nove dokumente." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Dohvatite dokumente iz SharePoint biblioteka dokumenata." +label = "SharePoint" + [portal.sources.types.unknown] label = "Izvor" +[portal.sources.types.webhook] +createNote = "URL za isporuku i tajna za potpisivanje generiraju se kada izradite ovaj webhook. Pošiljatelji šalju dokumente POST metodom na taj URL, a pravila koja se na njega pozivaju pokreću se po dolasku." +description = "Primajte dokumente putem potpisanog HTTP POST iz bilo kojeg vanjskog sustava." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL za isporuku" +secretNote = "Tajna za potpisivanje prikazuje se jednom, kada se webhook izradi. Ponovno izradite izvor da biste je rotirali." + +[portal.sources.types.webhook.reveal] +copy = "Kopiraj" +done = "Gotovo" +secret = "Tajna za potpisivanje" +secretHelp = "Potpišite neobrađeno tijelo svake isporuke ovim ključem (HMAC-SHA256) i pošaljite ga kao X-Stirling-Signature zaglavlje." +secretWarning = "Kopirajte tajnu za potpisivanje sada. Radi vaše sigurnosti prikazuje se samo jednom i ne može se dohvatiti kasnije." +title = "Webhook izrađen" +url = "URL za isporuku" +usage = "Pošaljite svaki dokument POST metodom kao neobrađeno tijelo zahtjeva na URL za isporuku s binarnom vrstom sadržaja (application/pdf ili application/octet-stream). Pravila koja se na njega pozivaju automatski se pokreću po dolasku." + [portal.sources.wizard] -back = "Natrag" -cancel = "Odustani" -continue = "Nastavi" -editTitle = "Uredi izvor" name = "Naziv" namePlaceholder = "npr. Prijem zahtjeva" -save = "Spremi promjene" -subtitle = "Korak {{current}} od {{total}} · {{label}}" -title = "Poveži izvor" type = "Vrsta" -[portal.sources.wizard.steps] -chooseType = "Odaberi vrstu" -configure = "Konfiguriraj" -review = "Pregledaj i poveži" +[portal.stepModal] +close = "Zatvori" [portal.usage] managePayment = "Upravljaj plaćanjem" @@ -8021,16 +8962,9 @@ summary = "Vlasnik je tima — upravlja resursima njegovih članova i dijeljenim 2 = "Pristup portalu putem zadane politike" 3 = "Sve što može Member" -[portal.welcome] -ariaLabel = "Dobro došli u Stirling PDF" -badge = "Open-source" -installEditor = "Instaliraj Editor" -inviteTeammates = "Pozovi članove tima" -openInBrowser = "Otvori u pregledniku" -perks = "Zauvijek besplatno · Self-hostable" -subtitle = "Najsigurniji PDF Editor na svijetu besplatan je za timove svih veličina. Uključuje 60+ PDF operacija i SSO." -title = "Dobro došli u" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Neograničeno" [printFile] title = "Ispis datoteke" @@ -8613,6 +9547,14 @@ text = "Zakrećite PDF stranice u smjeru kazaljke na satu ili suprotno u koracim [rotate.tooltip.header] title = "Pregled postavki zakretanja" +[ruler] +clearAll = "Očisti sve" +hideAllLabels = "Sakrij sve oznake" +hideSmallLabels = "Sakrij male oznake" +physicalValues = "Fizički" +scaled = "Skalirano" +showAllLabels = "Prikaži sve oznake" + [sanitize] desc = "Uklonite potencijalno štetne elemente iz PDF datoteka." sanitizationResults = "Rezultati sanitizacije" @@ -8654,31 +9596,45 @@ label = "Ukloni XMP metapodatke" [sanitize.steps] settings = "Postavke" -[sanitizePdf] -tags = "čisto, sigurno, sigurno, uklanjanje prijetnji" - [sanitizePDF] title = "Sanirajte PDF" +[sanitizePdf] +tags = "čisto, sigurno, sigurno, uklanjanje prijetnji" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Podesite veličinu stranice" +[scaleSettings] +activeScale = "Aktivno mjerilo" +apply = "Primijeni mjerilo" +applyCalibration = "Primijeni kalibraciju" +calculatedScale = "Izračunato mjerilo" +calibrate = "Kalibriraj" +calibrating = "Kalibriranje" +calibrationDistanceRequired = "Unesite stvarnu udaljenost veću od nule" +calibrationInvalid = "Nije moguće izračunati mjerilo iz ovog mjerenja" +calibrationPaperDistance = "Izmjerena udaljenost na stranici: {{distance}}" +calibrationTitle = "Kalibriraj mjerilo" +calibrationTooltip = "Izmjerite poznatu udaljenost za automatski izračun mjerila" +customScale = "Prilagođeno mjerilo" +noneSet = "Nije postavljeno prilagođeno mjerilo" +presets = "Unaprijed postavljena mjerila" +ratio = "Omjer mjerila" +ratioHelp = "Omjer: 1 jedinica stranice = X stvarnih jedinica" +ratioPlaceholder = "npr. 100" +ratioPositive = "Omjer mora biti veći od nule" +ratioRequired = "Omjer je obavezan" +realDistance = "Stvarna udaljenost" +realDistancePlaceholder = "npr. 5" +reset = "Vrati na zadane vrijednosti" +unit = "Jedinica" +unitInvalid = "Nevažeća jedinica: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Kutni prag:" -10 = "Postavlja veličinu obruba koji se dodaje i uklanja kako bi se spriječili bijeli obrubi u ispisu (zadano: 1)." -2 = "Postavlja minimalni apsolutni kut potreban za rotiranje slike (zadano: 10)." -3 = "Tolerancija:" -4 = "Određuje raspon varijacije boje oko procijenjene boje pozadine (zadano: 30)." -5 = "Minimalna površina:" -6 = "Postavlja minimalni prag površine za fotografiju (zadano: 10000)." -7 = "Minimalna konturna površina:" -8 = "Postavlja minimalni prag površine konture za fotografiju" -9 = "Veličina obruba:" - [scannerImageSplit] submit = "Izdvoji skenirane slike" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Što ovo radi" whatThisDoesDesc = "Automatski pronalazi i izdvaja svaku fotografiju sa skenirane stranice ili složene slike—bez ručnog izrezivanja." whenToUse = "Kada koristiti" +[ScannerImageSplit.selectText] +1 = "Kutni prag:" +10 = "Postavlja veličinu obruba koji se dodaje i uklanja kako bi se spriječili bijeli obrubi u ispisu (zadano: 1)." +2 = "Postavlja minimalni apsolutni kut potreban za rotiranje slike (zadano: 10)." +3 = "Tolerancija:" +4 = "Određuje raspon varijacije boje oko procijenjene boje pozadine (zadano: 30)." +5 = "Minimalna površina:" +6 = "Postavlja minimalni prag površine za fotografiju (zadano: 10000)." +7 = "Minimalna konturna površina:" +8 = "Postavlja minimalni prag površine konture za fotografiju" +9 = "Veličina obruba:" + [search] noResults = "Nema rezultata" placeholder = "Unesite pojam za pretraživanje..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Vaš Stirling-PDF poslužitelj je izvan mreže i \"{{ [session] expired = "Vaš sesija je istekla. Molim vas da osvježite stranicu i pokušate ponovno." -[sessionManagement.tooltip] -header = "Upravljanje sesijama potpisivanja" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Novi sudionici dodaju se na kraj redoslijeda potpisivanja" -bullet2 = "Nije moguće dodavati sudionike nakon dovršetka sesije" -bullet3 = "Svaki sudionik prima obavijest kada dođe njegov red" -description = "Možete dodavati više sudionika u aktivnu sesiju bilo kada prije dovršavanja." -title = "Dodavanje sudionika" - -[sessionManagement.tooltip.finalization] -bullet1 = "Potpuno dovršavanje: svi sudionici su potpisali" -bullet2 = "Djelomično dovršavanje: neki sudionici još nisu potpisali" -bullet3 = "Nepotpisani sudionici bit će isključeni iz konačnog dokumenta" -bullet4 = "Nakon dovršavanja možete učitati potpisani PDF u aktivne datoteke" -description = "Dovršavanje objedinjuje sve potpise u jedan potpisani PDF. Ovu radnju nije moguće poništiti." -title = "Dovršavanje sesije" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Nije moguće ukloniti sudionike koji su već potpisali" -bullet2 = "Uklonjeni sudionici više ne primaju obavijesti" -bullet3 = "Redoslijed potpisivanja automatski se prilagođava" -description = "Sudionici se mogu ukloniti iz sesija prije nego potpišu." -title = "Uklanjanje sudionika" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Svaki se potpis primjenjuje sekvencijalno na PDF" -bullet2 = "Kasniji potpisnici mogu vidjeti ranije potpise" -bullet3 = "Ključno za tijekove odobravanja i pravne lance čuvanja" -description = "Redoslijed koji odredite pri stvaranju sesije definira tko potpisuje prvi." -title = "Redoslijed potpisa" - [settings] close = "Zatvori" +[settings.ai] +documents = "Dokumenti i RAG" +general = "Općenito" +limits = "Ograničenja i performanse" +models = "Modeli i pružatelji usluga" +title = "AI" + [settings.configuration] advanced = "Napredno" database = "Baza podataka" endpoints = "Endpointi" features = "Značajke" +folderAccess = "Pristup mapama" mcp = "MCP Server" storageSharing = "Pohrana i dijeljenje datoteka" systemSettings = "Postavke sustava" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Zadani PDF uređivač" defaultPdfEditorActive = "Stirling PDF je vaš zadani PDF uređivač" defaultPdfEditorChecking = "Provjera..." defaultPdfEditorInactive = "Druga je aplikacija postavljena kao zadana" +defaultPdfEditorRemind = "Podsjeti me da postavim kao zadano" +defaultPdfEditorRemindDescription = "Prikaži natpis kada Stirling PDF nije vaša zadana PDF aplikacija." defaultPdfEditorSet = "Već je zadano" defaultStartupView = "Zadani prikaz pri pokretanju" defaultStartupViewDescription = "Odaberite koja je kartica aktivna u lijevom stupcu pri pokretanju aplikacije" @@ -8817,9 +9763,18 @@ languageDescription = "Odaberite jezik prikaza" logout = "Odjava" setAsDefault = "Postavi kao zadano" theme = "Tema" +themeAccent = "Boja naglaska" +themeAccentCustom = "Prilagođena boja" +themeAccentCustomHint = "Prilagođeno (preporučene zadane vrijednosti)" +themeAccentDark = "Boja naglaska za tamni način rada" +themeAccentDefault = "Zadano" +themeAccentDefaultHint = "Zadana tema (preporučeno)" +themeAccentDescription = "Gumbi, poveznice i isticanja koriste je, a ona suptilno nijansira aplikaciju. Svijetli i tamni način rada imaju svaki svoju. Sustav koristi onu koja je aktivna." +themeAccentLight = "Boja naglaska za svijetli način rada" themeDark = "Tamna" themeDescription = "Prebacite između svijetlog i tamnog načina" themeLight = "Svijetla" +themeReset = "Vrati temu na zadano" themeSystem = "System" title = "Općenito" user = "Korisnik" @@ -8833,6 +9788,12 @@ intro = "Omogućite autentikaciju korisnika, upravljanje timovima i značajke ra learnMore = "Saznajte više u dokumentaciji" title = "Za sistemske administratore" +[settings.general.loginLanding] +description = "Odaberite koja se aplikacija otvara kada se prijavite u Stirling Cloud." +editor = "Uređivač" +processor = "Procesor" +title = "Nakon prijave" + [settings.general.mode] fullscreen = "Cijeli zaslon" sidebar = "Bočna traka" @@ -9013,9 +9974,6 @@ title = "Pravila i privatnost" [settings.preferences] title = "Preferencije" -[settings.search] -placeholder = "Pretraži stranice postavki..." - [settings.security] description = "Ažurirajte svoju lozinku kako biste osigurali račun." title = "Sigurnost" @@ -9041,6 +9999,11 @@ security = "Sigurnost" telegram = "Telegram" title = "Sigurnost i autentikacija" +[settings.signOut] +confirm = "Jeste li sigurni da se želite odjaviti?" +submit = "Odjavi se" +title = "Odjava" + [settings.team] title = "Tim" @@ -9156,6 +10119,7 @@ last = "Zadnja stranica" maintainRatio = "Uključi/isključi održavanje omjera" next = "Sljedeća stranica" noSavedSigs = "Nema sacuvanih potpisa pronađenih" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Osobni potpisi" previous = "Prethodna stranica" redo = "Ponovi" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Uklanjanje pozadine nije uspjelo" hint = "Učitajte PNG ili JPG sliku svojega potpisa" label = "Učitaj sliku potpisa" placeholder = "Odaberite slikovnu datoteku" +previewAlt = "Current image signature" processing = "Obrada slike..." removeBackground = "Ukloni bijelu pozadinu (učini prozirnom)" @@ -9207,6 +10172,17 @@ saved = "Odaberite spremljeni potpis iznad, zatim kliknite bilo gdje na PDF da b text = "Nakon što iznad unesete svoje ime, kliknite bilo gdje na PDF-u da postavite svoj potpis." title = "Kako dodati potpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Pomakni potpis" pause = "Pauziraj postavljanje" @@ -9653,10 +10629,30 @@ title = "Prenesi na poslužitelj" updateButton = "Ažuriraj na poslužitelju" uploadButton = "Prenesi na poslužitelj" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kliknite ovdje" +fallback = "Ako se nije automatski otvorilo," +opening = "Otvaranje Swagger UI u novoj kartici..." tags = "api,documentation,swagger,endpoints,development" title = "API dokumentacija" @@ -9764,12 +10760,39 @@ selectFilesHint = "Odaberite datoteke u Aktivnim datotekama za pokretanje ovog a singleFileScope = "Primjenjuje se samo na: {{fileName}}" viewerMode = "Prebacite se u uređivač datoteka da biste odabrali više datoteka." +[toolFormat] +ANY = "bilo koja datoteka" +CBR = "arhiva stripa (CBR)" +CBZ = "arhiva stripa (CBZ)" +CSV = "CSV datoteka" +EBOOK = "e-knjiga" +EMAIL = "poruka e-pošte" +EXCEL = "proračunska tablica" +HTML = "web-stranica" +IMAGE = "slike" +JAVASCRIPT = "JavaScript datoteka" +JSON = "JSON datoteka" +MARKDOWN = "Markdown datoteka" +NONE = "nema datoteke" +PCL = "PCL datoteka za ispis" +PDF = "PDF" +PDF_ENCRYPTED = "PDF zaštićen lozinkom" +POSTSCRIPT = "PostScript datoteka" +PPT = "prezentacija" +TEXT = "tekstna datoteka" +VIDEO = "video" +WORD = "Word dokument" +XML = "XML datoteka" +XPS = "XPS dokument" +ZIP = "ZIP arhiva" + [toolPanel] alpha = "Alfa" backToAllTools = "Natrag na sve alate" collapse = "Sažmi panel" expand = "Proširi panel" goBack = "Vrati se" +pdfTools = "PDF alati" placeholder = "Odaberite alat za početak" premiumFeature = "Premium značajka:" toolsHeader = "Alati" @@ -9806,11 +10829,6 @@ noToolsFound = "Nije pronađen nijedan alat" quickAccess = "BRZI PRISTUP" searchPlaceholder = "Pretraži alate..." -[toolPicker.categories] -advancedTools = "Napredni alati" -recommendedTools = "Preporučeni alati" -standardTools = "Standardni alati" - [toolPicker.subcategories] advancedFormatting = "Napredno formatiranje" automation = "Automatizacija" @@ -9824,6 +10842,9 @@ removal = "Uklanjanje" signing = "Potpisivanje" verification = "Provjera" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nije pronađen nijedan alat" noTools = "Nema dostupnih alata" @@ -9950,28 +10971,53 @@ title = "Korisnici" you = "(vi)" [users.action] +cancelInvite = "Otkaži pozivnicu" deleteTeam = "Izbriši tim" disableMfa = "Resetiraj MFA" move = "Premjesti u tim" reinstate = "Ponovno uključi" remove = "Ukloni iz organizacije" +removeTeam = "Ukloni iz tima" rename = "Preimenuj tim" resetPw = "Resetiraj lozinku" suspend = "Suspendiraj" unlock = "Otključaj račun" +[users.activity] +daysAgo = "Prije {{count}} d" +hoursAgo = "Prije {{count}} h" +justNow = "Upravo sada" +minutesAgo = "Prije {{count}} min" +monthsAgo_one = "Prije {{count}} mjesec" +monthsAgo_other = "Prije {{count}} mjeseci" +never = "Nikad" +weeksAgo_one = "Prije {{count}} tjedan" +weeksAgo_other = "Prije {{count}} tjedana" +yearsAgo_one = "Prije {{count}} godinu" +yearsAgo_other = "Prije {{count}} godina" + [users.cap] addProcessor = "+ Processor" approver = "Odobrava pravilo" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Otkazati pozivnicu za {{email}}? Neće se moći pridružiti putem trenutačne poveznice." +cancelInviteTitle = "Otkaži pozivnicu" deleteTeamBody = "Izbrisati tim {{name}}? Tim prvo mora biti prazan - premjestite njegove članove u drugi tim i ne smije još uvijek posjedovati konfiguracije integracija." deleteTeamTitle = "Izbriši tim" disableMfaBody = "Ukloniti MFA registraciju za {{name}}? Ponovno će je postaviti pri sljedećoj prijavi ako je potrebno." disableMfaTitle = "Resetiraj MFA" removeBody = "Trajno ukloniti {{name}} iz organizacije? To se ne može poništiti." +removeTeamBody = "Ukloniti {{name}} iz tima? Zadržat će svoj račun, ali će izgubiti pristup resursima ovog tima." removeTitle = "Ukloni člana" [users.empty] @@ -9982,10 +11028,8 @@ title = "Još nema članova" addToTeam = "Dodaj u tim" guestCount = "{{count}} gost" guests = "Gosti" -guestsDesc = "Vanjski suradnici, ograničeni na ono što ste podijelili. Samo Editor." ledBy = "vodi {{owner}}" org = "Organizacija" -orgDesc = "Vlasnici s ovlastima na razini organizacije i odobravanjem pravila" owners = "{{count}} vlasnik" team = "{{name}} tim" teamMeta = "{{count}} osoba" @@ -10022,6 +11066,18 @@ username = "Korisničko ime" usernameError = "Korisničko ime mora imati najmanje 3 znaka" usernamePlaceholder = "jsmith" +[users.invites] +by = "Poziv poslao/la {{who}}" +cancel = "Otkaži" +expiresInDays_one = "Istječe za {{count}} dan" +expiresInDays_other = "Istječe za {{count}} dana" +expiresToday = "Istječe danas" +title = "Pozivnice na čekanju" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Nešto je pošlo po zlu pri povezivanju s pozadinskim sustavom ili nemate pristup. Pokušajte ponovno." title = "Nije moguće učitati članove" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Pretraži priloge" title = "Prilozi" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Naslov knjižne oznake" +bookmarkTitleRequired = "Naslov knjižne oznake je obavezan" closeSidebar = "Zatvori bočnu traku knjižnih oznaka" collapseAll = "Sažmi sve knjižne oznake" +empty = "No bookmarks in this document" expandAll = "Proširi sve knjižne oznake" +searchPlaceholder = "Pretraži knjižne oznake" +title = "Bookmarks" [viewer.comments] addComment = "Dodaj komentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Pronađi u dokumentu" moreActions = "Više radnji" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Stranica {{page}}" placingHint = "Kliknite stranicu za postavljanje… (odustani)" removeCommentOnly = "Ukloni samo komentar" saveReply = "Spremi odgovor" +searchPlaceholder = "Search comments" title = "Komentari" typeComment = "Komentar" typeInsertText = "Umetni tekst" @@ -10262,6 +11325,7 @@ unsavedDesc = "Imate nespremljene promjene" closeSidebar = "Zatvori bočnu traku slojeva" hideAll = "Sakrij sve slojeve" showAll = "Prikaži sve slojeve" +title = "Layers" [viewer.link] delete = "Izbriši poveznicu" @@ -10605,30 +11669,6 @@ title = "Tekst vodenog žiga" image = "Slika" text = "Tekst" -[wetSignature.tooltip] -header = "Metode izrade potpisa" - -[wetSignature.tooltip.draw] -bullet1 = "Prilagodite boju i debljinu olovke" -bullet2 = "Brišite i ponovno crtajte dok ne budete zadovoljni" -bullet3 = "Radi na uređajima s dodirnim zaslonom (tableti, telefoni)" -description = "Izradite rukom pisani potpis pomoću miša ili dodirnog zaslona. Najbolje za osobne, autentične potpise." -title = "Nacrtaj potpis" - -[wetSignature.tooltip.type] -bullet1 = "Odaberite između više fontova" -bullet2 = "Prilagodite veličinu i boju teksta" -bullet3 = "Idealno za standardizirane potpise" -description = "Generirajte potpis iz upisanog teksta. Brzo i dosljedno, pogodno za poslovne dokumente." -title = "Upiši potpis" - -[wetSignature.tooltip.upload] -bullet1 = "Podržava PNG, JPG i druge slikovne formate" -bullet2 = "Za najbolje rezultate preporučuju se prozirne pozadine" -bullet3 = "Slika će se promijeniti veličinom kako bi odgovarala području potpisa" -description = "Učitajte unaprijed izrađenu sliku potpisa. Idealno ako imate skenirani potpis ili logotip tvrtke." -title = "Učitaj sliku potpisa" - [workbenchBar] activeFiles = "Aktivne datoteke" annotations = "Anotacije" @@ -10642,6 +11682,7 @@ exitRedaction = "Izađi iz načina zatamnjivanja" exportAll = "Izvezi PDF" exportSelected = "Izvezi odabrane stranice" formFill = "Ispuni obrazac" +hideToolbar = "Hide toolbar" multiTool = "Višenamjenski alat" panMode = "Način pomicanja" print = "Ispis PDF-a" @@ -10652,6 +11693,7 @@ redact = "Zatamni" rotateLeft = "Rotiraj ulijevo" rotateRight = "Rotiraj udesno" ruler = "Ravnalo / Mjerenje" +rulerSettings = "Postavke mjerila" save = "Spremi" saveAll = "Spremi sve" saveAs = "Spremi kao" @@ -10661,6 +11703,7 @@ selectAll = "Odaberi sve" selectByNumber = "Odaberi po brojevima stranica" selectLanguage = "Odaberi jezik" share = "Dijeli" +showToolbar = "Show toolbar" toggleAnnotations = "Uključi/isključi vidljivost anotacija" toggleAttachments = "Uključi/isključi privitke" toggleBookmarks = "Uključi/isključi oznake" diff --git a/frontend/editor/public/locales/hu-HU/translation.toml b/frontend/editor/public/locales/hu-HU/translation.toml index 26e7835c1b..cebcbeb964 100644 --- a/frontend/editor/public/locales/hu-HU/translation.toml +++ b/frontend/editor/public/locales/hu-HU/translation.toml @@ -1,7 +1,6 @@ alphabet = "ABC" apply = "Alkalmaz" applyAndContinue = "Mentés és kilépés" -areYouSure = "Biztosan elhagyja az oldalt?" back = "Vissza" black = "Fekete" blue = "Kék" @@ -46,10 +45,8 @@ filesSelected = "fájl kiválasztva" font = "Betűtípus" fontSizeTooltip = "A lapszám szövegének mérete pontban. A nagyobb szám nagyobb szöveget eredményez." fontTypeTooltip = "A lapszámok betűcsaládja. Válassza a dokumentum stílusához illően." -genericSubmit = "Küldés" green = "Zöld" help = "Súgó" -imgPrompt = "Kép kiválasztása" incorrectPasswordMessage = "A jelenlegi jelszó helytelen." info = "Információ" invalidUndoData = "Nem lehet visszavonni: érvénytelen műveleti adatok" @@ -62,7 +59,6 @@ loginAgreementTitle = "Bejelentkezési megállapodás" logOut = "Kijelentkezés" marginTooltip = "A lapszám és az oldal széle közötti távolság." moreOptions = "További beállítások" -multiPdfPrompt = "PDF-fájlok kiválasztása (2+)" never = "Soha" no = "Nem" noFileSelected = "Nincs fájl kiválasztva. Kérjük, töltsön fel egyet." @@ -80,7 +76,6 @@ pageOrderPrompt = "Egyedi oldalsorrend (Adja meg az oldalszámokat vesszővel el pages = "Oldal" pageSelectionPrompt = "Egyedi oldalválasztás (Adja meg az oldalszámokat vesszővel elválasztva, pl. 1,5,6 vagy használjon függvényeket, pl. 2n+1):" password = "Jelszó" -pdfPrompt = "PDF-fájl kiválasztása" pin = "Fájl rögzítése (aktív marad az eszköz futtatása után)" pro = "Pro" processingComplete = "A fájl elkészült." @@ -110,6 +105,7 @@ undoSuccess = "A művelet sikeresen visszavonva" unknown = "Ismeretlen" unpin = "Rögzítés feloldása (csere az eszköz futtatása után)" unsavedChanges = "Mentetlen változtatásai vannak a PDF-en." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Mentetlen változtatások" unsupported = "Nem támogatott" username = "Felhasználónév" @@ -356,6 +352,7 @@ text = "Ezek a jogosultságok szabályozzák, mit tehetnek a felhasználók a PD title = "Jogosultságok módosítása" [addStamp] +preview = "Bélyeg előnézete" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Kézi letöltések: kattintson a hivatkozásokra, és helyezze a noLanguages = "Nem találhatók tessdata nyelvek a beállított könyvtárban." permissionNotice = "A tessdata elérési út nem írható. A letöltések a böngészőben nyílnak meg; mentse a .traineddata fájlokat kézzel a tessdata mappába." +[admin.settings.ai.documents] +description = "Konfigurálja a dokumentumokkal kapcsolatos kérdések megválaszolásához használt beágyazási modellt és visszakeresési beállításokat. Mentéskor az AI-motorra lesz alkalmazva." +title = "Dokumentumok és RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Hagyja üresen a motor saját környezeti hitelesítő adatainak használatához. Saját üzemeltetésű, egymotoros telepítésekre vonatkozik." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Mentve – hagyja üresen az aktuális kulcs megtartásához" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Az OpenAI-kompatibilis / Ollama embeddings végpont alap URL-je, pl. http://ollama:11434/v1. Megbízható belső végpontra kell mutatnia (SSRF-érzékeny)." +label = "Embedding alap URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding modell neve. Szabad szöveg; a javaslatok csak támpontok." +label = "Embedding modell" + +[admin.settings.ai.documents.embeddingProvider] +description = "A dokumentumszöveg vektoros beágyazásokká alakításához használt szolgáltató." +label = "Embedding szolgáltató" + +[admin.settings.ai.documents.maxSearches] +description = "A visszakeresési keresések maximális száma, amelyet az ügynök kérésenként futtathat." +label = "Keresések maximuma" + +[admin.settings.ai.documents.reindexNote] +body = "Az embedding modell módosítása azonnal életbe lép, de az előző modellel indexelt dokumentumokat újra kell indexelni, hogy a keresés helyes eredményeket adjon." +title = "Újraindexelés szükséges" + +[admin.settings.ai.documents.saved] +reindexNote = "Ha módosította az embedding modellt, indexelje újra a meglévő dokumentumokat, hogy a keresés az új modellt használja." + +[admin.settings.ai.documents.topK] +description = "Keresésenként visszakeresett, legrelevánsabb részletek száma." +label = "Top K" + +[admin.settings.ai.general] +description = "Csatlakoztassa a Stirlinget a Python AI-motorhoz, és válassza ki, mely AI-képességek legyenek elérhetők. A módosítások újraindításkor lépnek életbe." +title = "AI-motor" + +[admin.settings.ai.general.capabilities] +description = "Kapcsolja be vagy ki az egyes AI-funkciókat. A letiltott funkciók el vannak rejtve az alkalmazásban." +title = "Képességek" + +[admin.settings.ai.general.enabled] +description = "Főkapcsoló. Kikapcsolt állapotban nem érhetők el AI-eszközök, ügynökök vagy motorhívások." +label = "AI engedélyezése" + +[admin.settings.ai.general.features.chat] +description = "Beszélgetős asszisztens PDF-ekkel való munkához." +label = "Chat asszisztens" + +[admin.settings.ai.general.features.classify] +description = "Dokumentumok automatikus kategorizálása típus vagy tartalom alapján." +label = "Dokumentumosztályozás" + +[admin.settings.ai.general.features.createPdf] +description = "Új PDF generálása (pl. HTML-ből) AI-ügynök segítségével." +label = "PDF létrehozása promptból" + +[admin.settings.ai.general.features.documentQuestions] +description = "Kérdezzen, és kapjon válaszokat egy feltöltött dokumentum alapján." +label = "Dokumentumkérdések" + +[admin.settings.ai.general.features.mathAuditor] +description = "Dokumentumok ellenőrzése matematikai és numerikus hibák szempontjából." +label = "Matematikai ellenőr" + +[admin.settings.ai.general.features.pdfComment] +description = "AI által írt áttekintési megjegyzések és annotációk hozzáadása PDF-hez." +label = "PDF-megjegyzés ügynök" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Időtúllépés nagyobb ügynökműveletekhez, például dokumentumgeneráláshoz." +label = "Hosszú futású időtúllépés (másodperc)" + +[admin.settings.ai.general.note] +body = "Az AI-motor külön szolgáltatásként fut. A megosztott titka" +body2 = "konténerkörnyezeti változóval van beállítva. A szolgáltatói API key-k megadhatók ezeken az oldalakon, vagy átadhatók motor környezeti változóként; az itt megadott kulcsok mentéskor továbbításra kerülnek a motorhoz." +title = "Az AI-motorról" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Időtúllépés streamelt (tokenenkénti) chat válaszokhoz." +label = "Stream időtúllépés (másodperc)" + +[admin.settings.ai.general.test] +button = "Kapcsolat tesztelése" +failBody = "Az AI-motor nem válaszolt. Ellenőrizze az URL-t, hogy a motor konténere fut-e, és hogy az AI engedélyezve van-e (engedélyezés után újraindítás szükséges)." +failTitle = "Az AI-motor nem érhető el" +okBody = "Az AI-motor válaszolt az állapotellenőrzésre." +okTitle = "Az AI-motor elérhető" + +[admin.settings.ai.general.timeoutSeconds] +description = "Időtúllépés a motorhoz intézett szabványos AI-kérésekhez." +label = "Kérés időtúllépése (másodperc)" + +[admin.settings.ai.general.url] +description = "A Python AI-motor belső URL-je, pl. http://stirling-pdf-engine:5001." +label = "AI-motor URL" + +[admin.settings.ai.limits] +description = "Korlátok arra, mennyi munkát végezhetnek az AI-kérések, és hány futhat párhuzamosan. Mentéskor az AI-motorra lesz alkalmazva." +title = "Korlátok és teljesítmény" + +[admin.settings.ai.limits.maxCharacters] +description = "Védőkorlát: utasítsa el azokat az AI-kéréseket, amelyek kinyert szövege meghaladja ezt a hosszúságot." +label = "Karakterek maximuma kérésenként" + +[admin.settings.ai.limits.maxPages] +description = "Védőkorlát: utasítsa el azokat az AI-kéréseket, amelyek ennél több PDF-oldalt tartalmaznak." +label = "Oldalak maximuma kérésenként" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Az egész motorban egyszerre folyamatban lévő modellhívások maximális száma." +label = "Modell maximális párhuzamossága" + +[admin.settings.ai.models] +description = "Válassza ki az LLM-szolgáltatót és az AI-motor által használt okos/gyors modelleket. Mentéskor az AI-motorra lesz alkalmazva." +title = "Modellek és szolgáltatók" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Hagyja üresen a motor saját környezeti hitelesítő adatainak használatához. Saját üzemeltetésű, egymotoros telepítésekre vonatkozik." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Mentve – hagyja üresen az aktuális kulcs megtartásához" + +[admin.settings.ai.models.baseUrl] +description = "Az OpenAI-kompatibilis / Ollama végpont alap URL-je, pl. http://ollama:11434/v1." +label = "Szolgáltató alap URL-je" +warning = "Az alap URL-nek megbízható belső végpontra kell mutatnia. A motor szerveroldali kéréseket küld neki, ezért a nem megbízható érték SSRF-érzékeny." + +[admin.settings.ai.models.fastMaxTokens] +description = "A gyors modell kimeneti tokenjeinek maximális száma." +label = "Gyors modell tokenmaximuma" + +[admin.settings.ai.models.fastModel] +description = "Olcsóbb, gyorsabb modell könnyű feladatokhoz. Szabad szöveg; a javaslatok csak támpontok." +label = "Gyors modell" + +[admin.settings.ai.models.provider] +description = "Melyik LLM-szolgáltatóval kommunikáljon a motor." +label = "Szolgáltató" + +[admin.settings.ai.models.smartMaxTokens] +description = "Az okos modell kimeneti tokenjeinek maximális száma." +label = "Okos modell tokenmaximuma" + +[admin.settings.ai.models.smartModel] +description = "Nagy képességű modell összetett érveléshez. Szabad szöveg; a javaslatok csak támpontok." +label = "Okos modell" + +[admin.settings.ai.saved] +body = "A módosítások automatikusan továbbításra kerülnek az AI-motorhoz." +bodyNoPush = "A beállítások mentve. Akkor lépnek életbe, amikor az AI-motor legközelebb betölti a konfigurációját." +title = "AI-beállítások mentve" + [admin.settings.badge] clickToUpgrade = "Kattintson a csomag részleteihez" @@ -938,6 +1094,25 @@ label = "Újragenerálás indításkor" description = "A tanúsítvány érvényességének napokban megadott száma" label = "Tanúsítvány érvényessége (nap)" +[admin.settings.folderAccess] +description = "Könyvtárak, amelyekből a mappaforrások és mappakimenetek olvashatnak, illetve amelyekbe írhatnak. Ez biztonsági határ: az automatizálások soha nem mutathatnak a listán kívüli szerverútvonalra." +securityNote = "Hagyja üresen a mappaforrások és -kimenetek teljes letiltásához. A Stirling saját konfigurációs könyvtára mindig tiltott." +title = "Mappahozzáférés" + +[admin.settings.folderAccess.implied] +description = "Ezek a Stirling által kezelt könyvtárak mindig engedélyezettek, és itt nem módosíthatók." +serverStorage = "Szerver fájltárolója" +title = "Mindig engedélyezett" +watchedFolder = "Pipeline figyelt mappája" + +[admin.settings.folderAccess.roots] +add = "Hozzáadás" +empty = "Nincs engedélyezett mappa. A mappaforrások és -kimenetek jelenleg le vannak tiltva." +hint = "Adjon meg abszolút útvonalakat, például /data/inbox." +label = "Engedélyezett mappagyökerek" +placeholder = "/data/inbox" +remove = "Mappagyökér eltávolítása" + [admin.settings.general] description = "Rendszerszintű alkalmazásbeállítások konfigurálása, beleértve az arculatot és az alapértelmezett viselkedést." system = "Rendszer" @@ -1704,6 +1879,9 @@ width = "Szélesség" [app] description = "Az ingyenes Adobe Acrobat alternatíva (10M+ letöltés)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Átalakítás PDF/A-3b formátumba" convertToPdfA3bDescription = "Beágyazott mellékleteket tartalmazó archiválási PDF-et hoz létre" @@ -1842,6 +2020,15 @@ export = "Exportálás" accessDenied = "Hozzáférés megtagadva" insufficientPermissions = "Nincs jogosultsága a művelet végrehajtásához." +[auth.callback] +completing = "Hitelesítés befejezése" +invalidToken = "OAuth bejelentkezés sikertelen – érvénytelen token." +missingToken = "OAuth bejelentkezés sikertelen – nem érkezett token." +oauthFailed = "OAuth bejelentkezés sikertelen. Próbálja újra." +pleaseWait = "Kérjük, várjon, amíg befejezzük a bejelentkezést." +signedOut = "Kijelentkezett. Kérjük, jelentkezzen be újra." +windowMayClose = "A befejezés után bezárhatja ezt az ablakot." + [auth.displayName] guest = "Vendég" user = "Felhasználó" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Biztonságossá teszi a PDF-eket a potenciálisan rosszind description = "Ez az eszköz automatikusan átnevezi a PDF-fájlokat a tartalmuk alapján. Elemzi a dokumentumot, hogy a szövegből a legalkalmasabb címet találja meg." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatikus elforgatás" +tags = "automatikus elforgatás,tájolás javítása,fejjel lefelé,oldalra fordítva,kiegyenesítés,tájolás,tájolás felismerése,elforgatás javítása,automatikus tájolás,szkennelt,OSD" + +[autoRotate.confidenceThreshold] +title = "Megbízhatósági küszöb" + +[autoRotate.detectionMode] +auto = "Automatikus" +osd = "Csak OCR" +text = "Csak szöveg" +title = "Felismerési módszer" + +[autoRotate.error] +failed = "Hiba történt a PDF automatikus elforgatása közben." + +[autoRotate.inferUndetected] +title = "Alacsony megbízhatóságú oldalak elforgatása egyezéshez" + +[autoRotate.report] +applied = "{{degrees}}° óramutató járásával megegyezően" +confidenceValue = "Megbízhatóság {{value}}" +noChange = "Nincs változás" +pageLabel = "{{number}}. oldal" +summary = "{{total}} oldalból {{rotated}} elforgatva, {{unchanged}} változatlanul hagyva" +title = "Felismerési jelentés" + +[autoRotate.report.method] +inferred = "Egyeztetve" +none = "Kihagyva" +osd = "OCR" +text = "Szöveg" + +[autoRotate.report.note] +belowThreshold = "Küszöb alatt" +blankPage = "Az oldal üres" +inferredFromDocument = "Ugyanaz, mint a többi oldal" +noDominantDirection = "Vegyes szövegirányok" +osdFailed = "Nem található olvasható szöveg" +osdNoVerdict = "Nem állapítható meg, melyik irány a felfelé" +tesseractUnavailable = "OCR nincs telepítve" +tooFewGlyphs = "Túl kevés szöveg" + +[autoRotate.results] +title = "Automatikus elforgatás eredményei" + +[autoRotate.settings] +title = "Beállítások" + +[autoRotate.tooltip.confidenceThreshold] +text = "Mennyire kell biztosnak lennie az ellenőrzésnek, mielőtt egy oldal elforgatásra kerül. Csökkentse, hogy több oldal forduljon el, növelje, hogy több oldal maradjon érintetlen." +title = "Megbízhatósági küszöb" + +[autoRotate.tooltip.description] +text = "Meghatározza, melyik irányban kell állnia az egyes oldalaknak, és a megfelelő irányba fordítja őket. A már álló helyzetű oldalak változatlanok maradnak." + +[autoRotate.tooltip.detectionMode] +text = "Az Automatikus először az oldalon lévő szöveget olvassa, majd ha ez nem elég, képként vizsgálja az oldalt. A Csak szöveg kihagyja a képes ellenőrzést, ami a soha nem szkennelt dokumentumoknál a leggyorsabb. A Csak OCR minden oldalt képként kezel." +title = "Felismerési módszer" + +[autoRotate.tooltip.header] +title = "Automatikus elforgatás" + +[autoRotate.tooltip.inferUndetected] +text = "Az üres vagy majdnem üres oldalak önmagukban nem ellenőrizhetők. Ha minden olvasható oldal ugyanazt az elforgatást igényli, ezek az oldalak is hozzájuk igazodva fordulnak el." +title = "Alacsony megbízhatóságú oldalak elforgatása egyezéshez" + [autoSizeSplitPDF] tags = "pdf,felosztás,dokumentum,rendszerezés" @@ -2189,33 +2443,6 @@ keywords = "Kulcsszavak: odd, even" numbers = "Számok/tartományok: 5, 10-20" progressions = "Sorozatok: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tanúsítványtípusok" - -[certificateChoice.tooltip.organization] -bullet1 = "Rendszeradminisztrátorok kezelik" -bullet2 = "Jogosult felhasználók között megosztva" -bullet3 = "A vállalatot képviseli, nem egyéni felhasználót" -bullet4 = "Ajánlott: hivatalos dokumentumok, csapat-aláírások" -description = "A szervezet által biztosított megosztott tanúsítvány. Vállalati szintű aláírási jogosultsághoz használható." -title = "Szervezeti tanúsítvány" - -[certificateChoice.tooltip.personal] -bullet1 = "Első használatkor automatikusan generálódik" -bullet2 = "A felhasználói fiókjához kapcsolódik" -bullet3 = "Nem osztható meg más felhasználókkal" -bullet4 = "Ajánlott: személyes dokumentumok, egyéni felelősségvállalás" -description = "A felhasználói fiókjához egyedileg generált tanúsítvány. Egyéni aláírásokhoz megfelelő." -title = "Személyes tanúsítvány" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX fájlt és jelszót igényel" -bullet2 = "Külső hitelesítésszolgáltató bocsáthatja ki" -bullet3 = "Magasabb bizalmi szint jogi dokumentumokhoz" -bullet4 = "Ajánlott: jogilag kötelező erejű szerződések, külső hitelesítés" -description = "Használja saját PKCS#12 tanúsítványfájlját. Teljes kontrollt biztosít a tanúsítvány tulajdonságai felett." -title = "Egyéni P12 feltöltése" - [certSign] awaitingSignatures = "Aláírásokra vár" chooseCertificate = "Tanúsítványfájl kiválasztása" @@ -2542,34 +2769,6 @@ server = "Szerver" stepTitle = "Tanúsítvány forrása" upload = "Feltöltés" -[certSign.tooltip.header] -title = "Aláírások kezelése" - -[certSign.tooltip.overview] -bullet1 = "Meglévő aláírások és érvényességük ellenőrzése" -bullet2 = "Részletes információk megtekintése az aláírókról és tanúsítványokról" -bullet3 = "Új digitális aláírások hozzáadása a dokumentumok védelméhez" -bullet4 = "Több fájl támogatása könnyű navigációval" -text = "Ez az eszköz lehetővé teszi, hogy ellenőrizze, digitálisan alá vannak-e írva a PDF-ek, és új digitális aláírásokat adjon hozzá. A digitális aláírások igazolják, ki hozta létre vagy hagyta jóvá a dokumentumot, és jelzik, hogy megváltozott-e az aláírás óta." -title = "Mire képes ez az eszköz?" - -[certSign.tooltip.signing] -bullet1 = "Támogatott formátumok: PEM, PKCS12, JKS és kiszolgálói tanúsítvány formátumok" -bullet2 = "Az aláírás megjelenítése vagy elrejtése a PDF-en" -bullet3 = "Ok, hely és aláíró neve hozzáadása" -bullet4 = "Kiválaszthatja, melyik oldalra kerüljön a látható aláírás" -bullet5 = "Kiszolgálói tanúsítvány használata az egyszerű 'Aláírás Stirling PDF-fel' opcióhoz" -text = "PDF aláírásához digitális tanúsítványra van szükség (például PEM, PKCS12 vagy JKS). Az aláírást megjelenítheti a dokumentumon, vagy tarthatja láthatatlanul, csak biztonsági céllal." -title = "Aláírás hozzáadása" - -[certSign.tooltip.validation] -bullet1 = "Megmutatja, hogy az aláírások érvényesek vagy érvénytelenek" -bullet2 = "Megjeleníti az aláíró adatait és az aláírás dátumát" -bullet3 = "Ellenőrzi, hogy a dokumentum módosult-e az aláírás után" -bullet4 = "Ellenőrzéshez használhat egyéni tanúsítványokat" -text = "Ellenőrzéskor az eszköz megmondja, érvényesek-e, ki írta alá a dokumentumot, mikor történt az aláírás, és hogy megváltozott-e a dokumentum az aláírás óta." -title = "Aláírások ellenőrzése" - [changeCreds] changeUsername = "Felhasználónév frissítése. A frissítés után ki lesz jelentkeztetve." credsUpdated = "Fiók frissítve" @@ -2737,12 +2936,16 @@ text = "Ezeket a jogosultságokat tegye megváltoztathatatlanná a \"Jelszó hoz [chat.actions] copy = "Üzenet másolása" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling ügynök beállításai" +assistant = "AI Assistant" clearChat = "Csevegés törlése" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ez a művelet a felhőkrediteket használja" tooltip = "Szín kiválasztása a képernyőről" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Szín kiválasztása" [common] +actions = "Actions" back = "Vissza" cancel = "Mégse" close = "Bezárás" +codeSample = "Code sample" collapse = "Összecsukás" confirm = "Megerősítés" continue = "Folytatás" copied = "Másolva!" copy = "Másolás" done = "Kész" +enterValue = "Enter value" error = "Hiba" expand = "Kibontás" loading = "Betöltés..." next = "Következő" +open = "Open" preview = "Előnézet" previous = "Előző" refresh = "Frissítés" +remaining = "Hátralévő" retry = "Újra" save = "Mentés" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Kijelöltek törlése" @@ -2988,6 +3199,7 @@ title = "Tömörítési mód" [compress.settings] desiredSize = "Kívánt fájlméret" desiredSizePlaceholder = "Méret megadása" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "A tömörítés egyszerű módja a fájlméret csökkentésének. Válassza a Fájlméret lehetőséget a célméret megadásához, és mi ehhez igazítjuk a minőséget. Válassza a Minőség lehetőséget a tömörítés erősségének kézi beállításához." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF beállítások" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Először válasszon forrásformátumot" settings = "Beállítások" single = "Egyetlen" @@ -3264,24 +3494,6 @@ description = "Ezek a sütik elengedhetetlenek a weboldal megfelelő működés posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Fejlett monitorozás" -allInOneWorkspace = "Minden egyben PDF munkaterület (megjelenítő, eszközök és agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Dedikált támogatás és SLA-k" -fasterThroughput = "10× gyorsabb áteresztőképesség" -fullyPrivateFiles = "Teljesen privát fájlok" -largeFileProcessing = "Nagy fájlok feldolgozása" -orgWideAccess = "Szervezetszintű hozzáférés-vezérlés" -premiumAiModels = "Prémium AI modellek" -prioritySupport = "Elsőbbségi támogatás" -privateDocCloud = "Privát dokumentumfelhő" -ragFineTuning = "RAG + finomhangolás" -secureApiAccess = "Biztonságos API-hozzáférés" -standardThroughput = "Normál áteresztőképesség" -unlimitedApiAccess = "Korlátlan API-hozzáférés" -unlimitedSeats = "Korlátlan felhasználói helyek" - [crop] autoCrop = "Üres terület automatikus levágása" noFileSelected = "Válasszon egy PDF-fájlt a vágás megkezdéséhez" @@ -3346,6 +3558,7 @@ message = "Nem sikerült beállítani az alapértelmezett PDF-kezelőt" title = "Hiba" [defaultApp.prompt] +dontRemind = "Ne emlékeztessen újra" message = "Állítsa a Stirling PDF-et a PDF-fájlok megnyitásának alapértelmezett alkalmazásává." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Keresés..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Szerkesztés" @@ -3644,11 +3861,13 @@ shareSelected = "Kijelöltek megosztása" sharing = "Megosztás" showAll = "Összes megjelenítése" showHistory = "Előzmények megjelenítése" +sortBy = "Sort files" sortByDate = "Rendezés dátum szerint" sortByName = "Rendezés név szerint" sortBySize = "Rendezés méret szerint" storage = "Tárhely" storageState = "Tárhely" +storageUsed = "Storage used" synced = "Szinkronizálva" title = "PDF fájlok feltöltése" toolChain = "Alkalmazott eszközök" @@ -3694,13 +3913,17 @@ addFiles = "Fájlok hozzáadása" addingFiles = "Fájlok hozzáadása…" collapse = "Oldalsáv összecsukása" customizeGroups = "Csoportok testreszabása" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "A kezdéshez nyisson meg fájlokat" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Oldalsáv kinyitása" -files = "Fájlok" googleDrive = "Google Drive" googleDriveDisabled = "A Google Drive nincs beállítva" leaveMyFiles = "Kilépés a Saját fájlokból" +library = "PDF-könyvtár" myFiles = "Saját fájlok" noFiles = "Még nincsenek fájlok" openFileManager = "Fájlkezelő megnyitása" @@ -3708,37 +3931,36 @@ openFromComputer = "Megnyitás a számítógépről" openSettings = "Beállítások megnyitása" other = "Egyéb" recent = "Legutóbbi" -search = "Keresés" -searchPlaceholder = "Fájlok keresése..." viewAll = "Mind a(z) {{count}} fájl megtekintése" [fileSidebar.fileItem] closeViewer = "Megjelenítő bezárása" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Megnyitás a megjelenítőben" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Hozzáadás" -addLabel = "Címke hozzáadása…" -categoryIconAria = "Ikon kiválasztása ehhez: {{name}}" -createCategory = "Kategória létrehozása" -delete = "Kategória törlése" done = "Kész" -hide = "Kategória elrejtése" -newCategory = "Új kategórianév…" -removeLabel = "{{name}} eltávolítása" -rename = "Átnevezés" reset = "Alapértelmezések visszaállítása" -search = "Címkék keresése…" -show = "Kategória megjelenítése" subtitle = "Csoportosítsa fájljait szülőkategóriákba. Adjon meglévő vagy új címkéket egy kategóriához, nevezze át, vagy hozzon létre sajátot. Azok a fájlok, amelyek egyik kategóriájába sem tartoznak, az „Egyéb” alatt jelennek meg." title = "Oldalsáv kategóriái" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Hozzáadás a munkaterülethez" addToWorkspaceCount = "{{count}} hozzáadása a munkaterülethez" @@ -3749,7 +3971,6 @@ backToMyFiles = "Vissza a Saját fájlokhoz" breadcrumbs = "Mappaútvonal" cancel = "Mégse" classification = "Besorolás" -clearSearch = "Keresés törlése" clearSelection = "Kijelölés törlése" closeDetails = "Részletek bezárása" create = "Létrehozás" @@ -3781,6 +4002,7 @@ downloadAll = "Összes letöltése" downloadVersion = "E verzió letöltése" dropOverlay = "Húzza ide a fájlokat a feltöltéshez" dropOverlaySub = "A fájlok a Helyi fülön jelennek meg. Használja az 'Áthelyezés' vagy a 'Mentés felhőbe' lehetőséget a mappába rendezéshez." +duplicate = "Duplicate" file = "Fájl" fileInfo = "File info" fileMenu = "Fájlműveletek" @@ -3808,13 +4030,12 @@ refresh = "Frissítés a szerverről" remove = "Törlés" removeVersion = "E verzió eltávolítása" rename = "Átnevezés" +renamed = "Átnevezve" renameFolder = "Mappa átnevezése" resizeFolderTree = "Mappafa átméretezése (nyilak, Shift a nagyobb lépésekhez; dupla kattintás az automatikus illesztéshez)" save = "Mentés" saveToServer = "Mentés a szerverre" saveToServerDisabledHint = "A mentés a szerverre ezen a szerveren nincs engedélyezve. Kérje meg az adminisztrátort, hogy engedélyezze." -search = "Keresés" -searchPlaceholder = "Keresés ebben a mappában és almappáiban" selectAll = "Összes kijelölése" selectAllHint = "Kattintson az összes kijelöléséhez. Tipp: tartsa lenyomva a Ctrl-t (vagy Cmd-et) az egyenkénti hozzáadáshoz, a Shiftet a tartomány kijelöléséhez." selectedCount = "{{count}} kijelölve" @@ -3874,6 +4095,10 @@ title = "Még nincsenek felhőfájlok" hint = "A feltöltés nélkül mentett fájlok itt maradnak. Húzzon ide egy fájlt a hozzáadáshoz." title = "Nincsenek csak helyi fájlok" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Az Ön által megnyitott vagy szerkesztett fájlok itt jelennek meg." title = "Még semmi sem módosult" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nem sikerült a mappát törölni." deleteFolderFailedDetail = "Nem sikerült törölni a mappát: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nem sikerült módosítani a mappa megjelenését." folderAppearanceFailedDetail = "Nem sikerült módosítani a mappa megjelenését: {{message}}" moveFilesFailed = "Nem sikerült áthelyezni a fájlokat." @@ -3910,9 +4137,7 @@ count = "Fájlok" folder = "Mappa" labels = "Címkék" modified = "Módosítva" -name = "Név" size = "Méret" -toolHistoryAtVersion = "Halmozott eszközlánc" totalSize = "Teljes méret" type = "Típus" versionHistory = "Verziótörténet" @@ -3945,6 +4170,11 @@ localHint = "Csak ebben a böngészőben tárolva" shared = "Megosztott" sharedHint = "Önnel linken keresztül megosztva" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Legrégebbi elöl" modifiedDesc = "Legújabb elöl" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Nincsenek fájlok a tárhelyen. Először nyisson meg n open = "Megnyitás" openFile = "Fájl megnyitása" openFiles = "Fájlok megnyitása" +selectFile = "Select {{name}}" selectFromStorage = "Kiválasztás a tárhelyről" upload = "Feltöltés" uploadFile = "Fájl feltöltése" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Minden mentve" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV kinyerése sikertelen" +extractXlsxError = "XLSX kinyerése sikertelen" +filled = "filled" flattenAfterFilling = "Kitöltés után lapítás" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "kötelező" +requiredFieldsError = "Kérjük, töltse ki az összes kötelező mezőt" rescanFields = "Mezők újraolvasása" rescanFormFields = "Űrlapmezők újraolvasása" save = "Mentés" +saveShortcut = "Ctrl+S a mentéshez" +unsavedChanges = "Nem mentett módosítások" [formFill.sidebar] close = "Oldalsáv bezárása" @@ -4309,6 +4552,11 @@ desc = "Automatikusan átnevezi a PDF fájlt az észlelt fejléc alapján" tags = "automatikus,fejlécalapú,rendez,átcímkéz" title = "PDF fájl automatikus átnevezése" +[home.autoRotate] +desc = "Az egyes oldalak tájolásának felismerése és automatikus függőlegesre forgatása." +tags = "automatikus elforgatás,tájolás javítása,fejjel lefelé,oldalra fordítva,kiegyenesítés,tájolás,tájolás felismerése,elforgatás javítása,automatikus tájolás,szkennelt,OSD" +title = "Automatikus elforgatás" + [home.autoSizeSplitPDF] tags = "automatikus,feloszt,méret" title = "Auto felosztás méret/darab" @@ -4608,15 +4856,12 @@ hint = "Töltsön fel PNG, JPG, SVG vagy más képfájlt a PDF-re helyezéshez. label = "Kép feltöltése" placeholder = "Képfájl kiválasztása" -[imageToPdf] -tags = "konverzió,kép,jpg,fotó,fénykép" - [imageToPDF] header = "Kép PDF-be" title = "Kép PDF-be" -[infoBanner] -dismiss = "Bezárás" +[imageToPdf] +tags = "konverzió,kép,jpg,fotó,fénykép" [invite] acceptError = "Nem sikerült létrehozni a fiókot" @@ -4647,11 +4892,9 @@ welcomeTitle = "Meghívták!" [landing] addFiles = "Fájlok hozzáadása" -heroSubtitle = "Húzza ide vagy adjon hozzá egy meglévő PDF-et a kezdéshez." mobileUpload = "Feltöltés mobilról" openFromComputer = "Megnyitás a gépről" uploadFromComputer = "Feltöltés a számítógépről" -workbenchEmptyStateHero = "Dobjon ide bárhová egy PDF-et" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Adja meg felhasználónevét" failedToSignIn = "Sikertelen bejelentkezés ezzel: {{provider}}: {{message}}" forgotPassword = "Elfelejtette a jelszavát?" loggingIn = "Bejelentkezés..." -logIn = "Bejelentkezés" login = "Bejelentkezés" magicLinkSent = "Magic Link elküldve ide: {{email}}! Ellenőrizze e-mailjét, és kattintson a linkre a bejelentkezéshez." mfaCode = "Hitelesítési kód" @@ -4715,26 +4957,10 @@ signInWith = "Bejelentkezés ezzel:" title = "Bejelentkezés" unexpectedError = "Váratlan hiba: {{message}}" updatePassword = "Jelszó frissítése" -useEmailInstead = "Bejelentkezés e-maillel" useMagicLink = "Használjon Magic Linket" username = "Felhasználónév" youAreLoggedIn = "Be van jelentkezve!" -[login.slides.edit] -alt = "PDF-ek szerkesztése" -subtitle = "Több mint egy tucat eszközzel a PDF-ek kitakarásához, aláírásához, olvasásához és kezeléséhez biztosan megtalálja, amit keres." -title = "PDF-ek szerkesztése a kívánt információk megjelenítéséhez/védelméhez" - -[login.slides.overview] -alt = "Stirling PDF áttekintés" -subtitle = "Adatvédelem-központú felhőcsomag PDF-ekhez, amellyel konvertálhat, aláírhat, kitakarhat és kezelhet dokumentumokat, több mint 50 további hatékony eszközzel." -title = "Az összes PDF-megoldás egy helyen." - -[login.slides.secure] -alt = "PDF-ek védelme" -subtitle = "Adjon hozzá jelszavakat, takarja ki a tartalmat, és kezelje a tanúsítványokat egyszerűen." -title = "Érzékeny információk védelme a PDF-ekben" - [margin] large = "Nagy" medium = "Közepes" @@ -4818,6 +5044,34 @@ uploadSuccess = "Sikeres feltöltés!" uploadSuccessMessage = "A képei átkerültek." validating = "Munkamenet ellenőrzése..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Szkenneljen a fotók feltöltéséhez. A képek automatikusan PDF-be konvertálódnak." descriptionNoConvert = "Szkenneljen a fotók feltöltéséhez a mobileszközéről." @@ -4836,8 +5090,13 @@ title = "Feltöltés mobilról" tags = "Többfunkciós eszköz,Több művelet,UI,húzd és ejtsd,frontend,kliens oldali,interaktív,kezelhető,mozgatás" title = "PDF többfunkciós eszköz" -[navbar] -search = "Keresés" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "A hitelesítés nem volt sikeres. Bezárhatja ezt az ablakot, és megpróbálhatja újra." @@ -4966,6 +5225,7 @@ activeFiles = "Az Aktív fájlok nézet megmutatja az összes o allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Bezárás" cropSettings = "Most, hogy kiválasztottuk a vágandó fájlt, beállíthatjuk a Vágás eszközt, hogy megadjuk a PDF kivágandó területét." +dialogLabel = "Onboarding" fileCheckbox = "Egy fájlra kattintva kijelöli azt feldolgozásra. Több fájlt is kijelölhet kötegelt műveletekhez." fileReplacement = "A módosított fájl automatikusan felülírja az eredetit a Munkaterületen, így könnyen további eszközökön futtathatja." filesButton = "A Gyors hozzáférés sávon lévő Fájlok gombbal tölthet fel PDF-eket az eszközök használatához." @@ -4976,6 +5236,7 @@ pinButton = "Használhatja a Rögzítés gombot, ha azt szeretn results = "Miután az eszköz lefutott, az Ellenőrzés lépés előnézetet mutat az eredményekről ebben a panelben, és lehetővé teszi a művelet visszavonását vagy a fájl letöltését." runButton = "Miután beállította az eszközt, ezzel a gombbal futtathatja azt az összes kijelölt PDF-en." selectCropTool = "Válasszuk ki a Vágás eszközt, hogy bemutassuk egy eszköz használatát." +stepOf = "{{current}}/{{total}}. lépés" toolInterface = "Ez a Vágás eszköz felülete. Ahogy látható, még nincs itt sok minden, mert még nem adtunk hozzá PDF-fájlokat." workbench = "Ez a Munkaterület – a fő terület, ahol megtekintheti és szerkesztheti a PDF-jeit." wrapUp = "Készen is van! Megismerte az alkalmazás fő területeit és használatukat. A túrát bármikor újra megtekintheti a Súgó gombra kattintva." @@ -4988,6 +5249,26 @@ showMeAround = "Körbevezetés" skipForNow = "Kihagyás most" skipTheTour = "Körbevezetés kihagyása" +[onboarding.checklist] +dismiss = "Elvetés" +title = "Stirling PDF beállítása" + +[onboarding.checklist.downloadDesktop] +description = "Futtassa a Stirlinget natívan a gépén" +title = "Stirling letöltése asztali gépre" + +[onboarding.checklist.inviteTeam] +description = "Működjön együtt a csapatával" +title = "Csapattagok meghívása" + +[onboarding.checklist.shareAnalytics] +description = "Segítsen a Stirling fejlesztésében" +title = "Anonim használati adatok megosztása" + +[onboarding.checklist.takeTour] +description = "Nézze meg egy gyors bemutatóban, hogyan működik a Stirling" +title = "Bemutató megtekintése" + [onboarding.desktopInstall] body = "A Stirling asztali alkalmazásként működik a legjobban. Használhatja offline, gyorsabban érheti el a dokumentumokat, és helyben szerkeszthet a gépén." selectOs = "Operációs rendszer kiválasztása" @@ -4996,7 +5277,21 @@ titleWithOs = "Letöltés: {{osLabel}}" [onboarding.mfa] authenticationCode = "Hitelesítési kód" +description = "Tegye biztonságosabbá fiókját egy hitelesítő alkalmazás összekapcsolásával. Olvassa be a QR code-ot, vagy adja meg a beállítási kulcsot, majd a befejezéshez erősítse meg a 6 jegyű kódot." +enable = "MFA engedélyezése" +enableError = "A kétfaktoros hitelesítés engedélyezése nem sikerült. Ellenőrizze a kódot, és próbálja újra." +enterCode = "Adja meg az alkalmazásból származó 6 jegyű kódot." +enterCodeError = "A folytatáshoz adja meg a hitelesítési kódot." +logout = "Kijelentkezés" +openAuthenticator = "Nyissa meg a Google Authenticator, Authy vagy 1Password alkalmazást." qrCodeLoading = "QR-kód előállítása…" +regenerateQrCode = "QR code újragenerálása" +scanQrCode = "Olvassa be a QR code-ot, vagy adja meg alább a beállítási kulcsot." +setupError = "A kétfaktoros beállítás elindítása nem sikerült. Próbálja újra." +setupKey = "Beállítási kulcs (kézi bevitel)" +stepByStep = "Lépésről lépésre" +success = "Az MFA engedélyezve lett. Most már folytathatja." +title = "Többtényezős hitelesítés beállítása" [onboarding.planOverview] adminBodyLoginDisabled = "A bejelentkezési mód engedélyezése után kezelheti a felhasználókat, beállításokat és figyelheti a szerver állapotát. A szerver első {{freeTierLimit}} felhasználója ingyen használhatja a Stirlinget." @@ -5005,6 +5300,11 @@ adminTitle = "Admin áttekintés" userBody = "Hívja meg a csapattagokat, rendeljen szerepköröket, és tartsa rendezve dokumentumait egy biztonságos munkaterületen. Engedélyezze a bejelentkezési módot, amikor továbblépne az egyéni használatról." userTitle = "Csomag áttekintése" +[onboarding.processorIntro] +body = "A Stirling mostantól Szabályokat futtat — automatizált szabályokat, amelyek érkezéskor minden dokumentumot osztályoznak, védenek és feldolgoznak. Állítsa be őket, és kövesse a futásokat a Processor felületén." +cta = "A Processor megtekintése" +title = "Ismerje meg a Stirling Processort" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Előre fizetett év" + +[payg.prepaid.banner] +exhausted = "Az előre fizetett kapacitása elfogyott; a mért számlázás újraindult. Töltse fel, hogy megőrizze a kedvezményt." +expiring_one = "Az előre fizetett éve {{count}} nap múlva lejár. A fel nem használt kapacitás lejár — töltse fel egy új év indításához." +expiring_other = "Az előre fizetett éve {{count}} nap múlva lejár. A fel nem használt kapacitás lejár — töltse fel egy új év indításához." +low = "Az előre fizetett kapacitásának {{pct}}%-a maradt. Bármikor feltöltheti, hogy a feldolgozás megszakítás nélkül folytatódjon." + +[payg.prepaid.card] +subtitle = "A mért számlázás előtt és a költési korlátján kívül kerül felhasználásra." +title = "Előre fizetett kapacitás" + +[payg.prepaid.meter] +capSuffix = "{{total}} előre fizetett kreditből" +expires = "Lejár: {{date}}" + +[payg.prepaid.state] +exhausted = "Elfogyott" +healthy = "Bőven maradt" +low = "Kevés maradt" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF konvertálása PDF/A formátumba" tags = "archívum,hosszú távú,szabvány,konvertálás,tárolás,megőrzés" title = "PDF konvertálása PDF/A formátumba" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "nyomtatás,szabvány,átalakítás,gyártás,előnyomdai,archiválás" title = "PDF PDF/X-re" @@ -5822,6 +6151,7 @@ small = "500 kredit" xsmall = "100 kredit" [plan.availablePlans] +currency = "Billing currency" subtitle = "Válassza ki az igényeinek megfelelő csomagot" title = "Elérhető csomagok" @@ -5937,49 +6267,12 @@ keyDescription = "Illessze be az e-mailben kapott licenckulcsot" success = "A licenc aktiválva!" successMessage = "A licenc sikeresen aktiválva. Most bezárhatja ezt az ablakot." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "érvényesítve" -enforcing = "Érvényesítés folyamatban..." -failed = "Az érvényesítés sikertelen" outputsUnavailable = "A házirend kimenetei már nem tölthetők le." partialOutputsUnavailable = "A házirend egyes kimenetei már nem tölthetők le." -retrying = "Foglalt, újrapróbálás..." runNotFound = "Az érvényesítési futtatás már nem található." -step = "lépés {{current}}/{{total}}" timedOut = "Időtúllépés történt az érvényesítésben, mielőtt a futtatás befejeződhetett volna." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Bezárás" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "A(z) {{names}} alkalmazása" applyingProgress = "A(z) {{names}} alkalmazása ({{done}} / {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} / {{total}} fájlt nem sikerült feldolgozni, és v failureTitle = "Exportálva teljes érvényesítés nélkül" printPolicyAppliedBody = "Ez a PDF frissítve lett, hogy megfeleljen egy házirendnek. Tekintse át a változtatásokat, majd nyomtasson újra." printPolicyAppliedTitle = "Házirend alkalmazva nyomtatás előtt" -queued = "+{{count}} várólistán" successTitle = "{{names}} alkalmazva" summaryMore = "{{first}}, {{second}} és még {{more}}" summaryTwo = "{{first}} és {{second}}" -[policies.enforcement.triggerVerb] -convert = "Érvényesítés konvertálás előtt" -default = "Érvényesítés" -export = "Érvényesítés exportálás előtt" -input = "Érvényesítés importáláskor" -print = "Érvényesítés nyomtatás előtt" - -[policies.field] -accessLog = "Hozzáférési napló" -archiveAfter = "Archiválás ennyi idő után" -auditTrail = "Auditnapló" -belowThreshold = "Küszöbérték alatt" -destination = "Célhely" -frameworks = "Keretrendszerek" -immutableHold = "Módosíthatatlan visszatartás" -keepFor = "Megőrzés ennyi ideig" -minConfidence = "Min. megbízhatóság" -notify = "Értesítés útvonalon" -onViolation = "Ha nem megfelelő" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Hozzáadás" -addPlaceholder = "Címke hozzáadása…" -customNote = "A csapatához igazítva." -defaultNote = "A beépített alapértelmezés használata, megosztva a csapatával." -duplicate = "„{{name}}” már létezik." -edit = "Címkék szerkesztése" -empty = "Még nincsenek címkék." -export = "JSON exportálása" -iconAria = "Ikon kiválasztása ehhez: {{name}}" -import = "JSON importálása" -importError = "Nem sikerült importálni a fájlt." -managedNote = "A csapatcímkéket a csapatvezető kezeli." -modalSubtitle = "Megosztva az egész csapattal. A besoroló kiválasztja az egyes dokumentumokhoz illő címkéket." -modalTitle = "Besorolási címkék" -pickIcon = "{{label}} ikon használata" -removeAria = "{{name}} eltávolítása" -resetToDefault = "Alapértelmezés visszaállítása" -saveForTeam = "Mentés a csapat számára" -saving = "Mentés…" -sectionLabel = "Besorolási címkék" -startFromScratch = "Kezdés elölről" -teamCount = "csapatcímkék" -tooLong = "A címkék legfeljebb {{max}} karakter hosszúak lehetnek." -ungrouped = "Csoportosítatlan" -view = "Címkék megtekintése" +categoryCount = "kategória" +hideCategory = "Kategória elrejtése" +labelCount = "címke" +sharedNote = "Ezek a címkék beépítettek, és az egész csapatával meg vannak osztva." +showCategory = "Kategória megjelenítése" [policies.pii] account = "Számlaszámok (címkézett)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US routing számok (ABA)" ssn = "Társadalombiztosítási számok" -[policies.settings] -noneExport = "Jelenleg nincs exportáláskor futó szabályzat." -noneUpload = "Jelenleg nincs feltöltéskor futó szabályzat." -onExport = "Exportáláskor" -onUpload = "Feltöltéskor" -reorderHandle = "Húzza az átrendezéshez" -runOrderDesc = "Ha egynél több szabályzat fut ugyanarra az eseményre, ebben a sorrendben futnak — mindegyik az előző szabályzat kimenetén. Húzza az átrendezéshez." -title = "Szabályzatbeállítások" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Szabályzat beállításai" -policySettings = "Szabályzatbeállítások" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Sikertelen szabályzatok újrapróbálása ({{count}})" -rowProgress = "{{completed}} / {{total}} fájl feldolgozva" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Mi az a szabályzat?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatikusan megkeresi és kitakarja az érzékeny adatokat — például a társadalombiztosítási és kártyaszámokat — így a dokumentumban nem olvashatók." -sanitize = "Eltávolítja a rejtett JavaScriptet a fájlból, így megnyitáskor semmi nem fut le automatikusan." -watermark = "Látható jelölést helyez el (pl. \"Bizalmas\") minden oldalra." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Bezárás" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} érvényesítése..." badgeRan = "{{name}} szabályzat lefutott ezen a fájlon" +badgeRunning = "{{name}} fut..." blockingAction = "{{action}} blokkolva a szabályzat érvényesítése közben, kérjük, várjon..." dismiss = "Átfedés elvetése" enforcingTitle = "Szabályzat érvényesítése..." @@ -6151,6 +6316,7 @@ viewAnyway = "Fájl megtekintése (a szabályzat érvényesítése még tart)" [policyOption] autoRedactPhi = "PHI automatikus kitakarása" blockExport = "Exportálás blokkolása" +ccpa = "CCPA" contracts = "Szerződések" documents = "Dokumentumok" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor csomag" subscribed = "Processor csomag" unlinked = "Nincs összekapcsolva" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap dokumentumból" -sectionsAriaLabel = "Ügynöképítő szakaszai" -selectorAriaLabel = "Ügynökök" -subtitle = "Tervezze meg, tesztelje és élesítse azokat az AI ügynököket, amelyek besorolják, kinyerik és továbbítják a dokumentumait. Határozzon meg forgatókönyveket, korlátozza az eszközhozzáférést, futtasson golden setet, és tegyen közzé egy verziót." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Mégse" -dropzoneText = "Válasszon mintadokumentumot (PDF vagy kép)" -lead = "Dobjon ide egy mintadokumentumot, és javasolunk forgatókönyveket és egy kinyerési sémát, amelyet finomíthat. Semmi sem kerül közzétételre, amíg át nem nézi." -submit = "Ügynök bootstrapelése" -subtitle = "Új ügynök indítása egy reprezentatív fájlból" -title = "Bootstrap dokumentumból" - -[portal.agentBuilder.empty] -description = "Bootstrapeljen egy ügynököt egy mintadokumentumból, hogy elindítsa a forgatókönyveit és a kinyerési sémáját, majd finomítsa és tegye közzé." -title = "Még nincsenek ügynökök" - -[portal.agentBuilder.evals] -casesPassing = "Sikeres esetek" -columnCase = "Eval eset" -columnLatency = "Késleltetés" -columnResult = "Eredmény" -fail = "sikertelen" -goldenSetPassRate = "Golden-set sikerességi arány" -latencyMs = "{{ms}} ms" -notRun = "nem futott" -pass = "sikeres" -passRate = "Sikerességi arány" -runEvals = "Evalok futtatása" - -[portal.agentBuilder.evals.empty] -description = "Az evalok a forgatókönyveit ismételhető golden setté alakítják. Váltson magasabb csomagra, hogy időben kövesse a sikerességi arányt, és ehhez kösse a közzétételeket." -title = "Még nincs golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "golden seteken keresztül" -activeAgents = "Aktív ügynökök" -avgPassRate = "Átlagos eval sikerességi arány" -fleetWide = "teljes flottában" -latestPublished = "Legutóbb közzétéve" -scenarios = "Forgatókönyvek" -testCases = "tesztesetek" -totalDescription_one = "összesen {{count}}" -totalDescription_other = "összesen {{count}}" - -[portal.agentBuilder.scenarios] -add = "Hozzáadás" -addScenario = "Forgatókönyv hozzáadása" -enable = "Engedélyezés" -expectationLabel = "Várt viselkedés" -expectationPlaceholder = "Mit tegyen az ügynök" -inEval = "evalban" -mute = "Némítás" -muted = "némítva" -nameLabel = "Név" -namePlaceholder = "pl. megfelelőségi eszkaláció" - -[portal.agentBuilder.tabs] -evals = "Evalok" -scenarios = "Forgatókönyvek" -tools = "Eszközök" -versions = "Verziók" - -[portal.agentBuilder.tools] -broadAccess = "Széles körű hozzáférés" -deniedHint = "A kiválasztott eszközök blokkolva vannak. Minden más továbbra is hívható." -deniedTools = "Tiltott eszközök" -governanceGate = "Az eszközirányítás az Enterprise csomagban érhető el." -restricted = "Korlátozott" -restrictedAccess = "Korlátozott eszközhozzáférés" -restrictedDescription = "Engedélyezzen minden eszközt, kivéve az alább tiltottakat." - -[portal.agentBuilder.versions] -current = "aktuális" -historyGate = "A teljes verzióelőzmény és a visszaállítás az Enterprise csomagban érhető el." -publish = "Közzététel" -rollBack = "Visszaállítás" - [portal.assistant] close = "Asszisztens bezárása" error = "Nem sikerült elérni az asszisztenst." @@ -6365,9 +6451,21 @@ typing = "Gépel" loading = "Betöltés" redirectingToEditor = "Átirányítás a szerkesztőbe..." +[portal.billing.activation] +maybeLater = "Talán később" +title = "Processor bekapcsolása" + +[portal.billing.activation.payg] +sub = "Állítson be havi korlátot. Azért fizet, amit futtat." +title = "Használat alapú fizetés" + +[portal.billing.activation.prepay] +badge = "2 hónap ingyen" +sub = "Egy számla, kártyás vagy banki átutalás." +title = "Fizessen előre egy évre" + [portal.billing.checkout] noClientSecret = "Az Edge function nem adott vissza client_secret értéket." -subtitle = "Adjon hozzá egy kártyát, hogy továbbhaladhasson az ingyenes Editor-csomag keretén túl. A többit a Stripe kezeli." title = "Processor csomag bekapcsolása" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "A fizetés sikerült, de az aktiválás a szokásosnál kicsit tovább t close = "Bezárás" title = "Majdnem kész" +[portal.billing.checkout.cap] +amountAria = "Havi költési korlát" +back = "Vissza" +continue = "Tovább a fizetéshez" +error = "Nem sikerült beállítani a költési korlátot" +finePrint = "A számlák havonta készülnek. Bármikor lemondható." +noLimit = "Nincs korlát" +note = "Soha nem számlázunk a korlátja felett. A feldolgozás egyszerűen szünetel." +perMonth = "/ hó" +title = "Költési korlát beállítása" + +[portal.billing.checkout.card] +fields = "Kártyaszám · HH / ÉÉ · CVC · ZIP" +label = "Kártyaadatok" +note = "A kártyaadatokat a Stripe gyűjti. A Stirling soha nem tárol PAN-t vagy CVC-t." + [portal.billing.checkout.error] title = "Nem sikerült elindítani a fizetést" @@ -6383,10 +6497,9 @@ body = "A fizetés sikerült. Bekapcsoljuk a mért feldolgozást az összekapcso hint = "Kérjük, hagyja nyitva ezt az ablakot." title = "Processor csomag aktiválása..." -[portal.billing.checkout.notConfigured] -bodyAfter = "a portál env-ben az alkalmazáson belüli fizetés engedélyezéséhez." -bodyBefore = "Állítsa be:" -title = "Stripe nincs konfigurálva" +[portal.billing.checkout.pay] +limit = "Havi költési korlát: {{amount}}/hó. A feldolgozás a korlátnál szünetel." +limitNoCap = "Nincs költési korlát. A feldolgozás nem szünetel. A számlák havonta készülnek." [portal.billing.enterpriseUpsell] cta = "Enterprise ajánlat összeállítása" @@ -6409,10 +6522,12 @@ currentPlan = "Aktuális csomag" freeForever = "Örökké ingyenes" noTeamResolved = "A tárcájához még nincs csapat hozzárendelve — frissítsen, és próbálja újra." ownerOnly = "Csak a csapat tulajdonosa kapcsolhatja be a Processor csomagot." +payInvoice = "Számla kifizetése a befejezéshez" planName = "Editor" ssoIncluded = "SSO mellékelve" switchOnProcessor = "Processor bekapcsolása →" unlimitedUsers = "Korlátlan felhasználó" +viewQuote = "Árajánlat megtekintése" [portal.billing.invoices] columnAmount = "Összeg" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Közvetlen API-kérések" segmentApiLabel = "API" segmentAutomationDesc = "Automatizálások és pipeline-ok" segmentAutomationLabel = "Automatizálás" +sizeMultiplier = "{{formatted}} PDF méretszorzót használt" +summary = "{{unique}} egyedi · {{units}} mérési egység · {{avg}} átlag PDF-enként" +summaryNoRate = "{{unique}} egyedi · {{units}} mérési egység" unit = "mért PDF-ek" +unitsPending_one = "{{units}} mérési egység vár szinkronizálásra a kapcsolt példányokból" +unitsPending_other = "{{units}} mérési egység vár szinkronizálásra a kapcsolt példányokból" + +[portal.billing.prepaid] +capSuffix = "{{total}} előre fizetett kreditből" +expires = "Lejár: {{date}}" +eyebrow = "Előre fizetett kapacitás" +note = "A mért számlázás előtt és a költési korlátján kívül kerül felhasználásra." +topUp = "Feltöltés" + +[portal.billing.prepaid.buy] +back = "Vissza" +calcTitle = "Éves fizetés kiszámítása" +cancel = "Mégse" +cancelPurchase = "Vásárlás megszakítása" +close = "Bezárás" +continueToPayment = "Tovább a fizetéshez" +finalise = "Véglegesítés" +invoiceErrorTitle = "Nem sikerült létrehozni a számlát" +loadingTitle = "Árajánlat betöltése" +payTitle = "Fizessen az évéért" +step = "{{current}}/{{total}}. lépés" + +[portal.billing.prepaid.calc] +change = "Módosítás" +deployRow = "Telepítés" +done = "Kész" +enterpriseHint = "Ez vállalati lépték. Ennél a mennyiségnél a vállalati díjak minden önkiszolgáló kedvezménynél jobbak. Díjrögzítés, feltételek és árajánlat percek alatt." +governanceRow = "Irányítás" +governanceValue = "{{label}} · {{count}} szabály" +handlesLabel = "Az Ön Processora" +handlesValue = "{{volume}} kredit / hó kezelésére képes" +pipelinesLabel = "Pipeline-ok" +policiesMeta = "{{count}} szabály" +poolCaption = "Egy {{credits}} kredites keret az évre. A nehéz hónapok a könnyű hónapoktól vesznek kölcsön." +priceLabel = "Az Ön éve · 12 hónap 10 hónap áráért" +rateUnknown = "A pontos árat a pénztárnál mutatjuk." +savings = "Megtakarítás: {{amount}} · 2 hónap ingyen." +sizingRow = "Méret" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Felhasználók összesen" + +[portal.billing.prepaid.consent] +label = "Tudomásul veszem, hogy amikor az előre fizetett kapacitásom elfogy vagy 12 hónap után lejár, a feldolgozás automatikusan a szokásos mért, használat alapú díjon folytatódik (a költési korlátomig), hacsak le nem mondom, és hogy bármikor lemondhatom a számlázási portálon." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "A Stirling kezeli. Percek alatt működik." +cloudValue = "Stirling Cloud · Kezelt" +selfhost = "Saját üzemeltetésű" +selfhostDesc = "Privát infrastruktúrán fut. Ugyanaz a díj." +selfhostValue = "Saját üzemeltetésű · Privát infrastruktúra" + +[portal.billing.prepaid.offer] +cta = "Ajánlat áttekintése" +subtitle = "Fizessen előre egy évnyi PDF-feldolgozásért, és kapjon két hónapot ingyen; ez a mért számlázás előtt, a költési korlátján kívül kerül felhasználásra." +title = "Kapjon 12 hónapot 10 hónap áráért" + +[portal.billing.prepaid.pay] +accountName = "Számlatulajdonos neve" +accountNamePlaceholder = "pl. Jane Smith" +companyName = "Cégnév (opcionális)" +companyNamePlaceholder = "pl. Acme Inc." +downloadInvoice = "Számla letöltése" +payOnline = "Online fizetés" +poNumber = "PO-szám (opcionális)" +poNumberPlaceholder = "pl. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Speciális" +advancedDesc = "A Pipeline-ok végzik a feldolgozás nagy részét." +none = "Nincs" +noneDesc = "Még nem futtat Pipeline-okat. Bármikor bekapcsolhatja őket." +standard = "Standard" +standardDesc = "Néhány Pipeline újrafeldolgozza a beérkező PDF-eket." + +[portal.billing.prepaid.posture] +essentials = "Alapok" +essentialsDesc = "Osztályozás, az alapértelmezett, plusz megosztás" +governed = "Irányított" +governedDesc = "Biztonsággal és útválasztással bővül" +regulated = "Szabályozott" +regulatedDesc = "Minden kategória, beleértve a megfelelést, megőrzést és bevitelt" + +[portal.billing.prepaid.review] +download = "Árajánlat letöltése (PDF)" +downloading = "Előkészítés…" +share = "megosztás jóváhagyásra" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "25 MB alatti fájlok, nincs adatdíj" +heavy = "Nehéz" +heavyDesc = "Szkennelt vagy képekben gazdag, rendszeresen 50 MB+" +standard = "Standard" +standardDesc = "Többnyire kicsi, néhány szkennelés 25 MB felett" + +[portal.billing.prepaid.state] +exhausted = "Elfogyott" +healthy = "Bőven maradt" +low = "Kevés maradt" [portal.billing.spendLimit] adjustLimit = "Limit módosítása" @@ -6494,6 +6713,8 @@ label = "Várhatóan túllépi." [portal.billing.spendThisMonth] eyebrow = "Költés ebben a hónapban" +freeRemaining_one = "{{formatted}} ingyenes PDF maradt" +freeRemaining_other = "{{formatted}} ingyenes PDF maradt" processed_one = "{{formattedCount}} PDF feldolgozva." processed_other = "{{formattedCount}} PDF feldolgozva." processedWithRate_one = "{{formattedCount}} PDF feldolgozva, darabonként {{rate}} áron." @@ -6510,6 +6731,7 @@ reachedTitle = "Havi költési limit elérve" title = "Nem sikerült megnyitni a Stripe portált" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "felhasználva a(z) {{allowance}} ingyenes PDF-ből" capSuffix_other = "felhasználva a(z) {{allowance}} ingyenes PDF-ből" eyebrow = "Processor próbaidőszak" @@ -6534,6 +6756,376 @@ signature = "aláírás" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Új kapcsolat" +createTitleFor = "{{name}} csatlakoztatása" +delete = "Törlés" +edit = "Szerkesztés" +editTitle = "Kapcsolat szerkesztése" +unsupportedType = "Ez a kapcsolattípus itt nem szerkeszthető." + +[portal.connections.categories.advanced] +description = "Olyan szolgáltatáshoz, amelyet még nem támogatunk." +label = "Speciális" + +[portal.connections.categories.audit] +description = "Rögzítse, mi történt egy dokumentummal ott, ahol az auditorai már keresik." +label = "Audit- és megfelelőségi naplózás" + +[portal.connections.categories.notify] +description = "Értesítsen egy másik rendszert arról, hogy valami történt." +label = "Értesítés és munkafolyamat" + +[portal.connections.categories.security] +description = "Vizsgáljon vagy címkézzen egy dokumentumot, mielőtt továbbhalad." +label = "Biztonság és osztályozás" + +[portal.connections.categories.signing] +description = "Küldjön ki egy dokumentumot aláírásra vagy közjegyzői hitelesítésre." +label = "E-aláírás és közjegyzői hitelesítés" + +[portal.connections.categories.storage] +description = "Ahova a feldolgozott dokumentumok kerülnek – bucket, meghajtó, ticket vagy oldal." +label = "Fájl és csatolás" + +[portal.connections.commonFields.accessToken] +helperText = "A Stirling ezt titkosítva tárolja, és többé nem jeleníti meg." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Az ezt a kapcsolatot használó lépések csak ezen URL alatti útvonalakat hívhatnak meg, másik hostot soha." +label = "Szerver URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Jelszó" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Felhasználónév" + +[portal.connections.commonFields.webhookUrl] +helperText = "Illessze be a szolgáltatás által adott URL-t. Csak az alatta lévő útvonalak hívhatók meg." +label = "Webhook URL" + +[portal.connections.fields] +name = "Kapcsolat neve" +namePlaceholder = "pl. Éles" + +[portal.connections.picker] +cancel = "Mégse" +createNew = "Új kapcsolat..." +placeholder = "Kapcsolat kiválasztása" +save = "Kapcsolat mentése" + +[portal.connections.picker2] +back = "Integráció módosítása" +heading = "Mit szeretne csatlakoztatni?" +noResultsBody = "Ezt még nem támogatjuk – jelezze nekünk, és megvizsgáljuk a hozzáadását." +noResultsTitle = "Nincs találat erre: \"{{query}}\"" +searchPlaceholder = "Integrációk keresése – próbálja: \"sign\", \"ocr\" vagy egy terméknév" + +[portal.connections.types.api] +description = "Hívjon meg bármilyen HTTP API-t egy szabályból. Ön választja ki az URL-t, a hitelesítést és a payloadot." +label = "Egyéni API" + +[portal.connections.types.api.fields.authType] +label = "Hitelesítés" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Felhasználónév és jelszó" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token egy egyéni headerben" + +[portal.connections.types.api.fields.authType.options.none] +label = "Nincs" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Bejelentkezés tokenért" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Az ezt a kapcsolatot használó lépések csak ezen URL alatti útvonalakat hívhatnak meg, másik hostot soha." +label = "Alap URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header neve" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Bejelentkezési útvonal" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Jelszó" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Ha az API letöltési linkkel válaszol, csak ezek a hostok kérhetők le. Vesszővel válassza el. Hagyja üresen, hogy csak az alap URL saját hostja legyen engedélyezett." +label = "Engedélyezett eredményhostok" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "A token küldése ezzel a headerrel" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "A válaszheader, amelyben a bejelentkezési hívás visszaadja a tokent, pl. X-Auth-Token." +label = "Token válaszheader" + +[portal.connections.types.api.fields.username] +label = "Felhasználónév" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Dokumentumok kártevő-ellenőrzése egy Ön által futtatott szerveren – semmi sem hagyja el a hálózatát." +label = "ClamAV (saját üzemeltetésű)" + +[portal.connections.types.cloudmersive] +description = "Vírus- és tartalomellenőrzés dokumentumokhoz egy szabályban." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Makrókat, végrehajtható fájlokat, szkripteket vagy beágyazott objektumokat tartalmazó dokumentumok blokkolása – nem csak ismert kártevők." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Feldolgozott dokumentumok csatolása egy Confluence oldalhoz." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Dokumentumok küldése ConsignO Cloud (Notarius) szolgáltatásba elektronikus aláíráshoz és közjegyzői hitelesítéshez." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Az Ön ConsignO Cloud környezete, beleértve a /api/v1 részt." +label = "Szerver URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Jelszó" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Csak akkor szükséges, ha a szervezete tenantokat használ." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Felhasználónév" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Üzenet közzététele Discordon, amikor egy szabály feldolgoz egy dokumentumot." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Audit esemény indexelése Elasticsearchben minden alkalommal, amikor egy szabály kezel egy dokumentumot." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "FTP vagy FTPS szerver lekérdezése új dokumentumokért." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Kapcsolati mód" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktív" +passive = "Passzív (ajánlott)" + +[portal.connections.types.ftp.fields.security] +helperText = "A sima FTP titkosítás nélkül küldi a hitelesítő adatokat és fájlokat; válassza az FTPS-t, ha a szerver támogatja." +label = "Titkosítás" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Nincs (sima FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Üzenet közzététele egy Google Chat térben, amikor egy szabály fut." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Feldolgozott dokumentumok csatolása egy Jira issue-hoz." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Dokumentum vagy értesítés küldése emailben, amikor egy szabály fut." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Feldolgozott dokumentumok elhelyezése a Nextcloudban." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App jelszó" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Személyes adatok felismerése dokumentumszövegben egy Ön által futtatott Presidio szerverrel." +label = "Presidio (saját üzemeltetésű)" + +[portal.connections.types.purview] +description = "Microsoft Purview bizalmassági címkék olvasása és alkalmazása PDF-eken." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcionális. Csak címkék név szerinti kiválasztásához szükséges; a címkézés enélkül is működik." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Minden címkére SiteId-ként kerül rábélyegzésre, hogy a Purview-kompatibilis eszközök tudják, melyik szervezet alkalmazta." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Egy S3 bucket, amelyből a források olvashatnak, és amelybe a pipeline kimenetek írhatnak." +label = "S3 tároló" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Hagyja üresen AWS esetén. Állítsa be MinIO-hoz vagy más S3-kompatibilis tárolóhoz." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Régió" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Dokumentum vagy értesítés küldése emailben, amikor egy szabály fut." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "SFTP (SSH) szerver lekérdezése új dokumentumokért." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcionális. A szerver SHA-256 host key fingerprintje (innen: `ssh-keyscan host | ssh-keygen -lf -`). Ha be van állítva, csak az ezt a kulcsot bemutató szerver fogadható el; ha üres, az első csatlakozáskor látott kulcs rögzítésre kerül, és a későbbi változások elutasításra kerülnek." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Kulcs jelszava" + +[portal.connections.types.sftp.fields.password] +helperText = "Hagyja üresen, ha helyette privát kulccsal hitelesít." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcionális. Illesszen be egy OpenSSH privát kulcsot, hogy jelszó helyett kulccsal hitelesítsen." +label = "Privát kulcs" + +[portal.connections.types.slack] +description = "Üzenet közzététele Slackben, amikor egy szabály feldolgoz egy dokumentumot." +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS hálózati megosztás figyelése (Windows vagy Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Hálózati megosztás" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcionális. A Windows domain vagy munkacsoport, ha a szervere használ ilyet." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Megosztás" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Audit esemény küldése a Splunknak minden alkalommal, amikor egy szabály kezel egy dokumentumot." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Audit esemény küldése egy Sumo Logic HTTP collectornak." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Üzenet közzététele egy Teams csatornában, amikor egy szabály fut." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Tegyen közzé minden dokumentumot, vagy csak a részleteket egy Ön által választott URL-re." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Zap vagy Make forgatókönyv indítása egy szabályból." +label = "Zapier & Make" + +[portal.docs] +browse = "Dokumentáció böngészése" +viewSource = "Forrás megtekintése GitHubon" + [portal.docs.authentication] codeCaption = "minden kérés" eyebrow = "ELSŐ LÉPÉSEK" @@ -6624,6 +7216,9 @@ eyebrow = "KÉSZSÉGEK" lead = "Csomagolt, névvel ellátott képességek, amelyeket az ügynöke egyetlen eszközként hív meg. Minden készség determinisztikus műveletlánc, csatolt evalokkal." title = "Ügynökkészségek" +[portal.docs.toc] +title = "Ezen az oldalon" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCIA" @@ -6697,6 +7292,8 @@ type = "Típus" user = "Felhasználó" [portal.documents.queue.empty] +connectSource = "Forrás csatlakoztatása" +createPipeline = "Pipeline létrehozása" description = "Ahogy a források dokumentumokat küldenek a pipeline-jaiba, azok itt jelennek meg felülvizsgálatra." title = "Nincsenek dokumentumok a sorban" @@ -6712,11 +7309,11 @@ editorAction = "Szerkesztő" empty = "Egy dokumentum sem felel meg ennek a szűrőnek." rowActions = "Sorműveletek" sensitiveLabel = "Érzékeny" -sensitiveTitle = "Érzékeny — hozzáférés szükséges" [portal.documents.table.columns] action = "Pipeline / művelet" document = "Dokumentum" +labels = "Labels" product = "Termék" status = "Állapot" time = "Idő" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Utoljára látható" region = "Régió" status = "Állapot" +target = "Target" version = "Verzió" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Ez a nézet váratlan hibába ütközött. Próbálja újra, vagy retry = "Próbálja újra" title = "Valami hiba történt ezen az oldalon" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Vissza" +body = "Töltse le az asztali alkalmazást, vagy üzemeltesse saját maga a szervert." +done = "Kész" +guide = "A(z) {{name}} útmutató megnyitása" +openInBrowser = "Megnyitás inkább böngészőben" +sectionDesktop = "Asztali alkalmazás" +sectionSelfHosted = "Saját üzemeltetésű" +title = "A Stirling PDF Editor telepítése" + +[portal.home.download.docker] +detailBody = "Futtassa a hivatalos image-et a szerverén." +note = "Megnyílik a :8080 címen. Alapértelmezett bejelentkezés: admin / stirling." +tagline = "A szerver futtatása konténerben" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Telepítse a hivatalos Helm chartot." +note = "Adjon hozzá egy PVC-t a /configs könyvtárhoz a beállítások megőrzéséhez." +tagline = "Telepítés klaszterbe Helm segítségével" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Natív buildek az Ön disztribúciójához." +guideBtn = "Linux telepítési lehetőségek megtekintése" +note = "AppImage, .deb és .rpm buildek érhetők el a gyakori disztribúciókhoz." +tagline = "Natív asztali alkalmazás az Ön disztribúciójához" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Vagy Homebrew használatával" +detailBody = "Futtassa a telepítőt, vagy használja a Homebrew-t." +downloadBtn = "Letöltés macOS rendszerre" +tagline = "Natív alkalmazás Apple silicon és Intel gépekhez" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Futtassa a .jar fájlt bare metal szerveren." +downloadBtn = "A .jar letöltése" +note = "Java 21+ szükséges. A :8080 címen nyílik meg; alapértelmezett bejelentkezés: admin / stirling." +runLabel = "Ezután futtassa" +tagline = "Futtassa a .jar fájlt bare metal szerveren" +title = "Manuális szerverbeállítás" + +[portal.home.download.windows] +altLabel = "Vagy winget használatával" +detailBody = "Futtassa a telepítőt, vagy használjon csomagkezelőt." +downloadBtn = "Telepítő letöltése" +tagline = "Natív alkalmazás Microsoft Windows rendszerhez" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktív" -invite = "Csapattagok meghívása" +activeOfDeployed = "{{active}} / {{total}} aktív ebben a hónapban" name = "Stirling PDF Editor" open = "Megnyitás böngészőben" updated = "frissítve: {{time}}" +[portal.home.editor.deploy] +finish = "Telepítés befejezése" +options = "Telepítési lehetőségek megtekintése" +start = "Editor telepítése" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Jó napot" evening = "Jó estét" morning = "Jó reggelt" -[portal.home.onboarding] -dismiss = "Beállítás elvetése" -notStarted = "Nincs elkezdve" -progress = "{{done}} / {{total}} kész" -title = "Fejezze be a beállítást" - -[portal.home.onboarding.enterprise] -body = "Szervezetszintű SSO + SCIM + RBAC, lekötött mennyiségi árképzés és air-gapped telepítés." -cta = "Próba indítása" -ctaQuote = "Ajánlat kérése" -lead = "250+ alkalmazott esetén." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Telepítse az asztali alkalmazást, örökre ingyen." -title = "PDF Editor letöltése" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktív · {{recommended}} ajánlott" -chip = "{{active}} aktív" -title = "Szabályzatok bekapcsolása" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} csatlakoztatva · minden PDF szabályozva van, ahová érkezik" -chip = "{{connected}} csatlakoztatva" -title = "Források csatlakoztatása" - -[portal.home.quickActions] -subtitle = "Mai fő feladatok" -title = "Gyors műveletek" - -[portal.home.quickActions.buildPipeline] -blurb = "3 lépéses összeállító a tipizált műveletkönyvtár felett" -title = "Pipeline létrehozása" - -[portal.home.quickActions.connectSource] -blurb = "S3, ügynökök, webhookok, figyelt mappák" -title = "Forrás csatlakoztatása" - -[portal.home.quickActions.issueApiKey] -blurb = "Korlátozott hatókörű kulcs sebességkorlátokkal és IP allowlisttel" -title = "API kulcs kiadása" - [portal.infrastructure] manageEditorDeployment = "Editor telepítés kezelése" sectionsAriaLabel = "Infrastruktúra szakaszai" @@ -6878,13 +7541,9 @@ heading = "API kulcsok" subheading = "Korlátozott hatókörű hitelesítő adatok kulcsonkénti sebességkorlátokkal, engedélyekkel és IP allowlistekkel." [portal.infrastructure.apiKeys.card] -allowedIps = "Engedélyezett IP-k" -anyIp = "Bármely IP (nincs allowlist)" created = "Létrehozva" lastUsed = "Utoljára használva" -permissions = "Engedélyek" -rateLimit = "Sebességkorlát" -rateLimitValue = "{{value}} kérés/perc" +revoke = "Kulcs visszavonása" usageMonth = "E havi használat" usageToday = "Mai használat" @@ -6892,6 +7551,15 @@ usageToday = "Mai használat" description = "Hozzon létre egy korlátozott hatókörű kulcsot a Stirling API hívásának megkezdéséhez." title = "Még nincsenek API kulcsok" +[portal.infrastructure.apiKeys.error] +load = "Nem sikerült betölteni az API-kulcsait. Kérjük, próbálja újra." + +[portal.infrastructure.apiKeys.revoke] +body = "Visszavonja ezt: \"{{name}}\"? Minden integráció, amely még ezt a kulcsot használja, azonnal 401 hibákat fog kapni. Ez nem vonható vissza." +cancel = "Mégse" +confirm = "Kulcs visszavonása" +title = "API-kulcs visszavonása" + [portal.infrastructure.audit] filterAriaLabel = "Audit események szűrése kategória szerint" heading = "Auditnaplók" @@ -6935,6 +7603,7 @@ all = "Összes" auth = "Auth" config = "Konfiguráció" elevation = "Jogosultság-emelés" +policy = "Házirend" processing = "Feldolgozás" security = "Biztonság" @@ -6944,19 +7613,30 @@ title = "Nincs hozzáférése" [portal.infrastructure.audit.metrics] config = "Konfiguráció" -elevation = "Jogosultság-emelés" +policy = "Házirend" processing = "Feldolgozás" totalEvents = "Összes esemény · 24 óra" +[portal.infrastructure.auditCatLabel] +auth = "Hitelesítés" +config = "Konfiguráció" +elevation = "Jogosultságszint-emelés" +policy = "Házirend" +processing = "Feldolgozás" +security = "Biztonság" + +[portal.infrastructure.auditStatusLabel] +danger = "Hiba" +info = "Információ" +success = "Sikeres" +warning = "Figyelmeztetés" + [portal.infrastructure.createKey] cancel = "Mégse" createKey = "Kulcs létrehozása" done = "Kész" -ipAllowlistHelper = "Vesszővel elválasztott CIDR tartományok. Hagyja üresen bármely IP engedélyezéséhez." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Kulcs neve" keyNamePlaceholder = "pl. Production · ingest" -permissionsLabel = "Engedélyek" secretKeyCaption = "Titkos kulcs" secretWarning = "Tárolja ezt secrets managerben. A Stirling csak hash-t tárol — később nincs mód a visszaállítására." subtitle = "A kulcs hatókörét a szükséges minimumra korlátozza. Bármikor rotálhatja vagy visszavonhatja." @@ -6964,207 +7644,9 @@ subtitleCreated = "Másolja ki most ezt a titkot — többé nem jelenik meg." title = "API kulcs létrehozása" titleCreated = "Kulcs létrehozva" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/perc" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Telepítette" -environment = "Környezet" -product = "Termék" -status = "Állapot" -version = "Verzió" -when = "Mikor" - -[portal.infrastructure.deployments.recent] -heading = "Legutóbbi telepítések" -subheading = "A legújabb bevezetések termékek és környezetek szerint." - -[portal.infrastructure.deployments.regionColumns] -instances = "Példányok" -latency = "Késleltetés" -load = "Terhelés" -p99 = "P99" -region = "Régió" -status = "Állapot" -throughput = "Átbocsátás" -uptime = "Üzemidő" -version = "Verzió" - -[portal.infrastructure.deployments.regions] -heading = "Régiók" -subheading = "Élő állapot minden telepített Stirling régióhoz — késleltetés, terhelés és bevezetési verzió." - -[portal.infrastructure.deployments.regions.empty] -description = "A telepített régiók itt jelennek meg, amint a munkaterülete ki van építve." -title = "Nincsenek telepített régiók" - -[portal.infrastructure.models] -heading = "Modellek" -msValue = "{{value}} ms" -subheading = "A modellkatalógus és az útválasztás, amely a dokumentumfeldolgozást működteti a munkaterületén." - -[portal.infrastructure.models.byom] -description = "Regisztráljon egy on-prem vagy self-hosted modellt, és rögzítse egy régióhoz adatrezidencia-kötött feldolgozáshoz." -title = "Hozza saját modelljét" - -[portal.infrastructure.models.catalogue] -heading = "Katalógus" -sub = "A munkaterületéhez elérhető kezelt modellek élő késleltetéssel és költséggel." -subEnterprise = "Kezelt, saját és on-prem modellek — régiónkénti rögzítéssel." - -[portal.infrastructure.models.catalogue.empty] -description = "A munkaterület katalógusában lévő modellek itt jelennek meg." -title = "Nincsenek elérhető modellek" - -[portal.infrastructure.models.columns] -cost = "Költség" -latency = "Késleltetés" -load = "Terhelés" -model = "Modell" -status = "Állapot" -type = "Típus" -version = "Verzió" - -[portal.infrastructure.models.metrics] -activeModels = "Aktív modellek" -avgLatency = "Átlagos késleltetés" -included = "Tartalmazza" -monthlySpend = "Havi modellköltség" - -[portal.infrastructure.models.routing] -empty = "Nincsenek konfigurált útválasztási szabályok." -heading = "Útválasztási szabályok" -sub = "Melyik modell kezeli az egyes műveleteket. Az alapértelmezett akkor érvényes, ha nincs szűkebb egyező szabály." -subLocked = "Műveletek irányítása adott modellekhez — fizetős csomagokban érhető el." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Frissítsen Pro csomagra annak szabályozásához, hogy melyik modell kezelje az egyes műveleteket és dokumentumtípusokat." -title = "A modellútválasztás fizetős funkció" - -[portal.infrastructure.models.routingColumns] -default = "Alapértelmezett" -docType = "Dokumentumtípus" -modelForAria = "Modell ehhez: {{operation}}" -operation = "Művelet" -routedTo = "Ide irányítva" - -[portal.infrastructure.security.access.byok] -description = "Adjon meg egy kulcsot a saját KMS-éből. A Stirling ezzel titkosít, de továbbra is olvasni tud." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "A kulcsok soha nem hagyják el a KMS-ét. A Stirling csak titkosított szöveget tárol." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "A Stirling kezeli a titkosítási kulcsokat. A legegyszerűbb — nincs kulcsművelet az Ön oldalán." -label = "Stirling által tárolt kulcsok" - -[portal.infrastructure.security.accessPolicy] -heading = "Dokumentum-hozzáférési szabályzat" -subheading = "Szabályozza, hogy ki fejtheti vissza a feldolgozott dokumentumokat nyugalmi állapotban." - -[portal.infrastructure.security.attestations] -heading = "Megfelelőségi tanúsítások" -noReport = "Nincs elérhető jelentés" -subheading = "Keretrendszerenkénti auditállapot, jelentésekkel a tanúsított kontrollokról." -viewReport = "Jelentés megtekintése →" - -[portal.infrastructure.security.compliance] -heading = "Megfelelőség" -subheading = "A Stirling platformot lefedő tanúsítások és minősítések." - -[portal.infrastructure.security.empty] -description = "A munkaterület biztonsági konfigurációja itt fog megjelenni." -title = "A biztonsági állapot nem érhető el" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK esetén a titkosítási kulcsok soha nem hagyják el a KMS-ét. A Stirling csak olyan titkosított szöveget tárol és dolgoz fel, amelyet bármikor visszavonhat." -title = "A Stirling nem tudja visszafejteni a dokumentumait" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nincsenek konfigurált IP tartományok — minden IP engedélyezett." -heading = "IP allowlist" -sub = "Az API hozzáférés ezekre a CIDR tartományokra van korlátozva." -subLocked = "API hozzáférés korlátozása ismert IP tartományokra — fizetős csomagokban érhető el." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Frissítsen Pro csomagra az API hozzáférés adott hálózatokra korlátozásához." -title = "Az IP allowlist fizetős funkció" - -[portal.infrastructure.security.ipColumns] -added = "Hozzáadva" -addedBy = "Hozzáadta" -cidr = "CIDR" -label = "Címke" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmus" -heading = "Titkosítási kulcsok kezelése" -keyId = "Kulcsazonosító" -lastRotated = "Utolsó rotálás" -rotateKey = "Kulcs rotálása" -rotationPolicy = "Rotálási szabályzat" -subheading = "A dokumentumokat nyugalmi állapotban titkosító kulcsok felügyelete — ki fejthet vissza, és hogyan rotálódnak a kulcsok." - -[portal.infrastructure.security.managedBanner] -description = "A bring-your-own-key (BYOK) és hold-your-own-key (HYOK) kulcsfelügyelet Enterprise csomagban érhető el. Frissítsen, hogy kulcsokat adjon meg a saját KMS-éből." -title = "A kulcsokat a Stirling kezeli az Ön csomagjában" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Ázsia és Csendes-óceáni térség" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR adatperem" -label = "Európai Unió" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Egyesült Államok" - -[portal.infrastructure.security.residencyHeader] -heading = "Adatrezidencia" -subheading = "Ahol a dokumentumok tárolása és feldolgozása történik." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} felhasználva" - -[portal.infrastructure.storage.empty] -description = "A csatlakoztatott tárhely és használat itt jelenik meg." -title = "Nincs konfigurált tárhely" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktív" -activeRange = "0–{{value}} nap" -archived = "Archiválva" -coldStorage = "hideg tároló" -deleted = "Törölve" -never = "soha" -purged = "véglegesen törölve" - -[portal.infrastructure.storage.providers] -connect = "Csatlakozás" -connected = "Csatlakoztatva" -heading = "Csatlakoztatott szolgáltatók" -subheading = "Ahol a feldolgozott artefaktumok íródnak." - -[portal.infrastructure.storage.retention] -heading = "Megőrzés" -subheading = "Meddig maradnak meg az artefaktumok az életciklus szerinti törlés előtt." -windowLabel = "Alapértelmezett megőrzési időszak" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} nap" -days_other = "{{count}} nap" -never = "Soha ne törölje" - -[portal.infrastructure.storage.totalUsage] -heading = "Teljes használat" -progressLabel = "Felhasznált tárhely" -subheading = "Az összes csatlakoztatott szolgáltatónál felhasznált tárhely." +revoked = "Visszavonva" [portal.infrastructure.tabs] apiKeys = "API kulcsok" @@ -7174,6 +7656,49 @@ models = "Modellek" security = "Biztonság" storage = "Tárhely" +[portal.integrations] +availableHeading = "Elérhető" +comingSoonHeading = "Hamarosan" +connect = "Csatlakoztatás" +connectionCount_one = "{{count}} kapcsolat" +connectionCount_other = "{{count}} kapcsolat" +customApi = "Egyéni API" +searchPlaceholder = "Integrációk keresése" +subtitle = "Minden, amellyel a Stirling kommunikálni tud. A hitelesítő adatok itt egyszer jönnek létre, majd források, házirendek és folyamatok hivatkoznak rájuk." +title = "Integrációk" +typedName = "{{tool}} neve" + +[portal.integrations.filters] +all = "Összes" +audit = "Audit" +connected = "Csatlakoztatva" +notify = "Értesítés" +security = "Biztonság" +signing = "Aláírás" +storage = "Tárolás" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Csatlakoztatva" + +[portal.integrations.table] +integration = "Integráció" +worksWith = "Ezzel működik" + +[portal.integrations.worksWith] +pipelines = "Folyamatok" +policies = "Házirendek" +sources = "Források" + +[portal.legal] +draft = "{{label}} · piszkozat" +loadError = "Nem sikerült betölteni ezt a dokumentumot. Kérjük, próbálja újra." +loading = "Betöltés…" +title = "Jogi dokumentum" + [portal.nav] agent-builder = "Agent Builder" components = "Komponensek" @@ -7182,33 +7707,17 @@ documents = "Dokumentumok" editor = "Editor" home = "Kezdőlap" infrastructure = "Infrastruktúra" +integrations = "Integrációk" pipelines = "Pipeline-ok" policies = "Szabályzatok" procurement = "Beszerzés" -settings = "Beállítások" sources = "Források" usage = "Használat és számlázás" users = "Felhasználók" -[portal.notifications] -markAllRead = "Összes megjelölése olvasottként" -title = "Értesítések" -viewAll = "Összes megtekintése" - -[portal.notifications.ariaLabel] -none = "Értesítések, nincs olvasatlan" -unread_one = "Értesítések, {{count}} olvasatlan" -unread_other = "Értesítések, {{count}} olvasatlan" - -[portal.notifications.count] -allRead = "mind olvasott" -loading = "betöltés" -new_one = "{{count}} új" -new_other = "{{count}} új" - -[portal.notifications.empty] -description = "Nincsenek új értesítések." -title = "Minden naprakész" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Minden automatizált dokumentum-pipeline a backendben: műveletek rendezett lánca források egy halmazán, trigger alapján futtatva. Kattintson egy sorra a lépések és források megtekintéséhez." @@ -7218,54 +7727,87 @@ title = "Pipeline-ok" newPipeline = "Új pipeline" [portal.pipelines.builder] -addStep = "Eszköz hozzáadása" +activate = "Activate" back = "Vissza a pipeline-okhoz" +cannotFollow = "Nem tudja fogadni ezt: {{produced}}" +chooseAccount = "Válasszon fiókot" +chooseDestination = "Válasszon célhelyet" +chooseOperation = "Válassza ki, mit végezzen ez a lépés" +chooseSource = "Válasszon forrást" discard = "Módosítások elvetése" -enabled = "Engedélyezve" +inputs = "Bemenet" +inputSource = "Bemeneti forrás" +inputTrigger = "Trigger" keepEditing = "Szerkesztés folytatása" -needsUpload = "Feltöltött fájl szükséges" +moreActions = "More actions" +needsConfiguring = "Beállítás szükséges" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nincs a keresésnek megfelelő eszköz." -pipelineSettings = "Pipeline beállításai" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Eszközök keresése" -selectToolBody = "Adjon hozzá egy eszközt a pipeline felépítéséhez." -selectToolTitle = "Még nincsenek eszközök" -toolSettings = "Eszköz beállításai" +sendToSystem = "Küldés másik rendszerbe" +stepsIncompatible = "Ezek a lépések nem futtathatók azon, amit az előző lépés előállít: {{tools}}." +stepsNeedSetup = "Ezeket a lépéseket mentés előtt még be kell állítani: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Ismeretlen művelet, változatlanul megtartva." unsavedBody = "Nem mentett módosításai vannak. Mentse őket kilépés előtt, vagy elveti őket?" unsavedTitle = "Nem mentett módosítások" -uploadUnsupported = "A feltöltött fájlok még nem támogatottak pipeline-okban, ezért ezek a lépések nem menthetők: {{tools}}." usesDefaults = "Alapértelmezett beállításokkal fut" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Az előző lépés összes fájlját egyesíti" +fan-out = "Az előző lépés minden fájljára egyszer fut le" +format-mismatch = "{{accepts}} szükséges, de az előző lépés ezt állítja elő: {{produced}}" +output-uncertain = "Lehet, hogy nem fut: az előző lépés kimenete a beállításától függ" +source-mismatch = "{{accepts}} szükséges, de ennek a folyamatnak a bemenete: {{produced}}" +undeclared-operation = "Nem ellenőrizhető, hogy ez a lépés mit fogad el" [portal.pipelines.composer] addTool = "Eszköz hozzáadása" -cancel = "Mégse" -chainEmpty = "Adjon hozzá egy eszközt a pipeline építésének megkezdéséhez." create = "Pipeline létrehozása" -directory = "Kimeneti mappa" -directoryHelp = "Abszolút elérési út a szerveren. A konfigurált engedélyezett mappákon belül kell lennie." +createPaused = "Create paused" editingUnsupported = "Ezeknek az eszközparamétereknek a szerkesztésre való megjelenítése még nem támogatott." -moveDown = "Mozgatás le" -moveUp = "Mozgatás fel" +editSource = "Edit source" name = "Név" namePlaceholder = "pl. Kitakarítási átvizsgálás" -noSources = "Még nincsenek csatlakoztatott források. A pipeline továbbra is futtatható közvetlenül neki átadott fájlokon." noToolSettings = "Ennek az eszköznek nincsenek konfigurálható beállításai." -operations_one = "Művelet ({{count}})" -operations_other = "Műveletek ({{count}})" output = "Kimenet" -removeStep = "Művelet eltávolítása" save = "Módosítások mentése" scheduleEvery = "Futtatás minden" -sources = "Források" -sourcesLoading = "Források betöltése..." trigger = "Trigger" triggerManual = "Csak manuális" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "nap" hours = "óra" minutes = "perc" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Törli ezt: \"{{name}}\"? Ez nem vonható vissza." cancel = "Mégse" @@ -7273,28 +7815,62 @@ confirm = "Törlés" title = "Törli a pipeline-t?" [portal.pipelines.detail] +clearHistory = "Előzmények törlése" delete = "Pipeline törlése" run = "Futtatás most" [portal.pipelines.empty] action = "Pipeline létrehozása" +connectSource = "Forrás csatlakoztatása" description = "Hozza létre első pipeline-ját: válassza ki a forrásokat, amelyeken fut, fűzze össze a műveleteket, és adja meg, hová kerüljön a kimenet." title = "Még nincsenek pipeline-ok" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktív" paused = "Szüneteltetve" total = "Pipeline-ok" -[portal.pipelines.output] -folder = "Írás mappába" -inline = "Fájlok visszaadása" - [portal.pipelines.run] +allProcessed_one = "Nincs mit futtatni: a forrás {{count}} dokumentuma már fel lett dolgozva." +allProcessed_other = "Nincs mit futtatni: a forrásokban található mind a(z) {{count}} dokumentum már fel lett dolgozva." completed_one = "Futtatás befejezve." completed_other = "Mind a(z) {{count}} futtatás befejeződött." empty = "Nincs mit futtatni: a forrásoknak nem voltak feldolgozandó dokumentumaik." failed = "A futtatás sikertelen: {{error}}" +inFlight = "Nincs új futtatnivaló: a dokumentumok feldolgozása még folyamatban van egy korábbi futtatásból." +parked_one = "Nincs mit futtatni: {{count}} dokumentum korábban meghiúsult és félre lett téve. Javítsa ki az okot, majd törölje az előzményeket az újrapróbálkozáshoz." +parked_other = "Nincs mit futtatni: {{count}} dokumentum korábban meghiúsult és félre lett téve. Javítsa ki az okot, majd törölje az előzményeket az újrapróbálkozáshoz." running = "A futtatás elindult; még folyamatban van." timeout = "A futtatás a vártnál tovább tart; lehet, hogy a háttérben még befejeződik." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Források" status = "Állapot" steps = "Lépések" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Mappafigyelés" @@ -7314,12 +7891,18 @@ manual = "Manuális" schedule = "Ütemezett" [portal.policies] +defaultName = "{{category}} házirend" subtitle = "Állandó automatizmusok, amelyek minden dokumentumon érvényesítenek egy eszköz-pipeline-t. Minden szabályzat feltöltéskor vagy exportáláskor lép működésbe, lefuttatja az eszközláncát, és az eredeti mellett menti a kikényszerített verziót." title = "Szabályzatok" [portal.policies.card] comingSoon = "Frissítés Enterprise-ra" notSetUp = "Nincs beállítva" +requiresAiEngine = "AI motor szükséges" + +[portal.policies.categories.classification] +desc = "Azonosítja minden dokumentum típusát feltöltéskor, és metaadatcímkékkel látja el a rendszerezéshez és kereséshez." +label = "Osztályozás" [portal.policies.categories.compliance] desc = "HIPAA, GDPR, SOC 2 vagy FedRAMP követelmények érvényesítése minden dokumentumon." @@ -7344,6 +7927,13 @@ label = "Biztonság" [portal.policies.config] scopeAll = "Minden dokumentum" +[portal.policies.config.classification] +summary = "Minden feltöltött dokumentumot osztályoz a csapat címkéi alapján, és metaadatcímkékkel látja el." + +[portal.policies.config.classification.rules] +0 = "Osztályozás" +1 = "Metaadatok címkézése" + [portal.policies.config.compliance] summary = "Ellenőrzi a dokumentumokat szabályozási keretrendszerek alapján, mielőtt elhagynák a rendszert." @@ -7371,6 +7961,29 @@ minConfidence = "Min. megbízhatóság" 2 = "Név" 3 = "Normalizálás" +[portal.policies.config.purview] +label = "Microsoft Purview érzékenységi címke alkalmazása" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Címke ID" +labelIdHelp = "A címke GUID-ja a Purview portálról. A címke alkalmazása metaadatokat ír a PDF-re; titkosítani nem tud, ahhoz a Microsoft kliens szükséges." +labelName = "Címke neve (opcionális)" +labelNameHelp = "Az ID mellett rögzítve, hogy az emberek és eszközök felismerjék a címkét." + +[portal.policies.config.purview.fields.method] +help = "A Purview megkülönbözteti a házirend által alkalmazott címkét attól, amelyet egy személy választott." +label = "Hogyan legyen rögzítve a címke" +privileged = "Személy által választottként" +standard = "Automatikusan alkalmazottként" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview címke olvasása" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Beolvassa a dokumentumon már meglévő címkét, hogy a későbbi lépések ez alapján működhessenek." + [portal.policies.config.retention] summary = "Érvényesíti, meddig maradjanak meg a dokumentumok, mikor kerüljenek archiválásra, és mikor legyenek törölve." @@ -7438,6 +8051,7 @@ title = "Még nincs tevékenység" [portal.policies.endpoints] addWatermark = "Vízjel" autoRedact = "PII kitakarása" +classifyAndLabel = "Osztályozás" compressPdf = "Tömörítés" flatten = "Lapítás" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "A házirendek mentve vannak, és a kapcsolat helyreállása után retry = "Újra" title = "Backend nem érhető el" +[portal.policies.operations] +change = "A lépés tevékenységének módosítása" +noResults = "Nincs ilyen lépés. Próbáljon meg egy terméknevet, vagy ezt: \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Lépések keresése – próbálja ezt: \"scan\", \"attach\" vagy egy terméknevet" + +[portal.policies.operations.bodyMode] +binary = "Nyers fájlbájtok" +json = "JSON payloadban" +multipart = "Fájlfeltöltésként (multipart)" + +[portal.policies.operations.clamavScan] +description = "A saját ClamAV szervere ellenőrzi a dokumentumot; semmi sem hagyja el a hálózatát." +label = "Víruskeresés (saját üzemeltetésű)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Elutasítja a makrókat, futtatható fájlokat, szkripteket vagy beágyazott objektumokat tartalmazó dokumentumokat." +label = "Aktív tartalom blokkolása" + +[portal.policies.operations.cloudmersiveScan] +description = "A Cloudmersive ellenőrzi a dokumentumot, és a futtatás leáll, ha nem tiszta." +label = "Víruskeresés" + +[portal.policies.operations.confluenceAttach] +description = "A feldolgozott dokumentumot egy oldalhoz csatolja." +label = "Csatolás Confluence oldalhoz" + +[portal.policies.operations.consignoSubmit] +description = "Elküldi a dokumentumot aláírásra a ConsignO Cloudba." +label = "Küldés aláírásra (ConsignO)" +note = "Elküldi a dokumentumot. Az aláírt példány lekérése még nem támogatott, mert egy lépés nem tud eredményt átadni egy későbbi lépésnek." + +[portal.policies.operations.customApiCall] +description = "Ön állítja össze a hívást: útvonal, method, törzs és válaszkezelés." +label = "Egyéni API hívása" + +[portal.policies.operations.discordNotify] +description = "Értesít egy csatornát, hogy a házirend kezelt egy dokumentumot." +label = "Üzenet közzététele Discordon" + +[portal.policies.operations.elasticIndex] +description = "Kereshető dokumentumként rögzíti, hogy mit végzett a házirend." +label = "Audit esemény indexelése Elasticsearchben" + +[portal.policies.operations.fields.bodyMode] +helperText = "A Multipart feltöltési API-khoz megfelelő; a JSON olyan API-khoz, amelyek a fájlt payloadban várják; a binary a nyers bájtokat küldi." +label = "A dokumentum küldésének módja" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Változatlanul elküldött JSON törzs. A {{document.base64}} tartalmazza a fájlt; a {{document.*}} és {{run.*}} dokumentumonként kerül kitöltésre." +label = "JSON törzs" + +[portal.policies.operations.fields.connection] +label = "Fiók" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Az űrlapmező, amely alatt az API a dokumentumot várja." +label = "Fájlmező neve" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Feladó" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Header név/érték párok JSON objektuma. A kapcsolat hitelesítő adatai automatikusan hozzáadódnak." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Az üzenet törzseként lesz elküldve. A {{document.*}} és {{run.*}} dokumentumonként kerül kitöltésre." +label = "Üzenet" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Oldal ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Hozzáfűzve a kapcsolat alap URL-jéhez. Soha nem érhet el másik hostot." +label = "Útvonal" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Hová írja a drive-on. A {{document.filename}} dokumentumonként kerül kitöltésre." +label = "Célútvonal" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "A jelentés érintetlenül hagyja a dokumentumot. A csere lecseréli arra, ami visszaérkezik." +label = "Mi történjen a válasszal" + +[portal.policies.operations.fields.signerEmail] +label = "Aláíró e-mail-címe" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Tárgy" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "A Presidio ezt a szöveget elemzi. Használja a {{document.title}} értéket, vagy illessze be az ellenőrizendő szöveget." +label = "Ellenőrizendő szöveg" + +[portal.policies.operations.fields.to] +label = "Címzett" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud felhasználónév" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Értesít egy space-t, hogy a házirend kezelt egy dokumentumot." +label = "Üzenet közzététele Google Chatben" + +[portal.policies.operations.jiraAttach] +description = "A feldolgozott dokumentumot egy issue-hoz csatolja." +label = "Csatolás Jira issue-hoz" + +[portal.policies.operations.mailgunEmail] +description = "Elküldi a feldolgozott dokumentumot mellékletként." +label = "Dokumentum küldése e-mailben (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "A feldolgozott dokumentumot egy mappába írja." +label = "Feltöltés Nextcloudba" + +[portal.policies.operations.presidioAnalyze] +description = "A saját Presidio szervere jelenti a megtalált személyes adatokat." +label = "Személyes adatok észlelése" +note = "A Presidio szöveget olvas, nem fájlokat. Adja meg az ellenőrizendő szöveget — maga a dokumentum nem lesz feltöltve." + +[portal.policies.operations.responseMode] +replace = "A dokumentum cseréje a válaszra" +report = "Dokumentum megtartása, válasz rögzítése" + +[portal.policies.operations.sendgridEmail] +description = "Elküldi a feldolgozott dokumentumot mellékletként." +label = "Dokumentum küldése e-mailben (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Értesít egy csatornát, hogy a házirend kezelt egy dokumentumot." +label = "Üzenet közzététele Slackben" + +[portal.policies.operations.splunkEvent] +description = "Rögzíti, mit végzett a házirend ott, ahol az auditorai már keresnek." +label = "Audit esemény küldése Splunkba" + +[portal.policies.operations.sumologicEvent] +description = "Rögzíti, mit végzett a házirend egy HTTP gyűjtőbe." +label = "Audit esemény küldése Sumo Logicba" + +[portal.policies.operations.teamsNotify] +description = "Értesít egy csatornát, hogy a házirend kezelt egy dokumentumot." +label = "Üzenet közzététele Teamsben" + +[portal.policies.operations.webhookPost] +description = "Magát a dokumentumot küldi el egy Ön által választott URL-re." +label = "Dokumentum küldése webhookra" + +[portal.policies.operations.zapierNotify] +description = "Átadja a futtatás részleteit az automatizációnak." +label = "Zap vagy Make scenario indítása" + [portal.policies.stats] activeFor = "Aktív" dataProcessed = "Feldolgozott adatok" @@ -7473,6 +8282,13 @@ label = "Érvényesített dokumentumok" description = "Konfigurálva, de nem fut" label = "Szüneteltetve" +[portal.policies.table] +appliesTo = "Erre vonatkozik" +docs = "Érvényesített dok." +enforces = "Érvényesít" +policy = "Házirend" +status = "Állapot" + [portal.policies.wizard.actions] back = "Vissza" cancel = "Mégse" @@ -7480,6 +8296,10 @@ continue = "Folytatás" enablePolicy = "Házirend engedélyezése" saveChanges = "Módosítások mentése" +[portal.policies.wizard.capability.classify] +desc = "Azonosítja a dokumentum típusát a csapat címkéi alapján, és megcímkézi, így kategória szerint rendszerezhető és kereshető." +label = "Dokumentum osztályozása" + [portal.policies.wizard.capability.compress] desc = "Kisebb fájlméretre tömöríti a dokumentumot." label = "Fájlméret csökkentése" @@ -7500,10 +8320,18 @@ label = "Érzékeny adatok kitakarása" desc = "Eltávolítja a rejtett JavaScriptet, hogy a dokumentum megnyitásakor semmi se futhasson automatikusan." label = "Aktív tartalom eltávolítása" +[portal.policies.wizard.capability.timestampPdf] +desc = "Bizonyítja, hogy a dokumentum ebben a pontos formában létezett egy adott időpontban, független időbélyeg-szolgáltató használatával. Csak hash kerül elküldésre – a dokumentum soha nem hagyja el a szerverét." +label = "Megbízható időbélyeg hozzáadása" + [portal.policies.wizard.capability.watermark] desc = "Látható jelölést (pl. „Bizalmas”) helyez el minden oldalon." label = "Vízjel alkalmazása" +[portal.policies.wizard.classification] +description = "Minden feltöltött dokumentum osztályozásra kerül a beépített címkék alapján, és megkapja a hozzá illő típusokat. A címkekészlet a teljes csapatban közös." +labelsHeading = "Osztályozási címkék" + [portal.policies.wizard.errors] noTools = "Először engedélyezzen legalább egy eszközt a munkafolyamatban." saveFailed = "Nem sikerült menteni a házirendet. Kérjük, próbálja újra." @@ -7548,73 +8376,118 @@ setUp = "{{category}} házirend beállítása" [portal.policies.wizard.workflow] description = "Válassza ki, mit tegyen ez a házirend minden általa feldolgozott dokumentummal." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktív" -noRule = "Még nincs érvényesített szabály" -subtitle = "Állandó automatizációk, amelyeken minden dokumentum áthalad, függetlenül attól, melyik pipeline kezeli." -title = "Mi fut a PDF-jein" - [portal.policySummary.action] -comingSoon = "Váltás Enterprise csomagra" -configure = "Konfigurálás" setUp = "Beállítás" -[portal.policySummary.column] -activeRule = "Aktív szabály" -policy = "Házirend" -status = "Állapot" +[portal.processorFlow] +footnote = "A számlálók az elmúlt 24 órára vonatkoznak. A folyamat sebessége szemléltető jellegű." +liveBadge = "Élő" +stats = "{{connected}} csatlakoztatva · {{processed}} PDF feldolgozva" +title = "PDF Processor" -[portal.policySummary.empty] -description = "A házirendek konfigurálása után a kategóriák itt jelennek meg." -title = "Még nincsenek házirendek" +[portal.processorFlow.lens] +ariaLabel = "Nézet mód" +flow = "Folyamat" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktív" -off = "Ki" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 óra" +failed = "Sikertelen" +heading = "Eredmények" +success = "Kézbesítve" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktív" +count = "{{n}} · 24 óra" +heading = "Házirendek" +setUp = "Beállítás" soon = "Hamarosan" -[portal.processingStatus] -managePlan = "Csomag kezelése" -volumeSuffix = "feldolgozott PDF · az elmúlt 30 nap" +[portal.processorFlow.sankey] +waist = "Házirendek · {{n}} aktív" + +[portal.processorFlow.sankey.empty] +description = "Csatlakoztasson egy forrást, és kapcsoljon be egy házirendet, hogy lássa a dokumentumok áramlását a feldolgozón keresztül." +title = "Még nincs folyamat" + +[portal.processorFlow.sources] +comingSoonTag = "Csatlakoztatás" +editor = "Stirling PDF Editor" +heading = "Források" +perDay = "{{n}} / 24 óra" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-mail fogadás" [portal.procurement] -reset = "Beszerzés visszaállítása (demo)" subtitle = "Értékelje, szerződtesse és vezesse be csapatát. Minden dokumentum egy helyen." title = "Beszerzés" -[portal.procurement.action] -download = "Letöltés" -pay = "Fizetés most" -request = "Kérés" -sign = "Áttekintés és aláírás" -upload = "Feltöltés" - [portal.procurement.agreement] agreeCta = "Elfogadás és előfizetés" +confidential = "Bizalmas" confirm = "Elolvastam és elfogadom a Stirling Enterprise Agreement feltételeit." -eyebrow = "Megállapodás" -intro = "Egy összevont megállapodás fedi le az ügyletet: Master Service Agreement, Order Form, EULA és Data Processing Agreement. Tekintse át, majd fogadja el az ajánlat elkötelezett előfizetéssé alakításához." -title = "Enterprise megállapodás áttekintése" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA és DPA egyetlen aláírásban egyesítve." +download = "Letöltés" +downloadDraftError = "Nem sikerült létrehozni a megállapodás PDF-jét – a dokumentumrenderelő nem érhető el. Kérjük, próbálja újra rövidesen, vagy lépjen kapcsolatba a támogatással." +downloadError = "Nem sikerült letölteni az aláírt megállapodást. Kérjük, próbálja újra." +legalName = "Jogi személy neve" +legalNamePlaceholder = "Az aláíró jogi személy" +loadError = "Nem sikerült betölteni a megállapodást. Kérjük, próbálja újra." +loading = "Megállapodás betöltése..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Módosítások kérése" +scrollHint = "Görgessen az áttekintéshez" +signatory = "Aláíró neve" +signatoryPlaceholder = "Teljes név" +signatoryTitle = "Aláíró beosztása" +signatoryTitlePlaceholder = "pl. vezető jogtanácsos" +signError = "Nem sikerült rögzíteni az aláírását. Kérjük, próbálja újra." [portal.procurement.builder] addons = "Kiegészítők" +addressLine1 = "Cím 1. sora" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Cím 2. sora" +addressLine2Placeholder = "Lakosztály, emelet, épület" back = "Vissza" businessName = "Cégnév" businessNamePlaceholder = "Az Ön cége" +city = "Város" +cityPlaceholder = "San Francisco" +completeRequired = "Kérjük, töltse ki a kötelező mezőket (*-gal jelölve) érvényes e-mail-címmel az ajánlat létrehozása előtt." +contactEmail = "Kapcsolattartó e-mail-címe" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kapcsolattartó neve" +contactNamePlaceholder = "Jane Doe" continue = "Folytatás" -country = "Ország" -countryEuro = "Eurózóna (EUR €)" -countryUK = "Egyesült Királyság (GBP £)" -countryUS = "Egyesült Államok (USD $)" +done = "Kész" eula = "Elolvastam és elfogadom a Stirling Enterprise EULA feltételeit. Ez szabályozza az ajánlatból létrehozott megállapodást." generate = "Ajánlat generálása" -included = "Tartalmazza" indemnification = "IP kártalanítás" indemnificationSub = "Az EULA szerint védelmet nyújtunk a jogosult IP igényekkel szemben" -offlineLicense = "Offline / air-gapped licenc" -offlineLicenseSub = "Letölthető licencfájl air-gapped self-hosted példányhoz" +paperEyebrow = "Enterprise ajánlat" +paperFor = "Készült számára" +pdfSize = "PDF mérete" +poNumber = "PO szám" +poNumberPlaceholder = "Opcionális" +postalCode = "Irányítószám" +postalCodePlaceholder = "94105" +posture = "Irányítás" +posture_count = "~{{count}} házirend" +posture_essentials = "Alapok" +posture_essentialsSub = "Osztályozás + Megosztás — az alapértelmezések" +posture_governed = "Irányított" +posture_governedSub = "Biztonságot és útválasztást ad hozzá" +posture_regulated = "Szabályozott" +posture_regulatedSub = "Minden kategória — Megfelelőség, Megőrzés, Bevitel" qbr = "Negyedéves üzleti áttekintések" qbrSub = "Az Ön SE-je negyedévente áttekinti a használatot és az ütemtervet" +region = "Állam / régió" +regionPlaceholder = "California" running = "{{annual}} / év · {{years}} éves {{tcv}}" s1Sub = "Az Ön csapata és az évente várhatóan futtatott PDF-ek." s1Title = "Mennyit fog feldolgozni?" @@ -7623,6 +8496,12 @@ s2Title = "Elköteleződés és szolgáltatás" s3Sub = "Az ajánlathoz és az abból generált megállapodáshoz." s3Title = "Az Ön adatai" serviceLevel = "Szolgáltatási szint" +size_compact = "Kompakt" +size_compactSub = "Többnyire szöveg, 1 MB alatt" +size_heavy = "Nagy" +size_heavySub = "Szkennelt vagy sok képet tartalmaz, 5 MB+" +size_standard = "Standard" +size_standardSub = "Vegyes szöveg és képek, 1–5 MB" slDedicated = "Dedikált" slDedicatedSub = "4 munkaóra · dedikált fiókmenedzser · +30%" slPriority = "Prioritás" @@ -7630,6 +8509,8 @@ slPrioritySub = "Azonos munkanapon · név szerint kijelölt CSM · +15%" slStandard = "Standard" slStandardSub = "Következő munkanap · megosztott CSM · tartalmazza" stepOf = "{{n}}. lépés / {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Opcionális" term = "Időtartam" termDiscount = "{{pct}}% többéves elköteleződési kedvezmény alkalmazva" title = "Ajánlat összeállítása" @@ -7637,6 +8518,7 @@ training = "Bevezetés és képzés" trainingSub = "Élő alkalmak a csapat elindításához" users = "Összes felhasználó" usersPlaceholder = "pl. 250" +viewEula = "Elolvasás" volEstimated = "Becslés {{count}} felhasználó alapján (~2 000 PDF fejenként, automatizálással együtt). Szerkessze, ha pontosabbat tud." volManual = "Az Ön megadott értékét használjuk. Bármikor újrabecsülheti a csapatméret alapján." volNoUsers = "Nem biztos benne? Adja meg a csapat méretét, és megbecsüljük." @@ -7645,55 +8527,55 @@ volumePlaceholder = "pl. 1 000 000" years_one = "{{count}} év" years_other = "{{count}} év" -[portal.procurement.docs] -count_one = "{{count}} dokumentum" -count_other = "{{count}} dokumentum" -done = "Kész" -here = "Itt tart" -hide = "Elrejtés" -optional = "Opcionális" -paidAddon = "Fizetős kiegészítő" -show = "Megjelenítés" -subtitle = "Minden, amire az út egyes lépéseinél szüksége van, akkor jelenik meg, amikor az ügylet odaér." -supportingSubtitle = "SOC 2, biztonsági áttekintések, adóűrlapok és egyebek, készen, amikor a biztonsági vagy beszerzési csapat kéri. Egyesek egyszeri díjjal járnak." -supportingTitle = "Az értékelés támogatása" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form és DPA egyetlen aláírásban." +download = "Letöltés" +eula = "EULA és kereskedelmi feltételek" +eulaSub = "A szoftver és az önkiszolgáló kereskedelmi feltételek." +invoice = "Számla" +invoiceSub = "Az első előfizetési számlája." +laterInvoice = "Elfogadás után érhető el" +laterQuote = "Az ajánlat kiadása után érhető el" +quote = "Ajánlat" +quoteSub = "Tételes Enterprise ajánlata." +sla = "SLA melléklet" +slaSub = "Támogatási szintek és válaszidő-célok." +subprocessors = "Alfeldolgozók" +subprocessorsSub = "Harmadik felek, amelyek adatokat dolgoznak fel az Ön nevében." +subtitle = "Üzleti dokumentumai, bármikor elérhetők." title = "Dokumentumok" -upcoming = "Közelgő" +view = "Megtekintés" [portal.procurement.error] title = "Valami hiba történt" [portal.procurement.hero] -company = "Az Ön enterprise ügylete" +barAria = "{{current}}. szakasz / {{total}}" ctaAgreement = "Megállapodás áttekintése és aláírása" -ctaLive = "Élőben van" -ctaPayment = "Fizetés hozzáadása" +ctaExploring = "Próbaidőszak beállítása" ctaQuote = "Ajánlat áttekintése" +ctaReviewQuote = "Ajánlat áttekintése" ctaTrial = "Ajánlat összeállítása" +documents = "Dokumentumok" eyebrow = "Enterprise beszerzés" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Csapattársak meghívása" -keyDocs = "Kulcsdokumentumok" -nextStep = "Következő lépés: {{action}}" -notStarted = "Nincs elkezdve" +licenseKey = "Licenckulcs" +liveSub = "Szervezete kiépítve, licence aktív." +liveTitle = "Élőben van a Stirling Enterprise-on" +next = "Következő: {{stage}}" open = "Beszerzés megnyitása" scheduleCall = "Hívás ütemezése" -setup1Sub = "Hívja meg csapattársait" -setup1Title = "PDF Editor telepítése" -setup2Sub = "Kapcsolja be a feldolgozást a szerkesztőkben és más forrásokban" -setup2Title = "PDF Processor csatlakoztatása" -setup3Sub = "Kapcsolja be a Security, Compliance, Routing vagy Retention funkciókat, amikor szüksége van rájuk" -setup3Title = "Ajánlott házirendek hozzáadása" +sentenceAgreement = "a megállapodás áttekintése és aláírása" +sentenceLive = "kiépítés és bevezetés" +sentencePayment = "beszerzési megrendelés és fizetés" +sentenceQuote = "az ajánlat áttekintése és elfogadása" +sentenceTrial = "próba-munkaterület beállítása" [portal.procurement.journey] daysLeft_one = "{{count}} nap maradt" daysLeft_other = "{{count}} nap maradt" -engineerLabel = "Az Ön solutions engineer-je" -eyebrow = "Az Ön bevezetése" -live = "Élőben van a Stirling Enterprise-on" -nextStep = "Következő lépés: {{action}}" -subtitle = "Az Ön solutions engineer-je minden lépésnél jelen van. Egyszerre egy következő művelet; a teljes ellenőrzőlista lent található." -title = "A próbaidőszaktól az éles működésig, egy vezetett úton" -trialTitle = "Enterprise próbaidőszak" [portal.procurement.journeySteps.agreement] blurb = "Egy aláírás lefedi az MSA-t, order formot, EULA-t és DPA-t." @@ -7727,6 +8609,9 @@ downloadError = "Még nem sikerült létrehozni az offline licencfájlt — kér downloadOffline = "Offline licenc letöltése (.lic)" hint = "Illessze be ezt a kulcsot egy self-hosted példányba az aktiváláshoz, vagy őrizze meg a nyilvántartásához. Tartsa biztonságban." label = "Az Ön licenckulcsa" +subtitle = "Aktiváljon egy saját üzemeltetésű példányt ezzel a kulccsal, vagy őrizze meg a nyilvántartásához." +title = "Az Ön licenckulcsa" +trialFileHint = "Ez az Ön próba licencfájlja. Miután a megállapodás létrejött, térjen vissza, és töltse le újra — az online licenckulccsal ellentétben a .lic fájl nem frissül magától." [portal.procurement.link] cta = "Fiók összekapcsolása" @@ -7739,65 +8624,62 @@ description = "Az előfizetése aktív, és a licence ki lett állítva. A csapa eyebrow = "Élő" title = "Élőben van a Stirling Enterprise-on" -[portal.procurement.locked] -description = "A próbakulcsok, elkötelezett mennyiségi ajánlatok, az egyaláírásos megállapodás, a fizetés és a dokumentumnapló mind itt lesznek, miután elindít egy enterprise értékelést." -eyebrow = "Csak Enterprise" -talkToSales = "Kapcsolat az értékesítéssel" -title = "A beszerzési folyamat Enterprise csomaggal nyílik meg" - [portal.procurement.milestone] -accept = "Elfogadás és folytatás" -description = "Töltse le a PDF-et, hogy megossza a csapatával, térjen vissza elfogadni, amikor készen áll, vagy módosítsa." -download = "PDF letöltése" downloadError = "Még nem sikerült letölteni az ajánlat PDF-et — kérjük, próbálja újra egy pillanat múlva." -edit = "Ajánlat szerkesztése" -eyebrow = "{{number}}. ajánlat" -perYear = " / év" -preparedFor = "Készült: {{company}}" -tcv = "{{value}} teljes szerződéses érték" -title = "Az ajánlata elkészült" - -[portal.procurement.modal] -cancel = "Mégse" -chooseFile = "Fájl kiválasztása" -close = "Bezárás" -downloadBody = "A letöltés hamarosan elindul." -downloadCta = "Letöltés" -downloadTitle = "Letöltés" -noFile = "Nincs kiválasztva fájl" -payBody = "Fizesse ki elkötelezett szerződését kártyával vagy banki átutalással a Stripe-on keresztül. A munkaterülete a fizetés teljesülése után azonnal létrejön." -payCta = "Folytatás a Stripe-ra" -payTitle = "Fizetés megerősítése" -requestBodyFree = "Ezt kérésre generáljuk. Erősítse meg, és a solutions engineer-je hamarosan elküldi." -requestBodyPaid = "Ez fizetős kiegészítő. Erősítse meg, és a solutions engineer-je felméri, majd elküldi a dokumentációt." -requestCta = "Kérés" -requestTitle = "Dokumentum kérése" -signBody = "Megnyitja a Stirling Enterprise Agreementet elektronikus aláírásra: egy aláírás lefedi az MSA-t, order formot, EULA-t és DPA-t. Automatikusan ellenjegyezzük, és továbblép a fizetéshez." -signCta = "Megnyitás aláírásra" -signTitle = "Megállapodás áttekintése és aláírása" -uploadBody = "Küldje el nekünk a PO-t, és a feltételei szerint számlázunk. Húzza be a PDF-et, vagy válasszon fájlt lent." -uploadCta = "Beszerzési megrendelés feltöltése" -uploadTitle = "Beszerzési megrendelés feltöltése" [portal.procurement.payment] description = "Az ajánlatát elfogadtuk, és a licence már aktív — a csapata azonnal kezdhet. Fizesse ki az első számlát, amikor készen áll; itt fizetheti ki vagy töltheti le, email nélkül." +downloadAgreement = "Aláírt megállapodás letöltése" downloadInvoice = "Számla letöltése" -simulate = "Fizetés beérkezésének szimulálása (demo)" +eyebrow = "Fizetés" title = "Előfizetés létrehozva" viewInvoice = "Számla megtekintése és fizetése" +[portal.procurement.review] +acceptCta = "Ajánlat elfogadása" +annual = "Éves díj (1. év)" +downloadCta = "Ajánlat letöltése" +poNumber = "Beszerzési megrendelés: {{po}}" +renewal = "A futamidő után {{amount}}/év áron újul meg (+{{pct}}% CPI)" +tcv = "{{years}} éves futamidő, előre teljes egészében fizetve: {{tcv}}" +validUntil = "Érvényes eddig: {{date}}" + [portal.procurement.schedule] fallback = "Nem sikerült betölteni az ütemezőt." fallbackLink = "Ütemezés megnyitása új lapon" +loading = "Ütemező betöltése..." subtitle = "A solutions engineer-je végigvezeti csapatát a bevezetésen. Válasszon Önnek megfelelő időpontot." title = "Hívás ütemezése" -[portal.procurement.status] -action = "Teendő szükséges" -available = "Elérhető" -complete = "Kész" -pending = "Függőben" -request = "Kérésre" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Zárt, nincs kimenő kapcsolat" +back = "Vissza" +businessName = "Vállalkozás neve" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Felügyelt, leggyorsabb kezdés" +continue = "Folytatás" +deployment = "Hová legyen telepítve?" +eula = "Elfogadom a Stirling EULA-t és a kereskedelmi feltételeket." +fullName = "Teljes név" +fullNamePlaceholder = "Az Ön neve" +invites = "Csapattagok meghívása · opcionális" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Hívás ütemezése" +seats = "Tesztelő személyek" +seatsPlaceholder = "pl. 15" +selfhost = "Saját üzemeltetésű" +selfhostSub = "Docker vagy Kubernetes" +start = "Próbaidőszak indítása" +stepOf = "{{n}}. lépés / {{total}}" +subtitle = "14 napig ingyenes, kártya nem szükséges." +subtitleDetails = "Néhány adat az ajánlatához és a megállapodáshoz." +talkFirst = "Inkább előbb beszélne?" +title = "Próbaidőszak beállítása" +viewEula = "Elolvasás" +workEmail = "Munkahelyi e-mail" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "A hosszabbítás 7 napot ad hozzá, és értesíti a solutions engineer-jét." @@ -7808,109 +8690,113 @@ maxed = "Elérte a maximumot" subtitle = "Az ingyenes próbaidőszak {{date}} dátumig tart. Kártya nem szükséges." title = "Enterprise próbaidőszak" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Elkötelezett mennyiségi árazás, szervezetszintű SSO + SCIM + RBAC, 90 napos megváltoztathatatlan audit és dedikált SE." -homeCta = "Próbaidőszak indítása →" -homeHeadline = "Dolgozzon fel több millió PDF-et." - -[portal.recentActivity] -title = "Legutóbbi tevékenység" -viewAll = "Összes megtekintése" - -[portal.recentActivity.empty] -description = "A pipeline futtatások, telepítések és agent események itt jelennek meg." -title = "Még nincs itt semmi" - -[portal.search] -ariaLabel = "Keresés" -placeholder = "Keresés a Stirlingben — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "A gyorsműveletek itt jelennek meg, amint elérhetők." -noActionsTitle = "Nincsenek gyorsműveletek" -noMatches = "Nincs találat erre: \"{{query}}\"" -noMatchesDescription = "Próbáljon másik kulcsszót, vagy böngéssze a katalógust." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Fiók összekapcsolása" -[portal.shell.header] -accountFallback = "Fiók" -accountMenu = "Fiókmenü" -darkMode = "Sötét mód" -lightMode = "Világos mód" -search = "Keresés" -searchPlaceholder = "Keresés…" -signOut = "Kijelentkezés" -switchToDark = "Váltás sötét témára" -switchToLight = "Váltás világos témára" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumentum" linkAccount = "Stirling-fiók összekapcsolása" -planEditor = "Editor csomag" -planEnterprise = "Enterprise csomag" -planProcessor = "Processor csomag" primaryNav = "Elsődleges navigáció" switchApp = "Alkalmazás váltása" +[portal.shell.topbar] +closeNav = "Navigáció bezárása" +openNav = "Navigáció megnyitása" +search = "Keresés" + [portal.sources] subtitle = "Újrahasználható bemeneti kapcsolatok, amelyek dokumentumokat juttatnak a Stirlingbe. Konfiguráljon egy kapcsolatot egyszer, majd hivatkozzon rá tetszőleges számú házirendből. Kattintson egy sorra a konfigurációjához és annak megtekintéséhez, mely házirendek használják." title = "Források" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Forrás csatlakoztatása" +[portal.sources.builder] +back = "Vissza a forrásokhoz" +backToTypes = "Minden forrástípus" +cancel = "Mégse" +chooseHint = "Válassza ki, honnan érkeznek a dokumentumok. A kiszürkített csatlakozók úton vannak." +comingSoon = "Hamarosan" +comingSoonHeading = "Hamarosan" +create = "Forrás létrehozása" +createTitle = "Forrás csatlakoztatása" +delete = "Törlés" +editTitle = "Forrás szerkesztése" +enabled = "Engedélyezve" +save = "Módosítások mentése" + +[portal.sources.builder.folderAccess] +description = "A mappaautomatizálások csak olyan mappákat használhatnak, amelyeket egy rendszergazda engedélyezett. Adja hozzá a Mappahozzáférés beállításainál, majd próbálja újra." +openSettings = "Mappahozzáférés beállításai" +title = "Ez a mappa nincs engedélyezve" + [portal.sources.delete] body = "Törli ezt: \"{{name}}\"? Ez nem vonható vissza. Az erre hivatkozó házirendeket frissíteni kellene." cancel = "Mégse" confirm = "Törlés" title = "Törli a forrást?" -[portal.sources.detail] -closeAriaLabel = "Részletek bezárása" -delete = "Forrás törlése" -docs24h = "Elmúlt 24 óra" -docs30d = "Elmúlt 30 nap" -docsTotal = "Összes látott" -docsTrend = "Dokumentumok az elmúlt 30 napban" -documents = "Dokumentumok" -edit = "Szerkesztés" -notReferenced = "Egy házirend sem hivatkozik rá, ezért biztonságosan törölhető." -pause = "Szüneteltetés" -resume = "Folytatás" -subtitle = "{{type}} · {{status}}" -usedBy = "Használja" - -[portal.sources.empty] -description = "Csatlakoztasson egy mappát (és hamarosan felhőtárhelyet), hogy a házirendeknek legyen honnan dokumentumokat beolvasniuk." -title = "Még nincs csatlakoztatott forrás" - [portal.sources.kpi] inUse = "Használatban" total = "Kapcsolatok" unused = "Nem használt" +[portal.sources.networkFields.connection] +helperText = "A tárolt kapcsolat a szerver címével és hitelesítő adataival. Minden rá hivatkozó forrás újra felhasználja." +label = "Kapcsolat" + +[portal.sources.networkFields.directory] +helperText = "A szerveren lekérdezendő mappa, a bejelentkezési home-hoz vagy a megosztás gyökeréhez képest. Hagyja üresen a gyökérhez." +label = "Mappa" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "A Consume eltávolít minden fájlt a szerverről, miután minden házirend feldolgozta." +label = "Olvasási mód" + +[portal.sources.networkFields.mode.options] +consume = "Consume: minden fájl feldolgozása egyszer" +snapshot = "Snapshot: mappa újraolvasása minden futtatáskor" + +[portal.sources.networkFields.recursive] +label = "Mappamélység" + +[portal.sources.networkFields.recursive.options] +all = "Almappák belefoglalása" +top = "Csak felső szint" + [portal.sources.status] active = "Aktív" disabled = "Letiltva" unused = "Nem használt" [portal.sources.table] +documents = "Dokumentumok" source = "Forrás" status = "Állapot" +type = "Type" usedBy = "Házirendek" +[portal.sources.types.box] +description = "Dokumentumok behúzása a csapata által már megosztott Box mappákból." +label = "Box" + +[portal.sources.types.dropbox] +description = "Dokumentumok behúzása egy megosztott Dropbox mappából." +label = "Dropbox" + [portal.sources.types.editor] +description = "A csapat által az editorban feldolgozott dokumentumok, házirend- és AI futtatásokon keresztül." label = "Editor" +noPolicies = "Még nem futott házirend az editorból." + +[portal.sources.types.email] +description = "A dedikált postaládába küldött mellékleteket dokumentumokká alakítja." +label = "E-mail bejövő fiók" [portal.sources.types.folder] description = "Figyeljen egy könyvtárat a szerveren az új dokumentumokért." @@ -7943,25 +8829,80 @@ label = "Mappamélység" all = "Almappák belefoglalása" top = "Csak felső szint" +[portal.sources.types.ftp] +description = "Új dokumentumok keresése egy FTP vagy FTPS szerver mappájában." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Dokumentumok behúzása megosztott Google Drive mappákból." +label = "Google Drive" + +[portal.sources.types.network] +description = "SMB hálózati megosztás figyelése új dokumentumokhoz." +label = "Hálózati drive" + +[portal.sources.types.onedrive] +description = "OneDrive mappák figyelése új dokumentumokhoz." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Dokumentumok behúzása Amazon S3 vagy S3-kompatibilis bucketből." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "A tárolt kapcsolat, amely a bucketet és a hitelesítő adatokat tartalmazza. Minden rá hivatkozó forrás és folyamatkimenet újra felhasználja." +label = "Kapcsolat" + +[portal.sources.types.s3.fields.mode] +helperText = "A Consume eltávolít minden objektumot a bucketből, miután minden házirend feldolgozta." +label = "Olvasási mód" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: minden objektum feldolgozása egyszer" +snapshot = "Snapshot: bucket újraolvasása minden futtatáskor" + +[portal.sources.types.s3.fields.prefix] +helperText = "Csak azok az objektumok kerülnek feldolgozásra, amelyek kulcsai ezzel a prefixszel kezdődnek." +label = "Kulcs prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Új dokumentumok keresése egy SFTP drop mappában." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Dokumentumok behúzása SharePoint dokumentumtárakból." +label = "SharePoint" + [portal.sources.types.unknown] label = "Forrás" +[portal.sources.types.webhook] +createNote = "A webhook létrehozásakor delivery URL és aláírási titok jön létre. A küldők POST kéréssel dokumentumokat küldenek erre az URL-re, és a hivatkozó házirendek érkezéskor lefutnak." +description = "Dokumentumok fogadása aláírt HTTP POST kéréssel bármely külső rendszerből." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Az aláírási titok egyszer jelenik meg, a webhook létrehozásakor. A forgatáshoz hozza létre újra a forrást." + +[portal.sources.types.webhook.reveal] +copy = "Másolás" +done = "Kész" +secret = "Aláírási titok" +secretHelp = "Írja alá minden kézbesítés nyers törzsét ezzel a kulccsal (HMAC-SHA256), és küldje el X-Stirling-Signature headerként." +secretWarning = "Másolja ki most az aláírási titkot. Biztonsága érdekében csak egyszer jelenik meg, és később nem kérhető le." +title = "Webhook létrehozva" +url = "Delivery URL" +usage = "Küldjön minden dokumentumot POST kéréssel nyers request bodyként a delivery URL-re binary content type-pal (application/pdf vagy application/octet-stream). A hivatkozó házirendek érkezéskor automatikusan lefutnak." + [portal.sources.wizard] -back = "Vissza" -cancel = "Mégse" -continue = "Folytatás" -editTitle = "Forrás szerkesztése" name = "Név" namePlaceholder = "pl. Kárigények fogadása" -save = "Módosítások mentése" -subtitle = "{{current}}. lépés / {{total}} · {{label}}" -title = "Forrás csatlakoztatása" type = "Típus" -[portal.sources.wizard.steps] -chooseType = "Típus kiválasztása" -configure = "Konfigurálás" -review = "Áttekintés és csatlakoztatás" +[portal.stepModal] +close = "Bezárás" [portal.usage] managePayment = "Fizetés kezelése" @@ -8021,16 +8962,9 @@ summary = "Egy csapat tulajdonosa — kezeli a tagok erőforrásait és megoszto 2 = "Portálhozzáférés az alapértelmezett házirenden keresztül" 3 = "Minden, amit a tag megtehet" -[portal.welcome] -ariaLabel = "Üdvözli a Stirling PDF" -badge = "Nyílt forráskódú" -installEditor = "Editor telepítése" -inviteTeammates = "Csapattársak meghívása" -openInBrowser = "Megnyitás böngészőben" -perks = "Örökre ingyenes · Self-hostable" -subtitle = "A világ legbiztonságosabb PDF Editorja ingyenes bármilyen méretű csapat számára. Több mint 60 PDF műveletet és SSO-t tartalmaz." -title = "Üdvözli a" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Korlátlan" [printFile] title = "Fájl nyomtatása" @@ -8613,6 +9547,14 @@ text = "Forgassa el a PDF oldalakat az óramutató járásával megegyezően vag [rotate.tooltip.header] title = "Forgatási beállítások áttekintése" +[ruler] +clearAll = "Összes törlése" +hideAllLabels = "Összes címke elrejtése" +hideSmallLabels = "Kis címkék elrejtése" +physicalValues = "Fizikai" +scaled = "Méretezett" +showAllLabels = "Összes címke megjelenítése" + [sanitize] desc = "Potenciálisan ártalmas elemek eltávolítása PDF fájlokból." sanitizationResults = "Tisztítás eredményei" @@ -8654,31 +9596,45 @@ label = "XMP metaadatok eltávolítása" [sanitize.steps] settings = "Beállítások" -[sanitizePdf] -tags = "tisztítás,biztonság,védelem,veszélyek eltávolítása" - [sanitizePDF] title = "PDF tisztítása" +[sanitizePdf] +tags = "tisztítás,biztonság,védelem,veszélyek eltávolítása" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Oldalméret beállítása" +[scaleSettings] +activeScale = "Aktív skála" +apply = "Skála alkalmazása" +applyCalibration = "Kalibrálás alkalmazása" +calculatedScale = "Számított skála" +calibrate = "Kalibrálás" +calibrating = "Kalibrálás" +calibrationDistanceRequired = "Adjon meg nullánál nagyobb valós távolságot" +calibrationInvalid = "Ebből a mérésből nem számítható ki a méretarány" +calibrationPaperDistance = "Mért oldaltávolság: {{distance}}" +calibrationTitle = "Méretarány kalibrálása" +calibrationTooltip = "Mérjen meg egy ismert távolságot a méretarány automatikus kiszámításához" +customScale = "Egyéni méretarány" +noneSet = "Nincs beállítva egyéni méretarány" +presets = "Előre beállított méretarányok" +ratio = "Méretarány" +ratioHelp = "Arány: 1 oldalegység = X valós egység" +ratioPlaceholder = "pl. 100" +ratioPositive = "Az aránynak nullánál nagyobbnak kell lennie" +ratioRequired = "Az arány megadása kötelező" +realDistance = "Valós távolság" +realDistancePlaceholder = "pl. 5" +reset = "Visszaállítás alapértelmezettre" +unit = "Egység" +unitInvalid = "Érvénytelen egység: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Szög küszöbérték:" -10 = "A hozzáadott és eltávolított keret méretének beállítása a fehér keretek elkerülése érdekében (alapértelmezett: 1)." -2 = "A kép forgatásához szükséges minimális abszolút szög beállítása (alapértelmezett: 10)." -3 = "Tolerancia:" -4 = "A becsült háttérszín körüli színváltozási tartomány meghatározása (alapértelmezett: 30)." -5 = "Minimális terület:" -6 = "A fotó minimális területének küszöbértéke (alapértelmezett: 10000)." -7 = "Minimális kontúr terület:" -8 = "A fotó minimális kontúr területének küszöbértéke" -9 = "Keret mérete:" - [scannerImageSplit] submit = "Képbeolvasások kinyerése" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Mit csinál ez" whatThisDoesDesc = "Automatikusan megtalálja és kinyeri az egyes fotókat egy szkennelt oldalról vagy kompozit képről—kézi vágás nélkül." whenToUse = "Mikor használja" +[ScannerImageSplit.selectText] +1 = "Szög küszöbérték:" +10 = "A hozzáadott és eltávolított keret méretének beállítása a fehér keretek elkerülése érdekében (alapértelmezett: 1)." +2 = "A kép forgatásához szükséges minimális abszolút szög beállítása (alapértelmezett: 10)." +3 = "Tolerancia:" +4 = "A becsült háttérszín körüli színváltozási tartomány meghatározása (alapértelmezett: 30)." +5 = "Minimális terület:" +6 = "A fotó minimális területének küszöbértéke (alapértelmezett: 10000)." +7 = "Minimális kontúr terület:" +8 = "A fotó minimális kontúr területének küszöbértéke" +9 = "Keret mérete:" + [search] noResults = "Nincs találat" placeholder = "Adja meg a keresőkifejezést..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "A Stirling-PDF szervere offline, és a(z) \"{{endpoin [session] expired = "A munkamenet lejárt. Kérjük, frissítse az oldalt és próbálja újra." -[sessionManagement.tooltip] -header = "Aláírási munkamenetek kezelése" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Az új résztvevők az aláírási sorrend végére kerülnek" -bullet2 = "Véglegesítés után nem adhatók hozzá résztvevők" -bullet3 = "Minden résztvevő értesítést kap, amikor rá kerül a sor" -description = "A véglegesítésig bármikor adhat hozzá résztvevőket egy aktív munkamenethez." -title = "Résztvevők hozzáadása" - -[sessionManagement.tooltip.finalization] -bullet1 = "Teljes véglegesítés: minden résztvevő aláírt" -bullet2 = "Részleges véglegesítés: néhány résztvevő még nem írt alá" -bullet3 = "Az aláíratlan résztvevők kimaradnak a végleges dokumentumból" -bullet4 = "Véglegesítés után az aláírt PDF betölthető az aktív fájlok közé" -description = "A véglegesítés az összes aláírást egyetlen aláírt PDF-be egyesíti. Ez a művelet nem vonható vissza." -title = "Munkamenet véglegesítése" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "A már aláírt résztvevők nem távolíthatók el" -bullet2 = "Az eltávolított résztvevők nem kapnak több értesítést" -bullet3 = "Az aláírási sorrend automatikusan igazodik" -description = "A résztvevők az aláírás előtt eltávolíthatók a munkamenetből." -title = "Résztvevők eltávolítása" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Minden aláírás szekvenciálisan kerül a PDF-re" -bullet2 = "A későbbi aláírók láthatják a korábbi aláírásokat" -bullet3 = "Létfontosságú jóváhagyási folyamatoknál és jogi nyomonkövetésnél" -description = "A munkamenet létrehozásakor megadott sorrend határozza meg, ki ír alá először." -title = "Aláírási sorrend" - [settings] close = "Bezárás" +[settings.ai] +documents = "Dokumentumok és RAG" +general = "Általános" +limits = "Korlátok és teljesítmény" +models = "Modellek és szolgáltatók" +title = "AI" + [settings.configuration] advanced = "Speciális" database = "Adatbázis" endpoints = "Végpontok" features = "Funkciók" +folderAccess = "Mappahozzáférés" mcp = "MCP Server" storageSharing = "Fájltárolás és megosztás" systemSettings = "Rendszerbeállítások" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Alapértelmezett PDF-szerkesztő" defaultPdfEditorActive = "A Stirling PDF az alapértelmezett PDF-szerkesztő" defaultPdfEditorChecking = "Ellenőrzés..." defaultPdfEditorInactive = "Más alkalmazás van alapértelmezettként beállítva" +defaultPdfEditorRemind = "Emlékeztessen, hogy alapértelmezettként állítsam be" +defaultPdfEditorRemindDescription = "Banner megjelenítése, ha a Stirling PDF nem az alapértelmezett PDF-alkalmazása." defaultPdfEditorSet = "Már alapértelmezett" defaultStartupView = "Alapértelmezett nézet indításkor" defaultStartupViewDescription = "Válassza ki, melyik lap legyen aktív a bal oldali oszlopban az alkalmazás indításakor" @@ -8817,9 +9763,18 @@ languageDescription = "Válassza ki a megjelenítés nyelvét" logout = "Kijelentkezés" setAsDefault = "Beállítás alapértelmezettként" theme = "Téma" +themeAccent = "Kiemelőszín" +themeAccentCustom = "Egyéni szín" +themeAccentCustomHint = "Egyéni (az alapértelmezettek ajánlottak)" +themeAccentDark = "Sötét mód kiemelőszíne" +themeAccentDefault = "Alapértelmezett" +themeAccentDefaultHint = "Alapértelmezett téma (ajánlott)" +themeAccentDescription = "A gombok, hivatkozások és kiemelések ezt követik, és finoman árnyalják az alkalmazást. A világos és a sötét módnak külön-külön sajátja van. A rendszer azt használja, amelyik aktív." +themeAccentLight = "Világos mód kiemelőszíne" themeDark = "Sötét" themeDescription = "Váltás világos és sötét mód között" themeLight = "Világos" +themeReset = "Téma visszaállítása alapértelmezettre" themeSystem = "System" title = "Általános" user = "Felhasználó" @@ -8833,6 +9788,12 @@ intro = "Engedélyezze a felhasználói hitelesítést, csapatkezelést és munk learnMore = "További információ a dokumentációban" title = "Rendszergazdáknak" +[settings.general.loginLanding] +description = "Válassza ki, melyik alkalmazás nyíljon meg, amikor bejelentkezik a Stirling Cloudba." +editor = "Szerkesztő" +processor = "Feldolgozó" +title = "Bejelentkezés után" + [settings.general.mode] fullscreen = "Teljes képernyő" sidebar = "Oldalsáv" @@ -9013,9 +9974,6 @@ title = "Irányelvek és adatvédelem" [settings.preferences] title = "Beállítások" -[settings.search] -placeholder = "Keresés a beállítások között..." - [settings.security] description = "Frissítse jelszavát fiókja biztonságáért." title = "Biztonság" @@ -9041,6 +9999,11 @@ security = "Biztonság" telegram = "Telegram" title = "Biztonság és hitelesítés" +[settings.signOut] +confirm = "Biztosan ki szeretne jelentkezni?" +submit = "Kijelentkezés" +title = "Kijelentkezés" + [settings.team] title = "Csapat" @@ -9156,6 +10119,7 @@ last = "Utolsó oldal" maintainRatio = "Képarány fenntartása váltása" next = "Következő oldal" noSavedSigs = "Nincsenek mentett aláírások" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Személyes aláírások" previous = "Előző oldal" redo = "Újra" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "A háttér eltávolítása nem sikerült" hint = "Töltse fel az aláírását tartalmazó PNG vagy JPG képet" label = "Aláíráskép feltöltése" placeholder = "Képfájl kiválasztása" +previewAlt = "Current image signature" processing = "Kép feldolgozása..." removeBackground = "Fehér háttér eltávolítása (átlátszóvá tétel)" @@ -9207,6 +10172,17 @@ saved = "Válasszon ki fent egy mentett aláírást, majd kattintson a PDF bárm text = "Miután megadta a nevét fent, kattintson bárhová a PDF-ben az aláírás elhelyezéséhez." title = "Hogyan adjon hozzá aláírást" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Aláírás mozgatása" pause = "Elhelyezés szüneteltetése" @@ -9653,10 +10629,30 @@ title = "Feltöltés a szerverre" updateButton = "Frissítés a szerveren" uploadButton = "Feltöltés a szerverre" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kattintson ide" +fallback = "Ha nem nyílt meg automatikusan," +opening = "Swagger UI megnyitása új lapon..." tags = "api,dokumentáció,swagger,végpontok,fejlesztés" title = "API-dokumentáció" @@ -9764,12 +10760,39 @@ selectFilesHint = "Válasszon fájlokat az Aktív fájlok között az eszköz fu singleFileScope = "Csak erre alkalmazva: {{fileName}}" viewerMode = "Váltson a fájlszerkesztőre több fájl kiválasztásához." +[toolFormat] +ANY = "bármilyen fájl" +CBR = "képregényarchívum (CBR)" +CBZ = "képregényarchívum (CBZ)" +CSV = "CSV-fájl" +EBOOK = "e-könyv" +EMAIL = "e-mail" +EXCEL = "táblázat" +HTML = "weboldal" +IMAGE = "képek" +JAVASCRIPT = "JavaScript-fájl" +JSON = "JSON-fájl" +MARKDOWN = "Markdown-fájl" +NONE = "nincs fájl" +PCL = "PCL-nyomtatófájl" +PDF = "PDF" +PDF_ENCRYPTED = "jelszóval védett PDF" +POSTSCRIPT = "PostScript-fájl" +PPT = "prezentáció" +TEXT = "szövegfájl" +VIDEO = "videó" +WORD = "Word-dokumentum" +XML = "XML-fájl" +XPS = "XPS-dokumentum" +ZIP = "ZIP-archívum" + [toolPanel] alpha = "Alfa" backToAllTools = "Vissza az összes eszközhöz" collapse = "Panel összecsukása" expand = "Panel kinyitása" goBack = "Vissza" +pdfTools = "PDF-eszközök" placeholder = "Válasszon egy eszközt a kezdéshez" premiumFeature = "Prémium funkció:" toolsHeader = "Eszközök" @@ -9806,11 +10829,6 @@ noToolsFound = "Nem találhatók eszközök" quickAccess = "GYORS ELÉRÉS" searchPlaceholder = "Eszközök keresése..." -[toolPicker.categories] -advancedTools = "Speciális eszközök" -recommendedTools = "Ajánlott eszközök" -standardTools = "Standard eszközök" - [toolPicker.subcategories] advancedFormatting = "Speciális formázás" automation = "Automatizálás" @@ -9824,6 +10842,9 @@ removal = "Eltávolítás" signing = "Aláírás" verification = "Ellenőrzés" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nem találhatók eszközök" noTools = "Nem állnak rendelkezésre eszközök" @@ -9950,28 +10971,53 @@ title = "Felhasználók" you = "(Ön)" [users.action] +cancelInvite = "Meghívás visszavonása" deleteTeam = "Csapat törlése" disableMfa = "MFA visszaállítása" move = "Áthelyezés csapatba" reinstate = "Visszaállítás" remove = "Eltávolítás a szervezetből" +removeTeam = "Eltávolítás a csapatból" rename = "Csapat átnevezése" resetPw = "Jelszó visszaállítása" suspend = "Felfüggesztés" unlock = "Fiók zárolásának feloldása" +[users.activity] +daysAgo = "{{count}} napja" +hoursAgo = "{{count}} órája" +justNow = "Épp most" +minutesAgo = "{{count}} perce" +monthsAgo_one = "{{count}} hónapja" +monthsAgo_other = "{{count}} hónapja" +never = "Soha" +weeksAgo_one = "{{count}} hete" +weeksAgo_other = "{{count}} hete" +yearsAgo_one = "{{count}} éve" +yearsAgo_other = "{{count}} éve" + [users.cap] addProcessor = "+ Processor" approver = "Jóváhagyja a házirendet" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Visszavonja a(z) {{email}} címre küldött meghívást? A jelenlegi hivatkozással nem fog tudni csatlakozni." +cancelInviteTitle = "Meghívás visszavonása" deleteTeamBody = "Törli a(z) {{name}} csapatot? A csapatnak előbb üresnek kell lennie - helyezze át tagjait egy másik csapatba, és nem birtokolhat integrációs konfigurációkat." deleteTeamTitle = "Csapat törlése" disableMfaBody = "Eltávolítja {{name}} MFA regisztrációját? Ha szükséges, a következő bejelentkezéskor újra beállítja." disableMfaTitle = "MFA visszaállítása" removeBody = "Véglegesen eltávolítja {{name}} felhasználót a szervezetből? Ez nem vonható vissza." +removeTeamBody = "Eltávolítja {{name}} személyt a csapatból? A fiókja megmarad, de elveszíti a hozzáférést a csapat erőforrásaihoz." removeTitle = "Tag eltávolítása" [users.empty] @@ -9982,10 +11028,8 @@ title = "Még nincsenek tagok" addToTeam = "Hozzáadás csapathoz" guestCount = "{{count}} vendég" guests = "Vendégek" -guestsDesc = "Külső együttműködők, a megosztott tartalomra korlátozva. Csak Editor." ledBy = "vezeti: {{owner}}" org = "Szervezet" -orgDesc = "Tulajdonosok szervezetszintű jogosultsággal és házirend-jóváhagyással" owners = "{{count}} tulajdonos" team = "{{name}} csapat" teamMeta = "{{count}} ember" @@ -10022,6 +11066,18 @@ username = "Felhasználónév" usernameError = "A felhasználónévnek legalább 3 karakterből kell állnia" usernamePlaceholder = "jsmith" +[users.invites] +by = "Meghívta: {{who}}" +cancel = "Visszavonás" +expiresInDays_one = "{{count}} nap múlva lejár" +expiresInDays_other = "{{count}} nap múlva lejár" +expiresToday = "Ma lejár" +title = "Függőben lévő meghívások" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Hiba történt a backend elérésekor, vagy nincs hozzáférésed. Próbáld újra." title = "Nem sikerült betölteni a tagokat" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Mellékletek keresése" title = "Mellékletek" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Könyvjelző címe" +bookmarkTitleRequired = "A könyvjelző címe kötelező" closeSidebar = "Könyvjelzők oldalsávjának bezárása" collapseAll = "Összes könyvjelző összecsukása" +empty = "No bookmarks in this document" expandAll = "Összes könyvjelző kibontása" +searchPlaceholder = "Könyvjelzők keresése" +title = "Bookmarks" [viewer.comments] addComment = "Megjegyzés hozzáadása" @@ -10236,10 +11297,12 @@ locateAnnotation = "Megkeresés a dokumentumban" moreActions = "További műveletek" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Oldal {{page}}" placingHint = "Kattintson egy oldalra az elhelyezéshez… (mégse)" removeCommentOnly = "Csak a megjegyzés eltávolítása" saveReply = "Válasz mentése" +searchPlaceholder = "Search comments" title = "Megjegyzések" typeComment = "Megjegyzés" typeInsertText = "Szöveg beszúrása" @@ -10262,6 +11325,7 @@ unsavedDesc = "Mentetlen módosításai vannak" closeSidebar = "Rétegek oldalsávjának bezárása" hideAll = "Összes réteg elrejtése" showAll = "Összes réteg megjelenítése" +title = "Layers" [viewer.link] delete = "Hivatkozás törlése" @@ -10605,30 +11669,6 @@ title = "Vízjel szövege" image = "Kép" text = "Szöveg" -[wetSignature.tooltip] -header = "Aláírás létrehozási módjai" - -[wetSignature.tooltip.draw] -bullet1 = "Toll színének és vastagságának testreszabása" -bullet2 = "Törlés és újrarajzolás, amíg elégedett" -bullet3 = "Működik érintőkészülékeken (táblagépek, telefonok)" -description = "Kézzel írt aláírást hozhat létre egérrel vagy érintőképernyővel. Személyes, hiteles aláírásokhoz a legjobb." -title = "Aláírás rajzolása" - -[wetSignature.tooltip.type] -bullet1 = "Válasszon több betűtípus közül" -bullet2 = "Szöveg méretének és színének testreszabása" -bullet3 = "Tökéletes szabványosított aláírásokhoz" -description = "Begépelt szövegből generál aláírást. Gyors és egységes, üzleti dokumentumokhoz alkalmas." -title = "Aláírás begépelése" - -[wetSignature.tooltip.upload] -bullet1 = "Támogatja a PNG, JPG és más képformátumokat" -bullet2 = "A legjobb eredmény érdekében átlátszó hátteret javaslunk" -bullet3 = "A képet átméretezzük, hogy illeszkedjen az aláírási területhez" -description = "Töltsön fel előre elkészített aláírásképet. Ideális, ha van beszkennelt aláírása vagy céges logója." -title = "Aláíráskép feltöltése" - [workbenchBar] activeFiles = "Aktív fájlok" annotations = "Annotációk" @@ -10642,6 +11682,7 @@ exitRedaction = "Kilépés a kitakarási módból" exportAll = "PDF exportálása" exportSelected = "Kijelölt oldalak exportálása" formFill = "Űrlap kitöltése" +hideToolbar = "Hide toolbar" multiTool = "Többfunkciós eszköz" panMode = "Mozgatás mód" print = "PDF nyomtatása" @@ -10652,6 +11693,7 @@ redact = "Kitakarás" rotateLeft = "Forgatás balra" rotateRight = "Forgatás jobbra" ruler = "Vonalzó / Mérés" +rulerSettings = "Méretarány beállításai" save = "Mentés" saveAll = "Összes mentése" saveAs = "Mentés másként" @@ -10661,6 +11703,7 @@ selectAll = "Összes kijelölése" selectByNumber = "Kiválasztás oldalszámok szerint" selectLanguage = "Nyelv kiválasztása" share = "Megosztás" +showToolbar = "Show toolbar" toggleAnnotations = "Annotációk láthatóságának be-/kikapcsolása" toggleAttachments = "Mellékletek be-/kikapcsolása" toggleBookmarks = "Könyvjelzők be-/kikapcsolása" diff --git a/frontend/editor/public/locales/id-ID/translation.toml b/frontend/editor/public/locales/id-ID/translation.toml index 6b3ed7d7e5..5b3a35918a 100644 --- a/frontend/editor/public/locales/id-ID/translation.toml +++ b/frontend/editor/public/locales/id-ID/translation.toml @@ -1,7 +1,6 @@ alphabet = "Abjad" apply = "Terapkan" applyAndContinue = "Simpan & Keluar" -areYouSure = "Apakah Anda yakin ingin keluar?" back = "Kembali" black = "Hitam" blue = "Biru" @@ -46,10 +45,8 @@ filesSelected = "berkas dipilih" font = "Jenis huruf" fontSizeTooltip = "Ukuran teks nomor halaman dalam poin. Angka yang lebih besar membuat teks lebih besar." fontTypeTooltip = "Keluarga font untuk nomor halaman. Pilih sesuai gaya dokumen Anda." -genericSubmit = "Kirim" green = "Hijau" help = "Bantuan" -imgPrompt = "Pilih Gambar" incorrectPasswordMessage = "Kata sandi saat ini salah." info = "Informasi" invalidUndoData = "Tidak dapat membatalkan: data operasi tidak valid" @@ -62,7 +59,6 @@ loginAgreementTitle = "Perjanjian Login" logOut = "Keluar" marginTooltip = "Jarak antara nomor halaman dan tepi halaman." moreOptions = "Opsi Lainnya" -multiPdfPrompt = "Pilih PDF (2+)" never = "Tidak pernah" no = "Tidak" noFileSelected = "Tidak ada file yang dipilih. Harap unggah satu." @@ -80,7 +76,6 @@ pageOrderPrompt = "Urutan Halaman Khusus (Masukkan daftar nomor halaman yang dip pages = "Halaman-halaman" pageSelectionPrompt = "Pemilihan Halaman Kustom (Masukkan daftar nomor halaman dipisahkan dengan koma 1,5,6 atau Fungsi seperti 2n+1) :" password = "Kata sandi" -pdfPrompt = "Pilih PDF" pin = "Sematkan File (tetap aktif setelah alat dijalankan)" pro = "Pro" processingComplete = "File Anda siap." @@ -110,6 +105,7 @@ undoSuccess = "Operasi berhasil dibatalkan" unknown = "Tidak diketahui" unpin = "Lepas Sematan File (ganti setelah alat dijalankan)" unsavedChanges = "Anda memiliki perubahan yang belum disimpan pada PDF Anda." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Perubahan Belum Disimpan" unsupported = "Tidak didukung" username = "Nama pengguna" @@ -356,6 +352,7 @@ text = "Izin ini mengontrol apa yang dapat dilakukan pengguna dengan PDF. Paling title = "Ganti Perizinan" [addStamp] +preview = "Pratinjau Stempel" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Unduhan manual: klik tautan dan tempatkan berkas ke folder tessda noLanguages = "Tidak ditemukan bahasa tessdata di direktori yang dikonfigurasi." permissionNotice = "Jalur tessdata tidak dapat ditulisi. Unduhan akan dibuka di peramban; silakan simpan berkas .traineddata secara manual ke dalam folder tessdata." +[admin.settings.ai.documents] +description = "Konfigurasikan model embedding dan pengaturan retrieval yang digunakan untuk menjawab pertanyaan atas dokumen. Diterapkan ke mesin AI saat disimpan." +title = "Dokumen & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Biarkan kosong untuk menggunakan kredensial environment milik mesin. Berlaku untuk deployment self-hosted dengan satu mesin." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Tersimpan - biarkan kosong untuk mempertahankan key saat ini" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL endpoint embedding yang kompatibel dengan OpenAI / Ollama, mis. http://ollama:11434/v1. Harus mengarah ke endpoint internal tepercaya (sensitif terhadap SSRF)." +label = "Base URL embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Nama model embedding. Teks bebas; saran hanya sebagai petunjuk." +label = "Model embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provider yang digunakan untuk mengubah teks dokumen menjadi embedding vektor." +label = "Provider embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Jumlah maksimum pencarian retrieval yang dapat dijalankan agen per permintaan." +label = "Pencarian maks" + +[admin.settings.ai.documents.reindexNote] +body = "Mengubah model embedding langsung berlaku, tetapi dokumen yang diindeks dengan model sebelumnya harus diindeks ulang agar pencarian mengembalikan hasil yang benar." +title = "Indeks ulang diperlukan" + +[admin.settings.ai.documents.saved] +reindexNote = "Jika Anda mengubah model embedding, indeks ulang dokumen yang ada agar pencarian menggunakan model baru." + +[admin.settings.ai.documents.topK] +description = "Jumlah chunk paling relevan yang diambil per pencarian." +label = "Top K" + +[admin.settings.ai.general] +description = "Hubungkan Stirling ke mesin AI Python dan pilih kemampuan AI mana yang ditampilkan. Perubahan berlaku saat dimulai ulang." +title = "Mesin AI" + +[admin.settings.ai.general.capabilities] +description = "Aktifkan atau nonaktifkan fitur AI satu per satu. Fitur yang dinonaktifkan disembunyikan di aplikasi." +title = "Kemampuan" + +[admin.settings.ai.general.enabled] +description = "Sakelar utama. Saat nonaktif, tidak ada alat AI, agen, atau panggilan mesin yang tersedia." +label = "Aktifkan AI" + +[admin.settings.ai.general.features.chat] +description = "Asisten percakapan untuk bekerja dengan PDF." +label = "Asisten chat" + +[admin.settings.ai.general.features.classify] +description = "Kategorikan dokumen secara otomatis berdasarkan jenis atau konten." +label = "Klasifikasi dokumen" + +[admin.settings.ai.general.features.createPdf] +description = "Buat PDF baru (mis. dari HTML) melalui agen AI." +label = "Buat PDF dari prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Ajukan pertanyaan dan dapatkan jawaban berdasarkan dokumen yang diunggah." +label = "Pertanyaan dokumen" + +[admin.settings.ai.general.features.mathAuditor] +description = "Tinjau dokumen untuk kesalahan matematika dan numerik." +label = "Auditor matematika" + +[admin.settings.ai.general.features.pdfComment] +description = "Tambahkan komentar ulasan dan anotasi yang ditulis AI ke PDF." +label = "Agen komentar PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Batas waktu untuk operasi agen yang lebih berat seperti pembuatan dokumen." +label = "Batas waktu proses panjang (detik)" + +[admin.settings.ai.general.note] +body = "Mesin AI berjalan sebagai layanan terpisah. Secret bersama miliknya" +body2 = "diatur melalui variabel environment container. Provider API key dapat dimasukkan di halaman ini atau diberikan sebagai variabel environment mesin; key yang dimasukkan di sini dikirim ke mesin saat disimpan." +title = "Tentang mesin AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Batas waktu untuk respons chat yang di-stream (token demi token)." +label = "Batas waktu stream (detik)" + +[admin.settings.ai.general.test] +button = "Uji koneksi" +failBody = "Mesin AI tidak merespons. Periksa URL, pastikan container mesin berjalan, dan AI diaktifkan (perlu dimulai ulang setelah diaktifkan)." +failTitle = "Mesin AI tidak dapat dijangkau" +okBody = "Mesin AI merespons pemeriksaan kesehatan." +okTitle = "Mesin AI dapat dijangkau" + +[admin.settings.ai.general.timeoutSeconds] +description = "Batas waktu untuk permintaan AI standar ke mesin." +label = "Batas waktu permintaan (detik)" + +[admin.settings.ai.general.url] +description = "URL internal mesin AI Python, mis. http://stirling-pdf-engine:5001." +label = "URL mesin AI" + +[admin.settings.ai.limits] +description = "Guardrail untuk seberapa banyak pekerjaan yang dapat dilakukan permintaan AI dan berapa banyak yang berjalan bersamaan. Diterapkan ke mesin AI saat disimpan." +title = "Batas & Performa" + +[admin.settings.ai.limits.maxCharacters] +description = "Guardrail: tolak permintaan AI yang teks hasil ekstraksinya melebihi panjang ini." +label = "Karakter maks per permintaan" + +[admin.settings.ai.limits.maxPages] +description = "Guardrail: tolak permintaan AI yang melebihi jumlah halaman PDF ini." +label = "Halaman maks per permintaan" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Jumlah maksimum panggilan model yang sedang berjalan secara bersamaan di seluruh mesin." +label = "Konkurensi maks model" + +[admin.settings.ai.models] +description = "Pilih provider LLM dan model smart/fast yang digunakan mesin AI. Diterapkan ke mesin AI saat disimpan." +title = "Model & Provider" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Biarkan kosong untuk menggunakan kredensial environment milik mesin. Berlaku untuk deployment self-hosted dengan satu mesin." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Tersimpan - biarkan kosong untuk mempertahankan key saat ini" + +[admin.settings.ai.models.baseUrl] +description = "Base URL endpoint yang kompatibel dengan OpenAI / Ollama, mis. http://ollama:11434/v1." +label = "Base URL provider" +warning = "Base URL harus mengarah ke endpoint internal tepercaya. Mesin akan membuat permintaan sisi server ke sana, sehingga nilai yang tidak tepercaya sensitif terhadap SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Token output maksimum untuk model fast." +label = "Token maks model fast" + +[admin.settings.ai.models.fastModel] +description = "Model yang lebih murah dan lebih cepat untuk tugas ringan. Teks bebas; saran hanya sebagai petunjuk." +label = "Model fast" + +[admin.settings.ai.models.provider] +description = "Provider LLM yang dihubungi mesin." +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "Token output maksimum untuk model smart." +label = "Token maks model smart" + +[admin.settings.ai.models.smartModel] +description = "Model berkemampuan tinggi untuk penalaran kompleks. Teks bebas; saran hanya sebagai petunjuk." +label = "Model smart" + +[admin.settings.ai.saved] +body = "Perubahan dikirim ke mesin AI secara otomatis." +bodyNoPush = "Pengaturan disimpan. Pengaturan akan berlaku saat mesin AI berikutnya mengambil konfigurasinya." +title = "Pengaturan AI disimpan" + [admin.settings.badge] clickToUpgrade = "Klik untuk melihat detail paket" @@ -938,6 +1094,25 @@ label = "Buat Ulang saat Mulai" description = "Jumlah hari sertifikat akan berlaku" label = "Masa Berlaku Sertifikat (hari)" +[admin.settings.folderAccess] +description = "Direktori yang boleh dibaca dan ditulis oleh sumber folder dan output folder. Ini adalah batas keamanan: automations tidak pernah dapat diarahkan ke path server di luar daftar ini." +securityNote = "Biarkan kosong untuk menonaktifkan sumber dan output folder sepenuhnya. Direktori konfigurasi milik Stirling selalu tidak boleh diakses." +title = "Akses Folder" + +[admin.settings.folderAccess.implied] +description = "Direktori yang dikelola Stirling ini selalu diizinkan dan tidak dapat diubah di sini." +serverStorage = "Penyimpanan file server" +title = "Selalu diizinkan" +watchedFolder = "Folder yang dipantau Pipeline" + +[admin.settings.folderAccess.roots] +add = "Tambah" +empty = "Tidak ada folder yang diizinkan. Sumber dan output folder saat ini dinonaktifkan." +hint = "Masukkan path absolut, misalnya /data/inbox." +label = "Root folder yang diizinkan" +placeholder = "/data/inbox" +remove = "Hapus root folder" + [admin.settings.general] description = "Konfigurasikan pengaturan aplikasi seluruh sistem termasuk branding dan perilaku bawaan." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Lebar" [app] description = "Alternatif Adobe Acrobat Gratis (10M+ Unduhan)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konversi ke PDF/A-3b" convertToPdfA3bDescription = "Membuat PDF arsip dengan lampiran tersemat" @@ -1842,6 +2020,15 @@ export = "Ekspor" accessDenied = "Akses Ditolak" insufficientPermissions = "Anda tidak memiliki izin untuk melakukan tindakan ini." +[auth.callback] +completing = "Menyelesaikan autentikasi" +invalidToken = "Login OAuth gagal - token tidak valid." +missingToken = "Login OAuth gagal - tidak ada token yang diterima." +oauthFailed = "Login OAuth gagal. Silakan coba lagi." +pleaseWait = "Harap tunggu sementara kami menyelesaikan proses masuk Anda." +signedOut = "Anda telah keluar. Silakan masuk lagi." +windowMayClose = "Anda dapat menutup jendela ini setelah selesai." + [auth.displayName] guest = "Tamu" user = "Pengguna" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Mengamankan dokumen PDF dengan menghapus konten berpotensi description = "Alat ini akan secara otomatis mengganti nama file PDF berdasarkan kontennya. Ini menganalisis dokumen untuk menemukan judul yang paling sesuai dari teks." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Putar Otomatis" +tags = "putar otomatis,perbaiki orientasi,terbalik,miring,luruskan,orientasi,deteksi orientasi,koreksi rotasi,orientasi otomatis,dipindai,OSD" + +[autoRotate.confidenceThreshold] +title = "Ambang keyakinan" + +[autoRotate.detectionMode] +auto = "Otomatis" +osd = "Hanya OCR" +text = "Hanya teks" +title = "Metode deteksi" + +[autoRotate.error] +failed = "Terjadi kesalahan saat memutar PDF secara otomatis." + +[autoRotate.inferUndetected] +title = "Putar halaman berkeyakinan rendah agar sesuai" + +[autoRotate.report] +applied = "{{degrees}}° searah jarum jam" +confidenceValue = "Keyakinan {{value}}" +noChange = "Tidak ada perubahan" +pageLabel = "Halaman {{number}}" +summary = "{{rotated}} dari {{total}} halaman diputar, {{unchanged}} dibiarkan apa adanya" +title = "Laporan deteksi" + +[autoRotate.report.method] +inferred = "Dicocokkan" +none = "Dilewati" +osd = "OCR" +text = "Teks" + +[autoRotate.report.note] +belowThreshold = "Di bawah ambang" +blankPage = "Halaman kosong" +inferredFromDocument = "Sama seperti halaman lainnya" +noDominantDirection = "Arah teks bercampur" +osdFailed = "Tidak ditemukan teks yang dapat dibaca" +osdNoVerdict = "Tidak dapat menentukan arah atas" +tesseractUnavailable = "OCR tidak terpasang" +tooFewGlyphs = "Terlalu sedikit teks" + +[autoRotate.results] +title = "Hasil Putar Otomatis" + +[autoRotate.settings] +title = "Pengaturan" + +[autoRotate.tooltip.confidenceThreshold] +text = "Seberapa yakin pemeriksaan harus sebelum halaman diputar. Turunkan untuk memutar lebih banyak halaman, naikkan untuk membiarkan lebih banyak halaman tidak tersentuh." +title = "Ambang keyakinan" + +[autoRotate.tooltip.description] +text = "Menentukan arah atas setiap halaman dan memutarnya ke arah yang benar. Halaman yang sudah tegak dibiarkan apa adanya." + +[autoRotate.tooltip.detectionMode] +text = "Otomatis membaca teks di halaman terlebih dahulu, lalu melihat halaman sebagai gambar jika itu belum cukup. Hanya teks melewati pemeriksaan gambar, yang paling cepat untuk dokumen yang tidak pernah dipindai. Hanya OCR memperlakukan setiap halaman sebagai gambar." +title = "Metode deteksi" + +[autoRotate.tooltip.header] +title = "Putar Otomatis" + +[autoRotate.tooltip.inferUndetected] +text = "Halaman kosong atau hampir kosong tidak dapat diperiksa sendiri. Ketika setiap halaman yang dapat dibaca membutuhkan putaran yang sama, halaman-halaman ini diputar agar sesuai." +title = "Putar halaman berkeyakinan rendah agar sesuai" + [autoSizeSplitPDF] tags = "pdf, membagi, dokumen, organisasi" @@ -2189,33 +2443,6 @@ keywords = "Kata kunci: odd, even" numbers = "Angka/rentang: 5, 10-20" progressions = "Progresi: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Jenis Sertifikat" - -[certificateChoice.tooltip.organization] -bullet1 = "Dikelola oleh administrator sistem" -bullet2 = "Dibagikan kepada pengguna yang berwenang" -bullet3 = "Mewakili identitas perusahaan, bukan individu" -bullet4 = "Terbaik untuk: Dokumen resmi, tanda tangan tim" -description = "Sertifikat bersama yang disediakan oleh organisasi Anda. Digunakan untuk kewenangan penandatanganan seluruh perusahaan." -title = "Sertifikat Organisasi" - -[certificateChoice.tooltip.personal] -bullet1 = "Dibuat otomatis saat pertama kali digunakan" -bullet2 = "Terikat pada akun pengguna Anda" -bullet3 = "Tidak dapat dibagikan dengan pengguna lain" -bullet4 = "Terbaik untuk: Dokumen pribadi, akuntabilitas individu" -description = "Sertifikat yang dibuat otomatis dan unik untuk akun pengguna Anda. Cocok untuk tanda tangan individu." -title = "Sertifikat Pribadi" - -[certificateChoice.tooltip.upload] -bullet1 = "Memerlukan file P12/PFX dan kata sandi" -bullet2 = "Dapat diterbitkan oleh Otoritas Sertifikat eksternal" -bullet3 = "Tingkat kepercayaan lebih tinggi untuk dokumen legal" -bullet4 = "Terbaik untuk: Kontrak yang mengikat secara hukum, validasi eksternal" -description = "Gunakan file sertifikat PKCS#12 milik Anda. Memberikan kontrol penuh atas properti sertifikat." -title = "Unggah P12 Kustom" - [certSign] awaitingSignatures = "Menunggu tanda tangan" chooseCertificate = "Pilih File Sertifikat" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Sumber sertifikat" upload = "Unggah" -[certSign.tooltip.header] -title = "Tentang Mengelola Tanda Tangan" - -[certSign.tooltip.overview] -bullet1 = "Periksa tanda tangan yang ada dan validitasnya" -bullet2 = "Lihat informasi terperinci tentang penanda tangan dan sertifikat" -bullet3 = "Tambahkan tanda tangan digital baru untuk mengamankan dokumen Anda" -bullet4 = "Mendukung banyak file dengan navigasi mudah" -text = "Alat ini memungkinkan Anda memeriksa apakah PDF Anda ditandatangani secara digital dan menambahkan tanda tangan digital baru. Tanda tangan digital membuktikan siapa yang membuat atau menyetujui dokumen dan menunjukkan apakah dokumen telah berubah sejak ditandatangani." -title = "Apa yang dapat dilakukan alat ini?" - -[certSign.tooltip.signing] -bullet1 = "Mendukung format sertifikat PEM, PKCS12, JKS, dan server" -bullet2 = "Opsi untuk menampilkan atau menyembunyikan tanda tangan pada PDF" -bullet3 = "Tambahkan alasan, lokasi, dan nama penanda tangan" -bullet4 = "Pilih halaman tempat meletakkan tanda tangan yang terlihat" -bullet5 = "Gunakan sertifikat server untuk opsi sederhana 'Sign with Stirling-PDF'" -text = "Untuk menandatangani PDF, Anda memerlukan sertifikat digital (seperti PEM, PKCS12, atau JKS). Anda dapat memilih agar tanda tangan terlihat pada dokumen atau tetap tidak terlihat hanya untuk keamanan." -title = "Menambahkan Tanda Tangan" - -[certSign.tooltip.validation] -bullet1 = "Menunjukkan apakah tanda tangan valid atau tidak valid" -bullet2 = "Menampilkan informasi penanda tangan dan tanggal penandatanganan" -bullet3 = "Memeriksa apakah dokumen dimodifikasi setelah penandatanganan" -bullet4 = "Dapat menggunakan sertifikat kustom untuk verifikasi" -text = "Saat Anda memeriksa tanda tangan, alat ini memberi tahu apakah tanda tangan valid, siapa yang menandatangani dokumen, kapan ditandatangani, dan apakah dokumen telah berubah sejak penandatanganan." -title = "Memeriksa Tanda Tangan" - [changeCreds] changeUsername = "Perbarui nama pengguna Anda. Anda akan keluar setelah memperbarui." credsUpdated = "Akun diperbarui" @@ -2737,12 +2936,16 @@ text = "Agar izin ini tidak dapat diubah, gunakan alat Tambah Kata Sandi untuk m [chat.actions] copy = "Salin pesan" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opsi agen Stirling" +assistant = "AI Assistant" clearChat = "Hapus obrolan" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Operasi ini akan menggunakan kredit cloud Anda" tooltip = "Pilih warna dari layar" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Pilih warna" [common] +actions = "Actions" back = "Kembali" cancel = "Batal" close = "Tutup" +codeSample = "Code sample" collapse = "Ciutkan" confirm = "Konfirmasi" continue = "Lanjut" copied = "Disalin!" copy = "Salin" done = "Selesai" +enterValue = "Enter value" error = "Error" expand = "Perluas" loading = "Memuat..." next = "Berikutnya" +open = "Open" preview = "Pratinjau" previous = "Sebelumnya" refresh = "Muat Ulang" +remaining = "Tersisa" retry = "Coba Lagi" save = "Simpan" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Bersihkan yang dipilih" @@ -2988,6 +3199,7 @@ title = "Metode Kompresi" [compress.settings] desiredSize = "Ukuran file yang diinginkan" desiredSizePlaceholder = "Masukkan ukuran" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Kompresi adalah cara mudah untuk mengurangi ukuran file. Pilih Ukuran File untuk memasukkan ukuran target dan biarkan kami menyesuaikan kualitas untuk Anda. Pilih Kualitas untuk mengatur kekuatan kompresi secara manual." @@ -3190,6 +3402,24 @@ pdfOptions = "Opsi PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Pilih format sumber terlebih dahulu" settings = "Pengaturan" single = "Tunggal" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Pemantauan lanjutan" -allInOneWorkspace = "Ruang kerja PDF serba ada (penampil, alat & agen)" -apiSandbox = "Sandbox API" -dedicatedSupportSlas = "Dukungan khusus & SLA" -fasterThroughput = "Throughput 10x lebih cepat" -fullyPrivateFiles = "File sepenuhnya privat" -largeFileProcessing = "Pemrosesan file besar" -orgWideAccess = "Kontrol akses seluruh organisasi" -premiumAiModels = "Model AI premium" -prioritySupport = "Dukungan prioritas" -privateDocCloud = "Cloud Dokumen Privat" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Akses API aman" -standardThroughput = "Throughput standar" -unlimitedApiAccess = "Akses API tanpa batas" -unlimitedSeats = "Kursi tanpa batas" - [crop] autoCrop = "Pangkas otomatis ruang kosong" noFileSelected = "Pilih file PDF untuk mulai memotong" @@ -3346,6 +3558,7 @@ message = "Gagal menyetel penangan PDF default" title = "Error" [defaultApp.prompt] +dontRemind = "Jangan ingatkan saya lagi" message = "Jadikan Stirling PDF aplikasi default untuk membuka file PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Cari..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Edit" @@ -3644,11 +3861,13 @@ shareSelected = "Bagikan yang Dipilih" sharing = "Berbagi" showAll = "Tampilkan Semua" showHistory = "Tampilkan Riwayat" +sortBy = "Sort files" sortByDate = "Urutkan berdasarkan Tanggal" sortByName = "Urutkan berdasarkan Nama" sortBySize = "Urutkan berdasarkan Ukuran" storage = "Penyimpanan" storageState = "Penyimpanan" +storageUsed = "Storage used" synced = "Tersinkron" title = "Unggah File PDF" toolChain = "Alat yang Diterapkan" @@ -3694,13 +3913,17 @@ addFiles = "Tambahkan file" addingFiles = "Menambahkan file…" collapse = "Ciutkan bilah samping" customizeGroups = "Sesuaikan grup" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Buka file untuk memulai" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Perluas bilah samping" -files = "File" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive belum dikonfigurasi" leaveMyFiles = "Keluar dari File Saya" +library = "Perpustakaan PDF" myFiles = "File Saya" noFiles = "Belum ada file" openFileManager = "Buka pengelola file" @@ -3708,37 +3931,36 @@ openFromComputer = "Buka dari komputer" openSettings = "Buka pengaturan" other = "Lainnya" recent = "Terbaru" -search = "Cari" -searchPlaceholder = "Cari file..." viewAll = "Lihat semua {{count}} file" [fileSidebar.fileItem] closeViewer = "Tutup penampil" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Buka di penampil" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Tambah" -addLabel = "Tambahkan label…" -categoryIconAria = "Pilih ikon untuk {{name}}" -createCategory = "Buat kategori" -delete = "Hapus kategori" done = "Selesai" -hide = "Sembunyikan kategori" -newCategory = "Nama kategori baru…" -removeLabel = "Hapus {{name}}" -rename = "Ganti nama" reset = "Reset ke default" -search = "Cari label…" -show = "Tampilkan kategori" subtitle = "Kelompokkan file Anda ke dalam kategori induk. Tambahkan label yang sudah ada atau label baru ke kategori, ganti namanya, atau buat sendiri. File yang tidak termasuk dalam kategori mana pun akan muncul di “Lainnya”." title = "Kategori sidebar" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Tambahkan ke ruang kerja" addToWorkspaceCount = "Tambahkan {{count}} ke ruang kerja" @@ -3749,7 +3971,6 @@ backToMyFiles = "Kembali ke File Saya" breadcrumbs = "Jalur folder" cancel = "Batal" classification = "Klasifikasi" -clearSearch = "Hapus pencarian" clearSelection = "Hapus pilihan" closeDetails = "Tutup detail" create = "Buat" @@ -3781,6 +4002,7 @@ downloadAll = "Unduh semua" downloadVersion = "Unduh versi ini" dropOverlay = "Lepaskan file untuk mengunggah" dropOverlaySub = "File dimulai di Lokal. Gunakan 'Pindahkan ke' atau 'Simpan ke cloud' untuk mengaturnya ke dalam folder." +duplicate = "Duplicate" file = "File" fileInfo = "File info" fileMenu = "Tindakan file" @@ -3808,13 +4030,12 @@ refresh = "Segarkan dari server" remove = "Hapus" removeVersion = "Hapus versi ini" rename = "Ganti nama" +renamed = "Diganti nama" renameFolder = "Ganti nama folder" resizeFolderTree = "Ubah ukuran pohon folder (tombol panah, Shift untuk langkah lebih besar; klik ganda untuk menyesuaikan otomatis)" save = "Simpan" saveToServer = "Simpan ke server" saveToServerDisabledHint = "Menyimpan ke server tidak diaktifkan di server ini. Minta administrator Anda untuk mengaktifkannya." -search = "Cari" -searchPlaceholder = "Cari folder ini & subfoldernya" selectAll = "Pilih semua" selectAllHint = "Klik untuk memilih semua. Tip: tahan Ctrl (atau Cmd) untuk menambah file satu per satu, Shift untuk memilih rentang." selectedCount = "{{count}} dipilih" @@ -3874,6 +4095,10 @@ title = "Belum ada file cloud" hint = "File yang disimpan tanpa mengunggah tetap di sini. Lepaskan file untuk menambahkannya." title = "Tidak ada file khusus lokal" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "File yang Anda buka atau edit akan muncul di sini." title = "Belum ada yang diubah" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Tidak dapat menghapus folder." deleteFolderFailedDetail = "Tidak dapat menghapus folder: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Tidak dapat memperbarui tampilan folder." folderAppearanceFailedDetail = "Tidak dapat memperbarui tampilan folder: {{message}}" moveFilesFailed = "Tidak dapat memindahkan file." @@ -3910,9 +4137,7 @@ count = "File" folder = "Folder" labels = "Label" modified = "Diubah" -name = "Nama" size = "Ukuran" -toolHistoryAtVersion = "Rangkaian alat kumulatif" totalSize = "Total ukuran" type = "Jenis" versionHistory = "Perjalanan versi" @@ -3945,6 +4170,11 @@ localHint = "Hanya disimpan di peramban ini" shared = "Dibagikan" sharedHint = "Dibagikan kepada Anda melalui tautan" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Terlama dulu" modifiedDesc = "Terbaru dulu" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Tidak ada file di penyimpanan. Buka beberapa file terleb open = "Buka" openFile = "Buka File" openFiles = "Buka File" +selectFile = "Select {{name}}" selectFromStorage = "Pilih dari Penyimpanan" upload = "Unggah" uploadFile = "Unggah File" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Semua tersimpan" +analyzingFields = "Analysing form fields..." +extractCsvError = "Gagal mengekstrak CSV" +extractXlsxError = "Gagal mengekstrak XLSX" +filled = "filled" flattenAfterFilling = "Ratakan setelah mengisi" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "wajib" +requiredFieldsError = "Harap isi semua bidang wajib" rescanFields = "Pindai ulang bidang" rescanFormFields = "Pindai ulang bidang formulir" save = "Simpan" +saveShortcut = "Ctrl+S untuk menyimpan" +unsavedChanges = "Perubahan belum disimpan" [formFill.sidebar] close = "Tutup bilah sisi" @@ -4309,6 +4552,11 @@ desc = "Mengganti nama file PDF secara otomatis berdasarkan header yang terdetek tags = "deteksi-otomatis,berbasis-header,atur,ganti-nama" title = "Ganti Nama File PDF Otomatis" +[home.autoRotate] +desc = "Deteksi orientasi setiap halaman dan putar tegak secara otomatis." +tags = "putar otomatis,perbaiki orientasi,terbalik,miring,luruskan,orientasi,deteksi orientasi,koreksi rotasi,orientasi otomatis,dipindai,OSD" +title = "Putar Otomatis" + [home.autoSizeSplitPDF] tags = "otomatis,pecah,ukuran" title = "Pisah otomatis ukuran/jumlah" @@ -4608,15 +4856,12 @@ hint = "Unggah file PNG, JPG, SVG, atau gambar lain untuk ditempatkan pada PDF. label = "Unggah Gambar" placeholder = "Pilih file gambar" -[imageToPdf] -tags = "konversi,img,jpg,gambar,foto" - [imageToPDF] header = "Gambar ke PDF" title = "Gambar ke PDF" -[infoBanner] -dismiss = "Tutup" +[imageToPdf] +tags = "konversi,img,jpg,gambar,foto" [invite] acceptError = "Gagal membuat akun" @@ -4647,11 +4892,9 @@ welcomeTitle = "Anda telah diundang!" [landing] addFiles = "Tambahkan File" -heroSubtitle = "Seret dan letakkan atau tambahkan PDF yang sudah ada untuk memulai." mobileUpload = "Unggah dari Ponsel" openFromComputer = "Buka dari komputer" uploadFromComputer = "Unggah dari komputer" -workbenchEmptyStateHero = "Letakkan PDF di mana saja" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Masukkan nama pengguna" failedToSignIn = "Gagal masuk dengan {{provider}}: {{message}}" forgotPassword = "Lupa kata sandi?" loggingIn = "Sedang Masuk..." -logIn = "Masuk" login = "Masuk" magicLinkSent = "Tautan ajaib dikirim ke {{email}}! Periksa email Anda dan klik tautannya untuk masuk." mfaCode = "Kode Autentikasi" @@ -4715,26 +4957,10 @@ signInWith = "Masuk dengan" title = "Masuk" unexpectedError = "Kesalahan tak terduga: {{message}}" updatePassword = "Perbarui kata sandi" -useEmailInstead = "Masuk dengan email" useMagicLink = "Gunakan tautan ajaib sebagai gantinya" username = "Nama pengguna" youAreLoggedIn = "Anda sudah masuk!" -[login.slides.edit] -alt = "Edit PDF" -subtitle = "Dengan lebih dari selusin alat untuk membantu Anda menyensor, menandatangani, membaca, dan memanipulasi PDF, Anda pasti menemukan yang Anda cari." -title = "Edit PDF untuk menampilkan/mengamankan informasi yang Anda inginkan" - -[login.slides.overview] -alt = "Sekilas Stirling PDF" -subtitle = "Suite cloud berfokus privasi untuk PDF yang memungkinkan Anda mengonversi, menandatangani, menyensor, dan mengelola dokumen, ditambah 50+ alat hebat lainnya." -title = "Satu tempat untuk semua kebutuhan PDF Anda." - -[login.slides.secure] -alt = "Amankan PDF" -subtitle = "Tambahkan kata sandi, sensor konten, dan kelola sertifikat dengan mudah." -title = "Lindungi informasi sensitif di PDF Anda" - [margin] large = "Besar" medium = "Sedang" @@ -4818,6 +5044,34 @@ uploadSuccess = "Unggah Berhasil!" uploadSuccessMessage = "Gambar Anda telah ditransfer." validating = "Memvalidasi sesi..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Pindai untuk mengunggah foto. Gambar otomatis dikonversi ke PDF." descriptionNoConvert = "Pindai untuk mengunggah foto dari perangkat seluler Anda." @@ -4836,8 +5090,13 @@ title = "Unggah dari Ponsel" tags = "Alat multi,Operasi multi,UI,klik seret,front end,sisi klien,interaktif,sulit diatur,pindah" title = "Alat Multi PDF" -[navbar] -search = "Cari" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentikasi tidak berhasil. Anda dapat menutup jendela ini dan mencoba lagi." @@ -4966,6 +5225,7 @@ activeFiles = "Tampilan Active Files menampilkan semua PDF yang allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Tutup" cropSettings = "Sekarang setelah kita memilih file yang ingin dipotong, kita dapat mengonfigurasi alat Crop untuk memilih area yang ingin kita potong pada PDF." +dialogLabel = "Onboarding" fileCheckbox = "Mengklik salah satu file akan memilihnya untuk diproses. Anda dapat memilih beberapa file untuk operasi batch." fileReplacement = "File yang telah diubah akan menggantikan file asli di Workbench secara otomatis, sehingga Anda dapat dengan mudah menjalankannya melalui lebih banyak alat." filesButton = "Tombol Files pada bilah Akses Cepat memungkinkan Anda mengunggah PDF untuk menggunakan alat." @@ -4976,6 +5236,7 @@ pinButton = "Anda dapat menggunakan tombol Pin jika Anda ingin results = "Setelah alat selesai dijalankan, langkah Review akan menampilkan pratinjau hasil di panel ini, dan memungkinkan Anda membatalkan operasi atau mengunduh file. " runButton = "Setelah alat dikonfigurasi, tombol ini memungkinkan Anda menjalankan alat pada semua PDF yang dipilih." selectCropTool = "Mari pilih alat Crop untuk mendemonstrasikan cara menggunakan salah satu alat." +stepOf = "Langkah {{current}} dari {{total}}" toolInterface = "Ini adalah antarmuka alat Crop. Seperti yang Anda lihat, belum banyak karena kita belum menambahkan file PDF untuk dikerjakan." workbench = "Ini adalah Workbench - area utama tempat Anda melihat dan mengedit PDF." wrapUp = "Selesai! Anda telah mempelajari area utama aplikasi dan cara menggunakannya. Klik tombol Bantuan kapan saja untuk melihat tur ini lagi." @@ -4988,6 +5249,26 @@ showMeAround = "Tunjukkan saya" skipForNow = "Lewati dulu" skipTheTour = "Lewati tur" +[onboarding.checklist] +dismiss = "Tutup" +title = "Siapkan Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Jalankan Stirling secara native di mesin Anda" +title = "Unduh Stirling untuk Desktop" + +[onboarding.checklist.inviteTeam] +description = "Berkolaborasi dengan tim Anda" +title = "Undang anggota tim" + +[onboarding.checklist.shareAnalytics] +description = "Bantu meningkatkan Stirling" +title = "Bagikan data penggunaan anonim" + +[onboarding.checklist.takeTour] +description = "Lihat cara kerja Stirling dalam panduan singkat" +title = "Ikuti tur" + [onboarding.desktopInstall] body = "Stirling paling baik digunakan sebagai aplikasi desktop. Anda dapat menggunakannya offline, mengakses dokumen lebih cepat, dan mengedit secara lokal di komputer Anda." selectOs = "Pilih sistem operasi" @@ -4996,7 +5277,21 @@ titleWithOs = "Unduh untuk {{osLabel}}" [onboarding.mfa] authenticationCode = "Kode autentikasi" +description = "Amankan akun Anda dengan menautkan aplikasi autentikator. Pindai QR code atau masukkan kunci penyiapan, lalu konfirmasi kode 6 digit untuk menyelesaikan." +enable = "Aktifkan MFA" +enableError = "Tidak dapat mengaktifkan autentikasi dua faktor. Periksa kode dan coba lagi." +enterCode = "Masukkan kode 6 digit dari aplikasi Anda." +enterCodeError = "Masukkan kode autentikasi untuk melanjutkan." +logout = "Keluar" +openAuthenticator = "Buka Google Authenticator, Authy, atau 1Password." qrCodeLoading = "Membuat kode QR Anda…" +regenerateQrCode = "Buat ulang QR code" +scanQrCode = "Pindai QR code atau masukkan kunci penyiapan di bawah." +setupError = "Tidak dapat memulai penyiapan dua faktor. Silakan coba lagi." +setupKey = "Kunci penyiapan (entri manual)" +stepByStep = "Langkah demi langkah" +success = "MFA telah diaktifkan. Anda sekarang dapat melanjutkan." +title = "Penyiapan Autentikasi Multi-Faktor" [onboarding.planOverview] adminBodyLoginDisabled = "Setelah Anda mengaktifkan mode login, Anda dapat mengelola pengguna, mengonfigurasi pengaturan, dan memantau kesehatan server. {{freeTierLimit}} orang pertama di server Anda dapat menggunakan Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Ringkasan Admin" userBody = "Undang rekan, tetapkan peran, dan jaga dokumen Anda tetap teratur dalam satu workspace yang aman. Aktifkan mode login kapan pun Anda siap berkembang dari penggunaan solo." userTitle = "Ringkasan Paket" +[onboarding.processorIntro] +body = "Stirling kini menjalankan Kebijakan — aturan otomatis yang mengklasifikasi, mengamankan, dan memproses setiap dokumen saat tiba. Siapkan dan pantau proses berjalan di Processor." +cta = "Lihat Processor" +title = "Lihat Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Tahun prabayar" + +[payg.prepaid.banner] +exhausted = "Kapasitas prabayar Anda telah habis; tagihan terukur telah dilanjutkan. Isi ulang untuk mempertahankan diskon." +expiring_one = "Tahun prabayar Anda berakhir dalam {{count}} hari. Kapasitas yang tidak terpakai akan kedaluwarsa — isi ulang untuk memulai tahun baru." +expiring_other = "Tahun prabayar Anda berakhir dalam {{count}} hari. Kapasitas yang tidak terpakai akan kedaluwarsa — isi ulang untuk memulai tahun baru." +low = "Kapasitas prabayar Anda tersisa {{pct}}%. Isi ulang kapan saja agar pemrosesan tidak terputus." + +[payg.prepaid.card] +subtitle = "Digunakan sebelum tagihan terukur dan di luar batas pengeluaran Anda." +title = "Kapasitas prabayar" + +[payg.prepaid.meter] +capSuffix = "dari {{total}} kredit prabayar" +expires = "Kedaluwarsa {{date}}" + +[payg.prepaid.state] +exhausted = "Habis" +healthy = "Masih banyak" +low = "Hampir habis" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF ke PDF/A" tags = "arsip, jangka panjang, standar, konversi, penyimpanan, pelestarian" title = "PDF Ke PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "cetak,standar,konversi,produksi,pracetak,arsip" title = "PDF ke PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Kredit" xsmall = "100 Kredit" [plan.availablePlans] +currency = "Billing currency" subtitle = "Pilih paket yang sesuai kebutuhan Anda" title = "Paket yang Tersedia" @@ -5937,49 +6267,12 @@ keyDescription = "Tempel kunci lisensi dari email Anda" success = "Lisensi Diaktifkan!" successMessage = "Lisensi Anda berhasil diaktifkan. Anda sekarang dapat menutup jendela ini." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "diterapkan" -enforcing = "Menegakkan..." -failed = "Penegakan gagal" outputsUnavailable = "Output kebijakan tidak lagi tersedia untuk diunduh." partialOutputsUnavailable = "Beberapa output kebijakan tidak lagi tersedia untuk diunduh." -retrying = "Sibuk, mencoba lagi..." runNotFound = "Proses penegakan tidak dapat ditemukan lagi." -step = "langkah {{current}}/{{total}}" timedOut = "Penegakan mencapai batas waktu sebelum proses selesai." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Tutup" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Menerapkan {{names}}" applyingProgress = "Menerapkan {{names}} ({{done}} dari {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} dari {{total}} file tidak dapat diproses dan diekspo failureTitle = "Diekspor tanpa penegakan penuh" printPolicyAppliedBody = "PDF ini diperbarui agar memenuhi kebijakan. Tinjau perubahan, lalu cetak lagi." printPolicyAppliedTitle = "Kebijakan diterapkan sebelum mencetak" -queued = "+{{count}} dalam antrean" successTitle = "{{names}} diterapkan" summaryMore = "{{first}}, {{second}} dan {{more}} lainnya" summaryTwo = "{{first}} dan {{second}}" -[policies.enforcement.triggerVerb] -convert = "Menegakkan sebelum konversi" -default = "Menegakkan" -export = "Menegakkan sebelum ekspor" -input = "Menegakkan saat impor" -print = "Menegakkan sebelum mencetak" - -[policies.field] -accessLog = "Log akses" -archiveAfter = "Arsipkan setelah" -auditTrail = "Jejak audit" -belowThreshold = "Di bawah ambang batas" -destination = "Tujuan" -frameworks = "Frameworks" -immutableHold = "Penahanan immutable" -keepFor = "Simpan selama" -minConfidence = "Keyakinan min" -notify = "Beri tahu saat dirutekan" -onViolation = "Saat tidak patuh" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Tambah" -addPlaceholder = "Tambahkan label…" -customNote = "Disesuaikan untuk tim Anda." -defaultNote = "Menggunakan default bawaan, dibagikan dengan tim Anda." -duplicate = "\"{{name}}\" sudah ada." -edit = "Edit label" -empty = "Belum ada label." -export = "Ekspor JSON" -iconAria = "Pilih ikon untuk {{name}}" -import = "Impor JSON" -importError = "Tidak dapat mengimpor file tersebut." -managedNote = "Label tim dikelola oleh pemimpin tim Anda." -modalSubtitle = "Dibagikan dengan seluruh tim Anda. Pengklasifikasi memilih label yang cocok untuk setiap dokumen." -modalTitle = "Label klasifikasi" -pickIcon = "Gunakan ikon {{label}}" -removeAria = "Hapus {{name}}" -resetToDefault = "Reset ke default" -saveForTeam = "Simpan untuk tim" -saving = "Menyimpan…" -sectionLabel = "Label klasifikasi" -startFromScratch = "Mulai dari awal" -teamCount = "label tim" -tooLong = "Label maksimal {{max}} karakter." -ungrouped = "Tidak dikelompokkan" -view = "Lihat label" +categoryCount = "kategori" +hideCategory = "Sembunyikan kategori" +labelCount = "label" +sharedNote = "Label ini bawaan dan dibagikan ke seluruh tim Anda." +showCategory = "Tampilkan kategori" [policies.pii] account = "Nomor rekening (berlabel)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Nomor routing AS (ABA)" ssn = "Nomor Social Security" -[policies.settings] -noneExport = "Saat ini tidak ada kebijakan yang berjalan saat ekspor." -noneUpload = "Saat ini tidak ada kebijakan yang berjalan saat upload." -onExport = "Saat ekspor" -onUpload = "Saat upload" -reorderHandle = "Seret untuk menyusun ulang" -runOrderDesc = "Saat lebih dari satu kebijakan berjalan pada pemicu yang sama, kebijakan berjalan dalam urutan ini — masing-masing pada keluaran kebijakan sebelumnya. Seret untuk menyusun ulang." -title = "Pengaturan kebijakan" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opsi kebijakan" -policySettings = "Pengaturan kebijakan" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Coba lagi kebijakan yang gagal ({{count}})" -rowProgress = "{{completed}} dari {{total}} file diproses" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Apa itu kebijakan?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Secara otomatis menemukan dan menghitamkan detail sensitif — seperti nomor Social Security dan kartu — agar tidak dapat dibaca di dokumen." -sanitize = "Menghapus JavaScript tersembunyi dari file, sehingga tidak ada yang berjalan otomatis saat seseorang membukanya." -watermark = "Mencap tanda yang terlihat (mis. \"Confidential\") di setiap halaman." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Tutup" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} sedang diterapkan..." badgeRan = "Kebijakan {{name}} berjalan pada file ini" +badgeRunning = "{{name}} berjalan..." blockingAction = "{{action}} diblokir saat menerapkan kebijakan, harap tunggu..." dismiss = "Tutup overlay" enforcingTitle = "Menerapkan kebijakan..." @@ -6151,6 +6316,7 @@ viewAnyway = "Lihat file (kebijakan masih diterapkan)" [policyOption] autoRedactPhi = "Redaksi otomatis PHI" blockExport = "Blokir ekspor" +ccpa = "CCPA" contracts = "Kontrak" documents = "Dokumen" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Paket Editor" subscribed = "Paket Processor" unlinked = "Belum tertaut" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap dari dokumen" -sectionsAriaLabel = "Bagian pembuat agen" -selectorAriaLabel = "Agen" -subtitle = "Rancang, uji, dan rilis agen AI yang mengklasifikasikan, mengekstrak dari, dan merutekan dokumen Anda. Tentukan skenario, batasi akses alat, jalankan golden set, dan publikasikan versi." -title = "Pembuat Agen" - -[portal.agentBuilder.bootstrap] -cancel = "Batal" -dropzoneText = "Pilih dokumen sampel (PDF atau gambar)" -lead = "Letakkan dokumen sampel dan kami akan mengusulkan skenario serta skema ekstraksi yang dapat Anda sempurnakan. Tidak ada yang dipublikasikan hingga Anda meninjaunya." -submit = "Bootstrap agen" -subtitle = "Awali agen baru dari satu file representatif" -title = "Bootstrap dari dokumen" - -[portal.agentBuilder.empty] -description = "Bootstrap agen dari dokumen sampel untuk mengawali skenario dan skema ekstraksinya, lalu sempurnakan dan publikasikan." -title = "Belum ada agen" - -[portal.agentBuilder.evals] -casesPassing = "Kasus lulus" -columnCase = "Kasus evaluasi" -columnLatency = "Latensi" -columnResult = "Hasil" -fail = "gagal" -goldenSetPassRate = "Tingkat kelulusan golden set" -latencyMs = "{{ms}} ms" -notRun = "belum dijalankan" -pass = "lulus" -passRate = "Tingkat kelulusan" -runEvals = "Jalankan evaluasi" - -[portal.agentBuilder.evals.empty] -description = "Evaluasi mengubah skenario Anda menjadi golden set yang dapat diulang. Upgrade untuk merekam tingkat kelulusan dari waktu ke waktu dan membatasi publikasi berdasarkan itu." -title = "Belum ada golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "di seluruh golden set" -activeAgents = "Agen aktif" -avgPassRate = "Rata-rata tingkat kelulusan evaluasi" -fleetWide = "seluruh fleet" -latestPublished = "Terbaru dipublikasikan" -scenarios = "Skenario" -testCases = "kasus uji" -totalDescription_one = "{{count}} total" -totalDescription_other = "{{count}} total" - -[portal.agentBuilder.scenarios] -add = "Tambah" -addScenario = "Tambah skenario" -enable = "Aktifkan" -expectationLabel = "Perilaku yang diharapkan" -expectationPlaceholder = "Apa yang harus dilakukan agen" -inEval = "dalam evaluasi" -mute = "Bisukan" -muted = "dibisukan" -nameLabel = "Nama" -namePlaceholder = "mis. Eskalasi kepatuhan" - -[portal.agentBuilder.tabs] -evals = "Evaluasi" -scenarios = "Skenario" -tools = "Alat" -versions = "Versi" - -[portal.agentBuilder.tools] -broadAccess = "Akses luas" -deniedHint = "Alat yang dipilih diblokir. Yang lainnya tetap dapat dipanggil." -deniedTools = "Alat yang ditolak" -governanceGate = "Tata kelola alat tersedia pada paket Enterprise." -restricted = "Dibatasi" -restrictedAccess = "Akses alat terbatas" -restrictedDescription = "Izinkan setiap alat kecuali yang Anda tolak di bawah." - -[portal.agentBuilder.versions] -current = "saat ini" -historyGate = "Riwayat versi lengkap dan rollback tersedia pada paket Enterprise." -publish = "Publikasikan" -rollBack = "Kembalikan" - [portal.assistant] close = "Tutup asisten" error = "Tidak dapat menghubungi asisten." @@ -6365,9 +6451,21 @@ typing = "Mengetik" loading = "Memuat" redirectingToEditor = "Mengalihkan ke editor..." +[portal.billing.activation] +maybeLater = "Nanti saja" +title = "Aktifkan Processor" + +[portal.billing.activation.payg] +sub = "Tetapkan batas bulanan. Bayar sesuai yang Anda jalankan." +title = "Bayar sesuai pemakaian" + +[portal.billing.activation.prepay] +badge = "2 bulan gratis" +sub = "Satu faktur, kartu atau transfer bank." +title = "Bayar setahun di muka" + [portal.billing.checkout] noClientSecret = "Edge function mengembalikan no client_secret." -subtitle = "Tambahkan kartu untuk terus melampaui jatah gratis paket Editor Anda. Stripe menangani sisanya." title = "Aktifkan paket Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Pembayaran Anda berhasil, tetapi aktivasi memerlukan waktu sedikit lebih close = "Tutup" title = "Hampir selesai" +[portal.billing.checkout.cap] +amountAria = "Batas pengeluaran bulanan" +back = "Kembali" +continue = "Lanjutkan ke pembayaran" +error = "Tidak dapat menetapkan batas pengeluaran Anda" +finePrint = "Faktur diterbitkan bulanan. Batalkan kapan saja." +noLimit = "Tanpa batas" +note = "Anda tidak pernah ditagih melewati batas Anda. Pemrosesan hanya dijeda." +perMonth = "/ bln" +title = "Tetapkan batas pengeluaran Anda" + +[portal.billing.checkout.card] +fields = "Nomor kartu · MM / YY · CVC · ZIP" +label = "Detail kartu" +note = "Detail kartu dikumpulkan oleh Stripe. Stirling tidak pernah menyimpan PAN atau CVC." + [portal.billing.checkout.error] title = "Tidak dapat memulai checkout" @@ -6383,10 +6497,9 @@ body = "Pembayaran Anda berhasil. Kami mengaktifkan pemrosesan bermeter di selur hint = "Harap biarkan jendela ini tetap terbuka." title = "Mengaktifkan paket Processor Anda..." -[portal.billing.checkout.notConfigured] -bodyAfter = "di env portal untuk mengaktifkan checkout dalam aplikasi." -bodyBefore = "Atur" -title = "Stripe belum dikonfigurasi" +[portal.billing.checkout.pay] +limit = "Batas pengeluaran bulanan {{amount}}/bln. Pemrosesan dijeda saat mencapai batas." +limitNoCap = "Tanpa batas pengeluaran. Pemrosesan tidak akan dijeda. Faktur diterbitkan bulanan." [portal.billing.enterpriseUpsell] cta = "Buat penawaran Enterprise Anda" @@ -6409,10 +6522,12 @@ currentPlan = "Paket saat ini" freeForever = "Gratis selamanya" noTeamResolved = "Belum ada tim yang ditentukan pada dompet Anda — segarkan dan coba lagi." ownerOnly = "Hanya pemilik tim yang dapat mengaktifkan paket Processor." +payInvoice = "Bayar faktur untuk menyelesaikan" planName = "Editor" ssoIncluded = "SSO termasuk" switchOnProcessor = "Aktifkan Processor →" unlimitedUsers = "Pengguna tak terbatas" +viewQuote = "Lihat penawaran" [portal.billing.invoices] columnAmount = "Jumlah" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Permintaan API langsung" segmentApiLabel = "API" segmentAutomationDesc = "Otomatisasi & pipeline" segmentAutomationLabel = "Otomatisasi" +sizeMultiplier = "{{formatted}} PDF menggunakan pengali ukuran" +summary = "{{unique}} unik · {{units}} unit meter · rata-rata {{avg}} per PDF" +summaryNoRate = "{{unique}} unik · {{units}} unit meter" unit = "PDF bermeter" +unitsPending_one = "{{units}} unit meter menunggu sinkronisasi dari instans tertaut" +unitsPending_other = "{{units}} unit meter menunggu sinkronisasi dari instans tertaut" + +[portal.billing.prepaid] +capSuffix = "dari {{total}} kredit prabayar" +expires = "Kedaluwarsa {{date}}" +eyebrow = "Kapasitas prabayar" +note = "Digunakan sebelum tagihan terukur dan di luar batas pengeluaran Anda." +topUp = "Isi ulang" + +[portal.billing.prepaid.buy] +back = "Kembali" +calcTitle = "Hitung pembayaran tahunan Anda" +cancel = "Batal" +cancelPurchase = "Batalkan pembelian" +close = "Tutup" +continueToPayment = "Lanjutkan ke pembayaran" +finalise = "Finalisasi" +invoiceErrorTitle = "Tidak dapat membuat faktur" +loadingTitle = "Memuat penawaran Anda" +payTitle = "Bayar untuk tahun Anda" +step = "Langkah {{current}} dari {{total}}" + +[portal.billing.prepaid.calc] +change = "Ubah" +deployRow = "Deployment" +done = "Selesai" +enterpriseHint = "Ini adalah skala enterprise. Pada volume ini, tarif enterprise mengalahkan diskon self-serve apa pun. Penguncian tarif, ketentuan, dan penawaran dalam hitungan menit." +governanceRow = "Tata kelola" +governanceValue = "{{label}} · {{count}} kebijakan" +handlesLabel = "Processor Anda" +handlesValue = "menangani {{volume}} kredit / bln" +pipelinesLabel = "Pipeline" +policiesMeta = "{{count}} kebijakan" +poolCaption = "Satu pool berisi {{credits}} kredit untuk tahun ini. Bulan yang berat meminjam dari bulan yang ringan." +priceLabel = "Tahun Anda · 12 bulan dengan harga 10 bulan" +rateUnknown = "Kami akan menampilkan harga tepatnya saat checkout." +savings = "Anda menghemat {{amount}} · 2 bulan gratis." +sizingRow = "Ukuran" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Total pengguna" + +[portal.billing.prepaid.consent] +label = "Saya memahami bahwa saat kapasitas prabayar saya habis atau kedaluwarsa setelah 12 bulan, pemrosesan secara otomatis berlanjut dengan tarif bayar sesuai pemakaian terukur standar (hingga batas pengeluaran saya) kecuali saya membatalkan, dan bahwa saya dapat membatalkan kapan saja dari portal penagihan." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Dikelola oleh Stirling. Aktif dalam hitungan menit." +cloudValue = "Stirling Cloud · Dikelola" +selfhost = "Self-hosted" +selfhostDesc = "Berjalan di infrastruktur pribadi. Tarif sama." +selfhostValue = "Self-hosted · Infrastruktur pribadi" + +[portal.billing.prepaid.offer] +cta = "Tinjau penawaran" +subtitle = "Bayar setahun pemrosesan PDF di muka dan dapatkan dua bulan gratis, digunakan sebelum tagihan terukur, di luar batas pengeluaran Anda." +title = "Dapatkan 12 bulan dengan harga 10 bulan" + +[portal.billing.prepaid.pay] +accountName = "Nama pemegang akun" +accountNamePlaceholder = "mis. Jane Smith" +companyName = "Nama perusahaan (opsional)" +companyNamePlaceholder = "mis. Acme Inc." +downloadInvoice = "Unduh faktur" +payOnline = "Bayar online" +poNumber = "Nomor PO (opsional)" +poNumberPlaceholder = "mis. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Lanjutan" +advancedDesc = "Pipeline menggerakkan sebagian besar pemrosesan Anda." +none = "Tidak ada" +noneDesc = "Belum menjalankan Pipeline. Aktifkan kapan saja." +standard = "Standar" +standardDesc = "Beberapa Pipeline memproses ulang PDF yang masuk." + +[portal.billing.prepaid.posture] +essentials = "Esensial" +essentialsDesc = "Klasifikasi, default, plus Berbagi" +governed = "Terkelola" +governedDesc = "Menambahkan Keamanan dan Routing" +regulated = "Teregulasi" +regulatedDesc = "Setiap kategori, termasuk Kepatuhan, Retensi, Ingesti" + +[portal.billing.prepaid.review] +download = "Unduh penawaran (PDF)" +downloading = "Menyiapkan…" +share = "bagikan untuk persetujuan" + +[portal.billing.prepaid.size] +compact = "Ringkas" +compactDesc = "File di bawah 25 MB, tanpa biaya data" +heavy = "Berat" +heavyDesc = "Dipindai atau banyak gambar, rutin 50 MB+" +standard = "Standar" +standardDesc = "Sebagian besar kecil, beberapa scan melewati 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Habis" +healthy = "Masih banyak" +low = "Hampir habis" [portal.billing.spendLimit] adjustLimit = "Sesuaikan batas" @@ -6494,6 +6713,8 @@ label = "Diproyeksikan melebihi." [portal.billing.spendThisMonth] eyebrow = "Pengeluaran bulan ini" +freeRemaining_one = "{{formatted}} PDF gratis tersisa" +freeRemaining_other = "{{formatted}} PDF gratis tersisa" processed_one = "{{formattedCount}} PDF diproses." processed_other = "{{formattedCount}} PDF diproses." processedWithRate_one = "{{formattedCount}} PDF diproses, dengan {{rate}} masing-masing." @@ -6510,6 +6731,7 @@ reachedTitle = "Batas pengeluaran bulanan tercapai" title = "Tidak dapat membuka portal Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "dari {{allowance}} PDF gratis digunakan" capSuffix_other = "dari {{allowance}} PDF gratis digunakan" eyebrow = "Uji coba Processor" @@ -6534,6 +6756,376 @@ signature = "tanda tangan" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Koneksi baru" +createTitleFor = "Hubungkan {{name}}" +delete = "Hapus" +edit = "Edit" +editTitle = "Edit koneksi" +unsupportedType = "Jenis koneksi ini tidak dapat diedit di sini." + +[portal.connections.categories.advanced] +description = "Untuk layanan yang belum kami dukung." +label = "Lanjutan" + +[portal.connections.categories.audit] +description = "Catat apa yang terjadi pada dokumen, di tempat auditor Anda sudah memeriksanya." +label = "Pencatatan audit & kepatuhan" + +[portal.connections.categories.notify] +description = "Beri tahu sistem lain bahwa sesuatu terjadi." +label = "Notifikasi & workflow" + +[portal.connections.categories.security] +description = "Pindai atau beri label dokumen sebelum berpindah lanjut." +label = "Keamanan & klasifikasi" + +[portal.connections.categories.signing] +description = "Kirim dokumen untuk ditandatangani atau dinotariskan." +label = "Tanda tangan elektronik & notarisasi" + +[portal.connections.categories.storage] +description = "Tempat dokumen yang diproses berakhir - bucket, drive, tiket, atau halaman." +label = "File & lampirkan" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling menyimpannya terenkripsi dan tidak pernah menampilkannya lagi." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Langkah yang menggunakan koneksi ini hanya dapat memanggil path di bawah URL ini, tidak pernah host lain." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Kata sandi" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Nama pengguna" + +[portal.connections.commonFields.webhookUrl] +helperText = "Tempel URL yang diberikan layanan kepada Anda. Hanya path di bawahnya yang dapat dipanggil." +label = "Webhook URL" + +[portal.connections.fields] +name = "Nama koneksi" +namePlaceholder = "mis. Produksi" + +[portal.connections.picker] +cancel = "Batal" +createNew = "Koneksi baru..." +placeholder = "Pilih koneksi" +save = "Simpan koneksi" + +[portal.connections.picker2] +back = "Ubah integrasi" +heading = "Apa yang ingin Anda hubungkan?" +noResultsBody = "Kami belum mendukungnya - beri tahu kami tentang hal itu dan kami akan mempertimbangkan untuk menambahkannya." +noResultsTitle = "Tidak ada yang cocok dengan \"{{query}}\"" +searchPlaceholder = "Cari integrasi - coba \"sign\", \"ocr\" atau nama produk" + +[portal.connections.types.api] +description = "Panggil HTTP API apa pun dari kebijakan. Anda memilih URL, autentikasi, dan payload." +label = "API khusus" + +[portal.connections.types.api.fields.authType] +label = "Autentikasi" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nama pengguna dan kata sandi" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token di header khusus" + +[portal.connections.types.api.fields.authType.options.none] +label = "Tidak ada" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Login untuk token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Langkah yang menggunakan koneksi ini hanya dapat memanggil path di bawah URL ini, tidak pernah host lain." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nama header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Path login" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Kata sandi" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Jika API membalas dengan tautan unduhan, hanya host ini yang boleh diambil. Pisahkan dengan koma. Biarkan kosong untuk hanya mengizinkan host milik base URL sendiri." +label = "Host hasil yang diizinkan" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Kirim token sebagai header ini" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Header respons tempat panggilan login mengembalikan token, mis. X-Auth-Token." +label = "Header respons token" + +[portal.connections.types.api.fields.username] +label = "Nama pengguna" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Pindai dokumen dari malware di server yang Anda jalankan - tidak ada yang keluar dari jaringan Anda." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Pemindaian virus dan konten untuk dokumen dalam kebijakan." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokir dokumen yang berisi makro, executable, skrip, atau objek tertanam - bukan hanya malware yang dikenal." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Lampirkan dokumen yang diproses ke halaman Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Kirim dokumen ke ConsignO Cloud (Notarius) untuk tanda tangan elektronik dan notarisasi." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Environment ConsignO Cloud Anda, termasuk /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Kata sandi" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Hanya diperlukan jika organisasi Anda menggunakan tenant." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nama pengguna" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Posting pesan ke Discord saat kebijakan memproses dokumen." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indeks event audit di Elasticsearch setiap kali kebijakan menangani dokumen." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Polling server FTP atau FTPS untuk dokumen baru." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Mode koneksi" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (disarankan)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP biasa mengirim kredensial dan file tanpa enkripsi; pilih FTPS jika server mendukungnya." +label = "Enkripsi" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Tidak ada (FTP biasa)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Posting pesan ke ruang Google Chat saat kebijakan berjalan." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Lampirkan dokumen yang diproses ke issue Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Kirim email dokumen atau notifikasi saat kebijakan berjalan." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Simpan dokumen yang diproses ke Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Deteksi data pribadi dalam teks dokumen menggunakan server Presidio yang Anda jalankan." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Baca dan terapkan label sensitivitas Microsoft Purview pada PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opsional. Hanya diperlukan untuk memilih label berdasarkan nama; pelabelan berfungsi tanpanya." +label = "ID aplikasi (client)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Dicapkan ke setiap label sebagai SiteId-nya, agar alat yang sadar Purview mengetahui organisasi mana yang menerapkannya." +label = "ID direktori (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Bucket S3 yang dapat dibaca oleh sumber dan dapat ditulis oleh output Pipeline." +label = "Penyimpanan S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Biarkan kosong untuk AWS. Atur ini untuk MinIO atau penyimpanan lain yang kompatibel dengan S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Kirim email dokumen atau notifikasi saat kebijakan berjalan." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Polling server SFTP (SSH) untuk dokumen baru." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opsional. Fingerprint host key SHA-256 server (dari `ssh-keyscan host | ssh-keygen -lf -`). Jika diatur, hanya server yang menyajikan key ini yang diterima; jika kosong, key yang terlihat pada koneksi pertama akan dipin dan perubahan berikutnya ditolak." +label = "Fingerprint host key" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Passphrase key" + +[portal.connections.types.sftp.fields.password] +helperText = "Biarkan kosong jika Anda mengautentikasi dengan private key sebagai gantinya." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opsional. Tempel private key OpenSSH untuk mengautentikasi dengan key alih-alih kata sandi." +label = "Private key" + +[portal.connections.types.slack] +description = "Posting pesan ke Slack saat kebijakan memproses dokumen." +label = "Slack" + +[portal.connections.types.smb] +description = "Pantau share jaringan SMB / CIFS (Windows atau Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Share jaringan" + +[portal.connections.types.smb.fields.domain] +helperText = "Opsional. Domain atau workgroup Windows, jika server Anda menggunakannya." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Kirim event audit ke Splunk setiap kali kebijakan menangani dokumen." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Kirim event audit ke kolektor HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Posting pesan ke channel Teams saat kebijakan berjalan." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Posting setiap dokumen, atau hanya detailnya, ke URL yang Anda pilih." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Picu skenario Zap atau Make dari kebijakan." +label = "Zapier & Make" + +[portal.docs] +browse = "Jelajahi dokumentasi" +viewSource = "Lihat sumber di GitHub" + [portal.docs.authentication] codeCaption = "setiap permintaan" eyebrow = "MEMULAI" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Kemampuan bernama yang dibundel dan dipanggil agen Anda sebagai satu alat. Setiap skill adalah rantai operasi deterministik dengan evaluasi terlampir." title = "Skill agen" +[portal.docs.toc] +title = "Di halaman ini" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERENSI API" @@ -6697,6 +7292,8 @@ type = "Jenis" user = "Pengguna" [portal.documents.queue.empty] +connectSource = "Hubungkan sumber" +createPipeline = "Buat Pipeline" description = "Saat sumber memasukkan dokumen ke pipeline Anda, dokumen akan muncul di sini untuk ditinjau." title = "Tidak ada dokumen dalam antrean" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Tidak ada dokumen yang cocok dengan filter ini." rowActions = "Tindakan baris" sensitiveLabel = "Sensitif" -sensitiveTitle = "Sensitif — akses diperlukan" [portal.documents.table.columns] action = "Pipeline / Tindakan" document = "Dokumen" +labels = "Labels" product = "Produk" status = "Status" time = "Waktu" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Terakhir terlihat" region = "Region" status = "Status" +target = "Target" version = "Versi" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Tampilan ini mengalami error tak terduga. Coba lagi, atau pilih b retry = "Coba lagi" title = "Terjadi kesalahan pada halaman ini" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Kembali" +body = "Unduh aplikasi desktop atau self-host server." +done = "Selesai" +guide = "Buka panduan {{name}}" +openInBrowser = "Buka di browser saja" +sectionDesktop = "Aplikasi desktop" +sectionSelfHosted = "Self-hosted" +title = "Instal Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Jalankan image resmi di server Anda." +note = "Terbuka di :8080. Login default: admin / stirling." +tagline = "Jalankan server dalam container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instal chart Helm resmi." +note = "Tambahkan PVC untuk /configs agar pengaturan tetap tersimpan." +tagline = "Deploy ke cluster dengan Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Build native untuk distribusi Anda." +guideBtn = "Lihat opsi instal Linux" +note = "Build AppImage, .deb, dan .rpm tersedia untuk distro umum." +tagline = "Aplikasi desktop native untuk distro Anda" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Atau melalui Homebrew" +detailBody = "Jalankan installer, atau gunakan Homebrew." +downloadBtn = "Unduh untuk macOS" +tagline = "Aplikasi native untuk Apple silicon dan Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Jalankan .jar di server bare metal." +downloadBtn = "Unduh .jar" +note = "Memerlukan Java 21+. Dibuka di :8080; login default admin / stirling." +runLabel = "Lalu jalankan" +tagline = "Jalankan .jar di server bare metal" +title = "Penyiapan server manual" + +[portal.home.download.windows] +altLabel = "Atau melalui winget" +detailBody = "Jalankan installer, atau gunakan package manager." +downloadBtn = "Unduh installer" +tagline = "Aplikasi native untuk Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktif" -invite = "Undang rekan tim" +activeOfDeployed = "{{active}} dari {{total}} aktif bulan ini" name = "Stirling PDF Editor" open = "Buka di browser" updated = "diperbarui {{time}}" +[portal.home.editor.deploy] +finish = "Selesaikan deployment" +options = "Lihat opsi deployment" +start = "Deploy Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Selamat siang" evening = "Selamat malam" morning = "Selamat pagi" -[portal.home.onboarding] -dismiss = "Abaikan pengaturan" -notStarted = "Belum dimulai" -progress = "{{done}} dari {{total}} selesai" -title = "Selesaikan pengaturan" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC seluruh organisasi, harga volume berkomitmen, dan penerapan air-gapped." -cta = "Mulai Uji Coba" -ctaQuote = "Dapatkan Penawaran" -lead = "Untuk 250+ karyawan." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Terapkan aplikasi desktop, gratis selamanya." -title = "Download PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktif · {{recommended}} direkomendasikan" -chip = "{{active}} aktif" -title = "Aktifkan kebijakan" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} terhubung · setiap PDF diatur di tempatnya berada" -chip = "{{connected}} terhubung" -title = "Hubungkan sumber Anda" - -[portal.home.quickActions] -subtitle = "Tugas utama untuk hari ini" -title = "Tindakan cepat" - -[portal.home.quickActions.buildPipeline] -blurb = "Composer 3 langkah di atas pustaka op bertipe" -title = "Buat pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agen, webhook, folder terpantau" -title = "Hubungkan sumber" - -[portal.home.quickActions.issueApiKey] -blurb = "Kunci terbatas dengan batas laju dan IP allowlist" -title = "Terbitkan API key" - [portal.infrastructure] manageEditorDeployment = "Kelola penerapan Editor" sectionsAriaLabel = "Bagian infrastruktur" @@ -6878,13 +7541,9 @@ heading = "API key" subheading = "Kredensial terbatas dengan batas laju per kunci, izin, dan IP allowlist." [portal.infrastructure.apiKeys.card] -allowedIps = "IP yang diizinkan" -anyIp = "IP apa pun (tanpa allowlist)" created = "Dibuat" lastUsed = "Terakhir digunakan" -permissions = "Izin" -rateLimit = "Batas laju" -rateLimitValue = "{{value}} req/min" +revoke = "Cabut key" usageMonth = "Penggunaan bulan ini" usageToday = "Penggunaan hari ini" @@ -6892,6 +7551,15 @@ usageToday = "Penggunaan hari ini" description = "Buat kunci terbatas untuk mulai memanggil Stirling API." title = "Belum ada API key" +[portal.infrastructure.apiKeys.error] +load = "Tidak dapat memuat API key Anda. Silakan coba lagi." + +[portal.infrastructure.apiKeys.revoke] +body = "Cabut \"{{name}}\"? Integrasi apa pun yang masih menggunakan key ini akan segera mulai menerima error 401. Ini tidak dapat dibatalkan." +cancel = "Batal" +confirm = "Cabut key" +title = "Cabut API key" + [portal.infrastructure.audit] filterAriaLabel = "Filter peristiwa audit berdasarkan kategori" heading = "Log audit" @@ -6935,6 +7603,7 @@ all = "Semua" auth = "Auth" config = "Konfigurasi" elevation = "Eskalasi" +policy = "Kebijakan" processing = "Pemrosesan" security = "Keamanan" @@ -6944,19 +7613,30 @@ title = "Anda tidak memiliki akses" [portal.infrastructure.audit.metrics] config = "Konfigurasi" -elevation = "Eskalasi" +policy = "Kebijakan" processing = "Pemrosesan" totalEvents = "Total peristiwa · 24 jam" +[portal.infrastructure.auditCatLabel] +auth = "Autentikasi" +config = "Konfigurasi" +elevation = "Eskalasi" +policy = "Kebijakan" +processing = "Pemrosesan" +security = "Keamanan" + +[portal.infrastructure.auditStatusLabel] +danger = "Error" +info = "Info" +success = "Berhasil" +warning = "Peringatan" + [portal.infrastructure.createKey] cancel = "Batal" createKey = "Buat kunci" done = "Selesai" -ipAllowlistHelper = "Rentang CIDR yang dipisahkan koma. Biarkan kosong untuk mengizinkan IP apa pun." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Nama kunci" keyNamePlaceholder = "mis. Production · ingest" -permissionsLabel = "Izin" secretKeyCaption = "Kunci rahasia" secretWarning = "Simpan ini di pengelola rahasia. Stirling hanya menyimpan hash — tidak ada cara untuk memulihkannya nanti." subtitle = "Batasi kunci ke minimum yang dibutuhkan. Anda dapat merotasi atau mencabutnya kapan saja." @@ -6964,207 +7644,9 @@ subtitleCreated = "Salin rahasia ini sekarang — tidak akan ditampilkan lagi." title = "Buat API key" titleCreated = "Kunci dibuat" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Diterapkan oleh" -environment = "Environment" -product = "Produk" -status = "Status" -version = "Versi" -when = "Kapan" - -[portal.infrastructure.deployments.recent] -heading = "Penerapan terbaru" -subheading = "Rollout terbaru di seluruh produk dan environment." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instans" -latency = "Latensi" -load = "Beban" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Throughput" -uptime = "Uptime" -version = "Versi" - -[portal.infrastructure.deployments.regions] -heading = "Region" -subheading = "Kesehatan langsung untuk setiap region Stirling yang diterapkan — latensi, beban, dan versi rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "Region yang diterapkan muncul di sini setelah workspace Anda disediakan." -title = "Tidak ada region yang diterapkan" - -[portal.infrastructure.models] -heading = "Model" -msValue = "{{value}} ms" -subheading = "Katalog model dan routing yang mendukung pemrosesan dokumen di seluruh workspace Anda." - -[portal.infrastructure.models.byom] -description = "Daftarkan model on-prem atau yang dihosting sendiri dan pin ke region untuk pemrosesan terikat residensi data." -title = "Bawa model Anda sendiri" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Model terkelola yang tersedia untuk workspace Anda, dengan latensi dan biaya langsung." -subEnterprise = "Model terkelola, bawa-sendiri, dan on-prem — dengan pinning per region tersedia." - -[portal.infrastructure.models.catalogue.empty] -description = "Model dalam katalog workspace Anda muncul di sini." -title = "Tidak ada model tersedia" - -[portal.infrastructure.models.columns] -cost = "Biaya" -latency = "Latensi" -load = "Beban" -model = "Model" -status = "Status" -type = "Jenis" -version = "Versi" - -[portal.infrastructure.models.metrics] -activeModels = "Model aktif" -avgLatency = "Latensi rata-rata" -included = "Termasuk" -monthlySpend = "Pengeluaran model bulanan" - -[portal.infrastructure.models.routing] -empty = "Tidak ada aturan routing yang dikonfigurasi." -heading = "Aturan routing" -sub = "Model mana yang menangani setiap operasi. Default berlaku saat tidak ada aturan yang lebih spesifik yang cocok." -subLocked = "Rutekan operasi ke model tertentu — tersedia pada paket berbayar." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Tingkatkan ke Pro untuk mengontrol model mana yang menangani setiap operasi dan jenis dokumen." -title = "Routing model adalah fitur berbayar" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "Jenis dokumen" -modelForAria = "Model untuk {{operation}}" -operation = "Operasi" -routedTo = "Dirutekan ke" - -[portal.infrastructure.security.access.byok] -description = "Sediakan kunci dari KMS Anda sendiri. Stirling mengenkripsi dengannya tetapi masih dapat membaca." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Kunci tidak pernah meninggalkan KMS Anda. Stirling hanya menyimpan ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling mengelola kunci enkripsi. Paling sederhana — tanpa operasi kunci di sisi Anda." -label = "Kunci yang dipegang Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Kebijakan akses dokumen" -subheading = "Mengontrol siapa yang dapat mendekripsi dokumen yang diproses saat disimpan." - -[portal.infrastructure.security.attestations] -heading = "Atestasi kepatuhan" -noReport = "Tidak ada laporan tersedia" -subheading = "Postur audit per framework, dengan laporan tersedia pada kontrol yang diaudit." -viewReport = "Lihat laporan →" - -[portal.infrastructure.security.compliance] -heading = "Kepatuhan" -subheading = "Atestasi dan sertifikasi yang mencakup platform Stirling." - -[portal.infrastructure.security.empty] -description = "Konfigurasi keamanan workspace Anda akan muncul di sini." -title = "Postur keamanan tidak tersedia" - -[portal.infrastructure.security.hyokBanner] -description = "Dengan HYOK, kunci enkripsi tidak pernah meninggalkan KMS Anda. Stirling menyimpan dan memproses hanya ciphertext yang dapat Anda cabut kapan saja." -title = "Stirling tidak dapat mendekripsi dokumen Anda" - -[portal.infrastructure.security.ipAllowlist] -empty = "Tidak ada rentang IP yang dikonfigurasi — semua IP diizinkan." -heading = "IP allowlist" -sub = "Akses API dibatasi ke rentang CIDR ini." -subLocked = "Batasi akses API ke rentang IP yang dikenal — tersedia pada paket berbayar." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Tingkatkan ke Pro untuk membatasi akses API ke jaringan tertentu." -title = "IP allowlisting adalah fitur berbayar" - -[portal.infrastructure.security.ipColumns] -added = "Ditambahkan" -addedBy = "Ditambahkan oleh" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Manajemen kunci enkripsi" -keyId = "Pengidentifikasi kunci" -lastRotated = "Terakhir dirotasi" -rotateKey = "Rotasi kunci" -rotationPolicy = "Kebijakan rotasi" -subheading = "Penitipan kunci yang mengenkripsi dokumen saat disimpan — siapa yang dapat mendekripsi, dan bagaimana kunci dirotasi." - -[portal.infrastructure.security.managedBanner] -description = "Penitipan bring-your-own-key (BYOK) dan hold-your-own-key (HYOK) tersedia pada Enterprise. Tingkatkan untuk menyediakan kunci dari KMS Anda sendiri." -title = "Kunci dikelola oleh Stirling pada paket Anda" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · batas data GDPR" -label = "European Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "United States" - -[portal.infrastructure.security.residencyHeader] -heading = "Residensi data" -subheading = "Tempat dokumen disimpan dan diproses." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} digunakan" - -[portal.infrastructure.storage.empty] -description = "Penyimpanan yang terhubung dan penggunaan muncul di sini." -title = "Tidak ada penyimpanan yang dikonfigurasi" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktif" -activeRange = "0–{{value}}h" -archived = "Diarsipkan" -coldStorage = "cold storage" -deleted = "Dihapus" -never = "tidak pernah" -purged = "dibersihkan" - -[portal.infrastructure.storage.providers] -connect = "Hubungkan" -connected = "Terhubung" -heading = "Penyedia terhubung" -subheading = "Tempat artefak yang diproses ditulis." - -[portal.infrastructure.storage.retention] -heading = "Retensi" -subheading = "Berapa lama artefak disimpan sebelum penghapusan siklus hidup." -windowLabel = "Jendela retensi default" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} hari" -days_other = "{{count}} hari" -never = "Jangan pernah hapus" - -[portal.infrastructure.storage.totalUsage] -heading = "Total penggunaan" -progressLabel = "Penyimpanan digunakan" -subheading = "Penyimpanan yang digunakan di seluruh penyedia terhubung." +revoked = "Dicabut" [portal.infrastructure.tabs] apiKeys = "API Key" @@ -7174,6 +7656,49 @@ models = "Model" security = "Keamanan" storage = "Penyimpanan" +[portal.integrations] +availableHeading = "Tersedia" +comingSoonHeading = "Segera hadir" +connect = "Hubungkan" +connectionCount_one = "{{count}} koneksi" +connectionCount_other = "{{count}} koneksi" +customApi = "API kustom" +searchPlaceholder = "Cari integrasi" +subtitle = "Semua yang dapat diajak berkomunikasi oleh Stirling. Kredensial dibuat sekali di sini, lalu direferensikan oleh sumber, kebijakan, dan pipeline." +title = "Integrasi" +typedName = "Nama {{tool}}" + +[portal.integrations.filters] +all = "Semua" +audit = "Audit" +connected = "Terhubung" +notify = "Notifikasi" +security = "Keamanan" +signing = "Penandatanganan" +storage = "Penyimpanan" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Terhubung" + +[portal.integrations.table] +integration = "Integrasi" +worksWith = "Berfungsi dengan" + +[portal.integrations.worksWith] +pipelines = "Pipeline" +policies = "Kebijakan" +sources = "Sumber" + +[portal.legal] +draft = "{{label}} · draf" +loadError = "Tidak dapat memuat dokumen ini. Silakan coba lagi." +loading = "Memuat…" +title = "Dokumen hukum" + [portal.nav] agent-builder = "Agent Builder" components = "Komponen" @@ -7182,33 +7707,17 @@ documents = "Dokumen" editor = "Editor" home = "Beranda" infrastructure = "Infrastruktur" +integrations = "Integrasi" pipelines = "Pipeline" policies = "Kebijakan" procurement = "Pengadaan" -settings = "Pengaturan" sources = "Sumber" usage = "Penggunaan & Penagihan" users = "Pengguna" -[portal.notifications] -markAllRead = "Tandai semua dibaca" -title = "Notifikasi" -viewAll = "Lihat semua" - -[portal.notifications.ariaLabel] -none = "Notifikasi, tidak ada yang belum dibaca" -unread_one = "Notifikasi, {{count}} belum dibaca" -unread_other = "Notifikasi, {{count}} belum dibaca" - -[portal.notifications.count] -allRead = "semua dibaca" -loading = "memuat" -new_one = "{{count}} baru" -new_other = "{{count}} baru" - -[portal.notifications.empty] -description = "Tidak ada notifikasi baru." -title = "Anda sudah mengikuti semua" +[portal.nav.section] +platform = "Platform PDF" +processor = "Pemroses PDF" [portal.pipelines] subtitle = "Setiap pipeline dokumen otomatis di backend: rantai operasi terurut pada sekumpulan sumber, dijalankan pada trigger. Klik baris untuk melihat langkah dan sumbernya." @@ -7218,54 +7727,87 @@ title = "Pipeline" newPipeline = "Pipeline baru" [portal.pipelines.builder] -addStep = "Tambah alat" +activate = "Activate" back = "Kembali ke pipeline" +cannotFollow = "Tidak dapat mengambil {{produced}}" +chooseAccount = "Pilih akun" +chooseDestination = "Pilih tujuan" +chooseOperation = "Pilih apa yang dilakukan langkah ini" +chooseSource = "Pilih sumber" discard = "Buang perubahan" -enabled = "Diaktifkan" +inputs = "Input" +inputSource = "Sumber input" +inputTrigger = "Pemicu" keepEditing = "Lanjutkan mengedit" -needsUpload = "Memerlukan file yang di-upload" +moreActions = "More actions" +needsConfiguring = "Perlu disiapkan" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Tidak ada alat yang cocok dengan pencarian Anda." -pipelineSettings = "Pengaturan pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Cari alat" -selectToolBody = "Tambahkan alat untuk membangun pipeline Anda." -selectToolTitle = "Belum ada alat" -toolSettings = "Pengaturan alat" +sendToSystem = "Kirim ke sistem lain" +stepsIncompatible = "Langkah-langkah ini tidak dapat berjalan pada hasil dari langkah sebelumnya: {{tools}}." +stepsNeedSetup = "Langkah-langkah ini masih perlu disiapkan sebelum menyimpan: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operasi tidak dikenali, dipertahankan apa adanya." unsavedBody = "Anda memiliki perubahan yang belum disimpan. Simpan sebelum keluar, atau buang?" unsavedTitle = "Perubahan belum disimpan" -uploadUnsupported = "File yang di-upload belum didukung di pipeline, jadi langkah ini tidak dapat disimpan: {{tools}}." usesDefaults = "Berjalan dengan pengaturan default" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Menggabungkan setiap file dari langkah sebelumnya" +fan-out = "Berjalan sekali per file dari langkah sebelumnya" +format-mismatch = "Memerlukan {{accepts}}, tetapi langkah sebelumnya menghasilkan {{produced}}" +output-uncertain = "Mungkin tidak berjalan: output langkah sebelumnya bergantung pada cara penyiapannya" +source-mismatch = "Memerlukan {{accepts}}, tetapi input pipeline ini adalah {{produced}}" +undeclared-operation = "Tidak dapat memeriksa apa yang diterima langkah ini" [portal.pipelines.composer] addTool = "Tambah alat" -cancel = "Batal" -chainEmpty = "Tambahkan alat untuk mulai membangun pipeline Anda." create = "Buat pipeline" -directory = "Folder output" -directoryHelp = "Path absolut di server. Harus berada dalam folder yang diizinkan yang dikonfigurasi." +createPaused = "Create paused" editingUnsupported = "Menampilkan parameter alat ini untuk diedit belum didukung." -moveDown = "Pindah ke bawah" -moveUp = "Pindah ke atas" +editSource = "Edit source" name = "Nama" namePlaceholder = "mis. Redaction sweep" -noSources = "Belum ada sumber yang terhubung. Pipeline masih dapat berjalan pada file yang diberikan langsung kepadanya." noToolSettings = "Alat ini tidak memiliki pengaturan yang dapat dikonfigurasi." -operations_one = "Operasi ({{count}})" -operations_other = "Operasi ({{count}})" output = "Output" -removeStep = "Hapus operasi" save = "Simpan perubahan" scheduleEvery = "Jalankan setiap" -sources = "Sumber" -sourcesLoading = "Memuat sumber..." trigger = "Trigger" triggerManual = "Hanya manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "hari" hours = "jam" minutes = "menit" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Hapus \"{{name}}\"? Ini tidak dapat dibatalkan." cancel = "Batal" @@ -7273,28 +7815,62 @@ confirm = "Hapus" title = "Hapus pipeline?" [portal.pipelines.detail] +clearHistory = "Hapus riwayat" delete = "Hapus pipeline" run = "Jalankan sekarang" [portal.pipelines.empty] action = "Buat pipeline" +connectSource = "Hubungkan sumber" description = "Buat pipeline pertama Anda: pilih sumber yang dijalankannya, rangkai operasi, dan pilih tempat output dituju." title = "Belum ada pipeline" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktif" paused = "Dijeda" total = "Pipeline" -[portal.pipelines.output] -folder = "Tulis ke folder" -inline = "Kembalikan file" - [portal.pipelines.run] +allProcessed_one = "Tidak ada yang dijalankan: {{count}} dokumen sumber sudah diproses." +allProcessed_other = "Tidak ada yang dijalankan: semua {{count}} dokumen di sumber sudah diproses." completed_one = "Jalankan selesai." completed_other = "Semua {{count}} proses berjalan selesai." empty = "Tidak ada yang dijalankan: sumber tidak memiliki dokumen untuk diproses." failed = "Proses berjalan gagal: {{error}}" +inFlight = "Tidak ada yang baru untuk dijalankan: dokumen masih diproses dari proses sebelumnya." +parked_one = "Tidak ada yang dijalankan: {{count}} dokumen sebelumnya gagal dan diparkir. Perbaiki penyebabnya, lalu hapus riwayat untuk mencobanya lagi." +parked_other = "Tidak ada yang dijalankan: {{count}} dokumen sebelumnya gagal dan diparkir. Perbaiki penyebabnya, lalu hapus riwayat untuk mencobanya lagi." running = "Proses berjalan dimulai; masih berlangsung." timeout = "Proses berjalan memakan waktu lebih lama dari yang diharapkan; mungkin masih selesai di latar belakang." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Sumber" status = "Status" steps = "Langkah" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Pemantauan folder" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Terjadwal" [portal.policies] +defaultName = "Kebijakan {{category}}" subtitle = "Otomasi tetap yang menerapkan pipeline alat pada setiap dokumen. Setiap kebijakan dipicu saat upload atau ekspor, menjalankan rantai alatnya, dan menyimpan versi yang diberlakukan bersama aslinya." title = "Kebijakan" [portal.policies.card] comingSoon = "Tingkatkan ke Enterprise" notSetUp = "Belum disiapkan" +requiresAiEngine = "Memerlukan mesin AI" + +[portal.policies.categories.classification] +desc = "Identifikasi jenis setiap dokumen saat upload dan beri tag metadata untuk pengarsipan dan pencarian." +label = "Klasifikasi" [portal.policies.categories.compliance] desc = "Terapkan persyaratan HIPAA, GDPR, SOC 2, atau FedRAMP pada setiap dokumen." @@ -7344,6 +7927,13 @@ label = "Keamanan" [portal.policies.config] scopeAll = "Semua dokumen" +[portal.policies.config.classification] +summary = "Mengklasifikasikan setiap dokumen yang di-upload berdasarkan label tim Anda dan memberi tag metadata." + +[portal.policies.config.classification.rules] +0 = "Klasifikasikan" +1 = "Beri tag metadata" + [portal.policies.config.compliance] summary = "Memvalidasi dokumen terhadap framework regulasi sebelum keluar dari sistem." @@ -7371,6 +7961,29 @@ minConfidence = "Kepercayaan minimum" 2 = "Nama" 3 = "Normalkan" +[portal.policies.config.purview] +label = "Terapkan label sensitivitas Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant Purview" +labelId = "ID label" +labelIdHelp = "GUID label, dari portal Purview. Menerapkan label menulis metadatanya ke PDF; ini tidak dapat mengenkripsi, yang memerlukan klien Microsoft." +labelName = "Nama label (opsional)" +labelNameHelp = "Direkam bersama ID agar orang dan alat dapat mengenali label." + +[portal.policies.config.purview.fields.method] +help = "Purview membedakan label yang diterapkan kebijakan dari label yang dipilih seseorang." +label = "Cara label direkam" +privileged = "Sebagai dipilih oleh seseorang" +standard = "Sebagai diterapkan otomatis" + +[portal.policies.config.purviewRead] +label = "Baca label Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant Purview" +connectionHelp = "Membaca label yang sudah dibawa dokumen, sehingga langkah berikutnya dapat bertindak berdasarkan label tersebut." + [portal.policies.config.retention] summary = "Menerapkan berapa lama dokumen disimpan, kapan diarsipkan, dan kapan dihapus." @@ -7438,6 +8051,7 @@ title = "Belum ada aktivitas" [portal.policies.endpoints] addWatermark = "Watermark" autoRedact = "Redaksi PII" +classifyAndLabel = "Klasifikasikan" compressPdf = "Kompres" flatten = "Ratakan" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Kebijakan Anda disimpan dan akan muncul setelah koneksi dipulihka retry = "Coba lagi" title = "Backend tidak tersedia" +[portal.policies.operations] +change = "Ubah apa yang dilakukan langkah ini" +noResults = "Tidak ada langkah yang cocok. Coba nama produk, atau \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Cari langkah - coba \"scan\", \"attach\" atau nama produk" + +[portal.policies.operations.bodyMode] +binary = "Byte file mentah" +json = "Di dalam payload JSON" +multipart = "Sebagai upload file (multipart)" + +[portal.policies.operations.clamavScan] +description = "Server ClamAV Anda sendiri memeriksa dokumen; tidak ada yang keluar dari jaringan Anda." +label = "Pindai virus (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Menolak dokumen yang membawa makro, executable, skrip, atau objek tertanam." +label = "Blokir konten aktif" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive memeriksa dokumen dan proses berhenti jika dokumen tidak bersih." +label = "Pindai virus" + +[portal.policies.operations.confluenceAttach] +description = "Mengarsipkan dokumen yang diproses ke sebuah halaman." +label = "Lampirkan ke halaman Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Mengirim dokumen ke ConsignO Cloud untuk ditandatangani." +label = "Kirim untuk tanda tangan (ConsignO)" +note = "Mengirim dokumen. Mengambil salinan yang ditandatangani belum didukung, karena sebuah langkah tidak dapat meneruskan hasil ke langkah berikutnya." + +[portal.policies.operations.customApiCall] +description = "Susun panggilan sendiri: path, method, body, dan penanganan respons." +label = "Panggil API kustom" + +[portal.policies.operations.discordNotify] +description = "Memberi tahu channel bahwa kebijakan telah menangani dokumen." +label = "Posting pesan ke Discord" + +[portal.policies.operations.elasticIndex] +description = "Mencatat tindakan kebijakan sebagai dokumen yang dapat dicari." +label = "Indekskan peristiwa audit di Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart cocok untuk API upload; JSON cocok untuk API yang menginginkan file di dalam payload; binary mengirim byte mentah." +label = "Cara mengirim dokumen" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Body JSON dikirim apa adanya. {{document.base64}} membawa file; {{document.*}} dan {{run.*}} diisi per dokumen." +label = "Body JSON" + +[portal.policies.operations.fields.connection] +label = "Akun" + +[portal.policies.operations.fields.domain] +label = "Domain Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Field formulir yang diharapkan API untuk dokumen." +label = "Nama field file" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Dari" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Objek JSON berisi pasangan nama/nilai header. Kredensial koneksi ditambahkan untuk Anda." +label = "Header" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Dikirim sebagai body pesan. {{document.*}} dan {{run.*}} diisi per dokumen." +label = "Pesan" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "ID halaman" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Ditambahkan ke URL dasar koneksi. Ini tidak pernah dapat menjangkau host lain." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Lokasi di drive untuk menuliskannya. {{document.filename}} diisi per dokumen." +label = "Path tujuan" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Laporan membiarkan dokumen tidak berubah. Ganti menukarnya dengan apa pun yang dikembalikan." +label = "Apa yang dilakukan dengan balasan" + +[portal.policies.operations.fields.signerEmail] +label = "Email penanda tangan" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Subjek" +placeholder = "Diproses: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio menganalisis teks ini. Gunakan {{document.title}} atau tempel teks yang ingin diperiksa." +label = "Teks untuk diperiksa" + +[portal.policies.operations.fields.to] +label = "Ke" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Username Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Memberi tahu space bahwa kebijakan telah menangani dokumen." +label = "Posting pesan ke Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Mengarsipkan dokumen yang diproses ke sebuah issue." +label = "Lampirkan ke issue Jira" + +[portal.policies.operations.mailgunEmail] +description = "Mengirim dokumen yang diproses sebagai lampiran." +label = "Email dokumen (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Menulis dokumen yang diproses ke dalam folder." +label = "Upload ke Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Server Presidio Anda sendiri melaporkan data pribadi yang ditemukannya." +label = "Deteksi data pribadi" +note = "Presidio membaca teks, bukan file. Berikan teks yang ingin Anda periksa — dokumen itu sendiri tidak di-upload." + +[portal.policies.operations.responseMode] +replace = "Ganti dokumen dengan balasan" +report = "Pertahankan dokumen, catat balasan" + +[portal.policies.operations.sendgridEmail] +description = "Mengirim dokumen yang diproses sebagai lampiran." +label = "Email dokumen (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Memberi tahu channel bahwa kebijakan telah menangani dokumen." +label = "Posting pesan ke Slack" + +[portal.policies.operations.splunkEvent] +description = "Mencatat tindakan kebijakan, di tempat auditor Anda sudah melihat." +label = "Kirim peristiwa audit ke Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Mencatat tindakan kebijakan ke kolektor HTTP." +label = "Kirim peristiwa audit ke Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Memberi tahu channel bahwa kebijakan telah menangani dokumen." +label = "Posting pesan ke Teams" + +[portal.policies.operations.webhookPost] +description = "Mengirim dokumen itu sendiri ke URL yang Anda pilih." +label = "Posting dokumen ke webhook" + +[portal.policies.operations.zapierNotify] +description = "Menyerahkan detail proses ke otomatisasi Anda." +label = "Picu skenario Zap atau Make" + [portal.policies.stats] activeFor = "Aktif" dataProcessed = "Data diproses" @@ -7473,6 +8282,13 @@ label = "Dokumen diterapkan" description = "Dikonfigurasi tetapi tidak berjalan" label = "Dijeda" +[portal.policies.table] +appliesTo = "Berlaku untuk" +docs = "Dokumen ditegakkan" +enforces = "Menegakkan" +policy = "Kebijakan" +status = "Status" + [portal.policies.wizard.actions] back = "Kembali" cancel = "Batal" @@ -7480,6 +8296,10 @@ continue = "Lanjutkan" enablePolicy = "Aktifkan kebijakan" saveChanges = "Simpan perubahan" +[portal.policies.wizard.capability.classify] +desc = "Mengidentifikasi jenis dokumen dari label tim Anda dan memberi tag, sehingga dapat diarsipkan dan dicari berdasarkan kategori." +label = "Klasifikasikan dokumen" + [portal.policies.wizard.capability.compress] desc = "Mengompres dokumen menjadi ukuran file yang lebih kecil." label = "Kurangi ukuran file" @@ -7500,10 +8320,18 @@ label = "Redaksi informasi sensitif" desc = "Menghapus JavaScript tersembunyi sehingga tidak ada yang dapat berjalan otomatis saat dokumen dibuka." label = "Hapus konten aktif" +[portal.policies.wizard.capability.timestampPdf] +desc = "Membuktikan dokumen ada dalam bentuk persis ini pada suatu titik waktu, menggunakan otoritas timestamp independen. Hanya hash yang dikirim - dokumen tidak pernah meninggalkan server Anda." +label = "Tambahkan timestamp tepercaya" + [portal.policies.wizard.capability.watermark] desc = "Memberi cap tanda yang terlihat (mis. “Rahasia”) di setiap halaman." label = "Terapkan watermark" +[portal.policies.wizard.classification] +description = "Setiap dokumen yang di-upload diklasifikasikan berdasarkan label bawaan dan diberi tag dengan jenis yang sesuai. Kumpulan label dibagikan ke seluruh tim Anda." +labelsHeading = "Label klasifikasi" + [portal.policies.wizard.errors] noTools = "Aktifkan setidaknya satu alat dalam workflow terlebih dahulu." saveFailed = "Tidak dapat menyimpan kebijakan. Silakan coba lagi." @@ -7548,73 +8376,118 @@ setUp = "Siapkan kebijakan {{category}}" [portal.policies.wizard.workflow] description = "Pilih apa yang dilakukan kebijakan ini pada setiap dokumen yang diprosesnya." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktif" -noRule = "Belum ada aturan yang diterapkan" -subtitle = "Otomatisasi tetap yang dilalui setiap dokumen, terlepas dari pipeline mana yang menanganinya." -title = "Apa yang berjalan pada PDF Anda" - [portal.policySummary.action] -comingSoon = "Upgrade ke Enterprise" -configure = "Konfigurasi" setUp = "Siapkan" -[portal.policySummary.column] -activeRule = "Aturan aktif" -policy = "Kebijakan" -status = "Status" +[portal.processorFlow] +footnote = "Hitungan berdasarkan 24 jam terakhir. Kecepatan alur bersifat ilustratif." +liveBadge = "Live" +stats = "{{connected}} terhubung · {{processed}} PDF diproses" +title = "Pemroses PDF" -[portal.policySummary.empty] -description = "Setelah kebijakan dikonfigurasi, kategori akan muncul di sini." -title = "Belum ada kebijakan" +[portal.processorFlow.lens] +ariaLabel = "Mode tampilan" +flow = "Alur" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktif" -off = "Nonaktif" +[portal.processorFlow.outcomes] +count = "{{n}} · 24j" +failed = "Gagal" +heading = "Hasil" +success = "Terkirim" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktif" +count = "{{n}} · 24j" +heading = "Kebijakan" +setUp = "Siapkan" soon = "Segera" -[portal.processingStatus] -managePlan = "Kelola paket" -volumeSuffix = "PDF diproses · 30 hari terakhir" +[portal.processorFlow.sankey] +waist = "Kebijakan · {{n}} aktif" + +[portal.processorFlow.sankey.empty] +description = "Hubungkan sumber dan aktifkan kebijakan untuk melihat dokumen mengalir melalui pemroses." +title = "Belum ada alur" + +[portal.processorFlow.sources] +comingSoonTag = "Hubungkan" +editor = "Stirling PDF Editor" +heading = "Sumber" +perDay = "{{n}} / 24j" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Penerimaan email" [portal.procurement] -reset = "Reset pengadaan (demo)" subtitle = "Buat tim Anda dievaluasi, dikontrak, dan di-onboard. Setiap dokumen di satu tempat." title = "Pengadaan" -[portal.procurement.action] -download = "Download" -pay = "Bayar sekarang" -request = "Minta" -sign = "Tinjau & tanda tangani" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Setujui & berlangganan" +confidential = "Rahasia" confirm = "Saya telah membaca dan menyetujui Stirling Enterprise Agreement." -eyebrow = "Perjanjian" -intro = "Satu perjanjian gabungan mencakup kesepakatan Anda: Master Service Agreement, Order Form, EULA, dan Data Processing Agreement. Tinjau, lalu setujui untuk menerima penawaran menjadi langganan berkomitmen." -title = "Tinjau perjanjian enterprise Anda" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA, dan DPA, digabungkan menjadi satu tanda tangan." +download = "Unduh" +downloadDraftError = "Tidak dapat membuat PDF perjanjian - perender dokumen tidak tersedia. Silakan coba lagi sebentar lagi atau hubungi dukungan." +downloadError = "Tidak dapat mengunduh perjanjian yang ditandatangani. Silakan coba lagi." +legalName = "Nama entitas hukum" +legalNamePlaceholder = "Entitas hukum yang akan menandatangani" +loadError = "Tidak dapat memuat perjanjian. Silakan coba lagi." +loading = "Memuat perjanjian..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Minta perubahan" +scrollHint = "Gulir untuk meninjau" +signatory = "Nama penanda tangan" +signatoryPlaceholder = "Nama lengkap" +signatoryTitle = "Jabatan penanda tangan" +signatoryTitlePlaceholder = "mis. General Counsel" +signError = "Tidak dapat merekam tanda tangan Anda. Silakan coba lagi." [portal.procurement.builder] addons = "Add-on" +addressLine1 = "Baris alamat 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Baris alamat 2" +addressLine2Placeholder = "Suite, lantai, gedung" back = "Kembali" businessName = "Nama bisnis" businessNamePlaceholder = "Perusahaan Anda" +city = "Kota" +cityPlaceholder = "San Francisco" +completeRequired = "Silakan lengkapi field wajib (ditandai *) dengan email yang valid sebelum membuat penawaran." +contactEmail = "Email kontak" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nama kontak" +contactNamePlaceholder = "Jane Doe" continue = "Lanjutkan" -country = "Negara" -countryEuro = "Eurozone (EUR €)" -countryUK = "United Kingdom (GBP £)" -countryUS = "United States (USD $)" +done = "Selesai" eula = "Saya telah membaca dan menyetujui Stirling Enterprise EULA. Ini mengatur perjanjian yang dihasilkan dari penawaran ini." generate = "Buat penawaran" -included = "Termasuk" indemnification = "Indemnifikasi IP" indemnificationSub = "Kami membela klaim IP yang memenuhi syarat, sesuai EULA" -offlineLicense = "Lisensi offline / air-gapped" -offlineLicenseSub = "File lisensi yang dapat didownload untuk instance self-hosted air-gapped" +paperEyebrow = "Penawaran enterprise" +paperFor = "Disiapkan untuk" +pdfSize = "Ukuran PDF" +poNumber = "Nomor PO" +poNumberPlaceholder = "Opsional" +postalCode = "Kode pos" +postalCodePlaceholder = "94105" +posture = "Tata kelola" +posture_count = "~{{count}} kebijakan" +posture_essentials = "Esensial" +posture_essentialsSub = "Klasifikasi + Berbagi — default" +posture_governed = "Terkelola" +posture_governedSub = "Menambahkan Keamanan dan Routing" +posture_regulated = "Teregulasi" +posture_regulatedSub = "Setiap kategori — Kepatuhan, Retensi, Ingesti" qbr = "Tinjauan bisnis triwulanan" qbrSub = "SE Anda meninjau penggunaan dan roadmap setiap kuartal" +region = "Negara bagian / wilayah" +regionPlaceholder = "California" running = "{{annual}} / thn · {{years}} thn {{tcv}}" s1Sub = "Tim Anda, dan PDF yang Anda perkirakan akan dijalankan setiap tahun." s1Title = "Berapa banyak yang akan Anda proses?" @@ -7623,6 +8496,12 @@ s2Title = "Komitmen dan layanan" s3Sub = "Untuk penawaran dan perjanjian yang dihasilkannya." s3Title = "Detail Anda" serviceLevel = "Level layanan" +size_compact = "Ringkas" +size_compactSub = "Sebagian besar teks, di bawah 1 MB" +size_heavy = "Berat" +size_heavySub = "Dipindai atau banyak gambar, 5 MB+" +size_standard = "Standard" +size_standardSub = "Campuran teks dan gambar, 1 hingga 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 jam kerja · manajer akun khusus · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "Hari kerja yang sama · CSM bernama · +15%" slStandard = "Standard" slStandardSub = "Hari kerja berikutnya · CSM bersama · termasuk" stepOf = "Langkah {{n}} dari {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Opsional" term = "Jangka waktu" termDiscount = "Diskon komitmen multi-tahun {{pct}}% diterapkan" title = "Buat penawaran Anda" @@ -7637,6 +8518,7 @@ training = "Onboarding & pelatihan" trainingSub = "Sesi langsung untuk membuat tim Anda berjalan" users = "Total pengguna" usersPlaceholder = "mis. 250" +viewEula = "Baca" volEstimated = "Diperkirakan dari {{count}} pengguna (~2.000 PDF masing-masing, termasuk otomatisasi). Edit jika Anda lebih tahu." volManual = "Menggunakan angka Anda. Perkirakan ulang dari ukuran tim Anda kapan saja." volNoUsers = "Tidak yakin? Masukkan ukuran tim Anda dan kami akan memperkirakannya." @@ -7645,55 +8527,55 @@ volumePlaceholder = "mis. 1.000.000" years_one = "{{count}} tahun" years_other = "{{count}} tahun" -[portal.procurement.docs] -count_one = "{{count}} dokumen" -count_other = "{{count}} dokumen" -done = "Selesai" -here = "Anda di sini" -hide = "Sembunyikan" -optional = "Opsional" -paidAddon = "Add-on berbayar" -show = "Tampilkan" -subtitle = "Semua yang Anda butuhkan di setiap langkah perjalanan, ditampilkan saat kesepakatan bergerak melaluinya." -supportingSubtitle = "SOC 2, tinjauan keamanan, formulir pajak, dan lainnya, siap saat tim keamanan atau pengadaan Anda meminta. Beberapa dikenakan biaya satu kali." -supportingTitle = "Mendukung evaluasi Anda" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form, dan DPA dalam satu tanda tangan." +download = "Unduh" +eula = "EULA & Ketentuan Komersial" +eulaSub = "Perangkat lunak dan ketentuan komersial self-serve." +invoice = "Faktur" +invoiceSub = "Faktur langganan pertama Anda." +laterInvoice = "Tersedia setelah Anda menerima" +laterQuote = "Tersedia setelah penawaran Anda diterbitkan" +quote = "Penawaran" +quoteSub = "Penawaran enterprise terperinci Anda." +sla = "Lampiran SLA" +slaSub = "Tingkat dukungan dan target respons." +subprocessors = "Subprocessor" +subprocessorsSub = "Pihak ketiga yang memproses data atas nama Anda." +subtitle = "Dokumen kesepakatan Anda, tersedia kapan saja." title = "Dokumen" -upcoming = "Mendatang" +view = "Lihat" [portal.procurement.error] title = "Terjadi kesalahan" [portal.procurement.hero] -company = "Kesepakatan enterprise Anda" +barAria = "Tahap {{current}} dari {{total}}" ctaAgreement = "Tinjau & tanda tangani perjanjian" -ctaLive = "Anda sudah live" -ctaPayment = "Tambahkan pembayaran" +ctaExploring = "Siapkan uji coba Anda" ctaQuote = "Tinjau penawaran Anda" +ctaReviewQuote = "Tinjau penawaran" ctaTrial = "Buat penawaran Anda" +documents = "Dokumen" eyebrow = "Pengadaan enterprise" +eyebrowCompany = "Enterprise {{company}}" inviteTeammates = "Undang rekan tim" -keyDocs = "Dokumen utama" -nextStep = "Langkah berikutnya: {{action}}" -notStarted = "Belum dimulai" +licenseKey = "Key lisensi" +liveSub = "Organisasi Anda sudah diprovisikan dan lisensi Anda aktif." +liveTitle = "Anda sudah live di Stirling Enterprise" +next = "Berikutnya: {{stage}}" open = "Buka pengadaan" scheduleCall = "Jadwalkan panggilan" -setup1Sub = "Undang rekan tim Anda" -setup1Title = "Deploy PDF Editor" -setup2Sub = "Aktifkan pemrosesan di seluruh editor dan sumber lain" -setup2Title = "Hubungkan PDF Processor" -setup3Sub = "Aktifkan Security, Compliance, Routing, atau Retention saat Anda membutuhkannya" -setup3Title = "Tambahkan kebijakan yang direkomendasikan" +sentenceAgreement = "tinjau dan tandatangani perjanjian" +sentenceLive = "provisioning dan peluncuran" +sentencePayment = "purchase order dan pembayaran" +sentenceQuote = "tinjau dan terima penawaran Anda" +sentenceTrial = "siapkan workspace uji coba Anda" [portal.procurement.journey] daysLeft_one = "Sisa {{count}} hari" daysLeft_other = "Sisa {{count}} hari" -engineerLabel = "Engineer solusi Anda" -eyebrow = "Rollout Anda" -live = "Anda sudah live di Stirling Enterprise" -nextStep = "Langkah berikutnya: {{action}}" -subtitle = "Engineer solusi Anda ada di setiap langkah. Satu tindakan berikutnya pada satu waktu; checklist lengkap ada di bawah." -title = "Dari uji coba hingga live, satu jalur terpandu" -trialTitle = "Uji coba Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Satu tanda tangan mencakup MSA, order form, EULA, dan DPA." @@ -7727,6 +8609,9 @@ downloadError = "Belum dapat membuat file lisensi offline — silakan coba lagi downloadOffline = "Download lisensi offline (.lic)" hint = "Tempel kunci ini ke instance self-hosted untuk mengaktifkannya, atau simpan untuk catatan Anda. Jaga tetap aman." label = "Kunci lisensi Anda" +subtitle = "Aktifkan instance self-hosted dengan key ini, atau simpan untuk arsip Anda." +title = "Key lisensi Anda" +trialFileHint = "Ini adalah file lisensi uji coba Anda. Setelah perjanjian Anda siap, kembali dan unduh lagi — tidak seperti key lisensi online, file .lic tidak akan diperbarui sendiri." [portal.procurement.link] cta = "Tautkan akun" @@ -7739,65 +8624,62 @@ description = "Langganan Anda aktif dan lisensi Anda telah diterbitkan. Tim Anda eyebrow = "Live" title = "Anda sudah live di Stirling Enterprise" -[portal.procurement.locked] -description = "Kunci uji coba, penawaran volume berkomitmen, perjanjian satu tanda tangan, pembayaran, dan buku besar dokumen Anda semuanya ada di sini setelah Anda memulai evaluasi enterprise." -eyebrow = "Khusus Enterprise" -talkToSales = "Bicara dengan sales" -title = "Jalur pengadaan terbuka dengan Enterprise" - [portal.procurement.milestone] -accept = "Terima & lanjutkan" -description = "Download PDF untuk membagikannya dengan tim Anda, kembali untuk menerima saat Anda siap, atau buat perubahan." -download = "Download PDF" downloadError = "Belum dapat mendownload PDF penawaran — silakan coba lagi sebentar lagi." -edit = "Edit penawaran" -eyebrow = "Penawaran {{number}}" -perYear = " / thn" -preparedFor = "Disiapkan untuk {{company}}" -tcv = "{{value}} total nilai kontrak" -title = "Penawaran Anda siap" - -[portal.procurement.modal] -cancel = "Batal" -chooseFile = "Pilih file" -close = "Tutup" -downloadBody = "Download Anda akan segera dimulai." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Tidak ada file yang dipilih" -payBody = "Bayar kontrak berkomitmen Anda dengan kartu atau transfer bank melalui Stripe. Workspace Anda diprovision segera setelah pembayaran lunas." -payCta = "Lanjutkan ke Stripe" -payTitle = "Konfirmasi pembayaran" -requestBodyFree = "Kami membuat ini sesuai permintaan. Konfirmasi dan engineer solusi Anda akan segera mengirimkannya." -requestBodyPaid = "Ini adalah add-on berbayar. Konfirmasi dan engineer solusi Anda akan menentukannya serta mengirim dokumen." -requestCta = "Minta" -requestTitle = "Minta dokumen ini" -signBody = "Membuka Stirling Enterprise Agreement untuk e-signature: satu tanda tangan mencakup MSA, order form, EULA, dan DPA. Kami melakukan countersign otomatis dan Anda maju ke pembayaran." -signCta = "Buka untuk tanda tangan" -signTitle = "Tinjau dan tanda tangani perjanjian Anda" -uploadBody = "Kirim PO Anda kepada kami dan kami menagih sesuai ketentuan Anda. Seret PDF atau pilih file di bawah." -uploadCta = "Upload purchase order" -uploadTitle = "Upload purchase order Anda" [portal.procurement.payment] description = "Penawaran Anda diterima dan lisensi Anda sudah aktif — tim Anda dapat langsung mulai. Bayar invoice pertama saat Anda siap; Anda dapat membayar atau mendownloadnya di sini, tanpa perlu email." +downloadAgreement = "Unduh perjanjian yang ditandatangani" downloadInvoice = "Download invoice" -simulate = "Simulasikan pembayaran diterima (demo)" +eyebrow = "Pembayaran" title = "Langganan dibuat" viewInvoice = "Lihat & bayar invoice" +[portal.procurement.review] +acceptCta = "Terima penawaran" +annual = "Biaya tahunan (tahun 1)" +downloadCta = "Unduh penawaran" +poNumber = "Purchase order: {{po}}" +renewal = "Diperpanjang pada {{amount}}/tahun setelah masa berlaku (+{{pct}}% CPI)" +tcv = "Masa berlaku {{years}} tahun, dibayar penuh di muka: {{tcv}}" +validUntil = "Berlaku hingga {{date}}" + [portal.procurement.schedule] fallback = "Tidak dapat memuat scheduler." fallbackLink = "Buka penjadwalan di tab baru" +loading = "Memuat penjadwal..." subtitle = "Engineer solusi Anda akan memandu tim Anda melalui rollout. Pilih waktu yang sesuai untuk Anda." title = "Jadwalkan panggilan" -[portal.procurement.status] -action = "Perlu tindakan" -available = "Tersedia" -complete = "Selesai" -pending = "Tertunda" -request = "Atas permintaan" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Tersegel, tanpa outbound" +back = "Kembali" +businessName = "Nama bisnis" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Terkelola, mulai paling cepat" +continue = "Lanjutkan" +deployment = "Di mana harus di-deploy?" +eula = "Saya menyetujui Stirling EULA & Ketentuan Komersial." +fullName = "Nama lengkap" +fullNamePlaceholder = "Nama Anda" +invites = "Undang rekan tim · opsional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Jadwalkan panggilan" +seats = "Orang yang menguji" +seatsPlaceholder = "mis. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker atau Kubernetes" +start = "Mulai uji coba" +stepOf = "Langkah {{n}} dari {{total}}" +subtitle = "Gratis selama 14 hari, tidak perlu kartu." +subtitleDetails = "Beberapa detail untuk penawaran dan perjanjian Anda." +talkFirst = "Lebih suka bicara dulu?" +title = "Siapkan uji coba Anda" +viewEula = "Baca" +workEmail = "Email kerja" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Memperpanjang menambahkan 7 hari dan memberi tahu engineer solusi Anda." @@ -7808,109 +8690,113 @@ maxed = "Maksimal" subtitle = "Uji coba gratis Anda berjalan hingga {{date}}. Tidak perlu kartu." title = "Uji coba Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Harga volume berkomitmen, SSO + SCIM + RBAC seluruh org, audit immutable 90 hari, dan SE khusus." -homeCta = "Mulai Uji Coba →" -homeHeadline = "Proses jutaan PDF." - -[portal.recentActivity] -title = "Aktivitas terbaru" -viewAll = "Lihat semua" - -[portal.recentActivity.empty] -description = "Eksekusi pipeline, deploy, dan event agen akan muncul di sini." -title = "Belum ada apa pun di sini" - -[portal.search] -ariaLabel = "Cari" -placeholder = "Cari Stirling — endpoint, pipeline, dokumen…" - -[portal.search.empty] -noActionsDescription = "Tindakan cepat akan muncul di sini setelah tersedia." -noActionsTitle = "Tidak ada tindakan cepat" -noMatches = "Tidak ada kecocokan untuk \"{{query}}\"" -noMatchesDescription = "Coba kata kunci lain atau jelajahi katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Tautan akun" -[portal.shell.header] -accountFallback = "Akun" -accountMenu = "Menu akun" -darkMode = "Mode gelap" -lightMode = "Mode terang" -search = "Cari" -searchPlaceholder = "Cari…" -signOut = "Keluar" -switchToDark = "Beralih ke tema gelap" -switchToLight = "Beralih ke tema terang" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumen" linkAccount = "Tautkan akun Stirling" -planEditor = "Paket Editor" -planEnterprise = "Paket Enterprise" -planProcessor = "Paket Processor" primaryNav = "Navigasi utama" switchApp = "Ganti aplikasi" +[portal.shell.topbar] +closeNav = "Tutup navigasi" +openNav = "Buka navigasi" +search = "Cari" + [portal.sources] subtitle = "Koneksi input yang dapat digunakan ulang untuk memasukkan dokumen ke Stirling. Konfigurasi koneksi sekali, lalu referensikan dari sejumlah kebijakan. Klik baris untuk konfigurasinya dan kebijakan mana yang menggunakannya." title = "Sumber" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Hubungkan sumber" +[portal.sources.builder] +back = "Kembali ke sumber" +backToTypes = "Semua jenis sumber" +cancel = "Batal" +chooseHint = "Pilih dari mana dokumen berasal. Konektor yang diabu-abukan sedang disiapkan." +comingSoon = "Segera hadir" +comingSoonHeading = "Segera hadir" +create = "Buat sumber" +createTitle = "Hubungkan sumber" +delete = "Hapus" +editTitle = "Edit sumber" +enabled = "Diaktifkan" +save = "Simpan perubahan" + +[portal.sources.builder.folderAccess] +description = "Otomatisasi folder hanya dapat menggunakan folder yang diizinkan administrator. Tambahkan di pengaturan Akses Folder, lalu coba lagi." +openSettings = "Pengaturan Akses Folder" +title = "Folder ini tidak diizinkan" + [portal.sources.delete] body = "Hapus \"{{name}}\"? Tindakan ini tidak dapat dibatalkan. Kebijakan yang mereferensikannya perlu diperbarui." cancel = "Batal" confirm = "Hapus" title = "Hapus sumber?" -[portal.sources.detail] -closeAriaLabel = "Tutup detail" -delete = "Hapus sumber" -docs24h = "24 jam terakhir" -docs30d = "30 hari terakhir" -docsTotal = "Total terlihat" -docsTrend = "Dokumen selama 30 hari terakhir" -documents = "Dokumen" -edit = "Edit" -notReferenced = "Tidak direferensikan oleh kebijakan apa pun, jadi aman untuk dihapus." -pause = "Jeda" -resume = "Lanjutkan" -subtitle = "{{type}} · {{status}}" -usedBy = "Digunakan oleh" - -[portal.sources.empty] -description = "Hubungkan folder (dan, segera, cloud storage) agar kebijakan Anda memiliki tempat untuk menarik dokumen." -title = "Belum ada sumber yang terhubung" - [portal.sources.kpi] inUse = "Digunakan" total = "Koneksi" unused = "Tidak digunakan" +[portal.sources.networkFields.connection] +helperText = "Koneksi tersimpan dengan alamat server dan kredensial. Digunakan ulang oleh setiap sumber yang mereferensikannya." +label = "Koneksi" + +[portal.sources.networkFields.directory] +helperText = "Folder di server untuk dipolling, relatif terhadap home login atau root share. Biarkan kosong untuk root." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume menghapus setiap file dari server setelah setiap kebijakan memprosesnya." +label = "Mode baca" + +[portal.sources.networkFields.mode.options] +consume = "Consume: proses setiap file sekali" +snapshot = "Snapshot: baca ulang folder setiap proses" + +[portal.sources.networkFields.recursive] +label = "Kedalaman folder" + +[portal.sources.networkFields.recursive.options] +all = "Sertakan subfolder" +top = "Hanya level teratas" + [portal.sources.status] active = "Aktif" disabled = "Dinonaktifkan" unused = "Tidak digunakan" [portal.sources.table] +documents = "Dokumen" source = "Sumber" status = "Status" +type = "Type" usedBy = "Kebijakan" +[portal.sources.types.box] +description = "Ambil dokumen dari folder Box yang sudah dibagikan tim Anda." +label = "Box" + +[portal.sources.types.dropbox] +description = "Ambil dokumen dari folder Dropbox bersama." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumen yang telah diproses tim Anda di editor, di seluruh proses kebijakan dan AI." label = "Editor" +noPolicies = "Belum ada kebijakan yang dijalankan dari editor." + +[portal.sources.types.email] +description = "Ubah lampiran yang dikirim ke mailbox khusus menjadi dokumen." +label = "Kotak masuk email" [portal.sources.types.folder] description = "Pantau direktori di server untuk dokumen baru." @@ -7943,25 +8829,80 @@ label = "Kedalaman folder" all = "Sertakan subfolder" top = "Hanya level teratas" +[portal.sources.types.ftp] +description = "Polling folder server FTP atau FTPS untuk dokumen baru." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Ambil dokumen dari folder Google Drive bersama." +label = "Google Drive" + +[portal.sources.types.network] +description = "Pantau SMB network share untuk dokumen baru." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "Pantau folder OneDrive untuk dokumen baru." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Ambil dokumen dari bucket Amazon S3 atau yang kompatibel dengan S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Koneksi tersimpan yang menyimpan bucket dan kredensial. Digunakan ulang oleh setiap sumber dan output pipeline yang mereferensikannya." +label = "Koneksi" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume menghapus setiap objek dari bucket setelah setiap kebijakan memprosesnya." +label = "Mode baca" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: proses setiap objek sekali" +snapshot = "Snapshot: baca ulang bucket setiap proses" + +[portal.sources.types.s3.fields.prefix] +helperText = "Hanya objek yang key-nya dimulai dengan prefix ini yang diproses." +label = "Prefix key" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Polling folder drop SFTP untuk dokumen baru." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Ambil dokumen dari pustaka dokumen SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Sumber" +[portal.sources.types.webhook] +createNote = "URL pengiriman dan secret penandatanganan dibuat saat Anda membuat webhook ini. Pengirim melakukan POST dokumen ke URL tersebut dan kebijakan yang mereferensikan berjalan saat tiba." +description = "Terima dokumen melalui HTTP POST bertanda tangan dari sistem eksternal mana pun." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL pengiriman" +secretNote = "Secret penandatanganan ditampilkan sekali, saat webhook dibuat. Buat ulang sumber untuk menggantinya." + +[portal.sources.types.webhook.reveal] +copy = "Salin" +done = "Selesai" +secret = "Secret penandatanganan" +secretHelp = "Tandatangani body mentah setiap pengiriman dengan key ini (HMAC-SHA256) dan kirim sebagai header X-Stirling-Signature." +secretWarning = "Salin secret penandatanganan sekarang. Demi keamanan Anda, ini hanya ditampilkan sekali dan tidak dapat diambil lagi nanti." +title = "Webhook dibuat" +url = "URL pengiriman" +usage = "POST setiap dokumen sebagai body request mentah ke URL pengiriman dengan jenis konten binary (application/pdf atau application/octet-stream). Kebijakan yang mereferensikan berjalan otomatis saat tiba." + [portal.sources.wizard] -back = "Kembali" -cancel = "Batal" -continue = "Lanjutkan" -editTitle = "Edit sumber" name = "Nama" namePlaceholder = "mis. Penerimaan klaim" -save = "Simpan perubahan" -subtitle = "Langkah {{current}} dari {{total}} · {{label}}" -title = "Hubungkan sumber" type = "Tipe" -[portal.sources.wizard.steps] -chooseType = "Pilih tipe" -configure = "Konfigurasi" -review = "Tinjau & hubungkan" +[portal.stepModal] +close = "Tutup" [portal.usage] managePayment = "Kelola Pembayaran" @@ -8021,16 +8962,9 @@ summary = "Memiliki tim — mengelola sumber daya anggota dan konfigurasi bersam 2 = "Akses portal melalui kebijakan default" 3 = "Semua yang dapat dilakukan Anggota" -[portal.welcome] -ariaLabel = "Selamat datang di Stirling PDF" -badge = "Open-source" -installEditor = "Instal Editor" -inviteTeammates = "Undang rekan tim" -openInBrowser = "Buka di browser" -perks = "Gratis selamanya · Dapat self-host" -subtitle = "PDF Editor paling aman di dunia gratis untuk tim dari semua ukuran. Termasuk 60+ operasi PDF dan SSO." -title = "Selamat datang di" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Tidak terbatas" [printFile] title = "Cetak File" @@ -8613,6 +9547,14 @@ text = "Putar halaman PDF Anda searah atau berlawanan arah jarum jam dalam kelip [rotate.tooltip.header] title = "Ikhtisar Pengaturan Rotasi" +[ruler] +clearAll = "Hapus semua" +hideAllLabels = "Sembunyikan semua label" +hideSmallLabels = "Sembunyikan label kecil" +physicalValues = "Fisik" +scaled = "Diskalakan" +showAllLabels = "Tampilkan semua label" + [sanitize] desc = "Hapus elemen yang berpotensi berbahaya dari file PDF." sanitizationResults = "Hasil Sanitasi" @@ -8654,31 +9596,45 @@ label = "Hapus Metadata XMP" [sanitize.steps] settings = "Pengaturan" -[sanitizePdf] -tags = "bersih, terlindungi, aman, menghilangkan ancaman" - [sanitizePDF] title = "Bersihkan PDF" +[sanitizePdf] +tags = "bersih, terlindungi, aman, menghilangkan ancaman" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Sesuaikan skala halaman" +[scaleSettings] +activeScale = "Skala Aktif" +apply = "Terapkan Skala" +applyCalibration = "Terapkan Kalibrasi" +calculatedScale = "Skala terhitung" +calibrate = "Kalibrasi" +calibrating = "Mengkalibrasi" +calibrationDistanceRequired = "Masukkan jarak dunia nyata yang lebih besar dari nol" +calibrationInvalid = "Tidak dapat menghitung skala dari pengukuran ini" +calibrationPaperDistance = "Jarak halaman terukur: {{distance}}" +calibrationTitle = "Kalibrasi Skala" +calibrationTooltip = "Ukur jarak yang diketahui untuk menghitung skala secara otomatis" +customScale = "Skala Kustom" +noneSet = "Tidak ada skala kustom yang diatur" +presets = "Skala Preset" +ratio = "Rasio Skala" +ratioHelp = "Rasio: 1 unit halaman = X unit dunia nyata" +ratioPlaceholder = "mis., 100" +ratioPositive = "Rasio harus lebih besar dari nol" +ratioRequired = "Rasio wajib diisi" +realDistance = "Jarak dunia nyata" +realDistancePlaceholder = "mis., 5" +reset = "Atur Ulang ke Default" +unit = "Unit" +unitInvalid = "Unit tidak valid: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Ambang Batas Sudut:" -10 = "Menetapkan ukuran batas yang ditambahkan dan dihapus untuk mencegah batas putih pada output (default: 1)." -2 = "Menetapkan sudut absolut minimum yang diperlukan agar gambar dapat diputar (default: 10)." -3 = "Toleransi:" -4 = "Menentukan kisaran variasi warna di sekitar perkiraan warna latar belakang (default: 30)." -5 = "Area Minimum:" -6 = "Menetapkan ambang batas area minimum untuk foto (default: 10000)." -7 = "Area Kontur Minimum:" -8 = "Menetapkan ambang batas area kontur minimum untuk foto" -9 = "Ukuran Batas:" - [scannerImageSplit] submit = "Ekstrak Pindai Gambar" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Apa fungsinya" whatThisDoesDesc = "Secara otomatis menemukan dan mengekstrak tiap foto dari halaman hasil pindai atau gambar komposit—tanpa pemotongan manual." whenToUse = "Kapan digunakan" +[ScannerImageSplit.selectText] +1 = "Ambang Batas Sudut:" +10 = "Menetapkan ukuran batas yang ditambahkan dan dihapus untuk mencegah batas putih pada output (default: 1)." +2 = "Menetapkan sudut absolut minimum yang diperlukan agar gambar dapat diputar (default: 10)." +3 = "Toleransi:" +4 = "Menentukan kisaran variasi warna di sekitar perkiraan warna latar belakang (default: 30)." +5 = "Area Minimum:" +6 = "Menetapkan ambang batas area minimum untuk foto (default: 10000)." +7 = "Area Kontur Minimum:" +8 = "Menetapkan ambang batas area kontur minimum untuk foto" +9 = "Ukuran Batas:" + [search] noResults = "Tidak ada hasil" placeholder = "Masukkan kata pencarian..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Server Stirling-PDF Anda sedang offline dan \"{{endpo [session] expired = "Sesi Anda telah kedaluwarsa. Silakan muat ulang halaman dan coba lagi." -[sessionManagement.tooltip] -header = "Mengelola Sesi Penandatanganan" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Peserta baru ditambahkan di akhir urutan penandatanganan" -bullet2 = "Tidak dapat menambah peserta setelah sesi difinalisasi" -bullet3 = "Setiap peserta menerima pemberitahuan saat gilirannya" -description = "Anda dapat menambahkan lebih banyak peserta ke sesi aktif kapan saja sebelum finalisasi." -title = "Menambahkan Peserta" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalisasi penuh: Semua peserta telah menandatangani" -bullet2 = "Finalisasi parsial: Beberapa peserta belum menandatangani" -bullet3 = "Peserta yang belum menandatangani akan dikecualikan dari dokumen final" -bullet4 = "Setelah difinalisasi, Anda dapat memuat PDF bertanda tangan ke file aktif" -description = "Finalisasi menggabungkan semua tanda tangan menjadi satu PDF yang ditandatangani. Tindakan ini tidak dapat dibatalkan." -title = "Finalisasi Sesi" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Tidak dapat menghapus peserta yang sudah menandatangani" -bullet2 = "Peserta yang dihapus tidak lagi menerima pemberitahuan" -bullet3 = "Urutan penandatanganan menyesuaikan secara otomatis" -description = "Peserta dapat dihapus dari sesi sebelum mereka menandatangani." -title = "Menghapus Peserta" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Setiap tanda tangan diterapkan secara berurutan ke PDF" -bullet2 = "Penanda tangan berikutnya dapat melihat tanda tangan sebelumnya" -bullet3 = "Kritis untuk alur persetujuan dan rantai penjagaan hukum" -description = "Urutan yang Anda tentukan saat membuat sesi menentukan siapa yang menandatangani terlebih dahulu." -title = "Urutan Tanda Tangan" - [settings] close = "Tutup" +[settings.ai] +documents = "Dokumen & RAG" +general = "Umum" +limits = "Batas & Performa" +models = "Model & Penyedia" +title = "AI" + [settings.configuration] advanced = "Lanjutan" database = "Database" endpoints = "Endpoint" features = "Fitur" +folderAccess = "Akses Folder" mcp = "MCP Server" storageSharing = "Penyimpanan File & Berbagi" systemSettings = "Pengaturan Sistem" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Editor PDF default" defaultPdfEditorActive = "Stirling PDF adalah editor PDF default Anda" defaultPdfEditorChecking = "Memeriksa..." defaultPdfEditorInactive = "Aplikasi lain disetel sebagai default" +defaultPdfEditorRemind = "Ingatkan saya untuk mengatur sebagai default" +defaultPdfEditorRemindDescription = "Tampilkan banner saat Stirling PDF bukan aplikasi PDF default Anda." defaultPdfEditorSet = "Sudah Default" defaultStartupView = "Tampilan default saat aplikasi dibuka" defaultStartupViewDescription = "Pilih tab yang aktif di kolom kiri saat aplikasi dimulai" @@ -8817,9 +9763,18 @@ languageDescription = "Pilih bahasa tampilan" logout = "Keluar" setAsDefault = "Jadikan Default" theme = "Tema" +themeAccent = "Warna aksen" +themeAccentCustom = "Warna kustom" +themeAccentCustomHint = "Kustom (default disarankan)" +themeAccentDark = "Warna aksen mode gelap" +themeAccentDefault = "Default" +themeAccentDefaultHint = "Tema default (disarankan)" +themeAccentDescription = "Tombol, tautan, dan sorotan mengikutinya, dan memberi semburat halus pada aplikasi. Mode terang dan gelap masing-masing memilikinya sendiri. Sistem menggunakan mana pun yang aktif." +themeAccentLight = "Warna aksen mode terang" themeDark = "Gelap" themeDescription = "Beralih antara mode terang dan gelap" themeLight = "Terang" +themeReset = "Pulihkan tema ke default" themeSystem = "System" title = "Umum" user = "Pengguna" @@ -8833,6 +9788,12 @@ intro = "Aktifkan autentikasi pengguna, manajemen tim, dan fitur ruang kerja unt learnMore = "Pelajari lebih lanjut di dokumentasi" title = "Untuk Administrator Sistem" +[settings.general.loginLanding] +description = "Pilih aplikasi mana yang terbuka saat Anda masuk ke Stirling Cloud." +editor = "Editor" +processor = "Pemroses" +title = "Setelah masuk" + [settings.general.mode] fullscreen = "Layar penuh" sidebar = "Bilah sisi" @@ -9013,9 +9974,6 @@ title = "Kebijakan & Privasi" [settings.preferences] title = "Preferensi" -[settings.search] -placeholder = "Cari halaman pengaturan..." - [settings.security] description = "Perbarui kata sandi Anda untuk menjaga keamanan akun." title = "Keamanan" @@ -9041,6 +9999,11 @@ security = "Keamanan" telegram = "Telegram" title = "Keamanan & Autentikasi" +[settings.signOut] +confirm = "Apakah Anda yakin ingin keluar?" +submit = "Keluar" +title = "Keluar" + [settings.team] title = "Tim" @@ -9156,6 +10119,7 @@ last = "Halaman terakhir" maintainRatio = "Alihkan pertahankan rasio aspek" next = "Halaman berikutnya" noSavedSigs = "Tidak ditemukan tanda tangan yang disimpan" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Tanda Tangan Pribadi" previous = "Halaman sebelumnya" redo = "Ulangi" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Penghapusan latar belakang gagal" hint = "Unggah gambar PNG atau JPG dari tanda tangan Anda" label = "Unggah gambar tanda tangan" placeholder = "Pilih berkas gambar" +previewAlt = "Current image signature" processing = "Memproses gambar..." removeBackground = "Hapus latar belakang putih (buat transparan)" @@ -9207,6 +10172,17 @@ saved = "Pilih tanda tangan tersimpan di atas, lalu klik di mana saja pada PDF u text = "Setelah memasukkan nama Anda di atas, klik di mana saja pada PDF untuk menempatkan tanda tangan Anda." title = "Cara menambahkan tanda tangan" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Pindahkan Tanda Tangan" pause = "Jeda penempatan" @@ -9653,10 +10629,30 @@ title = "Unggah ke Server" updateButton = "Perbarui di Server" uploadButton = "Unggah ke Server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klik di sini" +fallback = "Jika tidak terbuka secara otomatis," +opening = "Membuka Swagger UI di tab baru..." tags = "api,dokumentasi,swagger,endpoint,pengembangan" title = "Dokumentasi API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Pilih file di File Aktif untuk menjalankan alat ini" singleFileScope = "Hanya diterapkan pada: {{fileName}}" viewerMode = "Beralih ke editor file untuk memilih beberapa file." +[toolFormat] +ANY = "file apa pun" +CBR = "arsip buku komik (CBR)" +CBZ = "arsip buku komik (CBZ)" +CSV = "file CSV" +EBOOK = "ebook" +EMAIL = "email" +EXCEL = "spreadsheet" +HTML = "halaman web" +IMAGE = "gambar" +JAVASCRIPT = "file JavaScript" +JSON = "file JSON" +MARKDOWN = "file Markdown" +NONE = "tidak ada file" +PCL = "file cetak PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF yang dilindungi kata sandi" +POSTSCRIPT = "file PostScript" +PPT = "presentasi" +TEXT = "file teks" +VIDEO = "video" +WORD = "dokumen Word" +XML = "file XML" +XPS = "dokumen XPS" +ZIP = "arsip ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Kembali ke semua alat" collapse = "Ciutkan panel" expand = "Perluas panel" goBack = "Kembali" +pdfTools = "Alat PDF" placeholder = "Pilih alat untuk memulai" premiumFeature = "Fitur premium:" toolsHeader = "Alat" @@ -9806,11 +10829,6 @@ noToolsFound = "Tidak ada alat yang ditemukan" quickAccess = "AKSES CEPAT" searchPlaceholder = "Cari alat..." -[toolPicker.categories] -advancedTools = "Alat Lanjutan" -recommendedTools = "Alat Rekomendasi" -standardTools = "Alat Standar" - [toolPicker.subcategories] advancedFormatting = "Pemformatan Lanjutan" automation = "Otomatisasi" @@ -9824,6 +10842,9 @@ removal = "Penghapusan" signing = "Penandatanganan" verification = "Verifikasi" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Tidak ada alat ditemukan" noTools = "Tidak ada alat tersedia" @@ -9950,28 +10971,53 @@ title = "Pengguna" you = "(Anda)" [users.action] +cancelInvite = "Batalkan undangan" deleteTeam = "Hapus tim" disableMfa = "Reset MFA" move = "Pindahkan ke tim" reinstate = "Aktifkan kembali" remove = "Hapus dari org" +removeTeam = "Hapus dari tim" rename = "Rename tim" resetPw = "Reset password" suspend = "Tangguhkan" unlock = "Buka kunci akun" +[users.activity] +daysAgo = "{{count}} hr lalu" +hoursAgo = "{{count}} jam lalu" +justNow = "Baru saja" +minutesAgo = "{{count}} mnt lalu" +monthsAgo_one = "{{count}} bulan lalu" +monthsAgo_other = "{{count}} bulan lalu" +never = "Tidak pernah" +weeksAgo_one = "{{count}} minggu lalu" +weeksAgo_other = "{{count}} minggu lalu" +yearsAgo_one = "{{count}} tahun lalu" +yearsAgo_other = "{{count}} tahun lalu" + [users.cap] addProcessor = "+ Processor" approver = "Menyetujui kebijakan" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Batalkan undangan ke {{email}}? Mereka tidak akan dapat bergabung dengan tautan saat ini." +cancelInviteTitle = "Batalkan undangan" deleteTeamBody = "Hapus tim {{name}}? Tim harus kosong terlebih dahulu - pindahkan anggotanya ke tim lain, dan tidak boleh masih memiliki konfigurasi integrasi apa pun." deleteTeamTitle = "Hapus tim" disableMfaBody = "Hapus pendaftaran MFA {{name}}? Mereka akan menyiapkannya lagi saat login berikutnya jika diperlukan." disableMfaTitle = "Reset MFA" removeBody = "Hapus {{name}} secara permanen dari organisasi? Tindakan ini tidak dapat dibatalkan." +removeTeamBody = "Hapus {{name}} dari tim? Mereka tetap memiliki akun tetapi kehilangan akses ke sumber daya tim ini." removeTitle = "Hapus anggota" [users.empty] @@ -9982,10 +11028,8 @@ title = "Belum ada anggota" addToTeam = "Tambahkan ke tim" guestCount = "{{count}} tamu" guests = "Tamu" -guestsDesc = "Kolaborator eksternal, dibatasi pada yang Anda bagikan. Hanya Editor." ledBy = "dipimpin oleh {{owner}}" org = "Organisasi" -orgDesc = "Pemilik dengan otoritas seluruh org dan persetujuan kebijakan" owners = "{{count}} pemilik" team = "Tim {{name}}" teamMeta = "{{count}} orang" @@ -10022,6 +11066,18 @@ username = "Nama pengguna" usernameError = "Nama pengguna harus minimal 3 karakter" usernamePlaceholder = "jsmith" +[users.invites] +by = "Diundang oleh {{who}}" +cancel = "Batal" +expiresInDays_one = "Kedaluwarsa dalam {{count}} hari" +expiresInDays_other = "Kedaluwarsa dalam {{count}} hari" +expiresToday = "Kedaluwarsa hari ini" +title = "Undangan tertunda" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Terjadi kesalahan saat menghubungi backend, atau Anda tidak memiliki akses. Coba lagi." title = "Tidak dapat memuat anggota" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Cari lampiran" title = "Lampiran" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Judul bookmark" +bookmarkTitleRequired = "Judul bookmark wajib diisi" closeSidebar = "Tutup bilah sisi bookmark" collapseAll = "Ciutkan semua bookmark" +empty = "No bookmarks in this document" expandAll = "Luaskan semua bookmark" +searchPlaceholder = "Cari bookmark" +title = "Bookmarks" [viewer.comments] addComment = "Tambahkan komentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Temukan di dokumen" moreActions = "Tindakan lainnya" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Halaman {{page}}" placingHint = "Klik sebuah halaman untuk menempatkan… (batalkan)" removeCommentOnly = "Hanya hapus komentar" saveReply = "Simpan balasan" +searchPlaceholder = "Search comments" title = "Komentar" typeComment = "Komentar" typeInsertText = "Sisipkan Teks" @@ -10262,6 +11325,7 @@ unsavedDesc = "Anda memiliki perubahan yang belum disimpan" closeSidebar = "Tutup bilah sisi layer" hideAll = "Sembunyikan semua layer" showAll = "Tampilkan semua layer" +title = "Layers" [viewer.link] delete = "Hapus tautan" @@ -10605,30 +11669,6 @@ title = "Teks Tanda Air" image = "Gambar" text = "Teks" -[wetSignature.tooltip] -header = "Metode Pembuatan Tanda Tangan" - -[wetSignature.tooltip.draw] -bullet1 = "Sesuaikan warna dan ketebalan pena" -bullet2 = "Hapus dan gambar ulang hingga sesuai" -bullet3 = "Berfungsi pada perangkat sentuh (tablet, ponsel)" -description = "Buat tanda tangan tulisan tangan menggunakan mouse atau layar sentuh Anda. Terbaik untuk tanda tangan yang personal dan autentik." -title = "Gambar Tanda Tangan" - -[wetSignature.tooltip.type] -bullet1 = "Pilih dari berbagai font" -bullet2 = "Sesuaikan ukuran dan warna teks" -bullet3 = "Sangat cocok untuk tanda tangan standar" -description = "Hasilkan tanda tangan dari teks yang diketik. Cepat dan konsisten, cocok untuk dokumen bisnis." -title = "Ketik Tanda Tangan" - -[wetSignature.tooltip.upload] -bullet1 = "Mendukung PNG, JPG, dan format gambar lainnya" -bullet2 = "Latar belakang transparan direkomendasikan untuk hasil terbaik" -bullet3 = "Gambar akan diubah ukurannya agar sesuai dengan area tanda tangan" -description = "Unggah gambar tanda tangan yang sudah dibuat. Ideal jika Anda memiliki tanda tangan hasil pemindaian atau logo perusahaan." -title = "Unggah Gambar Tanda Tangan" - [workbenchBar] activeFiles = "File Aktif" annotations = "Anotasi" @@ -10642,6 +11682,7 @@ exitRedaction = "Keluar dari Mode Redaksi" exportAll = "Ekspor PDF" exportSelected = "Ekspor Halaman yang Dipilih" formFill = "Isi Formulir" +hideToolbar = "Hide toolbar" multiTool = "Multi-Tool" panMode = "Mode Geser" print = "Cetak PDF" @@ -10652,6 +11693,7 @@ redact = "Redaksi" rotateLeft = "Putar Kiri" rotateRight = "Putar Kanan" ruler = "Penggaris / Ukur" +rulerSettings = "Pengaturan Skala" save = "Simpan" saveAll = "Simpan Semua" saveAs = "Simpan Sebagai" @@ -10661,6 +11703,7 @@ selectAll = "Pilih Semua" selectByNumber = "Pilih berdasarkan Nomor Halaman" selectLanguage = "Pilih bahasa" share = "Bagikan" +showToolbar = "Show toolbar" toggleAnnotations = "Tampilkan/Sembunyikan Anotasi" toggleAttachments = "Tampilkan/Sembunyikan Lampiran" toggleBookmarks = "Tampilkan/Sembunyikan Penanda Halaman" diff --git a/frontend/editor/public/locales/it-IT/translation.toml b/frontend/editor/public/locales/it-IT/translation.toml index 0503fcb3c6..53bfd1fd46 100644 --- a/frontend/editor/public/locales/it-IT/translation.toml +++ b/frontend/editor/public/locales/it-IT/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabeto" apply = "Applica" applyAndContinue = "Applica e continua" -areYouSure = "Sei sicuro di voler uscire?" back = "Indietro" black = "Nero" blue = "Blu" @@ -46,10 +45,8 @@ filesSelected = "file selezionati" font = "Carattere" fontSizeTooltip = "Dimensione del testo del numero di pagina in punti. Numeri maggiori creano testo più grande." fontTypeTooltip = "Famiglia di font per i numeri di pagina. Scegli in base allo stile del documento." -genericSubmit = "Invia" green = "Verde" help = "Aiuto" -imgPrompt = "Scegli immagine/i" incorrectPasswordMessage = "La password attuale non è corretta." info = "Info" invalidUndoData = "Impossibile annullare: dati operazione non validi" @@ -62,7 +59,6 @@ loginAgreementTitle = "Accordo di accesso" logOut = "Esci" marginTooltip = "Distanza tra il numero di pagina e il bordo della pagina." moreOptions = "Altre opzioni" -multiPdfPrompt = "Scegli 2 o più PDF" never = "Mai" no = "No" noFileSelected = "Nessun file selezionato. Caricane uno." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordine delle pagine (inserisci una lista di numeri separati d pages = "Pagine" pageSelectionPrompt = "Selezione pagina personalizzata (inserisci un elenco separato da virgole di numeri di pagina 1,5,6 o funzioni come 2n+1) :" password = "Password" -pdfPrompt = "Scegli PDF" pin = "Blocca file (mantieni attivo dopo l'esecuzione dello strumento)" pro = "Pro" processingComplete = "Il tuo file è pronto." @@ -110,6 +105,7 @@ undoSuccess = "Operazione annullata con successo" unknown = "Sconosciuto" unpin = "Sblocca file (sostituisci dopo l'esecuzione dello strumento)" unsavedChanges = "Hai modifiche non salvate al tuo PDF. Cosa vuoi fare?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Modifiche non salvate" unsupported = "Non supportato" username = "Nome utente" @@ -356,6 +352,7 @@ text = "Questi permessi controllano ciò che gli utenti possono fare con il PDF. title = "Cambia Permessi" [addStamp] +preview = "Anteprima timbro" tags = "timbro,marchio,sigillo,approvato,rifiutato,riservato,strumento per timbro,timbro in gomma,timbro data,timbro di approvazione,ricevuto,nullo,copia,originale" [AddStampRequest] @@ -508,7 +505,7 @@ green = "Verde" noPreview = "Seleziona un PDF per l'anteprima" red = "Rosso" saturation = "Saturazione:" -tags = "contrast,brightness,saturation,adjust colors,color correction,enhance,lighten,darken,improve quality,color balance,hue,vibrance" +tags = "contrasto,luminosità,saturazione,regolazione dei colori,correzione del colore,miglioramento,schiarimento,oscuramento,miglioramento della qualità,bilanciamento del colore,tonalità,vivacità" title = "Regola il contrasto" [adjustContrast.error] @@ -691,6 +688,165 @@ manualLinks = "Download manuali: fai clic sui link e inserisci i file nella cart noLanguages = "Nessuna lingua tessdata trovata nella directory configurata." permissionNotice = "Il percorso tessdata non è scrivibile. I download verranno aperti nel browser; salva manualmente i file .traineddata nella cartella tessdata." +[admin.settings.ai.documents] +description = "Configura il modello di embedding e le impostazioni di recupero usate per rispondere a domande sui documenti. Applicato al motore AI al salvataggio." +title = "Documenti e RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Lascia vuoto per usare le credenziali di ambiente del motore. Si applica alle distribuzioni self-hosted con motore singolo." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Salvata - lascia vuoto per mantenere la chiave corrente" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL di base dell'endpoint di embeddings compatibile con OpenAI / Ollama, es. http://ollama:11434/v1. Deve puntare a un endpoint interno attendibile (sensibile a SSRF)." +label = "URL di base embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Nome del modello di embedding. Testo libero; i suggerimenti sono solo indicazioni." +label = "Modello di embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provider usato per trasformare il testo del documento in embedding vettoriali." +label = "Provider di embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Numero massimo di ricerche di recupero che l'agente può eseguire per richiesta." +label = "Ricerche max" + +[admin.settings.ai.documents.reindexNote] +body = "La modifica del modello di embedding ha effetto immediato, ma i documenti indicizzati con il modello precedente devono essere reindicizzati affinché la ricerca restituisca risultati corretti." +title = "Reindicizzazione richiesta" + +[admin.settings.ai.documents.saved] +reindexNote = "Se hai modificato il modello di embedding, reindicizza i documenti esistenti affinché la ricerca usi il nuovo modello." + +[admin.settings.ai.documents.topK] +description = "Numero di blocchi più pertinenti recuperati per ricerca." +label = "Top K" + +[admin.settings.ai.general] +description = "Connetti Stirling al motore AI Python e scegli quali funzionalità AI esporre. Le modifiche si applicano al riavvio." +title = "Motore AI" + +[admin.settings.ai.general.capabilities] +description = "Attiva o disattiva singole funzionalità AI. Le funzionalità disabilitate sono nascoste nell'app." +title = "Funzionalità" + +[admin.settings.ai.general.enabled] +description = "Interruttore principale. Quando è disattivato, non sono disponibili strumenti AI, agenti o chiamate al motore." +label = "Abilita AI" + +[admin.settings.ai.general.features.chat] +description = "Assistente conversazionale per lavorare con i PDF." +label = "Assistente chat" + +[admin.settings.ai.general.features.classify] +description = "Categorizza automaticamente i documenti per tipo o contenuto." +label = "Classificazione documenti" + +[admin.settings.ai.general.features.createPdf] +description = "Genera un nuovo PDF (es. da HTML) tramite un agente AI." +label = "Crea PDF da prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Fai domande e ottieni risposte basate su un documento caricato." +label = "Domande sui documenti" + +[admin.settings.ai.general.features.mathAuditor] +description = "Esamina i documenti per individuare errori matematici e numerici." +label = "Revisore matematico" + +[admin.settings.ai.general.features.pdfComment] +description = "Aggiungi commenti di revisione e annotazioni generati dall'AI a un PDF." +label = "Agente commenti PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout per operazioni dell'agente più pesanti, come la generazione di documenti." +label = "Timeout operazioni lunghe (secondi)" + +[admin.settings.ai.general.note] +body = "Il motore AI viene eseguito come servizio separato. Il suo segreto condiviso" +body2 = "è impostato tramite una variabile di ambiente del container. Le API key dei provider possono essere inserite in queste pagine o fornite come variabili di ambiente del motore; le chiavi inserite qui vengono inviate al motore al salvataggio." +title = "Informazioni sul motore AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout per le risposte chat in streaming (token per token)." +label = "Timeout stream (secondi)" + +[admin.settings.ai.general.test] +button = "Test connessione" +failBody = "Il motore AI non ha risposto. Controlla l'URL, che il container del motore sia in esecuzione e che l'AI sia abilitata (dopo l'abilitazione è necessario un riavvio)." +failTitle = "Motore AI non raggiungibile" +okBody = "Il motore AI ha risposto a un controllo di integrità." +okTitle = "Motore AI raggiungibile" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout per le richieste AI standard al motore." +label = "Timeout richiesta (secondi)" + +[admin.settings.ai.general.url] +description = "URL interno del motore AI Python, es. http://stirling-pdf-engine:5001." +label = "URL motore AI" + +[admin.settings.ai.limits] +description = "Limiti di protezione per quanto lavoro possono eseguire le richieste AI e quante possono essere eseguite contemporaneamente. Applicati al motore AI al salvataggio." +title = "Limiti e prestazioni" + +[admin.settings.ai.limits.maxCharacters] +description = "Limite di protezione: rifiuta le richieste AI il cui testo estratto supera questa lunghezza." +label = "Caratteri max per richiesta" + +[admin.settings.ai.limits.maxPages] +description = "Limite di protezione: rifiuta le richieste AI oltre questo numero di pagine PDF." +label = "Pagine max per richiesta" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Numero massimo di chiamate al modello simultanee in corso sull'intero motore." +label = "Concorrenza max modello" + +[admin.settings.ai.models] +description = "Scegli il provider LLM e i modelli smart/veloci usati dal motore AI. Applicato al motore AI al salvataggio." +title = "Modelli e provider" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Lascia vuoto per usare le credenziali di ambiente del motore. Si applica alle distribuzioni self-hosted con motore singolo." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Salvata - lascia vuoto per mantenere la chiave corrente" + +[admin.settings.ai.models.baseUrl] +description = "URL di base dell'endpoint compatibile con OpenAI / Ollama, es. http://ollama:11434/v1." +label = "URL di base provider" +warning = "L'URL di base deve puntare a un endpoint interno attendibile. Il motore effettuerà richieste server-side verso di esso, quindi un valore non attendibile è sensibile a SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Token di output massimi per il modello veloce." +label = "Token max modello veloce" + +[admin.settings.ai.models.fastModel] +description = "Modello più economico e veloce per attività leggere. Testo libero; i suggerimenti sono solo indicazioni." +label = "Modello veloce" + +[admin.settings.ai.models.provider] +description = "Con quale provider LLM comunica il motore." +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "Token di output massimi per il modello smart." +label = "Token max modello smart" + +[admin.settings.ai.models.smartModel] +description = "Modello ad alta capacità per ragionamenti complessi. Testo libero; i suggerimenti sono solo indicazioni." +label = "Modello smart" + +[admin.settings.ai.saved] +body = "Le modifiche vengono inviate automaticamente al motore AI." +bodyNoPush = "Impostazioni salvate. Verranno applicate la prossima volta che il motore AI caricherà la sua configurazione." +title = "Impostazioni AI salvate" + [admin.settings.badge] clickToUpgrade = "Fai clic per vedere i dettagli del piano" @@ -938,6 +1094,25 @@ label = "Rigenera all'avvio" description = "Numero di giorni di validità del certificato" label = "Validità certificato (giorni)" +[admin.settings.folderAccess] +description = "Directory da cui le sorgenti cartella e gli output cartella possono leggere e in cui possono scrivere. Questo è un confine di sicurezza: le automazioni non possono mai puntare a un percorso del server fuori da questo elenco." +securityNote = "Lascia vuoto per disabilitare completamente sorgenti e output cartella. La directory di configurazione di Stirling è sempre vietata." +title = "Accesso alle cartelle" + +[admin.settings.folderAccess.implied] +description = "Queste directory gestite da Stirling sono sempre consentite e non possono essere modificate qui." +serverStorage = "Archiviazione file del server" +title = "Sempre consentite" +watchedFolder = "Cartella monitorata della pipeline" + +[admin.settings.folderAccess.roots] +add = "Aggiungi" +empty = "Nessuna cartella consentita. Le sorgenti e gli output cartella sono attualmente disabilitati." +hint = "Inserisci percorsi assoluti, ad esempio /data/inbox." +label = "Radici cartella consentite" +placeholder = "/data/inbox" +remove = "Rimuovi radice cartella" + [admin.settings.general] description = "Configura le impostazioni applicative a livello di sistema, incluse personalizzazione e comportamento predefinito." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Spessore" [app] description = "L’alternativa gratuita ad Adobe Acrobat (10M+ download)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Converti in PDF/A-3b" convertToPdfA3bDescription = "Crea un PDF per archiviazione con allegati incorporati" @@ -1842,6 +2020,15 @@ export = "Esporta" accessDenied = "Accesso negato" insufficientPermissions = "Non hai l'autorizzazione per eseguire questa azione." +[auth.callback] +completing = "Completamento autenticazione" +invalidToken = "Accesso OAuth non riuscito - token non valido." +missingToken = "Accesso OAuth non riuscito - nessun token ricevuto." +oauthFailed = "Accesso OAuth non riuscito. Riprova." +pleaseWait = "Attendi mentre completiamo l'accesso." +signedOut = "Sei stato disconnesso. Accedi di nuovo." +windowMayClose = "Puoi chiudere questa finestra al termine." + [auth.displayName] guest = "Ospite" user = "Utente" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Protegge i documenti PDF rimuovendo contenuti potenzialmen description = "Questo strumento rinomina automaticamente i file PDF in base al loro contenuto. Analizza il documento per trovare il titolo più adatto dal testo." tags = "rilevamento automatico,basato sull'intestazione,organizza,rietichetta,rinomina automatica,rinomina intelligente,rinomina in base al contenuto,nome file,denominazione file,rileva titolo" +[autoRotate] +submit = "Rotazione automatica" +tags = "rotazione automatica,correggi orientamento,capovolto,di lato,raddrizza,orienta,rileva orientamento,correggi rotazione,orientamento automatico,scansionato,OSD" + +[autoRotate.confidenceThreshold] +title = "Soglia di confidenza" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Solo OCR" +text = "Solo testo" +title = "Metodo di rilevamento" + +[autoRotate.error] +failed = "Si è verificato un errore durante la rotazione automatica del PDF." + +[autoRotate.inferUndetected] +title = "Ruota pagine a bassa confidenza per uniformarle" + +[autoRotate.report] +applied = "{{degrees}}° in senso orario" +confidenceValue = "Confidenza {{value}}" +noChange = "Nessuna modifica" +pageLabel = "Pagina {{number}}" +summary = "{{rotated}} di {{total}} pagine ruotate, {{unchanged}} lasciate così come sono" +title = "Report di rilevamento" + +[autoRotate.report.method] +inferred = "Uniformata" +none = "Saltata" +osd = "OCR" +text = "Testo" + +[autoRotate.report.note] +belowThreshold = "Sotto la soglia" +blankPage = "La pagina è vuota" +inferredFromDocument = "Come le altre pagine" +noDominantDirection = "Direzioni del testo miste" +osdFailed = "Nessun testo leggibile trovato" +osdNoVerdict = "Impossibile determinare l'orientamento" +tesseractUnavailable = "OCR non installato" +tooFewGlyphs = "Troppo poco testo" + +[autoRotate.results] +title = "Risultati rotazione automatica" + +[autoRotate.settings] +title = "Impostazioni" + +[autoRotate.tooltip.confidenceThreshold] +text = "Quanto deve essere certo il controllo prima che una pagina venga ruotata. Abbassala per ruotare più pagine, aumentala per lasciare più pagine invariate." +title = "Soglia di confidenza" + +[autoRotate.tooltip.description] +text = "Determina l'orientamento corretto di ogni pagina e la ruota nel verso giusto. Le pagine già dritte vengono lasciate invariate." + +[autoRotate.tooltip.detectionMode] +text = "Auto legge prima il testo sulla pagina, poi analizza la pagina come immagine se non è sufficiente. Solo testo salta il controllo dell'immagine, il più rapido per documenti mai scansionati. Solo OCR tratta ogni pagina come immagine." +title = "Metodo di rilevamento" + +[autoRotate.tooltip.header] +title = "Rotazione automatica" + +[autoRotate.tooltip.inferUndetected] +text = "Le pagine vuote o quasi vuote non possono essere controllate da sole. Quando tutte le pagine leggibili richiedono la stessa rotazione, queste pagine vengono ruotate per uniformarle." +title = "Ruota pagine a bassa confidenza per uniformarle" + [autoSizeSplitPDF] tags = "pdf,diviso,documento,organizzazione" @@ -2189,33 +2443,6 @@ keywords = "Parole chiave: dispari, pari" numbers = "Numeri/intervalli: 5, 10-20" progressions = "Progressioni: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipi di certificato" - -[certificateChoice.tooltip.organization] -bullet1 = "Gestito dagli amministratori di sistema" -bullet2 = "Condiviso tra gli utenti autorizzati" -bullet3 = "Rappresenta l'identità dell'azienda, non dell'individuo" -bullet4 = "Ideale per: documenti ufficiali, firme del team" -description = "Un certificato condiviso fornito dalla tua organizzazione. Usato per l'autorità di firma a livello aziendale." -title = "Certificato dell'organizzazione" - -[certificateChoice.tooltip.personal] -bullet1 = "Generato automaticamente al primo utilizzo" -bullet2 = "Collegato al tuo account utente" -bullet3 = "Non può essere condiviso con altri utenti" -bullet4 = "Ideale per: documenti personali, responsabilità individuale" -description = "Un certificato generato automaticamente e unico per il tuo account utente. Adatto a firme individuali." -title = "Certificato personale" - -[certificateChoice.tooltip.upload] -bullet1 = "Richiede file P12/PFX e password" -bullet2 = "Può essere emesso da Autorità di Certificazione esterne" -bullet3 = "Livello di fiducia più elevato per documenti legali" -bullet4 = "Ideale per: contratti legalmente vincolanti, validazione esterna" -description = "Usa il tuo file di certificato PKCS#12. Fornisce il pieno controllo sulle proprietà del certificato." -title = "Carica P12 personalizzato" - [certSign] awaitingSignatures = "In attesa di firme" chooseCertificate = "Scegli il file del certificato" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Origine certificato" upload = "Carica" -[certSign.tooltip.header] -title = "Gestione firme: informazioni" - -[certSign.tooltip.overview] -bullet1 = "Verifica le firme esistenti e la loro validità" -bullet2 = "Vedi informazioni dettagliate su firmatari e certificati" -bullet3 = "Aggiungi nuove firme digitali per proteggere i documenti" -bullet4 = "Supporto per più file con navigazione semplice" -text = "Questo strumento ti consente di controllare se i tuoi PDF sono firmati digitalmente e di aggiungere nuove firme digitali. Le firme digitali dimostrano chi ha creato o approvato un documento e se è stato modificato dopo la firma." -title = "Cosa può fare questo strumento?" - -[certSign.tooltip.signing] -bullet1 = "Supporta formati PEM, PKCS12, JKS e certificato server" -bullet2 = "Opzione per mostrare o nascondere la firma nel PDF" -bullet3 = "Aggiungi motivo, luogo e nome del firmatario" -bullet4 = "Scegli in quale pagina posizionare le firme visibili" -bullet5 = "Usa certificato server per semplice opzione 'Firma con Stirling-PDF'" -text = "Per firmare un PDF è necessario un certificato digitale (come PEM, PKCS12 o JKS). Puoi scegliere di rendere la firma visibile sul documento o mantenerla invisibile solo per sicurezza." -title = "Aggiunta delle firme" - -[certSign.tooltip.validation] -bullet1 = "Mostra se le firme sono valide o non valide" -bullet2 = "Visualizza informazioni sul firmatario e la data di firma" -bullet3 = "Controlla se il documento è stato modificato dopo la firma" -bullet4 = "Può usare certificati personalizzati per la verifica" -text = "Quando controlli le firme, lo strumento indica se sono valide, chi ha firmato, quando e se il documento è stato modificato dopo la firma." -title = "Verifica delle firme" - [changeCreds] changeUsername = "Aggiorna il nome utente. Verrai disconnesso dopo l'aggiornamento." credsUpdated = "Account aggiornato" @@ -2737,12 +2936,16 @@ text = "Per rendere questi permessi immodificabili, usa lo strumento Aggiungi pa [chat.actions] copy = "Copia messaggio" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Chiudi chat" open = "Apri Stirling assistente IA" [chat.header] agentMenu = "Opzioni agente Stirling" +assistant = "AI Assistant" clearChat = "Cancella chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Questa operazione utilizzerà i tuoi crediti cloud" tooltip = "Preleva il colore dallo schermo" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Scegli il colore" [common] +actions = "Actions" back = "Indietro" cancel = "Annulla" close = "Chiudi" +codeSample = "Code sample" collapse = "Comprimi" confirm = "Conferma" continue = "Continua" copied = "Copiato!" copy = "Copia" done = "Fatto" +enterValue = "Enter value" error = "Errore" expand = "Espandi" loading = "Caricamento..." next = "Successivo" +open = "Open" preview = "Anteprima" previous = "Precedente" refresh = "Aggiorna" +remaining = "Rimanenti" retry = "Riprova" save = "Salva" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Cancella selezionati" @@ -2988,6 +3199,7 @@ title = "Metodo di compressione" [compress.settings] desiredSize = "Dimensione file desiderata" desiredSizePlaceholder = "Inserisci dimensione" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "La compressione è un modo semplice per ridurre la dimensione del file. Scegli Dimensione File per inserire una dimensione obiettivo e lascia che regoliamo la qualità per te. Scegli Qualità per impostare manualmente l’intensità della compressione." @@ -3107,6 +3319,8 @@ description = "Il Model Context Protocol (MCP) consente agli assistenti IA come guestInfo = "Gli utenti ospite non possono connettere client MCP. Crea un account per utilizzare il server MCP e consentire al tuo assistente IA di eseguire gli strumenti Stirling PDF per tuo conto." navLabel = "Server MCP" tip = "Ogni azione eseguita dal tuo assistente viene effettuata con il tuo account e viene conteggiata nel tuo consumo, esattamente come quando usi Stirling PDF direttamente." +title = "MCP Server" +viewApiKeys = "Visualizza API key" [config.mcp.copy] configLabel = "Configurazione" @@ -3188,6 +3402,24 @@ pdfOptions = "Opzioni PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Seleziona prima un formato sorgente" settings = "Impostazioni" single = "Singolo" @@ -3262,24 +3494,6 @@ description = "Questi cookie sono essenziali per il corretto funzionamento del s posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitoraggio avanzato" -allInOneWorkspace = "Workspace PDF tutto-in-uno (visualizzatore, strumenti e agente)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Supporto dedicato e SLA" -fasterThroughput = "Capacità di elaborazione 10x più veloce" -fullyPrivateFiles = "File completamente privati" -largeFileProcessing = "Elaborazione file di grandi dimensioni" -orgWideAccess = "Controlli di accesso a livello di organizzazione" -premiumAiModels = "Modelli AI premium" -prioritySupport = "Supporto prioritario" -privateDocCloud = "Documento Cloud privato" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Accesso API sicuro" -standardThroughput = "Throughput standard" -unlimitedApiAccess = "Accesso API illimitato" -unlimitedSeats = "Utenti illimitati" - [crop] autoCrop = "Ritaglio automatico spazi bianchi" noFileSelected = "Seleziona un file PDF per iniziare il ritaglio" @@ -3344,6 +3558,7 @@ message = "Impossibile impostare il gestore PDF predefinito" title = "Errore" [defaultApp.prompt] +dontRemind = "Non ricordarmelo più" message = "Rendi Stirling PDF l'app predefinita per aprire i file PDF." [defaultApp.settingsOpened] @@ -3383,7 +3598,11 @@ tags = "automazione,cartella,scansione,cartella di controllo,cartella di tendenz tags = "SSO,single sign-on,autenticazione,SAML,OAuth,OIDC,login,enterprise,provider di identità,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Cerca..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Modifica" @@ -3491,8 +3710,8 @@ title = "Rimuovi la password per continuare" unlock = "Sblocca e continua" unlockAll = "Usa per tutti ({{count}})" unlockAllPartialFail = "Password errata per: {{names}}" -unlockAllSuccess_one = "Unlocked {{count}} file." -unlockAllSuccess_other = "Unlocked {{count}} files." +unlockAllSuccess_one = "Sbloccato {{count}} file." +unlockAllSuccess_other = "Sbloccato {{count}} files." unlockPrompt = "Sblocca il PDF per continuare" viewerLocked = "Questo PDF è protetto da password" viewerUnlock = "Sblocca" @@ -3642,11 +3861,13 @@ shareSelected = "Condividi selezionati" sharing = "Condivisione" showAll = "Mostra tutto" showHistory = "Mostra cronologia" +sortBy = "Sort files" sortByDate = "Ordina per data" sortByName = "Ordina per nome" sortBySize = "Ordina per dimensione" storage = "Archiviazione" storageState = "Archiviazione" +storageUsed = "Storage used" synced = "Sincronizzato" title = "Carica file PDF" toolChain = "Strumenti applicati" @@ -3692,13 +3913,17 @@ addFiles = "Aggiungi file" addingFiles = "Aggiunta file…" collapse = "Comprimi barra laterale" customizeGroups = "Personalizza gruppi" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Apri file per iniziare" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Espandi barra laterale" -files = "File" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive non è configurato" leaveMyFiles = "Lascia I miei file" +library = "Libreria PDF" myFiles = "I miei file" noFiles = "Ancora nessun file" openFileManager = "Apri gestore file" @@ -3706,37 +3931,36 @@ openFromComputer = "Apri dal computer" openSettings = "Apri impostazioni" other = "Altro" recent = "Recenti" -search = "Cerca" -searchPlaceholder = "Cerca file..." viewAll = "Visualizza tutti i {{count}} file" [fileSidebar.fileItem] closeViewer = "Chiudi visualizzatore" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Elimina" +duplicate = "Duplicate" moreActions = "Altre azioni" openInViewer = "Apri nel visualizzatore" +rename = "Rename" savedToServer = "Salvato sul server" updateOnServer = "Aggiorna sul server" uploadToServer = "Carica sul server" versionHistory = "Cronologia versioni" [fileSidebar.groupsModal] -add = "Aggiungi" -addLabel = "Aggiungi un'etichetta…" -categoryIconAria = "Scegli un'icona per {{name}}" -createCategory = "Crea categoria" -delete = "Elimina categoria" done = "Fine" -hide = "Nascondi categoria" -newCategory = "Nome nuova categoria…" -removeLabel = "Rimuovi {{name}}" -rename = "Rinomina" reset = "Ripristina impostazioni predefinite" -search = "Cerca etichette…" -show = "Mostra categoria" subtitle = "Raggruppa i tuoi file in categorie principali. Aggiungi etichette esistenti o nuove a una categoria, rinominala o creane una tua. I file che non rientrano in nessuna delle tue categorie vengono visualizzati in “Altro”." title = "Categorie della barra laterale" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Aggiungi allo spazio di lavoro" addToWorkspaceCount = "Aggiungi {{count}} allo spazio di lavoro" @@ -3747,7 +3971,6 @@ backToMyFiles = "Torna ai miei file" breadcrumbs = "Percorso cartella" cancel = "Annulla" classification = "Classificazione" -clearSearch = "Cancella ricerca" clearSelection = "Cancella selezione" closeDetails = "Chiudi dettagli" create = "Crea" @@ -3779,6 +4002,7 @@ downloadAll = "Scarica tutto" downloadVersion = "Scarica questa versione" dropOverlay = "Rilascia i file per caricarli" dropOverlaySub = "I file iniziano in Locale. Usa 'Sposta in' o 'Salva nel cloud' per organizzarli in una cartella." +duplicate = "Duplicate" file = "File" fileInfo = "File info" fileMenu = "Azioni file" @@ -3806,13 +4030,12 @@ refresh = "Aggiorna dal server" remove = "Elimina" removeVersion = "Rimuovi questa versione" rename = "Rinomina" +renamed = "Rinominato" renameFolder = "Rinomina cartella" resizeFolderTree = "Ridimensiona albero cartelle (tasti freccia, Shift per passi più grandi; doppio clic per adattamento automatico)" save = "Salva" saveToServer = "Salva sul server" saveToServerDisabledHint = "Il salvataggio sul server non è abilitato su questo server. Chiedi al tuo amministratore di abilitarlo." -search = "Cerca" -searchPlaceholder = "Cerca in questa cartella e sottocartelle" selectAll = "Seleziona tutto" selectAllHint = "Fai clic per selezionare tutto. Suggerimento: tieni premuto Ctrl (o Cmd) per aggiungere i file uno alla volta, Shift per selezionare un intervallo." selectedCount = "{{count}} selezionati" @@ -3872,6 +4095,10 @@ title = "Nessun file cloud ancora" hint = "I file salvati senza caricamento restano qui. Rilascia un file per aggiungerne uno." title = "Nessun file solo locale" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "I file che apri o modifichi appariranno qui." title = "Ancora nulla di modificato" @@ -3891,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Impossibile eliminare la cartella." deleteFolderFailedDetail = "Impossibile eliminare la cartella: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Impossibile aggiornare l'aspetto della cartella." folderAppearanceFailedDetail = "Impossibile aggiornare l'aspetto della cartella: {{message}}" moveFilesFailed = "Impossibile spostare i file." @@ -3908,9 +4137,7 @@ count = "File" folder = "Cartella" labels = "Etichette" modified = "Modificato" -name = "Nome" size = "Dimensione" -toolHistoryAtVersion = "Catena di strumenti cumulativa" totalSize = "Dimensione totale" type = "Tipo" versionHistory = "Percorso delle versioni" @@ -3943,6 +4170,11 @@ localHint = "Archiviato solo in questo browser" shared = "Condivisi" sharedHint = "Condivisi con te tramite link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Più vecchi prima" modifiedDesc = "Più recenti prima" @@ -3998,6 +4230,7 @@ noFilesInStorageOpen = "Nessun file disponibile nell'archivio. Apri prima alcuni open = "Apri" openFile = "Apri file" openFiles = "Apri file" +selectFile = "Select {{name}}" selectFromStorage = "Seleziona dall’archiviazione" upload = "Carica" uploadFile = "Carica file" @@ -4070,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tutto salvato" +analyzingFields = "Analysing form fields..." +extractCsvError = "Impossibile estrarre CSV" +extractXlsxError = "Impossibile estrarre XLSX" +filled = "filled" flattenAfterFilling = "Appiattisci dopo la compilazione" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obb" +requiredFieldsError = "Compila tutti i campi obbligatori" rescanFields = "Riscansiona campi" rescanFormFields = "Riscansiona i campi del modulo" save = "Salva" +saveShortcut = "Ctrl+S per salvare" +unsavedChanges = "Modifiche non salvate" [formFill.sidebar] close = "Chiudi barra laterale" @@ -4202,8 +4447,8 @@ location = "Località:" logo = "Logo:" logoHidden = "Nessun logo" logoShown = "Logo Stirling PDF visualizzato" -participantCount_one = "{{count}} participant will sign in order" -participantCount_other = "{{count}} participants will sign in order" +participantCount_one = "{{count}} partecipante firmerà in ordine" +participantCount_other = "{{count}} partecipanti firmeranno in ordine" participants = "Partecipanti" reason = "Motivo:" send = "Invia richieste di firma" @@ -4221,8 +4466,8 @@ title = "Seleziona documento" [groupSigning.steps.selectParticipants] continue = "Continua alle impostazioni firma" -count_one = "{{count}} participant selected" -count_other = "{{count}} participants selected" +count_one = "{{count}} partecipante selezionato" +count_other = "{{count}} partecipanti selezionati" label = "Seleziona partecipanti" placeholder = "Scegli i partecipanti che devono firmare..." title = "Scegli partecipanti" @@ -4307,6 +4552,11 @@ desc = "Rinomina automaticamente un file PDF in base all’intestazione rilevata tags = "auto-rilevamento,basato su intestazione,organizza,rinomina" title = "Rinomina automatica file PDF" +[home.autoRotate] +desc = "Rileva l'orientamento di ogni pagina e ruotala automaticamente in posizione verticale." +tags = "rotazione automatica,correggi orientamento,capovolto,di lato,raddrizza,orienta,rileva orientamento,correggi rotazione,orientamento automatico,scansionato,OSD" +title = "Rotazione automatica" + [home.autoSizeSplitPDF] tags = "auto,dividi,dimensione" title = "Dividi auto per peso/pagine" @@ -4606,15 +4856,12 @@ hint = "Carica un PNG, JPG, SVG o un altro file immagine da posizionare sul PDF. label = "Carica immagine" placeholder = "Seleziona file immagine" -[imageToPdf] -tags = "conversione,img,jpg,immagine,foto" - [imageToPDF] header = "Immagine a PDF" title = "Immagine a PDF" -[infoBanner] -dismiss = "Ignora" +[imageToPdf] +tags = "conversione,img,jpg,immagine,foto" [invite] acceptError = "Impossibile creare l'account" @@ -4645,11 +4892,9 @@ welcomeTitle = "Sei stato invitato!" [landing] addFiles = "Aggiungi file" -heroSubtitle = "Trascina o aggiungi un PDF esistente per iniziare." mobileUpload = "Carica da dispositivo mobile" openFromComputer = "Apri dal computer" uploadFromComputer = "Carica dal computer" -workbenchEmptyStateHero = "Rilascia un PDF ovunque" [language] direction = "ltr" @@ -4689,7 +4934,6 @@ enterUsername = "Inserisci nome utente" failedToSignIn = "Accesso con {{provider}} non riuscito: {{message}}" forgotPassword = "Hai dimenticato la password?" loggingIn = "Accesso in corso..." -logIn = "Accedi" login = "Accedi" magicLinkSent = "Magic link inviato a {{email}}! Controlla la posta e clicca il link per accedere." mfaCode = "Codice di autenticazione" @@ -4713,26 +4957,10 @@ signInWith = "Accedi con" title = "Accedi" unexpectedError = "Errore imprevisto: {{message}}" updatePassword = "Aggiorna password" -useEmailInstead = "Accedi con email" useMagicLink = "Usa invece il magic link" username = "Nome utente" youAreLoggedIn = "Hai effettuato l’accesso!" -[login.slides.edit] -alt = "Modifica PDF" -subtitle = "Con oltre una dozzina di strumenti per oscurare, firmare, leggere e manipolare i PDF, troverai sicuramente ciò che cerchi." -title = "Modifica i PDF per mostrare/proteggere le informazioni che desideri" - -[login.slides.overview] -alt = "Panoramica di Stirling PDF" -subtitle = "Suite cloud incentrata sulla privacy per PDF che ti permette di convertire, firmare, oscurare e gestire documenti, con oltre 50 strumenti potenti." -title = "La soluzione unica per tutte le tue esigenze PDF." - -[login.slides.secure] -alt = "Proteggi i PDF" -subtitle = "Aggiungi password, oscura contenuti e gestisci i certificati con facilità." -title = "Proteggi le informazioni sensibili nei tuoi PDF" - [margin] large = "Grande" medium = "Medio" @@ -4816,6 +5044,34 @@ uploadSuccess = "Caricamento riuscito!" uploadSuccessMessage = "Le tue immagini sono state trasferite." validating = "Validazione della sessione..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scansiona per caricare foto. Le immagini vengono convertite automaticamente in PDF." descriptionNoConvert = "Scansiona per caricare foto dal tuo dispositivo mobile." @@ -4834,8 +5090,13 @@ title = "Carica da dispositivo mobile" tags = "Strumento multiplo,operazione multipla,interfaccia utente,trascinamento clic,front-end,lato client" title = "Multifunzione PDF" -[navbar] -search = "Cerca" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "L'autenticazione non è andata a buon fine. Puoi chiudere questa finestra e riprovare." @@ -4964,6 +5225,7 @@ activeFiles = "La vista File attivi mostra tutti i PDF caricati allTools = "Questo è il pannello Tutti gli Strumenti, dove puoi sfogliare e selezionare tutti gli strumenti PDF disponibili." close = "Chiudi" cropSettings = "Ora che abbiamo selezionato il file che vogliamo ritagliare, possiamo configurare lo strumento Ritaglia per scegliere l'area a cui ritagliare il PDF." +dialogLabel = "Onboarding" fileCheckbox = "Facendo clic su uno dei file lo selezioni per l'elaborazione. Puoi selezionare più file per operazioni in blocco." fileReplacement = "Il file modificato sostituirà automaticamente l'originale nel Banco di lavoro, consentendoti di farlo passare facilmente attraverso altri strumenti." filesButton = "Il pulsante File sulla barra di accesso rapido consente di caricare PDF su cui usare gli strumenti." @@ -4974,6 +5236,7 @@ pinButton = "Puoi usare il pulsante Blocca se preferisci che i results = "Dopo che lo strumento ha terminato l'esecuzione, la fase di Revisione mostrerà un'anteprima dei risultati in questo pannello e ti permetterà di annullare l'operazione o scaricare il file. " runButton = "Una volta configurato lo strumento, questo pulsante consente di eseguirlo su tutti i PDF selezionati." selectCropTool = "Selezioniamo lo strumento Ritaglia per mostrare come usarne uno." +stepOf = "Passaggio {{current}} di {{total}}" toolInterface = "Questa è l'interfaccia dello strumento Ritaglia. Come vedi, non c'è molto perché non abbiamo ancora aggiunto file PDF su cui lavorare." workbench = "Questo è il Banco di lavoro - l'area principale in cui visualizzare e modificare i PDF." wrapUp = "Tutto pronto! Hai imparato le aree principali dell'app e come usarle. Fai clic sul pulsante Guida quando vuoi per rivedere questo tour." @@ -4986,6 +5249,26 @@ showMeAround = "Fammi fare un tour" skipForNow = "Salta per ora" skipTheTour = "Salta il tour" +[onboarding.checklist] +dismiss = "Ignora" +title = "Configura Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Esegui Stirling nativamente sul tuo computer" +title = "Scarica Stirling per Desktop" + +[onboarding.checklist.inviteTeam] +description = "Collabora con il tuo team" +title = "Invita membri del team" + +[onboarding.checklist.shareAnalytics] +description = "Aiuta a migliorare Stirling" +title = "Condividi dati di utilizzo anonimi" + +[onboarding.checklist.takeTour] +description = "Scopri come funziona Stirling con una rapida panoramica" +title = "Fai il tour" + [onboarding.desktopInstall] body = "Stirling funziona al meglio come app desktop. Puoi usarla offline, accedere più velocemente ai documenti e modificare localmente sul tuo computer." selectOs = "Seleziona sistema operativo" @@ -4994,7 +5277,21 @@ titleWithOs = "Download per {{osLabel}}" [onboarding.mfa] authenticationCode = "Codice di autenticazione" +description = "Proteggi il tuo account collegando un'app di autenticazione. Scansiona il codice QR o inserisci la chiave di configurazione, quindi conferma il codice a 6 cifre per completare." +enable = "Abilita MFA" +enableError = "Impossibile abilitare l'autenticazione a due fattori. Controlla il codice e riprova." +enterCode = "Inserisci il codice a 6 cifre dalla tua app." +enterCodeError = "Inserisci il codice di autenticazione per continuare." +logout = "Logout" +openAuthenticator = "Apri Google Authenticator, Authy o 1Password." qrCodeLoading = "Generazione del tuo codice QR…" +regenerateQrCode = "Rigenera codice QR" +scanQrCode = "Scansiona il codice QR o inserisci la chiave di configurazione qui sotto." +setupError = "Impossibile avviare la configurazione a due fattori. Riprova." +setupKey = "Chiave di configurazione (inserimento manuale)" +stepByStep = "Passo dopo passo" +success = "MFA è stata abilitata. Ora puoi continuare." +title = "Configurazione autenticazione multifattore" [onboarding.planOverview] adminBodyLoginDisabled = "Una volta abilitata la modalità login, potrai gestire utenti, configurare impostazioni e monitorare lo stato del server. Le prime {{freeTierLimit}} persone sul tuo server possono usare Stirling gratuitamente." @@ -5003,6 +5300,11 @@ adminTitle = "Panoramica admin" userBody = "Invita i colleghi, assegna ruoli e tieni i documenti organizzati in un unico spazio di lavoro sicuro. Abilita la modalità login quando sei pronto a crescere oltre l'uso individuale." userTitle = "Panoramica piano" +[onboarding.processorIntro] +body = "Stirling ora esegue Policies — regole automatizzate che classificano, proteggono ed elaborano ogni documento al suo arrivo. Configurale e monitora le esecuzioni nel Processor." +cta = "Scopri il Processor" +title = "Scopri Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "L'editor è ora completamente gratuito." premium = "Abbiamo aggiunto tantissime nuove funzionalità, tra cui le Policy e la Chat con l'Agente." @@ -5353,13 +5655,14 @@ subtitle = "Supera la soglia dei 500 PDF gratuiti con l'automazione, l'IA e l'AP title = "Attiva il piano Processor" [payg.free.editor] -eeyebrow = "Piano Editor · Sempre gratuito" +eyebrow = "Piano Editor · Sempre gratuito" [payg.free.header] freeBody = "Visualizza, modifica, unisci, dividi, firma, inserisci filigrana, comprimi, converti ed esegui l'OCR manuale — tutto quello che vuoi, indipendentemente da dove lo avvii." freeTitle = "Modifica PDF illimitata" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} PDF gratuiti" metaCategories = "Automazione · IA · Richieste API" @@ -5399,6 +5702,28 @@ docs = "PDF" subtitle = "PDF fatturabili che ogni membro del team ha elaborato in questo periodo." title = "Utilizzo dei membri del team" +[payg.prepaid] +chip = "Anno prepagato" + +[payg.prepaid.banner] +exhausted = "La tua capacità prepagata è esaurita; la fatturazione a consumo è ripresa. Ricarica per mantenere lo sconto." +expiring_one = "Il tuo anno prepagato termina tra {{count}} giorno. La capacità inutilizzata scadrà — ricarica per iniziare un nuovo anno." +expiring_other = "Il tuo anno prepagato termina tra {{count}} giorni. La capacità inutilizzata scadrà — ricarica per iniziare un nuovo anno." +low = "Ti resta il {{pct}}% della tua capacità prepagata. Ricarica in qualsiasi momento per continuare l'elaborazione senza interruzioni." + +[payg.prepaid.card] +subtitle = "Usata prima della fatturazione a consumo e fuori dal tuo limite di spesa." +title = "Capacità prepagata" + +[payg.prepaid.meter] +capSuffix = "di {{total}} crediti prepagati" +expires = "Scade il {{date}}" + +[payg.prepaid.state] +exhausted = "Esaurita" +healthy = "Ampia disponibilità" +low = "In esaurimento" + [payg.role] leader = "Proprietario del team" member = "Membro" @@ -5417,6 +5742,7 @@ automation = "le automazioni" default = "questa funzionalità" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ limite di {{amount}}" metaCategories = "Automazione · IA · Spesa API" resets = "Si azzera a ogni periodo di fatturazione" @@ -5745,6 +6071,11 @@ header = "Da PDF a PDF/A" tags = "archivio,a lungo termine,standard,conversione,archiviazione,conservazione" title = "Da PDF a PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "stampa,standard,conversione,produzione,prestampa,archiviazione" title = "PDF in PDF/X" @@ -5820,6 +6151,7 @@ small = "500 Crediti" xsmall = "100 Crediti" [plan.availablePlans] +currency = "Billing currency" subtitle = "Scegli il piano adatto alle tue esigenze" title = "Piani disponibili" @@ -5935,49 +6267,12 @@ keyDescription = "Incolla la chiave di licenza dall'email" success = "Licenza attivata!" successMessage = "La licenza è stata attivata correttamente. Ora puoi chiudere questa finestra." -[policies] -deleteConfirmBody = "Questo rimuoverà la policy e il relativo flusso di lavoro. I documenti già elaborati non subiranno modifiche." -deleteConfirmTitle = "Eliminare la policy {{label}}?" - [policies.activity] -enforced = "applicata" -enforcing = "Applicazione in corso..." -failed = "Applicazione non riuscita" outputsUnavailable = "Gli output delle policy non sono più disponibili per il download." partialOutputsUnavailable = "Alcuni output delle policy non sono più disponibili per il download." -retrying = "Occupato, nuovo tentativo..." runNotFound = "L'esecuzione dell'applicazione non è più reperibile." -step = "passo {{current}}/{{total}}" timedOut = "L'applicazione è andata in timeout prima che l'esecuzione potesse terminare." -[policies.catalog] -compliance = "Conformità" -ingestion = "Acquisizione dati" -retention = "Conservazione" -routing = "Instradamento" -security = "Sicurezza" - -[policies.detail] -close = "Chiudi" -editSettings = "Modifica impostazioni" -enforces = "Applica" -managedByOrg = "Gestito dalla tua organizzazione. Contatta un responsabile del team per modificare questa policy." -noActivityDescription = "I documenti appariranno qui non appena questa policy verrà eseguita." -noActivityTitle = "Ancora nessuna attività" -onEveryUpload = "Su ogni caricamento" -originalsNote = "I file originali rimangono invariati • La versione con le regole applicate viene salvata a fianco" -pause = "Metti in pausa" -recentActivity = "Attività recente" -resume = "Riprendi" -retry = "Riprova" -showLess = "Mostra meno" -showMore = "Mostra di più" -statActive = "Attive" -statDataProcessed = "Dati elaborati" -statDocsEnforced = "Doc. con policy applicate" -statusActive = "Attiva" -statusPaused = "In pausa" - [policies.enforcement] applying = "Applicazione di {{names}}" applyingProgress = "Applicazione di {{names}} ({{done}} di {{total}})" @@ -5987,61 +6282,16 @@ failureBody = "Non è stato possibile elaborare {{failures}} su {{total}} file, failureTitle = "Esportato senza applicazione completa" printPolicyAppliedBody = "Questo PDF è stato aggiornato per rispettare una policy. Controlla le modifiche, poi stampa di nuovo." printPolicyAppliedTitle = "Policy applicata prima della stampa" -queued = "+{{count}} in coda" successTitle = "{{names}} applicate" summaryMore = "{{first}}, {{second}} e altri {{more}}" summaryTwo = "{{first}} e {{second}}" -[policies.enforcement.triggerVerb] -convert = "Applicazione prima della conversione" -default = "Applicazione" -export = "Applicazione prima dell'esportazione" -input = "Applicazione all'importazione" -print = "Applicazione prima della stampa" - -[policies.field] -accessLog = "Log di accesso" -archiveAfter = "Archivia dopo" -auditTrail = "Audit trail" -belowThreshold = "Sotto la soglia" -destination = "Destinazione" -frameworks = "Framework" -immutableHold = "Blocco immutabile" -keepFor = "Conserva per" -minConfidence = "Confidenza min." -notify = "Notifica su instradamento" -onViolation = "Quando non conforme" -webhookUrl = "URL webhook" - -[policies.fields] -selectedCount = "{{count}} selezionati" - [policies.labels] -add = "Aggiungi" -addPlaceholder = "Aggiungi un'etichetta…" -customNote = "Personalizzato per il tuo team." -defaultNote = "Uso dell'impostazione predefinita integrata, condivisa con il tuo team." -duplicate = "\"{{name}}\" esiste già." -edit = "Modifica etichette" -empty = "Nessuna etichetta ancora." -export = "Esporta JSON" -iconAria = "Scegli un'icona per {{name}}" -import = "Importa JSON" -importError = "Impossibile importare quel file." -managedNote = "Le etichette del team sono gestite dal leader del team." -modalSubtitle = "Condivise con tutto il tuo team. Il classificatore sceglie le etichette adatte a ciascun documento." -modalTitle = "Etichette di classificazione" -pickIcon = "Usa l'icona {{label}}" -removeAria = "Rimuovi {{name}}" -resetToDefault = "Ripristina predefinito" -saveForTeam = "Salva per il team" -saving = "Salvataggio…" -sectionLabel = "Etichette di classificazione" -startFromScratch = "Inizia da zero" -teamCount = "etichette del team" -tooLong = "Le etichette possono contenere al massimo {{max}} caratteri." -ungrouped = "Non raggruppate" -view = "Visualizza etichette" +categoryCount = "categorie" +hideCategory = "Nascondi categoria" +labelCount = "etichette" +sharedNote = "Queste etichette sono integrate e condivise con tutto il tuo team." +showCategory = "Mostra categoria" [policies.pii] account = "Numeri di conto (etichettati)" @@ -6050,97 +6300,14 @@ email = "Indirizzi email" fieldLabel = "Dati sensibili (PII) da oscurare" iban = "IBAN" phone = "Numeri di telefono" -pplaceholder = "Seleziona i tipi di PII" +placeholder = "Seleziona tipi di PII" routing = "Numeri di instradamento USA (ABA)" ssn = "Numeri di previdenza sociale (SSN)" -[policies.settings] -noneExport = "Attualmente nessun criterio viene eseguito all'esportazione." -noneUpload = "Attualmente nessun criterio viene eseguito al caricamento." -onExport = "All'esportazione" -onUpload = "Al caricamento" -reorderHandle = "Trascina per riordinare" -runOrderDesc = "Quando più criteri vengono eseguiti sullo stesso trigger, vengono eseguiti in questo ordine — ciascuno sull'output del criterio precedente. Trascina per riordinare." -title = "Impostazioni criteri" - -[policies.sidebar] -activeCount = "{{count}} attive" -infoTooltip = "Una policy è un insieme fisso di strumenti che viene eseguito automaticamente ogni volta che viene attivato (ad esempio, all'arrivo di un nuovo documento), applicando regole come l'oscuramento dei dati sensibili (PII) senza passaggi manuali." -loading = "Caricamento in corso…" -optionsAriaLabel = "Opzioni criterio" -policySettings = "Impostazioni criteri" -railAriaLabel = "Policy {{label}} — {{status}}" -railSuffixActive = " (Attiva)" -railSuffixPaused = " (In pausa)" -retryFailed = "Riprova criteri non riusciti ({{count}})" -rowProgress = "{{completed}} di {{total}} file elaborati" -setUp = "Configura" -title = "Policy" -upgradeToEnterprise = "Passa alla versione Enterprise" -whatIsPolicy = "Che cos'è un criterio?" - -[policies.status] -active = "Attiva" -paused = "In pausa" -setup = "Configura" - -[policies.toolConfig] -enableAriaLabel = "Abilita {{tool}}" -infoAriaLabel = "Cosa fa lo strumento {{tool}}?" - -[policies.toolConfig.info] -redact = "Trova automaticamente e oscura i dettagli sensibili — come numeri di Social Security e di carta — in modo che non possano essere letti nel documento." -sanitize = "Rimuove il JavaScript nascosto dal file, così nulla può essere eseguito automaticamente quando qualcuno lo apre." -watermark = "Appone un contrassegno visibile (es. \"Confidential\") su ogni pagina." - -[policies.wizard] -allDocTypesDescription = "Abilita la policy di Classificazione per filtrare in base al tipo di documento." -allDocTypesTitle = "Tutti i tipi di documento" -back = "Indietro" -builderDesc = "Costruisci la sequenza di strumenti che questa policy eseguirà su ciascun documento." -clear = "Cancella" -close = "Chiudi" -continue = "Continua" -docTypesLabel = "Tipi di documento" -edit = "Modifica" -editTitle = "Modifica policy {{label}}" -enablePolicy = "Abilita policy" -filenameAutoNumber = "Numerazione automatica" -filenamePositionAria = "Posizione nome file" -filenamePrefix = "Prefisso" -filenameSuffix = "Suffisso" -filenameTextAria = "Testo nome file" -filenameTextPlaceholder = "Testo da aggiungere (opzionale)" -lockedDescription = "Contatta un responsabile del team per modificare questa policy." -lockedTitle = "Gestito dalla tua organizzazione" -maxRetriesLabel = "Tentativi massimi" -noToolsError = "Aggiungi prima almeno uno strumento configurato al flusso di lavoro." -outputAsLabel = "Output come" -outputFilenameSubhead = "Nome del file di output" -outputModeAria = "Modalità di output" -outputNewFile = "Nuovo file" -outputNewVersion = "Nuova versione" -outputRetriesLabel = "Output e tentativi" -outputSubhead = "Output" -retryDelayAria = "Minuti di ritardo tentativo" -retryDelayLabel = "Ritardo tentativo (min)" -runOnExport = "Esportazione" -runOnLabel = "Esegui su" -runOnSubhead = "Esegui su" -runOnUpload = "Caricamento" -saveChanges = "Salva modifiche" -saveError = "Impossibile salvare la policy. Riprova." -setupClassification = "Configura Classificazione" -setupTitle = "Configura policy {{label}}" -sourcesDesc = "Scegli dove eseguire questa policy e a quali tipi di documento applicarla." -sourcesLabel = "Origini" -stepOf = "Passaggio {{step}} di {{total}}" -toolChainDesc = "Configura gli strumenti che questa policy eseguirà su ciascun documento." -typesSelected = "{{count}} tipi selezionati" - [policy] badgeEnforcing = "Applicazione di {{name}} in corso..." badgeRan = "Il criterio {{name}} è stato eseguito su questo file" +badgeRunning = "{{name}} in esecuzione..." blockingAction = "{{action}} bloccata durante l'applicazione del criterio, attendi..." dismiss = "Chiudi overlay" enforcingTitle = "Applicazione criterio in corso..." @@ -6149,6 +6316,7 @@ viewAnyway = "Visualizza file (criterio ancora in applicazione)" [policyOption] autoRedactPhi = "Oscura automaticamente PHI" blockExport = "Blocca esportazione" +ccpa = "CCPA" contracts = "Contratti" documents = "Documenti" fedramp = "FedRAMP" @@ -6267,86 +6435,6 @@ free = "Piano Editor" subscribed = "Piano Processor" unlinked = "Non collegato" -[portal.agentBuilder] -bootstrapFromDocument = "Inizializza da documento" -sectionsAriaLabel = "Sezioni del generatore di agenti" -selectorAriaLabel = "Agenti" -subtitle = "Progetta, testa e distribuisci gli agenti AI che classificano, estraggono e instradano i tuoi documenti. Definisci scenari, limita l'accesso agli strumenti, esegui un golden set e pubblica una versione." -title = "Generatore di agenti" - -[portal.agentBuilder.bootstrap] -cancel = "Annulla" -dropzoneText = "Scegli un documento di esempio (PDF o immagine)" -lead = "Rilascia un documento di esempio e proporremo scenari e uno schema di estrazione che potrai perfezionare. Non verrà pubblicato nulla finché non lo avrai revisionato." -submit = "Inizializza agente" -subtitle = "Avvia un nuovo agente da un file rappresentativo" -title = "Inizializza da un documento" - -[portal.agentBuilder.empty] -description = "Inizializza un agente da un documento di esempio per avviare i suoi scenari e lo schema di estrazione, quindi perfeziona e pubblica." -title = "Nessun agente ancora" - -[portal.agentBuilder.evals] -casesPassing = "Casi superati" -columnCase = "Caso di valutazione" -columnLatency = "Latenza" -columnResult = "Risultato" -fail = "non riuscito" -goldenSetPassRate = "Tasso di superamento golden set" -latencyMs = "{{ms}} ms" -notRun = "non eseguito" -pass = "superato" -passRate = "Tasso di superamento" -runEvals = "Esegui valutazioni" - -[portal.agentBuilder.evals.empty] -description = "Le valutazioni trasformano i tuoi scenari in un golden set ripetibile. Esegui l'upgrade per acquisire il tasso di superamento nel tempo e vincolare le pubblicazioni a esso." -title = "Nessun golden set ancora" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "tra golden set" -activeAgents = "Agenti attivi" -avgPassRate = "Tasso medio superamento valutazioni" -fleetWide = "su tutta la flotta" -latestPublished = "Ultima pubblicazione" -scenarios = "Scenari" -testCases = "casi di test" -totalDescription_one = "{{count}} totale" -totalDescription_other = "{{count}} totali" - -[portal.agentBuilder.scenarios] -add = "Aggiungi" -addScenario = "Aggiungi scenario" -enable = "Abilita" -expectationLabel = "Comportamento previsto" -expectationPlaceholder = "Cosa dovrebbe fare l'agente" -inEval = "in valutazione" -mute = "Silenzia" -muted = "silenziato" -nameLabel = "Nome" -namePlaceholder = "es. Escalation conformità" - -[portal.agentBuilder.tabs] -evals = "Valutazioni" -scenarios = "Scenari" -tools = "Strumenti" -versions = "Versioni" - -[portal.agentBuilder.tools] -broadAccess = "Accesso ampio" -deniedHint = "Gli strumenti selezionati sono bloccati. Tutto il resto resta richiamabile." -deniedTools = "Strumenti negati" -governanceGate = "La governance degli strumenti è disponibile nel piano Enterprise." -restricted = "Limitato" -restrictedAccess = "Accesso limitato agli strumenti" -restrictedDescription = "Consenti ogni strumento tranne quelli che neghi di seguito." - -[portal.agentBuilder.versions] -current = "corrente" -historyGate = "La cronologia completa delle versioni e il rollback sono disponibili nel piano Enterprise." -publish = "Pubblica" -rollBack = "Esegui rollback" - [portal.assistant] close = "Chiudi assistente" error = "Impossibile raggiungere l'assistente." @@ -6363,9 +6451,21 @@ typing = "Sta scrivendo" loading = "Caricamento" redirectingToEditor = "Reindirizzamento all'editor..." +[portal.billing.activation] +maybeLater = "Forse più tardi" +title = "Attiva il Processor" + +[portal.billing.activation.payg] +sub = "Imposta un limite mensile. Paghi per ciò che esegui." +title = "Pay as you go" + +[portal.billing.activation.prepay] +badge = "2 mesi gratis" +sub = "Una fattura, carta o bonifico bancario." +title = "Paga un anno in anticipo" + [portal.billing.checkout] noClientSecret = "La funzione edge non ha restituito client_secret." -subtitle = "Aggiungi una carta per continuare oltre il credito gratuito del piano Editor. Stripe gestisce il resto." title = "Attiva il piano Processor" [portal.billing.checkout.activationSlow] @@ -6373,6 +6473,22 @@ body = "Il pagamento è riuscito, ma l'attivazione sta richiedendo un po' più d close = "Chiudi" title = "Ci siamo quasi" +[portal.billing.checkout.cap] +amountAria = "Limite di spesa mensile" +back = "Indietro" +continue = "Continua al pagamento" +error = "Impossibile impostare il limite di spesa" +finePrint = "Le fatture vengono emesse mensilmente. Annulla in qualsiasi momento." +noLimit = "Nessun limite" +note = "Non ti verrà mai addebitato oltre il tuo limite. L'elaborazione viene semplicemente sospesa." +perMonth = "/ mese" +title = "Imposta il limite di spesa" + +[portal.billing.checkout.card] +fields = "Numero carta · MM / AA · CVC · ZIP" +label = "Dettagli carta" +note = "Dettagli carta raccolti da Stripe. Stirling non memorizza mai PAN o CVC." + [portal.billing.checkout.error] title = "Impossibile avviare il checkout" @@ -6381,10 +6497,9 @@ body = "Il pagamento è andato a buon fine. Stiamo attivando l'elaborazione a co hint = "Tieni aperta questa finestra." title = "Attivazione del piano Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "nell'env del portale per abilitare il checkout in-app." -bodyBefore = "Imposta" -title = "Stripe non configurato" +[portal.billing.checkout.pay] +limit = "Limite di spesa mensile {{amount}}/mese. L'elaborazione si sospende al limite." +limitNoCap = "Nessun limite di spesa. L'elaborazione non si sospenderà. Le fatture vengono emesse mensilmente." [portal.billing.enterpriseUpsell] cta = "Crea il tuo preventivo Enterprise" @@ -6407,10 +6522,12 @@ currentPlan = "Piano attuale" freeForever = "Gratis per sempre" noTeamResolved = "Nessun team è ancora risolto sul tuo portafoglio — aggiorna e riprova." ownerOnly = "Solo il proprietario del team può attivare il piano Processor." +payInvoice = "Paga fattura per completare" planName = "Editor" ssoIncluded = "SSO incluso" switchOnProcessor = "Attiva Processor →" unlimitedUsers = "Utenti illimitati" +viewQuote = "Visualizza preventivo" [portal.billing.invoices] columnAmount = "Importo" @@ -6463,7 +6580,111 @@ segmentApiDesc = "Richieste API dirette" segmentApiLabel = "API" segmentAutomationDesc = "Automazioni e pipeline" segmentAutomationLabel = "Automazione" +sizeMultiplier = "{{formatted}} PDF hanno usato un moltiplicatore di dimensione" +summary = "{{unique}} unici · {{units}} unità contatore · {{avg}} media per PDF" +summaryNoRate = "{{unique}} unici · {{units}} unità contatore" unit = "PDF a consumo" +unitsPending_one = "{{units}} unità contatore in attesa di sincronizzazione dalle istanze collegate" +unitsPending_other = "{{units}} unità contatore in attesa di sincronizzazione dalle istanze collegate" + +[portal.billing.prepaid] +capSuffix = "di {{total}} crediti prepagati" +expires = "Scade il {{date}}" +eyebrow = "Capacità prepagata" +note = "Usata prima della fatturazione a consumo e fuori dal tuo limite di spesa." +topUp = "Ricarica" + +[portal.billing.prepaid.buy] +back = "Indietro" +calcTitle = "Calcola il tuo pagamento annuale" +cancel = "Annulla" +cancelPurchase = "Annulla acquisto" +close = "Chiudi" +continueToPayment = "Continua al pagamento" +finalise = "Finalizza" +invoiceErrorTitle = "Impossibile generare la fattura" +loadingTitle = "Caricamento del preventivo" +payTitle = "Paga il tuo anno" +step = "Passaggio {{current}} di {{total}}" + +[portal.billing.prepaid.calc] +change = "Modifica" +deployRow = "Distribuzione" +done = "Fatto" +enterpriseHint = "Questa è scala enterprise. A questo volume, le tariffe enterprise superano qualsiasi sconto self-service. Blocco tariffa, termini e preventivo in pochi minuti." +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} policy" +handlesLabel = "Il tuo Processor" +handlesValue = "gestisce {{volume}} crediti / mese" +pipelinesLabel = "Pipeline" +policiesMeta = "{{count}} policy" +poolCaption = "Un unico pool di {{credits}} crediti per l'anno. I mesi pesanti prendono in prestito da quelli leggeri." +priceLabel = "Il tuo anno · 12 mesi al prezzo di 10" +rateUnknown = "Mostreremo il prezzo esatto al checkout." +savings = "Risparmi {{amount}} · 2 mesi gratis." +sizingRow = "Dimensionamento" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Utenti totali" + +[portal.billing.prepaid.consent] +label = "Comprendo che quando la mia capacità prepagata sarà esaurita o scadrà dopo 12 mesi, l'elaborazione continuerà automaticamente alla tariffa standard a consumo pay-as-you-go (fino al mio limite di spesa), salvo annullamento, e che posso annullare in qualsiasi momento dal portale di fatturazione." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gestito da Stirling. Attivo in pochi minuti." +cloudValue = "Stirling Cloud · Gestito" +selfhost = "Self-hosted" +selfhostDesc = "Eseguito su infrastruttura privata. Stessa tariffa." +selfhostValue = "Self-hosted · Infrastruttura privata" + +[portal.billing.prepaid.offer] +cta = "Rivedi offerta" +subtitle = "Paga in anticipo un anno di elaborazione PDF e ottieni due mesi gratis, usati prima della fatturazione a consumo, fuori dal tuo limite di spesa." +title = "Ottieni 12 mesi al prezzo di 10" + +[portal.billing.prepaid.pay] +accountName = "Nome intestatario account" +accountNamePlaceholder = "es. Jane Smith" +companyName = "Nome azienda (opzionale)" +companyNamePlaceholder = "es. Acme Inc." +downloadInvoice = "Scarica fattura" +payOnline = "Paga online" +poNumber = "Numero PO (opzionale)" +poNumberPlaceholder = "es. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avanzate" +advancedDesc = "Le pipeline gestiscono la maggior parte della tua elaborazione." +none = "Nessuna" +noneDesc = "Non stai ancora eseguendo pipeline. Attivale in qualsiasi momento." +standard = "Standard" +standardDesc = "Alcune pipeline rielaborano i PDF in arrivo." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Classificazione, predefinita, più condivisione" +governed = "Governed" +governedDesc = "Aggiunge sicurezza e routing" +regulated = "Regulated" +regulatedDesc = "Ogni categoria, incl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Scarica preventivo (PDF)" +downloading = "Preparazione…" +share = "condividi per approvazione" + +[portal.billing.prepaid.size] +compact = "Compatta" +compactDesc = "File sotto 25 MB, nessun addebito dati" +heavy = "Pesante" +heavyDesc = "Scansionati o ricchi di immagini, abitualmente 50 MB+" +standard = "Standard" +standardDesc = "Per lo più piccoli, alcune scansioni oltre 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Esaurita" +healthy = "Ampia disponibilità" +low = "In esaurimento" [portal.billing.spendLimit] adjustLimit = "Modifica limite" @@ -6492,6 +6713,8 @@ label = "Previsto superamento." [portal.billing.spendThisMonth] eyebrow = "Spesa di questo mese" +freeRemaining_one = "{{formatted}} PDF gratuito rimanente" +freeRemaining_other = "{{formatted}} PDF gratuiti rimanenti" processed_one = "{{formattedCount}} PDF elaborato." processed_other = "{{formattedCount}} PDF elaborati." processedWithRate_one = "{{formattedCount}} PDF elaborato, a {{rate}} ciascuno." @@ -6508,6 +6731,7 @@ reachedTitle = "Limite di spesa mensile raggiunto" title = "Impossibile aprire il portale Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "di {{allowance}} PDF gratuiti usati" capSuffix_other = "di {{allowance}} PDF gratuiti usati" eyebrow = "Prova Processor" @@ -6532,6 +6756,376 @@ signature = "firma" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nuova connessione" +createTitleFor = "Connetti {{name}}" +delete = "Elimina" +edit = "Modifica" +editTitle = "Modifica connessione" +unsupportedType = "Questo tipo di connessione non può essere modificato qui." + +[portal.connections.categories.advanced] +description = "Per un servizio che non supportiamo ancora." +label = "Avanzate" + +[portal.connections.categories.audit] +description = "Registra cosa è successo a un documento, dove i tuoi revisori già controllano." +label = "Logging audit e compliance" + +[portal.connections.categories.notify] +description = "Comunica a un altro sistema che è successo qualcosa." +label = "Notifica e workflow" + +[portal.connections.categories.security] +description = "Scansiona o etichetta un documento prima che prosegua." +label = "Sicurezza e classificazione" + +[portal.connections.categories.signing] +description = "Invia un documento per la firma o la notarizzazione." +label = "Firma elettronica e notarizzazione" + +[portal.connections.categories.storage] +description = "Dove finiscono i documenti elaborati - un bucket, un drive, un ticket o una pagina." +label = "File e allegati" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling lo archivia crittografato e non lo mostra più." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "I passaggi che usano questa connessione possono chiamare solo percorsi sotto questo URL, mai un altro host." +label = "URL server" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Porta" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "Incolla l'URL fornito dal servizio. Possono essere chiamati solo percorsi sotto di esso." +label = "Webhook URL" + +[portal.connections.fields] +name = "Nome connessione" +namePlaceholder = "es. Produzione" + +[portal.connections.picker] +cancel = "Annulla" +createNew = "Nuova connessione..." +placeholder = "Seleziona una connessione" +save = "Salva connessione" + +[portal.connections.picker2] +back = "Cambia integrazione" +heading = "Cosa vuoi connettere?" +noResultsBody = "Non lo supportiamo ancora - parlacene e valuteremo di aggiungerlo." +noResultsTitle = "Nessuna corrispondenza per \"{{query}}\"" +searchPlaceholder = "Cerca integrazioni - prova \"sign\", \"ocr\" o un nome prodotto" + +[portal.connections.types.api] +description = "Chiama qualsiasi HTTP API da una policy. Scegli URL, autenticazione e payload." +label = "API personalizzata" + +[portal.connections.types.api.fields.authType] +label = "Autenticazione" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username e password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token in un header personalizzato" + +[portal.connections.types.api.fields.authType.options.none] +label = "Nessuna" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Accedi per un token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "I passaggi che usano questa connessione possono chiamare solo percorsi sotto questo URL, mai un altro host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nome header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Percorso login" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Se l'API risponde con un link di download, possono essere recuperati solo questi host. Separa con virgole. Lascia vuoto per consentire solo l'host dell'URL di base." +label = "Host risultato consentiti" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Invia il token come questo header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "L'header di risposta in cui la chiamata di login restituisce il token, es. X-Auth-Token." +label = "Header risposta token" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scansiona documenti alla ricerca di malware su un server che gestisci - nulla lascia la tua rete." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Scansione virus e contenuti per documenti in una policy." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blocca documenti contenenti macro, eseguibili, script o oggetti incorporati - non solo malware noto." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Allega documenti elaborati a una pagina Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Invia documenti a ConsignO Cloud (Notarius) per firma elettronica e notarizzazione." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Il tuo ambiente ConsignO Cloud, incluso /api/v1." +label = "URL server" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Necessario solo se la tua organizzazione usa tenant." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Pubblica un messaggio su Discord quando una policy elabora un documento." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indicizza un evento di audit in Elasticsearch ogni volta che una policy gestisce un documento." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Interroga un server FTP o FTPS per nuovi documenti." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Modalità connessione" + +[portal.connections.types.ftp.fields.passive.options] +active = "Attiva" +passive = "Passiva (consigliata)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP semplice invia credenziali e file non crittografati; scegli FTPS se il server lo supporta." +label = "Crittografia" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (esplicito / AUTH TLS)" +implicit = "FTPS (implicito)" +none = "Nessuna (FTP semplice)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Pubblica un messaggio in uno spazio Google Chat quando una policy viene eseguita." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Allega documenti elaborati a un issue Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Invia un documento o una notifica via email quando una policy viene eseguita." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Archivia documenti elaborati in Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Rileva dati personali nel testo dei documenti usando un server Presidio che gestisci." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Leggi e applica etichette di sensibilità Microsoft Purview sui PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opzionale. Necessario solo per scegliere etichette per nome; l'etichettatura funziona anche senza." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Applicato a ogni etichetta come SiteId, così gli strumenti compatibili con Purview sanno quale organizzazione l'ha applicata." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Un bucket S3 da cui le sorgenti possono leggere e in cui gli output della pipeline possono scrivere." +label = "Archiviazione S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Lascia vuoto per AWS. Impostalo per MinIO o un altro archivio compatibile con S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Regione" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Invia un documento o una notifica via email quando una policy viene eseguita." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Interroga un server SFTP (SSH) per nuovi documenti." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opzionale. L'impronta della chiave host SHA-256 del server (da `ssh-keyscan host | ssh-keygen -lf -`). Se impostata, viene accettato solo un server che presenta questa chiave; se vuota, la chiave vista alla prima connessione viene fissata e le modifiche successive vengono rifiutate." +label = "Impronta chiave host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Passphrase chiave" + +[portal.connections.types.sftp.fields.password] +helperText = "Lascia vuoto se ti autentichi invece con una chiave privata." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opzionale. Incolla una chiave privata OpenSSH per autenticarti con una chiave invece che con una password." +label = "Chiave privata" + +[portal.connections.types.slack] +description = "Pubblica un messaggio su Slack quando una policy elabora un documento." +label = "Slack" + +[portal.connections.types.smb] +description = "Monitora una condivisione di rete SMB / CIFS (Windows o Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Condivisione di rete" + +[portal.connections.types.smb.fields.domain] +helperText = "Opzionale. Il dominio Windows o workgroup, se il server ne usa uno." +label = "Dominio" + +[portal.connections.types.smb.fields.share] +label = "Condivisione" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Invia un evento di audit a Splunk ogni volta che una policy gestisce un documento." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Invia un evento di audit a un collector HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Pubblica un messaggio in un canale Teams quando una policy viene eseguita." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Pubblica ogni documento, o solo i dettagli, a un URL che scegli." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Attiva uno scenario Zap o Make da una policy." +label = "Zapier e Make" + +[portal.docs] +browse = "Sfoglia docs" +viewSource = "Visualizza sorgente su GitHub" + [portal.docs.authentication] codeCaption = "ogni richiesta" eyebrow = "PER INIZIARE" @@ -6622,6 +7216,9 @@ eyebrow = "SKILL" lead = "Capacità incluse e denominate che il tuo agente invoca come singolo strumento. Ogni skill è una catena di operazioni deterministica con valutazioni associate." title = "Skill agente" +[portal.docs.toc] +title = "In questa pagina" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "RIFERIMENTO API" @@ -6695,6 +7292,8 @@ type = "Tipo" user = "Utente" [portal.documents.queue.empty] +connectSource = "Connetti una sorgente" +createPipeline = "Crea una pipeline" description = "Man mano che le origini inviano documenti alle tue pipeline, appariranno qui per la revisione." title = "Nessun documento in coda" @@ -6710,11 +7309,11 @@ editorAction = "Editor" empty = "Nessun documento corrisponde a questo filtro." rowActions = "Azioni riga" sensitiveLabel = "Sensibile" -sensitiveTitle = "Sensibile: accesso richiesto" [portal.documents.table.columns] action = "Pipeline / Azione" document = "Documento" +labels = "Labels" product = "Prodotto" status = "Stato" time = "Ora" @@ -6730,6 +7329,7 @@ host = "Host" lastSeen = "Ultima visualizzazione" region = "Regione" status = "Stato" +target = "Target" version = "Versione" [portal.editorAdmin.health.empty] @@ -6804,13 +7404,121 @@ description = "Questa vista ha riscontrato un errore imprevisto. Riprova oppure retry = "Riprova" title = "Qualcosa è andato storto in questa pagina" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Indietro" +body = "Scarica l'app desktop o ospita autonomamente il server." +done = "Fatto" +guide = "Apri la guida {{name}}" +openInBrowser = "Apri invece nel browser" +sectionDesktop = "App desktop" +sectionSelfHosted = "Self-hosted" +title = "Installa Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Esegui l'immagine ufficiale sul tuo server." +note = "Si apre su :8080. Accesso predefinito: admin / stirling." +tagline = "Esegui il server in un container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installa il chart Helm ufficiale." +note = "Aggiungi un PVC per /configs per mantenere le impostazioni." +tagline = "Distribuisci in un cluster con Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Build native per la tua distribuzione." +guideBtn = "Visualizza le opzioni di installazione per Linux" +note = "Sono disponibili build AppImage, .deb e .rpm per le distro più comuni." +tagline = "App desktop nativa per la tua distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Oppure tramite Homebrew" +detailBody = "Esegui l'installer oppure usa Homebrew." +downloadBtn = "Scarica per macOS" +tagline = "App nativa per Apple silicon e Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Esegui il .jar su un server bare metal." +downloadBtn = "Scarica il .jar" +note = "Richiede Java 21+. Si apre su :8080; accesso predefinito admin / stirling." +runLabel = "Poi eseguilo" +tagline = "Esegui il .jar su un server bare metal" +title = "Configurazione manuale del server" + +[portal.home.download.windows] +altLabel = "Oppure tramite winget" +detailBody = "Esegui l'installer oppure usa un gestore di pacchetti." +downloadBtn = "Scarica installer" +tagline = "App nativa per Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} attivi" -invite = "Invita compagni di team" +activeOfDeployed = "{{active}} di {{total}} attivi questo mese" name = "Stirling PDF Editor" open = "Apri nel browser" updated = "aggiornato {{time}}" +[portal.home.editor.deploy] +finish = "Completa la distribuzione" +options = "Visualizza opzioni di distribuzione" +start = "Distribuisci l'Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6821,49 +7529,6 @@ afternoon = "Buon pomeriggio" evening = "Buonasera" morning = "Buongiorno" -[portal.home.onboarding] -dismiss = "Ignora configurazione" -notStarted = "Non avviato" -progress = "{{done}} di {{total}} completati" -title = "Completa la configurazione" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC a livello di organizzazione, prezzi a volume impegnato e distribuzione air-gapped." -cta = "Avvia prova" -ctaQuote = "Richiedi preventivo" -lead = "Per oltre 250 dipendenti." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Distribuisci l'app desktop, gratis per sempre." -title = "Scarica il PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} attive · {{recommended}} consigliate" -chip = "{{active}} attive" -title = "Attiva i criteri" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} connesse · ogni PDF governato dove arriva" -chip = "{{connected}} connesse" -title = "Connetti le tue origini" - -[portal.home.quickActions] -subtitle = "Attività principali per oggi" -title = "Azioni rapide" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositore in 3 passaggi sulla libreria di operazioni tipizzate" -title = "Crea una pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooks, cartelle monitorate" -title = "Connetti un'origine" - -[portal.home.quickActions.issueApiKey] -blurb = "Chiave con ambito, limiti di frequenza e allowlist IP" -title = "Emetti una chiave API" - [portal.infrastructure] manageEditorDeployment = "Gestisci la distribuzione dell'Editor" sectionsAriaLabel = "Sezioni dell'infrastruttura" @@ -6876,13 +7541,9 @@ heading = "Chiavi API" subheading = "Credenziali con ambito, limiti di frequenza per chiave, autorizzazioni e allowlist IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IP consentiti" -anyIp = "Qualsiasi IP (nessuna allowlist)" created = "Creata" lastUsed = "Ultimo utilizzo" -permissions = "Autorizzazioni" -rateLimit = "Limite di frequenza" -rateLimitValue = "{{value}} req/min" +revoke = "Revoca chiave" usageMonth = "Utilizzo questo mese" usageToday = "Utilizzo oggi" @@ -6890,6 +7551,15 @@ usageToday = "Utilizzo oggi" description = "Crea una chiave con ambito per iniziare a chiamare la Stirling API." title = "Ancora nessuna chiave API" +[portal.infrastructure.apiKeys.error] +load = "Impossibile caricare le tue chiavi API. Riprova." + +[portal.infrastructure.apiKeys.revoke] +body = "Revocare \"{{name}}\"? Qualsiasi integrazione che usa ancora questa chiave inizierà subito a ricevere errori 401. Questa azione non può essere annullata." +cancel = "Annulla" +confirm = "Revoca chiave" +title = "Revoca chiave API" + [portal.infrastructure.audit] filterAriaLabel = "Filtra gli eventi di audit per categoria" heading = "Log di audit" @@ -6933,6 +7603,7 @@ all = "Tutti" auth = "Autenticazione" config = "Config" elevation = "Elevazione" +policy = "Policy" processing = "Elaborazione" security = "Sicurezza" @@ -6942,19 +7613,30 @@ title = "Non hai accesso" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Elevazione" +policy = "Policy" processing = "Elaborazione" totalEvents = "Eventi totali · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Configurazione" +elevation = "Elevazione" +policy = "Policy" +processing = "Elaborazione" +security = "Sicurezza" + +[portal.infrastructure.auditStatusLabel] +danger = "Errore" +info = "Info" +success = "Successo" +warning = "Avviso" + [portal.infrastructure.createKey] cancel = "Annulla" createKey = "Crea chiave" done = "Fatto" -ipAllowlistHelper = "Intervalli CIDR separati da virgole. Lascia vuoto per consentire qualsiasi IP." -ipAllowlistLabel = "Allowlist IP" keyNameLabel = "Nome chiave" keyNamePlaceholder = "es. Produzione · acquisizione" -permissionsLabel = "Autorizzazioni" secretKeyCaption = "Chiave segreta" secretWarning = "Archiviala in un gestore di segreti. Stirling memorizza sempre e solo un hash: non è possibile recuperarla in seguito." subtitle = "Limita la chiave al minimo necessario. Puoi ruotarla o revocarla in qualsiasi momento." @@ -6962,207 +7644,9 @@ subtitleCreated = "Copia ora questo segreto: non verrà mostrato di nuovo." title = "Crea chiave API" titleCreated = "Chiave creata" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Distribuito da" -environment = "Ambiente" -product = "Prodotto" -status = "Stato" -version = "Versione" -when = "Quando" - -[portal.infrastructure.deployments.recent] -heading = "Distribuzioni recenti" -subheading = "Gli ultimi rollout tra prodotti e ambienti." - -[portal.infrastructure.deployments.regionColumns] -instances = "Istanze" -latency = "Latenza" -load = "Carico" -p99 = "P99" -region = "Regione" -status = "Stato" -throughput = "Throughput" -uptime = "Uptime" -version = "Versione" - -[portal.infrastructure.deployments.regions] -heading = "Regioni" -subheading = "Integrità in tempo reale per ogni regione Stirling distribuita: latenza, carico e versione di rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "Le regioni distribuite appariranno qui una volta eseguito il provisioning del tuo workspace." -title = "Nessuna regione distribuita" - -[portal.infrastructure.models] -heading = "Modelli" -msValue = "{{value}} ms" -subheading = "Il catalogo dei modelli e il routing che alimentano l'elaborazione dei documenti nel tuo workspace." - -[portal.infrastructure.models.byom] -description = "Registra un modello on-prem o self-hosted e fissalo a una regione per l'elaborazione vincolata alla residenza dei dati." -title = "Porta il tuo modello" - -[portal.infrastructure.models.catalogue] -heading = "Catalogo" -sub = "Modelli gestiti disponibili per il tuo workspace, con latenza e costi in tempo reale." -subEnterprise = "Modelli gestiti, bring-your-own e on-prem, con pinning per regione disponibile." - -[portal.infrastructure.models.catalogue.empty] -description = "I modelli nel catalogo del tuo workspace appariranno qui." -title = "Nessun modello disponibile" - -[portal.infrastructure.models.columns] -cost = "Costo" -latency = "Latenza" -load = "Carico" -model = "Modello" -status = "Stato" -type = "Tipo" -version = "Versione" - -[portal.infrastructure.models.metrics] -activeModels = "Modelli attivi" -avgLatency = "Latenza media" -included = "Incluso" -monthlySpend = "Spesa mensile modelli" - -[portal.infrastructure.models.routing] -empty = "Nessuna regola di routing configurata." -heading = "Regole di routing" -sub = "Quale modello gestisce ogni operazione. Il predefinito si applica quando nessuna regola più specifica corrisponde." -subLocked = "Instrada le operazioni a modelli specifici: disponibile nei piani a pagamento." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Passa a Pro per controllare quale modello gestisce ogni operazione e tipo di documento." -title = "Il routing dei modelli è una funzionalità a pagamento" - -[portal.infrastructure.models.routingColumns] -default = "Predefinito" -docType = "Tipo di documento" -modelForAria = "Modello per {{operation}}" -operation = "Operazione" -routedTo = "Instradato a" - -[portal.infrastructure.security.access.byok] -description = "Fornisci una chiave dal tuo KMS. Stirling cifra con essa ma può comunque leggere." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Le chiavi non lasciano mai il tuo KMS. Stirling conserva solo ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling gestisce le chiavi di crittografia. Il più semplice: zero operazioni sulle chiavi da parte tua." -label = "Chiavi detenute da Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Criterio di accesso ai documenti" -subheading = "Controlla chi può decifrare i documenti elaborati a riposo." - -[portal.infrastructure.security.attestations] -heading = "Attestazioni di conformità" -noReport = "Nessun report disponibile" -subheading = "Postura di audit framework per framework, con report disponibili sui controlli attestati." -viewReport = "Visualizza report →" - -[portal.infrastructure.security.compliance] -heading = "Conformità" -subheading = "Attestazioni e certificazioni che coprono la piattaforma Stirling." - -[portal.infrastructure.security.empty] -description = "La configurazione di sicurezza del tuo workspace apparirà qui." -title = "Postura di sicurezza non disponibile" - -[portal.infrastructure.security.hyokBanner] -description = "Con HYOK, le chiavi di crittografia non lasciano mai il tuo KMS. Stirling archivia ed elabora solo ciphertext che puoi revocare in qualsiasi momento." -title = "Stirling non può decifrare i tuoi documenti" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nessun intervallo IP configurato: tutti gli IP sono consentiti." -heading = "Allowlist IP" -sub = "L'accesso API è limitato a questi intervalli CIDR." -subLocked = "Limita l'accesso API a intervalli IP noti: disponibile nei piani a pagamento." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Passa a Pro per limitare l'accesso API a reti specifiche." -title = "L'allowlist IP è una funzionalità a pagamento" - -[portal.infrastructure.security.ipColumns] -added = "Aggiunto" -addedBy = "Aggiunto da" -cidr = "CIDR" -label = "Etichetta" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmo" -heading = "Gestione delle chiavi di crittografia" -keyId = "Identificatore chiave" -lastRotated = "Ultima rotazione" -rotateKey = "Ruota chiave" -rotationPolicy = "Criterio di rotazione" -subheading = "Custodia delle chiavi che cifrano i documenti a riposo: chi può decifrare e come ruotano le chiavi." - -[portal.infrastructure.security.managedBanner] -description = "La custodia bring-your-own-key (BYOK) e hold-your-own-key (HYOK) è disponibile su Enterprise. Aggiorna per fornire chiavi dal tuo KMS." -title = "Le chiavi sono gestite da Stirling nel tuo piano" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacifico" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · limite dati GDPR" -label = "Unione Europea" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Stati Uniti" - -[portal.infrastructure.security.residencyHeader] -heading = "Residenza dei dati" -subheading = "Dove i documenti vengono archiviati ed elaborati." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} usato" - -[portal.infrastructure.storage.empty] -description = "Lo storage connesso e l'utilizzo appariranno qui." -title = "Nessuno storage configurato" - -[portal.infrastructure.storage.lifecycle] -active = "Attivo" -activeRange = "0–{{value}}d" -archived = "Archiviato" -coldStorage = "cold storage" -deleted = "Eliminato" -never = "mai" -purged = "eliminato definitivamente" - -[portal.infrastructure.storage.providers] -connect = "Connetti" -connected = "Connesso" -heading = "Provider connessi" -subheading = "Dove vengono scritti gli artefatti elaborati." - -[portal.infrastructure.storage.retention] -heading = "Conservazione" -subheading = "Per quanto tempo gli artefatti vengono conservati prima dell'eliminazione del ciclo di vita." -windowLabel = "Finestra di conservazione predefinita" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} giorno" -days_other = "{{count}} giorni" -never = "Non eliminare mai" - -[portal.infrastructure.storage.totalUsage] -heading = "Utilizzo totale" -progressLabel = "Storage utilizzato" -subheading = "Storage consumato su tutti i provider connessi." +[portal.infrastructure.keyLabel] +active = "Attiva" +revoked = "Revocata" [portal.infrastructure.tabs] apiKeys = "Chiavi API" @@ -7172,6 +7656,49 @@ models = "Modelli" security = "Sicurezza" storage = "Storage" +[portal.integrations] +availableHeading = "Disponibili" +comingSoonHeading = "In arrivo" +connect = "Connetti" +connectionCount_one = "{{count}} connessione" +connectionCount_other = "{{count}} connessioni" +customApi = "API personalizzata" +searchPlaceholder = "Cerca integrazioni" +subtitle = "Tutto ciò con cui Stirling può comunicare. Le credenziali vengono create una sola volta qui, poi referenziate da origini, policy e pipeline." +title = "Integrazioni" +typedName = "Nome {{tool}}" + +[portal.integrations.filters] +all = "Tutte" +audit = "Audit" +connected = "Connesse" +notify = "Notifica" +security = "Sicurezza" +signing = "Firma" +storage = "Archiviazione" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Connessa" + +[portal.integrations.table] +integration = "Integrazione" +worksWith = "Funziona con" + +[portal.integrations.worksWith] +pipelines = "Pipeline" +policies = "Policy" +sources = "Origini" + +[portal.legal] +draft = "{{label}} · bozza" +loadError = "Impossibile caricare questo documento. Riprova." +loading = "Caricamento…" +title = "Documento legale" + [portal.nav] agent-builder = "Agent Builder" components = "Componenti" @@ -7180,33 +7707,17 @@ documents = "Documenti" editor = "Editor" home = "Home" infrastructure = "Infrastruttura" +integrations = "Integrazioni" pipelines = "Pipeline" policies = "Criteri" procurement = "Approvvigionamento" -settings = "Impostazioni" sources = "Origini" usage = "Utilizzo e fatturazione" users = "Utenti" -[portal.notifications] -markAllRead = "Segna tutte come lette" -title = "Notifiche" -viewAll = "Visualizza tutte" - -[portal.notifications.ariaLabel] -none = "Notifiche, nessuna non letta" -unread_one = "Notifiche, {{count}} non letta" -unread_other = "Notifiche, {{count}} non lette" - -[portal.notifications.count] -allRead = "tutte lette" -loading = "caricamento" -new_one = "{{count}} nuova" -new_other = "{{count}} nuove" - -[portal.notifications.empty] -description = "Nessuna nuova notifica." -title = "Sei in pari" +[portal.nav.section] +platform = "Piattaforma PDF" +processor = "Processore PDF" [portal.pipelines] subtitle = "Ogni pipeline documentale automatizzata sul backend: una catena ordinata di operazioni su un insieme di origini, eseguita da un trigger. Fai clic su una riga per vederne passaggi e origini." @@ -7216,54 +7727,87 @@ title = "Pipeline" newPipeline = "Nuova pipeline" [portal.pipelines.builder] -addStep = "Aggiungi strumento" +activate = "Activate" back = "Torna alle pipeline" +cannotFollow = "Impossibile accettare {{produced}}" +chooseAccount = "Scegli un account" +chooseDestination = "Scegli una destinazione" +chooseOperation = "Scegli cosa fa questo passaggio" +chooseSource = "Scegli un'origine" discard = "Scarta modifiche" -enabled = "Abilitata" +inputs = "Input" +inputSource = "Origine input" +inputTrigger = "Trigger" keepEditing = "Continua a modificare" -needsUpload = "Richiede un file caricato" +moreActions = "More actions" +needsConfiguring = "Richiede configurazione" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nessuno strumento corrisponde alla tua ricerca." -pipelineSettings = "Impostazioni pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Cerca strumenti" -selectToolBody = "Aggiungi uno strumento per creare la tua pipeline." -selectToolTitle = "Ancora nessuno strumento" -toolSettings = "Impostazioni strumento" +sendToSystem = "Invia a un altro sistema" +stepsIncompatible = "Questi passaggi non possono essere eseguiti su ciò che produce il passaggio precedente: {{tools}}." +stepsNeedSetup = "Questi passaggi devono ancora essere configurati prima del salvataggio: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operazione non riconosciuta, mantenuta così com'è." unsavedBody = "Hai modifiche non salvate. Salvale prima di uscire o scartarle?" unsavedTitle = "Modifiche non salvate" -uploadUnsupported = "I file caricati non sono ancora supportati nelle pipeline, quindi questi passaggi non possono essere salvati: {{tools}}." usesDefaults = "Esegue con impostazioni predefinite" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combina ogni file del passaggio precedente" +fan-out = "Viene eseguito una volta per file dal passaggio precedente" +format-mismatch = "Richiede {{accepts}}, ma il passaggio precedente produce {{produced}}" +output-uncertain = "Potrebbe non essere eseguito: l'output del passaggio precedente dipende da come è configurato" +source-mismatch = "Richiede {{accepts}}, ma l'input di questa pipeline è {{produced}}" +undeclared-operation = "Impossibile verificare cosa accetta questo passaggio" [portal.pipelines.composer] addTool = "Aggiungi strumento" -cancel = "Annulla" -chainEmpty = "Aggiungi uno strumento per iniziare a creare la tua pipeline." create = "Crea pipeline" -directory = "Cartella di output" -directoryHelp = "Percorso assoluto sul server. Deve trovarsi nelle cartelle consentite configurate." +createPaused = "Create paused" editingUnsupported = "La visualizzazione di questi parametri dello strumento per la modifica non è ancora supportata." -moveDown = "Sposta giù" -moveUp = "Sposta su" +editSource = "Edit source" name = "Nome" namePlaceholder = "es. Scansione redazione" -noSources = "Nessuna origine ancora connessa. La pipeline può comunque essere eseguita sui file forniti direttamente." noToolSettings = "Questo strumento non ha impostazioni configurabili." -operations_one = "Operazione ({{count}})" -operations_other = "Operazioni ({{count}})" output = "Output" -removeStep = "Rimuovi operazione" save = "Salva modifiche" scheduleEvery = "Esegui ogni" -sources = "Origini" -sourcesLoading = "Caricamento origini..." trigger = "Trigger" triggerManual = "Solo manuale" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "giorni" hours = "ore" minutes = "minuti" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Eliminare \"{{name}}\"? Questa azione non può essere annullata." cancel = "Annulla" @@ -7271,28 +7815,62 @@ confirm = "Elimina" title = "Eliminare la pipeline?" [portal.pipelines.detail] +clearHistory = "Cancella cronologia" delete = "Elimina pipeline" run = "Esegui ora" [portal.pipelines.empty] action = "Crea una pipeline" +connectSource = "Connetti un'origine" description = "Crea la tua prima pipeline: scegli le origini su cui viene eseguita, concatena le operazioni e scegli dove inviare l'output." title = "Ancora nessuna pipeline" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Attive" paused = "In pausa" total = "Pipeline" -[portal.pipelines.output] -folder = "Scrivi nella cartella" -inline = "Restituisci file" - [portal.pipelines.run] +allProcessed_one = "Niente da eseguire: il {{count}} documento dell'origine è già stato elaborato." +allProcessed_other = "Niente da eseguire: tutti i {{count}} documenti nelle origini sono già stati elaborati." completed_one = "Esecuzione completata." completed_other = "Tutte le {{count}} esecuzioni completate." empty = "Niente da eseguire: le origini non avevano documenti da elaborare." failed = "Esecuzione non riuscita: {{error}}" +inFlight = "Niente di nuovo da eseguire: i documenti sono ancora in elaborazione da un'esecuzione precedente." +parked_one = "Niente da eseguire: {{count}} documento non è riuscito in precedenza ed è parcheggiato. Correggi la causa, poi cancella la cronologia per riprovarlo." +parked_other = "Niente da eseguire: {{count}} documenti non sono riusciti in precedenza e sono parcheggiati. Correggi la causa, poi cancella la cronologia per riprovarli." running = "Esecuzione avviata; ancora in corso." timeout = "L'esecuzione richiede più tempo del previsto; potrebbe comunque completarsi in background." @@ -7305,6 +7883,7 @@ name = "Pipeline" sources = "Origini" status = "Stato" steps = "Passaggi" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Monitoraggio cartella" @@ -7312,12 +7891,18 @@ manual = "Manuale" schedule = "Pianificato" [portal.policies] +defaultName = "Policy {{category}}" subtitle = "Automazioni permanenti che applicano una pipeline di strumenti a ogni documento. Ogni criterio si attiva al caricamento o all'esportazione, esegue la sua catena di strumenti e salva la versione applicata insieme all'originale." title = "Criteri" [portal.policies.card] comingSoon = "Passa a Enterprise" notSetUp = "Non configurato" +requiresAiEngine = "Richiede motore AI" + +[portal.policies.categories.classification] +desc = "Identifica il tipo di ogni documento al caricamento e tagga i suoi metadati per archiviazione e ricerca." +label = "Classificazione" [portal.policies.categories.compliance] desc = "Applica i requisiti HIPAA, GDPR, SOC 2 o FedRAMP a ogni documento." @@ -7342,6 +7927,13 @@ label = "Sicurezza" [portal.policies.config] scopeAll = "Tutti i documenti" +[portal.policies.config.classification] +summary = "Classifica ogni documento caricato rispetto alle etichette del tuo team e ne tagga i metadati." + +[portal.policies.config.classification.rules] +0 = "Classifica" +1 = "Tagga metadati" + [portal.policies.config.compliance] summary = "Convalida i documenti rispetto ai framework normativi prima che lascino il sistema." @@ -7369,6 +7961,29 @@ minConfidence = "Attendibilità minima" 2 = "Nomina" 3 = "Normalizza" +[portal.policies.config.purview] +label = "Applica un'etichetta di riservatezza Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant Purview" +labelId = "ID etichetta" +labelIdHelp = "Il GUID dell'etichetta, dal portale Purview. L'applicazione di un'etichetta scrive i suoi metadati sul PDF; non può crittografare, cosa che richiede il client Microsoft." +labelName = "Nome etichetta (opzionale)" +labelNameHelp = "Registrato insieme all'ID così persone e strumenti possono riconoscere l'etichetta." + +[portal.policies.config.purview.fields.method] +help = "Purview distingue un'etichetta applicata da una policy da una scelta da una persona." +label = "Come viene registrata l'etichetta" +privileged = "Come scelta da una persona" +standard = "Come applicata automaticamente" + +[portal.policies.config.purviewRead] +label = "Leggi un'etichetta Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant Purview" +connectionHelp = "Legge l'etichetta già presente sul documento, così i passaggi successivi possono agire su di essa." + [portal.policies.config.retention] summary = "Applica per quanto tempo vengono conservati i documenti, quando archiviarli e quando eliminarli." @@ -7436,6 +8051,7 @@ title = "Nessuna attività ancora" [portal.policies.endpoints] addWatermark = "Filigrana" autoRedact = "Oscura PII" +classifyAndLabel = "Classifica" compressPdf = "Comprimi" flatten = "Appiattisci" ocrPdf = "OCR" @@ -7446,6 +8062,201 @@ description = "I tuoi criteri sono salvati e appariranno quando la connessione s retry = "Riprova" title = "Backend non disponibile" +[portal.policies.operations] +change = "Modifica cosa fa questo passaggio" +noResults = "Nessun passaggio corrisponde. Prova un nome prodotto, oppure \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Cerca passaggi - prova \"scan\", \"attach\" o un nome prodotto" + +[portal.policies.operations.bodyMode] +binary = "Byte grezzi del file" +json = "All'interno di un payload JSON" +multipart = "Come caricamento file (multipart)" + +[portal.policies.operations.clamavScan] +description = "Il tuo server ClamAV controlla il documento; nulla lascia la tua rete." +label = "Scansiona alla ricerca di virus (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rifiuta documenti che contengono macro, eseguibili, script o oggetti incorporati." +label = "Blocca contenuti attivi" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive controlla il documento e l'esecuzione si interrompe se non è pulito." +label = "Scansiona alla ricerca di virus" + +[portal.policies.operations.confluenceAttach] +description = "Archivia il documento elaborato in una pagina." +label = "Allega a una pagina Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Invia il documento a ConsignO Cloud per la firma." +label = "Invia per la firma (ConsignO)" +note = "Invia il documento. Il recupero della copia firmata non è ancora supportato, perché un passaggio non può passare un risultato a un passaggio successivo." + +[portal.policies.operations.customApiCall] +description = "Crea tu la chiamata: percorso, metodo, body e gestione della risposta." +label = "Chiama una API personalizzata" + +[portal.policies.operations.discordNotify] +description = "Informa un canale che la policy ha gestito un documento." +label = "Pubblica un messaggio su Discord" + +[portal.policies.operations.elasticIndex] +description = "Registra ciò che ha fatto la policy come documento ricercabile." +label = "Indicizza un evento di audit in Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart è adatto alle API di upload; JSON è adatto alle API che vogliono il file dentro un payload; binary invia i byte grezzi." +label = "Come inviare il documento" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Un body JSON inviato così com'è. {{document.base64}} contiene il file; {{document.*}} e {{run.*}} vengono compilati per documento." +label = "Body JSON" + +[portal.policies.operations.fields.connection] +label = "Account" + +[portal.policies.operations.fields.domain] +label = "Dominio Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Il campo del modulo in cui l'API si aspetta il documento." +label = "Nome campo file" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Da" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Un oggetto JSON di coppie nome/valore degli header. La credenziale della connessione viene aggiunta per te." +label = "Header" + +[portal.policies.operations.fields.index] +label = "Indice" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Chiave issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Inviato come body del messaggio. {{document.*}} e {{run.*}} vengono compilati per documento." +label = "Messaggio" + +[portal.policies.operations.fields.method] +label = "Metodo" + +[portal.policies.operations.fields.pageId] +label = "ID pagina" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Aggiunto all'URL base della connessione. Non può mai raggiungere un altro host." +label = "Percorso" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Dove scriverlo nel drive. {{document.filename}} viene compilato per documento." +label = "Percorso di destinazione" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report lascia il documento invariato. Replace lo sostituisce con ciò che viene restituito." +label = "Cosa fare con la risposta" + +[portal.policies.operations.fields.signerEmail] +label = "Email del firmatario" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Oggetto" +placeholder = "Elaborato: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizza questo testo. Usa {{document.title}} o incolla il testo che vuoi controllare." +label = "Testo da controllare" + +[portal.policies.operations.fields.to] +label = "A" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nome utente Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informa uno spazio che la policy ha gestito un documento." +label = "Pubblica un messaggio su Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Archivia il documento elaborato in una issue." +label = "Allega a una issue Jira" + +[portal.policies.operations.mailgunEmail] +description = "Invia il documento elaborato come allegato." +label = "Invia il documento via Email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Scrive il documento elaborato in una cartella." +label = "Carica su Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Il tuo server Presidio segnala i dati personali che trova." +label = "Rileva dati personali" +note = "Presidio legge testo, non file. Fornisci il testo che vuoi controllare — il documento stesso non viene caricato." + +[portal.policies.operations.responseMode] +replace = "Sostituisci il documento con la risposta" +report = "Mantieni il documento, registra la risposta" + +[portal.policies.operations.sendgridEmail] +description = "Invia il documento elaborato come allegato." +label = "Invia il documento via Email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informa un canale che la policy ha gestito un documento." +label = "Pubblica un messaggio su Slack" + +[portal.policies.operations.splunkEvent] +description = "Registra ciò che ha fatto la policy, dove i tuoi auditor già controllano." +label = "Invia un evento di audit a Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registra ciò che ha fatto la policy in un collector HTTP." +label = "Invia un evento di audit a Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informa un canale che la policy ha gestito un documento." +label = "Pubblica un messaggio su Teams" + +[portal.policies.operations.webhookPost] +description = "Invia il documento stesso a un URL che scegli." +label = "Pubblica il documento su un webhook" + +[portal.policies.operations.zapierNotify] +description = "Passa i dettagli dell'esecuzione alla tua automazione." +label = "Attiva uno scenario Zap o Make" + [portal.policies.stats] activeFor = "Attivo" dataProcessed = "Dati elaborati" @@ -7471,6 +8282,13 @@ label = "Doc applicati" description = "Configurati ma non attivi" label = "In pausa" +[portal.policies.table] +appliesTo = "Si applica a" +docs = "Doc applicati" +enforces = "Applica" +policy = "Policy" +status = "Stato" + [portal.policies.wizard.actions] back = "Indietro" cancel = "Annulla" @@ -7478,6 +8296,10 @@ continue = "Continua" enablePolicy = "Abilita criterio" saveChanges = "Salva modifiche" +[portal.policies.wizard.capability.classify] +desc = "Identifica il tipo del documento dalle etichette del tuo team e lo tagga, così viene archiviato e cercato per categoria." +label = "Classifica il documento" + [portal.policies.wizard.capability.compress] desc = "Comprime il documento riducendo la dimensione del file." label = "Riduci dimensione file" @@ -7498,10 +8320,18 @@ label = "Oscura informazioni sensibili" desc = "Rimuove JavaScript nascosto in modo che nulla possa essere eseguito automaticamente all'apertura del documento." label = "Rimuovi contenuto attivo" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dimostra che il documento esisteva in questa forma esatta in un dato momento, usando un'autorità di marcatura temporale indipendente. Viene inviato solo un hash - il documento non lascia mai il tuo server." +label = "Aggiungi una marca temporale attendibile" + [portal.policies.wizard.capability.watermark] desc = "Applica un segno visibile (es. “Riservato”) su ogni pagina." label = "Applica una filigrana" +[portal.policies.wizard.classification] +description = "Ogni documento caricato viene classificato rispetto alle etichette integrate e taggato con i tipi adatti. Il set di etichette è condiviso in tutto il team." +labelsHeading = "Etichette di classificazione" + [portal.policies.wizard.errors] noTools = "Abilita prima almeno uno strumento nel workflow." saveFailed = "Impossibile salvare il criterio. Riprova." @@ -7546,73 +8376,118 @@ setUp = "Configura criterio {{category}}" [portal.policies.wizard.workflow] description = "Scegli cosa fa questo criterio a ogni documento che elabora." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} attivi" -noRule = "Nessuna regola applicata ancora" -subtitle = "Automazioni permanenti attraverso cui passa ogni documento, indipendentemente dalla pipeline che lo gestisce." -title = "Cosa viene eseguito sui tuoi PDF" - [portal.policySummary.action] -comingSoon = "Passa a Enterprise" -configure = "Configura" setUp = "Configura" -[portal.policySummary.column] -activeRule = "Regola attiva" -policy = "Criterio" -status = "Stato" +[portal.processorFlow] +footnote = "I conteggi riguardano le ultime 24 ore. La velocità del flusso è illustrativa." +liveBadge = "Live" +stats = "{{connected}} connessi · {{processed}} PDF elaborati" +title = "Processore PDF" -[portal.policySummary.empty] -description = "Una volta configurati i criteri, le categorie appariranno qui." -title = "Nessun criterio ancora" +[portal.processorFlow.lens] +ariaLabel = "Modalità di visualizzazione" +flow = "Flusso" +sankey = "Sankey" -[portal.policySummary.state] -active = "Attivo" -off = "Disattivato" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Non riusciti" +heading = "Risultati" +success = "Consegnati" + +[portal.processorFlow.policies] +activeCount = "{{n}} attive" +count = "{{n}} · 24h" +heading = "Policy" +setUp = "Configura" soon = "Presto" -[portal.processingStatus] -managePlan = "Gestisci piano" -volumeSuffix = "PDF elaborati · ultimi 30 giorni" +[portal.processorFlow.sankey] +waist = "Policy · {{n}} attive" + +[portal.processorFlow.sankey.empty] +description = "Connetti un'origine e attiva una policy per osservare i documenti scorrere nel processore." +title = "Ancora nessun flusso" + +[portal.processorFlow.sources] +comingSoonTag = "Connetti" +editor = "Editor Stirling PDF" +heading = "Origini" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Acquisizione Email" [portal.procurement] -reset = "Reimposta procurement (demo)" subtitle = "Fai valutare, contrattualizzare e avviare il tuo team. Ogni documento in un unico posto." title = "Procurement" -[portal.procurement.action] -download = "Download" -pay = "Paga ora" -request = "Richiedi" -sign = "Rivedi e firma" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Accetta e abbonati" +confidential = "Riservato" confirm = "Ho letto e accetto lo Stirling Enterprise Agreement." -eyebrow = "Accordo" -intro = "Un unico accordo combinato copre la tua offerta: Master Service Agreement, Order Form, EULA e Data Processing Agreement. Rivedilo, quindi accetta per trasformare il preventivo in un abbonamento vincolante." -title = "Rivedi il tuo accordo enterprise" +docName = "Contratto Stirling Enterprise" +docSub = "MSA, Order Form, EULA e DPA, combinati in un'unica firma." +download = "Scarica" +downloadDraftError = "Impossibile generare il PDF del contratto - il renderer del documento non è disponibile. Riprova tra poco o contatta il supporto." +downloadError = "Impossibile scaricare il contratto firmato. Riprova." +legalName = "Nome entità legale" +legalNamePlaceholder = "L'entità legale che firmerà" +loadError = "Impossibile caricare il contratto. Riprova." +loading = "Caricamento del contratto..." +ref = "Rif {{ref}} · {{version}}" +requestChanges = "Richiedi modifiche" +scrollHint = "Scorri per rivedere" +signatory = "Nome firmatario" +signatoryPlaceholder = "Nome completo" +signatoryTitle = "Titolo del firmatario" +signatoryTitlePlaceholder = "es. Responsabile legale" +signError = "Impossibile registrare la tua firma. Riprova." [portal.procurement.builder] addons = "Componenti aggiuntivi" +addressLine1 = "Indirizzo riga 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Indirizzo riga 2" +addressLine2Placeholder = "Suite, piano, edificio" back = "Indietro" businessName = "Nome azienda" businessNamePlaceholder = "La tua azienda" +city = "Città" +cityPlaceholder = "San Francisco" +completeRequired = "Completa i campi obbligatori (contrassegnati con *) con un'email valida prima di generare il preventivo." +contactEmail = "Email contatto" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nome contatto" +contactNamePlaceholder = "Jane Doe" continue = "Continua" -country = "Paese" -countryEuro = "Eurozona (EUR €)" -countryUK = "Regno Unito (GBP £)" -countryUS = "Stati Uniti (USD $)" +done = "Fatto" eula = "Ho letto e accetto la Stirling Enterprise EULA. Regola l'accordo generato da questo preventivo." generate = "Genera preventivo" -included = "Incluso" indemnification = "Indennizzo IP" indemnificationSub = "Difendiamo le richieste IP idonee, secondo la EULA" -offlineLicense = "Licenza offline / air-gapped" -offlineLicenseSub = "Un file di licenza scaricabile per un'istanza self-hosted air-gapped" +paperEyebrow = "Preventivo Enterprise" +paperFor = "Preparato per" +pdfSize = "Dimensione PDF" +poNumber = "Numero PO" +poNumberPlaceholder = "Opzionale" +postalCode = "CAP" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} policy" +posture_essentials = "Essentials" +posture_essentialsSub = "Classificazione + Condivisione — i valori predefiniti" +posture_governed = "Governed" +posture_governedSub = "Aggiunge Sicurezza e Routing" +posture_regulated = "Regulated" +posture_regulatedSub = "Ogni categoria — Conformità, Conservazione, Ingestione" qbr = "Revisioni aziendali trimestrali" qbrSub = "Il tuo SE rivede utilizzo e roadmap ogni trimestre" +region = "Stato / regione" +regionPlaceholder = "California" running = "{{annual}} / anno · {{years}} anni {{tcv}}" s1Sub = "Il tuo team e i PDF che prevedi di elaborare ogni anno." s1Title = "Quanto elaborerai?" @@ -7621,6 +8496,12 @@ s2Title = "Impegno e servizio" s3Sub = "Per il preventivo e l'accordo che genera." s3Title = "I tuoi dettagli" serviceLevel = "Livello di servizio" +size_compact = "Compatto" +size_compactSub = "Prevalentemente testo, sotto 1 MB" +size_heavy = "Pesante" +size_heavySub = "Scansionato o ricco di immagini, 5 MB+" +size_standard = "Standard" +size_standardSub = "Testo e immagini misti, da 1 a 5 MB" slDedicated = "Dedicato" slDedicatedSub = "4 ore lavorative · account manager dedicato · +30%" slPriority = "Prioritario" @@ -7628,6 +8509,8 @@ slPrioritySub = "Stesso giorno lavorativo · CSM nominato · +15%" slStandard = "Standard" slStandardSub = "Giorno lavorativo successivo · CSM condiviso · incluso" stepOf = "Passaggio {{n}} di {{total}}" +taxId = "Partita IVA / Codice fiscale" +taxIdPlaceholder = "Opzionale" term = "Durata" termDiscount = "Sconto del {{pct}}% per impegno pluriennale applicato" title = "Crea il tuo preventivo" @@ -7635,6 +8518,7 @@ training = "Onboarding e formazione" trainingSub = "Sessioni live per far partire il tuo team" users = "Utenti totali" usersPlaceholder = "es. 250" +viewEula = "Leggilo" volEstimated = "Stimato da {{count}} utenti (~2.000 PDF ciascuno, inclusa l'automazione). Modifica se hai dati più precisi." volManual = "Stiamo usando la tua cifra. Puoi stimare di nuovo in base alla dimensione del team in qualsiasi momento." volNoUsers = "Non sei sicuro? Inserisci la dimensione del team e la stimeremo." @@ -7643,55 +8527,55 @@ volumePlaceholder = "es. 1.000.000" years_one = "{{count}} anno" years_other = "{{count}} anni" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} doc" -done = "Fatto" -here = "Sei qui" -hide = "Nascondi" -optional = "Facoltativo" -paidAddon = "Componente aggiuntivo a pagamento" -show = "Mostra" -subtitle = "Tutto ciò di cui hai bisogno in ogni passaggio del percorso, mostrato man mano che l'offerta avanza." -supportingSubtitle = "SOC 2, revisioni di sicurezza, moduli fiscali e altro, pronti quando il tuo team sicurezza o procurement li richiede. Alcuni comportano una tariffa una tantum." -supportingTitle = "Supporto alla tua valutazione" +[portal.procurement.documents] +agreement = "Contratto Enterprise" +agreementSub = "MSA, Order Form e DPA in un'unica firma." +download = "Scarica" +eula = "EULA e termini commerciali" +eulaSub = "Il software e i termini commerciali self-service." +invoice = "Fattura" +invoiceSub = "La fattura del tuo primo abbonamento." +laterInvoice = "Disponibile dopo l'accettazione" +laterQuote = "Disponibile una volta emesso il preventivo" +quote = "Preventivo" +quoteSub = "Il tuo preventivo Enterprise dettagliato." +sla = "Allegato SLA" +slaSub = "Livelli di supporto e obiettivi di risposta." +subprocessors = "Sub-responsabili" +subprocessorsSub = "Terze parti che elaborano dati per tuo conto." +subtitle = "La documentazione del tuo accordo, disponibile in qualsiasi momento." title = "Documenti" -upcoming = "In arrivo" +view = "Visualizza" [portal.procurement.error] title = "Qualcosa è andato storto" [portal.procurement.hero] -company = "La tua offerta enterprise" +barAria = "Fase {{current}} di {{total}}" ctaAgreement = "Rivedi e firma l'accordo" -ctaLive = "Sei live" -ctaPayment = "Aggiungi pagamento" +ctaExploring = "Configura la tua prova" ctaQuote = "Rivedi il preventivo" +ctaReviewQuote = "Rivedi preventivo" ctaTrial = "Crea il preventivo" +documents = "Documenti" eyebrow = "Procurement enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invita compagni di team" -keyDocs = "Documenti chiave" -nextStep = "Passaggio successivo: {{action}}" -notStarted = "Non iniziato" +licenseKey = "Chiave di licenza" +liveSub = "La tua organizzazione è stata predisposta e la tua licenza è attiva." +liveTitle = "Sei live su Stirling Enterprise" +next = "Successivo: {{stage}}" open = "Apri procurement" scheduleCall = "Pianifica una chiamata" -setup1Sub = "Invita i tuoi compagni di team" -setup1Title = "Distribuisci il PDF Editor" -setup2Sub = "Attiva l'elaborazione su editor e altre origini" -setup2Title = "Collega il PDF Processor" -setup3Sub = "Attiva Sicurezza, Conformità, Instradamento o Conservazione quando ne hai bisogno" -setup3Title = "Aggiungi criteri consigliati" +sentenceAgreement = "rivedere e firmare il contratto" +sentenceLive = "provisioning e rollout" +sentencePayment = "ordine di acquisto e pagamento" +sentenceQuote = "rivedere e accettare il preventivo" +sentenceTrial = "configurare il tuo workspace di prova" [portal.procurement.journey] daysLeft_one = "{{count}} giorno rimasto" daysLeft_other = "{{count}} giorni rimasti" -engineerLabel = "Il tuo solutions engineer" -eyebrow = "Il tuo rollout" -live = "Sei live su Stirling Enterprise" -nextStep = "Passaggio successivo: {{action}}" -subtitle = "Il tuo solutions engineer ti accompagna in ogni passaggio. Un'azione successiva alla volta; la checklist completa è sotto." -title = "Dalla trial al live, un percorso guidato" -trialTitle = "Trial Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Una firma copre MSA, order form, EULA e DPA." @@ -7725,6 +8609,9 @@ downloadError = "Impossibile generare il file di licenza offline per il momento downloadOffline = "Download licenza offline (.lic)" hint = "Incolla questa chiave in un'istanza self-hosted per attivarla, oppure conservala per i tuoi registri. Tienila al sicuro." label = "La tua chiave di licenza" +subtitle = "Attiva un'istanza self-hosted con questa chiave, oppure conservala per i tuoi archivi." +title = "La tua chiave di licenza" +trialFileHint = "Questo è il tuo file di licenza di prova. Una volta concluso il contratto, torna e scaricalo di nuovo — a differenza della chiave di licenza online, il file .lic non si aggiorna da solo." [portal.procurement.link] cta = "Collega account" @@ -7737,65 +8624,62 @@ description = "Il tuo abbonamento è attivo e la tua licenza è stata emessa. Il eyebrow = "Live" title = "Sei live su Stirling Enterprise" -[portal.procurement.locked] -description = "Chiavi trial, preventivi a volume impegnato, accordo con firma unica, pagamento e registro documenti saranno tutti qui quando avvii una valutazione enterprise." -eyebrow = "Solo Enterprise" -talkToSales = "Parla con le vendite" -title = "Il percorso procurement si apre con Enterprise" - [portal.procurement.milestone] -accept = "Accetta e continua" -description = "Scarica il PDF per condividerlo con il tuo team, torna ad accettarlo quando sei pronto oppure apporta modifiche." -download = "Download PDF" downloadError = "Impossibile scaricare il PDF del preventivo per il momento — riprova tra poco." -edit = "Modifica preventivo" -eyebrow = "Preventivo {{number}}" -perYear = " / anno" -preparedFor = "Preparato per {{company}}" -tcv = "{{value}} valore totale del contratto" -title = "Il tuo preventivo è pronto" - -[portal.procurement.modal] -cancel = "Annulla" -chooseFile = "Scegli file" -close = "Chiudi" -downloadBody = "Il download inizierà a breve." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Nessun file selezionato" -payBody = "Paga il contratto impegnato con carta o bonifico bancario tramite Stripe. Il provisioning del tuo workspace viene effettuato appena il pagamento viene confermato." -payCta = "Continua su Stripe" -payTitle = "Conferma pagamento" -requestBodyFree = "Lo generiamo su richiesta. Conferma e il tuo solutions engineer lo invierà a breve." -requestBodyPaid = "Questo è un componente aggiuntivo a pagamento. Conferma e il tuo solutions engineer ne definirà l'ambito e invierà la documentazione." -requestCta = "Richiedi" -requestTitle = "Richiedi questo documento" -signBody = "Apre lo Stirling Enterprise Agreement per la firma elettronica: una firma copre MSA, order form, EULA e DPA. Controfirmiamo automaticamente e passi al pagamento." -signCta = "Apri per la firma" -signTitle = "Rivedi e firma il tuo accordo" -uploadBody = "Inviaci il tuo PO e fattureremo in base ai tuoi termini. Trascina il PDF o scegli un file qui sotto." -uploadCta = "Upload ordine di acquisto" -uploadTitle = "Upload del tuo ordine di acquisto" [portal.procurement.payment] description = "Il tuo preventivo è accettato e la tua licenza è già attiva — il tuo team può iniziare subito. Paga la prima fattura quando sei pronto; puoi pagarla o scaricarla qui, senza bisogno di email." +downloadAgreement = "Scarica contratto firmato" downloadInvoice = "Download fattura" -simulate = "Simula pagamento ricevuto (demo)" +eyebrow = "Pagamento" title = "Abbonamento creato" viewInvoice = "Visualizza e paga fattura" +[portal.procurement.review] +acceptCta = "Accetta preventivo" +annual = "Canone annuale (anno 1)" +downloadCta = "Scarica preventivo" +poNumber = "Ordine di acquisto: {{po}}" +renewal = "Si rinnova a {{amount}}/anno dopo il termine (+{{pct}}% CPI)" +tcv = "Termine di {{years}} anni, pagato interamente in anticipo: {{tcv}}" +validUntil = "Valido fino al {{date}}" + [portal.procurement.schedule] fallback = "Impossibile caricare il pianificatore." fallbackLink = "Apri la pianificazione in una nuova scheda" +loading = "Caricamento scheduler..." subtitle = "Il tuo solutions engineer guiderà il team nel rollout. Scegli un orario adatto a te." title = "Pianifica una chiamata" -[portal.procurement.status] -action = "Azione richiesta" -available = "Disponibile" -complete = "Completato" -pending = "In sospeso" -request = "Su richiesta" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sigillato, nessuna uscita" +back = "Indietro" +businessName = "Nome azienda" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gestito, avvio più rapido" +continue = "Continua" +deployment = "Dove deve essere distribuito?" +eula = "Accetto la Stirling EULA e i termini commerciali." +fullName = "Nome completo" +fullNamePlaceholder = "Il tuo nome" +invites = "Invita compagni di team · opzionale" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Pianifica una chiamata" +seats = "Persone che testano" +seatsPlaceholder = "es. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker o Kubernetes" +start = "Avvia prova" +stepOf = "Passaggio {{n}} di {{total}}" +subtitle = "Gratis per 14 giorni, nessuna carta richiesta." +subtitleDetails = "Alcuni dettagli per il tuo preventivo e contratto." +talkFirst = "Preferisci parlarne prima?" +title = "Configura la tua prova" +viewEula = "Leggilo" +workEmail = "Email di lavoro" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "L'estensione aggiunge 7 giorni e avvisa il tuo solutions engineer." @@ -7806,109 +8690,113 @@ maxed = "Limite raggiunto" subtitle = "La tua trial gratuita dura fino al {{date}}. Nessuna carta richiesta." title = "Trial Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Prezzi a volume impegnato, SSO + SCIM + RBAC a livello di organizzazione, audit immutabile di 90 giorni e un SE dedicato." -homeCta = "Avvia Trial →" -homeHeadline = "Elabora milioni di PDF." - -[portal.recentActivity] -title = "Attività recente" -viewAll = "Visualizza tutto" - -[portal.recentActivity.empty] -description = "Esecuzioni pipeline, deploy ed eventi agente appariranno qui." -title = "Ancora nulla qui" - -[portal.search] -ariaLabel = "Cerca" -placeholder = "Cerca in Stirling — endpoint, pipeline, doc…" - -[portal.search.empty] -noActionsDescription = "Le azioni rapide appariranno qui quando saranno disponibili." -noActionsTitle = "Nessuna azione rapida" -noMatches = "Nessuna corrispondenza per \"{{query}}\"" -noMatchesDescription = "Prova una parola chiave diversa o sfoglia il catalogo." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Collegamento account" -[portal.shell.header] -accountFallback = "Account" -accountMenu = "Menu account" -darkMode = "Modalità scura" -lightMode = "Modalità chiara" -search = "Cerca" -searchPlaceholder = "Cerca…" -signOut = "Esci" -switchToDark = "Passa al tema scuro" -switchToLight = "Passa al tema chiaro" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} doc" linkAccount = "Collega account Stirling" -planEditor = "Piano Editor" -planEnterprise = "Piano Enterprise" -planProcessor = "Piano Processor" primaryNav = "Navigazione principale" switchApp = "Cambia app" +[portal.shell.topbar] +closeNav = "Chiudi navigazione" +openNav = "Apri navigazione" +search = "Cerca" + [portal.sources] subtitle = "Connessioni di input riutilizzabili che alimentano documenti in Stirling. Configura una connessione una volta, quindi referenziala da qualsiasi numero di criteri. Fai clic su una riga per la sua configurazione e per vedere quali criteri la usano." title = "Origini" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Collega origine" +[portal.sources.builder] +back = "Torna alle origini" +backToTypes = "Tutti i tipi di origine" +cancel = "Annulla" +chooseHint = "Scegli da dove provengono i documenti. I connettori disattivati sono in arrivo." +comingSoon = "In arrivo" +comingSoonHeading = "In arrivo" +create = "Crea origine" +createTitle = "Connetti un'origine" +delete = "Elimina" +editTitle = "Modifica origine" +enabled = "Abilitata" +save = "Salva modifiche" + +[portal.sources.builder.folderAccess] +description = "Le automazioni delle cartelle possono usare solo cartelle consentite da un amministratore. Aggiungila nelle impostazioni di Accesso cartella, poi riprova." +openSettings = "Impostazioni Accesso cartella" +title = "Questa cartella non è consentita" + [portal.sources.delete] body = "Eliminare \"{{name}}\"? Questa operazione non può essere annullata. I criteri che la referenziano dovranno essere aggiornati." cancel = "Annulla" confirm = "Elimina" title = "Eliminare origine?" -[portal.sources.detail] -closeAriaLabel = "Chiudi dettaglio" -delete = "Elimina origine" -docs24h = "Ultime 24h" -docs30d = "Ultimi 30 giorni" -docsTotal = "Totale rilevati" -docsTrend = "Documenti negli ultimi 30 giorni" -documents = "Documenti" -edit = "Modifica" -notReferenced = "Non referenziata da alcun criterio, quindi è sicura da eliminare." -pause = "Metti in pausa" -resume = "Riprendi" -subtitle = "{{type}} · {{status}}" -usedBy = "Usata da" - -[portal.sources.empty] -description = "Collega una cartella (e, presto, cloud storage) così i tuoi criteri avranno un posto da cui prelevare documenti." -title = "Nessuna origine collegata ancora" - [portal.sources.kpi] inUse = "In uso" total = "Connessioni" unused = "Non usate" +[portal.sources.networkFields.connection] +helperText = "La connessione salvata con indirizzo del server e credenziali. Riutilizzata da ogni origine che la referenzia." +label = "Connessione" + +[portal.sources.networkFields.directory] +helperText = "Cartella sul server da monitorare, relativa alla home di accesso o alla root della condivisione. Lascia vuoto per la root." +label = "Cartella" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume rimuove ogni file dal server una volta che ogni policy lo ha elaborato." +label = "Modalità di lettura" + +[portal.sources.networkFields.mode.options] +consume = "Consume: elabora ogni file una volta" +snapshot = "Snapshot: rileggi la cartella a ogni esecuzione" + +[portal.sources.networkFields.recursive] +label = "Profondità cartella" + +[portal.sources.networkFields.recursive.options] +all = "Includi sottocartelle" +top = "Solo livello superiore" + [portal.sources.status] active = "Attiva" disabled = "Disabilitata" unused = "Non usata" [portal.sources.table] +documents = "Documenti" source = "Origine" status = "Stato" +type = "Type" usedBy = "Criteri" +[portal.sources.types.box] +description = "Recupera documenti dalle cartelle Box che il tuo team condivide già." +label = "Box" + +[portal.sources.types.dropbox] +description = "Recupera documenti da una cartella Dropbox condivisa." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documenti che il tuo team ha elaborato nell'editor, attraverso esecuzioni di policy e AI." label = "Editor" +noPolicies = "Nessuna policy ancora eseguita dall'editor." + +[portal.sources.types.email] +description = "Trasforma gli allegati inviati a una casella dedicata in documenti." +label = "Casella Email" [portal.sources.types.folder] description = "Monitora una directory sul server per nuovi documenti." @@ -7941,25 +8829,80 @@ label = "Profondità cartella" all = "Includi sottocartelle" top = "Solo livello superiore" +[portal.sources.types.ftp] +description = "Monitora una cartella di server FTP o FTPS per nuovi documenti." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Recupera documenti da cartelle Google Drive condivise." +label = "Google Drive" + +[portal.sources.types.network] +description = "Monitora una condivisione di rete SMB per nuovi documenti." +label = "Drive di rete" + +[portal.sources.types.onedrive] +description = "Monitora cartelle OneDrive per nuovi documenti." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Recupera documenti da un bucket Amazon S3 o compatibile con S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "La connessione salvata che contiene bucket e credenziali. Riutilizzata da ogni origine e output di pipeline che la referenzia." +label = "Connessione" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume rimuove ogni oggetto dal bucket una volta che ogni policy lo ha elaborato." +label = "Modalità di lettura" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: elabora ogni oggetto una volta" +snapshot = "Snapshot: rileggi il bucket a ogni esecuzione" + +[portal.sources.types.s3.fields.prefix] +helperText = "Vengono elaborati solo gli oggetti le cui chiavi iniziano con questo prefisso." +label = "Prefisso chiave" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Monitora una cartella di drop SFTP per nuovi documenti." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Recupera documenti dalle librerie documenti SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Origine" +[portal.sources.types.webhook] +createNote = "Un URL di consegna e un secret di firma vengono generati quando crei questo webhook. I mittenti inviano documenti con POST a quell'URL e le policy che lo referenziano vengono eseguite all'arrivo." +description = "Ricevi documenti tramite HTTP POST firmato da qualsiasi sistema esterno." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL di consegna" +secretNote = "Il secret di firma viene mostrato una volta, quando il webhook viene creato. Ricrea l'origine per ruotarlo." + +[portal.sources.types.webhook.reveal] +copy = "Copia" +done = "Fatto" +secret = "Secret di firma" +secretHelp = "Firma il body grezzo di ogni consegna con questa chiave (HMAC-SHA256) e invialo come header X-Stirling-Signature." +secretWarning = "Copia ora il secret di firma. Per la tua sicurezza viene mostrato solo una volta e non può essere recuperato in seguito." +title = "Webhook creato" +url = "URL di consegna" +usage = "Invia ogni documento con POST come body grezzo della richiesta all'URL di consegna con un tipo di contenuto binario (application/pdf o application/octet-stream). Le policy che lo referenziano vengono eseguite automaticamente all'arrivo." + [portal.sources.wizard] -back = "Indietro" -cancel = "Annulla" -continue = "Continua" -editTitle = "Modifica origine" name = "Nome" namePlaceholder = "es. Acquisizione reclami" -save = "Salva modifiche" -subtitle = "Passaggio {{current}} di {{total}} · {{label}}" -title = "Collega un'origine" type = "Tipo" -[portal.sources.wizard.steps] -chooseType = "Scegli tipo" -configure = "Configura" -review = "Rivedi e collega" +[portal.stepModal] +close = "Chiudi" [portal.usage] managePayment = "Gestisci pagamento" @@ -8019,16 +8962,9 @@ summary = "Possiede un team — gestisce le risorse dei membri e le configurazio 2 = "Accesso al portale tramite il criterio predefinito" 3 = "Tutto ciò che può fare un membro" -[portal.welcome] -ariaLabel = "Benvenuto in Stirling PDF" -badge = "Open-source" -installEditor = "Installa l'Editor" -inviteTeammates = "Invita compagni di team" -openInBrowser = "Apri nel browser" -perks = "Gratis per sempre · Self-hostable" -subtitle = "Il PDF Editor più sicuro al mondo è gratuito per team di ogni dimensione. Include oltre 60 operazioni PDF e SSO." -title = "Benvenuto in" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Illimitati" [printFile] title = "Stampa file" @@ -8374,7 +9310,7 @@ title = "Informazioni su Rimuovi annotazioni" [removeBlanks] submit = "Rimuovi" -tags = "delete,clean,empty,remove blank,delete blank pages,empty pages,white pages,remove empty,clean up,cleanup blank" +tags = "eliminare,pulire,svuotare,rimuovere pagine bianche,eliminare le pagine vuote,pagine vuote,pagine bianche,rimuovere il vuoto,pulire,pulire bianche" title = "Rimuovi spazi vuoti" [removeBlanks.error] @@ -8611,6 +9547,14 @@ text = "Ruota le pagine del tuo PDF in incrementi di 90 gradi in senso orario o [rotate.tooltip.header] title = "Panoramica impostazioni Rotazione" +[ruler] +clearAll = "Cancella tutto" +hideAllLabels = "Nascondi tutte le etichette" +hideSmallLabels = "Nascondi etichette piccole" +physicalValues = "Fisico" +scaled = "In scala" +showAllLabels = "Mostra tutte le etichette" + [sanitize] desc = "Rimuovi elementi potenzialmente dannosi dai PDF." sanitizationResults = "Risultati sanitizzazione" @@ -8652,31 +9596,45 @@ label = "Rimuovi metadati XMP" [sanitize.steps] settings = "Impostazioni" -[sanitizePdf] -tags = "pulire,proteggere,rimuovere le minacce" - [sanitizePDF] title = "Pulire PDF" +[sanitizePdf] +tags = "pulire,proteggere,rimuovere le minacce" + [scalePages] tags = "ridimensiona,regola,scala,dimensioni pagina,ridimensiona pagina,scala pagina,cambia dimensione,regola dimensione,ingrandisci,riduci pagina,adatta alla pagina,A4,formato lettera" title = "Regola la scala della pagina" +[scaleSettings] +activeScale = "Scala attiva" +apply = "Applica scala" +applyCalibration = "Applica calibrazione" +calculatedScale = "Scala calcolata" +calibrate = "Calibra" +calibrating = "Calibrazione in corso" +calibrationDistanceRequired = "Inserisci una distanza reale maggiore di zero" +calibrationInvalid = "Impossibile calcolare la scala da questa misurazione" +calibrationPaperDistance = "Distanza misurata sulla pagina: {{distance}}" +calibrationTitle = "Calibra scala" +calibrationTooltip = "Misura una distanza nota per calcolare automaticamente la scala" +customScale = "Scala personalizzata" +noneSet = "Nessuna scala personalizzata impostata" +presets = "Scale preimpostate" +ratio = "Rapporto di scala" +ratioHelp = "Rapporto: 1 unità della pagina = X unità reali" +ratioPlaceholder = "es. 100" +ratioPositive = "Il rapporto deve essere maggiore di zero" +ratioRequired = "Il rapporto è obbligatorio" +realDistance = "Distanza reale" +realDistancePlaceholder = "es. 5" +reset = "Ripristina impostazioni predefinite" +unit = "Unità" +unitInvalid = "Unità non valida: {{scaleUnit}}" + [scannerEffect] tags = "scansione,simulazione,creazione,scansione falsa,aspetto scansionato,effetto scanner,far sembrare scansionato,effetto fotocopia,simulazione scanner,scansione realistica" -[ScannerImageSplit.selectText] -1 = "Soglia angolo:" -10 = "Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1)." -2 = "Imposta il minimo angolo richiesto perché l'immagine venga ruotata (predefinito: 10)." -3 = "Tolleranza:" -4 = "Imposta lo spettro di colori attorno al colore di sfondo stimato (predefinito: 30)." -5 = "Area minima:" -6 = "Imposta l'area minima di una foto (default: 10000)." -7 = "Area di contorno minima:" -8 = "Imposta l'area minima del contorno di una foto" -9 = "Spessore bordo:" - [scannerImageSplit] submit = "Estrai scansioni di immagini" tags = "separare,rilevamento automatico,scansioni,multifoto,organizzare" @@ -8708,6 +9666,18 @@ whatThisDoes = "Cosa fa" whatThisDoesDesc = "Trova ed estrae automaticamente ogni foto da una pagina scansionata o da un'immagine composita - senza ritaglio manuale." whenToUse = "Quando usare" +[ScannerImageSplit.selectText] +1 = "Soglia angolo:" +10 = "Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1)." +2 = "Imposta il minimo angolo richiesto perché l'immagine venga ruotata (predefinito: 10)." +3 = "Tolleranza:" +4 = "Imposta lo spettro di colori attorno al colore di sfondo stimato (predefinito: 30)." +5 = "Area minima:" +6 = "Imposta l'area minima di una foto (default: 10000)." +7 = "Area di contorno minima:" +8 = "Imposta l'area minima del contorno di una foto" +9 = "Spessore bordo:" + [search] noResults = "Nessun risultato trovato" placeholder = "Inserisci termine di ricerca..." @@ -8725,46 +9695,22 @@ toolNotAvailableLocally = "Il tuo server Stirling-PDF è offline e \"{{endpoint} [session] expired = "La tua sessione è scaduta. Aggiorna la pagina e riprova." -[sessionManagement.tooltip] -header = "Gestione delle sessioni di firma" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "I nuovi partecipanti vengono aggiunti alla fine dell'ordine di firma" -bullet2 = "Non è possibile aggiungere partecipanti dopo la finalizzazione della sessione" -bullet3 = "Ogni partecipante riceve una notifica quando è il suo turno" -description = "Puoi aggiungere altri partecipanti a una sessione attiva in qualsiasi momento prima della finalizzazione." -title = "Aggiunta dei partecipanti" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalizzazione completa: tutti i partecipanti hanno firmato" -bullet2 = "Finalizzazione parziale: alcuni partecipanti non hanno ancora firmato" -bullet3 = "I partecipanti senza firma saranno esclusi dal documento finale" -bullet4 = "Una volta finalizzata, puoi caricare il PDF firmato nei file attivi" -description = "La finalizzazione combina tutte le firme in un unico PDF firmato. Questa azione non può essere annullata." -title = "Finalizzazione della sessione" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Non è possibile rimuovere i partecipanti che hanno già firmato" -bullet2 = "I partecipanti rimossi non riceveranno più notifiche" -bullet3 = "L'ordine di firma si adegua automaticamente" -description = "I partecipanti possono essere rimossi dalle sessioni prima di firmare." -title = "Rimozione dei partecipanti" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Ogni firma viene applicata in sequenza al PDF" -bullet2 = "I firmatari successivi possono vedere le firme precedenti" -bullet3 = "Critico per flussi di approvazione e catene di custodia legali" -description = "L'ordine che specifichi durante la creazione della sessione determina chi firma per primo." -title = "Ordine di firma" - [settings] close = "Chiudi" +[settings.ai] +documents = "Documenti e RAG" +general = "Generale" +limits = "Limiti e prestazioni" +models = "Modelli e provider" +title = "IA" + [settings.configuration] advanced = "Avanzate" database = "Database" endpoints = "Endpoint" features = "Funzionalità" +folderAccess = "Accesso alle cartelle" mcp = "MCP Server" storageSharing = "Archiviazione file e condivisione" systemSettings = "Impostazioni di sistema" @@ -8798,6 +9744,8 @@ defaultPdfEditor = "Editor PDF predefinito" defaultPdfEditorActive = "Stirling PDF è il tuo editor PDF predefinito" defaultPdfEditorChecking = "Verifica..." defaultPdfEditorInactive = "Un'altra applicazione è impostata come predefinita" +defaultPdfEditorRemind = "Ricordami di impostare come predefinito" +defaultPdfEditorRemindDescription = "Mostra un banner quando Stirling PDF non è la tua applicazione PDF predefinita." defaultPdfEditorSet = "Già predefinito" defaultStartupView = "Vista predefinita all'avvio" defaultStartupViewDescription = "Scegli quale scheda è attiva nella colonna sinistra all'avvio dell'app" @@ -8815,9 +9763,18 @@ languageDescription = "Scegli la lingua di visualizzazione" logout = "Esci" setAsDefault = "Imposta come predefinito" theme = "Tema" +themeAccent = "Colore accento" +themeAccentCustom = "Colore personalizzato" +themeAccentCustomHint = "Personalizzato (predefiniti consigliati)" +themeAccentDark = "Colore accento modalità scura" +themeAccentDefault = "Predefinito" +themeAccentDefaultHint = "Tema predefinito (consigliato)" +themeAccentDescription = "Pulsanti, link ed evidenziazioni lo seguono e colorano leggermente l'app. La modalità chiara e quella scura ne hanno uno ciascuna. Il sistema usa quello attivo." +themeAccentLight = "Colore accento modalità chiara" themeDark = "Scuro" themeDescription = "Passa tra modalità chiara e scura" themeLight = "Chiaro" +themeReset = "Ripristina tema predefinito" themeSystem = "Sistema" title = "Generale" user = "Utente" @@ -8831,6 +9788,12 @@ intro = "Abilita autenticazione utente, gestione dei team e funzionalità dell'a learnMore = "Scopri di più nella documentazione" title = "Per gli amministratori di sistema" +[settings.general.loginLanding] +description = "Scegli quale app si apre quando accedi a Stirling Cloud." +editor = "Editor" +processor = "Elaboratore" +title = "Dopo l'accesso" + [settings.general.mode] fullscreen = "Schermo intero" sidebar = "Barra laterale" @@ -9011,9 +9974,6 @@ title = "Policy e privacy" [settings.preferences] title = "Preferenze" -[settings.search] -placeholder = "Cerca nelle pagine delle impostazioni..." - [settings.security] description = "Aggiorna la password per mantenere sicuro il tuo account." title = "Sicurezza" @@ -9039,6 +9999,11 @@ security = "Sicurezza" telegram = "Telegram" title = "Sicurezza e autenticazione" +[settings.signOut] +confirm = "Sei sicuro di voler uscire?" +submit = "Esci" +title = "Esci" + [settings.team] title = "Team" @@ -9154,6 +10119,7 @@ last = "Ultima pagina" maintainRatio = "Attiva il mantenimento delle proporzioni" next = "Prossima pagina" noSavedSigs = "Nessuna firma salvata trovata" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Firme personali" previous = "Pagina precedente" redo = "Rifare" @@ -9192,6 +10158,7 @@ backgroundRemovalFailedTitle = "Rimozione dello sfondo non riuscita" hint = "Carica un'immagine PNG o JPG della tua firma" label = "Carica immagine firma" placeholder = "Seleziona file immagine" +previewAlt = "Current image signature" processing = "Elaborazione immagine..." removeBackground = "Rimuovi sfondo bianco (rendi trasparente)" @@ -9205,6 +10172,17 @@ saved = "Seleziona una firma salvata sopra, poi clicca ovunque sul PDF per posiz text = "Dopo aver inserito il tuo nome sopra, clicca in un punto del PDF per inserire la tua firma." title = "Come aggiungere la firma" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Sposta firma" pause = "Metti in pausa posizionamento" @@ -9651,10 +10629,30 @@ title = "Carica sul server" updateButton = "Aggiorna sul server" uploadButton = "Carica sul server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Sondaggio su Stirling-PDF" [swagger] +clickHere = "fai clic qui" +fallback = "Se non si è aperto automaticamente," +opening = "Apertura di Swagger UI in una nuova scheda..." tags = "api,documentazione,swagger,endpoint,sviluppo" title = "Documentazione API" @@ -9762,12 +10760,39 @@ selectFilesHint = "Seleziona i file in File attivi per eseguire questo strumento singleFileScope = "Applicato solo a: {{fileName}}" viewerMode = "Passa all'editor di file per selezionare più file." +[toolFormat] +ANY = "qualsiasi file" +CBR = "un archivio di fumetti (CBR)" +CBZ = "un archivio di fumetti (CBZ)" +CSV = "un file CSV" +EBOOK = "un ebook" +EMAIL = "un'email" +EXCEL = "un foglio di calcolo" +HTML = "una pagina web" +IMAGE = "immagini" +JAVASCRIPT = "un file JavaScript" +JSON = "un file JSON" +MARKDOWN = "un file Markdown" +NONE = "nessun file" +PCL = "un file di stampa PCL" +PDF = "un PDF" +PDF_ENCRYPTED = "un PDF protetto da password" +POSTSCRIPT = "un file PostScript" +PPT = "una presentazione" +TEXT = "un file di testo" +VIDEO = "un video" +WORD = "un documento Word" +XML = "un file XML" +XPS = "un documento XPS" +ZIP = "un archivio ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Torna a tutti gli strumenti" collapse = "Comprimi pannello" expand = "Espandi pannello" goBack = "Torna indietro" +pdfTools = "Strumenti PDF" placeholder = "Scegli uno strumento per iniziare" premiumFeature = "Funzione Premium:" toolsHeader = "Strumenti" @@ -9804,11 +10829,6 @@ noToolsFound = "Nessuno strumento trovato" quickAccess = "ACCESSO RAPIDO" searchPlaceholder = "Cerca strumenti..." -[toolPicker.categories] -advancedTools = "Strumenti avanzati" -recommendedTools = "Strumenti consigliati" -standardTools = "Strumenti standard" - [toolPicker.subcategories] advancedFormatting = "Formattazione avanzata" automation = "Automazione" @@ -9822,6 +10842,9 @@ removal = "Rimozione" signing = "Firma" verification = "Verifica" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nessuno strumento trovato" noTools = "Nessuno strumento disponibile" @@ -9948,28 +10971,53 @@ title = "Utenti" you = "(tu)" [users.action] +cancelInvite = "Annulla invito" deleteTeam = "Elimina team" disableMfa = "Reimposta MFA" move = "Sposta nel team" reinstate = "Ripristina" remove = "Rimuovi dall'org" +removeTeam = "Rimuovi dal team" rename = "Rinomina team" resetPw = "Reimposta password" suspend = "Sospendi" unlock = "Sblocca account" +[users.activity] +daysAgo = "{{count}} g fa" +hoursAgo = "{{count}} h fa" +justNow = "Proprio ora" +minutesAgo = "{{count}} min fa" +monthsAgo_one = "{{count}} mese fa" +monthsAgo_other = "{{count}} mesi fa" +never = "Mai" +weeksAgo_one = "{{count}} settimana fa" +weeksAgo_other = "{{count}} settimane fa" +yearsAgo_one = "{{count}} anno fa" +yearsAgo_other = "{{count}} anni fa" + [users.cap] addProcessor = "+ Processor" approver = "Approva criterio" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Annullare l'invito a {{email}}? Non potrà unirsi con il link attuale." +cancelInviteTitle = "Annulla invito" deleteTeamBody = "Eliminare il team {{name}}? Il team deve prima essere vuoto: sposta i suoi membri in un altro team e non può ancora possedere configurazioni di integrazione." deleteTeamTitle = "Elimina team" disableMfaBody = "Rimuovere l'iscrizione MFA di {{name}}? La configurerà di nuovo al prossimo login, se richiesto." disableMfaTitle = "Reimposta MFA" removeBody = "Rimuovere definitivamente {{name}} dall'organizzazione? Questa operazione non può essere annullata." +removeTeamBody = "Rimuovere {{name}} dal team? Manterrà il proprio account ma perderà l'accesso alle risorse di questo team." removeTitle = "Rimuovi membro" [users.empty] @@ -9980,10 +11028,8 @@ title = "Nessun membro ancora" addToTeam = "Aggiungi al team" guestCount = "{{count}} ospiti" guests = "Ospite" -guestsDesc = "Collaboratori esterni, limitati a ciò che hai condiviso. Solo Editor." ledBy = "guidato da {{owner}}" org = "Organizzazione" -orgDesc = "Proprietari con autorità su tutta l'organizzazione e approvazione dei criteri" owners = "{{count}} proprietario" team = "Team {{name}}" teamMeta = "{{count}} persone" @@ -10020,6 +11066,18 @@ username = "Nome utente" usernameError = "Il nome utente deve contenere almeno 3 caratteri" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invitato da {{who}}" +cancel = "Annulla" +expiresInDays_one = "Scade tra {{count}} giorno" +expiresInDays_other = "Scade tra {{count}} giorni" +expiresToday = "Scade oggi" +title = "Inviti in sospeso" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Qualcosa è andato storto nel contattare il backend, oppure non hai accesso. Riprova." title = "Impossibile caricare i membri" @@ -10141,10 +11199,10 @@ generatedAt = "Generato" noPdf = "Il report PDF sarà disponibile dopo una validazione riuscita." page = "Pagina" shortTitle = "Riepilogo firma" -signatureCountLabel_one = "{{count}} signature" -signatureCountLabel_other = "{{count}} signatures" -signaturesFound_one = "{{count}} signature detected" -signaturesFound_other = "{{count}} signatures detected" +signatureCountLabel_one = "{{count}} firma" +signatureCountLabel_other = "{{count}} firme" +signaturesFound_one = "{{count}} firma rilevata" +signaturesFound_other = "{{count}} firme rilevate" signaturesInvalid = "{{count}} non valide" signaturesUnverified_one = "{{count}} necessita verifica" signaturesUnverified_other = "{{count}} necessitano verifica" @@ -10209,10 +11267,15 @@ searchPlaceholder = "Cerca allegati" title = "Allegati" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Titolo del segnalibro" +bookmarkTitleRequired = "Il titolo del segnalibro è obbligatorio" closeSidebar = "Chiudi la barra laterale dei segnalibri" collapseAll = "Comprimi tutti i segnalibri" +empty = "No bookmarks in this document" expandAll = "Espandi tutti i segnalibri" +searchPlaceholder = "Cerca segnalibri" +title = "Bookmarks" [viewer.comments] addComment = "Aggiungi commento" @@ -10234,10 +11297,12 @@ locateAnnotation = "Individua nel documento" moreActions = "Altre azioni" nComments_one = "{{count}} commento" nComments_other = "{{count}} commenti" +noMatch = "No comments match your search" pageLabel = "Pagina {{page}}" placingHint = "Fai clic su una pagina per posizionare… (annulla)" removeCommentOnly = "Rimuovi solo il commento" saveReply = "Salva risposta" +searchPlaceholder = "Search comments" title = "Commenti" typeComment = "Commento" typeInsertText = "Inserisci testo" @@ -10260,6 +11325,7 @@ unsavedDesc = "Hai modifiche non salvate" closeSidebar = "Chiudi la barra laterale dei livelli" hideAll = "Nascondi tutti i livelli" showAll = "Mostra tutti i livelli" +title = "Layers" [viewer.link] delete = "Elimina link" @@ -10603,30 +11669,6 @@ title = "Testo filigrana" image = "Immagine" text = "Testo" -[wetSignature.tooltip] -header = "Metodi di creazione della firma" - -[wetSignature.tooltip.draw] -bullet1 = "Personalizza colore e spessore della penna" -bullet2 = "Cancella e ridisegna finché non sei soddisfatto" -bullet3 = "Funziona su dispositivi touch (tablet, telefoni)" -description = "Crea una firma scritta a mano usando il mouse o il touchscreen. Ideale per firme personali e autentiche." -title = "Disegna firma" - -[wetSignature.tooltip.type] -bullet1 = "Scegli tra più font" -bullet2 = "Personalizza dimensione e colore del testo" -bullet3 = "Perfetto per firme standardizzate" -description = "Genera una firma dal testo digitato. Veloce e coerente, adatta a documenti aziendali." -title = "Digita firma" - -[wetSignature.tooltip.upload] -bullet1 = "Supporta PNG, JPG e altri formati immagine" -bullet2 = "Sfondi trasparenti consigliati per risultati migliori" -bullet3 = "L'immagine verrà ridimensionata per adattarsi all'area della firma" -description = "Carica un'immagine di firma preesistente. Ideale se hai una firma scansionata o un logo aziendale." -title = "Carica immagine della firma" - [workbenchBar] activeFiles = "File attivi" annotations = "Annotazioni" @@ -10640,6 +11682,7 @@ exitRedaction = "Esci dalla modalità di oscuramento" exportAll = "Esporta PDF" exportSelected = "Esporta pagine selezionate" formFill = "Compila modulo" +hideToolbar = "Hide toolbar" multiTool = "Strumento multiplo" panMode = "Modalità mano" print = "Stampa PDF" @@ -10650,6 +11693,7 @@ redact = "Oscura" rotateLeft = "Ruota a sinistra" rotateRight = "Ruota a destra" ruler = "Righello / Misura" +rulerSettings = "Impostazioni scala" save = "Salva" saveAll = "Salva tutto" saveAs = "Salva con nome" @@ -10659,6 +11703,7 @@ selectAll = "Seleziona tutto" selectByNumber = "Seleziona per numeri di pagina" selectLanguage = "Seleziona lingua" share = "Condividi" +showToolbar = "Show toolbar" toggleAnnotations = "Mostra/Nascondi annotazioni" toggleAttachments = "Mostra/Nascondi allegati" toggleBookmarks = "Mostra/Nascondi segnalibri" diff --git a/frontend/editor/public/locales/ja-JP/translation.toml b/frontend/editor/public/locales/ja-JP/translation.toml index 40b1ef6da7..2d67a7427a 100644 --- a/frontend/editor/public/locales/ja-JP/translation.toml +++ b/frontend/editor/public/locales/ja-JP/translation.toml @@ -1,7 +1,6 @@ alphabet = "アルファベット" apply = "適用" applyAndContinue = "適用して続行" -areYouSure = "本当に離れますか?" back = "戻る" black = "黒" blue = "青" @@ -46,10 +45,8 @@ filesSelected = "選択されたファイル" font = "フォント" fontSizeTooltip = "ページ番号テキストのポイントサイズ。大きい数値ほど文字が大きくなります。" fontTypeTooltip = "ページ番号のフォントファミリー。文書のスタイルに合わせて選択してください。" -genericSubmit = "送信" green = "緑" help = "ヘルプ" -imgPrompt = "画像を選択" incorrectPasswordMessage = "現在のパスワードが正しくありません。" info = "情報" invalidUndoData = "元に戻せません: 無効な操作データです" @@ -62,7 +59,6 @@ loginAgreementTitle = "ログイン同意" logOut = "ログアウト" marginTooltip = "ページ番号とページ端との距離。" moreOptions = "その他のオプション" -multiPdfPrompt = "PDFを選択 (2つ以上)" never = "なし" no = "いいえ" noFileSelected = "ファイルが選択されていません。アップロードしてください。" @@ -80,7 +76,6 @@ pageOrderPrompt = "ページ順序 (ページ番号をカンマ区切り又は2n pages = "ページ" pageSelectionPrompt = "カスタムページ選択(ページ番号1、5、6または2n + 1などの関数のコンマ区切りリストを入力します):" password = "パスワード" -pdfPrompt = "PDFを選択" pin = "ファイルをピン留め(ツール実行後も保持)" pro = "Pro" processingComplete = "ファイルの準備ができました。" @@ -110,6 +105,7 @@ undoSuccess = "操作を元に戻しました" unknown = "不明" unpin = "ピン留めを解除(ツール実行後に置き換え)" unsavedChanges = "PDF に未保存の変更があります。" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "未保存の変更" unsupported = "未対応" username = "ユーザー名" @@ -356,6 +352,7 @@ text = "これらの権限は、PDF でユーザーができることを制御 title = "権限の変更" [addStamp] +preview = "スタンプをプレビュー" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "手動ダウンロード: リンクをクリックし、ファイ noLanguages = "設定されたディレクトリに tessdata 言語が見つかりません。" permissionNotice = "tessdata パスに書き込めません。ダウンロードはブラウザで開かれます。.traineddata ファイルを手動で tessdata フォルダーに保存してください。" +[admin.settings.ai.documents] +description = "ドキュメントに関する質問に回答するために使用する埋め込みモデルと取得設定を構成します。保存すると AI エンジンに適用されます。" +title = "ドキュメント & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "エンジン独自の環境認証情報を使用する場合は空白のままにします。セルフホストの単一エンジン構成に適用されます。" +generic = "埋め込み API key" +openai = "OpenAI API key" +setPlaceholder = "保存済み - 現在の key を保持するには空白のままにしてください" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI 互換 / Ollama 埋め込みエンドポイントの Base URL。例: http://ollama:11434/v1。信頼できる内部エンドポイントを指す必要があります(SSRF に注意)。" +label = "埋め込み base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "埋め込みモデル名。自由入力です。候補はヒントにすぎません。" +label = "埋め込みモデル" + +[admin.settings.ai.documents.embeddingProvider] +description = "ドキュメントテキストをベクトル埋め込みに変換するために使用するプロバイダー。" +label = "埋め込みプロバイダー" + +[admin.settings.ai.documents.maxSearches] +description = "エージェントが 1 リクエストあたり実行できる取得検索の最大数。" +label = "最大検索数" + +[admin.settings.ai.documents.reindexNote] +body = "埋め込みモデルの変更はすぐに有効になりますが、以前のモデルでインデックス化されたドキュメントは、検索で正しい結果を返すために再インデックス化する必要があります。" +title = "再インデックスが必要です" + +[admin.settings.ai.documents.saved] +reindexNote = "埋め込みモデルを変更した場合は、検索で新しいモデルが使用されるように既存のドキュメントを再インデックス化してください。" + +[admin.settings.ai.documents.topK] +description = "検索ごとに取得する最も関連性の高いチャンクの数。" +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling を Python AI エンジンに接続し、公開する AI 機能を選択します。変更は再起動後に適用されます。" +title = "AI エンジン" + +[admin.settings.ai.general.capabilities] +description = "個々の AI 機能をオンまたはオフにします。無効化された機能はアプリで非表示になります。" +title = "機能" + +[admin.settings.ai.general.enabled] +description = "マスタースイッチ。オフの場合、AI ツール、エージェント、エンジン呼び出しは利用できません。" +label = "AI を有効化" + +[admin.settings.ai.general.features.chat] +description = "PDF を扱うための会話型アシスタント。" +label = "チャットアシスタント" + +[admin.settings.ai.general.features.classify] +description = "ドキュメントを種類または内容で自動的に分類します。" +label = "ドキュメント分類" + +[admin.settings.ai.general.features.createPdf] +description = "AI エージェントを介して新しい PDF(例: HTML から)を生成します。" +label = "プロンプトから PDF を作成" + +[admin.settings.ai.general.features.documentQuestions] +description = "アップロードされたドキュメントに基づいて質問し、回答を取得します。" +label = "ドキュメントへの質問" + +[admin.settings.ai.general.features.mathAuditor] +description = "ドキュメントの数学的および数値的な誤りをレビューします。" +label = "数学監査" + +[admin.settings.ai.general.features.pdfComment] +description = "AI が作成したレビューコメントと注釈を PDF に追加します。" +label = "PDF コメントエージェント" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "ドキュメント生成など、負荷の高いエージェント操作のタイムアウト。" +label = "長時間実行タイムアウト(秒)" + +[admin.settings.ai.general.note] +body = "AI エンジンは別サービスとして実行されます。その共有シークレット" +body2 = "はコンテナ環境変数で設定されます。プロバイダー API key はこれらのページで入力するか、エンジン環境変数として指定できます。ここで入力した key は保存時にエンジンへ送信されます。" +title = "AI エンジンについて" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "ストリーミング(トークン単位)チャット応答のタイムアウト。" +label = "ストリームタイムアウト(秒)" + +[admin.settings.ai.general.test] +button = "接続をテスト" +failBody = "AI エンジンが応答しませんでした。URL、エンジンコンテナが実行中であること、AI が有効であること(有効化後は再起動が必要)を確認してください。" +failTitle = "AI エンジンに到達できません" +okBody = "AI エンジンがヘルスチェックに応答しました。" +okTitle = "AI エンジンに到達できます" + +[admin.settings.ai.general.timeoutSeconds] +description = "エンジンへの標準 AI リクエストのタイムアウト。" +label = "リクエストタイムアウト(秒)" + +[admin.settings.ai.general.url] +description = "Python AI エンジンの内部 URL。例: http://stirling-pdf-engine:5001。" +label = "AI エンジン URL" + +[admin.settings.ai.limits] +description = "AI リクエストが実行できる作業量と同時実行数のガードレール。保存すると AI エンジンに適用されます。" +title = "制限 & パフォーマンス" + +[admin.settings.ai.limits.maxCharacters] +description = "ガードレール: 抽出されたテキストがこの長さを超える AI リクエストを拒否します。" +label = "リクエストあたりの最大文字数" + +[admin.settings.ai.limits.maxPages] +description = "ガードレール: このページ数を超える PDF の AI リクエストを拒否します。" +label = "リクエストあたりの最大ページ数" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "エンジン全体で同時に処理中にできるモデル呼び出しの最大数。" +label = "モデル最大同時実行数" + +[admin.settings.ai.models] +description = "AI エンジンが使用する LLM プロバイダーとスマート/高速モデルを選択します。保存すると AI エンジンに適用されます。" +title = "モデル & プロバイダー" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "エンジン独自の環境認証情報を使用する場合は空白のままにします。セルフホストの単一エンジン構成に適用されます。" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "保存済み - 現在の key を保持するには空白のままにしてください" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI 互換 / Ollama エンドポイントの Base URL。例: http://ollama:11434/v1。" +label = "プロバイダー base URL" +warning = "base URL は信頼できる内部エンドポイントを指す必要があります。エンジンはそこへサーバー側リクエストを行うため、信頼できない値は SSRF に関して危険です。" + +[admin.settings.ai.models.fastMaxTokens] +description = "高速モデルの最大出力トークン数。" +label = "高速モデル最大トークン数" + +[admin.settings.ai.models.fastModel] +description = "軽量タスク向けの低コストで高速なモデル。自由入力です。候補はヒントにすぎません。" +label = "高速モデル" + +[admin.settings.ai.models.provider] +description = "エンジンが通信する LLM プロバイダー。" +label = "プロバイダー" + +[admin.settings.ai.models.smartMaxTokens] +description = "スマートモデルの最大出力トークン数。" +label = "スマートモデル最大トークン数" + +[admin.settings.ai.models.smartModel] +description = "複雑な推論向けの高性能モデル。自由入力です。候補はヒントにすぎません。" +label = "スマートモデル" + +[admin.settings.ai.saved] +body = "変更は AI エンジンに自動的に送信されます。" +bodyNoPush = "設定を保存しました。AI エンジンが次に構成を取得したときに適用されます。" +title = "AI 設定を保存しました" + [admin.settings.badge] clickToUpgrade = "クリックしてプラン詳細を表示" @@ -938,6 +1094,25 @@ label = "起動時に再生成" description = "証明書の有効日数" label = "証明書の有効期間(日)" +[admin.settings.folderAccess] +description = "フォルダーソースとフォルダー出力が読み書きできるディレクトリ。これはセキュリティ境界です。自動化はこのリスト外のサーバーパスを指すことはできません。" +securityNote = "フォルダーソースと出力を完全に無効化するには、ここを空のままにします。Stirling 独自の構成ディレクトリは常にアクセス禁止です。" +title = "フォルダーアクセス" + +[admin.settings.folderAccess.implied] +description = "これらの Stirling 管理ディレクトリは常に許可され、ここでは変更できません。" +serverStorage = "サーバーファイルストレージ" +title = "常に許可" +watchedFolder = "Pipeline 監視フォルダー" + +[admin.settings.folderAccess.roots] +add = "追加" +empty = "許可されたフォルダーはありません。フォルダーソースと出力は現在無効です。" +hint = "絶対パスを入力してください。例: /data/inbox。" +label = "許可されたフォルダールート" +placeholder = "/data/inbox" +remove = "フォルダールートを削除" + [admin.settings.general] description = "ブランディングや既定の動作など、アプリ全体の設定を構成します。" system = "システム" @@ -1704,6 +1879,9 @@ width = "幅" [app] description = "無料の Adobe Acrobat 代替(1,000万+ ダウンロード)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b に変換" convertToPdfA3bDescription = "添付ファイルを埋め込んだアーカイブ用 PDF を作成します" @@ -1842,6 +2020,15 @@ export = "エクスポート" accessDenied = "アクセス拒否" insufficientPermissions = "この操作を実行する権限がありません。" +[auth.callback] +completing = "認証を完了しています" +invalidToken = "OAuth ログインに失敗しました - 無効なトークンです。" +missingToken = "OAuth ログインに失敗しました - トークンを受信していません。" +oauthFailed = "OAuth ログインに失敗しました。もう一度お試しください。" +pleaseWait = "サインインを完了するまでお待ちください。" +signedOut = "サインアウトしました。もう一度サインインしてください。" +windowMayClose = "完了したらこのウィンドウを閉じることができます。" + [auth.displayName] guest = "ゲスト" user = "ユーザー" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "JavaScript や埋め込みファイルなどの潜在的 description = "このツールは内容に基づいて PDF ファイル名を自動でリネームします。文書内のテキストから最適なタイトルを抽出して適用します。" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "自動回転" +tags = "自動回転,向きを修正,上下逆,横向き,まっすぐにする,向き,向きを検出,回転を修正,自動方向調整,スキャン済み,OSD" + +[autoRotate.confidenceThreshold] +title = "信頼度しきい値" + +[autoRotate.detectionMode] +auto = "自動" +osd = "OCR のみ" +text = "テキストのみ" +title = "検出方法" + +[autoRotate.error] +failed = "PDF の自動回転中にエラーが発生しました。" + +[autoRotate.inferUndetected] +title = "低信頼度ページを合わせて回転" + +[autoRotate.report] +applied = "{{degrees}}° 時計回り" +confidenceValue = "信頼度 {{value}}" +noChange = "変更なし" +pageLabel = "ページ {{number}}" +summary = "{{total}} ページ中 {{rotated}} ページを回転、{{unchanged}} ページはそのまま" +title = "検出レポート" + +[autoRotate.report.method] +inferred = "一致" +none = "スキップ" +osd = "OCR" +text = "テキスト" + +[autoRotate.report.note] +belowThreshold = "しきい値未満" +blankPage = "ページが空白です" +inferredFromDocument = "他のページと同じ" +noDominantDirection = "テキスト方向が混在しています" +osdFailed = "読み取り可能なテキストが見つかりません" +osdNoVerdict = "上下方向を判定できませんでした" +tesseractUnavailable = "OCR がインストールされていません" +tooFewGlyphs = "テキストが少なすぎます" + +[autoRotate.results] +title = "自動回転結果" + +[autoRotate.settings] +title = "設定" + +[autoRotate.tooltip.confidenceThreshold] +text = "ページを回転する前にチェックがどの程度確実である必要があるかを指定します。値を下げるとより多くのページを回転し、上げるとより多くのページをそのままにします。" +title = "信頼度しきい値" + +[autoRotate.tooltip.description] +text = "各ページの正しい上下方向を判断し、正しい向きに回転します。すでに正しい向きのページはそのままにします。" + +[autoRotate.tooltip.detectionMode] +text = "自動ではまずページ上のテキストを読み取り、それだけで不十分な場合はページを画像として確認します。テキストのみでは画像チェックをスキップするため、スキャンされていないドキュメントでは最速です。OCR のみではすべてのページを画像として扱います。" +title = "検出方法" + +[autoRotate.tooltip.header] +title = "自動回転" + +[autoRotate.tooltip.inferUndetected] +text = "空白またはほぼ空白のページは単独では確認できません。読み取れたすべてのページが同じ回転を必要とする場合、これらのページもそれに合わせて回転します。" +title = "低信頼度ページを合わせて回転" + [autoSizeSplitPDF] tags = "pdf,分割,ドキュメント,整理" @@ -2189,33 +2443,6 @@ keywords = "キーワード:odd, even" numbers = "数値/範囲:5、10-20" progressions = "等差列:3n、4n+1" -[certificateChoice.tooltip] -header = "証明書の種類" - -[certificateChoice.tooltip.organization] -bullet1 = "システム管理者が管理" -bullet2 = "権限のあるユーザー間で共有" -bullet3 = "個人ではなく会社の身元を表す" -bullet4 = "用途: 公式文書、チームの署名" -description = "組織が提供する共有証明書。全社的な署名権限に使用されます。" -title = "組織証明書" - -[certificateChoice.tooltip.personal] -bullet1 = "初回使用時に自動生成" -bullet2 = "あなたのユーザーアカウントに紐づく" -bullet3 = "他のユーザーと共有不可" -bullet4 = "用途: 個人文書、個人の責任追跡" -description = "あなたのユーザーアカウント専用に自動生成される証明書。個人の署名に適しています。" -title = "個人証明書" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX ファイルとパスワードが必要" -bullet2 = "外部の認証局で発行可能" -bullet3 = "法的文書向けの高い信頼レベル" -bullet4 = "用途: 法的拘束力のある契約、外部検証" -description = "自分の PKCS#12 証明書ファイルを使用します。証明書プロパティを完全に制御できます。" -title = "カスタム P12 をアップロード" - [certSign] awaitingSignatures = "署名待ち" chooseCertificate = "証明書ファイルを選択" @@ -2542,34 +2769,6 @@ server = "サーバー" stepTitle = "証明書の取得元" upload = "アップロード" -[certSign.tooltip.header] -title = "署名の管理について" - -[certSign.tooltip.overview] -bullet1 = "既存の署名とその有効性を確認" -bullet2 = "署名者と証明書の詳細を表示" -bullet3 = "新しい電子署名を追加して文書を保護" -bullet4 = "複数ファイルをサポートし、簡単に移動" -text = "PDF が電子署名されているかの確認や、新しい電子署名の追加ができます。電子署名は文書の作成者や承認者を証明し、署名後に変更があったかを示します。" -title = "このツールでできること" - -[certSign.tooltip.signing] -bullet1 = "PEM、PKCS12、JKS、サーバー証明書形式に対応" -bullet2 = "署名を PDF 上に表示/非表示にするオプション" -bullet3 = "理由、場所、署名者名を追加" -bullet4 = "可視署名の配置ページを選択" -bullet5 = "「Stirling-PDF で署名」用のサーバー証明書を使用可能" -text = "PDF に署名するには、PEM、PKCS12、JKS などの電子証明書が必要です。署名を文書上に表示することも、セキュリティ目的のみで非表示にすることもできます。" -title = "署名の追加" - -[certSign.tooltip.validation] -bullet1 = "署名が有効か無効かを表示" -bullet2 = "署名者情報と署名日時を表示" -bullet3 = "署名後に文書が変更されたかを確認" -bullet4 = "検証にカスタム証明書を使用可能" -text = "署名の確認では、署名が有効か、誰がいつ署名したか、署名後に文書が変更されたかを知らせます。" -title = "署名の検証" - [changeCreds] changeUsername = "ユーザー名を更新します。更新後はログアウトされます。" credsUpdated = "アカウントを更新しました" @@ -2737,12 +2936,16 @@ text = "これらの権限を変更不可にするには、パスワード追加 [chat.actions] copy = "メッセージをコピー" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling エージェントのオプション" +assistant = "AI Assistant" clearChat = "チャットをクリア" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "この操作はクラウドクレジットを消費します" tooltip = "画面から色を取得" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "色を選択" [common] +actions = "Actions" back = "戻る" cancel = "キャンセル" close = "閉じる" +codeSample = "Code sample" collapse = "折りたたむ" confirm = "確認" continue = "続行" copied = "コピーしました!" copy = "コピー" done = "完了" +enterValue = "Enter value" error = "エラー" expand = "展開" loading = "読み込み中..." next = "次へ" +open = "Open" preview = "プレビュー" previous = "前へ" refresh = "更新" +remaining = "残り" retry = "再試行" save = "保存" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "選択をクリア" @@ -2988,6 +3199,7 @@ title = "圧縮方式" [compress.settings] desiredSize = "目標ファイルサイズ" desiredSizePlaceholder = "サイズを入力" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "圧縮はファイルサイズを簡単に減らす方法です。「ファイルサイズ」を選ぶと目標サイズを入力でき、品質は自動調整します。「品質」を選ぶと圧縮強度を手動設定できます。" @@ -3190,6 +3402,24 @@ pdfOptions = "PDF オプション" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "先に変換元形式を選択してください" settings = "設定" single = "単一" @@ -3264,24 +3494,6 @@ description = "これらのCookieはウェブサイトが正常に機能する posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "高度なモニタリング" -allInOneWorkspace = "オールインワンの PDF ワークスペース(ビューア、ツール、エージェント)" -apiSandbox = "API サンドボックス" -dedicatedSupportSlas = "専用サポートと SLA" -fasterThroughput = "スループット 10 倍" -fullyPrivateFiles = "完全にプライベートなファイル" -largeFileProcessing = "大容量ファイルの処理" -orgWideAccess = "組織全体のアクセス制御" -premiumAiModels = "プレミアム AI モデル" -prioritySupport = "優先サポート" -privateDocCloud = "プライベートドキュメントクラウド" -ragFineTuning = "RAG + ファインチューニング" -secureApiAccess = "セキュアな API アクセス" -standardThroughput = "標準スループット" -unlimitedApiAccess = "API アクセス無制限" -unlimitedSeats = "ユーザー数無制限" - [crop] autoCrop = "余白を自動トリミング" noFileSelected = "トリミングを開始する PDF ファイルを選択してください" @@ -3346,6 +3558,7 @@ message = "既定のPDFハンドラーの設定に失敗しました" title = "エラー" [defaultApp.prompt] +dontRemind = "今後表示しない" message = "PDFファイルを開く既定のアプリとして Stirling PDF を設定します。" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "検索..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "編集" @@ -3644,11 +3861,13 @@ shareSelected = "選択項目を共有" sharing = "共有" showAll = "すべて表示" showHistory = "履歴を表示" +sortBy = "Sort files" sortByDate = "日付で並べ替え" sortByName = "名前で並べ替え" sortBySize = "サイズで並べ替え" storage = "ストレージ" storageState = "ストレージ" +storageUsed = "Storage used" synced = "同期済み" title = "PDF ファイルをアップロード" toolChain = "適用ツール" @@ -3694,13 +3913,17 @@ addFiles = "ファイルを追加" addingFiles = "ファイルを追加中…" collapse = "サイドバーを折りたたむ" customizeGroups = "グループをカスタマイズ" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "開始するにはファイルを開いてください" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "サイドバーを展開" -files = "ファイル" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive が未設定です" leaveMyFiles = "マイファイルを離れる" +library = "PDF ライブラリ" myFiles = "マイファイル" noFiles = "まだファイルがありません" openFileManager = "ファイルマネージャーを開く" @@ -3708,37 +3931,36 @@ openFromComputer = "コンピューターから開く" openSettings = "設定を開く" other = "その他" recent = "最近" -search = "検索" -searchPlaceholder = "ファイルを検索..." viewAll = "{{count}} 個のファイルをすべて表示" [fileSidebar.fileItem] closeViewer = "ビューアを閉じる" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "ビューアで開く" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "追加" -addLabel = "ラベルを追加…" -categoryIconAria = "{{name}} のアイコンを選択" -createCategory = "カテゴリを作成" -delete = "カテゴリを削除" done = "完了" -hide = "カテゴリを非表示" -newCategory = "新しいカテゴリ名…" -removeLabel = "{{name}} を削除" -rename = "名前を変更" reset = "デフォルトにリセット" -search = "ラベルを検索…" -show = "カテゴリを表示" subtitle = "ファイルを親カテゴリにグループ化します。既存または新しいラベルをカテゴリに追加したり、名前を変更したり、独自のカテゴリを作成したりできます。どのカテゴリにも属さないファイルは「その他」に表示されます。" title = "サイドバーのカテゴリ" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "ワークスペースに追加" addToWorkspaceCount = "{{count}} 件をワークスペースに追加" @@ -3749,7 +3971,6 @@ backToMyFiles = "マイファイルに戻る" breadcrumbs = "フォルダパス" cancel = "キャンセル" classification = "分類" -clearSearch = "検索をクリア" clearSelection = "選択をクリア" closeDetails = "詳細を閉じる" create = "作成" @@ -3781,6 +4002,7 @@ downloadAll = "すべてダウンロード" downloadVersion = "このバージョンをダウンロード" dropOverlay = "ドロップしてアップロード" dropOverlaySub = "ファイルは最初はローカルに追加されます。フォルダに整理するには『Move to』または『Save to cloud』を使用してください。" +duplicate = "Duplicate" file = "ファイル" fileInfo = "File info" fileMenu = "ファイルの操作" @@ -3808,13 +4030,12 @@ refresh = "サーバーから再読み込み" remove = "削除" removeVersion = "このバージョンを削除" rename = "名前の変更" +renamed = "名前を変更しました" renameFolder = "フォルダ名を変更" resizeFolderTree = "フォルダツリーのサイズ変更(矢印キー、Shiftで大きく移動;ダブルクリックで自動調整)" save = "保存" saveToServer = "サーバーに保存" saveToServerDisabledHint = "このサーバーでサーバーへの保存は有効になっていません。管理者に有効化を依頼してください。" -search = "検索" -searchPlaceholder = "このフォルダとサブフォルダを検索" selectAll = "すべて選択" selectAllHint = "クリックですべて選択。ヒント: Ctrl(または Cmd)でファイルを個別に追加、Shift で範囲選択。" selectedCount = "{{count}} 件を選択" @@ -3874,6 +4095,10 @@ title = "まだクラウドファイルがありません" hint = "アップロードせずに保存したファイルはここに残ります。ファイルをドロップして追加できます。" title = "ローカル専用のファイルはありません" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "開いたり編集したファイルがここに表示されます。" title = "まだ変更はありません" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "フォルダを削除できませんでした。" deleteFolderFailedDetail = "フォルダを削除できませんでした: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "フォルダの外観を更新できませんでした。" folderAppearanceFailedDetail = "フォルダの外観を更新できませんでした: {{message}}" moveFilesFailed = "ファイルを移動できませんでした。" @@ -3910,9 +4137,7 @@ count = "ファイル" folder = "フォルダ" labels = "ラベル" modified = "更新日時" -name = "名前" size = "サイズ" -toolHistoryAtVersion = "累積ツールチェーン" totalSize = "合計サイズ" type = "種類" versionHistory = "バージョンの履歴" @@ -3945,6 +4170,11 @@ localHint = "このブラウザにのみ保存" shared = "共有" sharedHint = "リンクであなたと共有" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "古い順" modifiedDesc = "新しい順" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "ストレージに利用可能なファイルがあり open = "開く" openFile = "ファイルを開く" openFiles = "複数ファイルを開く" +selectFile = "Select {{name}}" selectFromStorage = "ストレージから選択" upload = "アップロード" uploadFile = "ファイルをアップロード" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "すべて保存済み" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV の抽出に失敗しました" +extractXlsxError = "XLSX の抽出に失敗しました" +filled = "filled" flattenAfterFilling = "入力後にフラット化" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "必須" +requiredFieldsError = "必須フィールドをすべて入力してください" rescanFields = "フィールドを再スキャン" rescanFormFields = "フォームフィールドを再スキャン" save = "保存" +saveShortcut = "Ctrl+S で保存" +unsavedChanges = "未保存の変更" [formFill.sidebar] close = "サイドバーを閉じる" @@ -4309,6 +4552,11 @@ desc = "検出されたヘッダーに基づいて PDF ファイル名を自動 tags = "自動検出,ヘッダー基準,整理,ラベル変更" title = "PDF ファイル名を自動変更" +[home.autoRotate] +desc = "各ページの向きを検出し、自動的に正しい向きへ回転します。" +tags = "自動回転,向きを修正,上下逆,横向き,まっすぐにする,向き,向きを検出,回転を修正,自動方向調整,スキャン済み,OSD" +title = "自動回転" + [home.autoSizeSplitPDF] tags = "自動,分割,サイズ" title = "サイズ・数による自動分割" @@ -4608,15 +4856,12 @@ hint = "PDF 上に配置する PNG、JPG、SVG などの画像ファイルをア label = "画像をアップロード" placeholder = "画像ファイルを選択" -[imageToPdf] -tags = "conversion,img,jpg,picture,photo,psd,photoshop" - [imageToPDF] header = "画像をPDFに変換" title = "画像をPDFに変換" -[infoBanner] -dismiss = "閉じる" +[imageToPdf] +tags = "conversion,img,jpg,picture,photo,psd,photoshop" [invite] acceptError = "アカウントの作成に失敗しました" @@ -4647,11 +4892,9 @@ welcomeTitle = "招待されています!" [landing] addFiles = "ファイルを追加" -heroSubtitle = "既存の PDF をドロップまたは追加して開始してください。" mobileUpload = "モバイルからアップロード" openFromComputer = "コンピューターから開く" uploadFromComputer = "コンピュータからアップロード" -workbenchEmptyStateHero = "PDF をどこにでもドロップ" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "ユーザー名を入力" failedToSignIn = "{{provider}} でのサインインに失敗しました:{{message}}" forgotPassword = "パスワードをお忘れですか?" loggingIn = "サインイン中..." -logIn = "ログイン" login = "ログイン" magicLinkSent = "マジックリンクを {{email}} に送信しました! メールを確認してリンクをクリックし、サインインしてください。" mfaCode = "認証コード" @@ -4715,26 +4957,10 @@ signInWith = "次でサインイン" title = "サインイン" unexpectedError = "予期しないエラー:{{message}}" updatePassword = "パスワードを更新" -useEmailInstead = "メールでログイン" useMagicLink = "代わりにマジックリンクを使う" username = "ユーザー名" youAreLoggedIn = "ログイン済みです!" -[login.slides.edit] -alt = "PDF を編集" -subtitle = "墨消し、署名、閲覧、操作のための十数種のツールを備え、求める機能が必ず見つかります。" -title = "必要な情報を表示・保護するために PDF を編集" - -[login.slides.overview] -alt = "Stirling PDF の概要" -subtitle = "プライバシー最優先の PDF 向けクラウドスイート。変換、署名、墨消し、文書管理に加え、50 以上の強力なツールを提供。" -title = "PDF のためのワンストップソリューション" - -[login.slides.secure] -alt = "PDF を保護" -subtitle = "パスワード追加、墨消し、証明書管理を簡単に。" -title = "PDF の機密情報を保護" - [margin] large = "大" medium = "中" @@ -4818,6 +5044,34 @@ uploadSuccess = "アップロード成功!" uploadSuccessMessage = "画像の転送が完了しました。" validating = "セッションを検証中..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "スキャンして写真をアップロード。画像は自動で PDF に変換されます。" descriptionNoConvert = "モバイルデバイスから写真をアップロードするためにスキャンします。" @@ -4836,8 +5090,13 @@ title = "モバイルからアップロード" tags = "マルチツール,マルチ操作,UI,クリック&ドラッグ,フロントエンド,クライアントサイド,インタラクティブ,操作可能,移動,削除,移行,分割" title = "PDFマルチツール" -[navbar] -search = "検索" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "認証に失敗しました。このウィンドウを閉じて、もう一度お試しください。" @@ -4966,6 +5225,7 @@ activeFiles = "アクティブファイル ビューには、 allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "閉じる" cropSettings = "切り抜きたいファイルを選択したら、PDF を切り抜く範囲を指定するために切り抜きツールを設定できます。" +dialogLabel = "Onboarding" fileCheckbox = "ファイルをクリックすると、処理対象として選択されます。バッチ処理のために複数選択も可能です。" fileReplacement = "変更されたファイルはワークベンチ内で元のファイルを自動的に置き換えるため、さらに別のツールに簡単に回せます。" filesButton = "クイックアクセスバーの ファイル ボタンから、ツールで使用する PDF をアップロードできます。" @@ -4976,6 +5236,7 @@ pinButton = "ツール実行後もファイルをアクティブのままにし results = "ツールの実行が終わると、レビュー ステップで結果のプレビューがこのパネルに表示され、操作の取り消しやファイルのダウンロードができます。" runButton = "ツールの設定が完了したら、このボタンで選択したすべての PDF に実行できます。" selectCropTool = "ツールの使い方を説明するため、まずは 切り抜き ツールを選択しましょう。" +stepOf = "ステップ {{current}} / {{total}}" toolInterface = "こちらが 切り抜き ツールの画面です。まだ作業用の PDF ファイルを追加していないため、表示項目は多くありません。" workbench = "ここが ワークベンチ です。PDF を表示・編集するメインエリアです。" wrapUp = "準備完了です!アプリの主要エリアとその使い方を学びました。ヘルプ ボタンを押せば、いつでもこのツアーを再表示できます。" @@ -4988,6 +5249,26 @@ showMeAround = "ツアーを見る" skipForNow = "今はスキップ" skipTheTour = "ツアーをスキップ" +[onboarding.checklist] +dismiss = "閉じる" +title = "Stirling PDF をセットアップ" + +[onboarding.checklist.downloadDesktop] +description = "お使いのマシンで Stirling をネイティブ実行" +title = "デスクトップ版 Stirling をダウンロード" + +[onboarding.checklist.inviteTeam] +description = "チームと共同作業" +title = "チームメンバーを招待" + +[onboarding.checklist.shareAnalytics] +description = "Stirling の改善に協力" +title = "匿名の使用状況データを共有" + +[onboarding.checklist.takeTour] +description = "簡単な案内で Stirling の動作を確認" +title = "ツアーを開始" + [onboarding.desktopInstall] body = "Stirling はデスクトップアプリとして最適に動作します。オフラインでも使えて、文書に素早くアクセスし、PC上でローカル編集できます。" selectOs = "オペレーティングシステムを選択" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}} 用をダウンロード" [onboarding.mfa] authenticationCode = "認証コード" +description = "認証アプリをリンクしてアカウントを保護します。QR code をスキャンするかセットアップ key を入力し、6 桁のコードを確認して完了します。" +enable = "MFA を有効化" +enableError = "二要素認証を有効化できません。コードを確認してもう一度お試しください。" +enterCode = "アプリの 6 桁のコードを入力してください。" +enterCodeError = "続行するには認証コードを入力してください。" +logout = "ログアウト" +openAuthenticator = "Google Authenticator、Authy、または 1Password を開きます。" qrCodeLoading = "QRコードを生成中…" +regenerateQrCode = "QR code を再生成" +scanQrCode = "QR code をスキャンするか、下のセットアップ key を入力してください。" +setupError = "二要素設定を開始できません。もう一度お試しください。" +setupKey = "セットアップ key(手動入力)" +stepByStep = "ステップごと" +success = "MFA が有効になりました。続行できます。" +title = "多要素認証のセットアップ" [onboarding.planOverview] adminBodyLoginDisabled = "ログインモードを有効にすると、ユーザー管理、設定の構成、サーバーの健全性監視が可能です。サーバー上の最初の {{freeTierLimit}} 人は無料で Stirling を利用できます。" @@ -5005,6 +5300,11 @@ adminTitle = "管理者向け概要" userBody = "メンバーを招待し、役割を割り当て、1つの安全なワークスペースで文書を整理できます。ソロ利用を超えて拡大する準備ができたら、いつでもログインモードを有効にしてください。" userTitle = "プラン概要" +[onboarding.processorIntro] +body = "Stirling は到着した各ドキュメントを分類、保護、処理する自動ルールである Policies を実行するようになりました。Processor で設定し、実行状況を監視できます。" +cta = "Processor を確認" +title = "Stirling Processor を確認" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "年払い前払い" + +[payg.prepaid.banner] +exhausted = "前払い容量を使い切りました。従量課金が再開されました。割引を維持するには追加購入してください。" +expiring_one = "前払い年はあと {{count}} 日で終了します。未使用容量は期限切れになります — 新しい年を開始するには追加購入してください。" +expiring_other = "前払い年はあと {{count}} 日で終了します。未使用容量は期限切れになります — 新しい年を開始するには追加購入してください。" +low = "前払い容量の残りが {{pct}}% です。処理を中断しないために、いつでも追加購入できます。" + +[payg.prepaid.card] +subtitle = "従量課金の前、および利用上限の外で使用されます。" +title = "前払い容量" + +[payg.prepaid.meter] +capSuffix = "{{total}} 前払いクレジット中" +expires = "有効期限 {{date}}" + +[payg.prepaid.state] +exhausted = "使い切りました" +healthy = "十分に残っています" +low = "残りわずか" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDFをPDF/Aに変換" tags = "アーカイブ,長期,標準,変換,保存,保全" title = "PDFをPDF/Aに変換" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "印刷,標準,変換,制作,プリプレス,アーカイブ" title = "PDF を PDF/X に" @@ -5822,6 +6151,7 @@ small = "500 クレジット" xsmall = "100 クレジット" [plan.availablePlans] +currency = "Billing currency" subtitle = "ニーズに合ったプランを選択" title = "利用可能なプラン" @@ -5937,49 +6267,12 @@ keyDescription = "メールに記載のライセンスキーを貼り付け" success = "ライセンスを有効化しました!" successMessage = "ライセンスは正常に有効化されました。このウィンドウを閉じても構いません。" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "適用済み" -enforcing = "適用中..." -failed = "適用に失敗しました" outputsUnavailable = "ポリシーの出力は、もうダウンロードできません。" partialOutputsUnavailable = "一部のポリシー出力は、ダウンロードできなくなりました。" -retrying = "ビジーのため再試行しています..." runNotFound = "適用の実行が見つからなくなりました。" -step = "ステップ {{current}}/{{total}}" timedOut = "実行が完了する前に適用がタイムアウトしました。" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "閉じる" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} を適用中" applyingProgress = "{{names}} を適用中({{total}} 中 {{done}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} 個のファイルのうち {{failures}} 個を処理 failureTitle = "完全な適用なしでエクスポート" printPolicyAppliedBody = "この PDF はポリシーを満たすよう更新されました。変更内容を確認してから、もう一度印刷してください。" printPolicyAppliedTitle = "印刷前にポリシーを適用" -queued = "+{{count}} 件をキューに追加" successTitle = "{{names}} を適用しました" summaryMore = "{{first}}、{{second}}、ほか {{more}} 件" summaryTwo = "{{first}} と {{second}}" -[policies.enforcement.triggerVerb] -convert = "変換前に適用" -default = "適用中" -export = "エクスポート前に適用" -input = "インポート時に適用" -print = "印刷前に適用" - -[policies.field] -accessLog = "アクセスログ" -archiveAfter = "次の期間後にアーカイブ" -auditTrail = "監査証跡" -belowThreshold = "しきい値未満" -destination = "送信先" -frameworks = "フレームワーク" -immutableHold = "変更不可の保持" -keepFor = "保持期間" -minConfidence = "最小信頼度" -notify = "ルート時に通知" -onViolation = "非準拠時" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "追加" -addPlaceholder = "ラベルを追加…" -customNote = "チーム向けにカスタマイズ済みです。" -defaultNote = "組み込みのデフォルトを使用しており、チームと共有されています。" -duplicate = "\"{{name}}\" はすでに存在します。" -edit = "ラベルを編集" -empty = "ラベルはまだありません。" -export = "JSON をエクスポート" -iconAria = "{{name}} のアイコンを選択" -import = "JSON をインポート" -importError = "そのファイルをインポートできませんでした。" -managedNote = "チームラベルはチームリーダーによって管理されています。" -modalSubtitle = "チーム全体で共有されます。分類機能が各ドキュメントに適したラベルを選択します。" -modalTitle = "分類ラベル" -pickIcon = "{{label}} アイコンを使用" -removeAria = "{{name}} を削除" -resetToDefault = "デフォルトにリセット" -saveForTeam = "チーム用に保存" -saving = "保存中…" -sectionLabel = "分類ラベル" -startFromScratch = "最初から始める" -teamCount = "チームラベル" -tooLong = "ラベルは最大 {{max}} 文字までです。" -ungrouped = "未グループ" -view = "ラベルを表示" +categoryCount = "カテゴリ" +hideCategory = "カテゴリを非表示" +labelCount = "ラベル" +sharedNote = "これらのラベルは組み込みで、チーム全体で共有されます。" +showCategory = "カテゴリを表示" [policies.pii] account = "アカウント番号(ラベル付き)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "米国のルーティング番号(ABA)" ssn = "社会保障番号" -[policies.settings] -noneExport = "現在、エクスポート時に実行されるポリシーはありません。" -noneUpload = "現在、アップロード時に実行されるポリシーはありません。" -onExport = "エクスポート時" -onUpload = "アップロード時" -reorderHandle = "ドラッグして並べ替え" -runOrderDesc = "同じトリガーで複数のポリシーが実行される場合、この順序で実行されます — 各ポリシーは前のポリシーの出力に対して実行されます。ドラッグして並べ替えます。" -title = "ポリシー設定" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "ポリシーオプション" -policySettings = "ポリシー設定" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "失敗したポリシーを再試行({{count}})" -rowProgress = "{{total}} 個中 {{completed}} 個のファイルを処理済み" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "ポリシーとは?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "社会保障番号やカード番号などの機密情報を自動的に検出して黒塗りにし、文書内で読めないようにします。" -sanitize = "ファイルから隠れた JavaScript を削除し、開いたときに自動的に実行されないようにします。" -watermark = "全ページに見えるマーク(例: \"Confidential\")を表示します。" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "閉じる" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} を適用中..." badgeRan = "{{name}} ポリシーがこのファイルで実行されました" +badgeRunning = "{{name}} 実行中..." blockingAction = "ポリシー適用中のため {{action}} がブロックされました。お待ちください..." dismiss = "オーバーレイを閉じる" enforcingTitle = "ポリシーを適用中..." @@ -6151,6 +6316,7 @@ viewAnyway = "ファイルを表示(ポリシーはまだ適用中)" [policyOption] autoRedactPhi = "PHIを自動墨消し" blockExport = "エクスポートをブロック" +ccpa = "CCPA" contracts = "契約書" documents = "ドキュメント" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editorプラン" subscribed = "Processorプラン" unlinked = "未リンク" -[portal.agentBuilder] -bootstrapFromDocument = "ドキュメントから初期生成" -sectionsAriaLabel = "エージェントビルダーのセクション" -selectorAriaLabel = "エージェント" -subtitle = "ドキュメントを分類、抽出、ルーティングする AI エージェントを設計、テスト、公開します。シナリオを定義し、ツールアクセスを制限し、ゴールデンセットを実行して、バージョンを公開します。" -title = "エージェントビルダー" - -[portal.agentBuilder.bootstrap] -cancel = "キャンセル" -dropzoneText = "サンプルドキュメント(PDF または画像)を選択" -lead = "サンプルドキュメントをドロップすると、調整可能なシナリオと抽出スキーマを提案します。レビューするまで何も公開されません。" -submit = "エージェントを初期生成" -subtitle = "代表的なファイルから新しいエージェントを作成" -title = "ドキュメントから初期生成" - -[portal.agentBuilder.empty] -description = "サンプルドキュメントからエージェントを初期生成してシナリオと抽出スキーマの土台を作成し、調整して公開します。" -title = "エージェントはまだありません" - -[portal.agentBuilder.evals] -casesPassing = "合格ケース" -columnCase = "評価ケース" -columnLatency = "レイテンシ" -columnResult = "結果" -fail = "失敗" -goldenSetPassRate = "ゴールデンセット合格率" -latencyMs = "{{ms}} ms" -notRun = "未実行" -pass = "合格" -passRate = "合格率" -runEvals = "評価を実行" - -[portal.agentBuilder.evals.empty] -description = "評価により、シナリオを再現可能なゴールデンセットに変換できます。アップグレードすると、合格率の推移を記録し、それを公開の条件にできます。" -title = "ゴールデンセットはまだありません" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "ゴールデンセット全体" -activeAgents = "アクティブなエージェント" -avgPassRate = "平均評価合格率" -fleetWide = "全体" -latestPublished = "最新の公開済み" -scenarios = "シナリオ" -testCases = "テストケース" -totalDescription_one = "合計 {{count}}" -totalDescription_other = "合計 {{count}}" - -[portal.agentBuilder.scenarios] -add = "追加" -addScenario = "シナリオを追加" -enable = "有効化" -expectationLabel = "期待される動作" -expectationPlaceholder = "エージェントが行うべきこと" -inEval = "評価中" -mute = "ミュート" -muted = "ミュート済み" -nameLabel = "名前" -namePlaceholder = "例:コンプライアンスのエスカレーション" - -[portal.agentBuilder.tabs] -evals = "評価" -scenarios = "シナリオ" -tools = "ツール" -versions = "バージョン" - -[portal.agentBuilder.tools] -broadAccess = "広範なアクセス" -deniedHint = "選択したツールはブロックされています。それ以外は引き続き呼び出せます。" -deniedTools = "拒否されたツール" -governanceGate = "ツールガバナンスは Enterpriseプランで利用できます。" -restricted = "制限付き" -restrictedAccess = "制限付きツールアクセス" -restrictedDescription = "下で拒否したものを除くすべてのツールを許可します。" - -[portal.agentBuilder.versions] -current = "現在" -historyGate = "完全なバージョン履歴とロールバックは Enterpriseプランで利用できます。" -publish = "公開" -rollBack = "ロールバック" - [portal.assistant] close = "アシスタントを閉じる" error = "アシスタントに接続できませんでした。" @@ -6365,9 +6451,21 @@ typing = "入力中" loading = "読み込み中" redirectingToEditor = "エディターにリダイレクト中..." +[portal.billing.activation] +maybeLater = "後で" +title = "Processor を有効化" + +[portal.billing.activation.payg] +sub = "月額上限を設定します。実行した分だけ支払います。" +title = "従量課金" + +[portal.billing.activation.prepay] +badge = "2 か月無料" +sub = "請求書 1 通、カードまたは銀行振込。" +title = "1 年分を前払い" + [portal.billing.checkout] noClientSecret = "Edge function が client_secret を返しませんでした。" -subtitle = "無料の Editorプラン付与分を超えて続行するにはカードを追加してください。残りは Stripe が処理します。" title = "Processorプランを有効にする" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "お支払いは成功しましたが、有効化に通常より少し時 close = "閉じる" title = "もう少しです" +[portal.billing.checkout.cap] +amountAria = "月額利用上限" +back = "戻る" +continue = "支払いへ進む" +error = "利用上限を設定できませんでした" +finePrint = "請求書は毎月発行されます。いつでもキャンセルできます。" +noLimit = "上限なし" +note = "上限を超えて請求されることはありません。処理が一時停止するだけです。" +perMonth = "/月" +title = "利用上限を設定" + +[portal.billing.checkout.card] +fields = "カード番号 · MM / YY · CVC · ZIP" +label = "カード詳細" +note = "カード詳細は Stripe が収集します。Stirling が PAN または CVC を保存することはありません。" + [portal.billing.checkout.error] title = "チェックアウトを開始できませんでした" @@ -6383,10 +6497,9 @@ body = "お支払いが完了しました。リンクされたインスタンス hint = "このウィンドウを開いたままにしてください。" title = "Processorプランを有効化中..." -[portal.billing.checkout.notConfigured] -bodyAfter = "を設定して、アプリ内チェックアウトを有効にしてください。" -bodyBefore = "設定" -title = "Stripe が設定されていません" +[portal.billing.checkout.pay] +limit = "月額利用上限 {{amount}}/月。上限に達すると処理が一時停止します。" +limitNoCap = "利用上限なし。処理は一時停止しません。請求書は毎月発行されます。" [portal.billing.enterpriseUpsell] cta = "Enterprise 見積もりを作成" @@ -6409,10 +6522,12 @@ currentPlan = "現在のプラン" freeForever = "ずっと無料" noTeamResolved = "ウォレット上でチームがまだ解決されていません — 更新してもう一度お試しください。" ownerOnly = "チームオーナーのみが Processorプランを有効にできます。" +payInvoice = "完了するには請求書を支払う" planName = "Editor" ssoIncluded = "SSO 含む" switchOnProcessor = "Processor を有効にする →" unlimitedUsers = "無制限のユーザー" +viewQuote = "見積もりを表示" [portal.billing.invoices] columnAmount = "金額" @@ -6465,7 +6580,111 @@ segmentApiDesc = "直接 API リクエスト" segmentApiLabel = "API" segmentAutomationDesc = "自動化とパイプライン" segmentAutomationLabel = "自動化" +sizeMultiplier = "{{formatted}} PDFs にサイズ倍率が適用されました" +summary = "{{unique}} 個のユニーク · {{units}} メーター単位 · PDF あたり平均 {{avg}}" +summaryNoRate = "{{unique}} 個のユニーク · {{units}} メーター単位" unit = "従量制PDF" +unitsPending_one = "リンク済みインスタンスから {{units}} メーター単位の同期待ち" +unitsPending_other = "リンク済みインスタンスから {{units}} メーター単位の同期待ち" + +[portal.billing.prepaid] +capSuffix = "{{total}} 前払いクレジット中" +expires = "有効期限 {{date}}" +eyebrow = "前払い容量" +note = "従量課金の前、および利用上限の外で使用されます。" +topUp = "追加購入" + +[portal.billing.prepaid.buy] +back = "戻る" +calcTitle = "年間支払いを計算" +cancel = "キャンセル" +cancelPurchase = "購入をキャンセル" +close = "閉じる" +continueToPayment = "支払いへ進む" +finalise = "確定" +invoiceErrorTitle = "請求書を生成できませんでした" +loadingTitle = "見積もりを読み込んでいます" +payTitle = "1 年分を支払う" +step = "ステップ {{current}} / {{total}}" + +[portal.billing.prepaid.calc] +change = "変更" +deployRow = "デプロイ" +done = "完了" +enterpriseHint = "これはエンタープライズ規模です。このボリュームでは、エンタープライズ料金がセルフサービス割引を上回ります。料金固定、条件、見積もりを数分で取得できます。" +governanceRow = "ガバナンス" +governanceValue = "{{label}} · {{count}} policies" +handlesLabel = "あなたの Processor" +handlesValue = "{{volume}} クレジット/月を処理" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policies" +poolCaption = "年間 {{credits}} クレジットの 1 つのプール。使用量の多い月は少ない月から借りられます。" +priceLabel = "年間 · 10 か月分の料金で 12 か月" +rateUnknown = "チェックアウト時に正確な価格を表示します。" +savings = "{{amount}} お得 · 2 か月無料。" +sizingRow = "サイズ設定" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "総ユーザー数" + +[portal.billing.prepaid.consent] +label = "前払い容量を使い切った場合、または 12 か月後に期限切れになった場合、キャンセルしない限り標準の従量課金料金(利用上限まで)で処理が自動的に継続されること、また請求ポータルからいつでもキャンセルできることを理解しています。" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling による管理。数分で利用開始。" +cloudValue = "Stirling Cloud · 管理済み" +selfhost = "セルフホスト" +selfhostDesc = "プライベートインフラで実行。同じ料金。" +selfhostValue = "セルフホスト · プライベートインフラ" + +[portal.billing.prepaid.offer] +cta = "オファーを確認" +subtitle = "PDF 処理を 1 年分前払いすると 2 か月無料になります。従量課金の前、利用上限の外で使用されます。" +title = "10 か月分の料金で 12 か月利用" + +[portal.billing.prepaid.pay] +accountName = "口座名義人名" +accountNamePlaceholder = "例: Jane Smith" +companyName = "会社名(任意)" +companyNamePlaceholder = "例: Acme Inc." +downloadInvoice = "請求書をダウンロード" +payOnline = "オンラインで支払う" +poNumber = "PO 番号(任意)" +poNumberPlaceholder = "例: PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "高度" +advancedDesc = "Pipelines が処理の大部分を担います。" +none = "なし" +noneDesc = "まだ Pipelines を実行していません。いつでも有効にできます。" +standard = "標準" +standardDesc = "いくつかの Pipelines が到着した PDFs を再処理します。" + +[portal.billing.prepaid.posture] +essentials = "基本" +essentialsDesc = "分類(デフォルト)に加えて共有" +governed = "管理対象" +governedDesc = "セキュリティとルーティングを追加" +regulated = "規制対応" +regulatedDesc = "Compliance、Retention、Ingestion を含むすべてのカテゴリ" + +[portal.billing.prepaid.review] +download = "見積もりをダウンロード(PDF)" +downloading = "準備中…" +share = "承認のために共有" + +[portal.billing.prepaid.size] +compact = "コンパクト" +compactDesc = "25 MB 未満のファイル、データ料金なし" +heavy = "ヘビー" +heavyDesc = "スキャンまたは画像が多く、通常 50 MB 以上" +standard = "標準" +standardDesc = "主に小さく、一部のスキャンが 25 MB 超" + +[portal.billing.prepaid.state] +exhausted = "使い切りました" +healthy = "十分に残っています" +low = "残りわずか" [portal.billing.spendLimit] adjustLimit = "上限を調整" @@ -6494,6 +6713,8 @@ label = "超過が予測されています。" [portal.billing.spendThisMonth] eyebrow = "今月の利用額" +freeRemaining_one = "無料 PDF が {{formatted}} 件残っています" +freeRemaining_other = "無料 PDFs が {{formatted}} 件残っています" processed_one = "{{formattedCount}} 件のPDFを処理しました。" processed_other = "{{formattedCount}} 件のPDFを処理しました。" processedWithRate_one = "{{formattedCount}} 件のPDFを処理しました。単価は {{rate}} です。" @@ -6510,6 +6731,7 @@ reachedTitle = "月間利用額上限に達しました" title = "Stripe ポータルを開けませんでした" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} 件の無料PDFのうち使用済み" capSuffix_other = "{{allowance}} 件の無料PDFのうち使用済み" eyebrow = "Processor トライアル" @@ -6534,6 +6756,376 @@ signature = "署名" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "新しい接続" +createTitleFor = "{{name}} に接続" +delete = "削除" +edit = "編集" +editTitle = "接続を編集" +unsupportedType = "この接続タイプはここでは編集できません。" + +[portal.connections.categories.advanced] +description = "まだサポートしていないサービス向け。" +label = "高度" + +[portal.connections.categories.audit] +description = "ドキュメントに何が起きたかを、監査担当者がすでに確認する場所に記録します。" +label = "監査 & コンプライアンスログ" + +[portal.connections.categories.notify] +description = "何かが発生したことを別のシステムに通知します。" +label = "通知 & ワークフロー" + +[portal.connections.categories.security] +description = "ドキュメントが次に進む前にスキャンまたはラベル付けします。" +label = "セキュリティ & 分類" + +[portal.connections.categories.signing] +description = "署名または公証のためにドキュメントを送信します。" +label = "電子署名 & 公証" + +[portal.connections.categories.storage] +description = "処理済みドキュメントの保存先 - バケット、ドライブ、チケット、またはページ。" +label = "ファイル & 添付" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling はこれを暗号化して保存し、二度と表示しません。" +label = "アクセストークン" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "この接続を使用するステップは、この URL 配下のパスのみ呼び出せます。別のホストは呼び出せません。" +label = "サーバー URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "サービスから提供された URL を貼り付けてください。その配下のパスのみ呼び出せます。" +label = "Webhook URL" + +[portal.connections.fields] +name = "接続名" +namePlaceholder = "例: Production" + +[portal.connections.picker] +cancel = "キャンセル" +createNew = "新しい接続..." +placeholder = "接続を選択" +save = "接続を保存" + +[portal.connections.picker2] +back = "統合を変更" +heading = "何に接続しますか?" +noResultsBody = "まだサポートしていません - 詳細をお知らせください。追加を検討します。" +noResultsTitle = "\"{{query}}\" に一致するものがありません" +searchPlaceholder = "統合を検索 - 「sign」、「ocr」、または製品名を試してください" + +[portal.connections.types.api] +description = "policy から任意の HTTP API を呼び出します。URL、認証、ペイロードを選択します。" +label = "カスタム API" + +[portal.connections.types.api.fields.authType] +label = "認証" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username と password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "カスタムヘッダー内のトークン" + +[portal.connections.types.api.fields.authType.options.none] +label = "なし" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "トークン取得のためログイン" + +[portal.connections.types.api.fields.baseUrl] +helperText = "この接続を使用するステップは、この URL 配下のパスのみ呼び出せます。別のホストは呼び出せません。" +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "ヘッダー名" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "ログインパス" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API がダウンロードリンクを返す場合、これらのホストのみ取得できます。カンマで区切ってください。空白の場合は base URL 自身のホストのみ許可されます。" +label = "許可された結果ホスト" + +[portal.connections.types.api.fields.token] +label = "トークン" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "このヘッダーとしてトークンを送信" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "ログイン呼び出しがトークンを返すレスポンスヘッダー。例: X-Auth-Token。" +label = "トークンレスポンスヘッダー" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "運用しているサーバー上でドキュメントのマルウェアをスキャンします - ネットワーク外には何も出ません。" +label = "ClamAV(セルフホスト)" + +[portal.connections.types.cloudmersive] +description = "policy 内のドキュメントに対するウイルスおよびコンテンツスキャン。" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "既知のマルウェアだけでなく、マクロ、実行ファイル、スクリプト、埋め込みオブジェクトを含むドキュメントをブロックします。" +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "処理済みドキュメントを Confluence ページに添付します。" +label = "Confluence" + +[portal.connections.types.consigno] +description = "電子署名と公証のためにドキュメントを ConsignO Cloud(Notarius)に送信します。" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "/api/v1 を含む ConsignO Cloud 環境。" +label = "サーバー URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "組織がテナントを使用している場合にのみ必要です。" +label = "テナント" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "policy がドキュメントを処理したときに Discord へメッセージを投稿します。" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "policy がドキュメントを処理するたびに Elasticsearch に監査イベントをインデックス化します。" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "新しいドキュメントを FTP または FTPS サーバーからポーリングします。" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "接続モード" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive(推奨)" + +[portal.connections.types.ftp.fields.security] +helperText = "通常の FTP は認証情報とファイルを暗号化せずに送信します。サーバーが対応している場合は FTPS を選択してください。" +label = "暗号化" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS(explicit / AUTH TLS)" +implicit = "FTPS(implicit)" +none = "なし(通常の FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "policy 実行時に Google Chat スペースへメッセージを投稿します。" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "処理済みドキュメントを Jira issue に添付します。" +label = "Jira" + +[portal.connections.types.mailgun] +description = "policy 実行時にドキュメントまたは通知をメール送信します。" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "処理済みドキュメントを Nextcloud に保存します。" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "運用している Presidio サーバーを使用してドキュメントテキスト内の個人データを検出します。" +label = "Presidio(セルフホスト)" + +[portal.connections.types.purview] +description = "PDF 上の Microsoft Purview 秘密度ラベルを読み取り、適用します。" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "任意。名前でラベルを選択する場合にのみ必要です。ラベル付けはこれがなくても動作します。" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "すべてのラベルに SiteId としてスタンプされるため、Purview 対応ツールはどの組織が適用したかを認識できます。" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "ソースが読み取り、pipeline 出力が書き込みできる S3 バケット。" +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS の場合は空白のままにします。MinIO または別の S3 互換ストアの場合に設定します。" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "policy 実行時にドキュメントまたは通知をメール送信します。" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "新しいドキュメントを SFTP(SSH)サーバーからポーリングします。" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "任意。サーバーの SHA-256 ホスト key fingerprint(`ssh-keyscan host | ssh-keygen -lf -` から取得)。設定すると、この key を提示するサーバーのみ受け入れられます。空白の場合、初回接続時に確認された key が固定され、以後の変更は拒否されます。" +label = "ホスト key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "代わりに private key で認証する場合は空白のままにします。" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "任意。password の代わりに key で認証するには OpenSSH private key を貼り付けてください。" +label = "Private key" + +[portal.connections.types.slack] +description = "policy がドキュメントを処理したときに Slack へメッセージを投稿します。" +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS ネットワーク共有(Windows または Samba)を監視します。" +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "任意。サーバーが使用している場合の Windows domain または workgroup。" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "policy がドキュメントを処理するたびに Splunk へ監査イベントを送信します。" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "監査イベントを Sumo Logic HTTP collector に送信します。" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "policy 実行時に Teams チャンネルへメッセージを投稿します。" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "各ドキュメント、または詳細のみを選択した URL に投稿します。" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "policy から Zap または Make シナリオをトリガーします。" +label = "Zapier & Make" + +[portal.docs] +browse = "ドキュメントを参照" +viewSource = "GitHub でソースを表示" + [portal.docs.authentication] codeCaption = "すべてのリクエスト" eyebrow = "はじめに" @@ -6624,6 +7216,9 @@ eyebrow = "スキル" lead = "エージェントが単一ツールとして呼び出す、バンドルされた名前付き機能です。各スキルは評価付きの決定論的な操作チェーンです。" title = "エージェントスキル" +[portal.docs.toc] +title = "このページの内容" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API リファレンス" @@ -6697,6 +7292,8 @@ type = "種類" user = "ユーザー" [portal.documents.queue.empty] +connectSource = "ソースに接続" +createPipeline = "pipeline を作成" description = "ソースがパイプラインにドキュメントを供給すると、レビュー用にここに表示されます。" title = "キューにドキュメントはありません" @@ -6712,11 +7309,11 @@ editorAction = "エディター" empty = "このフィルターに一致するドキュメントはありません。" rowActions = "行のアクション" sensitiveLabel = "機密" -sensitiveTitle = "機密 — アクセスが必要" [portal.documents.table.columns] action = "パイプライン / アクション" document = "ドキュメント" +labels = "Labels" product = "製品" status = "ステータス" time = "時刻" @@ -6732,6 +7329,7 @@ host = "ホスト" lastSeen = "最終確認" region = "リージョン" status = "ステータス" +target = "Target" version = "バージョン" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "このビューで予期しないエラーが発生しました retry = "再試行" title = "このページで問題が発生しました" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "戻る" +body = "デスクトップアプリをダウンロードするか、サーバーをセルフホストします。" +done = "完了" +guide = "{{name}} ガイドを開く" +openInBrowser = "代わりにブラウザーで開く" +sectionDesktop = "デスクトップアプリ" +sectionSelfHosted = "セルフホスト" +title = "Stirling PDF Editor をインストール" + +[portal.home.download.docker] +detailBody = "公式イメージをサーバーで実行します。" +note = ":8080 で開きます。デフォルトログイン: admin / stirling。" +tagline = "コンテナでサーバーを実行" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "公式 Helm チャートをインストールします。" +note = "設定を永続化するには、/configs 用の PVC を追加してください。" +tagline = "Helm でクラスターにデプロイ" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "お使いのディストリビューション向けのネイティブビルド。" +guideBtn = "Linux インストールオプションを表示" +note = "一般的なディストロ向けに AppImage、.deb、.rpm ビルドを利用できます。" +tagline = "お使いのディストロ向けネイティブデスクトップアプリ" +title = "Linux" + +[portal.home.download.mac] +altLabel = "または Homebrew 経由" +detailBody = "インストーラーを実行するか、Homebrew を使用します。" +downloadBtn = "macOS 用にダウンロード" +tagline = "Apple silicon と Intel 向けネイティブアプリ" +title = "macOS" + +[portal.home.download.manual] +detailBody = ".jar をベアメタルサーバーで実行します。" +downloadBtn = ".jar をダウンロード" +note = "Java 21+ が必要です。:8080 で開きます。デフォルトログインは admin / stirling です。" +runLabel = "次に実行" +tagline = ".jar をベアメタルサーバーで実行" +title = "手動サーバーセットアップ" + +[portal.home.download.windows] +altLabel = "または winget 経由" +detailBody = "インストーラーを実行するか、パッケージマネージャーを使用します。" +downloadBtn = "インストーラーをダウンロード" +tagline = "Microsoft Windows 向けネイティブアプリ" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} アクティブ" -invite = "チームメイトを招待" +activeOfDeployed = "今月は {{total}} 件中 {{active}} 件がアクティブ" name = "Stirling PDF Editor" open = "ブラウザーで開く" updated = "{{time}} 更新" +[portal.home.editor.deploy] +finish = "デプロイを完了" +options = "デプロイオプションを表示" +start = "Editor をデプロイ" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "セルフホスト型 · Docker" @@ -6823,49 +7529,6 @@ afternoon = "こんにちは" evening = "こんばんは" morning = "おはようございます" -[portal.home.onboarding] -dismiss = "セットアップを閉じる" -notStarted = "未開始" -progress = "{{total}} 件中 {{done}} 件完了" -title = "セットアップを完了" - -[portal.home.onboarding.enterprise] -body = "組織全体の SSO + SCIM + RBAC、コミット済みボリューム料金、エアギャップ環境でのデプロイメント。" -cta = "トライアルを開始" -ctaQuote = "見積もりを取得" -lead = "従業員 250 名以上向け。" -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "デスクトップアプリをデプロイ。永久無料です。" -title = "PDF Editor をダウンロード" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} 件アクティブ · {{recommended}} 件推奨" -chip = "{{active}} 件アクティブ" -title = "ポリシーを有効化" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} 件接続済み · 配置先を問わずすべての PDF を管理" -chip = "{{connected}} 件接続済み" -title = "ソースを接続" - -[portal.home.quickActions] -subtitle = "今日の主要タスク" -title = "クイックアクション" - -[portal.home.quickActions.buildPipeline] -blurb = "型付き操作ライブラリを使った 3 ステップのコンポーザー" -title = "パイプラインを構築" - -[portal.home.quickActions.connectSource] -blurb = "S3、エージェント、webhook、監視フォルダー" -title = "ソースを接続" - -[portal.home.quickActions.issueApiKey] -blurb = "レート制限と IP allowlist 付きのスコープ化されたキー" -title = "API キーを発行" - [portal.infrastructure] manageEditorDeployment = "Editor デプロイメントを管理" sectionsAriaLabel = "インフラストラクチャセクション" @@ -6878,13 +7541,9 @@ heading = "API キー" subheading = "キーごとのレート制限、権限、IP allowlist を備えたスコープ化された認証情報。" [portal.infrastructure.apiKeys.card] -allowedIps = "許可された IP" -anyIp = "任意の IP(allowlist なし)" created = "作成日" lastUsed = "最終使用" -permissions = "権限" -rateLimit = "レート制限" -rateLimitValue = "{{value}} req/min" +revoke = "キーを取り消す" usageMonth = "今月の使用量" usageToday = "今日の使用量" @@ -6892,6 +7551,15 @@ usageToday = "今日の使用量" description = "スコープ化されたキーを作成して Stirling API の呼び出しを開始します。" title = "API キーはまだありません" +[portal.infrastructure.apiKeys.error] +load = "API キーを読み込めませんでした。もう一度お試しください。" + +[portal.infrastructure.apiKeys.revoke] +body = "「{{name}}」を取り消しますか?このキーをまだ使用している連携は、ただちに 401 エラーを受け取り始めます。これは元に戻せません。" +cancel = "キャンセル" +confirm = "キーを取り消す" +title = "API キーを取り消す" + [portal.infrastructure.audit] filterAriaLabel = "カテゴリで監査イベントをフィルター" heading = "監査ログ" @@ -6935,6 +7603,7 @@ all = "すべて" auth = "認証" config = "設定" elevation = "昇格" +policy = "ポリシー" processing = "処理" security = "セキュリティ" @@ -6944,19 +7613,30 @@ title = "アクセス権がありません" [portal.infrastructure.audit.metrics] config = "設定" -elevation = "昇格" +policy = "ポリシー" processing = "処理" totalEvents = "合計イベント · 24 時間" +[portal.infrastructure.auditCatLabel] +auth = "認証" +config = "設定" +elevation = "権限昇格" +policy = "ポリシー" +processing = "処理" +security = "セキュリティ" + +[portal.infrastructure.auditStatusLabel] +danger = "エラー" +info = "情報" +success = "成功" +warning = "警告" + [portal.infrastructure.createKey] cancel = "キャンセル" createKey = "キーを作成" done = "完了" -ipAllowlistHelper = "カンマ区切りの CIDR 範囲。任意の IP を許可するには空欄のままにします。" -ipAllowlistLabel = "IP allowlist" keyNameLabel = "キー名" keyNamePlaceholder = "例: Production · ingest" -permissionsLabel = "権限" secretKeyCaption = "シークレットキー" secretWarning = "これをシークレットマネージャーに保存してください。Stirling はハッシュのみを保存します — 後で復元する方法はありません。" subtitle = "キーのスコープを必要最小限にします。いつでもローテーションまたは失効できます。" @@ -6964,207 +7644,9 @@ subtitleCreated = "今すぐこのシークレットをコピーしてくださ title = "API キーを作成" titleCreated = "キーが作成されました" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "デプロイ者" -environment = "環境" -product = "製品" -status = "ステータス" -version = "バージョン" -when = "日時" - -[portal.infrastructure.deployments.recent] -heading = "最近のデプロイメント" -subheading = "製品と環境全体の最新のロールアウト。" - -[portal.infrastructure.deployments.regionColumns] -instances = "インスタンス" -latency = "レイテンシ" -load = "負荷" -p99 = "P99" -region = "リージョン" -status = "ステータス" -throughput = "スループット" -uptime = "稼働時間" -version = "バージョン" - -[portal.infrastructure.deployments.regions] -heading = "リージョン" -subheading = "デプロイ済みのすべての Stirling リージョンのライブ健全性 — レイテンシ、負荷、ロールアウトバージョン。" - -[portal.infrastructure.deployments.regions.empty] -description = "ワークスペースがプロビジョニングされると、デプロイ済みリージョンがここに表示されます。" -title = "デプロイ済みリージョンはありません" - -[portal.infrastructure.models] -heading = "モデル" -msValue = "{{value}} ms" -subheading = "ワークスペース全体のドキュメント処理を支えるモデルカタログとルーティング。" - -[portal.infrastructure.models.byom] -description = "オンプレミスまたはセルフホスト型モデルを登録し、データレジデンシー制約のある処理のためにリージョンに固定します。" -title = "独自モデルを持ち込む" - -[portal.infrastructure.models.catalogue] -heading = "カタログ" -sub = "ワークスペースで利用可能なマネージドモデル。ライブのレイテンシとコスト付き。" -subEnterprise = "マネージド、持ち込み、オンプレミスモデル — リージョンごとの固定が利用可能。" - -[portal.infrastructure.models.catalogue.empty] -description = "ワークスペースのカタログ内のモデルがここに表示されます。" -title = "利用可能なモデルはありません" - -[portal.infrastructure.models.columns] -cost = "コスト" -latency = "レイテンシ" -load = "負荷" -model = "モデル" -status = "ステータス" -type = "種類" -version = "バージョン" - -[portal.infrastructure.models.metrics] -activeModels = "アクティブモデル" -avgLatency = "平均レイテンシ" -included = "含まれる" -monthlySpend = "月間モデル費用" - -[portal.infrastructure.models.routing] -empty = "ルーティングルールは設定されていません。" -heading = "ルーティングルール" -sub = "各操作をどのモデルが処理するか。より限定的なルールに一致しない場合はデフォルトが適用されます。" -subLocked = "操作を特定のモデルにルーティング — 有料プランで利用可能です。" - -[portal.infrastructure.models.routing.lockedBanner] -description = "Pro にアップグレードすると、各操作とドキュメントタイプをどのモデルが処理するかを制御できます。" -title = "モデルルーティングは有料機能です" - -[portal.infrastructure.models.routingColumns] -default = "デフォルト" -docType = "ドキュメントタイプ" -modelForAria = "{{operation}} のモデル" -operation = "操作" -routedTo = "ルーティング先" - -[portal.infrastructure.security.access.byok] -description = "独自の KMS からキーを提供します。Stirling はそのキーで暗号化しますが、引き続き読み取り可能です。" -label = "独自キーを持ち込む(BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "キーは KMS から外に出ません。Stirling は暗号文のみを保持します。" -label = "独自キーを保持する(HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling が暗号化キーを管理します。最も簡単で、ユーザー側のキー運用は不要です。" -label = "Stirling 保持のキー" - -[portal.infrastructure.security.accessPolicy] -heading = "ドキュメントアクセスポリシー" -subheading = "保存中の処理済みドキュメントを誰が復号できるかを制御します。" - -[portal.infrastructure.security.attestations] -heading = "コンプライアンス証明" -noReport = "利用可能なレポートはありません" -subheading = "フレームワークごとの監査態勢。証明済み統制についてはレポートを利用できます。" -viewReport = "レポートを表示 →" - -[portal.infrastructure.security.compliance] -heading = "コンプライアンス" -subheading = "Stirling プラットフォームを対象とする証明と認証。" - -[portal.infrastructure.security.empty] -description = "ワークスペースのセキュリティ設定がここに表示されます。" -title = "セキュリティ態勢を利用できません" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK では、暗号化キーが KMS から外に出ることはありません。Stirling は暗号文のみを保存および処理し、いつでも取り消すことができます。" -title = "Stirling はドキュメントを復号できません" - -[portal.infrastructure.security.ipAllowlist] -empty = "設定された IP 範囲はありません — すべての IP が許可されています。" -heading = "IP allowlist" -sub = "API アクセスはこれらの CIDR 範囲に制限されています。" -subLocked = "API アクセスを既知の IP 範囲に制限 — 有料プランで利用可能です。" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Pro にアップグレードすると、API アクセスを特定のネットワークに制限できます。" -title = "IP allowlist は有料機能です" - -[portal.infrastructure.security.ipColumns] -added = "追加日" -addedBy = "追加者" -cidr = "CIDR" -label = "ラベル" - -[portal.infrastructure.security.keyManagement] -algorithm = "アルゴリズム" -heading = "暗号化キー管理" -keyId = "キー識別子" -lastRotated = "最終ローテーション" -rotateKey = "キーをローテーション" -rotationPolicy = "ローテーションポリシー" -subheading = "保存中のドキュメントを暗号化するキーの管理 — 誰が復号できるか、キーをどのようにローテーションするか。" - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key(BYOK)と hold-your-own-key(HYOK)の管理は Enterprise で利用できます。独自の KMS からキーを提供するにはアップグレードしてください。" -title = "お使いのプランではキーは Stirling によって管理されます" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "アジア太平洋" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR データ境界" -label = "欧州連合" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "米国" - -[portal.infrastructure.security.residencyHeader] -heading = "データレジデンシー" -subheading = "ドキュメントが保存および処理される場所。" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} 使用済み" - -[portal.infrastructure.storage.empty] -description = "接続されたストレージと使用量がここに表示されます。" -title = "ストレージが設定されていません" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "アクティブ" -activeRange = "0–{{value}}d" -archived = "アーカイブ済み" -coldStorage = "コールドストレージ" -deleted = "削除済み" -never = "なし" -purged = "パージ済み" - -[portal.infrastructure.storage.providers] -connect = "接続" -connected = "接続済み" -heading = "接続済みプロバイダー" -subheading = "処理済みアーティファクトの書き込み先。" - -[portal.infrastructure.storage.retention] -heading = "保持" -subheading = "ライフサイクル削除までアーティファクトを保持する期間。" -windowLabel = "デフォルトの保持期間" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} 日" -days_other = "{{count}} 日" -never = "削除しない" - -[portal.infrastructure.storage.totalUsage] -heading = "合計使用量" -progressLabel = "使用済みストレージ" -subheading = "接続済みプロバイダー全体で消費されたストレージ。" +revoked = "取り消し済み" [portal.infrastructure.tabs] apiKeys = "API キー" @@ -7174,6 +7656,49 @@ models = "モデル" security = "セキュリティ" storage = "ストレージ" +[portal.integrations] +availableHeading = "利用可能" +comingSoonHeading = "近日対応予定" +connect = "接続" +connectionCount_one = "{{count}} 件の接続" +connectionCount_other = "{{count}} 件の接続" +customApi = "カスタム API" +searchPlaceholder = "連携を検索" +subtitle = "Stirling が通信できるすべてのものです。認証情報はここで一度作成され、その後ソース、ポリシー、パイプラインから参照されます。" +title = "連携" +typedName = "{{tool}} 名" + +[portal.integrations.filters] +all = "すべて" +audit = "監査" +connected = "接続済み" +notify = "通知" +security = "セキュリティ" +signing = "署名" +storage = "ストレージ" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "接続済み" + +[portal.integrations.table] +integration = "連携" +worksWith = "連携先" + +[portal.integrations.worksWith] +pipelines = "パイプライン" +policies = "ポリシー" +sources = "ソース" + +[portal.legal] +draft = "{{label}} · 下書き" +loadError = "このドキュメントを読み込めませんでした。もう一度お試しください。" +loading = "読み込み中…" +title = "法的文書" + [portal.nav] agent-builder = "Agent Builder" components = "コンポーネント" @@ -7182,33 +7707,17 @@ documents = "ドキュメント" editor = "Editor" home = "ホーム" infrastructure = "インフラストラクチャ" +integrations = "連携" pipelines = "パイプライン" policies = "ポリシー" procurement = "調達" -settings = "設定" sources = "ソース" usage = "使用量と請求" users = "ユーザー" -[portal.notifications] -markAllRead = "すべて既読にする" -title = "通知" -viewAll = "すべて表示" - -[portal.notifications.ariaLabel] -none = "通知、未読なし" -unread_one = "通知、{{count}} 件未読" -unread_other = "通知、{{count}} 件未読" - -[portal.notifications.count] -allRead = "すべて既読" -loading = "読み込み中" -new_one = "{{count}} 件の新着" -new_other = "{{count}} 件の新着" - -[portal.notifications.empty] -description = "新しい通知はありません。" -title = "すべて確認済みです" +[portal.nav.section] +platform = "PDF プラットフォーム" +processor = "PDF プロセッサー" [portal.pipelines] subtitle = "バックエンド上のすべての自動ドキュメントパイプライン: ソースセットに対する操作の順序付きチェーンで、トリガーによって実行されます。行をクリックすると、そのステップとソースが表示されます。" @@ -7218,54 +7727,87 @@ title = "パイプライン" newPipeline = "新しいパイプライン" [portal.pipelines.builder] -addStep = "ツールを追加" +activate = "Activate" back = "パイプラインに戻る" +cannotFollow = "{{produced}} を受け取れません" +chooseAccount = "アカウントを選択" +chooseDestination = "宛先を選択" +chooseOperation = "このステップの処理を選択" +chooseSource = "ソースを選択" discard = "変更を破棄" -enabled = "有効" +inputs = "入力" +inputSource = "入力ソース" +inputTrigger = "トリガー" keepEditing = "編集を続ける" -needsUpload = "アップロードされたファイルが必要" +moreActions = "More actions" +needsConfiguring = "設定が必要です" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "検索に一致するツールはありません。" -pipelineSettings = "パイプライン設定" +pause = "Pause" +rename = "Rename pipeline" searchTools = "ツールを検索" -selectToolBody = "ツールを追加してパイプラインを構築します。" -selectToolTitle = "ツールはまだありません" -toolSettings = "ツール設定" +sendToSystem = "別のシステムへ送信" +stepsIncompatible = "これらのステップは、前のステップが生成するものでは実行できません: {{tools}}。" +stepsNeedSetup = "保存する前に、これらのステップの設定がまだ必要です: {{tools}}。" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "認識できない操作です。そのまま保持されました。" unsavedBody = "未保存の変更があります。離れる前に保存しますか、それとも破棄しますか?" unsavedTitle = "未保存の変更" -uploadUnsupported = "アップロードされたファイルはまだパイプラインでサポートされていないため、これらのステップは保存できません: {{tools}}。" usesDefaults = "デフォルト設定で実行" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "前のステップからのすべてのファイルを結合します" +fan-out = "前のステップからのファイルごとに 1 回実行します" +format-mismatch = "{{accepts}} が必要ですが、前のステップは {{produced}} を生成します" +output-uncertain = "実行されない可能性があります: 前のステップの出力は設定方法に依存します" +source-mismatch = "{{accepts}} が必要ですが、このパイプラインの入力は {{produced}} です" +undeclared-operation = "このステップが受け付けるものを確認できません" [portal.pipelines.composer] addTool = "ツールを追加" -cancel = "キャンセル" -chainEmpty = "ツールを追加してパイプラインの構築を開始します。" create = "パイプラインを作成" -directory = "出力フォルダー" -directoryHelp = "サーバー上の絶対パス。設定された許可フォルダー内である必要があります。" +createPaused = "Create paused" editingUnsupported = "これらのツールパラメーターを編集用に表示することはまだサポートされていません。" -moveDown = "下へ移動" -moveUp = "上へ移動" +editSource = "Edit source" name = "名前" namePlaceholder = "例: Redaction sweep" -noSources = "まだ接続されたソースはありません。パイプラインは、直接提供されたファイルに対して引き続き実行できます。" noToolSettings = "このツールには設定可能な項目がありません。" -operations_one = "操作({{count}})" -operations_other = "操作({{count}})" output = "出力" -removeStep = "操作を削除" save = "変更を保存" scheduleEvery = "実行間隔" -sources = "ソース" -sourcesLoading = "ソースを読み込み中..." trigger = "トリガー" triggerManual = "手動のみ" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "日" hours = "時間" minutes = "分" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" を削除しますか?この操作は元に戻せません。" cancel = "キャンセル" @@ -7273,28 +7815,62 @@ confirm = "削除" title = "パイプラインを削除しますか?" [portal.pipelines.detail] +clearHistory = "履歴をクリア" delete = "パイプラインを削除" run = "今すぐ実行" [portal.pipelines.empty] action = "パイプラインを作成" +connectSource = "ソースを接続" description = "最初のパイプラインを作成します: 実行対象のソースを選び、操作を連結し、出力先を選択します。" title = "パイプラインはまだありません" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "アクティブ" paused = "一時停止中" total = "パイプライン" -[portal.pipelines.output] -folder = "フォルダーに書き込む" -inline = "ファイルを返す" - [portal.pipelines.run] +allProcessed_one = "実行するものがありません: ソースの {{count}} 件のドキュメントはすでに処理済みです。" +allProcessed_other = "実行するものがありません: ソース内の全 {{count}} 件のドキュメントはすでに処理済みです。" completed_one = "実行が完了しました。" completed_other = "{{count}} 件の実行がすべて完了しました。" empty = "実行するものがありません: ソースに処理対象のドキュメントがありませんでした。" failed = "実行に失敗しました: {{error}}" +inFlight = "新しく実行するものがありません: 以前の実行のドキュメントがまだ処理中です。" +parked_one = "実行するものがありません: {{count}} 件のドキュメントは以前に失敗して保留中です。原因を修正してから、履歴をクリアして再試行してください。" +parked_other = "実行するものがありません: {{count}} 件のドキュメントは以前に失敗して保留中です。原因を修正してから、履歴をクリアして再試行してください。" running = "実行を開始しました。まだ進行中です。" timeout = "実行に予想より時間がかかっています。バックグラウンドで完了する可能性があります。" @@ -7307,6 +7883,7 @@ name = "パイプライン" sources = "ソース" status = "ステータス" steps = "ステップ" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "フォルダー監視" @@ -7314,12 +7891,18 @@ manual = "手動" schedule = "スケジュール済み" [portal.policies] +defaultName = "{{category}} ポリシー" subtitle = "すべてのドキュメントにツールパイプラインを適用する常設の自動化です。各ポリシーはアップロードまたはエクスポート時に発火し、ツールチェーンを実行し、適用済みバージョンを元のファイルと並べて保存します。" title = "ポリシー" [portal.policies.card] comingSoon = "Enterprise にアップグレード" notSetUp = "未設定" +requiresAiEngine = "AI エンジンが必要です" + +[portal.policies.categories.classification] +desc = "アップロード時に各ドキュメントの種類を識別し、ファイリングと検索のためにメタデータにタグ付けします。" +label = "分類" [portal.policies.categories.compliance] desc = "すべてのドキュメントに HIPAA、GDPR、SOC 2、または FedRAMP 要件を適用します。" @@ -7344,6 +7927,13 @@ label = "セキュリティ" [portal.policies.config] scopeAll = "すべてのドキュメント" +[portal.policies.config.classification] +summary = "アップロードされたすべてのドキュメントをチームのラベルに照らして分類し、メタデータにタグ付けします。" + +[portal.policies.config.classification.rules] +0 = "分類" +1 = "メタデータにタグ付け" + [portal.policies.config.compliance] summary = "ドキュメントがシステムを離れる前に、規制フレームワークに照らして検証します。" @@ -7371,6 +7961,29 @@ minConfidence = "最小信頼度" 2 = "命名" 3 = "正規化" +[portal.policies.config.purview] +label = "Microsoft Purview 感度ラベルを適用" + +[portal.policies.config.purview.fields] +connection = "Purview テナント" +labelId = "ラベル ID" +labelIdHelp = "Purview ポータルからのラベルの GUID です。ラベルを適用すると、そのメタデータが PDF に書き込まれます。暗号化はできません。暗号化には Microsoft クライアントが必要です。" +labelName = "ラベル名(任意)" +labelNameHelp = "人やツールがラベルを認識できるように、ID と一緒に記録されます。" + +[portal.policies.config.purview.fields.method] +help = "Purview は、ポリシーが適用したラベルと、ユーザーが選択したラベルを区別します。" +label = "ラベルの記録方法" +privileged = "ユーザーが選択したものとして" +standard = "自動適用されたものとして" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview ラベルを読み取る" + +[portal.policies.config.purviewRead.fields] +connection = "Purview テナント" +connectionHelp = "ドキュメントにすでに付与されているラベルを読み取り、後続のステップがそれに基づいて動作できるようにします。" + [portal.policies.config.retention] summary = "ドキュメントの保持期間、アーカイブ時期、削除時期を適用します。" @@ -7438,6 +8051,7 @@ title = "まだアクティビティはありません" [portal.policies.endpoints] addWatermark = "透かし" autoRedact = "PII を墨消し" +classifyAndLabel = "分類" compressPdf = "圧縮" flatten = "フラット化" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "ポリシーは保存されており、接続が復元される retry = "再試行" title = "Backend を利用できません" +[portal.policies.operations] +change = "このステップの処理を変更" +noResults = "一致するステップがありません。製品名、または「scan」「notify」「attach」を試してください。" +searchPlaceholder = "ステップを検索 - 「scan」「attach」または製品名を試してください" + +[portal.policies.operations.bodyMode] +binary = "生のファイルバイト" +json = "JSON ペイロード内" +multipart = "ファイルアップロードとして(multipart)" + +[portal.policies.operations.clamavScan] +description = "独自の ClamAV サーバーがドキュメントをチェックします。ネットワーク外へは何も送信されません。" +label = "ウイルスをスキャン(セルフホスト)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "マクロ、実行ファイル、スクリプト、埋め込みオブジェクトを含むドキュメントを拒否します。" +label = "アクティブコンテンツをブロック" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive がドキュメントをチェックし、クリーンでない場合は実行を停止します。" +label = "ウイルスをスキャン" + +[portal.policies.operations.confluenceAttach] +description = "処理済みドキュメントをページに保存します。" +label = "Confluence ページに添付" + +[portal.policies.operations.consignoSubmit] +description = "署名のためにドキュメントを ConsignO Cloud に送信します。" +label = "署名に送信(ConsignO)" +note = "ドキュメントを送信します。ステップが結果を後続ステップに渡せないため、署名済みコピーの取得はまだサポートされていません。" + +[portal.policies.operations.customApiCall] +description = "パス、メソッド、本文、レスポンス処理を自分で作成します。" +label = "カスタム API を呼び出す" + +[portal.policies.operations.discordNotify] +description = "ポリシーがドキュメントを処理したことをチャンネルに通知します。" +label = "Discord にメッセージを投稿" + +[portal.policies.operations.elasticIndex] +description = "ポリシーが行ったことを検索可能なドキュメントとして記録します。" +label = "Elasticsearch に監査イベントをインデックス化" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart はアップロード API に適しています。JSON はペイロード内にファイルを求める API に適しています。binary は生のバイトを送信します。" +label = "ドキュメントの送信方法" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "そのまま送信される JSON 本文です。{{document.base64}} はファイルを保持し、{{document.*}} と {{run.*}} はドキュメントごとに入力されます。" +label = "JSON 本文" + +[portal.policies.operations.fields.connection] +label = "アカウント" + +[portal.policies.operations.fields.domain] +label = "Mailgun ドメイン" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API がドキュメントを受け取ることを期待するフォームフィールドです。" +label = "ファイルフィールド名" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "From" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "ヘッダー名/値ペアの JSON オブジェクトです。接続の認証情報は自動的に追加されます。" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "課題キー" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "メッセージ本文として送信されます。{{document.*}} と {{run.*}} はドキュメントごとに入力されます。" +label = "メッセージ" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "ページ ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "接続のベース URL に追加されます。別のホストには絶対に到達できません。" +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "ドライブ内の書き込み先です。{{document.filename}} はドキュメントごとに入力されます。" +label = "宛先パス" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "レポートはドキュメントを変更しません。置換は返ってきた内容で差し替えます。" +label = "返信の処理方法" + +[portal.policies.operations.fields.signerEmail] +label = "署名者のメール" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "件名" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio はこのテキストを分析します。{{document.title}} を使用するか、チェックしたいテキストを貼り付けてください。" +label = "チェックするテキスト" + +[portal.policies.operations.fields.to] +label = "To" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud ユーザー名" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "ポリシーがドキュメントを処理したことをスペースに通知します。" +label = "Google Chat にメッセージを投稿" + +[portal.policies.operations.jiraAttach] +description = "処理済みドキュメントを課題に保存します。" +label = "Jira 課題に添付" + +[portal.policies.operations.mailgunEmail] +description = "処理済みドキュメントを添付ファイルとして送信します。" +label = "ドキュメントをメール送信(Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "処理済みドキュメントをフォルダーに書き込みます。" +label = "Nextcloud にアップロード" + +[portal.policies.operations.presidioAnalyze] +description = "独自の Presidio サーバーが検出した個人データを報告します。" +label = "個人データを検出" +note = "Presidio はファイルではなくテキストを読み取ります。チェックしたいテキストを指定してください。ドキュメント自体はアップロードされません。" + +[portal.policies.operations.responseMode] +replace = "ドキュメントを返信で置換" +report = "ドキュメントを保持し、返信を記録" + +[portal.policies.operations.sendgridEmail] +description = "処理済みドキュメントを添付ファイルとして送信します。" +label = "ドキュメントをメール送信(SendGrid)" + +[portal.policies.operations.slackNotify] +description = "ポリシーがドキュメントを処理したことをチャンネルに通知します。" +label = "Slack にメッセージを投稿" + +[portal.policies.operations.splunkEvent] +description = "監査担当者がすでに確認している場所に、ポリシーが行ったことを記録します。" +label = "監査イベントを Splunk に送信" + +[portal.policies.operations.sumologicEvent] +description = "ポリシーが行ったことを HTTP コレクターに記録します。" +label = "監査イベントを Sumo Logic に送信" + +[portal.policies.operations.teamsNotify] +description = "ポリシーがドキュメントを処理したことをチャンネルに通知します。" +label = "Teams にメッセージを投稿" + +[portal.policies.operations.webhookPost] +description = "選択した URL にドキュメント自体を送信します。" +label = "ドキュメントを webhook に投稿" + +[portal.policies.operations.zapierNotify] +description = "実行の詳細を自動化に渡します。" +label = "Zap または Make シナリオをトリガー" + [portal.policies.stats] activeFor = "アクティブ" dataProcessed = "処理済みデータ" @@ -7473,6 +8282,13 @@ label = "適用済みドキュメント" description = "設定済みだが実行されていません" label = "一時停止中" +[portal.policies.table] +appliesTo = "適用先" +docs = "適用済みドキュメント" +enforces = "強制内容" +policy = "ポリシー" +status = "ステータス" + [portal.policies.wizard.actions] back = "戻る" cancel = "キャンセル" @@ -7480,6 +8296,10 @@ continue = "続行" enablePolicy = "ポリシーを有効化" saveChanges = "変更を保存" +[portal.policies.wizard.capability.classify] +desc = "チームのラベルからドキュメントの種類を識別してタグ付けし、カテゴリ別にファイルおよび検索できるようにします。" +label = "ドキュメントを分類" + [portal.policies.wizard.capability.compress] desc = "ドキュメントを圧縮してファイルサイズを小さくします。" label = "ファイルサイズを削減" @@ -7500,10 +8320,18 @@ label = "機密情報を墨消し" desc = "隠れた JavaScript を削除し、ドキュメントを開いたときに自動実行されないようにします。" label = "アクティブコンテンツを除去" +[portal.policies.wizard.capability.timestampPdf] +desc = "独立したタイムスタンプ機関を使用して、その時点でドキュメントがこの正確な形式で存在したことを証明します。送信されるのはハッシュのみで、ドキュメントがサーバーから出ることはありません。" +label = "信頼できるタイムスタンプを追加" + [portal.policies.wizard.capability.watermark] desc = "すべてのページに表示可能なマーク(例:「機密」)を押印します。" label = "透かしを適用" +[portal.policies.wizard.classification] +description = "アップロードされたすべてのドキュメントは組み込みラベルに照らして分類され、該当する種類でタグ付けされます。ラベルセットはチーム全体で共有されます。" +labelsHeading = "分類ラベル" + [portal.policies.wizard.errors] noTools = "まずワークフローで少なくとも1つのツールを有効にしてください。" saveFailed = "ポリシーを保存できませんでした。もう一度お試しください。" @@ -7548,73 +8376,118 @@ setUp = "{{category}} ポリシーをセットアップ" [portal.policies.wizard.workflow] description = "このポリシーが処理する各ドキュメントに何を行うかを選択します。" -[portal.policySummary] -activeSummary = "{{active}} / {{total}} アクティブ" -noRule = "まだ適用されているルールはありません" -subtitle = "どのパイプラインが処理するかに関係なく、すべてのドキュメントが通過する常設の自動化です。" -title = "PDF に対して実行される内容" - [portal.policySummary.action] -comingSoon = "Enterprise にアップグレード" -configure = "設定" setUp = "セットアップ" -[portal.policySummary.column] -activeRule = "アクティブなルール" -policy = "ポリシー" -status = "ステータス" +[portal.processorFlow] +footnote = "件数は過去 24 時間のものです。フロー速度は例示です。" +liveBadge = "ライブ" +stats = "{{connected}} 接続済み · {{processed}} 件の PDF を処理" +title = "PDF プロセッサー" -[portal.policySummary.empty] -description = "ポリシーが設定されると、カテゴリがここに表示されます。" -title = "まだポリシーはありません" +[portal.processorFlow.lens] +ariaLabel = "表示モード" +flow = "フロー" +sankey = "Sankey" -[portal.policySummary.state] -active = "アクティブ" -off = "オフ" -soon = "近日公開" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "失敗" +heading = "結果" +success = "配信済み" -[portal.processingStatus] -managePlan = "プランを管理" -volumeSuffix = "処理済み PDF · 過去30日間" +[portal.processorFlow.policies] +activeCount = "{{n}} 件アクティブ" +count = "{{n}} · 24h" +heading = "ポリシー" +setUp = "セットアップ" +soon = "近日対応" + +[portal.processorFlow.sankey] +waist = "ポリシー · {{n}} 件アクティブ" + +[portal.processorFlow.sankey.empty] +description = "ソースを接続し、ポリシーを有効にすると、ドキュメントがプロセッサーを流れる様子を確認できます。" +title = "まだフローがありません" + +[portal.processorFlow.sources] +comingSoonTag = "接続" +editor = "Stirling PDF Editor" +heading = "ソース" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "メール取り込み" [portal.procurement] -reset = "調達をリセット(デモ)" subtitle = "チームの評価、契約、オンボーディングを進めます。すべてのドキュメントを一か所に。" title = "調達" -[portal.procurement.action] -download = "ダウンロード" -pay = "今すぐ支払う" -request = "リクエスト" -sign = "確認して署名" -upload = "アップロード" - [portal.procurement.agreement] agreeCta = "同意して購読" +confidential = "機密" confirm = "Stirling Enterprise Agreement を読み、同意します。" -eyebrow = "契約" -intro = "1つの統合契約で、Master Service Agreement、Order Form、EULA、Data Processing Agreement をカバーします。内容を確認し、見積もりをコミット済みサブスクリプションとして受け入れることに同意してください。" -title = "エンタープライズ契約を確認" +docName = "Stirling Enterprise 契約書" +docSub = "MSA、注文書、EULA、DPA を 1 つの署名にまとめたものです。" +download = "ダウンロード" +downloadDraftError = "契約書 PDF を生成できませんでした - ドキュメントレンダラーを利用できません。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。" +downloadError = "署名済み契約書をダウンロードできませんでした。もう一度お試しください。" +legalName = "法人名" +legalNamePlaceholder = "署名する法人" +loadError = "契約書を読み込めませんでした。もう一度お試しください。" +loading = "契約書を読み込み中..." +ref = "参照 {{ref}} · {{version}}" +requestChanges = "変更を依頼" +scrollHint = "スクロールして確認" +signatory = "署名者名" +signatoryPlaceholder = "氏名" +signatoryTitle = "署名者の役職" +signatoryTitlePlaceholder = "例: General Counsel" +signError = "署名を記録できませんでした。もう一度お試しください。" [portal.procurement.builder] addons = "アドオン" +addressLine1 = "住所行 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "住所行 2" +addressLine2Placeholder = "スイート、階、建物" back = "戻る" businessName = "会社名" businessNamePlaceholder = "貴社名" +city = "市区町村" +cityPlaceholder = "San Francisco" +completeRequired = "見積を生成する前に、必須フィールド(* 印)を有効なメールで入力してください。" +contactEmail = "連絡先メール" +contactEmailPlaceholder = "jane@acme.com" +contactName = "連絡先名" +contactNamePlaceholder = "Jane Doe" continue = "続行" -country = "国" -countryEuro = "ユーロ圏 (EUR €)" -countryUK = "英国 (GBP £)" -countryUS = "米国 (USD $)" +done = "完了" eula = "Stirling Enterprise EULA を読み、同意します。この見積もりから生成される契約に適用されます。" generate = "見積もりを生成" -included = "含まれています" indemnification = "IP 補償" indemnificationSub = "EULA に従い、対象となる IP 請求に対して当社が防御します" -offlineLicense = "オフライン / エアギャップ ライセンス" -offlineLicenseSub = "エアギャップのセルフホストインスタンス用にダウンロード可能なライセンスファイル" +paperEyebrow = "エンタープライズ見積" +paperFor = "宛先" +pdfSize = "PDF サイズ" +poNumber = "PO 番号" +poNumberPlaceholder = "任意" +postalCode = "郵便番号" +postalCodePlaceholder = "94105" +posture = "ガバナンス" +posture_count = "約{{count}}個のポリシー" +posture_essentials = "基本" +posture_essentialsSub = "分類 + 共有 — デフォルト" +posture_governed = "ガバナンス対応" +posture_governedSub = "セキュリティとルーティングを追加" +posture_regulated = "規制対応" +posture_regulatedSub = "すべてのカテゴリ — コンプライアンス、保持、取り込み" qbr = "四半期ビジネスレビュー" qbrSub = "担当 SE が四半期ごとに使用状況とロードマップをレビューします" +region = "州 / 地域" +regionPlaceholder = "California" running = "{{annual}} / 年 · {{years}}年 {{tcv}}" s1Sub = "チームと、年間に処理する予定の PDF。" s1Title = "どのくらい処理しますか?" @@ -7623,6 +8496,12 @@ s2Title = "契約期間とサービス" s3Sub = "見積もりと、そこから生成される契約に使用します。" s3Title = "お客様情報" serviceLevel = "サービスレベル" +size_compact = "コンパクト" +size_compactSub = "主にテキスト、1 MB 未満" +size_heavy = "大容量" +size_heavySub = "スキャン済みまたは画像が多い、5 MB+" +size_standard = "標準" +size_standardSub = "テキストと画像の混在、1~5 MB" slDedicated = "専任" slDedicatedSub = "4営業時間 · 専任アカウントマネージャー · +30%" slPriority = "優先" @@ -7630,6 +8509,8 @@ slPrioritySub = "同一営業日 · 指名 CSM · +15%" slStandard = "標準" slStandardSub = "翌営業日 · 共有 CSM · 含まれています" stepOf = "ステップ {{n}} / {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "任意" term = "期間" termDiscount = "{{pct}}% の複数年契約割引が適用されました" title = "見積もりを作成" @@ -7637,6 +8518,7 @@ training = "オンボーディングとトレーニング" trainingSub = "チームが使い始められるようにするライブセッション" users = "総ユーザー数" usersPlaceholder = "例: 250" +viewEula = "読む" volEstimated = "{{count}} ユーザーから推定(各ユーザー約2,000 PDF、オートメーションを含む)。より正確な数が分かる場合は編集してください。" volManual = "入力した数値を使用しています。いつでもチームサイズから再推定できます。" volNoUsers = "不明ですか?チームサイズを入力すると推定します。" @@ -7645,55 +8527,55 @@ volumePlaceholder = "例: 1,000,000" years_one = "{{count}}年" years_other = "{{count}}年" -[portal.procurement.docs] -count_one = "{{count}}件のドキュメント" -count_other = "{{count}}件のドキュメント" -done = "完了" -here = "現在位置" -hide = "非表示" -optional = "任意" -paidAddon = "有料アドオン" -show = "表示" -subtitle = "手続きの各ステップで必要なすべてが、取引の進行に合わせて表示されます。" -supportingSubtitle = "SOC 2、セキュリティレビュー、税務書類などを、セキュリティチームや調達チームから求められたときに利用できます。一部は一回限りの料金が発生します。" -supportingTitle = "評価をサポート" +[portal.procurement.documents] +agreement = "エンタープライズ契約書" +agreementSub = "MSA、注文書、DPA を 1 つの署名にまとめたものです。" +download = "ダウンロード" +eula = "EULA と商用条件" +eulaSub = "ソフトウェアおよびセルフサービスの商用条件です。" +invoice = "請求書" +invoiceSub = "初回サブスクリプション請求書です。" +laterInvoice = "承諾後に利用可能" +laterQuote = "見積発行後に利用可能" +quote = "見積" +quoteSub = "項目別のエンタープライズ見積です。" +sla = "SLA 別紙" +slaSub = "サポート階層と応答目標です。" +subprocessors = "サブプロセッサー" +subprocessorsSub = "お客様に代わってデータを処理する第三者です。" +subtitle = "取引関連書類はいつでも利用できます。" title = "ドキュメント" -upcoming = "今後" +view = "表示" [portal.procurement.error] title = "問題が発生しました" [portal.procurement.hero] -company = "エンタープライズ契約" +barAria = "{{total}} 中 {{current}} 段階" ctaAgreement = "契約を確認して署名" -ctaLive = "利用開始済み" -ctaPayment = "支払いを追加" +ctaExploring = "トライアルをセットアップ" ctaQuote = "見積もりを確認" +ctaReviewQuote = "見積を確認" ctaTrial = "見積もりを作成" +documents = "ドキュメント" eyebrow = "Enterprise 調達" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "チームメイトを招待" -keyDocs = "主要ドキュメント" -nextStep = "次のステップ: {{action}}" -notStarted = "未開始" +licenseKey = "ライセンスキー" +liveSub = "組織はプロビジョニング済みで、ライセンスは有効です。" +liveTitle = "Stirling Enterprise で本番稼働中です" +next = "次: {{stage}}" open = "調達を開く" scheduleCall = "通話を予約" -setup1Sub = "チームメイトを招待します" -setup1Title = "PDF エディターを導入" -setup2Sub = "エディターやその他のソース全体で処理を有効にします" -setup2Title = "PDF プロセッサーを接続" -setup3Sub = "必要に応じて Security、Compliance、Routing、Retention を有効にします" -setup3Title = "推奨ポリシーを追加" +sentenceAgreement = "契約書の確認と署名" +sentenceLive = "プロビジョニングとロールアウト" +sentencePayment = "発注書と支払い" +sentenceQuote = "見積の確認と承諾" +sentenceTrial = "トライアルワークスペースのセットアップ" [portal.procurement.journey] daysLeft_one = "残り {{count}}日" daysLeft_other = "残り {{count}}日" -engineerLabel = "担当ソリューションエンジニア" -eyebrow = "ロールアウト" -live = "Stirling Enterprise で利用開始済みです" -nextStep = "次のステップ: {{action}}" -subtitle = "担当ソリューションエンジニアがすべてのステップをサポートします。一度に1つの次のアクションを進め、完全なチェックリストは下にあります。" -title = "トライアルから本番利用まで、1つのガイド付きパスで" -trialTitle = "Enterprise トライアル" [portal.procurement.journeySteps.agreement] blurb = "1つの署名で MSA、注文書、EULA、DPA をカバーします。" @@ -7727,6 +8609,9 @@ downloadError = "オフラインライセンスファイルをまだ生成でき downloadOffline = "オフラインライセンス (.lic) をダウンロード" hint = "このキーをセルフホストインスタンスに貼り付けて有効化するか、記録用に保管してください。安全に保管してください。" label = "ライセンスキー" +subtitle = "このキーでセルフホストインスタンスをアクティブ化するか、記録用に保管してください。" +title = "ライセンスキー" +trialFileHint = "これはトライアルライセンスファイルです。契約が成立したら、戻ってもう一度ダウンロードしてください。オンラインライセンスキーとは異なり、.lic ファイルは自動更新されません。" [portal.procurement.link] cta = "アカウントをリンク" @@ -7739,65 +8624,62 @@ description = "サブスクリプションがアクティブで、ライセン eyebrow = "本番稼働" title = "Stirling Enterprise で利用開始済みです" -[portal.procurement.locked] -description = "Enterprise 評価を開始すると、トライアルキー、コミット済みボリューム見積もり、1署名の契約、支払い、ドキュメント台帳がすべてここに表示されます。" -eyebrow = "Enterprise のみ" -talkToSales = "営業に相談" -title = "調達トラックは Enterprise で利用できます" - [portal.procurement.milestone] -accept = "承諾して続行" -description = "PDF をダウンロードしてチームと共有し、準備ができたら戻って承諾するか、変更を加えてください。" -download = "PDF をダウンロード" downloadError = "見積もり PDF をまだダウンロードできませんでした。しばらくしてからもう一度お試しください。" -edit = "見積もりを編集" -eyebrow = "見積もり {{number}}" -perYear = " / 年" -preparedFor = "{{company}} 向けに作成" -tcv = "総契約価値 {{value}}" -title = "見積もりの準備ができました" - -[portal.procurement.modal] -cancel = "キャンセル" -chooseFile = "ファイルを選択" -close = "閉じる" -downloadBody = "まもなくダウンロードが開始されます。" -downloadCta = "ダウンロード" -downloadTitle = "ダウンロード" -noFile = "ファイルが選択されていません" -payBody = "Stripe を通じて、コミット済み契約をカードまたは銀行振込で支払います。支払いが確認され次第、ワークスペースがプロビジョニングされます。" -payCta = "Stripe に進む" -payTitle = "支払いを確認" -requestBodyFree = "これはオンデマンドで生成します。確認すると、担当ソリューションエンジニアがまもなく送付します。" -requestBodyPaid = "これは有料アドオンです。確認すると、担当ソリューションエンジニアが範囲を確認し、書類を送付します。" -requestCta = "リクエスト" -requestTitle = "このドキュメントをリクエスト" -signBody = "Stirling Enterprise Agreement を電子署名用に開きます。1つの署名で MSA、注文書、EULA、DPA をカバーします。当社が自動的に副署し、支払いに進みます。" -signCta = "署名用に開く" -signTitle = "契約を確認して署名" -uploadBody = "PO を送信してください。お客様の条件に基づいて請求します。PDF をドラッグするか、下からファイルを選択してください。" -uploadCta = "注文書をアップロード" -uploadTitle = "注文書をアップロード" [portal.procurement.payment] description = "見積もりは承諾済みで、ライセンスはすでに有効です。チームはすぐに開始できます。準備ができたら最初の請求書を支払ってください。ここで支払うかダウンロードでき、メールは不要です。" +downloadAgreement = "署名済み契約書をダウンロード" downloadInvoice = "請求書をダウンロード" -simulate = "支払い受領をシミュレート(デモ)" +eyebrow = "支払い" title = "サブスクリプションが作成されました" viewInvoice = "請求書を表示して支払う" +[portal.procurement.review] +acceptCta = "見積を承諾" +annual = "年間料金(1 年目)" +downloadCta = "見積をダウンロード" +poNumber = "発注書: {{po}}" +renewal = "期間終了後は {{amount}}/年で更新(+{{pct}}% CPI)" +tcv = "{{years}} 年契約、前払い一括: {{tcv}}" +validUntil = "有効期限 {{date}}" + [portal.procurement.schedule] fallback = "スケジューラーを読み込めませんでした。" fallbackLink = "新しいタブでスケジュールを開く" +loading = "スケジューラーを読み込み中..." subtitle = "担当ソリューションエンジニアがロールアウトをチームに案内します。都合のよい時間を選択してください。" title = "通話を予約" -[portal.procurement.status] -action = "アクションが必要" -available = "利用可能" -complete = "完了" -pending = "保留中" -request = "リクエスト時" +[portal.procurement.setup] +airgap = "エアギャップ" +airgapSub = "密閉、アウトバウンドなし" +back = "戻る" +businessName = "会社名" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "マネージド、最速開始" +continue = "続行" +deployment = "どこにデプロイしますか?" +eula = "Stirling EULA と商用条件に同意します。" +fullName = "氏名" +fullNamePlaceholder = "あなたの名前" +invites = "チームメイトを招待 · 任意" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "通話を予約" +seats = "テスト人数" +seatsPlaceholder = "例: 15" +selfhost = "セルフホスト" +selfhostSub = "Docker または Kubernetes" +start = "トライアルを開始" +stepOf = "ステップ {{n}} / {{total}}" +subtitle = "14 日間無料、カード不要。" +subtitleDetails = "見積と契約書のために、いくつかの詳細を入力してください。" +talkFirst = "まず相談しますか?" +title = "トライアルをセットアップ" +viewEula = "読む" +workEmail = "勤務先メール" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "延長すると7日追加され、担当ソリューションエンジニアに通知されます。" @@ -7808,109 +8690,113 @@ maxed = "上限に達しました" subtitle = "無料トライアルは {{date}} まで有効です。カードは不要です。" title = "Enterprise トライアル" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "コミット済みボリューム価格、組織全体の SSO + SCIM + RBAC、90日間の改ざん不可監査、専任 SE。" -homeCta = "トライアルを開始 →" -homeHeadline = "数百万の PDF を処理。" - -[portal.recentActivity] -title = "最近のアクティビティ" -viewAll = "すべて表示" - -[portal.recentActivity.empty] -description = "パイプラインの実行、デプロイ、エージェントイベントがここに表示されます。" -title = "まだ何もありません" - -[portal.search] -ariaLabel = "検索" -placeholder = "Stirling を検索 — エンドポイント、パイプライン、ドキュメント…" - -[portal.search.empty] -noActionsDescription = "利用可能になると、クイックアクションがここに表示されます。" -noActionsTitle = "クイックアクションはありません" -noMatches = "「{{query}}」に一致するものはありません" -noMatchesDescription = "別のキーワードを試すか、カタログを参照してください。" - [portal.settings.groups] admin = "管理者" [portal.settings.sections] account-link = "アカウントリンク" -[portal.shell.header] -accountFallback = "アカウント" -accountMenu = "アカウントメニュー" -darkMode = "ダークモード" -lightMode = "ライトモード" -search = "検索" -searchPlaceholder = "検索…" -signOut = "サインアウト" -switchToDark = "ダークテーマに切り替え" -switchToLight = "ライトテーマに切り替え" - [portal.shell.sidebar] appEditor = "エディター" appProcessor = "プロセッサー" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}}件のドキュメント" linkAccount = "Stirling アカウントをリンク" -planEditor = "エディタープラン" -planEnterprise = "Enterprise プラン" -planProcessor = "プロセッサープラン" primaryNav = "プライマリナビゲーション" switchApp = "アプリを切り替え" +[portal.shell.topbar] +closeNav = "ナビゲーションを閉じる" +openNav = "ナビゲーションを開く" +search = "検索" + [portal.sources] subtitle = "ドキュメントを Stirling に供給する再利用可能な入力接続です。一度接続を設定すれば、任意の数のポリシーから参照できます。行をクリックすると、その設定と使用中のポリシーを確認できます。" title = "ソース" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "ソースを接続" +[portal.sources.builder] +back = "ソースに戻る" +backToTypes = "すべてのソースタイプ" +cancel = "キャンセル" +chooseHint = "ドキュメントの取得元を選択してください。グレー表示のコネクターは近日対応予定です。" +comingSoon = "近日対応予定" +comingSoonHeading = "近日対応予定" +create = "ソースを作成" +createTitle = "ソースを接続" +delete = "削除" +editTitle = "ソースを編集" +enabled = "有効" +save = "変更を保存" + +[portal.sources.builder.folderAccess] +description = "フォルダー自動化は、管理者が許可したフォルダーのみ使用できます。フォルダーアクセス設定で追加してから、もう一度お試しください。" +openSettings = "フォルダーアクセス設定" +title = "このフォルダーは許可されていません" + [portal.sources.delete] body = "「{{name}}」を削除しますか?この操作は元に戻せません。これを参照しているポリシーは更新が必要になります。" cancel = "キャンセル" confirm = "削除" title = "ソースを削除しますか?" -[portal.sources.detail] -closeAriaLabel = "詳細を閉じる" -delete = "ソースを削除" -docs24h = "過去24時間" -docs30d = "過去30日間" -docsTotal = "確認済み合計" -docsTrend = "過去30日間のドキュメント" -documents = "ドキュメント" -edit = "編集" -notReferenced = "どのポリシーからも参照されていないため、安全に削除できます。" -pause = "一時停止" -resume = "再開" -subtitle = "{{type}} · {{status}}" -usedBy = "使用元" - -[portal.sources.empty] -description = "フォルダー(まもなくクラウドストレージも)を接続して、ポリシーがドキュメントを取得できる場所を用意します。" -title = "まだソースが接続されていません" - [portal.sources.kpi] inUse = "使用中" total = "接続" unused = "未使用" +[portal.sources.networkFields.connection] +helperText = "サーバーアドレスと認証情報を持つ保存済み接続です。それを参照するすべてのソースで再利用されます。" +label = "接続" + +[portal.sources.networkFields.directory] +helperText = "ポーリングするサーバー上のフォルダーです。ログインホームまたは共有ルートからの相対パスです。ルートの場合は空白のままにします。" +label = "フォルダー" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume は、すべてのポリシーで処理された後、各ファイルをサーバーから削除します。" +label = "読み取りモード" + +[portal.sources.networkFields.mode.options] +consume = "Consume: 各ファイルを 1 回処理" +snapshot = "Snapshot: 実行ごとにフォルダーを再読み取り" + +[portal.sources.networkFields.recursive] +label = "フォルダー階層" + +[portal.sources.networkFields.recursive.options] +all = "サブフォルダーを含める" +top = "最上位のみ" + [portal.sources.status] active = "アクティブ" disabled = "無効" unused = "未使用" [portal.sources.table] +documents = "ドキュメント" source = "ソース" status = "ステータス" +type = "Type" usedBy = "ポリシー" +[portal.sources.types.box] +description = "チームがすでに共有している Box フォルダーからドキュメントを取得します。" +label = "Box" + +[portal.sources.types.dropbox] +description = "共有 Dropbox フォルダーからドキュメントを取得します。" +label = "Dropbox" + [portal.sources.types.editor] +description = "チームがエディターで処理したドキュメントです。ポリシー実行と AI 実行を横断します。" label = "エディター" +noPolicies = "エディターから実行されたポリシーはまだありません。" + +[portal.sources.types.email] +description = "専用メールボックスに送信された添付ファイルをドキュメントに変換します。" +label = "メール受信箱" [portal.sources.types.folder] description = "サーバー上のディレクトリを監視して新しいドキュメントを検出します。" @@ -7943,25 +8829,80 @@ label = "フォルダーの深さ" all = "サブフォルダーを含める" top = "最上位のみ" +[portal.sources.types.ftp] +description = "新しいドキュメントがないか FTP または FTPS サーバーフォルダーをポーリングします。" +label = "FTP" + +[portal.sources.types.googledrive] +description = "共有 Google Drive フォルダーからドキュメントを取得します。" +label = "Google Drive" + +[portal.sources.types.network] +description = "新しいドキュメントがないか SMB ネットワーク共有を監視します。" +label = "ネットワークドライブ" + +[portal.sources.types.onedrive] +description = "新しいドキュメントがないか OneDrive フォルダーを監視します。" +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 または S3 互換バケットからドキュメントを取得します。" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "バケットと認証情報を保持する保存済み接続です。それを参照するすべてのソースとパイプライン出力で再利用されます。" +label = "接続" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume は、すべてのポリシーで処理された後、各オブジェクトをバケットから削除します。" +label = "読み取りモード" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: 各オブジェクトを 1 回処理" +snapshot = "Snapshot: 実行ごとにバケットを再読み取り" + +[portal.sources.types.s3.fields.prefix] +helperText = "キーがこのプレフィックスで始まるオブジェクトのみ処理されます。" +label = "キープレフィックス" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "新しいドキュメントがないか SFTP ドロップフォルダーをポーリングします。" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint ドキュメントライブラリからドキュメントを取得します。" +label = "SharePoint" + [portal.sources.types.unknown] label = "ソース" +[portal.sources.types.webhook] +createNote = "この webhook を作成すると、配信 URL と署名シークレットが生成されます。送信者はその URL にドキュメントを POST し、参照しているポリシーが到着時に実行されます。" +description = "任意の外部システムから、署名付き HTTP POST でドキュメントを受信します。" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "配信 URL" +secretNote = "署名シークレットは webhook 作成時に一度だけ表示されます。ローテーションするにはソースを再作成してください。" + +[portal.sources.types.webhook.reveal] +copy = "コピー" +done = "完了" +secret = "署名シークレット" +secretHelp = "このキーで各配信の生の本文に署名(HMAC-SHA256)し、X-Stirling-Signature ヘッダーとして送信してください。" +secretWarning = "今すぐ署名シークレットをコピーしてください。セキュリティのため、一度だけ表示され、後で取得することはできません。" +title = "Webhook を作成しました" +url = "配信 URL" +usage = "各ドキュメントを、バイナリコンテンツタイプ(application/pdf または application/octet-stream)で生のリクエスト本文として配信 URL に POST してください。参照しているポリシーは到着時に自動実行されます。" + [portal.sources.wizard] -back = "戻る" -cancel = "キャンセル" -continue = "続行" -editTitle = "ソースを編集" name = "名前" namePlaceholder = "例: 請求受付" -save = "変更を保存" -subtitle = "ステップ {{current}} / {{total}} · {{label}}" -title = "ソースを接続" type = "タイプ" -[portal.sources.wizard.steps] -chooseType = "タイプを選択" -configure = "設定" -review = "確認して接続" +[portal.stepModal] +close = "閉じる" [portal.usage] managePayment = "支払いを管理" @@ -8021,16 +8962,9 @@ summary = "チームを所有し、そのメンバーのリソースと共有設 2 = "デフォルトポリシーによるポータルアクセス" 3 = "メンバーができるすべて" -[portal.welcome] -ariaLabel = "Stirling PDF へようこそ" -badge = "Open-source" -installEditor = "エディターをインストール" -inviteTeammates = "チームメイトを招待" -openInBrowser = "ブラウザーで開く" -perks = "永久無料 · セルフホスト可能" -subtitle = "世界で最も安全な PDF エディターは、あらゆる規模のチームに無料で提供されます。60以上の PDF 操作と SSO が含まれます。" -title = "ようこそ" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · 無制限" [printFile] title = "ファイルの印刷" @@ -8613,6 +9547,14 @@ text = "PDF のページを90度単位で時計回りまたは反時計回りに [rotate.tooltip.header] title = "回転設定の概要" +[ruler] +clearAll = "すべてクリア" +hideAllLabels = "すべてのラベルを非表示" +hideSmallLabels = "小さいラベルを非表示" +physicalValues = "物理" +scaled = "スケーリング済み" +showAllLabels = "すべてのラベルを表示" + [sanitize] desc = "PDF ファイルから潜在的に有害な要素を削除します。" sanitizationResults = "サニタイズ結果" @@ -8654,31 +9596,45 @@ label = "XMP メタデータを削除" [sanitize.steps] settings = "設定" -[sanitizePdf] -tags = "クリーン,安全,保護,脅威除去" - [sanitizePDF] title = "PDFをサニタイズ" +[sanitizePdf] +tags = "クリーン,安全,保護,脅威除去" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "ページの縮尺の調整" +[scaleSettings] +activeScale = "有効な縮尺" +apply = "縮尺を適用" +applyCalibration = "キャリブレーションを適用" +calculatedScale = "計算された縮尺" +calibrate = "キャリブレーション" +calibrating = "キャリブレーション中" +calibrationDistanceRequired = "0より大きい実際の距離を入力してください" +calibrationInvalid = "この測定値から縮尺を計算できません" +calibrationPaperDistance = "測定されたページ上の距離: {{distance}}" +calibrationTitle = "縮尺をキャリブレーション" +calibrationTooltip = "既知の距離を測定して縮尺を自動計算します" +customScale = "カスタム縮尺" +noneSet = "カスタム縮尺が設定されていません" +presets = "プリセット縮尺" +ratio = "縮尺比" +ratioHelp = "比率: 1ページ単位 = X実世界単位" +ratioPlaceholder = "例: 100" +ratioPositive = "比率は0より大きい必要があります" +ratioRequired = "比率は必須です" +realDistance = "実際の距離" +realDistancePlaceholder = "例: 5" +reset = "デフォルトにリセット" +unit = "単位" +unitInvalid = "無効な単位: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "角度のしきい値:" -10 = "出力に白い縁取りが出ないように追加・削除される境界線の大きさを設定 (初期値:1)。" -2 = "画像を回転させるために必要な絶対角度の最小値を設定 (初期値:10)。" -3 = "許容範囲:" -4 = "推定された背景色周辺のカラーバリエーションの範囲を決定 (初期値:30)。" -5 = "最小面積:" -6 = "画像の最小面積のしきい値を設定 (初期値:10000)。" -7 = "最小輪郭面積:" -8 = "画像の最小の輪郭面積のしきい値を設定。" -9 = "境界線サイズ:" - [scannerImageSplit] submit = "画像スキャンを抽出" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "できること" whatThisDoesDesc = "スキャンしたページや合成画像から各写真を自動検出して抽出します。手動のトリミングは不要です。" whenToUse = "使用タイミング" +[ScannerImageSplit.selectText] +1 = "角度のしきい値:" +10 = "出力に白い縁取りが出ないように追加・削除される境界線の大きさを設定 (初期値:1)。" +2 = "画像を回転させるために必要な絶対角度の最小値を設定 (初期値:10)。" +3 = "許容範囲:" +4 = "推定された背景色周辺のカラーバリエーションの範囲を決定 (初期値:30)。" +5 = "最小面積:" +6 = "画像の最小面積のしきい値を設定 (初期値:10000)。" +7 = "最小輪郭面積:" +8 = "画像の最小の輪郭面積のしきい値を設定。" +9 = "境界線サイズ:" + [search] noResults = "結果が見つかりませんでした" placeholder = "検索語を入力..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Stirling-PDF サーバーがオフラインで、ロ [session] expired = "セッションが期限切れです。ページを更新してもう一度お試しください。" -[sessionManagement.tooltip] -header = "署名セッションの管理" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "新しい参加者は署名順序の最後に追加されます" -bullet2 = "セッションの最終化後は参加者を追加できません" -bullet3 = "各参加者は自分の順番になると通知を受け取ります" -description = "最終化前であれば、アクティブなセッションにいつでも参加者を追加できます。" -title = "参加者の追加" - -[sessionManagement.tooltip.finalization] -bullet1 = "完全最終化: すべての参加者が署名済み" -bullet2 = "部分最終化: 一部の参加者がまだ署名していません" -bullet3 = "未署名の参加者は最終ドキュメントから除外されます" -bullet4 = "最終化後、署名済みPDFをアクティブファイルに読み込めます" -description = "最終化はすべての署名を1つの署名済みPDFにまとめます。この操作は元に戻せません。" -title = "セッションの最終化" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "既に署名した参加者は削除できません" -bullet2 = "削除された参加者には以降通知されません" -bullet3 = "署名順序は自動的に調整されます" -description = "署名前であればセッションから参加者を削除できます。" -title = "参加者の削除" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "各署名は順番にPDFへ適用されます" -bullet2 = "後の署名者は先の署名を確認できます" -bullet3 = "承認ワークフローや法的な証跡に不可欠です" -description = "セッション作成時に指定する順序が、誰が最初に署名するかを決定します。" -title = "署名の順序" - [settings] close = "閉じる" +[settings.ai] +documents = "ドキュメント & RAG" +general = "一般" +limits = "制限 & パフォーマンス" +models = "モデル & プロバイダー" +title = "AI" + [settings.configuration] advanced = "詳細設定" database = "データベース" endpoints = "エンドポイント" features = "機能" +folderAccess = "フォルダーアクセス" mcp = "MCP Server" storageSharing = "ファイルストレージと共有" systemSettings = "システム設定" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "既定のPDFエディター" defaultPdfEditorActive = "Stirling PDF は既定のPDFエディターです" defaultPdfEditorChecking = "確認中..." defaultPdfEditorInactive = "別のアプリが既定に設定されています" +defaultPdfEditorRemind = "デフォルトに設定するよう通知する" +defaultPdfEditorRemindDescription = "Stirling PDF がデフォルトの PDF アプリケーションでない場合にバナーを表示します。" defaultPdfEditorSet = "既に既定です" defaultStartupView = "起動時のデフォルト表示" defaultStartupViewDescription = "アプリ起動時に左側の列でアクティブにするタブを選択します" @@ -8817,9 +9763,18 @@ languageDescription = "表示言語を選択" logout = "ログアウト" setAsDefault = "既定に設定" theme = "テーマ" +themeAccent = "アクセントカラー" +themeAccentCustom = "カスタムカラー" +themeAccentCustomHint = "カスタム(デフォルト推奨)" +themeAccentDark = "ダークモードのアクセントカラー" +themeAccentDefault = "デフォルト" +themeAccentDefaultHint = "デフォルトテーマ(推奨)" +themeAccentDescription = "ボタン、リンク、ハイライトに適用され、アプリに控えめな色合いを加えます。ライトとダークにはそれぞれ独自の設定があります。システムでは現在有効なものが使用されます。" +themeAccentLight = "ライトモードのアクセントカラー" themeDark = "ダーク" themeDescription = "ライト/ダークモードを切り替え" themeLight = "ライト" +themeReset = "テーマをデフォルトに戻す" themeSystem = "System" title = "一般" user = "ユーザー" @@ -8833,6 +9788,12 @@ intro = "組織向けにユーザー認証、チーム管理、ワークスペ learnMore = "ドキュメントで詳しく見る" title = "システム管理者向け" +[settings.general.loginLanding] +description = "Stirling Cloud にサインインしたときに開くアプリを選択します。" +editor = "エディター" +processor = "プロセッサー" +title = "サインイン後" + [settings.general.mode] fullscreen = "フルスクリーン" sidebar = "サイドバー" @@ -9013,9 +9974,6 @@ title = "ポリシーとプライバシー" [settings.preferences] title = "環境設定" -[settings.search] -placeholder = "設定ページを検索..." - [settings.security] description = "アカウントを安全に保つため、パスワードを更新してください。" title = "セキュリティ" @@ -9041,6 +9999,11 @@ security = "セキュリティ" telegram = "Telegram" title = "セキュリティと認証" +[settings.signOut] +confirm = "サインアウトしてもよろしいですか?" +submit = "サインアウト" +title = "サインアウト" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "最後のページ" maintainRatio = "アスペクト比を維持を切替え" next = "次のページ" noSavedSigs = "保存された署名が見つかりません" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "個人署名" previous = "前のページ" redo = "やり直す" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "背景の削除に失敗" hint = "署名の PNG または JPG 画像をアップロードしてください" label = "署名画像をアップロード" placeholder = "画像ファイルを選択" +previewAlt = "Current image signature" processing = "画像を処理中..." removeBackground = "白背景を削除(透過化)" @@ -9207,6 +10172,17 @@ saved = "上の保存済み署名を選択し、PDF上の任意の場所をク text = "上で氏名を入力した後、PDF 上の任意の場所をクリックして署名を配置します。" title = "署名の追加方法" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "署名を移動" pause = "配置を一時停止" @@ -9653,10 +10629,30 @@ title = "サーバーにアップロード" updateButton = "サーバー上で更新" uploadButton = "サーバーにアップロード" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "ここをクリック" +fallback = "自動的に開かなかった場合は、" +opening = "Swagger UI を新しいタブで開いています..." tags = "API,ドキュメント,swagger,エンドポイント,開発" title = "API ドキュメント" @@ -9764,12 +10760,39 @@ selectFilesHint = "このツールを実行するには、アクティブなフ singleFileScope = "適用対象: {{fileName}} のみ" viewerMode = "複数ファイルを選択するにはファイルエディタに切り替えてください。" +[toolFormat] +ANY = "任意のファイル" +CBR = "コミックブックアーカイブ(CBR)" +CBZ = "コミックブックアーカイブ(CBZ)" +CSV = "CSV ファイル" +EBOOK = "電子書籍" +EMAIL = "メール" +EXCEL = "スプレッドシート" +HTML = "Web ページ" +IMAGE = "画像" +JAVASCRIPT = "JavaScript ファイル" +JSON = "JSON ファイル" +MARKDOWN = "Markdown ファイル" +NONE = "ファイルなし" +PCL = "PCL 印刷ファイル" +PDF = "PDF" +PDF_ENCRYPTED = "パスワード保護された PDF" +POSTSCRIPT = "PostScript ファイル" +PPT = "プレゼンテーション" +TEXT = "テキストファイル" +VIDEO = "動画" +WORD = "Word 文書" +XML = "XML ファイル" +XPS = "XPS 文書" +ZIP = "ZIP アーカイブ" + [toolPanel] alpha = "アルファ" backToAllTools = "すべてのツールに戻る" collapse = "パネルを折りたたむ" expand = "パネルを展開" goBack = "戻る" +pdfTools = "PDF ツール" placeholder = "ツールを選択して開始" premiumFeature = "プレミアム機能:" toolsHeader = "ツール" @@ -9806,11 +10829,6 @@ noToolsFound = "ツールが見つかりません" quickAccess = "クイックアクセス" searchPlaceholder = "ツールを検索..." -[toolPicker.categories] -advancedTools = "高度なツール" -recommendedTools = "おすすめのツール" -standardTools = "標準ツール" - [toolPicker.subcategories] advancedFormatting = "高度なフォーマット" automation = "自動化" @@ -9824,6 +10842,9 @@ removal = "削除" signing = "署名" verification = "検証" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "ツールが見つかりません" noTools = "利用可能なツールがありません" @@ -9950,28 +10971,53 @@ title = "ユーザー" you = "(あなた)" [users.action] +cancelInvite = "招待をキャンセル" deleteTeam = "チームを削除" disableMfa = "MFA をリセット" move = "チームへ移動" reinstate = "復帰" remove = "組織から削除" +removeTeam = "チームから削除" rename = "チーム名を変更" resetPw = "パスワードをリセット" suspend = "停止" unlock = "アカウントのロック解除" +[users.activity] +daysAgo = "{{count}}日前" +hoursAgo = "{{count}}時間前" +justNow = "たった今" +minutesAgo = "{{count}}分前" +monthsAgo_one = "{{count}}か月前" +monthsAgo_other = "{{count}}か月前" +never = "なし" +weeksAgo_one = "{{count}}週間前" +weeksAgo_other = "{{count}}週間前" +yearsAgo_one = "{{count}}年前" +yearsAgo_other = "{{count}}年前" + [users.cap] addProcessor = "+ プロセッサー" approver = "ポリシーを承認" editor = "エディター" processor = "プロセッサー" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}} への招待をキャンセルしますか?現在のリンクでは参加できなくなります。" +cancelInviteTitle = "招待をキャンセル" deleteTeamBody = "{{name}} チームを削除しますか?先にチームが空である必要があります。メンバーを別のチームに移動し、連携設定を所有していない状態にしてください。" deleteTeamTitle = "チームを削除" disableMfaBody = "{{name}} の MFA 登録を削除しますか?必要な場合、次回ログイン時に再設定します。" disableMfaTitle = "MFA をリセット" removeBody = "{{name}} を組織から完全に削除しますか?この操作は元に戻せません。" +removeTeamBody = "{{name}} をチームから削除しますか?アカウントは保持されますが、このチームのリソースへのアクセス権を失います。" removeTitle = "メンバーを削除" [users.empty] @@ -9982,10 +11028,8 @@ title = "まだメンバーはいません" addToTeam = "チームに追加" guestCount = "{{count}}人のゲスト" guests = "ゲスト" -guestsDesc = "外部共同作業者。共有された範囲に限定されます。エディターのみ。" ledBy = "{{owner}} が主導" org = "組織" -orgDesc = "組織全体の権限とポリシー承認権を持つオーナー" owners = "{{count}}人のオーナー" team = "{{name}} チーム" teamMeta = "{{count}}人" @@ -10022,6 +11066,18 @@ username = "ユーザー名" usernameError = "ユーザー名は3文字以上である必要があります" usernamePlaceholder = "jsmith" +[users.invites] +by = "{{who}} による招待" +cancel = "キャンセル" +expiresInDays_one = "{{count}}日後に期限切れ" +expiresInDays_other = "{{count}}日後に期限切れ" +expiresToday = "今日期限切れ" +title = "保留中の招待" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "バックエンドへの接続で問題が発生したか、アクセス権がありません。もう一度お試しください。" title = "メンバーを読み込めませんでした" @@ -10211,10 +11267,15 @@ searchPlaceholder = "添付ファイルを検索" title = "添付ファイル" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "ブックマークのタイトル" +bookmarkTitleRequired = "ブックマークのタイトルは必須です" closeSidebar = "ブックマークのサイドバーを閉じる" collapseAll = "すべてのブックマークを折りたたむ" +empty = "No bookmarks in this document" expandAll = "すべてのブックマークを展開" +searchPlaceholder = "ブックマークを検索" +title = "Bookmarks" [viewer.comments] addComment = "コメントを追加" @@ -10236,10 +11297,12 @@ locateAnnotation = "ドキュメント内で表示" moreActions = "その他の操作" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "ページ {{page}}" placingHint = "配置するにはページをクリック…(キャンセル)" removeCommentOnly = "コメントのみ削除" saveReply = "返信を保存" +searchPlaceholder = "Search comments" title = "コメント" typeComment = "コメント" typeInsertText = "テキストを挿入" @@ -10262,6 +11325,7 @@ unsavedDesc = "未保存の変更があります" closeSidebar = "レイヤーのサイドバーを閉じる" hideAll = "すべてのレイヤーを非表示" showAll = "すべてのレイヤーを表示" +title = "Layers" [viewer.link] delete = "リンクを削除" @@ -10605,30 +11669,6 @@ title = "透かしテキスト" image = "画像" text = "テキスト" -[wetSignature.tooltip] -header = "署名の作成方法" - -[wetSignature.tooltip.draw] -bullet1 = "ペンの色と太さをカスタマイズ" -bullet2 = "納得いくまでクリアして描き直し" -bullet3 = "タッチデバイス(タブレット・スマートフォン)に対応" -description = "マウスやタッチスクリーンで手書きの署名を作成します。個人的で本物らしい署名に最適です。" -title = "手書きで署名" - -[wetSignature.tooltip.type] -bullet1 = "複数のフォントから選択" -bullet2 = "文字サイズと色をカスタマイズ" -bullet3 = "定型的な署名に最適" -description = "入力したテキストから署名を生成します。高速で一貫性があり、ビジネス文書に適しています。" -title = "テキストで署名" - -[wetSignature.tooltip.upload] -bullet1 = "PNG、JPG などの画像形式に対応" -bullet2 = "最良の結果のため透明背景を推奨" -bullet3 = "署名領域に合わせて画像をリサイズ" -description = "作成済みの署名画像をアップロードします。スキャンした署名や会社ロゴがある場合に最適です。" -title = "署名画像をアップロード" - [workbenchBar] activeFiles = "アクティブなファイル" annotations = "注釈" @@ -10642,6 +11682,7 @@ exitRedaction = "墨消しモードを終了" exportAll = "PDF をエクスポート" exportSelected = "選択したページをエクスポート" formFill = "フォーム入力" +hideToolbar = "Hide toolbar" multiTool = "マルチツール" panMode = "パンモード" print = "PDF を印刷" @@ -10652,6 +11693,7 @@ redact = "墨消し" rotateLeft = "左に回転" rotateRight = "右に回転" ruler = "定規 / 測定" +rulerSettings = "縮尺設定" save = "保存" saveAll = "すべて保存" saveAs = "名前を付けて保存" @@ -10661,6 +11703,7 @@ selectAll = "すべて選択" selectByNumber = "ページ番号で選択" selectLanguage = "言語を選択" share = "共有" +showToolbar = "Show toolbar" toggleAnnotations = "注釈の表示を切り替え" toggleAttachments = "添付ファイルを切り替え" toggleBookmarks = "ブックマークを切り替え" diff --git a/frontend/editor/public/locales/ko-KR/translation.toml b/frontend/editor/public/locales/ko-KR/translation.toml index 89fd87a954..415ef009f1 100644 --- a/frontend/editor/public/locales/ko-KR/translation.toml +++ b/frontend/editor/public/locales/ko-KR/translation.toml @@ -1,7 +1,6 @@ alphabet = "알파벳" apply = "적용" applyAndContinue = "저장하고 나가기" -areYouSure = "정말 떠나시겠습니까?" back = "뒤로" black = "검정" blue = "파랑" @@ -46,10 +45,8 @@ filesSelected = "개의 파일이 선택됨" font = "글꼴" fontSizeTooltip = "페이지 번호 텍스트의 포인트 크기입니다. 숫자가 클수록 텍스트가 커집니다." fontTypeTooltip = "페이지 번호에 사용할 글꼴 계열입니다. 문서 스타일에 맞게 선택하세요." -genericSubmit = "제출" green = "초록" help = "도움말" -imgPrompt = "이미지 선택" incorrectPasswordMessage = "현재 비밀번호가 올바르지 않습니다." info = "정보" invalidUndoData = "실행 취소할 수 없음: 잘못된 작업 데이터" @@ -62,7 +59,6 @@ loginAgreementTitle = "로그인 동의" logOut = "로그아웃" marginTooltip = "페이지 번호와 페이지 가장자리 사이의 거리입니다." moreOptions = "추가 옵션" -multiPdfPrompt = "PDF 선택 (2개 이상)" never = "안 함" no = "아니오" noFileSelected = "선택된 파일이 없습니다. 업로드해 주세요." @@ -80,7 +76,6 @@ pageOrderPrompt = "사용자 지정 페이지 순서 (쉼표로 구분된 페이 pages = "페이지" pageSelectionPrompt = "사용자 지정 페이지 선택 (페이지 번호 1,5,6 또는 2n+1과 같은 함수를 쉼표로 구분하여 목록 입력):" password = "비밀번호" -pdfPrompt = "PDF 선택" pin = "파일 고정(도구 실행 후에도 유지)" pro = "프로" processingComplete = "파일이 준비되었습니다." @@ -110,6 +105,7 @@ undoSuccess = "작업을 성공적으로 취소했습니다" unknown = "알 수 없음" unpin = "파일 고정 해제(도구 실행 후 교체)" unsavedChanges = "PDF에 저장되지 않은 변경 사항이 있습니다." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "저장되지 않은 변경 사항" unsupported = "지원되지 않음" username = "사용자 이름" @@ -356,6 +352,7 @@ text = "이 권한은 사용자가 PDF로 무엇을 할 수 있는지를 제어 title = "권한 변경" [addStamp] +preview = "스탬프 미리보기" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "수동 다운로드: 링크를 클릭하고 파일을 tessdata noLanguages = "구성된 디렉터리에서 tessdata 언어를 찾을 수 없습니다." permissionNotice = "tessdata 경로에 쓸 수 없습니다. 다운로드는 브라우저에서 열리며, .traineddata 파일을 직접 tessdata 폴더에 저장하세요." +[admin.settings.ai.documents] +description = "문서에 대한 질문에 답변하는 데 사용되는 임베딩 모델 및 검색 설정을 구성합니다. 저장 시 AI 엔진에 적용됩니다." +title = "문서 및 RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "엔진 자체 환경 자격 증명을 사용하려면 비워 두세요. 자체 호스팅 단일 엔진 배포에 적용됩니다." +generic = "임베딩 API key" +openai = "OpenAI API key" +setPlaceholder = "저장됨 - 현재 key를 유지하려면 비워 두세요" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI 호환 / Ollama 임베딩 엔드포인트의 기본 URL입니다. 예: http://ollama:11434/v1. 신뢰할 수 있는 내부 엔드포인트를 가리켜야 합니다(SSRF에 민감)." +label = "임베딩 기본 URL" + +[admin.settings.ai.documents.embeddingModel] +description = "임베딩 모델 이름입니다. 자유 텍스트이며, 제안은 힌트일 뿐입니다." +label = "임베딩 모델" + +[admin.settings.ai.documents.embeddingProvider] +description = "문서 텍스트를 벡터 임베딩으로 변환하는 데 사용되는 제공업체입니다." +label = "임베딩 제공업체" + +[admin.settings.ai.documents.maxSearches] +description = "에이전트가 요청당 실행할 수 있는 검색 검색의 최대 횟수입니다." +label = "최대 검색 수" + +[admin.settings.ai.documents.reindexNote] +body = "임베딩 모델 변경은 즉시 적용되지만, 이전 모델로 인덱싱된 문서는 검색이 올바른 결과를 반환하도록 다시 인덱싱해야 합니다." +title = "재인덱싱 필요" + +[admin.settings.ai.documents.saved] +reindexNote = "임베딩 모델을 변경한 경우, 검색이 새 모델을 사용하도록 기존 문서를 다시 인덱싱하세요." + +[admin.settings.ai.documents.topK] +description = "검색당 가져오는 가장 관련성 높은 청크 수입니다." +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling을 Python AI 엔진에 연결하고 노출할 AI 기능을 선택합니다. 변경 사항은 다시 시작 시 적용됩니다." +title = "AI 엔진" + +[admin.settings.ai.general.capabilities] +description = "개별 AI 기능을 켜거나 끕니다. 비활성화된 기능은 앱에서 숨겨집니다." +title = "기능" + +[admin.settings.ai.general.enabled] +description = "마스터 스위치입니다. 꺼져 있으면 AI 도구, 에이전트 또는 엔진 호출을 사용할 수 없습니다." +label = "AI 활성화" + +[admin.settings.ai.general.features.chat] +description = "PDF 작업을 위한 대화형 어시스턴트입니다." +label = "채팅 어시스턴트" + +[admin.settings.ai.general.features.classify] +description = "문서를 유형 또는 내용별로 자동 분류합니다." +label = "문서 분류" + +[admin.settings.ai.general.features.createPdf] +description = "AI 에이전트를 통해 새 PDF를 생성합니다(예: HTML에서)." +label = "프롬프트에서 PDF 만들기" + +[admin.settings.ai.general.features.documentQuestions] +description = "업로드한 문서에 근거한 질문과 답변을 제공합니다." +label = "문서 질문" + +[admin.settings.ai.general.features.mathAuditor] +description = "문서의 수학적 및 수치 오류를 검토합니다." +label = "수학 감사자" + +[admin.settings.ai.general.features.pdfComment] +description = "PDF에 AI가 작성한 검토 댓글과 주석을 추가합니다." +label = "PDF 댓글 에이전트" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "문서 생성과 같은 무거운 에이전트 작업의 시간 제한입니다." +label = "장시간 실행 시간 제한(초)" + +[admin.settings.ai.general.note] +body = "AI 엔진은 별도 서비스로 실행됩니다. 공유 비밀" +body2 = "은 컨테이너 환경 변수를 통해 설정됩니다. 제공업체 API key는 이 페이지에 입력하거나 엔진 환경 변수로 제공할 수 있으며, 여기에 입력한 key는 저장 시 엔진으로 전송됩니다." +title = "AI 엔진 정보" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "스트리밍(토큰 단위) 채팅 응답의 시간 제한입니다." +label = "스트림 시간 제한(초)" + +[admin.settings.ai.general.test] +button = "연결 테스트" +failBody = "AI 엔진이 응답하지 않았습니다. URL, 엔진 컨테이너가 실행 중인지, AI가 활성화되어 있는지 확인하세요(활성화 후 다시 시작해야 함)." +failTitle = "AI 엔진에 연결할 수 없음" +okBody = "AI 엔진이 상태 확인에 응답했습니다." +okTitle = "AI 엔진에 연결됨" + +[admin.settings.ai.general.timeoutSeconds] +description = "엔진으로 보내는 표준 AI 요청의 시간 제한입니다." +label = "요청 시간 제한(초)" + +[admin.settings.ai.general.url] +description = "Python AI 엔진의 내부 URL입니다. 예: http://stirling-pdf-engine:5001." +label = "AI 엔진 URL" + +[admin.settings.ai.limits] +description = "AI 요청이 수행할 수 있는 작업량과 동시에 실행되는 수에 대한 보호 장치입니다. 저장 시 AI 엔진에 적용됩니다." +title = "제한 및 성능" + +[admin.settings.ai.limits.maxCharacters] +description = "보호 장치: 추출된 텍스트가 이 길이를 초과하는 AI 요청을 거부합니다." +label = "요청당 최대 문자 수" + +[admin.settings.ai.limits.maxPages] +description = "보호 장치: 이 페이지 수를 초과하는 PDF에 대한 AI 요청을 거부합니다." +label = "요청당 최대 페이지 수" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "전체 엔진에서 동시에 진행 중인 모델 호출의 최대 수입니다." +label = "모델 최대 동시 실행 수" + +[admin.settings.ai.models] +description = "AI 엔진이 사용하는 LLM 제공업체와 스마트/빠른 모델을 선택합니다. 저장 시 AI 엔진에 적용됩니다." +title = "모델 및 제공업체" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "엔진 자체 환경 자격 증명을 사용하려면 비워 두세요. 자체 호스팅 단일 엔진 배포에 적용됩니다." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "저장됨 - 현재 key를 유지하려면 비워 두세요" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI 호환 / Ollama 엔드포인트의 기본 URL입니다. 예: http://ollama:11434/v1." +label = "제공업체 기본 URL" +warning = "기본 URL은 신뢰할 수 있는 내부 엔드포인트를 가리켜야 합니다. 엔진이 서버 측 요청을 보내므로 신뢰할 수 없는 값은 SSRF에 민감합니다." + +[admin.settings.ai.models.fastMaxTokens] +description = "빠른 모델의 최대 출력 토큰 수입니다." +label = "빠른 모델 최대 토큰 수" + +[admin.settings.ai.models.fastModel] +description = "가벼운 작업을 위한 더 저렴하고 빠른 모델입니다. 자유 텍스트이며, 제안은 힌트일 뿐입니다." +label = "빠른 모델" + +[admin.settings.ai.models.provider] +description = "엔진이 통신하는 LLM 제공업체입니다." +label = "제공업체" + +[admin.settings.ai.models.smartMaxTokens] +description = "스마트 모델의 최대 출력 토큰 수입니다." +label = "스마트 모델 최대 토큰 수" + +[admin.settings.ai.models.smartModel] +description = "복잡한 추론을 위한 고성능 모델입니다. 자유 텍스트이며, 제안은 힌트일 뿐입니다." +label = "스마트 모델" + +[admin.settings.ai.saved] +body = "변경 사항이 AI 엔진으로 자동 전송됩니다." +bodyNoPush = "설정이 저장되었습니다. AI 엔진이 다음에 구성을 가져올 때 적용됩니다." +title = "AI 설정 저장됨" + [admin.settings.badge] clickToUpgrade = "클릭하여 플랜 상세 보기" @@ -938,6 +1094,25 @@ label = "시작 시 재생성" description = "인증서의 유효 일수" label = "인증서 유효 기간(일)" +[admin.settings.folderAccess] +description = "폴더 소스와 폴더 출력이 읽고 쓸 수 있는 디렉터리입니다. 이는 보안 경계입니다. 자동화는 이 목록 밖의 서버 경로를 가리킬 수 없습니다." +securityNote = "폴더 소스와 출력을 완전히 비활성화하려면 비워 두세요. Stirling 자체 구성 디렉터리는 항상 접근할 수 없습니다." +title = "폴더 접근" + +[admin.settings.folderAccess.implied] +description = "이 Stirling 관리 디렉터리는 항상 허용되며 여기서 변경할 수 없습니다." +serverStorage = "서버 파일 저장소" +title = "항상 허용됨" +watchedFolder = "Pipeline 감시 폴더" + +[admin.settings.folderAccess.roots] +add = "추가" +empty = "허용된 폴더가 없습니다. 폴더 소스와 출력은 현재 비활성화되어 있습니다." +hint = "절대 경로를 입력하세요. 예: /data/inbox." +label = "허용된 폴더 루트" +placeholder = "/data/inbox" +remove = "폴더 루트 제거" + [admin.settings.general] description = "브랜딩 및 기본 동작을 포함한 시스템 전반의 애플리케이션 설정을 구성합니다." system = "시스템" @@ -1704,6 +1879,9 @@ width = "너비" [app] description = "무료 Adobe Acrobat 대안(1,000만+ 다운로드)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b로 변환" convertToPdfA3bDescription = "첨부 파일이 포함된 보관용 PDF를 생성합니다" @@ -1842,6 +2020,15 @@ export = "내보내기" accessDenied = "접근 거부" insufficientPermissions = "이 작업을 수행할 권한이 없습니다." +[auth.callback] +completing = "인증 완료 중" +invalidToken = "OAuth 로그인 실패 - 잘못된 토큰입니다." +missingToken = "OAuth 로그인 실패 - 토큰을 받지 못했습니다." +oauthFailed = "OAuth 로그인에 실패했습니다. 다시 시도하세요." +pleaseWait = "로그인 완료 중입니다. 잠시 기다려 주세요." +signedOut = "로그아웃되었습니다. 다시 로그인하세요." +windowMayClose = "완료되면 이 창을 닫아도 됩니다." + [auth.displayName] guest = "게스트" user = "사용자" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "JavaScript 및 포함된 파일과 같은 잠재적으로 description = "이 도구는 내용에 따라 PDF 파일 이름을 자동으로 바꿉니다. 문서를 분석하여 텍스트에서 가장 적합한 제목을 찾습니다." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "자동 회전" +tags = "자동 회전,방향 수정,거꾸로,옆으로,바로잡기,방향,방향 감지,회전 보정,자동 방향,스캔됨,OSD" + +[autoRotate.confidenceThreshold] +title = "신뢰도 임계값" + +[autoRotate.detectionMode] +auto = "자동" +osd = "OCR만" +text = "텍스트만" +title = "감지 방법" + +[autoRotate.error] +failed = "PDF를 자동 회전하는 동안 오류가 발생했습니다." + +[autoRotate.inferUndetected] +title = "낮은 신뢰도 페이지도 맞춰 회전" + +[autoRotate.report] +applied = "{{degrees}}° 시계 방향" +confidenceValue = "신뢰도 {{value}}" +noChange = "변경 없음" +pageLabel = "페이지 {{number}}" +summary = "{{total}}페이지 중 {{rotated}}페이지가 회전되었고, {{unchanged}}페이지는 그대로 유지되었습니다" +title = "감지 보고서" + +[autoRotate.report.method] +inferred = "일치됨" +none = "건너뜀" +osd = "OCR" +text = "텍스트" + +[autoRotate.report.note] +belowThreshold = "임계값 미만" +blankPage = "빈 페이지입니다" +inferredFromDocument = "다른 페이지와 동일" +noDominantDirection = "텍스트 방향이 혼재됨" +osdFailed = "읽을 수 있는 텍스트를 찾을 수 없음" +osdNoVerdict = "어느 쪽이 위인지 판단할 수 없음" +tesseractUnavailable = "OCR이 설치되어 있지 않음" +tooFewGlyphs = "텍스트가 너무 적음" + +[autoRotate.results] +title = "자동 회전 결과" + +[autoRotate.settings] +title = "설정" + +[autoRotate.tooltip.confidenceThreshold] +text = "페이지를 회전하기 전에 검사 결과가 얼마나 확실해야 하는지입니다. 더 많은 페이지를 회전하려면 낮추고, 더 많은 페이지를 그대로 두려면 높이세요." +title = "신뢰도 임계값" + +[autoRotate.tooltip.description] +text = "각 페이지가 어느 방향이어야 하는지 판단하고 올바른 방향으로 회전합니다. 이미 똑바른 페이지는 그대로 둡니다." + +[autoRotate.tooltip.detectionMode] +text = "자동은 먼저 페이지의 텍스트를 읽고, 충분하지 않으면 페이지를 이미지로 확인합니다. 텍스트만은 이미지 검사를 건너뛰며 스캔된 적 없는 문서에 가장 빠릅니다. OCR만은 모든 페이지를 이미지로 처리합니다." +title = "감지 방법" + +[autoRotate.tooltip.header] +title = "자동 회전" + +[autoRotate.tooltip.inferUndetected] +text = "빈 페이지 또는 거의 빈 페이지는 단독으로 확인할 수 없습니다. 읽을 수 있는 모든 페이지가 같은 회전이 필요하면, 이러한 페이지도 그에 맞춰 회전됩니다." +title = "낮은 신뢰도 페이지도 맞춰 회전" + [autoSizeSplitPDF] tags = "pdf,분할,문서,정리" @@ -2189,33 +2443,6 @@ keywords = "키워드: odd, even" numbers = "숫자/범위: 5, 10-20" progressions = "등차 수열: 3n, 4n+1" -[certificateChoice.tooltip] -header = "인증서 유형" - -[certificateChoice.tooltip.organization] -bullet1 = "시스템 관리자가 관리" -bullet2 = "인가된 사용자 간 공유" -bullet3 = "개인이 아닌 회사 신원을 대표" -bullet4 = "권장 용도: 공식 문서, 팀 서명" -description = "조직에서 제공하는 공유 인증서입니다. 전사적 서명 권한에 사용됩니다." -title = "조직 인증서" - -[certificateChoice.tooltip.personal] -bullet1 = "첫 사용 시 자동 생성" -bullet2 = "사용자 계정과 연결" -bullet3 = "다른 사용자와 공유 불가" -bullet4 = "권장 용도: 개인 문서, 개인 책임" -description = "사용자 계정에 고유한 자동 생성 인증서입니다. 개인 서명에 적합합니다." -title = "개인 인증서" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX 파일과 비밀번호 필요" -bullet2 = "외부 인증 기관에서 발급 가능" -bullet3 = "법적 문서에 더 높은 신뢰 수준" -bullet4 = "권장 용도: 법적 효력 있는 계약, 외부 검증" -description = "PKCS#12 인증서 파일을 직접 사용합니다. 인증서 속성을 완전히 제어할 수 있습니다." -title = "사용자 지정 P12 업로드" - [certSign] awaitingSignatures = "서명 대기 중" chooseCertificate = "인증서 파일 선택" @@ -2542,34 +2769,6 @@ server = "서버" stepTitle = "인증서 원본" upload = "업로드" -[certSign.tooltip.header] -title = "서명 관리 안내" - -[certSign.tooltip.overview] -bullet1 = "기존 서명과 유효성 확인" -bullet2 = "서명자 및 인증서의 상세 정보 보기" -bullet3 = "문서 보안을 위해 새 디지털 서명 추가" -bullet4 = "여러 파일을 지원하며 쉽게 탐색" -text = "이 도구는 PDF에 디지털 서명이 있는지 확인하고 새 디지털 서명을 추가할 수 있게 합니다. 디지털 서명은 문서를 누가 작성하거나 승인했는지 증명하고, 서명 이후 변경되었는지도 보여줍니다." -title = "이 도구로 할 수 있는 일" - -[certSign.tooltip.signing] -bullet1 = "PEM, PKCS12, JKS 및 서버 인증서 형식 지원" -bullet2 = "PDF에서 서명을 표시/숨기기 옵션" -bullet3 = "사유, 위치, 서명자 이름 추가" -bullet4 = "표시 서명을 배치할 페이지 선택" -bullet5 = "서버 인증서를 사용하여 간단한 'Sign with Stirling-PDF' 옵션 사용" -text = "PDF에 서명하려면 디지털 인증서(PEM, PKCS12, JKS 등)가 필요합니다. 문서에 서명을 보이게 하거나 보안용으로만 보이지 않게 할 수 있습니다." -title = "서명 추가" - -[certSign.tooltip.validation] -bullet1 = "서명이 유효한지 여부 표시" -bullet2 = "서명자 정보와 서명 날짜 표시" -bullet3 = "서명 후 문서가 수정되었는지 확인" -bullet4 = "검증에 사용자 지정 인증서를 사용할 수 있음" -text = "서명을 검사하면, 유효한지, 누가 언제 서명했는지, 서명 이후 문서가 변경되었는지를 알려줍니다." -title = "서명 확인" - [changeCreds] changeUsername = "사용자 이름을 업데이트합니다. 업데이트 후 로그아웃됩니다." credsUpdated = "계정이 업데이트되었습니다" @@ -2737,12 +2936,16 @@ text = "이 권한을 변경할 수 없도록 하려면, 비밀번호 추가 도 [chat.actions] copy = "메시지 복사" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling 에이전트 옵션" +assistant = "AI Assistant" clearChat = "채팅 지우기" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "이 작업은 클라우드 크레딧을 사용합니다" tooltip = "화면에서 색상 선택" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "색상 선택" [common] +actions = "Actions" back = "뒤로" cancel = "취소" close = "닫기" +codeSample = "Code sample" collapse = "접기" confirm = "확인" continue = "계속" copied = "복사됨!" copy = "복사" done = "완료" +enterValue = "Enter value" error = "오류" expand = "펼치기" loading = "불러오는 중..." next = "다음" +open = "Open" preview = "미리보기" previous = "이전" refresh = "새로고침" +remaining = "남음" retry = "다시 시도" save = "저장" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "선택 항목 지우기" @@ -2988,6 +3199,7 @@ title = "압축 방식" [compress.settings] desiredSize = "원하는 파일 크기" desiredSizePlaceholder = "크기 입력" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "압축은 파일 크기를 줄이는 쉬운 방법입니다. 파일 크기를 선택하면 목표 크기를 입력하고 품질은 저희가 조정합니다. 품질을 선택하면 압축 강도를 직접 설정할 수 있습니다." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF 옵션" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "먼저 원본 형식을 선택하세요" settings = "설정" single = "단일" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "고급 모니터링" -allInOneWorkspace = "올인원 PDF 작업 공간(뷰어, 도구 & 에이전트)" -apiSandbox = "API 샌드박스" -dedicatedSupportSlas = "전담 지원 및 SLA" -fasterThroughput = "처리량 10배 향상" -fullyPrivateFiles = "파일 완전 비공개" -largeFileProcessing = "대용량 파일 처리" -orgWideAccess = "조직 전체 액세스 제어" -premiumAiModels = "프리미엄 AI 모델" -prioritySupport = "우선 지원" -privateDocCloud = "프라이빗 문서 클라우드" -ragFineTuning = "RAG + 파인튜닝" -secureApiAccess = "보안 API 액세스" -standardThroughput = "표준 처리량" -unlimitedApiAccess = "무제한 API 액세스" -unlimitedSeats = "무제한 좌석" - [crop] autoCrop = "여백 자동 자르기" noFileSelected = "자르기를 시작하려면 PDF 파일을 선택하세요" @@ -3346,6 +3558,7 @@ message = "기본 PDF 처리기 설정에 실패했습니다" title = "오류" [defaultApp.prompt] +dontRemind = "다시 알리지 않기" message = "PDF 파일을 여는 기본 앱으로 Stirling PDF를 설정하세요." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "검색..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "편집" @@ -3644,11 +3861,13 @@ shareSelected = "선택 항목 공유" sharing = "공유" showAll = "모두 표시" showHistory = "기록 표시" +sortBy = "Sort files" sortByDate = "날짜순 정렬" sortByName = "이름순 정렬" sortBySize = "크기순 정렬" storage = "저장소" storageState = "저장소" +storageUsed = "Storage used" synced = "동기화됨" title = "PDF 파일 업로드" toolChain = "적용된 도구" @@ -3694,13 +3913,17 @@ addFiles = "파일 추가" addingFiles = "파일 추가 중…" collapse = "사이드바 접기" customizeGroups = "그룹 사용자 지정" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "시작하려면 파일을 여세요" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "사이드바 펼치기" -files = "파일" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive가 구성되지 않았습니다" leaveMyFiles = "내 파일 나가기" +library = "PDF 라이브러리" myFiles = "내 파일" noFiles = "아직 파일이 없습니다" openFileManager = "파일 관리자 열기" @@ -3708,37 +3931,36 @@ openFromComputer = "컴퓨터에서 열기" openSettings = "설정 열기" other = "기타" recent = "최근" -search = "검색" -searchPlaceholder = "파일 검색..." viewAll = "{{count}}개 파일 모두 보기" [fileSidebar.fileItem] closeViewer = "뷰어 닫기" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "뷰어에서 열기" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "추가" -addLabel = "라벨 추가…" -categoryIconAria = "{{name}}의 아이콘 선택" -createCategory = "카테고리 만들기" -delete = "카테고리 삭제" done = "완료" -hide = "카테고리 숨기기" -newCategory = "새 카테고리 이름…" -removeLabel = "{{name}} 제거" -rename = "이름 변경" reset = "기본값으로 재설정" -search = "라벨 검색…" -show = "카테고리 표시" subtitle = "파일을 상위 카테고리로 그룹화하세요. 기존 또는 새 라벨을 카테고리에 추가하거나, 이름을 변경하거나, 직접 만들 수 있습니다. 어떤 카테고리에도 속하지 않는 파일은 “기타” 아래에 표시됩니다." title = "사이드바 카테고리" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "작업 공간에 추가" addToWorkspaceCount = "{{count}}개를 작업 공간에 추가" @@ -3749,7 +3971,6 @@ backToMyFiles = "내 파일로 돌아가기" breadcrumbs = "폴더 경로" cancel = "취소" classification = "분류" -clearSearch = "검색 지우기" clearSelection = "선택 해제" closeDetails = "세부정보 닫기" create = "생성" @@ -3781,6 +4002,7 @@ downloadAll = "전체 다운로드" downloadVersion = "이 버전 다운로드" dropOverlay = "업로드하려면 파일을 놓으세요" dropOverlaySub = "파일은 로컬에서 시작됩니다. '이동' 또는 '클라우드에 저장'으로 폴더에 정리하세요." +duplicate = "Duplicate" file = "파일" fileInfo = "File info" fileMenu = "파일 작업" @@ -3808,13 +4030,12 @@ refresh = "서버에서 새로고침" remove = "삭제" removeVersion = "이 버전 제거" rename = "이름 변경" +renamed = "이름 변경됨" renameFolder = "폴더 이름 변경" resizeFolderTree = "폴더 트리 크기 조절(화살표 키, 크게 조절은 Shift; 자동 맞춤은 더블 클릭)" save = "저장" saveToServer = "서버에 저장" saveToServerDisabledHint = "이 서버에서는 서버로 저장이 활성화되어 있지 않습니다. 관리자에게 활성화를 요청하세요." -search = "검색" -searchPlaceholder = "이 폴더 및 하위 폴더 검색" selectAll = "모두 선택" selectAllHint = "모두 선택하려면 클릭. 팁: Ctrl(또는 Cmd)로 하나씩 추가, Shift로 범위 선택." selectedCount = "{{count}}개 선택됨" @@ -3874,6 +4095,10 @@ title = "아직 클라우드 파일이 없습니다" hint = "업로드하지 않고 저장한 파일은 여기에 남습니다. 파일을 놓아 추가하세요." title = "로컬 전용 파일 없음" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "열거나 편집한 파일이 여기에 표시됩니다." title = "아직 수정된 항목이 없습니다" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "폴더를 삭제할 수 없습니다." deleteFolderFailedDetail = "폴더를 삭제할 수 없습니다: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "폴더 모양을 업데이트할 수 없습니다." folderAppearanceFailedDetail = "폴더 모양을 업데이트할 수 없습니다: {{message}}" moveFilesFailed = "파일을 이동할 수 없습니다." @@ -3910,9 +4137,7 @@ count = "파일" folder = "폴더" labels = "라벨" modified = "수정됨" -name = "이름" size = "크기" -toolHistoryAtVersion = "누적 도구 체인" totalSize = "총 크기" type = "유형" versionHistory = "버전 기록" @@ -3945,6 +4170,11 @@ localHint = "이 브라우저에만 저장됨" shared = "공유됨" sharedHint = "링크로 공유됨" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "오래된 순" modifiedDesc = "최신 순" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "저장소에 파일이 없습니다. 먼저 파일을 open = "열기" openFile = "파일 열기" openFiles = "여러 파일 열기" +selectFile = "Select {{name}}" selectFromStorage = "저장소에서 선택" upload = "업로드" uploadFile = "파일 업로드" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "모두 저장됨" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV 추출 실패" +extractXlsxError = "XLSX 추출 실패" +filled = "filled" flattenAfterFilling = "채운 후 평면화" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "필수" +requiredFieldsError = "모든 필수 필드를 입력하세요" rescanFields = "필드 재검색" rescanFormFields = "양식 필드 재검색" save = "저장" +saveShortcut = "저장하려면 Ctrl+S" +unsavedChanges = "저장되지 않은 변경 사항" [formFill.sidebar] close = "사이드바 닫기" @@ -4309,6 +4552,11 @@ desc = "감지된 헤더를 기반으로 PDF 파일 이름을 자동으로 변 tags = "자동 감지,헤더 기반,정리,이름 변경" title = "PDF 파일 자동 이름 변경" +[home.autoRotate] +desc = "각 페이지의 방향을 감지하고 자동으로 똑바로 회전합니다." +tags = "자동 회전,방향 수정,거꾸로,옆으로,바로잡기,방향,방향 감지,회전 보정,자동 방향,스캔됨,OSD" +title = "자동 회전" + [home.autoSizeSplitPDF] tags = "자동,분할,크기" title = "크기/개수별 자동 분할" @@ -4608,15 +4856,12 @@ hint = "PDF에 배치할 PNG, JPG, SVG 또는 기타 이미지 파일을 업로 label = "이미지 업로드" placeholder = "이미지 파일 선택" -[imageToPdf] -tags = "변환,이미지,jpg,사진" - [imageToPDF] header = "이미지를 PDF로" title = "이미지를 PDF로" -[infoBanner] -dismiss = "닫기" +[imageToPdf] +tags = "변환,이미지,jpg,사진" [invite] acceptError = "계정 생성에 실패했습니다" @@ -4647,11 +4892,9 @@ welcomeTitle = "초대를 받으셨습니다!" [landing] addFiles = "파일 추가" -heroSubtitle = "시작하려면 기존 PDF를 끌어놓거나 추가하세요." mobileUpload = "모바일에서 업로드" openFromComputer = "컴퓨터에서 열기" uploadFromComputer = "컴퓨터에서 업로드" -workbenchEmptyStateHero = "어디에나 PDF를 놓으세요" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "사용자 이름 입력" failedToSignIn = "{{provider}}로 로그인하지 못했습니다: {{message}}" forgotPassword = "비밀번호를 잊으셨나요?" loggingIn = "로그인 중..." -logIn = "로그인" login = "로그인" magicLinkSent = "매직 링크가 {{email}}로 전송되었습니다! 이메일을 확인하고 링크를 클릭해 로그인하세요." mfaCode = "인증 코드" @@ -4715,26 +4957,10 @@ signInWith = "다음으로 로그인" title = "로그인" unexpectedError = "예기치 않은 오류: {{message}}" updatePassword = "비밀번호 업데이트" -useEmailInstead = "이메일로 로그인" useMagicLink = "대신 매직 링크 사용" username = "사용자 이름" youAreLoggedIn = "로그인되었습니다!" -[login.slides.edit] -alt = "PDF 편집" -subtitle = "PDF 가리기, 서명, 읽기 및 처리에 도움이 되는 십여 개 이상의 도구로 원하는 기능을 찾을 수 있습니다." -title = "원하는 정보를 표시/보호하도록 PDF를 편집하세요" - -[login.slides.overview] -alt = "Stirling PDF 개요" -subtitle = "개인정보 보호를 최우선으로 하는 PDF 클라우드 제품군으로, 변환, 서명, 가리기, 문서 관리 등 50개 이상의 강력한 도구를 제공합니다." -title = "모든 PDF 작업을 한곳에서" - -[login.slides.secure] -alt = "PDF 보호" -subtitle = "비밀번호 추가, 콘텐츠 가리기, 인증서 관리를 손쉽게." -title = "PDF의 민감한 정보를 보호하세요" - [margin] large = "크게" medium = "중간" @@ -4818,6 +5044,34 @@ uploadSuccess = "업로드 성공!" uploadSuccessMessage = "이미지가 전송되었습니다." validating = "세션 검증 중..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "스캔하여 사진을 업로드하세요. 이미지는 자동으로 PDF로 변환됩니다." descriptionNoConvert = "모바일 기기에서 사진을 스캔해 업로드하세요." @@ -4836,8 +5090,13 @@ title = "모바일에서 업로드" tags = "멀티 도구,다중 작업,UI,클릭 드래그,프론트엔드,클라이언트 사이드,대화형,상호작용,이동,삭제,마이그레이션,분할" title = "PDF 멀티 도구" -[navbar] -search = "검색" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "인증에 성공하지 못했습니다. 이 창을 닫고 다시 시도하세요." @@ -4966,6 +5225,7 @@ activeFiles = "활성 파일 보기에는 도구에 로드한 allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "닫기" cropSettings = "이제 자르려는 파일을 선택했으므로, 자르기 도구를 구성하여 PDF를 자를 영역을 지정할 수 있습니다." +dialogLabel = "Onboarding" fileCheckbox = "파일을 클릭하면 처리 대상으로 선택됩니다. 배치 작업을 위해 여러 파일을 선택할 수 있습니다." fileReplacement = "수정된 파일은 워크벤치에서 원본 파일을 자동으로 대체하므로, 더 많은 도구로 쉽게 이어서 작업할 수 있습니다." filesButton = "빠른 액세스 막대의 파일 버튼을 사용하면 도구로 처리할 PDF를 업로드할 수 있습니다." @@ -4976,6 +5236,7 @@ pinButton = "도구 실행 후에도 파일을 계속 활성 상태로 유지하 results = "도구 실행이 완료되면 검토 단계에서 이 패널에 결과 미리보기가 표시되며, 작업을 되돌리거나 파일을 다운로드할 수 있습니다. " runButton = "도구 구성을 마치면, 이 버튼으로 선택된 모든 PDF에 도구를 실행할 수 있습니다." selectCropTool = "도구 사용 방법을 보여주기 위해 자르기 도구를 선택해 보겠습니다." +stepOf = "{{total}}단계 중 {{current}}단계" toolInterface = "여기가 자르기 도구 인터페이스입니다. 아직 작업할 PDF 파일을 추가하지 않았기 때문에 볼 내용이 많지 않습니다." workbench = "여기는 워크벤치입니다. PDF를 보고 편집하는 주요 영역입니다." wrapUp = "이제 준비 완료입니다! 앱의 주요 영역과 사용 방법을 살펴보았습니다. 언제든지 도움말 버튼을 클릭하여 이 투어를 다시 볼 수 있습니다." @@ -4988,6 +5249,26 @@ showMeAround = "둘러보기" skipForNow = "나중에 건너뛰기" skipTheTour = "투어 건너뛰기" +[onboarding.checklist] +dismiss = "닫기" +title = "Stirling PDF 설정" + +[onboarding.checklist.downloadDesktop] +description = "컴퓨터에서 Stirling을 네이티브로 실행" +title = "Stirling 데스크톱 다운로드" + +[onboarding.checklist.inviteTeam] +description = "팀과 협업" +title = "팀원 초대" + +[onboarding.checklist.shareAnalytics] +description = "Stirling 개선에 도움 주기" +title = "익명 사용 데이터 공유" + +[onboarding.checklist.takeTour] +description = "빠른 안내로 Stirling 작동 방식 보기" +title = "둘러보기" + [onboarding.desktopInstall] body = "Stirling은 데스크톱 앱으로 사용할 때 가장 잘 작동합니다. 오프라인으로 사용하고, 문서를 더 빠르게 열며, 컴퓨터에서 로컬로 편집할 수 있습니다." selectOs = "운영 체제 선택" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}}용 다운로드" [onboarding.mfa] authenticationCode = "인증 코드" +description = "인증 앱을 연결하여 계정을 보호하세요. QR 코드를 스캔하거나 설정 key를 입력한 다음, 6자리 코드를 확인하여 완료하세요." +enable = "MFA 활성화" +enableError = "2단계 인증을 활성화할 수 없습니다. 코드를 확인하고 다시 시도하세요." +enterCode = "앱의 6자리 코드를 입력하세요." +enterCodeError = "계속하려면 인증 코드를 입력하세요." +logout = "로그아웃" +openAuthenticator = "Google Authenticator, Authy 또는 1Password를 여세요." qrCodeLoading = "QR 코드 생성 중…" +regenerateQrCode = "QR 코드 다시 생성" +scanQrCode = "QR 코드를 스캔하거나 아래 설정 key를 입력하세요." +setupError = "2단계 설정을 시작할 수 없습니다. 다시 시도하세요." +setupKey = "설정 key(수동 입력)" +stepByStep = "단계별" +success = "MFA가 활성화되었습니다. 이제 계속할 수 있습니다." +title = "다중 인증 설정" [onboarding.planOverview] adminBodyLoginDisabled = "로그인 모드를 활성화하면 사용자 관리, 설정 구성, 서버 상태 모니터링을 할 수 있습니다. 서버의 첫 {{freeTierLimit}}명은 무료로 Stirling을 사용할 수 있습니다." @@ -5005,6 +5300,11 @@ adminTitle = "관리자 개요" userBody = "팀원을 초대하고 역할을 지정하며 문서를 하나의 안전한 작업 공간에서 정리하세요. 솔로 사용을 넘어 성장할 준비가 되면 언제든 로그인 모드를 활성화하세요." userTitle = "플랜 개요" +[onboarding.processorIntro] +body = "Stirling은 이제 정책을 실행합니다 — 문서가 도착할 때마다 분류, 보호, 처리하는 자동화 규칙입니다. Processor에서 설정하고 실행을 모니터링하세요." +cta = "Processor 확인하기" +title = "Stirling Processor 확인하기" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "선불 연간" + +[payg.prepaid.banner] +exhausted = "선불 용량을 모두 사용했습니다. 종량제 청구가 재개되었습니다. 할인을 유지하려면 충전하세요." +expiring_one = "선불 연간 기간이 {{count}}일 후 종료됩니다. 사용하지 않은 용량은 만료됩니다 — 새 연도를 시작하려면 충전하세요." +expiring_other = "선불 연간 기간이 {{count}}일 후 종료됩니다. 사용하지 않은 용량은 만료됩니다 — 새 연도를 시작하려면 충전하세요." +low = "선불 용량이 {{pct}}% 남았습니다. 처리가 중단되지 않도록 언제든지 충전하세요." + +[payg.prepaid.card] +subtitle = "종량제 청구 전에 사용되며 지출 한도 외부에서 사용됩니다." +title = "선불 용량" + +[payg.prepaid.meter] +capSuffix = "총 {{total}} 선불 크레딧 중" +expires = "{{date}} 만료" + +[payg.prepaid.state] +exhausted = "모두 사용됨" +healthy = "충분히 남음" +low = "부족해지는 중" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF를 PDF/A로" tags = "아카이브,장기,표준,변환,저장,보존" title = "PDF를 PDF/A로" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "인쇄,표준,변환,제작,프리프레스,보관" title = "PDF를 PDF/X로" @@ -5822,6 +6151,7 @@ small = "500 크레딧" xsmall = "100 크레딧" [plan.availablePlans] +currency = "Billing currency" subtitle = "필요에 맞는 요금제를 선택하세요" title = "이용 가능한 요금제" @@ -5937,49 +6267,12 @@ keyDescription = "이메일의 라이선스 키를 붙여넣으세요" success = "라이선스 활성화됨!" successMessage = "라이선스가 성공적으로 활성화되었습니다. 이제 이 창을 닫아도 됩니다." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "정책 적용됨" -enforcing = "정책 적용 중..." -failed = "정책 적용 실패" outputsUnavailable = "정책 출력물을 더 이상 다운로드할 수 없습니다." partialOutputsUnavailable = "일부 정책 출력물을 더 이상 다운로드할 수 없습니다." -retrying = "바쁨, 재시도 중..." runNotFound = "정책 적용 실행을 더 이상 찾을 수 없습니다." -step = "단계 {{current}}/{{total}}" timedOut = "실행이 완료되기 전에 정책 적용이 시간 초과되었습니다." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "닫기" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} 적용 중" applyingProgress = "{{names}} 적용 중 ({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}}개 중 {{failures}}개 파일을 처리할 수 없어 failureTitle = "완전한 정책 적용 없이 내보냄" printPolicyAppliedBody = "정책을 충족하도록 이 PDF가 업데이트되었습니다. 변경 사항을 확인한 후 다시 인쇄하세요." printPolicyAppliedTitle = "인쇄 전에 정책 적용됨" -queued = "+{{count}}개 대기 중" successTitle = "{{names}} 적용됨" summaryMore = "{{first}}, {{second}} 및 {{more}}개 더" summaryTwo = "{{first}} 및 {{second}}" -[policies.enforcement.triggerVerb] -convert = "변환 전에 정책 적용" -default = "정책 적용" -export = "내보내기 전에 정책 적용" -input = "가져오기 시 정책 적용" -print = "인쇄 전에 정책 적용" - -[policies.field] -accessLog = "액세스 로그" -archiveAfter = "다음 이후 보관" -auditTrail = "감사 추적" -belowThreshold = "임계값 미만" -destination = "대상" -frameworks = "프레임워크" -immutableHold = "변경 불가 보류" -keepFor = "보관 기간" -minConfidence = "최소 신뢰도" -notify = "라우팅 시 알림" -onViolation = "비준수 시" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "추가" -addPlaceholder = "라벨 추가…" -customNote = "팀에 맞게 사용자 지정되었습니다." -defaultNote = "기본 내장값을 사용 중이며 팀과 공유됩니다." -duplicate = "\"{{name}}\"이(가) 이미 존재합니다." -edit = "라벨 편집" -empty = "아직 라벨이 없습니다." -export = "JSON 내보내기" -iconAria = "{{name}}의 아이콘 선택" -import = "JSON 가져오기" -importError = "해당 파일을 가져올 수 없습니다." -managedNote = "팀 라벨은 팀 리더가 관리합니다." -modalSubtitle = "전체 팀과 공유됩니다. 분류기가 각 문서에 맞는 라벨을 선택합니다." -modalTitle = "분류 라벨" -pickIcon = "{{label}} 아이콘 사용" -removeAria = "{{name}} 제거" -resetToDefault = "기본값으로 재설정" -saveForTeam = "팀용으로 저장" -saving = "저장 중…" -sectionLabel = "분류 라벨" -startFromScratch = "처음부터 시작" -teamCount = "팀 라벨" -tooLong = "라벨은 최대 {{max}}자까지 가능합니다." -ungrouped = "그룹 없음" -view = "라벨 보기" +categoryCount = "카테고리" +hideCategory = "카테고리 숨기기" +labelCount = "레이블" +sharedNote = "이 레이블은 기본 제공되며 전체 팀에서 공유됩니다." +showCategory = "카테고리 표시" [policies.pii] account = "계좌 번호(라벨 지정됨)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "미국 라우팅 번호(ABA)" ssn = "사회보장 번호" -[policies.settings] -noneExport = "현재 내보내기 시 실행되는 정책이 없습니다." -noneUpload = "현재 업로드 시 실행되는 정책이 없습니다." -onExport = "내보내기 시" -onUpload = "업로드 시" -reorderHandle = "드래그하여 순서 변경" -runOrderDesc = "둘 이상의 정책이 같은 트리거에서 실행되면 이 순서대로 실행됩니다 — 각 정책은 이전 정책의 출력에 대해 실행됩니다. 드래그하여 순서를 변경하세요." -title = "정책 설정" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "정책 옵션" -policySettings = "정책 설정" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "실패한 정책 다시 시도({{count}})" -rowProgress = "{{total}}개 파일 중 {{completed}}개 처리됨" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "정책이란 무엇인가요?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "사회보장 번호나 카드 번호와 같은 민감한 정보를 자동으로 찾아 문서에서 읽을 수 없도록 검게 가립니다." -sanitize = "파일에서 숨겨진 JavaScript를 제거하여 문서를 열 때 어떤 것도 자동으로 실행되지 않도록 합니다." -watermark = "모든 페이지에 보이는 표시(예: \"Confidential\")를 찍습니다." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "닫기" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} 적용 중..." badgeRan = "이 파일에 {{name}} 정책이 실행되었습니다" +badgeRunning = "{{name}} 실행 중..." blockingAction = "정책 적용 중 {{action}}이(가) 차단되었습니다. 잠시 기다려 주세요..." dismiss = "오버레이 닫기" enforcingTitle = "정책 적용 중..." @@ -6151,6 +6316,7 @@ viewAnyway = "파일 보기(정책은 계속 적용 중)" [policyOption] autoRedactPhi = "PHI 자동 수정" blockExport = "내보내기 차단" +ccpa = "CCPA" contracts = "계약서" documents = "문서" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor 플랜" subscribed = "Processor 플랜" unlinked = "연결되지 않음" -[portal.agentBuilder] -bootstrapFromDocument = "문서에서 Bootstrap" -sectionsAriaLabel = "Agent Builder 섹션" -selectorAriaLabel = "Agent" -subtitle = "문서를 분류하고, 추출하고, 라우팅하는 AI agent를 설계, 테스트, 배포하세요. 시나리오를 정의하고, 도구 액세스를 제한하고, golden set을 실행하고, 버전을 게시하세요." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "취소" -dropzoneText = "샘플 문서(PDF 또는 이미지) 선택" -lead = "샘플 문서를 놓으면 조정할 수 있는 시나리오와 추출 스키마를 제안합니다. 검토하기 전까지는 아무것도 게시되지 않습니다." -submit = "Agent Bootstrap" -subtitle = "대표 파일 하나로 새 agent 시드" -title = "문서에서 Bootstrap" - -[portal.agentBuilder.empty] -description = "샘플 문서에서 agent를 Bootstrap하여 시나리오와 추출 스키마를 시드한 다음 조정하고 게시하세요." -title = "아직 agent가 없습니다" - -[portal.agentBuilder.evals] -casesPassing = "통과한 케이스" -columnCase = "Eval 케이스" -columnLatency = "지연 시간" -columnResult = "결과" -fail = "실패" -goldenSetPassRate = "Golden-set 통과율" -latencyMs = "{{ms}} ms" -notRun = "실행 안 됨" -pass = "통과" -passRate = "통과율" -runEvals = "eval 실행" - -[portal.agentBuilder.evals.empty] -description = "Eval은 시나리오를 반복 가능한 golden set으로 만듭니다. 업그레이드하면 시간 경과에 따른 통과율을 캡처하고 이를 기준으로 게시를 제어할 수 있습니다." -title = "아직 golden set이 없습니다" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "golden set 전체" -activeAgents = "활성 agent" -avgPassRate = "평균 eval 통과율" -fleetWide = "전체 fleet" -latestPublished = "최신 게시" -scenarios = "시나리오" -testCases = "테스트 케이스" -totalDescription_one = "총 {{count}}개" -totalDescription_other = "총 {{count}}개" - -[portal.agentBuilder.scenarios] -add = "추가" -addScenario = "시나리오 추가" -enable = "활성화" -expectationLabel = "예상 동작" -expectationPlaceholder = "agent가 수행해야 할 작업" -inEval = "eval 중" -mute = "음소거" -muted = "음소거됨" -nameLabel = "이름" -namePlaceholder = "예: 규정 준수 에스컬레이션" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "시나리오" -tools = "도구" -versions = "버전" - -[portal.agentBuilder.tools] -broadAccess = "광범위한 액세스" -deniedHint = "선택한 도구가 차단되었습니다. 그 외 모든 도구는 호출 가능합니다." -deniedTools = "거부된 도구" -governanceGate = "도구 거버넌스는 Enterprise 플랜에서 사용할 수 있습니다." -restricted = "제한됨" -restrictedAccess = "제한된 도구 액세스" -restrictedDescription = "아래에서 거부한 도구를 제외한 모든 도구를 허용합니다." - -[portal.agentBuilder.versions] -current = "현재" -historyGate = "전체 버전 기록 및 롤백은 Enterprise 플랜에서 사용할 수 있습니다." -publish = "게시" -rollBack = "롤백" - [portal.assistant] close = "Assistant 닫기" error = "Assistant에 연결할 수 없습니다." @@ -6365,9 +6451,21 @@ typing = "입력 중" loading = "로딩 중" redirectingToEditor = "Editor로 리디렉션 중..." +[portal.billing.activation] +maybeLater = "나중에" +title = "Processor 켜기" + +[portal.billing.activation.payg] +sub = "월간 한도를 설정하세요. 실행한 만큼 결제합니다." +title = "사용한 만큼 결제" + +[portal.billing.activation.prepay] +badge = "2개월 무료" +sub = "청구서 하나, 카드 또는 은행 송금." +title = "1년 선불" + [portal.billing.checkout] noClientSecret = "Edge function이 client_secret을 반환하지 않았습니다." -subtitle = "무료 Editor 플랜 제공량을 넘어 계속 사용하려면 카드를 추가하세요. 나머지는 Stripe가 처리합니다." title = "Processor 플랜 켜기" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "결제는 성공했지만 활성화가 평소보다 조금 더 오래 close = "닫기" title = "거의 완료되었습니다" +[portal.billing.checkout.cap] +amountAria = "월간 지출 한도" +back = "뒤로" +continue = "결제로 계속" +error = "지출 한도를 설정할 수 없습니다" +finePrint = "청구서는 매월 발행됩니다. 언제든지 취소할 수 있습니다." +noLimit = "한도 없음" +note = "한도를 초과해 청구되지 않습니다. 처리는 일시 중지될 뿐입니다." +perMonth = "/월" +title = "지출 한도 설정" + +[portal.billing.checkout.card] +fields = "카드 번호 · MM / YY · CVC · ZIP" +label = "카드 정보" +note = "카드 정보는 Stripe에서 수집합니다. Stirling은 PAN 또는 CVC를 저장하지 않습니다." + [portal.billing.checkout.error] title = "Checkout을 시작할 수 없음" @@ -6383,10 +6497,9 @@ body = "결제가 완료되었습니다. 연결된 인스턴스 전반에서 종 hint = "이 창을 열어 두세요." title = "Processor 플랜 활성화 중..." -[portal.billing.checkout.notConfigured] -bodyAfter = "을(를) portal env에 설정하여 앱 내 checkout을 활성화하세요." -bodyBefore = "설정" -title = "Stripe가 구성되지 않음" +[portal.billing.checkout.pay] +limit = "월간 지출 한도 {{amount}}/월. 한도에 도달하면 처리가 일시 중지됩니다." +limitNoCap = "지출 한도 없음. 처리가 일시 중지되지 않습니다. 청구서는 매월 발행됩니다." [portal.billing.enterpriseUpsell] cta = "Enterprise 견적 만들기" @@ -6409,10 +6522,12 @@ currentPlan = "현재 플랜" freeForever = "영구 무료" noTeamResolved = "지갑에서 아직 팀이 확인되지 않았습니다 — 새로 고침 후 다시 시도하세요." ownerOnly = "팀 소유자만 Processor 플랜을 켤 수 있습니다." +payInvoice = "완료하려면 청구서 결제" planName = "Editor" ssoIncluded = "SSO 포함" switchOnProcessor = "Processor 켜기 →" unlimitedUsers = "무제한 사용자" +viewQuote = "견적 보기" [portal.billing.invoices] columnAmount = "금액" @@ -6465,7 +6580,111 @@ segmentApiDesc = "직접 API 요청" segmentApiLabel = "API" segmentAutomationDesc = "자동화 및 파이프라인" segmentAutomationLabel = "자동화" +sizeMultiplier = "{{formatted}} PDFs에 크기 배수가 적용됨" +summary = "{{unique}} 고유 · {{units}} 미터 단위 · PDF당 평균 {{avg}}" +summaryNoRate = "{{unique}} 고유 · {{units}} 미터 단위" unit = "종량제 PDFs" +unitsPending_one = "연결된 인스턴스에서 {{units}} 미터 단위 동기화 대기 중" +unitsPending_other = "연결된 인스턴스에서 {{units}} 미터 단위 동기화 대기 중" + +[portal.billing.prepaid] +capSuffix = "총 {{total}} 선불 크레딧 중" +expires = "{{date}} 만료" +eyebrow = "선불 용량" +note = "종량제 청구 전에 사용되며 지출 한도 외부에서 사용됩니다." +topUp = "충전" + +[portal.billing.prepaid.buy] +back = "뒤로" +calcTitle = "연간 결제 금액 계산" +cancel = "취소" +cancelPurchase = "구매 취소" +close = "닫기" +continueToPayment = "결제로 계속" +finalise = "완료" +invoiceErrorTitle = "청구서를 생성할 수 없습니다" +loadingTitle = "견적 불러오는 중" +payTitle = "연간 요금 결제" +step = "{{total}}단계 중 {{current}}단계" + +[portal.billing.prepaid.calc] +change = "변경" +deployRow = "배포" +done = "완료" +enterpriseHint = "이는 엔터프라이즈 규모입니다. 이 볼륨에서는 엔터프라이즈 요금이 어떤 셀프서비스 할인보다 유리합니다. 요금 고정, 조건, 견적을 몇 분 안에 제공합니다." +governanceRow = "거버넌스" +governanceValue = "{{label}} · 정책 {{count}}개" +handlesLabel = "내 Processor" +handlesValue = "월 {{volume}} 크레딧 처리" +pipelinesLabel = "파이프라인" +policiesMeta = "정책 {{count}}개" +poolCaption = "1년 동안 {{credits}} 크레딧의 단일 풀입니다. 사용량이 많은 달은 적은 달에서 빌려 씁니다." +priceLabel = "내 연간 요금 · 10개월 가격으로 12개월" +rateUnknown = "체크아웃 시 정확한 가격을 표시합니다." +savings = "{{amount}} 절약 · 2개월 무료." +sizingRow = "규모 산정" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "총 사용자" + +[portal.billing.prepaid.consent] +label = "선불 용량을 모두 사용하거나 12개월 후 만료되면, 취소하지 않는 한 처리가 표준 종량제 요금(내 지출 한도까지)으로 자동 계속되며, 청구 포털에서 언제든지 취소할 수 있음을 이해합니다." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling에서 관리합니다. 몇 분 안에 시작됩니다." +cloudValue = "Stirling Cloud · 관리형" +selfhost = "자체 호스팅" +selfhostDesc = "개인 인프라에서 실행됩니다. 동일 요금." +selfhostValue = "자체 호스팅 · 개인 인프라" + +[portal.billing.prepaid.offer] +cta = "제안 검토" +subtitle = "PDF 처리 1년치를 선불로 결제하고 2개월을 무료로 받으세요. 종량제 청구 전에, 지출 한도 외부에서 사용됩니다." +title = "10개월 가격으로 12개월 이용" + +[portal.billing.prepaid.pay] +accountName = "계정 소유자 이름" +accountNamePlaceholder = "예: Jane Smith" +companyName = "회사명(선택 사항)" +companyNamePlaceholder = "예: Acme Inc." +downloadInvoice = "청구서 다운로드" +payOnline = "온라인 결제" +poNumber = "PO 번호(선택 사항)" +poNumberPlaceholder = "예: PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "고급" +advancedDesc = "파이프라인이 대부분의 처리를 담당합니다." +none = "없음" +noneDesc = "아직 파이프라인을 실행하지 않습니다. 언제든지 켤 수 있습니다." +standard = "표준" +standardDesc = "몇 개의 파이프라인이 도착하는 PDFs를 다시 처리합니다." + +[portal.billing.prepaid.posture] +essentials = "필수" +essentialsDesc = "분류, 기본값, 공유 포함" +governed = "관리됨" +governedDesc = "보안 및 라우팅 추가" +regulated = "규제됨" +regulatedDesc = "Compliance, Retention, Ingestion 포함 모든 카테고리" + +[portal.billing.prepaid.review] +download = "견적 다운로드(PDF)" +downloading = "준비 중…" +share = "승인을 위해 공유" + +[portal.billing.prepaid.size] +compact = "컴팩트" +compactDesc = "25 MB 미만 파일, 데이터 요금 없음" +heavy = "무거움" +heavyDesc = "스캔 또는 이미지가 많음, 보통 50 MB+" +standard = "표준" +standardDesc = "대부분 작고, 일부 스캔은 25 MB 초과" + +[portal.billing.prepaid.state] +exhausted = "모두 사용됨" +healthy = "충분히 남음" +low = "부족해지는 중" [portal.billing.spendLimit] adjustLimit = "한도 조정" @@ -6494,6 +6713,8 @@ label = "초과 예상." [portal.billing.spendThisMonth] eyebrow = "이번 달 지출" +freeRemaining_one = "무료 PDF {{formatted}}개 남음" +freeRemaining_other = "무료 PDFs {{formatted}}개 남음" processed_one = "{{formattedCount}} PDF 처리됨." processed_other = "{{formattedCount}} PDFs 처리됨." processedWithRate_one = "{{formattedCount}} PDF 처리됨, 각 {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "월별 지출 한도에 도달했습니다" title = "Stripe portal을 열 수 없음" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "무료 PDFs {{allowance}}개 중 사용됨" capSuffix_other = "무료 PDFs {{allowance}}개 중 사용됨" eyebrow = "Processor 평가판" @@ -6534,6 +6756,376 @@ signature = "서명" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "새 연결" +createTitleFor = "{{name}} 연결" +delete = "삭제" +edit = "편집" +editTitle = "연결 편집" +unsupportedType = "이 연결 유형은 여기서 편집할 수 없습니다." + +[portal.connections.categories.advanced] +description = "아직 지원하지 않는 서비스용입니다." +label = "고급" + +[portal.connections.categories.audit] +description = "감사자가 이미 확인하는 곳에 문서에 발생한 일을 기록합니다." +label = "감사 및 규정 준수 로깅" + +[portal.connections.categories.notify] +description = "다른 시스템에 어떤 일이 발생했음을 알립니다." +label = "알림 및 워크플로" + +[portal.connections.categories.security] +description = "문서가 이동하기 전에 스캔하거나 레이블을 지정합니다." +label = "보안 및 분류" + +[portal.connections.categories.signing] +description = "서명 또는 공증을 위해 문서를 보냅니다." +label = "전자 서명 및 공증" + +[portal.connections.categories.storage] +description = "처리된 문서가 도착하는 위치 - 버킷, 드라이브, 티켓 또는 페이지입니다." +label = "파일 및 첨부" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling은 이를 암호화하여 저장하며 다시 표시하지 않습니다." +label = "액세스 토큰" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "이 연결을 사용하는 단계는 이 URL 아래의 경로만 호출할 수 있으며, 다른 호스트는 호출할 수 없습니다." +label = "서버 URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "서비스가 제공한 URL을 붙여넣으세요. 그 아래의 경로만 호출할 수 있습니다." +label = "Webhook URL" + +[portal.connections.fields] +name = "연결 이름" +namePlaceholder = "예: Production" + +[portal.connections.picker] +cancel = "취소" +createNew = "새 연결..." +placeholder = "연결 선택" +save = "연결 저장" + +[portal.connections.picker2] +back = "통합 변경" +heading = "무엇을 연결하시겠습니까?" +noResultsBody = "아직 지원하지 않습니다 - 알려주시면 추가를 검토하겠습니다." +noResultsTitle = "\"{{query}}\"와 일치하는 항목 없음" +searchPlaceholder = "통합 검색 - \"sign\", \"ocr\" 또는 제품 이름을 시도해 보세요" + +[portal.connections.types.api] +description = "정책에서 임의의 HTTP API를 호출합니다. URL, 인증 및 페이로드를 선택합니다." +label = "사용자 지정 API" + +[portal.connections.types.api.fields.authType] +label = "인증" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username 및 password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "사용자 지정 헤더의 토큰" + +[portal.connections.types.api.fields.authType.options.none] +label = "없음" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "토큰을 받기 위해 로그인" + +[portal.connections.types.api.fields.baseUrl] +helperText = "이 연결을 사용하는 단계는 이 URL 아래의 경로만 호출할 수 있으며, 다른 호스트는 호출할 수 없습니다." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "헤더 이름" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "로그인 경로" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API가 다운로드 링크로 응답하면, 이 호스트만 가져올 수 있습니다. 쉼표로 구분하세요. 비워 두면 기본 URL 자체 호스트만 허용됩니다." +label = "허용된 결과 호스트" + +[portal.connections.types.api.fields.token] +label = "토큰" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "이 헤더로 토큰 보내기" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "로그인 호출이 토큰을 반환하는 응답 헤더입니다. 예: X-Auth-Token." +label = "토큰 응답 헤더" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "운영 중인 서버에서 문서의 malware를 스캔합니다 - 네트워크를 벗어나지 않습니다." +label = "ClamAV (자체 호스팅)" + +[portal.connections.types.cloudmersive] +description = "정책에서 문서의 바이러스 및 콘텐츠를 스캔합니다." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "알려진 malware뿐 아니라 매크로, 실행 파일, 스크립트 또는 임베디드 객체가 포함된 문서를 차단합니다." +label = "Cloudmersive 고급 스캔" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "처리된 문서를 Confluence 페이지에 첨부합니다." +label = "Confluence" + +[portal.connections.types.consigno] +description = "전자 서명 및 공증을 위해 문서를 ConsignO Cloud (Notarius)로 보냅니다." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "/api/v1을 포함한 ConsignO Cloud 환경입니다." +label = "서버 URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "조직에서 테넌트를 사용하는 경우에만 필요합니다." +label = "테넌트" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "정책이 문서를 처리할 때 Discord에 메시지를 게시합니다." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "정책이 문서를 처리할 때마다 Elasticsearch에 감사 이벤트를 인덱싱합니다." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "새 문서를 위해 FTP 또는 FTPS 서버를 폴링합니다." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "연결 모드" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (권장)" + +[portal.connections.types.ftp.fields.security] +helperText = "일반 FTP는 자격 증명과 파일을 암호화하지 않고 전송합니다. 서버가 지원하면 FTPS를 선택하세요." +label = "암호화" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (명시적 / AUTH TLS)" +implicit = "FTPS (암시적)" +none = "없음(일반 FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "정책이 실행될 때 Google Chat 공간에 메시지를 게시합니다." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "처리된 문서를 Jira 이슈에 첨부합니다." +label = "Jira" + +[portal.connections.types.mailgun] +description = "정책이 실행될 때 문서 또는 알림을 Email로 보냅니다." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "처리된 문서를 Nextcloud에 파일로 저장합니다." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "앱 password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "운영 중인 Presidio 서버를 사용하여 문서 텍스트의 개인 데이터를 감지합니다." +label = "Presidio (자체 호스팅)" + +[portal.connections.types.purview] +description = "PDF에서 Microsoft Purview 민감도 레이블을 읽고 적용합니다." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "선택 사항입니다. 이름으로 레이블을 선택할 때만 필요하며, 없어도 레이블 지정은 작동합니다." +label = "애플리케이션(클라이언트) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "클라이언트 secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "모든 레이블에 SiteId로 찍혀 Purview 인식 도구가 어느 조직이 적용했는지 알 수 있습니다." +label = "디렉터리(테넌트) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "소스가 읽고 파이프라인 출력이 쓸 수 있는 S3 버킷입니다." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "액세스 key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS의 경우 비워 두세요. MinIO 또는 다른 S3 호환 저장소의 경우 이를 설정하세요." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "정책이 실행될 때 문서 또는 알림을 Email로 보냅니다." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "새 문서를 위해 SFTP (SSH) 서버를 폴링합니다." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "선택 사항입니다. 서버의 SHA-256 호스트 key 지문입니다(`ssh-keyscan host | ssh-keygen -lf -`에서). 설정하면 이 key를 제시하는 서버만 허용됩니다. 비워 두면 첫 연결 시 보인 key가 고정되고 이후 변경은 거부됩니다." +label = "호스트 key 지문" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "대신 private key로 인증하는 경우 비워 두세요." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "선택 사항입니다. password 대신 key로 인증하려면 OpenSSH private key를 붙여넣으세요." +label = "Private key" + +[portal.connections.types.slack] +description = "정책이 문서를 처리할 때 Slack에 메시지를 게시합니다." +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS 네트워크 공유(Windows 또는 Samba)를 감시합니다." +hostPlaceholder = "fileserver.example.com" +label = "SMB / 네트워크 공유" + +[portal.connections.types.smb.fields.domain] +helperText = "선택 사항입니다. 서버에서 사용하는 경우 Windows 도메인 또는 작업 그룹입니다." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "공유" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "정책이 문서를 처리할 때마다 Splunk에 감사 이벤트를 보냅니다." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Sumo Logic HTTP 수집기로 감사 이벤트를 보냅니다." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "정책이 실행될 때 Teams 채널에 메시지를 게시합니다." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "각 문서 또는 세부 정보만 선택한 URL로 게시합니다." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "정책에서 Zap 또는 Make 시나리오를 트리거합니다." +label = "Zapier & Make" + +[portal.docs] +browse = "문서 탐색" +viewSource = "GitHub에서 소스 보기" + [portal.docs.authentication] codeCaption = "모든 요청" eyebrow = "시작하기" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "agent가 단일 도구로 호출하는 번들형 명명된 기능입니다. 각 skill은 eval이 연결된 결정적 작업 체인입니다." title = "Agent skills" +[portal.docs.toc] +title = "이 페이지에서" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "유형" user = "사용자" [portal.documents.queue.empty] +connectSource = "소스 연결" +createPipeline = "파이프라인 만들기" description = "소스가 문서를 파이프라인에 공급하면 검토를 위해 여기에 표시됩니다." title = "대기열에 문서가 없습니다" @@ -6712,11 +7309,11 @@ editorAction = "편집자" empty = "이 필터와 일치하는 문서가 없습니다." rowActions = "행 작업" sensitiveLabel = "민감" -sensitiveTitle = "민감 — 접근 필요" [portal.documents.table.columns] action = "파이프라인 / 작업" document = "문서" +labels = "Labels" product = "제품" status = "상태" time = "시간" @@ -6732,6 +7329,7 @@ host = "호스트" lastSeen = "마지막 확인" region = "지역" status = "상태" +target = "Target" version = "버전" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "이 보기에서 예기치 않은 오류가 발생했습니다. retry = "다시 시도" title = "이 페이지에서 문제가 발생했습니다" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "뒤로" +body = "데스크톱 앱을 다운로드하거나 서버를 자체 호스팅하세요." +done = "완료" +guide = "{{name}} 가이드 열기" +openInBrowser = "대신 브라우저에서 열기" +sectionDesktop = "데스크톱 앱" +sectionSelfHosted = "자체 호스팅" +title = "Stirling PDF Editor 설치" + +[portal.home.download.docker] +detailBody = "서버에서 공식 이미지를 실행하세요." +note = ":8080에서 열립니다. 기본 로그인: admin / stirling." +tagline = "컨테이너에서 서버 실행" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "공식 Helm chart를 설치합니다." +note = "설정을 유지하려면 /configs용 PVC를 추가하세요." +tagline = "Helm으로 클러스터에 배포" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "사용 중인 배포판용 네이티브 빌드입니다." +guideBtn = "Linux 설치 옵션 보기" +note = "일반적인 배포판용 AppImage, .deb 및 .rpm 빌드를 사용할 수 있습니다." +tagline = "사용 중인 배포판용 네이티브 데스크톱 앱" +title = "Linux" + +[portal.home.download.mac] +altLabel = "또는 Homebrew 사용" +detailBody = "설치 프로그램을 실행하거나 Homebrew를 사용하세요." +downloadBtn = "macOS용 다운로드" +tagline = "Apple silicon 및 Intel용 네이티브 앱" +title = "macOS" + +[portal.home.download.manual] +detailBody = "베어 메탈 서버에서 .jar를 실행하세요." +downloadBtn = ".jar 다운로드" +note = "Java 21+가 필요합니다. :8080에서 열리며, 기본 로그인은 admin / stirling입니다." +runLabel = "그런 다음 실행하세요" +tagline = "베어 메탈 서버에서 .jar 실행" +title = "수동 서버 설정" + +[portal.home.download.windows] +altLabel = "또는 winget 사용" +detailBody = "설치 프로그램을 실행하거나 패키지 관리자를 사용하세요." +downloadBtn = "설치 프로그램 다운로드" +tagline = "Microsoft Windows용 네이티브 앱" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}}명 활성" -invite = "팀원 초대" +activeOfDeployed = "이번 달 활성 {{active}} / {{total}}" name = "Stirling PDF Editor" open = "브라우저에서 열기" updated = "{{time}} 업데이트됨" +[portal.home.editor.deploy] +finish = "배포 완료" +options = "배포 옵션 보기" +start = "Editor 배포" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "셀프 호스팅 · Docker" @@ -6823,49 +7529,6 @@ afternoon = "좋은 오후입니다" evening = "좋은 저녁입니다" morning = "좋은 아침입니다" -[portal.home.onboarding] -dismiss = "설정 닫기" -notStarted = "시작되지 않음" -progress = "{{total}}개 중 {{done}}개 완료" -title = "설정 완료" - -[portal.home.onboarding.enterprise] -body = "조직 전체 SSO + SCIM + RBAC, 약정 사용량 가격, 네트워크 격리 배포." -cta = "평가판 시작" -ctaQuote = "견적 받기" -lead = "직원 250명 이상 대상." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "데스크톱 앱을 배포하세요. 영구 무료입니다." -title = "PDF Editor 다운로드" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}}개 활성 · {{recommended}}개 권장" -chip = "{{active}}개 활성" -title = "정책 켜기" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}}개 연결됨 · 도착하는 모든 PDF 관리" -chip = "{{connected}}개 연결됨" -title = "소스 연결" - -[portal.home.quickActions] -subtitle = "오늘의 주요 작업" -title = "빠른 작업" - -[portal.home.quickActions.buildPipeline] -blurb = "유형화된 작업 라이브러리 기반 3단계 작성기" -title = "파이프라인 구축" - -[portal.home.quickActions.connectSource] -blurb = "S3, 에이전트, webhooks, 감시 폴더" -title = "소스 연결" - -[portal.home.quickActions.issueApiKey] -blurb = "속도 제한 및 IP 허용 목록이 있는 범위 지정 키" -title = "API 키 발급" - [portal.infrastructure] manageEditorDeployment = "Editor 배포 관리" sectionsAriaLabel = "인프라 섹션" @@ -6878,13 +7541,9 @@ heading = "API 키" subheading = "키별 속도 제한, 권한, IP 허용 목록이 있는 범위 지정 자격 증명입니다." [portal.infrastructure.apiKeys.card] -allowedIps = "허용된 IP" -anyIp = "모든 IP(허용 목록 없음)" created = "생성됨" lastUsed = "마지막 사용" -permissions = "권한" -rateLimit = "속도 제한" -rateLimitValue = "{{value}} req/min" +revoke = "키 폐기" usageMonth = "이번 달 사용량" usageToday = "오늘 사용량" @@ -6892,6 +7551,15 @@ usageToday = "오늘 사용량" description = "Stirling API 호출을 시작하려면 범위 지정 키를 생성하세요." title = "아직 API 키가 없습니다" +[portal.infrastructure.apiKeys.error] +load = "API 키를 불러올 수 없습니다. 다시 시도하세요." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\"을(를) 폐기하시겠습니까? 이 키를 계속 사용하는 모든 통합은 즉시 401 오류를 받기 시작합니다. 이 작업은 되돌릴 수 없습니다." +cancel = "취소" +confirm = "키 폐기" +title = "API 키 폐기" + [portal.infrastructure.audit] filterAriaLabel = "카테고리별 감사 이벤트 필터링" heading = "감사 로그" @@ -6935,6 +7603,7 @@ all = "전체" auth = "인증" config = "구성" elevation = "권한 상승" +policy = "정책" processing = "처리" security = "보안" @@ -6944,19 +7613,30 @@ title = "접근 권한이 없습니다" [portal.infrastructure.audit.metrics] config = "구성" -elevation = "권한 상승" +policy = "정책" processing = "처리" totalEvents = "총 이벤트 · 24시간" +[portal.infrastructure.auditCatLabel] +auth = "인증" +config = "구성" +elevation = "권한 상승" +policy = "정책" +processing = "처리" +security = "보안" + +[portal.infrastructure.auditStatusLabel] +danger = "오류" +info = "정보" +success = "성공" +warning = "경고" + [portal.infrastructure.createKey] cancel = "취소" createKey = "키 생성" done = "완료" -ipAllowlistHelper = "쉼표로 구분된 CIDR 범위입니다. 모든 IP를 허용하려면 비워 두세요." -ipAllowlistLabel = "IP 허용 목록" keyNameLabel = "키 이름" keyNamePlaceholder = "예: Production · ingest" -permissionsLabel = "권한" secretKeyCaption = "비밀 키" secretWarning = "이를 secrets manager에 저장하세요. Stirling은 항상 해시만 저장하므로 나중에 복구할 방법이 없습니다." subtitle = "키 범위는 필요한 최소한으로 지정하세요. 언제든지 교체하거나 취소할 수 있습니다." @@ -6964,207 +7644,9 @@ subtitleCreated = "지금 이 비밀을 복사하세요 — 다시 표시되지 title = "API 키 생성" titleCreated = "키 생성됨" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "배포자" -environment = "환경" -product = "제품" -status = "상태" -version = "버전" -when = "시점" - -[portal.infrastructure.deployments.recent] -heading = "최근 배포" -subheading = "제품 및 환경 전반의 최신 롤아웃입니다." - -[portal.infrastructure.deployments.regionColumns] -instances = "인스턴스" -latency = "지연 시간" -load = "부하" -p99 = "P99" -region = "지역" -status = "상태" -throughput = "처리량" -uptime = "가동 시간" -version = "버전" - -[portal.infrastructure.deployments.regions] -heading = "지역" -subheading = "배포된 모든 Stirling 지역의 실시간 상태 — 지연 시간, 부하, 롤아웃 버전." - -[portal.infrastructure.deployments.regions.empty] -description = "워크스페이스가 프로비저닝되면 배포된 지역이 여기에 표시됩니다." -title = "배포된 지역 없음" - -[portal.infrastructure.models] -heading = "모델" -msValue = "{{value}} ms" -subheading = "워크스페이스 전반의 문서 처리를 구동하는 모델 카탈로그 및 라우팅입니다." - -[portal.infrastructure.models.byom] -description = "온프레미스 또는 셀프 호스팅 모델을 등록하고 데이터 레지던시 기반 처리를 위해 지역에 고정하세요." -title = "자체 모델 가져오기" - -[portal.infrastructure.models.catalogue] -heading = "카탈로그" -sub = "워크스페이스에서 사용할 수 있는 관리형 모델이며, 실시간 지연 시간과 비용을 포함합니다." -subEnterprise = "관리형, 자체 제공, 온프레미스 모델 — 지역별 고정 사용 가능." - -[portal.infrastructure.models.catalogue.empty] -description = "워크스페이스 카탈로그의 모델이 여기에 표시됩니다." -title = "사용 가능한 모델 없음" - -[portal.infrastructure.models.columns] -cost = "비용" -latency = "지연 시간" -load = "부하" -model = "모델" -status = "상태" -type = "유형" -version = "버전" - -[portal.infrastructure.models.metrics] -activeModels = "활성 모델" -avgLatency = "평균 지연 시간" -included = "포함됨" -monthlySpend = "월별 모델 지출" - -[portal.infrastructure.models.routing] -empty = "구성된 라우팅 규칙이 없습니다." -heading = "라우팅 규칙" -sub = "각 작업을 처리할 모델입니다. 더 구체적인 규칙과 일치하지 않을 때 기본값이 적용됩니다." -subLocked = "작업을 특정 모델로 라우팅 — 유료 플랜에서 사용 가능." - -[portal.infrastructure.models.routing.lockedBanner] -description = "각 작업과 문서 유형을 처리할 모델을 제어하려면 Pro로 업그레이드하세요." -title = "모델 라우팅은 유료 기능입니다" - -[portal.infrastructure.models.routingColumns] -default = "기본값" -docType = "문서 유형" -modelForAria = "{{operation}}용 모델" -operation = "작업" -routedTo = "라우팅 대상" - -[portal.infrastructure.security.access.byok] -description = "자체 KMS의 키를 제공하세요. Stirling은 이 키로 암호화하지만 여전히 읽을 수 있습니다." -label = "자체 키 가져오기(BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "키는 KMS를 벗어나지 않습니다. Stirling은 암호문만 보유합니다." -label = "자체 키 보유(HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling이 암호화 키를 관리합니다. 가장 간단하며 사용자가 키 작업을 할 필요가 없습니다." -label = "Stirling 보유 키" - -[portal.infrastructure.security.accessPolicy] -heading = "문서 접근 정책" -subheading = "저장된 처리 완료 문서를 복호화할 수 있는 사용자를 제어합니다." - -[portal.infrastructure.security.attestations] -heading = "컴플라이언스 증명" -noReport = "사용 가능한 보고서 없음" -subheading = "증명된 제어에 대한 보고서가 제공되는 프레임워크별 감사 상태입니다." -viewReport = "보고서 보기 →" - -[portal.infrastructure.security.compliance] -heading = "컴플라이언스" -subheading = "Stirling 플랫폼을 포괄하는 증명 및 인증입니다." - -[portal.infrastructure.security.empty] -description = "워크스페이스의 보안 구성이 여기에 표시됩니다." -title = "보안 상태를 사용할 수 없음" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK를 사용하면 암호화 키가 KMS를 벗어나지 않습니다. Stirling은 언제든지 취소할 수 있는 암호문만 저장하고 처리합니다." -title = "Stirling은 문서를 복호화할 수 없습니다" - -[portal.infrastructure.security.ipAllowlist] -empty = "구성된 IP 범위가 없습니다 — 모든 IP가 허용됩니다." -heading = "IP 허용 목록" -sub = "API 접근이 이러한 CIDR 범위로 제한됩니다." -subLocked = "API 접근을 알려진 IP 범위로 제한 — 유료 플랜에서 사용 가능." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "API 접근을 특정 네트워크로 제한하려면 Pro로 업그레이드하세요." -title = "IP 허용 목록은 유료 기능입니다" - -[portal.infrastructure.security.ipColumns] -added = "추가됨" -addedBy = "추가한 사람" -cidr = "CIDR" -label = "레이블" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "암호화 키 관리" -keyId = "키 식별자" -lastRotated = "마지막 교체" -rotateKey = "키 교체" -rotationPolicy = "교체 정책" -subheading = "저장된 문서를 암호화하는 키의 보관 관리 — 복호화 가능 주체 및 키 교체 방식." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key(BYOK) 및 hold-your-own-key(HYOK) 보관 관리는 Enterprise에서 사용할 수 있습니다. 자체 KMS의 키를 제공하려면 업그레이드하세요." -title = "현재 플랜에서는 키가 Stirling에서 관리됩니다" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "아시아 태평양" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR 데이터 경계" -label = "유럽 연합" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "미국" - -[portal.infrastructure.security.residencyHeader] -heading = "데이터 레지던시" -subheading = "문서가 저장되고 처리되는 위치입니다." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} 사용됨" - -[portal.infrastructure.storage.empty] -description = "연결된 스토리지와 사용량이 여기에 표시됩니다." -title = "구성된 스토리지 없음" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "활성" -activeRange = "0–{{value}}d" -archived = "보관됨" -coldStorage = "cold storage" -deleted = "삭제됨" -never = "없음" -purged = "영구 삭제됨" - -[portal.infrastructure.storage.providers] -connect = "연결" -connected = "연결됨" -heading = "연결된 제공업체" -subheading = "처리된 아티팩트가 기록되는 위치입니다." - -[portal.infrastructure.storage.retention] -heading = "보존" -subheading = "수명 주기 삭제 전 아티팩트가 보관되는 기간입니다." -windowLabel = "기본 보존 기간" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}}일" -days_other = "{{count}}일" -never = "삭제하지 않음" - -[portal.infrastructure.storage.totalUsage] -heading = "총 사용량" -progressLabel = "사용된 스토리지" -subheading = "연결된 모든 제공업체에서 소비된 스토리지입니다." +revoked = "폐기됨" [portal.infrastructure.tabs] apiKeys = "API 키" @@ -7174,6 +7656,49 @@ models = "모델" security = "보안" storage = "스토리지" +[portal.integrations] +availableHeading = "사용 가능" +comingSoonHeading = "곧 제공 예정" +connect = "연결" +connectionCount_one = "{{count}}개 연결" +connectionCount_other = "{{count}}개 연결" +customApi = "사용자 지정 API" +searchPlaceholder = "통합 검색" +subtitle = "Stirling이 통신할 수 있는 모든 항목입니다. 자격 증명은 여기에서 한 번 생성한 뒤 소스, 정책 및 파이프라인에서 참조됩니다." +title = "통합" +typedName = "{{tool}} 이름" + +[portal.integrations.filters] +all = "모두" +audit = "감사" +connected = "연결됨" +notify = "알림" +security = "보안" +signing = "서명" +storage = "스토리지" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "연결됨" + +[portal.integrations.table] +integration = "통합" +worksWith = "함께 작동" + +[portal.integrations.worksWith] +pipelines = "파이프라인" +policies = "정책" +sources = "소스" + +[portal.legal] +draft = "{{label}} · 초안" +loadError = "이 문서를 불러올 수 없습니다. 다시 시도하세요." +loading = "불러오는 중…" +title = "법적 문서" + [portal.nav] agent-builder = "Agent Builder" components = "구성 요소" @@ -7182,33 +7707,17 @@ documents = "문서" editor = "Editor" home = "홈" infrastructure = "인프라" +integrations = "통합" pipelines = "파이프라인" policies = "정책" procurement = "구매" -settings = "설정" sources = "소스" usage = "사용량 및 청구" users = "사용자" -[portal.notifications] -markAllRead = "모두 읽음으로 표시" -title = "알림" -viewAll = "모두 보기" - -[portal.notifications.ariaLabel] -none = "알림, 읽지 않은 항목 없음" -unread_one = "알림, 읽지 않은 항목 {{count}}개" -unread_other = "알림, 읽지 않은 항목 {{count}}개" - -[portal.notifications.count] -allRead = "모두 읽음" -loading = "로드 중" -new_one = "새 항목 {{count}}개" -new_other = "새 항목 {{count}}개" - -[portal.notifications.empty] -description = "새 알림이 없습니다." -title = "모두 확인했습니다" +[portal.nav.section] +platform = "PDF 플랫폼" +processor = "PDF 프로세서" [portal.pipelines] subtitle = "백엔드의 모든 자동 문서 파이프라인: 소스 집합에 대한 순서 지정된 작업 체인이 트리거에 의해 실행됩니다. 행을 클릭하면 단계와 소스를 볼 수 있습니다." @@ -7218,54 +7727,87 @@ title = "파이프라인" newPipeline = "새 파이프라인" [portal.pipelines.builder] -addStep = "도구 추가" +activate = "Activate" back = "파이프라인으로 돌아가기" +cannotFollow = "{{produced}}을(를) 사용할 수 없습니다" +chooseAccount = "계정 선택" +chooseDestination = "대상 선택" +chooseOperation = "이 단계에서 수행할 작업 선택" +chooseSource = "소스 선택" discard = "변경 사항 버리기" -enabled = "활성화됨" +inputs = "입력" +inputSource = "입력 소스" +inputTrigger = "트리거" keepEditing = "계속 편집" -needsUpload = "업로드된 파일 필요" +moreActions = "More actions" +needsConfiguring = "설정 필요" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "검색과 일치하는 도구가 없습니다." -pipelineSettings = "파이프라인 설정" +pause = "Pause" +rename = "Rename pipeline" searchTools = "도구 검색" -selectToolBody = "파이프라인을 구축하려면 도구를 추가하세요." -selectToolTitle = "아직 도구가 없습니다" -toolSettings = "도구 설정" +sendToSystem = "다른 시스템으로 보내기" +stepsIncompatible = "이 단계들은 이전 단계가 생성한 항목에서 실행할 수 없습니다: {{tools}}." +stepsNeedSetup = "저장하기 전에 아직 설정이 필요한 단계입니다: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "인식되지 않는 작업이며, 그대로 유지되었습니다." unsavedBody = "저장되지 않은 변경 사항이 있습니다. 나가기 전에 저장하시겠습니까, 아니면 버리시겠습니까?" unsavedTitle = "저장되지 않은 변경 사항" -uploadUnsupported = "업로드된 파일은 아직 파이프라인에서 지원되지 않으므로 다음 단계는 저장할 수 없습니다: {{tools}}." usesDefaults = "기본 설정으로 실행" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "이전 단계의 모든 파일을 결합합니다" +fan-out = "이전 단계의 파일마다 한 번씩 실행됩니다" +format-mismatch = "{{accepts}}이(가) 필요하지만, 이전 단계는 {{produced}}을(를) 생성합니다" +output-uncertain = "실행되지 않을 수 있음: 이전 단계의 출력은 설정 방식에 따라 달라집니다" +source-mismatch = "{{accepts}}이(가) 필요하지만, 이 파이프라인의 입력은 {{produced}}입니다" +undeclared-operation = "이 단계가 무엇을 허용하는지 확인할 수 없습니다" [portal.pipelines.composer] addTool = "도구 추가" -cancel = "취소" -chainEmpty = "파이프라인 구축을 시작하려면 도구를 추가하세요." create = "파이프라인 생성" -directory = "출력 폴더" -directoryHelp = "서버의 절대 경로입니다. 구성된 허용 폴더 내에 있어야 합니다." +createPaused = "Create paused" editingUnsupported = "편집을 위해 이러한 도구 매개변수를 표시하는 기능은 아직 지원되지 않습니다." -moveDown = "아래로 이동" -moveUp = "위로 이동" +editSource = "Edit source" name = "이름" namePlaceholder = "예: Redaction sweep" -noSources = "아직 연결된 소스가 없습니다. 파이프라인은 직접 제공된 파일에서도 계속 실행할 수 있습니다." noToolSettings = "이 도구에는 구성 가능한 설정이 없습니다." -operations_one = "작업({{count}})" -operations_other = "작업({{count}})" output = "출력" -removeStep = "작업 제거" save = "변경 사항 저장" scheduleEvery = "실행 주기" -sources = "소스" -sourcesLoading = "소스 로드 중..." trigger = "트리거" triggerManual = "수동만" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "일" hours = "시간" minutes = "분" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다." cancel = "취소" @@ -7273,28 +7815,62 @@ confirm = "삭제" title = "파이프라인을 삭제하시겠습니까?" [portal.pipelines.detail] +clearHistory = "기록 지우기" delete = "파이프라인 삭제" run = "지금 실행" [portal.pipelines.empty] action = "파이프라인 생성" +connectSource = "소스 연결" description = "첫 번째 파이프라인을 생성하세요. 실행할 소스를 선택하고, 작업을 연결하며, 출력 위치를 선택하세요." title = "아직 파이프라인이 없습니다" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "활성" paused = "일시 중지됨" total = "파이프라인" -[portal.pipelines.output] -folder = "폴더에 쓰기" -inline = "파일 반환" - [portal.pipelines.run] +allProcessed_one = "실행할 항목이 없습니다: 소스의 문서 {{count}}개는 이미 처리되었습니다." +allProcessed_other = "실행할 항목이 없습니다: 소스의 문서 {{count}}개는 모두 이미 처리되었습니다." completed_one = "실행이 완료되었습니다." completed_other = "{{count}}개 실행이 모두 완료되었습니다." empty = "실행할 항목이 없습니다. 소스에 처리할 문서가 없습니다." failed = "실행 실패: {{error}}" +inFlight = "새로 실행할 항목이 없습니다: 이전 실행의 문서가 아직 처리 중입니다." +parked_one = "실행할 항목이 없습니다: 문서 {{count}}개가 이전에 실패하여 보류되어 있습니다. 원인을 수정한 다음 기록을 지워 다시 시도하세요." +parked_other = "실행할 항목이 없습니다: 문서 {{count}}개가 이전에 실패하여 보류되어 있습니다. 원인을 수정한 다음 기록을 지워 다시 시도하세요." running = "실행이 시작되었으며 아직 진행 중입니다." timeout = "실행이 예상보다 오래 걸리고 있습니다. 백그라운드에서 계속 완료될 수 있습니다." @@ -7307,6 +7883,7 @@ name = "파이프라인" sources = "소스" status = "상태" steps = "단계" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "폴더 감시" @@ -7314,12 +7891,18 @@ manual = "수동" schedule = "예약됨" [portal.policies] +defaultName = "{{category}} 정책" subtitle = "모든 문서에 도구 파이프라인을 적용하는 상시 자동화입니다. 각 정책은 업로드 또는 내보내기 시 실행되고, 도구 체인을 실행한 뒤 적용된 버전을 원본과 함께 저장합니다." title = "정책" [portal.policies.card] comingSoon = "Enterprise로 업그레이드" notSetUp = "설정되지 않음" +requiresAiEngine = "AI 엔진 필요" + +[portal.policies.categories.classification] +desc = "업로드 시 각 문서의 유형을 식별하고 정리 및 검색을 위해 메타데이터에 태그를 지정합니다." +label = "분류" [portal.policies.categories.compliance] desc = "모든 문서에 HIPAA, GDPR, SOC 2 또는 FedRAMP 요구 사항을 적용합니다." @@ -7344,6 +7927,13 @@ label = "보안" [portal.policies.config] scopeAll = "모든 문서" +[portal.policies.config.classification] +summary = "업로드된 모든 문서를 팀의 레이블에 따라 분류하고 메타데이터에 태그를 지정합니다." + +[portal.policies.config.classification.rules] +0 = "분류" +1 = "메타데이터 태그 지정" + [portal.policies.config.compliance] summary = "문서가 시스템을 떠나기 전에 규제 프레임워크에 맞는지 검증합니다." @@ -7371,6 +7961,29 @@ minConfidence = "최소 신뢰도" 2 = "이름 지정" 3 = "정규화" +[portal.policies.config.purview] +label = "Microsoft Purview 민감도 레이블 적용" + +[portal.policies.config.purview.fields] +connection = "Purview 테넌트" +labelId = "레이블 ID" +labelIdHelp = "Purview 포털의 레이블 GUID입니다. 레이블을 적용하면 해당 메타데이터가 PDF에 기록됩니다. 암호화는 할 수 없으며, 암호화에는 Microsoft 클라이언트가 필요합니다." +labelName = "레이블 이름(선택 사항)" +labelNameHelp = "사람과 도구가 레이블을 인식할 수 있도록 ID와 함께 기록됩니다." + +[portal.policies.config.purview.fields.method] +help = "Purview는 정책이 적용한 레이블과 사람이 선택한 레이블을 구분합니다." +label = "레이블 기록 방식" +privileged = "사람이 선택한 것으로" +standard = "자동으로 적용된 것으로" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview 레이블 읽기" + +[portal.policies.config.purviewRead.fields] +connection = "Purview 테넌트" +connectionHelp = "문서가 이미 가지고 있는 레이블을 읽어 이후 단계에서 이를 기반으로 작업할 수 있게 합니다." + [portal.policies.config.retention] summary = "문서를 보관할 기간, 보관 처리 시점, 삭제 시점을 적용합니다." @@ -7438,6 +8051,7 @@ title = "아직 활동 없음" [portal.policies.endpoints] addWatermark = "워터마크" autoRedact = "PII 마스킹" +classifyAndLabel = "분류" compressPdf = "압축" flatten = "병합" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "정책이 저장되었으며 연결이 복원되면 표시됩니 retry = "다시 시도" title = "Backend를 사용할 수 없음" +[portal.policies.operations] +change = "이 단계에서 수행할 작업 변경" +noResults = "일치하는 단계가 없습니다. 제품 이름 또는 \"scan\", \"notify\", \"attach\"를 시도해 보세요." +searchPlaceholder = "단계 검색 - \"scan\", \"attach\" 또는 제품 이름을 시도해 보세요" + +[portal.policies.operations.bodyMode] +binary = "원시 파일 바이트" +json = "JSON 페이로드 내부" +multipart = "파일 업로드로(multipart)" + +[portal.policies.operations.clamavScan] +description = "사용자 자체 ClamAV 서버가 문서를 검사하며, 네트워크 밖으로 아무것도 나가지 않습니다." +label = "바이러스 검사(셀프 호스팅)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "매크로, 실행 파일, 스크립트 또는 임베디드 객체가 포함된 문서를 거부합니다." +label = "활성 콘텐츠 차단" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive가 문서를 검사하고 깨끗하지 않으면 실행이 중지됩니다." +label = "바이러스 검사" + +[portal.policies.operations.confluenceAttach] +description = "처리된 문서를 페이지에 파일로 첨부합니다." +label = "Confluence 페이지에 첨부" + +[portal.policies.operations.consignoSubmit] +description = "서명을 위해 문서를 ConsignO Cloud에 제출합니다." +label = "서명 요청 보내기(ConsignO)" +note = "문서를 제출합니다. 단계가 결과를 이후 단계에 전달할 수 없기 때문에 서명된 사본 검색은 아직 지원되지 않습니다." + +[portal.policies.operations.customApiCall] +description = "경로, 메서드, 본문 및 응답 처리를 직접 작성합니다." +label = "사용자 지정 API 호출" + +[portal.policies.operations.discordNotify] +description = "정책이 문서를 처리했음을 채널에 알립니다." +label = "Discord에 메시지 게시" + +[portal.policies.operations.elasticIndex] +description = "정책이 수행한 작업을 검색 가능한 문서로 기록합니다." +label = "Elasticsearch에 감사 이벤트 인덱싱" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart는 업로드 API에 적합하고, JSON은 페이로드 안에 파일을 원하는 API에 적합하며, binary는 원시 바이트를 보냅니다." +label = "문서 전송 방식" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "그대로 전송되는 JSON 본문입니다. {{document.base64}}에는 파일이 들어가며, {{document.*}} 및 {{run.*}}은 문서별로 채워집니다." +label = "JSON 본문" + +[portal.policies.operations.fields.connection] +label = "계정" + +[portal.policies.operations.fields.domain] +label = "Mailgun 도메인" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API가 문서를 받을 것으로 예상하는 양식 필드입니다." +label = "파일 필드 이름" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "보낸 사람" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "헤더 이름/값 쌍의 JSON 객체입니다. 연결의 자격 증명은 자동으로 추가됩니다." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "이슈 키" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "메시지 본문으로 전송됩니다. {{document.*}} 및 {{run.*}}은 문서별로 채워집니다." +label = "메시지" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "페이지 ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "연결의 기본 URL에 추가됩니다. 다른 호스트에는 절대 도달할 수 없습니다." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "드라이브에서 기록할 위치입니다. {{document.filename}}은 문서별로 채워집니다." +label = "대상 경로" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "보고서는 문서를 그대로 둡니다. 교체는 반환된 내용으로 문서를 바꿉니다." +label = "응답 처리 방법" + +[portal.policies.operations.fields.signerEmail] +label = "서명자 이메일" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "제목" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio가 이 텍스트를 분석합니다. {{document.title}}을 사용하거나 확인할 텍스트를 붙여넣으세요." +label = "확인할 텍스트" + +[portal.policies.operations.fields.to] +label = "받는 사람" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud 사용자 이름" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "정책이 문서를 처리했음을 스페이스에 알립니다." +label = "Google Chat에 메시지 게시" + +[portal.policies.operations.jiraAttach] +description = "처리된 문서를 이슈에 파일로 첨부합니다." +label = "Jira 이슈에 첨부" + +[portal.policies.operations.mailgunEmail] +description = "처리된 문서를 첨부 파일로 보냅니다." +label = "문서 이메일 보내기(Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "처리된 문서를 폴더에 기록합니다." +label = "Nextcloud에 업로드" + +[portal.policies.operations.presidioAnalyze] +description = "사용자 자체 Presidio 서버가 발견한 개인 데이터를 보고합니다." +label = "개인 데이터 감지" +note = "Presidio는 파일이 아니라 텍스트를 읽습니다. 확인할 텍스트를 제공하세요 — 문서 자체는 업로드되지 않습니다." + +[portal.policies.operations.responseMode] +replace = "응답으로 문서 교체" +report = "문서는 유지하고 응답 기록" + +[portal.policies.operations.sendgridEmail] +description = "처리된 문서를 첨부 파일로 보냅니다." +label = "문서 이메일 보내기(SendGrid)" + +[portal.policies.operations.slackNotify] +description = "정책이 문서를 처리했음을 채널에 알립니다." +label = "Slack에 메시지 게시" + +[portal.policies.operations.splunkEvent] +description = "감사자가 이미 확인하는 위치에 정책이 수행한 작업을 기록합니다." +label = "Splunk에 감사 이벤트 보내기" + +[portal.policies.operations.sumologicEvent] +description = "정책이 수행한 작업을 HTTP 수집기에 기록합니다." +label = "Sumo Logic에 감사 이벤트 보내기" + +[portal.policies.operations.teamsNotify] +description = "정책이 문서를 처리했음을 채널에 알립니다." +label = "Teams에 메시지 게시" + +[portal.policies.operations.webhookPost] +description = "선택한 URL로 문서 자체를 보냅니다." +label = "webhook에 문서 게시" + +[portal.policies.operations.zapierNotify] +description = "실행 세부 정보를 자동화로 전달합니다." +label = "Zap 또는 Make 시나리오 트리거" + [portal.policies.stats] activeFor = "활성" dataProcessed = "처리된 데이터" @@ -7473,6 +8282,13 @@ label = "적용된 문서" description = "구성되었지만 실행되지 않음" label = "일시 중지됨" +[portal.policies.table] +appliesTo = "적용 대상" +docs = "적용된 문서" +enforces = "시행" +policy = "정책" +status = "상태" + [portal.policies.wizard.actions] back = "뒤로" cancel = "취소" @@ -7480,6 +8296,10 @@ continue = "계속" enablePolicy = "정책 활성화" saveChanges = "변경 사항 저장" +[portal.policies.wizard.capability.classify] +desc = "팀의 레이블에서 문서 유형을 식별하고 태그를 지정하여 범주별로 정리하고 검색할 수 있게 합니다." +label = "문서 분류" + [portal.policies.wizard.capability.compress] desc = "문서를 더 작은 파일 크기로 압축합니다." label = "파일 크기 줄이기" @@ -7500,10 +8320,18 @@ label = "민감한 정보 마스킹" desc = "숨겨진 JavaScript를 제거하여 문서를 열 때 아무것도 자동으로 실행되지 않게 합니다." label = "활성 콘텐츠 제거" +[portal.policies.wizard.capability.timestampPdf] +desc = "독립적인 타임스탬프 기관을 사용해 특정 시점에 문서가 이 정확한 형태로 존재했음을 증명합니다. 해시만 전송되며, 문서는 서버를 떠나지 않습니다." +label = "신뢰할 수 있는 타임스탬프 추가" + [portal.policies.wizard.capability.watermark] desc = "모든 페이지에 표시되는 표시(예: “기밀”)를 찍습니다." label = "워터마크 적용" +[portal.policies.wizard.classification] +description = "업로드된 모든 문서는 기본 제공 레이블에 따라 분류되고 맞는 유형으로 태그가 지정됩니다. 레이블 세트는 전체 팀에서 공유됩니다." +labelsHeading = "분류 레이블" + [portal.policies.wizard.errors] noTools = "먼저 워크플로에서 하나 이상의 도구를 활성화하세요." saveFailed = "정책을 저장할 수 없습니다. 다시 시도하세요." @@ -7548,73 +8376,118 @@ setUp = "{{category}} 정책 설정" [portal.policies.wizard.workflow] description = "이 정책이 처리하는 모든 문서에 수행할 작업을 선택하세요." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} 활성" -noRule = "아직 적용된 규칙 없음" -subtitle = "어떤 파이프라인이 처리하든 모든 문서가 거치는 상시 자동화입니다." -title = "PDF에서 실행되는 항목" - [portal.policySummary.action] -comingSoon = "Enterprise로 업그레이드" -configure = "구성" setUp = "설정" -[portal.policySummary.column] -activeRule = "활성 규칙" -policy = "정책" -status = "상태" +[portal.processorFlow] +footnote = "집계는 지난 24시간 기준입니다. 흐름 속도는 예시용입니다." +liveBadge = "라이브" +stats = "{{connected}}개 연결됨 · PDF {{processed}}개 처리됨" +title = "PDF 프로세서" -[portal.policySummary.empty] -description = "정책이 구성되면 카테고리가 여기에 표시됩니다." -title = "아직 정책 없음" +[portal.processorFlow.lens] +ariaLabel = "보기 모드" +flow = "흐름" +sankey = "Sankey" -[portal.policySummary.state] -active = "활성" -off = "꺼짐" +[portal.processorFlow.outcomes] +count = "{{n}} · 24시간" +failed = "실패" +heading = "결과" +success = "전달됨" + +[portal.processorFlow.policies] +activeCount = "{{n}}개 활성" +count = "{{n}} · 24시간" +heading = "정책" +setUp = "설정" soon = "곧 제공" -[portal.processingStatus] -managePlan = "플랜 관리" -volumeSuffix = "처리된 PDFs · 최근 30일" +[portal.processorFlow.sankey] +waist = "정책 · {{n}}개 활성" + +[portal.processorFlow.sankey.empty] +description = "문서가 프로세서를 통해 흐르는 모습을 보려면 소스를 연결하고 정책을 켜세요." +title = "아직 흐름 없음" + +[portal.processorFlow.sources] +comingSoonTag = "연결" +editor = "Stirling PDF Editor" +heading = "소스" +perDay = "{{n}} / 24시간" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email 수신" [portal.procurement] -reset = "조달 재설정(데모)" subtitle = "팀 평가, 계약, 온보딩을 완료하세요. 모든 문서를 한곳에서 관리합니다." title = "조달" -[portal.procurement.action] -download = "다운로드" -pay = "지금 결제" -request = "요청" -sign = "검토 및 서명" -upload = "업로드" - [portal.procurement.agreement] agreeCta = "동의하고 구독" +confidential = "기밀" confirm = "Stirling Enterprise Agreement를 읽었으며 이에 동의합니다." -eyebrow = "계약" -intro = "하나의 통합 계약으로 거래를 포괄합니다: Master Service Agreement, Order Form, EULA, Data Processing Agreement. 검토한 후 견적을 확정 구독으로 수락하려면 동의하세요." -title = "엔터프라이즈 계약 검토" +docName = "Stirling Enterprise 계약서" +docSub = "MSA, 주문서, EULA 및 DPA가 하나의 서명으로 결합되었습니다." +download = "다운로드" +downloadDraftError = "계약서 PDF를 생성할 수 없습니다 - 문서 렌더러를 사용할 수 없습니다. 잠시 후 다시 시도하거나 지원팀에 문의하세요." +downloadError = "서명된 계약서를 다운로드할 수 없습니다. 다시 시도하세요." +legalName = "법인명" +legalNamePlaceholder = "서명할 법인" +loadError = "계약서를 불러올 수 없습니다. 다시 시도하세요." +loading = "계약서를 불러오는 중..." +ref = "참조 {{ref}} · {{version}}" +requestChanges = "변경 요청" +scrollHint = "검토하려면 스크롤" +signatory = "서명자 이름" +signatoryPlaceholder = "전체 이름" +signatoryTitle = "서명자 직함" +signatoryTitlePlaceholder = "예: General Counsel" +signError = "서명을 기록할 수 없습니다. 다시 시도하세요." [portal.procurement.builder] addons = "추가 기능" +addressLine1 = "주소 줄 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "주소 줄 2" +addressLine2Placeholder = "스위트, 층, 건물" back = "뒤로" businessName = "회사명" businessNamePlaceholder = "귀사" +city = "시/군/구" +cityPlaceholder = "San Francisco" +completeRequired = "견적을 생성하기 전에 필수 필드(* 표시)를 유효한 이메일과 함께 완료하세요." +contactEmail = "연락처 이메일" +contactEmailPlaceholder = "jane@acme.com" +contactName = "연락처 이름" +contactNamePlaceholder = "Jane Doe" continue = "계속" -country = "국가" -countryEuro = "유로존(EUR €)" -countryUK = "영국(GBP £)" -countryUS = "미국(USD $)" +done = "완료" eula = "Stirling Enterprise EULA를 읽었으며 이에 동의합니다. 이 견적에서 생성되는 계약에 적용됩니다." generate = "견적 생성" -included = "포함됨" indemnification = "IP 면책" indemnificationSub = "EULA에 따라 적격 IP 클레임을 방어합니다" -offlineLicense = "오프라인 / 에어갭 라이선스" -offlineLicenseSub = "에어갭 self-hosted 인스턴스용 다운로드 가능한 라이선스 파일" +paperEyebrow = "Enterprise 견적" +paperFor = "대상" +pdfSize = "PDF 크기" +poNumber = "PO 번호" +poNumberPlaceholder = "선택 사항" +postalCode = "우편번호" +postalCodePlaceholder = "94105" +posture = "거버넌스" +posture_count = "~{{count}}개 정책" +posture_essentials = "Essentials" +posture_essentialsSub = "분류 + 공유 — 기본값" +posture_governed = "Governed" +posture_governedSub = "보안 및 라우팅 추가" +posture_regulated = "Regulated" +posture_regulatedSub = "모든 범주 — 규정 준수, 보존, 수집" qbr = "분기별 비즈니스 리뷰" qbrSub = "담당 SE가 매 분기 사용량과 로드맵을 검토합니다" +region = "주 / 지역" +regionPlaceholder = "California" running = "{{annual}} / 년 · {{years}}년 {{tcv}}" s1Sub = "귀하의 팀과 매년 실행할 것으로 예상되는 PDFs입니다." s1Title = "얼마나 처리하시겠습니까?" @@ -7623,6 +8496,12 @@ s2Title = "약정 및 서비스" s3Sub = "견적 및 그로부터 생성되는 계약에 사용됩니다." s3Title = "상세 정보" serviceLevel = "서비스 수준" +size_compact = "Compact" +size_compactSub = "대부분 텍스트, 1 MB 미만" +size_heavy = "Heavy" +size_heavySub = "스캔 또는 이미지 많음, 5 MB+" +size_standard = "Standard" +size_standardSub = "텍스트와 이미지 혼합, 1~5 MB" slDedicated = "전담" slDedicatedSub = "영업일 기준 4시간 · 전담 계정 매니저 · +30%" slPriority = "우선" @@ -7630,6 +8509,8 @@ slPrioritySub = "영업일 당일 · 지정 CSM · +15%" slStandard = "표준" slStandardSub = "다음 영업일 · 공유 CSM · 포함" stepOf = "{{total}}단계 중 {{n}}단계" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "선택 사항" term = "기간" termDiscount = "{{pct}}% 다년 약정 할인 적용됨" title = "견적 만들기" @@ -7637,6 +8518,7 @@ training = "온보딩 및 교육" trainingSub = "팀이 바로 사용할 수 있도록 라이브 세션 제공" users = "전체 사용자" usersPlaceholder = "예: 250" +viewEula = "읽기" volEstimated = "{{count}}명의 사용자 기준 예상(~각 2,000 PDFs, 자동화 포함). 더 정확한 수치를 알고 있다면 편집하세요." volManual = "입력한 수치를 사용 중입니다. 언제든지 팀 규모를 기준으로 다시 추정할 수 있습니다." volNoUsers = "잘 모르시나요? 팀 규모를 입력하면 추정해 드립니다." @@ -7645,55 +8527,55 @@ volumePlaceholder = "예: 1,000,000" years_one = "{{count}}년" years_other = "{{count}}년" -[portal.procurement.docs] -count_one = "{{count}}개 문서" -count_other = "{{count}}개 문서" -done = "완료" -here = "현재 위치" -hide = "숨기기" -optional = "선택 사항" -paidAddon = "유료 추가 기능" -show = "표시" -subtitle = "여정의 각 단계에서 필요한 모든 항목이 거래 진행에 맞춰 표시됩니다." -supportingSubtitle = "SOC 2, 보안 검토, 세금 양식 등 보안 또는 조달 팀이 요청할 때 준비되어 있습니다. 일부는 일회성 수수료가 부과됩니다." -supportingTitle = "평가 지원" +[portal.procurement.documents] +agreement = "Enterprise 계약서" +agreementSub = "MSA, 주문서 및 DPA가 하나의 서명으로 제공됩니다." +download = "다운로드" +eula = "EULA 및 상업 조건" +eulaSub = "소프트웨어 및 셀프 서비스 상업 조건입니다." +invoice = "인보이스" +invoiceSub = "첫 구독 인보이스입니다." +laterInvoice = "수락 후 사용 가능" +laterQuote = "견적이 발행되면 사용 가능" +quote = "견적" +quoteSub = "항목별 Enterprise 견적입니다." +sla = "SLA 별첨" +slaSub = "지원 등급 및 응답 목표입니다." +subprocessors = "하위 처리자" +subprocessorsSub = "귀하를 대신해 데이터를 처리하는 제3자입니다." +subtitle = "거래 서류를 언제든지 사용할 수 있습니다." title = "문서" -upcoming = "예정" +view = "보기" [portal.procurement.error] title = "문제가 발생했습니다" [portal.procurement.hero] -company = "엔터프라이즈 거래" +barAria = "{{total}}단계 중 {{current}}단계" ctaAgreement = "계약 검토 및 서명" -ctaLive = "라이브 상태입니다" -ctaPayment = "결제 추가" +ctaExploring = "평가판 설정" ctaQuote = "견적 검토" +ctaReviewQuote = "견적 검토" ctaTrial = "견적 만들기" +documents = "문서" eyebrow = "Enterprise 조달" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "팀원 초대" -keyDocs = "주요 문서" -nextStep = "다음 단계: {{action}}" -notStarted = "시작 안 함" +licenseKey = "라이선스 키" +liveSub = "조직이 프로비저닝되었고 라이선스가 활성 상태입니다." +liveTitle = "Stirling Enterprise에서 라이브 상태입니다" +next = "다음: {{stage}}" open = "조달 열기" scheduleCall = "통화 예약" -setup1Sub = "팀원을 초대하세요" -setup1Title = "PDF Editor 배포" -setup2Sub = "편집기 및 기타 소스 전반에서 처리를 켜세요" -setup2Title = "PDF Processor 연결" -setup3Sub = "필요할 때 보안, 컴플라이언스, 라우팅 또는 보존을 켜세요" -setup3Title = "권장 정책 추가" +sentenceAgreement = "계약서 검토 및 서명" +sentenceLive = "프로비저닝 및 롤아웃" +sentencePayment = "구매 주문 및 결제" +sentenceQuote = "견적 검토 및 수락" +sentenceTrial = "평가판 워크스페이스 설정" [portal.procurement.journey] daysLeft_one = "{{count}}일 남음" daysLeft_other = "{{count}}일 남음" -engineerLabel = "담당 솔루션 엔지니어" -eyebrow = "롤아웃" -live = "Stirling Enterprise에서 라이브 상태입니다" -nextStep = "다음 단계: {{action}}" -subtitle = "담당 솔루션 엔지니어가 모든 단계에 함께합니다. 한 번에 하나의 다음 작업을 진행하며, 전체 체크리스트는 아래에 있습니다." -title = "평가판에서 라이브까지, 하나의 안내 경로" -trialTitle = "Enterprise 평가판" [portal.procurement.journeySteps.agreement] blurb = "하나의 서명으로 MSA, 주문서, EULA, DPA를 포괄합니다." @@ -7727,6 +8609,9 @@ downloadError = "아직 오프라인 라이선스 파일을 생성할 수 없습 downloadOffline = "오프라인 라이선스 다운로드(.lic)" hint = "이 키를 self-hosted 인스턴스에 붙여넣어 활성화하거나 기록용으로 보관하세요. 안전하게 보관하세요." label = "라이선스 키" +subtitle = "이 키로 셀프 호스팅 인스턴스를 활성화하거나 기록용으로 보관하세요." +title = "라이선스 키" +trialFileHint = "이것은 평가판 라이선스 파일입니다. 계약이 완료되면 돌아와 다시 다운로드하세요 — 온라인 라이선스 키와 달리 .lic 파일은 자동으로 업데이트되지 않습니다." [portal.procurement.link] cta = "계정 연결" @@ -7739,65 +8624,62 @@ description = "구독이 활성화되었으며 라이선스가 발급되었습 eyebrow = "라이브" title = "Stirling Enterprise에서 라이브 상태입니다" -[portal.procurement.locked] -description = "엔터프라이즈 평가를 시작하면 평가판 키, 약정 볼륨 견적, 단일 서명 계약, 결제 및 문서 원장이 모두 여기에 표시됩니다." -eyebrow = "Enterprise 전용" -talkToSales = "영업팀에 문의" -title = "조달 트랙은 Enterprise에서 열립니다" - [portal.procurement.milestone] -accept = "수락하고 계속" -description = "PDF를 다운로드하여 팀과 공유하고, 준비되면 돌아와 수락하거나 변경하세요." -download = "PDF 다운로드" downloadError = "아직 견적 PDF를 다운로드할 수 없습니다. 잠시 후 다시 시도하세요." -edit = "견적 편집" -eyebrow = "견적 {{number}}" -perYear = " / 년" -preparedFor = "{{company}}용으로 준비됨" -tcv = "총 계약 가치 {{value}}" -title = "견적이 준비되었습니다" - -[portal.procurement.modal] -cancel = "취소" -chooseFile = "파일 선택" -close = "닫기" -downloadBody = "다운로드가 곧 시작됩니다." -downloadCta = "다운로드" -downloadTitle = "다운로드" -noFile = "선택된 파일 없음" -payBody = "Stripe를 통해 카드 또는 은행 송금으로 약정 계약을 결제하세요. 결제가 확인되는 즉시 워크스페이스가 프로비저닝됩니다." -payCta = "Stripe로 계속" -payTitle = "결제 확인" -requestBodyFree = "요청 시 생성합니다. 확인하면 담당 솔루션 엔지니어가 곧 보내드립니다." -requestBodyPaid = "유료 추가 기능입니다. 확인하면 담당 솔루션 엔지니어가 범위를 정하고 서류를 보내드립니다." -requestCta = "요청" -requestTitle = "이 문서 요청" -signBody = "전자 서명을 위해 Stirling Enterprise Agreement를 엽니다. 하나의 서명으로 MSA, 주문서, EULA, DPA를 포괄합니다. 당사가 자동으로 대응 서명하고 결제로 진행됩니다." -signCta = "서명용으로 열기" -signTitle = "계약 검토 및 서명" -uploadBody = "PO를 보내주시면 귀하의 조건에 맞춰 송장을 발행합니다. PDF를 끌어오거나 아래에서 파일을 선택하세요." -uploadCta = "구매 주문서 업로드" -uploadTitle = "구매 주문서 업로드" [portal.procurement.payment] description = "견적이 수락되었고 라이선스가 이미 활성화되어 팀이 바로 시작할 수 있습니다. 준비되면 첫 번째 송장을 결제하세요. 이메일 없이 여기에서 결제하거나 다운로드할 수 있습니다." +downloadAgreement = "서명된 계약서 다운로드" downloadInvoice = "송장 다운로드" -simulate = "결제 수신 시뮬레이션(데모)" +eyebrow = "결제" title = "구독 생성됨" viewInvoice = "송장 보기 및 결제" +[portal.procurement.review] +acceptCta = "견적 수락" +annual = "연간 요금(1년 차)" +downloadCta = "견적 다운로드" +poNumber = "구매 주문: {{po}}" +renewal = "기간 이후 {{amount}}/년으로 갱신(+{{pct}}% CPI)" +tcv = "{{years}}년 기간, 선불 일시납: {{tcv}}" +validUntil = "유효 기간: {{date}}" + [portal.procurement.schedule] fallback = "스케줄러를 로드할 수 없습니다." fallbackLink = "새 탭에서 일정 예약 열기" +loading = "스케줄러를 불러오는 중..." subtitle = "담당 솔루션 엔지니어가 팀의 롤아웃을 안내합니다. 편한 시간을 선택하세요." title = "통화 예약" -[portal.procurement.status] -action = "작업 필요" -available = "사용 가능" -complete = "완료" -pending = "대기 중" -request = "요청 시" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "밀폐형, 아웃바운드 없음" +back = "뒤로" +businessName = "사업자명" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "관리형, 가장 빠른 시작" +continue = "계속" +deployment = "어디에 배포할까요?" +eula = "Stirling EULA 및 상업 조건에 동의합니다." +fullName = "전체 이름" +fullNamePlaceholder = "이름" +invites = "팀원 초대 · 선택 사항" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "통화 예약" +seats = "테스트 인원" +seatsPlaceholder = "예: 15" +selfhost = "셀프 호스팅" +selfhostSub = "Docker 또는 Kubernetes" +start = "평가판 시작" +stepOf = "{{total}}단계 중 {{n}}단계" +subtitle = "14일 무료, 카드 필요 없음." +subtitleDetails = "견적 및 계약을 위한 몇 가지 세부 정보입니다." +talkFirst = "먼저 상담을 원하시나요?" +title = "평가판 설정" +viewEula = "읽기" +workEmail = "업무용 이메일" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "연장하면 7일이 추가되고 담당 솔루션 엔지니어에게 알림이 전송됩니다." @@ -7808,109 +8690,113 @@ maxed = "한도 도달" subtitle = "무료 평가판은 {{date}}까지 진행됩니다. 카드가 필요하지 않습니다." title = "Enterprise 평가판" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "약정 볼륨 가격, 조직 전체 SSO + SCIM + RBAC, 90일 변경 불가능 감사, 전담 SE." -homeCta = "평가판 시작 →" -homeHeadline = "수백만 개의 PDFs를 처리하세요." - -[portal.recentActivity] -title = "최근 활동" -viewAll = "모두 보기" - -[portal.recentActivity.empty] -description = "파이프라인 실행, 배포 및 에이전트 이벤트가 여기에 표시됩니다." -title = "아직 아무것도 없음" - -[portal.search] -ariaLabel = "검색" -placeholder = "Stirling 검색 — 엔드포인트, 파이프라인, 문서…" - -[portal.search.empty] -noActionsDescription = "빠른 작업을 사용할 수 있게 되면 여기에 표시됩니다." -noActionsTitle = "빠른 작업 없음" -noMatches = "\"{{query}}\"에 대한 일치 항목 없음" -noMatchesDescription = "다른 키워드를 시도하거나 카탈로그를 찾아보세요." - [portal.settings.groups] admin = "관리자" [portal.settings.sections] account-link = "계정 연결" -[portal.shell.header] -accountFallback = "계정" -accountMenu = "계정 메뉴" -darkMode = "다크 모드" -lightMode = "라이트 모드" -search = "검색" -searchPlaceholder = "검색…" -signOut = "로그아웃" -switchToDark = "다크 테마로 전환" -switchToLight = "라이트 테마로 전환" - [portal.shell.sidebar] appEditor = "편집기" appProcessor = "프로세서" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}}개 문서" linkAccount = "Stirling 계정 연결" -planEditor = "Editor 플랜" -planEnterprise = "Enterprise 플랜" -planProcessor = "Processor 플랜" primaryNav = "기본 탐색" switchApp = "앱 전환" +[portal.shell.topbar] +closeNav = "탐색 닫기" +openNav = "탐색 열기" +search = "검색" + [portal.sources] subtitle = "문서를 Stirling으로 공급하는 재사용 가능한 입력 연결입니다. 연결을 한 번 구성한 다음 여러 정책에서 참조하세요. 행을 클릭하면 해당 구성과 사용하는 정책을 볼 수 있습니다." title = "소스" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "소스 연결" +[portal.sources.builder] +back = "소스로 돌아가기" +backToTypes = "모든 소스 유형" +cancel = "취소" +chooseHint = "문서가 어디에서 오는지 선택하세요. 회색으로 표시된 커넥터는 곧 제공됩니다." +comingSoon = "곧 제공 예정" +comingSoonHeading = "곧 제공 예정" +create = "소스 생성" +createTitle = "소스 연결" +delete = "삭제" +editTitle = "소스 편집" +enabled = "활성화됨" +save = "변경 사항 저장" + +[portal.sources.builder.folderAccess] +description = "폴더 자동화는 관리자가 허용한 폴더만 사용할 수 있습니다. 폴더 액세스 설정에서 추가한 다음 다시 시도하세요." +openSettings = "폴더 액세스 설정" +title = "이 폴더는 허용되지 않습니다" + [portal.sources.delete] body = "\"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 이를 참조하는 정책은 업데이트해야 합니다." cancel = "취소" confirm = "삭제" title = "소스를 삭제하시겠습니까?" -[portal.sources.detail] -closeAriaLabel = "세부 정보 닫기" -delete = "소스 삭제" -docs24h = "최근 24시간" -docs30d = "최근 30일" -docsTotal = "총 확인됨" -docsTrend = "최근 30일간 문서" -documents = "문서" -edit = "편집" -notReferenced = "어떤 정책에서도 참조하지 않으므로 삭제해도 안전합니다." -pause = "일시 중지" -resume = "재개" -subtitle = "{{type}} · {{status}}" -usedBy = "사용 중" - -[portal.sources.empty] -description = "정책이 문서를 가져올 위치를 가질 수 있도록 폴더(곧 cloud storage도)를 연결하세요." -title = "아직 연결된 소스 없음" - [portal.sources.kpi] inUse = "사용 중" total = "연결" unused = "미사용" +[portal.sources.networkFields.connection] +helperText = "서버 주소와 자격 증명이 저장된 연결입니다. 이를 참조하는 모든 소스에서 재사용됩니다." +label = "연결" + +[portal.sources.networkFields.directory] +helperText = "로그인 홈 또는 공유 루트를 기준으로 폴링할 서버의 폴더입니다. 루트의 경우 비워 두세요." +label = "폴더" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "소비는 모든 정책이 처리한 후 서버에서 각 파일을 제거합니다." +label = "읽기 모드" + +[portal.sources.networkFields.mode.options] +consume = "소비: 각 파일을 한 번 처리" +snapshot = "스냅샷: 실행할 때마다 폴더 다시 읽기" + +[portal.sources.networkFields.recursive] +label = "폴더 깊이" + +[portal.sources.networkFields.recursive.options] +all = "하위 폴더 포함" +top = "최상위 수준만" + [portal.sources.status] active = "활성" disabled = "비활성화됨" unused = "미사용" [portal.sources.table] +documents = "문서" source = "소스" status = "상태" +type = "Type" usedBy = "정책" +[portal.sources.types.box] +description = "팀이 이미 공유하는 Box 폴더에서 문서를 가져옵니다." +label = "Box" + +[portal.sources.types.dropbox] +description = "공유 Dropbox 폴더에서 문서를 가져옵니다." +label = "Dropbox" + [portal.sources.types.editor] +description = "정책 및 AI 실행 전반에서 팀이 Editor에서 처리한 문서입니다." label = "편집기" +noPolicies = "아직 Editor에서 실행된 정책이 없습니다." + +[portal.sources.types.email] +description = "전용 메일함으로 전송된 첨부 파일을 문서로 변환합니다." +label = "Email 받은편지함" [portal.sources.types.folder] description = "서버의 디렉터리에서 새 문서를 감시합니다." @@ -7943,25 +8829,80 @@ label = "폴더 깊이" all = "하위 폴더 포함" top = "최상위 수준만" +[portal.sources.types.ftp] +description = "새 문서를 위해 FTP 또는 FTPS 서버 폴더를 폴링합니다." +label = "FTP" + +[portal.sources.types.googledrive] +description = "공유 Google Drive 폴더에서 문서를 가져옵니다." +label = "Google Drive" + +[portal.sources.types.network] +description = "새 문서를 위해 SMB 네트워크 공유를 감시합니다." +label = "네트워크 드라이브" + +[portal.sources.types.onedrive] +description = "새 문서를 위해 OneDrive 폴더를 감시합니다." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 또는 S3 호환 버킷에서 문서를 가져옵니다." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "버킷과 자격 증명을 보유한 저장된 연결입니다. 이를 참조하는 모든 소스 및 파이프라인 출력에서 재사용됩니다." +label = "연결" + +[portal.sources.types.s3.fields.mode] +helperText = "소비는 모든 정책이 처리한 후 버킷에서 각 객체를 제거합니다." +label = "읽기 모드" + +[portal.sources.types.s3.fields.mode.options] +consume = "소비: 각 객체를 한 번 처리" +snapshot = "스냅샷: 실행할 때마다 버킷 다시 읽기" + +[portal.sources.types.s3.fields.prefix] +helperText = "키가 이 접두사로 시작하는 객체만 처리됩니다." +label = "키 접두사" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "새 문서를 위해 SFTP 드롭 폴더를 폴링합니다." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint 문서 라이브러리에서 문서를 가져옵니다." +label = "SharePoint" + [portal.sources.types.unknown] label = "소스" +[portal.sources.types.webhook] +createNote = "이 webhook을 생성하면 전달 URL과 서명 시크릿이 생성됩니다. 발신자는 해당 URL로 문서를 POST하고, 참조하는 정책은 도착 시 실행됩니다." +description = "외부 시스템에서 서명된 HTTP POST로 문서를 수신합니다." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "전달 URL" +secretNote = "서명 시크릿은 webhook이 생성될 때 한 번만 표시됩니다. 재발급하려면 소스를 다시 생성하세요." + +[portal.sources.types.webhook.reveal] +copy = "복사" +done = "완료" +secret = "서명 시크릿" +secretHelp = "이 키(HMAC-SHA256)로 각 전달의 원시 본문에 서명하고 X-Stirling-Signature 헤더로 보내세요." +secretWarning = "지금 서명 시크릿을 복사하세요. 보안을 위해 한 번만 표시되며 나중에 검색할 수 없습니다." +title = "Webhook 생성됨" +url = "전달 URL" +usage = "각 문서를 binary 콘텐츠 유형(application/pdf 또는 application/octet-stream)의 원시 요청 본문으로 전달 URL에 POST하세요. 참조하는 정책은 도착 시 자동으로 실행됩니다." + [portal.sources.wizard] -back = "뒤로" -cancel = "취소" -continue = "계속" -editTitle = "소스 편집" name = "이름" namePlaceholder = "예: 클레임 접수" -save = "변경 사항 저장" -subtitle = "{{total}}단계 중 {{current}}단계 · {{label}}" -title = "소스 연결" type = "유형" -[portal.sources.wizard.steps] -chooseType = "유형 선택" -configure = "구성" -review = "검토 및 연결" +[portal.stepModal] +close = "닫기" [portal.usage] managePayment = "결제 관리" @@ -8021,16 +8962,9 @@ summary = "팀을 소유하며, 팀 구성원의 리소스와 공유 구성을 2 = "기본 정책을 통한 포털 액세스" 3 = "구성원이 할 수 있는 모든 작업" -[portal.welcome] -ariaLabel = "Stirling PDF에 오신 것을 환영합니다" -badge = "Open-source" -installEditor = "Editor 설치" -inviteTeammates = "팀원 초대" -openInBrowser = "브라우저에서 열기" -perks = "영구 무료 · Self-hostable" -subtitle = "세계에서 가장 안전한 PDF Editor는 모든 규모의 팀에게 무료입니다. 60개 이상의 PDF 작업과 SSO가 포함됩니다." -title = "환영합니다" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · 무제한" [printFile] title = "파일 인쇄" @@ -8613,6 +9547,14 @@ text = "PDF 페이지를 90도 단위로 시계 방향 또는 반시계 방향 [rotate.tooltip.header] title = "회전 설정 개요" +[ruler] +clearAll = "모두 지우기" +hideAllLabels = "모든 레이블 숨기기" +hideSmallLabels = "작은 레이블 숨기기" +physicalValues = "물리적" +scaled = "크기 조정됨" +showAllLabels = "모든 레이블 표시" + [sanitize] desc = "PDF 파일에서 잠재적으로 유해한 요소를 제거합니다." sanitizationResults = "무해화 결과" @@ -8654,31 +9596,45 @@ label = "XMP 메타데이터 제거" [sanitize.steps] settings = "설정" -[sanitizePdf] -tags = "청소,보안,안전,위협-제거" - [sanitizePDF] title = "PDF 정리" +[sanitizePdf] +tags = "청소,보안,안전,위협-제거" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "페이지 크기 조정" +[scaleSettings] +activeScale = "활성 스케일" +apply = "스케일 적용" +applyCalibration = "보정 적용" +calculatedScale = "계산된 스케일" +calibrate = "보정" +calibrating = "보정 중" +calibrationDistanceRequired = "0보다 큰 실제 거리를 입력하세요" +calibrationInvalid = "이 측정값으로 축척을 계산할 수 없습니다" +calibrationPaperDistance = "측정된 페이지 거리: {{distance}}" +calibrationTitle = "축척 보정" +calibrationTooltip = "알려진 거리를 측정하여 축척을 자동으로 계산합니다" +customScale = "사용자 지정 축척" +noneSet = "설정된 사용자 지정 축척 없음" +presets = "사전 설정 축척" +ratio = "축척 비율" +ratioHelp = "비율: 페이지 단위 1 = 실제 단위 X" +ratioPlaceholder = "예: 100" +ratioPositive = "비율은 0보다 커야 합니다" +ratioRequired = "비율은 필수입니다" +realDistance = "실제 거리" +realDistancePlaceholder = "예: 5" +reset = "기본값으로 재설정" +unit = "단위" +unitInvalid = "잘못된 단위: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "각도 임계값:" -10 = "출력에서 흰색 테두리를 방지하기 위해 추가 및 제거되는 테두리의 크기를 설정합니다(기본값: 1)." -2 = "이미지 회전에 필요한 최소 절대 각도를 설정합니다(기본값: 10)." -3 = "허용 오차:" -4 = "예상 배경색 주변의 색상 변화 범위를 결정합니다(기본값: 30)." -5 = "최소 영역:" -6 = "사진의 최소 영역 임계값을 설정합니다(기본값: 10000)." -7 = "최소 윤곽 영역:" -8 = "사진의 최소 윤곽선 영역 임계값을 설정합니다" -9 = "테두리 크기:" - [scannerImageSplit] submit = "이미지 스캔 추출" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "기능" whatThisDoesDesc = "스캔된 페이지나 합성 이미지에서 각 사진을 자동으로 찾아 추출합니다—수동 크롭이 필요 없습니다." whenToUse = "사용 시점" +[ScannerImageSplit.selectText] +1 = "각도 임계값:" +10 = "출력에서 흰색 테두리를 방지하기 위해 추가 및 제거되는 테두리의 크기를 설정합니다(기본값: 1)." +2 = "이미지 회전에 필요한 최소 절대 각도를 설정합니다(기본값: 10)." +3 = "허용 오차:" +4 = "예상 배경색 주변의 색상 변화 범위를 결정합니다(기본값: 30)." +5 = "최소 영역:" +6 = "사진의 최소 영역 임계값을 설정합니다(기본값: 10000)." +7 = "최소 윤곽 영역:" +8 = "사진의 최소 윤곽선 영역 임계값을 설정합니다" +9 = "테두리 크기:" + [search] noResults = "결과 없음" placeholder = "검색어 입력..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Stirling-PDF 서버가 오프라인이며 로컬 백 [session] expired = "세션이 만료되었습니다. 페이지를 새로 고침하고 다시 시도하세요." -[sessionManagement.tooltip] -header = "서명 세션 관리" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "새 참여자는 서명 순서의 마지막에 추가됩니다" -bullet2 = "세션이 최종 완료된 후에는 참여자를 추가할 수 없습니다" -bullet3 = "각 참여자는 자신의 차례가 되면 알림을 받습니다" -description = "최종 완료 전에는 언제든지 활성 세션에 참여자를 더 추가할 수 있습니다." -title = "참여자 추가" - -[sessionManagement.tooltip.finalization] -bullet1 = "전체 최종 완료: 모든 참여자가 서명함" -bullet2 = "부분 최종 완료: 일부 참여자가 아직 서명하지 않음" -bullet3 = "서명하지 않은 참여자는 최종 문서에서 제외됩니다" -bullet4 = "최종 완료 후 서명된 PDF를 활성 파일로 로드할 수 있습니다" -description = "최종 완료는 모든 서명을 하나의 서명된 PDF로 결합합니다. 이 작업은 되돌릴 수 없습니다." -title = "세션 최종 완료" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "이미 서명한 참여자는 제거할 수 없습니다" -bullet2 = "제거된 참여자에게는 더 이상 알림이 전송되지 않습니다" -bullet3 = "서명 순서는 자동으로 조정됩니다" -description = "참여자는 서명 전에 세션에서 제거할 수 있습니다." -title = "참여자 제거" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "각 서명은 PDF에 순차적으로 적용됩니다" -bullet2 = "나중에 서명하는 사람은 이전 서명을 볼 수 있습니다" -bullet3 = "승인 워크플로우와 법적 인수 절차에 중요합니다" -description = "세션 생성 시 지정한 순서가 누가 먼저 서명할지를 결정합니다." -title = "서명 순서" - [settings] close = "닫기" +[settings.ai] +documents = "문서 및 RAG" +general = "일반" +limits = "제한 및 성능" +models = "모델 및 제공업체" +title = "AI" + [settings.configuration] advanced = "고급" database = "데이터베이스" endpoints = "엔드포인트" features = "기능" +folderAccess = "폴더 접근" mcp = "MCP Server" storageSharing = "파일 저장 및 공유" systemSettings = "시스템 설정" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "기본 PDF 편집기" defaultPdfEditorActive = "Stirling PDF가 기본 PDF 편집기입니다" defaultPdfEditorChecking = "확인 중..." defaultPdfEditorInactive = "다른 앱이 기본값으로 설정됨" +defaultPdfEditorRemind = "기본값으로 설정하도록 알림" +defaultPdfEditorRemindDescription = "Stirling PDF가 기본 PDF 애플리케이션이 아닐 때 배너를 표시합니다." defaultPdfEditorSet = "이미 기본값" defaultStartupView = "시작 시 기본 보기" defaultStartupViewDescription = "앱 시작 시 왼쪽 열에서 활성화할 탭을 선택하세요." @@ -8817,9 +9763,18 @@ languageDescription = "표시 언어를 선택하세요" logout = "로그아웃" setAsDefault = "기본값으로 설정" theme = "테마" +themeAccent = "강조 색상" +themeAccentCustom = "사용자 지정 색상" +themeAccentCustomHint = "사용자 지정(기본값 권장)" +themeAccentDark = "다크 모드 강조 색상" +themeAccentDefault = "기본값" +themeAccentDefaultHint = "기본 테마(권장)" +themeAccentDescription = "버튼, 링크 및 강조 표시가 이를 따르며 앱에 은은한 색조를 입힙니다. 라이트와 다크는 각각 별도로 적용됩니다. 시스템은 활성 상태인 것을 사용합니다." +themeAccentLight = "라이트 모드 강조 색상" themeDark = "다크" themeDescription = "라이트 모드와 다크 모드를 전환합니다" themeLight = "라이트" +themeReset = "테마를 기본값으로 복원" themeSystem = "System" title = "일반" user = "사용자" @@ -8833,6 +9788,12 @@ intro = "조직을 위한 사용자 인증, 팀 관리, 워크스페이스 기 learnMore = "문서에서 자세히 알아보기" title = "시스템 관리자용" +[settings.general.loginLanding] +description = "Stirling Cloud에 로그인할 때 열릴 앱을 선택하세요." +editor = "편집기" +processor = "처리기" +title = "로그인 후" + [settings.general.mode] fullscreen = "전체 화면" sidebar = "사이드바" @@ -9013,9 +9974,6 @@ title = "정책 및 개인정보 보호" [settings.preferences] title = "환경설정" -[settings.search] -placeholder = "설정 페이지 검색..." - [settings.security] description = "계정을 안전하게 유지하려면 비밀번호를 업데이트하세요." title = "보안" @@ -9041,6 +9999,11 @@ security = "보안" telegram = "Telegram" title = "보안 및 인증" +[settings.signOut] +confirm = "로그아웃하시겠습니까?" +submit = "로그아웃" +title = "로그아웃" + [settings.team] title = "팀" @@ -9156,6 +10119,7 @@ last = "마지막 페이지" maintainRatio = "종횡비 유지 토글" next = "다음 페이지" noSavedSigs = "저장된 서명이 없습니다" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "개인 서명" previous = "이전 페이지" redo = "다시 실행" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "배경 제거 실패" hint = "서명 PNG 또는 JPG 이미지를 업로드하세요" label = "서명 이미지 업로드" placeholder = "이미지 파일 선택" +previewAlt = "Current image signature" processing = "이미지 처리 중..." removeBackground = "흰색 배경 제거(투명 처리)" @@ -9207,6 +10172,17 @@ saved = "위에서 저장된 서명을 선택한 뒤, PDF를 클릭하여 배치 text = "위에 이름을 입력한 후, PDF의 아무 곳이나 클릭하여 서명을 배치하세요." title = "서명 추가 방법" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "서명 이동" pause = "배치 일시중지" @@ -9653,10 +10629,30 @@ title = "서버로 업로드" updateButton = "서버에서 업데이트" uploadButton = "서버로 업로드" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "여기를 클릭하세요" +fallback = "자동으로 열리지 않았다면," +opening = "새 탭에서 Swagger UI를 여는 중..." tags = "api,문서,swagger,엔드포인트,개발" title = "API 문서" @@ -9764,12 +10760,39 @@ selectFilesHint = "이 도구를 실행하려면 활성 파일에서 파일을 singleFileScope = "다음에만 적용: {{fileName}}" viewerMode = "여러 파일을 선택하려면 파일 편집기로 전환하세요." +[toolFormat] +ANY = "모든 파일" +CBR = "만화책 아카이브(CBR)" +CBZ = "만화책 아카이브(CBZ)" +CSV = "CSV 파일" +EBOOK = "전자책" +EMAIL = "이메일" +EXCEL = "스프레드시트" +HTML = "웹 페이지" +IMAGE = "이미지" +JAVASCRIPT = "JavaScript 파일" +JSON = "JSON 파일" +MARKDOWN = "Markdown 파일" +NONE = "파일 없음" +PCL = "PCL 인쇄 파일" +PDF = "PDF" +PDF_ENCRYPTED = "암호로 보호된 PDF" +POSTSCRIPT = "PostScript 파일" +PPT = "프레젠테이션" +TEXT = "텍스트 파일" +VIDEO = "비디오" +WORD = "Word 문서" +XML = "XML 파일" +XPS = "XPS 문서" +ZIP = "ZIP 아카이브" + [toolPanel] alpha = "알파" backToAllTools = "모든 도구로 돌아가기" collapse = "패널 접기" expand = "패널 펼치기" goBack = "뒤로 가기" +pdfTools = "PDF 도구" placeholder = "시작하려면 도구를 선택하세요" premiumFeature = "프리미엄 기능:" toolsHeader = "도구" @@ -9806,11 +10829,6 @@ noToolsFound = "도구를 찾을 수 없음" quickAccess = "빠른 액세스" searchPlaceholder = "도구 검색..." -[toolPicker.categories] -advancedTools = "고급 도구" -recommendedTools = "추천 도구" -standardTools = "표준 도구" - [toolPicker.subcategories] advancedFormatting = "고급 서식" automation = "자동화" @@ -9824,6 +10842,9 @@ removal = "제거" signing = "서명" verification = "검증" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "도구를 찾을 수 없습니다" noTools = "사용 가능한 도구가 없습니다" @@ -9950,28 +10971,53 @@ title = "사용자" you = "(나)" [users.action] +cancelInvite = "초대 취소" deleteTeam = "팀 삭제" disableMfa = "MFA 재설정" move = "팀으로 이동" reinstate = "복원" remove = "조직에서 제거" +removeTeam = "팀에서 제거" rename = "팀 이름 변경" resetPw = "비밀번호 재설정" suspend = "일시 정지" unlock = "계정 잠금 해제" +[users.activity] +daysAgo = "{{count}}일 전" +hoursAgo = "{{count}}시간 전" +justNow = "방금 전" +minutesAgo = "{{count}}분 전" +monthsAgo_one = "{{count}}개월 전" +monthsAgo_other = "{{count}}개월 전" +never = "없음" +weeksAgo_one = "{{count}}주 전" +weeksAgo_other = "{{count}}주 전" +yearsAgo_one = "{{count}}년 전" +yearsAgo_other = "{{count}}년 전" + [users.cap] addProcessor = "+ Processor" approver = "정책 승인" editor = "편집기" processor = "프로세서" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}}에 대한 초대를 취소하시겠습니까? 현재 링크로는 참여할 수 없게 됩니다." +cancelInviteTitle = "초대 취소" deleteTeamBody = "{{name}} 팀을 삭제하시겠습니까? 먼저 팀이 비어 있어야 합니다. 구성원을 다른 팀으로 이동해야 하며, 여전히 통합 구성을 소유하고 있으면 안 됩니다." deleteTeamTitle = "팀 삭제" disableMfaBody = "{{name}}의 MFA 등록을 제거하시겠습니까? 필요한 경우 다음 로그인 시 다시 설정하게 됩니다." disableMfaTitle = "MFA 재설정" removeBody = "{{name}}을(를) 조직에서 영구적으로 제거하시겠습니까? 이 작업은 되돌릴 수 없습니다." +removeTeamBody = "{{name}}님을 팀에서 제거하시겠습니까? 계정은 유지되지만 이 팀의 리소스에 대한 접근 권한은 잃게 됩니다." removeTitle = "구성원 제거" [users.empty] @@ -9982,10 +11028,8 @@ title = "아직 구성원 없음" addToTeam = "팀에 추가" guestCount = "{{count}}명 게스트" guests = "게스트" -guestsDesc = "외부 협업자, 공유한 범위로 제한됨. 편집기만 사용." ledBy = "{{owner}}이(가) 이끎" org = "조직" -orgDesc = "조직 전체 권한 및 정책 승인을 가진 소유자" owners = "{{count}}명 소유자" team = "{{name}} 팀" teamMeta = "{{count}}명" @@ -10022,6 +11066,18 @@ username = "사용자 이름" usernameError = "사용자 이름은 최소 3자 이상이어야 합니다" usernamePlaceholder = "jsmith" +[users.invites] +by = "{{who}}님이 초대함" +cancel = "취소" +expiresInDays_one = "{{count}}일 후 만료" +expiresInDays_other = "{{count}}일 후 만료" +expiresToday = "오늘 만료" +title = "대기 중인 초대" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "백엔드에 연결하는 중 문제가 발생했거나 접근 권한이 없습니다. 다시 시도하세요." title = "멤버를 불러올 수 없습니다" @@ -10211,10 +11267,15 @@ searchPlaceholder = "첨부 파일 검색" title = "첨부 파일" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "북마크 제목" +bookmarkTitleRequired = "북마크 제목은 필수입니다" closeSidebar = "북마크 사이드바 닫기" collapseAll = "모든 북마크 접기" +empty = "No bookmarks in this document" expandAll = "모든 북마크 펼치기" +searchPlaceholder = "북마크 검색" +title = "Bookmarks" [viewer.comments] addComment = "댓글 추가" @@ -10236,10 +11297,12 @@ locateAnnotation = "문서에서 찾기" moreActions = "추가 작업" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "페이지 {{page}}" placingHint = "페이지를 클릭하여 배치… (취소)" removeCommentOnly = "댓글만 제거" saveReply = "답글 저장" +searchPlaceholder = "Search comments" title = "댓글" typeComment = "댓글" typeInsertText = "텍스트 삽입" @@ -10262,6 +11325,7 @@ unsavedDesc = "저장되지 않은 변경 사항이 있습니다" closeSidebar = "레이어 사이드바 닫기" hideAll = "모든 레이어 숨기기" showAll = "모든 레이어 표시" +title = "Layers" [viewer.link] delete = "링크 삭제" @@ -10605,30 +11669,6 @@ title = "워터마크 텍스트" image = "이미지" text = "텍스트" -[wetSignature.tooltip] -header = "서명 생성 방법" - -[wetSignature.tooltip.draw] -bullet1 = "펜 색상과 두께 사용자 지정" -bullet2 = "만족할 때까지 지우고 다시 그리기" -bullet3 = "터치 기기(태블릿, 휴대폰) 지원" -description = "마우스 또는 터치스크린을 사용해 손글씨 서명을 만듭니다. 개인적이고 진정성 있는 서명에 적합합니다." -title = "서명 그리기" - -[wetSignature.tooltip.type] -bullet1 = "여러 글꼴 중에서 선택" -bullet2 = "텍스트 크기와 색상 사용자 지정" -bullet3 = "표준화된 서명에 적합" -description = "입력한 텍스트로 서명을 생성합니다. 빠르고 일관되어 비즈니스 문서에 적합합니다." -title = "서명 입력" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG 및 기타 이미지 형식 지원" -bullet2 = "최상의 결과를 위해 투명 배경 권장" -bullet3 = "서명 영역에 맞도록 이미지 크기 조정" -description = "미리 만든 서명 이미지를 업로드합니다. 스캔한 서명이나 회사 로고가 있는 경우에 이상적입니다." -title = "서명 이미지 업로드" - [workbenchBar] activeFiles = "활성 파일" annotations = "주석" @@ -10642,6 +11682,7 @@ exitRedaction = "가리기 모드 종료" exportAll = "PDF 내보내기" exportSelected = "선택한 페이지 내보내기" formFill = "양식 채우기" +hideToolbar = "Hide toolbar" multiTool = "멀티 도구" panMode = "이동 모드" print = "PDF 인쇄" @@ -10652,6 +11693,7 @@ redact = "가리기" rotateLeft = "왼쪽으로 회전" rotateRight = "오른쪽으로 회전" ruler = "자/측정" +rulerSettings = "축척 설정" save = "저장" saveAll = "모두 저장" saveAs = "다른 이름으로 저장" @@ -10661,6 +11703,7 @@ selectAll = "모두 선택" selectByNumber = "페이지 번호로 선택" selectLanguage = "언어 선택" share = "공유" +showToolbar = "Show toolbar" toggleAnnotations = "주석 표시 전환" toggleAttachments = "첨부 파일 표시 전환" toggleBookmarks = "책갈피 표시 전환" diff --git a/frontend/editor/public/locales/ml-ML/translation.toml b/frontend/editor/public/locales/ml-ML/translation.toml index 8dcb039c88..ac5f6c5967 100644 --- a/frontend/editor/public/locales/ml-ML/translation.toml +++ b/frontend/editor/public/locales/ml-ML/translation.toml @@ -1,7 +1,6 @@ alphabet = "അക്ഷരമാല" apply = "പ്രയോഗിക്കുക" applyAndContinue = "സേവ് ചെയ്ത് പുറത്ത് പോകുക" -areYouSure = "നിങ്ങൾക്ക് പോകണമെന്ന് തീർച്ചയാണോ?" back = "തിരികെ" black = "കറുപ്പ്" blue = "നീല" @@ -46,10 +45,8 @@ filesSelected = "ഫയലുകൾ തിരഞ്ഞെടുത്തു" font = "അക്ഷരം" fontSizeTooltip = "പേജ് നമ്പർ ടെക്സ്റ്റിന്റെ പോയിന്റ് സൈസ്. വലിയ നമ്പറുകൾ വലുതായ ടെക്സ്റ്റ് സൃഷ്ടിക്കും." fontTypeTooltip = "പേജ് നമ്പറുകളുടെ ഫോണ്ട് കുടുംബം. നിങ്ങളുടെ ഡോക്യുമെന്റിന്റെ ശൈലിയനുസരിച്ച് തിരഞ്ഞെടുക്കുക." -genericSubmit = "സമർപ്പിക്കുക" green = "പച്ച" help = "സഹായം" -imgPrompt = "ചിത്രം(ങ്ങൾ) തിരഞ്ഞെടുക്കുക" incorrectPasswordMessage = "നിലവിലെ പാസ്‌വേഡ് തെറ്റാണ്." info = "വിവരം" invalidUndoData = "റദ്ദാക്കാൻ കഴിയില്ല: അസാധുവായ പ്രവർത്തന ഡാറ്റ" @@ -62,7 +59,6 @@ loginAgreementTitle = "ലോഗിൻ കരാർ" logOut = "ലോഗൗട്ട് ചെയ്യുക" marginTooltip = "പേജ് നമ്പറും പേജിന്റെ അരവുമുള്ള ദൂരം." moreOptions = "കൂടുതൽ ഓപ്ഷനുകൾ" -multiPdfPrompt = "PDF-കൾ തിരഞ്ഞെടുക്കുക (2+)" never = "ഒരിക്കലും" no = "ഇല്ല" noFileSelected = "ഫയലൊന്നും തിരഞ്ഞെടുത്തിട്ടില്ല. ദയവായി ഒരെണ്ണം അപ്‌ലോഡ് ചെയ്യുക." @@ -80,7 +76,6 @@ pageOrderPrompt = "ഇഷ്ടാനുസൃത പേജ് ക്രമം ( pages = "പേജുകൾ" pageSelectionPrompt = "ഇഷ്ടാനുസൃത പേജ് തിരഞ്ഞെടുക്കൽ (പേജ് നമ്പറുകളുടെ കോമയാൽ വേർതിരിച്ച ലിസ്റ്റ് 1,5,6 അല്ലെങ്കിൽ 2n+1 പോലുള്ള ഫംഗ്ഷനുകൾ നൽകുക) :" password = "പാസ്‌വേഡ്" -pdfPrompt = "PDF(കൾ) തിരഞ്ഞെടുക്കുക" pin = "ഫയൽ പിൻ ചെയ്യുക (ടൂൾ പ്രവർത്തനത്തിന് ശേഷം സജീവമായി നിലനിർത്തുക)" pro = "പ്രോ" processingComplete = "താങ്കളുടെ ഫയൽ തയ്യാറായി." @@ -110,6 +105,7 @@ undoSuccess = "പ്രവർത്തനം വിജയകരമായി റ unknown = "അജ്ഞാതം" unpin = "പിൻ നീക്കം ചെയ്യുക (ടൂൾ പ്രവർത്തനത്തിന് ശേഷം മാറ്റിസ്ഥാപിക്കുക)" unsavedChanges = "നിങ്ങളുടെ PDF-ിൽ സംരക്ഷിക്കാത്ത മാറ്റങ്ങൾ ഉണ്ട്." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "സംരക്ഷിക്കാത്ത മാറ്റങ്ങൾ" unsupported = "പിന്തുണയില്ല" username = "ഉപയോക്തൃനാമം" @@ -356,6 +352,7 @@ text = "ഈ അനുമതികൾ ഉപയോഗിച്ച് ഉപയോ title = "അനുമതികൾ മാറ്റുക" [addStamp] +preview = "സ്റ്റാമ്പ് പ്രിവ്യൂ" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "മാനുവൽ ഡൗൺലോഡുകൾ: ലിങ്ക noLanguages = "ക്രമീകരിച്ച ഡയറക്ടറിയിൽ tessdata ഭാഷകളൊന്നും കണ്ടെത്തിയില്ല." permissionNotice = "tessdata പാത എഴുതാനാവില്ല. ഡൗൺലോഡുകൾ ബ്രൗസറിൽ തുറക്കും; ദയവായി .traineddata ഫയലുകൾ കൈകൊണ്ട് tessdata ഫോൾഡറിൽ സേവ് ചെയ്യുക." +[admin.settings.ai.documents] +description = "ഡോക്യുമെന്റുകളിലെ ചോദ്യങ്ങൾക്ക് ഉത്തരം നൽകാൻ ഉപയോഗിക്കുന്ന embedding മോഡലും retrieval ക്രമീകരണങ്ങളും കോൺഫിഗർ ചെയ്യുക. സേവ് ചെയ്യുമ്പോൾ AI engine-ന് ബാധകമാകും." +title = "Documents & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "engine-ന്റെ സ്വന്തം environment credential ഉപയോഗിക്കാൻ ശൂന്യമായി വിടുക. Self-hosted single-engine deployments-ന് ബാധകം." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "സേവ് ചെയ്തു - നിലവിലെ key നിലനിർത്താൻ ശൂന്യമായി വിടുക" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI-compatible / Ollama embeddings endpoint-ന്റെ Base URL, ഉദാ. http://ollama:11434/v1. വിശ്വസനീയമായ internal endpoint-ലേക്ക് പോയിന്റ് ചെയ്യണം (SSRF-sensitive)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding model പേര്. Free text; നിർദ്ദേശങ്ങൾ സൂചനകൾ മാത്രമാണ്." +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "ഡോക്യുമെന്റ് ടെക്സ്റ്റ് vector embeddings ആക്കാൻ ഉപയോഗിക്കുന്ന provider." +label = "Embedding provider" + +[admin.settings.ai.documents.maxSearches] +description = "ഓരോ അഭ്യർത്ഥനയ്ക്കും agent നടത്താവുന്ന retrieval searches-ന്റെ പരമാവധി എണ്ണം." +label = "പരമാവധി searches" + +[admin.settings.ai.documents.reindexNote] +body = "Embedding model മാറ്റുന്നത് ഉടൻ പ്രാബല്യത്തിൽ വരും, പക്ഷേ മുൻ മോഡൽ ഉപയോഗിച്ച് index ചെയ്ത ഡോക്യുമെന്റുകൾ search ശരിയായ ഫലങ്ങൾ നൽകാൻ വീണ്ടും index ചെയ്യണം." +title = "വീണ്ടും index ചെയ്യേണ്ടതുണ്ട്" + +[admin.settings.ai.documents.saved] +reindexNote = "നിങ്ങൾ embedding model മാറ്റിയിട്ടുണ്ടെങ്കിൽ, search പുതിയ മോഡൽ ഉപയോഗിക്കാൻ നിലവിലുള്ള ഡോക്യുമെന്റുകൾ വീണ്ടും index ചെയ്യുക." + +[admin.settings.ai.documents.topK] +description = "ഓരോ search-ലും retrieve ചെയ്യുന്ന ഏറ്റവും പ്രസക്തമായ chunks-ന്റെ എണ്ണം." +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling-നെ Python AI engine-ലേക്ക് കണക്റ്റ് ചെയ്ത് ഏത് AI കഴിവുകളാണ് കാണിക്കേണ്ടതെന്ന് തിരഞ്ഞെടുക്കുക. മാറ്റങ്ങൾ restart ചെയ്യുമ്പോൾ ബാധകമാകും." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "ഓരോ AI സവിശേഷതയും ഓൺ അല്ലെങ്കിൽ ഓഫ് ചെയ്യുക. പ്രവർത്തനരഹിതമാക്കിയ സവിശേഷതകൾ app-ൽ മറയ്ക്കും." +title = "കഴിവുകൾ" + +[admin.settings.ai.general.enabled] +description = "പ്രധാന switch. ഓഫ് ആയിരിക്കുമ്പോൾ AI tools, agents, അല്ലെങ്കിൽ engine calls ഒന്നും ലഭ്യമല്ല." +label = "AI പ്രവർത്തനക്ഷമമാക്കുക" + +[admin.settings.ai.general.features.chat] +description = "PDFs-ുമായി പ്രവർത്തിക്കാൻ സംഭാഷണ assistant." +label = "Chat assistant" + +[admin.settings.ai.general.features.classify] +description = "ഡോക്യുമെന്റുകൾ തരം അല്ലെങ്കിൽ ഉള്ളടക്കം പ്രകാരം സ്വയമേവ വർഗ്ഗീകരിക്കുക." +label = "ഡോക്യുമെന്റ് വർഗ്ഗീകരണം" + +[admin.settings.ai.general.features.createPdf] +description = "AI agent വഴി (ഉദാ. HTML-ൽ നിന്ന്) ഒരു പുതിയ PDF സൃഷ്ടിക്കുക." +label = "prompt-ൽ നിന്ന് PDF സൃഷ്ടിക്കുക" + +[admin.settings.ai.general.features.documentQuestions] +description = "അപ്‌ലോഡ് ചെയ്ത ഡോക്യുമെന്റിനെ അടിസ്ഥാനമാക്കി ചോദ്യങ്ങൾ ചോദിച്ച് ഉത്തരങ്ങൾ നേടുക." +label = "ഡോക്യുമെന്റ് ചോദ്യങ്ങൾ" + +[admin.settings.ai.general.features.mathAuditor] +description = "ഗണിത, സംഖ്യാത്മക പിശകുകൾക്കായി ഡോക്യുമെന്റുകൾ പരിശോധിക്കുക." +label = "Math auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "PDF-ലേക്ക് AI എഴുതിയ review comments-കളും annotations-കളും ചേർക്കുക." +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "ഡോക്യുമെന്റ് generation പോലുള്ള കൂടുതൽ ഭാരമുള്ള agent operations-നുള്ള timeout." +label = "ദൈർഘ്യമേറിയ timeout (seconds)" + +[admin.settings.ai.general.note] +body = "AI engine ഒരു പ്രത്യേക service ആയി പ്രവർത്തിക്കുന്നു. അതിന്റെ shared secret" +body2 = "ഒരു container environment variable വഴി സജ്ജീകരിച്ചിരിക്കുന്നു. Provider API keys ഈ പേജുകളിൽ നൽകുകയോ engine environment variables ആയി നൽകുകയോ ചെയ്യാം; ഇവിടെ നൽകിയ keys സേവ് ചെയ്യുമ്പോൾ engine-ലേക്ക് push ചെയ്യും." +title = "AI engine കുറിച്ച്" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "stream ചെയ്ത (token-by-token) chat responses-നുള്ള timeout." +label = "Stream timeout (seconds)" + +[admin.settings.ai.general.test] +button = "കണക്ഷൻ ടെസ്റ്റ് ചെയ്യുക" +failBody = "AI engine പ്രതികരിച്ചില്ല. URL, engine container പ്രവർത്തിക്കുന്നുണ്ടോ, AI പ്രവർത്തനക്ഷമമാണോ (പ്രവർത്തനക്ഷമമാക്കിയ ശേഷം restart ആവശ്യമാണ്) എന്നിവ പരിശോധിക്കുക." +failTitle = "AI engine എത്തിച്ചേരാനാകില്ല" +okBody = "AI engine health check-ന് പ്രതികരിച്ചു." +okTitle = "AI engine എത്തിച്ചേരാവുന്നതാണ്" + +[admin.settings.ai.general.timeoutSeconds] +description = "engine-ലേക്കുള്ള സാധാരണ AI requests-നുള്ള timeout." +label = "Request timeout (seconds)" + +[admin.settings.ai.general.url] +description = "Python AI engine-ന്റെ internal URL, ഉദാ. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "AI requests എത്ര ജോലി ചെയ്യാമെന്നും ഒരേസമയം എത്ര എണ്ണം പ്രവർത്തിക്കാമെന്നും നിയന്ത്രിക്കുന്ന guardrails. സേവ് ചെയ്യുമ്പോൾ AI engine-ന് ബാധകമാകും." +title = "പരിധികളും പ്രകടനവും" + +[admin.settings.ai.limits.maxCharacters] +description = "Guardrail: extracted text ഈ ദൈർഘ്യം കവിഞ്ഞ AI requests നിരസിക്കുക." +label = "ഓരോ request-ലും പരമാവധി characters" + +[admin.settings.ai.limits.maxPages] +description = "Guardrail: ഇത്രയും PDF pages-ൽ കൂടുതലുള്ള AI requests നിരസിക്കുക." +label = "ഓരോ request-ലും പരമാവധി pages" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "മുഴുവൻ engine-ലും ഒരേസമയം നടക്കാവുന്ന in-flight model calls-ന്റെ പരമാവധി എണ്ണം." +label = "Model max concurrency" + +[admin.settings.ai.models] +description = "AI engine ഉപയോഗിക്കുന്ന LLM provider-ഉം smart/fast models-ഉം തിരഞ്ഞെടുക്കുക. സേവ് ചെയ്യുമ്പോൾ AI engine-ന് ബാധകമാകും." +title = "Models & Providers" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "engine-ന്റെ സ്വന്തം environment credential ഉപയോഗിക്കാൻ ശൂന്യമായി വിടുക. Self-hosted single-engine deployments-ന് ബാധകം." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "സേവ് ചെയ്തു - നിലവിലെ key നിലനിർത്താൻ ശൂന്യമായി വിടുക" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI-compatible / Ollama endpoint-ന്റെ Base URL, ഉദാ. http://ollama:11434/v1." +label = "Provider base URL" +warning = "base URL വിശ്വസനീയമായ internal endpoint-ലേക്ക് പോയിന്റ് ചെയ്യണം. engine അതിലേക്ക് server-side requests നടത്തും, അതിനാൽ വിശ്വസനീയമല്ലാത്ത value SSRF-sensitive ആണ്." + +[admin.settings.ai.models.fastMaxTokens] +description = "fast model-നുള്ള പരമാവധി output tokens." +label = "Fast model max tokens" + +[admin.settings.ai.models.fastModel] +description = "ലഘു tasks-നുള്ള ചെലവ് കുറഞ്ഞ, വേഗത്തിലുള്ള model. Free text; നിർദ്ദേശങ്ങൾ സൂചനകൾ മാത്രമാണ്." +label = "Fast model" + +[admin.settings.ai.models.provider] +description = "engine സംസാരിക്കുന്ന LLM provider." +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "smart model-നുള്ള പരമാവധി output tokens." +label = "Smart model max tokens" + +[admin.settings.ai.models.smartModel] +description = "സങ്കീർണ്ണ reasoning-നുള്ള ഉയർന്ന കഴിവുള്ള model. Free text; നിർദ്ദേശങ്ങൾ സൂചനകൾ മാത്രമാണ്." +label = "Smart model" + +[admin.settings.ai.saved] +body = "മാറ്റങ്ങൾ സ്വയമേവ AI engine-ലേക്ക് push ചെയ്യും." +bodyNoPush = "Settings സേവ് ചെയ്തു. AI engine അടുത്ത തവണ configuration എടുക്കുമ്പോൾ അവ ബാധകമാകും." +title = "AI settings സേവ് ചെയ്തു" + [admin.settings.badge] clickToUpgrade = "പ്ലാൻ വിശദാംശങ്ങൾ കാണാൻ ക്ലിക്ക് ചെയ്യുക" @@ -938,6 +1094,25 @@ label = "സ്റ്റാർട്ടപ്പിൽ വീണ്ടും സ description = "സർട്ടിഫിക്കറ്റ് പ്രാബല്യത്തിൽ നിൽക്കുന്ന ദിവസങ്ങൾ" label = "സർട്ടിഫിക്കറ്റ് പ്രാബല്യം (ദിവസങ്ങൾ)" +[admin.settings.folderAccess] +description = "folder sources-നും folder outputs-നും വായിക്കാനും എഴുതാനും അനുവദിച്ചിരിക്കുന്ന directories. ഇത് ഒരു security boundary ആണ്: automations ഈ പട്ടികയ്ക്ക് പുറത്തുള്ള server path-ലേക്ക് ഒരിക്കലും പോയിന്റ് ചെയ്യാൻ കഴിയില്ല." +securityNote = "folder sources-ും outputs-ും പൂർണ്ണമായി പ്രവർത്തനരഹിതമാക്കാൻ ഇത് ശൂന്യമായി വിടുക. Stirling-ന്റെ സ്വന്തം configuration directory എപ്പോഴും off-limits ആണ്." +title = "Folder Access" + +[admin.settings.folderAccess.implied] +description = "Stirling കൈകാര്യം ചെയ്യുന്ന ഈ directories എപ്പോഴും അനുവദിച്ചിരിക്കുന്നു, ഇവിടെ മാറ്റാൻ കഴിയില്ല." +serverStorage = "Server file storage" +title = "എപ്പോഴും അനുവദിച്ചത്" +watchedFolder = "Pipeline watched folder" + +[admin.settings.folderAccess.roots] +add = "ചേർക്കുക" +empty = "folders ഒന്നും അനുവദിച്ചിട്ടില്ല. Folder sources-ും outputs-ും ഇപ്പോൾ പ്രവർത്തനരഹിതമാണ്." +hint = "absolute paths നൽകുക, ഉദാഹരണത്തിന് /data/inbox." +label = "Allowed folder roots" +placeholder = "/data/inbox" +remove = "folder root നീക്കംചെയ്യുക" + [admin.settings.general] description = "ബ്രാൻഡിംഗ്, ഡീഫാൾട്ട് പെരുമാറ്റം എന്നിവ ഉൾപ്പെടെ സിസ്റ്റം-വ്യാപക ആപ്ലിക്കേഷൻ സജ്ജീകരണങ്ങൾ ക്രമീകരിക്കുക." system = "സിസ്റ്റം" @@ -1704,6 +1879,9 @@ width = "വീതി" [app] description = "സൗജന്യ Adobe Acrobat പകരം (10M+ ഡൗൺലോഡുകൾ)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b ആയി മാറ്റുക" convertToPdfA3bDescription = "എംബെഡുചെയ്ത അറ്റാച്ച്മെന്റുകളോടുകൂടിയ ആർക്കൈവൽ PDF സൃഷ്ടിക്കുന്നു" @@ -1842,6 +2020,15 @@ export = "എക്സ്പോർട്ട്" accessDenied = "ആക്‌സസ് നിരസിച്ചു" insufficientPermissions = "ഈ പ്രവർത്തനം നടത്താൻ നിങ്ങൾക്ക് അനുമതിയില്ല." +[auth.callback] +completing = "authentication പൂർത്തിയാക്കുന്നു" +invalidToken = "OAuth login പരാജയപ്പെട്ടു - invalid token." +missingToken = "OAuth login പരാജയപ്പെട്ടു - token ലഭിച്ചില്ല." +oauthFailed = "OAuth login പരാജയപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക." +pleaseWait = "നിങ്ങളെ sign in ചെയ്യുന്നത് പൂർത്തിയാക്കുന്നത് വരെ ദയവായി കാത്തിരിക്കുക." +signedOut = "നിങ്ങൾ sign out ചെയ്തു. ദയവായി വീണ്ടും sign in ചെയ്യുക." +windowMayClose = "പൂർത്തിയായാൽ നിങ്ങൾക്ക് ഈ window അടയ്ക്കാം." + [auth.displayName] guest = "അതിഥി" user = "ഉപയോക്താവ്" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "JavaScript, എംബെഡഡ് ഫയലുകൾ പ description = "ഈ ടൂൾ ഉള്ളടക്കത്തെ അടിസ്ഥാനമാക്കി PDF ഫയലുകൾ സ്വയം പുനർനാമകരണം ചെയ്യും. രേഖയിലെ ടെക്സ്റ്റിൽ നിന്ന് ഏറ്റവും അനുയോജ്യമായ ശീർഷകം കണ്ടെത്താൻ ഇത് വിശകലനം ചെയ്യും." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "സ്വയമേവ തിരിക്കുക" +tags = "സ്വയമേവ തിരിക്കുക,orientation ശരിയാക്കുക,തലകീഴായി,വശത്തേക്ക്,നേരെയാക്കുക,orient,orientation കണ്ടെത്തുക,rotation ശരിയാക്കുക,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "വിശ്വാസ്യത threshold" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "OCR മാത്രം" +text = "Text മാത്രം" +title = "കണ്ടെത്തൽ രീതി" + +[autoRotate.error] +failed = "PDF സ്വയമേവ തിരിക്കുമ്പോൾ ഒരു പിശക് സംഭവിച്ചു." + +[autoRotate.inferUndetected] +title = "കുറഞ്ഞ വിശ്വാസ്യതയുള്ള pages പൊരുത്തപ്പെടാൻ തിരിക്കുക" + +[autoRotate.report] +applied = "{{degrees}}° CW" +confidenceValue = "വിശ്വാസ്യത {{value}}" +noChange = "മാറ്റമില്ല" +pageLabel = "Page {{number}}" +summary = "{{total}} pages-ൽ {{rotated}} തിരിച്ചു, {{unchanged}} അതേപടി വിട്ടു" +title = "കണ്ടെത്തൽ റിപ്പോർട്ട്" + +[autoRotate.report.method] +inferred = "പൊരുത്തപ്പെടുത്തി" +none = "ഒഴിവാക്കി" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "threshold-ന് താഴെ" +blankPage = "Page ശൂന്യമാണ്" +inferredFromDocument = "മറ്റ് pages പോലെ തന്നെ" +noDominantDirection = "മിശ്ര text directions" +osdFailed = "വായിക്കാവുന്ന text കണ്ടെത്തിയില്ല" +osdNoVerdict = "ഏത് ദിശയാണ് മുകളെന്ന് പറയാനായില്ല" +tesseractUnavailable = "OCR ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല" +tooFewGlyphs = "വളരെ കുറച്ച് text" + +[autoRotate.results] +title = "Auto Rotate ഫലങ്ങൾ" + +[autoRotate.settings] +title = "Settings" + +[autoRotate.tooltip.confidenceThreshold] +text = "ഒരു page തിരിക്കുന്നതിന് മുമ്പ് പരിശോധനയ്ക്ക് എത്ര ഉറപ്പ് വേണമെന്നത്. കൂടുതൽ pages തിരിക്കാൻ ഇത് കുറയ്ക്കുക, കൂടുതൽ pages തൊടാതെ വിടാൻ ഇത് കൂട്ടുക." +title = "വിശ്വാസ്യത threshold" + +[autoRotate.tooltip.description] +text = "ഓരോ page ഏത് ദിശയിലായിരിക്കണമെന്ന് കണ്ടെത്തി ശരിയായ ദിശയിലേക്ക് തിരിക്കുന്നു. ഇതിനകം നേരെയിരിക്കുന്ന pages അങ്ങനെ തന്നെ വിടും." + +[autoRotate.tooltip.detectionMode] +text = "Auto ആദ്യം page-ലെ text വായിക്കുന്നു, അത് മതിയാകുന്നില്ലെങ്കിൽ page ഒരു image ആയി പരിശോധിക്കുന്നു. Text മാത്രം image check ഒഴിവാക്കുന്നു, ഒരിക്കലും scan ചെയ്തിട്ടില്ലാത്ത documents-ന് ഇത് ഏറ്റവും വേഗമാണ്. OCR മാത്രം ഓരോ page-നെയും image ആയി കാണും." +title = "കണ്ടെത്തൽ രീതി" + +[autoRotate.tooltip.header] +title = "Auto Rotate" + +[autoRotate.tooltip.inferUndetected] +text = "ശൂന്യമായ അല്ലെങ്കിൽ ഏകദേശം ശൂന്യമായ pages സ്വതന്ത്രമായി പരിശോധിക്കാൻ കഴിയില്ല. വായിക്കാൻ കഴിഞ്ഞ ഓരോ page-നും ഒരേ തിരിവ് ആവശ്യമാണെങ്കിൽ, ഇവയും അതിനോട് പൊരുത്തപ്പെടാൻ തിരിക്കും." +title = "കുറഞ്ഞ വിശ്വാസ്യതയുള്ള pages പൊരുത്തപ്പെടാൻ തിരിക്കുക" + [autoSizeSplitPDF] tags = "pdf,വിഭജിക്കുക,പ്രമാണം,സംഘടന" @@ -2189,33 +2443,6 @@ keywords = "കീവേഡുകൾ: odd, even" numbers = "സംഖ്യകൾ/പരിധികൾ: 5, 10-20" progressions = "പുരോഗതികൾ: 3n, 4n+1" -[certificateChoice.tooltip] -header = "സർട്ടിഫിക്കറ്റ് തരങ്ങൾ" - -[certificateChoice.tooltip.organization] -bullet1 = "സിസ്റ്റം അഡ്മിൻമാർ നിയന്ത്രിക്കുന്നു" -bullet2 = "അധികാരപ്പെടുത്തിയ ഉപയോക്താക്കൾ തമ്മിൽ പങ്കിടുന്നു" -bullet3 = "വ്യക്തിയുടേതല്ല, കമ്പനിയുടെ ഐഡന്റിറ്റിയെ പ്രതിനിധീകരിക്കുന്നു" -bullet4 = "ഏറ്റവും നല്ലത്: ഔദ്യോഗിക ഡോക്യുമെന്റുകൾ, ടീം ഒപ്പുകൾ" -description = "നിങ്ങളുടെ ഓർഗനൈസേഷൻ നൽകുന്ന ഒരു ഷെയർഡ് സർട്ടിഫിക്കറ്റ്. കമ്പനി-വ്യാപക ഒപ്പിടൽ അധികാരത്തിന് ഉപയോഗിക്കുന്നു." -title = "ഓർഗനൈസേഷൻ സർട്ടിഫിക്കറ്റ്" - -[certificateChoice.tooltip.personal] -bullet1 = "ആദ്യം ഉപയോഗിക്കുമ്പോൾ സ്വയം സൃഷ്ടിക്കുന്നു" -bullet2 = "നിങ്ങളുടെ ഉപയോക്തൃ അക്കൗണ്ടുമായി ബന്ധിപ്പിച്ചിരിക്കുന്നത്" -bullet3 = "മറ്റു ഉപയോക്താക്കൾക്കൊപ്പം പങ്കിടാൻ കഴിയില്ല" -bullet4 = "ഏറ്റവും നല്ലത്: വ്യക്തിഗത ഡോക്യുമെന്റുകൾ, വ്യക്തിഗത ഉത്തരവാദിത്തം" -description = "നിങ്ങളുടെ ഉപയോക്തൃ അക്കൗണ്ടിന് പ്രത്യേകമായി സ്വയം സൃഷ്ടിച്ച സർട്ടിഫിക്കറ്റ്. വ്യക്തിഗത ഒപ്പുകൾക്ക് അനുയോജ്യം." -title = "പേഴ്‌സണൽ സർട്ടിഫിക്കറ്റ്" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX ഫയലും പാസ്‌വേഡും ആവശ്യമാണ്" -bullet2 = "ബാഹ്യ Certificate Authorities നൽകുന്നവയായിരിക്കും" -bullet3 = "നിയമപരമായ ഡോക്യുമെന്റുകൾക്ക് ഉയർന്ന വിശ്വാസനില" -bullet4 = "ഏറ്റവും നല്ലത്: നിയമപരമായ കരാറുകൾ, ബാഹ്യ സാധൂകരണം" -description = "നിങ്ങളുടെ സ്വന്തം PKCS#12 സർട്ടിഫിക്കറ്റ് ഫയൽ ഉപയോഗിക്കുക. സർട്ടിഫിക്കറ്റ് ഗുണങ്ങളിലേയ്ക്കുള്ള പൂർണ്ണ നിയന്ത്രണം നൽകുന്നു." -title = "ഇഷ്ടാനുസൃത P12 അപ്‌ലോഡ് ചെയ്യുക" - [certSign] awaitingSignatures = "ഒപ്പുകൾ കാത്തിരിക്കുന്നു" chooseCertificate = "സർട്ടിഫിക്കറ്റ് ഫയൽ തിരഞ്ഞെടുക്കുക" @@ -2542,34 +2769,6 @@ server = "സെർവർ" stepTitle = "സർട്ടിഫിക്കറ്റിന്റെ ഉറവിടം" upload = "അപ്‌ലോഡ്" -[certSign.tooltip.header] -title = "സിഗ്നേച്ചറുകൾ പരിപാലിക്കൽ കുറിച്ച്" - -[certSign.tooltip.overview] -bullet1 = "നിലവിലുള്ള ഒപ്പുകളും അവയുടെ സാധുതയും പരിശോധിക്കുക" -bullet2 = "ഒപ്പിടുന്നവനും സർട്ടിഫിക്കറ്റുകളുടെയും വിശദവിവരങ്ങൾ കാണുക" -bullet3 = "നിങ്ങളുടെ ഡോക്യുമെന്റുകൾ സുരക്ഷിതമാക്കാൻ പുതിയ ഡിജിറ്റൽ ഒപ്പുകൾ ചേർക്കുക" -bullet4 = "എളുപ്പത്തിൽ നാവിഗേഷൻ നൽകി ഒന്നിലധികം ഫയലുകൾക്ക് പിന്തുണ" -text = "ഈ ടൂൾ നിങ്ങളുടെ PDF-കൾ ഡിജിറ്റൽ ഒപ്പിട്ടിട്ടുണ്ടോ എന്ന് പരിശോധിക്കാനും പുതിയ ഡിജിറ്റൽ ഒപ്പുകൾ ചേർക്കാനും അനുവദിക്കുന്നു. ഡിജിറ്റൽ ഒപ്പുകൾ ആരാണ് ഒരു ഡോക്യുമെന്റ് സൃഷ്ടിച്ചതോ അംഗീകരിച്ചതോ എന്ന് തെളിയിക്കുകയും ഒപ്പിട്ടതിന് ശേഷം അത് മാറ്റമാണോ വന്നതെന്ന് കാണിക്കുകയും ചെയ്യുന്നു." -title = "ഈ ടൂൾ എന്താണ് ചെയ്യുന്നത്?" - -[certSign.tooltip.signing] -bullet1 = "PEM, PKCS12, JKS, സെർവർ സർട്ടിഫിക്കറ്റ് ഫോർമാറ്റുകൾ പിന്തുണയ്ക്കുന്നു" -bullet2 = "PDF-ൽ ഒപ്പ് കാണിക്കാനോ മറയ്ക്കാനോ ഓപ്ഷൻ" -bullet3 = "കാരണം, സ്ഥലം, ഒപ്പിടുന്നവന്റെ പേര് എന്നിവ ചേർക്കുക" -bullet4 = "ദൃശ്യ ഒപ്പുകൾ ഏത് പേജിൽ വെക്കണമെന്ന് തിരഞ്ഞെടുക്കുക" -bullet5 = "ലളിതമായ 'Sign with Stirling-PDF' ഓപ്ഷനിന് സെർവർ സർട്ടിഫിക്കറ്റ് ഉപയോഗിക്കുക" -text = "ഒരു PDF ഒപ്പിടാൻ, നിങ്ങൾക്ക് ഒരു ഡിജിറ്റൽ സർട്ടിഫിക്കറ്റ് (ഉദാ., PEM, PKCS12, അല്ലെങ്കിൽ JKS) ആവശ്യമുണ്ട്. ഒപ്പിനെ ഡോക്യുമെന്റിൽ ദൃശ്യമാക്കാനോ സുരക്ഷയ്ക്കായി അദൃശ്യമാക്കാനോ നിങ്ങൾക്ക് തിരഞ്ഞെടുക്കാം." -title = "ഒപ്പുകൾ ചേർക്കൽ" - -[certSign.tooltip.validation] -bullet1 = "ഒപ്പുകൾ സാധുവാണോ അസാധുവാണോ എന്ന് കാണിക്കുന്നു" -bullet2 = "ഒപ്പിടുന്നവരുടെ വിവരങ്ങളും ഒപ്പിട്ട തീയതിയും പ്രദർശിപ്പിക്കുന്നു" -bullet3 = "ഒപ്പിട്ടതിന് ശേഷം ഡോക്യുമെന്റ് മാറ്റപ്പെട്ടിട്ടുണ്ടോയെന്ന് പരിശോധിക്കുന്നു" -bullet4 = "പരിശോധനയ്ക്കായി ഇച്ഛാനുസൃത സർട്ടിഫിക്കറ്റുകൾ ഉപയോഗിക്കാം" -text = "നിങ്ങൾ ഒപ്പുകൾ പരിശോധിക്കുമ്പോൾ, അവ സാധുവാണോ, ആരാണ് ഒപ്പിട്ടത്, എപ്പോൾ ഒപ്പിട്ടത്, ഒപ്പിട്ടതിന് ശേഷം ഡോക്യുമെന്റിൽ മാറ്റമുണ്ടോ എന്നിവ ടൂൾ അറിയിക്കും." -title = "ഒപ്പുകൾ പരിശോധന" - [changeCreds] changeUsername = "നിങ്ങളുടെ യൂസർനെയിം അപ്‌ഡേറ്റ് ചെയ്യുക. അപ്‌ഡേറ്റിന് ശേഷം നിങ്ങൾ ലോഗ് ഔട്ട് ചെയ്യപ്പെടും." credsUpdated = "അക്കൗണ്ട് അപ്‌ഡേറ്റ് ചെയ്തു" @@ -2737,12 +2936,16 @@ text = "ഈ അനുമതികൾ മാറ്റാൻ പറ്റാത് [chat.actions] copy = "സന്ദേശം പകർത്തുക" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling ഏജന്റ് ഓപ്ഷനുകൾ" +assistant = "AI Assistant" clearChat = "ചാറ്റ് മായ്ക്കുക" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "ഈ പ്രവർത്തനം നിങ്ങളുടെ ക് tooltip = "സ്ക്രീനിൽ നിന്ന് നിറം എടുക്കുക" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "നിറം തിരഞ്ഞെടുക്കുക" [common] +actions = "Actions" back = "തിരികെ" cancel = "റദ്ദാക്കുക" close = "അടയ്ക്കുക" +codeSample = "Code sample" collapse = "ചുരുക്കുക" confirm = "സ്ഥിരീകരിക്കുക" continue = "തുടരുക" copied = "പകർത്തി!" copy = "പകർത്തുക" done = "പൂർത്തിയായി" +enterValue = "Enter value" error = "പിശക്" expand = "വിപുലീകരിക്കുക" loading = "ലോഡുചെയ്യുന്നു..." next = "അടുത്തത്" +open = "Open" preview = "മുന്‍കാഴ്ച" previous = "മുൻപ്" refresh = "പുതുക്കുക" +remaining = "ബാക്കി" retry = "വീണ്ടും ശ്രമിക്കുക" save = "സംരക്ഷിക്കുക" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "തിരഞ്ഞെടുത്തത് ക്ലിയർ ചെയ്യുക" @@ -2988,6 +3199,7 @@ title = "കംപ്രഷൻ രീതി" [compress.settings] desiredSize = "ആവശ്യമായ ഫയൽ വലുപ്പം" desiredSizePlaceholder = "വലുപ്പം നൽകുക" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "കംപ്രഷൻ ഫയൽ വലിപ്പം കുറയ്ക്കാനുള്ള എളുപ്പ മാർഗമാണ്. File Size തിരഞ്ഞെടുക്കുക, ലക്ഷ്യ വലിപ്പം നൽകുക, ഞങ്ങൾ ഗുണനിലവാരം ക്രമീകരിക്കും. Quality തിരഞ്ഞെടുക്കുക, കംപ്രഷൻ ശക്തി കൈയോടെ സജ്ജമാക്കാൻ." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF ഓപ്ഷനുകൾ" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "ആദ്യം ഒരു ഉറവിട ഫോർമാറ്റ് തിരഞ്ഞെടുക്കുക" settings = "ക്രമീകരണങ്ങൾ" single = "ഒറ്റ" @@ -3264,24 +3494,6 @@ description = "വെബ്സൈറ്റ് ശരിയായി പ്രവ posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "ഉന്നത മോണിറ്ററിംഗ്" -allInOneWorkspace = "ഓൾ-ഇൻ-വൺ PDF വർക്ക്‌സ്‌പേസ് (viewer, tools & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Dedicated support & SLAs" -fasterThroughput = "10x വേഗത്തിലുള്ള throughput" -fullyPrivateFiles = "പൂർണ്ണ സ്വകാര്യ ഫയലുകൾ" -largeFileProcessing = "വലിയ ഫയൽ പ്രോസസ്സിംഗ്" -orgWideAccess = "സംഘടനാതല ആക്‌സസ് നിയന്ത്രണങ്ങൾ" -premiumAiModels = "പ്രീമിയം AI മോഡലുകൾ" -prioritySupport = "പ്രാഥമിക പിന്തുണ" -privateDocCloud = "സ്വകാര്യ ഡോക്യുമെന്റ് ക്ലൗഡ്" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "സുരക്ഷിത API ആക്‌സസ്" -standardThroughput = "സ്റ്റാൻഡേഡ് throughput" -unlimitedApiAccess = "പരിധിയില്ലാത്ത API ആക്‌സസ്" -unlimitedSeats = "പരിധിയില്ലാത്ത സീറ്റുകൾ" - [crop] autoCrop = "ശൂന്യസ്ഥലം സ്വയം ക്രോപ്പ് ചെയ്യുക" noFileSelected = "ക്രോപ്പ് ആരംഭിക്കാൻ ഒരു PDF ഫയൽ തിരഞ്ഞെടുക്കുക" @@ -3346,6 +3558,7 @@ message = "സ്വതേയുള്ള PDF ഹാൻഡ്ലർ സെറ് title = "പിശക്" [defaultApp.prompt] +dontRemind = "വീണ്ടും ഓർമ്മിപ്പിക്കരുത്" message = "PDF ഫയലുകൾ തുറക്കാനുള്ള സ്വതേയുള്ള ആപ്പായി Stirling PDF-നെ നിർണ്ണയിക്കുക." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "തിരയുക..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "തിരുത്തുക" @@ -3644,11 +3861,13 @@ shareSelected = "തിരഞ്ഞെടുത്തത് പങ്കിട sharing = "പങ്കിടൽ" showAll = "എല്ലാം കാണിക്കുക" showHistory = "ചരിത്രം കാണിക്കുക" +sortBy = "Sort files" sortByDate = "തീയതി അനുസരിച്ച് ക്രമീകരിക്കുക" sortByName = "പേര് അനുസരിച്ച് ക്രമീകരിക്കുക" sortBySize = "വലുപ്പം അനുസരിച്ച് ക്രമീകരിക്കുക" storage = "സ്റ്റോറേജ്" storageState = "സംഭരണം" +storageUsed = "Storage used" synced = "സിങ്ക് ചെയ്തു" title = "PDF ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യുക" toolChain = "പ്രയോഗിച്ച ടൂളുകൾ" @@ -3694,13 +3913,17 @@ addFiles = "ഫയലുകൾ ചേർക്കുക" addingFiles = "ഫയലുകൾ ചേർക്കുന്നു…" collapse = "സൈഡ്ബാർ ചുരുക്കുക" customizeGroups = "ഗ്രൂപ്പുകൾ ഇഷ്ടാനുസൃതമാക്കുക" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "തുടങ്ങാൻ ഫയലുകൾ തുറക്കുക" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "സൈഡ്ബാർ വിപുലീകരിക്കുക" -files = "ഫയലുകൾ" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive ക്രമീകരിച്ചിട്ടില്ല" leaveMyFiles = "എന്റെ ഫയലുകൾ വിടുക" +library = "PDF Library" myFiles = "എന്റെ ഫയലുകൾ" noFiles = "ഫയലുകളൊന്നുമില്ല" openFileManager = "ഫയൽ മാനേജർ തുറക്കുക" @@ -3708,37 +3931,36 @@ openFromComputer = "കമ്പ്യൂട്ടറിൽ നിന്ന് openSettings = "ക്രമീകരണങ്ങൾ തുറക്കുക" other = "മറ്റുള്ളവ" recent = "സമീപകാലം" -search = "തിരയുക" -searchPlaceholder = "ഫയലുകൾ തിരയുക..." viewAll = "എല്ലാ {{count}} ഫയലുകളും കാണുക" [fileSidebar.fileItem] closeViewer = "വ്യൂവർ അടയ്ക്കുക" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "വ്യൂവറിൽ തുറക്കുക" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "ചേർക്കുക" -addLabel = "ഒരു ലേബൽ ചേർക്കുക…" -categoryIconAria = "{{name}}-ക്കായി ഒരു ഐക്കൺ തിരഞ്ഞെടുക്കുക" -createCategory = "വിഭാഗം സൃഷ്ടിക്കുക" -delete = "വിഭാഗം ഇല്ലാതാക്കുക" done = "പൂർത്തിയായി" -hide = "വിഭാഗം മറയ്ക്കുക" -newCategory = "പുതിയ വിഭാഗത്തിന്റെ പേര്…" -removeLabel = "{{name}} നീക്കം ചെയ്യുക" -rename = "പേരുമാറ്റുക" reset = "ഡിഫോൾട്ടുകളിലേക്ക് പുനഃസജ്ജമാക്കുക" -search = "ലേബലുകൾ തിരയുക…" -show = "വിഭാഗം കാണിക്കുക" subtitle = "നിങ്ങളുടെ ഫയലുകൾ പാരന്റ് വിഭാഗങ്ങളായി ഗ്രൂപ്പ് ചെയ്യുക. നിലവിലുള്ളതോ പുതിയതോ ആയ ലേബലുകൾ ഒരു വിഭാഗത്തിലേക്ക് ചേർക്കുക, അതിന്റെ പേര് മാറ്റുക, അല്ലെങ്കിൽ നിങ്ങളുടെ സ്വന്തം വിഭാഗം സൃഷ്ടിക്കുക. നിങ്ങളുടെ വിഭാഗങ്ങളിലൊന്നിലും ഇല്ലാത്ത ഫയലുകൾ “മറ്റുള്ളവ” എന്നതിന് കീഴിൽ കാണിക്കും." title = "സൈഡ്‌ബാർ വിഭാഗങ്ങൾ" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "വർക്ക്‌സ്‌പേസിലേക്ക് ചേർക്കുക" addToWorkspaceCount = "{{count}} എണ്ണം വർക്ക്‌സ്‌പേസിലേക്ക് ചേർക്കുക" @@ -3749,7 +3971,6 @@ backToMyFiles = "എന്റെ ഫയലുകളിലേക്ക് തി breadcrumbs = "ഫോൾഡർ പാത" cancel = "റദ്ദാക്കുക" classification = "വർഗ്ഗീകരണം" -clearSearch = "തിരയൽ മായ്ക്കുക" clearSelection = "തിരഞ്ഞെടുപ്പ് നീക്കുക" closeDetails = "വിശദാംശങ്ങൾ അടയ്ക്കുക" create = "സൃഷ്ടിക്കുക" @@ -3781,6 +4002,7 @@ downloadAll = "എല്ലാം ഡൗൺലോഡ് ചെയ്യുക" downloadVersion = "ഈ വേർഷൻ ഡൗൺലോഡ് ചെയ്യുക" dropOverlay = "അപ്‌ലോഡ് ചെയ്യാൻ ഫയലുകൾ ഇടുക" dropOverlaySub = "ഫയലുകൾ ആദ്യം Local-ലാണ് തുടങ്ങുന്നത്. ഫോൾഡറിലാക്കാൻ 'Move to' അല്ലെങ്കിൽ 'Save to cloud' ഉപയോഗിക്കുക." +duplicate = "Duplicate" file = "ഫയൽ" fileInfo = "File info" fileMenu = "ഫയൽ പ്രവർത്തനങ്ങൾ" @@ -3808,13 +4030,12 @@ refresh = "സെർവറിൽ നിന്ന് റിഫ്രഷ് ചെ remove = "ഇല്ലാതാക്കുക" removeVersion = "ഈ വേർഷൻ നീക്കുക" rename = "പേര് മാറ്റുക" +renamed = "പേരുമാറ്റി" renameFolder = "ഫോൾഡറിന്റെ പേര് മാറ്റുക" resizeFolderTree = "ഫോൾഡർ ട്രിയുടെ വലുപ്പം മാറ്റുക (അറോ കീകൾ, വലുതിന് Shift; ഓട്ടോ-ഫിറ്റ് ചെയ്യാൻ ഡബിൾ-ക്ലിക്ക്)" save = "സംരക്ഷിക്കുക" saveToServer = "സെർവറിലേക്ക് സംരക്ഷിക്കുക" saveToServerDisabledHint = "ഈ സെർവറിൽ സെർവറിലേക്ക് സംരക്ഷിക്കൽ പ്രവർത്തനക്ഷമമാക്കിയിട്ടില്ല. അത് പ്രവർത്തനക്ഷമമാക്കാൻ അഡ്മിനെ ബന്ധപ്പെടുക." -search = "തിരയുക" -searchPlaceholder = "ഈ ഫോൾഡറിലും ഉപഫോൾഡറുകളിലും തിരയുക" selectAll = "എല്ലാം തിരഞ്ഞെടുക്കുക" selectAllHint = "എല്ലാം തിരഞ്ഞെടുക്കാൻ ക്ലിക്ക് ചെയ്യുക. ടിപ്പ്: ഓരോന്നായി ചേർക്കാൻ Ctrl (അല്ലെങ്കിൽ Cmd) അമർത്തി പിടിക്കുക, പരിധി തിരഞ്ഞെടുക്കാൻ Shift." selectedCount = "{{count}} തെരഞ്ഞെടുത്തു" @@ -3874,6 +4095,10 @@ title = "ഇനിയും ക്ലൗഡ് ഫയലുകളില്ല" hint = "അപ്‌ലോഡ് ചെയ്യാതെ സംരക്ഷിച്ച ഫയലുകൾ ഇവിടെ തന്നെ. ചേർക്കാൻ ഒരു ഫയൽ ഇടുക." title = "ലോകൽ-മാത്രമായ ഫയലുകളില്ല" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "നിങ്ങൾ തുറക്കുന്നോ എഡിറ്റ് ചെയ്യുന്നോ ചെയ്യുന്ന ഫയലുകൾ ഇവിടെ പ്രത്യക്ഷപ്പെടും." title = "ഇനിയും ഒന്നും മാറ്റിയിട്ടില്ല" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "ഫോൾഡർ ഇല്ലാതാക്കാൻ കഴിഞ്ഞില്ല." deleteFolderFailedDetail = "ഫോൾഡർ ഇല്ലാതാക്കാൻ കഴിഞ്ഞില്ല: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "ഫോൾഡർ രൂപഭാവം അപ്‌ഡേറ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല." folderAppearanceFailedDetail = "ഫോൾഡർ രൂപഭാവം അപ്‌ഡേറ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല: {{message}}" moveFilesFailed = "ഫയലുകൾ മാറ്റാൻ കഴിഞ്ഞില്ല." @@ -3910,9 +4137,7 @@ count = "ഫയലുകൾ" folder = "ഫോൾഡർ" labels = "ലേബലുകൾ" modified = "മാറ്റിയത്" -name = "പേര്" size = "വലുപ്പം" -toolHistoryAtVersion = "കൂട്ടിച്ചേർന്ന ടൂൾ ചെയിൻ" totalSize = "ആകെ വലുപ്പം" type = "തരം" versionHistory = "വേർഷൻ യാത്ര" @@ -3945,6 +4170,11 @@ localHint = "ഈ ബ്രൗസറിൽ മാത്രം സംഭരിച shared = "പങ്കിട്ടത്" sharedHint = "ലിങ്ക് വഴി നിങ്ങളോടൊപ്പം പങ്കിട്ടത്" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "പഴയത് ആദ്യം" modifiedDesc = "പുതിയത് ആദ്യം" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "സ്റ്റോറേജിൽ ഫയലുകളി open = "തുറക്കുക" openFile = "ഫയൽ തുറക്കുക" openFiles = "ഫയലുകൾ തുറക്കുക" +selectFile = "Select {{name}}" selectFromStorage = "സ്റ്റോറേജിൽ നിന്ന് തിരഞ്ഞെടുക്കുക" upload = "അപ്‌ലോഡ്" uploadFile = "ഫയൽ അപ്‌ലോഡ് ചെയ്യുക" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "എല്ലാം സേവ് ചെയ്തു" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV extract ചെയ്യാനായില്ല" +extractXlsxError = "XLSX extract ചെയ്യാനായില്ല" +filled = "filled" flattenAfterFilling = "പൂരിപ്പിച്ചതിന് ശേഷം ഫ്ലാറ്റൻ ചെയ്യുക" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "ആവ." +requiredFieldsError = "ആവശ്യമായ എല്ലാ fields-ഉം പൂരിപ്പിക്കുക" rescanFields = "ഫീൽഡുകൾ വീണ്ടും സ്കാൻ ചെയ്യുക" rescanFormFields = "ഫോം ഫീൽഡുകൾ വീണ്ടും സ്കാൻ ചെയ്യുക" save = "സംരക്ഷിക്കുക" +saveShortcut = "സേവ് ചെയ്യാൻ Ctrl+S" +unsavedChanges = "സേവ് ചെയ്യാത്ത മാറ്റങ്ങൾ" [formFill.sidebar] close = "സൈഡ്ബാർ അടയ്ക്കുക" @@ -4309,6 +4552,11 @@ desc = "കണ്ടെത്തിയ ഹെഡറിനെ അടിസ്ഥ tags = "ഓട്ടോ-ഡിറ്റക്ട്,ഹെഡർ-അടിസ്ഥാനത്തിൽ,സംഘടിപ്പിക്കുക,പുനർലേബൽ" title = "PDF ഓട്ടോ റീനെയിം" +[home.autoRotate] +desc = "ഓരോ page-ന്റെയും orientation കണ്ടെത്തി സ്വയമേവ നേരെയാക്കി തിരിക്കുക." +tags = "സ്വയമേവ തിരിക്കുക,orientation ശരിയാക്കുക,തലകീഴായി,വശത്തേക്ക്,നേരെയാക്കുക,orient,orientation കണ്ടെത്തുക,rotation ശരിയാക്കുക,auto orient,scanned,OSD" +title = "Auto Rotate" + [home.autoSizeSplitPDF] tags = "ഓട്ടോ,സ്പ്ലിറ്റ്,വലുപ്പം" title = "സൈസ്/കൗണ്ട് ഓട്ടോ സ്പ്ലിറ്റ്" @@ -4608,15 +4856,12 @@ hint = "PDF-ലിടാൻ PNG, JPG, SVG, അല്ലെങ്കിൽ മറ label = "ഇമേജ് അപ്‌ലോഡ് ചെയ്യുക" placeholder = "ഇമേജ് ഫയൽ തിരഞ്ഞെടുക്കുക" -[imageToPdf] -tags = "പരിവർത്തനം,img,jpg,ചിത്രം,ഫോട്ടോ" - [imageToPDF] header = "ചിത്രം PDF-ലേക്ക്" title = "ചിത്രം PDF-ലേക്ക്" -[infoBanner] -dismiss = "അടയ്ക്കുക" +[imageToPdf] +tags = "പരിവർത്തനം,img,jpg,ചിത്രം,ഫോട്ടോ" [invite] acceptError = "അക്കൗണ്ട് സൃഷ്ടിക്കൽ പരാജയപ്പെട്ടു" @@ -4647,11 +4892,9 @@ welcomeTitle = "നിങ്ങൾ ക്ഷണിക്കപ്പെട്ട [landing] addFiles = "ഫയലുകൾ ചേർക്കുക" -heroSubtitle = "തുടങ്ങാൻ ഒരു PDF വലിച്ചിടുകയോ നിലവിലുള്ളത് ചേർക്കുകയോ ചെയ്യുക." mobileUpload = "മൊബൈൽ മുതൽ അപ്ലോഡ് ചെയ്യുക" openFromComputer = "കമ്പ്യൂട്ടറിൽ നിന്ന് തുറക്കുക" uploadFromComputer = "കമ്പ്യൂട്ടറിൽ നിന്ന് അപ്ലോഡ് ചെയ്യുക" -workbenchEmptyStateHero = "ഒരു PDF എവിടെയും ഇടുക" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "യൂസർനെയിം നൽകുക" failedToSignIn = "{{provider}} ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യൽ പരാജയപ്പെട്ടു: {{message}}" forgotPassword = "നിങ്ങളുടെ പാസ്‌വേഡ് മറന്നോ?" loggingIn = "ലോഗിൻ ചെയ്യുന്നു..." -logIn = "ലോഗിൻ" login = "ലോഗിൻ" magicLinkSent = "മാജിക് ലിങ്ക് {{email}}-ലേക്ക് അയച്ചു! നിങ്ങളുടെ ഇമെയിൽ പരിശോധിച്ച് സൈൻ ഇൻ ചെയ്യാൻ ലിങ്ക് ക്ലിക്ക് ചെയ്യുക." mfaCode = "സ്ഥിരീകരണ കോഡ്" @@ -4715,26 +4957,10 @@ signInWith = "ഇത് ഉപയോഗിച്ച് സൈൻ ഇൻ ചെ title = "സൈൻ ഇൻ ചെയ്യുക" unexpectedError = "പ്രതീക്ഷിക്കാത്ത പിശക്: {{message}}" updatePassword = "പാസ്‌വേഡ് അപ്‌ഡേറ്റ് ചെയ്യുക" -useEmailInstead = "ഇമെയിൽ ഉപയോഗിച്ച് ലോഗിൻ" useMagicLink = "പകരം മാജിക് ലിങ്ക് ഉപയോഗിക്കുക" username = "യൂസർനെയിം" youAreLoggedIn = "നിങ്ങൾ ലോഗിൻ ചെയ്തിരിക്കുന്നു!" -[login.slides.edit] -alt = "PDF-കൾ എഡിറ്റ് ചെയ്യുക" -subtitle = "PDF-കൾ redact, sign, read, manipulate ചെയ്യാൻ സഹായിക്കുന്ന പന്ത്രണ്ടിലധികം tools ഉള്ളതിനാൽ, നിങ്ങൾ അന്വേഷിക്കുന്നത് തീർച്ചയായും കണ്ടെത്തും." -title = "ആവശ്യമായ വിവരങ്ങൾ പ്രദർശിപ്പിക്കാനും/സുരക്ഷിതമാക്കാനും PDF-കൾ എഡിറ്റ് ചെയ്യുക" - -[login.slides.overview] -alt = "Stirling PDF അവലോകനം" -subtitle = "PDF-കൾ convert, sign, redact ചെയ്ത് മാനേജുചെയ്യാൻ കഴിയുന്ന, സ്വകാര്യത-ആദ്യം cloud suite, കൂടെ 50+ ശക്തമായ tools." -title = "എല്ലാ PDF ആവശ്യങ്ങൾക്കും നിങ്ങളുടെ ഒറ്റ-സ്റ്റോപ്പ് ഷോപ്പ്." - -[login.slides.secure] -alt = "PDF-കൾ സുരക്ഷിതമാക്കുക" -subtitle = "പാസ്‌വേഡുകൾ ചേർക്കുക, ഉള്ളടക്കം redact ചെയ്യുക, സർട്ടിഫിക്കറ്റുകൾ എളുപ്പത്തിൽ മാനേജുചെയ്യുക." -title = "നിങ്ങളുടെ PDF-കളിലെ സെൻസിറ്റീവ് വിവരങ്ങളെ സംരക്ഷിക്കുക" - [margin] large = "വലുത്" medium = "ഇടത്തരം" @@ -4818,6 +5044,34 @@ uploadSuccess = "അപ്‌ലോഡ് വിജയകരം!" uploadSuccessMessage = "നിങ്ങളുടെ ചിത്രങ്ങൾ മാറ്റി." validating = "സെഷൻ ശരിവെയ്ക്കുന്നു..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "സ്കാൻ ചെയ്ത് ചിത്രങ്ങൾ അപ്‌ലോഡ് ചെയ്യുക. ചിത്രങ്ങൾ സ്വയമേവ PDF-യായി മാറും." descriptionNoConvert = "നിങ്ങളുടെ മൊബൈൽ ഉപകരണത്തിൽ നിന്ന് ചിത്രങ്ങൾ അപ്‌ലോഡ് ചെയ്യാൻ സ്കാൻ ചെയ്യുക." @@ -4836,8 +5090,13 @@ title = "മൊബൈലിൽ നിന്ന് അപ്‌ലോഡ് ച tags = "മൾട്ടി ടൂൾ,മൾട്ടി ഓപ്പറേഷൻ,യുഐ,ക്ലിക്ക് ഡ്രാഗ്,ഫ്രണ്ട് എൻഡ്,ക്ലയിന്റ് സൈഡ്,ഇന്ററാക്ടീവ്,അനങ്ങാത്ത,നീക്കുക,ഇല്ലാതാക്കുക,കൈമാറുക,വിഭജിക്കുക" title = "PDF മൾട്ടി ടൂൾ" -[navbar] -search = "തിരയുക" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "ഓതന്റിക്കേഷൻ വിജയിച്ചില്ല. ഈ വിൻഡോ അടച്ച് വീണ്ടും ശ്രമിക്കാം." @@ -4966,6 +5225,7 @@ activeFiles = "ആക്ടീവ് ഫയലുകൾ കാ allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "അടയ്ക്കുക" cropSettings = "ഇപ്പോൾ നമുക്ക് ക്രോപ്പ് ചെയ്യാൻ വേണ്ട ഫയൽ തെരഞ്ഞെടുത്തതിനാൽ, PDF ക്രോപ്പ് ചെയ്യേണ്ട പ്രദേശം തീരുമാനിക്കാൻ ക്രോപ്പ് ടൂൾ ക്രമീകരിക്കാം." +dialogLabel = "Onboarding" fileCheckbox = "ഫയലുകളിൽ ഒന്നിൽ ക്ലിക്കുചെയ്താൽ അത് പ്രോസസിംഗിനായി തിരഞ്ഞെടുക്കപ്പെടും. ബാച്ച് പ്രവർത്തനങ്ങൾക്ക് നിങ്ങൾക്ക് ഒന്നിലധികം ഫയലുകൾ തിരഞ്ഞെടുക്കാം." fileReplacement = "മാറ്റം വരുത്തിയ ഫയൽ വർക്ക്ബെഞ്ചിലെ യഥാർത്ഥ ഫയലിനെ സ്വമേധയായി മാറ്റിസ്ഥാപിക്കും, ഇതിലൂടെ കൂടുതൽ ടൂളുകൾ പ്രവർത്തിപ്പിക്കുന്നത് എളുപ്പമാകും." filesButton = "ക്വിക് ആക്സസ് ബാറിലെ ഫയലുകൾ ബട്ടൺ ഉപയോഗിച്ച് നിങ്ങൾക്ക് ടൂളുകളിൽ ഉപയോഗിക്കാൻ PDFകൾ അപ്‌ലോഡ് ചെയ്യാം." @@ -4976,6 +5236,7 @@ pinButton = "ടൂളുകൾ പ്രവർത്തിപ്പിച്ച results = "ടൂൾ പ്രവർത്തിച്ചു കഴിഞ്ഞാൽ, റിവ്യൂ ഘട്ടത്തിൽ ഈ പാനലിൽ ഫലങ്ങളുടെ പ്രിവ്യൂ കാണിക്കുകയും പ്രവർത്തനം റദ്ദാക്കുകയോ ഫയൽ ഡൗൺലോഡ് ചെയ്യുകയോ ചെയ്യാനുമാകും." runButton = "ടൂൾ ക്രമീകരിച്ച ശേഷം, തെരഞ്ഞെടുത്ത എല്ലാ PDFകളിലും ടൂൾ പ്രവർത്തിപ്പിക്കാൻ ഈ ബട്ടൺ ഉപയോഗിക്കാം." selectCropTool = "ക്രോപ്പ് ടൂൾ എങ്ങനെ ഉപയോഗിക്കാമെന്ന് കാണിക്കാൻ നമുക്ക് അത് തിരഞ്ഞെടുക്കാം." +stepOf = "Step {{current}} of {{total}}" toolInterface = "ഇതാണ് ക്രോപ്പ് ടൂളിന്റെ ഇന്റർഫേസ്. ഇപ്പോഴും പ്രവർത്തിപ്പിക്കാൻ PDF ഫയലുകൾ ചേർത്തിട്ടില്ലാത്തതിനാൽ ഇവിടെ അധികമൊന്നുമില്ലെന്ന് നിങ്ങൾ കാണുന്നു." workbench = "ഇതാണ് വർക്ക്ബെഞ്ച് - നിങ്ങളുടെ PDFകൾ കാണുകയും തിരുത്തുകയും ചെയ്യുന്ന പ്രധാന പ്രദേശം." wrapUp = "എല്ലാം സജ്ജമാണ്! ആപ്പിന്റെ പ്രധാന പ്രദേശങ്ങളും അവ എങ്ങനെ ഉപയോഗിക്കാമെന്നും നിങ്ങൾ പഠിച്ചു. ഈ ടൂർ വീണ്ടും കാണാൻ ഏതുസമയം വേണമെങ്കിലും സഹായം ബട്ടൺ അമർത്തുക." @@ -4988,6 +5249,26 @@ showMeAround = "ടൂർ കാണിക്കുക" skipForNow = "ഇപ്പോൾ ഒഴിവാക്കുക" skipTheTour = "ടൂർ ഒഴിവാക്കുക" +[onboarding.checklist] +dismiss = "ഒഴിവാക്കുക" +title = "Stirling PDF സജ്ജീകരിക്കുക" + +[onboarding.checklist.downloadDesktop] +description = "നിങ്ങളുടെ machine-ൽ Stirling native ആയി run ചെയ്യുക" +title = "Desktop-നായി Stirling download ചെയ്യുക" + +[onboarding.checklist.inviteTeam] +description = "നിങ്ങളുടെ team-നൊപ്പം സഹകരിക്കുക" +title = "team members-നെ ക്ഷണിക്കുക" + +[onboarding.checklist.shareAnalytics] +description = "Stirling മെച്ചപ്പെടുത്താൻ സഹായിക്കുക" +title = "anonymous usage data പങ്കിടുക" + +[onboarding.checklist.takeTour] +description = "ഒരു quick walkthrough-ൽ Stirling എങ്ങനെ പ്രവർത്തിക്കുന്നുവെന്ന് കാണുക" +title = "tour കാണുക" + [onboarding.desktopInstall] body = "Stirling ഡെസ്ക്ടോപ്പ് ആപ്പായി പ്രവർത്തിക്കുമ്പോൾ ഏറ്റവും മികച്ചതാണ്. നിങ്ങൾക്ക് ഓഫ്‌ലൈൻ ഉപയോഗിക്കാം, ഡോക്യുമെന്റുകൾ വേഗത്തിൽ ആക്‌സസ് ചെയ്യാം, നിങ്ങളുടെ കമ്പ്യൂട്ടറിൽ ലൊക്കലായി എഡിറ്റ് ചെയ്യാനും കഴിയും." selectOs = "ഓപ്പറേറ്റിംഗ് സിസ്റ്റം തിരഞ്ഞെടുക്കുക" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}}-ക്കായി ഡൗൺലോഡ്" [onboarding.mfa] authenticationCode = "അധികൃതീകരണ കോഡ്" +description = "ഒരു authenticator app link ചെയ്ത് നിങ്ങളുടെ account സുരക്ഷിതമാക്കുക. QR code scan ചെയ്യുക അല്ലെങ്കിൽ setup key നൽകുക, തുടർന്ന് പൂർത്തിയാക്കാൻ 6-digit code സ്ഥിരീകരിക്കുക." +enable = "MFA പ്രവർത്തനക്ഷമമാക്കുക" +enableError = "two-factor authentication പ്രവർത്തനക്ഷമമാക്കാനായില്ല. code പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക." +enterCode = "നിങ്ങളുടെ app-ൽ നിന്നുള്ള 6-digit code നൽകുക." +enterCodeError = "തുടരാൻ authentication code നൽകുക." +logout = "Logout" +openAuthenticator = "Google Authenticator, Authy, അല്ലെങ്കിൽ 1Password തുറക്കുക." qrCodeLoading = "നിങ്ങളുടെ QR കോഡ് സൃഷ്ടിക്കുന്നു…" +regenerateQrCode = "QR code വീണ്ടും സൃഷ്ടിക്കുക" +scanQrCode = "QR code scan ചെയ്യുക അല്ലെങ്കിൽ താഴെയുള്ള setup key നൽകുക." +setupError = "two-factor setup ആരംഭിക്കാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." +setupKey = "Setup key (manual entry)" +stepByStep = "Step-by-step" +success = "MFA പ്രവർത്തനക്ഷമമാക്കി. നിങ്ങൾക്ക് ഇപ്പോൾ തുടരാം." +title = "Multi-Factor Authentication Setup" [onboarding.planOverview] adminBodyLoginDisabled = "ലോഗിൻ മോഡ് സജീവമാക്കിയാൽ, നിങ്ങൾക്ക് ഉപയോക്താക്കളെയും സെറ്റിങ്ങുകളെയും മാനേജ് ചെയ്യാനും സെർവർ ഹെൽത്ത് മോണിറ്റർ ചെയ്യാനും കഴിയും. നിങ്ങളുടെ സെർവറിലെ ആദ്യ {{freeTierLimit}} പേർക്ക് Stirling സൗജന്യമായി ഉപയോഗിക്കാം." @@ -5005,6 +5300,11 @@ adminTitle = "അഡ്മിൻ അവലോകനം" userBody = "ടീംമേറ്റ്‌കളെ ക്ഷണിക്കുക, റോളുകൾ നിയോഗിക്കുക, നിങ്ങളുടെ ഡോക്യുമെന്റുകൾ ഒരു സുരക്ഷിത വർക്ക്സ്പേസിൽ ക്രമീകരിച്ച് സൂക്ഷിക്കുക. സോളോ ഉപയോഗത്തേക്കാൾ വളരാൻ തയ്യാറാകുമ്പോൾ ഏത് സമയത്തും ലോഗിൻ മോഡ് പ്രാപ്തമാക്കുക." userTitle = "പ്ലാൻ അവലോകനം" +[onboarding.processorIntro] +body = "Stirling ഇപ്പോൾ Policies പ്രവർത്തിപ്പിക്കുന്നു — ഓരോ document എത്തുമ്പോഴും classify, secure, process ചെയ്യുന്ന automated rules. അവ setup ചെയ്ത് runs Processor-ൽ monitor ചെയ്യുക." +cta = "Processor പരിശോധിക്കുക" +title = "Stirling Processor പരിശോധിക്കുക" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "മുൻകൂർ പണമടച്ച വർഷം" + +[payg.prepaid.banner] +exhausted = "നിങ്ങളുടെ prepaid capacity ഉപയോഗിച്ചു തീർന്നു; metered billing വീണ്ടും തുടങ്ങി. discount നിലനിർത്താൻ top up ചെയ്യുക." +expiring_one = "നിങ്ങളുടെ prepaid year {{count}} ദിവസത്തിൽ അവസാനിക്കുന്നു. ഉപയോഗിക്കാത്ത capacity കാലഹരണപ്പെടും — പുതിയ വർഷം ആരംഭിക്കാൻ top up ചെയ്യുക." +expiring_other = "നിങ്ങളുടെ prepaid year {{count}} ദിവസങ്ങളിൽ അവസാനിക്കുന്നു. ഉപയോഗിക്കാത്ത capacity കാലഹരണപ്പെടും — പുതിയ വർഷം ആരംഭിക്കാൻ top up ചെയ്യുക." +low = "നിങ്ങളുടെ prepaid capacity-യുടെ {{pct}}% മാത്രം ബാക്കി. processing തടസ്സമില്ലാതെ തുടരാൻ എപ്പോൾ വേണമെങ്കിലും top up ചെയ്യുക." + +[payg.prepaid.card] +subtitle = "metered billing-നും spend cap-നും പുറത്തായി ആദ്യം ഉപയോഗിക്കുന്നു." +title = "Prepaid capacity" + +[payg.prepaid.meter] +capSuffix = "{{total}} prepaid credits-ൽ" +expires = "{{date}}-ന് കാലഹരണപ്പെടും" + +[payg.prepaid.state] +exhausted = "ഉപയോഗിച്ചു തീർന്നു" +healthy = "ധാരാളം ബാക്കി" +low = "കുറഞ്ഞുവരുന്നു" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF PDF/A-ലേക്ക്" tags = "ആർക്കൈവ്,ദീർഘകാല,മാനദണ്ഡം,പരിവർത്തനം,സംഭരണം,സംരക്ഷണം" title = "PDF PDF/A-ലേക്ക്" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "പ്രിന്റ്,സ്റ്റാൻഡേർഡ്,കൺവർഷൻ,പ്രൊഡക്ഷൻ,പ്രീപ്രസ്,ആർക്കൈവ്" title = "PDF-നെ PDF/X-ലേക്ക്" @@ -5822,6 +6151,7 @@ small = "500 ക്രെഡിറ്റുകൾ" xsmall = "100 ക്രെഡിറ്റുകൾ" [plan.availablePlans] +currency = "Billing currency" subtitle = "നിങ്ങളുടെ ആവശ്യങ്ങൾക്ക് അനുയോജ്യമായ പ്ലാൻ തിരഞ്ഞെടുക്കുക" title = "ലഭ്യമായ പ്ലാനുകൾ" @@ -5937,49 +6267,12 @@ keyDescription = "ഇമെയിലിൽ നിന്ന് ലൈസൻസ് success = "ലൈസൻസ് സജീവമായി!" successMessage = "നിങ്ങളുടെ ലൈസൻസ് വിജയകരമായി സജീവപ്പെടുത്തി. നിങ്ങൾക്ക് ഇപ്പോൾ ഈ ജാലകം അടയ്ക്കാം." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "പ്രാബല്യപ്പെടുത്തി" -enforcing = "പ്രാബല്യപ്പെടുത്തുന്നു..." -failed = "പ്രാബല്യപ്പെടുത്തൽ പരാജയപ്പെട്ടു" outputsUnavailable = "നയത്തിന്റെ ഔട്ട്‌പുട്ടുകൾ ഇനി ഡൗൺലോഡ് ചെയ്യാൻ ലഭ്യമല്ല." partialOutputsUnavailable = "ചില നയ ഔട്ട്‌പുട്ടുകൾ ഇനി ഡൗൺലോഡ് ചെയ്യാൻ ലഭ്യമല്ല." -retrying = "തിരക്കിലാണ്, വീണ്ടും ശ്രമിക്കുന്നു..." runNotFound = "പ്രാബല്യപ്പെടുത്തൽ റൺ ഇനി കണ്ടെത്താനായില്ല." -step = "ഘട്ടം {{current}}/{{total}}" timedOut = "റൺ പൂർത്തിയാകുന്നതിന് മുമ്പ് പ്രാബല്യപ്പെടുത്തൽ ടൈംഔട്ട് ആയി." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "അടയ്ക്കുക" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "പ്രാബല്യപ്പെടുത്തുന്നു {{names}}" applyingProgress = "പ്രാബല്യപ്പെടുത്തുന്നു {{names}} ({{total}} ൽ {{done}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} ഫയലുകളിൽ {{failures}} എണ്ണം failureTitle = "പൂർണ്ണ പ്രാബല്യപ്പെടുത്തൽ ഇല്ലാതെ എക്സ്പോർട്ട് ചെയ്തു" printPolicyAppliedBody = "ഒരു നയം പാലിക്കുന്നതിനായി ഈ PDF പുതുക്കി. മാറ്റങ്ങൾ പരിശോധിച്ച് വീണ്ടും പ്രിന്റ് ചെയ്യുക." printPolicyAppliedTitle = "പ്രിന്റ് ചെയ്യുന്നതിന് മുമ്പ് നയം പ്രാബല്യപ്പെടുത്തി" -queued = "+{{count}} ക്യൂവിൽ" successTitle = "{{names}} പ്രാബല്യപ്പെടുത്തി" summaryMore = "{{first}}, {{second}} കൂടാതെ {{more}} എണ്ണം കൂടി" summaryTwo = "{{first}} കൂടാതെ {{second}}" -[policies.enforcement.triggerVerb] -convert = "പരിവർത്തനം ചെയ്യുന്നതിന് മുമ്പ് പ്രാബല്യപ്പെടുത്തുന്നു" -default = "പ്രാബല്യപ്പെടുത്തുന്നു" -export = "എക്സ്പോർട്ടിന് മുമ്പ് പ്രാബല്യപ്പെടുത്തുന്നു" -input = "ഇംപോർട്ട് ചെയ്യുമ്പോൾ പ്രാബല്യപ്പെടുത്തുന്നു" -print = "പ്രിന്റ് ചെയ്യുന്നതിനുമുമ്പ് പ്രാബല്യപ്പെടുത്തുന്നു" - -[policies.field] -accessLog = "ആക്സസ് ലോഗ്" -archiveAfter = "ഇതിനുശേഷം ആർക്കൈവ് ചെയ്യുക" -auditTrail = "ഓഡിറ്റ് ട്രെയിൽ" -belowThreshold = "പരിധിക്കു താഴെ" -destination = "ലക്ഷ്യസ്ഥാനം" -frameworks = "ഫ്രെയിംവർക്കുകൾ" -immutableHold = "മാറ്റാനാകാത്ത ഹോൾഡ്" -keepFor = "സൂക്ഷിക്കേണ്ട കാലം" -minConfidence = "കുറഞ്ഞ ആത്മവിശ്വാസം" -notify = "റൂട്ടിൽ അറിയിക്കുക" -onViolation = "അനുസരണക്കേട് ഉണ്ടായാൽ" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "ചേർക്കുക" -addPlaceholder = "ഒരു ലേബൽ ചേർക്കുക…" -customNote = "നിങ്ങളുടെ ടീമിനായി ഇഷ്ടാനുസൃതമാക്കി." -defaultNote = "നിങ്ങളുടെ ടീമുമായി പങ്കിടുന്ന ബിൽറ്റ്-ഇൻ ഡിഫോൾട്ട് ഉപയോഗിക്കുന്നു." -duplicate = "\"{{name}}\" ഇതിനകം നിലവിലുണ്ട്." -edit = "ലേബലുകൾ തിരുത്തുക" -empty = "ഇനിയും ലേബലുകളില്ല." -export = "JSON എക്സ്പോർട്ട് ചെയ്യുക" -iconAria = "{{name}}-ക്കായി ഒരു ഐക്കൺ തിരഞ്ഞെടുക്കുക" -import = "JSON ഇമ്പോർട്ട് ചെയ്യുക" -importError = "ആ ഫയൽ ഇമ്പോർട്ട് ചെയ്യാനായില്ല." -managedNote = "ടീം ലേബലുകൾ നിങ്ങളുടെ ടീം ലീഡർ മാനേജ് ചെയ്യുന്നു." -modalSubtitle = "നിങ്ങളുടെ മുഴുവൻ ടീമുമായി പങ്കിട്ടത്. ഓരോ രേഖയ്ക്കും യോജിച്ച ലേബലുകൾ classifier തിരഞ്ഞെടുക്കും." -modalTitle = "വർഗ്ഗീകരണ ലേബലുകൾ" -pickIcon = "{{label}} ഐക്കൺ ഉപയോഗിക്കുക" -removeAria = "{{name}} നീക്കം ചെയ്യുക" -resetToDefault = "ഡിഫോൾട്ടിലേക്ക് പുനഃസജ്ജമാക്കുക" -saveForTeam = "ടീമിനായി സംരക്ഷിക്കുക" -saving = "സംരക്ഷിക്കുന്നു…" -sectionLabel = "വർഗ്ഗീകരണ ലേബലുകൾ" -startFromScratch = "ആരംഭത്തിൽ നിന്ന് തുടങ്ങുക" -teamCount = "ടീം ലേബലുകൾ" -tooLong = "ലേബലുകൾ പരമാവധി {{max}} അക്ഷരങ്ങൾ മാത്രമേ ആകാവൂ." -ungrouped = "ഗ്രൂപ്പ് ചെയ്യാത്തത്" -view = "ലേബലുകൾ കാണുക" +categoryCount = "categories" +hideCategory = "category മറയ്ക്കുക" +labelCount = "labels" +sharedNote = "ഈ labels built in ആണ്, നിങ്ങളുടെ മുഴുവൻ team-ലും പങ്കിടുന്നു." +showCategory = "category കാണിക്കുക" [policies.pii] account = "അക്കൗണ്ട് നമ്പറുകൾ (ലേബൽ ചെയ്തവ)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US റൂട്ടിങ് നമ്പറുകൾ (ABA)" ssn = "സോഷ്യൽ സെക്യൂരിറ്റി നമ്പറുകൾ" -[policies.settings] -noneExport = "ഇപ്പോൾ എക്സ്പോർട്ടിൽ നയങ്ങളൊന്നും പ്രവർത്തിക്കുന്നില്ല." -noneUpload = "ഇപ്പോൾ upload-ൽ നയങ്ങളൊന്നും പ്രവർത്തിക്കുന്നില്ല." -onExport = "എക്സ്പോർട്ടിൽ" -onUpload = "upload-ൽ" -reorderHandle = "ക്രമം മാറ്റാൻ വലിച്ചിടുക" -runOrderDesc = "ഒരേ trigger-ൽ ഒന്നിലധികം നയങ്ങൾ പ്രവർത്തിക്കുമ്പോൾ, അവ ഈ ക്രമത്തിൽ പ്രവർത്തിക്കും — ഓരോന്നും മുമ്പത്തെ നയത്തിന്റെ ഔട്ട്പുട്ടിൽ. ക്രമം മാറ്റാൻ വലിച്ചിടുക." -title = "നയ ക്രമീകരണങ്ങൾ" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "നയ ഓപ്ഷനുകൾ" -policySettings = "നയ ക്രമീകരണങ്ങൾ" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "പരാജയപ്പെട്ട നയങ്ങൾ വീണ്ടും ശ്രമിക്കുക ({{count}})" -rowProgress = "{{total}} ഫയലുകളിൽ {{completed}} പ്രോസസ് ചെയ്തു" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "നയം എന്താണ്?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "സോഷ്യൽ സെക്യൂരിറ്റി, കാർഡ് നമ്പറുകൾ പോലെയുള്ള സെൻസിറ്റീവ് വിവരങ്ങൾ സ്വയമേവ കണ്ടെത്തി കറുപ്പിച്ചു മറയ്ക്കും — അതിനാൽ പ്രമാണത്തിൽ അവ വായിക്കാൻ സാധിക്കില്ല." -sanitize = "ഫയലിൽ നിന്നുള്ള മറഞ്ഞിരിക്കുന്ന JavaScript നീക്കം ചെയ്യും, അതിനാൽ ആരെങ്കിലും അത് തുറക്കുമ്പോൾ ഒന്നും സ്വയമേവ പ്രവർത്തിക്കില്ല." -watermark = "ഓരോ പേജിലും ദൃശ്യമായ ഒരു മുദ്ര (ഉദാ: \"Confidential\") പതിപ്പിക്കും." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "അടയ്ക്കുക" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} നടപ്പിലാക്കുന്നു..." badgeRan = "ഈ ഫയലിൽ {{name}} നയം പ്രവർത്തിച്ചു" +badgeRunning = "{{name}} പ്രവർത്തിക്കുന്നു..." blockingAction = "നയം നടപ്പിലാക്കുന്നതിനിടെ {{action}} തടഞ്ഞു, ദയവായി കാത്തിരിക്കുക..." dismiss = "ഓവർലേ നിരസിക്കുക" enforcingTitle = "നയം നടപ്പിലാക്കുന്നു..." @@ -6151,6 +6316,7 @@ viewAnyway = "ഫയൽ കാണുക (നയം ഇപ്പോഴും ന [policyOption] autoRedactPhi = "PHI സ്വയമേവ റിഡാക്റ്റ് ചെയ്യുക" blockExport = "എക്സ്പോർട്ട് തടയുക" +ccpa = "CCPA" contracts = "കരാറുകൾ" documents = "രേഖകൾ" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor plan" subscribed = "Processor plan" unlinked = "ലിങ്ക് ചെയ്തിട്ടില്ല" -[portal.agentBuilder] -bootstrapFromDocument = "രേഖയിൽ നിന്ന് bootstrap ചെയ്യുക" -sectionsAriaLabel = "Agent builder വിഭാഗങ്ങൾ" -selectorAriaLabel = "Agents" -subtitle = "നിങ്ങളുടെ രേഖകൾ classify, extract, route ചെയ്യുന്ന AI agents രൂപകൽപ്പന ചെയ്യുക, പരീക്ഷിക്കുക, ship ചെയ്യുക. Scenarios നിർവചിക്കുക, tool access നിയന്ത്രിക്കുക, golden set പ്രവർത്തിപ്പിക്കുക, ഒരു version publish ചെയ്യുക." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "റദ്ദാക്കുക" -dropzoneText = "ഒരു sample document തിരഞ്ഞെടുക്കുക (PDF അല്ലെങ്കിൽ ചിത്രം)" -lead = "ഒരു sample document ഇടുക; നിങ്ങൾക്ക് refine ചെയ്യാവുന്ന scenarios-ഉം extraction schema-യും ഞങ്ങൾ നിർദ്ദേശിക്കും. നിങ്ങൾ review ചെയ്യുന്നതുവരെ ഒന്നും publish ചെയ്യില്ല." -submit = "Agent bootstrap ചെയ്യുക" -subtitle = "ഒരു representative file-ൽ നിന്ന് പുതിയ agent seed ചെയ്യുക" -title = "ഒരു രേഖയിൽ നിന്ന് bootstrap ചെയ്യുക" - -[portal.agentBuilder.empty] -description = "ഒരു sample document-ൽ നിന്ന് agent bootstrap ചെയ്ത് അതിന്റെ scenarios-ഉം extraction schema-യും seed ചെയ്യുക, തുടർന്ന് refine ചെയ്ത് publish ചെയ്യുക." -title = "ഇനിയും agents ഇല്ല" - -[portal.agentBuilder.evals] -casesPassing = "വിജയിക്കുന്ന cases" -columnCase = "Eval case" -columnLatency = "Latency" -columnResult = "ഫലം" -fail = "പരാജയം" -goldenSetPassRate = "Golden-set pass rate" -latencyMs = "{{ms}} ms" -notRun = "പ്രവർത്തിപ്പിച്ചിട്ടില്ല" -pass = "വിജയം" -passRate = "Pass rate" -runEvals = "Evals പ്രവർത്തിപ്പിക്കുക" - -[portal.agentBuilder.evals.empty] -description = "Evals നിങ്ങളുടെ scenarios-നെ repeatable golden set ആക്കുന്നു. കാലക്രമേണ pass-rate capture ചെയ്യാനും അതിന്റെ അടിസ്ഥാനത്തിൽ publishes gate ചെയ്യാനും upgrade ചെയ്യുക." -title = "ഇനിയും golden set ഇല്ല" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "golden sets മുഴുവൻ" -activeAgents = "സജീവ agents" -avgPassRate = "ശരാശരി eval pass rate" -fleetWide = "fleet-wide" -latestPublished = "ഏറ്റവും പുതുതായി publish ചെയ്തത്" -scenarios = "Scenarios" -testCases = "test cases" -totalDescription_one = "ആകെ {{count}}" -totalDescription_other = "ആകെ {{count}}" - -[portal.agentBuilder.scenarios] -add = "ചേർക്കുക" -addScenario = "Scenario ചേർക്കുക" -enable = "പ്രവർത്തനക്ഷമമാക്കുക" -expectationLabel = "പ്രതീക്ഷിക്കുന്ന പെരുമാറ്റം" -expectationPlaceholder = "Agent ചെയ്യേണ്ടത് എന്ത്" -inEval = "eval-ൽ" -mute = "Mute ചെയ്യുക" -muted = "muted" -nameLabel = "പേര്" -namePlaceholder = "ഉദാ. Compliance escalation" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarios" -tools = "Tools" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "വിശാല access" -deniedHint = "തിരഞ്ഞെടുത്ത tools തടഞ്ഞിരിക്കുന്നു. മറ്റെല്ലാം callable ആയി തുടരും." -deniedTools = "നിരസിച്ച tools" -governanceGate = "Tool governance Enterprise plan-ൽ ലഭ്യമാണ്." -restricted = "നിയന്ത്രിതം" -restrictedAccess = "നിയന്ത്രിത tool access" -restrictedDescription = "താഴെ നിങ്ങൾ നിരസിക്കുന്നവ ഒഴികെ എല്ലാ tools-ക്കും അനുമതി നൽകുക." - -[portal.agentBuilder.versions] -current = "നിലവിലെ" -historyGate = "പൂർണ്ണ version history-യും rollback-ഉം Enterprise plan-ൽ ലഭ്യമാണ്." -publish = "Publish ചെയ്യുക" -rollBack = "Roll back ചെയ്യുക" - [portal.assistant] close = "സഹായി അടയ്ക്കുക" error = "സഹായിയെ ബന്ധപ്പെടാനായില്ല." @@ -6365,9 +6451,21 @@ typing = "ടൈപ്പ് ചെയ്യുന്നു" loading = "ലോഡ് ചെയ്യുന്നു" redirectingToEditor = "editor-ലേക്ക് redirect ചെയ്യുന്നു..." +[portal.billing.activation] +maybeLater = "പിന്നീട് നോക്കാം" +title = "Processor ഓൺ ചെയ്യുക" + +[portal.billing.activation.payg] +sub = "ഒരു monthly limit സജ്ജീകരിക്കുക. നിങ്ങൾ run ചെയ്യുന്നതിന് മാത്രം പണമടയ്ക്കുക." +title = "Pay as you go" + +[portal.billing.activation.prepay] +badge = "2 മാസം സൗജന്യം" +sub = "ഒരു invoice, card അല്ലെങ്കിൽ bank transfer." +title = "ഒരു വർഷം മുൻകൂർ പണമടയ്ക്കുക" + [portal.billing.checkout] noClientSecret = "Edge function client_secret തിരികെ നൽകിയില്ല." -subtitle = "നിങ്ങളുടെ free Editor-plan grant കഴിഞ്ഞും തുടരാൻ ഒരു card ചേർക്കുക. ബാക്കി Stripe കൈകാര്യം ചെയ്യും." title = "Processor plan പ്രവർത്തനക്ഷമമാക്കുക" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "നിങ്ങളുടെ payment വിജയിച്ചു, പക close = "അടയ്ക്കുക" title = "കുറച്ച് മാത്രം ബാക്കി" +[portal.billing.checkout.cap] +amountAria = "Monthly spend limit" +back = "Back" +continue = "payment-ലേക്ക് തുടരുക" +error = "നിങ്ങളുടെ spend limit സജ്ജീകരിക്കാനായില്ല" +finePrint = "Invoices monthly post ചെയ്യും. എപ്പോൾ വേണമെങ്കിലും cancel ചെയ്യാം." +noLimit = "പരിധിയില്ല" +note = "നിങ്ങളുടെ limit കടന്ന് നിങ്ങളെ ഒരിക്കലും bill ചെയ്യില്ല. Processing വെറും pause ചെയ്യും." +perMonth = "/ മാസം" +title = "നിങ്ങളുടെ spend limit സജ്ജീകരിക്കുക" + +[portal.billing.checkout.card] +fields = "കാർഡ് നമ്പർ · MM / YY · CVC · ZIP" +label = "Card details" +note = "Card details Stripe ശേഖരിക്കുന്നു. Stirling PAN അല്ലെങ്കിൽ CVC ഒരിക്കലും store ചെയ്യുന്നില്ല." + [portal.billing.checkout.error] title = "Checkout ആരംഭിക്കാനായില്ല" @@ -6383,10 +6497,9 @@ body = "നിങ്ങളുടെ payment പൂർത്തിയായി. hint = "ദയവായി ഈ window തുറന്നുതന്നെ വയ്ക്കുക." title = "നിങ്ങളുടെ Processor plan activate ചെയ്യുന്നു..." -[portal.billing.checkout.notConfigured] -bodyAfter = "portal env-ൽ in-app checkout സജ്ജമാക്കാൻ." -bodyBefore = "സജ്ജമാക്കുക" -title = "Stripe കോൺഫിഗർ ചെയ്തിട്ടില്ല" +[portal.billing.checkout.pay] +limit = "Monthly spend limit {{amount}}/mo. limit-ൽ processing pause ചെയ്യും." +limitNoCap = "Spend limit ഇല്ല. Processing pause ചെയ്യില്ല. Invoices monthly post ചെയ്യും." [portal.billing.enterpriseUpsell] cta = "നിങ്ങളുടെ Enterprise quote നിർമ്മിക്കുക" @@ -6409,10 +6522,12 @@ currentPlan = "നിലവിലെ plan" freeForever = "എന്നേക്കും സൗജന്യം" noTeamResolved = "നിങ്ങളുടെ wallet-ൽ ഇതുവരെ team resolved ആയിട്ടില്ല — refresh ചെയ്ത് വീണ്ടും ശ്രമിക്കുക." ownerOnly = "ടീം owner-ക്ക് മാത്രമേ Processor plan ഓൺ ചെയ്യാൻ കഴിയൂ." +payInvoice = "പൂർത്തിയാക്കാൻ invoice അടയ്ക്കുക" planName = "Editor" ssoIncluded = "SSO ഉൾപ്പെടുത്തിയിരിക്കുന്നു" switchOnProcessor = "Processor ഓൺ ചെയ്യുക →" unlimitedUsers = "അപരിമിത users" +viewQuote = "quote കാണുക" [portal.billing.invoices] columnAmount = "തുക" @@ -6465,7 +6580,111 @@ segmentApiDesc = "നേരിട്ടുള്ള API requests" segmentApiLabel = "API" segmentAutomationDesc = "Automations & pipelines" segmentAutomationLabel = "Automation" +sizeMultiplier = "{{formatted}} PDFs ഒരു size multiplier ഉപയോഗിച്ചു" +summary = "{{unique}} unique · {{units}} meter units · PDF-ന് ശരാശരി {{avg}}" +summaryNoRate = "{{unique}} unique · {{units}} meter units" unit = "metered PDFs" +unitsPending_one = "linked instances-ൽ നിന്ന് {{units}} meter unit sync ചെയ്യാൻ ബാക്കി" +unitsPending_other = "linked instances-ൽ നിന്ന് {{units}} meter units sync ചെയ്യാൻ ബാക്കി" + +[portal.billing.prepaid] +capSuffix = "{{total}} prepaid credits-ൽ" +expires = "{{date}}-ന് കാലഹരണപ്പെടും" +eyebrow = "Prepaid capacity" +note = "metered billing-നും spend limit-നും പുറത്തായി ആദ്യം ഉപയോഗിക്കുന്നു." +topUp = "Top up" + +[portal.billing.prepaid.buy] +back = "Back" +calcTitle = "നിങ്ങളുടെ annual payment കണക്കാക്കുക" +cancel = "Cancel" +cancelPurchase = "purchase cancel ചെയ്യുക" +close = "Close" +continueToPayment = "payment-ലേക്ക് തുടരുക" +finalise = "Finalise" +invoiceErrorTitle = "invoice generate ചെയ്യാനായില്ല" +loadingTitle = "നിങ്ങളുടെ quote loading ചെയ്യുന്നു" +payTitle = "നിങ്ങളുടെ വർഷത്തിനായി പണമടയ്ക്കുക" +step = "Step {{current}} of {{total}}" + +[portal.billing.prepaid.calc] +change = "മാറ്റുക" +deployRow = "Deployment" +done = "Done" +enterpriseHint = "ഇത് enterprise scale ആണ്. ഈ volume-ൽ, enterprise rates ഏത് self-serve discount-നേക്കാളും മെച്ചമാണ്. Rate lock, terms, കൂടാതെ മിനിറ്റുകൾക്കുള്ളിൽ ഒരു quote." +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} policies" +handlesLabel = "നിങ്ങളുടെ Processor" +handlesValue = "{{volume}} credits / mo കൈകാര്യം ചെയ്യുന്നു" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policies" +poolCaption = "വർഷത്തേക്ക് {{credits}} credits-ന്റെ ഒരു pool. Heavy months light ones-ൽ നിന്ന് borrow ചെയ്യും." +priceLabel = "നിങ്ങളുടെ വർഷം · 10 മാസത്തിന്റെ വിലയ്ക്ക് 12 മാസം" +rateUnknown = "checkout-ൽ ഞങ്ങൾ കൃത്യമായ വില കാണിക്കും." +savings = "നിങ്ങൾ {{amount}} ലാഭിക്കുന്നു · 2 മാസം സൗജന്യം." +sizingRow = "Sizing" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "മൊത്തം users" + +[portal.billing.prepaid.consent] +label = "എന്റെ prepaid capacity ഉപയോഗിച്ചു തീരുകയോ 12 മാസത്തിന് ശേഷം കാലഹരണപ്പെടുകയോ ചെയ്യുമ്പോൾ, ഞാൻ cancel ചെയ്യാത്ത പക്ഷം processing സാധാരണ metered pay-as-you-go rate-ൽ (എന്റെ spend limit വരെ) സ്വയമേവ തുടരുമെന്നും, billing portal-ൽ നിന്ന് എപ്പോൾ വേണമെങ്കിലും cancel ചെയ്യാമെന്നും ഞാൻ മനസ്സിലാക്കുന്നു." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling manage ചെയ്യുന്നു. മിനിറ്റുകൾക്കുള്ളിൽ live." +cloudValue = "Stirling Cloud · Managed" +selfhost = "Self-hosted" +selfhostDesc = "private infrastructure-ൽ runs. അതേ rate." +selfhostValue = "Self-hosted · Private infrastructure" + +[portal.billing.prepaid.offer] +cta = "offer review ചെയ്യുക" +subtitle = "ഒരു വർഷത്തെ PDF processing-ന് മുൻകൂർ പണമടച്ച് രണ്ട് മാസം സൗജന്യമായി നേടുക, metered billing-ന് മുമ്പ്, നിങ്ങളുടെ spend limit-ന് പുറത്തായി ഉപയോഗിക്കും." +title = "10 മാസത്തിന്റെ വിലയ്ക്ക് 12 മാസം നേടുക" + +[portal.billing.prepaid.pay] +accountName = "Account holder name" +accountNamePlaceholder = "ഉദാ. Jane Smith" +companyName = "Company name (optional)" +companyNamePlaceholder = "ഉദാ. Acme Inc." +downloadInvoice = "invoice download ചെയ്യുക" +payOnline = "online ആയി പണമടയ്ക്കുക" +poNumber = "PO number (optional)" +poNumberPlaceholder = "ഉദാ. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Advanced" +advancedDesc = "Pipelines നിങ്ങളുടെ processing-ന്റെ ഭൂരിഭാഗവും drive ചെയ്യുന്നു." +none = "None" +noneDesc = "ഇനിയും pipelines run ചെയ്യുന്നില്ല. എപ്പോൾ വേണമെങ്കിലും അവ on ചെയ്യുക." +standard = "Standard" +standardDesc = "ചില pipelines എത്തുന്ന PDFs വീണ്ടും process ചെയ്യുന്നു." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Classification, default, കൂടാതെ Sharing" +governed = "Governed" +governedDesc = "Securityയും Routingയും ചേർക്കുന്നു" +regulated = "Regulated" +regulatedDesc = "എല്ലാ category-കളും, Compliance, Retention, Ingestion ഉൾപ്പെടെ" + +[portal.billing.prepaid.review] +download = "quote download ചെയ്യുക (PDF)" +downloading = "തയ്യാറാക്കുന്നു…" +share = "approval-നായി share ചെയ്യുക" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "25 MB-ൽ താഴെയുള്ള files, data charges ഇല്ല" +heavy = "Heavy" +heavyDesc = "Scanned അല്ലെങ്കിൽ image-heavy, സാധാരണയായി 50 MB+" +standard = "Standard" +standardDesc = "മിക്കതും ചെറുത്, ചില scans 25 MB കടക്കും" + +[portal.billing.prepaid.state] +exhausted = "ഉപയോഗിച്ചു തീർന്നു" +healthy = "ധാരാളം ബാക്കി" +low = "കുറഞ്ഞുവരുന്നു" [portal.billing.spendLimit] adjustLimit = "Limit ക്രമീകരിക്കുക" @@ -6494,6 +6713,8 @@ label = "മറികടക്കുമെന്ന് പ്രവചിക് [portal.billing.spendThisMonth] eyebrow = "ഈ മാസത്തെ ചെലവ്" +freeRemaining_one = "{{formatted}} free PDF ബാക്കി" +freeRemaining_other = "{{formatted}} free PDFs ബാക്കി" processed_one = "{{formattedCount}} PDF പ്രോസസ് ചെയ്തു." processed_other = "{{formattedCount}} PDFs പ്രോസസ് ചെയ്തു." processedWithRate_one = "{{formattedCount}} PDF പ്രോസസ് ചെയ്തു, ഓരോന്നിന് {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "Monthly spend limit എത്തി" title = "Stripe portal തുറക്കാനായില്ല" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} free PDFs-ൽ ഉപയോഗിച്ചത്" capSuffix_other = "{{allowance}} free PDFs-ൽ ഉപയോഗിച്ചത്" eyebrow = "Processor trial" @@ -6534,6 +6756,376 @@ signature = "signature" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "പുതിയ connection" +createTitleFor = "{{name}} connect ചെയ്യുക" +delete = "Delete" +edit = "Edit" +editTitle = "connection edit ചെയ്യുക" +unsupportedType = "ഈ connection type ഇവിടെ edit ചെയ്യാൻ കഴിയില്ല." + +[portal.connections.categories.advanced] +description = "ഞങ്ങൾ ഇതുവരെ support ചെയ്യാത്ത service-നായി." +label = "Advanced" + +[portal.connections.categories.audit] +description = "ഒരു document-ന് എന്ത് സംഭവിച്ചു, നിങ്ങളുടെ auditors ഇതിനകം നോക്കുന്ന സ്ഥലത്ത് record ചെയ്യുക." +label = "Audit & compliance logging" + +[portal.connections.categories.notify] +description = "എന്തെങ്കിലും സംഭവിച്ചതായി മറ്റൊരു system-നെ അറിയിക്കുക." +label = "Notify & workflow" + +[portal.connections.categories.security] +description = "ഒരു document മുന്നോട്ട് നീങ്ങുന്നതിന് മുമ്പ് scan ചെയ്യുക അല്ലെങ്കിൽ label ചെയ്യുക." +label = "Security & classification" + +[portal.connections.categories.signing] +description = "ഒരു document ഒപ്പിടാനോ notarize ചെയ്യാനോ പുറത്തേക്ക് അയയ്ക്കുക." +label = "E-signature & notarization" + +[portal.connections.categories.storage] +description = "Processed documents അവസാനിക്കുന്നത് എവിടെ - ഒരു bucket, drive, ticket അല്ലെങ്കിൽ page." +label = "File & attach" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling ഇത് encrypted ആയി store ചെയ്യുന്നു, വീണ്ടും ഒരിക്കലും കാണിക്കില്ല." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "ഈ connection ഉപയോഗിക്കുന്ന steps-ന് ഈ URL-ന് കീഴിലുള്ള paths മാത്രം call ചെയ്യാം, മറ്റൊരു host ഒരിക്കലും അല്ല." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Password" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Username" + +[portal.connections.commonFields.webhookUrl] +helperText = "service നിങ്ങള്ക്ക് നൽകിയ URL paste ചെയ്യുക. അതിന് കീഴിലുള്ള paths മാത്രം call ചെയ്യാം." +label = "Webhook URL" + +[portal.connections.fields] +name = "Connection name" +namePlaceholder = "ഉദാ. Production" + +[portal.connections.picker] +cancel = "Cancel" +createNew = "പുതിയ connection..." +placeholder = "ഒരു connection തിരഞ്ഞെടുക്കുക" +save = "connection സേവ് ചെയ്യുക" + +[portal.connections.picker2] +back = "integration മാറ്റുക" +heading = "നിങ്ങൾ എന്താണ് connect ചെയ്യാൻ ആഗ്രഹിക്കുന്നത്?" +noResultsBody = "ഞങ്ങൾ ഇത് ഇതുവരെ support ചെയ്യുന്നില്ല - അതിനെക്കുറിച്ച് ഞങ്ങളോട് പറയൂ, അത് ചേർക്കുന്നത് നോക്കാം." +noResultsTitle = "\"{{query}}\"-ന് ഒന്നും match ചെയ്യുന്നില്ല" +searchPlaceholder = "integrations search ചെയ്യുക - \"sign\", \"ocr\" അല്ലെങ്കിൽ product name ശ്രമിക്കുക" + +[portal.connections.types.api] +description = "policy-യിൽ നിന്ന് ഏത് HTTP API-യും call ചെയ്യുക. URL, authentication, payload എന്നിവ നിങ്ങൾ തിരഞ്ഞെടുക്കുന്നു." +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "Authentication" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Username and password" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "custom header-ൽ token" + +[portal.connections.types.api.fields.authType.options.none] +label = "None" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "token-നായി log in ചെയ്യുക" + +[portal.connections.types.api.fields.baseUrl] +helperText = "ഈ connection ഉപയോഗിക്കുന്ന steps-ന് ഈ URL-ന് കീഴിലുള്ള paths മാത്രം call ചെയ്യാം, മറ്റൊരു host ഒരിക്കലും അല്ല." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header name" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Login path" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Password" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API ഒരു download link-നൊപ്പം reply ചെയ്താൽ, ഈ hosts മാത്രം fetch ചെയ്യാം. comma ഉപയോഗിച്ച് വേർതിരിക്കുക. base URL-ന്റെ സ്വന്തം host മാത്രം അനുവദിക്കാൻ ശൂന്യമായി വിടുക." +label = "Allowed result hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "ഈ header ആയി token അയയ്ക്കുക" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "login call token തിരികെ നൽകുന്ന response header, ഉദാ. X-Auth-Token." +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "Username" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "നിങ്ങൾ run ചെയ്യുന്ന server-ൽ documents malware-നായി scan ചെയ്യുക - ഒന്നും നിങ്ങളുടെ network വിട്ടുപോകില്ല." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "policy-യിലെ documents-ന് virus-യും content scanning-ഉം." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "പരിചിതമായ malware മാത്രമല്ല, macros, executables, scripts അല്ലെങ്കിൽ embedded objects അടങ്ങിയ documents block ചെയ്യുക." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "processed documents ഒരു Confluence page-ലേക്ക് attach ചെയ്യുക." +label = "Confluence" + +[portal.connections.types.consigno] +description = "electronic signature-നും notarization-നും documents ConsignO Cloud (Notarius)-ലേക്ക് അയയ്ക്കുക." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "നിങ്ങളുടെ ConsignO Cloud environment, /api/v1 ഉൾപ്പെടെ." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Password" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "നിങ്ങളുടെ organisation tenants ഉപയോഗിക്കുന്നുവെങ്കിൽ മാത്രം ആവശ്യം." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Username" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "ഒരു policy document process ചെയ്യുമ്പോൾ Discord-ലേക്ക് message post ചെയ്യുക." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "ഓരോ തവണയും policy document കൈകാര്യം ചെയ്യുമ്പോൾ Elasticsearch-ൽ ഒരു audit event index ചെയ്യുക." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "പുതിയ documents-നായി FTP അല്ലെങ്കിൽ FTPS server poll ചെയ്യുക." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Connection mode" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (recommended)" + +[portal.connections.types.ftp.fields.security] +helperText = "Plain FTP credentials-ഉം files-ഉം unencrypted ആയി അയയ്ക്കുന്നു; server support ചെയ്യുന്നുവെങ്കിൽ FTPS തിരഞ്ഞെടുക്കുക." +label = "Encryption" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "None (plain FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "ഒരു policy run ചെയ്യുമ്പോൾ Google Chat space-ലേക്ക് message post ചെയ്യുക." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "processed documents ഒരു Jira issue-ലേക്ക് attach ചെയ്യുക." +label = "Jira" + +[portal.connections.types.mailgun] +description = "ഒരു policy run ചെയ്യുമ്പോൾ document അല്ലെങ്കിൽ notification email ചെയ്യുക." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "processed documents Nextcloud-ലേക്ക് file ചെയ്യുക." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "നിങ്ങൾ run ചെയ്യുന്ന Presidio server ഉപയോഗിച്ച് document text-ൽ personal data കണ്ടെത്തുക." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "PDFs-ൽ Microsoft Purview sensitivity labels വായിച്ച് apply ചെയ്യുക." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Optional. labels പേര് പ്രകാരം pick ചെയ്യാൻ മാത്രം ആവശ്യം; ഇതില്ലാതെയും labelling പ്രവർത്തിക്കും." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "ഓരോ label-ലും അതിന്റെ SiteId ആയി stamped ചെയ്യുന്നു, അതിനാൽ Purview-aware tools ഏത് organisation ആണ് അത് apply ചെയ്തതെന്ന് അറിയും." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "sources-ന് read ചെയ്യാനും pipeline outputs-ന് write ചെയ്യാനും കഴിയുന്ന ഒരു S3 bucket." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS-നായി ശൂന്യമായി വിടുക. MinIO അല്ലെങ്കിൽ മറ്റൊരു S3-compatible store-നായി ഇത് സജ്ജീകരിക്കുക." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "ഒരു policy run ചെയ്യുമ്പോൾ document അല്ലെങ്കിൽ notification email ചെയ്യുക." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "പുതിയ documents-നായി SFTP (SSH) server poll ചെയ്യുക." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optional. server-ന്റെ SHA-256 host key fingerprint (`ssh-keyscan host | ssh-keygen -lf -`-ൽ നിന്ന്). set ചെയ്താൽ, ഈ key present ചെയ്യുന്ന server മാത്രം accept ചെയ്യും; ശൂന്യമായാൽ, ആദ്യ connect-ൽ കണ്ട key pin ചെയ്യും, പിന്നീടുള്ള changes refuse ചെയ്യും." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "private key ഉപയോഗിച്ച് authenticate ചെയ്യുന്നുവെങ്കിൽ ശൂന്യമായി വിടുക." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optional. password-ന് പകരം key ഉപയോഗിച്ച് authenticate ചെയ്യാൻ OpenSSH private key paste ചെയ്യുക." +label = "Private key" + +[portal.connections.types.slack] +description = "ഒരു policy document process ചെയ്യുമ്പോൾ Slack-ലേക്ക് message post ചെയ്യുക." +label = "Slack" + +[portal.connections.types.smb] +description = "SMB / CIFS network share (Windows അല്ലെങ്കിൽ Samba) watch ചെയ്യുക." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Optional. നിങ്ങളുടെ server ഉപയോഗിക്കുന്നുവെങ്കിൽ Windows domain അല്ലെങ്കിൽ workgroup." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "ഓരോ തവണയും policy document കൈകാര്യം ചെയ്യുമ്പോൾ Splunk-ലേക്ക് audit event അയയ്ക്കുക." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Sumo Logic HTTP collector-ലേക്ക് audit event അയയ്ക്കുക." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "ഒരു policy run ചെയ്യുമ്പോൾ Teams channel-ലേക്ക് message post ചെയ്യുക." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "ഓരോ document-ഉം, അല്ലെങ്കിൽ details മാത്രം, നിങ്ങൾ തിരഞ്ഞെടുക്കുന്ന URL-ലേക്ക് post ചെയ്യുക." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "policy-യിൽ നിന്ന് Zap അല്ലെങ്കിൽ Make scenario trigger ചെയ്യുക." +label = "Zapier & Make" + +[portal.docs] +browse = "docs browse ചെയ്യുക" +viewSource = "source GitHub-ൽ കാണുക" + [portal.docs.authentication] codeCaption = "ഓരോ request-വും" eyebrow = "GETTING STARTED" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "നിങ്ങളുടെ agent ഒരൊറ്റ tool ആയി invoke ചെയ്യുന്ന bundled, named capabilities. ഓരോ skill-വും evals attached ആയ deterministic op chain ആണ്." title = "Agent skills" +[portal.docs.toc] +title = "ഈ page-ൽ" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "തരം" user = "ഉപയോക്താവ്" [portal.documents.queue.empty] +connectSource = "ഒരു source connect ചെയ്യുക" +createPipeline = "ഒരു pipeline സൃഷ്ടിക്കുക" description = "ഉറവിടങ്ങൾ നിങ്ങളുടെ പൈപ്പ്‌ലൈനുകളിലേക്ക് ഡോക്യുമെന്റുകൾ നൽകുമ്പോൾ അവ അവലോകനത്തിനായി ഇവിടെ ദൃശ്യമാകും." title = "ക്യൂവിൽ ഡോക്യുമെന്റുകളില്ല" @@ -6712,11 +7309,11 @@ editorAction = "എഡിറ്റർ" empty = "ഈ ഫിൽട്ടറുമായി പൊരുത്തപ്പെടുന്ന ഡോക്യുമെന്റുകളില്ല." rowActions = "വരി പ്രവർത്തനങ്ങൾ" sensitiveLabel = "സെൻസിറ്റീവ്" -sensitiveTitle = "സെൻസിറ്റീവ് — പ്രവേശനം ആവശ്യമാണ്" [portal.documents.table.columns] action = "പൈപ്പ്‌ലൈൻ / പ്രവർത്തനം" document = "ഡോക്യുമെന്റ്" +labels = "Labels" product = "ഉൽപ്പന്നം" status = "സ്ഥിതി" time = "സമയം" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "അവസാനം കണ്ടത്" region = "Region" status = "സ്ഥിതി" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "ഈ view-ൽ അപ്രതീക്ഷിത പിശക് retry = "വീണ്ടും ശ്രമിക്കുക" title = "ഈ പേജിൽ എന്തോ പിഴച്ചു" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Back" +body = "desktop app download ചെയ്യുക അല്ലെങ്കിൽ server self-host ചെയ്യുക." +done = "Done" +guide = "{{name}} guide തുറക്കുക" +openInBrowser = "പകരം browser-ൽ തുറക്കുക" +sectionDesktop = "Desktop app" +sectionSelfHosted = "Self-hosted" +title = "Stirling PDF Editor install ചെയ്യുക" + +[portal.home.download.docker] +detailBody = "നിങ്ങളുടെ server-ൽ official image run ചെയ്യുക." +note = ":8080-ൽ തുറക്കും. Default login: admin / stirling." +tagline = "server ഒരു container-ൽ run ചെയ്യുക" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "ഔദ്യോഗിക Helm chart ഇൻസ്റ്റാൾ ചെയ്യുക." +note = "ക്രമീകരണങ്ങൾ നിലനിർത്താൻ /configs-നായി ഒരു PVC ചേർക്കുക." +tagline = "Helm ഉപയോഗിച്ച് ഒരു cluster-ലേക്ക് deploy ചെയ്യുക" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "നിങ്ങളുടെ distribution-നുള്ള native builds." +guideBtn = "Linux ഇൻസ്റ്റാൾ ഓപ്ഷനുകൾ കാണുക" +note = "സാധാരണ distros-നായി AppImage, .deb, .rpm builds ലഭ്യമാണ്." +tagline = "നിങ്ങളുടെ distro-യ്ക്കുള്ള native desktop app" +title = "Linux" + +[portal.home.download.mac] +altLabel = "അല്ലെങ്കിൽ Homebrew വഴി" +detailBody = "installer പ്രവർത്തിപ്പിക്കുക, അല്ലെങ്കിൽ Homebrew ഉപയോഗിക്കുക." +downloadBtn = "macOS-നായി ഡൗൺലോഡ് ചെയ്യുക" +tagline = "Apple silicon, Intel എന്നിവയ്ക്കുള്ള native app" +title = "macOS" + +[portal.home.download.manual] +detailBody = "bare metal server-ൽ .jar പ്രവർത്തിപ്പിക്കുക." +downloadBtn = ".jar ഡൗൺലോഡ് ചെയ്യുക" +note = "Java 21+ ആവശ്യമാണ്. :8080-ൽ തുറക്കും; default login admin / stirling." +runLabel = "തുടർന്ന് ഇത് പ്രവർത്തിപ്പിക്കുക" +tagline = "bare metal server-ൽ .jar പ്രവർത്തിപ്പിക്കുക" +title = "Manual server setup" + +[portal.home.download.windows] +altLabel = "അല്ലെങ്കിൽ winget വഴി" +detailBody = "installer പ്രവർത്തിപ്പിക്കുക, അല്ലെങ്കിൽ package manager ഉപയോഗിക്കുക." +downloadBtn = "installer ഡൗൺലോഡ് ചെയ്യുക" +tagline = "Microsoft Windows-നുള്ള native app" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} സജീവം" -invite = "സഹപ്രവർത്തകരെ ക്ഷണിക്കുക" +activeOfDeployed = "ഈ മാസം {{total}}-ൽ {{active}} active" name = "Stirling PDF Editor" open = "Browser-ൽ തുറക്കുക" updated = "{{time}} അപ്ഡേറ്റ് ചെയ്തു" +[portal.home.editor.deploy] +finish = "deployment പൂർത്തിയാക്കുക" +options = "deployment ഓപ്ഷനുകൾ കാണുക" +start = "Editor deploy ചെയ്യുക" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "ശുഭ ഉച്ചയ്ക്ക്" evening = "ശുഭ സന്ധ്യ" morning = "ശുഭ പ്രഭാതം" -[portal.home.onboarding] -dismiss = "Setup ഒഴിവാക്കുക" -notStarted = "ആരംഭിച്ചിട്ടില്ല" -progress = "{{total}}-ൽ {{done}} പൂർത്തിയായി" -title = "Setup പൂർത്തിയാക്കുക" - -[portal.home.onboarding.enterprise] -body = "Org-wide SSO + SCIM + RBAC, committed volume pricing, air-gapped deployment." -cta = "Trial ആരംഭിക്കുക" -ctaQuote = "Quote നേടുക" -lead = "250+ ജീവനക്കാർക്കായി." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Desktop app വിന്യസിക്കുക, എന്നും സൗജന്യം." -title = "PDF Editor ഡൗൺലോഡ് ചെയ്യുക" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} സജീവം · {{recommended}} ശുപാർശ ചെയ്തത്" -chip = "{{active}} സജീവം" -title = "നയങ്ങൾ ഓണാക്കുക" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} connect ചെയ്തു · എത്തുന്നിടത്ത് ഓരോ PDF-വും governed" -chip = "{{connected}} connect ചെയ്തു" -title = "നിങ്ങളുടെ ഉറവിടങ്ങൾ connect ചെയ്യുക" - -[portal.home.quickActions] -subtitle = "ഇന്നത്തെ പ്രധാന tasks" -title = "Quick actions" - -[portal.home.quickActions.buildPipeline] -blurb = "Typed op library-യിലെ 3-step composer" -title = "ഒരു പൈപ്പ്‌ലൈൻ നിർമ്മിക്കുക" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, watched folders" -title = "ഒരു ഉറവിടം connect ചെയ്യുക" - -[portal.home.quickActions.issueApiKey] -blurb = "Rate limits, IP allowlist എന്നിവയുള്ള scoped key" -title = "ഒരു API key നൽകുക" - [portal.infrastructure] manageEditorDeployment = "Editor വിന്യാസം നിയന്ത്രിക്കുക" sectionsAriaLabel = "Infrastructure വിഭാഗങ്ങൾ" @@ -6878,13 +7541,9 @@ heading = "API keys" subheading = "Per-key rate limits, permissions, IP allowlists എന്നിവയുള്ള scoped credentials." [portal.infrastructure.apiKeys.card] -allowedIps = "അനുവദിച്ച IPs" -anyIp = "ഏതെങ്കിലും IP (allowlist ഇല്ല)" created = "സൃഷ്ടിച്ചത്" lastUsed = "അവസാനം ഉപയോഗിച്ചത്" -permissions = "അനുമതികൾ" -rateLimit = "Rate limit" -rateLimitValue = "{{value}} req/min" +revoke = "key revoke ചെയ്യുക" usageMonth = "ഈ മാസത്തെ ഉപയോഗം" usageToday = "ഇന്നത്തെ ഉപയോഗം" @@ -6892,6 +7551,15 @@ usageToday = "ഇന്നത്തെ ഉപയോഗം" description = "Stirling API വിളിക്കാൻ തുടങ്ങാൻ ഒരു scoped key സൃഷ്ടിക്കുക." title = "ഇതുവരെ API keys ഇല്ല" +[portal.infrastructure.apiKeys.error] +load = "നിങ്ങളുടെ API keys load ചെയ്യാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" revoke ചെയ്യണോ? ഈ key ഇപ്പോഴും ഉപയോഗിക്കുന്ന ഏതെങ്കിലും integration ഉടൻ 401 errors സ്വീകരിക്കാൻ തുടങ്ങും. ഇത് പഴയപടിയാക്കാൻ കഴിയില്ല." +cancel = "റദ്ദാക്കുക" +confirm = "key revoke ചെയ്യുക" +title = "API key revoke ചെയ്യുക" + [portal.infrastructure.audit] filterAriaLabel = "Category പ്രകാരം audit events ഫിൽട്ടർ ചെയ്യുക" heading = "Audit logs" @@ -6935,6 +7603,7 @@ all = "എല്ലാം" auth = "Auth" config = "Config" elevation = "Elevation" +policy = "നയം" processing = "Processing" security = "Security" @@ -6944,19 +7613,30 @@ title = "നിങ്ങൾക്ക് access ഇല്ല" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Elevation" +policy = "നയം" processing = "Processing" totalEvents = "ആകെ events · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Config" +elevation = "Elevation" +policy = "നയം" +processing = "Processing" +security = "Security" + +[portal.infrastructure.auditStatusLabel] +danger = "പിശക്" +info = "വിവരം" +success = "വിജയം" +warning = "മുന്നറിയിപ്പ്" + [portal.infrastructure.createKey] cancel = "റദ്ദാക്കുക" createKey = "Key സൃഷ്ടിക്കുക" done = "പൂർത്തിയായി" -ipAllowlistHelper = "Comma-separated CIDR ranges. ഏതെങ്കിലും IP അനുവദിക്കാൻ ശൂന്യമായി വിടുക." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Key പേര്" keyNamePlaceholder = "ഉദാ. Production · ingest" -permissionsLabel = "അനുമതികൾ" secretKeyCaption = "Secret key" secretWarning = "ഇത് secrets manager-ൽ സൂക്ഷിക്കുക. Stirling എപ്പോഴും hash മാത്രമേ സൂക്ഷിക്കൂ — പിന്നീട് ഇത് വീണ്ടെടുക്കാൻ മാർഗമില്ല." subtitle = "Key-ക്ക് ആവശ്യമായ കുറഞ്ഞ scope മാത്രം നൽകുക. ഏത് സമയത്തും rotate ചെയ്യുകയോ revoke ചെയ്യുകയോ ചെയ്യാം." @@ -6964,207 +7644,9 @@ subtitleCreated = "ഈ secret ഇപ്പോൾ copy ചെയ്യുക — title = "API key സൃഷ്ടിക്കുക" titleCreated = "Key സൃഷ്ടിച്ചു" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "വിന്യസിച്ചത്" -environment = "Environment" -product = "ഉൽപ്പന്നം" -status = "സ്ഥിതി" -version = "Version" -when = "എപ്പോൾ" - -[portal.infrastructure.deployments.recent] -heading = "സമീപകാല deployments" -subheading = "ഉൽപ്പന്നങ്ങളിലെയും environments-കളിലെയും ഏറ്റവും പുതിയ rollouts." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instances" -latency = "Latency" -load = "Load" -p99 = "P99" -region = "Region" -status = "സ്ഥിതി" -throughput = "Throughput" -uptime = "Uptime" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regions" -subheading = "വിന്യസിച്ച ഓരോ Stirling region-ന്റെയും live health — latency, load, rollout version." - -[portal.infrastructure.deployments.regions.empty] -description = "നിങ്ങളുടെ workspace provision ചെയ്തുകഴിഞ്ഞാൽ deployed regions ഇവിടെ ദൃശ്യമാകും." -title = "Regions വിന്യസിച്ചിട്ടില്ല" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "നിങ്ങളുടെ workspace-ിലുടനീളം document processing പ്രവർത്തിപ്പിക്കുന്ന model catalogue, routing എന്നിവ." - -[portal.infrastructure.models.byom] -description = "Data-residency-bound processing-നായി on-prem അല്ലെങ്കിൽ self-hosted model register ചെയ്ത് ഒരു region-ലേക്ക് pin ചെയ്യുക." -title = "നിങ്ങളുടെ സ്വന്തം model കൊണ്ടുവരിക" - -[portal.infrastructure.models.catalogue] -heading = "Catalogue" -sub = "Live latency, cost എന്നിവയോടെ നിങ്ങളുടെ workspace-ന് ലഭ്യമായ managed models." -subEnterprise = "Managed, bring-your-own, on-prem models — per-region pinning ലഭ്യമാണ്." - -[portal.infrastructure.models.catalogue.empty] -description = "നിങ്ങളുടെ workspace-ന്റെ catalogue-ിലുള്ള models ഇവിടെ ദൃശ്യമാകും." -title = "Models ലഭ്യമല്ല" - -[portal.infrastructure.models.columns] -cost = "ചെലവ്" -latency = "Latency" -load = "Load" -model = "Model" -status = "സ്ഥിതി" -type = "തരം" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "സജീവ models" -avgLatency = "ശരാശരി latency" -included = "ഉൾപ്പെടുത്തി" -monthlySpend = "മാസിക model ചെലവ്" - -[portal.infrastructure.models.routing] -empty = "Routing rules configured ചെയ്തിട്ടില്ല." -heading = "Routing rules" -sub = "ഓരോ operation-യും ഏത് model കൈകാര്യം ചെയ്യുന്നു. കൂടുതൽ narrow rule ഒന്നും match ചെയ്യാത്തപ്പോൾ default ബാധകമാണ്." -subLocked = "Operations പ്രത്യേക models-ലേക്ക് route ചെയ്യുക — paid plans-ൽ ലഭ്യമാണ്." - -[portal.infrastructure.models.routing.lockedBanner] -description = "ഓരോ operation, document type എന്നിവ ഏത് model കൈകാര്യം ചെയ്യണമെന്ന് നിയന്ത്രിക്കാൻ Pro-യിലേക്ക് upgrade ചെയ്യുക." -title = "Model routing ഒരു paid feature ആണ്" - -[portal.infrastructure.models.routingColumns] -default = "Default" -docType = "ഡോക്യുമെന്റ് തരം" -modelForAria = "{{operation}}-ക്കുള്ള model" -operation = "Operation" -routedTo = "Route ചെയ്തത്" - -[portal.infrastructure.security.access.byok] -description = "നിങ്ങളുടെ സ്വന്തം KMS-ൽ നിന്ന് key നൽകുക. Stirling അത് ഉപയോഗിച്ച് encrypt ചെയ്യുന്നു, പക്ഷേ ഇപ്പോഴും വായിക്കാം." -label = "നിങ്ങളുടെ സ്വന്തം key കൊണ്ടുവരിക (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Keys ഒരിക്കലും നിങ്ങളുടെ KMS വിട്ടുപോകില്ല. Stirling ciphertext മാത്രം കൈവശം വയ്ക്കുന്നു." -label = "നിങ്ങളുടെ സ്വന്തം key കൈവശം വയ്ക്കുക (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling encryption keys കൈകാര്യം ചെയ്യുന്നു. ഏറ്റവും ലളിതം — നിങ്ങളുടെ ഭാഗത്ത് key ops ഒന്നുമില്ല." -label = "Stirling-held keys" - -[portal.infrastructure.security.accessPolicy] -heading = "ഡോക്യുമെന്റ് access policy" -subheading = "At rest ഉള്ള processed documents ആർക്കെല്ലാം decrypt ചെയ്യാനാകുമെന്ന് നിയന്ത്രിക്കുന്നു." - -[portal.infrastructure.security.attestations] -heading = "Compliance attestations" -noReport = "Report ലഭ്യമല്ല" -subheading = "Attested controls-ൽ reports ലഭ്യമായ framework-by-framework audit posture." -viewReport = "Report കാണുക →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Stirling platform-നെ ഉൾക്കൊള്ളുന്ന attestations, certifications." - -[portal.infrastructure.security.empty] -description = "നിങ്ങളുടെ workspace-ന്റെ security configuration ഇവിടെ ദൃശ്യമാകും." -title = "Security posture ലഭ്യമല്ല" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK ഉപയോഗിക്കുമ്പോൾ encryption keys നിങ്ങളുടെ KMS വിട്ടുപോകില്ല. Stirling നിങ്ങൾക്ക് ഏത് സമയത്തും revoke ചെയ്യാനാകുന്ന ciphertext മാത്രം store, process ചെയ്യുന്നു." -title = "Stirling-ന് നിങ്ങളുടെ ഡോക്യുമെന്റുകൾ decrypt ചെയ്യാനാവില്ല" - -[portal.infrastructure.security.ipAllowlist] -empty = "IP ranges configured ചെയ്തിട്ടില്ല — എല്ലാ IPs-ഉം അനുവദിച്ചിരിക്കുന്നു." -heading = "IP allowlist" -sub = "API access ഈ CIDR ranges-ലേക്ക് പരിമിതപ്പെടുത്തിയിരിക്കുന്നു." -subLocked = "പരിചിതമായ IP ranges-ലേക്ക് API access പരിമിതപ്പെടുത്തുക — paid plans-ൽ ലഭ്യമാണ്." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "നിർദ്ദിഷ്ട networks-ലേക്ക് API access പരിമിതപ്പെടുത്താൻ Pro-യിലേക്ക് upgrade ചെയ്യുക." -title = "IP allowlisting ഒരു paid feature ആണ്" - -[portal.infrastructure.security.ipColumns] -added = "ചേർത്തത്" -addedBy = "ചേർത്തത്" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Encryption key management" -keyId = "Key identifier" -lastRotated = "അവസാനം rotated" -rotateKey = "Key rotate ചെയ്യുക" -rotationPolicy = "Rotation policy" -subheading = "At rest ഉള്ള documents encrypt ചെയ്യുന്ന keys-ന്റെ custody — ആർക്കെല്ലാം decrypt ചെയ്യാം, keys എങ്ങനെ rotate ചെയ്യും." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK), hold-your-own-key (HYOK) custody എന്നിവ Enterprise-ൽ ലഭ്യമാണ്. നിങ്ങളുടെ സ്വന്തം KMS-ൽ നിന്ന് keys നൽകാൻ upgrade ചെയ്യുക." -title = "നിങ്ങളുടെ plan-ൽ keys Stirling കൈകാര്യം ചെയ്യുന്നു" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "European Union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "United States" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "ഡോക്യുമെന്റുകൾ എവിടെ store, process ചെയ്യുന്നു." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} ഉപയോഗിച്ചു" - -[portal.infrastructure.storage.empty] -description = "Connected storage, usage എന്നിവ ഇവിടെ ദൃശ്യമാകും." -title = "Storage configured ചെയ്തിട്ടില്ല" - -[portal.infrastructure.storage.lifecycle] -active = "സജീവം" -activeRange = "0–{{value}}d" -archived = "Archived" -coldStorage = "cold storage" -deleted = "Deleted" -never = "ഒരിക്കലും" -purged = "purged" - -[portal.infrastructure.storage.providers] -connect = "Connect" -connected = "Connected" -heading = "Connected providers" -subheading = "Processed artifacts എഴുതുന്ന ഇടം." - -[portal.infrastructure.storage.retention] -heading = "Retention" -subheading = "Lifecycle deletion-ന് മുമ്പ് artifacts എത്രകാലം സൂക്ഷിക്കും." -windowLabel = "Default retention window" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} ദിവസം" -days_other = "{{count}} ദിവസം" -never = "ഒരിക്കലും delete ചെയ്യരുത്" - -[portal.infrastructure.storage.totalUsage] -heading = "ആകെ ഉപയോഗം" -progressLabel = "ഉപയോഗിച്ച storage" -subheading = "Connected providers മുഴുവൻ ഉപയോഗിച്ച storage." +[portal.infrastructure.keyLabel] +active = "Active" +revoked = "Revoked" [portal.infrastructure.tabs] apiKeys = "API Keys" @@ -7174,6 +7656,49 @@ models = "Models" security = "Security" storage = "Storage" +[portal.integrations] +availableHeading = "ലഭ്യമാണ്" +comingSoonHeading = "ഉടൻ വരുന്നു" +connect = "ബന്ധിപ്പിക്കുക" +connectionCount_one = "{{count}} കണക്ഷൻ" +connectionCount_other = "{{count}} കണക്ഷനുകൾ" +customApi = "Custom API" +searchPlaceholder = "integrations തിരയുക" +subtitle = "Stirling-ന് ആശയവിനിമയം നടത്താൻ കഴിയുന്ന എല്ലാം. Credentials ഇവിടെ ഒരിക്കൽ സൃഷ്ടിച്ച ശേഷം sources, policies, pipelines എന്നിവയിൽ reference ചെയ്യുന്നു." +title = "Integrations" +typedName = "{{tool}} പേര്" + +[portal.integrations.filters] +all = "എല്ലാം" +audit = "Audit" +connected = "ബന്ധിപ്പിച്ചു" +notify = "Notify" +security = "Security" +signing = "Signing" +storage = "Storage" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "ബന്ധിപ്പിച്ചു" + +[portal.integrations.table] +integration = "Integration" +worksWith = "ഇവയുമായി പ്രവർത്തിക്കുന്നു" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Policies" +sources = "Sources" + +[portal.legal] +draft = "{{label}} · draft" +loadError = "ഈ ഡോക്യുമെന്റ് load ചെയ്യാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." +loading = "Load ചെയ്യുന്നു…" +title = "നിയമ ഡോക്യുമെന്റ്" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7182,33 +7707,17 @@ documents = "ഡോക്യുമെന്റുകൾ" editor = "Editor" home = "Home" infrastructure = "Infrastructure" +integrations = "Integrations" pipelines = "പൈപ്പ്‌ലൈനുകൾ" policies = "നയങ്ങൾ" procurement = "Procurement" -settings = "Settings" sources = "ഉറവിടങ്ങൾ" usage = "Usage & Billing" users = "Users" -[portal.notifications] -markAllRead = "എല്ലാം read ആയി അടയാളപ്പെടുത്തുക" -title = "Notifications" -viewAll = "എല്ലാം കാണുക" - -[portal.notifications.ariaLabel] -none = "Notifications, unread ഒന്നുമില്ല" -unread_one = "Notifications, {{count}} unread" -unread_other = "Notifications, {{count}} unread" - -[portal.notifications.count] -allRead = "എല്ലാം read" -loading = "loading" -new_one = "{{count}} പുതിയത്" -new_other = "{{count}} പുതിയത്" - -[portal.notifications.empty] -description = "പുതിയ notifications ഇല്ല." -title = "നിങ്ങൾ എല്ലാം കണ്ടു കഴിഞ്ഞു" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Backend-ലുള്ള ഓരോ automated document pipeline: ഒരു കൂട്ടം sources-ൽ operations-ന്റെ ordered chain, trigger-ൽ run ചെയ്യുന്നു. അതിന്റെ steps, sources കാണാൻ ഒരു row click ചെയ്യുക." @@ -7218,54 +7727,87 @@ title = "പൈപ്പ്‌ലൈനുകൾ" newPipeline = "പുതിയ പൈപ്പ്‌ലൈൻ" [portal.pipelines.builder] -addStep = "Tool ചേർക്കുക" +activate = "Activate" back = "പൈപ്പ്‌ലൈനുകളിലേക്ക് മടങ്ങുക" +cannotFollow = "{{produced}} സ്വീകരിക്കാൻ കഴിയില്ല" +chooseAccount = "ഒരു account തിരഞ്ഞെടുക്കുക" +chooseDestination = "ഒരു destination തിരഞ്ഞെടുക്കുക" +chooseOperation = "ഈ step ചെയ്യേണ്ടത് തിരഞ്ഞെടുക്കുക" +chooseSource = "ഒരു source തിരഞ്ഞെടുക്കുക" discard = "മാറ്റങ്ങൾ ഉപേക്ഷിക്കുക" -enabled = "Enabled" +inputs = "Input" +inputSource = "Input source" +inputTrigger = "Trigger" keepEditing = "Editing തുടരുക" -needsUpload = "Uploaded file ആവശ്യമാണ്" +moreActions = "More actions" +needsConfiguring = "സജ്ജീകരണം ആവശ്യമാണ്" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "നിങ്ങളുടെ search-നോട് പൊരുത്തപ്പെടുന്ന tools ഇല്ല." -pipelineSettings = "പൈപ്പ്‌ലൈൻ settings" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Tools search ചെയ്യുക" -selectToolBody = "നിങ്ങളുടെ പൈപ്പ്‌ലൈൻ നിർമ്മിക്കാൻ ഒരു tool ചേർക്കുക." -selectToolTitle = "ഇതുവരെ tools ഇല്ല" -toolSettings = "Tool settings" +sendToSystem = "മറ്റൊരു system-ലേക്ക് അയയ്ക്കുക" +stepsIncompatible = "മുമ്പത്തെ step സൃഷ്ടിക്കുന്നതിൽ ഈ steps പ്രവർത്തിക്കാൻ കഴിയില്ല: {{tools}}." +stepsNeedSetup = "save ചെയ്യുന്നതിന് മുമ്പ് ഈ steps-ന് ഇനിയും സജ്ജീകരണം ആവശ്യമാണ്: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "തിരിച്ചറിയാത്ത operation, അതേപടി സൂക്ഷിച്ചു." unsavedBody = "നിങ്ങൾക്ക് save ചെയ്യാത്ത മാറ്റങ്ങളുണ്ട്. പോകുന്നതിനുമുമ്പ് അവ save ചെയ്യണോ, അല്ലെങ്കിൽ ഉപേക്ഷിക്കണോ?" unsavedTitle = "Save ചെയ്യാത്ത മാറ്റങ്ങൾ" -uploadUnsupported = "Uploaded files ഇതുവരെ pipelines-ൽ support ചെയ്യുന്നില്ല, അതിനാൽ ഈ steps save ചെയ്യാൻ കഴിയില്ല: {{tools}}." usesDefaults = "Default settings ഉപയോഗിച്ച് run ചെയ്യുന്നു" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "മുമ്പത്തെ step-ൽ നിന്നുള്ള ഓരോ file-ഉം സംയോജിപ്പിക്കുന്നു" +fan-out = "മുമ്പത്തെ step-ൽ നിന്നുള്ള ഓരോ file-ക്കും ഒരിക്കൽ പ്രവർത്തിക്കുന്നു" +format-mismatch = "{{accepts}} ആവശ്യമാണ്, പക്ഷേ മുമ്പത്തെ step {{produced}} സൃഷ്ടിക്കുന്നു" +output-uncertain = "പ്രവർത്തിക്കണമെന്നില്ല: മുമ്പത്തെ step-ന്റെ output അതിന്റെ സജ്ജീകരണത്തെ ആശ്രയിക്കുന്നു" +source-mismatch = "{{accepts}} ആവശ്യമാണ്, പക്ഷേ ഈ pipeline-ന്റെ input {{produced}} ആണ്" +undeclared-operation = "ഈ step എന്താണ് സ്വീകരിക്കുന്നതെന്ന് പരിശോധിക്കാൻ കഴിയില്ല" [portal.pipelines.composer] addTool = "Tool ചേർക്കുക" -cancel = "റദ്ദാക്കുക" -chainEmpty = "നിങ്ങളുടെ പൈപ്പ്‌ലൈൻ നിർമ്മിക്കാൻ ഒരു tool ചേർക്കുക." create = "പൈപ്പ്‌ലൈൻ സൃഷ്ടിക്കുക" -directory = "Output folder" -directoryHelp = "Server-ലെ absolute path. Configured allowed folders-ന്റെ ഉള്ളിലായിരിക്കണം." +createPaused = "Create paused" editingUnsupported = "Editing-നായി ഈ tool params പ്രദർശിപ്പിക്കുന്നത് ഇതുവരെ support ചെയ്യുന്നില്ല." -moveDown = "താഴേക്ക് നീക്കുക" -moveUp = "മുകളിലേക്ക് നീക്കുക" +editSource = "Edit source" name = "പേര്" namePlaceholder = "ഉദാ. Redaction sweep" -noSources = "ഇതുവരെ sources connected ചെയ്തിട്ടില്ല. നേരിട്ട് നൽകുന്ന files-ൽ pipeline ഇപ്പോഴും run ചെയ്യാം." noToolSettings = "ഈ tool-ന് configurable settings ഇല്ല." -operations_one = "Operation ({{count}})" -operations_other = "Operations ({{count}})" output = "Output" -removeStep = "Operation നീക്കം ചെയ്യുക" save = "മാറ്റങ്ങൾ save ചെയ്യുക" scheduleEvery = "ഓരോ" -sources = "ഉറവിടങ്ങൾ" -sourcesLoading = "Sources load ചെയ്യുന്നു..." trigger = "Trigger" triggerManual = "Manual മാത്രം" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "ദിവസങ്ങൾ" hours = "മണിക്കൂറുകൾ" minutes = "മിനിറ്റുകൾ" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" delete ചെയ്യണോ? ഇത് പഴയപടി ആക്കാൻ കഴിയില്ല." cancel = "റദ്ദാക്കുക" @@ -7273,28 +7815,62 @@ confirm = "Delete" title = "Pipeline delete ചെയ്യണോ?" [portal.pipelines.detail] +clearHistory = "history മായ്ക്കുക" delete = "Pipeline delete ചെയ്യുക" run = "ഇപ്പോൾ run ചെയ്യുക" [portal.pipelines.empty] action = "ഒരു പൈപ്പ്‌ലൈൻ സൃഷ്ടിക്കുക" +connectSource = "ഒരു source ബന്ധിപ്പിക്കുക" description = "നിങ്ങളുടെ ആദ്യ പൈപ്പ്‌ലൈൻ സൃഷ്ടിക്കുക: അത് run ചെയ്യുന്ന sources തിരഞ്ഞെടുക്കുക, operations chain ചെയ്യുക, output എവിടേക്ക് പോകണമെന്ന് തിരഞ്ഞെടുക്കുക." title = "ഇതുവരെ പൈപ്പ്‌ലൈനുകളില്ല" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "സജീവം" paused = "Paused" total = "പൈപ്പ്‌ലൈനുകൾ" -[portal.pipelines.output] -folder = "Folder-ലേക്ക് എഴുതുക" -inline = "Files തിരികെ നൽകുക" - [portal.pipelines.run] +allProcessed_one = "പ്രവർത്തിപ്പിക്കാൻ ഒന്നുമില്ല: source-ന്റെ {{count}} ഡോക്യുമെന്റ് ഇതിനകം process ചെയ്തു." +allProcessed_other = "പ്രവർത്തിപ്പിക്കാൻ ഒന്നുമില്ല: sources-ലുള്ള എല്ലാ {{count}} ഡോക്യുമെന്റുകളും ഇതിനകം process ചെയ്തു." completed_one = "Run പൂർത്തിയായി." completed_other = "എല്ലാ {{count}} runs പൂർത്തിയായി." empty = "Run ചെയ്യാൻ ഒന്നുമില്ല: sources-ൽ process ചെയ്യാൻ documents ഇല്ലായിരുന്നു." failed = "Run പരാജയപ്പെട്ടു: {{error}}" +inFlight = "പ്രവർത്തിപ്പിക്കാൻ പുതിയത് ഒന്നുമില്ല: മുമ്പത്തെ run-ൽ നിന്നുള്ള ഡോക്യുമെന്റുകൾ ഇപ്പോഴും process ചെയ്യുന്നു." +parked_one = "പ്രവർത്തിപ്പിക്കാൻ ഒന്നുമില്ല: {{count}} ഡോക്യുമെന്റ് മുമ്പ് പരാജയപ്പെട്ടു, parked ആണ്. കാരണം പരിഹരിക്കുക, തുടർന്ന് വീണ്ടും ശ്രമിക്കാൻ history മായ്ക്കുക." +parked_other = "പ്രവർത്തിപ്പിക്കാൻ ഒന്നുമില്ല: {{count}} ഡോക്യുമെന്റുകൾ മുമ്പ് പരാജയപ്പെട്ടു, parked ആണ്. കാരണം പരിഹരിക്കുക, തുടർന്ന് വീണ്ടും ശ്രമിക്കാൻ history മായ്ക്കുക." running = "Run ആരംഭിച്ചു; ഇപ്പോഴും പുരോഗമിക്കുന്നു." timeout = "Run പ്രതീക്ഷിച്ചതിലും കൂടുതൽ സമയം എടുക്കുന്നു; അത് background-ൽ പൂർത്തിയാകാം." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "ഉറവിടങ്ങൾ" status = "സ്ഥിതി" steps = "Steps" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Folder watch" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Scheduled" [portal.policies] +defaultName = "{{category}} നയം" subtitle = "ഓരോ ഡോക്യുമെന്റിലും ഒരു tool pipeline നടപ്പാക്കുന്ന standing automations. ഓരോ policy-യും upload അല്ലെങ്കിൽ export സമയത്ത് fire ചെയ്യുന്നു, അതിന്റെ tool chain run ചെയ്ത് enforced version original-ിനൊപ്പം save ചെയ്യുന്നു." title = "നയങ്ങൾ" [portal.policies.card] comingSoon = "Enterprise-ലേക്ക് upgrade ചെയ്യുക" notSetUp = "സജ്ജമാക്കിയിട്ടില്ല" +requiresAiEngine = "AI engine ആവശ്യമാണ്" + +[portal.policies.categories.classification] +desc = "upload ചെയ്യുമ്പോൾ ഓരോ ഡോക്യുമെന്റിന്റെയും type തിരിച്ചറിഞ്ഞ് filing-നും search-നും metadata tag ചെയ്യുക." +label = "വർഗ്ഗീകരണം" [portal.policies.categories.compliance] desc = "ഓരോ ഡോക്യുമെന്റിലും HIPAA, GDPR, SOC 2, അല്ലെങ്കിൽ FedRAMP ആവശ്യകതകൾ enforce ചെയ്യുക." @@ -7344,6 +7927,13 @@ label = "Security" [portal.policies.config] scopeAll = "എല്ലാ ഡോക്യുമെന്റുകളും" +[portal.policies.config.classification] +summary = "upload ചെയ്യുന്ന ഓരോ ഡോക്യുമെന്റിനെയും നിങ്ങളുടെ team-ന്റെ labels-നോട് classify ചെയ്ത് അതിന്റെ metadata tag ചെയ്യുന്നു." + +[portal.policies.config.classification.rules] +0 = "Classify" +1 = "metadata tag ചെയ്യുക" + [portal.policies.config.compliance] summary = "System വിട്ടുപോകുന്നതിന് മുമ്പ് regulatory frameworks-നെതിരെ documents validate ചെയ്യുന്നു." @@ -7371,6 +7961,29 @@ minConfidence = "കുറഞ്ഞ confidence" 2 = "Name" 3 = "Normalize" +[portal.policies.config.purview] +label = "Microsoft Purview sensitivity label പ്രയോഗിക്കുക" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "Purview portal-ിൽ നിന്നുള്ള label-ന്റെ GUID. ഒരു label പ്രയോഗിക്കുന്നത് അതിന്റെ metadata PDF-ലേക്ക് എഴുതും; encrypt ചെയ്യാൻ കഴിയില്ല, അതിന് Microsoft client ആവശ്യമാണ്." +labelName = "Label പേര് (optional)" +labelNameHelp = "ആളുകൾക്കും tools-നും label തിരിച്ചറിയാൻ ID-ക്കൊപ്പം രേഖപ്പെടുത്തുന്നു." + +[portal.policies.config.purview.fields.method] +help = "ഒരു policy പ്രയോഗിച്ച label-നെയും ഒരാൾ തിരഞ്ഞെടുത്ത label-നെയും Purview വേർതിരിക്കുന്നു." +label = "label എങ്ങനെ രേഖപ്പെടുത്തുന്നു" +privileged = "ഒരാൾ തിരഞ്ഞെടുത്തതുപോലെ" +standard = "സ്വയമേവ പ്രയോഗിച്ചതുപോലെ" + +[portal.policies.config.purviewRead] +label = "Microsoft Purview label വായിക്കുക" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "ഡോക്യുമെന്റ് ഇതിനകം വഹിക്കുന്ന label വായിക്കുന്നു, അതിനാൽ പിന്നീട് steps-ന് അതനുസരിച്ച് പ്രവർത്തിക്കാം." + [portal.policies.config.retention] summary = "ഡോക്യുമെന്റുകൾ എത്രകാലം സൂക്ഷിക്കണം, എപ്പോൾ archive ചെയ്യണം, എപ്പോൾ delete ചെയ്യണം എന്നിവ enforce ചെയ്യുന്നു." @@ -7438,6 +8051,7 @@ title = "ഇതുവരെ പ്രവർത്തനമൊന്നുമി [portal.policies.endpoints] addWatermark = "Watermark" autoRedact = "PII റിഡാക്റ്റ് ചെയ്യുക" +classifyAndLabel = "Classify" compressPdf = "കംപ്രസ് ചെയ്യുക" flatten = "ഫ്ലാറ്റൻ ചെയ്യുക" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "നിങ്ങളുടെ നയങ്ങൾ സംരക്ഷ retry = "വീണ്ടും ശ്രമിക്കുക" title = "Backend ലഭ്യമല്ല" +[portal.policies.operations] +change = "ഈ step ചെയ്യുന്നത് മാറ്റുക" +noResults = "അതുമായി പൊരുത്തപ്പെടുന്ന step ഇല്ല. ഒരു product name, അല്ലെങ്കിൽ \"scan\", \"notify\", \"attach\" ശ്രമിക്കുക." +searchPlaceholder = "steps തിരയുക - \"scan\", \"attach\" അല്ലെങ്കിൽ product name ശ്രമിക്കുക" + +[portal.policies.operations.bodyMode] +binary = "Raw file bytes" +json = "JSON payload-ന്റെ ഉള്ളിൽ" +multipart = "file upload ആയി (multipart)" + +[portal.policies.operations.clamavScan] +description = "നിങ്ങളുടെ സ്വന്തം ClamAV server ഡോക്യുമെന്റ് പരിശോധിക്കുന്നു; നിങ്ങളുടെ network-ിന് പുറത്തേക്ക് ഒന്നും പോകുന്നില്ല." +label = "viruses-നായി scan ചെയ്യുക (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "macros, executables, scripts അല്ലെങ്കിൽ embedded objects ഉള്ള ഡോക്യുമെന്റുകൾ നിരസിക്കുന്നു." +label = "active content block ചെയ്യുക" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive ഡോക്യുമെന്റ് പരിശോധിക്കുന്നു, അത് clean അല്ലെങ്കിൽ run നിർത്തും." +label = "viruses-നായി scan ചെയ്യുക" + +[portal.policies.operations.confluenceAttach] +description = "processed ഡോക്യുമെന്റ് ഒരു page-ൽ file ചെയ്യുന്നു." +label = "Confluence page-ലേക്ക് attach ചെയ്യുക" + +[portal.policies.operations.consignoSubmit] +description = "signing-നായി ഡോക്യുമെന്റ് ConsignO Cloud-ലേക്ക് submit ചെയ്യുന്നു." +label = "signature-നായി അയയ്ക്കുക (ConsignO)" +note = "ഡോക്യുമെന്റ് submit ചെയ്യുന്നു. signed copy retrieve ചെയ്യുന്നത് ഇപ്പോൾ പിന്തുണയ്ക്കുന്നില്ല, കാരണം ഒരു step-ന് പിന്നീട് വരുന്ന step-ലേക്ക് result കൈമാറാൻ കഴിയില്ല." + +[portal.policies.operations.customApiCall] +description = "call നിങ്ങൾ തന്നെ author ചെയ്യുക: path, method, body, response handling." +label = "custom API call ചെയ്യുക" + +[portal.policies.operations.discordNotify] +description = "policy ഒരു ഡോക്യുമെന്റ് കൈകാര്യം ചെയ്തതായി channel-നെ അറിയിക്കുന്നു." +label = "Discord-ലേക്ക് message post ചെയ്യുക" + +[portal.policies.operations.elasticIndex] +description = "policy ചെയ്തത് searchable document ആയി രേഖപ്പെടുത്തുന്നു." +label = "Elasticsearch-ൽ audit event index ചെയ്യുക" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart upload APIs-ന് അനുയോജ്യം; file payload-ന്റെ ഉള്ളിൽ വേണമെന്നുള്ള APIs-ന് JSON അനുയോജ്യം; binary raw bytes അയയ്ക്കുന്നു." +label = "ഡോക്യുമെന്റ് എങ്ങനെ അയയ്ക്കണം" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "അതേപടി അയക്കുന്ന JSON body. {{document.base64}} file വഹിക്കുന്നു; {{document.*}} ഉം {{run.*}} ഉം ഓരോ ഡോക്യുമെന്റിനും പൂരിപ്പിക്കും." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Account" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API ഡോക്യുമെന്റ് പ്രതീക്ഷിക്കുന്ന form field." +label = "File field name" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "From" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "header name/value pairs-ന്റെ JSON object. connection-ന്റെ credential നിങ്ങൾക്കായി ചേർക്കും." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "message body ആയി അയയ്ക്കുന്നു. {{document.*}} ഉം {{run.*}} ഉം ഓരോ ഡോക്യുമെന്റിനും പൂരിപ്പിക്കും." +label = "Message" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "connection-ന്റെ base URL-ലേക്ക് append ചെയ്യുന്നു. ഇത് ഒരിക്കലും മറ്റൊരു host-ലേക്ക് എത്തില്ല." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "drive-ൽ ഇത് എഴുതേണ്ട സ്ഥലം. {{document.filename}} ഓരോ ഡോക്യുമെന്റിനും പൂരിപ്പിക്കും." +label = "Destination path" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report ഡോക്യുമെന്റ് മാറ്റമില്ലാതെ വിടുന്നു. Replace മടങ്ങിവരുന്നതുമായി അത് മാറ്റിസ്ഥാപിക്കുന്നു." +label = "reply-യോടെ എന്ത് ചെയ്യണം" + +[portal.policies.operations.fields.signerEmail] +label = "Signer-ന്റെ email" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Subject" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio ഈ text വിശകലനം ചെയ്യുന്നു. {{document.title}} ഉപയോഗിക്കുക അല്ലെങ്കിൽ പരിശോധിക്കേണ്ട text paste ചെയ്യുക." +label = "പരിശോധിക്കേണ്ട text" + +[portal.policies.operations.fields.to] +label = "To" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud username" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "policy ഒരു ഡോക്യുമെന്റ് കൈകാര്യം ചെയ്തതായി space-നെ അറിയിക്കുന്നു." +label = "Google Chat-ലേക്ക് message post ചെയ്യുക" + +[portal.policies.operations.jiraAttach] +description = "processed ഡോക്യുമെന്റ് ഒരു issue-ലേക്ക് file ചെയ്യുന്നു." +label = "Jira issue-ലേക്ക് attach ചെയ്യുക" + +[portal.policies.operations.mailgunEmail] +description = "processed ഡോക്യുമെന്റ് attachment ആയി അയയ്ക്കുന്നു." +label = "ഡോക്യുമെന്റ് email ചെയ്യുക (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "processed ഡോക്യുമെന്റ് ഒരു folder-ലേക്ക് എഴുതുന്നു." +label = "Nextcloud-ലേക്ക് upload ചെയ്യുക" + +[portal.policies.operations.presidioAnalyze] +description = "നിങ്ങളുടെ സ്വന്തം Presidio server കണ്ടെത്തുന്ന personal data report ചെയ്യുന്നു." +label = "personal data കണ്ടെത്തുക" +note = "Presidio text വായിക്കുന്നു, files അല്ല. പരിശോധിക്കേണ്ട text നൽകുക — ഡോക്യുമെന്റ് തന്നെ upload ചെയ്യുന്നില്ല." + +[portal.policies.operations.responseMode] +replace = "ഡോക്യുമെന്റിനെ reply ഉപയോഗിച്ച് മാറ്റിസ്ഥാപിക്കുക" +report = "ഡോക്യുമെന്റ് നിലനിർത്തുക, reply രേഖപ്പെടുത്തുക" + +[portal.policies.operations.sendgridEmail] +description = "processed ഡോക്യുമെന്റ് attachment ആയി അയയ്ക്കുന്നു." +label = "ഡോക്യുമെന്റ് email ചെയ്യുക (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "policy ഒരു ഡോക്യുമെന്റ് കൈകാര്യം ചെയ്തതായി channel-നെ അറിയിക്കുന്നു." +label = "Slack-ലേക്ക് message post ചെയ്യുക" + +[portal.policies.operations.splunkEvent] +description = "നിങ്ങളുടെ auditors ഇതിനകം നോക്കുന്നിടത്ത് policy ചെയ്തത് രേഖപ്പെടുത്തുന്നു." +label = "Splunk-ലേക്ക് audit event അയയ്ക്കുക" + +[portal.policies.operations.sumologicEvent] +description = "policy ചെയ്തത് HTTP collector-ലേക്ക് രേഖപ്പെടുത്തുന്നു." +label = "Sumo Logic-ലേക്ക് audit event അയയ്ക്കുക" + +[portal.policies.operations.teamsNotify] +description = "policy ഒരു ഡോക്യുമെന്റ് കൈകാര്യം ചെയ്തതായി channel-നെ അറിയിക്കുന്നു." +label = "Teams-ലേക്ക് message post ചെയ്യുക" + +[portal.policies.operations.webhookPost] +description = "നിങ്ങൾ തിരഞ്ഞെടുക്കുന്ന URL-ലേക്ക് ഡോക്യുമെന്റ് തന്നെ അയയ്ക്കുന്നു." +label = "ഡോക്യുമെന്റ് webhook-ലേക്ക് post ചെയ്യുക" + +[portal.policies.operations.zapierNotify] +description = "run-ന്റെ details നിങ്ങളുടെ automation-ലേക്ക് കൈമാറുന്നു." +label = "Zap അല്ലെങ്കിൽ Make scenario trigger ചെയ്യുക" + [portal.policies.stats] activeFor = "സജീവം" dataProcessed = "പ്രോസസ്സ് ചെയ്ത ഡാറ്റ" @@ -7473,6 +8282,13 @@ label = "നടപ്പാക്കിയ ഡോക്യുമെന്റു description = "ക്രമീകരിച്ചിട്ടുണ്ടെങ്കിലും പ്രവർത്തിക്കുന്നില്ല" label = "താൽക്കാലികമായി നിർത്തിയത്" +[portal.policies.table] +appliesTo = "ബാധകമാകുന്നത്" +docs = "നടപ്പാക്കിയ Docs" +enforces = "Enforces" +policy = "നയം" +status = "Status" + [portal.policies.wizard.actions] back = "പിന്നോട്ട്" cancel = "റദ്ദാക്കുക" @@ -7480,6 +8296,10 @@ continue = "തുടരുക" enablePolicy = "നയം പ്രവർത്തനക്ഷമമാക്കുക" saveChanges = "മാറ്റങ്ങൾ സംരക്ഷിക്കുക" +[portal.policies.wizard.capability.classify] +desc = "നിങ്ങളുടെ team-ന്റെ labels-ൽ നിന്ന് ഡോക്യുമെന്റിന്റെ type തിരിച്ചറിഞ്ഞ് അത് tag ചെയ്യുന്നു, അതിനാൽ category പ്രകാരം file ചെയ്യാനും search ചെയ്യാനും കഴിയും." +label = "ഡോക്യുമെന്റ് classify ചെയ്യുക" + [portal.policies.wizard.capability.compress] desc = "ഡോക്യുമെന്റിനെ ചെറിയ ഫയൽ വലുപ്പത്തിലേക്ക് കംപ്രസ് ചെയ്യുന്നു." label = "ഫയൽ വലുപ്പം കുറയ്ക്കുക" @@ -7500,10 +8320,18 @@ label = "സെൻസിറ്റീവ് വിവരങ്ങൾ റിഡാ desc = "ഡോക്യുമെന്റ് തുറക്കുമ്പോൾ ഒന്നും സ്വയമേവ പ്രവർത്തിക്കാതിരിക്കാൻ മറഞ്ഞിരിക്കുന്ന JavaScript നീക്കംചെയ്യുന്നു." label = "സജീവ ഉള്ളടക്കം നീക്കംചെയ്യുക" +[portal.policies.wizard.capability.timestampPdf] +desc = "സ്വതന്ത്ര timestamp authority ഉപയോഗിച്ച് ഡോക്യുമെന്റ് ഒരു സമയബിന്ദുവിൽ ഈ കൃത്യമായ രൂപത്തിൽ ഉണ്ടായിരുന്നുവെന്ന് തെളിയിക്കുന്നു. ഒരു hash മാത്രമാണ് അയയ്ക്കുന്നത് - ഡോക്യുമെന്റ് നിങ്ങളുടെ server വിട്ടുപോകുന്നില്ല." +label = "trusted timestamp ചേർക്കുക" + [portal.policies.wizard.capability.watermark] desc = "ഓരോ പേജിലും കാണാവുന്ന അടയാളം (ഉദാ. “Confidential”) പതിക്കുന്നു." label = "Watermark പ്രയോഗിക്കുക" +[portal.policies.wizard.classification] +description = "upload ചെയ്യുന്ന ഓരോ ഡോക്യുമെന്റും built-in labels-നോട് classify ചെയ്ത് പൊരുത്തപ്പെടുന്ന types ഉപയോഗിച്ച് tag ചെയ്യുന്നു. Label set നിങ്ങളുടെ മുഴുവൻ team-ലും shared ആണ്." +labelsHeading = "Classification labels" + [portal.policies.wizard.errors] noTools = "ആദ്യം workflow-ൽ കുറഞ്ഞത് ഒരു tool എങ്കിലും പ്രവർത്തനക്ഷമമാക്കുക." saveFailed = "നയം സംരക്ഷിക്കാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." @@ -7548,73 +8376,118 @@ setUp = "{{category}} നയം സജ്ജീകരിക്കുക" [portal.policies.wizard.workflow] description = "ഈ നയം പ്രോസസ്സ് ചെയ്യുന്ന ഓരോ ഡോക്യുമെന്റിനും എന്ത് ചെയ്യണമെന്ന് തിരഞ്ഞെടുക്കുക." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} സജീവം" -noRule = "ഇതുവരെ നിയമം നടപ്പാക്കിയിട്ടില്ല" -subtitle = "ഏത് pipeline ആണ് കൈകാര്യം ചെയ്യുന്നതെന്നതുമായി ബന്ധമില്ലാതെ, ഓരോ ഡോക്യുമെന്റും കടന്നുപോകുന്ന സ്ഥിരമായ ഓട്ടോമേഷനുകൾ." -title = "നിങ്ങളുടെ PDFs-ൽ പ്രവർത്തിക്കുന്നത്" - [portal.policySummary.action] -comingSoon = "Enterprise-ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക" -configure = "ക്രമീകരിക്കുക" setUp = "സജ്ജീകരിക്കുക" -[portal.policySummary.column] -activeRule = "സജീവ നിയമം" -policy = "നയം" -status = "സ്ഥിതി" +[portal.processorFlow] +footnote = "കണക്കുകൾ കഴിഞ്ഞ 24 മണിക്കൂറിനുള്ളതാണ്. Flow speed സൂചനയ്ക്കായി മാത്രം." +liveBadge = "Live" +stats = "{{connected}} connected · {{processed}} PDFs processed" +title = "PDF Processor" -[portal.policySummary.empty] -description = "നയങ്ങൾ ക്രമീകരിച്ചുകഴിഞ്ഞാൽ വിഭാഗങ്ങൾ ഇവിടെ പ്രത്യക്ഷപ്പെടും." -title = "ഇതുവരെ നയങ്ങളില്ല" +[portal.processorFlow.lens] +ariaLabel = "View mode" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "സജീവം" -off = "ഓഫ്" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "പരാജയപ്പെട്ടു" +heading = "Outcomes" +success = "Delivered" + +[portal.processorFlow.policies] +activeCount = "{{n}} active" +count = "{{n}} · 24h" +heading = "Policies" +setUp = "സജ്ജീകരിക്കുക" soon = "ഉടൻ" -[portal.processingStatus] -managePlan = "പ്ലാൻ നിയന്ത്രിക്കുക" -volumeSuffix = "പ്രോസസ്സ് ചെയ്ത PDFs · കഴിഞ്ഞ 30 ദിവസം" +[portal.processorFlow.sankey] +waist = "Policies · {{n}} active" + +[portal.processorFlow.sankey.empty] +description = "processor വഴി ഡോക്യുമെന്റുകൾ flow ചെയ്യുന്നത് കാണാൻ ഒരു source ബന്ധിപ്പിച്ച് ഒരു policy on ചെയ്യുക." +title = "ഇതുവരെ flow ഇല്ല" + +[portal.processorFlow.sources] +comingSoonTag = "Connect" +editor = "Stirling PDF Editor" +heading = "Sources" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email intake" [portal.procurement] -reset = "പ്രോക്യൂർമെന്റ് റീസെറ്റ് ചെയ്യുക (ഡെമോ)" subtitle = "നിങ്ങളുടെ ടീമിനെ വിലയിരുത്തി, കരാറിലാക്കി, ഓൺബോർഡ് ചെയ്യുക. ഓരോ ഡോക്യുമെന്റും ഒരിടത്ത്." title = "പ്രോക്യൂർമെന്റ്" -[portal.procurement.action] -download = "ഡൗൺലോഡ്" -pay = "ഇപ്പോൾ അടയ്ക്കുക" -request = "അഭ്യർത്ഥിക്കുക" -sign = "പരിശോധിച്ച് ഒപ്പിടുക" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "അംഗീകരിച്ച് subscribe ചെയ്യുക" +confidential = "Confidential" confirm = "ഞാൻ Stirling Enterprise Agreement വായിച്ചു, അംഗീകരിക്കുന്നു." -eyebrow = "കരാർ" -intro = "Master Service Agreement, Order Form, EULA, Data Processing Agreement എന്നിവ ഉൾപ്പെടെ നിങ്ങളുടെ ഡീലിനെ ഒരു സംയുക്ത കരാർ കവർ ചെയ്യുന്നു. അത് പരിശോധിച്ച്, ക്വോട്ട് committed subscription ആയി സ്വീകരിക്കാൻ സമ്മതിക്കുക." -title = "നിങ്ങളുടെ enterprise agreement പരിശോധിക്കുക" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA, DPA എന്നിവ ഒരൊറ്റ signature-ലേക്ക് സംയോജിപ്പിച്ചത്." +download = "ഡൗൺലോഡ്" +downloadDraftError = "agreement PDF generate ചെയ്യാനായില്ല - document renderer ലഭ്യമല്ല. ദയവായി കുറച്ച് കഴിഞ്ഞ് വീണ്ടും ശ്രമിക്കുക അല്ലെങ്കിൽ support-നെ ബന്ധപ്പെടുക." +downloadError = "signed agreement ഡൗൺലോഡ് ചെയ്യാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." +legalName = "Legal entity name" +legalNamePlaceholder = "sign ചെയ്യുന്ന legal entity" +loadError = "agreement load ചെയ്യാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." +loading = "agreement load ചെയ്യുന്നു..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "മാറ്റങ്ങൾ അഭ്യർത്ഥിക്കുക" +scrollHint = "review ചെയ്യാൻ scroll ചെയ്യുക" +signatory = "Signatory name" +signatoryPlaceholder = "പൂർണ്ണ പേര്" +signatoryTitle = "Signatory title" +signatoryTitlePlaceholder = "ഉദാ. General Counsel" +signError = "നിങ്ങളുടെ signature record ചെയ്യാനായില്ല. ദയവായി വീണ്ടും ശ്രമിക്കുക." [portal.procurement.builder] addons = "Add-ons" +addressLine1 = "Address line 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Address line 2" +addressLine2Placeholder = "Suite, floor, building" back = "പിന്നോട്ട്" businessName = "ബിസിനസ് പേര്" businessNamePlaceholder = "നിങ്ങളുടെ കമ്പനി" +city = "City" +cityPlaceholder = "San Francisco" +completeRequired = "quote generate ചെയ്യുന്നതിന് മുമ്പ് ആവശ്യമായ fields ( * അടയാളപ്പെടുത്തിയവ) valid email സഹിതം പൂർത്തിയാക്കുക." +contactEmail = "Contact email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Contact name" +contactNamePlaceholder = "Jane Doe" continue = "തുടരുക" -country = "രാജ്യം" -countryEuro = "Eurozone (EUR €)" -countryUK = "United Kingdom (GBP £)" -countryUS = "United States (USD $)" +done = "പൂർത്തിയായി" eula = "ഞാൻ Stirling Enterprise EULA വായിച്ചു, അംഗീകരിക്കുന്നു. ഈ ക്വോട്ടിൽ നിന്ന് സൃഷ്ടിക്കുന്ന കരാറിനെ ഇത് നിയന്ത്രിക്കുന്നു." generate = "ക്വോട്ട് സൃഷ്ടിക്കുക" -included = "ഉൾപ്പെടുത്തിയിട്ടുണ്ട്" indemnification = "IP indemnification" indemnificationSub = "EULA പ്രകാരം യോഗ്യമായ IP ക്ലെയിമുകൾ ഞങ്ങൾ പ്രതിരോധിക്കുന്നു" -offlineLicense = "Offline / air-gapped ലൈസൻസ്" -offlineLicenseSub = "air-gapped self-hosted instance-നായി ഡൗൺലോഡ് ചെയ്യാവുന്ന ലൈസൻസ് ഫയൽ" +paperEyebrow = "Enterprise quote" +paperFor = "Prepared for" +pdfSize = "PDF size" +poNumber = "PO number" +poNumberPlaceholder = "Optional" +postalCode = "Postal code" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} policies" +posture_essentials = "Essentials" +posture_essentialsSub = "Classification + Sharing — defaults" +posture_governed = "Governed" +posture_governedSub = "Security, Routing എന്നിവ ചേർക്കുന്നു" +posture_regulated = "Regulated" +posture_regulatedSub = "ഓരോ category — Compliance, Retention, Ingestion" qbr = "ത്രൈമാസ ബിസിനസ് അവലോകനങ്ങൾ" qbrSub = "നിങ്ങളുടെ SE ഓരോ ത്രൈമാസവും ഉപയോഗവും roadmap-ഉം അവലോകനം ചെയ്യും" +region = "State / region" +regionPlaceholder = "California" running = "{{annual}} / yr · {{years}}-yr {{tcv}}" s1Sub = "നിങ്ങളുടെ ടീവും ഓരോ വർഷവും പ്രവർത്തിപ്പിക്കുമെന്ന് പ്രതീക്ഷിക്കുന്ന PDFs-ഉം." s1Title = "നിങ്ങൾ എത്ര പ്രോസസ്സ് ചെയ്യും?" @@ -7623,6 +8496,12 @@ s2Title = "Commitment and service" s3Sub = "ക്വോട്ടിനും അതിൽ നിന്ന് സൃഷ്ടിക്കുന്ന കരാറിനുമായി." s3Title = "നിങ്ങളുടെ വിശദാംശങ്ങൾ" serviceLevel = "Service level" +size_compact = "Compact" +size_compactSub = "കൂടുതലും text, 1 MB-ൽ താഴെ" +size_heavy = "Heavy" +size_heavySub = "Scanned അല്ലെങ്കിൽ image-heavy, 5 MB+" +size_standard = "Standard" +size_standardSub = "text, images എന്നിവ mix ചെയ്തത്, 1 മുതൽ 5 MB വരെ" slDedicated = "Dedicated" slDedicatedSub = "4 ബിസിനസ് മണിക്കൂർ · dedicated account manager · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "അതേ ബിസിനസ് ദിവസം · named CSM · slStandard = "Standard" slStandardSub = "അടുത്ത ബിസിനസ് ദിവസം · shared CSM · ഉൾപ്പെടുത്തി" stepOf = "ഘട്ടം {{n}} / {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Optional" term = "കാലാവധി" termDiscount = "{{pct}}% multi-year commitment discount പ്രയോഗിച്ചു" title = "നിങ്ങളുടെ ക്വോട്ട് നിർമ്മിക്കുക" @@ -7637,6 +8518,7 @@ training = "Onboarding & training" trainingSub = "നിങ്ങളുടെ ടീമിനെ പ്രവർത്തനക്ഷമമാക്കാൻ live sessions" users = "മൊത്തം ഉപയോക്താക്കൾ" usersPlaceholder = "ഉദാ. 250" +viewEula = "വായിക്കുക" volEstimated = "{{count}} ഉപയോക്താക്കളിൽ നിന്ന് കണക്കാക്കി (~ഓരോരുത്തർക്കും 2,000 PDFs, automation ഉൾപ്പെടെ). നിങ്ങൾക്ക് കൂടുതൽ കൃത്യമായി അറിയുമെങ്കിൽ തിരുത്തുക." volManual = "നിങ്ങളുടെ കണക്ക് ഉപയോഗിക്കുന്നു. ഏത് സമയത്തും നിങ്ങളുടെ ടീം വലുപ്പത്തിൽ നിന്ന് വീണ്ടും കണക്കാക്കാം." volNoUsers = "ഉറപ്പില്ലേ? നിങ്ങളുടെ ടീം വലുപ്പം നൽകൂ, ഞങ്ങൾ കണക്കാക്കാം." @@ -7645,55 +8527,55 @@ volumePlaceholder = "ഉദാ. 1,000,000" years_one = "{{count}} വർഷം" years_other = "{{count}} വർഷങ്ങൾ" -[portal.procurement.docs] -count_one = "{{count}} ഡോക്യുമെന്റ്" -count_other = "{{count}} ഡോക്യുമെന്റുകൾ" -done = "പൂർത്തിയായി" -here = "നിങ്ങൾ ഇവിടെ" -hide = "മറയ്ക്കുക" -optional = "ഓപ്ഷണൽ" -paidAddon = "Paid add-on" -show = "കാണിക്കുക" -subtitle = "യാത്രയിലെ ഓരോ ഘട്ടത്തിലും നിങ്ങൾക്ക് വേണ്ടതെല്ലാം, deal മുന്നേറുമ്പോൾ പ്രദർശിപ്പിക്കുന്നു." -supportingSubtitle = "SOC 2, security reviews, tax forms എന്നിവയും അതിലധികവും, നിങ്ങളുടെ security അല്ലെങ്കിൽ procurement ടീം ചോദിക്കുമ്പോൾ തയ്യാറായി. ചിലതിന് ഒറ്റത്തവണ ഫീസ് ഉണ്ടായിരിക്കും." -supportingTitle = "നിങ്ങളുടെ മൂല്യനിർണ്ണയത്തെ പിന്തുണയ്ക്കുന്നു" -title = "ഡോക്യുമെന്റുകൾ" -upcoming = "വരാനിരിക്കുന്നത്" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form, DPA എന്നിവ ഒരൊറ്റ signature-ൽ." +download = "ഡൗൺലോഡ്" +eula = "EULA & Commercial Terms" +eulaSub = "software-വും self-serve commercial terms-ും." +invoice = "Invoice" +invoiceSub = "നിങ്ങളുടെ ആദ്യ subscription invoice." +laterInvoice = "നിങ്ങൾ accept ചെയ്ത ശേഷം ലഭ്യം" +laterQuote = "നിങ്ങളുടെ quote issue ചെയ്താൽ ലഭ്യം" +quote = "Quote" +quoteSub = "നിങ്ങളുടെ itemised enterprise quote." +sla = "SLA Exhibit" +slaSub = "Support tiers, response targets എന്നിവ." +subprocessors = "Subprocessors" +subprocessorsSub = "നിങ്ങളുടെ വേണ്ടി data process ചെയ്യുന്ന third parties." +subtitle = "നിങ്ങളുടെ deal paperwork, ഏത് സമയത്തും ലഭ്യം." +title = "Documents" +view = "കാണുക" [portal.procurement.error] title = "എന്തോ പിശക് സംഭവിച്ചു" [portal.procurement.hero] -company = "നിങ്ങളുടെ enterprise deal" +barAria = "Stage {{current}} of {{total}}" ctaAgreement = "കരാർ പരിശോധിച്ച് ഒപ്പിടുക" -ctaLive = "നിങ്ങൾ live ആണ്" -ctaPayment = "പേയ്മെന്റ് ചേർക്കുക" +ctaExploring = "നിങ്ങളുടെ trial സജ്ജീകരിക്കുക" ctaQuote = "നിങ്ങളുടെ ക്വോട്ട് പരിശോധിക്കുക" +ctaReviewQuote = "quote review ചെയ്യുക" ctaTrial = "നിങ്ങളുടെ ക്വോട്ട് നിർമ്മിക്കുക" +documents = "Documents" eyebrow = "Enterprise procurement" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "ടീം അംഗങ്ങളെ ക്ഷണിക്കുക" -keyDocs = "പ്രധാന ഡോക്യുമെന്റുകൾ" -nextStep = "അടുത്ത ഘട്ടം: {{action}}" -notStarted = "ആരംഭിച്ചിട്ടില്ല" +licenseKey = "Licence key" +liveSub = "നിങ്ങളുടെ organization provision ചെയ്തു, നിങ്ങളുടെ licence active ആണ്." +liveTitle = "നിങ്ങൾ Stirling Enterprise-ൽ live ആണ്" +next = "Next: {{stage}}" open = "പ്രോക്യൂർമെന്റ് തുറക്കുക" scheduleCall = "കോൾ ഷെഡ്യൂൾ ചെയ്യുക" -setup1Sub = "നിങ്ങളുടെ ടീം അംഗങ്ങളെ ക്ഷണിക്കുക" -setup1Title = "PDF Editor വിന്യസിക്കുക" -setup2Sub = "എഡിറ്ററുകളിലും മറ്റു ഉറവിടങ്ങളിലും processing ഓൺ ചെയ്യുക" -setup2Title = "PDF Processor കണക്റ്റ് ചെയ്യുക" -setup3Sub = "ആവശ്യമുള്ളപ്പോൾ Security, Compliance, Routing, അല്ലെങ്കിൽ Retention ഓൺ ചെയ്യുക" -setup3Title = "ശുപാർശ ചെയ്യുന്ന നയങ്ങൾ ചേർക്കുക" +sentenceAgreement = "agreement review ചെയ്ത് sign ചെയ്യുക" +sentenceLive = "provisioning and rollout" +sentencePayment = "purchase order and payment" +sentenceQuote = "നിങ്ങളുടെ quote review ചെയ്ത് accept ചെയ്യുക" +sentenceTrial = "നിങ്ങളുടെ trial workspace സജ്ജീകരിക്കുക" [portal.procurement.journey] daysLeft_one = "{{count}} ദിവസം ശേഷിക്കുന്നു" daysLeft_other = "{{count}} ദിവസങ്ങൾ ശേഷിക്കുന്നു" -engineerLabel = "നിങ്ങളുടെ solutions engineer" -eyebrow = "നിങ്ങളുടെ rollout" -live = "നിങ്ങൾ Stirling Enterprise-ൽ live ആണ്" -nextStep = "അടുത്ത ഘട്ടം: {{action}}" -subtitle = "നിങ്ങളുടെ solutions engineer ഓരോ ഘട്ടത്തിലും നിങ്ങളോടൊപ്പം. ഒരേസമയം ഒരു അടുത്ത പ്രവർത്തനം; പൂർണ്ണ checklist താഴെയുണ്ട്." -title = "Trial മുതൽ live വരെ, ഒരു guided path" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "ഒരു signature MSA, order form, EULA, DPA എന്നിവ കവർ ചെയ്യുന്നു." @@ -7727,6 +8609,9 @@ downloadError = "Offline ലൈസൻസ് ഫയൽ ഇതുവരെ സൃ downloadOffline = "Offline ലൈസൻസ് (.lic) ഡൗൺലോഡ് ചെയ്യുക" hint = "ഇത് activate ചെയ്യാൻ ഈ key self-hosted instance-ൽ ഒട്ടിക്കുക, അല്ലെങ്കിൽ നിങ്ങളുടെ രേഖകൾക്കായി സൂക്ഷിക്കുക. സുരക്ഷിതമായി സൂക്ഷിക്കുക." label = "നിങ്ങളുടെ ലൈസൻസ് key" +subtitle = "ഈ key ഉപയോഗിച്ച് self-hosted instance activate ചെയ്യുക, അല്ലെങ്കിൽ നിങ്ങളുടെ records-നായി സൂക്ഷിക്കുക." +title = "നിങ്ങളുടെ licence key" +trialFileHint = "ഇത് നിങ്ങളുടെ trial licence file ആണ്. നിങ്ങളുടെ agreement പ്രാബല്യത്തിൽ വന്നാൽ, തിരികെ വന്ന് ഇത് വീണ്ടും ഡൗൺലോഡ് ചെയ്യുക — online licence key-നെ പോലെ അല്ല, .lic file സ്വയം update ചെയ്യില്ല." [portal.procurement.link] cta = "അക്കൗണ്ട് ലിങ്ക് ചെയ്യുക" @@ -7739,65 +8624,62 @@ description = "നിങ്ങളുടെ subscription സജീവമാണ് eyebrow = "Live" title = "നിങ്ങൾ Stirling Enterprise-ൽ live ആണ്" -[portal.procurement.locked] -description = "Enterprise evaluation ആരംഭിച്ചുകഴിഞ്ഞാൽ trial keys, committed-volume quotes, one-signature agreement, payment, നിങ്ങളുടെ document ledger എന്നിവ എല്ലാം ഇവിടെ ഉണ്ടാകും." -eyebrow = "Enterprise മാത്രം" -talkToSales = "Sales-നോട് സംസാരിക്കുക" -title = "പ്രോക്യൂർമെന്റ് ട്രാക്ക് Enterprise-ോടെയാണ് തുറക്കുന്നത്" - [portal.procurement.milestone] -accept = "അംഗീകരിച്ച് തുടരുക" -description = "നിങ്ങളുടെ ടീമുമായി പങ്കിടാൻ PDF ഡൗൺലോഡ് ചെയ്യുക, തയ്യാറാകുമ്പോൾ അംഗീകരിക്കാൻ തിരികെ വരുക, അല്ലെങ്കിൽ മാറ്റങ്ങൾ വരുത്തുക." -download = "PDF ഡൗൺലോഡ് ചെയ്യുക" downloadError = "ക്വോട്ട് PDF ഇതുവരെ ഡൗൺലോഡ് ചെയ്യാനായില്ല — ദയവായി കുറച്ച് കഴിഞ്ഞ് വീണ്ടും ശ്രമിക്കുക." -edit = "ക്വോട്ട് തിരുത്തുക" -eyebrow = "ക്വോട്ട് {{number}}" -perYear = " / yr" -preparedFor = "{{company}}-നായി തയ്യാറാക്കി" -tcv = "{{value}} total contract value" -title = "നിങ്ങളുടെ ക്വോട്ട് തയ്യാറാണ്" - -[portal.procurement.modal] -cancel = "റദ്ദാക്കുക" -chooseFile = "ഫയൽ തിരഞ്ഞെടുക്കുക" -close = "അടയ്ക്കുക" -downloadBody = "നിങ്ങളുടെ ഡൗൺലോഡ് ഉടൻ ആരംഭിക്കും." -downloadCta = "ഡൗൺലോഡ്" -downloadTitle = "ഡൗൺലോഡ്" -noFile = "ഫയൽ തിരഞ്ഞെടുത്തിട്ടില്ല" -payBody = "Stripe വഴി കാർഡോ bank transfer-ഓ ഉപയോഗിച്ച് നിങ്ങളുടെ committed contract അടയ്ക്കുക. പേയ്മെന്റ് clear ആയ ഉടൻ നിങ്ങളുടെ workspace provision ചെയ്യും." -payCta = "Stripe-ലേക്ക് തുടരുക" -payTitle = "പേയ്മെന്റ് സ്ഥിരീകരിക്കുക" -requestBodyFree = "ആവശ്യാനുസരണം ഞങ്ങൾ ഇത് സൃഷ്ടിക്കുന്നു. സ്ഥിരീകരിക്കുക; നിങ്ങളുടെ solutions engineer അത് ഉടൻ അയക്കും." -requestBodyPaid = "ഇത് paid add-on ആണ്. സ്ഥിരീകരിക്കുക; നിങ്ങളുടെ solutions engineer അത് scope ചെയ്ത് paperwork അയക്കും." -requestCta = "അഭ്യർത്ഥിക്കുക" -requestTitle = "ഈ ഡോക്യുമെന്റ് അഭ്യർത്ഥിക്കുക" -signBody = "E-signature-നായി Stirling Enterprise Agreement തുറക്കുന്നു: ഒരു signature MSA, order form, EULA, DPA എന്നിവ കവർ ചെയ്യുന്നു. ഞങ്ങൾ സ്വയമേവ countersign ചെയ്യും, തുടർന്ന് നിങ്ങൾ payment-ലേക്ക് മുന്നേറും." -signCta = "Signature-നായി തുറക്കുക" -signTitle = "നിങ്ങളുടെ കരാർ പരിശോധിച്ച് ഒപ്പിടുക" -uploadBody = "നിങ്ങളുടെ PO ഞങ്ങൾക്ക് അയയ്ക്കുക, നിങ്ങളുടെ terms പ്രകാരം ഞങ്ങൾ invoice ചെയ്യും. PDF വലിച്ചിടുക അല്ലെങ്കിൽ താഴെ നിന്ന് ഒരു ഫയൽ തിരഞ്ഞെടുക്കുക." -uploadCta = "Purchase order upload ചെയ്യുക" -uploadTitle = "നിങ്ങളുടെ purchase order upload ചെയ്യുക" [portal.procurement.payment] description = "നിങ്ങളുടെ ക്വോട്ട് അംഗീകരിച്ചു, ലൈസൻസ് ഇതിനകം സജീവമാണ് — നിങ്ങളുടെ ടീമിന് ഉടൻ തുടങ്ങാം. തയ്യാറാകുമ്പോൾ ആദ്യ invoice അടയ്ക്കുക; email ആവശ്യമില്ലാതെ ഇവിടെ അടയ്ക്കുകയോ ഡൗൺലോഡ് ചെയ്യുകയോ ചെയ്യാം." +downloadAgreement = "signed agreement ഡൗൺലോഡ് ചെയ്യുക" downloadInvoice = "Invoice ഡൗൺലോഡ് ചെയ്യുക" -simulate = "Payment received simulate ചെയ്യുക (ഡെമോ)" +eyebrow = "Payment" title = "Subscription സൃഷ്ടിച്ചു" viewInvoice = "Invoice കാണുക & അടയ്ക്കുക" +[portal.procurement.review] +acceptCta = "quote accept ചെയ്യുക" +annual = "Annual fee (year 1)" +downloadCta = "quote ഡൗൺലോഡ് ചെയ്യുക" +poNumber = "Purchase order: {{po}}" +renewal = "term-ന് ശേഷം {{amount}}/year ആയി പുതുക്കും (+{{pct}}% CPI)" +tcv = "{{years}}-year term, മുഴുവൻ തുകയും മുൻകൂർ അടച്ചു: {{tcv}}" +validUntil = "{{date}} വരെ valid" + [portal.procurement.schedule] fallback = "Scheduler ലോഡ് ചെയ്യാനായില്ല." fallbackLink = "Scheduling പുതിയ tab-ൽ തുറക്കുക" +loading = "scheduler load ചെയ്യുന്നു..." subtitle = "നിങ്ങളുടെ solutions engineer നിങ്ങളുടെ ടീമിനെ rollout-ിലൂടെ നയിക്കും. നിങ്ങൾക്ക് അനുയോജ്യമായ സമയം തിരഞ്ഞെടുക്കുക." title = "കോൾ ഷെഡ്യൂൾ ചെയ്യുക" -[portal.procurement.status] -action = "പ്രവർത്തനം ആവശ്യമാണ്" -available = "ലഭ്യം" -complete = "പൂർത്തിയായി" -pending = "ബാക്കി" -request = "അഭ്യർത്ഥന പ്രകാരം" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sealed, outbound ഇല്ല" +back = "പിന്നോട്ട്" +businessName = "Business name" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Managed, ഏറ്റവും വേഗത്തിലുള്ള start" +continue = "തുടരുക" +deployment = "എവിടെയാണ് deploy ചെയ്യേണ്ടത്?" +eula = "Stirling EULA & Commercial Terms ഞാൻ അംഗീകരിക്കുന്നു." +fullName = "പൂർണ്ണ പേര്" +fullNamePlaceholder = "നിങ്ങളുടെ പേര്" +invites = "teammates ക്ഷണിക്കുക · optional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "call schedule ചെയ്യുക" +seats = "പരിശോധിക്കുന്ന ആളുകൾ" +seatsPlaceholder = "ഉദാ. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker അല്ലെങ്കിൽ Kubernetes" +start = "trial ആരംഭിക്കുക" +stepOf = "Step {{n}} of {{total}}" +subtitle = "14 ദിവസത്തേക്ക് free, card ആവശ്യമില്ല." +subtitleDetails = "നിങ്ങളുടെ quote-നും agreement-നും കുറച്ച് details." +talkFirst = "ആദ്യം സംസാരിക്കണോ?" +title = "നിങ്ങളുടെ trial സജ്ജീകരിക്കുക" +viewEula = "വായിക്കുക" +workEmail = "Work email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "നീട്ടുന്നത് 7 ദിവസം ചേർക്കുകയും നിങ്ങളുടെ solutions engineer-നെ അറിയിക്കുകയും ചെയ്യും." @@ -7808,109 +8690,113 @@ maxed = "പരമാവധി എത്തി" subtitle = "നിങ്ങളുടെ free trial {{date}} വരെ പ്രവർത്തിക്കും. കാർഡ് ആവശ്യമില്ല." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Committed volume pricing, org-wide SSO + SCIM + RBAC, 90-day immutable audit, കൂടാതെ dedicated SE." -homeCta = "Trial ആരംഭിക്കുക →" -homeHeadline = "മില്ല്യൺ കണക്കിന് PDFs പ്രോസസ്സ് ചെയ്യുക." - -[portal.recentActivity] -title = "സമീപകാല പ്രവർത്തനം" -viewAll = "എല്ലാം കാണുക" - -[portal.recentActivity.empty] -description = "Pipeline runs, deploys, agent events എന്നിവ ഇവിടെ പ്രത്യക്ഷപ്പെടും." -title = "ഇവിടെ ഇതുവരെ ഒന്നുമില്ല" - -[portal.search] -ariaLabel = "തിരയുക" -placeholder = "Stirling തിരയുക — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Quick actions ലഭ്യമാകുമ്പോൾ ഇവിടെ പ്രത്യക്ഷപ്പെടും." -noActionsTitle = "Quick actions ഇല്ല" -noMatches = "\"{{query}}\" എന്നതിനൊന്നും പൊരുത്തങ്ങളില്ല" -noMatchesDescription = "മറ്റൊരു keyword ശ്രമിക്കുക അല്ലെങ്കിൽ catalogue browse ചെയ്യുക." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Account link" -[portal.shell.header] -accountFallback = "അക്കൗണ്ട്" -accountMenu = "അക്കൗണ്ട് menu" -darkMode = "Dark mode" -lightMode = "Light mode" -search = "തിരയുക" -searchPlaceholder = "തിരയുക…" -signOut = "Sign out" -switchToDark = "Dark theme-ലേക്ക് മാറ്റുക" -switchToLight = "Light theme-ലേക്ക് മാറ്റുക" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Stirling account ലിങ്ക് ചെയ്യുക" -planEditor = "Editor plan" -planEnterprise = "Enterprise plan" -planProcessor = "Processor plan" primaryNav = "Primary navigation" switchApp = "App മാറുക" +[portal.shell.topbar] +closeNav = "navigation അടയ്ക്കുക" +openNav = "navigation തുറക്കുക" +search = "തിരയുക" + [portal.sources] subtitle = "Stirling-ലേക്ക് ഡോക്യുമെന്റുകൾ നൽകുന്ന വീണ്ടും ഉപയോഗിക്കാവുന്ന input connections. ഒരു connection ഒരിക്കൽ configure ചെയ്ത്, പിന്നീട് എത്ര നയങ്ങളിൽ വേണമെങ്കിലും reference ചെയ്യുക. അതിന്റെ config-നും അത് ഉപയോഗിക്കുന്ന നയങ്ങൾക്കും ഒരു row ക്ലിക്ക് ചെയ്യുക." title = "ഉറവിടങ്ങൾ" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "ഉറവിടം കണക്റ്റ് ചെയ്യുക" +[portal.sources.builder] +back = "sources-ലേക്ക് മടങ്ങുക" +backToTypes = "എല്ലാ source types" +cancel = "റദ്ദാക്കുക" +chooseHint = "ഡോക്യുമെന്റുകൾ എവിടെ നിന്നാണ് വരുന്നതെന്ന് തിരഞ്ഞെടുക്കുക. Greyed-out connectors വരാനിരിക്കുകയാണ്." +comingSoon = "ഉടൻ വരുന്നു" +comingSoonHeading = "ഉടൻ വരുന്നു" +create = "source സൃഷ്ടിക്കുക" +createTitle = "ഒരു source ബന്ധിപ്പിക്കുക" +delete = "ഇല്ലാതാക്കുക" +editTitle = "source edit ചെയ്യുക" +enabled = "Enabled" +save = "മാറ്റങ്ങൾ save ചെയ്യുക" + +[portal.sources.builder.folderAccess] +description = "Folder automations-ന് administrator അനുവദിച്ച folders മാത്രമേ ഉപയോഗിക്കാനാകൂ. Folder Access settings-ൽ അത് ചേർത്ത ശേഷം വീണ്ടും ശ്രമിക്കുക." +openSettings = "Folder Access settings" +title = "ഈ folder അനുവദിച്ചിട്ടില്ല" + [portal.sources.delete] body = "\"{{name}}\" ഇല്ലാതാക്കണോ? ഇത് പഴയപടിയാക്കാനാവില്ല. അതിനെ reference ചെയ്യുന്ന നയങ്ങൾ update ചെയ്യേണ്ടിവരും." cancel = "റദ്ദാക്കുക" confirm = "ഇല്ലാതാക്കുക" title = "ഉറവിടം ഇല്ലാതാക്കണോ?" -[portal.sources.detail] -closeAriaLabel = "വിശദാംശം അടയ്ക്കുക" -delete = "ഉറവിടം ഇല്ലാതാക്കുക" -docs24h = "കഴിഞ്ഞ 24h" -docs30d = "കഴിഞ്ഞ 30 ദിവസം" -docsTotal = "മൊത്തം കണ്ടത്" -docsTrend = "കഴിഞ്ഞ 30 ദിവസങ്ങളിലെ ഡോക്യുമെന്റുകൾ" -documents = "ഡോക്യുമെന്റുകൾ" -edit = "തിരുത്തുക" -notReferenced = "ഒരു നയവും reference ചെയ്യുന്നില്ല, അതിനാൽ ഇത് ഇല്ലാതാക്കുന്നത് സുരക്ഷിതമാണ്." -pause = "താൽക്കാലികമായി നിർത്തുക" -resume = "പുനരാരംഭിക്കുക" -subtitle = "{{type}} · {{status}}" -usedBy = "ഉപയോഗിക്കുന്നത്" - -[portal.sources.empty] -description = "നിങ്ങളുടെ നയങ്ങൾക്ക് ഡോക്യുമെന്റുകൾ എടുക്കാൻ ഇടമുണ്ടാകാൻ ഒരു folder (ഉടൻ cloud storage-ും) കണക്റ്റ് ചെയ്യുക." -title = "ഇതുവരെ ഉറവിടങ്ങൾ കണക്റ്റ് ചെയ്തിട്ടില്ല" - [portal.sources.kpi] inUse = "ഉപയോഗത്തിലാണ്" total = "Connections" unused = "ഉപയോഗിക്കാത്തത്" +[portal.sources.networkFields.connection] +helperText = "server address, credentials എന്നിവയുള്ള stored connection. ഇതിനെ reference ചെയ്യുന്ന ഓരോ source-ഉം വീണ്ടും ഉപയോഗിക്കുന്നു." +label = "Connection" + +[portal.sources.networkFields.directory] +helperText = "poll ചെയ്യേണ്ട server-ിലെ folder, login home അല്ലെങ്കിൽ share root-നോട് relative. root-നായി blank വിടുക." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "ഓരോ policy-യും process ചെയ്ത ശേഷം Consume server-ിൽ നിന്ന് ഓരോ file-ഉം നീക്കം ചെയ്യുന്നു." +label = "Read mode" + +[portal.sources.networkFields.mode.options] +consume = "Consume: ഓരോ file-ഉം ഒരിക്കൽ process ചെയ്യുക" +snapshot = "Snapshot: ഓരോ run-ലും folder വീണ്ടും വായിക്കുക" + +[portal.sources.networkFields.recursive] +label = "Folder depth" + +[portal.sources.networkFields.recursive.options] +all = "subfolders ഉൾപ്പെടുത്തുക" +top = "Top level മാത്രം" + [portal.sources.status] active = "സജീവം" disabled = "പ്രവർത്തനരഹിതം" unused = "ഉപയോഗിക്കാത്തത്" [portal.sources.table] +documents = "Documents" source = "ഉറവിടം" status = "സ്ഥിതി" +type = "Type" usedBy = "നയങ്ങൾ" +[portal.sources.types.box] +description = "നിങ്ങളുടെ team ഇതിനകം share ചെയ്യുന്ന Box folders-ൽ നിന്ന് ഡോക്യുമെന്റുകൾ pull ചെയ്യുക." +label = "Box" + +[portal.sources.types.dropbox] +description = "shared Dropbox folder-ൽ നിന്ന് ഡോക്യുമെന്റുകൾ pull ചെയ്യുക." +label = "Dropbox" + [portal.sources.types.editor] +description = "policy, AI runs എന്നിവയിലുടനീളം നിങ്ങളുടെ team editor-ൽ process ചെയ്ത ഡോക്യുമെന്റുകൾ." label = "Editor" +noPolicies = "editor-ൽ നിന്ന് ഇതുവരെ policies run ചെയ്തിട്ടില്ല." + +[portal.sources.types.email] +description = "dedicated mailbox-ലേക്ക് അയച്ച attachments ഡോക്യുമെന്റുകളാക്കി മാറ്റുക." +label = "Email inbox" [portal.sources.types.folder] description = "പുതിയ ഡോക്യുമെന്റുകൾക്കായി server-ിലെ ഒരു directory നിരീക്ഷിക്കുക." @@ -7943,25 +8829,80 @@ label = "Folder depth" all = "Subfolders ഉൾപ്പെടുത്തുക" top = "Top level മാത്രം" +[portal.sources.types.ftp] +description = "പുതിയ ഡോക്യുമെന്റുകൾക്കായി FTP അല്ലെങ്കിൽ FTPS server folder poll ചെയ്യുക." +label = "FTP" + +[portal.sources.types.googledrive] +description = "shared Google Drive folders-ൽ നിന്ന് ഡോക്യുമെന്റുകൾ pull ചെയ്യുക." +label = "Google Drive" + +[portal.sources.types.network] +description = "പുതിയ ഡോക്യുമെന്റുകൾക്കായി SMB network share നിരീക്ഷിക്കുക." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "പുതിയ ഡോക്യുമെന്റുകൾക്കായി OneDrive folders നിരീക്ഷിക്കുക." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Amazon S3 അല്ലെങ്കിൽ S3-compatible bucket-ൽ നിന്ന് ഡോക്യുമെന്റുകൾ pull ചെയ്യുക." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "bucket-ഉം credentials-ഉം കൈവശമുള്ള stored connection. ഇതിനെ reference ചെയ്യുന്ന ഓരോ source, pipeline output എന്നിവയും വീണ്ടും ഉപയോഗിക്കുന്നു." +label = "Connection" + +[portal.sources.types.s3.fields.mode] +helperText = "ഓരോ policy-യും process ചെയ്ത ശേഷം Consume bucket-ൽ നിന്ന് ഓരോ object-ഉം നീക്കം ചെയ്യുന്നു." +label = "Read mode" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: ഓരോ object-ഉം ഒരിക്കൽ process ചെയ്യുക" +snapshot = "Snapshot: ഓരോ run-ലും bucket വീണ്ടും വായിക്കുക" + +[portal.sources.types.s3.fields.prefix] +helperText = "ഈ prefix-ൽ തുടങ്ങുന്ന keys ഉള്ള objects മാത്രം process ചെയ്യും." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "പുതിയ ഡോക്യുമെന്റുകൾക്കായി SFTP drop folder poll ചെയ്യുക." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint document libraries-ൽ നിന്ന് ഡോക്യുമെന്റുകൾ pull ചെയ്യുക." +label = "SharePoint" + [portal.sources.types.unknown] label = "ഉറവിടം" +[portal.sources.types.webhook] +createNote = "ഈ webhook സൃഷ്ടിക്കുമ്പോൾ delivery URL-ഉം signing secret-ഉം generate ചെയ്യും. Senders ആ URL-ലേക്ക് documents POST ചെയ്യുന്നു, referencing policies arrival-ൽ run ചെയ്യും." +description = "ഏതൊരു external system-ൽ നിന്നുമുള്ള signed HTTP POST വഴി ഡോക്യുമെന്റുകൾ സ്വീകരിക്കുക." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "webhook സൃഷ്ടിക്കുമ്പോൾ signing secret ഒരിക്കൽ മാത്രം കാണിക്കും. അത് roll ചെയ്യാൻ source വീണ്ടും സൃഷ്ടിക്കുക." + +[portal.sources.types.webhook.reveal] +copy = "Copy" +done = "പൂർത്തിയായി" +secret = "Signing secret" +secretHelp = "ഈ key (HMAC-SHA256) ഉപയോഗിച്ച് ഓരോ delivery-യുടെയും raw body sign ചെയ്ത് X-Stirling-Signature header ആയി അയയ്ക്കുക." +secretWarning = "signing secret ഇപ്പോൾ copy ചെയ്യുക. നിങ്ങളുടെ സുരക്ഷയ്ക്കായി ഇത് ഒരിക്കൽ മാത്രം കാണിക്കും, പിന്നീട് retrieve ചെയ്യാൻ കഴിയില്ല." +title = "Webhook സൃഷ്ടിച്ചു" +url = "Delivery URL" +usage = "binary content type (application/pdf അല്ലെങ്കിൽ application/octet-stream) ഉപയോഗിച്ച് delivery URL-ലേക്ക് raw request body ആയി ഓരോ ഡോക്യുമെന്റും POST ചെയ്യുക. Referencing policies arrival-ൽ സ്വയമേവ run ചെയ്യും." + [portal.sources.wizard] -back = "പിന്നോട്ട്" -cancel = "റദ്ദാക്കുക" -continue = "തുടരുക" -editTitle = "ഉറവിടം തിരുത്തുക" name = "പേര്" namePlaceholder = "ഉദാ. Claims intake" -save = "മാറ്റങ്ങൾ സംരക്ഷിക്കുക" -subtitle = "ഘട്ടം {{current}} / {{total}} · {{label}}" -title = "ഉറവിടം കണക്റ്റ് ചെയ്യുക" type = "തരം" -[portal.sources.wizard.steps] -chooseType = "തരം തിരഞ്ഞെടുക്കുക" -configure = "Configure" -review = "Review & connect" +[portal.stepModal] +close = "അടയ്ക്കുക" [portal.usage] managePayment = "Payment നിയന്ത്രിക്കുക" @@ -8021,16 +8962,9 @@ summary = "ഒരു team-ന്റെ ഉടമ — അതിന്റെ membe 2 = "Default policy വഴി portal access" 3 = "Member ചെയ്യാനാകുന്നതെല്ലാം" -[portal.welcome] -ariaLabel = "Stirling PDF-ലേക്ക് സ്വാഗതം" -badge = "Open-source" -installEditor = "Editor install ചെയ്യുക" -inviteTeammates = "ടീം അംഗങ്ങളെ ക്ഷണിക്കുക" -openInBrowser = "Browser-ൽ തുറക്കുക" -perks = "എന്നേക്കും സൗജന്യം · Self-hostable" -subtitle = "ലോകത്തിലെ ഏറ്റവും സുരക്ഷിതമായ PDF Editor എല്ലാ വലുപ്പത്തിലുള്ള ടീമുകൾക്കും സൗജന്യമാണ്. 60+ PDF operations, SSO എന്നിവ ഉൾപ്പെടുന്നു." -title = "സ്വാഗതം" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Unlimited" [printFile] title = "ഫയൽ അച്ചടിക്കുക" @@ -8613,6 +9547,14 @@ text = "നിങ്ങളുടെ PDF പേജുകൾ 90-ഡിഗ്രി [rotate.tooltip.header] title = "റോട്ടേഷൻ ക്രമീകരണങ്ങളുടെ അവലോകനം" +[ruler] +clearAll = "എല്ലാം മായ്ക്കുക" +hideAllLabels = "എല്ലാ labels-ഉം മറയ്ക്കുക" +hideSmallLabels = "ചെറിയ labels മറയ്ക്കുക" +physicalValues = "Physical" +scaled = "Scaled" +showAllLabels = "എല്ലാ labels-ഉം കാണിക്കുക" + [sanitize] desc = "PDF ഫയലുകളിൽ നിന്ന് സാധ്യതയുള്ള ഹാനികരമായ ഘടകങ്ങൾ നീക്കംചെയ്യുക." sanitizationResults = "സാനിറ്റൈസേഷൻ ഫലങ്ങൾ" @@ -8654,31 +9596,45 @@ label = "XMP മെറ്റാഡാറ്റ നീക്കംചെയ്യ [sanitize.steps] settings = "സജ്ജീകരണങ്ങൾ" -[sanitizePdf] -tags = "വൃത്തിയാക്കുക,സുരക്ഷിതമാക്കുക,സുരക്ഷിതം,ഭീഷണികൾ-നീക്കം ചെയ്യുക" - [sanitizePDF] title = "PDF ശുദ്ധീകരിക്കുക" +[sanitizePdf] +tags = "വൃത്തിയാക്കുക,സുരക്ഷിതമാക്കുക,സുരക്ഷിതം,ഭീഷണികൾ-നീക്കം ചെയ്യുക" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "പേജ്-സ്കെയിൽ ക്രമീകരിക്കുക" +[scaleSettings] +activeScale = "Active Scale" +apply = "Scale പ്രയോഗിക്കുക" +applyCalibration = "Calibration പ്രയോഗിക്കുക" +calculatedScale = "Calculated scale" +calibrate = "Calibrate" +calibrating = "കാലിബ്രേറ്റ് ചെയ്യുന്നു" +calibrationDistanceRequired = "പൂജ്യത്തേക്കാൾ കൂടുതലുള്ള യഥാർത്ഥ ലോകത്തിലെ ദൂരം നൽകുക" +calibrationInvalid = "ഈ അളവിൽ നിന്ന് സ്കെയിൽ കണക്കാക്കാൻ കഴിയുന്നില്ല" +calibrationPaperDistance = "അളന്ന പേജ് ദൂരം: {{distance}}" +calibrationTitle = "സ്കെയിൽ കാലിബ്രേറ്റ് ചെയ്യുക" +calibrationTooltip = "സ്കെയിൽ സ്വയമേവ കണക്കാക്കാൻ അറിയാവുന്ന ഒരു ദൂരം അളക്കുക" +customScale = "ഇഷ്ടാനുസൃത സ്കെയിൽ" +noneSet = "ഇഷ്ടാനുസൃത സ്കെയിൽ സജ്ജമാക്കിയിട്ടില്ല" +presets = "മുൻകൂട്ടി സജ്ജമാക്കിയ സ്കെയിലുകൾ" +ratio = "സ്കെയിൽ അനുപാതം" +ratioHelp = "അനുപാതം: 1 പേജ് യൂണിറ്റ് = X യഥാർത്ഥ ലോകത്തിലെ യൂണിറ്റുകൾ" +ratioPlaceholder = "ഉദാ., 100" +ratioPositive = "അനുപാതം പൂജ്യത്തേക്കാൾ കൂടുതലായിരിക്കണം" +ratioRequired = "അനുപാതം ആവശ്യമാണ്" +realDistance = "യഥാർത്ഥ ലോകത്തിലെ ദൂരം" +realDistancePlaceholder = "ഉദാ., 5" +reset = "ഡിഫോൾട്ടുകളിലേക്ക് റീസെറ്റ് ചെയ്യുക" +unit = "യൂണിറ്റ്" +unitInvalid = "അസാധുവായ യൂണിറ്റ്: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "കോൺ പരിധി:" -10 = "ഔട്ട്‌പുട്ടിൽ വെളുത്ത അതിരുകൾ ഉണ്ടാകുന്നത് തടയാൻ ചേർത്തതും നീക്കം ചെയ്തതുമായ അതിർത്തിയുടെ വലുപ്പം സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 1)." -2 = "ചിത്രം തിരിക്കുന്നതിന് ആവശ്യമായ ഏറ്റവും കുറഞ്ഞ കേവല കോൺ സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 10)." -3 = "സഹിഷ്ണുത:" -4 = "കണക്കാക്കിയ പശ്ചാത്തല നിറത്തിന് ചുറ്റുമുള്ള വർണ്ണ വ്യതിയാനത്തിന്റെ പരിധി നിർണ്ണയിക്കുന്നു (സ്ഥിരസ്ഥിതി: 30)." -5 = "കുറഞ്ഞ വിസ്തീർണ്ണം:" -6 = "ഒരു ഫോട്ടോയ്ക്കുള്ള കുറഞ്ഞ വിസ്തീർണ്ണ പരിധി സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 10000)." -7 = "കുറഞ്ഞ കോണ്ടൂർ വിസ്തീർണ്ണം:" -8 = "ഒരു ഫോട്ടോയ്ക്കുള്ള കുറഞ്ഞ കോണ്ടൂർ വിസ്തീർണ്ണ പരിധി സജ്ജമാക്കുന്നു" -9 = "അതിർത്തി വലുപ്പം:" - [scannerImageSplit] submit = "ചിത്ര സ്കാൻസ് എടുക്കുക" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "ഇത് എന്താണ് ചെയ്യുന്നത whatThisDoesDesc = "സ്കാൻ ചെയ്ത ഒരു പേജിലോ കോമ്പോസിറ്റ് ചിത്രത്തിലോ ഉള്ള ഓരോ ഫോട്ടോയും സ്വയം കണ്ടെത്തി വേർതിരിക്കുന്നു—മാനുവൽ ക്രോപ്പിംഗ് വേണ്ട." whenToUse = "എപ്പോൾ ഉപയോഗിക്കാം" +[ScannerImageSplit.selectText] +1 = "കോൺ പരിധി:" +10 = "ഔട്ട്‌പുട്ടിൽ വെളുത്ത അതിരുകൾ ഉണ്ടാകുന്നത് തടയാൻ ചേർത്തതും നീക്കം ചെയ്തതുമായ അതിർത്തിയുടെ വലുപ്പം സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 1)." +2 = "ചിത്രം തിരിക്കുന്നതിന് ആവശ്യമായ ഏറ്റവും കുറഞ്ഞ കേവല കോൺ സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 10)." +3 = "സഹിഷ്ണുത:" +4 = "കണക്കാക്കിയ പശ്ചാത്തല നിറത്തിന് ചുറ്റുമുള്ള വർണ്ണ വ്യതിയാനത്തിന്റെ പരിധി നിർണ്ണയിക്കുന്നു (സ്ഥിരസ്ഥിതി: 30)." +5 = "കുറഞ്ഞ വിസ്തീർണ്ണം:" +6 = "ഒരു ഫോട്ടോയ്ക്കുള്ള കുറഞ്ഞ വിസ്തീർണ്ണ പരിധി സജ്ജമാക്കുന്നു (സ്ഥിരസ്ഥിതി: 10000)." +7 = "കുറഞ്ഞ കോണ്ടൂർ വിസ്തീർണ്ണം:" +8 = "ഒരു ഫോട്ടോയ്ക്കുള്ള കുറഞ്ഞ കോണ്ടൂർ വിസ്തീർണ്ണ പരിധി സജ്ജമാക്കുന്നു" +9 = "അതിർത്തി വലുപ്പം:" + [search] noResults = "ഫലങ്ങളൊന്നും കണ്ടെത്താനായില്ല" placeholder = "തിരയാനുള്ള പദം നൽകുക..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "നിങ്ങളുടെ Stirling-PDF സെർവ [session] expired = "നിങ്ങളുടെ സെഷൻ കാലഹരണപ്പെട്ടു. ദയവായി പേജ് പുതുക്കി വീണ്ടും ശ്രമിക്കുക." -[sessionManagement.tooltip] -header = "സൈൻ ചെയ്യുന്ന സെഷനുകളുടെ മാനേജ്മെന്റ്" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "പുതിയ പങ്കാളികൾ ഒപ്പിടൽ ക്രമത്തിന്റെ അവസാനത്തിലേക്ക് ചേർക്കപ്പെടും" -bullet2 = "സെഷൻ അന്തിമീകരിച്ചതിന് ശേഷം പങ്കാളികളെ ചേർക്കാൻ കഴിയില്ല" -bullet3 = "ആരുടേയും ടേൺ വരുമ്പോൾ ഓരോ പങ്കാളിക്കും ഒരു അറിയിപ്പ് ലഭിക്കും" -description = "അന്തിമീകരണത്തിന് മുമ്പ് ഏത് സമയത്തും ഒരു സജീവ സെഷനിലേക്ക് കൂടുതൽ പങ്കാളികളെ ചേർക്കാം." -title = "പങ്കാളികളെ ചേർക്കൽ" - -[sessionManagement.tooltip.finalization] -bullet1 = "പൂർണ്ണ അന്തിമീകരണം: എല്ലാ പങ്കാളികളും ഒപ്പിട്ടു" -bullet2 = "ഭാഗിക അന്തിമീകരണം: ചിലർ ഇനിയും ഒപ്പിട്ടിട്ടില്ല" -bullet3 = "ഒപ്പിടാത്ത പങ്കാളികളെ അന്തിമ ഡോക്യുമെന്റിൽ നിന്ന് ഒഴിവാക്കും" -bullet4 = "അന്തിമീകരിച്ചുകഴിഞ്ഞാൽ, ഒപ്പിട്ട PDF സജീവ ഫയലുകളിലേക്ക് ലോഡ് ചെയ്യാം" -description = "അന്തിമീകരണം എല്ലാ ഒപ്പുകളും ഒരൊറ്റ ഒപ്പിട്ട PDF-ലേക്ക് സംയോജിപ്പിക്കുന്നു. ഈ നടപടി പിൻവലിക്കാൻ കഴിയില്ല." -title = "സെഷൻ അന്തിമീകരണം" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "ഇതിനകം ഒപ്പിട്ട പങ്കാളികളെ നീക്കാൻ കഴിയില്ല" -bullet2 = "നീക്കിയ പങ്കാളികൾക്ക് ഇനി അറിയിപ്പുകൾ ലഭിക്കില്ല" -bullet3 = "ഒപ്പിടൽ ക്രമം സ്വയം ക്രമീകരിക്കും" -description = "ഒപ്പിടുന്നതിന് മുമ്പ് പങ്കാളികളെ സെഷനിൽ നിന്ന് നീക്കാം." -title = "പങ്കാളികളെ നീക്കൽ" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "ഏതു ഒപ്പും PDF-ൽ ക്രമാനുസൃതമായി പ്രയോഗിക്കും" -bullet2 = "പിന്നീട് ഒപ്പിടുന്നവർ മുൻപ് ഇടപ്പെട്ട ഒപ്പുകൾ കാണാം" -bullet3 = "അംഗീകൃത പ്രവാഹങ്ങൾക്കും നിയമപരമായ കൈമാറ്റ ശൃംഖലകൾക്കും നിർണായകം" -description = "സെഷൻ സൃഷ്ടിക്കുമ്പോൾ നിങ്ങൾ വ്യക്തമാക്കുന്ന ക്രമമാണ് ആദ്യം ആരാണ് ഒപ്പിടുക എന്നത് നിർണ്ണയിക്കുന്നത്." -title = "ഒപ്പിടൽ ക്രമം" - [settings] close = "അടയ്ക്കുക" +[settings.ai] +documents = "പ്രമാണങ്ങൾ & RAG" +general = "പൊതുവായത്" +limits = "പരിധികളും പ്രകടനവും" +models = "മോഡലുകളും ദാതാക്കളും" +title = "AI" + [settings.configuration] advanced = "അഡ്വാൻസ്ഡ്" database = "ഡാറ്റാബേസ്" endpoints = "എൻഡ്പോയിന്റുകൾ" features = "ഫീച്ചറുകൾ" +folderAccess = "ഫോൾഡർ ആക്‌സസ്" mcp = "MCP Server" storageSharing = "ഫയൽ സംഭരണവും പങ്കിടലും" systemSettings = "സിസ്റ്റം ക്രമീകരണങ്ങൾ" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "സ്വതേയുള്ള PDF എഡിറ്റർ" defaultPdfEditorActive = "Stirling PDF നിങ്ങളുടെ സ്വതേയുള്ള PDF എഡിറ്ററാണ്" defaultPdfEditorChecking = "പരിശോധിക്കുന്നു..." defaultPdfEditorInactive = "മറ്റൊരു അപ്ലിക്കേഷൻ സ്വതേയുള്ളതായി സജ്ജമാക്കിയിരിക്കുന്നു" +defaultPdfEditorRemind = "ഡിഫോൾട്ടായി സജ്ജമാക്കാൻ എന്നെ ഓർമ്മിപ്പിക്കുക" +defaultPdfEditorRemindDescription = "Stirling PDF നിങ്ങളുടെ ഡിഫോൾട്ട് PDF ആപ്ലിക്കേഷൻ അല്ലാത്തപ്പോൾ ഒരു ബാനർ കാണിക്കുക." defaultPdfEditorSet = "ഇതിനകം സ്വതേയുള്ളത്" defaultStartupView = "തുറക്കുമ്പോൾ സ്വതവേയുള്ള ദൃശ്യം" defaultStartupViewDescription = "ആപ്പ് തുടങ്ങുമ്പോൾ ഇടത് കോളത്തിലെ ഏത് ടാബ് സജീവമായിരിക്കണം എന്ന് തിരഞ്ഞെടുക്കുക" @@ -8817,9 +9763,18 @@ languageDescription = "പ്രദർശന ഭാഷ തിരഞ്ഞെട logout = "ലോഗ് ഔട്ട്" setAsDefault = "സ്വതേയുള്ളതാക്കി സജ്ജമാക്കുക" theme = "തീം" +themeAccent = "ആക്‌സന്റ് നിറം" +themeAccentCustom = "ഇഷ്ടാനുസൃത നിറം" +themeAccentCustomHint = "ഇഷ്ടാനുസൃതം (ഡിഫോൾട്ടുകൾ ശുപാർശ ചെയ്യുന്നു)" +themeAccentDark = "ഡാർക്ക് മോഡ് ആക്‌സന്റ് നിറം" +themeAccentDefault = "ഡിഫോൾട്ട്" +themeAccentDefaultHint = "ഡിഫോൾട്ട് തീം (ശുപാർശ ചെയ്യുന്നു)" +themeAccentDescription = "ബട്ടണുകൾ, ലിങ്കുകൾ, ഹൈലൈറ്റുകൾ എന്നിവ ഇത് പിന്തുടരുന്നു, കൂടാതെ ആപ്പിന് സൂക്ഷ്മമായ നിറഭേദം നൽകുന്നു. ലൈറ്റിനും ഡാർക്കിനും പ്രത്യേകം ഉണ്ട്. System സജീവമായത് ഉപയോഗിക്കുന്നു." +themeAccentLight = "ലൈറ്റ് മോഡ് ആക്‌സന്റ് നിറം" themeDark = "ഡാർക്ക്" themeDescription = "ലൈറ്റ്, ഡാർക്ക് മോഡുകൾക്ക് ഇടയിൽ മാറുക" themeLight = "ലൈറ്റ്" +themeReset = "തീം ഡിഫോൾട്ടിലേക്ക് പുനഃസ്ഥാപിക്കുക" themeSystem = "System" title = "ജനറൽ" user = "ഉപയോക്താവ്" @@ -8833,6 +9788,12 @@ intro = "നിങ്ങളുടെ ഓർഗനൈസേഷനായി യൂ learnMore = "ഡോക്യുമെന്റേഷനിൽ കൂടുതൽ അറിയുക" title = "സിസ്റ്റം അഡ്മിനിസ്ട്രേറ്റർമാർക്കായി" +[settings.general.loginLanding] +description = "Stirling Cloud-ലേക്ക് സൈൻ ഇൻ ചെയ്യുമ്പോൾ ഏത് ആപ്പ് തുറക്കണമെന്ന് തിരഞ്ഞെടുക്കുക." +editor = "എഡിറ്റർ" +processor = "പ്രോസസർ" +title = "സൈൻ ഇൻ ചെയ്ത ശേഷം" + [settings.general.mode] fullscreen = "ഫുൾസ്ക്രീൻ" sidebar = "സൈഡ്ബാർ" @@ -9013,9 +9974,6 @@ title = "നയങ്ങളും സ്വകാര്യതയും" [settings.preferences] title = "അഭിരുചികൾ" -[settings.search] -placeholder = "സജ്ജീകരണ പേജുകൾ തിരയുക..." - [settings.security] description = "അക്കൗണ്ട് സുരക്ഷിതമായി നിലനിർത്താൻ പാസ്‌വേഡ് അപ്‌ഡേറ്റ് ചെയ്യുക." title = "സുരക്ഷ" @@ -9041,6 +9999,11 @@ security = "സുരക്ഷ" telegram = "Telegram" title = "സുരക്ഷയും ഓതന്റിക്കേഷനും" +[settings.signOut] +confirm = "നിങ്ങൾക്ക് സൈൻ ഔട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?" +submit = "സൈൻ ഔട്ട് ചെയ്യുക" +title = "സൈൻ ഔട്ട്" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "അവസാന പേജ്" maintainRatio = "വീക്ഷണാനുപാതം നിലനിർത്തുക ടോഗിൾ ചെയ്യുക" next = "അടുത്ത പേജ്" noSavedSigs = "സംരക്ഷിച്ച ഒപ്പുകളൊന്നും കണ്ടെത്തിയില്ല" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "വ്യക്തിഗത ഒപ്പുകൾ" previous = "മുമ്പത്തെ പേജ്" redo = "വീണ്ടും ചെയ്യുക" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "ബാക്ക്ഗ്രൗണ്ട് ന hint = "നിങ്ങളുടെ ഒപ്പിന്റെ PNG അല്ലെങ്കിൽ JPG ചിത്രം അപ്‌ലോഡ് ചെയ്യുക" label = "ഒപ്പിന്റെ ചിത്രം അപ്‌ലോഡ് ചെയ്യുക" placeholder = "ഇമേജ് ഫയൽ തിരഞ്ഞെടുക്കുക" +previewAlt = "Current image signature" processing = "ഇമേജ് പ്രോസസ്സ് ചെയ്യുന്നു..." removeBackground = "വെള്ള ബാക്ക്ഗ്രൗണ്ട് നീക്കംചെയ്യുക (ട്രാൻസ്‌പാരന്റ് ആക്കുക)" @@ -9207,6 +10172,17 @@ saved = "മുകളിൽ നിന്ന് ഒരു സേവ് ചെയ text = "മുകളിൽ നിങ്ങളുടെ പേര് നൽകിയ ശേഷം, PDF-ിലെ ഏതെങ്കിലും സ്ഥലത്ത് ക്ലിക്ക് ചെയ്ത് ഒപ്പ് സ്ഥാപിക്കുക." title = "ഒപ്പ് എങ്ങനെ ചേർക്കാം" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "ഒപ്പ് നീക്കുക" pause = "സ്ഥാപിക്കൽ നിർത്തുക" @@ -9653,10 +10629,30 @@ title = "സെർവറിലേക്ക് അപ്‌ലോഡ് ചെയ updateButton = "സെർവറിൽ അപ്‌ഡേറ്റ് ചെയ്യുക" uploadButton = "സെർവറിലേക്ക് അപ്‌ലോഡ് ചെയ്യുക" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "ഇവിടെ ക്ലിക്ക് ചെയ്യുക" +fallback = "അത് സ്വയമേവ തുറന്നില്ലെങ്കിൽ," +opening = "പുതിയ ടാബിൽ Swagger UI തുറക്കുന്നു..." tags = "api,documentation,swagger,endpoints,development" title = "API ഡോക്യുമെന്റേഷൻ" @@ -9764,12 +10760,39 @@ selectFilesHint = "ഈ ടൂൾ പ്രവർത്തിപ്പിക് singleFileScope = "ഇത് ബാധകമാകുന്നത് {{fileName}}-ൽ മാത്രം" viewerMode = "ഒന്നിലധികം ഫയലുകൾ തിരഞ്ഞെടുക്കാൻ ഫയൽ എഡിറ്ററിലേക്ക് മാറുക." +[toolFormat] +ANY = "ഏതെങ്കിലും ഫയൽ" +CBR = "ഒരു കോമിക് ബുക്ക് ആർക്കൈവ് (CBR)" +CBZ = "ഒരു കോമിക് ബുക്ക് ആർക്കൈവ് (CBZ)" +CSV = "ഒരു CSV ഫയൽ" +EBOOK = "ഒരു ഇബുക്ക്" +EMAIL = "ഒരു ഇമെയിൽ" +EXCEL = "ഒരു സ്പ്രെഡ്ഷീറ്റ്" +HTML = "ഒരു വെബ് പേജ്" +IMAGE = "ചിത്രങ്ങൾ" +JAVASCRIPT = "ഒരു JavaScript ഫയൽ" +JSON = "ഒരു JSON ഫയൽ" +MARKDOWN = "ഒരു Markdown ഫയൽ" +NONE = "ഫയൽ ഇല്ല" +PCL = "ഒരു PCL പ്രിന്റ് ഫയൽ" +PDF = "ഒരു PDF" +PDF_ENCRYPTED = "പാസ്‌വേഡ്-സംരക്ഷിത PDF" +POSTSCRIPT = "ഒരു PostScript ഫയൽ" +PPT = "ഒരു അവതരണം" +TEXT = "ഒരു ടെക്സ്റ്റ് ഫയൽ" +VIDEO = "ഒരു വീഡിയോ" +WORD = "ഒരു Word പ്രമാണം" +XML = "ഒരു XML ഫയൽ" +XPS = "ഒരു XPS പ്രമാണം" +ZIP = "ഒരു ZIP ആർക്കൈവ്" + [toolPanel] alpha = "Alpha" backToAllTools = "എല്ലാ ടൂളുകളിലേക്കും തിരികെ" collapse = "പാനൽ ചുരുക്കുക" expand = "പാനൽ വിപുലീകരിക്കുക" goBack = "തിരികെ പോകുക" +pdfTools = "PDF ടൂളുകൾ" placeholder = "തുടങ്ങാൻ ഒരു ടൂൾ തിരഞ്ഞെടുക്കുക" premiumFeature = "പ്രീമിയം ഫീച്ചർ:" toolsHeader = "ടൂളുകൾ" @@ -9806,11 +10829,6 @@ noToolsFound = "ടൂളുകളൊന്നും കണ്ടെത്താ quickAccess = "ക്ഷിപ്ര പ്രവേശനം" searchPlaceholder = "ടോളുകൾ തിരയുക..." -[toolPicker.categories] -advancedTools = "അഡ്വാൻസ്ഡ് ടൂളുകൾ" -recommendedTools = "റിക്കമൻഡഡ് ടൂളുകൾ" -standardTools = "സ്റ്റാൻഡേർഡ് ടൂളുകൾ" - [toolPicker.subcategories] advancedFormatting = "അഡ്വാൻസ്ഡ് ഫോർമാറ്റിംഗ്" automation = "ഓട്ടോമേഷൻ" @@ -9824,6 +10842,9 @@ removal = "നീക്കം" signing = "ഒപ്പിടൽ" verification = "സ്ഥിരീകരണം" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "ഒരു ടൂളും കണ്ടെത്തിയില്ല" noTools = "ടൂളുകൾ ലഭ്യമല്ല" @@ -9950,28 +10971,53 @@ title = "ഉപയോക്താക്കൾ" you = "(നിങ്ങൾ)" [users.action] +cancelInvite = "ക്ഷണം റദ്ദാക്കുക" deleteTeam = "Team ഇല്ലാതാക്കുക" disableMfa = "MFA റീസെറ്റ് ചെയ്യുക" move = "Team-ലേക്ക് മാറ്റുക" reinstate = "വീണ്ടും സജീവമാക്കുക" remove = "Org-ൽ നിന്ന് നീക്കംചെയ്യുക" +removeTeam = "ടീമിൽ നിന്ന് നീക്കം ചെയ്യുക" rename = "Team rename ചെയ്യുക" resetPw = "Password റീസെറ്റ് ചെയ്യുക" suspend = "Suspend" unlock = "Account unlock ചെയ്യുക" +[users.activity] +daysAgo = "{{count}}d മുമ്പ്" +hoursAgo = "{{count}}h മുമ്പ്" +justNow = "ഇപ്പോൾ" +minutesAgo = "{{count}}m മുമ്പ്" +monthsAgo_one = "{{count}} മാസം മുമ്പ്" +monthsAgo_other = "{{count}} മാസം മുമ്പ്" +never = "ഒരിക്കലും ഇല്ല" +weeksAgo_one = "{{count}} ആഴ്ച മുമ്പ്" +weeksAgo_other = "{{count}} ആഴ്ച മുമ്പ്" +yearsAgo_one = "{{count}} വർഷം മുമ്പ്" +yearsAgo_other = "{{count}} വർഷം മുമ്പ്" + [users.cap] addProcessor = "+ Processor" approver = "നയം അംഗീകരിക്കുന്നു" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}}-ലേക്കുള്ള ക്ഷണം റദ്ദാക്കണോ? നിലവിലെ ലിങ്ക് ഉപയോഗിച്ച് അവർക്ക് ചേരാൻ കഴിയില്ല." +cancelInviteTitle = "ക്ഷണം റദ്ദാക്കുക" deleteTeamBody = "{{name}} team ഇല്ലാതാക്കണോ? ആദ്യം team empty ആയിരിക്കണം - അതിലെ members-നെ മറ്റൊരു team-ലേക്ക് മാറ്റുക, കൂടാതെ അതിന് integration configs സ്വന്തമായി ഉണ്ടായിരിക്കരുത്." deleteTeamTitle = "Team ഇല്ലാതാക്കുക" disableMfaBody = "{{name}}-ന്റെ MFA enrolment നീക്കംചെയ്യണോ? ആവശ്യമെങ്കിൽ അവർ അടുത്ത login-ൽ വീണ്ടും സജ്ജീകരിക്കും." disableMfaTitle = "MFA റീസെറ്റ് ചെയ്യുക" removeBody = "{{name}}-നെ organization-ൽ നിന്ന് സ്ഥിരമായി നീക്കംചെയ്യണോ? ഇത് പഴയപടിയാക്കാനാവില്ല." +removeTeamBody = "{{name}}-നെ ടീമിൽ നിന്ന് നീക്കം ചെയ്യണോ? അവർക്ക് അക്കൗണ്ട് തുടരും, പക്ഷേ ഈ ടീമിന്റെ വിഭവങ്ങളിലേക്കുള്ള ആക്‌സസ് നഷ്ടപ്പെടും." removeTitle = "Member നീക്കംചെയ്യുക" [users.empty] @@ -9982,10 +11028,8 @@ title = "ഇതുവരെ members ഇല്ല" addToTeam = "Team-ലേക്ക് ചേർക്കുക" guestCount = "{{count}} guest" guests = "Guests" -guestsDesc = "External collaborators, നിങ്ങൾ പങ്കിട്ടതിലേക്കു മാത്രം പരിമിതം. Editor മാത്രം." ledBy = "{{owner}} നയിക്കുന്നു" org = "Organization" -orgDesc = "Org-wide authority-യും policy approval-ഉം ഉള്ള owners" owners = "{{count}} owner" team = "{{name}} team" teamMeta = "{{count}} ആളുകൾ" @@ -10022,6 +11066,18 @@ username = "ഉപയോക്തൃനാമം" usernameError = "ഉപയോക്തൃനാമത്തിന് കുറഞ്ഞത് 3 അക്ഷരങ്ങൾ വേണം" usernamePlaceholder = "jsmith" +[users.invites] +by = "ക്ഷണിച്ചത് {{who}}" +cancel = "റദ്ദാക്കുക" +expiresInDays_one = "{{count}} ദിവസത്തിനകം കാലഹരണപ്പെടും" +expiresInDays_other = "{{count}} ദിവസത്തിനകം കാലഹരണപ്പെടും" +expiresToday = "ഇന്ന് കാലഹരണപ്പെടും" +title = "തീർപ്പാകാത്ത ക്ഷണങ്ങൾ" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "ബാക്കെൻഡുമായി ബന്ധപ്പെടുന്നതിൽ എന്തോ പിശക് സംഭവിച്ചു, അല്ലെങ്കിൽ നിങ്ങൾക്ക് ആക്സസ് ഇല്ല. വീണ്ടും ശ്രമിക്കുക." title = "അംഗങ്ങളെ ലോഡ് ചെയ്യാനായില്ല" @@ -10211,10 +11267,15 @@ searchPlaceholder = "അറ്റാച്ച്മെന്റുകൾ തി title = "അറ്റാച്ച്മെന്റുകൾ" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "ബുക്ക്‌മാർക്ക് ശീർഷകം" +bookmarkTitleRequired = "ബുക്ക്‌മാർക്ക് ശീർഷകം ആവശ്യമാണ്" closeSidebar = "ബുക്ക്‌മാർക്കുകളുടെ സൈഡ്‌ബാർ അടയ്ക്കുക" collapseAll = "എല്ലാ ബുക്ക്‌മാർക്കുകളും ചുരുക്കുക" +empty = "No bookmarks in this document" expandAll = "എല്ലാ ബുക്ക്‌മാർക്കുകളും വിപുലീകരിക്കുക" +searchPlaceholder = "ബുക്ക്‌മാർക്കുകൾ തിരയുക" +title = "Bookmarks" [viewer.comments] addComment = "കമന്റ് ചേർക്കുക" @@ -10236,10 +11297,12 @@ locateAnnotation = "ഡോക്യുമെന്റിൽ കണ്ടെത moreActions = "കൂടുതൽ പ്രവർത്തികൾ" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "പേജ് {{page}}" placingHint = "ഇടാൻ ഒരു പേജിൽ ക്ലിക്ക് ചെയ്യുക… (റദ്ദാക്കുക)" removeCommentOnly = "കമന്റ് മാത്രം നീക്കുക" saveReply = "മറുപടി സംരക്ഷിക്കുക" +searchPlaceholder = "Search comments" title = "കമന്റുകൾ" typeComment = "കമന്റ്" typeInsertText = "ടെക്സ്റ്റ് ചേർക്കുക" @@ -10262,6 +11325,7 @@ unsavedDesc = "നിങ്ങൾക്ക് സംരക്ഷിക്കാ closeSidebar = "ലെയറുകളുടെ സൈഡ്‌ബാർ അടയ്ക്കുക" hideAll = "എല്ലാ ലെയറുകളും മറയ്ക്കുക" showAll = "എല്ലാ ലെയറുകളും കാണിക്കുക" +title = "Layers" [viewer.link] delete = "ലിങ്ക് ഇല്ലാതാക്കുക" @@ -10605,30 +11669,6 @@ title = "വാട്ടർമാർക്ക് ടെക്സ്റ്റ്" image = "ഇമേജ്" text = "ടെക്സ്റ്റ്" -[wetSignature.tooltip] -header = "കയ്യൊപ്പ് സൃഷ്ടിക്കുന്ന രീതികൾ" - -[wetSignature.tooltip.draw] -bullet1 = "പേനയുടെ നിറവും കനംവും ഇഷ്ടാനുസരണം മാറ്റുക" -bullet2 = "തൃപ്തിപ്പെടുന്നതുവരെ മായ്ച്ച് വീണ്ടും വരയ്ക്കുക" -bullet3 = "ടച്ച് ഉപകരണങ്ങളിൽ (ടാബ്ലെറ്റുകൾ, ഫോൺ) പ്രവർത്തിക്കും" -description = "നിങ്ങളുടെ മൗസ് അല്ലെങ്കിൽ ടച്ച്‌സ്‌ക്രീൻ ഉപയോഗിച്ച് കൈയെഴുത്ത് കൈയ്യൊപ്പ് സൃഷ്ടിക്കുക. വ്യക്തിപരവും യഥാർത്ഥവുമായ കൈയ്യൊപ്പുകൾക്ക് ഏറ്റവും ഉചിതം." -title = "കയ്യൊപ്പ് വരയ്‌ക്കുക" - -[wetSignature.tooltip.type] -bullet1 = "പല ഫോണ്ടുകളിൽ നിന്ന് തിരഞ്ഞെടുക്കുക" -bullet2 = "ടെക്സ്റ്റിന്റെ വലിപ്പവും നിറവും ഇഷ്ടാനുസരണം മാറ്റുക" -bullet3 = "സ്റ്റാൻഡേർഡൈസ്ഡ് കൈയ്യൊപ്പുകൾക്ക് ഏറ്റവും അനുയോജ്യം" -description = "ടൈപ്പ് ചെയ്ത ടെക്സ്റ്റിൽ നിന്ന് ഒരു കൈയ്യൊപ്പ് സൃഷ്ടിക്കുക. വേഗത്തിലും സ്ഥിരതയോടെയും, ബിസിനസ് ഡോക്യുമെന്റുകൾക്ക് അനുയോജ്യം." -title = "കയ്യൊപ്പ് ടൈപ്പ് ചെയ്യുക" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG, മറ്റ് ഇമേജ് ഫോർമാറ്റുകൾ പിന്തുണയ്ക്കുന്നു" -bullet2 = "മികച്ച ഫലങ്ങൾക്കായി സുതാര്യമായ പശ്ചാത്തലം ശുപാർശ ചെയ്യുന്നു" -bullet3 = "കയ്യൊപ്പ് പ്രദേശത്തേക്ക് ഒതുങ്ങുന്നതിനായി ചിത്രത്തിന്റെ വലിപ്പം മാറ്റും" -description = "മുമ്പേ സൃഷ്ടിച്ച കയ്യൊപ്പ് ചിത്രം അപ്‌ലോഡ് ചെയ്യുക. സ്കാൻ ചെയ്ത കൈയ്യൊപ്പോ കമ്പനി ലോഗോവോ ഉണ്ടെങ്കിൽ ഏറ്റവും അനുയോജ്യം." -title = "കയ്യൊപ്പിന്റെ ചിത്രം അപ്‌ലോഡ് ചെയ്യുക" - [workbenchBar] activeFiles = "സജീവ ഫയലുകൾ" annotations = "അനോട്ടേഷനുകൾ" @@ -10642,6 +11682,7 @@ exitRedaction = "റെഡാക്ഷൻ മോഡിൽ നിന്ന് പ exportAll = "PDF എക്സ്പോർട്ട് ചെയ്യുക" exportSelected = "തിരഞ്ഞെടുത്ത പേജുകൾ എക്സ്പോർട്ട് ചെയ്യുക" formFill = "ഫോം പൂരിപ്പിക്കുക" +hideToolbar = "Hide toolbar" multiTool = "മൾട്ടി-ടൂൾ" panMode = "പാൻ മോഡ്" print = "PDF അച്ചടിക്കുക" @@ -10652,6 +11693,7 @@ redact = "റെഡാക്റ്റ്" rotateLeft = "ഇടത്തേക്ക് തിരിക്കുക" rotateRight = "വലത്തേക്ക് തിരിക്കുക" ruler = "റൂളർ / അളക്കൽ" +rulerSettings = "സ്കെയിൽ ക്രമീകരണങ്ങൾ" save = "സേവ്" saveAll = "എല്ലാം സേവ് ചെയ്യുക" saveAs = "പുതിയ പേരിൽ സേവ് ചെയ്യുക" @@ -10661,6 +11703,7 @@ selectAll = "എല്ലാം തിരഞ്ഞെടുക്കുക" selectByNumber = "പേജ് നമ്പർ പ്രകാരം തിരഞ്ഞെടുക്കുക" selectLanguage = "ഭാഷ തിരഞ്ഞെടുക്കുക" share = "പങ്കിടുക" +showToolbar = "Show toolbar" toggleAnnotations = "അനോട്ടേഷനുകളുടെ ദൃശ്യത ടോഗിൾ ചെയ്യുക" toggleAttachments = "അറ്റാച്ച്മെന്റുകൾ ടോഗിൾ ചെയ്യുക" toggleBookmarks = "ബുക്ക്മാർക്കുകൾ ടോഗിൾ ചെയ്യുക" diff --git a/frontend/editor/public/locales/nl-NL/translation.toml b/frontend/editor/public/locales/nl-NL/translation.toml index 62f935c195..e65bda307b 100644 --- a/frontend/editor/public/locales/nl-NL/translation.toml +++ b/frontend/editor/public/locales/nl-NL/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Toepassen" applyAndContinue = "Opslaan en verlaten" -areYouSure = "Weet u zeker dat u wilt vertrekken?" back = "Terug" black = "Zwart" blue = "Blauw" @@ -46,10 +45,8 @@ filesSelected = "Bestanden geselecteerd" font = "Lettertype" fontSizeTooltip = "Grootte van de paginanummertekst in punten. Grotere getallen geven grotere tekst." fontTypeTooltip = "Lettertypefamilie voor de paginanummers. Kies passend bij de stijl van uw document." -genericSubmit = "Indienen" green = "Groen" help = "Hulp" -imgPrompt = "Selecteer afbeelding(en)" incorrectPasswordMessage = "Huidige wachtwoord is onjuist." info = "Informatie" invalidUndoData = "Kan niet ongedaan maken: ongeldige bewerkingsgegevens" @@ -62,7 +59,6 @@ loginAgreementTitle = "Inlogovereenkomst" logOut = "Uitloggen" marginTooltip = "Afstand tussen het paginanummer en de rand van de pagina." moreOptions = "Meer opties" -multiPdfPrompt = "PDF's (2+) selecteren" never = "Nooit" no = "Nee" noFileSelected = "Geen bestand geselecteerd. Upload er een." @@ -80,7 +76,6 @@ pageOrderPrompt = "Aangepaste pagina volgorde (Voer een komma-gescheiden lijst v pages = "Pagen" pageSelectionPrompt = "Aangepaste pagina selectie (Voer een komma-gescheiden lijst van paginanummer 1,5,6 of functies zoals 2n+1 in) :" password = "Wachtwoord" -pdfPrompt = "PDF(-en) selecteren" pin = "Bestand vastzetten (actief houden na tooluitvoering)" pro = "Pro" processingComplete = "Je bestand is klaar." @@ -110,6 +105,7 @@ undoSuccess = "Bewerking succesvol ongedaan gemaakt" unknown = "Onbekend" unpin = "Bestand losmaken (vervangen na tooluitvoering)" unsavedChanges = "U heeft niet-opgeslagen wijzigingen in uw PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Niet-opgeslagen wijzigingen" unsupported = "Niet ondersteund" username = "Gebruikersnaam" @@ -356,6 +352,7 @@ text = "Deze machtigingen bepalen wat gebruikers met de PDF kunnen doen. Het mee title = "Rechten wijzigen" [addStamp] +preview = "Stempelvoorbeeld" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Handmatige downloads: klik op de links en plaats de bestanden in noLanguages = "Geen tessdata-talen gevonden in de geconfigureerde map." permissionNotice = "Het tessdata-pad is niet schrijfbaar. Downloads worden in de browser geopend; sla de .traineddata-bestanden handmatig op in de tessdata-map." +[admin.settings.ai.documents] +description = "Configureer het embedding-model en de ophaalinstellingen die worden gebruikt om vragen over documenten te beantwoorden. Toegepast op de AI engine bij opslaan." +title = "Documenten & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Laat leeg om de eigen omgevingsreferentie van de engine te gebruiken. Van toepassing op zelfgehoste single-engine-implementaties." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Opgeslagen - laat leeg om de huidige key te behouden" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL van het OpenAI-compatibele / Ollama embeddings-endpoint, bijv. http://ollama:11434/v1. Moet verwijzen naar een vertrouwd intern endpoint (SSRF-gevoelig)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Naam van het embedding-model. Vrije tekst; suggesties zijn alleen hints." +label = "Embedding-model" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provider die wordt gebruikt om documenttekst om te zetten in vector-embeddings." +label = "Embedding-provider" + +[admin.settings.ai.documents.maxSearches] +description = "Maximumaantal ophaalzoekopdrachten dat de agent per verzoek mag uitvoeren." +label = "Max. zoekopdrachten" + +[admin.settings.ai.documents.reindexNote] +body = "Het wijzigen van het embedding-model wordt direct van kracht, maar documenten die met het vorige model zijn geïndexeerd, moeten opnieuw worden geïndexeerd zodat zoeken juiste resultaten oplevert." +title = "Opnieuw indexeren vereist" + +[admin.settings.ai.documents.saved] +reindexNote = "Als je het embedding-model hebt gewijzigd, indexeer bestaande documenten dan opnieuw zodat zoeken het nieuwe model gebruikt." + +[admin.settings.ai.documents.topK] +description = "Aantal meest relevante chunks dat per zoekopdracht wordt opgehaald." +label = "Top K" + +[admin.settings.ai.general] +description = "Verbind Stirling met de Python AI engine en kies welke AI-mogelijkheden beschikbaar zijn. Wijzigingen worden toegepast na herstart." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Schakel afzonderlijke AI-functies in of uit. Uitgeschakelde functies worden verborgen in de app." +title = "Mogelijkheden" + +[admin.settings.ai.general.enabled] +description = "Hoofdschakelaar. Wanneer uitgeschakeld, zijn er geen AI-tools, agents of engine-aanroepen beschikbaar." +label = "AI inschakelen" + +[admin.settings.ai.general.features.chat] +description = "Gespreksassistent voor werken met PDFs." +label = "Chatassistent" + +[admin.settings.ai.general.features.classify] +description = "Categoriseer documenten automatisch op type of inhoud." +label = "Documentclassificatie" + +[admin.settings.ai.general.features.createPdf] +description = "Genereer een nieuwe PDF (bijv. vanuit HTML) via een AI agent." +label = "PDF maken vanuit prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Stel vragen en krijg antwoorden op basis van een geüpload document." +label = "Documentvragen" + +[admin.settings.ai.general.features.mathAuditor] +description = "Controleer documenten op wiskundige en numerieke fouten." +label = "Wiskunde-auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "Voeg door AI geschreven beoordelingsopmerkingen en annotaties toe aan een PDF." +label = "PDF-opmerkingenagent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Time-out voor zwaardere agentbewerkingen zoals documentgeneratie." +label = "Time-out voor langlopende taken (seconden)" + +[admin.settings.ai.general.note] +body = "De AI engine draait als een aparte service. Het gedeelde geheim" +body2 = "wordt ingesteld via een containeromgevingsvariabele. Provider API keys kunnen op deze pagina's worden ingevoerd of als engine-omgevingsvariabelen worden opgegeven; keys die hier worden ingevoerd, worden bij opslaan naar de engine gepusht." +title = "Over de AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Time-out voor gestreamde (token-voor-token) chatantwoorden." +label = "Stream-time-out (seconden)" + +[admin.settings.ai.general.test] +button = "Verbinding testen" +failBody = "De AI engine reageerde niet. Controleer de URL, of de engine-container draait en of AI is ingeschakeld (na inschakelen is een herstart nodig)." +failTitle = "AI engine onbereikbaar" +okBody = "De AI engine reageerde op een health check." +okTitle = "AI engine bereikbaar" + +[admin.settings.ai.general.timeoutSeconds] +description = "Time-out voor standaard AI-verzoeken aan de engine." +label = "Verzoektime-out (seconden)" + +[admin.settings.ai.general.url] +description = "Interne URL van de Python AI engine, bijv. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Vangrails voor hoeveel werk AI-verzoeken mogen uitvoeren en hoeveel er gelijktijdig draaien. Toegepast op de AI engine bij opslaan." +title = "Limieten & prestaties" + +[admin.settings.ai.limits.maxCharacters] +description = "Vangrail: wijs AI-verzoeken af waarvan de geëxtraheerde tekst deze lengte overschrijdt." +label = "Max. tekens per verzoek" + +[admin.settings.ai.limits.maxPages] +description = "Vangrail: wijs AI-verzoeken af met meer dan dit aantal PDF-pagina's." +label = "Max. pagina's per verzoek" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximumaantal gelijktijdige modelaanroepen in uitvoering over de hele engine." +label = "Max. gelijktijdigheid model" + +[admin.settings.ai.models] +description = "Kies de LLM-provider en de slimme/snelle modellen die de AI engine gebruikt. Toegepast op de AI engine bij opslaan." +title = "Modellen & providers" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Laat leeg om de eigen omgevingsreferentie van de engine te gebruiken. Van toepassing op zelfgehoste single-engine-implementaties." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Opgeslagen - laat leeg om de huidige key te behouden" + +[admin.settings.ai.models.baseUrl] +description = "Base URL van het OpenAI-compatibele / Ollama-endpoint, bijv. http://ollama:11434/v1." +label = "Provider base URL" +warning = "De base URL moet verwijzen naar een vertrouwd intern endpoint. De engine doet er server-side verzoeken naartoe, dus een niet-vertrouwde waarde is SSRF-gevoelig." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximumaantal outputtokens voor het snelle model." +label = "Max. tokens snel model" + +[admin.settings.ai.models.fastModel] +description = "Goedkoper, sneller model voor lichte taken. Vrije tekst; suggesties zijn alleen hints." +label = "Snel model" + +[admin.settings.ai.models.provider] +description = "Met welke LLM-provider de engine communiceert." +label = "Provider" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximumaantal outputtokens voor het slimme model." +label = "Max. tokens slim model" + +[admin.settings.ai.models.smartModel] +description = "Model met hoge capaciteit voor complexe redeneringen. Vrije tekst; suggesties zijn alleen hints." +label = "Slim model" + +[admin.settings.ai.saved] +body = "Wijzigingen worden automatisch naar de AI engine gepusht." +bodyNoPush = "Instellingen opgeslagen. Ze worden toegepast de volgende keer dat de AI engine de configuratie ophaalt." +title = "AI-instellingen opgeslagen" + [admin.settings.badge] clickToUpgrade = "Click to view plan details" @@ -938,6 +1094,25 @@ label = "Opnieuw genereren bij opstarten" description = "Aantal dagen dat het certificaat geldig is" label = "Certificaatgeldigheid (dagen)" +[admin.settings.folderAccess] +description = "Mappen waaruit mapbronnen en mapuitvoer mogen lezen en naar mogen schrijven. Dit is een beveiligingsgrens: automatiseringen kunnen nooit naar een serverpad buiten deze lijst worden verwezen." +securityNote = "Laat dit leeg om mapbronnen en mapuitvoer volledig uit te schakelen. De eigen configuratiemap van Stirling is altijd verboden terrein." +title = "Maptoegang" + +[admin.settings.folderAccess.implied] +description = "Deze door Stirling beheerde mappen zijn altijd toegestaan en kunnen hier niet worden gewijzigd." +serverStorage = "Serverbestandsopslag" +title = "Altijd toegestaan" +watchedFolder = "Bewaakte Pipeline-map" + +[admin.settings.folderAccess.roots] +add = "Toevoegen" +empty = "Geen mappen toegestaan. Mapbronnen en mapuitvoer zijn momenteel uitgeschakeld." +hint = "Voer absolute paden in, bijvoorbeeld /data/inbox." +label = "Toegestane maproots" +placeholder = "/data/inbox" +remove = "Maproot verwijderen" + [admin.settings.general] description = "Configureer toepassingsinstellingen voor het hele systeem, inclusief branding en standaardgedrag." system = "Systeem" @@ -1704,6 +1879,9 @@ width = "Breedte" [app] description = "Het gratis alternatief voor Adobe Acrobat (10M+ downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Converteren naar PDF/A-3b" convertToPdfA3bDescription = "Maakt een archief-PDF met ingesloten bijlagen" @@ -1842,6 +2020,15 @@ export = "Exporteren" accessDenied = "Toegang geweigerd" insufficientPermissions = "U hebt geen toestemming om deze actie uit te voeren." +[auth.callback] +completing = "Authenticatie voltooien" +invalidToken = "OAuth-aanmelding mislukt - ongeldig token." +missingToken = "OAuth-aanmelding mislukt - geen token ontvangen." +oauthFailed = "OAuth-aanmelding mislukt. Probeer het opnieuw." +pleaseWait = "Even geduld terwijl we je aanmelding afronden." +signedOut = "Je bent afgemeld. Meld je opnieuw aan." +windowMayClose = "Je kunt dit venster sluiten zodra dit is voltooid." + [auth.displayName] guest = "Gast" user = "Gebruiker" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Beveiligt PDF-documenten door mogelijk kwaadaardige inhoud description = "Deze tool hernoemt PDF-bestanden automatisch op basis van hun inhoud. Het analyseert het document om de meest geschikte titel uit de tekst te vinden." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatisch roteren" +tags = "automatisch roteren,oriëntatie herstellen,ondersteboven,zijwaarts,rechttrekken,oriënteren,oriëntatie detecteren,rotatie corrigeren,automatisch oriënteren,gescand,OSD" + +[autoRotate.confidenceThreshold] +title = "Betrouwbaarheidsdrempel" + +[autoRotate.detectionMode] +auto = "Automatisch" +osd = "Alleen OCR" +text = "Alleen tekst" +title = "Detectiemethode" + +[autoRotate.error] +failed = "Er is een fout opgetreden bij het automatisch roteren van de PDF." + +[autoRotate.inferUndetected] +title = "Pagina's met lage betrouwbaarheid mee roteren" + +[autoRotate.report] +applied = "{{degrees}}° met de klok mee" +confidenceValue = "Betrouwbaarheid {{value}}" +noChange = "Geen wijziging" +pageLabel = "Pagina {{number}}" +summary = "{{rotated}} van {{total}} pagina's geroteerd, {{unchanged}} ongewijzigd gelaten" +title = "Detectierapport" + +[autoRotate.report.method] +inferred = "Overeenkomend" +none = "Overgeslagen" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Onder drempel" +blankPage = "Pagina is leeg" +inferredFromDocument = "Hetzelfde als de andere pagina's" +noDominantDirection = "Gemengde tekstrichtingen" +osdFailed = "Geen leesbare tekst gevonden" +osdNoVerdict = "Kon niet bepalen wat boven is" +tesseractUnavailable = "OCR niet geïnstalleerd" +tooFewGlyphs = "Te weinig tekst" + +[autoRotate.results] +title = "Resultaten automatisch roteren" + +[autoRotate.settings] +title = "Instellingen" + +[autoRotate.tooltip.confidenceThreshold] +text = "Hoe zeker de controle moet zijn voordat een pagina wordt gedraaid. Verlaag dit om meer pagina's te roteren, verhoog dit om meer pagina's ongemoeid te laten." +title = "Betrouwbaarheidsdrempel" + +[autoRotate.tooltip.description] +text = "Bepaalt hoe elke pagina rechtop moet staan en draait deze de juiste kant op. Pagina's die al rechtop staan, blijven ongemoeid." + +[autoRotate.tooltip.detectionMode] +text = "Automatisch leest eerst de tekst op de pagina en bekijkt daarna de pagina als afbeelding als dat niet genoeg is. Alleen tekst slaat de afbeeldingscontrole over, wat het snelst is voor documenten die nooit zijn gescand. Alleen OCR behandelt elke pagina als afbeelding." +title = "Detectiemethode" + +[autoRotate.tooltip.header] +title = "Automatisch roteren" + +[autoRotate.tooltip.inferUndetected] +text = "Lege of bijna lege pagina's kunnen niet afzonderlijk worden gecontroleerd. Wanneer elke leesbare pagina dezelfde draai nodig heeft, worden deze pagina's mee gedraaid zodat ze overeenkomen." +title = "Pagina's met lage betrouwbaarheid mee roteren" + [autoSizeSplitPDF] tags = "pdf,splitsen,document,organiseren" @@ -2189,33 +2443,6 @@ keywords = "Sleutelwoorden: odd, even" numbers = "Cijfers/reeksen: 5, 10-20" progressions = "Voortgangen: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Certificaattypen" - -[certificateChoice.tooltip.organization] -bullet1 = "Beheerd door systeembeheerders" -bullet2 = "Gedeeld met geautoriseerde gebruikers" -bullet3 = "Vertegenwoordigt bedrijfsidentiteit, niet individueel" -bullet4 = "Beste voor: Officiële documenten, teamhandtekeningen" -description = "Een gedeeld certificaat dat door uw organisatie wordt geleverd. Gebruikt voor organisatiebrede ondertekeningsbevoegdheid." -title = "Organisatiecertificaat" - -[certificateChoice.tooltip.personal] -bullet1 = "Automatisch gegenereerd bij eerste gebruik" -bullet2 = "Gekoppeld aan uw gebruikersaccount" -bullet3 = "Kan niet met andere gebruikers worden gedeeld" -bullet4 = "Beste voor: Persoonlijke documenten, individuele verantwoordelijkheid" -description = "Een automatisch gegenereerd certificaat dat uniek is voor uw gebruikersaccount. Geschikt voor individuele handtekeningen." -title = "Persoonlijk certificaat" - -[certificateChoice.tooltip.upload] -bullet1 = "Vereist P12/PFX-bestand en wachtwoord" -bullet2 = "Kan worden uitgegeven door externe certificaatautoriteiten" -bullet3 = "Hoger vertrouwensniveau voor juridische documenten" -bullet4 = "Beste voor: Juridisch bindende contracten, externe validatie" -description = "Gebruik uw eigen PKCS#12-certificaatbestand. Biedt volledige controle over certificaateigenschappen." -title = "Aangepaste P12 uploaden" - [certSign] awaitingSignatures = "In afwachting van handtekeningen" chooseCertificate = "Certificaatbestand kiezen" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Certificaatbron" upload = "Uploaden" -[certSign.tooltip.header] -title = "Over het beheren van handtekeningen" - -[certSign.tooltip.overview] -bullet1 = "Controleer bestaande handtekeningen en hun geldigheid" -bullet2 = "Bekijk gedetailleerde informatie over ondertekenaars en certificaten" -bullet3 = "Voeg nieuwe digitale handtekeningen toe om uw documenten te beveiligen" -bullet4 = "Meerdere bestanden ondersteund met eenvoudige navigatie" -text = "Met deze tool kunt u controleren of uw PDF's digitaal zijn ondertekend en nieuwe digitale handtekeningen toevoegen. Digitale handtekeningen bewijzen wie een document heeft gemaakt of goedgekeurd en tonen of het sinds de ondertekening is gewijzigd." -title = "Wat kan deze tool doen?" - -[certSign.tooltip.signing] -bullet1 = "Ondersteunt PEM, PKCS12, JKS en servercertificaatformaten" -bullet2 = "Optie om de handtekening op de PDF te tonen of te verbergen" -bullet3 = "Reden, locatie en naam van ondertekenaar toevoegen" -bullet4 = "Kies op welke pagina zichtbare handtekeningen worden geplaatst" -bullet5 = "Gebruik servercertificaat voor de eenvoudige optie 'Ondertekenen met Stirling-PDF'" -text = "Om een PDF te ondertekenen, hebt u een digitaal certificaat nodig (zoals PEM, PKCS12 of JKS). U kunt ervoor kiezen de handtekening zichtbaar te maken in het document of onzichtbaar te laten voor alleen beveiliging." -title = "Handtekeningen toevoegen" - -[certSign.tooltip.validation] -bullet1 = "Toont of handtekeningen geldig of ongeldig zijn" -bullet2 = "Toont informatie over ondertekenaar en datum van ondertekening" -bullet3 = "Controleert of het document na ondertekening is gewijzigd" -bullet4 = "Kan aangepaste certificaten gebruiken voor verificatie" -text = "Wanneer u handtekeningen controleert, geeft de tool aan of ze geldig zijn, wie het document heeft ondertekend, wanneer het is ondertekend en of het document na ondertekening is gewijzigd." -title = "Handtekeningen controleren" - [changeCreds] changeUsername = "Werk uw gebruikersnaam bij. U wordt uitgelogd na het bijwerken." credsUpdated = "Account bijgewerkt" @@ -2737,12 +2936,16 @@ text = "Om deze machtigingen niet wijzigbaar te maken, gebruik de tool Wachtwoor [chat.actions] copy = "Bericht kopiëren" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling-agentopties" +assistant = "AI Assistant" clearChat = "Chat wissen" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Deze bewerking gebruikt je cloud-credits" tooltip = "Kleur van het scherm kiezen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Kies kleur" [common] +actions = "Actions" back = "Terug" cancel = "Annuleren" close = "Sluiten" +codeSample = "Code sample" collapse = "Samenvouwen" confirm = "Bevestigen" continue = "Doorgaan" copied = "Gekopieerd!" copy = "Kopiëren" done = "Gereed" +enterValue = "Enter value" error = "Fout" expand = "Uitvouwen" loading = "Laden..." next = "Volgende" +open = "Open" preview = "Voorbeeld" previous = "Vorige" refresh = "Vernieuwen" +remaining = "Resterend" retry = "Opnieuw proberen" save = "Opslaan" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Selectie wissen" @@ -2988,6 +3199,7 @@ title = "Compressiemethode" [compress.settings] desiredSize = "Gewenste bestandsgrootte" desiredSizePlaceholder = "Grootte invoeren" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Compressie is een eenvoudige manier om uw bestandsgrootte te verkleinen. Kies Bestandsgrootte om een doelformaat in te voeren en laat ons de kwaliteit voor u aanpassen. Kies Kwaliteit om de compressiesterkte handmatig in te stellen." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF-opties" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Selecteer eerst een bronformaat" settings = "Instellingen" single = "Enkel" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Geavanceerde monitoring" -allInOneWorkspace = "Alles-in-één PDF-werkruimte (viewer, tools & agent)" -apiSandbox = "API-sandbox" -dedicatedSupportSlas = "Toegewijde support & SLA's" -fasterThroughput = "10x snellere doorvoer" -fullyPrivateFiles = "Volledige bestandsprivacy" -largeFileProcessing = "Verwerking van grote bestanden" -orgWideAccess = "Organisatiebrede toegangscontrole" -premiumAiModels = "Premium AI-modellen" -prioritySupport = "Prioritaire support" -privateDocCloud = "Privé Document Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Beveiligde API-toegang" -standardThroughput = "Standaard doorvoer" -unlimitedApiAccess = "Onbeperkte API-toegang" -unlimitedSeats = "Onbeperkte seats" - [crop] autoCrop = "Witruimte autom. bijsnijden" noFileSelected = "Selecteer een PDF-bestand om te beginnen met bijsnijden" @@ -3346,6 +3558,7 @@ message = "Standaard PDF-verwerker instellen mislukt" title = "Fout" [defaultApp.prompt] +dontRemind = "Herinner mij niet opnieuw" message = "Maak Stirling PDF de standaardapp voor het openen van PDF-bestanden." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Zoeken..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Bewerken" @@ -3644,11 +3861,13 @@ shareSelected = "Geselecteerde delen" sharing = "Delen" showAll = "Alles weergeven" showHistory = "Geschiedenis weergeven" +sortBy = "Sort files" sortByDate = "Sorteren op datum" sortByName = "Sorteren op naam" sortBySize = "Sorteren op grootte" storage = "Opslag" storageState = "Opslag" +storageUsed = "Storage used" synced = "Gesynchroniseerd" title = "PDF-bestanden uploaden" toolChain = "Toegepaste tools" @@ -3694,13 +3913,17 @@ addFiles = "Bestanden toevoegen" addingFiles = "Bestanden toevoegen…" collapse = "Zijbalk samenvouwen" customizeGroups = "Groepen aanpassen" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Open bestanden om te beginnen" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Zijbalk uitvouwen" -files = "Bestanden" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive is niet geconfigureerd" leaveMyFiles = "Mijn bestanden verlaten" +library = "PDF-bibliotheek" myFiles = "Mijn bestanden" noFiles = "Nog geen bestanden" openFileManager = "Bestandsbeheer openen" @@ -3708,37 +3931,36 @@ openFromComputer = "Openen vanaf computer" openSettings = "Instellingen openen" other = "Overig" recent = "Recent" -search = "Zoeken" -searchPlaceholder = "Bestanden zoeken..." viewAll = "Alle {{count}} bestanden bekijken" [fileSidebar.fileItem] closeViewer = "Viewer sluiten" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Openen in viewer" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Toevoegen" -addLabel = "Een label toevoegen…" -categoryIconAria = "Kies een pictogram voor {{name}}" -createCategory = "Categorie maken" -delete = "Categorie verwijderen" done = "Gereed" -hide = "Categorie verbergen" -newCategory = "Nieuwe categorienaam…" -removeLabel = "{{name}} verwijderen" -rename = "Naam wijzigen" reset = "Terugzetten naar standaardinstellingen" -search = "Labels zoeken…" -show = "Categorie tonen" subtitle = "Groepeer je bestanden in hoofdcategorieën. Voeg bestaande of nieuwe labels toe aan een categorie, wijzig de naam ervan of maak je eigen categorie. Bestanden die in geen van je categorieën staan, verschijnen onder “Overig”." title = "Zijbalkcategorieën" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Aan werkruimte toevoegen" addToWorkspaceCount = "{{count}} aan werkruimte toevoegen" @@ -3749,7 +3971,6 @@ backToMyFiles = "Terug naar Mijn bestanden" breadcrumbs = "Mappad" cancel = "Annuleren" classification = "Classificatie" -clearSearch = "Zoekopdracht wissen" clearSelection = "Selectie wissen" closeDetails = "Details sluiten" create = "Maken" @@ -3781,6 +4002,7 @@ downloadAll = "Alles downloaden" downloadVersion = "Deze versie downloaden" dropOverlay = "Sleep bestanden hierheen om te uploaden" dropOverlaySub = "Bestanden beginnen in Lokaal. Gebruik 'Verplaatsen naar' of 'Opslaan in cloud' om ze in een map te organiseren." +duplicate = "Duplicate" file = "Bestand" fileInfo = "File info" fileMenu = "Bestandsacties" @@ -3808,13 +4030,12 @@ refresh = "Vernieuwen vanaf server" remove = "Verwijderen" removeVersion = "Deze versie verwijderen" rename = "Naam wijzigen" +renamed = "Hernoemd" renameFolder = "Mapnaam wijzigen" resizeFolderTree = "Mapstructuur vergroten/verkleinen (pijltjestoetsen, Shift voor grotere stappen; dubbelklik om automatisch te passen)" save = "Opslaan" saveToServer = "Opslaan op server" saveToServerDisabledHint = "Opslaan op de server is niet ingeschakeld op deze server. Vraag uw beheerder om dit in te schakelen." -search = "Zoeken" -searchPlaceholder = "Deze map en submappen doorzoeken" selectAll = "Alles selecteren" selectAllHint = "Klik om alles te selecteren. Tip: houd Ctrl (of Cmd) ingedrukt om bestanden één voor één toe te voegen, Shift om een bereik te selecteren." selectedCount = "{{count}} geselecteerd" @@ -3874,6 +4095,10 @@ title = "Nog geen cloudbestanden" hint = "Bestanden die zijn opgeslagen zonder te uploaden blijven hier. Zet een bestand neer om er een toe te voegen." title = "Geen alleen-lokale bestanden" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Bestanden die u opent of bewerkt, verschijnen hier." title = "Nog niets gewijzigd" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Kan map niet verwijderen." deleteFolderFailedDetail = "Kan map niet verwijderen: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Kan mapuiterlijk niet bijwerken." folderAppearanceFailedDetail = "Kan mapuiterlijk niet bijwerken: {{message}}" moveFilesFailed = "Kan bestanden niet verplaatsen." @@ -3910,9 +4137,7 @@ count = "Bestanden" folder = "Map" labels = "Labels" modified = "Gewijzigd" -name = "Naam" size = "Grootte" -toolHistoryAtVersion = "Cumulatieve toolketen" totalSize = "Totale grootte" type = "Type" versionHistory = "Versietraject" @@ -3945,6 +4170,11 @@ localHint = "Alleen opgeslagen in deze browser" shared = "Gedeeld" sharedHint = "Met u gedeeld via link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Oudste eerst" modifiedDesc = "Recent eerst" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Geen bestanden beschikbaar in opslag. Open eerst enkele open = "Openen" openFile = "Bestand openen" openFiles = "Bestanden openen" +selectFile = "Select {{name}}" selectFromStorage = "Selecteren uit opslag" upload = "Uploaden" uploadFile = "Bestand uploaden" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Alles opgeslagen" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV extraheren mislukt" +extractXlsxError = "XLSX extraheren mislukt" +filled = "filled" flattenAfterFilling = "Afvlakken na invullen" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "verpl." +requiredFieldsError = "Vul alle verplichte velden in" rescanFields = "Velden opnieuw scannen" rescanFormFields = "Formuliervelden opnieuw scannen" save = "Opslaan" +saveShortcut = "Ctrl+S om op te slaan" +unsavedChanges = "Niet-opgeslagen wijzigingen" [formFill.sidebar] close = "Zijbalk sluiten" @@ -4309,6 +4552,11 @@ desc = "Hernoemt een PDF-bestand automatisch op basis van de gedetecteerde kop" tags = "automatisch detecteren,op kop gebaseerd,organiseren,herlabelen" title = "PDF automatisch hernoemen" +[home.autoRotate] +desc = "Detecteer de oriëntatie van elke pagina en roteer deze automatisch rechtop." +tags = "automatisch roteren,oriëntatie herstellen,ondersteboven,zijwaarts,rechttrekken,oriënteren,oriëntatie detecteren,rotatie corrigeren,automatisch oriënteren,gescand,OSD" +title = "Automatisch roteren" + [home.autoSizeSplitPDF] tags = "auto,splitsen,grootte" title = "Automatisch splitsen op grootte/aantal" @@ -4608,15 +4856,12 @@ hint = "Upload een PNG, JPG, SVG of ander afbeeldingsbestand om op de PDF te pla label = "Afbeelding uploaden" placeholder = "Afbeeldingsbestand selecteren" -[imageToPdf] -tags = "conversie,img,jpg,foto" - [imageToPDF] header = "Afbeelding naar PDF" title = "Afbeelding naar PDF" -[infoBanner] -dismiss = "Negeren" +[imageToPdf] +tags = "conversie,img,jpg,foto" [invite] acceptError = "Account aanmaken mislukt" @@ -4647,11 +4892,9 @@ welcomeTitle = "U bent uitgenodigd!" [landing] addFiles = "Bestanden toevoegen" -heroSubtitle = "Sleep of voeg een bestaande PDF toe om te beginnen." mobileUpload = "Upload from Mobile" openFromComputer = "Openen vanaf computer" uploadFromComputer = "Uploaden vanaf computer" -workbenchEmptyStateHero = "Zet ergens een PDF neer" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Voer een gebruikersnaam in" failedToSignIn = "Inloggen met {{provider}} mislukt: {{message}}" forgotPassword = "Wachtwoord vergeten?" loggingIn = "Inloggen..." -logIn = "Inloggen" login = "Inloggen" magicLinkSent = "Magic link verzonden naar {{email}}! Controleer uw e-mail en klik op de link om in te loggen." mfaCode = "Authenticatiecode" @@ -4715,26 +4957,10 @@ signInWith = "Inloggen met" title = "Inloggen" unexpectedError = "Onverwachte fout: {{message}}" updatePassword = "Wachtwoord bijwerken" -useEmailInstead = "Inloggen met e-mail" useMagicLink = "Gebruik in plaats daarvan een magic link" username = "Gebruikersnaam" youAreLoggedIn = "U bent ingelogd!" -[login.slides.edit] -alt = "PDF's bewerken" -subtitle = "Met meer dan een dozijn tools om PDF's te anonimiseren, ondertekenen, lezen en bewerken vindt u zeker wat u zoekt." -title = "Bewerk PDF's om de gewenste informatie te tonen/beveiligen" - -[login.slides.overview] -alt = "Stirling PDF-overzicht" -subtitle = "Een privacy-first cloud suite voor PDF's waarmee u documenten kunt converteren, ondertekenen, anonimiseren en beheren, plus 50+ andere krachtige tools." -title = "Uw alles-in-een oplossing voor al uw PDF-behoeften." - -[login.slides.secure] -alt = "PDF's beveiligen" -subtitle = "Voeg wachtwoorden toe, anonimiseer inhoud en beheer certificaten met gemak." -title = "Bescherm gevoelige informatie in uw PDF's" - [margin] large = "Groot" medium = "Middelgroot" @@ -4818,6 +5044,34 @@ uploadSuccess = "Upload Successful!" uploadSuccessMessage = "Your images have been transferred." validating = "Validating session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scan to upload photos. Images auto-convert to PDF." descriptionNoConvert = "Scan to upload photos from your mobile device." @@ -4836,8 +5090,13 @@ title = "Upload from Mobile" tags = "Multitool,meerdere bewerkingen,UI,klik sleep,voorkant,clientzijde,interactief,beweegbaar,verplaats" title = "PDF Multitool" -[navbar] -search = "Zoeken" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Authenticatie is niet geslaagd. U kunt dit venster sluiten en het opnieuw proberen." @@ -4966,6 +5225,7 @@ activeFiles = "De weergave Actieve bestanden toont alle PDF's d allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Sluiten" cropSettings = "Nu we het bestand hebben geselecteerd dat we willen bijsnijden, kunnen we de tool Bijsnijden configureren om het gebied te kiezen waarnaar we de PDF willen bijsnijden." +dialogLabel = "Onboarding" fileCheckbox = "Door op een van de bestanden te klikken selecteert u het voor verwerking. U kunt meerdere bestanden selecteren voor batchbewerkingen." fileReplacement = "Het gewijzigde bestand vervangt automatisch het originele bestand in de Werkbank, zodat u het eenvoudig door meer tools kunt halen." filesButton = "Met de knop Bestanden op de sneltoegangsbalk kunt u PDF's uploaden om de tools op toe te passen." @@ -4976,6 +5236,7 @@ pinButton = "U kunt de knop Vastzetten gebruiken als u wilt dat results = "Nadat de tool klaar is met uitvoeren, toont de stap Beoordeling een voorbeeld van de resultaten in dit paneel, en kunt u de bewerking ongedaan maken of het bestand downloaden." runButton = "Zodra de tool is geconfigureerd, kunt u met deze knop de tool uitvoeren op alle geselecteerde PDF's." selectCropTool = "Laten we de tool Bijsnijden selecteren om te laten zien hoe u een van de tools gebruikt." +stepOf = "Stap {{current}} van {{total}}" toolInterface = "Dit is de interface van de tool Bijsnijden. Zoals u ziet is er nog niet veel, omdat we nog geen PDF-bestanden hebben toegevoegd om mee te werken." workbench = "Dit is de Werkbank - het hoofdgebied waar u uw PDF's bekijkt en bewerkt." wrapUp = "U bent er klaar voor! U kent de belangrijkste onderdelen van de app en hoe u ze gebruikt. Klik wanneer u wilt op de knop Help om deze rondleiding opnieuw te zien." @@ -4988,6 +5249,26 @@ showMeAround = "Geef me een rondleiding" skipForNow = "Voor nu overslaan" skipTheTour = "Rondleiding overslaan" +[onboarding.checklist] +dismiss = "Sluiten" +title = "Stirling PDF instellen" + +[onboarding.checklist.downloadDesktop] +description = "Draai Stirling native op je machine" +title = "Stirling voor Desktop downloaden" + +[onboarding.checklist.inviteTeam] +description = "Werk samen met je team" +title = "Teamleden uitnodigen" + +[onboarding.checklist.shareAnalytics] +description = "Help Stirling verbeteren" +title = "Anonieme gebruiksgegevens delen" + +[onboarding.checklist.takeTour] +description = "Bekijk in een korte rondleiding hoe Stirling werkt" +title = "Rondleiding starten" + [onboarding.desktopInstall] body = "Stirling werkt het best als desktopapp. U kunt het offline gebruiken, sneller documenten openen en lokaal op uw computer bewerken." selectOs = "Besturingssysteem selecteren" @@ -4996,7 +5277,21 @@ titleWithOs = "Downloaden voor {{osLabel}}" [onboarding.mfa] authenticationCode = "Authenticatiecode" +description = "Beveilig je account door een authenticator-app te koppelen. Scan de QR code of voer de instelsleutel in en bevestig daarna de 6-cijferige code om af te ronden." +enable = "MFA inschakelen" +enableError = "Kan tweefactorauthenticatie niet inschakelen. Controleer de code en probeer het opnieuw." +enterCode = "Voer de 6-cijferige code uit je app in." +enterCodeError = "Voer de authenticatiecode in om door te gaan." +logout = "Uitloggen" +openAuthenticator = "Open Google Authenticator, Authy of 1Password." qrCodeLoading = "Uw QR-code genereren…" +regenerateQrCode = "QR code opnieuw genereren" +scanQrCode = "Scan de QR code of voer hieronder de instelsleutel in." +setupError = "Kan tweefactorinstelling niet starten. Probeer het opnieuw." +setupKey = "Instelsleutel (handmatige invoer)" +stepByStep = "Stap voor stap" +success = "MFA is ingeschakeld. Je kunt nu doorgaan." +title = "Multi-Factor Authentication instellen" [onboarding.planOverview] adminBodyLoginDisabled = "Zodra u de loginmodus inschakelt, kunt u gebruikers beheren, instellingen configureren en de servergezondheid monitoren. De eerste {{freeTierLimit}} personen op uw server gebruiken Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Admin-overzicht" userBody = "Nodig teamgenoten uit, wijs rollen toe en houd uw documenten georganiseerd in één veilige werkruimte. Schakel de loginmodus in wanneer u klaar bent om verder te groeien dan solo-gebruik." userTitle = "Abonnementoverzicht" +[onboarding.processorIntro] +body = "Stirling voert nu Beleid uit — geautomatiseerde regels die elk document classificeren, beveiligen en verwerken zodra het binnenkomt. Stel ze in en bewaak uitvoeringen in de Processor." +cta = "Bekijk de Processor" +title = "Bekijk de Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Vooruitbetaald jaar" + +[payg.prepaid.banner] +exhausted = "Je vooruitbetaalde capaciteit is opgebruikt; gefactureerd gebruik is hervat. Waardeer op om de korting te behouden." +expiring_one = "Je vooruitbetaalde jaar eindigt over {{count}} dag. Ongebruikte capaciteit verloopt — waardeer op om een nieuw jaar te starten." +expiring_other = "Je vooruitbetaalde jaar eindigt over {{count}} dagen. Ongebruikte capaciteit verloopt — waardeer op om een nieuw jaar te starten." +low = "Je hebt nog {{pct}}% van je vooruitbetaalde capaciteit over. Waardeer op elk moment op om verwerking zonder onderbreking voort te zetten." + +[payg.prepaid.card] +subtitle = "Gebruikt vóór gefactureerd gebruik en buiten je bestedingslimiet." +title = "Vooruitbetaalde capaciteit" + +[payg.prepaid.meter] +capSuffix = "van {{total}} vooruitbetaalde credits" +expires = "Verloopt {{date}}" + +[payg.prepaid.state] +exhausted = "Opgebruikt" +healthy = "Ruim voldoende over" +low = "Bijna op" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF naar PDF/A" tags = "archief,langdurig,standaard,conversie,opslag,bewaring" title = "PDF naar PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "print,standaard,conversie,productie,prepress,archief" title = "PDF naar PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Credits" xsmall = "100 Credits" [plan.availablePlans] +currency = "Billing currency" subtitle = "Kies het abonnement dat bij uw behoeften past" title = "Beschikbare abonnementen" @@ -5937,49 +6267,12 @@ keyDescription = "Plak de licentiesleutel uit uw e-mail" success = "Licentie geactiveerd!" successMessage = "Uw licentie is met succes geactiveerd. U kunt dit venster nu sluiten." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "afgedwongen" -enforcing = "Bezig met afdwingen..." -failed = "Afdwingen mislukt" outputsUnavailable = "Beleidsuitvoer is niet langer beschikbaar om te downloaden." partialOutputsUnavailable = "Sommige beleidsuitvoer is niet langer beschikbaar om te downloaden." -retrying = "Bezig, opnieuw proberen..." runNotFound = "De afdwingingsuitvoering kon niet meer worden gevonden." -step = "stap {{current}}/{{total}}" timedOut = "Time-out bij afdwingen voordat de uitvoering kon worden voltooid." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Sluiten" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Bezig met toepassen van {{names}}" applyingProgress = "Bezig met toepassen van {{names}} ({{done}} van {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} van {{total}} bestand(en) konden niet worden verwerk failureTitle = "Geëxporteerd zonder volledige afdwinging" printPolicyAppliedBody = "Deze PDF is bijgewerkt om aan een beleid te voldoen. Controleer de wijzigingen en druk vervolgens opnieuw af." printPolicyAppliedTitle = "Beleid toegepast vóór het afdrukken" -queued = "+{{count}} in wachtrij" successTitle = "{{names}} toegepast" summaryMore = "{{first}}, {{second}} en nog {{more}}" summaryTwo = "{{first}} en {{second}}" -[policies.enforcement.triggerVerb] -convert = "Afdwingen vóór converteren" -default = "Afdwingen" -export = "Afdwingen vóór exporteren" -input = "Afdwingen bij importeren" -print = "Afdwingen vóór afdrukken" - -[policies.field] -accessLog = "Toegangslog" -archiveAfter = "Archiveren na" -auditTrail = "Audittrail" -belowThreshold = "Onder drempelwaarde" -destination = "Bestemming" -frameworks = "Frameworks" -immutableHold = "Onveranderlijke blokkering" -keepFor = "Bewaren gedurende" -minConfidence = "Min. betrouwbaarheid" -notify = "Melden bij routering" -onViolation = "Bij niet-naleving" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Toevoegen" -addPlaceholder = "Een label toevoegen…" -customNote = "Aangepast voor je team." -defaultNote = "Gebruikt de ingebouwde standaard, gedeeld met je team." -duplicate = "\"{{name}}\" bestaat al." -edit = "Labels bewerken" -empty = "Nog geen labels." -export = "JSON exporteren" -iconAria = "Kies een pictogram voor {{name}}" -import = "JSON importeren" -importError = "Kon dat bestand niet importeren." -managedNote = "Teamlabels worden beheerd door je teamleider." -modalSubtitle = "Gedeeld met je hele team. De classifier kiest de labels die bij elk document passen." -modalTitle = "Classificatielabels" -pickIcon = "Pictogram {{label}} gebruiken" -removeAria = "{{name}} verwijderen" -resetToDefault = "Terugzetten naar standaard" -saveForTeam = "Opslaan voor team" -saving = "Opslaan…" -sectionLabel = "Classificatielabels" -startFromScratch = "Vanaf nul beginnen" -teamCount = "teamlabels" -tooLong = "Labels mogen maximaal {{max}} tekens bevatten." -ungrouped = "Niet gegroepeerd" -view = "Labels bekijken" +categoryCount = "categorieën" +hideCategory = "Categorie verbergen" +labelCount = "labels" +sharedNote = "Deze labels zijn ingebouwd en worden gedeeld met je hele team." +showCategory = "Categorie tonen" [policies.pii] account = "Rekeningnummers (gelabeld)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US-routingnummers (ABA)" ssn = "Social Security-nummers" -[policies.settings] -noneExport = "Er worden momenteel geen beleidsregels uitgevoerd bij export." -noneUpload = "Er worden momenteel geen beleidsregels uitgevoerd bij upload." -onExport = "Bij export" -onUpload = "Bij upload" -reorderHandle = "Slepen om te herschikken" -runOrderDesc = "Wanneer meer dan één beleid op dezelfde trigger wordt uitgevoerd, worden ze in deze volgorde uitgevoerd — elk op de uitvoer van het vorige beleid. Sleep om te herschikken." -title = "Beleidsinstellingen" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Beleidsopties" -policySettings = "Beleidsinstellingen" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Mislukte beleidsregels opnieuw proberen ({{count}})" -rowProgress = "{{completed}} van {{total}} bestanden verwerkt" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Wat is een beleid?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Zoekt automatisch gevoelige gegevens en maakt ze zwart — zoals Social Security- en kaartnummers — zodat ze niet in het document kunnen worden gelezen." -sanitize = "Verwijdert verborgen JavaScript uit het bestand, zodat er niets automatisch wordt uitgevoerd wanneer iemand het opent." -watermark = "Plaatst een zichtbaar merkteken (bijv. \"Confidential\") over elke pagina." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Sluiten" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} wordt afgedwongen..." badgeRan = "{{name}}-beleid is op dit bestand uitgevoerd" +badgeRunning = "{{name}} wordt uitgevoerd..." blockingAction = "{{action}} geblokkeerd tijdens afdwingen van beleid, even geduld..." dismiss = "Overlay sluiten" enforcingTitle = "Beleid afdwingen..." @@ -6151,6 +6316,7 @@ viewAnyway = "Bestand bekijken (beleid wordt nog afgedwongen)" [policyOption] autoRedactPhi = "PHI automatisch redigeren" blockExport = "Export blokkeren" +ccpa = "CCPA" contracts = "Contracten" documents = "Documenten" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor-abonnement" subscribed = "Processor-abonnement" unlinked = "Niet gekoppeld" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap vanuit document" -sectionsAriaLabel = "Secties van Agent Builder" -selectorAriaLabel = "Agents" -subtitle = "Ontwerp, test en publiceer de AI-agenten die je documenten classificeren, er gegevens uit extraheren en ze routeren. Definieer scenario's, begrens tooltoegang, voer een golden set uit en publiceer een versie." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Annuleren" -dropzoneText = "Kies een voorbeelddocument (PDF of afbeelding)" -lead = "Zet een voorbeelddocument neer en we stellen scenario's en een extractieschema voor dat je kunt verfijnen. Er wordt niets gepubliceerd totdat je het hebt beoordeeld." -submit = "Agent bootstrappen" -subtitle = "Start een nieuwe agent vanuit één representatief bestand" -title = "Bootstrap vanuit een document" - -[portal.agentBuilder.empty] -description = "Bootstrap een agent vanuit een voorbeelddocument om de scenario's en het extractieschema te starten, verfijn en publiceer daarna." -title = "Nog geen agents" - -[portal.agentBuilder.evals] -casesPassing = "Geslaagde cases" -columnCase = "Eval-case" -columnLatency = "Latentie" -columnResult = "Resultaat" -fail = "mislukt" -goldenSetPassRate = "Slagingspercentage golden set" -latencyMs = "{{ms}} ms" -notRun = "niet uitgevoerd" -pass = "geslaagd" -passRate = "Slagingspercentage" -runEvals = "Evals uitvoeren" - -[portal.agentBuilder.evals.empty] -description = "Evals maken van je scenario's een herhaalbare golden set. Upgrade om het slagingspercentage in de tijd vast te leggen en publicaties daarop te blokkeren." -title = "Nog geen golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "over golden sets" -activeAgents = "Actieve agents" -avgPassRate = "Gem. slagingspercentage evals" -fleetWide = "over de hele fleet" -latestPublished = "Laatst gepubliceerd" -scenarios = "Scenario's" -testCases = "testcases" -totalDescription_one = "{{count}} totaal" -totalDescription_other = "{{count}} totaal" - -[portal.agentBuilder.scenarios] -add = "Toevoegen" -addScenario = "Scenario toevoegen" -enable = "Inschakelen" -expectationLabel = "Verwacht gedrag" -expectationPlaceholder = "Wat de agent moet doen" -inEval = "in eval" -mute = "Dempen" -muted = "gedempt" -nameLabel = "Naam" -namePlaceholder = "bijv. compliance-escalatie" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenario's" -tools = "Tools" -versions = "Versies" - -[portal.agentBuilder.tools] -broadAccess = "Brede toegang" -deniedHint = "Geselecteerde tools zijn geblokkeerd. Al het overige blijft aanroepbaar." -deniedTools = "Geweigerde tools" -governanceGate = "Tool-governance is beschikbaar in het Enterprise-abonnement." -restricted = "Beperkt" -restrictedAccess = "Beperkte tooltoegang" -restrictedDescription = "Sta elke tool toe behalve de tools die je hieronder weigert." - -[portal.agentBuilder.versions] -current = "huidig" -historyGate = "Volledige versiegeschiedenis en rollback zijn beschikbaar in het Enterprise-abonnement." -publish = "Publiceren" -rollBack = "Terugdraaien" - [portal.assistant] close = "Assistent sluiten" error = "Kon de assistent niet bereiken." @@ -6365,9 +6451,21 @@ typing = "Aan het typen" loading = "Laden" redirectingToEditor = "Doorsturen naar de editor..." +[portal.billing.activation] +maybeLater = "Misschien later" +title = "Schakel de Processor in" + +[portal.billing.activation.payg] +sub = "Stel een maandelijkse limiet in. Betaal voor wat je uitvoert." +title = "Betalen naar gebruik" + +[portal.billing.activation.prepay] +badge = "2 maanden gratis" +sub = "Eén factuur, kaart of bankoverschrijving." +title = "Een jaar vooruitbetalen" + [portal.billing.checkout] noClientSecret = "Edge-functie heeft geen client_secret geretourneerd." -subtitle = "Voeg een kaart toe om door te gaan na je gratis Editor-abonnementstegoed. Stripe regelt de rest." title = "Processor-abonnement inschakelen" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Je betaling is gelukt, maar activering duurt iets langer dan normaal. He close = "Sluiten" title = "Bijna klaar" +[portal.billing.checkout.cap] +amountAria = "Maandelijkse bestedingslimiet" +back = "Terug" +continue = "Doorgaan naar betaling" +error = "Kan je bestedingslimiet niet instellen" +finePrint = "Facturen worden maandelijks verstuurd. Altijd opzegbaar." +noLimit = "Geen limiet" +note = "Je wordt nooit boven je limiet gefactureerd. Verwerking pauzeert alleen." +perMonth = "/ mnd" +title = "Stel je bestedingslimiet in" + +[portal.billing.checkout.card] +fields = "Kaartnummer · MM / JJ · CVC · ZIP" +label = "Kaartgegevens" +note = "Kaartgegevens worden verzameld door Stripe. Stirling slaat nooit PAN of CVC op." + [portal.billing.checkout.error] title = "Kon checkout niet starten" @@ -6383,10 +6497,9 @@ body = "Je betaling is verwerkt. We schakelen verbruiksgemeten verwerking in voo hint = "Houd dit venster open." title = "Je Processor-abonnement activeren..." -[portal.billing.checkout.notConfigured] -bodyAfter = "in de portal env om checkout in de app in te schakelen." -bodyBefore = "Stel" -title = "Stripe niet geconfigureerd" +[portal.billing.checkout.pay] +limit = "Maandelijkse bestedingslimiet {{amount}}/mnd. Verwerking pauzeert bij de limiet." +limitNoCap = "Geen bestedingslimiet. Verwerking pauzeert niet. Facturen worden maandelijks verstuurd." [portal.billing.enterpriseUpsell] cta = "Stel je Enterprise-offerte samen" @@ -6409,10 +6522,12 @@ currentPlan = "Huidig abonnement" freeForever = "Voor altijd gratis" noTeamResolved = "Er is nog geen team gekoppeld aan je wallet — vernieuw en probeer opnieuw." ownerOnly = "Alleen de teameigenaar kan het Processor-abonnement inschakelen." +payInvoice = "Factuur betalen om te voltooien" planName = "Editor" ssoIncluded = "SSO inbegrepen" switchOnProcessor = "Processor inschakelen →" unlimitedUsers = "Onbeperkt aantal gebruikers" +viewQuote = "Offerte bekijken" [portal.billing.invoices] columnAmount = "Bedrag" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Directe API-verzoeken" segmentApiLabel = "API" segmentAutomationDesc = "Automatiseringen en pipelines" segmentAutomationLabel = "Automatisering" +sizeMultiplier = "{{formatted}} PDFs gebruikten een groottevermenigvuldiger" +summary = "{{unique}} uniek · {{units}} meeteenheden · {{avg}} gem. per PDF" +summaryNoRate = "{{unique}} uniek · {{units}} meeteenheden" unit = "verbruiksgemeten PDF's" +unitsPending_one = "{{units}} meeteenheid wacht op sync vanaf gekoppelde instanties" +unitsPending_other = "{{units}} meeteenheden wachten op sync vanaf gekoppelde instanties" + +[portal.billing.prepaid] +capSuffix = "van {{total}} vooruitbetaalde credits" +expires = "Verloopt {{date}}" +eyebrow = "Vooruitbetaalde capaciteit" +note = "Gebruikt vóór gefactureerd gebruik en buiten je bestedingslimiet." +topUp = "Opwaarderen" + +[portal.billing.prepaid.buy] +back = "Terug" +calcTitle = "Bereken je jaarlijkse betaling" +cancel = "Annuleren" +cancelPurchase = "Aankoop annuleren" +close = "Sluiten" +continueToPayment = "Doorgaan naar betaling" +finalise = "Afronden" +invoiceErrorTitle = "Kan de factuur niet genereren" +loadingTitle = "Je offerte laden" +payTitle = "Betaal voor je jaar" +step = "Stap {{current}} van {{total}}" + +[portal.billing.prepaid.calc] +change = "Wijzigen" +deployRow = "Implementatie" +done = "Gereed" +enterpriseHint = "Dit is enterprise-schaal. Bij dit volume zijn enterprise-tarieven beter dan welke selfservicekorting ook. Tariefvastheid, voorwaarden en een offerte binnen enkele minuten." +governanceRow = "Governance" +governanceValue = "{{label}} · {{count}} beleidsregels" +handlesLabel = "Je Processor" +handlesValue = "verwerkt {{volume}} credits / mnd" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} beleidsregels" +poolCaption = "Eén pool van {{credits}} credits voor het jaar. Zware maanden lenen van lichte maanden." +priceLabel = "Je jaar · 12 maanden voor de prijs van 10" +rateUnknown = "We tonen de exacte prijs bij het afrekenen." +savings = "Je bespaart {{amount}} · 2 maanden gratis." +sizingRow = "Omvang" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Totaal aantal gebruikers" + +[portal.billing.prepaid.consent] +label = "Ik begrijp dat wanneer mijn vooruitbetaalde capaciteit is opgebruikt of na 12 maanden verloopt, verwerking automatisch doorgaat tegen het standaard gemeten pay-as-you-go-tarief (tot mijn bestedingslimiet), tenzij ik annuleer, en dat ik op elk moment kan annuleren via de billing portal." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Beheerd door Stirling. Live binnen enkele minuten." +cloudValue = "Stirling Cloud · Beheerd" +selfhost = "Zelfgehost" +selfhostDesc = "Draait op private infrastructuur. Zelfde tarief." +selfhostValue = "Zelfgehost · Private infrastructuur" + +[portal.billing.prepaid.offer] +cta = "Aanbod bekijken" +subtitle = "Betaal een jaar PDF-verwerking vooruit en krijg twee maanden gratis, gebruikt vóór gefactureerd gebruik, buiten je bestedingslimiet." +title = "Krijg 12 maanden voor de prijs van 10" + +[portal.billing.prepaid.pay] +accountName = "Naam rekeninghouder" +accountNamePlaceholder = "bijv. Jane Smith" +companyName = "Bedrijfsnaam (optioneel)" +companyNamePlaceholder = "bijv. Acme Inc." +downloadInvoice = "Factuur downloaden" +payOnline = "Online betalen" +poNumber = "PO-nummer (optioneel)" +poNumberPlaceholder = "bijv. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Geavanceerd" +advancedDesc = "Pipelines sturen het grootste deel van je verwerking aan." +none = "Geen" +noneDesc = "Er draaien nog geen pipelines. Schakel ze op elk moment in." +standard = "Standaard" +standardDesc = "Een paar pipelines verwerken binnenkomende PDFs opnieuw." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Classificatie, de standaard, plus Delen" +governed = "Governed" +governedDesc = "Voegt Beveiliging en Routering toe" +regulated = "Regulated" +regulatedDesc = "Elke categorie, incl. Compliance, Retentie, Ingestie" + +[portal.billing.prepaid.review] +download = "Offerte downloaden (PDF)" +downloading = "Voorbereiden…" +share = "delen voor goedkeuring" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "Bestanden onder 25 MB, geen datakosten" +heavy = "Zwaar" +heavyDesc = "Gescand of met veel afbeeldingen, doorgaans 50 MB+" +standard = "Standaard" +standardDesc = "Vooral klein, enkele scans boven 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Opgebruikt" +healthy = "Ruim voldoende over" +low = "Bijna op" [portal.billing.spendLimit] adjustLimit = "Limiet aanpassen" @@ -6494,6 +6713,8 @@ label = "Verwacht te overschrijden." [portal.billing.spendThisMonth] eyebrow = "Uitgaven deze maand" +freeRemaining_one = "{{formatted}} gratis PDF resterend" +freeRemaining_other = "{{formatted}} gratis PDFs resterend" processed_one = "{{formattedCount}} PDF verwerkt." processed_other = "{{formattedCount}} PDF's verwerkt." processedWithRate_one = "{{formattedCount}} PDF verwerkt, à {{rate}} per stuk." @@ -6510,6 +6731,7 @@ reachedTitle = "Maandelijkse uitgavelimiet bereikt" title = "Kon Stripe-portal niet openen" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "van {{allowance}} gratis PDF's gebruikt" capSuffix_other = "van {{allowance}} gratis PDF's gebruikt" eyebrow = "Processor-proefperiode" @@ -6534,6 +6756,376 @@ signature = "handtekening" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nieuwe verbinding" +createTitleFor = "{{name}} verbinden" +delete = "Verwijderen" +edit = "Bewerken" +editTitle = "Verbinding bewerken" +unsupportedType = "Dit verbindingstype kan hier niet worden bewerkt." + +[portal.connections.categories.advanced] +description = "Voor een service die we nog niet ondersteunen." +label = "Geavanceerd" + +[portal.connections.categories.audit] +description = "Leg vast wat er met een document is gebeurd, waar je auditors al kijken." +label = "Audit- & compliancelogging" + +[portal.connections.categories.notify] +description = "Vertel een ander systeem dat er iets is gebeurd." +label = "Melden & workflow" + +[portal.connections.categories.security] +description = "Scan of label een document voordat het verdergaat." +label = "Beveiliging & classificatie" + +[portal.connections.categories.signing] +description = "Stuur een document om te laten ondertekenen of notarieel vastleggen." +label = "E-handtekening & notariële vastlegging" + +[portal.connections.categories.storage] +description = "Waar verwerkte documenten terechtkomen - een bucket, een drive, een ticket of een pagina." +label = "Bestand & bijlage" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling slaat dit versleuteld op en toont het nooit meer." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Stappen die deze verbinding gebruiken, kunnen alleen paden onder deze URL aanroepen, nooit een andere host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "E-mail" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Wachtwoord" + +[portal.connections.commonFields.port] +label = "Poort" + +[portal.connections.commonFields.username] +label = "Gebruikersnaam" + +[portal.connections.commonFields.webhookUrl] +helperText = "Plak de URL die de service je heeft gegeven. Alleen paden daaronder kunnen worden aangeroepen." +label = "Webhook URL" + +[portal.connections.fields] +name = "Verbindingsnaam" +namePlaceholder = "bijv. Productie" + +[portal.connections.picker] +cancel = "Annuleren" +createNew = "Nieuwe verbinding..." +placeholder = "Selecteer een verbinding" +save = "Verbinding opslaan" + +[portal.connections.picker2] +back = "Integratie wijzigen" +heading = "Wat wil je verbinden?" +noResultsBody = "We ondersteunen dit nog niet - vertel ons erover en we bekijken of we het kunnen toevoegen." +noResultsTitle = "Niets komt overeen met \"{{query}}\"" +searchPlaceholder = "Integraties zoeken - probeer \"sign\", \"ocr\" of een productnaam" + +[portal.connections.types.api] +description = "Roep elke HTTP API aan vanuit een beleidsregel. Jij kiest de URL, authenticatie en payload." +label = "Aangepaste API" + +[portal.connections.types.api.fields.authType] +label = "Authenticatie" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Gebruikersnaam en wachtwoord" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token in een aangepaste header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Geen" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Inloggen voor een token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Stappen die deze verbinding gebruiken, kunnen alleen paden onder deze URL aanroepen, nooit een andere host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Headernaam" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Inlogpad" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Wachtwoord" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Als de API antwoordt met een downloadlink, mogen alleen deze hosts worden opgehaald. Scheid met komma's. Laat leeg om alleen de eigen host van de base URL toe te staan." +label = "Toegestane resultaathosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Verstuur de token als deze header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "De response-header waarin de login-aanroep de token retourneert, bijv. X-Auth-Token." +label = "Token-response-header" + +[portal.connections.types.api.fields.username] +label = "Gebruikersnaam" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scan documenten op malware op een server die je beheert - niets verlaat je netwerk." +label = "ClamAV (zelfgehost)" + +[portal.connections.types.cloudmersive] +description = "Virus- en contentscanning voor documenten in een beleidsregel." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokkeer documenten met macro's, uitvoerbare bestanden, scripts of ingesloten objecten - niet alleen bekende malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Voeg verwerkte documenten toe als bijlage aan een Confluence-pagina." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Stuur documenten naar ConsignO Cloud (Notarius) voor elektronische ondertekening en notariële vastlegging." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Je ConsignO Cloud-omgeving, inclusief /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Wachtwoord" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Alleen nodig als je organisatie tenants gebruikt." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Gebruikersnaam" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Plaats een bericht op Discord wanneer een beleidsregel een document verwerkt." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexeer een auditgebeurtenis in Elasticsearch telkens wanneer een beleidsregel een document behandelt." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Controleer een FTP- of FTPS-server op nieuwe documenten." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Verbindingsmodus" + +[portal.connections.types.ftp.fields.passive.options] +active = "Actief" +passive = "Passief (aanbevolen)" + +[portal.connections.types.ftp.fields.security] +helperText = "Gewone FTP verstuurt referenties en bestanden onversleuteld; kies FTPS als de server dit ondersteunt." +label = "Versleuteling" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (expliciet / AUTH TLS)" +implicit = "FTPS (impliciet)" +none = "Geen (gewone FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Plaats een bericht in een Google Chat-ruimte wanneer een beleidsregel wordt uitgevoerd." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Voeg verwerkte documenten toe als bijlage aan een Jira-issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "E-mail een document of melding wanneer een beleidsregel wordt uitgevoerd." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Sla verwerkte documenten op in Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App-wachtwoord" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detecteer persoonsgegevens in documenttekst met een Presidio-server die je beheert." +label = "Presidio (zelfgehost)" + +[portal.connections.types.purview] +description = "Lees en pas Microsoft Purview-gevoeligheidslabels toe op PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Optioneel. Alleen nodig om labels op naam te kiezen; labelen werkt ook zonder dit." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Wordt op elk label gestempeld als SiteId, zodat Purview-bewuste tools weten welke organisatie het heeft toegepast." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Een S3 bucket waaruit bronnen kunnen lezen en waar Pipeline-uitvoer naartoe kan schrijven." +label = "S3-opslag" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Laat leeg voor AWS. Stel dit in voor MinIO of een andere S3-compatibele opslag." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Regio" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "E-mail een document of melding wanneer een beleidsregel wordt uitgevoerd." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Controleer een SFTP (SSH)-server op nieuwe documenten." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Optioneel. De SHA-256 host key fingerprint van de server (van `ssh-keyscan host | ssh-keygen -lf -`). Indien ingesteld, wordt alleen een server geaccepteerd die deze key presenteert; indien leeg, wordt de key die bij de eerste verbinding wordt gezien gepind en worden latere wijzigingen geweigerd." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key-wachtzin" + +[portal.connections.types.sftp.fields.password] +helperText = "Laat leeg als je in plaats daarvan met een private key authenticeert." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Optioneel. Plak een OpenSSH private key om te authenticeren met een key in plaats van een wachtwoord." +label = "Private key" + +[portal.connections.types.slack] +description = "Plaats een bericht op Slack wanneer een beleidsregel een document verwerkt." +label = "Slack" + +[portal.connections.types.smb] +description = "Bewaak een SMB / CIFS-netwerkshare (Windows of Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Netwerkshare" + +[portal.connections.types.smb.fields.domain] +helperText = "Optioneel. Het Windows-domein of de werkgroep, als je server er een gebruikt." +label = "Domein" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Stuur een auditgebeurtenis naar Splunk telkens wanneer een beleidsregel een document behandelt." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Stuur een auditgebeurtenis naar een Sumo Logic HTTP-collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Plaats een bericht in een Teams-kanaal wanneer een beleidsregel wordt uitgevoerd." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Plaats elk document, of alleen de details, naar een URL die jij kiest." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Trigger een Zap- of Make-scenario vanuit een beleidsregel." +label = "Zapier & Make" + +[portal.docs] +browse = "Documentatie bekijken" +viewSource = "Bron bekijken op GitHub" + [portal.docs.authentication] codeCaption = "elk verzoek" eyebrow = "AAN DE SLAG" @@ -6624,6 +7216,9 @@ eyebrow = "VAARDIGHEDEN" lead = "Gebundelde, benoemde mogelijkheden die je agent als één tool aanroept. Elke vaardigheid is een deterministische op-chain met gekoppelde evals." title = "Agentvaardigheden" +[portal.docs.toc] +title = "Op deze pagina" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API-REFERENTIE" @@ -6697,6 +7292,8 @@ type = "Type" user = "Gebruiker" [portal.documents.queue.empty] +connectSource = "Een bron verbinden" +createPipeline = "Een pipeline maken" description = "Wanneer bronnen documenten naar je pipelines sturen, verschijnen ze hier ter beoordeling." title = "Geen documenten in de wachtrij" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Geen documenten komen overeen met dit filter." rowActions = "Rijacties" sensitiveLabel = "Gevoelig" -sensitiveTitle = "Gevoelig — toegang vereist" [portal.documents.table.columns] action = "Pipeline / Actie" document = "Document" +labels = "Labels" product = "Product" status = "Status" time = "Tijd" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Laatst gezien" region = "Regio" status = "Status" +target = "Target" version = "Versie" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Deze weergave heeft een onverwachte fout aangetroffen. Probeer he retry = "Opnieuw proberen" title = "Er is iets misgegaan op deze pagina" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Terug" +body = "Download de desktop-app of host de server zelf." +done = "Gereed" +guide = "Open de {{name}}-gids" +openInBrowser = "In plaats daarvan openen in browser" +sectionDesktop = "Desktop-app" +sectionSelfHosted = "Zelfgehost" +title = "Installeer de Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Draai de officiële image op je server." +note = "Opent op :8080. Standaardinlog: admin / stirling." +tagline = "Draai de server in een container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standaard" + +[portal.home.download.kubernetes] +detailBody = "Installeer de officiële Helm chart." +note = "Voeg een PVC toe voor /configs om instellingen te behouden." +tagline = "Implementeer naar een cluster met Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native builds voor je distributie." +guideBtn = "Linux-installatieopties bekijken" +note = "AppImage-, .deb- en .rpm-builds zijn beschikbaar voor gangbare distro's." +tagline = "Native desktop-app voor je distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Of via Homebrew" +detailBody = "Voer het installatieprogramma uit of gebruik Homebrew." +downloadBtn = "Downloaden voor macOS" +tagline = "Native app voor Apple silicon en Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Voer de .jar uit op een bare-metalserver." +downloadBtn = "De .jar downloaden" +note = "Vereist Java 21+. Opent op :8080; standaardlogin admin / stirling." +runLabel = "Voer het daarna uit" +tagline = "Voer de .jar uit op een bare-metalserver" +title = "Handmatige serverconfiguratie" + +[portal.home.download.windows] +altLabel = "Of via winget" +detailBody = "Voer het installatieprogramma uit of gebruik een package manager." +downloadBtn = "Installatieprogramma downloaden" +tagline = "Native app voor Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} actief" -invite = "Teamleden uitnodigen" +activeOfDeployed = "{{active}} van {{total}} actief deze maand" name = "Stirling PDF Editor" open = "Openen in browser" updated = "bijgewerkt {{time}}" +[portal.home.editor.deploy] +finish = "Implementatie voltooien" +options = "Implementatieopties bekijken" +start = "Editor implementeren" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Zelfgehost · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Goedemiddag" evening = "Goedenavond" morning = "Goedemorgen" -[portal.home.onboarding] -dismiss = "Installatie sluiten" -notStarted = "Niet gestart" -progress = "{{done}} van {{total}} voltooid" -title = "Installatie voltooien" - -[portal.home.onboarding.enterprise] -body = "Organisatiebrede SSO + SCIM + RBAC, vaste volumeprijzen en air-gapped implementatie." -cta = "Proefperiode starten" -ctaQuote = "Offerte aanvragen" -lead = "Voor 250+ medewerkers." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Implementeer de desktop-app, voor altijd gratis." -title = "Download de PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} actief · {{recommended}} aanbevolen" -chip = "{{active}} actief" -title = "Beleid inschakelen" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} verbonden · elke PDF beheerd waar die terechtkomt" -chip = "{{connected}} verbonden" -title = "Verbind je bronnen" - -[portal.home.quickActions] -subtitle = "Belangrijkste taken voor vandaag" -title = "Snelle acties" - -[portal.home.quickActions.buildPipeline] -blurb = "Composer in 3 stappen bovenop de getypte op-bibliotheek" -title = "Pipeline bouwen" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, bewaakte mappen" -title = "Bron verbinden" - -[portal.home.quickActions.issueApiKey] -blurb = "Scoped sleutel met rate limits en IP allowlist" -title = "API-sleutel uitgeven" - [portal.infrastructure] manageEditorDeployment = "Editor-implementatie beheren" sectionsAriaLabel = "Infrastructuursecties" @@ -6878,13 +7541,9 @@ heading = "API-sleutels" subheading = "Scoped referenties met rate limits per sleutel, machtigingen en IP allowlists." [portal.infrastructure.apiKeys.card] -allowedIps = "Toegestane IP's" -anyIp = "Elk IP (geen allowlist)" created = "Aangemaakt" lastUsed = "Laatst gebruikt" -permissions = "Machtigingen" -rateLimit = "Rate limit" -rateLimitValue = "{{value}} req/min" +revoke = "Sleutel intrekken" usageMonth = "Gebruik deze maand" usageToday = "Gebruik vandaag" @@ -6892,6 +7551,15 @@ usageToday = "Gebruik vandaag" description = "Maak een scoped sleutel om de Stirling API aan te roepen." title = "Nog geen API-sleutels" +[portal.infrastructure.apiKeys.error] +load = "Kon je API-sleutels niet laden. Probeer het opnieuw." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" intrekken? Elke integratie die deze sleutel nog gebruikt, krijgt onmiddellijk 401-fouten. Dit kan niet ongedaan worden gemaakt." +cancel = "Annuleren" +confirm = "Sleutel intrekken" +title = "API-sleutel intrekken" + [portal.infrastructure.audit] filterAriaLabel = "Auditgebeurtenissen filteren op categorie" heading = "Auditlogs" @@ -6935,6 +7603,7 @@ all = "Alle" auth = "Auth" config = "Config" elevation = "Verhoging" +policy = "Beleid" processing = "Verwerking" security = "Beveiliging" @@ -6944,19 +7613,30 @@ title = "Je hebt geen toegang" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Verhoging" +policy = "Beleid" processing = "Verwerking" totalEvents = "Totaal aantal gebeurtenissen · 24u" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Configuratie" +elevation = "Verhoging" +policy = "Beleid" +processing = "Verwerking" +security = "Beveiliging" + +[portal.infrastructure.auditStatusLabel] +danger = "Fout" +info = "Info" +success = "Succes" +warning = "Waarschuwing" + [portal.infrastructure.createKey] cancel = "Annuleren" createKey = "Sleutel maken" done = "Gereed" -ipAllowlistHelper = "Door komma's gescheiden CIDR-bereiken. Laat leeg om elk IP toe te staan." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Sleutelnaam" keyNamePlaceholder = "bijv. Productie · ingest" -permissionsLabel = "Machtigingen" secretKeyCaption = "Geheime sleutel" secretWarning = "Sla dit op in een secrets manager. Stirling slaat alleen ooit een hash op — er is geen manier om deze later te herstellen." subtitle = "Beperk de sleutel tot het minimum dat nodig is. Je kunt deze op elk moment roteren of intrekken." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopieer dit geheim nu — het wordt niet opnieuw getoond." title = "API-sleutel maken" titleCreated = "Sleutel aangemaakt" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Geïmplementeerd door" -environment = "Omgeving" -product = "Product" -status = "Status" -version = "Versie" -when = "Wanneer" - -[portal.infrastructure.deployments.recent] -heading = "Recente implementaties" -subheading = "De nieuwste rollouts over producten en omgevingen." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanties" -latency = "Latentie" -load = "Belasting" -p99 = "P99" -region = "Regio" -status = "Status" -throughput = "Doorvoer" -uptime = "Uptime" -version = "Versie" - -[portal.infrastructure.deployments.regions] -heading = "Regio's" -subheading = "Live gezondheid voor elke geïmplementeerde Stirling-regio — latentie, belasting en rolloutversie." - -[portal.infrastructure.deployments.regions.empty] -description = "Geïmplementeerde regio's verschijnen hier zodra je werkruimte is ingericht." -title = "Geen regio's geïmplementeerd" - -[portal.infrastructure.models] -heading = "Modellen" -msValue = "{{value}} ms" -subheading = "De modelcatalogus en routering die documentverwerking in je werkruimte aandrijven." - -[portal.infrastructure.models.byom] -description = "Registreer een on-prem of zelfgehost model en pin het aan een regio voor verwerking gebonden aan data residency." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Catalogus" -sub = "Beheerde modellen beschikbaar voor je werkruimte, met live latentie en kosten." -subEnterprise = "Beheerde, bring-your-own en on-prem modellen — met pinning per regio beschikbaar." - -[portal.infrastructure.models.catalogue.empty] -description = "Modellen in de catalogus van je werkruimte verschijnen hier." -title = "Geen modellen beschikbaar" - -[portal.infrastructure.models.columns] -cost = "Kosten" -latency = "Latentie" -load = "Belasting" -model = "Model" -status = "Status" -type = "Type" -version = "Versie" - -[portal.infrastructure.models.metrics] -activeModels = "Actieve modellen" -avgLatency = "Gem. latentie" -included = "Inbegrepen" -monthlySpend = "Maandelijkse modeluitgaven" - -[portal.infrastructure.models.routing] -empty = "Geen routeringsregels geconfigureerd." -heading = "Routeringsregels" -sub = "Welk model elke bewerking afhandelt. De standaard wordt toegepast wanneer geen specifiekere regel overeenkomt." -subLocked = "Routeer bewerkingen naar specifieke modellen — beschikbaar met betaalde abonnementen." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Upgrade naar Pro om te bepalen welk model elke bewerking en elk documenttype afhandelt." -title = "Modelroutering is een betaalde functie" - -[portal.infrastructure.models.routingColumns] -default = "Standaard" -docType = "Documenttype" -modelForAria = "Model voor {{operation}}" -operation = "Bewerking" -routedTo = "Gerouteerd naar" - -[portal.infrastructure.security.access.byok] -description = "Lever een sleutel uit je eigen KMS. Stirling versleutelt ermee, maar kan nog steeds lezen." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Sleutels verlaten je KMS nooit. Stirling bewaart alleen ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling beheert encryptiesleutels. Het eenvoudigst — geen key ops aan jouw kant." -label = "Door Stirling beheerde sleutels" - -[portal.infrastructure.security.accessPolicy] -heading = "Documenttoegangsbeleid" -subheading = "Bepaalt wie verwerkte documenten in rust kan ontsleutelen." - -[portal.infrastructure.security.attestations] -heading = "Compliance-attestaties" -noReport = "Geen rapport beschikbaar" -subheading = "Auditpostuur per framework, met rapporten beschikbaar voor geattesteerde controles." -viewReport = "Rapport bekijken →" - -[portal.infrastructure.security.compliance] -heading = "Compliance" -subheading = "Attestaties en certificeringen die het Stirling-platform dekken." - -[portal.infrastructure.security.empty] -description = "De beveiligingsconfiguratie van je werkruimte verschijnt hier." -title = "Beveiligingspostuur niet beschikbaar" - -[portal.infrastructure.security.hyokBanner] -description = "Met HYOK verlaten encryptiesleutels je KMS nooit. Stirling bewaart en verwerkt alleen ciphertext die je op elk moment kunt intrekken." -title = "Stirling kan je documenten niet ontsleutelen" - -[portal.infrastructure.security.ipAllowlist] -empty = "Geen IP-bereiken geconfigureerd — alle IP's toegestaan." -heading = "IP allowlist" -sub = "API-toegang is beperkt tot deze CIDR-bereiken." -subLocked = "Beperk API-toegang tot bekende IP-bereiken — beschikbaar met betaalde abonnementen." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Upgrade naar Pro om API-toegang tot specifieke netwerken te beperken." -title = "IP allowlisting is een betaalde functie" - -[portal.infrastructure.security.ipColumns] -added = "Toegevoegd" -addedBy = "Toegevoegd door" -cidr = "CIDR" -label = "Label" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritme" -heading = "Beheer van encryptiesleutels" -keyId = "Sleutel-ID" -lastRotated = "Laatst geroteerd" -rotateKey = "Sleutel roteren" -rotationPolicy = "Rotatiebeleid" -subheading = "Beheer van de sleutels die documenten in rust versleutelen — wie kan ontsleutelen en hoe sleutels roteren." - -[portal.infrastructure.security.managedBanner] -description = "Bring-your-own-key (BYOK) en hold-your-own-key (HYOK) sleutelbeheer zijn beschikbaar met Enterprise. Upgrade om sleutels uit je eigen KMS te leveren." -title = "Sleutels worden door Stirling beheerd in je abonnement" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Azië-Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "Europese Unie" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Verenigde Staten" - -[portal.infrastructure.security.residencyHeader] -heading = "Data residency" -subheading = "Waar documenten worden opgeslagen en verwerkt." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} gebruikt" - -[portal.infrastructure.storage.empty] -description = "Verbonden opslag en gebruik verschijnen hier." -title = "Geen opslag geconfigureerd" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Actief" -activeRange = "0–{{value}}d" -archived = "Gearchiveerd" -coldStorage = "koude opslag" -deleted = "Verwijderd" -never = "nooit" -purged = "opgeschoond" - -[portal.infrastructure.storage.providers] -connect = "Verbinden" -connected = "Verbonden" -heading = "Verbonden providers" -subheading = "Waar verwerkte artefacten worden weggeschreven." - -[portal.infrastructure.storage.retention] -heading = "Bewaartermijn" -subheading = "Hoe lang artefacten worden bewaard vóór lifecycle-verwijdering." -windowLabel = "Standaard bewaarperiode" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dag" -days_other = "{{count}} dagen" -never = "Nooit verwijderen" - -[portal.infrastructure.storage.totalUsage] -heading = "Totaal gebruik" -progressLabel = "Gebruikte opslag" -subheading = "Opslag gebruikt over alle verbonden providers." +revoked = "Ingetrokken" [portal.infrastructure.tabs] apiKeys = "API-sleutels" @@ -7174,6 +7656,49 @@ models = "Modellen" security = "Beveiliging" storage = "Opslag" +[portal.integrations] +availableHeading = "Beschikbaar" +comingSoonHeading = "Binnenkort beschikbaar" +connect = "Verbinden" +connectionCount_one = "{{count}} verbinding" +connectionCount_other = "{{count}} verbindingen" +customApi = "Aangepaste API" +searchPlaceholder = "Integraties zoeken" +subtitle = "Alles waarmee Stirling kan praten. Inloggegevens worden hier één keer aangemaakt en daarna gebruikt door bronnen, beleid en pijplijnen." +title = "Integraties" +typedName = "{{tool}}-naam" + +[portal.integrations.filters] +all = "Alles" +audit = "Audit" +connected = "Verbonden" +notify = "Melden" +security = "Beveiliging" +signing = "Ondertekening" +storage = "Opslag" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Verbonden" + +[portal.integrations.table] +integration = "Integratie" +worksWith = "Werkt met" + +[portal.integrations.worksWith] +pipelines = "Pijplijnen" +policies = "Beleid" +sources = "Bronnen" + +[portal.legal] +draft = "{{label}} · concept" +loadError = "Kon dit document niet laden. Probeer het opnieuw." +loading = "Laden…" +title = "Juridisch document" + [portal.nav] agent-builder = "Agent Builder" components = "Componenten" @@ -7182,33 +7707,17 @@ documents = "Documenten" editor = "Editor" home = "Home" infrastructure = "Infrastructuur" +integrations = "Integraties" pipelines = "Pipelines" policies = "Beleid" procurement = "Inkoop" -settings = "Instellingen" sources = "Bronnen" usage = "Gebruik en facturering" users = "Gebruikers" -[portal.notifications] -markAllRead = "Alles als gelezen markeren" -title = "Meldingen" -viewAll = "Alles bekijken" - -[portal.notifications.ariaLabel] -none = "Meldingen, geen ongelezen" -unread_one = "Meldingen, {{count}} ongelezen" -unread_other = "Meldingen, {{count}} ongelezen" - -[portal.notifications.count] -allRead = "alles gelezen" -loading = "laden" -new_one = "{{count}} nieuw" -new_other = "{{count}} nieuw" - -[portal.notifications.empty] -description = "Geen nieuwe meldingen." -title = "Je bent helemaal bijgewerkt" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Elke geautomatiseerde documentpipeline op de backend: een geordende keten van bewerkingen over een set bronnen, uitgevoerd door een trigger. Klik op een rij voor de stappen en bronnen." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Nieuwe pipeline" [portal.pipelines.builder] -addStep = "Tool toevoegen" +activate = "Activate" back = "Terug naar pipelines" +cannotFollow = "Kan {{produced}} niet verwerken" +chooseAccount = "Kies een account" +chooseDestination = "Kies een bestemming" +chooseOperation = "Kies wat deze stap doet" +chooseSource = "Kies een bron" discard = "Wijzigingen verwerpen" -enabled = "Ingeschakeld" +inputs = "Invoer" +inputSource = "Invoerbron" +inputTrigger = "Trigger" keepEditing = "Blijven bewerken" -needsUpload = "Vereist een geüpload bestand" +moreActions = "More actions" +needsConfiguring = "Moet worden ingesteld" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Geen tools komen overeen met je zoekopdracht." -pipelineSettings = "Pipeline-instellingen" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Tools zoeken" -selectToolBody = "Voeg een tool toe om je pipeline te bouwen." -selectToolTitle = "Nog geen tools" -toolSettings = "Toolinstellingen" +sendToSystem = "Naar een ander systeem verzenden" +stepsIncompatible = "Deze stappen kunnen niet worden uitgevoerd op wat hun vorige stap oplevert: {{tools}}." +stepsNeedSetup = "Deze stappen moeten nog worden ingesteld voordat je kunt opslaan: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Niet-herkende bewerking, ongewijzigd behouden." unsavedBody = "Je hebt niet-opgeslagen wijzigingen. Sla ze op voordat je weggaat, of verwerp ze?" unsavedTitle = "Niet-opgeslagen wijzigingen" -uploadUnsupported = "Geüploade bestanden worden nog niet ondersteund in pipelines, dus deze stappen kunnen niet worden opgeslagen: {{tools}}." usesDefaults = "Wordt uitgevoerd met standaardinstellingen" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combineert elk bestand uit de vorige stap" +fan-out = "Wordt één keer uitgevoerd per bestand uit de vorige stap" +format-mismatch = "Vereist {{accepts}}, maar de vorige stap levert {{produced}} op" +output-uncertain = "Wordt mogelijk niet uitgevoerd: de uitvoer van de vorige stap hangt af van hoe deze is ingesteld" +source-mismatch = "Vereist {{accepts}}, maar de invoer van deze pijplijn is {{produced}}" +undeclared-operation = "Kan niet controleren wat deze stap accepteert" [portal.pipelines.composer] addTool = "Tool toevoegen" -cancel = "Annuleren" -chainEmpty = "Voeg een tool toe om je pipeline te bouwen." create = "Pipeline maken" -directory = "Uitvoermap" -directoryHelp = "Absoluut pad op de server. Moet binnen de geconfigureerde toegestane mappen liggen." +createPaused = "Create paused" editingUnsupported = "Het weergeven van deze toolparams voor bewerking wordt nog niet ondersteund." -moveDown = "Omlaag verplaatsen" -moveUp = "Omhoog verplaatsen" +editSource = "Edit source" name = "Naam" namePlaceholder = "bijv. Redactiesweep" -noSources = "Nog geen bronnen verbonden. De pipeline kan nog steeds draaien op bestanden die er rechtstreeks aan worden geleverd." noToolSettings = "Deze tool heeft geen configureerbare instellingen." -operations_one = "Bewerking ({{count}})" -operations_other = "Bewerkingen ({{count}})" output = "Uitvoer" -removeStep = "Bewerking verwijderen" save = "Wijzigingen opslaan" scheduleEvery = "Uitvoeren elke" -sources = "Bronnen" -sourcesLoading = "Bronnen laden..." trigger = "Trigger" triggerManual = "Alleen handmatig" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dagen" hours = "uur" minutes = "minuten" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" verwijderen? Dit kan niet ongedaan worden gemaakt." cancel = "Annuleren" @@ -7273,28 +7815,62 @@ confirm = "Verwijderen" title = "Pipeline verwijderen?" [portal.pipelines.detail] +clearHistory = "Geschiedenis wissen" delete = "Pipeline verwijderen" run = "Nu uitvoeren" [portal.pipelines.empty] action = "Pipeline maken" +connectSource = "Een bron verbinden" description = "Maak je eerste pipeline: kies de bronnen waarop deze draait, koppel de bewerkingen en kies waar de uitvoer naartoe gaat." title = "Nog geen pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Actief" paused = "Gepauzeerd" total = "Pipelines" -[portal.pipelines.output] -folder = "Naar map schrijven" -inline = "Bestanden retourneren" - [portal.pipelines.run] +allProcessed_one = "Niets om uit te voeren: het {{count}} document van de bron is al verwerkt." +allProcessed_other = "Niets om uit te voeren: alle {{count}} documenten in de bronnen zijn al verwerkt." completed_one = "Run voltooid." completed_other = "Alle {{count}} runs voltooid." empty = "Niets om uit te voeren: de bronnen hadden geen documenten om te verwerken." failed = "Run mislukt: {{error}}" +inFlight = "Niets nieuws om uit te voeren: documenten worden nog verwerkt vanuit een eerdere uitvoering." +parked_one = "Niets om uit te voeren: {{count}} document is eerder mislukt en is geparkeerd. Los de oorzaak op en wis daarna de geschiedenis om het opnieuw te proberen." +parked_other = "Niets om uit te voeren: {{count}} documenten zijn eerder mislukt en zijn geparkeerd. Los de oorzaak op en wis daarna de geschiedenis om ze opnieuw te proberen." running = "Run gestart; nog bezig." timeout = "Run duurt langer dan verwacht; deze kan nog op de achtergrond worden voltooid." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Bronnen" status = "Status" steps = "Stappen" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Mapbewaking" @@ -7314,12 +7891,18 @@ manual = "Handmatig" schedule = "Gepland" [portal.policies] +defaultName = "{{category}}-beleid" subtitle = "Permanente automatiseringen die een toolpipeline afdwingen op elk document. Elk beleid wordt geactiveerd bij upload of export, voert de toolketen uit en slaat de afgedwongen versie naast het origineel op." title = "Beleid" [portal.policies.card] comingSoon = "Upgrade naar Enterprise" notSetUp = "Niet ingesteld" +requiresAiEngine = "Vereist AI-engine" + +[portal.policies.categories.classification] +desc = "Identificeer het type van elk document bij uploaden en tag de metadata voor archivering en zoeken." +label = "Classificatie" [portal.policies.categories.compliance] desc = "Dwing HIPAA-, GDPR-, SOC 2- of FedRAMP-vereisten af op elk document." @@ -7344,6 +7927,13 @@ label = "Beveiliging" [portal.policies.config] scopeAll = "Alle documenten" +[portal.policies.config.classification] +summary = "Classificeert elk geüpload document aan de hand van de labels van je team en tagt de metadata." + +[portal.policies.config.classification.rules] +0 = "Classificeren" +1 = "Metadata taggen" + [portal.policies.config.compliance] summary = "Valideert documenten tegen regelgevende frameworks voordat ze het systeem verlaten." @@ -7371,6 +7961,29 @@ minConfidence = "Min. betrouwbaarheid" 2 = "Naam geven" 3 = "Normaliseren" +[portal.policies.config.purview] +label = "Een Microsoft Purview-gevoeligheidslabel toepassen" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "De GUID van het label, uit de Purview-portal. Door een label toe te passen wordt de metadata ervan naar de PDF geschreven; het kan niet versleutelen, daarvoor is de Microsoft-client nodig." +labelName = "Labelnaam (optioneel)" +labelNameHelp = "Wordt naast de ID vastgelegd zodat mensen en tools het label kunnen herkennen." + +[portal.policies.config.purview.fields.method] +help = "Purview maakt onderscheid tussen een label dat door beleid is toegepast en een label dat door een persoon is gekozen." +label = "Hoe het label wordt vastgelegd" +privileged = "Zoals gekozen door een persoon" +standard = "Zoals automatisch toegepast" + +[portal.policies.config.purviewRead] +label = "Een Microsoft Purview-label lezen" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Leest het label dat het document al heeft, zodat latere stappen daarop kunnen reageren." + [portal.policies.config.retention] summary = "Dwingt af hoe lang documenten worden bewaard, wanneer ze worden gearchiveerd en wanneer ze worden verwijderd." @@ -7438,6 +8051,7 @@ title = "Nog geen activiteit" [portal.policies.endpoints] addWatermark = "Watermerk" autoRedact = "PII redigeren" +classifyAndLabel = "Classificeren" compressPdf = "Comprimeren" flatten = "Plat maken" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Je beleidsregels zijn opgeslagen en verschijnen zodra de verbindi retry = "Opnieuw proberen" title = "Backend niet beschikbaar" +[portal.policies.operations] +change = "Wijzigen wat deze stap doet" +noResults = "Geen stap komt hiermee overeen. Probeer een productnaam of \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Stappen zoeken - probeer \"scan\", \"attach\" of een productnaam" + +[portal.policies.operations.bodyMode] +binary = "Ruwe bestandsbytes" +json = "Binnen een JSON-payload" +multipart = "Als bestandsupload (multipart)" + +[portal.policies.operations.clamavScan] +description = "Je eigen ClamAV-server controleert het document; niets verlaat je netwerk." +label = "Scannen op virussen (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Weigert documenten met macro's, uitvoerbare bestanden, scripts of ingesloten objecten." +label = "Actieve inhoud blokkeren" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive controleert het document en de uitvoering stopt als het niet schoon is." +label = "Scannen op virussen" + +[portal.policies.operations.confluenceAttach] +description = "Slaat het verwerkte document op een pagina op." +label = "Aan een Confluence-pagina toevoegen" + +[portal.policies.operations.consignoSubmit] +description = "Dient het document in bij ConsignO Cloud voor ondertekening." +label = "Verzenden voor ondertekening (ConsignO)" +note = "Dient het document in. Het ophalen van de ondertekende kopie wordt nog niet ondersteund, omdat een stap geen resultaat kan doorgeven aan een latere stap." + +[portal.policies.operations.customApiCall] +description = "Stel de aanroep zelf samen: pad, methode, body en afhandeling van de reactie." +label = "Een aangepaste API aanroepen" + +[portal.policies.operations.discordNotify] +description = "Meldt aan een kanaal dat het beleid een document heeft afgehandeld." +label = "Een bericht op Discord plaatsen" + +[portal.policies.operations.elasticIndex] +description = "Legt vast wat het beleid deed als een doorzoekbaar document." +label = "Een auditgebeurtenis indexeren in Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart past bij upload-APIs; JSON past bij APIs die het bestand in een payload willen; binary verzendt de ruwe bytes." +label = "Hoe het document wordt verzonden" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Een JSON-body die ongewijzigd wordt verzonden. {{document.base64}} bevat het bestand; {{document.*}} en {{run.*}} worden per document ingevuld." +label = "JSON-body" + +[portal.policies.operations.fields.connection] +label = "Account" + +[portal.policies.operations.fields.domain] +label = "Mailgun-domein" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Het formulierveld waarin de API het document verwacht." +label = "Naam van bestandsveld" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Van" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Een JSON-object met headernaam/waarde-paren. De inloggegevens van de verbinding worden voor je toegevoegd." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Verzonden als de berichttekst. {{document.*}} en {{run.*}} worden per document ingevuld." +label = "Bericht" + +[portal.policies.operations.fields.method] +label = "Methode" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Toegevoegd aan de basis-URL van de verbinding. Het kan nooit een andere host bereiken." +label = "Pad" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Waar in de drive het moet worden geschreven. {{document.filename}} wordt per document ingevuld." +label = "Bestemmingspad" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapporteren laat het document ongemoeid. Vervangen wisselt het om voor wat er terugkomt." +label = "Wat te doen met het antwoord" + +[portal.policies.operations.fields.signerEmail] +label = "E-mailadres van ondertekenaar" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Onderwerp" +placeholder = "Verwerkt: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyseert deze tekst. Gebruik {{document.title}} of plak de tekst die je wilt laten controleren." +label = "Te controleren tekst" + +[portal.policies.operations.fields.to] +label = "Aan" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud-gebruikersnaam" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Meldt aan een space dat het beleid een document heeft afgehandeld." +label = "Een bericht in Google Chat plaatsen" + +[portal.policies.operations.jiraAttach] +description = "Slaat het verwerkte document op bij een issue." +label = "Aan een Jira-issue toevoegen" + +[portal.policies.operations.mailgunEmail] +description = "Verzendt het verwerkte document als bijlage." +label = "Het document e-mailen (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Schrijft het verwerkte document naar een map." +label = "Uploaden naar Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Je eigen Presidio-server rapporteert de persoonsgegevens die worden gevonden." +label = "Persoonsgegevens detecteren" +note = "Presidio leest tekst, geen bestanden. Geef de tekst op die je wilt laten controleren — het document zelf wordt niet geüpload." + +[portal.policies.operations.responseMode] +replace = "Het document vervangen door het antwoord" +report = "Het document behouden, het antwoord registreren" + +[portal.policies.operations.sendgridEmail] +description = "Verzendt het verwerkte document als bijlage." +label = "Het document e-mailen (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Meldt aan een kanaal dat het beleid een document heeft afgehandeld." +label = "Een bericht op Slack plaatsen" + +[portal.policies.operations.splunkEvent] +description = "Legt vast wat het beleid deed, waar je auditors al kijken." +label = "Een auditgebeurtenis naar Splunk verzenden" + +[portal.policies.operations.sumologicEvent] +description = "Legt vast wat het beleid deed naar een HTTP collector." +label = "Een auditgebeurtenis naar Sumo Logic verzenden" + +[portal.policies.operations.teamsNotify] +description = "Meldt aan een kanaal dat het beleid een document heeft afgehandeld." +label = "Een bericht op Teams plaatsen" + +[portal.policies.operations.webhookPost] +description = "Verzendt het document zelf naar een URL die je kiest." +label = "Het document naar een webhook posten" + +[portal.policies.operations.zapierNotify] +description = "Geeft de details van de uitvoering door aan je automatisering." +label = "Een Zap- of Make-scenario activeren" + [portal.policies.stats] activeFor = "Actief" dataProcessed = "Gegevens verwerkt" @@ -7473,6 +8282,13 @@ label = "Docs afgedwongen" description = "Geconfigureerd maar niet actief" label = "Gepauzeerd" +[portal.policies.table] +appliesTo = "Van toepassing op" +docs = "Docs afgedwongen" +enforces = "Dwingt af" +policy = "Beleid" +status = "Status" + [portal.policies.wizard.actions] back = "Terug" cancel = "Annuleren" @@ -7480,6 +8296,10 @@ continue = "Doorgaan" enablePolicy = "Beleidsregel inschakelen" saveChanges = "Wijzigingen opslaan" +[portal.policies.wizard.capability.classify] +desc = "Identificeert het type document aan de hand van de labels van je team en tagt het, zodat het per categorie wordt opgeslagen en doorzocht." +label = "Het document classificeren" + [portal.policies.wizard.capability.compress] desc = "Comprimeert het document naar een kleinere bestandsgrootte." label = "Bestandsgrootte verkleinen" @@ -7500,10 +8320,18 @@ label = "Gevoelige informatie redigeren" desc = "Verwijdert verborgen JavaScript zodat niets automatisch kan worden uitgevoerd wanneer het document wordt geopend." label = "Actieve inhoud verwijderen" +[portal.policies.wizard.capability.timestampPdf] +desc = "Bewijst dat het document op een bepaald moment in exact deze vorm bestond, met behulp van een onafhankelijke timestamp authority. Alleen een hash wordt verzonden - het document verlaat nooit je server." +label = "Een vertrouwd tijdstempel toevoegen" + [portal.policies.wizard.capability.watermark] desc = "Plaatst een zichtbare markering (bijv. “Vertrouwelijk”) op elke pagina." label = "Watermerk toepassen" +[portal.policies.wizard.classification] +description = "Elk geüpload document wordt geclassificeerd aan de hand van de ingebouwde labels en getagd met de types die passen. De labelset wordt gedeeld met je hele team." +labelsHeading = "Classificatielabels" + [portal.policies.wizard.errors] noTools = "Schakel eerst minstens één tool in de workflow in." saveFailed = "Kon de beleidsregel niet opslaan. Probeer het opnieuw." @@ -7548,73 +8376,118 @@ setUp = "Beleidsregel voor {{category}} instellen" [portal.policies.wizard.workflow] description = "Kies wat deze beleidsregel doet met elk document dat wordt verwerkt." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} actief" -noRule = "Nog geen regel afgedwongen" -subtitle = "Doorlopende automatiseringen waar elk document doorheen gaat, ongeacht welke pipeline het verwerkt." -title = "Wat op je PDFs wordt uitgevoerd" - [portal.policySummary.action] -comingSoon = "Upgraden naar Enterprise" -configure = "Configureren" setUp = "Instellen" -[portal.policySummary.column] -activeRule = "Actieve regel" -policy = "Beleidsregel" -status = "Status" +[portal.processorFlow] +footnote = "Aantallen zijn over de laatste 24 uur. De flowsnelheid is illustratief." +liveBadge = "Live" +stats = "{{connected}} verbonden · {{processed}} PDFs verwerkt" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Zodra beleidsregels zijn geconfigureerd, verschijnen de categorieën hier." -title = "Nog geen beleidsregels" +[portal.processorFlow.lens] +ariaLabel = "Weergavemodus" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "Actief" -off = "Uit" +[portal.processorFlow.outcomes] +count = "{{n}} · 24u" +failed = "Mislukt" +heading = "Resultaten" +success = "Afgeleverd" + +[portal.processorFlow.policies] +activeCount = "{{n}} actief" +count = "{{n}} · 24u" +heading = "Beleid" +setUp = "Instellen" soon = "Binnenkort" -[portal.processingStatus] -managePlan = "Abonnement beheren" -volumeSuffix = "PDFs verwerkt · laatste 30 dagen" +[portal.processorFlow.sankey] +waist = "Beleid · {{n}} actief" + +[portal.processorFlow.sankey.empty] +description = "Verbind een bron en schakel beleid in om documenten door de processor te zien stromen." +title = "Nog geen flow" + +[portal.processorFlow.sources] +comingSoonTag = "Verbinden" +editor = "Stirling PDF Editor" +heading = "Bronnen" +perDay = "{{n}} / 24u" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-mailinvoer" [portal.procurement] -reset = "Inkoop resetten (demo)" subtitle = "Laat je team evalueren, contracteren en onboarden. Elk document op één plek." title = "Inkoop" -[portal.procurement.action] -download = "Downloaden" -pay = "Nu betalen" -request = "Aanvragen" -sign = "Controleren & ondertekenen" -upload = "Uploaden" - [portal.procurement.agreement] agreeCta = "Akkoord gaan & abonneren" +confidential = "Vertrouwelijk" confirm = "Ik heb de Stirling Enterprise-overeenkomst gelezen en ga ermee akkoord." -eyebrow = "Overeenkomst" -intro = "Eén gecombineerde overeenkomst dekt je deal: Master Service Agreement, bestelformulier, EULA en Data Processing Agreement. Controleer deze en ga vervolgens akkoord om de offerte te accepteren als een vast abonnement." -title = "Controleer je Enterprise-overeenkomst" +docName = "Stirling Enterprise-overeenkomst" +docSub = "MSA, orderformulier, EULA en DPA, gecombineerd in één handtekening." +download = "Downloaden" +downloadDraftError = "Kon de overeenkomst-PDF niet genereren - de documentrenderer is niet beschikbaar. Probeer het binnenkort opnieuw of neem contact op met support." +downloadError = "Kon de ondertekende overeenkomst niet downloaden. Probeer het opnieuw." +legalName = "Naam juridische entiteit" +legalNamePlaceholder = "De juridische entiteit die zal ondertekenen" +loadError = "Kon de overeenkomst niet laden. Probeer het opnieuw." +loading = "Overeenkomst laden..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Wijzigingen aanvragen" +scrollHint = "Scroll om te controleren" +signatory = "Naam ondertekenaar" +signatoryPlaceholder = "Volledige naam" +signatoryTitle = "Functie van ondertekenaar" +signatoryTitlePlaceholder = "bijv. General Counsel" +signError = "Kon je handtekening niet vastleggen. Probeer het opnieuw." [portal.procurement.builder] addons = "Add-ons" +addressLine1 = "Adresregel 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresregel 2" +addressLine2Placeholder = "Suite, verdieping, gebouw" back = "Terug" businessName = "Bedrijfsnaam" businessNamePlaceholder = "Je bedrijf" +city = "Plaats" +cityPlaceholder = "San Francisco" +completeRequired = "Vul de verplichte velden (gemarkeerd met *) in met een geldig e-mailadres voordat je de offerte genereert." +contactEmail = "E-mailadres contactpersoon" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Naam contactpersoon" +contactNamePlaceholder = "Jane Doe" continue = "Doorgaan" -country = "Land" -countryEuro = "Eurozone (EUR €)" -countryUK = "Verenigd Koninkrijk (GBP £)" -countryUS = "Verenigde Staten (USD $)" +done = "Gereed" eula = "Ik heb de Stirling Enterprise EULA gelezen en ga ermee akkoord. Deze is van toepassing op de overeenkomst die uit deze offerte wordt gegenereerd." generate = "Offerte genereren" -included = "Inbegrepen" indemnification = "IP-vrijwaring" indemnificationSub = "Wij verdedigen kwalificerende IP-claims, volgens de EULA" -offlineLicense = "Offline / air-gapped licentie" -offlineLicenseSub = "Een downloadbaar licentiebestand voor een air-gapped self-hosted instantie" +paperEyebrow = "Enterprise-offerte" +paperFor = "Opgesteld voor" +pdfSize = "PDF-grootte" +poNumber = "PO-nummer" +poNumberPlaceholder = "Optioneel" +postalCode = "Postcode" +postalCodePlaceholder = "94105" +posture = "Governance" +posture_count = "~{{count}} beleid" +posture_essentials = "Essentials" +posture_essentialsSub = "Classificatie + Delen — de standaardinstellingen" +posture_governed = "Governed" +posture_governedSub = "Voegt Beveiliging en Routering toe" +posture_regulated = "Regulated" +posture_regulatedSub = "Elke categorie — Compliance, Bewaring, Ingestie" qbr = "Kwartaalreviews" qbrSub = "Je SE beoordeelt elk kwartaal gebruik en roadmap" +region = "Staat / regio" +regionPlaceholder = "Californië" running = "{{annual}} / jaar · {{years}} jaar {{tcv}}" s1Sub = "Je team en de PDFs die je elk jaar verwacht te verwerken." s1Title = "Hoeveel ga je verwerken?" @@ -7623,6 +8496,12 @@ s2Title = "Commitment en service" s3Sub = "Voor de offerte en de overeenkomst die deze genereert." s3Title = "Je gegevens" serviceLevel = "Serviceniveau" +size_compact = "Compact" +size_compactSub = "Vooral tekst, onder 1 MB" +size_heavy = "Zwaar" +size_heavySub = "Gescand of veel afbeeldingen, 5 MB+" +size_standard = "Standaard" +size_standardSub = "Gemengde tekst en afbeeldingen, 1 tot 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 kantooruren · dedicated accountmanager · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "Zelfde werkdag · toegewezen CSM · +15%" slStandard = "Standard" slStandardSub = "Volgende werkdag · gedeelde CSM · inbegrepen" stepOf = "Stap {{n}} van {{total}}" +taxId = "BTW / belastingnummer" +taxIdPlaceholder = "Optioneel" term = "Looptijd" termDiscount = "{{pct}}% korting voor meerjarig commitment toegepast" title = "Stel je offerte samen" @@ -7637,6 +8518,7 @@ training = "Onboarding & training" trainingSub = "Live sessies om je team op weg te helpen" users = "Totaal aantal gebruikers" usersPlaceholder = "bijv. 250" +viewEula = "Lees het" volEstimated = "Geschat op basis van {{count}} gebruikers (~2.000 PDFs elk, inclusief automatisering). Pas aan als je het beter weet." volManual = "Je eigen cijfer wordt gebruikt. Schat op elk moment opnieuw op basis van je teamgrootte." volNoUsers = "Niet zeker? Voer je teamgrootte in en wij schatten het." @@ -7645,55 +8527,55 @@ volumePlaceholder = "bijv. 1.000.000" years_one = "{{count}} jaar" years_other = "{{count}} jaar" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Klaar" -here = "Je bent hier" -hide = "Verbergen" -optional = "Optioneel" -paidAddon = "Betaalde add-on" -show = "Tonen" -subtitle = "Alles wat je nodig hebt bij elke stap van het traject, zichtbaar zodra de deal die stap bereikt." -supportingSubtitle = "SOC 2, beveiligingsreviews, belastingformulieren en meer, klaar wanneer je beveiligings- of inkoopteam erom vraagt. Voor sommige geldt een eenmalige vergoeding." -supportingTitle = "Ondersteuning van je evaluatie" +[portal.procurement.documents] +agreement = "Enterprise-overeenkomst" +agreementSub = "MSA, orderformulier en DPA in één handtekening." +download = "Downloaden" +eula = "EULA & commerciële voorwaarden" +eulaSub = "De software en selfservice commerciële voorwaarden." +invoice = "Factuur" +invoiceSub = "Je eerste abonnementsfactuur." +laterInvoice = "Beschikbaar nadat je accepteert" +laterQuote = "Beschikbaar zodra je offerte is uitgebracht" +quote = "Offerte" +quoteSub = "Je gespecificeerde enterprise-offerte." +sla = "SLA-bijlage" +slaSub = "Supportniveaus en responstijden." +subprocessors = "Subverwerkers" +subprocessorsSub = "Derden die namens jou gegevens verwerken." +subtitle = "Je dealdocumenten, altijd beschikbaar." title = "Documenten" -upcoming = "Aankomend" +view = "Bekijken" [portal.procurement.error] title = "Er is iets misgegaan" [portal.procurement.hero] -company = "Je Enterprise-deal" +barAria = "Fase {{current}} van {{total}}" ctaAgreement = "Overeenkomst controleren & ondertekenen" -ctaLive = "Je bent live" -ctaPayment = "Betaling toevoegen" +ctaExploring = "Je proefperiode instellen" ctaQuote = "Je offerte controleren" +ctaReviewQuote = "Offerte bekijken" ctaTrial = "Je offerte samenstellen" +documents = "Documenten" eyebrow = "Enterprise-inkoop" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Teamgenoten uitnodigen" -keyDocs = "Belangrijke documenten" -nextStep = "Volgende stap: {{action}}" -notStarted = "Niet gestart" +licenseKey = "Licentiesleutel" +liveSub = "Je organisatie is ingericht en je licentie is actief." +liveTitle = "Je bent live op Stirling Enterprise" +next = "Volgende: {{stage}}" open = "Inkoop openen" scheduleCall = "Gesprek plannen" -setup1Sub = "Nodig je teamgenoten uit" -setup1Title = "Implementeer de PDF Editor" -setup2Sub = "Schakel verwerking in voor editors en andere bronnen" -setup2Title = "Verbind de PDF Processor" -setup3Sub = "Schakel Security, Compliance, Routing of Retention in wanneer je ze nodig hebt" -setup3Title = "Aanbevolen beleidsregels toevoegen" +sentenceAgreement = "de overeenkomst bekijken en ondertekenen" +sentenceLive = "provisioning en uitrol" +sentencePayment = "inkooporder en betaling" +sentenceQuote = "je offerte bekijken en accepteren" +sentenceTrial = "je proefwerkruimte instellen" [portal.procurement.journey] daysLeft_one = "Nog {{count}} dag" daysLeft_other = "Nog {{count}} dagen" -engineerLabel = "Je solutions engineer" -eyebrow = "Je uitrol" -live = "Je bent live op Stirling Enterprise" -nextStep = "Volgende stap: {{action}}" -subtitle = "Je solutions engineer begeleidt elke stap. Eén volgende actie tegelijk; de volledige checklist staat hieronder." -title = "Van trial naar live, één begeleid pad" -trialTitle = "Enterprise-trial" [portal.procurement.journeySteps.agreement] blurb = "Eén handtekening dekt MSA, bestelformulier, EULA en DPA." @@ -7727,6 +8609,9 @@ downloadError = "Kon het offline licentiebestand nog niet genereren — probeer downloadOffline = "Offline licentie downloaden (.lic)" hint = "Plak deze sleutel in een self-hosted instantie om deze te activeren, of bewaar hem voor je administratie. Bewaar hem veilig." label = "Je licentiesleutel" +subtitle = "Activeer een self-hosted instantie met deze sleutel, of bewaar deze voor je administratie." +title = "Je licentiesleutel" +trialFileHint = "Dit is je proeflicentiebestand. Zodra je overeenkomst rond is, kom je terug en download je het opnieuw — anders dan de online licentiesleutel wordt het .lic-bestand niet vanzelf bijgewerkt." [portal.procurement.link] cta = "Account koppelen" @@ -7739,65 +8624,62 @@ description = "Je abonnement is actief en je licentie is uitgegeven. Je team is eyebrow = "Live" title = "Je bent live op Stirling Enterprise" -[portal.procurement.locked] -description = "Trialsleutels, offertes met vast volume, de overeenkomst met één handtekening, betaling en je documentregister staan hier allemaal zodra je een Enterprise-evaluatie start." -eyebrow = "Alleen Enterprise" -talkToSales = "Neem contact op met sales" -title = "Het inkooptraject opent met Enterprise" - [portal.procurement.milestone] -accept = "Accepteren & doorgaan" -description = "Download de PDF om deze met je team te delen, kom terug om te accepteren wanneer je klaar bent, of breng wijzigingen aan." -download = "PDF downloaden" downloadError = "Kon de offerte-PDF nog niet downloaden — probeer het over enkele ogenblikken opnieuw." -edit = "Offerte bewerken" -eyebrow = "Offerte {{number}}" -perYear = " / jaar" -preparedFor = "Opgesteld voor {{company}}" -tcv = "{{value}} totale contractwaarde" -title = "Je offerte is klaar" - -[portal.procurement.modal] -cancel = "Annuleren" -chooseFile = "Bestand kiezen" -close = "Sluiten" -downloadBody = "Je download begint binnenkort." -downloadCta = "Downloaden" -downloadTitle = "Downloaden" -noFile = "Geen bestand geselecteerd" -payBody = "Betaal je vaste contract via kaart of bankoverschrijving via Stripe. Je workspace wordt ingericht zodra de betaling is verwerkt." -payCta = "Doorgaan naar Stripe" -payTitle = "Betaling bevestigen" -requestBodyFree = "We genereren dit op aanvraag. Bevestig en je solutions engineer stuurt het binnenkort toe." -requestBodyPaid = "Dit is een betaalde add-on. Bevestig en je solutions engineer bepaalt de scope en stuurt de documenten." -requestCta = "Aanvragen" -requestTitle = "Dit document aanvragen" -signBody = "Opent de Stirling Enterprise-overeenkomst voor elektronische ondertekening: één handtekening dekt de MSA, het bestelformulier, de EULA en de DPA. Wij tekenen automatisch mede en je gaat door naar betaling." -signCta = "Openen voor ondertekening" -signTitle = "Controleer en onderteken je overeenkomst" -uploadBody = "Stuur ons je inkooporder en wij factureren volgens je voorwaarden. Sleep de PDF hierheen of kies hieronder een bestand." -uploadCta = "Inkooporder uploaden" -uploadTitle = "Upload je inkooporder" [portal.procurement.payment] description = "Je offerte is geaccepteerd en je licentie is al actief — je team kan meteen beginnen. Betaal de eerste factuur wanneer je er klaar voor bent; je kunt deze hier betalen of downloaden, geen e-mail nodig." +downloadAgreement = "Ondertekende overeenkomst downloaden" downloadInvoice = "Factuur downloaden" -simulate = "Ontvangen betaling simuleren (demo)" +eyebrow = "Betaling" title = "Abonnement aangemaakt" viewInvoice = "Factuur bekijken & betalen" +[portal.procurement.review] +acceptCta = "Offerte accepteren" +annual = "Jaarlijkse vergoeding (jaar 1)" +downloadCta = "Offerte downloaden" +poNumber = "Inkooporder: {{po}}" +renewal = "Verlengt tegen {{amount}}/jaar na de looptijd (+{{pct}}% CPI)" +tcv = "Looptijd van {{years}} jaar, volledig vooraf betaald: {{tcv}}" +validUntil = "Geldig tot {{date}}" + [portal.procurement.schedule] fallback = "Kon de planner niet laden." fallbackLink = "Planning openen in een nieuw tabblad" +loading = "Planner laden..." subtitle = "Je solutions engineer begeleidt je team door de uitrol. Kies een tijd die jou uitkomt." title = "Gesprek plannen" -[portal.procurement.status] -action = "Actie nodig" -available = "Beschikbaar" -complete = "Voltooid" -pending = "In behandeling" -request = "Op aanvraag" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Afgesloten, geen uitgaand verkeer" +back = "Terug" +businessName = "Bedrijfsnaam" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Beheerd, snelste start" +continue = "Doorgaan" +deployment = "Waar moet het worden geïmplementeerd?" +eula = "Ik ga akkoord met de Stirling EULA & commerciële voorwaarden." +fullName = "Volledige naam" +fullNamePlaceholder = "Je naam" +invites = "Teamgenoten uitnodigen · optioneel" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Een gesprek plannen" +seats = "Mensen die testen" +seatsPlaceholder = "bijv. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker of Kubernetes" +start = "Proefperiode starten" +stepOf = "Stap {{n}} van {{total}}" +subtitle = "14 dagen gratis, geen creditcard vereist." +subtitleDetails = "Een paar gegevens voor je offerte en overeenkomst." +talkFirst = "Wil je liever eerst praten?" +title = "Je proefperiode instellen" +viewEula = "Lees het" +workEmail = "Werk-e-mailadres" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Verlengen voegt 7 dagen toe en stelt je solutions engineer op de hoogte." @@ -7808,109 +8690,113 @@ maxed = "Maximum bereikt" subtitle = "Je gratis trial loopt tot en met {{date}}. Geen kaart vereist." title = "Enterprise-trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Prijzen op basis van vast volume, organisatiebrede SSO + SCIM + RBAC, 90 dagen onveranderbare audit en een dedicated SE." -homeCta = "Trial starten →" -homeHeadline = "Verwerk miljoenen PDFs." - -[portal.recentActivity] -title = "Recente activiteit" -viewAll = "Alles bekijken" - -[portal.recentActivity.empty] -description = "Pipeline-uitvoeringen, implementaties en agentgebeurtenissen verschijnen hier." -title = "Hier is nog niets" - -[portal.search] -ariaLabel = "Zoeken" -placeholder = "Zoek in Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Snelle acties verschijnen hier zodra ze beschikbaar zijn." -noActionsTitle = "Geen snelle acties" -noMatches = "Geen overeenkomsten voor \"{{query}}\"" -noMatchesDescription = "Probeer een ander trefwoord of blader door de catalogus." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Accountkoppeling" -[portal.shell.header] -accountFallback = "Account" -accountMenu = "Accountmenu" -darkMode = "Donkere modus" -lightMode = "Lichte modus" -search = "Zoeken" -searchPlaceholder = "Zoeken…" -signOut = "Uitloggen" -switchToDark = "Overschakelen naar donker thema" -switchToLight = "Overschakelen naar licht thema" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Stirling-account koppelen" -planEditor = "Editor-abonnement" -planEnterprise = "Enterprise-abonnement" -planProcessor = "Processor-abonnement" primaryNav = "Primaire navigatie" switchApp = "App wisselen" +[portal.shell.topbar] +closeNav = "Navigatie sluiten" +openNav = "Navigatie openen" +search = "Zoeken" + [portal.sources] subtitle = "Herbruikbare invoerverbindingen die documenten aan Stirling leveren. Configureer een verbinding één keer en verwijs er vervolgens vanuit meerdere beleidsregels naar. Klik op een rij voor de configuratie en welke beleidsregels deze gebruiken." title = "Bronnen" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Bron verbinden" +[portal.sources.builder] +back = "Terug naar bronnen" +backToTypes = "Alle brontypen" +cancel = "Annuleren" +chooseHint = "Kies waar documenten vandaan komen. Grijs gemaakte connectors zijn onderweg." +comingSoon = "Binnenkort beschikbaar" +comingSoonHeading = "Binnenkort beschikbaar" +create = "Bron maken" +createTitle = "Een bron verbinden" +delete = "Verwijderen" +editTitle = "Bron bewerken" +enabled = "Ingeschakeld" +save = "Wijzigingen opslaan" + +[portal.sources.builder.folderAccess] +description = "Mapautomatiseringen kunnen alleen mappen gebruiken die een beheerder heeft toegestaan. Voeg deze toe onder instellingen voor Maptoegang en probeer het daarna opnieuw." +openSettings = "Instellingen voor Maptoegang" +title = "Deze map is niet toegestaan" + [portal.sources.delete] body = "\"{{name}}\" verwijderen? Dit kan niet ongedaan worden gemaakt. Beleidsregels die ernaar verwijzen moeten worden bijgewerkt." cancel = "Annuleren" confirm = "Verwijderen" title = "Bron verwijderen?" -[portal.sources.detail] -closeAriaLabel = "Detail sluiten" -delete = "Bron verwijderen" -docs24h = "Laatste 24u" -docs30d = "Laatste 30 dagen" -docsTotal = "Totaal gezien" -docsTrend = "Documenten over de laatste 30 dagen" -documents = "Documenten" -edit = "Bewerken" -notReferenced = "Wordt door geen enkele beleidsregel gebruikt, dus kan veilig worden verwijderd." -pause = "Pauzeren" -resume = "Hervatten" -subtitle = "{{type}} · {{status}}" -usedBy = "Gebruikt door" - -[portal.sources.empty] -description = "Verbind een map (en binnenkort cloudopslag) zodat je beleidsregels ergens documenten vandaan kunnen halen." -title = "Nog geen bronnen verbonden" - [portal.sources.kpi] inUse = "In gebruik" total = "Verbindingen" unused = "Ongebruikt" +[portal.sources.networkFields.connection] +helperText = "De opgeslagen verbinding met het serveradres en de inloggegevens. Hergebruikt door elke bron die ernaar verwijst." +label = "Verbinding" + +[portal.sources.networkFields.directory] +helperText = "Map op de server om te pollen, relatief ten opzichte van de login-home of share-root. Laat leeg voor de root." +label = "Map" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume verwijdert elk bestand van de server zodra elk beleid het heeft verwerkt." +label = "Leesmodus" + +[portal.sources.networkFields.mode.options] +consume = "Consume: elk bestand één keer verwerken" +snapshot = "Snapshot: de map bij elke uitvoering opnieuw lezen" + +[portal.sources.networkFields.recursive] +label = "Mapdiepte" + +[portal.sources.networkFields.recursive.options] +all = "Submappen opnemen" +top = "Alleen hoogste niveau" + [portal.sources.status] active = "Actief" disabled = "Uitgeschakeld" unused = "Ongebruikt" [portal.sources.table] +documents = "Documenten" source = "Bron" status = "Status" +type = "Type" usedBy = "Beleidsregels" +[portal.sources.types.box] +description = "Haal documenten op uit Box-mappen die je team al deelt." +label = "Box" + +[portal.sources.types.dropbox] +description = "Haal documenten op uit een gedeelde Dropbox-map." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documenten die je team in de editor heeft verwerkt, over beleid en AI-uitvoeringen heen." label = "Editor" +noPolicies = "Nog geen beleid uitgevoerd vanuit de editor." + +[portal.sources.types.email] +description = "Zet bijlagen die naar een speciale mailbox worden gestuurd om in documenten." +label = "E-mailinbox" [portal.sources.types.folder] description = "Bewaak een directory op de server voor nieuwe documenten." @@ -7943,25 +8829,80 @@ label = "Mapdiepte" all = "Submappen opnemen" top = "Alleen bovenste niveau" +[portal.sources.types.ftp] +description = "Poll een FTP- of FTPS-servermap op nieuwe documenten." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Haal documenten op uit gedeelde Google Drive-mappen." +label = "Google Drive" + +[portal.sources.types.network] +description = "Bewaak een SMB-netwerkshare op nieuwe documenten." +label = "Netwerkdrive" + +[portal.sources.types.onedrive] +description = "Bewaak OneDrive-mappen op nieuwe documenten." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Haal documenten op uit een Amazon S3- of S3-compatibele bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "De opgeslagen verbinding met de bucket en inloggegevens. Hergebruikt door elke bron en pijplijnuitvoer die ernaar verwijst." +label = "Verbinding" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume verwijdert elk object uit de bucket zodra elk beleid het heeft verwerkt." +label = "Leesmodus" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: elk object één keer verwerken" +snapshot = "Snapshot: de bucket bij elke uitvoering opnieuw lezen" + +[portal.sources.types.s3.fields.prefix] +helperText = "Alleen objecten waarvan de keys met dit prefix beginnen, worden verwerkt." +label = "Key-prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Poll een SFTP-dropmap op nieuwe documenten." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Haal documenten op uit SharePoint-documentbibliotheken." +label = "SharePoint" + [portal.sources.types.unknown] label = "Bron" +[portal.sources.types.webhook] +createNote = "Een delivery URL en signing secret worden gegenereerd wanneer je deze webhook maakt. Afzenders POSTen documenten naar die URL en het verwijzende beleid wordt uitgevoerd bij aankomst." +description = "Ontvang documenten via ondertekende HTTP POST vanuit elk extern systeem." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Het signing secret wordt één keer getoond, wanneer de webhook wordt gemaakt. Maak de bron opnieuw aan om het te roteren." + +[portal.sources.types.webhook.reveal] +copy = "Kopiëren" +done = "Gereed" +secret = "Signing secret" +secretHelp = "Onderteken de ruwe body van elke levering met deze sleutel (HMAC-SHA256) en verzend deze als de X-Stirling-Signature-header." +secretWarning = "Kopieer het signing secret nu. Voor je veiligheid wordt het slechts één keer getoond en kan het later niet worden opgehaald." +title = "Webhook aangemaakt" +url = "Delivery URL" +usage = "POST elk document als de ruwe request body naar de delivery URL met een binair content type (application/pdf of application/octet-stream). Verwijzend beleid wordt automatisch uitgevoerd bij aankomst." + [portal.sources.wizard] -back = "Terug" -cancel = "Annuleren" -continue = "Doorgaan" -editTitle = "Bron bewerken" name = "Naam" namePlaceholder = "bijv. Claims intake" -save = "Wijzigingen opslaan" -subtitle = "Stap {{current}} van {{total}} · {{label}}" -title = "Een bron verbinden" type = "Type" -[portal.sources.wizard.steps] -chooseType = "Type kiezen" -configure = "Configureren" -review = "Controleren & verbinden" +[portal.stepModal] +close = "Sluiten" [portal.usage] managePayment = "Betaling beheren" @@ -8021,16 +8962,9 @@ summary = "Bezit een team — beheert de resources en gedeelde configuraties van 2 = "Portal-toegang via de standaardbeleidsregel" 3 = "Alles wat Member kan doen" -[portal.welcome] -ariaLabel = "Welkom bij Stirling PDF" -badge = "Open-source" -installEditor = "De Editor installeren" -inviteTeammates = "Teamgenoten uitnodigen" -openInBrowser = "Openen in browser" -perks = "Voor altijd gratis · Self-hostable" -subtitle = "De veiligste PDF Editor ter wereld is gratis voor teams van elke omvang. Inclusief 60+ PDF-bewerkingen en SSO." -title = "Welkom bij" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Onbeperkt" [printFile] title = "Print bestand" @@ -8613,6 +9547,14 @@ text = "Draai uw PDF-pagina's met de klok mee of tegen de klok in in stappen van [rotate.tooltip.header] title = "Overzicht rotatie-instellingen" +[ruler] +clearAll = "Alles wissen" +hideAllLabels = "Alle labels verbergen" +hideSmallLabels = "Kleine labels verbergen" +physicalValues = "Fysiek" +scaled = "Geschaald" +showAllLabels = "Alle labels tonen" + [sanitize] desc = "Verwijder mogelijk schadelijke elementen uit PDF-bestanden." sanitizationResults = "Opschoonresultaten" @@ -8654,31 +9596,45 @@ label = "XMP-metagegevens verwijderen" [sanitize.steps] settings = "Instellingen" -[sanitizePdf] -tags = "schoonmaken,veilig,veilig,bedreigingen verwijderen" - [sanitizePDF] title = "PDF opschonen" +[sanitizePdf] +tags = "schoonmaken,veilig,veilig,bedreigingen verwijderen" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Pagina-schaal aanpassen" +[scaleSettings] +activeScale = "Actieve schaal" +apply = "Schaal toepassen" +applyCalibration = "Kalibratie toepassen" +calculatedScale = "Berekende schaal" +calibrate = "Kalibreren" +calibrating = "Kalibreren" +calibrationDistanceRequired = "Voer een werkelijke afstand groter dan nul in" +calibrationInvalid = "Kan de schaal niet berekenen op basis van deze meting" +calibrationPaperDistance = "Gemeten pagina-afstand: {{distance}}" +calibrationTitle = "Schaal kalibreren" +calibrationTooltip = "Meet een bekende afstand om de schaal automatisch te berekenen" +customScale = "Aangepaste schaal" +noneSet = "Geen aangepaste schaal ingesteld" +presets = "Vooraf ingestelde schalen" +ratio = "Schaalverhouding" +ratioHelp = "Verhouding: 1 pagina-eenheid = X werkelijke eenheden" +ratioPlaceholder = "bijv. 100" +ratioPositive = "Verhouding moet groter zijn dan nul" +ratioRequired = "Verhouding is vereist" +realDistance = "Werkelijke afstand" +realDistancePlaceholder = "bijv. 5" +reset = "Terugzetten naar standaardwaarden" +unit = "Eenheid" +unitInvalid = "Ongeldige eenheid: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Hoek drempel:" -10 = "Stelt de grootte van de toegevoegde en verwijderde rand in om witte randen in de uitvoer te voorkomen (standaard: 1)." -2 = "Stelt de minimale absolute hoek in die nodig is om de afbeelding te roteren (standaard: 10)." -3 = "Tolerantie:" -4 = "Bepaalt het bereik van kleurvariatie rond de geschatte achtergrondkleur (standaard: 30)." -5 = "Minimum oppervlakte:" -6 = "Stelt de minimale oppervlakte drempel in voor een foto (standaard: 10000)." -7 = "Minimum contour oppervlakte:" -8 = "Stelt de minimale contour oppervlakte drempel in voor een foto" -9 = "Randgrootte:" - [scannerImageSplit] submit = "Afbeeldingsscans extraheren" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Wat dit doet" whatThisDoesDesc = "Detecteert en extraheert automatisch elke foto uit een gescande pagina of samengestelde afbeelding—geen handmatig bijsnijden." whenToUse = "Wanneer te gebruiken" +[ScannerImageSplit.selectText] +1 = "Hoek drempel:" +10 = "Stelt de grootte van de toegevoegde en verwijderde rand in om witte randen in de uitvoer te voorkomen (standaard: 1)." +2 = "Stelt de minimale absolute hoek in die nodig is om de afbeelding te roteren (standaard: 10)." +3 = "Tolerantie:" +4 = "Bepaalt het bereik van kleurvariatie rond de geschatte achtergrondkleur (standaard: 30)." +5 = "Minimum oppervlakte:" +6 = "Stelt de minimale oppervlakte drempel in voor een foto (standaard: 10000)." +7 = "Minimum contour oppervlakte:" +8 = "Stelt de minimale contour oppervlakte drempel in voor een foto" +9 = "Randgrootte:" + [search] noResults = "Geen resultaten gevonden" placeholder = "Voer zoekterm in..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Je Stirling-PDF-server is offline en \"{{endpoint}}\" [session] expired = "Uw sessie is verlopen. Voer de pagina opnieuw in en probeer het opnieuw." -[sessionManagement.tooltip] -header = "Ondertekeningssessies beheren" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nieuwe deelnemers worden aan het einde van de ondertekeningsvolgorde toegevoegd" -bullet2 = "Kan geen deelnemers toevoegen nadat sessie is gefinaliseerd" -bullet3 = "Elke deelnemer ontvangt een melding wanneer hij/zij aan de beurt is" -description = "U kunt op elk moment vóór de finalisatie meer deelnemers aan een actieve sessie toevoegen." -title = "Deelnemers toevoegen" - -[sessionManagement.tooltip.finalization] -bullet1 = "Volledige finalisatie: Alle deelnemers hebben getekend" -bullet2 = "Gedeeltelijke finalisatie: Sommige deelnemers hebben nog niet getekend" -bullet3 = "Niet-ondertekende deelnemers worden uitgesloten van het definitieve document" -bullet4 = "Na finalisatie kunt u de ondertekende PDF laden in actieve bestanden" -description = "Finalisatie combineert alle handtekeningen in één ondertekende PDF. Deze actie kan niet ongedaan worden gemaakt." -title = "Sessie-finalisatie" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Kan geen deelnemers verwijderen die al hebben getekend" -bullet2 = "Verwijderde deelnemers ontvangen geen meldingen meer" -bullet3 = "Ondertekeningsvolgorde wordt automatisch aangepast" -description = "Deelnemers kunnen vóór het tekenen uit sessies worden verwijderd." -title = "Deelnemers verwijderen" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Elke handtekening wordt sequentieel op de PDF toegepast" -bullet2 = "Latere ondertekenaars kunnen eerdere handtekeningen zien" -bullet3 = "Cruciaal voor goedkeuringsworkflows en juridische ketens van bewaring" -description = "De volgorde die u bij het maken van de sessie opgeeft, bepaalt wie als eerste tekent." -title = "Ondertekeningsvolgorde" - [settings] close = "Sluiten" +[settings.ai] +documents = "Documenten & RAG" +general = "Algemeen" +limits = "Limieten & prestaties" +models = "Modellen & providers" +title = "AI" + [settings.configuration] advanced = "Geavanceerd" database = "Database" endpoints = "Eindpunten" features = "Functies" +folderAccess = "Maptoegang" mcp = "MCP Server" storageSharing = "Bestandsopslag en delen" systemSettings = "Systeeminstellingen" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Standaard PDF-editor" defaultPdfEditorActive = "Stirling PDF is de standaard PDF-editor" defaultPdfEditorChecking = "Controleren..." defaultPdfEditorInactive = "Een andere app is als standaard ingesteld" +defaultPdfEditorRemind = "Herinner me eraan om als standaard in te stellen" +defaultPdfEditorRemindDescription = "Toon een banner wanneer Stirling PDF niet je standaard-PDF-toepassing is." defaultPdfEditorSet = "Al standaard" defaultStartupView = "Standaardweergave bij opstarten" defaultStartupViewDescription = "Kies welk tabblad in de linkerkolom actief is wanneer de app start" @@ -8817,9 +9763,18 @@ languageDescription = "Kies de weergavetaal" logout = "Afmelden" setAsDefault = "Als standaard instellen" theme = "Thema" +themeAccent = "Accentkleur" +themeAccentCustom = "Aangepaste kleur" +themeAccentCustomHint = "Aangepast (standaardwaarden aanbevolen)" +themeAccentDark = "Accentkleur donkere modus" +themeAccentDefault = "Standaard" +themeAccentDefaultHint = "Standaardthema (aanbevolen)" +themeAccentDescription = "Knoppen, links en markeringen volgen deze kleur, en de app krijgt subtiel die tint. Licht en donker hebben elk hun eigen kleur. Systeem gebruikt de actieve kleur." +themeAccentLight = "Accentkleur lichte modus" themeDark = "Donker" themeDescription = "Schakel tussen lichte en donkere modus" themeLight = "Licht" +themeReset = "Thema terugzetten naar standaard" themeSystem = "System" title = "Algemeen" user = "Gebruiker" @@ -8833,6 +9788,12 @@ intro = "Schakel gebruikersauthenticatie, teambeheer en werkruimtefuncties in vo learnMore = "Meer info in de documentatie" title = "Voor systeembeheerders" +[settings.general.loginLanding] +description = "Kies welke app wordt geopend wanneer je inlogt bij Stirling Cloud." +editor = "Editor" +processor = "Verwerker" +title = "Na het inloggen" + [settings.general.mode] fullscreen = "Volledig scherm" sidebar = "Zijbalk" @@ -9013,9 +9974,6 @@ title = "Beleid & Privacy" [settings.preferences] title = "Voorkeuren" -[settings.search] -placeholder = "Instellingen doorzoeken..." - [settings.security] description = "Werk uw wachtwoord bij om uw account veilig te houden." title = "Beveiliging" @@ -9041,6 +9999,11 @@ security = "Beveiliging" telegram = "Telegram" title = "Beveiliging & Authenticatie" +[settings.signOut] +confirm = "Weet je zeker dat je wilt uitloggen?" +submit = "Uitloggen" +title = "Uitloggen" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Laatste pagina" maintainRatio = "Aspectverhouding behouden" next = "Volgende pagina" noSavedSigs = "Geen opgeslagen signatuuren gevonden" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Persoonlijke Signatuuren" previous = "Vorige pagina" redo = "Opnieuw" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Verwijdering van achtergrond is mislukt" hint = "Upload een PNG- of JPG-afbeelding van uw handtekening" label = "Handtekeningafbeelding uploaden" placeholder = "Afbeeldingsbestand selecteren" +previewAlt = "Current image signature" processing = "Afbeelding verwerken..." removeBackground = "Witte achtergrond verwijderen (transparant maken)" @@ -9207,6 +10172,17 @@ saved = "Selecteer hierboven een opgeslagen handtekening en klik vervolgens erge text = "Na het invoeren van uw naam hierboven, klik ergens op de PDF om uw handtekening te plaatsen." title = "Hoe een handtekening toevoegen" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Handtekening verplaatsen" pause = "Plaatsing pauzeren" @@ -9653,10 +10629,30 @@ title = "Uploaden naar server" updateButton = "Bijwerken op server" uploadButton = "Uploaden naar server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klik hier" +fallback = "Als het niet automatisch is geopend," +opening = "Swagger UI wordt geopend in een nieuw tabblad..." tags = "api,documentatie,swagger,endpoints,ontwikkeling" title = "API-documentatie" @@ -9764,12 +10760,39 @@ selectFilesHint = "Selecteer bestanden in Actieve bestanden om deze tool uit te singleFileScope = "Alleen van toepassing op: {{fileName}}" viewerMode = "Schakel over naar de bestandseditor om meerdere bestanden te selecteren." +[toolFormat] +ANY = "elk bestand" +CBR = "een striparchief (CBR)" +CBZ = "een striparchief (CBZ)" +CSV = "een CSV-bestand" +EBOOK = "een e-book" +EMAIL = "een e-mail" +EXCEL = "een spreadsheet" +HTML = "een webpagina" +IMAGE = "afbeeldingen" +JAVASCRIPT = "een JavaScript-bestand" +JSON = "een JSON-bestand" +MARKDOWN = "een Markdown-bestand" +NONE = "geen bestand" +PCL = "een PCL-afdrukbestand" +PDF = "een PDF" +PDF_ENCRYPTED = "een met wachtwoord beveiligde PDF" +POSTSCRIPT = "een PostScript-bestand" +PPT = "een presentatie" +TEXT = "een tekstbestand" +VIDEO = "een video" +WORD = "een Word-document" +XML = "een XML-bestand" +XPS = "een XPS-document" +ZIP = "een ZIP-archief" + [toolPanel] alpha = "Alfa" backToAllTools = "Terug naar alle tools" collapse = "Paneel samenvouwen" expand = "Paneel uitvouwen" goBack = "Ga terug" +pdfTools = "PDF-tools" placeholder = "Kies een tool om te beginnen" premiumFeature = "Premiumfunctie:" toolsHeader = "Tools" @@ -9806,11 +10829,6 @@ noToolsFound = "Geen tools gevonden" quickAccess = "SNELLE TOEGANG" searchPlaceholder = "Tools zoeken..." -[toolPicker.categories] -advancedTools = "Geavanceerde tools" -recommendedTools = "Aanbevolen tools" -standardTools = "Standaardtools" - [toolPicker.subcategories] advancedFormatting = "Geavanceerde opmaak" automation = "Automatisering" @@ -9824,6 +10842,9 @@ removal = "Verwijdering" signing = "Ondertekenen" verification = "Verificatie" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Geen tools gevonden" noTools = "Geen tools beschikbaar" @@ -9950,28 +10971,53 @@ title = "Gebruikers" you = "(jij)" [users.action] +cancelInvite = "Uitnodiging annuleren" deleteTeam = "Team verwijderen" disableMfa = "MFA resetten" move = "Naar team verplaatsen" reinstate = "Herstellen" remove = "Uit org verwijderen" +removeTeam = "Uit team verwijderen" rename = "Team hernoemen" resetPw = "Wachtwoord resetten" suspend = "Schorsen" unlock = "Account ontgrendelen" +[users.activity] +daysAgo = "{{count}} d geleden" +hoursAgo = "{{count}} u geleden" +justNow = "Zojuist" +minutesAgo = "{{count}} min geleden" +monthsAgo_one = "{{count}} maand geleden" +monthsAgo_other = "{{count}} maanden geleden" +never = "Nooit" +weeksAgo_one = "{{count}} week geleden" +weeksAgo_other = "{{count}} weken geleden" +yearsAgo_one = "{{count}} jaar geleden" +yearsAgo_other = "{{count}} jaar geleden" + [users.cap] addProcessor = "+ Processor" approver = "Keurt beleidsregel goed" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Uitnodiging voor {{email}} annuleren? Deze persoon kan niet deelnemen met de huidige link." +cancelInviteTitle = "Uitnodiging annuleren" deleteTeamBody = "Het team {{name}} verwijderen? Het team moet eerst leeg zijn - verplaats de leden naar een ander team, en het mag geen integratieconfiguraties meer bezitten." deleteTeamTitle = "Team verwijderen" disableMfaBody = "MFA-registratie van {{name}} verwijderen? Diegene stelt dit bij de volgende login opnieuw in als het vereist is." disableMfaTitle = "MFA resetten" removeBody = "{{name}} permanent uit de organisatie verwijderen? Dit kan niet ongedaan worden gemaakt." +removeTeamBody = "{{name}} uit het team verwijderen? Deze persoon behoudt het account, maar verliest toegang tot de resources van dit team." removeTitle = "Lid verwijderen" [users.empty] @@ -9982,10 +11028,8 @@ title = "Nog geen leden" addToTeam = "Toevoegen aan team" guestCount = "{{count}} gast" guests = "Gasten" -guestsDesc = "Externe samenwerkers, beperkt tot wat je hebt gedeeld. Alleen Editor." ledBy = "geleid door {{owner}}" org = "Organisatie" -orgDesc = "Eigenaren met organisatiebrede bevoegdheid en beleidsgoedkeuring" owners = "{{count}} eigenaar" team = "Team {{name}}" teamMeta = "{{count}} personen" @@ -10022,6 +11066,18 @@ username = "Gebruikersnaam" usernameError = "Gebruikersnaam moet minimaal 3 tekens bevatten" usernamePlaceholder = "jsmith" +[users.invites] +by = "Uitgenodigd door {{who}}" +cancel = "Annuleren" +expiresInDays_one = "Verloopt over {{count}} dag" +expiresInDays_other = "Verloopt over {{count}} dagen" +expiresToday = "Verloopt vandaag" +title = "Uitnodigingen in behandeling" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Er is iets misgegaan bij het bereiken van de backend, of je hebt geen toegang. Probeer het opnieuw." title = "Kon leden niet laden" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Bijlagen zoeken" title = "Bijlagen" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bladwijzertitel" +bookmarkTitleRequired = "Bladwijzertitel is vereist" closeSidebar = "Zijbalk met bladwijzers sluiten" collapseAll = "Alle bladwijzers samenvouwen" +empty = "No bookmarks in this document" expandAll = "Alle bladwijzers uitvouwen" +searchPlaceholder = "Bladwijzers zoeken" +title = "Bookmarks" [viewer.comments] addComment = "Opmerking toevoegen" @@ -10236,10 +11297,12 @@ locateAnnotation = "Zoeken in document" moreActions = "Meer acties" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Pagina {{page}}" placingHint = "Klik op een pagina om te plaatsen… (annuleren)" removeCommentOnly = "Alleen opmerking verwijderen" saveReply = "Antwoord opslaan" +searchPlaceholder = "Search comments" title = "Opmerkingen" typeComment = "Opmerking" typeInsertText = "Tekst invoegen" @@ -10262,6 +11325,7 @@ unsavedDesc = "U heeft niet-opgeslagen wijzigingen" closeSidebar = "Zijbalk met lagen sluiten" hideAll = "Alle lagen verbergen" showAll = "Alle lagen tonen" +title = "Layers" [viewer.link] delete = "Link verwijderen" @@ -10605,30 +11669,6 @@ title = "Watermerktekst" image = "Afbeelding" text = "Tekst" -[wetSignature.tooltip] -header = "Methoden voor het maken van een handtekening" - -[wetSignature.tooltip.draw] -bullet1 = "Pas penkleur en -dikte aan" -bullet2 = "Wissen en opnieuw tekenen tot u tevreden bent" -bullet3 = "Werkt op aanraakapparaten (tablets, telefoons)" -description = "Maak een handgeschreven handtekening met uw muis of touchscreen. Het best voor persoonlijke, authentieke handtekeningen." -title = "Handtekening tekenen" - -[wetSignature.tooltip.type] -bullet1 = "Kies uit meerdere lettertypen" -bullet2 = "Pas tekstgrootte en -kleur aan" -bullet3 = "Perfect voor gestandaardiseerde handtekeningen" -description = "Genereer een handtekening vanuit getypte tekst. Snel en consistent, geschikt voor zakelijke documenten." -title = "Handtekening typen" - -[wetSignature.tooltip.upload] -bullet1 = "Ondersteunt PNG, JPG en andere afbeeldingsformaten" -bullet2 = "Transparante achtergronden aanbevolen voor de beste resultaten" -bullet3 = "Afbeelding wordt geschaald naar het handtekeninggebied" -description = "Upload een vooraf gemaakte handtekeningafbeelding. Ideaal als u een gescande handtekening of bedrijfslogo heeft." -title = "Handtekeningafbeelding uploaden" - [workbenchBar] activeFiles = "Actieve bestanden" annotations = "Annotaties" @@ -10642,6 +11682,7 @@ exitRedaction = "Zwartlakmodus afsluiten" exportAll = "PDF exporteren" exportSelected = "Geselecteerde pagina's exporteren" formFill = "Formulier invullen" +hideToolbar = "Hide toolbar" multiTool = "Multi-tool" panMode = "Pan-modus" print = "PDF afdrukken" @@ -10652,6 +11693,7 @@ redact = "Zwartlakken" rotateLeft = "Linksom roteren" rotateRight = "Rechtsom roteren" ruler = "Liniaal / Meten" +rulerSettings = "Schaalinstellingen" save = "Opslaan" saveAll = "Alles opslaan" saveAs = "Opslaan als" @@ -10661,6 +11703,7 @@ selectAll = "Alles selecteren" selectByNumber = "Selecteren op paginanummers" selectLanguage = "Taal selecteren" share = "Delen" +showToolbar = "Show toolbar" toggleAnnotations = "Annotaties tonen/verbergen" toggleAttachments = "Bijlagen tonen/verbergen" toggleBookmarks = "Bladwijzers tonen/verbergen" diff --git a/frontend/editor/public/locales/no-NB/translation.toml b/frontend/editor/public/locales/no-NB/translation.toml index ecd7abf2b2..307e603d81 100644 --- a/frontend/editor/public/locales/no-NB/translation.toml +++ b/frontend/editor/public/locales/no-NB/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Bruk" applyAndContinue = "Lagre og forlat" -areYouSure = "Er du sikker på at du vil forlate?" back = "Tilbake" black = "Svart" blue = "Blå" @@ -46,10 +45,8 @@ filesSelected = "filer valgt" font = "Skrifttype" fontSizeTooltip = "Størrelse på sidetalltekst i punkter. Større tall gir større tekst." fontTypeTooltip = "Skrifttype for sidetall. Velg basert på dokumentstilen din." -genericSubmit = "Send inn" green = "Grønn" help = "Hjelp" -imgPrompt = "Velg Bilde(r)" incorrectPasswordMessage = "Nåværende passord er feil." info = "Info" invalidUndoData = "Kan ikke angre: ugyldige operasjonsdata" @@ -62,7 +59,6 @@ loginAgreementTitle = "Innloggingsavtale" logOut = "Logg ut" marginTooltip = "Avstand mellom sidetallet og kanten av siden." moreOptions = "Flere alternativer" -multiPdfPrompt = "Velg PDF-filer (2+)" never = "Aldri" no = "Nei" noFileSelected = "Ingen fil valgt. Last opp en." @@ -80,7 +76,6 @@ pageOrderPrompt = "Tilpasset side rekkefølge (Skriv inn en kommaseparert liste pages = "Sider" pageSelectionPrompt = "Tilpasset Sidevalg (Skriv inn en kommaseparert liste over sidetall 1,5,6 eller Funksjoner som 2n+1):" password = "Passord" -pdfPrompt = "Velg PDF(er)" pin = "Fest fil (hold aktiv etter verktøykjøring)" pro = "Pro" processingComplete = "Filen din er klar." @@ -110,6 +105,7 @@ undoSuccess = "Operasjon angret" unknown = "Ukjent" unpin = "Løsne fil (erstatt etter verktøykjøring)" unsavedChanges = "Du har ulagrede endringer i PDF-en." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Ulagrede endringer" unsupported = "Ikke støttet" username = "Brukernavn" @@ -356,6 +352,7 @@ text = "Disse tillatelsene kontrollerer hva brukere kan gjøre med PDF-en. Mest title = "Endre tillatelser" [addStamp] +preview = "Forhåndsvis stempel" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manuelle nedlastinger: klikk på lenkene og legg filene i tessdat noLanguages = "Ingen tessdata-språk funnet i den konfigurerte katalogen." permissionNotice = "tessdata-stien er ikke skrivbar. Nedlastinger åpnes i nettleseren; lagre .traineddata-filene manuelt i tessdata-mappen." +[admin.settings.ai.documents] +description = "Konfigurer embedding-modellen og innstillingene for henting som brukes til å svare på spørsmål om dokumenter. Brukes på AI-motoren når det lagres." +title = "Dokumenter og RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "La stå tomt for å bruke motorens egen miljølegitimasjon. Gjelder selvhostede enkelmotordistribusjoner." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Lagret – la stå tomt for å beholde gjeldende nøkkel" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base-URL for OpenAI-kompatibelt / Ollama embeddings-endepunkt, f.eks. http://ollama:11434/v1. Må peke til et pålitelig internt endepunkt (SSRF-sensitivt)." +label = "Embedding base-URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Navn på embedding-modell. Fritekst; forslag er kun hint." +label = "Embedding-modell" + +[admin.settings.ai.documents.embeddingProvider] +description = "Leverandør som brukes til å gjøre dokumenttekst om til vektor-embeddings." +label = "Embedding-leverandør" + +[admin.settings.ai.documents.maxSearches] +description = "Maksimalt antall hentingssøk agenten kan kjøre per forespørsel." +label = "Maks søk" + +[admin.settings.ai.documents.reindexNote] +body = "Endring av embedding-modellen trer i kraft umiddelbart, men dokumenter som er indeksert med den forrige modellen, må reindekseres for at søk skal gi riktige resultater." +title = "Reindeksering kreves" + +[admin.settings.ai.documents.saved] +reindexNote = "Hvis du endret embedding-modellen, reindekser eksisterende dokumenter slik at søk bruker den nye modellen." + +[admin.settings.ai.documents.topK] +description = "Antall mest relevante deler som hentes per søk." +label = "Topp K" + +[admin.settings.ai.general] +description = "Koble Stirling til Python AI-motoren og velg hvilke AI-funksjoner som eksponeres. Endringer gjelder ved omstart." +title = "AI-motor" + +[admin.settings.ai.general.capabilities] +description = "Slå individuelle AI-funksjoner av eller på. Deaktiverte funksjoner skjules i appen." +title = "Funksjoner" + +[admin.settings.ai.general.enabled] +description = "Hovedbryter. Når den er av, er ingen AI-verktøy, agenter eller motorkall tilgjengelige." +label = "Aktiver AI" + +[admin.settings.ai.general.features.chat] +description = "Samtaleassistent for arbeid med PDF-er." +label = "Chatassistent" + +[admin.settings.ai.general.features.classify] +description = "Kategoriser dokumenter automatisk etter type eller innhold." +label = "Dokumentklassifisering" + +[admin.settings.ai.general.features.createPdf] +description = "Generer en ny PDF (f.eks. fra HTML) via en AI-agent." +label = "Opprett PDF fra ledetekst" + +[admin.settings.ai.general.features.documentQuestions] +description = "Still spørsmål og få svar forankret i et opplastet dokument." +label = "Dokumentspørsmål" + +[admin.settings.ai.general.features.mathAuditor] +description = "Gå gjennom dokumenter for matematiske og numeriske feil." +label = "Matematikkrevisor" + +[admin.settings.ai.general.features.pdfComment] +description = "Legg til AI-skrevne gjennomgangskommentarer og merknader i en PDF." +label = "PDF-kommentaragent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Tidsavbrudd for tyngre agentoperasjoner som dokumentgenerering." +label = "Tidsavbrudd for langvarige operasjoner (sekunder)" + +[admin.settings.ai.general.note] +body = "AI-motoren kjører som en separat tjeneste. Dens delte hemmelighet" +body2 = "angis via en containermiljøvariabel. Leverandør-API keys kan angis på disse sidene eller leveres som motormiljøvariabler; nøkler som angis her, sendes til motoren når de lagres." +title = "Om AI-motoren" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Tidsavbrudd for strømmede (token-for-token) chattsvar." +label = "Tidsavbrudd for strømming (sekunder)" + +[admin.settings.ai.general.test] +button = "Test tilkobling" +failBody = "AI-motoren svarte ikke. Sjekk URL, at motorcontaineren kjører, og at AI er aktivert (omstart kreves etter aktivering)." +failTitle = "AI-motor ikke tilgjengelig" +okBody = "AI-motoren svarte på en helsesjekk." +okTitle = "AI-motor tilgjengelig" + +[admin.settings.ai.general.timeoutSeconds] +description = "Tidsavbrudd for standard AI-forespørsler til motoren." +label = "Tidsavbrudd for forespørsel (sekunder)" + +[admin.settings.ai.general.url] +description = "Intern URL til Python AI-motoren, f.eks. http://stirling-pdf-engine:5001." +label = "AI-motor-URL" + +[admin.settings.ai.limits] +description = "Sikkerhetsrammer for hvor mye arbeid AI-forespørsler kan gjøre og hvor mange som kan kjøre samtidig. Brukes på AI-motoren når det lagres." +title = "Grenser og ytelse" + +[admin.settings.ai.limits.maxCharacters] +description = "Sikkerhetsramme: avvis AI-forespørsler der uttrukket tekst overstiger denne lengden." +label = "Maks tegn per forespørsel" + +[admin.settings.ai.limits.maxPages] +description = "Sikkerhetsramme: avvis AI-forespørsler over så mange PDF-sider." +label = "Maks sider per forespørsel" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maksimalt antall samtidige pågående modellkall på tvers av hele motoren." +label = "Maks samtidighet for modell" + +[admin.settings.ai.models] +description = "Velg LLM-leverandøren og de smarte/raske modellene AI-motoren bruker. Brukes på AI-motoren når det lagres." +title = "Modeller og leverandører" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "La stå tomt for å bruke motorens egen miljølegitimasjon. Gjelder selvhostede enkelmotordistribusjoner." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Lagret – la stå tomt for å beholde gjeldende nøkkel" + +[admin.settings.ai.models.baseUrl] +description = "Base-URL for OpenAI-kompatibelt / Ollama-endepunkt, f.eks. http://ollama:11434/v1." +label = "Leverandørens base-URL" +warning = "Base-URL må peke til et pålitelig internt endepunkt. Motoren vil gjøre serverbaserte forespørsler til den, så en upålitelig verdi er SSRF-sensitiv." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maksimalt antall utdata-tokens for den raske modellen." +label = "Maks tokens for rask modell" + +[admin.settings.ai.models.fastModel] +description = "Billigere og raskere modell for lette oppgaver. Fritekst; forslag er kun hint." +label = "Rask modell" + +[admin.settings.ai.models.provider] +description = "Hvilken LLM-leverandør motoren kommuniserer med." +label = "Leverandør" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maksimalt antall utdata-tokens for den smarte modellen." +label = "Maks tokens for smart modell" + +[admin.settings.ai.models.smartModel] +description = "Modell med høy kapasitet for kompleks resonnering. Fritekst; forslag er kun hint." +label = "Smart modell" + +[admin.settings.ai.saved] +body = "Endringer sendes automatisk til AI-motoren." +bodyNoPush = "Innstillinger lagret. De tas i bruk neste gang AI-motoren henter konfigurasjonen sin." +title = "AI-innstillinger lagret" + [admin.settings.badge] clickToUpgrade = "Klikk for å se plandetaljer" @@ -938,6 +1094,25 @@ label = "Regenerer ved oppstart" description = "Antall dager sertifikatet skal være gyldig" label = "Sertifikatets gyldighet (dager)" +[admin.settings.folderAccess] +description = "Kataloger som mappekilder og mappeutdata har lov til å lese fra og skrive til. Dette er en sikkerhetsgrense: automatiseringer kan aldri pekes mot en serversti utenfor denne listen." +securityNote = "La dette stå tomt for å deaktivere mappekilder og mappeutdata helt. Stirlings egen konfigurasjonskatalog er alltid forbudt." +title = "Mappetilgang" + +[admin.settings.folderAccess.implied] +description = "Disse Stirling-administrerte katalogene er alltid tillatt og kan ikke endres her." +serverStorage = "Serverfillagring" +title = "Alltid tillatt" +watchedFolder = "Pipeline-overvåket mappe" + +[admin.settings.folderAccess.roots] +add = "Legg til" +empty = "Ingen mapper er tillatt. Mappekilder og mappeutdata er deaktivert for øyeblikket." +hint = "Angi absolutte stier, for eksempel /data/inbox." +label = "Tillatte mapperøtter" +placeholder = "/data/inbox" +remove = "Fjern mapperot" + [admin.settings.general] description = "Konfigurer programinnstillinger for hele systemet, inkludert profilering og standardatferd." system = "System" @@ -1704,6 +1879,9 @@ width = "Bredde" [app] description = "Det gratis alternativet til Adobe Acrobat (10M+ nedlastinger)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konverter til PDF/A-3b" convertToPdfA3bDescription = "Oppretter en arkiv-PDF med innebygde vedlegg" @@ -1842,6 +2020,15 @@ export = "Eksporter" accessDenied = "Tilgang nektet" insufficientPermissions = "Du har ikke tillatelse til å utføre denne handlingen." +[auth.callback] +completing = "Fullfører autentisering" +invalidToken = "OAuth-innlogging mislyktes – ugyldig token." +missingToken = "OAuth-innlogging mislyktes – ingen token mottatt." +oauthFailed = "OAuth-innlogging mislyktes. Prøv igjen." +pleaseWait = "Vent mens vi fullfører innloggingen din." +signedOut = "Du er logget ut. Logg inn igjen." +windowMayClose = "Du kan lukke dette vinduet når det er fullført." + [auth.displayName] guest = "Gjest" user = "Bruker" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Sikrer PDF-dokumenter ved å fjerne potensielt ondsinnet i description = "Dette verktøyet vil automatisk gi PDF-filer nytt navn basert på innholdet. Det analyserer dokumentet for å finne den mest passende tittelen fra teksten." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Roter automatisk" +tags = "roter automatisk,fiks orientering,opp ned,sidelengs,rett opp,orienter,oppdag orientering,korriger rotasjon,auto-orienter,skannet,OSD" + +[autoRotate.confidenceThreshold] +title = "Sikkerhetsterskel" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Kun OCR" +text = "Kun tekst" +title = "Deteksjonsmetode" + +[autoRotate.error] +failed = "Det oppstod en feil under automatisk rotering av PDF-en." + +[autoRotate.inferUndetected] +title = "Roter sider med lav sikkerhet for å samsvare" + +[autoRotate.report] +applied = "{{degrees}}° med klokken" +confidenceValue = "Sikkerhet {{value}}" +noChange = "Ingen endring" +pageLabel = "Side {{number}}" +summary = "{{rotated}} av {{total}} sider rotert, {{unchanged}} beholdt som de er" +title = "Deteksjonsrapport" + +[autoRotate.report.method] +inferred = "Samsvarte" +none = "Hoppet over" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Under terskel" +blankPage = "Siden er tom" +inferredFromDocument = "Samme som de andre sidene" +noDominantDirection = "Blandede tekstretninger" +osdFailed = "Ingen lesbar tekst funnet" +osdNoVerdict = "Kunne ikke avgjøre hvilken vei som er opp" +tesseractUnavailable = "OCR ikke installert" +tooFewGlyphs = "For lite tekst" + +[autoRotate.results] +title = "Resultater for automatisk rotering" + +[autoRotate.settings] +title = "Innstillinger" + +[autoRotate.tooltip.confidenceThreshold] +text = "Hvor sikker kontrollen må være før en side roteres. Senk den for å rotere flere sider, hev den for å la flere sider være urørt." +title = "Sikkerhetsterskel" + +[autoRotate.tooltip.description] +text = "Finner ut hvilken vei hver side skal vende og roterer den riktig vei. Sider som allerede står riktig, blir stående." + +[autoRotate.tooltip.detectionMode] +text = "Auto leser først teksten på siden, og ser deretter på siden som et bilde hvis det ikke er nok. Kun tekst hopper over bildekontrollen, som er raskest for dokumenter som aldri ble skannet. Kun OCR behandler hver side som et bilde." +title = "Deteksjonsmetode" + +[autoRotate.tooltip.header] +title = "Roter automatisk" + +[autoRotate.tooltip.inferUndetected] +text = "Tomme eller nesten tomme sider kan ikke kontrolleres alene. Når alle sider som kunne leses trenger samme rotasjon, roteres disse sidene for å samsvare med dem." +title = "Roter sider med lav sikkerhet for å samsvare" + [autoSizeSplitPDF] tags = "pdf,del,dokument,organisering" @@ -2189,33 +2443,6 @@ keywords = "Nøkkelord: odd, even" numbers = "Tall/intervaller: 5, 10-20" progressions = "Progresjoner: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Sertifikatyper" - -[certificateChoice.tooltip.organization] -bullet1 = "Administreres av systemadministratorer" -bullet2 = "Deles på tvers av autoriserte brukere" -bullet3 = "Representerer virksomhetens identitet, ikke individet" -bullet4 = "Best for: Offisielle dokumenter, teamsignaturer" -description = "Et delt sertifikat levert av organisasjonen din. Brukes for signeringsmyndighet på tvers av selskapet." -title = "Organisasjonssertifikat" - -[certificateChoice.tooltip.personal] -bullet1 = "Genereres automatisk ved første bruk" -bullet2 = "Knyttet til brukerkontoen din" -bullet3 = "Kan ikke deles med andre brukere" -bullet4 = "Best for: Personlige dokumenter, individuell ansvarlighet" -description = "Et autogenerert sertifikat unikt for brukerkontoen din. Egnet for individuelle signaturer." -title = "Personlig sertifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Krever P12/PFX-fil og passord" -bullet2 = "Kan utstedes av eksterne sertifikatutstedere" -bullet3 = "Høyere tillitsnivå for juridiske dokumenter" -bullet4 = "Best for: Rettslig bindende kontrakter, ekstern validering" -description = "Bruk din egen PKCS#12-sertifikatfil. Gir full kontroll over sertifikategenskaper." -title = "Last opp egendefinert P12" - [certSign] awaitingSignatures = "Avventer signaturer" chooseCertificate = "Velg sertifikatfil" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Sertifikatkilde" upload = "Last opp" -[certSign.tooltip.header] -title = "Om håndtering av signaturer" - -[certSign.tooltip.overview] -bullet1 = "Sjekk eksisterende signaturer og deres gyldighet" -bullet2 = "Vis detaljert informasjon om underskrivere og sertifikater" -bullet3 = "Legg til nye digitale signaturer for å sikre dokumentene dine" -bullet4 = "Flere filer støttes med enkel navigasjon" -text = "Dette verktøyet lar deg sjekke om PDF-ene dine er digitalt signert og legge til nye digitale signaturer. Digitale signaturer beviser hvem som opprettet eller godkjente et dokument og viser om det er endret siden signering." -title = "Hva kan dette verktøyet gjøre?" - -[certSign.tooltip.signing] -bullet1 = "Støtter PEM, PKCS12, JKS og serversertifikat-formater" -bullet2 = "Valg for å vise eller skjule signaturen på PDF-en" -bullet3 = "Legg til årsak, sted og underskrivers navn" -bullet4 = "Velg hvilken side du vil plassere synlige signaturer på" -bullet5 = "Bruk serversertifikat for enkel «Signer med Stirling-PDF»-mulighet" -text = "For å signere en PDF, trenger du et digitalt sertifikat (som PEM, PKCS12 eller JKS). Du kan velge å gjøre signaturen synlig på dokumentet eller holde den usynlig kun for sikkerhet." -title = "Legge til signaturer" - -[certSign.tooltip.validation] -bullet1 = "Viser om signaturer er gyldige eller ugyldige" -bullet2 = "Viser informasjon om underskriver og signeringsdato" -bullet3 = "Sjekker om dokumentet ble endret etter signering" -bullet4 = "Kan bruke egendefinerte sertifikater for verifisering" -text = "Når du sjekker signaturer, forteller verktøyet deg om de er gyldige, hvem som signerte dokumentet, når det ble signert, og om dokumentet er endret siden signering." -title = "Kontroll av signaturer" - [changeCreds] changeUsername = "Oppdater brukernavnet ditt. Du blir logget ut etter oppdatering." credsUpdated = "Konto oppdatert" @@ -2737,12 +2936,16 @@ text = "For å gjøre disse tillatelsene uforanderlige, bruk verktøyet Legg til [chat.actions] copy = "Kopier melding" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling-agentalternativer" +assistant = "AI Assistant" clearChat = "Tøm chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Denne operasjonen vil bruke skykreditter" tooltip = "Velg farge fra skjermen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Velg farge" [common] +actions = "Actions" back = "Tilbake" cancel = "Avbryt" close = "Lukk" +codeSample = "Code sample" collapse = "Slå sammen" confirm = "Bekreft" continue = "Fortsett" copied = "Kopiert!" copy = "Kopier" done = "Ferdig" +enterValue = "Enter value" error = "Feil" expand = "Utvid" loading = "Laster..." next = "Neste" +open = "Open" preview = "Forhåndsvis" previous = "Forrige" refresh = "Oppdater" +remaining = "Gjenstår" retry = "Prøv igjen" save = "Lagre" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Tøm valgte" @@ -2988,6 +3199,7 @@ title = "Komprimeringsmetode" [compress.settings] desiredSize = "Ønsket filstørrelse" desiredSizePlaceholder = "Angi størrelse" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Komprimering er en enkel måte å redusere filstørrelsen på. Velg Filstørrelse for å angi en målstørrelse og la oss justere kvaliteten for deg. Velg Kvalitet for å sette komprimeringsstyrke manuelt." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF-alternativer" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Velg et kildeformat først" settings = "Innstillinger" single = "Enkel" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Avansert overvåking" -allInOneWorkspace = "Alt-i-ett PDF-arbeidsområde (viser, verktøy og agent)" -apiSandbox = "API-sandkasse" -dedicatedSupportSlas = "Dedikert støtte og SLA-er" -fasterThroughput = "10x raskere gjennomstrømming" -fullyPrivateFiles = "Fullt private filer" -largeFileProcessing = "Behandling av store filer" -orgWideAccess = "Organisasjonsomfattende tilgangskontroller" -premiumAiModels = "Premium AI-modeller" -prioritySupport = "Prioritert støtte" -privateDocCloud = "Privat dokumentsky" -ragFineTuning = "RAG + finjustering" -secureApiAccess = "Sikker API-tilgang" -standardThroughput = "Standard gjennomstrømming" -unlimitedApiAccess = "Ubegrenset API-tilgang" -unlimitedSeats = "Ubegrensede plasser" - [crop] autoCrop = "Beskjær hvite mellomrom automatisk" noFileSelected = "Velg en PDF-fil for å begynne beskjæring" @@ -3346,6 +3558,7 @@ message = "Kunne ikke angi standard PDF-behandler" title = "Feil" [defaultApp.prompt] +dontRemind = "Ikke minn meg på dette igjen" message = "Gjør Stirling PDF til standardappen for å åpne PDF-filer." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Søk..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Rediger" @@ -3644,11 +3861,13 @@ shareSelected = "Del valgte" sharing = "Deling" showAll = "Vis alle" showHistory = "Vis historikk" +sortBy = "Sort files" sortByDate = "Sorter etter dato" sortByName = "Sorter etter navn" sortBySize = "Sorter etter størrelse" storage = "Lagring" storageState = "Lagring" +storageUsed = "Storage used" synced = "Synkronisert" title = "Last opp PDF-filer" toolChain = "Brukte verktøy" @@ -3694,13 +3913,17 @@ addFiles = "Legg til filer" addingFiles = "Legger til filer…" collapse = "Skjul sidepanel" customizeGroups = "Tilpass grupper" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Åpne filer for å komme i gang" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Utvid sidepanel" -files = "Filer" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive er ikke konfigurert" leaveMyFiles = "Forlat Mine filer" +library = "PDF-bibliotek" myFiles = "Mine filer" noFiles = "Ingen filer ennå" openFileManager = "Åpne filbehandler" @@ -3708,37 +3931,36 @@ openFromComputer = "Åpne fra datamaskin" openSettings = "Åpne innstillinger" other = "Annet" recent = "Nylig" -search = "Søk" -searchPlaceholder = "Søk i filer..." viewAll = "Vis alle {{count}} filer" [fileSidebar.fileItem] closeViewer = "Lukk fremviser" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Åpne i fremviser" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Legg til" -addLabel = "Legg til en etikett…" -categoryIconAria = "Velg et ikon for {{name}}" -createCategory = "Opprett kategori" -delete = "Slett kategori" done = "Ferdig" -hide = "Skjul kategori" -newCategory = "Nytt kategorinavn…" -removeLabel = "Fjern {{name}}" -rename = "Gi nytt navn" reset = "Tilbakestill til standarder" -search = "Søk i etiketter…" -show = "Vis kategori" subtitle = "Grupper filene dine i overordnede kategorier. Legg til eksisterende eller nye etiketter i en kategori, gi den nytt navn, eller opprett din egen. Filer som ikke er i noen av kategoriene dine, vises under «Annet»." title = "Sidefeltkategorier" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Legg til i arbeidsområde" addToWorkspaceCount = "Legg {{count}} til i arbeidsområde" @@ -3749,7 +3971,6 @@ backToMyFiles = "Tilbake til Mine filer" breadcrumbs = "Mappesti" cancel = "Avbryt" classification = "Klassifisering" -clearSearch = "Tøm søk" clearSelection = "Tøm utvalg" closeDetails = "Lukk detaljer" create = "Opprett" @@ -3781,6 +4002,7 @@ downloadAll = "Last ned alle" downloadVersion = "Last ned denne versjonen" dropOverlay = "Slipp filer for å laste opp" dropOverlaySub = "Filer starter i Lokal. Bruk 'Flytt til' eller 'Lagre til skyen' for å organisere dem i en mappe." +duplicate = "Duplicate" file = "Fil" fileInfo = "File info" fileMenu = "Filhandlinger" @@ -3808,13 +4030,12 @@ refresh = "Oppdater fra server" remove = "Slett" removeVersion = "Fjern denne versjonen" rename = "Gi nytt navn" +renamed = "Omdøpt" renameFolder = "Gi mappen nytt navn" resizeFolderTree = "Endre størrelse på mappetre (piltaster, Shift for større steg; dobbeltklikk for å tilpasse automatisk)" save = "Lagre" saveToServer = "Lagre til server" saveToServerDisabledHint = "Lagring til serveren er ikke aktivert på denne serveren. Be administratoren om å aktivere det." -search = "Søk" -searchPlaceholder = "Søk i denne mappen og undermapper" selectAll = "Velg alt" selectAllHint = "Klikk for å velge alt. Tips: hold Ctrl (eller Cmd) for å legge til filer én om gangen, Shift for å velge et område." selectedCount = "{{count}} valgt" @@ -3874,6 +4095,10 @@ title = "Ingen skyfiler ennå" hint = "Filer lagret uten opplasting blir værende her. Slipp en fil for å legge til en." title = "Ingen kun-lokale filer" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Filer du åpner eller redigerer, vises her." title = "Ingenting endret ennå" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Kunne ikke slette mappe." deleteFolderFailedDetail = "Kunne ikke slette mappe: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Kunne ikke oppdatere mappeutseende." folderAppearanceFailedDetail = "Kunne ikke oppdatere mappeutseende: {{message}}" moveFilesFailed = "Kunne ikke flytte filer." @@ -3910,9 +4137,7 @@ count = "Filer" folder = "Mappe" labels = "Etiketter" modified = "Endret" -name = "Navn" size = "Størrelse" -toolHistoryAtVersion = "Samlet verktøykjede" totalSize = "Total størrelse" type = "Type" versionHistory = "Versjonsforløp" @@ -3945,6 +4170,11 @@ localHint = "Kun lagret i denne nettleseren" shared = "Delt" sharedHint = "Delt med deg via lenke" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Eldste først" modifiedDesc = "Nyeste først" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Ingen filer tilgjengelig i lagringen. Åpne noen filer f open = "Åpne" openFile = "Åpne fil" openFiles = "Åpne filer" +selectFile = "Select {{name}}" selectFromStorage = "Velg fra lagring" upload = "Last opp" uploadFile = "Last opp fil" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Alt lagret" +analyzingFields = "Analysing form fields..." +extractCsvError = "Kunne ikke trekke ut CSV" +extractXlsxError = "Kunne ikke trekke ut XLSX" +filled = "filled" flattenAfterFilling = "Flate ut etter utfylling" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl" +requiredFieldsError = "Fyll ut alle obligatoriske felt" rescanFields = "Skann felter på nytt" rescanFormFields = "Skann skjemafelter på nytt" save = "Lagre" +saveShortcut = "Ctrl+S for å lagre" +unsavedChanges = "Ulagrede endringer" [formFill.sidebar] close = "Lukk sidepanel" @@ -4309,6 +4552,11 @@ desc = "Gir automatisk nytt navn til en PDF-fil basert på den oppdagede overskr tags = "autogjenkjenning,overskriftsbasert,organiser,gi nytt navn" title = "Gi PDF-fil nytt navn automatisk" +[home.autoRotate] +desc = "Oppdag orienteringen til hver side og roter den automatisk riktig vei." +tags = "roter automatisk,fiks orientering,opp ned,sidelengs,rett opp,orienter,oppdag orientering,korriger rotasjon,auto-orienter,skannet,OSD" +title = "Roter automatisk" + [home.autoSizeSplitPDF] tags = "auto,del,størrelse" title = "Auto-del størrelse/antall" @@ -4608,15 +4856,12 @@ hint = "Last opp en PNG, JPG, SVG eller annen bildefil for å plassere på PDF-e label = "Last opp bilde" placeholder = "Velg bildefil" -[imageToPdf] -tags = "konvertering,bilde,jpg,foto" - [imageToPDF] header = "Bilde til PDF" title = "Bilde til PDF" -[infoBanner] -dismiss = "Lukk" +[imageToPdf] +tags = "konvertering,bilde,jpg,foto" [invite] acceptError = "Kunne ikke opprette konto" @@ -4647,11 +4892,9 @@ welcomeTitle = "Du er invitert!" [landing] addFiles = "Legg til filer" -heroSubtitle = "Slipp inn eller legg til en eksisterende PDF for å komme i gang." mobileUpload = "Last opp fra mobil" openFromComputer = "Åpne fra datamaskin" uploadFromComputer = "Last opp fra datamaskin" -workbenchEmptyStateHero = "Slipp en PDF hvor som helst" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Skriv inn brukernavn" failedToSignIn = "Kunne ikke logge inn med {{provider}}: {{message}}" forgotPassword = "Glemt passordet?" loggingIn = "Logger inn..." -logIn = "Logg inn" login = "Logg inn" magicLinkSent = "Magisk lenke sendt til {{email}}! Sjekk e-posten din og klikk på lenken for å logge inn." mfaCode = "Autentiseringskode" @@ -4715,26 +4957,10 @@ signInWith = "Logg inn med" title = "Logg inn" unexpectedError = "Uventet feil: {{message}}" updatePassword = "Oppdater passord" -useEmailInstead = "Logg inn med e-post" useMagicLink = "Bruk magisk lenke i stedet" username = "Brukernavn" youAreLoggedIn = "Du er logget inn!" -[login.slides.edit] -alt = "Rediger PDF-er" -subtitle = "Med over et dusin verktøy for å sladde, signere, lese og manipulere PDF-er finner du garantert det du trenger." -title = "Rediger PDF-er for å vise/sikre informasjonen du ønsker" - -[login.slides.overview] -alt = "Stirling PDF-oversikt" -subtitle = "En skyløsning for PDF med personvern i fokus som lar deg konvertere, signere, sladde og administrere dokumenter, samt 50+ andre kraftige verktøy." -title = "Alt du trenger for PDF på ett sted." - -[login.slides.secure] -alt = "Sikre PDF-er" -subtitle = "Legg til passord, sladd innhold og administrer sertifikater enkelt." -title = "Beskytt sensitiv informasjon i PDF-ene dine" - [margin] large = "Stor" medium = "Middels" @@ -4818,6 +5044,34 @@ uploadSuccess = "Opplasting vellykket!" uploadSuccessMessage = "Bildene dine er overført." validating = "Validerer økt..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skann for å laste opp bilder. Bilder konverteres automatisk til PDF." descriptionNoConvert = "Skann for å laste opp bilder fra mobilenheten din." @@ -4836,8 +5090,13 @@ title = "Last opp fra mobil" tags = "Multi Verktøy,Multi operasjon,UI,klikk dra,frontend,klientside,interaktiv,bevegelig" title = "PDF-multiverktøy" -[navbar] -search = "Søk" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentiseringen var ikke vellykket. Du kan lukke dette vinduet og prøve igjen." @@ -4966,6 +5225,7 @@ activeFiles = "Visningen Aktive filer viser alle PDF-ene du har allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Lukk" cropSettings = "Nå som vi har valgt filen vi vil beskjære, kan vi konfigurere Beskjær-verktøyet til å velge området vi vil beskjære PDF-en til." +dialogLabel = "Onboarding" fileCheckbox = "Å klikke på en av filene velger den for behandling. Du kan velge flere filer for batchoperasjoner." fileReplacement = "Den endrede filen vil erstatte originalfilen i Arbeidsbenken automatisk, slik at du enkelt kan kjøre den gjennom flere verktøy." filesButton = "Knappen Filer på hurtigtilgangslinjen lar deg laste opp PDF-er til bruk i verktøyene." @@ -4976,6 +5236,7 @@ pinButton = "Du kan bruke Fest-knappen hvis du heller vil at fi results = "Etter at verktøyet er ferdig kjørt, vil trinnet Gjennomgang vise en forhåndsvisning av resultatene i dette panelet, og la deg angre operasjonen eller laste ned filen. " runButton = "Når verktøyet er konfigurert, lar denne knappen deg kjøre verktøyet på alle de valgte PDF-ene." selectCropTool = "La oss velge Beskjær-verktøyet for å demonstrere hvordan du bruker et av verktøyene." +stepOf = "Trinn {{current}} av {{total}}" toolInterface = "Dette er Beskjær-verktøyets grensesnitt. Som du ser, er det ikke mye der fordi vi ikke har lagt til noen PDF-filer å jobbe med ennå." workbench = "Dette er Arbeidsbenken - hovedområdet der du viser og redigerer PDF-ene dine." wrapUp = "Da er du klar! Du har lært om hovedområdene i appen og hvordan du bruker dem. Klikk på Hjelp-knappen når som helst for å se denne omvisningen igjen." @@ -4988,6 +5249,26 @@ showMeAround = "Vis meg rundt" skipForNow = "Hopp over nå" skipTheTour = "Hopp over omvisningen" +[onboarding.checklist] +dismiss = "Avvis" +title = "Sett opp Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Kjør Stirling direkte på maskinen din" +title = "Last ned Stirling for skrivebord" + +[onboarding.checklist.inviteTeam] +description = "Samarbeid med teamet ditt" +title = "Inviter teammedlemmer" + +[onboarding.checklist.shareAnalytics] +description = "Hjelp til med å forbedre Stirling" +title = "Del anonyme bruksdata" + +[onboarding.checklist.takeTour] +description = "Se hvordan Stirling fungerer i en rask gjennomgang" +title = "Ta omvisningen" + [onboarding.desktopInstall] body = "Stirling fungerer best som en skrivebordsapp. Du kan bruke den offline, få raskere tilgang til dokumenter og gjøre redigeringer lokalt på datamaskinen." selectOs = "Velg operativsystem" @@ -4996,7 +5277,21 @@ titleWithOs = "Last ned for {{osLabel}}" [onboarding.mfa] authenticationCode = "Autentiseringskode" +description = "Sikre kontoen din ved å koble til en autentiseringsapp. Skann QR-koden eller angi oppsettnøkkelen, og bekreft deretter den 6-sifrede koden for å fullføre." +enable = "Aktiver MFA" +enableError = "Kan ikke aktivere tofaktorautentisering. Sjekk koden og prøv igjen." +enterCode = "Angi den 6-sifrede koden fra appen din." +enterCodeError = "Angi autentiseringskoden for å fortsette." +logout = "Logg ut" +openAuthenticator = "Åpne Google Authenticator, Authy eller 1Password." qrCodeLoading = "Genererer QR-koden din…" +regenerateQrCode = "Generer QR-kode på nytt" +scanQrCode = "Skann QR-koden eller angi oppsettnøkkelen nedenfor." +setupError = "Kan ikke starte tofaktoroppsett. Prøv igjen." +setupKey = "Oppsettnøkkel (manuell inntasting)" +stepByStep = "Trinn for trinn" +success = "MFA er aktivert. Du kan nå fortsette." +title = "Oppsett av flerfaktorautentisering" [onboarding.planOverview] adminBodyLoginDisabled = "Når du aktiverer innloggingsmodus, kan du administrere brukere, konfigurere innstillinger og overvåke serverstatus. De første {{freeTierLimit}} på serveren din kan bruke Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Admin-oversikt" userBody = "Inviter teammedlemmer, tildel roller og hold dokumentene organiserte i et sikkert arbeidsområde. Aktiver innloggingsmodus når du er klar til å vokse utover solobruk." userTitle = "Planoversikt" +[onboarding.processorIntro] +body = "Stirling kjører nå policyer — automatiserte regler som klassifiserer, sikrer og behandler hvert dokument når det ankommer. Sett dem opp og overvåk kjøringer i Processor." +cta = "Sjekk ut Processor" +title = "Sjekk ut Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Forhåndsbetalt år" + +[payg.prepaid.banner] +exhausted = "Den forhåndsbetalte kapasiteten din er brukt opp; målt fakturering er gjenopptatt. Fyll på for å beholde rabatten." +expiring_one = "Det forhåndsbetalte året ditt slutter om {{count}} dag. Ubrukt kapasitet utløper — fyll på for å starte et nytt år." +expiring_other = "Det forhåndsbetalte året ditt slutter om {{count}} dager. Ubrukt kapasitet utløper — fyll på for å starte et nytt år." +low = "Du er nede i {{pct}} % av den forhåndsbetalte kapasiteten din. Fyll på når som helst for å holde behandlingen uavbrutt." + +[payg.prepaid.card] +subtitle = "Brukes før målt fakturering og utenfor forbruksgrensen din." +title = "Forhåndsbetalt kapasitet" + +[payg.prepaid.meter] +capSuffix = "av {{total}} forhåndsbetalte kreditter" +expires = "Utløper {{date}}" + +[payg.prepaid.state] +exhausted = "Brukt opp" +healthy = "Mye igjen" +low = "Begynner å bli lite" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF til PDF/A" tags = "arkiv,langtidslagring,standard,konvertering,lagring,bevaring" title = "PDF til PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "utskrift,standard,konvertering,produksjon,trykklargjøring,arkiv" title = "PDF til PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kreditter" xsmall = "100 kreditter" [plan.availablePlans] +currency = "Billing currency" subtitle = "Velg planen som passer dine behov" title = "Tilgjengelige planer" @@ -5937,49 +6267,12 @@ keyDescription = "Lim inn lisensnøkkelen fra e-posten din" success = "Lisens aktivert!" successMessage = "Lisensen din er aktivert. Du kan nå lukke dette vinduet." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "håndhevet" -enforcing = "Håndhever..." -failed = "Håndheving mislyktes" outputsUnavailable = "Policy-utdata er ikke lenger tilgjengelige for nedlasting." partialOutputsUnavailable = "Noen policy-utdata er ikke lenger tilgjengelige for nedlasting." -retrying = "Opptatt, prøver på nytt..." runNotFound = "Håndhevingskjøringen kunne ikke lenger finnes." -step = "trinn {{current}}/{{total}}" timedOut = "Håndhevingen fikk tidsavbrudd før kjøringen kunne fullføres." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Lukk" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Tar i bruk {{names}}" applyingProgress = "Tar i bruk {{names}} ({{done}} av {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} av {{total}} filer kunne ikke behandles og ble ekspo failureTitle = "Eksportert uten full håndheving" printPolicyAppliedBody = "Denne PDF-en ble oppdatert for å oppfylle en policy. Gå gjennom endringene, og skriv deretter ut på nytt." printPolicyAppliedTitle = "Policy tatt i bruk før utskrift" -queued = "+{{count}} i kø" successTitle = "{{names}} tatt i bruk" summaryMore = "{{first}}, {{second}} og {{more}} til" summaryTwo = "{{first}} og {{second}}" -[policies.enforcement.triggerVerb] -convert = "Håndhever før konvertering" -default = "Håndhever" -export = "Håndhever før eksport" -input = "Håndhever ved import" -print = "Håndhever før utskrift" - -[policies.field] -accessLog = "Tilgangslogg" -archiveAfter = "Arkiver etter" -auditTrail = "Revisjonsspor" -belowThreshold = "Under terskel" -destination = "Destinasjon" -frameworks = "Rammeverk" -immutableHold = "Uforanderlig sperre" -keepFor = "Behold i" -minConfidence = "Min. sikkerhet" -notify = "Varsle ved ruting" -onViolation = "Ved manglende samsvar" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Legg til" -addPlaceholder = "Legg til en etikett…" -customNote = "Tilpasset for teamet ditt." -defaultNote = "Bruker den innebygde standarden, delt med teamet ditt." -duplicate = "\"{{name}}\" finnes allerede." -edit = "Rediger etiketter" -empty = "Ingen etiketter ennå." -export = "Eksporter JSON" -iconAria = "Velg et ikon for {{name}}" -import = "Importer JSON" -importError = "Kunne ikke importere den filen." -managedNote = "Teametiketter administreres av teamlederen din." -modalSubtitle = "Delt med hele teamet ditt. Klassifisereren velger etikettene som passer hvert dokument." -modalTitle = "Klassifiseringsetiketter" -pickIcon = "Bruk {{label}}-ikon" -removeAria = "Fjern {{name}}" -resetToDefault = "Tilbakestill til standard" -saveForTeam = "Lagre for team" -saving = "Lagrer…" -sectionLabel = "Klassifiseringsetiketter" -startFromScratch = "Start fra bunnen" -teamCount = "teametiketter" -tooLong = "Etiketter kan være høyst {{max}} tegn." -ungrouped = "Ugruppert" -view = "Vis etiketter" +categoryCount = "kategorier" +hideCategory = "Skjul kategori" +labelCount = "etiketter" +sharedNote = "Disse etikettene er innebygde og deles på tvers av hele teamet ditt." +showCategory = "Vis kategori" [policies.pii] account = "Kontonumre (merket)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Amerikanske routing-numre (ABA)" ssn = "Social Security-numre" -[policies.settings] -noneExport = "Ingen policyer kjøres for øyeblikket ved eksport." -noneUpload = "Ingen policyer kjøres for øyeblikket ved opplasting." -onExport = "Ved eksport" -onUpload = "Ved opplasting" -reorderHandle = "Dra for å endre rekkefølge" -runOrderDesc = "Når mer enn én policy kjøres på samme utløser, kjøres de i denne rekkefølgen — hver på utdataene fra forrige policy. Dra for å endre rekkefølge." -title = "Policyinnstillinger" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Policyalternativer" -policySettings = "Policyinnstillinger" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Prøv mislykkede policyer på nytt ({{count}})" -rowProgress = "{{completed}} av {{total}} filer behandlet" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Hva er en policy?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Finner automatisk og sverter ut sensitive opplysninger — som Social Security- og kortnumre — slik at de ikke kan leses i dokumentet." -sanitize = "Fjerner skjult JavaScript fra filen, slik at ingenting kan kjøres automatisk når noen åpner den." -watermark = "Setter et synlig stempel (f.eks. \"Konfidensielt\") over hver side." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Lukk" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} håndheves..." badgeRan = "{{name}}-policyen kjørte på denne filen" +badgeRunning = "{{name}} kjører..." blockingAction = "{{action}} blokkert mens policy håndheves, vent..." dismiss = "Avvis overlegg" enforcingTitle = "Håndhever policy..." @@ -6151,6 +6316,7 @@ viewAnyway = "Vis fil (policy håndheves fortsatt)" [policyOption] autoRedactPhi = "Sladd PHI automatisk" blockExport = "Blokker eksport" +ccpa = "CCPA" contracts = "Kontrakter" documents = "Dokumenter" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor-plan" subscribed = "Processor-plan" unlinked = "Ikke koblet til" -[portal.agentBuilder] -bootstrapFromDocument = "Initialiser fra dokument" -sectionsAriaLabel = "Agentbyggerseksjoner" -selectorAriaLabel = "Agenter" -subtitle = "Utform, test og lever AI-agentene som klassifiserer, trekker ut fra og ruter dokumentene dine. Definer scenarioer, avgrens verktøytilgang, kjør et golden set og publiser en versjon." -title = "Agentbygger" - -[portal.agentBuilder.bootstrap] -cancel = "Avbryt" -dropzoneText = "Velg et eksempeldokument (PDF eller bilde)" -lead = "Slipp et eksempeldokument, så foreslår vi scenarioer og et ekstraksjonsskjema du kan finjustere. Ingenting publiseres før du har gjennomgått det." -submit = "Initialiser agent" -subtitle = "Start en ny agent fra én representativ fil" -title = "Initialiser fra et dokument" - -[portal.agentBuilder.empty] -description = "Initialiser en agent fra et eksempeldokument for å starte scenarioene og ekstraksjonsskjemaet, og finjuster og publiser deretter." -title = "Ingen agenter ennå" - -[portal.agentBuilder.evals] -casesPassing = "Tilfeller bestått" -columnCase = "Eval-tilfelle" -columnLatency = "Latens" -columnResult = "Resultat" -fail = "mislykket" -goldenSetPassRate = "Beståttandel for golden set" -latencyMs = "{{ms}} ms" -notRun = "ikke kjørt" -pass = "bestått" -passRate = "Beståttandel" -runEvals = "Kjør evalueringer" - -[portal.agentBuilder.evals.empty] -description = "Evalueringer gjør scenarioene dine om til et repeterbart golden set. Oppgrader for å registrere beståttandel over tid og styre publiseringer etter den." -title = "Ingen golden set ennå" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "på tvers av golden sets" -activeAgents = "Aktive agenter" -avgPassRate = "Gj.sn. beståttandel for evalueringer" -fleetWide = "på tvers av flåten" -latestPublished = "Sist publisert" -scenarios = "Scenarioer" -testCases = "testtilfeller" -totalDescription_one = "{{count}} totalt" -totalDescription_other = "{{count}} totalt" - -[portal.agentBuilder.scenarios] -add = "Legg til" -addScenario = "Legg til scenario" -enable = "Aktiver" -expectationLabel = "Forventet atferd" -expectationPlaceholder = "Hva agenten skal gjøre" -inEval = "i evaluering" -mute = "Demp" -muted = "dempet" -nameLabel = "Navn" -namePlaceholder = "f.eks. eskalering av samsvar" - -[portal.agentBuilder.tabs] -evals = "Evalueringer" -scenarios = "Scenarioer" -tools = "Verktøy" -versions = "Versjoner" - -[portal.agentBuilder.tools] -broadAccess = "Bred tilgang" -deniedHint = "Valgte verktøy er blokkert. Alt annet kan fortsatt kalles." -deniedTools = "Avviste verktøy" -governanceGate = "Verktøystyring er tilgjengelig på Enterprise-planen." -restricted = "Begrenset" -restrictedAccess = "Begrenset verktøytilgang" -restrictedDescription = "Tillat alle verktøy unntatt de du avviser nedenfor." - -[portal.agentBuilder.versions] -current = "gjeldende" -historyGate = "Full versjonshistorikk og tilbakestilling er tilgjengelig på Enterprise-planen." -publish = "Publiser" -rollBack = "Rull tilbake" - [portal.assistant] close = "Lukk assistent" error = "Kunne ikke nå assistenten." @@ -6365,9 +6451,21 @@ typing = "Skriver" loading = "Laster" redirectingToEditor = "Omdirigerer til redigeringsprogrammet..." +[portal.billing.activation] +maybeLater = "Kanskje senere" +title = "Slå på Processor" + +[portal.billing.activation.payg] +sub = "Sett en månedlig grense. Betal for det du kjører." +title = "Betal etter forbruk" + +[portal.billing.activation.prepay] +badge = "2 måneder gratis" +sub = "Én faktura, kort eller bankoverføring." +title = "Forhåndsbetal et år" + [portal.billing.checkout] noClientSecret = "Edge-funksjonen returnerte ingen client_secret." -subtitle = "Legg til et kort for å fortsette etter den gratis tildelingen i Editor-planen. Stripe håndterer resten." title = "Slå på Processor-planen" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Betalingen din lyktes, men aktiveringen tar litt lengre tid enn vanlig. close = "Lukk" title = "Nesten ferdig" +[portal.billing.checkout.cap] +amountAria = "Månedlig forbruksgrense" +back = "Tilbake" +continue = "Fortsett til betaling" +error = "Kunne ikke sette forbruksgrensen din" +finePrint = "Fakturaer sendes månedlig. Avbryt når som helst." +noLimit = "Ingen grense" +note = "Du faktureres aldri over grensen din. Behandlingen pauses bare." +perMonth = "/ md" +title = "Sett forbruksgrensen din" + +[portal.billing.checkout.card] +fields = "Kortnummer · MM / ÅÅ · CVC · ZIP" +label = "Kortdetaljer" +note = "Kortdetaljer samles inn av Stripe. Stirling lagrer aldri PAN eller CVC." + [portal.billing.checkout.error] title = "Kunne ikke starte checkout" @@ -6383,10 +6497,9 @@ body = "Betalingen din gikk gjennom. Vi slår på målt behandling på tvers av hint = "Hold dette vinduet åpent." title = "Aktiverer Processor-planen din..." -[portal.billing.checkout.notConfigured] -bodyAfter = "i portal-env for å aktivere checkout i appen." -bodyBefore = "Angi" -title = "Stripe er ikke konfigurert" +[portal.billing.checkout.pay] +limit = "Månedlig forbruksgrense {{amount}}/md. Behandlingen pauses ved grensen." +limitNoCap = "Ingen forbruksgrense. Behandlingen pauses ikke. Fakturaer sendes månedlig." [portal.billing.enterpriseUpsell] cta = "Bygg ditt Enterprise-tilbud" @@ -6409,10 +6522,12 @@ currentPlan = "Gjeldende plan" freeForever = "Gratis for alltid" noTeamResolved = "Ingen team er løst på lommeboken din ennå — oppdater og prøv igjen." ownerOnly = "Bare teameieren kan slå på Processor-planen." +payInvoice = "Betal faktura for å fullføre" planName = "Editor" ssoIncluded = "SSO inkludert" switchOnProcessor = "Slå på Processor →" unlimitedUsers = "Ubegrenset antall brukere" +viewQuote = "Vis tilbud" [portal.billing.invoices] columnAmount = "Beløp" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direkte API-forespørsler" segmentApiLabel = "API" segmentAutomationDesc = "Automatiseringer og pipelines" segmentAutomationLabel = "Automatisering" +sizeMultiplier = "{{formatted}} PDF-er brukte en størrelsesmultiplikator" +summary = "{{unique}} unike · {{units}} målerenheter · {{avg}} gj.sn. per PDF" +summaryNoRate = "{{unique}} unike · {{units}} målerenheter" unit = "målte PDF-er" +unitsPending_one = "{{units}} målerenhet venter på synkronisering fra tilkoblede instanser" +unitsPending_other = "{{units}} målerenheter venter på synkronisering fra tilkoblede instanser" + +[portal.billing.prepaid] +capSuffix = "av {{total}} forhåndsbetalte kreditter" +expires = "Utløper {{date}}" +eyebrow = "Forhåndsbetalt kapasitet" +note = "Brukes før målt fakturering og utenfor forbruksgrensen din." +topUp = "Fyll på" + +[portal.billing.prepaid.buy] +back = "Tilbake" +calcTitle = "Beregn årsbetalingen din" +cancel = "Avbryt" +cancelPurchase = "Avbryt kjøp" +close = "Lukk" +continueToPayment = "Fortsett til betaling" +finalise = "Fullfør" +invoiceErrorTitle = "Kunne ikke generere fakturaen" +loadingTitle = "Laster tilbudet ditt" +payTitle = "Betal for året ditt" +step = "Trinn {{current}} av {{total}}" + +[portal.billing.prepaid.calc] +change = "Endre" +deployRow = "Distribusjon" +done = "Ferdig" +enterpriseHint = "Dette er enterprise-skala. Ved dette volumet slår enterprise-priser enhver selvbetjent rabatt. Prislås, vilkår og et tilbud på minutter." +governanceRow = "Styring" +governanceValue = "{{label}} · {{count}} policyer" +handlesLabel = "Din Processor" +handlesValue = "håndterer {{volume}} kreditter / md" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policyer" +poolCaption = "Én pool med {{credits}} kreditter for året. Tunge måneder låner fra lette." +priceLabel = "Ditt år · 12 måneder til prisen av 10" +rateUnknown = "Vi viser nøyaktig pris i kassen." +savings = "Du sparer {{amount}} · 2 måneder gratis." +sizingRow = "Dimensjonering" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Totalt antall brukere" + +[portal.billing.prepaid.consent] +label = "Jeg forstår at når den forhåndsbetalte kapasiteten min er brukt opp eller utløper etter 12 måneder, fortsetter behandlingen automatisk til standard målt betal-etter-forbruk-sats (opp til forbruksgrensen min) med mindre jeg avbryter, og at jeg kan avbryte når som helst fra faktureringsportalen." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Administrert av Stirling. I drift på minutter." +cloudValue = "Stirling Cloud · Administrert" +selfhost = "Selvhostet" +selfhostDesc = "Kjører på privat infrastruktur. Samme sats." +selfhostValue = "Selvhostet · Privat infrastruktur" + +[portal.billing.prepaid.offer] +cta = "Gjennomgå tilbud" +subtitle = "Forhåndsbetal et år med PDF-behandling og få to måneder gratis, brukt før målt fakturering, utenfor forbruksgrensen din." +title = "Få 12 måneder til prisen av 10" + +[portal.billing.prepaid.pay] +accountName = "Kontoinnehavers navn" +accountNamePlaceholder = "f.eks. Kari Nordmann" +companyName = "Firmanavn (valgfritt)" +companyNamePlaceholder = "f.eks. Acme Inc." +downloadInvoice = "Last ned faktura" +payOnline = "Betal på nett" +poNumber = "PO-nummer (valgfritt)" +poNumberPlaceholder = "f.eks. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avansert" +advancedDesc = "Pipelines driver det meste av behandlingen din." +none = "Ingen" +noneDesc = "Kjører ikke pipelines ennå. Slå dem på når som helst." +standard = "Standard" +standardDesc = "Noen få pipelines behandler ankommende PDF-er på nytt." + +[portal.billing.prepaid.posture] +essentials = "Essentials" +essentialsDesc = "Klassifisering, standarden, pluss deling" +governed = "Styrt" +governedDesc = "Legger til sikkerhet og ruting" +regulated = "Regulert" +regulatedDesc = "Hver kategori, inkl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Last ned tilbud (PDF)" +downloading = "Forbereder…" +share = "del for godkjenning" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "Filer under 25 MB, ingen datakostnader" +heavy = "Tung" +heavyDesc = "Skannet eller bildetung, rutinemessig 50 MB+" +standard = "Standard" +standardDesc = "For det meste små, noen skanninger over 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Brukt opp" +healthy = "Mye igjen" +low = "Begynner å bli lite" [portal.billing.spendLimit] adjustLimit = "Juster grense" @@ -6494,6 +6713,8 @@ label = "Forventes å overskrides." [portal.billing.spendThisMonth] eyebrow = "Forbruk denne måneden" +freeRemaining_one = "{{formatted}} gratis PDF gjenstår" +freeRemaining_other = "{{formatted}} gratis PDF-er gjenstår" processed_one = "{{formattedCount}} PDF behandlet." processed_other = "{{formattedCount}} PDF-er behandlet." processedWithRate_one = "{{formattedCount}} PDF behandlet, til {{rate}} hver." @@ -6510,6 +6731,7 @@ reachedTitle = "Månedlig forbruksgrense nådd" title = "Kunne ikke åpne Stripe-portalen" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "av {{allowance}} gratis PDF-er brukt" capSuffix_other = "av {{allowance}} gratis PDF-er brukt" eyebrow = "Processor-prøveperiode" @@ -6534,6 +6756,376 @@ signature = "signatur" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Ny tilkobling" +createTitleFor = "Koble til {{name}}" +delete = "Slett" +edit = "Rediger" +editTitle = "Rediger tilkobling" +unsupportedType = "Denne tilkoblingstypen kan ikke redigeres her." + +[portal.connections.categories.advanced] +description = "For en tjeneste vi ikke støtter ennå." +label = "Avansert" + +[portal.connections.categories.audit] +description = "Registrer hva som skjedde med et dokument, der revisorene dine allerede ser." +label = "Revisjons- og samsvarslogging" + +[portal.connections.categories.notify] +description = "Fortell et annet system at noe skjedde." +label = "Varsling og arbeidsflyt" + +[portal.connections.categories.security] +description = "Skann eller merk et dokument før det går videre." +label = "Sikkerhet og klassifisering" + +[portal.connections.categories.signing] +description = "Send et dokument ut for signering eller notarisering." +label = "E-signatur og notarisering" + +[portal.connections.categories.storage] +description = "Der behandlede dokumenter ender opp – en bucket, en disk, en sak eller en side." +label = "Fil og legg ved" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling lagrer dette kryptert og viser det aldri igjen." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Trinn som bruker denne tilkoblingen kan bare kalle stier under denne URL-en, aldri en annen host." +label = "Server-URL" + +[portal.connections.commonFields.email] +label = "E-post" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Passord" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Brukernavn" + +[portal.connections.commonFields.webhookUrl] +helperText = "Lim inn URL-en tjenesten ga deg. Bare stier under den kan kalles." +label = "Webhook URL" + +[portal.connections.fields] +name = "Tilkoblingsnavn" +namePlaceholder = "f.eks. Produksjon" + +[portal.connections.picker] +cancel = "Avbryt" +createNew = "Ny tilkobling..." +placeholder = "Velg en tilkobling" +save = "Lagre tilkobling" + +[portal.connections.picker2] +back = "Endre integrasjon" +heading = "Hva vil du koble til?" +noResultsBody = "Vi støtter det ikke ennå – fortell oss om det, så ser vi på å legge det til." +noResultsTitle = "Ingenting matcher \"{{query}}\"" +searchPlaceholder = "Søk i integrasjoner – prøv \"sign\", \"ocr\" eller et produktnavn" + +[portal.connections.types.api] +description = "Kall hvilken som helst HTTP API fra en policy. Du velger URL, autentisering og payload." +label = "Egendefinert API" + +[portal.connections.types.api.fields.authType] +label = "Autentisering" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Brukernavn og passord" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token i en egendefinert header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ingen" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Logg inn for en token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Trinn som bruker denne tilkoblingen kan bare kalle stier under denne URL-en, aldri en annen host." +label = "Base-URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header-navn" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Innloggingssti" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Passord" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Hvis API-et svarer med en nedlastingslenke, kan bare disse hostene hentes. Skill med komma. La stå tomt for kun å tillate base-URL-ens egen host." +label = "Tillatte resulthoster" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Send tokenen som denne headeren" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Responsheaderen innloggingskallet returnerer tokenen i, f.eks. X-Auth-Token." +label = "Token-responsheader" + +[portal.connections.types.api.fields.username] +label = "Brukernavn" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skann dokumenter for skadevare på en server du kjører – ingenting forlater nettverket ditt." +label = "ClamAV (selvhostet)" + +[portal.connections.types.cloudmersive] +description = "Virus- og innholdsskanning for dokumenter i en policy." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokker dokumenter som inneholder makroer, kjørbare filer, skript eller innebygde objekter – ikke bare kjent skadevare." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Legg ved behandlede dokumenter på en Confluence-side." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Send dokumenter til ConsignO Cloud (Notarius) for elektronisk signatur og notarisering." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Ditt ConsignO Cloud-miljø, inkludert /api/v1." +label = "Server-URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Passord" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Trengs bare hvis organisasjonen din bruker leietakere." +label = "Leietaker" + +[portal.connections.types.consigno.fields.username] +label = "Brukernavn" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publiser en melding til Discord når en policy behandler et dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indekser en revisjonshendelse i Elasticsearch hver gang en policy håndterer et dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Spørr en FTP- eller FTPS-server etter nye dokumenter." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Tilkoblingsmodus" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiv" +passive = "Passiv (anbefalt)" + +[portal.connections.types.ftp.fields.security] +helperText = "Vanlig FTP sender legitimasjon og filer ukryptert; velg FTPS hvis serveren støtter det." +label = "Kryptering" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (eksplisitt / AUTH TLS)" +implicit = "FTPS (implisitt)" +none = "Ingen (vanlig FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publiser en melding til et Google Chat-rom når en policy kjører." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Legg ved behandlede dokumenter i en Jira-sak." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Send et dokument eller et varsel på e-post når en policy kjører." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Arkiver behandlede dokumenter i Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App-passord" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Oppdag persondata i dokumenttekst ved hjelp av en Presidio-server du kjører." +label = "Presidio (selvhostet)" + +[portal.connections.types.purview] +description = "Les og bruk Microsoft Purview-sensitivitetsetiketter på PDF-er." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Valgfritt. Trengs bare for å velge etiketter etter navn; merking fungerer uten det." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stemples på hver etikett som dens SiteId, slik at Purview-aware verktøy vet hvilken organisasjon som brukte den." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "En S3 bucket som kilder kan lese fra og pipeline-utdata kan skrive til." +label = "S3-lagring" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "La stå tomt for AWS. Angi dette for MinIO eller en annen S3-kompatibel lagring." +label = "Endepunkt" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Send et dokument eller et varsel på e-post når en policy kjører." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Spørr en SFTP (SSH)-server etter nye dokumenter." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Valgfritt. Serverens SHA-256 host key fingerprint (fra `ssh-keyscan host | ssh-keygen -lf -`). Når angitt, godtas bare en server som presenterer denne nøkkelen; når tomt, festes nøkkelen som sees ved første tilkobling, og senere endringer avvises." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Nøkkelpassfrase" + +[portal.connections.types.sftp.fields.password] +helperText = "La stå tomt hvis du autentiserer med en privat nøkkel i stedet." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Valgfritt. Lim inn en OpenSSH privat nøkkel for å autentisere med en nøkkel i stedet for et passord." +label = "Privat nøkkel" + +[portal.connections.types.slack] +description = "Publiser en melding til Slack når en policy behandler et dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Overvåk en SMB / CIFS-nettverksressurs (Windows eller Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Nettverksressurs" + +[portal.connections.types.smb.fields.domain] +helperText = "Valgfritt. Windows-domenet eller arbeidsgruppen, hvis serveren din bruker en." +label = "Domene" + +[portal.connections.types.smb.fields.share] +label = "Ressurs" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Send en revisjonshendelse til Splunk hver gang en policy håndterer et dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Send en revisjonshendelse til en Sumo Logic HTTP-samler." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publiser en melding til en Teams-kanal når en policy kjører." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publiser hvert dokument, eller bare detaljene, til en URL du velger." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Utløs et Zap- eller Make-scenario fra en policy." +label = "Zapier og Make" + +[portal.docs] +browse = "Bla gjennom dokumentasjon" +viewSource = "Vis kilde på GitHub" + [portal.docs.authentication] codeCaption = "hver forespørsel" eyebrow = "KOM I GANG" @@ -6624,6 +7216,9 @@ eyebrow = "FERDIGHETER" lead = "Samlede, navngitte kapabiliteter agenten din påkaller som ett enkelt verktøy. Hver ferdighet er en deterministisk operasjonskjede med evalueringer vedlagt." title = "Agentferdigheter" +[portal.docs.toc] +title = "På denne siden" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API-REFERANSE" @@ -6697,6 +7292,8 @@ type = "Type" user = "Bruker" [portal.documents.queue.empty] +connectSource = "Koble til en kilde" +createPipeline = "Opprett en pipeline" description = "Når kilder sender dokumenter inn i dine pipelines, vises de her for gjennomgang." title = "Ingen dokumenter i køen" @@ -6712,11 +7309,11 @@ editorAction = "Redigerer" empty = "Ingen dokumenter samsvarer med dette filteret." rowActions = "Radhandlinger" sensitiveLabel = "Sensitivt" -sensitiveTitle = "Sensitivt — tilgang kreves" [portal.documents.table.columns] action = "Pipeline / handling" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Status" time = "Tid" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Sist sett" region = "Region" status = "Status" +target = "Target" version = "Versjon" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Denne visningen traff en uventet feil. Prøv igjen, eller velg en retry = "Prøv igjen" title = "Noe gikk galt på denne siden" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Tilbake" +body = "Last ned skrivebordsappen eller selvhost serveren." +done = "Ferdig" +guide = "Åpne {{name}}-veiledningen" +openInBrowser = "Åpne i nettleseren i stedet" +sectionDesktop = "Skrivebordsapp" +sectionSelfHosted = "Selvhostet" +title = "Installer Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Kjør det offisielle imaget på serveren din." +note = "Åpnes på :8080. Standard innlogging: admin / stirling." +tagline = "Kjør serveren i en container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installer det offisielle Helm chartet." +note = "Legg til en PVC for /configs for å bevare innstillingene." +tagline = "Distribuer til en klynge med Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Native bygg for distribusjonen din." +guideBtn = "Vis installasjonsalternativer for Linux" +note = "AppImage-, .deb- og .rpm-bygg er tilgjengelige for vanlige distroer." +tagline = "Native skrivebordsapp for distroen din" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Eller via Homebrew" +detailBody = "Kjør installasjonsprogrammet, eller bruk Homebrew." +downloadBtn = "Last ned for macOS" +tagline = "Native app for Apple silicon og Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Kjør .jar på en bare metal-server." +downloadBtn = "Last ned .jar" +note = "Krever Java 21+. Åpnes på :8080; standard innlogging admin / stirling." +runLabel = "Kjør den deretter" +tagline = "Kjør .jar på en bare metal-server" +title = "Manuelt serveroppsett" + +[portal.home.download.windows] +altLabel = "Eller via winget" +detailBody = "Kjør installasjonsprogrammet, eller bruk en pakkebehandler." +downloadBtn = "Last ned installasjonsprogram" +tagline = "Native app for Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktive" -invite = "Inviter lagkamerater" +activeOfDeployed = "{{active}} av {{total}} aktive denne måneden" name = "Stirling PDF Editor" open = "Åpne i nettleser" updated = "oppdatert {{time}}" +[portal.home.editor.deploy] +finish = "Fullfør distribusjon" +options = "Vis distribusjonsalternativer" +start = "Distribuer Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Selvhostet · Docker" @@ -6823,49 +7529,6 @@ afternoon = "God ettermiddag" evening = "God kveld" morning = "God morgen" -[portal.home.onboarding] -dismiss = "Avvis oppsett" -notStarted = "Ikke startet" -progress = "{{done}} av {{total}} fullført" -title = "Fullfør oppsettet" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC for hele organisasjonen, forpliktende volumprising og isolert distribusjon." -cta = "Start prøveperiode" -ctaQuote = "Få tilbud" -lead = "For 250+ ansatte." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Distribuer skrivebordsappen, gratis for alltid." -title = "Last ned PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktive · {{recommended}} anbefalt" -chip = "{{active}} aktive" -title = "Slå på policyer" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} tilkoblet · hver PDF styres der den lander" -chip = "{{connected}} tilkoblet" -title = "Koble til kildene dine" - -[portal.home.quickActions] -subtitle = "Viktigste oppgaver for i dag" -title = "Hurtighandlinger" - -[portal.home.quickActions.buildPipeline] -blurb = "3-trinns komponist over det typede operasjonsbiblioteket" -title = "Bygg en pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenter, webhooks, overvåkede mapper" -title = "Koble til en kilde" - -[portal.home.quickActions.issueApiKey] -blurb = "Avgrenset nøkkel med hastighetsgrenser og IP-tillatelsesliste" -title = "Utsted en API-nøkkel" - [portal.infrastructure] manageEditorDeployment = "Administrer Editor-distribusjon" sectionsAriaLabel = "Infrastrukturseksjoner" @@ -6878,13 +7541,9 @@ heading = "API-nøkler" subheading = "Avgrenset legitimasjon med hastighetsgrenser per nøkkel, tillatelser og IP-tillatelseslister." [portal.infrastructure.apiKeys.card] -allowedIps = "Tillatte IP-er" -anyIp = "Enhver IP (ingen tillatelsesliste)" created = "Opprettet" lastUsed = "Sist brukt" -permissions = "Tillatelser" -rateLimit = "Hastighetsgrense" -rateLimitValue = "{{value}} foresp./min" +revoke = "Tilbakekall nøkkel" usageMonth = "Bruk denne måneden" usageToday = "Bruk i dag" @@ -6892,6 +7551,15 @@ usageToday = "Bruk i dag" description = "Opprett en avgrenset nøkkel for å begynne å kalle Stirling API." title = "Ingen API-nøkler ennå" +[portal.infrastructure.apiKeys.error] +load = "Kunne ikke laste API-nøklene dine. Prøv igjen." + +[portal.infrastructure.apiKeys.revoke] +body = "Tilbakekall \"{{name}}\"? Integrasjoner som fortsatt bruker denne nøkkelen, vil umiddelbart begynne å motta 401-feil. Dette kan ikke angres." +cancel = "Avbryt" +confirm = "Tilbakekall nøkkel" +title = "Tilbakekall API-nøkkel" + [portal.infrastructure.audit] filterAriaLabel = "Filtrer revisjonshendelser etter kategori" heading = "Revisjonslogger" @@ -6935,6 +7603,7 @@ all = "Alle" auth = "Autentisering" config = "Konfigurasjon" elevation = "Tilgangsøkning" +policy = "Retningslinje" processing = "Behandling" security = "Sikkerhet" @@ -6944,19 +7613,30 @@ title = "Du har ikke tilgang" [portal.infrastructure.audit.metrics] config = "Konfigurasjon" -elevation = "Tilgangsøkning" +policy = "Retningslinje" processing = "Behandling" totalEvents = "Totalt antall hendelser · 24 t" +[portal.infrastructure.auditCatLabel] +auth = "Autentisering" +config = "Konfigurasjon" +elevation = "Heving" +policy = "Retningslinje" +processing = "Behandling" +security = "Sikkerhet" + +[portal.infrastructure.auditStatusLabel] +danger = "Feil" +info = "Info" +success = "Vellykket" +warning = "Advarsel" + [portal.infrastructure.createKey] cancel = "Avbryt" createKey = "Opprett nøkkel" done = "Ferdig" -ipAllowlistHelper = "Kommaseparerte CIDR-områder. La stå tomt for å tillate enhver IP." -ipAllowlistLabel = "IP-tillatelsesliste" keyNameLabel = "Nøkkelnavn" keyNamePlaceholder = "f.eks. Produksjon · inntak" -permissionsLabel = "Tillatelser" secretKeyCaption = "Hemmelig nøkkel" secretWarning = "Lagre dette i en hemmelighetsbehandler. Stirling lagrer bare en hash — det finnes ingen måte å gjenopprette den senere." subtitle = "Avgrens nøkkelen til det minste den trenger. Du kan rotere eller tilbakekalle når som helst." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopier denne hemmeligheten nå — den vises ikke igjen." title = "Opprett API-nøkkel" titleCreated = "Nøkkel opprettet" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Distribuert av" -environment = "Miljø" -product = "Produkt" -status = "Status" -version = "Versjon" -when = "Når" - -[portal.infrastructure.deployments.recent] -heading = "Nylige distribusjoner" -subheading = "De nyeste utrullingene på tvers av produkter og miljøer." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanser" -latency = "Latens" -load = "Belastning" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Gjennomstrømning" -uptime = "Oppetid" -version = "Versjon" - -[portal.infrastructure.deployments.regions] -heading = "Regioner" -subheading = "Sanntidshelse for hver distribuerte Stirling-region — latens, belastning og utrullingsversjon." - -[portal.infrastructure.deployments.regions.empty] -description = "Distribuerte regioner vises her når arbeidsområdet ditt er klargjort." -title = "Ingen regioner distribuert" - -[portal.infrastructure.models] -heading = "Modeller" -msValue = "{{value}} ms" -subheading = "Modellkatalogen og rutingen som driver dokumentbehandling på tvers av arbeidsområdet ditt." - -[portal.infrastructure.models.byom] -description = "Registrer en lokal eller selvhostet modell og fest den til en region for behandling bundet til datalokalitet." -title = "Ta med din egen modell" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Administrerte modeller tilgjengelige for arbeidsområdet ditt, med sanntidslatens og kostnad." -subEnterprise = "Administrerte, egne og lokale modeller — med festing per region tilgjengelig." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeller i katalogen til arbeidsområdet ditt vises her." -title = "Ingen modeller tilgjengelige" - -[portal.infrastructure.models.columns] -cost = "Kostnad" -latency = "Latens" -load = "Belastning" -model = "Modell" -status = "Status" -type = "Type" -version = "Versjon" - -[portal.infrastructure.models.metrics] -activeModels = "Aktive modeller" -avgLatency = "Gj.sn. latens" -included = "Inkludert" -monthlySpend = "Månedlige modellkostnader" - -[portal.infrastructure.models.routing] -empty = "Ingen rutingsregler konfigurert." -heading = "Rutingsregler" -sub = "Hvilken modell som håndterer hver operasjon. Standarden brukes når ingen smalere regel samsvarer." -subLocked = "Rut operasjoner til spesifikke modeller — tilgjengelig med betalte planer." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Oppgrader til Pro for å kontrollere hvilken modell som håndterer hver operasjon og dokumenttype." -title = "Modellruting er en betalt funksjon" - -[portal.infrastructure.models.routingColumns] -default = "Standard" -docType = "Dokumenttype" -modelForAria = "Modell for {{operation}}" -operation = "Operasjon" -routedTo = "Rutet til" - -[portal.infrastructure.security.access.byok] -description = "Oppgi en nøkkel fra din egen KMS. Stirling krypterer med den, men kan fortsatt lese." -label = "Ta med din egen nøkkel (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Nøkler forlater aldri din KMS. Stirling holder bare chiffertekst." -label = "Hold din egen nøkkel (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling administrerer krypteringsnøkler. Enklest — null nøkkeldrift på din side." -label = "Nøkler holdt av Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Policy for dokumenttilgang" -subheading = "Kontrollerer hvem som kan dekryptere behandlede dokumenter i ro." - -[portal.infrastructure.security.attestations] -heading = "Samsvarsattestasjoner" -noReport = "Ingen rapport tilgjengelig" -subheading = "Revisjonsstilling rammeverk for rammeverk, med rapporter tilgjengelige for attesterte kontroller." -viewReport = "Vis rapport →" - -[portal.infrastructure.security.compliance] -heading = "Samsvar" -subheading = "Attestasjoner og sertifiseringer som dekker Stirling-plattformen." - -[portal.infrastructure.security.empty] -description = "Sikkerhetskonfigurasjonen for arbeidsområdet ditt vises her." -title = "Sikkerhetsstilling utilgjengelig" - -[portal.infrastructure.security.hyokBanner] -description = "Med HYOK forlater krypteringsnøkler aldri din KMS. Stirling lagrer og behandler bare chiffertekst som du kan tilbakekalle når som helst." -title = "Stirling kan ikke dekryptere dokumentene dine" - -[portal.infrastructure.security.ipAllowlist] -empty = "Ingen IP-områder konfigurert — alle IP-er tillatt." -heading = "IP-tillatelsesliste" -sub = "API-tilgang er begrenset til disse CIDR-områdene." -subLocked = "Begrens API-tilgang til kjente IP-områder — tilgjengelig med betalte planer." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Oppgrader til Pro for å begrense API-tilgang til spesifikke nettverk." -title = "IP-tillatelseslisting er en betalt funksjon" - -[portal.infrastructure.security.ipColumns] -added = "Lagt til" -addedBy = "Lagt til av" -cidr = "CIDR" -label = "Etikett" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritme" -heading = "Administrasjon av krypteringsnøkler" -keyId = "Nøkkelidentifikator" -lastRotated = "Sist rotert" -rotateKey = "Roter nøkkel" -rotationPolicy = "Rotasjonspolicy" -subheading = "Forvaring av nøklene som krypterer dokumenter i ro — hvem som kan dekryptere, og hvordan nøkler roteres." - -[portal.infrastructure.security.managedBanner] -description = "Forvaring med ta-med-din-egen-nøkkel (BYOK) og hold-din-egen-nøkkel (HYOK) er tilgjengelig på Enterprise. Oppgrader for å oppgi nøkler fra din egen KMS." -title = "Nøkler administreres av Stirling på planen din" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia-Stillehavet" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR-datagrense" -label = "Den europeiske union" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "USA" - -[portal.infrastructure.security.residencyHeader] -heading = "Datalokalitet" -subheading = "Hvor dokumenter lagres og behandles." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} brukt" - -[portal.infrastructure.storage.empty] -description = "Tilkoblet lagring og bruk vises her." -title = "Ingen lagring konfigurert" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktiv" -activeRange = "0–{{value}}d" -archived = "Arkivert" -coldStorage = "kaldlagring" -deleted = "Slettet" -never = "aldri" -purged = "slettet permanent" - -[portal.infrastructure.storage.providers] -connect = "Koble til" -connected = "Tilkoblet" -heading = "Tilkoblede leverandører" -subheading = "Hvor behandlede artefakter skrives." - -[portal.infrastructure.storage.retention] -heading = "Oppbevaring" -subheading = "Hvor lenge artefakter beholdes før livssyklussletting." -windowLabel = "Standard oppbevaringsvindu" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dag" -days_other = "{{count}} dager" -never = "Slett aldri" - -[portal.infrastructure.storage.totalUsage] -heading = "Total bruk" -progressLabel = "Lagring brukt" -subheading = "Lagring brukt på tvers av alle tilkoblede leverandører." +revoked = "Tilbakekalt" [portal.infrastructure.tabs] apiKeys = "API-nøkler" @@ -7174,6 +7656,49 @@ models = "Modeller" security = "Sikkerhet" storage = "Lagring" +[portal.integrations] +availableHeading = "Tilgjengelig" +comingSoonHeading = "Kommer snart" +connect = "Koble til" +connectionCount_one = "{{count}} tilkobling" +connectionCount_other = "{{count}} tilkoblinger" +customApi = "Tilpasset API" +searchPlaceholder = "Søk i integrasjoner" +subtitle = "Alt Stirling kan snakke med. Legitimasjon opprettes én gang her, og refereres deretter av kilder, retningslinjer og pipelines." +title = "Integrasjoner" +typedName = "{{tool}}-navn" + +[portal.integrations.filters] +all = "Alle" +audit = "Revisjon" +connected = "Tilkoblet" +notify = "Varsle" +security = "Sikkerhet" +signing = "Signering" +storage = "Lagring" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Tilkoblet" + +[portal.integrations.table] +integration = "Integrasjon" +worksWith = "Fungerer med" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Retningslinjer" +sources = "Kilder" + +[portal.legal] +draft = "{{label}} · utkast" +loadError = "Kunne ikke laste dette dokumentet. Prøv igjen." +loading = "Laster…" +title = "Juridisk dokument" + [portal.nav] agent-builder = "Agentbygger" components = "Komponenter" @@ -7182,33 +7707,17 @@ documents = "Dokumenter" editor = "Redigerer" home = "Hjem" infrastructure = "Infrastruktur" +integrations = "Integrasjoner" pipelines = "Pipelines" policies = "Policyer" procurement = "Innkjøp" -settings = "Innstillinger" sources = "Kilder" usage = "Bruk og fakturering" users = "Brukere" -[portal.notifications] -markAllRead = "Merk alle som lest" -title = "Varsler" -viewAll = "Vis alle" - -[portal.notifications.ariaLabel] -none = "Varsler, ingen uleste" -unread_one = "Varsler, {{count}} ulest" -unread_other = "Varsler, {{count}} uleste" - -[portal.notifications.count] -allRead = "alle lest" -loading = "laster" -new_one = "{{count}} nytt" -new_other = "{{count}} nye" - -[portal.notifications.empty] -description = "Ingen nye varsler." -title = "Du er helt ajour" +[portal.nav.section] +platform = "PDF-plattform" +processor = "PDF-prosessor" [portal.pipelines] subtitle = "Hver automatiserte dokumentpipeline på backend: en ordnet kjede av operasjoner over et sett med kilder, kjørt på en utløser. Klikk på en rad for å se trinnene og kildene." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Ny pipeline" [portal.pipelines.builder] -addStep = "Legg til verktøy" +activate = "Activate" back = "Tilbake til pipelines" +cannotFollow = "Kan ikke ta {{produced}}" +chooseAccount = "Velg en konto" +chooseDestination = "Velg en destinasjon" +chooseOperation = "Velg hva dette trinnet gjør" +chooseSource = "Velg en kilde" discard = "Forkast endringer" -enabled = "Aktivert" +inputs = "Inndata" +inputSource = "Inndatakilde" +inputTrigger = "Utløser" keepEditing = "Fortsett å redigere" -needsUpload = "Trenger en opplastet fil" +moreActions = "More actions" +needsConfiguring = "Må settes opp" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Ingen verktøy samsvarer med søket ditt." -pipelineSettings = "Pipeline-innstillinger" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Søk i verktøy" -selectToolBody = "Legg til et verktøy for å bygge pipelinen din." -selectToolTitle = "Ingen verktøy ennå" -toolSettings = "Verktøyinnstillinger" +sendToSystem = "Send til et annet system" +stepsIncompatible = "Disse trinnene kan ikke kjøres på det forrige trinnet produserer: {{tools}}." +stepsNeedSetup = "Disse trinnene må fortsatt settes opp før lagring: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Ukjent operasjon, beholdt som den er." unsavedBody = "Du har ulagrede endringer. Lagre dem før du forlater, eller forkast dem?" unsavedTitle = "Ulagrede endringer" -uploadUnsupported = "Opplastede filer støttes ikke i pipelines ennå, så disse trinnene kan ikke lagres: {{tools}}." usesDefaults = "Kjører med standardinnstillinger" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinerer hver fil fra forrige trinn" +fan-out = "Kjører én gang per fil fra forrige trinn" +format-mismatch = "Trenger {{accepts}}, men forrige trinn produserer {{produced}}" +output-uncertain = "Kjører kanskje ikke: utdata fra forrige trinn avhenger av hvordan det er satt opp" +source-mismatch = "Trenger {{accepts}}, men inndata for denne pipelinen er {{produced}}" +undeclared-operation = "Kan ikke sjekke hva dette trinnet godtar" [portal.pipelines.composer] addTool = "Legg til verktøy" -cancel = "Avbryt" -chainEmpty = "Legg til et verktøy for å begynne å bygge pipelinen din." create = "Opprett pipeline" -directory = "Utdatamappe" -directoryHelp = "Absolutt sti på serveren. Må være innenfor de konfigurerte tillatte mappene." +createPaused = "Create paused" editingUnsupported = "Visning av disse verktøyparametrene for redigering støttes ikke ennå." -moveDown = "Flytt ned" -moveUp = "Flytt opp" +editSource = "Edit source" name = "Navn" namePlaceholder = "f.eks. Redigeringsgjennomgang" -noSources = "Ingen kilder tilkoblet ennå. Pipelinen kan fortsatt kjøre på filer som leveres direkte til den." noToolSettings = "Dette verktøyet har ingen konfigurerbare innstillinger." -operations_one = "Operasjon ({{count}})" -operations_other = "Operasjoner ({{count}})" output = "Utdata" -removeStep = "Fjern operasjon" save = "Lagre endringer" scheduleEvery = "Kjør hver" -sources = "Kilder" -sourcesLoading = "Laster kilder..." trigger = "Utløser" triggerManual = "Kun manuelt" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dager" hours = "timer" minutes = "minutter" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Slette \"{{name}}\"? Dette kan ikke angres." cancel = "Avbryt" @@ -7273,28 +7815,62 @@ confirm = "Slett" title = "Slette pipeline?" [portal.pipelines.detail] +clearHistory = "Tøm historikk" delete = "Slett pipeline" run = "Kjør nå" [portal.pipelines.empty] action = "Opprett en pipeline" +connectSource = "Koble til en kilde" description = "Opprett din første pipeline: velg kildene den kjører over, kjed operasjonene, og velg hvor utdata skal gå." title = "Ingen pipelines ennå" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktive" paused = "Satt på pause" total = "Pipelines" -[portal.pipelines.output] -folder = "Skriv til mappe" -inline = "Returner filer" - [portal.pipelines.run] +allProcessed_one = "Ingenting å kjøre: kildens {{count}} dokument er allerede behandlet." +allProcessed_other = "Ingenting å kjøre: alle {{count}} dokumenter i kildene er allerede behandlet." completed_one = "Kjøring fullført." completed_other = "Alle {{count}} kjøringer fullført." empty = "Ingenting å kjøre: kildene hadde ingen dokumenter å behandle." failed = "Kjøring mislyktes: {{error}}" +inFlight = "Ingenting nytt å kjøre: dokumenter behandles fortsatt fra en tidligere kjøring." +parked_one = "Ingenting å kjøre: {{count}} dokument feilet tidligere og er parkert. Rett årsaken, og tøm deretter historikken for å prøve på nytt." +parked_other = "Ingenting å kjøre: {{count}} dokumenter feilet tidligere og er parkert. Rett årsaken, og tøm deretter historikken for å prøve dem på nytt." running = "Kjøring startet; fortsatt pågår." timeout = "Kjøringen tar lengre tid enn forventet; den kan fortsatt fullføres i bakgrunnen." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Kilder" status = "Status" steps = "Trinn" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Mappeovervåking" @@ -7314,12 +7891,18 @@ manual = "Manuell" schedule = "Planlagt" [portal.policies] +defaultName = "{{category}}-retningslinje" subtitle = "Stående automatiseringer som håndhever en verktøypipeline på hvert dokument. Hver policy utløses ved opplasting eller eksport, kjører verktøykjeden sin, og lagrer den håndhevede versjonen ved siden av originalen." title = "Policyer" [portal.policies.card] comingSoon = "Oppgrader til Enterprise" notSetUp = "Ikke satt opp" +requiresAiEngine = "Krever AI-motor" + +[portal.policies.categories.classification] +desc = "Identifiser hvert dokuments type ved opplasting og tagg metadataene for arkivering og søk." +label = "Klassifisering" [portal.policies.categories.compliance] desc = "Håndhev HIPAA-, GDPR-, SOC 2- eller FedRAMP-krav på hvert dokument." @@ -7344,6 +7927,13 @@ label = "Sikkerhet" [portal.policies.config] scopeAll = "Alle dokumenter" +[portal.policies.config.classification] +summary = "Klassifiserer hvert opplastede dokument mot teamets etiketter og tagger metadataene." + +[portal.policies.config.classification.rules] +0 = "Klassifiser" +1 = "Tagg metadata" + [portal.policies.config.compliance] summary = "Validerer dokumenter mot regulatoriske rammeverk før de forlater systemet." @@ -7371,6 +7961,29 @@ minConfidence = "Min. konfidens" 2 = "Navngi" 3 = "Normaliser" +[portal.policies.config.purview] +label = "Bruk en Microsoft Purview-sensitivitetsetikett" + +[portal.policies.config.purview.fields] +connection = "Purview-leier" +labelId = "Etikett-ID" +labelIdHelp = "Etikettens GUID, fra Purview-portalen. Å bruke en etikett skriver metadataene til PDF-en; den kan ikke kryptere, noe som krever Microsoft-klienten." +labelName = "Etikettnavn (valgfritt)" +labelNameHelp = "Registreres sammen med ID-en slik at personer og verktøy kan gjenkjenne etiketten." + +[portal.policies.config.purview.fields.method] +help = "Purview skiller mellom en etikett som en retningslinje brukte, og en som en person valgte." +label = "Hvordan etiketten registreres" +privileged = "Som valgt av en person" +standard = "Som brukt automatisk" + +[portal.policies.config.purviewRead] +label = "Les en Microsoft Purview-etikett" + +[portal.policies.config.purviewRead.fields] +connection = "Purview-leier" +connectionHelp = "Leser etiketten dokumentet allerede har, slik at senere trinn kan handle på den." + [portal.policies.config.retention] summary = "Håndhever hvor lenge dokumenter beholdes, når de skal arkiveres, og når de skal slettes." @@ -7438,6 +8051,7 @@ title = "Ingen aktivitet ennå" [portal.policies.endpoints] addWatermark = "Vannmerke" autoRedact = "Sladd personopplysninger" +classifyAndLabel = "Klassifiser" compressPdf = "Komprimer" flatten = "Flat ut" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Policyene dine er lagret og vises når tilkoblingen er gjenoppret retry = "Prøv igjen" title = "Backend utilgjengelig" +[portal.policies.operations] +change = "Endre hva dette trinnet gjør" +noResults = "Ingen trinn samsvarer med det. Prøv et produktnavn, eller \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Søk i trinn – prøv \"scan\", \"attach\" eller et produktnavn" + +[portal.policies.operations.bodyMode] +binary = "Rå filbytes" +json = "Inne i en JSON-nyttelast" +multipart = "Som en filopplasting (multipart)" + +[portal.policies.operations.clamavScan] +description = "Din egen ClamAV-server sjekker dokumentet; ingenting forlater nettverket ditt." +label = "Skann etter virus (selvhostet)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Avviser dokumenter som inneholder makroer, kjørbare filer, skript eller innebygde objekter." +label = "Blokker aktivt innhold" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive sjekker dokumentet, og kjøringen stopper hvis det ikke er rent." +label = "Skann etter virus" + +[portal.policies.operations.confluenceAttach] +description = "Arkiverer det behandlede dokumentet på en side." +label = "Legg ved på en Confluence-side" + +[portal.policies.operations.consignoSubmit] +description = "Sender dokumentet til ConsignO Cloud for signering." +label = "Send til signering (ConsignO)" +note = "Sender dokumentet. Henting av den signerte kopien støttes ikke ennå, fordi et trinn ikke kan sende et resultat til et senere trinn." + +[portal.policies.operations.customApiCall] +description = "Lag kallet selv: sti, metode, brødtekst og svarhåndtering." +label = "Kall en tilpasset API" + +[portal.policies.operations.discordNotify] +description = "Forteller en kanal at retningslinjen håndterte et dokument." +label = "Publiser en melding til Discord" + +[portal.policies.operations.elasticIndex] +description = "Registrerer hva retningslinjen gjorde som et søkbart dokument." +label = "Indekser en revisjonshendelse i Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart passer for opplastings-API-er; JSON passer for API-er som vil ha filen inne i en nyttelast; binary sender råbytes." +label = "Hvordan dokumentet skal sendes" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "En JSON-brødtekst som sendes som den er. {{document.base64}} inneholder filen; {{document.*}} og {{run.*}} fylles ut per dokument." +label = "JSON-brødtekst" + +[portal.policies.operations.fields.connection] +label = "Konto" + +[portal.policies.operations.fields.domain] +label = "Mailgun-domene" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Skjemafeltet API-et forventer at dokumentet ligger under." +label = "Navn på filfelt" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Fra" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Et JSON-objekt med par av headernavn/-verdier. Tilkoblingens legitimasjon legges til for deg." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Saksnøkkel" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Sendes som meldingsteksten. {{document.*}} og {{run.*}} fylles ut per dokument." +label = "Melding" + +[portal.policies.operations.fields.method] +label = "Metode" + +[portal.policies.operations.fields.pageId] +label = "Side-ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Legges til tilkoblingens base-URL. Den kan aldri nå en annen vert." +label = "Sti" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Hvor i disken den skal skrives. {{document.filename}} fylles ut per dokument." +label = "Destinasjonssti" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapport lar dokumentet være urørt. Erstatt bytter det ut med det som kommer tilbake." +label = "Hva som skal gjøres med svaret" + +[portal.policies.operations.fields.signerEmail] +label = "Signatarens e-post" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Emne" +placeholder = "Behandlet: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyserer denne teksten. Bruk {{document.title}} eller lim inn teksten du vil sjekke." +label = "Tekst som skal sjekkes" + +[portal.policies.operations.fields.to] +label = "Til" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud-brukernavn" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Forteller et område at retningslinjen håndterte et dokument." +label = "Publiser en melding til Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arkiverer det behandlede dokumentet på en sak." +label = "Legg ved i en Jira-sak" + +[portal.policies.operations.mailgunEmail] +description = "Sender det behandlede dokumentet som et vedlegg." +label = "Send dokumentet på e-post (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Skriver det behandlede dokumentet til en mappe." +label = "Last opp til Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Din egen Presidio-server rapporterer personopplysningene den finner." +label = "Oppdag personopplysninger" +note = "Presidio leser tekst, ikke filer. Gi den teksten du vil sjekke — selve dokumentet lastes ikke opp." + +[portal.policies.operations.responseMode] +replace = "Erstatt dokumentet med svaret" +report = "Behold dokumentet, registrer svaret" + +[portal.policies.operations.sendgridEmail] +description = "Sender det behandlede dokumentet som et vedlegg." +label = "Send dokumentet på e-post (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Forteller en kanal at retningslinjen håndterte et dokument." +label = "Publiser en melding til Slack" + +[portal.policies.operations.splunkEvent] +description = "Registrerer hva retningslinjen gjorde, der revisorene dine allerede ser." +label = "Send en revisjonshendelse til Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registrerer hva retningslinjen gjorde til en HTTP-innsamler." +label = "Send en revisjonshendelse til Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Forteller en kanal at retningslinjen håndterte et dokument." +label = "Publiser en melding til Teams" + +[portal.policies.operations.webhookPost] +description = "Sender selve dokumentet til en URL du velger." +label = "Post dokumentet til en webhook" + +[portal.policies.operations.zapierNotify] +description = "Overleverer kjøringens detaljer til automatiseringen din." +label = "Utløs en Zap- eller Make-scenario" + [portal.policies.stats] activeFor = "Aktiv" dataProcessed = "Data behandlet" @@ -7473,6 +8282,13 @@ label = "Dokumenter håndhevet" description = "Konfigurert, men kjører ikke" label = "Satt på pause" +[portal.policies.table] +appliesTo = "Gjelder for" +docs = "Dokumenter håndhevet" +enforces = "Håndhever" +policy = "Retningslinje" +status = "Status" + [portal.policies.wizard.actions] back = "Tilbake" cancel = "Avbryt" @@ -7480,6 +8296,10 @@ continue = "Fortsett" enablePolicy = "Aktiver policy" saveChanges = "Lagre endringer" +[portal.policies.wizard.capability.classify] +desc = "Identifiserer dokumentets type fra teamets etiketter og tagger det, slik at det arkiveres og søkes etter kategori." +label = "Klassifiser dokumentet" + [portal.policies.wizard.capability.compress] desc = "Komprimerer dokumentet til en mindre filstørrelse." label = "Reduser filstørrelse" @@ -7500,10 +8320,18 @@ label = "Sladd sensitiv informasjon" desc = "Fjerner skjult JavaScript slik at ingenting kan kjøre automatisk når dokumentet åpnes." label = "Fjern aktivt innhold" +[portal.policies.wizard.capability.timestampPdf] +desc = "Beviser at dokumentet eksisterte i nøyaktig denne formen på et tidspunkt, ved hjelp av en uavhengig tidsstempelmyndighet. Bare en hash sendes – dokumentet forlater aldri serveren din." +label = "Legg til et betrodd tidsstempel" + [portal.policies.wizard.capability.watermark] desc = "Stempler et synlig merke (f.eks. «Konfidensielt») over hver side." label = "Bruk et vannmerke" +[portal.policies.wizard.classification] +description = "Hvert opplastede dokument klassifiseres mot de innebygde etikettene og tagges med typene som passer. Etikettsettet deles på tvers av hele teamet ditt." +labelsHeading = "Klassifiseringsetiketter" + [portal.policies.wizard.errors] noTools = "Aktiver minst ett verktøy i arbeidsflyten først." saveFailed = "Kunne ikke lagre policyen. Prøv igjen." @@ -7548,73 +8376,118 @@ setUp = "Sett opp {{category}}-policy" [portal.policies.wizard.workflow] description = "Velg hva denne policyen gjør med hvert dokument den behandler." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktive" -noRule = "Ingen regel håndhevet ennå" -subtitle = "Faste automatiseringer som hvert dokument går gjennom, uansett hvilken pipeline som håndterer det." -title = "Hva som kjøres på PDF-ene dine" - [portal.policySummary.action] -comingSoon = "Oppgrader til Enterprise" -configure = "Konfigurer" setUp = "Sett opp" -[portal.policySummary.column] -activeRule = "Aktiv regel" -policy = "Policy" -status = "Status" +[portal.processorFlow] +footnote = "Antall gjelder de siste 24 timene. Flythastigheten er illustrativ." +liveBadge = "Live" +stats = "{{connected}} tilkoblet · {{processed}} PDF-er behandlet" +title = "PDF-prosessor" -[portal.policySummary.empty] -description = "Når policyer er konfigurert, vises kategoriene her." -title = "Ingen policyer ennå" +[portal.processorFlow.lens] +ariaLabel = "Visningsmodus" +flow = "Flyt" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktiv" -off = "Av" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 t" +failed = "Mislyktes" +heading = "Resultater" +success = "Levert" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktive" +count = "{{n}} · 24 t" +heading = "Retningslinjer" +setUp = "Sett opp" soon = "Snart" -[portal.processingStatus] -managePlan = "Administrer abonnement" -volumeSuffix = "PDF-er behandlet · siste 30 dager" +[portal.processorFlow.sankey] +waist = "Retningslinjer · {{n}} aktive" + +[portal.processorFlow.sankey.empty] +description = "Koble til en kilde og slå på en retningslinje for å se dokumenter flyte gjennom prosessoren." +title = "Ingen flyt ennå" + +[portal.processorFlow.sources] +comingSoonTag = "Koble til" +editor = "Stirling PDF Editor" +heading = "Kilder" +perDay = "{{n}} / 24 t" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-postinntak" [portal.procurement] -reset = "Tilbakestill innkjøp (demo)" subtitle = "Få teamet evaluert, kontraktsfestet og onboardet. Alle dokumenter på ett sted." title = "Innkjøp" -[portal.procurement.action] -download = "Last ned" -pay = "Betal nå" -request = "Be om" -sign = "Se gjennom og signer" -upload = "Last opp" - [portal.procurement.agreement] agreeCta = "Godta og abonner" +confidential = "Konfidensielt" confirm = "Jeg har lest og godtar Stirling Enterprise Agreement." -eyebrow = "Avtale" -intro = "Én samlet avtale dekker avtalen din: Master Service Agreement, bestillingsskjema, EULA og databehandleravtale. Se gjennom den, og godta deretter for å akseptere tilbudet som et forpliktende abonnement." -title = "Se gjennom enterprise-avtalen din" +docName = "Stirling Enterprise-avtale" +docSub = "MSA, bestillingsskjema, EULA og DPA, kombinert i én signatur." +download = "Last ned" +downloadDraftError = "Kunne ikke generere avtale-PDF-en – dokumentgjengiveren er utilgjengelig. Prøv igjen om litt eller kontakt kundestøtte." +downloadError = "Kunne ikke laste ned den signerte avtalen. Prøv igjen." +legalName = "Juridisk enhetsnavn" +legalNamePlaceholder = "Den juridiske enheten som skal signere" +loadError = "Kunne ikke laste avtalen. Prøv igjen." +loading = "Laster avtalen..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Be om endringer" +scrollHint = "Rull for å gjennomgå" +signatory = "Signatarnavn" +signatoryPlaceholder = "Fullt navn" +signatoryTitle = "Signatartittel" +signatoryTitlePlaceholder = "f.eks. juridisk direktør" +signError = "Kunne ikke registrere signaturen din. Prøv igjen." [portal.procurement.builder] addons = "Tillegg" +addressLine1 = "Adresselinje 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresselinje 2" +addressLine2Placeholder = "Suite, etasje, bygning" back = "Tilbake" businessName = "Bedriftsnavn" businessNamePlaceholder = "Bedriften din" +city = "By" +cityPlaceholder = "San Francisco" +completeRequired = "Fyll ut de obligatoriske feltene (merket *) med en gyldig e-post før du genererer tilbudet." +contactEmail = "Kontakt-e-post" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktnavn" +contactNamePlaceholder = "Jane Doe" continue = "Fortsett" -country = "Land" -countryEuro = "Eurosonen (EUR €)" -countryUK = "Storbritannia (GBP £)" -countryUS = "USA (USD $)" +done = "Ferdig" eula = "Jeg har lest og godtar Stirling Enterprise EULA. Den styrer avtalen som genereres fra dette tilbudet." generate = "Generer tilbud" -included = "Inkludert" indemnification = "IP-skadesløsholdelse" indemnificationSub = "Vi forsvarer kvalifiserende IP-krav, i henhold til EULA" -offlineLicense = "Offline / air-gapped-lisens" -offlineLicenseSub = "En nedlastbar lisensfil for en air-gapped, selvdriftet instans" +paperEyebrow = "Enterprise-tilbud" +paperFor = "Utarbeidet for" +pdfSize = "PDF-størrelse" +poNumber = "PO-nummer" +poNumberPlaceholder = "Valgfritt" +postalCode = "Postnummer" +postalCodePlaceholder = "94105" +posture = "Styring" +posture_count = "~{{count}} retningslinjer" +posture_essentials = "Essensielt" +posture_essentialsSub = "Klassifisering + deling — standardinnstillingene" +posture_governed = "Styrt" +posture_governedSub = "Legger til sikkerhet og ruting" +posture_regulated = "Regulert" +posture_regulatedSub = "Hver kategori — samsvar, oppbevaring, inntak" qbr = "Kvartalsvise forretningsgjennomganger" qbrSub = "Din SE gjennomgår bruk og veikart hvert kvartal" +region = "Stat / region" +regionPlaceholder = "California" running = "{{annual}} / år · {{years}} år {{tcv}}" s1Sub = "Teamet ditt, og PDF-ene du forventer å kjøre hvert år." s1Title = "Hvor mye vil du behandle?" @@ -7623,6 +8496,12 @@ s2Title = "Forpliktelse og service" s3Sub = "For tilbudet og avtalen det genererer." s3Title = "Dine detaljer" serviceLevel = "Servicenivå" +size_compact = "Kompakt" +size_compactSub = "Mest tekst, under 1 MB" +size_heavy = "Tung" +size_heavySub = "Skannet eller bildetung, 5 MB+" +size_standard = "Standard" +size_standardSub = "Blandet tekst og bilder, 1 til 5 MB" slDedicated = "Dedikert" slDedicatedSub = "4 virketimer · dedikert kundeansvarlig · +30 %" slPriority = "Prioritet" @@ -7630,6 +8509,8 @@ slPrioritySub = "Samme virkedag · navngitt CSM · +15 %" slStandard = "Standard" slStandardSub = "Neste virkedag · delt CSM · inkludert" stepOf = "Trinn {{n}} av {{total}}" +taxId = "VAT / skatte-ID" +taxIdPlaceholder = "Valgfritt" term = "Periode" termDiscount = "{{pct}} % rabatt for flerårig forpliktelse brukt" title = "Bygg tilbudet ditt" @@ -7637,6 +8518,7 @@ training = "Onboarding og opplæring" trainingSub = "Live-økter for å få teamet ditt i gang" users = "Totalt antall brukere" usersPlaceholder = "f.eks. 250" +viewEula = "Les den" volEstimated = "Estimert fra {{count}} brukere (~2,000 PDF-er hver, inkludert automatisering). Rediger hvis du vet bedre." volManual = "Bruker tallet ditt. Estimer på nytt fra teamstørrelsen når som helst." volNoUsers = "Usikker? Angi teamstørrelsen din, så estimerer vi det." @@ -7645,55 +8527,55 @@ volumePlaceholder = "f.eks. 1,000,000" years_one = "{{count}} år" years_other = "{{count}} år" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumenter" -done = "Ferdig" -here = "Du er her" -hide = "Skjul" -optional = "Valgfritt" -paidAddon = "Betalt tillegg" -show = "Vis" -subtitle = "Alt du trenger på hvert trinn av reisen, vist etter hvert som avtalen går gjennom den." -supportingSubtitle = "SOC 2, sikkerhetsgjennomganger, skatteskjemaer og mer, klart når sikkerhets- eller innkjøpsteamet ditt spør. Noen har et engangsgebyr." -supportingTitle = "Støtter evalueringen din" +[portal.procurement.documents] +agreement = "Enterprise-avtale" +agreementSub = "MSA, bestillingsskjema og DPA i én signatur." +download = "Last ned" +eula = "EULA og kommersielle vilkår" +eulaSub = "Programvaren og kommersielle selvbetjeningsvilkår." +invoice = "Faktura" +invoiceSub = "Din første abonnementsfaktura." +laterInvoice = "Tilgjengelig etter at du godtar" +laterQuote = "Tilgjengelig når tilbudet ditt er utstedt" +quote = "Tilbud" +quoteSub = "Ditt spesifiserte Enterprise-tilbud." +sla = "SLA-vedlegg" +slaSub = "Støttenivåer og svarmål." +subprocessors = "Underdatabehandlere" +subprocessorsSub = "Tredjeparter som behandler data på dine vegne." +subtitle = "Avtaledokumentene dine, tilgjengelige når som helst." title = "Dokumenter" -upcoming = "Kommende" +view = "Vis" [portal.procurement.error] title = "Noe gikk galt" [portal.procurement.hero] -company = "Enterprise-avtalen din" +barAria = "Trinn {{current}} av {{total}}" ctaAgreement = "Se gjennom og signer avtale" -ctaLive = "Du er i drift" -ctaPayment = "Legg til betaling" +ctaExploring = "Sett opp prøveperioden din" ctaQuote = "Se gjennom tilbudet ditt" +ctaReviewQuote = "Gå gjennom tilbud" ctaTrial = "Bygg tilbudet ditt" +documents = "Dokumenter" eyebrow = "Enterprise-innkjøp" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Inviter teammedlemmer" -keyDocs = "Nøkkeldokumenter" -nextStep = "Neste trinn: {{action}}" -notStarted = "Ikke startet" +licenseKey = "Lisensnøkkel" +liveSub = "Organisasjonen din er klargjort, og lisensen din er aktiv." +liveTitle = "Du er live på Stirling Enterprise" +next = "Neste: {{stage}}" open = "Åpne innkjøp" scheduleCall = "Planlegg en samtale" -setup1Sub = "Inviter teammedlemmene dine" -setup1Title = "Rull ut PDF Editor" -setup2Sub = "Slå på behandling på tvers av editorer og andre kilder" -setup2Title = "Koble til PDF Processor" -setup3Sub = "Slå på sikkerhet, samsvar, ruting eller oppbevaring når du trenger dem" -setup3Title = "Legg til anbefalte policyer" +sentenceAgreement = "gå gjennom og signer avtalen" +sentenceLive = "klargjøring og utrulling" +sentencePayment = "innkjøpsordre og betaling" +sentenceQuote = "gå gjennom og godta tilbudet ditt" +sentenceTrial = "sett opp prøvearbeidsområdet ditt" [portal.procurement.journey] daysLeft_one = "{{count}} dag igjen" daysLeft_other = "{{count}} dager igjen" -engineerLabel = "Din løsningsingeniør" -eyebrow = "Utrullingen din" -live = "Du er i drift på Stirling Enterprise" -nextStep = "Neste trinn: {{action}}" -subtitle = "Løsningsingeniøren din er med på hvert trinn. Én neste handling om gangen; hele sjekklisten er nedenfor." -title = "Fra prøveperiode til drift, én veiledet vei" -trialTitle = "Enterprise-prøveperiode" [portal.procurement.journeySteps.agreement] blurb = "Én signatur dekker MSA, bestillingsskjema, EULA og DPA." @@ -7727,6 +8609,9 @@ downloadError = "Kunne ikke generere offline-lisensfilen ennå — prøv igjen o downloadOffline = "Last ned offline-lisens (.lic)" hint = "Lim inn denne nøkkelen i en selvdriftet instans for å aktivere den, eller behold den for arkivet ditt. Oppbevar den trygt." label = "Lisensnøkkelen din" +subtitle = "Aktiver en selvhostet instans med denne nøkkelen, eller behold den for arkivet ditt." +title = "Lisensnøkkelen din" +trialFileHint = "Dette er prøvelisensfilen din. Når avtalen din er på plass, kom tilbake og last den ned igjen — i motsetning til den nettbaserte lisensnøkkelen oppdateres ikke .lic-filen av seg selv." [portal.procurement.link] cta = "Koble til konto" @@ -7739,65 +8624,62 @@ description = "Abonnementet ditt er aktivt og lisensen din er utstedt. Teamet di eyebrow = "I drift" title = "Du er i drift på Stirling Enterprise" -[portal.procurement.locked] -description = "Prøvenøkler, tilbud for forpliktet volum, avtalen med én signatur, betaling og dokumentregisteret ditt finnes alle her når du starter en enterprise-evaluering." -eyebrow = "Kun Enterprise" -talkToSales = "Snakk med salg" -title = "Innkjøpsløpet åpnes med Enterprise" - [portal.procurement.milestone] -accept = "Godta og fortsett" -description = "Last ned PDF-en for å dele den med teamet ditt, kom tilbake for å godta når du er klar, eller gjør endringer." -download = "Last ned PDF" downloadError = "Kunne ikke laste ned tilbuds-PDF-en ennå — prøv igjen om et øyeblikk." -edit = "Rediger tilbud" -eyebrow = "Tilbud {{number}}" -perYear = " / år" -preparedFor = "Utarbeidet for {{company}}" -tcv = "{{value}} total kontraktsverdi" -title = "Tilbudet ditt er klart" - -[portal.procurement.modal] -cancel = "Avbryt" -chooseFile = "Velg fil" -close = "Lukk" -downloadBody = "Nedlastingen starter snart." -downloadCta = "Last ned" -downloadTitle = "Last ned" -noFile = "Ingen fil valgt" -payBody = "Betal den forpliktede kontrakten din med kort eller bankoverføring gjennom Stripe. Arbeidsområdet ditt klargjøres så snart betalingen er godkjent." -payCta = "Fortsett til Stripe" -payTitle = "Bekreft betaling" -requestBodyFree = "Vi genererer dette på forespørsel. Bekreft, så sender løsningsingeniøren din det over snart." -requestBodyPaid = "Dette er et betalt tillegg. Bekreft, så avgrenser løsningsingeniøren din det og sender papirarbeidet." -requestCta = "Be om" -requestTitle = "Be om dette dokumentet" -signBody = "Åpner Stirling Enterprise Agreement for e-signatur: én signatur dekker MSA, bestillingsskjema, EULA og DPA. Vi motsignerer automatisk, og du går videre til betaling." -signCta = "Åpne for signering" -signTitle = "Se gjennom og signer avtalen din" -uploadBody = "Send oss innkjøpsordren din, så fakturerer vi mot den på dine vilkår. Dra inn PDF-en eller velg en fil nedenfor." -uploadCta = "Last opp innkjøpsordre" -uploadTitle = "Last opp innkjøpsordren din" [portal.procurement.payment] description = "Tilbudet ditt er akseptert og lisensen din er allerede aktiv — teamet ditt kan starte med én gang. Betal den første fakturaen når du er klar; du kan betale eller laste den ned her, ingen e-post nødvendig." +downloadAgreement = "Last ned signert avtale" downloadInvoice = "Last ned faktura" -simulate = "Simuler mottatt betaling (demo)" +eyebrow = "Betaling" title = "Abonnement opprettet" viewInvoice = "Vis og betal faktura" +[portal.procurement.review] +acceptCta = "Godta tilbud" +annual = "Årsavgift (år 1)" +downloadCta = "Last ned tilbud" +poNumber = "Innkjøpsordre: {{po}}" +renewal = "Fornyes til {{amount}}/år etter perioden (+{{pct}} % KPI)" +tcv = "{{years}}-års periode, betalt i sin helhet på forhånd: {{tcv}}" +validUntil = "Gyldig til {{date}}" + [portal.procurement.schedule] fallback = "Kunne ikke laste planleggeren." fallbackLink = "Åpne planlegging i en ny fane" +loading = "Laster planlegger..." subtitle = "Løsningsingeniøren din vil gå gjennom utrullingen med teamet ditt. Velg et tidspunkt som passer deg." title = "Planlegg en samtale" -[portal.procurement.status] -action = "Handling kreves" -available = "Tilgjengelig" -complete = "Fullført" -pending = "Venter" -request = "På forespørsel" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Forseglet, ingen utgående trafikk" +back = "Tilbake" +businessName = "Bedriftsnavn" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Administrert, raskest start" +continue = "Fortsett" +deployment = "Hvor skal det distribueres?" +eula = "Jeg godtar Stirling EULA og kommersielle vilkår." +fullName = "Fullt navn" +fullNamePlaceholder = "Navnet ditt" +invites = "Inviter teammedlemmer · valgfritt" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Planlegg en samtale" +seats = "Personer som tester" +seatsPlaceholder = "f.eks. 15" +selfhost = "Selvhostet" +selfhostSub = "Docker eller Kubernetes" +start = "Start prøveperiode" +stepOf = "Trinn {{n}} av {{total}}" +subtitle = "Gratis i 14 dager, ingen kort kreves." +subtitleDetails = "Noen få detaljer for tilbudet og avtalen din." +talkFirst = "Foretrekker du å snakke først?" +title = "Sett opp prøveperioden din" +viewEula = "Les den" +workEmail = "Jobb-e-post" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Forlengelse legger til 7 dager og varsler løsningsingeniøren din." @@ -7808,109 +8690,113 @@ maxed = "Maksimert" subtitle = "Den gratis prøveperioden din varer til {{date}}. Ingen kort kreves." title = "Enterprise-prøveperiode" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Priser for forpliktet volum, SSO + SCIM + RBAC for hele organisasjonen, 90-dagers uforanderlig revisjonsspor og en dedikert SE." -homeCta = "Start prøveperiode →" -homeHeadline = "Behandle millioner av PDF-er." - -[portal.recentActivity] -title = "Nylig aktivitet" -viewAll = "Vis alle" - -[portal.recentActivity.empty] -description = "Pipeline-kjøringer, utrullinger og agenthendelser vises her." -title = "Ingenting her ennå" - -[portal.search] -ariaLabel = "Søk" -placeholder = "Søk i Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Hurtighandlinger vises her når de er tilgjengelige." -noActionsTitle = "Ingen hurtighandlinger" -noMatches = "Ingen treff for \"{{query}}\"" -noMatchesDescription = "Prøv et annet søkeord eller bla gjennom katalogen." - [portal.settings.groups] admin = "Administrator" [portal.settings.sections] account-link = "Kontokobling" -[portal.shell.header] -accountFallback = "Konto" -accountMenu = "Kontomeny" -darkMode = "Mørk modus" -lightMode = "Lys modus" -search = "Søk" -searchPlaceholder = "Søk…" -signOut = "Logg av" -switchToDark = "Bytt til mørkt tema" -switchToLight = "Bytt til lyst tema" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumenter" linkAccount = "Koble til Stirling-konto" -planEditor = "Editor-abonnement" -planEnterprise = "Enterprise-abonnement" -planProcessor = "Processor-abonnement" primaryNav = "Primær navigasjon" switchApp = "Bytt app" +[portal.shell.topbar] +closeNav = "Lukk navigasjon" +openNav = "Åpne navigasjon" +search = "Søk" + [portal.sources] subtitle = "Gjenbrukbare inndatakoblinger som mater dokumenter inn i Stirling. Konfigurer en kobling én gang, og referer deretter til den fra et hvilket som helst antall policyer. Klikk på en rad for konfigurasjonen og hvilke policyer som bruker den." title = "Kilder" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Koble til kilde" +[portal.sources.builder] +back = "Tilbake til kilder" +backToTypes = "Alle kildetyper" +cancel = "Avbryt" +chooseHint = "Velg hvor dokumenter kommer fra. Nedtonede tilkoblinger er på vei." +comingSoon = "Kommer snart" +comingSoonHeading = "Kommer snart" +create = "Opprett kilde" +createTitle = "Koble til en kilde" +delete = "Slett" +editTitle = "Rediger kilde" +enabled = "Aktivert" +save = "Lagre endringer" + +[portal.sources.builder.folderAccess] +description = "Mappeautomatiseringer kan bare bruke mapper en administrator har tillatt. Legg den til under innstillingene for mappetilgang, og prøv igjen." +openSettings = "Innstillinger for mappetilgang" +title = "Denne mappen er ikke tillatt" + [portal.sources.delete] body = "Slette \"{{name}}\"? Dette kan ikke angres. Policyer som refererer til den, må oppdateres." cancel = "Avbryt" confirm = "Slett" title = "Slette kilde?" -[portal.sources.detail] -closeAriaLabel = "Lukk detaljer" -delete = "Slett kilde" -docs24h = "Siste 24 t" -docs30d = "Siste 30 dager" -docsTotal = "Totalt sett" -docsTrend = "Dokumenter de siste 30 dagene" -documents = "Dokumenter" -edit = "Rediger" -notReferenced = "Den refereres ikke av noen policy, så den er trygg å slette." -pause = "Sett på pause" -resume = "Fortsett" -subtitle = "{{type}} · {{status}}" -usedBy = "Brukt av" - -[portal.sources.empty] -description = "Koble til en mappe (og snart skylagring) slik at policyene dine har et sted å hente dokumenter fra." -title = "Ingen kilder koblet til ennå" - [portal.sources.kpi] inUse = "I bruk" total = "Koblinger" unused = "Ubrukt" +[portal.sources.networkFields.connection] +helperText = "Den lagrede tilkoblingen med serveradresse og legitimasjon. Gjenbrukes av hver kilde som refererer til den." +label = "Tilkobling" + +[portal.sources.networkFields.directory] +helperText = "Mappe på serveren som skal sjekkes, relativt til innloggingshjemmet eller delingsroten. La stå tomt for roten." +label = "Mappe" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume fjerner hver fil fra serveren når hver retningslinje har behandlet den." +label = "Lesemodus" + +[portal.sources.networkFields.mode.options] +consume = "Consume: behandle hver fil én gang" +snapshot = "Snapshot: les mappen på nytt ved hver kjøring" + +[portal.sources.networkFields.recursive] +label = "Mappedybde" + +[portal.sources.networkFields.recursive.options] +all = "Inkluder undermapper" +top = "Kun øverste nivå" + [portal.sources.status] active = "Aktiv" disabled = "Deaktivert" unused = "Ubrukt" [portal.sources.table] +documents = "Dokumenter" source = "Kilde" status = "Status" +type = "Type" usedBy = "Policyer" +[portal.sources.types.box] +description = "Hent dokumenter fra Box-mapper teamet ditt allerede deler." +label = "Box" + +[portal.sources.types.dropbox] +description = "Hent dokumenter fra en delt Dropbox-mappe." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenter teamet ditt har behandlet i editoren, på tvers av retningslinje- og AI-kjøringer." label = "Editor" +noPolicies = "Ingen retningslinjer kjørt fra editoren ennå." + +[portal.sources.types.email] +description = "Gjør vedlegg sendt til en dedikert postboks om til dokumenter." +label = "E-postinnboks" [portal.sources.types.folder] description = "Overvåk en katalog på serveren for nye dokumenter." @@ -7943,25 +8829,80 @@ label = "Mappedybde" all = "Inkluder undermapper" top = "Kun toppnivå" +[portal.sources.types.ftp] +description = "Sjekk en FTP- eller FTPS-servermappe for nye dokumenter." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Hent dokumenter fra delte Google Drive-mapper." +label = "Google Drive" + +[portal.sources.types.network] +description = "Overvåk en SMB-nettverksdeling for nye dokumenter." +label = "Nettverksdisk" + +[portal.sources.types.onedrive] +description = "Overvåk OneDrive-mapper for nye dokumenter." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Hent dokumenter fra en Amazon S3- eller S3-kompatibel bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Den lagrede tilkoblingen som inneholder bucketen og legitimasjonen. Gjenbrukes av hver kilde og pipeline-utdata som refererer til den." +label = "Tilkobling" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume fjerner hvert objekt fra bucketen når hver retningslinje har behandlet det." +label = "Lesemodus" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: behandle hvert objekt én gang" +snapshot = "Snapshot: les bucketen på nytt ved hver kjøring" + +[portal.sources.types.s3.fields.prefix] +helperText = "Bare objekter med nøkler som starter med dette prefikset, behandles." +label = "Nøkkelprefiks" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Sjekk en SFTP-dropmappe for nye dokumenter." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Hent dokumenter fra SharePoint-dokumentbiblioteker." +label = "SharePoint" + [portal.sources.types.unknown] label = "Kilde" +[portal.sources.types.webhook] +createNote = "En leverings-URL og signeringshemmelighet genereres når du oppretter denne webhooken. Avsendere POST-er dokumenter til den URL-en, og de refererende retningslinjene kjøres ved ankomst." +description = "Motta dokumenter via signert HTTP POST fra et hvilket som helst eksternt system." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Leverings-URL" +secretNote = "Signeringshemmeligheten vises én gang, når webhooken opprettes. Opprett kilden på nytt for å rotere den." + +[portal.sources.types.webhook.reveal] +copy = "Kopier" +done = "Ferdig" +secret = "Signeringshemmelighet" +secretHelp = "Signer rå brødtekst for hver levering med denne nøkkelen (HMAC-SHA256) og send den som X-Stirling-Signature-headeren." +secretWarning = "Kopier signeringshemmeligheten nå. Av hensyn til sikkerheten din vises den bare én gang og kan ikke hentes senere." +title = "Webhook opprettet" +url = "Leverings-URL" +usage = "POST hvert dokument som rå forespørselsbrødtekst til leverings-URL-en med en binær innholdstype (application/pdf eller application/octet-stream). Refererende retningslinjer kjøres automatisk ved ankomst." + [portal.sources.wizard] -back = "Tilbake" -cancel = "Avbryt" -continue = "Fortsett" -editTitle = "Rediger kilde" name = "Navn" namePlaceholder = "f.eks. kravmottak" -save = "Lagre endringer" -subtitle = "Trinn {{current}} av {{total}} · {{label}}" -title = "Koble til en kilde" type = "Type" -[portal.sources.wizard.steps] -chooseType = "Velg type" -configure = "Konfigurer" -review = "Se gjennom og koble til" +[portal.stepModal] +close = "Lukk" [portal.usage] managePayment = "Administrer betaling" @@ -8021,16 +8962,9 @@ summary = "Eier et team — administrerer medlemmenes ressurser og delte konfigu 2 = "Portaltilgang via standardpolicyen" 3 = "Alt medlem kan gjøre" -[portal.welcome] -ariaLabel = "Velkommen til Stirling PDF" -badge = "Åpen kildekode" -installEditor = "Installer Editor" -inviteTeammates = "Inviter teammedlemmer" -openInBrowser = "Åpne i nettleser" -perks = "Gratis for alltid · Kan driftes selv" -subtitle = "Verdens sikreste PDF Editor er gratis for team i alle størrelser. Inkluderer 60+ PDF-operasjoner og SSO." -title = "Velkommen til" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Ubegrenset" [printFile] title = "Skriv ut fil" @@ -8613,6 +9547,14 @@ text = "Roter PDF-sidene med eller mot klokken i 90-graders trinn. Alle sider i [rotate.tooltip.header] title = "Oversikt over rotasjonsinnstillinger" +[ruler] +clearAll = "Fjern alt" +hideAllLabels = "Skjul alle etiketter" +hideSmallLabels = "Skjul små etiketter" +physicalValues = "Fysisk" +scaled = "Skalert" +showAllLabels = "Vis alle etiketter" + [sanitize] desc = "Fjern potensielt skadelige elementer fra PDF-filer." sanitizationResults = "Saniteringsresultater" @@ -8654,31 +9596,45 @@ label = "Fjern XMP-metadata" [sanitize.steps] settings = "Innstillinger" -[sanitizePdf] -tags = "rydde opp,sikker,trygg,fjern trusler" - [sanitizePDF] title = "Rensker PDF" +[sanitizePdf] +tags = "rydde opp,sikker,trygg,fjern trusler" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Juster side-skala" +[scaleSettings] +activeScale = "Aktiv skala" +apply = "Bruk skala" +applyCalibration = "Bruk kalibrering" +calculatedScale = "Beregnet skala" +calibrate = "Kalibrer" +calibrating = "Kalibrerer" +calibrationDistanceRequired = "Angi en faktisk avstand større enn null" +calibrationInvalid = "Kan ikke beregne målestokk fra denne målingen" +calibrationPaperDistance = "Målt sideavstand: {{distance}}" +calibrationTitle = "Kalibrer målestokk" +calibrationTooltip = "Mål en kjent avstand for å beregne målestokken automatisk" +customScale = "Egendefinert målestokk" +noneSet = "Ingen egendefinert målestokk angitt" +presets = "Forhåndsinnstilte målestokker" +ratio = "Målestokkforhold" +ratioHelp = "Forhold: 1 sideenhet = X faktiske enheter" +ratioPlaceholder = "f.eks. 100" +ratioPositive = "Forholdet må være større enn null" +ratioRequired = "Forhold er påkrevd" +realDistance = "Faktisk avstand" +realDistancePlaceholder = "f.eks. 5" +reset = "Tilbakestill til standard" +unit = "Enhet" +unitInvalid = "Ugyldig enhet: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Vinkelgrense:" -10 = "Angir størrelsen på kanten som legges til og fjernes for å forhindre hvite kanter i utdataen (standard: 1)." -2 = "Angir den minimale absolutte vinkelen som kreves for at bildet skal roteres (standard: 10)." -3 = "Toleranse:" -4 = "Bestemmer området for fargevariasjon rundt estimert bakgrunnsfarge (standard: 30)." -5 = "Minimumsområde:" -6 = "Angir minimumsområde terskel for et bilde (standard: 10000)." -7 = "Minimumskonturområde:" -8 = "Angir minimumskonturområde terskel for et bilde" -9 = "Kantstørrelse:" - [scannerImageSplit] submit = "Ekstraher bildeskanninger" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Hva dette gjør" whatThisDoesDesc = "Finner og ekstraherer automatisk hvert foto fra en skannet side eller et sammensatt bilde—ingen manuell beskjæring." whenToUse = "Når du bør bruke det" +[ScannerImageSplit.selectText] +1 = "Vinkelgrense:" +10 = "Angir størrelsen på kanten som legges til og fjernes for å forhindre hvite kanter i utdataen (standard: 1)." +2 = "Angir den minimale absolutte vinkelen som kreves for at bildet skal roteres (standard: 10)." +3 = "Toleranse:" +4 = "Bestemmer området for fargevariasjon rundt estimert bakgrunnsfarge (standard: 30)." +5 = "Minimumsområde:" +6 = "Angir minimumsområde terskel for et bilde (standard: 10000)." +7 = "Minimumskonturområde:" +8 = "Angir minimumskonturområde terskel for et bilde" +9 = "Kantstørrelse:" + [search] noResults = "Ingen treff" placeholder = "Skriv inn søkeord..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Stirling-PDF-serveren din er frakoblet og \"{{endpoin [session] expired = "Økten din har utløpt. Vennligst oppdater siden og prøv igjen." -[sessionManagement.tooltip] -header = "Administrere signeringsøkter" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nye deltakere legges til på slutten av signeringsrekkefølgen" -bullet2 = "Kan ikke legge til deltakere etter at økten er ferdigstilt" -bullet3 = "Hver deltaker mottar et varsel når det er deres tur" -description = "Du kan legge til flere deltakere i en aktiv økt når som helst før ferdigstilling." -title = "Legge til deltakere" - -[sessionManagement.tooltip.finalization] -bullet1 = "Full ferdigstillelse: Alle deltakere har signert" -bullet2 = "Delvis ferdigstillelse: Noen deltakere har ikke signert ennå" -bullet3 = "Usignerte deltakere blir utelatt fra det endelige dokumentet" -bullet4 = "Når ferdigstilt, kan du laste inn den signerte PDF-en i aktive filer" -description = "Ferdigstilling kombinerer alle signaturer i én signert PDF. Denne handlingen kan ikke angres." -title = "Ferdigstilling av økt" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Kan ikke fjerne deltakere som allerede har signert" -bullet2 = "Fjernede deltakere mottar ikke lenger varsler" -bullet3 = "Signeringsrekkefølgen justeres automatisk" -description = "Deltakere kan fjernes fra økter før de signerer." -title = "Fjerne deltakere" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Hver signatur legges til sekvensielt på PDF-en" -bullet2 = "Senere signerer kan se tidligere signaturer" -bullet3 = "Kritisk for godkjenningsflyter og juridisk sporbarhet" -description = "Rekkefølgen du angir når du oppretter økten bestemmer hvem som signerer først." -title = "Signeringsrekkefølge" - [settings] close = "Lukk" +[settings.ai] +documents = "Dokumenter og RAG" +general = "Generelt" +limits = "Grenser og ytelse" +models = "Modeller og leverandører" +title = "AI" + [settings.configuration] advanced = "Avansert" database = "Database" endpoints = "Endepunkter" features = "Funksjoner" +folderAccess = "Mappetilgang" mcp = "MCP Server" storageSharing = "Fil-lagring og deling" systemSettings = "Systeminnstillinger" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Standard PDF-redigerer" defaultPdfEditorActive = "Stirling PDF er din standard PDF-redigerer" defaultPdfEditorChecking = "Sjekker..." defaultPdfEditorInactive = "En annen app er satt som standard" +defaultPdfEditorRemind = "Minn meg på å angi som standard" +defaultPdfEditorRemindDescription = "Vis et banner når Stirling PDF ikke er standard PDF-programmet ditt." defaultPdfEditorSet = "Allerede standard" defaultStartupView = "Standardvisning ved oppstart" defaultStartupViewDescription = "Velg hvilken fane som er aktiv i venstre kolonne når appen starter" @@ -8817,9 +9763,18 @@ languageDescription = "Velg visningsspråk" logout = "Logg ut" setAsDefault = "Sett som standard" theme = "Tema" +themeAccent = "Aksentfarge" +themeAccentCustom = "Egendefinert farge" +themeAccentCustomHint = "Egendefinert (standard anbefales)" +themeAccentDark = "Aksentfarge for mørk modus" +themeAccentDefault = "Standard" +themeAccentDefaultHint = "Standardtema (anbefales)" +themeAccentDescription = "Knapper, lenker og uthevinger følger den, og den farger appen diskret. Lys og mørk har hver sin. System bruker den som er aktiv." +themeAccentLight = "Aksentfarge for lys modus" themeDark = "Mørk" themeDescription = "Bytt mellom lys og mørk modus" themeLight = "Lys" +themeReset = "Gjenopprett tema til standard" themeSystem = "System" title = "Generelt" user = "Bruker" @@ -8833,6 +9788,12 @@ intro = "Aktiver brukergodkjenning, teamadministrasjon og arbeidsområdefunksjon learnMore = "Les mer i dokumentasjonen" title = "For systemadministratorer" +[settings.general.loginLanding] +description = "Velg hvilken app som åpnes når du logger på Stirling Cloud." +editor = "Redigerer" +processor = "Prosessor" +title = "Etter pålogging" + [settings.general.mode] fullscreen = "Fullskjerm" sidebar = "Sidepanel" @@ -9013,9 +9974,6 @@ title = "Retningslinjer og personvern" [settings.preferences] title = "Preferanser" -[settings.search] -placeholder = "Søk i innstillingssider..." - [settings.security] description = "Oppdater passordet ditt for å holde kontoen sikker." title = "Sikkerhet" @@ -9041,6 +9999,11 @@ security = "Sikkerhet" telegram = "Telegram" title = "Sikkerhet og autentisering" +[settings.signOut] +confirm = "Er du sikker på at du vil logge av?" +submit = "Logg av" +title = "Logg av" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Siste side" maintainRatio = "Bytt behold sideforhold" next = "Neste side" noSavedSigs = "Ingen lagrede signaturer funnet" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personlige signaturer" previous = "Forrige side" redo = "Gjør om" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Fjerning av bakgrunn mislyktes" hint = "Last opp et PNG- eller JPG-bilde av signaturen din" label = "Last opp signaturbilde" placeholder = "Velg bildefil" +previewAlt = "Current image signature" processing = "Behandler bilde..." removeBackground = "Fjern hvit bakgrunn (gjør gjennomsiktig)" @@ -9207,6 +10172,17 @@ saved = "Velg en lagret signatur ovenfor, og klikk hvor som helst på PDF-en for text = "Når du har skrevet inn navnet ditt over, klikk hvor som helst i PDF-en for å plassere signaturen." title = "Slik legger du til signatur" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Flytt signatur" pause = "Pause plassering" @@ -9653,10 +10629,30 @@ title = "Last opp til server" updateButton = "Oppdater på server" uploadButton = "Last opp til server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klikk her" +fallback = "Hvis den ikke åpnet automatisk," +opening = "Åpner Swagger UI i en ny fane..." tags = "api,dokumentasjon,swagger,endepunkter,utvikling" title = "API-dokumentasjon" @@ -9764,12 +10760,39 @@ selectFilesHint = "Velg filer i Aktive filer for å kjøre dette verktøyet" singleFileScope = "Gjelder kun: {{fileName}}" viewerMode = "Bytt til filredigereren for å velge flere filer." +[toolFormat] +ANY = "en hvilken som helst fil" +CBR = "et tegneseriearkiv (CBR)" +CBZ = "et tegneseriearkiv (CBZ)" +CSV = "en CSV fil" +EBOOK = "en e-bok" +EMAIL = "en e-post" +EXCEL = "et regneark" +HTML = "en nettside" +IMAGE = "bilder" +JAVASCRIPT = "en JavaScript fil" +JSON = "en JSON fil" +MARKDOWN = "en Markdown fil" +NONE = "ingen fil" +PCL = "en PCL utskriftsfil" +PDF = "en PDF" +PDF_ENCRYPTED = "en passordbeskyttet PDF" +POSTSCRIPT = "en PostScript fil" +PPT = "en presentasjon" +TEXT = "en tekstfil" +VIDEO = "en video" +WORD = "et Word dokument" +XML = "en XML fil" +XPS = "et XPS dokument" +ZIP = "et ZIP arkiv" + [toolPanel] alpha = "Alpha" backToAllTools = "Tilbake til alle verktøy" collapse = "Skjul panel" expand = "Utvid panel" goBack = "Gå tilbake" +pdfTools = "PDF-verktøy" placeholder = "Velg et verktøy for å komme i gang" premiumFeature = "Premium-funksjon:" toolsHeader = "Verktøy" @@ -9806,11 +10829,6 @@ noToolsFound = "Ingen verktøy funnet" quickAccess = "HURTIGTILGANG" searchPlaceholder = "Søk i verktøy..." -[toolPicker.categories] -advancedTools = "Avanserte verktøy" -recommendedTools = "Anbefalte verktøy" -standardTools = "Standardverktøy" - [toolPicker.subcategories] advancedFormatting = "Avansert formatering" automation = "Automatisering" @@ -9824,6 +10842,9 @@ removal = "Fjerning" signing = "Signering" verification = "Verifisering" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Ingen verktøy funnet" noTools = "Ingen verktøy tilgjengelig" @@ -9950,28 +10971,53 @@ title = "Brukere" you = "(deg)" [users.action] +cancelInvite = "Avbryt invitasjon" deleteTeam = "Slett team" disableMfa = "Tilbakestill MFA" move = "Flytt til team" reinstate = "Gjeninnsett" remove = "Fjern fra org" +removeTeam = "Fjern fra team" rename = "Gi team nytt navn" resetPw = "Tilbakestill passord" suspend = "Suspender" unlock = "Lås opp konto" +[users.activity] +daysAgo = "{{count}} d siden" +hoursAgo = "{{count}} t siden" +justNow = "Akkurat nå" +minutesAgo = "{{count}} min siden" +monthsAgo_one = "{{count}} måned siden" +monthsAgo_other = "{{count}} måneder siden" +never = "Aldri" +weeksAgo_one = "{{count}} uke siden" +weeksAgo_other = "{{count}} uker siden" +yearsAgo_one = "{{count}} år siden" +yearsAgo_other = "{{count}} år siden" + [users.cap] addProcessor = "+ Processor" approver = "Godkjenner policy" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Avbryt invitasjonen til {{email}}? De vil ikke kunne bli med med den nåværende lenken." +cancelInviteTitle = "Avbryt invitasjon" deleteTeamBody = "Slette teamet {{name}}? Teamet må være tomt først - flytt medlemmene til et annet team, og det kan ikke fortsatt eie noen integrasjonskonfigurasjoner." deleteTeamTitle = "Slett team" disableMfaBody = "Fjerne MFA-registreringen til {{name}}? De setter den opp igjen ved neste pålogging hvis det kreves." disableMfaTitle = "Tilbakestill MFA" removeBody = "Fjerne {{name}} permanent fra organisasjonen? Dette kan ikke angres." +removeTeamBody = "Fjerne {{name}} fra teamet? De beholder kontoen sin, men mister tilgang til dette teamets ressurser." removeTitle = "Fjern medlem" [users.empty] @@ -9982,10 +11028,8 @@ title = "Ingen medlemmer ennå" addToTeam = "Legg til i team" guestCount = "{{count}} gjest" guests = "Gjester" -guestsDesc = "Eksterne samarbeidspartnere, begrenset til det du delte. Kun Editor." ledBy = "ledet av {{owner}}" org = "Organisasjon" -orgDesc = "Eiere med myndighet på tvers av organisasjonen og policygodkjenning" owners = "{{count}} eier" team = "{{name}}-team" teamMeta = "{{count}} personer" @@ -10022,6 +11066,18 @@ username = "Brukernavn" usernameError = "Brukernavnet må være minst 3 tegn" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invitert av {{who}}" +cancel = "Avbryt" +expiresInDays_one = "Utløper om {{count}} dag" +expiresInDays_other = "Utløper om {{count}} dager" +expiresToday = "Utløper i dag" +title = "Ventende invitasjoner" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Noe gikk galt ved kontakt med backend, eller du har ikke tilgang. Prøv igjen." title = "Kunne ikke laste inn medlemmer" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Søk i vedlegg" title = "Vedlegg" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bokmerketittel" +bookmarkTitleRequired = "Bokmerketittel er påkrevd" closeSidebar = "Lukk sidepanelet for bokmerker" collapseAll = "Skjul alle bokmerker" +empty = "No bookmarks in this document" expandAll = "Utvid alle bokmerker" +searchPlaceholder = "Søk i bokmerker" +title = "Bookmarks" [viewer.comments] addComment = "Legg til kommentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Finn i dokument" moreActions = "Flere handlinger" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Side {{page}}" placingHint = "Klikk på en side for å plassere… (avbryt)" removeCommentOnly = "Fjern bare kommentaren" saveReply = "Lagre svar" +searchPlaceholder = "Search comments" title = "Kommentarer" typeComment = "Kommentar" typeInsertText = "Sett inn tekst" @@ -10262,6 +11325,7 @@ unsavedDesc = "Du har ulagrede endringer" closeSidebar = "Lukk sidepanelet for lag" hideAll = "Skjul alle lag" showAll = "Vis alle lag" +title = "Layers" [viewer.link] delete = "Slett lenke" @@ -10605,30 +11669,6 @@ title = "Vannmerketekst" image = "Bilde" text = "Tekst" -[wetSignature.tooltip] -header = "Metoder for oppretting av signatur" - -[wetSignature.tooltip.draw] -bullet1 = "Tilpass pennens farge og tykkelse" -bullet2 = "Slett og tegn på nytt til du er fornøyd" -bullet3 = "Fungerer på berøringsenheter (nettbrett, telefoner)" -description = "Lag en håndskrevet signatur med musen eller berøringsskjermen. Best for personlige, autentiske signaturer." -title = "Tegn signatur" - -[wetSignature.tooltip.type] -bullet1 = "Velg mellom flere skrifttyper" -bullet2 = "Tilpass tekststørrelse og -farge" -bullet3 = "Perfekt for standardiserte signaturer" -description = "Generer en signatur fra skrevet tekst. Rask og konsistent, egnet for forretningsdokumenter." -title = "Skriv signatur" - -[wetSignature.tooltip.upload] -bullet1 = "Støtter PNG, JPG og andre bildeformater" -bullet2 = "Gjennomsiktige bakgrunner anbefales for best resultat" -bullet3 = "Bildet vil størrelsestilpasses for å passe signaturområdet" -description = "Last opp et forhåndslaget signaturbilde. Ideelt hvis du har en skannet signatur eller firmalogo." -title = "Last opp signaturbilde" - [workbenchBar] activeFiles = "Aktive filer" annotations = "Annoteringer" @@ -10642,6 +11682,7 @@ exitRedaction = "Avslutt sladdemodus" exportAll = "Eksporter PDF" exportSelected = "Eksporter valgte sider" formFill = "Fyll ut skjema" +hideToolbar = "Hide toolbar" multiTool = "Multiverktøy" panMode = "Panoreringsmodus" print = "Skriv ut PDF" @@ -10652,6 +11693,7 @@ redact = "Sladd" rotateLeft = "Roter til venstre" rotateRight = "Roter til høyre" ruler = "Linjal / Måling" +rulerSettings = "Målestokkinnstillinger" save = "Lagre" saveAll = "Lagre alt" saveAs = "Lagre som" @@ -10661,6 +11703,7 @@ selectAll = "Velg alle" selectByNumber = "Velg etter sidenummer" selectLanguage = "Velg språk" share = "Del" +showToolbar = "Show toolbar" toggleAnnotations = "Veksle synlighet for annoteringer" toggleAttachments = "Veksle vedlegg" toggleBookmarks = "Veksle bokmerker" diff --git a/frontend/editor/public/locales/pl-PL/translation.toml b/frontend/editor/public/locales/pl-PL/translation.toml index 2fc22fffff..18568884ff 100644 --- a/frontend/editor/public/locales/pl-PL/translation.toml +++ b/frontend/editor/public/locales/pl-PL/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Zastosuj" applyAndContinue = "Zapisz i wyjdź" -areYouSure = "Na pewno chcesz wyjść?" back = "Wstecz" black = "czarny" blue = "niebieski" @@ -46,10 +45,8 @@ filesSelected = "wybrane pliki" font = "Czcionka" fontSizeTooltip = "Rozmiar tekstu numeru strony w punktach. Większe wartości tworzą większy tekst." fontTypeTooltip = "Rodzina czcionek dla numerów stron. Wybierz zgodnie ze stylem dokumentu." -genericSubmit = "Wyślij" green = "zielony" help = "Pomoc" -imgPrompt = "Wybierz obraz(y)" incorrectPasswordMessage = "Nieprawidłowe hasło." info = "informacje" invalidUndoData = "Nie można cofnąć: nieprawidłowe dane operacji" @@ -62,7 +59,6 @@ loginAgreementTitle = "Umowa logowania" logOut = "Wyloguj się" marginTooltip = "Odległość między numerem strony a krawędzią strony." moreOptions = "Więcej opcji" -multiPdfPrompt = "Wybierz PDF (2+)" never = "Nigdy" no = "nie" noFileSelected = "Nie wybrano pliku. Prześlij jeden." @@ -80,7 +76,6 @@ pageOrderPrompt = "Kolejność stron (wprowadź listę numerów stron oddzielony pages = "Strony" pageSelectionPrompt = "Niestandardowy wybór strony (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1) :" password = "hasło" -pdfPrompt = "Wybierz PDF" pin = "Przypnij plik (pozostaw aktywny po uruchomieniu narzędzia)" pro = "Pro" processingComplete = "Twój plik jest gotowy." @@ -110,6 +105,7 @@ undoSuccess = "Pomyślnie cofnięto operację" unknown = "Nieznany" unpin = "Odepnij plik (zastąp po uruchomieniu narzędzia)" unsavedChanges = "Masz niezapisane zmiany w swoim PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Niezapisane zmiany" unsupported = "Nieobsługiwane" username = "nazwa użytkownika" @@ -356,6 +352,7 @@ text = "Te uprawnienia kontrolują, co użytkownicy mogą robić z PDF. Najskute title = "Zmień uprawnienia" [addStamp] +preview = "Podgląd stempla" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Pobieranie ręczne: kliknij linki i umieść pliki w folderze tes noLanguages = "Nie znaleziono języków tessdata w skonfigurowanym katalogu." permissionNotice = "Ścieżka tessdata nie jest zapisywalna. Pobrania zostaną otwarte w przeglądarce; zapisz pliki .traineddata ręcznie w folderze tessdata." +[admin.settings.ai.documents] +description = "Skonfiguruj model embeddingów i ustawienia pobierania używane do odpowiadania na pytania dotyczące dokumentów. Po zapisaniu stosowane do silnika AI." +title = "Dokumenty i RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Pozostaw puste, aby użyć własnych poświadczeń środowiskowych silnika. Dotyczy samodzielnie hostowanych wdrożeń z jednym silnikiem." +generic = "Klucz API embeddingów" +openai = "Klucz API OpenAI" +setPlaceholder = "Zapisano — pozostaw puste, aby zachować bieżący klucz" +voyageai = "Klucz API VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Bazowy URL punktu końcowego embeddingów zgodnego z OpenAI / Ollama, np. http://ollama:11434/v1. Musi wskazywać zaufany wewnętrzny punkt końcowy (wrażliwe na SSRF)." +label = "Bazowy URL embeddingów" + +[admin.settings.ai.documents.embeddingModel] +description = "Nazwa modelu embeddingów. Dowolny tekst; sugestie są tylko podpowiedziami." +label = "Model embeddingów" + +[admin.settings.ai.documents.embeddingProvider] +description = "Dostawca używany do zamiany tekstu dokumentu na embeddingi wektorowe." +label = "Dostawca embeddingów" + +[admin.settings.ai.documents.maxSearches] +description = "Maksymalna liczba wyszukiwań pobierania, które agent może uruchomić na żądanie." +label = "Maks. liczba wyszukiwań" + +[admin.settings.ai.documents.reindexNote] +body = "Zmiana modelu embeddingów zaczyna działać natychmiast, ale dokumenty zindeksowane poprzednim modelem muszą zostać ponownie zindeksowane, aby wyszukiwanie zwracało poprawne wyniki." +title = "Wymagane ponowne indeksowanie" + +[admin.settings.ai.documents.saved] +reindexNote = "Jeśli zmieniono model embeddingów, ponownie zindeksuj istniejące dokumenty, aby wyszukiwanie używało nowego modelu." + +[admin.settings.ai.documents.topK] +description = "Liczba najtrafniejszych fragmentów pobieranych na wyszukiwanie." +label = "Top K" + +[admin.settings.ai.general] +description = "Połącz Stirling z silnikiem Python AI i wybierz, które funkcje AI mają być dostępne. Zmiany zostaną zastosowane po ponownym uruchomieniu." +title = "Silnik AI" + +[admin.settings.ai.general.capabilities] +description = "Włącz lub wyłącz poszczególne funkcje AI. Wyłączone funkcje są ukryte w aplikacji." +title = "Możliwości" + +[admin.settings.ai.general.enabled] +description = "Przełącznik główny. Gdy jest wyłączony, żadne narzędzia AI, agenci ani wywołania silnika nie są dostępne." +label = "Włącz AI" + +[admin.settings.ai.general.features.chat] +description = "Asystent konwersacyjny do pracy z PDF." +label = "Asystent czatu" + +[admin.settings.ai.general.features.classify] +description = "Automatycznie kategoryzuj dokumenty według typu lub zawartości." +label = "Klasyfikacja dokumentów" + +[admin.settings.ai.general.features.createPdf] +description = "Wygeneruj nowy PDF (np. z HTML) za pomocą agenta AI." +label = "Utwórz PDF z polecenia" + +[admin.settings.ai.general.features.documentQuestions] +description = "Zadawaj pytania i otrzymuj odpowiedzi oparte na przesłanym dokumencie." +label = "Pytania o dokument" + +[admin.settings.ai.general.features.mathAuditor] +description = "Sprawdzaj dokumenty pod kątem błędów matematycznych i liczbowych." +label = "Audytor matematyczny" + +[admin.settings.ai.general.features.pdfComment] +description = "Dodawaj komentarze recenzyjne i adnotacje utworzone przez AI do PDF." +label = "Agent komentarzy PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Limit czasu dla cięższych operacji agenta, takich jak generowanie dokumentów." +label = "Limit czasu długich operacji (sekundy)" + +[admin.settings.ai.general.note] +body = "Silnik AI działa jako oddzielna usługa. Jego wspólny sekret" +body2 = "jest ustawiany przez zmienną środowiskową kontenera. Klucze API dostawców można wprowadzić na tych stronach lub podać jako zmienne środowiskowe silnika; klucze wprowadzone tutaj są przesyłane do silnika po zapisaniu." +title = "O silniku AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Limit czasu dla strumieniowanych (token po tokenie) odpowiedzi czatu." +label = "Limit czasu strumienia (sekundy)" + +[admin.settings.ai.general.test] +button = "Testuj połączenie" +failBody = "Silnik AI nie odpowiedział. Sprawdź URL, czy kontener silnika działa oraz czy AI jest włączone (po włączeniu wymagane jest ponowne uruchomienie)." +failTitle = "Silnik AI nieosiągalny" +okBody = "Silnik AI odpowiedział na kontrolę stanu." +okTitle = "Silnik AI osiągalny" + +[admin.settings.ai.general.timeoutSeconds] +description = "Limit czasu dla standardowych żądań AI do silnika." +label = "Limit czasu żądania (sekundy)" + +[admin.settings.ai.general.url] +description = "Wewnętrzny URL silnika Python AI, np. http://stirling-pdf-engine:5001." +label = "URL silnika AI" + +[admin.settings.ai.limits] +description = "Zabezpieczenia określające, ile pracy mogą wykonać żądania AI i ile z nich może działać równocześnie. Po zapisaniu stosowane do silnika AI." +title = "Limity i wydajność" + +[admin.settings.ai.limits.maxCharacters] +description = "Zabezpieczenie: odrzuć żądania AI, których wyodrębniony tekst przekracza tę długość." +label = "Maks. liczba znaków na żądanie" + +[admin.settings.ai.limits.maxPages] +description = "Zabezpieczenie: odrzuć żądania AI przekraczające tę liczbę stron PDF." +label = "Maks. liczba stron na żądanie" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maksymalna liczba jednoczesnych wywołań modelu w toku w całym silniku." +label = "Maks. równoległość modelu" + +[admin.settings.ai.models] +description = "Wybierz dostawcę LLM oraz inteligentne/szybkie modele używane przez silnik AI. Po zapisaniu stosowane do silnika AI." +title = "Modele i dostawcy" + +[admin.settings.ai.models.apiKey] +anthropic = "Klucz API Anthropic" +description = "Pozostaw puste, aby użyć własnych poświadczeń środowiskowych silnika. Dotyczy samodzielnie hostowanych wdrożeń z jednym silnikiem." +generic = "Klucz API" +openai = "Klucz API OpenAI" +setPlaceholder = "Zapisano — pozostaw puste, aby zachować bieżący klucz" + +[admin.settings.ai.models.baseUrl] +description = "Bazowy URL punktu końcowego zgodnego z OpenAI / Ollama, np. http://ollama:11434/v1." +label = "Bazowy URL dostawcy" +warning = "Bazowy URL musi wskazywać zaufany wewnętrzny punkt końcowy. Silnik będzie wykonywać do niego żądania po stronie serwera, więc niezaufana wartość jest wrażliwa na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maksymalna liczba tokenów wyjściowych dla szybkiego modelu." +label = "Maks. tokeny szybkiego modelu" + +[admin.settings.ai.models.fastModel] +description = "Tańszy, szybszy model do lekkich zadań. Dowolny tekst; sugestie są tylko podpowiedziami." +label = "Szybki model" + +[admin.settings.ai.models.provider] +description = "Z którym dostawcą LLM komunikuje się silnik." +label = "Dostawca" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maksymalna liczba tokenów wyjściowych dla inteligentnego modelu." +label = "Maks. tokeny inteligentnego modelu" + +[admin.settings.ai.models.smartModel] +description = "Model o dużych możliwościach do złożonego rozumowania. Dowolny tekst; sugestie są tylko podpowiedziami." +label = "Inteligentny model" + +[admin.settings.ai.saved] +body = "Zmiany są automatycznie przesyłane do silnika AI." +bodyNoPush = "Ustawienia zapisane. Zostaną zastosowane następnym razem, gdy silnik AI pobierze swoją konfigurację." +title = "Ustawienia AI zapisane" + [admin.settings.badge] clickToUpgrade = "Kliknij, aby zobaczyć szczegóły planu" @@ -938,6 +1094,25 @@ label = "Generuj ponownie przy starcie" description = "Liczba dni ważności certyfikatu" label = "Ważność certyfikatu (dni)" +[admin.settings.folderAccess] +description = "Katalogi, z których źródła folderów i wyjścia folderów mogą czytać oraz do których mogą zapisywać. To granica bezpieczeństwa: automatyzacje nigdy nie mogą wskazywać ścieżki serwera spoza tej listy." +securityNote = "Pozostaw puste, aby całkowicie wyłączyć źródła folderów i wyjścia. Własny katalog konfiguracji Stirling jest zawsze niedostępny." +title = "Dostęp do folderów" + +[admin.settings.folderAccess.implied] +description = "Te katalogi zarządzane przez Stirling są zawsze dozwolone i nie można ich tutaj zmienić." +serverStorage = "Przechowywanie plików na serwerze" +title = "Zawsze dozwolone" +watchedFolder = "Folder obserwowany potoku" + +[admin.settings.folderAccess.roots] +add = "Dodaj" +empty = "Brak dozwolonych folderów. Źródła folderów i wyjścia są obecnie wyłączone." +hint = "Wprowadź ścieżki bezwzględne, na przykład /data/inbox." +label = "Dozwolone foldery główne" +placeholder = "/data/inbox" +remove = "Usuń folder główny" + [admin.settings.general] description = "Skonfiguruj ustawienia aplikacji w całym systemie, w tym branding i domyślne zachowanie." system = "System" @@ -1704,6 +1879,9 @@ width = "Szerokość" [app] description = "Darmowa alternatywa dla Adobe Acrobat (10 mln+ pobrań)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konwertuj do PDF/A-3b" convertToPdfA3bDescription = "Tworzy archiwalny PDF z osadzonymi załącznikami" @@ -1842,6 +2020,15 @@ export = "Eksport" accessDenied = "Odmowa dostępu" insufficientPermissions = "Nie masz uprawnień do wykonania tej czynności." +[auth.callback] +completing = "Kończenie uwierzytelniania" +invalidToken = "Logowanie OAuth nie powiodło się — nieprawidłowy token." +missingToken = "Logowanie OAuth nie powiodło się — nie otrzymano tokenu." +oauthFailed = "Logowanie OAuth nie powiodło się. Spróbuj ponownie." +pleaseWait = "Poczekaj, kończymy logowanie." +signedOut = "Wylogowano Cię. Zaloguj się ponownie." +windowMayClose = "Możesz zamknąć to okno po zakończeniu." + [auth.displayName] guest = "Gość" user = "Użytkownik" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Zabezpiecza dokumenty PDF poprzez usuwanie potencjalnie z description = "To narzędzie automatycznie zmieni nazwy plików PDF na podstawie ich zawartości. Analizuje dokument, aby znaleźć najbardziej odpowiedni tytuł z tekstu." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatycznie obróć" +tags = "automatyczne obracanie,napraw orientację,do góry nogami,bokiem,wyprostuj,orientuj,wykryj orientację,popraw obrót,automatyczna orientacja,zeskanowane,OSD" + +[autoRotate.confidenceThreshold] +title = "Próg pewności" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Tylko OCR" +text = "Tylko tekst" +title = "Metoda wykrywania" + +[autoRotate.error] +failed = "Wystąpił błąd podczas automatycznego obracania PDF." + +[autoRotate.inferUndetected] +title = "Obróć strony o niskiej pewności, aby pasowały" + +[autoRotate.report] +applied = "{{degrees}}° zgodnie z ruchem wskazówek zegara" +confidenceValue = "Pewność {{value}}" +noChange = "Bez zmian" +pageLabel = "Strona {{number}}" +summary = "Obrócono {{rotated}} z {{total}} stron, {{unchanged}} pozostawiono bez zmian" +title = "Raport wykrywania" + +[autoRotate.report.method] +inferred = "Dopasowano" +none = "Pominięto" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Poniżej progu" +blankPage = "Strona jest pusta" +inferredFromDocument = "Tak samo jak inne strony" +noDominantDirection = "Mieszane kierunki tekstu" +osdFailed = "Nie znaleziono czytelnego tekstu" +osdNoVerdict = "Nie udało się określić orientacji" +tesseractUnavailable = "OCR nie jest zainstalowany" +tooFewGlyphs = "Za mało tekstu" + +[autoRotate.results] +title = "Wyniki automatycznego obracania" + +[autoRotate.settings] +title = "Ustawienia" + +[autoRotate.tooltip.confidenceThreshold] +text = "Jak pewne musi być sprawdzenie, zanim strona zostanie obrócona. Obniż, aby obracać więcej stron, podnieś, aby pozostawiać więcej stron bez zmian." +title = "Próg pewności" + +[autoRotate.tooltip.description] +text = "Określa, jaką orientację powinna mieć każda strona, i obraca ją we właściwą stronę. Strony, które są już ustawione pionowo, pozostają bez zmian." + +[autoRotate.tooltip.detectionMode] +text = "Auto najpierw odczytuje tekst na stronie, a następnie analizuje stronę jako obraz, jeśli to nie wystarczy. Tylko tekst pomija sprawdzanie obrazu, co jest najszybsze dla dokumentów, które nigdy nie były skanowane. Tylko OCR traktuje każdą stronę jako obraz." +title = "Metoda wykrywania" + +[autoRotate.tooltip.header] +title = "Automatyczne obracanie" + +[autoRotate.tooltip.inferUndetected] +text = "Pustych lub prawie pustych stron nie można sprawdzić samodzielnie. Gdy każda strona, którą udało się odczytać, wymaga tego samego obrotu, te strony są obracane tak, aby do nich pasowały." +title = "Obróć strony o niskiej pewności, aby pasowały" + [autoSizeSplitPDF] tags = "pdf, dzielenie, dokument, organizacja" @@ -2189,33 +2443,6 @@ keywords = "Słowa kluczowe: odd, even" numbers = "Liczby/zakresy: 5, 10-20" progressions = "Progresje: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Typy certyfikatów" - -[certificateChoice.tooltip.organization] -bullet1 = "Zarządzany przez administratorów systemu" -bullet2 = "Współdzielony między uprawnionymi użytkownikami" -bullet3 = "Reprezentuje tożsamość firmy, nie osoby" -bullet4 = "Najlepsze do: oficjalnych dokumentów, podpisów zespołowych" -description = "Współdzielony certyfikat dostarczony przez twoją organizację. Używany do podpisywania w imieniu firmy." -title = "Certyfikat organizacji" - -[certificateChoice.tooltip.personal] -bullet1 = "Generowany automatycznie przy pierwszym użyciu" -bullet2 = "Powiązany z twoim kontem użytkownika" -bullet3 = "Nie może być współdzielony z innymi użytkownikami" -bullet4 = "Najlepsze do: dokumentów osobistych, indywidualnej odpowiedzialności" -description = "Automatycznie generowany certyfikat unikalny dla twojego konta użytkownika. Odpowiedni do indywidualnych podpisów." -title = "Certyfikat osobisty" - -[certificateChoice.tooltip.upload] -bullet1 = "Wymaga pliku P12/PFX i hasła" -bullet2 = "Może być wydany przez zewnętrzne urzędy certyfikacji" -bullet3 = "Wyższy poziom zaufania dla dokumentów prawnych" -bullet4 = "Najlepsze do: prawnie wiążących umów, zewnętrznej weryfikacji" -description = "Użyj własnego certyfikatu PKCS#12. Zapewnia pełną kontrolę nad właściwościami certyfikatu." -title = "Prześlij własny P12" - [certSign] awaitingSignatures = "Oczekiwanie na podpisy" chooseCertificate = "Wybierz plik certyfikatu" @@ -2542,34 +2769,6 @@ server = "Serwer" stepTitle = "Źródło certyfikatu" upload = "Prześlij" -[certSign.tooltip.header] -title = "O zarządzaniu podpisami" - -[certSign.tooltip.overview] -bullet1 = "Sprawdzaj istniejące podpisy i ich ważność" -bullet2 = "Wyświetlaj szczegółowe informacje o podpisujących i certyfikatach" -bullet3 = "Dodawaj nowe podpisy cyfrowe, aby zabezpieczyć dokumenty" -bullet4 = "Obsługa wielu plików z łatwą nawigacją" -text = "To narzędzie pozwala sprawdzić, czy Twoje PDF-y są cyfrowo podpisane, oraz dodać nowe podpisy cyfrowe. Podpisy cyfrowe potwierdzają, kto utworzył lub zatwierdził dokument, i pokazują, czy został zmieniony po podpisaniu." -title = "Co potrafi to narzędzie?" - -[certSign.tooltip.signing] -bullet1 = "Obsługuje formaty PEM, PKCS12, JKS oraz certyfikat serwera" -bullet2 = "Możliwość pokazania lub ukrycia podpisu w PDF" -bullet3 = "Dodaj powód, lokalizację i nazwisko podpisującego" -bullet4 = "Wybierz stronę dla widocznych podpisów" -bullet5 = "Użyj certyfikatu serwera dla prostej opcji 'Sign with Stirling-PDF'" -text = "Aby podpisać PDF, potrzebujesz certyfikatu cyfrowego (np. PEM, PKCS12 lub JKS). Możesz wybrać, czy podpis ma być widoczny w dokumencie, czy pozostać niewidoczny jedynie dla bezpieczeństwa." -title = "Dodawanie podpisów" - -[certSign.tooltip.validation] -bullet1 = "Pokazuje, czy podpisy są ważne lub nieważne" -bullet2 = "Wyświetla informacje o podpisującym i dacie podpisu" -bullet3 = "Sprawdza, czy dokument został zmodyfikowany po podpisaniu" -bullet4 = "Może używać niestandardowych certyfikatów do weryfikacji" -text = "Podczas sprawdzania podpisów narzędzie informuje, czy są ważne, kto podpisał dokument, kiedy został podpisany oraz czy dokument był zmieniany po podpisaniu." -title = "Sprawdzanie podpisów" - [changeCreds] changeUsername = "Zaktualizuj nazwę użytkownika. Zostaniesz wylogowany po aktualizacji." credsUpdated = "Konto zaktualizowane" @@ -2737,12 +2936,16 @@ text = "Aby uniemożliwić zmianę tych uprawnień, użyj narzędzia Dodaj hasł [chat.actions] copy = "Kopiuj wiadomość" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opcje agenta Stirling" +assistant = "AI Assistant" clearChat = "Wyczyść czat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ta operacja wykorzysta Twoje kredyty w chmurze" tooltip = "Wybierz kolor z ekranu" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Wybierz kolor" [common] +actions = "Actions" back = "Wstecz" cancel = "Anuluj" close = "Zamknij" +codeSample = "Code sample" collapse = "Zwiń" confirm = "Potwierdź" continue = "Kontynuuj" copied = "Skopiowano!" copy = "Kopiuj" done = "Gotowe" +enterValue = "Enter value" error = "Błąd" expand = "Rozwiń" loading = "Wczytywanie..." next = "Dalej" +open = "Open" preview = "Podgląd" previous = "Wstecz" refresh = "Odśwież" +remaining = "Pozostało" retry = "Ponów" save = "Zapisz" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Wyczyść zaznaczone" @@ -2988,6 +3199,7 @@ title = "Metoda kompresji" [compress.settings] desiredSize = "Docelowy rozmiar pliku" desiredSizePlaceholder = "Wprowadź rozmiar" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Kompresja to prosty sposób na zmniejszenie rozmiaru pliku. Wybierz File Size, aby podać docelowy rozmiar, a my dostosujemy jakość za Ciebie. Wybierz Quality, aby ręcznie ustawić siłę kompresji." @@ -3190,6 +3402,24 @@ pdfOptions = "Opcje PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Najpierw wybierz format źródłowy" settings = "Ustawienia" single = "Pojedynczy" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Zaawansowany monitoring" -allInOneWorkspace = "Wszechstronne środowisko pracy PDF (przeglądarka, narzędzia i agent)" -apiSandbox = "Piaskownica API" -dedicatedSupportSlas = "Dedykowane wsparcie i SLA" -fasterThroughput = "10× większa przepustowość" -fullyPrivateFiles = "W pełni prywatne pliki" -largeFileProcessing = "Przetwarzanie dużych plików" -orgWideAccess = "Kontrola dostępu w całej organizacji" -premiumAiModels = "Modele AI klasy premium" -prioritySupport = "Priorytetowe wsparcie" -privateDocCloud = "Prywatna chmura dokumentów" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Bezpieczny dostęp do API" -standardThroughput = "Standardowa przepustowość" -unlimitedApiAccess = "Nielimitowany dostęp do API" -unlimitedSeats = "Nielimitowana liczba użytkowników" - [crop] autoCrop = "Automatycznie przytnij białe obszary" noFileSelected = "Wybierz plik PDF, aby rozpocząć przycinanie" @@ -3346,6 +3558,7 @@ message = "Nie udało się ustawić domyślnej obsługi PDF" title = "Błąd" [defaultApp.prompt] +dontRemind = "Nie przypominaj ponownie" message = "Ustaw Stirling PDF jako domyślną aplikację do otwierania plików PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Szukaj..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Edytuj" @@ -3644,11 +3861,13 @@ shareSelected = "Udostępnij wybrane" sharing = "Udostępnianie" showAll = "Pokaż wszystko" showHistory = "Pokaż historię" +sortBy = "Sort files" sortByDate = "Sortuj według daty" sortByName = "Sortuj według nazwy" sortBySize = "Sortuj według rozmiaru" storage = "Magazyn" storageState = "Przechowywanie" +storageUsed = "Storage used" synced = "Zsynchronizowano" title = "Prześlij pliki PDF" toolChain = "Zastosowane narzędzia" @@ -3694,13 +3913,17 @@ addFiles = "Dodaj pliki" addingFiles = "Dodawanie plików…" collapse = "Zwiń pasek boczny" customizeGroups = "Dostosuj grupy" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Otwórz pliki, aby rozpocząć" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Rozwiń pasek boczny" -files = "Pliki" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive nie jest skonfigurowany" leaveMyFiles = "Wyjdź z Moich plików" +library = "Biblioteka PDF" myFiles = "Moje pliki" noFiles = "Brak plików" openFileManager = "Otwórz menedżer plików" @@ -3708,37 +3931,36 @@ openFromComputer = "Otwórz z komputera" openSettings = "Otwórz ustawienia" other = "Inne" recent = "Ostatnie" -search = "Szukaj" -searchPlaceholder = "Szukaj plików..." viewAll = "Wyświetl wszystkie pliki ({{count}})" [fileSidebar.fileItem] closeViewer = "Zamknij przeglądarkę" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Otwórz w przeglądarce" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Dodaj" -addLabel = "Dodaj etykietę…" -categoryIconAria = "Wybierz ikonę dla {{name}}" -createCategory = "Utwórz kategorię" -delete = "Usuń kategorię" done = "Gotowe" -hide = "Ukryj kategorię" -newCategory = "Nazwa nowej kategorii…" -removeLabel = "Usuń {{name}}" -rename = "Zmień nazwę" reset = "Przywróć domyślne" -search = "Szukaj etykiet…" -show = "Pokaż kategorię" subtitle = "Grupuj pliki w kategorie nadrzędne. Dodaj istniejące lub nowe etykiety do kategorii, zmień jej nazwę albo utwórz własną. Pliki spoza Twoich kategorii pojawią się w sekcji „Inne”." title = "Kategorie paska bocznego" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Dodaj do przestrzeni roboczej" addToWorkspaceCount = "Dodaj {{count}} do przestrzeni roboczej" @@ -3749,7 +3971,6 @@ backToMyFiles = "Wróć do Moich plików" breadcrumbs = "Ścieżka folderu" cancel = "Anuluj" classification = "Klasyfikacja" -clearSearch = "Wyczyść wyszukiwanie" clearSelection = "Wyczyść zaznaczenie" closeDetails = "Zamknij szczegóły" create = "Utwórz" @@ -3781,6 +4002,7 @@ downloadAll = "Pobierz wszystko" downloadVersion = "Pobierz tę wersję" dropOverlay = "Upuść pliki, aby je przesłać" dropOverlaySub = "Pliki trafiają najpierw do Lokalnych. Użyj 'Przenieś do' lub 'Zapisz w chmurze', aby umieścić je w folderze." +duplicate = "Duplicate" file = "Plik" fileInfo = "File info" fileMenu = "Akcje pliku" @@ -3808,13 +4030,12 @@ refresh = "Odśwież z serwera" remove = "Usuń" removeVersion = "Usuń tę wersję" rename = "Zmień nazwę" +renamed = "Zmieniono nazwę" renameFolder = "Zmień nazwę folderu" resizeFolderTree = "Zmień rozmiar drzewa folderów (strzałki, Shift dla większych kroków; dwuklik, aby dopasować automatycznie)" save = "Zapisz" saveToServer = "Zapisz na serwerze" saveToServerDisabledHint = "Zapisywanie na serwerze nie jest włączone na tym serwerze. Poproś administratora, aby je włączył." -search = "Szukaj" -searchPlaceholder = "Przeszukaj ten folder i podfoldery" selectAll = "Zaznacz wszystko" selectAllHint = "Kliknij, aby zaznaczyć wszystko. Wskazówka: przytrzymaj Ctrl (lub Cmd), aby dodawać pliki pojedynczo, a Shift, aby zaznaczyć zakres." selectedCount = "Wybrano: {{count}}" @@ -3874,6 +4095,10 @@ title = "Brak plików w chmurze" hint = "Pliki zapisane bez przesłania pozostają tutaj. Upuść plik, aby dodać." title = "Brak plików tylko lokalnych" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Pliki, które otwierasz lub edytujesz, pojawią się tutaj." title = "Jeszcze nic nie zmodyfikowano" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nie można było usunąć folderu." deleteFolderFailedDetail = "Nie można było usunąć folderu: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nie można było zaktualizować wyglądu folderu." folderAppearanceFailedDetail = "Nie można było zaktualizować wyglądu folderu: {{message}}" moveFilesFailed = "Nie można było przenieść plików." @@ -3910,9 +4137,7 @@ count = "Pliki" folder = "Folder" labels = "Etykiety" modified = "Zmodyfikowano" -name = "Nazwa" size = "Rozmiar" -toolHistoryAtVersion = "Skumulowany łańcuch narzędzi" totalSize = "Łączny rozmiar" type = "Typ" versionHistory = "Historia wersji" @@ -3945,6 +4170,11 @@ localHint = "Przechowywane tylko w tej przeglądarce" shared = "Udostępnione" sharedHint = "Udostępnione Tobie przez link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Najpierw najstarsze" modifiedDesc = "Najpierw najnowsze" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Brak plików w pamięci. Najpierw otwórz jakieś pliki. open = "Otwórz" openFile = "Otwórz plik" openFiles = "Otwórz pliki" +selectFile = "Select {{name}}" selectFromStorage = "Wybierz z magazynu" upload = "Prześlij" uploadFile = "Prześlij plik" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Wszystko zapisano" +analyzingFields = "Analysing form fields..." +extractCsvError = "Nie udało się wyodrębnić CSV" +extractXlsxError = "Nie udało się wyodrębnić XLSX" +filled = "filled" flattenAfterFilling = "Spłaszcz po wypełnieniu" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "wym." +requiredFieldsError = "Wypełnij wszystkie wymagane pola" rescanFields = "Skanuj pola ponownie" rescanFormFields = "Skanuj ponownie pola formularza" save = "Zapisz" +saveShortcut = "Ctrl+S, aby zapisać" +unsavedChanges = "Niezapisane zmiany" [formFill.sidebar] close = "Zamknij pasek boczny" @@ -4309,6 +4552,11 @@ desc = "Automatycznie zmienia nazwę pliku PDF na podstawie wykrytego nagłówka tags = "automatyczne wykrywanie,nagłówek,organizuj,przemianuj" title = "Autozmiana nazwy PDF" +[home.autoRotate] +desc = "Wykryj orientację każdej strony i automatycznie obróć ją do pionu." +tags = "automatyczne obracanie,napraw orientację,do góry nogami,bokiem,wyprostuj,orientuj,wykryj orientację,popraw obrót,automatyczna orientacja,zeskanowane,OSD" +title = "Automatyczne obracanie" + [home.autoSizeSplitPDF] tags = "auto,podziel,rozmiar" title = "Podziel (Rozmiar/Ilość stron)" @@ -4608,15 +4856,12 @@ hint = "Prześlij plik obrazu PNG, JPG, SVG lub inny, aby umieścić go w PDF. P label = "Prześlij obraz" placeholder = "Wybierz plik obrazu" -[imageToPdf] -tags = "konwersja,img,jpg,obraz,zdjęcie" - [imageToPDF] header = "Obraz na PDF" title = "Obraz na PDF" -[infoBanner] -dismiss = "Zamknij" +[imageToPdf] +tags = "konwersja,img,jpg,obraz,zdjęcie" [invite] acceptError = "Nie udało się utworzyć konta" @@ -4647,11 +4892,9 @@ welcomeTitle = "Zaproszono Cię!" [landing] addFiles = "Dodaj pliki" -heroSubtitle = "Upuść lub dodaj istniejący PDF, aby rozpocząć." mobileUpload = "Prześlij z telefonu" openFromComputer = "Otwórz z komputera" uploadFromComputer = "Prześlij z komputera" -workbenchEmptyStateHero = "Upuść PDF w dowolnym miejscu" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Wpisz nazwę użytkownika" failedToSignIn = "Nie udało się zalogować przez {{provider}}: {{message}}" forgotPassword = "Nie pamiętasz hasła?" loggingIn = "Logowanie..." -logIn = "Zaloguj się" login = "Logowanie" magicLinkSent = "Wysłano magiczny link na {{email}}! Sprawdź pocztę i kliknij link, aby się zalogować." mfaCode = "Kod uwierzytelniający" @@ -4715,26 +4957,10 @@ signInWith = "Zaloguj się przez" title = "Logowanie" unexpectedError = "Nieoczekiwany błąd: {{message}}" updatePassword = "Zaktualizuj hasło" -useEmailInstead = "Zaloguj się e‑mailem" useMagicLink = "Zamiast tego użyj magicznego linku" username = "Nazwa użytkownika" youAreLoggedIn = "Jesteś zalogowany!" -[login.slides.edit] -alt = "Edytuj PDF" -subtitle = "Ponad kilkanaście narzędzi do zaczerniania, podpisywania, czytania i modyfikowania PDF – znajdziesz to, czego potrzebujesz." -title = "Edytuj PDF, aby wyświetlać i zabezpieczać potrzebne informacje" - -[login.slides.overview] -alt = "Przegląd Stirling PDF" -subtitle = "Pakiet chmurowy z priorytetem prywatności do pracy z PDF: konwertuj, podpisuj, zaczerniaj i zarządzaj dokumentami oraz korzystaj z 50+ innych narzędzi." -title = "Jedno miejsce na wszystkie potrzeby związane z PDF." - -[login.slides.secure] -alt = "Zabezpiecz PDF" -subtitle = "Dodawaj hasła, zaczerniaj treść i zarządzaj certyfikatami z łatwością." -title = "Chroń wrażliwe informacje w plikach PDF" - [margin] large = "Duży" medium = "Średni" @@ -4818,6 +5044,34 @@ uploadSuccess = "Przesyłanie zakończone!" uploadSuccessMessage = "Twoje obrazy zostały przesłane." validating = "Weryfikowanie sesji..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Zeskanuj, aby przesłać zdjęcia. Obrazy są automatycznie konwertowane do PDF." descriptionNoConvert = "Zeskanuj, aby przesłać zdjęcia z urządzenia mobilnego." @@ -4836,8 +5090,13 @@ title = "Prześlij z telefonu" tags = "Wielofunkcyjne narzędzie, obsługa wielu operacji, interfejs użytkownika, przeciąganie kliknięć, front-end, strona klienta" title = "Narzędzie Wielofunkcyjne PDF" -[navbar] -search = "Szukaj" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Uwierzytelnienie nie zakończyło się pomyślnie. Możesz zamknąć to okno i spróbować ponownie." @@ -4966,6 +5225,7 @@ activeFiles = "Widok Aktywne pliki pokazuje wszystkie PDF-y za allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zamknij" cropSettings = "Teraz, gdy wybraliśmy plik, który chcemy przyciąć, możemy skonfigurować narzędzie Przytnij, aby wybrać obszar, do którego chcemy przyciąć PDF." +dialogLabel = "Onboarding" fileCheckbox = "Kliknięcie jednego z plików wybiera go do przetwarzania. Możesz wybrać wiele plików do operacji wsadowych." fileReplacement = "Zmodyfikowany plik automatycznie zastąpi oryginalny na Stole roboczym, co pozwoli łatwo przepuścić go przez kolejne narzędzia." filesButton = "Przycisk Pliki na pasku szybkiego dostępu umożliwia przesyłanie PDF-ów do użycia w narzędziach." @@ -4976,6 +5236,7 @@ pinButton = "Możesz użyć przycisku Przypnij, jeśli wolisz, results = "Po zakończeniu działania narzędzia krok Przegląd pokaże podgląd wyników w tym panelu i pozwoli cofnąć operację lub pobrać plik." runButton = "Po skonfigurowaniu narzędzia ten przycisk pozwala uruchomić je na wszystkich wybranych PDF-ach." selectCropTool = "Wybierzmy narzędzie Przytnij, aby pokazać, jak korzystać z jednego z narzędzi." +stepOf = "Krok {{current}} z {{total}}" toolInterface = "To jest interfejs narzędzia Przytnij. Jak widać, niewiele tu widać, ponieważ nie dodaliśmy jeszcze żadnych plików PDF do pracy." workbench = "To jest Stół roboczy - główny obszar, w którym przeglądasz i edytujesz swoje PDF-y." wrapUp = "Wszystko gotowe! Poznałeś główne obszary aplikacji i sposób ich użycia. Kliknij przycisk Pomoc, kiedy chcesz, aby ponownie zobaczyć tę wycieczkę." @@ -4988,6 +5249,26 @@ showMeAround = "Pokaż, co nowego" skipForNow = "Pomiń na razie" skipTheTour = "Pomiń przewodnik" +[onboarding.checklist] +dismiss = "Odrzuć" +title = "Skonfiguruj Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Uruchom Stirling natywnie na swoim komputerze" +title = "Pobierz Stirling dla komputerów" + +[onboarding.checklist.inviteTeam] +description = "Współpracuj ze swoim zespołem" +title = "Zaproś członków zespołu" + +[onboarding.checklist.shareAnalytics] +description = "Pomóż ulepszyć Stirling" +title = "Udostępnij anonimowe dane użycia" + +[onboarding.checklist.takeTour] +description = "Zobacz, jak działa Stirling w krótkim przewodniku" +title = "Rozpocznij prezentację" + [onboarding.desktopInstall] body = "Stirling najlepiej działa jako aplikacja desktopowa. Możesz używać go offline, szybciej otwierać dokumenty i wprowadzać zmiany lokalnie na komputerze." selectOs = "Wybierz system operacyjny" @@ -4996,7 +5277,21 @@ titleWithOs = "Pobierz dla {{osLabel}}" [onboarding.mfa] authenticationCode = "Kod uwierzytelniający" +description = "Zabezpiecz swoje konto, łącząc aplikację uwierzytelniającą. Zeskanuj kod QR lub wprowadź klucz konfiguracji, a następnie potwierdź 6-cyfrowy kod, aby zakończyć." +enable = "Włącz MFA" +enableError = "Nie można włączyć uwierzytelniania dwuskładnikowego. Sprawdź kod i spróbuj ponownie." +enterCode = "Wprowadź 6-cyfrowy kod z aplikacji." +enterCodeError = "Wprowadź kod uwierzytelniający, aby kontynuować." +logout = "Wyloguj" +openAuthenticator = "Otwórz Google Authenticator, Authy lub 1Password." qrCodeLoading = "Generowanie Twojego kodu QR…" +regenerateQrCode = "Wygeneruj ponownie kod QR" +scanQrCode = "Zeskanuj kod QR lub wprowadź poniższy klucz konfiguracji." +setupError = "Nie można rozpocząć konfiguracji uwierzytelniania dwuskładnikowego. Spróbuj ponownie." +setupKey = "Klucz konfiguracji (wpis ręczny)" +stepByStep = "Krok po kroku" +success = "MFA zostało włączone. Możesz teraz kontynuować." +title = "Konfiguracja uwierzytelniania wieloskładnikowego" [onboarding.planOverview] adminBodyLoginDisabled = "Po włączeniu trybu logowania możesz zarządzać użytkownikami, konfigurować ustawienia i monitorować kondycję serwera. Pierwsze {{freeTierLimit}} osób na Twoim serwerze korzysta ze Stirling bez opłat." @@ -5005,6 +5300,11 @@ adminTitle = "Przegląd dla administratora" userBody = "Zapraszaj członków zespołu, przypisuj role i utrzymuj porządek w dokumentach w jednym bezpiecznym miejscu pracy. Włącz tryb logowania, gdy będziesz gotów wyjść poza użycie solo." userTitle = "Przegląd planu" +[onboarding.processorIntro] +body = "Stirling uruchamia teraz Zasady — automatyczne reguły, które klasyfikują, zabezpieczają i przetwarzają każdy dokument po jego nadejściu. Skonfiguruj je i monitoruj uruchomienia w Processor." +cta = "Sprawdź Processor" +title = "Sprawdź Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Rok z góry" + +[payg.prepaid.banner] +exhausted = "Twoja przedpłacona pojemność została wykorzystana; rozliczanie licznikowe zostało wznowione. Doładuj, aby zachować rabat." +expiring_one = "Twój rok przedpłacony kończy się za {{count}} dzień. Niewykorzystana pojemność wygasa — doładuj, aby rozpocząć nowy rok." +expiring_other = "Twój rok przedpłacony kończy się za {{count}} dni. Niewykorzystana pojemność wygasa — doładuj, aby rozpocząć nowy rok." +low = "Pozostało Ci {{pct}}% przedpłaconej pojemności. Doładuj w dowolnym momencie, aby przetwarzanie nie zostało przerwane." + +[payg.prepaid.card] +subtitle = "Używane przed rozliczaniem licznikowym i poza limitem wydatków." +title = "Przedpłacona pojemność" + +[payg.prepaid.meter] +capSuffix = "z {{total}} przedpłaconych kredytów" +expires = "Wygasa {{date}}" + +[payg.prepaid.state] +exhausted = "Wykorzystano" +healthy = "Dużo pozostało" +low = "Kończy się" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF na PDF/A" tags = "archiwum, długoterminowe, standardowe, konwersja, przechowywanie, konserwacja" title = "PDF na PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "druk,standard,konwersja,produkcja,prepress,archiwizacja" title = "PDF do PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kredytów" xsmall = "100 kredytów" [plan.availablePlans] +currency = "Billing currency" subtitle = "Wybierz plan dopasowany do Twoich potrzeb" title = "Dostępne plany" @@ -5937,49 +6267,12 @@ keyDescription = "Wklej klucz licencyjny z e-maila" success = "Licencja aktywowana!" successMessage = "Twoja licencja została pomyślnie aktywowana. Możesz zamknąć to okno." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "wyegzekwowano" -enforcing = "Egzekwowanie..." -failed = "Egzekwowanie nie powiodło się" outputsUnavailable = "Wyniki polityki nie są już dostępne do pobrania." partialOutputsUnavailable = "Niektóre wyniki polityki nie są już dostępne do pobrania." -retrying = "Zajęte, ponawianie próby..." runNotFound = "Nie można już znaleźć przebiegu egzekwowania." -step = "krok {{current}}/{{total}}" timedOut = "Upłynął limit czasu egzekwowania, zanim przebieg mógł się zakończyć." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zamknij" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Stosowanie {{names}}" applyingProgress = "Stosowanie {{names}} ({{done}} z {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} z {{total}} plików nie można było przetworzyć i failureTitle = "Wyeksportowano bez pełnego egzekwowania" printPolicyAppliedBody = "Ten PDF został zaktualizowany, aby był zgodny z polityką. Sprawdź zmiany, a następnie wydrukuj ponownie." printPolicyAppliedTitle = "Politykę zastosowano przed drukowaniem" -queued = "+{{count}} w kolejce" successTitle = "{{names}} zastosowano" summaryMore = "{{first}}, {{second}} i {{more}} więcej" summaryTwo = "{{first}} i {{second}}" -[policies.enforcement.triggerVerb] -convert = "Egzekwowanie przed konwersją" -default = "Egzekwowanie" -export = "Egzekwowanie przed eksportem" -input = "Egzekwowanie przy imporcie" -print = "Egzekwowanie przed drukowaniem" - -[policies.field] -accessLog = "Dziennik dostępu" -archiveAfter = "Archiwizuj po" -auditTrail = "Ścieżka audytu" -belowThreshold = "Poniżej progu" -destination = "Miejsce docelowe" -frameworks = "Ramy zgodności" -immutableHold = "Niezmienna blokada" -keepFor = "Przechowuj przez" -minConfidence = "Minimalna pewność" -notify = "Powiadom przy kierowaniu" -onViolation = "Gdy niezgodne" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Dodaj" -addPlaceholder = "Dodaj etykietę…" -customNote = "Dostosowane dla Twojego zespołu." -defaultNote = "Używanie wbudowanych ustawień domyślnych, współdzielonych z zespołem." -duplicate = "\"{{name}}\" już istnieje." -edit = "Edytuj etykiety" -empty = "Nie ma jeszcze etykiet." -export = "Eksportuj JSON" -iconAria = "Wybierz ikonę dla {{name}}" -import = "Importuj JSON" -importError = "Nie udało się zaimportować tego pliku." -managedNote = "Etykietami zespołu zarządza lider zespołu." -modalSubtitle = "Współdzielone z całym zespołem. Klasyfikator wybiera etykiety pasujące do każdego dokumentu." -modalTitle = "Etykiety klasyfikacji" -pickIcon = "Użyj ikony {{label}}" -removeAria = "Usuń {{name}}" -resetToDefault = "Przywróć domyślne" -saveForTeam = "Zapisz dla zespołu" -saving = "Zapisywanie…" -sectionLabel = "Etykiety klasyfikacji" -startFromScratch = "Zacznij od zera" -teamCount = "etykiety zespołu" -tooLong = "Etykiety mogą mieć maksymalnie {{max}} znaków." -ungrouped = "Niepogrupowane" -view = "Wyświetl etykiety" +categoryCount = "kategorie" +hideCategory = "Ukryj kategorię" +labelCount = "etykiety" +sharedNote = "Te etykiety są wbudowane i współdzielone przez cały zespół." +showCategory = "Pokaż kategorię" [policies.pii] account = "Numery kont (oznaczone)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Numery routingu w USA (ABA)" ssn = "Numery Social Security" -[policies.settings] -noneExport = "Obecnie żadne zasady nie są uruchamiane przy eksporcie." -noneUpload = "Obecnie żadne zasady nie są uruchamiane przy przesyłaniu." -onExport = "Przy eksporcie" -onUpload = "Przy przesyłaniu" -reorderHandle = "Przeciągnij, aby zmienić kolejność" -runOrderDesc = "Gdy więcej niż jedna zasada działa na tym samym wyzwalaczu, są uruchamiane w tej kolejności — każda na wyniku poprzedniej zasady. Przeciągnij, aby zmienić kolejność." -title = "Ustawienia zasad" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opcje zasad" -policySettings = "Ustawienia zasad" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Ponów nieudane zasady ({{count}})" -rowProgress = "Przetworzono {{completed}} z {{total}} plików" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Czym jest zasada?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatycznie znajduje i zaczernia wrażliwe dane — takie jak numery Social Security i kart — aby nie można ich było odczytać w dokumencie." -sanitize = "Usuwa ukryty JavaScript z pliku, aby nic nie uruchamiało się automatycznie po jego otwarciu." -watermark = "Umieszcza widoczny znak (np. \"Poufne\") na każdej stronie." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zamknij" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "Wymuszanie {{name}}..." badgeRan = "Zasada {{name}} została uruchomiona dla tego pliku" +badgeRunning = "{{name}} działa..." blockingAction = "{{action}} zablokowano podczas wymuszania zasady, poczekaj..." dismiss = "Zamknij nakładkę" enforcingTitle = "Wymuszanie zasady..." @@ -6151,6 +6316,7 @@ viewAnyway = "Wyświetl plik (zasada nadal jest wymuszana)" [policyOption] autoRedactPhi = "Automatycznie zredaguj PHI" blockExport = "Zablokuj eksport" +ccpa = "CCPA" contracts = "Umowy" documents = "Dokumenty" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plan Editor" subscribed = "Plan Processor" unlinked = "Nie połączono" -[portal.agentBuilder] -bootstrapFromDocument = "Zainicjuj z dokumentu" -sectionsAriaLabel = "Sekcje kreatora agentów" -selectorAriaLabel = "Agenci" -subtitle = "Projektuj, testuj i wdrażaj agentów AI, którzy klasyfikują, wyodrębniają dane z dokumentów i kierują je dalej. Definiuj scenariusze, ograniczaj dostęp do narzędzi, uruchamiaj zestaw wzorcowy i publikuj wersję." -title = "Kreator agentów" - -[portal.agentBuilder.bootstrap] -cancel = "Anuluj" -dropzoneText = "Wybierz przykładowy dokument (PDF lub obraz)" -lead = "Upuść przykładowy dokument, a zaproponujemy scenariusze i schemat wyodrębniania, które możesz dopracować. Nic nie zostanie opublikowane, dopóki tego nie przejrzysz." -submit = "Zainicjuj agenta" -subtitle = "Zainicjuj nowego agenta na podstawie jednego reprezentatywnego pliku" -title = "Zainicjuj z dokumentu" - -[portal.agentBuilder.empty] -description = "Zainicjuj agenta na podstawie przykładowego dokumentu, aby utworzyć jego scenariusze i schemat wyodrębniania, a następnie dopracuj i opublikuj." -title = "Nie ma jeszcze agentów" - -[portal.agentBuilder.evals] -casesPassing = "Przypadki zaliczone" -columnCase = "Przypadek ewaluacyjny" -columnLatency = "Opóźnienie" -columnResult = "Wynik" -fail = "niezaliczone" -goldenSetPassRate = "Wskaźnik zaliczenia zestawu wzorcowego" -latencyMs = "{{ms}} ms" -notRun = "nie uruchomiono" -pass = "zaliczone" -passRate = "Wskaźnik zaliczeń" -runEvals = "Uruchom ewaluacje" - -[portal.agentBuilder.evals.empty] -description = "Ewaluacje zamieniają scenariusze w powtarzalny zestaw wzorcowy. Przejdź na wyższy plan, aby śledzić wskaźnik zaliczeń w czasie i uzależniać od niego publikacje." -title = "Nie ma jeszcze zestawu wzorcowego" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "w zestawach wzorcowych" -activeAgents = "Aktywni agenci" -avgPassRate = "Śr. wskaźnik zaliczeń ewaluacji" -fleetWide = "w całej flocie" -latestPublished = "Ostatnio opublikowane" -scenarios = "Scenariusze" -testCases = "przypadki testowe" -totalDescription_one = "łącznie {{count}}" -totalDescription_other = "łącznie {{count}}" - -[portal.agentBuilder.scenarios] -add = "Dodaj" -addScenario = "Dodaj scenariusz" -enable = "Włącz" -expectationLabel = "Oczekiwane zachowanie" -expectationPlaceholder = "Co agent powinien zrobić" -inEval = "w ewaluacji" -mute = "Wycisz" -muted = "wyciszone" -nameLabel = "Nazwa" -namePlaceholder = "np. Eskalacja zgodności" - -[portal.agentBuilder.tabs] -evals = "Ewaluacje" -scenarios = "Scenariusze" -tools = "Narzędzia" -versions = "Wersje" - -[portal.agentBuilder.tools] -broadAccess = "Szeroki dostęp" -deniedHint = "Wybrane narzędzia są zablokowane. Wszystkie pozostałe pozostają dostępne do wywołania." -deniedTools = "Zabronione narzędzia" -governanceGate = "Zarządzanie narzędziami jest dostępne w planie Enterprise." -restricted = "Ograniczone" -restrictedAccess = "Ograniczony dostęp do narzędzi" -restrictedDescription = "Zezwól na każde narzędzie oprócz tych, których odmówisz poniżej." - -[portal.agentBuilder.versions] -current = "bieżąca" -historyGate = "Pełna historia wersji i przywracanie są dostępne w planie Enterprise." -publish = "Opublikuj" -rollBack = "Przywróć" - [portal.assistant] close = "Zamknij asystenta" error = "Nie udało się połączyć z asystentem." @@ -6365,9 +6451,21 @@ typing = "Pisze" loading = "Ładowanie" redirectingToEditor = "Przekierowywanie do edytora..." +[portal.billing.activation] +maybeLater = "Może później" +title = "Włącz Processor" + +[portal.billing.activation.payg] +sub = "Ustaw miesięczny limit. Płać za to, co uruchamiasz." +title = "Płać zgodnie z użyciem" + +[portal.billing.activation.prepay] +badge = "2 miesiące gratis" +sub = "Jedna faktura, karta lub przelew bankowy." +title = "Zapłać za rok z góry" + [portal.billing.checkout] noClientSecret = "Funkcja edge nie zwróciła client_secret." -subtitle = "Dodaj kartę, aby kontynuować po wykorzystaniu bezpłatnego limitu planu Editor. Stripe zajmie się resztą." title = "Włącz plan Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Płatność się powiodła, ale aktywacja trwa trochę dłużej niż zwy close = "Zamknij" title = "Prawie gotowe" +[portal.billing.checkout.cap] +amountAria = "Miesięczny limit wydatków" +back = "Wstecz" +continue = "Przejdź do płatności" +error = "Nie udało się ustawić limitu wydatków" +finePrint = "Faktury wystawiane są co miesiąc. Anuluj w dowolnym momencie." +noLimit = "Bez limitu" +note = "Nigdy nie zapłacisz więcej niż wynosi Twój limit. Przetwarzanie po prostu się wstrzyma." +perMonth = "/ mies." +title = "Ustaw limit wydatków" + +[portal.billing.checkout.card] +fields = "Numer karty · MM / RR · CVC · ZIP" +label = "Dane karty" +note = "Dane karty są zbierane przez Stripe. Stirling nigdy nie przechowuje PAN ani CVC." + [portal.billing.checkout.error] title = "Nie udało się rozpocząć finalizacji zakupu" @@ -6383,10 +6497,9 @@ body = "Płatność została zrealizowana. Włączamy przetwarzanie rozliczane w hint = "Pozostaw to okno otwarte." title = "Aktywowanie planu Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "w env portalu, aby włączyć finalizację zakupu w aplikacji." -bodyBefore = "Ustaw" -title = "Stripe nie jest skonfigurowany" +[portal.billing.checkout.pay] +limit = "Miesięczny limit wydatków {{amount}}/mies. Przetwarzanie wstrzyma się po osiągnięciu limitu." +limitNoCap = "Brak limitu wydatków. Przetwarzanie nie zostanie wstrzymane. Faktury wystawiane są co miesiąc." [portal.billing.enterpriseUpsell] cta = "Utwórz wycenę Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Bieżący plan" freeForever = "Bezpłatnie na zawsze" noTeamResolved = "W Twoim portfelu nie ustalono jeszcze zespołu — odśwież i spróbuj ponownie." ownerOnly = "Tylko właściciel zespołu może włączyć plan Processor." +payInvoice = "Opłać fakturę, aby zakończyć" planName = "Editor" ssoIncluded = "SSO w cenie" switchOnProcessor = "Włącz Processor →" unlimitedUsers = "Nieograniczona liczba użytkowników" +viewQuote = "Zobacz wycenę" [portal.billing.invoices] columnAmount = "Kwota" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Bezpośrednie żądania API" segmentApiLabel = "API" segmentAutomationDesc = "Automatyzacje i potoki" segmentAutomationLabel = "Automatyzacja" +sizeMultiplier = "{{formatted}} PDF użyło mnożnika rozmiaru" +summary = "{{unique}} unikatowych · {{units}} jednostek licznika · średnio {{avg}} na PDF" +summaryNoRate = "{{unique}} unikatowych · {{units}} jednostek licznika" unit = "PDF rozliczane wg użycia" +unitsPending_one = "{{units}} jednostka licznika oczekuje na synchronizację z połączonych instancji" +unitsPending_other = "{{units}} jednostek licznika oczekuje na synchronizację z połączonych instancji" + +[portal.billing.prepaid] +capSuffix = "z {{total}} przedpłaconych kredytów" +expires = "Wygasa {{date}}" +eyebrow = "Przedpłacona pojemność" +note = "Używane przed rozliczaniem licznikowym i poza limitem wydatków." +topUp = "Doładuj" + +[portal.billing.prepaid.buy] +back = "Wstecz" +calcTitle = "Oblicz płatność roczną" +cancel = "Anuluj" +cancelPurchase = "Anuluj zakup" +close = "Zamknij" +continueToPayment = "Przejdź do płatności" +finalise = "Sfinalizuj" +invoiceErrorTitle = "Nie udało się wygenerować faktury" +loadingTitle = "Ładowanie wyceny" +payTitle = "Zapłać za rok" +step = "Krok {{current}} z {{total}}" + +[portal.billing.prepaid.calc] +change = "Zmień" +deployRow = "Wdrożenie" +done = "Gotowe" +enterpriseHint = "To skala enterprise. Przy takim wolumenie stawki enterprise przewyższają każdy rabat samoobsługowy. Blokada stawki, warunki i wycena w kilka minut." +governanceRow = "Zarządzanie" +governanceValue = "{{label}} · {{count}} zasad" +handlesLabel = "Twój Processor" +handlesValue = "obsługuje {{volume}} kredytów / mies." +pipelinesLabel = "Potoki" +policiesMeta = "{{count}} zasad" +poolCaption = "Jedna pula {{credits}} kredytów na rok. Cięższe miesiące korzystają z zapasu lżejszych." +priceLabel = "Twój rok · 12 miesięcy w cenie 10" +rateUnknown = "Pokażemy dokładną cenę przy finalizacji zakupu." +savings = "Oszczędzasz {{amount}} · 2 miesiące gratis." +sizingRow = "Rozmiar" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Łączna liczba użytkowników" + +[portal.billing.prepaid.consent] +label = "Rozumiem, że gdy moja przedpłacona pojemność zostanie wykorzystana lub wygaśnie po 12 miesiącach, przetwarzanie automatycznie będzie kontynuowane według standardowej licznikowej stawki pay-as-you-go (do mojego limitu wydatków), chyba że anuluję, oraz że mogę anulować w dowolnym momencie w portalu rozliczeń." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Zarządzane przez Stirling. Gotowe w kilka minut." +cloudValue = "Stirling Cloud · Zarządzane" +selfhost = "Self-hosted" +selfhostDesc = "Działa na prywatnej infrastrukturze. Ta sama stawka." +selfhostValue = "Self-hosted · Prywatna infrastruktura" + +[portal.billing.prepaid.offer] +cta = "Przejrzyj ofertę" +subtitle = "Zapłać z góry za rok przetwarzania PDF i otrzymaj dwa miesiące gratis, używane przed rozliczaniem licznikowym i poza limitem wydatków." +title = "Otrzymaj 12 miesięcy w cenie 10" + +[portal.billing.prepaid.pay] +accountName = "Nazwa posiadacza konta" +accountNamePlaceholder = "np. Anna Kowalska" +companyName = "Nazwa firmy (opcjonalnie)" +companyNamePlaceholder = "np. Acme Inc." +downloadInvoice = "Pobierz fakturę" +payOnline = "Zapłać online" +poNumber = "Numer PO (opcjonalnie)" +poNumberPlaceholder = "np. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Zaawansowane" +advancedDesc = "Potoki napędzają większość Twojego przetwarzania." +none = "Brak" +noneDesc = "Potoki jeszcze nie działają. Możesz je włączyć w dowolnym momencie." +standard = "Standardowe" +standardDesc = "Kilka potoków ponownie przetwarza przychodzące PDF." + +[portal.billing.prepaid.posture] +essentials = "Podstawy" +essentialsDesc = "Klasyfikacja, domyślnie, plus udostępnianie" +governed = "Zarządzane" +governedDesc = "Dodaje bezpieczeństwo i kierowanie" +regulated = "Regulowane" +regulatedDesc = "Każda kategoria, w tym zgodność, retencja, pobieranie" + +[portal.billing.prepaid.review] +download = "Pobierz wycenę (PDF)" +downloading = "Przygotowywanie…" +share = "udostępnij do zatwierdzenia" + +[portal.billing.prepaid.size] +compact = "Kompaktowy" +compactDesc = "Pliki poniżej 25 MB, brak opłat za dane" +heavy = "Ciężki" +heavyDesc = "Skanowane lub z dużą ilością obrazów, rutynowo 50 MB+" +standard = "Standardowy" +standardDesc = "Głównie małe, niektóre skany powyżej 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Wykorzystano" +healthy = "Dużo pozostało" +low = "Kończy się" [portal.billing.spendLimit] adjustLimit = "Dostosuj limit" @@ -6494,6 +6713,8 @@ label = "Prognozowane przekroczenie." [portal.billing.spendThisMonth] eyebrow = "Wydatki w tym miesiącu" +freeRemaining_one = "Pozostał {{formatted}} darmowy PDF" +freeRemaining_other = "Pozostało {{formatted}} darmowych PDF" processed_one = "Przetworzono {{formattedCount}} PDF." processed_other = "Przetworzono {{formattedCount}} PDF." processedWithRate_one = "Przetworzono {{formattedCount}} PDF, po {{rate}} za każdy." @@ -6510,6 +6731,7 @@ reachedTitle = "Osiągnięto miesięczny limit wydatków" title = "Nie udało się otworzyć portalu Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "z {{allowance}} bezpłatnych PDF wykorzystano" capSuffix_other = "z {{allowance}} bezpłatnych PDF wykorzystano" eyebrow = "Okres próbny Processor" @@ -6534,6 +6756,376 @@ signature = "podpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nowe połączenie" +createTitleFor = "Połącz {{name}}" +delete = "Usuń" +edit = "Edytuj" +editTitle = "Edytuj połączenie" +unsupportedType = "Tego typu połączenia nie można tutaj edytować." + +[portal.connections.categories.advanced] +description = "Dla usługi, której jeszcze nie obsługujemy." +label = "Zaawansowane" + +[portal.connections.categories.audit] +description = "Zapisz, co stało się z dokumentem, tam, gdzie już sprawdzają Twoi audytorzy." +label = "Logowanie audytu i zgodności" + +[portal.connections.categories.notify] +description = "Powiadom inny system, że coś się wydarzyło." +label = "Powiadomienia i przepływ pracy" + +[portal.connections.categories.security] +description = "Skanuj lub oznacz dokument, zanim zostanie przeniesiony dalej." +label = "Bezpieczeństwo i klasyfikacja" + +[portal.connections.categories.signing] +description = "Wyślij dokument do podpisu lub poświadczenia notarialnego." +label = "E-podpis i poświadczenie notarialne" + +[portal.connections.categories.storage] +description = "Gdzie trafiają przetworzone dokumenty — bucket, dysk, zgłoszenie lub strona." +label = "Plik i załącznik" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling przechowuje to zaszyfrowane i nigdy więcej tego nie pokazuje." +label = "Token dostępu" + +[portal.connections.commonFields.apiKey] +label = "Klucz API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Kroki używające tego połączenia mogą wywoływać tylko ścieżki pod tym URL, nigdy inny host." +label = "URL serwera" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Hasło" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Nazwa użytkownika" + +[portal.connections.commonFields.webhookUrl] +helperText = "Wklej URL podany przez usługę. Można wywoływać tylko ścieżki pod nim." +label = "Webhook URL" + +[portal.connections.fields] +name = "Nazwa połączenia" +namePlaceholder = "np. Produkcja" + +[portal.connections.picker] +cancel = "Anuluj" +createNew = "Nowe połączenie..." +placeholder = "Wybierz połączenie" +save = "Zapisz połączenie" + +[portal.connections.picker2] +back = "Zmień integrację" +heading = "Co chcesz połączyć?" +noResultsBody = "Jeszcze tego nie obsługujemy — powiedz nam o tym, a rozważymy dodanie." +noResultsTitle = "Nic nie pasuje do \"{{query}}\"" +searchPlaceholder = "Szukaj integracji — spróbuj \"sign\", \"ocr\" lub nazwy produktu" + +[portal.connections.types.api] +description = "Wywołaj dowolne HTTP API z zasady. Wybierasz URL, uwierzytelnianie i payload." +label = "Niestandardowe API" + +[portal.connections.types.api.fields.authType] +label = "Uwierzytelnianie" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nazwa użytkownika i hasło" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Token Bearer" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token w niestandardowym nagłówku" + +[portal.connections.types.api.fields.authType.options.none] +label = "Brak" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Zaloguj się po token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Kroki używające tego połączenia mogą wywoływać tylko ścieżki pod tym URL, nigdy inny host." +label = "Bazowy URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nazwa nagłówka" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Ścieżka logowania" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Hasło" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Jeśli API odpowie linkiem do pobrania, pobieranie będzie dozwolone tylko z tych hostów. Oddziel przecinkami. Pozostaw puste, aby zezwolić tylko na własny host bazowego URL." +label = "Dozwolone hosty wyników" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Wyślij token jako ten nagłówek" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Nagłówek odpowiedzi, w którym wywołanie logowania zwraca token, np. X-Auth-Token." +label = "Nagłówek odpowiedzi tokenu" + +[portal.connections.types.api.fields.username] +label = "Nazwa użytkownika" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skanuj dokumenty pod kątem malware na serwerze, który uruchamiasz — nic nie opuszcza Twojej sieci." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Skanowanie dokumentów pod kątem wirusów i treści w zasadzie." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokuj dokumenty zawierające makra, pliki wykonywalne, skrypty lub osadzone obiekty — nie tylko znane malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Dołącz przetworzone dokumenty do strony Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Wyślij dokumenty do ConsignO Cloud (Notarius) w celu podpisu elektronicznego i poświadczenia notarialnego." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Twoje środowisko ConsignO Cloud, w tym /api/v1." +label = "URL serwera" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Klucz API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Sekret API" + +[portal.connections.types.consigno.fields.password] +label = "Hasło" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potrzebne tylko wtedy, gdy Twoja organizacja używa tenantów." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nazwa użytkownika" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Opublikuj wiadomość na Discord, gdy zasada przetwarza dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indeksuj zdarzenie audytu w Elasticsearch za każdym razem, gdy zasada obsługuje dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Odpytuj serwer FTP lub FTPS o nowe dokumenty." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Tryb połączenia" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktywny" +passive = "Pasywny (zalecane)" + +[portal.connections.types.ftp.fields.security] +helperText = "Zwykły FTP wysyła poświadczenia i pliki bez szyfrowania; wybierz FTPS, jeśli serwer to obsługuje." +label = "Szyfrowanie" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (jawne / AUTH TLS)" +implicit = "FTPS (niejawne)" +none = "Brak (zwykły FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Opublikuj wiadomość w przestrzeni Google Chat, gdy działa zasada." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Dołącz przetworzone dokumenty do zgłoszenia Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Wyślij dokument lub powiadomienie e-mailem, gdy działa zasada." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Zapisuj przetworzone dokumenty w Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Hasło aplikacji" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Wykrywaj dane osobowe w tekście dokumentu za pomocą uruchamianego przez Ciebie serwera Presidio." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Odczytuj i stosuj etykiety poufności Microsoft Purview na PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcjonalne. Potrzebne tylko do wybierania etykiet po nazwie; etykietowanie działa bez tego." +label = "ID aplikacji (klienta)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Sekret klienta" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stemplowane na każdej etykiecie jako jej SiteId, aby narzędzia zgodne z Purview wiedziały, która organizacja ją zastosowała." +label = "ID katalogu (tenanta)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Bucket S3, z którego źródła mogą czytać i do którego wyjścia potoku mogą zapisywać." +label = "Magazyn S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID klucza dostępu" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Pozostaw puste dla AWS. Ustaw to dla MinIO lub innego magazynu zgodnego z S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Tajny klucz dostępu" + +[portal.connections.types.sendgrid] +description = "Wyślij dokument lub powiadomienie e-mailem, gdy działa zasada." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Odpytuj serwer SFTP (SSH) o nowe dokumenty." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcjonalne. Odcisk palca klucza hosta serwera SHA-256 (z `ssh-keyscan host | ssh-keygen -lf -`). Po ustawieniu akceptowany jest tylko serwer przedstawiający ten klucz; gdy puste, klucz zobaczony przy pierwszym połączeniu jest przypinany, a późniejsze zmiany są odrzucane." +label = "Odcisk palca klucza hosta" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Hasło klucza" + +[portal.connections.types.sftp.fields.password] +helperText = "Pozostaw puste, jeśli zamiast tego uwierzytelniasz się kluczem prywatnym." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcjonalne. Wklej klucz prywatny OpenSSH, aby uwierzytelniać się kluczem zamiast hasłem." +label = "Klucz prywatny" + +[portal.connections.types.slack] +description = "Opublikuj wiadomość na Slack, gdy zasada przetwarza dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Obserwuj udział sieciowy SMB / CIFS (Windows lub Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Udział sieciowy" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcjonalne. Domena Windows lub grupa robocza, jeśli serwer jej używa." +label = "Domena" + +[portal.connections.types.smb.fields.share] +label = "Udział" +placeholder = "dokumenty" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Wyślij zdarzenie audytu do Splunk za każdym razem, gdy zasada obsługuje dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Wyślij zdarzenie audytu do kolektora HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Opublikuj wiadomość w kanale Teams, gdy działa zasada." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Wyślij każdy dokument lub tylko szczegóły na wybrany URL." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Uruchom scenariusz Zap lub Make z zasady." +label = "Zapier i Make" + +[portal.docs] +browse = "Przeglądaj dokumentację" +viewSource = "Zobacz źródło na GitHub" + [portal.docs.authentication] codeCaption = "każde żądanie" eyebrow = "PIERWSZE KROKI" @@ -6624,6 +7216,9 @@ eyebrow = "UMIEJĘTNOŚCI" lead = "Dołączone, nazwane możliwości, które agent wywołuje jako pojedyncze narzędzie. Każda umiejętność to deterministyczny łańcuch operacji z dołączonymi ewaluacjami." title = "Umiejętności agenta" +[portal.docs.toc] +title = "Na tej stronie" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "DOKUMENTACJA API" @@ -6697,6 +7292,8 @@ type = "Typ" user = "Użytkownik" [portal.documents.queue.empty] +connectSource = "Połącz źródło" +createPipeline = "Utwórz potok" description = "Gdy źródła będą przekazywać dokumenty do Twoich potoków, pojawią się tutaj do przeglądu." title = "Brak dokumentów w kolejce" @@ -6712,11 +7309,11 @@ editorAction = "Edytor" empty = "Żadne dokumenty nie pasują do tego filtra." rowActions = "Działania wiersza" sensitiveLabel = "Wrażliwe" -sensitiveTitle = "Wrażliwe — wymagany dostęp" [portal.documents.table.columns] action = "Potok / Działanie" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Status" time = "Czas" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Ostatnio widziano" region = "Region" status = "Status" +target = "Target" version = "Wersja" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Ten widok napotkał nieoczekiwany błąd. Spróbuj ponownie albo retry = "Spróbuj ponownie" title = "Coś poszło nie tak na tej stronie" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Wstecz" +body = "Pobierz aplikację desktopową lub hostuj serwer samodzielnie." +done = "Gotowe" +guide = "Otwórz przewodnik {{name}}" +openInBrowser = "Otwórz w przeglądarce zamiast tego" +sectionDesktop = "Aplikacja desktopowa" +sectionSelfHosted = "Self-hosted" +title = "Zainstaluj Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Uruchom oficjalny obraz na swoim serwerze." +note = "Otwiera się na :8080. Domyślne logowanie: admin / stirling." +tagline = "Uruchom serwer w kontenerze" +title = "Docker" +variantFat = "Pełny" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Zainstaluj oficjalny chart Helm." +note = "Dodaj PVC dla /configs, aby zachować ustawienia." +tagline = "Wdróż w klastrze za pomocą Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Natywne kompilacje dla Twojej dystrybucji." +guideBtn = "Zobacz opcje instalacji Linux" +note = "Kompilacje AppImage, .deb i .rpm są dostępne dla popularnych dystrybucji." +tagline = "Natywna aplikacja desktopowa dla Twojej dystrybucji" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Lub przez Homebrew" +detailBody = "Uruchom instalator albo użyj Homebrew." +downloadBtn = "Pobierz dla macOS" +tagline = "Natywna aplikacja dla Apple silicon i Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Uruchom .jar na serwerze bare metal." +downloadBtn = "Pobierz .jar" +note = "Wymaga Java 21+. Otwiera się na :8080; domyślne logowanie admin / stirling." +runLabel = "Następnie uruchom" +tagline = "Uruchom .jar na serwerze bare metal" +title = "Ręczna konfiguracja serwera" + +[portal.home.download.windows] +altLabel = "Lub przez winget" +detailBody = "Uruchom instalator albo użyj menedżera pakietów." +downloadBtn = "Pobierz instalator" +tagline = "Natywna aplikacja dla Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktywnych" -invite = "Zaproś członków zespołu" +activeOfDeployed = "{{active}} z {{total}} aktywnych w tym miesiącu" name = "Stirling PDF Editor" open = "Otwórz w przeglądarce" updated = "zaktualizowano {{time}}" +[portal.home.editor.deploy] +finish = "Zakończ wdrożenie" +options = "Zobacz opcje wdrożenia" +start = "Wdróż Edytor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Hostowane samodzielnie · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dzień dobry" evening = "Dobry wieczór" morning = "Dzień dobry" -[portal.home.onboarding] -dismiss = "Odrzuć konfigurację" -notStarted = "Nie rozpoczęto" -progress = "Ukończono {{done}} z {{total}}" -title = "Dokończ konfigurację" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC w całej organizacji, uzgodnione ceny wolumenowe i wdrożenie air-gapped." -cta = "Rozpocznij wersję próbną" -ctaQuote = "Uzyskaj wycenę" -lead = "Dla ponad 250 pracowników." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Wdróż aplikację desktopową, darmową na zawsze." -title = "Pobierz PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktywnych · {{recommended}} zalecanych" -chip = "{{active}} aktywnych" -title = "Włącz zasady" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} połączonych · każdy PDF objęty zasadami tam, gdzie trafia" -chip = "{{connected}} połączonych" -title = "Połącz swoje źródła" - -[portal.home.quickActions] -subtitle = "Najważniejsze zadania na dziś" -title = "Szybkie działania" - -[portal.home.quickActions.buildPipeline] -blurb = "3-etapowy kreator oparty na typowanej bibliotece operacji" -title = "Zbuduj potok" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenci, webhooki, obserwowane foldery" -title = "Połącz źródło" - -[portal.home.quickActions.issueApiKey] -blurb = "Klucz o określonym zakresie z limitami szybkości i listą dozwolonych IP" -title = "Wydaj klucz API" - [portal.infrastructure] manageEditorDeployment = "Zarządzaj wdrożeniem Edytora" sectionsAriaLabel = "Sekcje infrastruktury" @@ -6878,13 +7541,9 @@ heading = "Klucze API" subheading = "Dane uwierzytelniające o określonym zakresie z limitami szybkości dla klucza, uprawnieniami i listami dozwolonych IP." [portal.infrastructure.apiKeys.card] -allowedIps = "Dozwolone IP" -anyIp = "Dowolne IP (brak listy dozwolonych)" created = "Utworzono" lastUsed = "Ostatnio użyto" -permissions = "Uprawnienia" -rateLimit = "Limit szybkości" -rateLimitValue = "{{value}} żądań/min" +revoke = "Unieważnij klucz" usageMonth = "Użycie w tym miesiącu" usageToday = "Użycie dzisiaj" @@ -6892,6 +7551,15 @@ usageToday = "Użycie dzisiaj" description = "Utwórz klucz o określonym zakresie, aby zacząć wywoływać Stirling API." title = "Brak kluczy API" +[portal.infrastructure.apiKeys.error] +load = "Nie udało się wczytać Twoich kluczy API. Spróbuj ponownie." + +[portal.infrastructure.apiKeys.revoke] +body = "Unieważnić „{{name}}”? Każda integracja nadal używająca tego klucza natychmiast zacznie otrzymywać błędy 401. Tej operacji nie można cofnąć." +cancel = "Anuluj" +confirm = "Unieważnij klucz" +title = "Unieważnij klucz API" + [portal.infrastructure.audit] filterAriaLabel = "Filtruj zdarzenia audytu według kategorii" heading = "Dzienniki audytu" @@ -6935,6 +7603,7 @@ all = "Wszystkie" auth = "Uwierzytelnianie" config = "Konfiguracja" elevation = "Podniesienie uprawnień" +policy = "Zasada" processing = "Przetwarzanie" security = "Bezpieczeństwo" @@ -6944,19 +7613,30 @@ title = "Nie masz dostępu" [portal.infrastructure.audit.metrics] config = "Konfiguracja" -elevation = "Podniesienie uprawnień" +policy = "Zasada" processing = "Przetwarzanie" totalEvents = "Łącznie zdarzeń · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Uwierzytelnianie" +config = "Konfiguracja" +elevation = "Podniesienie uprawnień" +policy = "Zasada" +processing = "Przetwarzanie" +security = "Bezpieczeństwo" + +[portal.infrastructure.auditStatusLabel] +danger = "Błąd" +info = "Informacja" +success = "Sukces" +warning = "Ostrzeżenie" + [portal.infrastructure.createKey] cancel = "Anuluj" createKey = "Utwórz klucz" done = "Gotowe" -ipAllowlistHelper = "Zakresy CIDR rozdzielone przecinkami. Pozostaw puste, aby zezwolić na dowolne IP." -ipAllowlistLabel = "Lista dozwolonych IP" keyNameLabel = "Nazwa klucza" keyNamePlaceholder = "np. Produkcja · ingest" -permissionsLabel = "Uprawnienia" secretKeyCaption = "Tajny klucz" secretWarning = "Przechowuj to w menedżerze sekretów. Stirling przechowuje wyłącznie hash — nie ma możliwości późniejszego odzyskania." subtitle = "Ogranicz zakres klucza do minimum, którego potrzebuje. Możesz go rotować lub unieważnić w dowolnym momencie." @@ -6964,207 +7644,9 @@ subtitleCreated = "Skopiuj ten sekret teraz — nie zostanie pokazany ponownie." title = "Utwórz klucz API" titleCreated = "Klucz utworzony" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Wdrożone przez" -environment = "Środowisko" -product = "Produkt" -status = "Status" -version = "Wersja" -when = "Kiedy" - -[portal.infrastructure.deployments.recent] -heading = "Ostatnie wdrożenia" -subheading = "Najnowsze wdrożenia produktów i środowisk." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instancje" -latency = "Opóźnienie" -load = "Obciążenie" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Przepustowość" -uptime = "Czas działania" -version = "Wersja" - -[portal.infrastructure.deployments.regions] -heading = "Regiony" -subheading = "Bieżąca kondycja każdego wdrożonego regionu Stirling — opóźnienie, obciążenie i wersja wdrożenia." - -[portal.infrastructure.deployments.regions.empty] -description = "Wdrożone regiony pojawią się tutaj po aprowizacji przestrzeni roboczej." -title = "Brak wdrożonych regionów" - -[portal.infrastructure.models] -heading = "Modele" -msValue = "{{value}} ms" -subheading = "Katalog modeli i routing, które napędzają przetwarzanie dokumentów w całej przestrzeni roboczej." - -[portal.infrastructure.models.byom] -description = "Zarejestruj model on-prem lub hostowany samodzielnie i przypnij go do regionu w celu przetwarzania związanego z rezydencją danych." -title = "Bring your own model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Modele zarządzane dostępne w Twojej przestrzeni roboczej, z bieżącym opóźnieniem i kosztem." -subEnterprise = "Modele zarządzane, bring-your-own i on-prem — z dostępnym przypinaniem do regionów." - -[portal.infrastructure.models.catalogue.empty] -description = "Modele w katalogu Twojej przestrzeni roboczej pojawią się tutaj." -title = "Brak dostępnych modeli" - -[portal.infrastructure.models.columns] -cost = "Koszt" -latency = "Opóźnienie" -load = "Obciążenie" -model = "Model" -status = "Status" -type = "Typ" -version = "Wersja" - -[portal.infrastructure.models.metrics] -activeModels = "Aktywne modele" -avgLatency = "Śr. opóźnienie" -included = "Wliczone" -monthlySpend = "Miesięczne wydatki na modele" - -[portal.infrastructure.models.routing] -empty = "Nie skonfigurowano reguł routingu." -heading = "Reguły routingu" -sub = "Który model obsługuje każdą operację. Domyślny jest stosowany, gdy nie pasuje żadna węższa reguła." -subLocked = "Kieruj operacje do określonych modeli — dostępne w płatnych planach." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Ulepsz do Pro, aby kontrolować, który model obsługuje każdą operację i typ dokumentu." -title = "Routing modeli to funkcja płatna" - -[portal.infrastructure.models.routingColumns] -default = "Domyślny" -docType = "Typ dokumentu" -modelForAria = "Model dla {{operation}}" -operation = "Operacja" -routedTo = "Kierowane do" - -[portal.infrastructure.security.access.byok] -description = "Dostarcz klucz z własnego KMS. Stirling szyfruje nim, ale nadal może odczytywać." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Klucze nigdy nie opuszczają Twojego KMS. Stirling przechowuje tylko szyfrogram." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling zarządza kluczami szyfrowania. Najprostsze — zero operacji na kluczach po Twojej stronie." -label = "Klucze przechowywane przez Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Zasada dostępu do dokumentów" -subheading = "Kontroluje, kto może odszyfrować przetworzone dokumenty w spoczynku." - -[portal.infrastructure.security.attestations] -heading = "Atestacje zgodności" -noReport = "Brak dostępnego raportu" -subheading = "Stan audytu według frameworków, z raportami dostępnymi dla poświadczonych kontroli." -viewReport = "Wyświetl raport →" - -[portal.infrastructure.security.compliance] -heading = "Zgodność" -subheading = "Atestacje i certyfikaty obejmujące platformę Stirling." - -[portal.infrastructure.security.empty] -description = "Konfiguracja bezpieczeństwa Twojej przestrzeni roboczej pojawi się tutaj." -title = "Stan bezpieczeństwa niedostępny" - -[portal.infrastructure.security.hyokBanner] -description = "Dzięki HYOK klucze szyfrowania nigdy nie opuszczają Twojego KMS. Stirling przechowuje i przetwarza tylko szyfrogram, który możesz w każdej chwili unieważnić." -title = "Stirling nie może odszyfrować Twoich dokumentów" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nie skonfigurowano zakresów IP — wszystkie IP są dozwolone." -heading = "Lista dozwolonych IP" -sub = "Dostęp do API jest ograniczony do tych zakresów CIDR." -subLocked = "Ogranicz dostęp do API do znanych zakresów IP — dostępne w płatnych planach." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Ulepsz do Pro, aby ograniczyć dostęp do API do określonych sieci." -title = "Lista dozwolonych IP to funkcja płatna" - -[portal.infrastructure.security.ipColumns] -added = "Dodano" -addedBy = "Dodane przez" -cidr = "CIDR" -label = "Etykieta" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorytm" -heading = "Zarządzanie kluczami szyfrowania" -keyId = "Identyfikator klucza" -lastRotated = "Ostatnio rotowano" -rotateKey = "Rotuj klucz" -rotationPolicy = "Zasada rotacji" -subheading = "Opieka nad kluczami szyfrującymi dokumenty w spoczynku — kto może odszyfrować i jak rotują klucze." - -[portal.infrastructure.security.managedBanner] -description = "Opieka bring-your-own-key (BYOK) i hold-your-own-key (HYOK) jest dostępna w Enterprise. Ulepsz, aby dostarczać klucze z własnego KMS." -title = "Klucze są zarządzane przez Stirling w Twoim planie" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Azja i Pacyfik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · granica danych GDPR" -label = "Unia Europejska" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Stany Zjednoczone" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezydencja danych" -subheading = "Gdzie dokumenty są przechowywane i przetwarzane." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} użyte" - -[portal.infrastructure.storage.empty] -description = "Połączony magazyn i użycie pojawią się tutaj." -title = "Brak skonfigurowanego magazynu" - -[portal.infrastructure.storage.lifecycle] -active = "Aktywne" -activeRange = "0–{{value}}d" -archived = "Zarchiwizowane" -coldStorage = "cold storage" -deleted = "Usunięte" -never = "nigdy" -purged = "wyczyszczone" - -[portal.infrastructure.storage.providers] -connect = "Połącz" -connected = "Połączono" -heading = "Połączeni dostawcy" -subheading = "Gdzie zapisywane są przetworzone artefakty." - -[portal.infrastructure.storage.retention] -heading = "Retencja" -subheading = "Jak długo artefakty są przechowywane przed usunięciem w cyklu życia." -windowLabel = "Domyślne okno retencji" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dzień" -days_other = "{{count}} dni" -never = "Nigdy nie usuwaj" - -[portal.infrastructure.storage.totalUsage] -heading = "Łączne użycie" -progressLabel = "Użyty magazyn" -subheading = "Magazyn wykorzystany we wszystkich połączonych dostawcach." +[portal.infrastructure.keyLabel] +active = "Aktywny" +revoked = "Unieważniony" [portal.infrastructure.tabs] apiKeys = "Klucze API" @@ -7174,6 +7656,49 @@ models = "Modele" security = "Bezpieczeństwo" storage = "Magazyn" +[portal.integrations] +availableHeading = "Dostępne" +comingSoonHeading = "Wkrótce" +connect = "Połącz" +connectionCount_one = "{{count}} połączenie" +connectionCount_other = "{{count}} połączeń" +customApi = "Niestandardowe API" +searchPlaceholder = "Szukaj integracji" +subtitle = "Wszystko, z czym Stirling może się komunikować. Dane uwierzytelniające są tworzone raz tutaj, a następnie używane przez źródła, zasady i potoki." +title = "Integracje" +typedName = "Nazwa {{tool}}" + +[portal.integrations.filters] +all = "Wszystkie" +audit = "Audyt" +connected = "Połączone" +notify = "Powiadomienia" +security = "Bezpieczeństwo" +signing = "Podpisywanie" +storage = "Magazyn" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Połączono" + +[portal.integrations.table] +integration = "Integracja" +worksWith = "Współpracuje z" + +[portal.integrations.worksWith] +pipelines = "Potoki" +policies = "Zasady" +sources = "Źródła" + +[portal.legal] +draft = "{{label}} · wersja robocza" +loadError = "Nie udało się wczytać tego dokumentu. Spróbuj ponownie." +loading = "Wczytywanie…" +title = "Dokument prawny" + [portal.nav] agent-builder = "Kreator agentów" components = "Komponenty" @@ -7182,33 +7707,17 @@ documents = "Dokumenty" editor = "Edytor" home = "Strona główna" infrastructure = "Infrastruktura" +integrations = "Integracje" pipelines = "Potoki" policies = "Zasady" procurement = "Zakupy" -settings = "Ustawienia" sources = "Źródła" usage = "Użycie i rozliczenia" users = "Użytkownicy" -[portal.notifications] -markAllRead = "Oznacz wszystkie jako przeczytane" -title = "Powiadomienia" -viewAll = "Wyświetl wszystkie" - -[portal.notifications.ariaLabel] -none = "Powiadomienia, brak nieprzeczytanych" -unread_one = "Powiadomienia, {{count}} nieprzeczytane" -unread_other = "Powiadomienia, {{count}} nieprzeczytanych" - -[portal.notifications.count] -allRead = "wszystkie przeczytane" -loading = "ładowanie" -new_one = "{{count}} nowe" -new_other = "{{count}} nowych" - -[portal.notifications.empty] -description = "Brak nowych powiadomień." -title = "Wszystko nadrobione" +[portal.nav.section] +platform = "Platforma PDF" +processor = "Procesor PDF" [portal.pipelines] subtitle = "Każdy zautomatyzowany potok dokumentów na backendzie: uporządkowany łańcuch operacji na zestawie źródeł, uruchamiany przez wyzwalacz. Kliknij wiersz, aby zobaczyć jego kroki i źródła." @@ -7218,54 +7727,87 @@ title = "Potoki" newPipeline = "Nowy potok" [portal.pipelines.builder] -addStep = "Dodaj narzędzie" +activate = "Activate" back = "Wróć do potoków" +cannotFollow = "Nie może przyjąć {{produced}}" +chooseAccount = "Wybierz konto" +chooseDestination = "Wybierz miejsce docelowe" +chooseOperation = "Wybierz, co robi ten krok" +chooseSource = "Wybierz źródło" discard = "Odrzuć zmiany" -enabled = "Włączone" +inputs = "Wejście" +inputSource = "Źródło wejściowe" +inputTrigger = "Wyzwalacz" keepEditing = "Kontynuuj edycję" -needsUpload = "Wymaga przesłanego pliku" +moreActions = "More actions" +needsConfiguring = "Wymaga konfiguracji" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Żadne narzędzia nie pasują do wyszukiwania." -pipelineSettings = "Ustawienia potoku" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Szukaj narzędzi" -selectToolBody = "Dodaj narzędzie, aby zbudować potok." -selectToolTitle = "Brak narzędzi" -toolSettings = "Ustawienia narzędzia" +sendToSystem = "Wyślij do innego systemu" +stepsIncompatible = "Te kroki nie mogą działać na tym, co wytwarza poprzedni krok: {{tools}}." +stepsNeedSetup = "Te kroki nadal wymagają konfiguracji przed zapisaniem: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Nierozpoznana operacja, zachowana bez zmian." unsavedBody = "Masz niezapisane zmiany. Zapisać je przed opuszczeniem czy odrzucić?" unsavedTitle = "Niezapisane zmiany" -uploadUnsupported = "Przesłane pliki nie są jeszcze obsługiwane w potokach, więc tych kroków nie można zapisać: {{tools}}." usesDefaults = "Uruchamia się z ustawieniami domyślnymi" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Łączy każdy plik z poprzedniego kroku" +fan-out = "Uruchamia się raz dla każdego pliku z poprzedniego kroku" +format-mismatch = "Wymaga {{accepts}}, ale poprzedni krok wytwarza {{produced}}" +output-uncertain = "Może się nie uruchomić: wynik poprzedniego kroku zależy od jego konfiguracji" +source-mismatch = "Wymaga {{accepts}}, ale wejściem tego potoku jest {{produced}}" +undeclared-operation = "Nie można sprawdzić, co akceptuje ten krok" [portal.pipelines.composer] addTool = "Dodaj narzędzie" -cancel = "Anuluj" -chainEmpty = "Dodaj narzędzie, aby rozpocząć budowanie potoku." create = "Utwórz potok" -directory = "Folder wyjściowy" -directoryHelp = "Ścieżka bezwzględna na serwerze. Musi znajdować się w skonfigurowanych dozwolonych folderach." +createPaused = "Create paused" editingUnsupported = "Wyświetlanie tych parametrów narzędzia do edycji nie jest jeszcze obsługiwane." -moveDown = "Przenieś w dół" -moveUp = "Przenieś w górę" +editSource = "Edit source" name = "Nazwa" namePlaceholder = "np. Przegląd redakcji" -noSources = "Nie połączono jeszcze żadnych źródeł. Potok nadal może działać na plikach dostarczonych bezpośrednio do niego." noToolSettings = "To narzędzie nie ma konfigurowalnych ustawień." -operations_one = "Operacja ({{count}})" -operations_other = "Operacje ({{count}})" output = "Wyjście" -removeStep = "Usuń operację" save = "Zapisz zmiany" scheduleEvery = "Uruchamiaj co" -sources = "Źródła" -sourcesLoading = "Ładowanie źródeł..." trigger = "Wyzwalacz" triggerManual = "Tylko ręcznie" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dni" hours = "godzin" minutes = "minut" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Usunąć \"{{name}}\"? Tego nie można cofnąć." cancel = "Anuluj" @@ -7273,28 +7815,62 @@ confirm = "Usuń" title = "Usunąć potok?" [portal.pipelines.detail] +clearHistory = "Wyczyść historię" delete = "Usuń potok" run = "Uruchom teraz" [portal.pipelines.empty] action = "Utwórz potok" +connectSource = "Połącz źródło" description = "Utwórz swój pierwszy potok: wybierz źródła, na których działa, połącz operacje w łańcuch i wybierz miejsce zapisu wyjścia." title = "Brak potoków" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktywne" paused = "Wstrzymane" total = "Potoki" -[portal.pipelines.output] -folder = "Zapisz do folderu" -inline = "Zwróć pliki" - [portal.pipelines.run] +allProcessed_one = "Nic do uruchomienia: {{count}} dokument ze źródła został już przetworzony." +allProcessed_other = "Nic do uruchomienia: wszystkie {{count}} dokumenty w źródłach zostały już przetworzone." completed_one = "Uruchomienie zakończone." completed_other = "Wszystkie {{count}} uruchomienia zakończone." empty = "Nie ma nic do uruchomienia: źródła nie miały dokumentów do przetworzenia." failed = "Uruchomienie nie powiodło się: {{error}}" +inFlight = "Nic nowego do uruchomienia: dokumenty są nadal przetwarzane z wcześniejszego uruchomienia." +parked_one = "Nic do uruchomienia: {{count}} dokument wcześniej zakończył się niepowodzeniem i jest zaparkowany. Usuń przyczynę, a następnie wyczyść historię, aby spróbować ponownie." +parked_other = "Nic do uruchomienia: {{count}} dokumentów wcześniej zakończyło się niepowodzeniem i jest zaparkowanych. Usuń przyczynę, a następnie wyczyść historię, aby spróbować ponownie." running = "Uruchomienie rozpoczęte; nadal trwa." timeout = "Uruchomienie trwa dłużej niż oczekiwano; może nadal zakończyć się w tle." @@ -7307,6 +7883,7 @@ name = "Potok" sources = "Źródła" status = "Status" steps = "Kroki" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Obserwacja folderu" @@ -7314,12 +7891,18 @@ manual = "Ręcznie" schedule = "Zaplanowane" [portal.policies] +defaultName = "Zasada {{category}}" subtitle = "Stałe automatyzacje, które wymuszają potok narzędzi na każdym dokumencie. Każda zasada uruchamia się przy przesyłaniu lub eksporcie, wykonuje swój łańcuch narzędzi i zapisuje wymuszoną wersję obok oryginału." title = "Zasady" [portal.policies.card] comingSoon = "Ulepsz do Enterprise" notSetUp = "Nie skonfigurowano" +requiresAiEngine = "Wymaga silnika AI" + +[portal.policies.categories.classification] +desc = "Identyfikuj typ każdego dokumentu podczas przesyłania i oznaczaj jego metadane do archiwizacji i wyszukiwania." +label = "Klasyfikacja" [portal.policies.categories.compliance] desc = "Wymuszaj wymagania HIPAA, GDPR, SOC 2 lub FedRAMP na każdym dokumencie." @@ -7344,6 +7927,13 @@ label = "Bezpieczeństwo" [portal.policies.config] scopeAll = "Wszystkie dokumenty" +[portal.policies.config.classification] +summary = "Klasyfikuje każdy przesłany dokument według etykiet zespołu i oznacza jego metadane." + +[portal.policies.config.classification.rules] +0 = "Klasyfikuj" +1 = "Oznacz metadane" + [portal.policies.config.compliance] summary = "Waliduje dokumenty względem frameworków regulacyjnych, zanim opuszczą system." @@ -7371,6 +7961,29 @@ minConfidence = "Min. pewność" 2 = "Nazwij" 3 = "Normalizuj" +[portal.policies.config.purview] +label = "Zastosuj etykietę poufności Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Dzierżawa Purview" +labelId = "ID etykiety" +labelIdHelp = "GUID etykiety z portalu Purview. Zastosowanie etykiety zapisuje jej metadane w PDF; nie może szyfrować, do czego potrzebny jest klient Microsoft." +labelName = "Nazwa etykiety (opcjonalnie)" +labelNameHelp = "Zapisywana razem z ID, aby użytkownicy i narzędzia mogły rozpoznać etykietę." + +[portal.policies.config.purview.fields.method] +help = "Purview odróżnia etykietę zastosowaną przez zasadę od tej wybranej przez osobę." +label = "Jak zapisywana jest etykieta" +privileged = "Jako wybrana przez osobę" +standard = "Jako zastosowana automatycznie" + +[portal.policies.config.purviewRead] +label = "Odczytaj etykietę Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Dzierżawa Purview" +connectionHelp = "Odczytuje etykietę, którą dokument już posiada, aby późniejsze kroki mogły na nią reagować." + [portal.policies.config.retention] summary = "Wymusza, jak długo dokumenty są przechowywane, kiedy archiwizować i kiedy usuwać." @@ -7438,6 +8051,7 @@ title = "Brak aktywności" [portal.policies.endpoints] addWatermark = "Znak wodny" autoRedact = "Redaguj PII" +classifyAndLabel = "Klasyfikuj" compressPdf = "Kompresuj" flatten = "Spłaszcz" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Twoje polityki są zapisane i pojawią się po przywróceniu poł retry = "Ponów próbę" title = "Backend niedostępny" +[portal.policies.operations] +change = "Zmień działanie tego kroku" +noResults = "Żaden krok nie pasuje. Spróbuj nazwy produktu albo „scan”, „notify”, „attach”." +searchPlaceholder = "Szukaj kroków — spróbuj „scan”, „attach” lub nazwy produktu" + +[portal.policies.operations.bodyMode] +binary = "Surowe bajty pliku" +json = "Wewnątrz ładunku JSON" +multipart = "Jako przesyłany plik (multipart)" + +[portal.policies.operations.clamavScan] +description = "Twój własny serwer ClamAV sprawdza dokument; nic nie opuszcza Twojej sieci." +label = "Skanuj w poszukiwaniu wirusów (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Odrzuca dokumenty zawierające makra, pliki wykonywalne, skrypty lub osadzone obiekty." +label = "Blokuj aktywną zawartość" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive sprawdza dokument, a uruchomienie zatrzymuje się, jeśli nie jest czysty." +label = "Skanuj w poszukiwaniu wirusów" + +[portal.policies.operations.confluenceAttach] +description = "Zapisuje przetworzony dokument na stronie." +label = "Dołącz do strony Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Przesyła dokument do ConsignO Cloud w celu podpisania." +label = "Wyślij do podpisu (ConsignO)" +note = "Przesyła dokument. Pobieranie podpisanej kopii nie jest jeszcze obsługiwane, ponieważ krok nie może przekazać wyniku do późniejszego kroku." + +[portal.policies.operations.customApiCall] +description = "Utwórz wywołanie samodzielnie: ścieżka, metoda, treść i obsługa odpowiedzi." +label = "Wywołaj niestandardowe API" + +[portal.policies.operations.discordNotify] +description = "Informuje kanał, że zasada obsłużyła dokument." +label = "Opublikuj wiadomość w Discord" + +[portal.policies.operations.elasticIndex] +description = "Zapisuje działania zasady jako dokument z możliwością wyszukiwania." +label = "Zindeksuj zdarzenie audytu w Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart pasuje do API przesyłania; JSON pasuje do API, które oczekują pliku wewnątrz ładunku; binary wysyła surowe bajty." +label = "Jak wysłać dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Treść JSON wysłana bez zmian. {{document.base64}} zawiera plik; {{document.*}} i {{run.*}} są uzupełniane dla każdego dokumentu." +label = "Treść JSON" + +[portal.policies.operations.fields.connection] +label = "Konto" + +[portal.policies.operations.fields.domain] +label = "Domena Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Pole formularza, pod którym API oczekuje dokumentu." +label = "Nazwa pola pliku" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Obiekt JSON z parami nazwa/wartość nagłówków. Dane uwierzytelniające połączenia są dodawane za Ciebie." +label = "Nagłówki" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Klucz zgłoszenia" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Wysyłane jako treść wiadomości. {{document.*}} i {{run.*}} są uzupełniane dla każdego dokumentu." +label = "Wiadomość" + +[portal.policies.operations.fields.method] +label = "Metoda" + +[portal.policies.operations.fields.pageId] +label = "ID strony" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Dołączane do bazowego URL połączenia. Nigdy nie może dotrzeć do innego hosta." +label = "Ścieżka" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Miejsce w dysku, w którym zapisać plik. {{document.filename}} jest uzupełniane dla każdego dokumentu." +label = "Ścieżka docelowa" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Raport pozostawia dokument bez zmian. Zamiana podmienia go na to, co zostanie zwrócone." +label = "Co zrobić z odpowiedzią" + +[portal.policies.operations.fields.signerEmail] +label = "Email podpisującego" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Temat" +placeholder = "Przetworzono: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizuje ten tekst. Użyj {{document.title}} lub wklej tekst, który chcesz sprawdzić." +label = "Tekst do sprawdzenia" + +[portal.policies.operations.fields.to] +label = "Do" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nazwa użytkownika Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informuje przestrzeń, że zasada obsłużyła dokument." +label = "Opublikuj wiadomość w Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Zapisuje przetworzony dokument w zgłoszeniu." +label = "Dołącz do zgłoszenia Jira" + +[portal.policies.operations.mailgunEmail] +description = "Wysyła przetworzony dokument jako załącznik." +label = "Wyślij dokument emailem (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Zapisuje przetworzony dokument w folderze." +label = "Prześlij do Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Twój własny serwer Presidio raportuje znalezione dane osobowe." +label = "Wykryj dane osobowe" +note = "Presidio odczytuje tekst, a nie pliki. Podaj tekst, który chcesz sprawdzić — sam dokument nie jest przesyłany." + +[portal.policies.operations.responseMode] +replace = "Zastąp dokument odpowiedzią" +report = "Zachowaj dokument, zapisz odpowiedź" + +[portal.policies.operations.sendgridEmail] +description = "Wysyła przetworzony dokument jako załącznik." +label = "Wyślij dokument emailem (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informuje kanał, że zasada obsłużyła dokument." +label = "Opublikuj wiadomość w Slack" + +[portal.policies.operations.splunkEvent] +description = "Zapisuje działania zasady tam, gdzie Twoi audytorzy już zaglądają." +label = "Wyślij zdarzenie audytu do Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Zapisuje działania zasady w kolektorze HTTP." +label = "Wyślij zdarzenie audytu do Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informuje kanał, że zasada obsłużyła dokument." +label = "Opublikuj wiadomość w Teams" + +[portal.policies.operations.webhookPost] +description = "Wysyła sam dokument na wybrany przez Ciebie URL." +label = "Opublikuj dokument do webhooka" + +[portal.policies.operations.zapierNotify] +description = "Przekazuje szczegóły uruchomienia do Twojej automatyzacji." +label = "Uruchom Zap lub scenariusz Make" + [portal.policies.stats] activeFor = "Aktywne" dataProcessed = "Przetworzone dane" @@ -7473,6 +8282,13 @@ label = "Egzekwowane dokumenty" description = "Skonfigurowane, ale nieuruchamiane" label = "Wstrzymane" +[portal.policies.table] +appliesTo = "Dotyczy" +docs = "Dokumenty objęte wymuszeniem" +enforces = "Wymusza" +policy = "Zasada" +status = "Status" + [portal.policies.wizard.actions] back = "Wstecz" cancel = "Anuluj" @@ -7480,6 +8296,10 @@ continue = "Kontynuuj" enablePolicy = "Włącz politykę" saveChanges = "Zapisz zmiany" +[portal.policies.wizard.capability.classify] +desc = "Identyfikuje typ dokumentu na podstawie etykiet zespołu i oznacza go, aby można było go archiwizować i wyszukiwać według kategorii." +label = "Klasyfikuj dokument" + [portal.policies.wizard.capability.compress] desc = "Kompresuje dokument do mniejszego rozmiaru pliku." label = "Zmniejsz rozmiar pliku" @@ -7500,10 +8320,18 @@ label = "Redaguj informacje poufne" desc = "Usuwa ukryty JavaScript, aby nic nie mogło uruchomić się automatycznie po otwarciu dokumentu." label = "Usuń aktywną zawartość" +[portal.policies.wizard.capability.timestampPdf] +desc = "Potwierdza, że dokument istniał dokładnie w tej formie w danym momencie, używając niezależnego urzędu znacznika czasu. Wysyłany jest tylko hash — dokument nigdy nie opuszcza Twojego serwera." +label = "Dodaj zaufany znacznik czasu" + [portal.policies.wizard.capability.watermark] desc = "Umieszcza widoczny znak (np. „Poufne”) na każdej stronie." label = "Zastosuj znak wodny" +[portal.policies.wizard.classification] +description = "Każdy przesłany dokument jest klasyfikowany według wbudowanych etykiet i oznaczany pasującymi typami. Zestaw etykiet jest wspólny dla całego zespołu." +labelsHeading = "Etykiety klasyfikacji" + [portal.policies.wizard.errors] noTools = "Najpierw włącz co najmniej jedno narzędzie w przepływie pracy." saveFailed = "Nie udało się zapisać polityki. Spróbuj ponownie." @@ -7548,73 +8376,118 @@ setUp = "Skonfiguruj politykę {{category}}" [portal.policies.wizard.workflow] description = "Wybierz, co ta polityka robi z każdym przetwarzanym dokumentem." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktywne" -noRule = "Nie egzekwowano jeszcze żadnej reguły" -subtitle = "Stałe automatyzacje, przez które przechodzi każdy dokument, niezależnie od tego, który potok go obsługuje." -title = "Co działa na Twoich PDF" - [portal.policySummary.action] -comingSoon = "Przejdź na Enterprise" -configure = "Skonfiguruj" setUp = "Skonfiguruj" -[portal.policySummary.column] -activeRule = "Aktywna reguła" -policy = "Polityka" -status = "Status" +[portal.processorFlow] +footnote = "Liczby obejmują ostatnie 24 godziny. Szybkość przepływu ma charakter poglądowy." +liveBadge = "Na żywo" +stats = "{{connected}} połączonych · {{processed}} przetworzonych PDF" +title = "Procesor PDF" -[portal.policySummary.empty] -description = "Po skonfigurowaniu polityk kategorie pojawią się tutaj." -title = "Brak polityk" +[portal.processorFlow.lens] +ariaLabel = "Tryb widoku" +flow = "Przepływ" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktywna" -off = "Wyłączona" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Niepowodzenie" +heading = "Wyniki" +success = "Dostarczono" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktywnych" +count = "{{n}} · 24h" +heading = "Zasady" +setUp = "Skonfiguruj" soon = "Wkrótce" -[portal.processingStatus] -managePlan = "Zarządzaj planem" -volumeSuffix = "Przetworzone pliki PDF · ostatnie 30 dni" +[portal.processorFlow.sankey] +waist = "Zasady · {{n}} aktywnych" + +[portal.processorFlow.sankey.empty] +description = "Połącz źródło i włącz zasadę, aby obserwować przepływ dokumentów przez procesor." +title = "Brak przepływu" + +[portal.processorFlow.sources] +comingSoonTag = "Połącz" +editor = "Stirling PDF Editor" +heading = "Źródła" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Odbiór emaili" [portal.procurement] -reset = "Zresetuj zakupy (demo)" subtitle = "Przeprowadź swój zespół przez ocenę, umowę i wdrożenie. Każdy dokument w jednym miejscu." title = "Zakupy" -[portal.procurement.action] -download = "Pobierz" -pay = "Zapłać teraz" -request = "Poproś" -sign = "Przejrzyj i podpisz" -upload = "Prześlij" - [portal.procurement.agreement] agreeCta = "Zgódź się i subskrybuj" +confidential = "Poufne" confirm = "Przeczytałem(-am) i akceptuję Stirling Enterprise Agreement." -eyebrow = "Umowa" -intro = "Jedna połączona umowa obejmuje Twoją ofertę: Master Service Agreement, formularz zamówienia, EULA i umowę przetwarzania danych. Przejrzyj ją, a następnie zaakceptuj, aby przyjąć wycenę jako zobowiązaną subskrypcję." -title = "Przejrzyj swoją umowę enterprise" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA i DPA połączone w jeden podpis." +download = "Pobierz" +downloadDraftError = "Nie udało się wygenerować PDF umowy — renderer dokumentów jest niedostępny. Spróbuj ponownie wkrótce lub skontaktuj się z pomocą techniczną." +downloadError = "Nie udało się pobrać podpisanej umowy. Spróbuj ponownie." +legalName = "Nazwa podmiotu prawnego" +legalNamePlaceholder = "Podmiot prawny, który podpisze" +loadError = "Nie udało się wczytać umowy. Spróbuj ponownie." +loading = "Wczytywanie umowy..." +ref = "Nr ref. {{ref}} · {{version}}" +requestChanges = "Poproś o zmiany" +scrollHint = "Przewiń, aby sprawdzić" +signatory = "Imię i nazwisko podpisującego" +signatoryPlaceholder = "Imię i nazwisko" +signatoryTitle = "Tytuł podpisującego" +signatoryTitlePlaceholder = "np. General Counsel" +signError = "Nie udało się zarejestrować Twojego podpisu. Spróbuj ponownie." [portal.procurement.builder] addons = "Dodatki" +addressLine1 = "Adres, wiersz 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adres, wiersz 2" +addressLine2Placeholder = "Apartament, piętro, budynek" back = "Wstecz" businessName = "Nazwa firmy" businessNamePlaceholder = "Twoja firma" +city = "Miasto" +cityPlaceholder = "San Francisco" +completeRequired = "Uzupełnij wymagane pola (oznaczone *) prawidłowym adresem email przed wygenerowaniem wyceny." +contactEmail = "Email kontaktowy" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Imię i nazwisko osoby kontaktowej" +contactNamePlaceholder = "Jane Doe" continue = "Kontynuuj" -country = "Kraj" -countryEuro = "Strefa euro (EUR €)" -countryUK = "Wielka Brytania (GBP £)" -countryUS = "Stany Zjednoczone (USD $)" +done = "Gotowe" eula = "Przeczytałem(-am) i akceptuję Stirling Enterprise EULA. Reguluje ona umowę wygenerowaną na podstawie tej wyceny." generate = "Wygeneruj wycenę" -included = "Wliczone" indemnification = "Odszkodowanie z tytułu IP" indemnificationSub = "Bronimy kwalifikujących się roszczeń IP zgodnie z EULA" -offlineLicense = "Licencja offline / air-gapped" -offlineLicenseSub = "Plik licencji do pobrania dla izolowanej, self-hosted instancji" +paperEyebrow = "Wycena Enterprise" +paperFor = "Przygotowano dla" +pdfSize = "Rozmiar PDF" +poNumber = "Numer zamówienia" +poNumberPlaceholder = "Opcjonalnie" +postalCode = "Kod pocztowy" +postalCodePlaceholder = "94105" +posture = "Ład organizacyjny" +posture_count = "~{{count}} zasad" +posture_essentials = "Podstawowe" +posture_essentialsSub = "Klasyfikacja + Udostępnianie — ustawienia domyślne" +posture_governed = "Zarządzane" +posture_governedSub = "Dodaje Bezpieczeństwo i Routing" +posture_regulated = "Regulowane" +posture_regulatedSub = "Każda kategoria — Zgodność, Retencja, Pobieranie" qbr = "Kwartalne przeglądy biznesowe" qbrSub = "Twój SE przegląda wykorzystanie i plan rozwoju co kwartał" +region = "Stan / region" +regionPlaceholder = "Kalifornia" running = "{{annual}} / rok · {{years}}-letni {{tcv}}" s1Sub = "Twój zespół i pliki PDF, które planujesz przetwarzać każdego roku." s1Title = "Ile będziesz przetwarzać?" @@ -7623,6 +8496,12 @@ s2Title = "Zobowiązanie i usługa" s3Sub = "Do wyceny i umowy, którą generuje." s3Title = "Twoje dane" serviceLevel = "Poziom usługi" +size_compact = "Kompaktowy" +size_compactSub = "Głównie tekst, poniżej 1 MB" +size_heavy = "Ciężki" +size_heavySub = "Skanowany lub z dużą liczbą obrazów, 5 MB+" +size_standard = "Standard" +size_standardSub = "Mieszany tekst i obrazy, od 1 do 5 MB" slDedicated = "Dedykowany" slDedicatedSub = "4 godziny robocze · dedykowany opiekun konta · +30%" slPriority = "Priorytetowy" @@ -7630,6 +8509,8 @@ slPrioritySub = "Ten sam dzień roboczy · wskazany CSM · +15%" slStandard = "Standardowy" slStandardSub = "Następny dzień roboczy · współdzielony CSM · wliczone" stepOf = "Krok {{n}} z {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Opcjonalnie" term = "Okres" termDiscount = "Zastosowano {{pct}}% rabatu za wieloletnie zobowiązanie" title = "Utwórz wycenę" @@ -7637,6 +8518,7 @@ training = "Wdrożenie i szkolenie" trainingSub = "Sesje na żywo, aby uruchomić Twój zespół" users = "Łączna liczba użytkowników" usersPlaceholder = "np. 250" +viewEula = "Przeczytaj" volEstimated = "Oszacowano na podstawie {{count}} użytkowników (~2 000 PDF każdy, w tym automatyzacja). Edytuj, jeśli znasz lepszą wartość." volManual = "Używamy podanej przez Ciebie wartości. W każdej chwili możesz ponownie oszacować na podstawie wielkości zespołu." volNoUsers = "Nie masz pewności? Wpisz wielkość zespołu, a oszacujemy ją." @@ -7645,55 +8527,55 @@ volumePlaceholder = "np. 1 000 000" years_one = "{{count}} rok" years_other = "{{count}} lat" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumentów" -done = "Gotowe" -here = "Jesteś tutaj" -hide = "Ukryj" -optional = "Opcjonalne" -paidAddon = "Płatny dodatek" -show = "Pokaż" -subtitle = "Wszystko, czego potrzebujesz na każdym etapie procesu, wyświetlane w miarę postępu transakcji." -supportingSubtitle = "SOC 2, przeglądy bezpieczeństwa, formularze podatkowe i więcej — gotowe, gdy poprosi o nie Twój zespół bezpieczeństwa lub zakupów. Niektóre wiążą się z jednorazową opłatą." -supportingTitle = "Wsparcie Twojej oceny" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form i DPA w jednym podpisie." +download = "Pobierz" +eula = "EULA i warunki komercyjne" +eulaSub = "Oprogramowanie i warunki komercyjne samoobsługi." +invoice = "Faktura" +invoiceSub = "Twoja pierwsza faktura za subskrypcję." +laterInvoice = "Dostępne po zaakceptowaniu" +laterQuote = "Dostępne po wystawieniu wyceny" +quote = "Wycena" +quoteSub = "Twoja szczegółowa wycena Enterprise." +sla = "Załącznik SLA" +slaSub = "Poziomy wsparcia i docelowe czasy odpowiedzi." +subprocessors = "Podprocesorzy" +subprocessorsSub = "Strony trzecie, które przetwarzają dane w Twoim imieniu." +subtitle = "Dokumenty transakcji, dostępne w dowolnym momencie." title = "Dokumenty" -upcoming = "Nadchodzące" +view = "Zobacz" [portal.procurement.error] title = "Coś poszło nie tak" [portal.procurement.hero] -company = "Twoja umowa enterprise" +barAria = "Etap {{current}} z {{total}}" ctaAgreement = "Przejrzyj i podpisz umowę" -ctaLive = "Działasz już na żywo" -ctaPayment = "Dodaj płatność" +ctaExploring = "Skonfiguruj wersję próbną" ctaQuote = "Przejrzyj wycenę" +ctaReviewQuote = "Sprawdź wycenę" ctaTrial = "Utwórz wycenę" +documents = "Dokumenty" eyebrow = "Zakupy enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Zaproś członków zespołu" -keyDocs = "Kluczowe dokumenty" -nextStep = "Następny krok: {{action}}" -notStarted = "Nie rozpoczęto" +licenseKey = "Klucz licencji" +liveSub = "Twoja organizacja została przygotowana, a licencja jest aktywna." +liveTitle = "Działasz już na Stirling Enterprise" +next = "Dalej: {{stage}}" open = "Otwórz zakupy" scheduleCall = "Zaplanuj rozmowę" -setup1Sub = "Zaproś członków zespołu" -setup1Title = "Wdróż PDF Editor" -setup2Sub = "Włącz przetwarzanie w edytorach i innych źródłach" -setup2Title = "Połącz PDF Processor" -setup3Sub = "Włącz Bezpieczeństwo, Zgodność, Routing lub Retencję, gdy ich potrzebujesz" -setup3Title = "Dodaj zalecane polityki" +sentenceAgreement = "sprawdzenie i podpisanie umowy" +sentenceLive = "prowizjonowanie i wdrożenie" +sentencePayment = "zamówienie zakupu i płatność" +sentenceQuote = "sprawdzenie i zaakceptowanie wyceny" +sentenceTrial = "konfiguracja próbnego obszaru roboczego" [portal.procurement.journey] daysLeft_one = "Pozostał {{count}} dzień" daysLeft_other = "Pozostało {{count}} dni" -engineerLabel = "Twój solutions engineer" -eyebrow = "Twoje wdrożenie" -live = "Działasz już na Stirling Enterprise" -nextStep = "Następny krok: {{action}}" -subtitle = "Twój solutions engineer jest z Tobą na każdym etapie. Jedno następne działanie naraz; pełna lista kontrolna znajduje się poniżej." -title = "Od wersji próbnej do działania na żywo — jedna prowadzona ścieżka" -trialTitle = "Wersja próbna Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Jeden podpis obejmuje MSA, formularz zamówienia, EULA i DPA." @@ -7727,6 +8609,9 @@ downloadError = "Nie udało się jeszcze wygenerować pliku licencji offline — downloadOffline = "Pobierz licencję offline (.lic)" hint = "Wklej ten klucz do self-hosted instancji, aby ją aktywować, albo zachowaj go w dokumentacji. Przechowuj go bezpiecznie." label = "Twój klucz licencyjny" +subtitle = "Aktywuj instancję self-hosted tym kluczem albo zachowaj go do swoich zapisów." +title = "Twój klucz licencji" +trialFileHint = "To jest plik licencji próbnej. Gdy umowa będzie gotowa, wróć i pobierz go ponownie — w przeciwieństwie do klucza licencji online plik .lic nie zaktualizuje się sam." [portal.procurement.link] cta = "Połącz konto" @@ -7739,65 +8624,62 @@ description = "Twoja subskrypcja jest aktywna, a licencja została wydana. Twój eyebrow = "Na żywo" title = "Działasz już na Stirling Enterprise" -[portal.procurement.locked] -description = "Klucze próbne, wyceny zobowiązanego wolumenu, umowa z jednym podpisem, płatność i rejestr dokumentów będą tutaj dostępne po rozpoczęciu oceny enterprise." -eyebrow = "Tylko Enterprise" -talkToSales = "Porozmawiaj ze sprzedażą" -title = "Ścieżka zakupów otwiera się z Enterprise" - [portal.procurement.milestone] -accept = "Zaakceptuj i kontynuuj" -description = "Pobierz PDF, aby udostępnić go zespołowi, wróć, aby zaakceptować, gdy będziesz gotowy, lub wprowadź zmiany." -download = "Pobierz PDF" downloadError = "Nie udało się jeszcze pobrać PDF z wyceną — spróbuj ponownie za chwilę." -edit = "Edytuj wycenę" -eyebrow = "Wycena {{number}}" -perYear = " / rok" -preparedFor = "Przygotowano dla {{company}}" -tcv = "{{value}} łącznej wartości umowy" -title = "Twoja wycena jest gotowa" - -[portal.procurement.modal] -cancel = "Anuluj" -chooseFile = "Wybierz plik" -close = "Zamknij" -downloadBody = "Pobieranie rozpocznie się wkrótce." -downloadCta = "Pobierz" -downloadTitle = "Pobierz" -noFile = "Nie wybrano pliku" -payBody = "Zapłać za zobowiązaną umowę kartą lub przelewem bankowym przez Stripe. Twój workspace zostanie przygotowany, gdy tylko płatność zostanie zaksięgowana." -payCta = "Przejdź do Stripe" -payTitle = "Potwierdź płatność" -requestBodyFree = "Generujemy to na żądanie. Potwierdź, a Twój solutions engineer wkrótce to prześle." -requestBodyPaid = "To płatny dodatek. Potwierdź, a Twój solutions engineer określi zakres i wyśle dokumenty." -requestCta = "Poproś" -requestTitle = "Poproś o ten dokument" -signBody = "Otwiera Stirling Enterprise Agreement do podpisu elektronicznego: jeden podpis obejmuje MSA, formularz zamówienia, EULA i DPA. Podpisujemy automatycznie i przechodzisz do płatności." -signCta = "Otwórz do podpisu" -signTitle = "Przejrzyj i podpisz umowę" -uploadBody = "Wyślij nam swoje PO, a wystawimy fakturę zgodnie z jego warunkami. Przeciągnij PDF lub wybierz plik poniżej." -uploadCta = "Prześlij zamówienie zakupu" -uploadTitle = "Prześlij swoje zamówienie zakupu" [portal.procurement.payment] description = "Twoja wycena została zaakceptowana, a licencja jest już aktywna — Twój zespół może zacząć od razu. Opłać pierwszą fakturę, gdy będziesz gotowy; możesz ją opłacić lub pobrać tutaj, bez potrzeby używania emaila." +downloadAgreement = "Pobierz podpisaną umowę" downloadInvoice = "Pobierz fakturę" -simulate = "Symuluj otrzymanie płatności (demo)" +eyebrow = "Płatność" title = "Subskrypcja utworzona" viewInvoice = "Wyświetl i opłać fakturę" +[portal.procurement.review] +acceptCta = "Akceptuj wycenę" +annual = "Opłata roczna (rok 1)" +downloadCta = "Pobierz wycenę" +poNumber = "Zamówienie zakupu: {{po}}" +renewal = "Odnawia się za {{amount}}/rok po zakończeniu okresu (+{{pct}}% CPI)" +tcv = "Okres {{years}} lat, płatne z góry w całości: {{tcv}}" +validUntil = "Ważne do {{date}}" + [portal.procurement.schedule] fallback = "Nie udało się załadować harmonogramu." fallbackLink = "Otwórz planowanie w nowej karcie" +loading = "Wczytywanie harmonogramu..." subtitle = "Twój solutions engineer przeprowadzi zespół przez wdrożenie. Wybierz dogodny termin." title = "Zaplanuj rozmowę" -[portal.procurement.status] -action = "Wymagane działanie" -available = "Dostępne" -complete = "Ukończone" -pending = "Oczekujące" -request = "Na żądanie" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Odizolowane, bez połączeń wychodzących" +back = "Wstecz" +businessName = "Nazwa firmy" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Zarządzane, najszybszy start" +continue = "Kontynuuj" +deployment = "Gdzie wdrożyć?" +eula = "Akceptuję Stirling EULA i warunki komercyjne." +fullName = "Imię i nazwisko" +fullNamePlaceholder = "Twoje imię i nazwisko" +invites = "Zaproś członków zespołu · opcjonalnie" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Umów rozmowę" +seats = "Osoby testujące" +seatsPlaceholder = "np. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker lub Kubernetes" +start = "Rozpocznij wersję próbną" +stepOf = "Krok {{n}} z {{total}}" +subtitle = "Bezpłatnie przez 14 dni, karta nie jest wymagana." +subtitleDetails = "Kilka informacji do wyceny i umowy." +talkFirst = "Wolisz najpierw porozmawiać?" +title = "Skonfiguruj wersję próbną" +viewEula = "Przeczytaj" +workEmail = "Email służbowy" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Przedłużenie dodaje 7 dni i powiadamia Twojego solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Limit wyczerpany" subtitle = "Twoja bezpłatna wersja próbna działa do {{date}}. Karta nie jest wymagana." title = "Wersja próbna Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Ceny dla zobowiązanego wolumenu, SSO + SCIM + RBAC dla całej organizacji, 90-dniowy niezmienny audyt i dedykowany SE." -homeCta = "Rozpocznij wersję próbną →" -homeHeadline = "Przetwarzaj miliony PDF." - -[portal.recentActivity] -title = "Ostatnia aktywność" -viewAll = "Wyświetl wszystko" - -[portal.recentActivity.empty] -description = "Uruchomienia potoków, wdrożenia i zdarzenia agentów pojawią się tutaj." -title = "Na razie nic tu nie ma" - -[portal.search] -ariaLabel = "Szukaj" -placeholder = "Szukaj w Stirling — endpointy, potoki, dokumenty…" - -[portal.search.empty] -noActionsDescription = "Szybkie działania pojawią się tutaj, gdy będą dostępne." -noActionsTitle = "Brak szybkich działań" -noMatches = "Brak wyników dla „{{query}}”" -noMatchesDescription = "Spróbuj innego słowa kluczowego lub przejrzyj katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Połączenie konta" -[portal.shell.header] -accountFallback = "Konto" -accountMenu = "Menu konta" -darkMode = "Tryb ciemny" -lightMode = "Tryb jasny" -search = "Szukaj" -searchPlaceholder = "Szukaj…" -signOut = "Wyloguj się" -switchToDark = "Przełącz na motyw ciemny" -switchToLight = "Przełącz na motyw jasny" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumentów" linkAccount = "Połącz konto Stirling" -planEditor = "Plan Editor" -planEnterprise = "Plan Enterprise" -planProcessor = "Plan Processor" primaryNav = "Nawigacja główna" switchApp = "Przełącz aplikację" +[portal.shell.topbar] +closeNav = "Zamknij nawigację" +openNav = "Otwórz nawigację" +search = "Szukaj" + [portal.sources] subtitle = "Wielokrotnego użytku połączenia wejściowe, które dostarczają dokumenty do Stirling. Skonfiguruj połączenie raz, a następnie odwołuj się do niego z dowolnej liczby polityk. Kliknij wiersz, aby zobaczyć jego konfigurację i polityki, które go używają." title = "Źródła" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Połącz źródło" +[portal.sources.builder] +back = "Wróć do źródeł" +backToTypes = "Wszystkie typy źródeł" +cancel = "Anuluj" +chooseHint = "Wybierz, skąd pochodzą dokumenty. Wyszarzone konektory są w przygotowaniu." +comingSoon = "Wkrótce" +comingSoonHeading = "Wkrótce" +create = "Utwórz źródło" +createTitle = "Połącz źródło" +delete = "Usuń" +editTitle = "Edytuj źródło" +enabled = "Włączone" +save = "Zapisz zmiany" + +[portal.sources.builder.folderAccess] +description = "Automatyzacje folderów mogą używać tylko folderów dopuszczonych przez administratora. Dodaj go w ustawieniach Dostępu do folderów, a następnie spróbuj ponownie." +openSettings = "Ustawienia Dostępu do folderów" +title = "Ten folder nie jest dozwolony" + [portal.sources.delete] body = "Usunąć „{{name}}”? Tej operacji nie można cofnąć. Polityki, które się do niego odwołują, trzeba będzie zaktualizować." cancel = "Anuluj" confirm = "Usuń" title = "Usunąć źródło?" -[portal.sources.detail] -closeAriaLabel = "Zamknij szczegóły" -delete = "Usuń źródło" -docs24h = "Ostatnie 24 godz." -docs30d = "Ostatnie 30 dni" -docsTotal = "Łącznie wykryte" -docsTrend = "Dokumenty z ostatnich 30 dni" -documents = "Dokumenty" -edit = "Edytuj" -notReferenced = "Nie odwołuje się do niego żadna polityka, więc można je bezpiecznie usunąć." -pause = "Wstrzymaj" -resume = "Wznów" -subtitle = "{{type}} · {{status}}" -usedBy = "Używane przez" - -[portal.sources.empty] -description = "Połącz folder (a wkrótce także magazyn w chmurze), aby Twoje polityki miały skąd pobierać dokumenty." -title = "Nie połączono jeszcze żadnych źródeł" - [portal.sources.kpi] inUse = "W użyciu" total = "Połączenia" unused = "Nieużywane" +[portal.sources.networkFields.connection] +helperText = "Zapisane połączenie z adresem serwera i danymi uwierzytelniającymi. Używane ponownie przez każde źródło, które się do niego odwołuje." +label = "Połączenie" + +[portal.sources.networkFields.directory] +helperText = "Folder na serwerze do odpytywania, względem katalogu domowego logowania lub katalogu głównego udziału. Pozostaw puste dla katalogu głównego." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume usuwa każdy plik z serwera, gdy zostanie przetworzony przez wszystkie zasady." +label = "Tryb odczytu" + +[portal.sources.networkFields.mode.options] +consume = "Consume: przetwórz każdy plik raz" +snapshot = "Snapshot: odczytuj folder ponownie przy każdym uruchomieniu" + +[portal.sources.networkFields.recursive] +label = "Głębokość folderu" + +[portal.sources.networkFields.recursive.options] +all = "Uwzględnij podfoldery" +top = "Tylko najwyższy poziom" + [portal.sources.status] active = "Aktywne" disabled = "Wyłączone" unused = "Nieużywane" [portal.sources.table] +documents = "Dokumenty" source = "Źródło" status = "Status" +type = "Type" usedBy = "Polityki" +[portal.sources.types.box] +description = "Pobieraj dokumenty z folderów Box już udostępnianych przez zespół." +label = "Box" + +[portal.sources.types.dropbox] +description = "Pobieraj dokumenty z udostępnionego folderu Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenty przetworzone przez zespół w edytorze, w ramach uruchomień zasad i AI." label = "Editor" +noPolicies = "Nie uruchomiono jeszcze żadnych zasad z edytora." + +[portal.sources.types.email] +description = "Zamieniaj załączniki wysłane do dedykowanej skrzynki pocztowej w dokumenty." +label = "Skrzynka email" [portal.sources.types.folder] description = "Monitoruj katalog na serwerze pod kątem nowych dokumentów." @@ -7943,25 +8829,80 @@ label = "Głębokość folderu" all = "Uwzględnij podfoldery" top = "Tylko najwyższy poziom" +[portal.sources.types.ftp] +description = "Odpytuj folder serwera FTP lub FTPS pod kątem nowych dokumentów." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Pobieraj dokumenty z udostępnionych folderów Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Monitoruj udział sieciowy SMB pod kątem nowych dokumentów." +label = "Dysk sieciowy" + +[portal.sources.types.onedrive] +description = "Monitoruj foldery OneDrive pod kątem nowych dokumentów." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Pobieraj dokumenty z bucketu Amazon S3 lub kompatybilnego z S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Zapisane połączenie zawierające bucket i dane uwierzytelniające. Używane ponownie przez każde źródło i wyjście potoku, które się do niego odwołuje." +label = "Połączenie" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume usuwa każdy obiekt z bucketu, gdy zostanie przetworzony przez wszystkie zasady." +label = "Tryb odczytu" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: przetwórz każdy obiekt raz" +snapshot = "Snapshot: odczytuj bucket ponownie przy każdym uruchomieniu" + +[portal.sources.types.s3.fields.prefix] +helperText = "Przetwarzane są tylko obiekty, których klucze zaczynają się od tego prefiksu." +label = "Prefiks klucza" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Odpytuj folder zrzutu SFTP pod kątem nowych dokumentów." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Pobieraj dokumenty z bibliotek dokumentów SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Źródło" +[portal.sources.types.webhook] +createNote = "URL dostarczania i sekret podpisywania są generowane podczas tworzenia tego webhooka. Nadawcy wysyłają dokumenty metodą POST na ten URL, a odwołujące się zasady uruchamiają się po nadejściu." +description = "Odbieraj dokumenty przez podpisany HTTP POST z dowolnego systemu zewnętrznego." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL dostarczania" +secretNote = "Sekret podpisywania jest wyświetlany raz, podczas tworzenia webhooka. Utwórz źródło ponownie, aby go zmienić." + +[portal.sources.types.webhook.reveal] +copy = "Kopiuj" +done = "Gotowe" +secret = "Sekret podpisywania" +secretHelp = "Podpisuj surową treść każdego dostarczenia tym kluczem (HMAC-SHA256) i wysyłaj ją jako nagłówek X-Stirling-Signature." +secretWarning = "Skopiuj sekret podpisywania teraz. Ze względów bezpieczeństwa jest wyświetlany tylko raz i nie można go później odzyskać." +title = "Utworzono webhook" +url = "URL dostarczania" +usage = "Wyślij każdy dokument metodą POST jako surową treść żądania na URL dostarczania z binarnym typem zawartości (application/pdf lub application/octet-stream). Odwołujące się zasady uruchamiają się automatycznie po nadejściu." + [portal.sources.wizard] -back = "Wstecz" -cancel = "Anuluj" -continue = "Kontynuuj" -editTitle = "Edytuj źródło" name = "Nazwa" namePlaceholder = "np. Przyjmowanie roszczeń" -save = "Zapisz zmiany" -subtitle = "Krok {{current}} z {{total}} · {{label}}" -title = "Połącz źródło" type = "Typ" -[portal.sources.wizard.steps] -chooseType = "Wybierz typ" -configure = "Skonfiguruj" -review = "Przejrzyj i połącz" +[portal.stepModal] +close = "Zamknij" [portal.usage] managePayment = "Zarządzaj płatnością" @@ -8021,16 +8962,9 @@ summary = "Jest właścicielem zespołu — zarządza zasobami jego członków i 2 = "Dostęp do portalu przez domyślną politykę" 3 = "Wszystko, co może robić Członek" -[portal.welcome] -ariaLabel = "Witamy w Stirling PDF" -badge = "Open-source" -installEditor = "Zainstaluj Editor" -inviteTeammates = "Zaproś członków zespołu" -openInBrowser = "Otwórz w przeglądarce" -perks = "Zawsze bezpłatne · Self-hostable" -subtitle = "Najbezpieczniejszy na świecie PDF Editor jest bezpłatny dla zespołów każdej wielkości. Zawiera ponad 60 operacji PDF i SSO." -title = "Witamy w" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Bez limitu" [printFile] title = "Drukuj plik" @@ -8613,6 +9547,14 @@ text = "Obracaj strony PDF zgodnie lub przeciwnie do ruchu wskazówek zegara w k [rotate.tooltip.header] title = "Przegląd ustawień obrotu" +[ruler] +clearAll = "Wyczyść wszystko" +hideAllLabels = "Ukryj wszystkie etykiety" +hideSmallLabels = "Ukryj małe etykiety" +physicalValues = "Fizyczne" +scaled = "Skalowane" +showAllLabels = "Pokaż wszystkie etykiety" + [sanitize] desc = "Usuń potencjalnie szkodliwe elementy z plików PDF." sanitizationResults = "Wyniki oczyszczania" @@ -8654,31 +9596,45 @@ label = "Usuń metadane XMP" [sanitize.steps] settings = "Ustawienia" -[sanitizePdf] -tags = "czyszczenie, ochrona, bezpieczeństwo, usuwanie zagrożeń" - [sanitizePDF] title = "Dezynfekuj PDF" +[sanitizePdf] +tags = "czyszczenie, ochrona, bezpieczeństwo, usuwanie zagrożeń" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Dopasuj rozmiar stron" +[scaleSettings] +activeScale = "Aktywna skala" +apply = "Zastosuj skalę" +applyCalibration = "Zastosuj kalibrację" +calculatedScale = "Obliczona skala" +calibrate = "Kalibruj" +calibrating = "Kalibracja" +calibrationDistanceRequired = "Wprowadź rzeczywistą odległość większą niż zero" +calibrationInvalid = "Nie można obliczyć skali na podstawie tego pomiaru" +calibrationPaperDistance = "Zmierzona odległość na stronie: {{distance}}" +calibrationTitle = "Kalibruj skalę" +calibrationTooltip = "Zmierz znaną odległość, aby automatycznie obliczyć skalę" +customScale = "Skala niestandardowa" +noneSet = "Nie ustawiono skali niestandardowej" +presets = "Skale predefiniowane" +ratio = "Współczynnik skali" +ratioHelp = "Współczynnik: 1 jednostka strony = X jednostek rzeczywistych" +ratioPlaceholder = "np. 100" +ratioPositive = "Współczynnik musi być większy niż zero" +ratioRequired = "Współczynnik jest wymagany" +realDistance = "Rzeczywista odległość" +realDistancePlaceholder = "np. 5" +reset = "Przywróć domyślne" +unit = "Jednostka" +unitInvalid = "Nieprawidłowa jednostka: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Próg kąta:" -10 = "Ustawia rozmiar dodawanego i usuwanego obramowania, aby uniknąć białych obramowań na wyjściu (domyślnie: 1)." -2 = "Ustawia minimalny kąt bezwzględny wymagany do obrócenia obrazu (domyślnie: 10)." -3 = "Tolerancja:" -4 = "Określa zakres zmienności kolorów wokół szacowanego koloru tła (domyślnie: 30)." -5 = "Minimalna powierzchnia:" -6 = "Ustawia próg minimalnego obszaru dla zdjęcia (domyślnie: 10000)." -7 = "Minimalny obszar konturu:" -8 = "Ustawia próg minimalnego obszaru konturu dla zdjęcia" -9 = "Rozmiar obramowania:" - [scannerImageSplit] submit = "Wyodrębnij skany obrazów" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Co robi" whatThisDoesDesc = "Automatycznie znajduje i wyodrębnia każde zdjęcie ze skanowanej strony lub obrazu zbiorczego — bez ręcznego kadrowania." whenToUse = "Kiedy używać" +[ScannerImageSplit.selectText] +1 = "Próg kąta:" +10 = "Ustawia rozmiar dodawanego i usuwanego obramowania, aby uniknąć białych obramowań na wyjściu (domyślnie: 1)." +2 = "Ustawia minimalny kąt bezwzględny wymagany do obrócenia obrazu (domyślnie: 10)." +3 = "Tolerancja:" +4 = "Określa zakres zmienności kolorów wokół szacowanego koloru tła (domyślnie: 30)." +5 = "Minimalna powierzchnia:" +6 = "Ustawia próg minimalnego obszaru dla zdjęcia (domyślnie: 10000)." +7 = "Minimalny obszar konturu:" +8 = "Ustawia próg minimalnego obszaru konturu dla zdjęcia" +9 = "Rozmiar obramowania:" + [search] noResults = "Brak wyników" placeholder = "Wpisz wyszukiwane hasło..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Twój serwer Stirling-PDF jest offline, a \"{{endpoin [session] expired = "Twoja sesja wygasła. Odśwież stronę i spróbuj ponownie." -[sessionManagement.tooltip] -header = "Zarządzanie sesjami podpisywania" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nowi uczestnicy są dodawani na koniec kolejności podpisywania" -bullet2 = "Nie można dodawać uczestników po sfinalizowaniu sesji" -bullet3 = "Każdy uczestnik otrzymuje powiadomienie, gdy przyjdzie jego kolej" -description = "Możesz dodawać kolejnych uczestników do aktywnej sesji w dowolnym momencie przed finalizacją." -title = "Dodawanie uczestników" - -[sessionManagement.tooltip.finalization] -bullet1 = "Pełna finalizacja: Wszyscy uczestnicy podpisali" -bullet2 = "Częściowa finalizacja: Niektórzy uczestnicy jeszcze nie podpisali" -bullet3 = "Uczestnicy bez podpisu zostaną wykluczeni z finalnego dokumentu" -bullet4 = "Po finalizacji możesz wczytać podpisany PDF do aktywnych plików" -description = "Finalizacja łączy wszystkie podpisy w jednym podpisanym PDF-ie. Tej czynności nie można cofnąć." -title = "Finalizacja sesji" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Nie można usuwać uczestników, którzy już podpisali" -bullet2 = "Usunięci uczestnicy nie będą już otrzymywać powiadomień" -bullet3 = "Kolejność podpisywania dostosuje się automatycznie" -description = "Uczestników można usuwać z sesji przed ich podpisem." -title = "Usuwanie uczestników" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Każdy podpis jest nakładany sekwencyjnie na PDF" -bullet2 = "Późniejsi sygnatariusze widzą wcześniejsze podpisy" -bullet3 = "Kluczowe dla procesów akceptacji i łańcucha dowodowego" -description = "Kolejność określona podczas tworzenia sesji decyduje, kto podpisuje jako pierwszy." -title = "Kolejność podpisów" - [settings] close = "Zamknij" +[settings.ai] +documents = "Dokumenty i RAG" +general = "Ogólne" +limits = "Limity i wydajność" +models = "Modele i dostawcy" +title = "AI" + [settings.configuration] advanced = "Zaawansowane" database = "Baza danych" endpoints = "Endpointy" features = "Funkcje" +folderAccess = "Dostęp do folderów" mcp = "MCP Server" storageSharing = "Przechowywanie plików i udostępnianie" systemSettings = "Ustawienia systemowe" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Domyślny edytor PDF" defaultPdfEditorActive = "Stirling PDF jest Twoim domyślnym edytorem PDF" defaultPdfEditorChecking = "Sprawdzanie..." defaultPdfEditorInactive = "Inna aplikacja jest ustawiona jako domyślna" +defaultPdfEditorRemind = "Przypomnij mi o ustawieniu jako domyślnej" +defaultPdfEditorRemindDescription = "Pokaż baner, gdy Stirling PDF nie jest domyślną aplikacją PDF." defaultPdfEditorSet = "Już domyślny" defaultStartupView = "Domyślny widok przy uruchomieniu" defaultStartupViewDescription = "Wybierz, która karta w lewej kolumnie ma być aktywna po uruchomieniu aplikacji" @@ -8817,9 +9763,18 @@ languageDescription = "Wybierz język wyświetlania" logout = "Wyloguj się" setAsDefault = "Ustaw jako domyślne" theme = "Motyw" +themeAccent = "Kolor akcentu" +themeAccentCustom = "Kolor niestandardowy" +themeAccentCustomHint = "Niestandardowy (zalecane domyślne)" +themeAccentDark = "Kolor akcentu trybu ciemnego" +themeAccentDefault = "Domyślny" +themeAccentDefaultHint = "Domyślny motyw (zalecane)" +themeAccentDescription = "Przyciski, linki i wyróżnienia używają tego koloru, a aplikacja jest nim subtelnie zabarwiona. Tryb jasny i ciemny mają własne ustawienia. System używa tego, który jest aktywny." +themeAccentLight = "Kolor akcentu trybu jasnego" themeDark = "Ciemny" themeDescription = "Przełącz między trybem jasnym i ciemnym" themeLight = "Jasny" +themeReset = "Przywróć motyw domyślny" themeSystem = "System" title = "Ogólne" user = "Użytkownik" @@ -8833,6 +9788,12 @@ intro = "Włącz uwierzytelnianie użytkowników, zarządzanie zespołem i funkc learnMore = "Więcej w dokumentacji" title = "Dla administratorów systemu" +[settings.general.loginLanding] +description = "Wybierz, która aplikacja otwiera się po zalogowaniu do Stirling Cloud." +editor = "Edytor" +processor = "Procesor" +title = "Po zalogowaniu" + [settings.general.mode] fullscreen = "Pełny ekran" sidebar = "Pasek boczny" @@ -9013,9 +9974,6 @@ title = "Zasady i prywatność" [settings.preferences] title = "Preferencje" -[settings.search] -placeholder = "Szukaj stron ustawień..." - [settings.security] description = "Zmień hasło, aby zachować bezpieczeństwo konta." title = "Bezpieczeństwo" @@ -9041,6 +9999,11 @@ security = "Bezpieczeństwo" telegram = "Telegram" title = "Bezpieczeństwo i uwierzytelnianie" +[settings.signOut] +confirm = "Czy na pewno chcesz się wylogować?" +submit = "Wyloguj się" +title = "Wyloguj się" + [settings.team] title = "Zespół" @@ -9156,6 +10119,7 @@ last = "Ostatnia strona" maintainRatio = "Przełącz zachowanie proporcji" next = "Następna strona" noSavedSigs = "Nie znaleziono zapisanych podpisów" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Podpisy osobiste" previous = "Poprzednia strona" redo = "Ponów" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Nie udało się usunąć tła" hint = "Prześlij obraz podpisu w formacie PNG lub JPG" label = "Prześlij obraz podpisu" placeholder = "Wybierz plik obrazu" +previewAlt = "Current image signature" processing = "Przetwarzanie obrazu..." removeBackground = "Usuń białe tło (uczyń przezroczystym)" @@ -9207,6 +10172,17 @@ saved = "Wybierz zapisany podpis powyżej, a następnie kliknij w dowolnym miejs text = "Po wpisaniu swojego imienia i nazwiska kliknij w dowolnym miejscu PDF, aby umieścić podpis." title = "Jak dodać podpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Przesuń podpis" pause = "Wstrzymaj umieszczanie" @@ -9653,10 +10629,30 @@ title = "Prześlij na serwer" updateButton = "Zaktualizuj na serwerze" uploadButton = "Prześlij na serwer" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kliknij tutaj" +fallback = "Jeśli nie otworzyło się automatycznie," +opening = "Otwieranie Swagger UI w nowej karcie..." tags = "api,dokumentacja,swagger,endpointy,rozwój" title = "Dokumentacja API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Wybierz pliki w Aktywnych plikach, aby uruchomić to narzędz singleFileScope = "Dotyczy tylko: {{fileName}}" viewerMode = "Przełącz do edytora plików, aby wybrać wiele plików." +[toolFormat] +ANY = "dowolny plik" +CBR = "archiwum komiksu (CBR)" +CBZ = "archiwum komiksu (CBZ)" +CSV = "plik CSV" +EBOOK = "ebook" +EMAIL = "wiadomość e-mail" +EXCEL = "arkusz kalkulacyjny" +HTML = "strona internetowa" +IMAGE = "obrazy" +JAVASCRIPT = "plik JavaScript" +JSON = "plik JSON" +MARKDOWN = "plik Markdown" +NONE = "brak pliku" +PCL = "plik wydruku PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF chroniony hasłem" +POSTSCRIPT = "plik PostScript" +PPT = "prezentacja" +TEXT = "plik tekstowy" +VIDEO = "wideo" +WORD = "dokument Word" +XML = "plik XML" +XPS = "dokument XPS" +ZIP = "archiwum ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Powrót do wszystkich narzędzi" collapse = "Zwiń panel" expand = "Rozwiń panel" goBack = "Wróć" +pdfTools = "Narzędzia PDF" placeholder = "Wybierz narzędzie, aby zacząć" premiumFeature = "Funkcja premium:" toolsHeader = "Narzędzia" @@ -9806,11 +10829,6 @@ noToolsFound = "Nie znaleziono narzędzi" quickAccess = "SZYBKI DOSTĘP" searchPlaceholder = "Szukaj narzędzi..." -[toolPicker.categories] -advancedTools = "Narzędzia zaawansowane" -recommendedTools = "Polecane narzędzia" -standardTools = "Narzędzia standardowe" - [toolPicker.subcategories] advancedFormatting = "Zaawansowane formatowanie" automation = "Automatyzacja" @@ -9824,6 +10842,9 @@ removal = "Usuwanie" signing = "Podpisywanie" verification = "Weryfikacja" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nie znaleziono narzędzi" noTools = "Brak dostępnych narzędzi" @@ -9950,28 +10971,53 @@ title = "Użytkownicy" you = "(Ty)" [users.action] +cancelInvite = "Anuluj zaproszenie" deleteTeam = "Usuń zespół" disableMfa = "Zresetuj MFA" move = "Przenieś do zespołu" reinstate = "Przywróć" remove = "Usuń z organizacji" +removeTeam = "Usuń z zespołu" rename = "Zmień nazwę zespołu" resetPw = "Zresetuj hasło" suspend = "Zawieś" unlock = "Odblokuj konto" +[users.activity] +daysAgo = "{{count}} dni temu" +hoursAgo = "{{count}} godz. temu" +justNow = "Przed chwilą" +minutesAgo = "{{count}} min temu" +monthsAgo_one = "{{count}} miesiąc temu" +monthsAgo_other = "{{count}} mies. temu" +never = "Nigdy" +weeksAgo_one = "{{count}} tydzień temu" +weeksAgo_other = "{{count}} tyg. temu" +yearsAgo_one = "{{count}} rok temu" +yearsAgo_other = "{{count}} lat temu" + [users.cap] addProcessor = "+ Processor" approver = "Zatwierdza politykę" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Anulować zaproszenie dla {{email}}? Ta osoba nie będzie mogła dołączyć za pomocą bieżącego linku." +cancelInviteTitle = "Anuluj zaproszenie" deleteTeamBody = "Usunąć zespół {{name}}? Najpierw zespół musi być pusty — przenieś jego członków do innego zespołu i upewnij się, że nie jest nadal właścicielem żadnych konfiguracji integracji." deleteTeamTitle = "Usuń zespół" disableMfaBody = "Usunąć rejestrację MFA użytkownika {{name}}? Jeśli będzie to wymagane, skonfiguruje ją ponownie przy następnym logowaniu." disableMfaTitle = "Zresetuj MFA" removeBody = "Trwale usunąć {{name}} z organizacji? Tej operacji nie można cofnąć." +removeTeamBody = "Usunąć {{name}} z zespołu? Konto pozostanie, ale utraci dostęp do zasobów tego zespołu." removeTitle = "Usuń członka" [users.empty] @@ -9982,10 +11028,8 @@ title = "Brak członków" addToTeam = "Dodaj do zespołu" guestCount = "{{count}} gość" guests = "Goście" -guestsDesc = "Zewnętrzni współpracownicy, ograniczeni do tego, co udostępniono. Tylko Editor." ledBy = "prowadzony przez {{owner}}" org = "Organizacja" -orgDesc = "Właściciele z uprawnieniami w całej organizacji i zatwierdzaniem polityk" owners = "{{count}} właściciel" team = "Zespół {{name}}" teamMeta = "{{count}} osób" @@ -10022,6 +11066,18 @@ username = "Nazwa użytkownika" usernameError = "Nazwa użytkownika musi mieć co najmniej 3 znaki" usernamePlaceholder = "jsmith" +[users.invites] +by = "Zaprosił(a): {{who}}" +cancel = "Anuluj" +expiresInDays_one = "Wygasa za {{count}} dzień" +expiresInDays_other = "Wygasa za {{count}} dni" +expiresToday = "Wygasa dzisiaj" +title = "Oczekujące zaproszenia" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Coś poszło nie tak podczas łączenia z backendem albo nie masz dostępu. Spróbuj ponownie." title = "Nie udało się załadować członków" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Szukaj w załącznikach" title = "Załączniki" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Tytuł zakładki" +bookmarkTitleRequired = "Tytuł zakładki jest wymagany" closeSidebar = "Zamknij panel boczny zakładek" collapseAll = "Zwiń wszystkie zakładki" +empty = "No bookmarks in this document" expandAll = "Rozwiń wszystkie zakładki" +searchPlaceholder = "Szukaj zakładek" +title = "Bookmarks" [viewer.comments] addComment = "Dodaj komentarz" @@ -10236,10 +11297,12 @@ locateAnnotation = "Zlokalizuj w dokumencie" moreActions = "Więcej działań" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Strona {{page}}" placingHint = "Kliknij stronę, aby umieścić… (anuluj)" removeCommentOnly = "Usuń tylko komentarz" saveReply = "Zapisz odpowiedź" +searchPlaceholder = "Search comments" title = "Komentarze" typeComment = "Komentarz" typeInsertText = "Wstaw tekst" @@ -10262,6 +11325,7 @@ unsavedDesc = "Masz niezapisane zmiany" closeSidebar = "Zamknij panel boczny warstw" hideAll = "Ukryj wszystkie warstwy" showAll = "Pokaż wszystkie warstwy" +title = "Layers" [viewer.link] delete = "Usuń link" @@ -10605,30 +11669,6 @@ title = "Tekst znaku wodnego" image = "Obraz" text = "Tekst" -[wetSignature.tooltip] -header = "Metody tworzenia podpisu" - -[wetSignature.tooltip.draw] -bullet1 = "Dostosuj kolor i grubość pióra" -bullet2 = "Czyść i rysuj ponownie, aż będziesz zadowolony" -bullet3 = "Działa na urządzeniach dotykowych (tablety, telefony)" -description = "Utwórz odręczny podpis za pomocą myszy lub ekranu dotykowego. Najlepsze dla osobistych, autentycznych podpisów." -title = "Narysuj podpis" - -[wetSignature.tooltip.type] -bullet1 = "Wybierz spośród wielu czcionek" -bullet2 = "Dostosuj rozmiar i kolor tekstu" -bullet3 = "Idealne do ustandaryzowanych podpisów" -description = "Wygeneruj podpis z wpisanego tekstu. Szybkie i spójne, odpowiednie do dokumentów biznesowych." -title = "Wpisz podpis" - -[wetSignature.tooltip.upload] -bullet1 = "Obsługuje PNG, JPG i inne formaty obrazów" -bullet2 = "Zalecane są przezroczyste tła dla najlepszych efektów" -bullet3 = "Obraz zostanie przeskalowany, aby dopasować do obszaru podpisu" -description = "Prześlij wcześniej utworzony obraz podpisu. Idealne, jeśli masz zeskanowany podpis lub logo firmy." -title = "Prześlij obraz podpisu" - [workbenchBar] activeFiles = "Aktywne pliki" annotations = "Adnotacje" @@ -10642,6 +11682,7 @@ exitRedaction = "Zakończ tryb zaczerniania" exportAll = "Eksportuj PDF" exportSelected = "Eksportuj wybrane strony" formFill = "Wypełnij formularz" +hideToolbar = "Hide toolbar" multiTool = "Wielofunkcyjne narzędzie" panMode = "Tryb przesuwania" print = "Drukuj PDF" @@ -10652,6 +11693,7 @@ redact = "Zaczernij" rotateLeft = "Obróć w lewo" rotateRight = "Obróć w prawo" ruler = "Linijka / Pomiar" +rulerSettings = "Ustawienia skali" save = "Zapisz" saveAll = "Zapisz wszystko" saveAs = "Zapisz jako" @@ -10661,6 +11703,7 @@ selectAll = "Zaznacz wszystko" selectByNumber = "Zaznacz według numerów stron" selectLanguage = "Wybierz język" share = "Udostępnij" +showToolbar = "Show toolbar" toggleAnnotations = "Pokaż/ukryj adnotacje" toggleAttachments = "Pokaż/ukryj załączniki" toggleBookmarks = "Pokaż/ukryj zakładki" diff --git a/frontend/editor/public/locales/pt-BR/translation.toml b/frontend/editor/public/locales/pt-BR/translation.toml index 6fbc440d20..b897248eb8 100644 --- a/frontend/editor/public/locales/pt-BR/translation.toml +++ b/frontend/editor/public/locales/pt-BR/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabeto" apply = "Aplicar" applyAndContinue = "Aplicar e continuar" -areYouSure = "Tem certeza de que deseja sair?" back = "Voltar" black = "Preto" blue = "Azul" @@ -29,7 +28,7 @@ customTextTooltip = "Formato personalizado opcional para os números de página. delete = "Apagar" details = "Detalhes" discardChanges = "Descartar alterações" -download = "Baixar (JSON)" +download = "Baixar" downloadPdf = "Baixar PDF" downloadUnavailable = "Download indisponível para este item" edit = "Editar" @@ -46,10 +45,8 @@ filesSelected = "Arquivos Selecionados" font = "Fonte" fontSizeTooltip = "Tamanho do texto do número da página em pontos. Números maiores criam texto maior." fontTypeTooltip = "Família de fontes para os números de página. Escolha conforme o estilo do seu documento." -genericSubmit = "Enviar" green = "Verde" help = "Ajuda" -imgPrompt = "Selecione a(s) Imagem(ns)" incorrectPasswordMessage = "A senha atual está incorreta." info = "Informações" invalidUndoData = "Não é possível desfazer: dados de operação inválidos" @@ -62,7 +59,6 @@ loginAgreementTitle = "Acordo de login" logOut = "Sair" marginTooltip = "Distância entre o número da página e a borda da página." moreOptions = "Mais opções" -multiPdfPrompt = "Selecione os PDFs (2+)" never = "Nunca" no = "Não" noFileSelected = "Nenhum arquivo selecionado. Por favo, envie um arquivo." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordem de Página Personalizada (Digite uma lista de números pages = "Páginas" pageSelectionPrompt = "Seleção de Página Personalizada (Digite uma lista de números de páginas, separadas por vírgula como 1,5,6 ou funções como 2n+1):" password = "Senha" -pdfPrompt = "Selecione o(s) PDF(s)" pin = "Fixar arquivo (manter ativo após execução da ferramenta)" pro = "Profissional" processingComplete = "Seu arquivo está pronto." @@ -110,6 +105,7 @@ undoSuccess = "Operação desfeita com sucesso" unknown = "Desconhecido" unpin = "Desafixar arquivo (substituir após execução da ferramenta)" unsavedChanges = "Você tem alterações não salvas no seu PDF. O que você gostaria de fazer?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Alterações não salvas" unsupported = "Não suportado" username = "Usuário" @@ -356,6 +352,7 @@ text = "Essas permissões controlam o que os usuários podem fazer com o PDF. S title = "Alterar Permissões" [addStamp] +preview = "Visualizar carimbo" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Downloads manuais: clique nos links e coloque os arquivos na past noLanguages = "Nenhum idioma tessdata encontrado no diretório configurado." permissionNotice = "O caminho do tessdata não é gravável. Os downloads serão abertos no navegador; salve os arquivos .traineddata manualmente na pasta tessdata." +[admin.settings.ai.documents] +description = "Configure o modelo de embedding e as configurações de recuperação usadas para responder a perguntas sobre documentos. Aplicado ao mecanismo de IA ao salvar." +title = "Documentos e RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Deixe em branco para usar a credencial de ambiente do próprio mecanismo. Aplica-se a implantações auto-hospedadas de mecanismo único." +generic = "Chave API de embedding" +openai = "Chave API da OpenAI" +setPlaceholder = "Salvo - deixe em branco para manter a chave atual" +voyageai = "Chave API da VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL base do endpoint de embeddings compatível com OpenAI / Ollama, por exemplo, http://ollama:11434/v1. Deve apontar para um endpoint interno confiável (sensível a SSRF)." +label = "URL base de embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Nome do modelo de embedding. Texto livre; as sugestões são apenas dicas." +label = "Modelo de embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provedor usado para transformar texto do documento em embeddings vetoriais." +label = "Provedor de embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Número máximo de buscas de recuperação que o agente pode executar por solicitação." +label = "Máx. buscas" + +[admin.settings.ai.documents.reindexNote] +body = "Alterar o modelo de embedding entra em vigor imediatamente, mas documentos indexados com o modelo anterior devem ser reindexados para que a busca retorne resultados corretos." +title = "Reindexação necessária" + +[admin.settings.ai.documents.saved] +reindexNote = "Se você alterou o modelo de embedding, reindexe os documentos existentes para que a busca use o novo modelo." + +[admin.settings.ai.documents.topK] +description = "Número de trechos mais relevantes recuperados por busca." +label = "Top K" + +[admin.settings.ai.general] +description = "Conecte o Stirling ao mecanismo de IA Python e escolha quais recursos de IA são expostos. As alterações são aplicadas na reinicialização." +title = "Mecanismo de IA" + +[admin.settings.ai.general.capabilities] +description = "Ative ou desative recursos individuais de IA. Recursos desativados ficam ocultos no app." +title = "Recursos" + +[admin.settings.ai.general.enabled] +description = "Chave geral. Quando desativada, nenhuma ferramenta de IA, agente ou chamada ao mecanismo fica disponível." +label = "Ativar IA" + +[admin.settings.ai.general.features.chat] +description = "Assistente conversacional para trabalhar com PDFs." +label = "Assistente de chat" + +[admin.settings.ai.general.features.classify] +description = "Categorize documentos automaticamente por tipo ou conteúdo." +label = "Classificação de documentos" + +[admin.settings.ai.general.features.createPdf] +description = "Gere um novo PDF (por exemplo, a partir de HTML) por meio de um agente de IA." +label = "Criar PDF a partir de prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Faça perguntas e obtenha respostas baseadas em um documento enviado." +label = "Perguntas sobre documentos" + +[admin.settings.ai.general.features.mathAuditor] +description = "Revise documentos em busca de erros matemáticos e numéricos." +label = "Auditor matemático" + +[admin.settings.ai.general.features.pdfComment] +description = "Adicione comentários de revisão e anotações criados por IA a um PDF." +label = "Agente de comentários em PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Tempo limite para operações de agente mais pesadas, como geração de documentos." +label = "Tempo limite de longa duração (segundos)" + +[admin.settings.ai.general.note] +body = "O mecanismo de IA é executado como um serviço separado. Seu segredo compartilhado" +body2 = "é definido por meio de uma variável de ambiente do contêiner. Chaves API de provedores podem ser inseridas nestas páginas ou fornecidas como variáveis de ambiente do mecanismo; as chaves inseridas aqui são enviadas ao mecanismo ao salvar." +title = "Sobre o mecanismo de IA" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Tempo limite para respostas de chat transmitidas (token por token)." +label = "Tempo limite de stream (segundos)" + +[admin.settings.ai.general.test] +button = "Testar conexão" +failBody = "O mecanismo de IA não respondeu. Verifique a URL, se o contêiner do mecanismo está em execução e se a IA está ativada (é necessário reiniciar após ativar)." +failTitle = "Mecanismo de IA inacessível" +okBody = "O mecanismo de IA respondeu a uma verificação de integridade." +okTitle = "Mecanismo de IA acessível" + +[admin.settings.ai.general.timeoutSeconds] +description = "Tempo limite para solicitações padrão de IA ao mecanismo." +label = "Tempo limite da solicitação (segundos)" + +[admin.settings.ai.general.url] +description = "URL interna do mecanismo de IA Python, por exemplo, http://stirling-pdf-engine:5001." +label = "URL do mecanismo de IA" + +[admin.settings.ai.limits] +description = "Proteções para quanto trabalho as solicitações de IA podem realizar e quantas podem executar simultaneamente. Aplicado ao mecanismo de IA ao salvar." +title = "Limites e desempenho" + +[admin.settings.ai.limits.maxCharacters] +description = "Proteção: rejeita solicitações de IA cujo texto extraído exceda este comprimento." +label = "Máx. caracteres por solicitação" + +[admin.settings.ai.limits.maxPages] +description = "Proteção: rejeita solicitações de IA acima deste número de páginas PDF." +label = "Máx. páginas por solicitação" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Número máximo de chamadas simultâneas ao modelo em andamento em todo o mecanismo." +label = "Concorrência máxima do modelo" + +[admin.settings.ai.models] +description = "Escolha o provedor de LLM e os modelos inteligentes/rápidos que o mecanismo de IA usa. Aplicado ao mecanismo de IA ao salvar." +title = "Modelos e provedores" + +[admin.settings.ai.models.apiKey] +anthropic = "Chave API da Anthropic" +description = "Deixe em branco para usar a credencial de ambiente do próprio mecanismo. Aplica-se a implantações auto-hospedadas de mecanismo único." +generic = "Chave API" +openai = "Chave API da OpenAI" +setPlaceholder = "Salvo - deixe em branco para manter a chave atual" + +[admin.settings.ai.models.baseUrl] +description = "URL base do endpoint compatível com OpenAI / Ollama, por exemplo, http://ollama:11434/v1." +label = "URL base do provedor" +warning = "A URL base deve apontar para um endpoint interno confiável. O mecanismo fará solicitações do lado do servidor para ela, portanto um valor não confiável é sensível a SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Máximo de tokens de saída para o modelo rápido." +label = "Máx. tokens do modelo rápido" + +[admin.settings.ai.models.fastModel] +description = "Modelo mais barato e rápido para tarefas leves. Texto livre; as sugestões são apenas dicas." +label = "Modelo rápido" + +[admin.settings.ai.models.provider] +description = "Com qual provedor de LLM o mecanismo se comunica." +label = "Provedor" + +[admin.settings.ai.models.smartMaxTokens] +description = "Máximo de tokens de saída para o modelo inteligente." +label = "Máx. tokens do modelo inteligente" + +[admin.settings.ai.models.smartModel] +description = "Modelo de alta capacidade para raciocínio complexo. Texto livre; as sugestões são apenas dicas." +label = "Modelo inteligente" + +[admin.settings.ai.saved] +body = "As alterações são enviadas automaticamente ao mecanismo de IA." +bodyNoPush = "Configurações salvas. Elas serão aplicadas na próxima vez que o mecanismo de IA carregar sua configuração." +title = "Configurações de IA salvas" + [admin.settings.badge] clickToUpgrade = "Clique para ver detalhes do plano" @@ -938,6 +1094,25 @@ label = "Regenerar na inicialização" description = "Número de dias em que o certificado será válido" label = "Validade do certificado (dias)" +[admin.settings.folderAccess] +description = "Diretórios que fontes de pasta e saídas de pasta têm permissão para ler e gravar. Este é um limite de segurança: automações nunca podem ser apontadas para um caminho do servidor fora desta lista." +securityNote = "Deixe vazio para desativar completamente fontes e saídas de pasta. O diretório de configuração do próprio Stirling está sempre fora dos limites." +title = "Acesso a pastas" + +[admin.settings.folderAccess.implied] +description = "Estes diretórios gerenciados pelo Stirling são sempre permitidos e não podem ser alterados aqui." +serverStorage = "Armazenamento de arquivos do servidor" +title = "Sempre permitido" +watchedFolder = "Pasta monitorada do pipeline" + +[admin.settings.folderAccess.roots] +add = "Adicionar" +empty = "Nenhuma pasta permitida. Fontes e saídas de pasta estão desativadas no momento." +hint = "Insira caminhos absolutos, por exemplo /data/inbox." +label = "Raízes de pasta permitidas" +placeholder = "/data/inbox" +remove = "Remover raiz de pasta" + [admin.settings.general] description = "Configure definições de aplicativo em todo o sistema, incluindo branding e comportamento padrão." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Largura" [app] description = "A alternativa gratuita ao Adobe Acrobat (10M+ downloads)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Converter para PDF/A-3b" convertToPdfA3bDescription = "Cria um PDF de arquivamento com anexos incorporados" @@ -1842,6 +2020,15 @@ export = "Exportar" accessDenied = "Acesso negado" insufficientPermissions = "Você não tem permissão para executar esta ação." +[auth.callback] +completing = "Concluindo autenticação" +invalidToken = "Falha no login OAuth - token inválido." +missingToken = "Falha no login OAuth - nenhum token recebido." +oauthFailed = "Falha no login OAuth. Tente novamente." +pleaseWait = "Aguarde enquanto concluímos seu login." +signedOut = "Você saiu. Faça login novamente." +windowMayClose = "Você pode fechar esta janela quando terminar." + [auth.displayName] guest = "Convidado" user = "Usuário" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Protege documentos PDF removendo conteúdo potencialmente description = "Esta ferramenta renomeará automaticamente arquivos PDF com base no conteúdo. Ela analisa o documento para encontrar o título mais adequado a partir do texto." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Girar automaticamente" +tags = "girar automaticamente,corrigir orientação,de cabeça para baixo,de lado,endireitar,orientar,detectar orientação,corrigir rotação,orientar automaticamente,digitalizado,OSD" + +[autoRotate.confidenceThreshold] +title = "Limite de confiança" + +[autoRotate.detectionMode] +auto = "Automático" +osd = "Somente OCR" +text = "Somente texto" +title = "Método de detecção" + +[autoRotate.error] +failed = "Ocorreu um erro ao girar automaticamente o PDF." + +[autoRotate.inferUndetected] +title = "Girar páginas de baixa confiança para corresponder" + +[autoRotate.report] +applied = "{{degrees}}° horário" +confidenceValue = "Confiança {{value}}" +noChange = "Sem alteração" +pageLabel = "Página {{number}}" +summary = "{{rotated}} de {{total}} páginas giradas, {{unchanged}} deixadas como estão" +title = "Relatório de detecção" + +[autoRotate.report.method] +inferred = "Correspondido" +none = "Ignorado" +osd = "OCR" +text = "Texto" + +[autoRotate.report.note] +belowThreshold = "Abaixo do limite" +blankPage = "A página está em branco" +inferredFromDocument = "Igual às outras páginas" +noDominantDirection = "Direções de texto mistas" +osdFailed = "Nenhum texto legível encontrado" +osdNoVerdict = "Não foi possível determinar a orientação" +tesseractUnavailable = "OCR não instalado" +tooFewGlyphs = "Texto insuficiente" + +[autoRotate.results] +title = "Resultados da rotação automática" + +[autoRotate.settings] +title = "Configurações" + +[autoRotate.tooltip.confidenceThreshold] +text = "Quão certa a verificação precisa estar antes de uma página ser girada. Diminua para girar mais páginas; aumente para deixar mais páginas intactas." +title = "Limite de confiança" + +[autoRotate.tooltip.description] +text = "Determina a orientação correta de cada página e a gira para a posição certa. Páginas que já estão na vertical são deixadas como estão." + +[autoRotate.tooltip.detectionMode] +text = "Automático lê primeiro o texto da página e, se isso não for suficiente, analisa a página como imagem. Somente texto ignora a verificação de imagem, sendo o mais rápido para documentos que nunca foram digitalizados. Somente OCR trata cada página como uma imagem." +title = "Método de detecção" + +[autoRotate.tooltip.header] +title = "Girar automaticamente" + +[autoRotate.tooltip.inferUndetected] +text = "Páginas em branco ou quase em branco não podem ser verificadas sozinhas. Quando todas as páginas que puderam ser lidas precisam da mesma rotação, essas páginas são giradas para corresponder." +title = "Girar páginas de baixa confiança para corresponder" + [autoSizeSplitPDF] tags = "pdf,divisão,documento,organização" @@ -2189,33 +2443,6 @@ keywords = "Palavras-chave: ímpar, par" numbers = "Números/intervalos: 5, 10-20" progressions = "Progressões: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipos de certificado" - -[certificateChoice.tooltip.organization] -bullet1 = "Gerenciado pelos administradores do sistema" -bullet2 = "Compartilhado entre usuários autorizados" -bullet3 = "Representa a identidade da empresa, não do indivíduo" -bullet4 = "Melhor para: Documentos oficiais, assinaturas de equipe" -description = "Um certificado compartilhado fornecido pela sua organização. Usado para autoridade de assinatura em nível da empresa." -title = "Certificado da organização" - -[certificateChoice.tooltip.personal] -bullet1 = "Gerado automaticamente no primeiro uso" -bullet2 = "Vinculado à sua conta de usuário" -bullet3 = "Não pode ser compartilhado com outros usuários" -bullet4 = "Melhor para: Documentos pessoais, responsabilidade individual" -description = "Um certificado gerado automaticamente, exclusivo para sua conta de usuário. Adequado para assinaturas individuais." -title = "Certificado pessoal" - -[certificateChoice.tooltip.upload] -bullet1 = "Requer arquivo P12/PFX e senha" -bullet2 = "Pode ser emitido por Autoridades Certificadoras externas" -bullet3 = "Maior nível de confiança para documentos legais" -bullet4 = "Melhor para: Contratos legalmente vinculantes, validação externa" -description = "Use seu próprio certificado PKCS#12. Fornece controle total sobre as propriedades do certificado." -title = "Enviar P12 personalizado" - [certSign] awaitingSignatures = "Aguardando assinaturas" chooseCertificate = "Escolher arquivo de certificado" @@ -2542,34 +2769,6 @@ server = "Servidor" stepTitle = "Origem do certificado" upload = "Enviar" -[certSign.tooltip.header] -title = "Sobre o gerenciamento de assinaturas" - -[certSign.tooltip.overview] -bullet1 = "Verifique as assinaturas existentes e sua validade" -bullet2 = "Veja informações detalhadas sobre signatários e certificados" -bullet3 = "Adicione novas assinaturas digitais para proteger seus documentos" -bullet4 = "Vários arquivos suportados com navegação fácil" -text = "Esta ferramenta permite verificar se seus PDFs estão assinados digitalmente e adicionar novas assinaturas digitais. Assinaturas digitais provam quem criou ou aprovou um documento e mostram se ele foi alterado desde a assinatura." -title = "O que esta ferramenta pode fazer?" - -[certSign.tooltip.signing] -bullet1 = "Suporta formatos de certificado PEM, PKCS12, JKS e de servidor" -bullet2 = "Opção para mostrar ou ocultar a assinatura no PDF" -bullet3 = "Adicione motivo, local e nome do signatário" -bullet4 = "Escolha em qual página colocar assinaturas visíveis" -bullet5 = "Use certificado do servidor para a opção simples 'Assinar com Stirling-PDF'" -text = "Para assinar um PDF, você precisa de um certificado digital (como PEM, PKCS12 ou JKS). Você pode optar por deixar a assinatura visível no documento ou mantê-la invisível apenas para segurança." -title = "Adicionando assinaturas" - -[certSign.tooltip.validation] -bullet1 = "Mostra se as assinaturas são válidas ou inválidas" -bullet2 = "Exibe informações do signatário e data da assinatura" -bullet3 = "Verifica se o documento foi modificado após a assinatura" -bullet4 = "Pode usar certificados personalizados para verificação" -text = "Ao verificar as assinaturas, a ferramenta informa se são válidas, quem assinou o documento, quando foi assinado e se o documento foi alterado desde a assinatura." -title = "Verificando assinaturas" - [changeCreds] changeUsername = "Atualize seu nome de usuário. Você será desconectado após a atualização." credsUpdated = "Conta atualizada" @@ -2737,12 +2936,16 @@ text = "Para tornar essas permissões imutáveis, use a ferramenta Adicionar sen [chat.actions] copy = "Copiar mensagem" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opções do agente Stirling" +assistant = "AI Assistant" clearChat = "Limpar chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Esta operação usará seus créditos de nuvem" tooltip = "Selecionar cor da tela" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Escolha a cor" [common] +actions = "Actions" back = "Voltar" cancel = "Cancelar" close = "Fechar" +codeSample = "Code sample" collapse = "Recolher" confirm = "Confirmar" continue = "Continuar" copied = "Copiado!" copy = "Copiar" done = "Concluído" +enterValue = "Enter value" error = "Erro" expand = "Expandir" loading = "Carregando..." next = "Próximo" +open = "Open" preview = "Pré-visualização" previous = "Anterior" refresh = "Atualizar" +remaining = "Restante" retry = "Tentar novamente" save = "Salvar" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Limpar selecionados" @@ -2988,6 +3199,7 @@ title = "Método de compressão" [compress.settings] desiredSize = "Tamanho de arquivo desejado" desiredSizePlaceholder = "Insira o tamanho" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "A compressão é uma forma fácil de reduzir o tamanho do arquivo. Escolha Tamanho do arquivo para informar um tamanho alvo e deixarmos a qualidade ajustada para você. Escolha Qualidade para definir manualmente a intensidade da compressão." @@ -3190,6 +3402,24 @@ pdfOptions = "Opções de PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Selecione primeiro um formato de origem" settings = "Configurações" single = "Único" @@ -3264,24 +3494,6 @@ description = "Estes cookies são essenciais para o bom funcionamento do site. E posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitoramento avançado" -allInOneWorkspace = "Espaço de trabalho PDF tudo-em-um (visualizador, ferramentas e agente)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Suporte dedicado e SLAs" -fasterThroughput = "Taxa 10x mais rápida" -fullyPrivateFiles = "Arquivos totalmente privados" -largeFileProcessing = "Processamento de arquivos grandes" -orgWideAccess = "Controles de acesso para toda a organização" -premiumAiModels = "Modelos de IA premium" -prioritySupport = "Suporte prioritário" -privateDocCloud = "Nuvem de documentos privada" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Acesso seguro à API" -standardThroughput = "Taxa de processamento padrão" -unlimitedApiAccess = "Acesso ilimitado à API" -unlimitedSeats = "Usuários ilimitados" - [crop] autoCrop = "Cortar automaticamente espaços em branco" noFileSelected = "Selecione um arquivo PDF para iniciar o corte" @@ -3346,6 +3558,7 @@ message = "Falha ao definir o manipulador de PDF padrão" title = "Erro" [defaultApp.prompt] +dontRemind = "Não me lembrar novamente" message = "Torne o Stirling PDF seu aplicativo padrão para abrir arquivos PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Pesquisar..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Editar" @@ -3574,7 +3791,7 @@ deleteAll = "Excluir tudo" deleteSelected = "Apagar Selecionados" deselectAll = "Desselecionar Tudo" details = "Detalhes do arquivo" -download = "Baixar (JSON)" +download = "Baixar" downloadSelected = "Baixar selecionados" dropFilesHere = "Solte os arquivos aqui" fileFormat = "Formato" @@ -3644,11 +3861,13 @@ shareSelected = "Compartilhar selecionados" sharing = "Compartilhamento" showAll = "Mostrar tudo" showHistory = "Mostrar histórico" +sortBy = "Sort files" sortByDate = "Ordenar por data" sortByName = "Ordenar por nome" sortBySize = "Ordenar por tamanho" storage = "Armazenamento" storageState = "Armazenamento" +storageUsed = "Storage used" synced = "Sincronizado" title = "Fazer upload de arquivos PDF" toolChain = "Ferramentas aplicadas" @@ -3694,13 +3913,17 @@ addFiles = "Adicionar arquivos" addingFiles = "Adicionando arquivos…" collapse = "Recolher barra lateral" customizeGroups = "Personalizar grupos" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Abra arquivos para começar" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expandir barra lateral" -files = "Arquivos" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive não está configurado" leaveMyFiles = "Sair de Meus arquivos" +library = "Biblioteca de PDFs" myFiles = "Meus arquivos" noFiles = "Ainda não há arquivos" openFileManager = "Abrir gerenciador de arquivos" @@ -3708,37 +3931,36 @@ openFromComputer = "Abrir do computador" openSettings = "Abrir configurações" other = "Outro" recent = "Recentes" -search = "Pesquisar" -searchPlaceholder = "Pesquisar arquivos..." viewAll = "Ver todos os {{count}} arquivos" [fileSidebar.fileItem] closeViewer = "Fechar visualizador" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Abrir no visualizador" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Adicionar" -addLabel = "Adicionar um rótulo…" -categoryIconAria = "Escolha um ícone para {{name}}" -createCategory = "Criar categoria" -delete = "Excluir categoria" done = "Concluído" -hide = "Ocultar categoria" -newCategory = "Nome da nova categoria…" -removeLabel = "Remover {{name}}" -rename = "Renomear" reset = "Redefinir para os padrões" -search = "Pesquisar rótulos…" -show = "Mostrar categoria" subtitle = "Agrupe seus arquivos em categorias principais. Adicione rótulos existentes ou novos a uma categoria, renomeie-a ou crie a sua própria. Arquivos que não estão em nenhuma das suas categorias aparecem em “Outro”." title = "Categorias da barra lateral" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Adicionar ao espaço de trabalho" addToWorkspaceCount = "Adicionar {{count}} ao espaço de trabalho" @@ -3749,7 +3971,6 @@ backToMyFiles = "Voltar para Meus arquivos" breadcrumbs = "Caminho da pasta" cancel = "Cancelar" classification = "Classificação" -clearSearch = "Limpar busca" clearSelection = "Limpar seleção" closeDetails = "Fechar detalhes" create = "Criar" @@ -3781,6 +4002,7 @@ downloadAll = "Baixar tudo" downloadVersion = "Baixar esta versão" dropOverlay = "Solte arquivos para enviar" dropOverlaySub = "Os arquivos começam em Local. Use 'Move to' ou 'Save to cloud' para organizá-los em uma pasta." +duplicate = "Duplicate" file = "Arquivo" fileInfo = "File info" fileMenu = "Ações do arquivo" @@ -3808,13 +4030,12 @@ refresh = "Atualizar do servidor" remove = "Excluir" removeVersion = "Remover esta versão" rename = "Renomear" +renamed = "Renomeado" renameFolder = "Renomear pasta" resizeFolderTree = "Redimensionar árvore de pastas (setas, Shift para passos maiores; duplo clique para ajustar automaticamente)" save = "Salvar" saveToServer = "Salvar no servidor" saveToServerDisabledHint = "Salvar no servidor não está habilitado neste servidor. Peça ao seu administrador para habilitar." -search = "Pesquisar" -searchPlaceholder = "Pesquisar nesta pasta e subpastas" selectAll = "Selecionar tudo" selectAllHint = "Clique para selecionar tudo. Dica: mantenha Ctrl (ou Cmd) para adicionar arquivos um a um, e Shift para selecionar um intervalo." selectedCount = "{{count}} selecionado(s)" @@ -3874,6 +4095,10 @@ title = "Ainda não há arquivos na nuvem" hint = "Arquivos salvos sem enviar ficam aqui. Solte um arquivo para adicionar." title = "Nenhum arquivo somente local" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Arquivos que você abrir ou editar aparecerão aqui." title = "Nada modificado ainda" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Não foi possível excluir a pasta." deleteFolderFailedDetail = "Não foi possível excluir a pasta: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Não foi possível atualizar a aparência da pasta." folderAppearanceFailedDetail = "Não foi possível atualizar a aparência da pasta: {{message}}" moveFilesFailed = "Não foi possível mover os arquivos." @@ -3910,9 +4137,7 @@ count = "Arquivos" folder = "Pasta" labels = "Rótulos" modified = "Modificado" -name = "Nome" size = "Tamanho" -toolHistoryAtVersion = "Cadeia de ferramentas cumulativa" totalSize = "Tamanho total" type = "Tipo" versionHistory = "Jornada da versão" @@ -3945,6 +4170,11 @@ localHint = "Armazenado apenas neste navegador" shared = "Compartilhado" sharedHint = "Compartilhado com você via link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Mais antigo primeiro" modifiedDesc = "Mais recente primeiro" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Nenhum arquivo disponível no armazenamento. Abra alguns open = "Abrir" openFile = "Abrir arquivo" openFiles = "Abrir arquivos" +selectFile = "Select {{name}}" selectFromStorage = "Selecionar do armazenamento" upload = "Fazer upload" uploadFile = "Fazer upload de arquivo" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tudo salvo" +analyzingFields = "Analysing form fields..." +extractCsvError = "Falha ao extrair CSV" +extractXlsxError = "Falha ao extrair XLSX" +filled = "filled" flattenAfterFilling = "Achatar após o preenchimento" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obr" +requiredFieldsError = "Preencha todos os campos obrigatórios" rescanFields = "Escanear campos novamente" rescanFormFields = "Escanear campos do formulário novamente" save = "Salvar" +saveShortcut = "Ctrl+S para salvar" +unsavedChanges = "Alterações não salvas" [formFill.sidebar] close = "Fechar barra lateral" @@ -4309,6 +4552,11 @@ desc = "Renomeia automaticamente um arquivo PDF com base no cabeçalho detectado tags = "detecção automática,baseado em cabeçalho,organizar,renomear" title = "Renomear PDF automaticamente" +[home.autoRotate] +desc = "Detecte a orientação de cada página e gire-a automaticamente para a posição correta." +tags = "girar automaticamente,corrigir orientação,de cabeça para baixo,de lado,endireitar,orientar,detectar orientação,corrigir rotação,orientar automaticamente,digitalizado,OSD" +title = "Girar automaticamente" + [home.autoSizeSplitPDF] tags = "auto,dividir,tamanho" title = "Divisão Manual do PDF" @@ -4608,15 +4856,12 @@ hint = "Envie uma imagem PNG, JPG, SVG ou outro arquivo de imagem para colocar n label = "Enviar imagem" placeholder = "Selecionar arquivo de imagem" -[imageToPdf] -tags = "conversão,img,jpg,imagem,foto" - [imageToPDF] header = "Imagem para PDF" title = "Imagem para PDF" -[infoBanner] -dismiss = "Dispensar" +[imageToPdf] +tags = "conversão,img,jpg,imagem,foto" [invite] acceptError = "Falha ao criar conta" @@ -4647,11 +4892,9 @@ welcomeTitle = "Você foi convidado!" [landing] addFiles = "Adicionar arquivos" -heroSubtitle = "Solte ou adicione um PDF existente para começar." mobileUpload = "Enviar do celular" openFromComputer = "Abrir do computador" uploadFromComputer = "Fazer upload do computador" -workbenchEmptyStateHero = "Solte um PDF em qualquer lugar" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Insira o nome de usuário" failedToSignIn = "Falha ao entrar com {{provider}}: {{message}}" forgotPassword = "Esqueceu sua senha?" loggingIn = "Entrando..." -logIn = "Entrar" login = "Login" magicLinkSent = "Link mágico enviado para {{email}}! Verifique seu e-mail e clique no link para entrar." mfaCode = "Código de autenticação" @@ -4715,26 +4957,10 @@ signInWith = "Entrar com" title = "Iniciar sessão" unexpectedError = "Erro inesperado: {{message}}" updatePassword = "Atualizar senha" -useEmailInstead = "Login com e-mail" useMagicLink = "Usar link mágico" username = "Nome de usuário" youAreLoggedIn = "Você está conectado!" -[login.slides.edit] -alt = "Editar PDFs" -subtitle = "Com mais de uma dúzia de ferramentas para ajudar a ocultar, assinar, ler e manipular PDFs, você certamente encontrará o que precisa." -title = "Edite PDFs para exibir/proteger as informações que você deseja" - -[login.slides.overview] -alt = "Visão geral do Stirling PDF" -subtitle = "Uma suíte em nuvem com foco em privacidade para PDFs que permite converter, assinar, ocultar e gerenciar documentos, além de 50+ outras ferramentas poderosas." -title = "Tudo o que você precisa para PDFs em um só lugar." - -[login.slides.secure] -alt = "Proteger PDFs" -subtitle = "Adicione senhas, oculte conteúdo e gerencie certificados com facilidade." -title = "Proteja informações sensíveis em seus PDFs" - [margin] large = "Grande" medium = "Médio" @@ -4818,6 +5044,34 @@ uploadSuccess = "Envio concluído!" uploadSuccessMessage = "Suas imagens foram transferidas." validating = "Validando sessão..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Escaneie para enviar fotos. As imagens convertem automaticamente para PDF." descriptionNoConvert = "Escaneie para enviar fotos do seu dispositivo móvel." @@ -4836,8 +5090,13 @@ title = "Enviar do celular" tags = "Multiferramentas,múltiplas operações,Interface do Usuário,Clique e arraste,front-end,lado do cliente,interativo,intratável,movimento,excluir,migrar,dividir" title = "Multiferramentas de PDF" -[navbar] -search = "Pesquisar" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "A autenticação não foi bem-sucedida. Você pode fechar esta janela e tentar novamente." @@ -4966,6 +5225,7 @@ activeFiles = "A visualização de Arquivos Ativos mostra todos allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Fechar" cropSettings = "Agora que selecionamos o arquivo que queremos recortar, podemos configurar a ferramenta Recortar para escolher a área para a qual queremos recortar o PDF." +dialogLabel = "Onboarding" fileCheckbox = "Clicar em um dos arquivos o seleciona para processamento. Você pode selecionar vários arquivos para operações em lote." fileReplacement = "O arquivo modificado substituirá automaticamente o arquivo original no Workbench, permitindo que você o passe facilmente por mais ferramentas." filesButton = "O botão Arquivos na barra de Acesso Rápido permite fazer upload de PDFs para usar nas ferramentas." @@ -4976,6 +5236,7 @@ pinButton = "Você pode usar o botão Fixar se preferir que seu results = "Após a ferramenta terminar, a etapa de Revisão mostrará uma prévia dos resultados neste painel, e permitirá desfazer a operação ou baixar o arquivo. " runButton = "Depois que a ferramenta for configurada, este botão permite executar a ferramenta em todos os PDFs selecionados." selectCropTool = "Vamos selecionar a ferramenta de Recortar para demonstrar como usar uma das ferramentas." +stepOf = "Etapa {{current}} de {{total}}" toolInterface = "Esta é a interface da ferramenta de Recortar. Como você pode ver, não há muito aqui porque ainda não adicionamos nenhum arquivo PDF para trabalhar." workbench = "Este é o Workbench — a área principal onde você visualiza e edita seus PDFs." wrapUp = "Tudo pronto! Você aprendeu sobre as áreas principais do app e como usá-las. Clique no botão Ajuda quando quiser para ver este tour novamente." @@ -4988,6 +5249,26 @@ showMeAround = "Me mostre por aí" skipForNow = "Pular por enquanto" skipTheTour = "Pular o tour" +[onboarding.checklist] +dismiss = "Dispensar" +title = "Configurar Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Execute o Stirling nativamente na sua máquina" +title = "Baixar Stirling para Desktop" + +[onboarding.checklist.inviteTeam] +description = "Colabore com sua equipe" +title = "Convidar membros da equipe" + +[onboarding.checklist.shareAnalytics] +description = "Ajude a melhorar o Stirling" +title = "Compartilhar dados anônimos de uso" + +[onboarding.checklist.takeTour] +description = "Veja como o Stirling funciona em um tour rápido" +title = "Fazer o tour" + [onboarding.desktopInstall] body = "O Stirling funciona melhor como app desktop. Você pode usá-lo offline, acessar documentos mais rápido e fazer edições localmente no computador." selectOs = "Selecionar sistema operacional" @@ -4996,7 +5277,21 @@ titleWithOs = "Download para {{osLabel}}" [onboarding.mfa] authenticationCode = "Código de autenticação" +description = "Proteja sua conta vinculando um app autenticador. Escaneie o QR code ou insira a chave de configuração e confirme o código de 6 dígitos para finalizar." +enable = "Ativar MFA" +enableError = "Não foi possível ativar a autenticação de dois fatores. Verifique o código e tente novamente." +enterCode = "Insira o código de 6 dígitos do seu app." +enterCodeError = "Insira o código de autenticação para continuar." +logout = "Sair" +openAuthenticator = "Abra o Google Authenticator, Authy ou 1Password." qrCodeLoading = "Gerando seu código QR…" +regenerateQrCode = "Gerar QR code novamente" +scanQrCode = "Escaneie o QR code ou insira a chave de configuração abaixo." +setupError = "Não foi possível iniciar a configuração de dois fatores. Tente novamente." +setupKey = "Chave de configuração (entrada manual)" +stepByStep = "Passo a passo" +success = "MFA foi ativado. Agora você pode continuar." +title = "Configuração de autenticação multifator" [onboarding.planOverview] adminBodyLoginDisabled = "Depois de ativar o modo de login, você poderá gerenciar usuários, configurar as configurações e monitorar a integridade do servidor. As primeiras {{freeTierLimit}} pessoas no seu servidor usam o Stirling gratuitamente." @@ -5005,6 +5300,11 @@ adminTitle = "Visão geral do admin" userBody = "Convide colegas, atribua funções e mantenha seus documentos organizados em um espaço de trabalho seguro. Ative o modo de login quando estiver pronto para ir além do uso individual." userTitle = "Visão geral do plano" +[onboarding.processorIntro] +body = "O Stirling agora executa Políticas — regras automatizadas que classificam, protegem e processam cada documento assim que ele chega. Configure-as e monitore as execuções no Processor." +cta = "Conheça o Processor" +title = "Conheça o Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Ano pré-pago" + +[payg.prepaid.banner] +exhausted = "Sua capacidade pré-paga acabou; a cobrança medida foi retomada. Recarregue para manter o desconto." +expiring_one = "Seu ano pré-pago termina em {{count}} dia. A capacidade não utilizada expira — recarregue para iniciar um novo ano." +expiring_other = "Seu ano pré-pago termina em {{count}} dias. A capacidade não utilizada expira — recarregue para iniciar um novo ano." +low = "Você está com {{pct}}% da sua capacidade pré-paga. Recarregue a qualquer momento para manter o processamento sem interrupções." + +[payg.prepaid.card] +subtitle = "Usada antes da cobrança medida e fora do seu limite de gastos." +title = "Capacidade pré-paga" + +[payg.prepaid.meter] +capSuffix = "de {{total}} créditos pré-pagos" +expires = "Expira em {{date}}" + +[payg.prepaid.state] +exhausted = "Esgotada" +healthy = "Muito restante" +low = "Ficando baixa" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF para PDF/A" tags = "arquivo,longo prazo,padrão,conversão,armazenamento,preservação" title = "PDF para PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "impressão,padrão,conversão,produção,pré-impressão,arquivo" title = "PDF para PDF/X" @@ -5822,6 +6151,7 @@ small = "500 créditos" xsmall = "100 créditos" [plan.availablePlans] +currency = "Billing currency" subtitle = "Escolha o plano que atende às suas necessidades" title = "Planos disponíveis" @@ -5937,49 +6267,12 @@ keyDescription = "Cole a chave de licença do seu e-mail" success = "Licença ativada!" successMessage = "Sua licença foi ativada com sucesso. Agora você pode fechar esta janela." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "aplicada" -enforcing = "Aplicando..." -failed = "Falha na aplicação" outputsUnavailable = "Os resultados da política não estão mais disponíveis para download." partialOutputsUnavailable = "Alguns resultados da política não estão mais disponíveis para download." -retrying = "Ocupado, tentando novamente..." runNotFound = "A execução da aplicação não pôde mais ser encontrada." -step = "etapa {{current}}/{{total}}" timedOut = "A aplicação atingiu o tempo limite antes que a execução fosse concluída." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Fechar" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Aplicando {{names}}" applyingProgress = "Aplicando {{names}} ({{done}} de {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} de {{total}} arquivo(s) não puderam ser processados failureTitle = "Exportado sem aplicação completa" printPolicyAppliedBody = "Este PDF foi atualizado para atender a uma política. Revise as alterações e imprima novamente." printPolicyAppliedTitle = "Política aplicada antes da impressão" -queued = "+{{count}} na fila" successTitle = "{{names}} aplicadas" summaryMore = "{{first}}, {{second}} e mais {{more}}" summaryTwo = "{{first}} e {{second}}" -[policies.enforcement.triggerVerb] -convert = "Aplicando políticas antes de converter" -default = "Aplicando políticas" -export = "Aplicando políticas antes de exportar" -input = "Aplicando políticas ao importar" -print = "Aplicando políticas antes de imprimir" - -[policies.field] -accessLog = "Log de acesso" -archiveAfter = "Arquivar após" -auditTrail = "Trilha de auditoria" -belowThreshold = "Abaixo do limite" -destination = "Destino" -frameworks = "Frameworks" -immutableHold = "Retenção imutável" -keepFor = "Manter por" -minConfidence = "Confiança mín." -notify = "Notificar no roteamento" -onViolation = "Quando não conforme" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Adicionar" -addPlaceholder = "Adicionar um rótulo…" -customNote = "Personalizado para sua equipe." -defaultNote = "Usando o padrão integrado, compartilhado com sua equipe." -duplicate = "\"{{name}}\" já existe." -edit = "Editar rótulos" -empty = "Ainda não há rótulos." -export = "Exportar JSON" -iconAria = "Escolha um ícone para {{name}}" -import = "Importar JSON" -importError = "Não foi possível importar esse arquivo." -managedNote = "Os rótulos da equipe são gerenciados pelo líder da equipe." -modalSubtitle = "Compartilhados com toda a sua equipe. O classificador escolhe os rótulos que se encaixam em cada documento." -modalTitle = "Rótulos de classificação" -pickIcon = "Usar ícone de {{label}}" -removeAria = "Remover {{name}}" -resetToDefault = "Redefinir para o padrão" -saveForTeam = "Salvar para a equipe" -saving = "Salvando…" -sectionLabel = "Rótulos de classificação" -startFromScratch = "Começar do zero" -teamCount = "rótulos da equipe" -tooLong = "Os rótulos podem ter no máximo {{max}} caracteres." -ungrouped = "Sem grupo" -view = "Ver rótulos" +categoryCount = "categorias" +hideCategory = "Ocultar categoria" +labelCount = "rótulos" +sharedNote = "Estes rótulos são integrados e compartilhados por toda a sua equipe." +showCategory = "Mostrar categoria" [policies.pii] account = "Números de conta (rotulados)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Números de roteamento bancário dos EUA (ABA)" ssn = "Números de Seguro Social (SSN)" -[policies.settings] -noneExport = "Nenhuma política é executada atualmente na exportação." -noneUpload = "Nenhuma política é executada atualmente no upload." -onExport = "Na exportação" -onUpload = "No upload" -reorderHandle = "Arraste para reordenar" -runOrderDesc = "Quando mais de uma política é executada no mesmo acionador, elas são executadas nesta ordem — cada uma sobre a saída da política anterior. Arraste para reordenar." -title = "Configurações de política" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opções de política" -policySettings = "Configurações de política" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Tentar novamente políticas com falha ({{count}})" -rowProgress = "{{completed}} de {{total}} arquivos processados" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "O que é uma política?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Localiza e oculta automaticamente informações confidenciais — como números de Seguro Social e de cartão — para que não possam ser lidas no documento." -sanitize = "Remove JavaScript oculto do arquivo, para que nada seja executado automaticamente quando alguém o abrir." -watermark = "Aplica uma marca visível (por exemplo, \"Confidencial\") em todas as páginas." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Fechar" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} aplicando..." badgeRan = "A política {{name}} foi executada neste arquivo" +badgeRunning = "{{name}} em execução..." blockingAction = "{{action}} bloqueado enquanto a política está sendo aplicada, aguarde..." dismiss = "Dispensar sobreposição" enforcingTitle = "Aplicando política..." @@ -6151,6 +6316,7 @@ viewAnyway = "Ver arquivo (política ainda sendo aplicada)" [policyOption] autoRedactPhi = "Redigir PHI automaticamente" blockExport = "Bloquear exportação" +ccpa = "CCPA" contracts = "Contratos" documents = "Documentos" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plano Editor" subscribed = "Plano Processor" unlinked = "Não vinculada" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap a partir de documento" -sectionsAriaLabel = "Seções do criador de agentes" -selectorAriaLabel = "Agentes" -subtitle = "Projete, teste e publique os agentes de IA que classificam, extraem e roteiam seus documentos. Defina cenários, limite o acesso a ferramentas, execute um conjunto de referência e publique uma versão." -title = "Criador de agentes" - -[portal.agentBuilder.bootstrap] -cancel = "Cancelar" -dropzoneText = "Escolha um documento de exemplo (PDF ou imagem)" -lead = "Solte um documento de exemplo e nós proporemos cenários e um esquema de extração que você poderá refinar. Nada é publicado até você revisar." -submit = "Fazer bootstrap do agente" -subtitle = "Inicie um novo agente a partir de um arquivo representativo" -title = "Bootstrap a partir de um documento" - -[portal.agentBuilder.empty] -description = "Faça bootstrap de um agente a partir de um documento de exemplo para iniciar seus cenários e esquema de extração, depois refine e publique." -title = "Ainda não há agentes" - -[portal.agentBuilder.evals] -casesPassing = "Casos aprovados" -columnCase = "Caso de avaliação" -columnLatency = "Latência" -columnResult = "Resultado" -fail = "falha" -goldenSetPassRate = "Taxa de aprovação do conjunto de referência" -latencyMs = "{{ms}} ms" -notRun = "não executado" -pass = "aprovado" -passRate = "Taxa de aprovação" -runEvals = "Executar avaliações" - -[portal.agentBuilder.evals.empty] -description = "Avaliações transformam seus cenários em um conjunto de referência repetível. Faça upgrade para capturar a taxa de aprovação ao longo do tempo e condicionar publicações a ela." -title = "Ainda não há conjunto de referência" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "em conjuntos de referência" -activeAgents = "Agentes ativos" -avgPassRate = "Taxa média de aprovação das avaliações" -fleetWide = "em toda a frota" -latestPublished = "Última publicação" -scenarios = "Cenários" -testCases = "casos de teste" -totalDescription_one = "{{count}} no total" -totalDescription_other = "{{count}} no total" - -[portal.agentBuilder.scenarios] -add = "Adicionar" -addScenario = "Adicionar cenário" -enable = "Habilitar" -expectationLabel = "Comportamento esperado" -expectationPlaceholder = "O que o agente deve fazer" -inEval = "em avaliação" -mute = "Silenciar" -muted = "silenciado" -nameLabel = "Nome" -namePlaceholder = "ex.: Escalonamento de conformidade" - -[portal.agentBuilder.tabs] -evals = "Avaliações" -scenarios = "Cenários" -tools = "Ferramentas" -versions = "Versões" - -[portal.agentBuilder.tools] -broadAccess = "Acesso amplo" -deniedHint = "As ferramentas selecionadas estão bloqueadas. Todo o restante permanece disponível para chamada." -deniedTools = "Ferramentas negadas" -governanceGate = "A governança de ferramentas está disponível no plano Enterprise." -restricted = "Restrito" -restrictedAccess = "Acesso restrito a ferramentas" -restrictedDescription = "Permitir todas as ferramentas, exceto as que você negar abaixo." - -[portal.agentBuilder.versions] -current = "atual" -historyGate = "Histórico completo de versões e reversão estão disponíveis no plano Enterprise." -publish = "Publicar" -rollBack = "Reverter" - [portal.assistant] close = "Fechar assistente" error = "Não foi possível acessar o assistente." @@ -6365,9 +6451,21 @@ typing = "Digitando" loading = "Carregando" redirectingToEditor = "Redirecionando para o editor..." +[portal.billing.activation] +maybeLater = "Talvez mais tarde" +title = "Ativar o Processor" + +[portal.billing.activation.payg] +sub = "Defina um limite mensal. Pague pelo que executar." +title = "Pague conforme o uso" + +[portal.billing.activation.prepay] +badge = "2 meses grátis" +sub = "Uma fatura, cartão ou transferência bancária." +title = "Pagar um ano antecipado" + [portal.billing.checkout] noClientSecret = "A função Edge não retornou client_secret." -subtitle = "Adicione um cartão para continuar além da concessão gratuita do plano Editor. O Stripe cuida do resto." title = "Ativar o plano Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Seu pagamento foi aprovado, mas a ativação está demorando um pouco ma close = "Fechar" title = "Quase lá" +[portal.billing.checkout.cap] +amountAria = "Limite mensal de gastos" +back = "Voltar" +continue = "Continuar para o pagamento" +error = "Não foi possível definir seu limite de gastos" +finePrint = "Faturas são emitidas mensalmente. Cancele a qualquer momento." +noLimit = "Sem limite" +note = "Você nunca é cobrado além do seu limite. O processamento apenas pausa." +perMonth = "/ mês" +title = "Defina seu limite de gastos" + +[portal.billing.checkout.card] +fields = "Número do cartão · MM / AA · CVC · CEP" +label = "Dados do cartão" +note = "Dados do cartão coletados pela Stripe. O Stirling nunca armazena PAN ou CVC." + [portal.billing.checkout.error] title = "Não foi possível iniciar o checkout" @@ -6383,10 +6497,9 @@ body = "Seu pagamento foi processado. Estamos ativando o processamento medido na hint = "Mantenha esta janela aberta." title = "Ativando seu plano Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "no env do portal para habilitar o checkout no aplicativo." -bodyBefore = "Defina" -title = "Stripe não configurado" +[portal.billing.checkout.pay] +limit = "Limite mensal de gastos {{amount}}/mês. O processamento pausa no limite." +limitNoCap = "Sem limite de gastos. O processamento não pausará. Faturas são emitidas mensalmente." [portal.billing.enterpriseUpsell] cta = "Monte sua cotação Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plano atual" freeForever = "Grátis para sempre" noTeamResolved = "Nenhuma equipe foi resolvida na sua carteira ainda — atualize e tente novamente." ownerOnly = "Somente o proprietário da equipe pode ativar o plano Processor." +payInvoice = "Pagar fatura para concluir" planName = "Editor" ssoIncluded = "SSO incluído" switchOnProcessor = "Ativar o Processor →" unlimitedUsers = "Usuários ilimitados" +viewQuote = "Ver cotação" [portal.billing.invoices] columnAmount = "Valor" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Solicitações diretas de API" segmentApiLabel = "API" segmentAutomationDesc = "Automações e pipelines" segmentAutomationLabel = "Automação" +sizeMultiplier = "{{formatted}} PDFs usaram um multiplicador de tamanho" +summary = "{{unique}} únicos · {{units}} unidades de medição · média de {{avg}} por PDF" +summaryNoRate = "{{unique}} únicos · {{units}} unidades de medição" unit = "PDFs medidos" +unitsPending_one = "{{units}} unidade de medição pendente de sincronização de instâncias vinculadas" +unitsPending_other = "{{units}} unidades de medição pendentes de sincronização de instâncias vinculadas" + +[portal.billing.prepaid] +capSuffix = "de {{total}} créditos pré-pagos" +expires = "Expira em {{date}}" +eyebrow = "Capacidade pré-paga" +note = "Usada antes da cobrança medida e fora do seu limite de gastos." +topUp = "Recarregar" + +[portal.billing.prepaid.buy] +back = "Voltar" +calcTitle = "Calcule seu pagamento anual" +cancel = "Cancelar" +cancelPurchase = "Cancelar compra" +close = "Fechar" +continueToPayment = "Continuar para o pagamento" +finalise = "Finalizar" +invoiceErrorTitle = "Não foi possível gerar a fatura" +loadingTitle = "Carregando sua cotação" +payTitle = "Pague pelo seu ano" +step = "Etapa {{current}} de {{total}}" + +[portal.billing.prepaid.calc] +change = "Alterar" +deployRow = "Implantação" +done = "Concluído" +enterpriseHint = "Esta é uma escala empresarial. Neste volume, tarifas empresariais superam qualquer desconto de autoatendimento. Bloqueio de tarifa, termos e cotação em minutos." +governanceRow = "Governança" +governanceValue = "{{label}} · {{count}} políticas" +handlesLabel = "Seu Processor" +handlesValue = "lida com {{volume}} créditos / mês" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} políticas" +poolCaption = "Um pool de {{credits}} créditos para o ano. Meses pesados emprestam dos leves." +priceLabel = "Seu ano · 12 meses pelo preço de 10" +rateUnknown = "Mostraremos o preço exato no checkout." +savings = "Você economiza {{amount}} · 2 meses grátis." +sizingRow = "Dimensionamento" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Total de usuários" + +[portal.billing.prepaid.consent] +label = "Entendo que, quando minha capacidade pré-paga for usada ou expirar após 12 meses, o processamento continuará automaticamente na tarifa padrão medida de pagamento conforme o uso (até meu limite de gastos), a menos que eu cancele, e que posso cancelar a qualquer momento pelo portal de cobrança." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gerenciado pelo Stirling. Ativo em minutos." +cloudValue = "Stirling Cloud · Gerenciado" +selfhost = "Auto-hospedado" +selfhostDesc = "Executa em infraestrutura privada. Mesma tarifa." +selfhostValue = "Auto-hospedado · Infraestrutura privada" + +[portal.billing.prepaid.offer] +cta = "Revisar oferta" +subtitle = "Pague antecipadamente um ano de processamento de PDF e ganhe dois meses grátis, usado antes da cobrança medida, fora do seu limite de gastos." +title = "Obtenha 12 meses pelo preço de 10" + +[portal.billing.prepaid.pay] +accountName = "Nome do titular da conta" +accountNamePlaceholder = "ex.: Jane Smith" +companyName = "Nome da empresa (opcional)" +companyNamePlaceholder = "ex.: Acme Inc." +downloadInvoice = "Baixar fatura" +payOnline = "Pagar online" +poNumber = "Número do pedido de compra (opcional)" +poNumberPlaceholder = "ex.: PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avançado" +advancedDesc = "Pipelines impulsionam a maior parte do seu processamento." +none = "Nenhum" +noneDesc = "Ainda não está executando pipelines. Ative-os a qualquer momento." +standard = "Padrão" +standardDesc = "Alguns pipelines reprocessam PDFs recebidos." + +[portal.billing.prepaid.posture] +essentials = "Essenciais" +essentialsDesc = "Classificação, o padrão, mais Compartilhamento" +governed = "Governado" +governedDesc = "Adiciona Segurança e Roteamento" +regulated = "Regulado" +regulatedDesc = "Todas as categorias, incl. Conformidade, Retenção, Ingestão" + +[portal.billing.prepaid.review] +download = "Baixar cotação (PDF)" +downloading = "Preparando…" +share = "compartilhar para aprovação" + +[portal.billing.prepaid.size] +compact = "Compacto" +compactDesc = "Arquivos abaixo de 25 MB, sem cobranças de dados" +heavy = "Pesado" +heavyDesc = "Digitalizados ou com muitas imagens, normalmente 50 MB+" +standard = "Padrão" +standardDesc = "Principalmente pequenos, alguns escaneamentos acima de 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Esgotada" +healthy = "Muito restante" +low = "Ficando baixa" [portal.billing.spendLimit] adjustLimit = "Ajustar limite" @@ -6494,6 +6713,8 @@ label = "Projetado para exceder." [portal.billing.spendThisMonth] eyebrow = "Gasto este mês" +freeRemaining_one = "{{formatted}} PDF gratuito restante" +freeRemaining_other = "{{formatted}} PDFs gratuitos restantes" processed_one = "{{formattedCount}} PDF processado." processed_other = "{{formattedCount}} PDFs processados." processedWithRate_one = "{{formattedCount}} PDF processado, a {{rate}} cada." @@ -6510,6 +6731,7 @@ reachedTitle = "Limite de gastos mensal atingido" title = "Não foi possível abrir o portal do Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "de {{allowance}} PDFs gratuitos usados" capSuffix_other = "de {{allowance}} PDFs gratuitos usados" eyebrow = "Teste do Processor" @@ -6534,6 +6756,376 @@ signature = "assinatura" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nova conexão" +createTitleFor = "Conectar {{name}}" +delete = "Excluir" +edit = "Editar" +editTitle = "Editar conexão" +unsupportedType = "Este tipo de conexão não pode ser editado aqui." + +[portal.connections.categories.advanced] +description = "Para um serviço que ainda não oferecemos suporte." +label = "Avançado" + +[portal.connections.categories.audit] +description = "Registre o que aconteceu com um documento, onde seus auditores já consultam." +label = "Registro de auditoria e conformidade" + +[portal.connections.categories.notify] +description = "Informe outro sistema de que algo aconteceu." +label = "Notificação e fluxo de trabalho" + +[portal.connections.categories.security] +description = "Escaneie ou rotule um documento antes que ele siga em frente." +label = "Segurança e classificação" + +[portal.connections.categories.signing] +description = "Envie um documento para ser assinado ou notarizado." +label = "Assinatura eletrônica e notarização" + +[portal.connections.categories.storage] +description = "Onde os documentos processados acabam - um bucket, um drive, um ticket ou uma página." +label = "Arquivo e anexo" + +[portal.connections.commonFields.accessToken] +helperText = "O Stirling armazena isto criptografado e nunca o mostra novamente." +label = "Token de acesso" + +[portal.connections.commonFields.apiKey] +label = "Chave API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Etapas que usam esta conexão só podem chamar caminhos sob esta URL, nunca outro host." +label = "URL do servidor" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Senha" + +[portal.connections.commonFields.port] +label = "Porta" + +[portal.connections.commonFields.username] +label = "Nome de usuário" + +[portal.connections.commonFields.webhookUrl] +helperText = "Cole a URL fornecida pelo serviço. Apenas caminhos sob ela podem ser chamados." +label = "Webhook URL" + +[portal.connections.fields] +name = "Nome da conexão" +namePlaceholder = "ex.: Produção" + +[portal.connections.picker] +cancel = "Cancelar" +createNew = "Nova conexão..." +placeholder = "Selecione uma conexão" +save = "Salvar conexão" + +[portal.connections.picker2] +back = "Alterar integração" +heading = "O que você deseja conectar?" +noResultsBody = "Ainda não oferecemos suporte a isso - conte-nos sobre ele e avaliaremos adicioná-lo." +noResultsTitle = "Nada corresponde a \"{{query}}\"" +searchPlaceholder = "Buscar integrações - tente \"assinar\", \"ocr\" ou um nome de produto" + +[portal.connections.types.api] +description = "Chame qualquer HTTP API a partir de uma política. Você escolhe a URL, a autenticação e o payload." +label = "API personalizada" + +[portal.connections.types.api.fields.authType] +label = "Autenticação" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nome de usuário e senha" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token em um cabeçalho personalizado" + +[portal.connections.types.api.fields.authType.options.none] +label = "Nenhuma" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Fazer login para obter um token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Etapas que usam esta conexão só podem chamar caminhos sob esta URL, nunca outro host." +label = "URL base" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nome do cabeçalho" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Caminho de login" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Senha" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Se a API responder com um link de download, apenas estes hosts poderão ser buscados. Separe com vírgulas. Deixe em branco para permitir apenas o próprio host da URL base." +label = "Hosts de resultado permitidos" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Enviar o token como este cabeçalho" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "O cabeçalho de resposta no qual a chamada de login retorna o token, por exemplo, X-Auth-Token." +label = "Cabeçalho de resposta do token" + +[portal.connections.types.api.fields.username] +label = "Nome de usuário" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Escaneie documentos em busca de malware em um servidor que você executa - nada sai da sua rede." +label = "ClamAV (auto-hospedado)" + +[portal.connections.types.cloudmersive] +description = "Escaneamento de vírus e conteúdo para documentos em uma política." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloqueie documentos que contenham macros, executáveis, scripts ou objetos incorporados - não apenas malware conhecido." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Anexe documentos processados a uma página do Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Envie documentos para ConsignO Cloud (Notarius) para assinatura eletrônica e notarização." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Seu ambiente ConsignO Cloud, incluindo /api/v1." +label = "URL do servidor" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Chave API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Segredo API" + +[portal.connections.types.consigno.fields.password] +label = "Senha" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Necessário apenas se sua organização usa tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nome de usuário" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publique uma mensagem no Discord quando uma política processar um documento." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexe um evento de auditoria no Elasticsearch sempre que uma política manipular um documento." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Consulte um servidor FTP ou FTPS em busca de novos documentos." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Modo de conexão" + +[portal.connections.types.ftp.fields.passive.options] +active = "Ativo" +passive = "Passivo (recomendado)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP simples envia credenciais e arquivos sem criptografia; escolha FTPS se o servidor oferecer suporte." +label = "Criptografia" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explícito / AUTH TLS)" +implicit = "FTPS (implícito)" +none = "Nenhuma (FTP simples)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publique uma mensagem em um espaço do Google Chat quando uma política for executada." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Anexe documentos processados a uma issue do Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Envie um documento ou uma notificação por email quando uma política for executada." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Arquive documentos processados no Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Senha do app" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detecte dados pessoais no texto do documento usando um servidor Presidio que você executa." +label = "Presidio (auto-hospedado)" + +[portal.connections.types.purview] +description = "Leia e aplique rótulos de sensibilidade do Microsoft Purview em PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcional. Necessário apenas para escolher rótulos por nome; a rotulagem funciona sem isso." +label = "ID do aplicativo (cliente)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Segredo do cliente" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Carimbado em cada rótulo como seu SiteId, para que ferramentas compatíveis com Purview saibam qual organização o aplicou." +label = "ID do diretório (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Um bucket S3 que as fontes podem ler e no qual as saídas de pipeline podem gravar." +label = "Armazenamento S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID da chave de acesso" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Deixe em branco para AWS. Defina isto para MinIO ou outro armazenamento compatível com S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Região" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Chave de acesso secreta" + +[portal.connections.types.sendgrid] +description = "Envie um documento ou uma notificação por email quando uma política for executada." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Consulte um servidor SFTP (SSH) em busca de novos documentos." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcional. A impressão digital SHA-256 da chave de host do servidor (de `ssh-keyscan host | ssh-keygen -lf -`). Quando definida, apenas um servidor que apresentar esta chave será aceito; quando em branco, a chave vista na primeira conexão é fixada e alterações posteriores são recusadas." +label = "Impressão digital da chave de host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frase secreta da chave" + +[portal.connections.types.sftp.fields.password] +helperText = "Deixe em branco se você autenticar com uma chave privada em vez disso." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcional. Cole uma chave privada OpenSSH para autenticar com uma chave em vez de uma senha." +label = "Chave privada" + +[portal.connections.types.slack] +description = "Publique uma mensagem no Slack quando uma política processar um documento." +label = "Slack" + +[portal.connections.types.smb] +description = "Monitore um compartilhamento de rede SMB / CIFS (Windows ou Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Compartilhamento de rede" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcional. O domínio ou grupo de trabalho Windows, se o servidor usar um." +label = "Domínio" + +[portal.connections.types.smb.fields.share] +label = "Compartilhamento" +placeholder = "documentos" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Envie um evento de auditoria para o Splunk sempre que uma política manipular um documento." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Envie um evento de auditoria para um coletor HTTP do Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publique uma mensagem em um canal do Teams quando uma política for executada." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publique cada documento, ou apenas os detalhes, em uma URL de sua escolha." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Acione um cenário Zap ou Make a partir de uma política." +label = "Zapier e Make" + +[portal.docs] +browse = "Navegar pelos docs" +viewSource = "Ver código-fonte no GitHub" + [portal.docs.authentication] codeCaption = "cada solicitação" eyebrow = "INTRODUÇÃO" @@ -6624,6 +7216,9 @@ eyebrow = "HABILIDADES" lead = "Capacidades agrupadas e nomeadas que seu agente invoca como uma única ferramenta. Cada habilidade é uma cadeia determinística de operações com avaliações anexadas." title = "Habilidades do agente" +[portal.docs.toc] +title = "Nesta página" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERÊNCIA DA API" @@ -6697,6 +7292,8 @@ type = "Tipo" user = "Usuário" [portal.documents.queue.empty] +connectSource = "Conectar uma fonte" +createPipeline = "Criar um pipeline" description = "À medida que as fontes enviarem documentos para seus pipelines, eles aparecerão aqui para revisão." title = "Nenhum documento na fila" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Nenhum documento corresponde a este filtro." rowActions = "Ações da linha" sensitiveLabel = "Sensível" -sensitiveTitle = "Sensível — acesso necessário" [portal.documents.table.columns] action = "Pipeline / Ação" document = "Documento" +labels = "Labels" product = "Produto" status = "Status" time = "Hora" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Visto por último" region = "Região" status = "Status" +target = "Target" version = "Versão" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Esta visualização encontrou um erro inesperado. Tente novamente retry = "Tentar novamente" title = "Algo deu errado nesta página" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Voltar" +body = "Baixe o app para desktop ou auto-hospede o servidor." +done = "Concluído" +guide = "Abrir o guia {{name}}" +openInBrowser = "Abrir no navegador em vez disso" +sectionDesktop = "App para desktop" +sectionSelfHosted = "Auto-hospedado" +title = "Instalar o Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Execute a imagem oficial no seu servidor." +note = "Abre em :8080. Login padrão: admin / stirling." +tagline = "Execute o servidor em um contêiner" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instale o chart Helm oficial." +note = "Adicione um PVC para /configs para persistir as configurações." +tagline = "Implante em um cluster com Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Builds nativos para sua distribuição." +guideBtn = "Ver opções de instalação no Linux" +note = "Builds AppImage, .deb e .rpm estão disponíveis para distros comuns." +tagline = "Aplicativo de desktop nativo para sua distro" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ou via Homebrew" +detailBody = "Execute o instalador ou use Homebrew." +downloadBtn = "Baixar para macOS" +tagline = "Aplicativo nativo para Apple silicon e Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Execute o .jar em um servidor bare metal." +downloadBtn = "Baixar o .jar" +note = "Requer Java 21+. Abre em :8080; login padrão admin / stirling." +runLabel = "Depois execute" +tagline = "Execute o .jar em um servidor bare metal" +title = "Configuração manual do servidor" + +[portal.home.download.windows] +altLabel = "Ou via winget" +detailBody = "Execute o instalador ou use um gerenciador de pacotes." +downloadBtn = "Baixar instalador" +tagline = "Aplicativo nativo para Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} ativos" -invite = "Convidar colegas de equipe" +activeOfDeployed = "{{active}} de {{total}} ativos este mês" name = "Stirling PDF Editor" open = "Abrir no navegador" updated = "atualizado {{time}}" +[portal.home.editor.deploy] +finish = "Concluir implantação" +options = "Ver opções de implantação" +start = "Implantar o Editor" + [portal.home.editor.target] cloud = "Nuvem gerenciada" docker = "Auto-hospedado · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Boa tarde" evening = "Boa noite" morning = "Bom dia" -[portal.home.onboarding] -dismiss = "Dispensar configuração" -notStarted = "Não iniciado" -progress = "{{done}} de {{total}} concluídos" -title = "Concluir configuração" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC em toda a organização, preços por volume comprometido e implantação air-gapped." -cta = "Iniciar teste" -ctaQuote = "Solicitar orçamento" -lead = "Para mais de 250 funcionários." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Implante o aplicativo desktop, grátis para sempre." -title = "Baixe o PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} ativas · {{recommended}} recomendadas" -chip = "{{active}} ativas" -title = "Ativar políticas" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} conectadas · todo PDF governado onde quer que chegue" -chip = "{{connected}} conectadas" -title = "Conecte suas fontes" - -[portal.home.quickActions] -subtitle = "Principais tarefas de hoje" -title = "Ações rápidas" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositor de 3 etapas sobre a biblioteca de operações tipadas" -title = "Criar um pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agentes, webhooks, pastas monitoradas" -title = "Conectar uma fonte" - -[portal.home.quickActions.issueApiKey] -blurb = "Chave com escopo, limites de taxa e allowlist de IP" -title = "Emitir uma chave de API" - [portal.infrastructure] manageEditorDeployment = "Gerenciar implantação do Editor" sectionsAriaLabel = "Seções de infraestrutura" @@ -6878,13 +7541,9 @@ heading = "Chaves de API" subheading = "Credenciais com escopo, limites de taxa por chave, permissões e allowlists de IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IPs permitidos" -anyIp = "Qualquer IP (sem allowlist)" created = "Criado" lastUsed = "Usado pela última vez" -permissions = "Permissões" -rateLimit = "Limite de taxa" -rateLimitValue = "{{value}} req/min" +revoke = "Revogar chave" usageMonth = "Uso neste mês" usageToday = "Uso hoje" @@ -6892,6 +7551,15 @@ usageToday = "Uso hoje" description = "Crie uma chave com escopo para começar a chamar a Stirling API." title = "Ainda não há chaves de API" +[portal.infrastructure.apiKeys.error] +load = "Não foi possível carregar suas chaves API. Tente novamente." + +[portal.infrastructure.apiKeys.revoke] +body = "Revogar \"{{name}}\"? Qualquer integração que ainda use esta chave começará imediatamente a receber erros 401. Isso não pode ser desfeito." +cancel = "Cancelar" +confirm = "Revogar chave" +title = "Revogar chave API" + [portal.infrastructure.audit] filterAriaLabel = "Filtrar eventos de auditoria por categoria" heading = "Logs de auditoria" @@ -6935,6 +7603,7 @@ all = "Todos" auth = "Auth" config = "Config" elevation = "Elevação" +policy = "Política" processing = "Processamento" security = "Segurança" @@ -6944,19 +7613,30 @@ title = "Você não tem acesso" [portal.infrastructure.audit.metrics] config = "Config" -elevation = "Elevação" +policy = "Política" processing = "Processamento" totalEvents = "Total de eventos · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autenticação" +config = "Configuração" +elevation = "Elevação" +policy = "Política" +processing = "Processamento" +security = "Segurança" + +[portal.infrastructure.auditStatusLabel] +danger = "Erro" +info = "Info" +success = "Sucesso" +warning = "Aviso" + [portal.infrastructure.createKey] cancel = "Cancelar" createKey = "Criar chave" done = "Concluído" -ipAllowlistHelper = "Intervalos CIDR separados por vírgulas. Deixe em branco para permitir qualquer IP." -ipAllowlistLabel = "Allowlist de IP" keyNameLabel = "Nome da chave" keyNamePlaceholder = "ex.: Produção · ingestão" -permissionsLabel = "Permissões" secretKeyCaption = "Chave secreta" secretWarning = "Armazene isto em um gerenciador de segredos. O Stirling armazena apenas um hash — não há como recuperá-lo depois." subtitle = "Defina o escopo da chave para o mínimo necessário. Você pode rotacionar ou revogar a qualquer momento." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copie este segredo agora — ele não será exibido novamente title = "Criar chave de API" titleCreated = "Chave criada" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Implantado por" -environment = "Ambiente" -product = "Produto" -status = "Status" -version = "Versão" -when = "Quando" - -[portal.infrastructure.deployments.recent] -heading = "Implantações recentes" -subheading = "Os lançamentos mais recentes entre produtos e ambientes." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instâncias" -latency = "Latência" -load = "Carga" -p99 = "P99" -region = "Região" -status = "Status" -throughput = "Throughput" -uptime = "Uptime" -version = "Versão" - -[portal.infrastructure.deployments.regions] -heading = "Regiões" -subheading = "Integridade ao vivo de cada região Stirling implantada — latência, carga e versão de rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "As regiões implantadas aparecerão aqui assim que seu workspace for provisionado." -title = "Nenhuma região implantada" - -[portal.infrastructure.models] -heading = "Modelos" -msValue = "{{value}} ms" -subheading = "O catálogo de modelos e roteamento que impulsiona o processamento de documentos em todo o seu workspace." - -[portal.infrastructure.models.byom] -description = "Registre um modelo on-prem ou auto-hospedado e fixe-o a uma região para processamento vinculado à residência de dados." -title = "Traga seu próprio modelo" - -[portal.infrastructure.models.catalogue] -heading = "Catálogo" -sub = "Modelos gerenciados disponíveis para seu workspace, com latência e custo ao vivo." -subEnterprise = "Modelos gerenciados, próprios e on-prem — com fixação por região disponível." - -[portal.infrastructure.models.catalogue.empty] -description = "Os modelos no catálogo do seu workspace aparecem aqui." -title = "Nenhum modelo disponível" - -[portal.infrastructure.models.columns] -cost = "Custo" -latency = "Latência" -load = "Carga" -model = "Modelo" -status = "Status" -type = "Tipo" -version = "Versão" - -[portal.infrastructure.models.metrics] -activeModels = "Modelos ativos" -avgLatency = "Latência média" -included = "Incluído" -monthlySpend = "Gasto mensal com modelos" - -[portal.infrastructure.models.routing] -empty = "Nenhuma regra de roteamento configurada." -heading = "Regras de roteamento" -sub = "Qual modelo lida com cada operação. O padrão se aplica quando nenhuma regra mais específica corresponde." -subLocked = "Direcione operações para modelos específicos — disponível em planos pagos." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Atualize para Pro para controlar qual modelo lida com cada operação e tipo de documento." -title = "Roteamento de modelos é um recurso pago" - -[portal.infrastructure.models.routingColumns] -default = "Padrão" -docType = "Tipo de documento" -modelForAria = "Modelo para {{operation}}" -operation = "Operação" -routedTo = "Roteado para" - -[portal.infrastructure.security.access.byok] -description = "Forneça uma chave do seu próprio KMS. O Stirling criptografa com ela, mas ainda consegue ler." -label = "Traga sua própria chave (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "As chaves nunca saem do seu KMS. O Stirling mantém apenas ciphertext." -label = "Mantenha sua própria chave (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "O Stirling gerencia as chaves de criptografia. O mais simples — zero operações de chave do seu lado." -label = "Chaves mantidas pelo Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Política de acesso a documentos" -subheading = "Controla quem pode descriptografar documentos processados em repouso." - -[portal.infrastructure.security.attestations] -heading = "Atestados de conformidade" -noReport = "Nenhum relatório disponível" -subheading = "Postura de auditoria framework por framework, com relatórios disponíveis para controles atestados." -viewReport = "Ver relatório →" - -[portal.infrastructure.security.compliance] -heading = "Conformidade" -subheading = "Atestados e certificações que cobrem a plataforma Stirling." - -[portal.infrastructure.security.empty] -description = "A configuração de segurança do seu workspace aparecerá aqui." -title = "Postura de segurança indisponível" - -[portal.infrastructure.security.hyokBanner] -description = "Com HYOK, as chaves de criptografia nunca saem do seu KMS. O Stirling armazena e processa apenas ciphertext que você pode revogar a qualquer momento." -title = "O Stirling não consegue descriptografar seus documentos" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nenhum intervalo de IP configurado — todos os IPs permitidos." -heading = "Allowlist de IP" -sub = "O acesso à API é restrito a estes intervalos CIDR." -subLocked = "Restrinja o acesso à API a intervalos de IP conhecidos — disponível em planos pagos." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Atualize para Pro para restringir o acesso à API a redes específicas." -title = "Allowlist de IP é um recurso pago" - -[portal.infrastructure.security.ipColumns] -added = "Adicionado" -addedBy = "Adicionado por" -cidr = "CIDR" -label = "Rótulo" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmo" -heading = "Gerenciamento de chaves de criptografia" -keyId = "Identificador da chave" -lastRotated = "Rotacionada pela última vez" -rotateKey = "Rotacionar chave" -rotationPolicy = "Política de rotação" -subheading = "Custódia das chaves que criptografam documentos em repouso — quem pode descriptografar e como as chaves são rotacionadas." - -[portal.infrastructure.security.managedBanner] -description = "Custódia bring-your-own-key (BYOK) e hold-your-own-key (HYOK) estão disponíveis no Enterprise. Atualize para fornecer chaves do seu próprio KMS." -title = "As chaves são gerenciadas pelo Stirling no seu plano" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Ásia-Pacífico" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · limite de dados GDPR" -label = "União Europeia" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Estados Unidos" - -[portal.infrastructure.security.residencyHeader] -heading = "Residência de dados" -subheading = "Onde os documentos são armazenados e processados." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} usado" - -[portal.infrastructure.storage.empty] -description = "Armazenamento conectado e uso aparecem aqui." -title = "Nenhum armazenamento configurado" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Ativo" -activeRange = "0–{{value}}d" -archived = "Arquivado" -coldStorage = "cold storage" -deleted = "Excluído" -never = "nunca" -purged = "expurgado" - -[portal.infrastructure.storage.providers] -connect = "Conectar" -connected = "Conectado" -heading = "Provedores conectados" -subheading = "Onde os artefatos processados são gravados." - -[portal.infrastructure.storage.retention] -heading = "Retenção" -subheading = "Por quanto tempo os artefatos são mantidos antes da exclusão do ciclo de vida." -windowLabel = "Janela de retenção padrão" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dia" -days_other = "{{count}} dias" -never = "Nunca excluir" - -[portal.infrastructure.storage.totalUsage] -heading = "Uso total" -progressLabel = "Armazenamento usado" -subheading = "Armazenamento consumido em todos os provedores conectados." +revoked = "Revogado" [portal.infrastructure.tabs] apiKeys = "Chaves de API" @@ -7174,6 +7656,49 @@ models = "Modelos" security = "Segurança" storage = "Armazenamento" +[portal.integrations] +availableHeading = "Disponíveis" +comingSoonHeading = "Em breve" +connect = "Conectar" +connectionCount_one = "{{count}} conexão" +connectionCount_other = "{{count}} conexões" +customApi = "API personalizada" +searchPlaceholder = "Pesquisar integrações" +subtitle = "Tudo com que o Stirling pode se comunicar. As credenciais são criadas uma vez aqui e depois referenciadas por fontes, políticas e pipelines." +title = "Integrações" +typedName = "Nome de {{tool}}" + +[portal.integrations.filters] +all = "Todos" +audit = "Auditoria" +connected = "Conectado" +notify = "Notificar" +security = "Segurança" +signing = "Assinatura" +storage = "Armazenamento" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Conectado" + +[portal.integrations.table] +integration = "Integração" +worksWith = "Funciona com" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Políticas" +sources = "Fontes" + +[portal.legal] +draft = "{{label}} · rascunho" +loadError = "Não foi possível carregar este documento. Tente novamente." +loading = "Carregando…" +title = "Documento legal" + [portal.nav] agent-builder = "Agent Builder" components = "Componentes" @@ -7182,33 +7707,17 @@ documents = "Documentos" editor = "Editor" home = "Início" infrastructure = "Infraestrutura" +integrations = "Integrações" pipelines = "Pipelines" policies = "Políticas" procurement = "Compras" -settings = "Configurações" sources = "Fontes" usage = "Uso e faturamento" users = "Usuários" -[portal.notifications] -markAllRead = "Marcar todas como lidas" -title = "Notificações" -viewAll = "Ver tudo" - -[portal.notifications.ariaLabel] -none = "Notificações, nenhuma não lida" -unread_one = "Notificações, {{count}} não lida" -unread_other = "Notificações, {{count}} não lidas" - -[portal.notifications.count] -allRead = "todas lidas" -loading = "carregando" -new_one = "{{count}} nova" -new_other = "{{count}} novas" - -[portal.notifications.empty] -description = "Nenhuma nova notificação." -title = "Você está em dia" +[portal.nav.section] +platform = "Plataforma PDF" +processor = "Processador PDF" [portal.pipelines] subtitle = "Cada pipeline automatizado de documentos no backend: uma cadeia ordenada de operações sobre um conjunto de fontes, executada por um gatilho. Clique em uma linha para ver suas etapas e fontes." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Novo pipeline" [portal.pipelines.builder] -addStep = "Adicionar ferramenta" +activate = "Activate" back = "Voltar para pipelines" +cannotFollow = "Não é possível receber {{produced}}" +chooseAccount = "Escolha uma conta" +chooseDestination = "Escolha um destino" +chooseOperation = "Escolha o que esta etapa faz" +chooseSource = "Escolha uma fonte" discard = "Descartar alterações" -enabled = "Ativado" +inputs = "Entrada" +inputSource = "Fonte de entrada" +inputTrigger = "Acionador" keepEditing = "Continuar editando" -needsUpload = "Precisa de um arquivo enviado" +moreActions = "More actions" +needsConfiguring = "Precisa ser configurado" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nenhuma ferramenta corresponde à sua busca." -pipelineSettings = "Configurações do pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Buscar ferramentas" -selectToolBody = "Adicione uma ferramenta para criar seu pipeline." -selectToolTitle = "Ainda não há ferramentas" -toolSettings = "Configurações da ferramenta" +sendToSystem = "Enviar para outro sistema" +stepsIncompatible = "Estas etapas não podem ser executadas no que a etapa anterior produz: {{tools}}." +stepsNeedSetup = "Estas etapas ainda precisam ser configuradas antes de salvar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operação não reconhecida, mantida como está." unsavedBody = "Você tem alterações não salvas. Salve-as antes de sair ou descarte-as?" unsavedTitle = "Alterações não salvas" -uploadUnsupported = "Arquivos enviados ainda não são suportados em pipelines, então estas etapas não podem ser salvas: {{tools}}." usesDefaults = "Executa com configurações padrão" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combina todos os arquivos da etapa anterior" +fan-out = "Executa uma vez por arquivo da etapa anterior" +format-mismatch = "Precisa de {{accepts}}, mas a etapa anterior produz {{produced}}" +output-uncertain = "Pode não executar: a saída da etapa anterior depende de como ela está configurada" +source-mismatch = "Precisa de {{accepts}}, mas a entrada deste pipeline é {{produced}}" +undeclared-operation = "Não é possível verificar o que esta etapa aceita" [portal.pipelines.composer] addTool = "Adicionar ferramenta" -cancel = "Cancelar" -chainEmpty = "Adicione uma ferramenta para começar a criar seu pipeline." create = "Criar pipeline" -directory = "Pasta de saída" -directoryHelp = "Caminho absoluto no servidor. Deve estar dentro das pastas permitidas configuradas." +createPaused = "Create paused" editingUnsupported = "A exibição destes parâmetros da ferramenta para edição ainda não é suportada." -moveDown = "Mover para baixo" -moveUp = "Mover para cima" +editSource = "Edit source" name = "Nome" namePlaceholder = "ex.: Varredura de redação" -noSources = "Nenhuma fonte conectada ainda. O pipeline ainda pode ser executado em arquivos fornecidos diretamente a ele." noToolSettings = "Esta ferramenta não tem configurações ajustáveis." -operations_one = "Operação ({{count}})" -operations_other = "Operações ({{count}})" output = "Saída" -removeStep = "Remover operação" save = "Salvar alterações" scheduleEvery = "Executar a cada" -sources = "Fontes" -sourcesLoading = "Carregando fontes..." trigger = "Gatilho" triggerManual = "Somente manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dias" hours = "horas" minutes = "minutos" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Excluir \"{{name}}\"? Isso não pode ser desfeito." cancel = "Cancelar" @@ -7273,28 +7815,62 @@ confirm = "Excluir" title = "Excluir pipeline?" [portal.pipelines.detail] +clearHistory = "Limpar histórico" delete = "Excluir pipeline" run = "Executar agora" [portal.pipelines.empty] action = "Criar um pipeline" +connectSource = "Conectar uma fonte" description = "Crie seu primeiro pipeline: escolha as fontes em que ele é executado, encadeie as operações e escolha para onde vai a saída." title = "Ainda não há pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Ativos" paused = "Pausados" total = "Pipelines" -[portal.pipelines.output] -folder = "Gravar na pasta" -inline = "Retornar arquivos" - [portal.pipelines.run] +allProcessed_one = "Nada para executar: o {{count}} documento da fonte já foi processado." +allProcessed_other = "Nada para executar: todos os {{count}} documentos nas fontes já foram processados." completed_one = "Execução concluída." completed_other = "Todas as {{count}} execuções concluídas." empty = "Nada para executar: as fontes não tinham documentos para processar." failed = "Falha na execução: {{error}}" +inFlight = "Nada novo para executar: documentos ainda estão sendo processados de uma execução anterior." +parked_one = "Nada para executar: {{count}} documento falhou anteriormente e está estacionado. Corrija a causa e limpe o histórico para tentar novamente." +parked_other = "Nada para executar: {{count}} documentos falharam anteriormente e estão estacionados. Corrija a causa e limpe o histórico para tentar novamente." running = "Execução iniciada; ainda em andamento." timeout = "A execução está demorando mais do que o esperado; ela ainda pode terminar em segundo plano." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Fontes" status = "Status" steps = "Etapas" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Monitoramento de pasta" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Agendado" [portal.policies] +defaultName = "Política de {{category}}" subtitle = "Automações permanentes que aplicam um pipeline de ferramentas a cada documento. Cada política é acionada no upload ou na exportação, executa sua cadeia de ferramentas e salva a versão aplicada junto ao original." title = "Políticas" [portal.policies.card] comingSoon = "Atualize para Enterprise" notSetUp = "Não configurado" +requiresAiEngine = "Requer mecanismo de AI" + +[portal.policies.categories.classification] +desc = "Identifique o tipo de cada documento no upload e marque seus metadados para arquivamento e pesquisa." +label = "Classificação" [portal.policies.categories.compliance] desc = "Aplique requisitos HIPAA, GDPR, SOC 2 ou FedRAMP a cada documento." @@ -7344,6 +7927,13 @@ label = "Segurança" [portal.policies.config] scopeAll = "Todos os documentos" +[portal.policies.config.classification] +summary = "Classifica cada documento enviado de acordo com os rótulos da sua equipe e marca seus metadados." + +[portal.policies.config.classification.rules] +0 = "Classificar" +1 = "Marcar metadados" + [portal.policies.config.compliance] summary = "Valida documentos em relação a frameworks regulatórios antes que saiam do sistema." @@ -7371,6 +7961,29 @@ minConfidence = "Confiança mínima" 2 = "Nomear" 3 = "Normalizar" +[portal.policies.config.purview] +label = "Aplicar um rótulo de sensibilidade do Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant Purview" +labelId = "ID do rótulo" +labelIdHelp = "O GUID do rótulo, do portal Purview. Aplicar um rótulo grava seus metadados no PDF; ele não pode criptografar, o que requer o cliente Microsoft." +labelName = "Nome do rótulo (opcional)" +labelNameHelp = "Registrado junto com o ID para que pessoas e ferramentas possam reconhecer o rótulo." + +[portal.policies.config.purview.fields.method] +help = "O Purview distingue um rótulo aplicado por uma política de um escolhido por uma pessoa." +label = "Como o rótulo é registrado" +privileged = "Como escolhido por uma pessoa" +standard = "Como aplicado automaticamente" + +[portal.policies.config.purviewRead] +label = "Ler um rótulo do Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant Purview" +connectionHelp = "Lê o rótulo que o documento já carrega, para que etapas posteriores possam agir sobre ele." + [portal.policies.config.retention] summary = "Aplica por quanto tempo os documentos são mantidos, quando arquivar e quando excluir." @@ -7438,6 +8051,7 @@ title = "Nenhuma atividade ainda" [portal.policies.endpoints] addWatermark = "Marca d'água" autoRedact = "Ocultar PII" +classifyAndLabel = "Classificar" compressPdf = "Comprimir" flatten = "Achatar" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Suas políticas foram salvas e aparecerão assim que a conexão f retry = "Tentar novamente" title = "Backend indisponível" +[portal.policies.operations] +change = "Alterar o que esta etapa faz" +noResults = "Nenhuma etapa corresponde a isso. Tente o nome de um produto ou \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Pesquisar etapas - tente \"scan\", \"attach\" ou o nome de um produto" + +[portal.policies.operations.bodyMode] +binary = "Bytes brutos do arquivo" +json = "Dentro de um payload JSON" +multipart = "Como upload de arquivo (multipart)" + +[portal.policies.operations.clamavScan] +description = "Seu próprio servidor ClamAV verifica o documento; nada sai da sua rede." +label = "Verificar vírus (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rejeita documentos que contenham macros, executáveis, scripts ou objetos incorporados." +label = "Bloquear conteúdo ativo" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive verifica o documento e a execução para se ele não estiver limpo." +label = "Verificar vírus" + +[portal.policies.operations.confluenceAttach] +description = "Arquiva o documento processado em uma página." +label = "Anexar a uma página do Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Envia o documento ao ConsignO Cloud para assinatura." +label = "Enviar para assinatura (ConsignO)" +note = "Envia o documento. A recuperação da cópia assinada ainda não é compatível, porque uma etapa não pode passar um resultado para uma etapa posterior." + +[portal.policies.operations.customApiCall] +description = "Crie a chamada você mesmo: caminho, método, corpo e tratamento da resposta." +label = "Chamar uma API personalizada" + +[portal.policies.operations.discordNotify] +description = "Avisa a um canal que a política tratou um documento." +label = "Publicar uma mensagem no Discord" + +[portal.policies.operations.elasticIndex] +description = "Registra o que a política fez como um documento pesquisável." +label = "Indexar um evento de auditoria no Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart é adequado para APIs de upload; JSON é adequado para APIs que querem o arquivo dentro de um payload; binário envia os bytes brutos." +label = "Como enviar o documento" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Um corpo JSON enviado como está. {{document.base64}} carrega o arquivo; {{document.*}} e {{run.*}} são preenchidos por documento." +label = "Corpo JSON" + +[portal.policies.operations.fields.connection] +label = "Conta" + +[portal.policies.operations.fields.domain] +label = "Domínio Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "O campo do formulário no qual a API espera o documento." +label = "Nome do campo do arquivo" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Um objeto JSON de pares de nome/valor de cabeçalho. A credencial da conexão é adicionada para você." +label = "Cabeçalhos" + +[portal.policies.operations.fields.index] +label = "Índice" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Chave da issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Enviado como o corpo da mensagem. {{document.*}} e {{run.*}} são preenchidos por documento." +label = "Mensagem" + +[portal.policies.operations.fields.method] +label = "Método" + +[portal.policies.operations.fields.pageId] +label = "ID da página" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Anexado à URL base da conexão. Nunca pode alcançar outro host." +label = "Caminho" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Onde gravá-lo no drive. {{document.filename}} é preenchido por documento." +label = "Caminho de destino" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Relatar deixa o documento intacto. Substituir o troca pelo que vier de volta." +label = "O que fazer com a resposta" + +[portal.policies.operations.fields.signerEmail] +label = "Email do signatário" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Assunto" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analisa este texto. Use {{document.title}} ou cole o texto que você quer verificar." +label = "Texto a verificar" + +[portal.policies.operations.fields.to] +label = "Para" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nome de usuário do Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Avisa a um espaço que a política tratou um documento." +label = "Publicar uma mensagem no Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arquiva o documento processado em uma issue." +label = "Anexar a uma issue do Jira" + +[portal.policies.operations.mailgunEmail] +description = "Envia o documento processado como anexo." +label = "Enviar o documento por email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Grava o documento processado em uma pasta." +label = "Upload para Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Seu próprio servidor Presidio relata os dados pessoais que encontra." +label = "Detectar dados pessoais" +note = "Presidio lê texto, não arquivos. Forneça o texto que você quer verificar — o documento em si não é enviado." + +[portal.policies.operations.responseMode] +replace = "Substituir o documento pela resposta" +report = "Manter o documento, registrar a resposta" + +[portal.policies.operations.sendgridEmail] +description = "Envia o documento processado como anexo." +label = "Enviar o documento por email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Avisa a um canal que a política tratou um documento." +label = "Publicar uma mensagem no Slack" + +[portal.policies.operations.splunkEvent] +description = "Registra o que a política fez, onde seus auditores já procuram." +label = "Enviar um evento de auditoria para Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registra o que a política fez em um coletor HTTP." +label = "Enviar um evento de auditoria para Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Avisa a um canal que a política tratou um documento." +label = "Publicar uma mensagem no Teams" + +[portal.policies.operations.webhookPost] +description = "Envia o próprio documento para uma URL que você escolher." +label = "Publicar o documento em um webhook" + +[portal.policies.operations.zapierNotify] +description = "Entrega os detalhes da execução à sua automação." +label = "Acionar um Zap ou cenário Make" + [portal.policies.stats] activeFor = "Ativa" dataProcessed = "Dados processados" @@ -7473,6 +8282,13 @@ label = "Docs aplicados" description = "Configuradas, mas não acionadas" label = "Pausadas" +[portal.policies.table] +appliesTo = "Aplica-se a" +docs = "Docs aplicados" +enforces = "Aplica" +policy = "Política" +status = "Status" + [portal.policies.wizard.actions] back = "Voltar" cancel = "Cancelar" @@ -7480,6 +8296,10 @@ continue = "Continuar" enablePolicy = "Ativar política" saveChanges = "Salvar alterações" +[portal.policies.wizard.capability.classify] +desc = "Identifica o tipo do documento a partir dos rótulos da sua equipe e o marca, para que seja arquivado e pesquisado por categoria." +label = "Classificar o documento" + [portal.policies.wizard.capability.compress] desc = "Comprime o documento para um tamanho de arquivo menor." label = "Reduzir tamanho do arquivo" @@ -7500,10 +8320,18 @@ label = "Ocultar informações sensíveis" desc = "Remove JavaScript oculto para que nada possa ser executado automaticamente quando o documento for aberto." label = "Remover conteúdo ativo" +[portal.policies.wizard.capability.timestampPdf] +desc = "Prova que o documento existia nesta forma exata em um ponto no tempo, usando uma autoridade de timestamp independente. Apenas um hash é enviado - o documento nunca sai do seu servidor." +label = "Adicionar um timestamp confiável" + [portal.policies.wizard.capability.watermark] desc = "Carimba uma marca visível (por exemplo, “Confidencial”) em todas as páginas." label = "Aplicar uma marca d'água" +[portal.policies.wizard.classification] +description = "Cada documento enviado é classificado de acordo com os rótulos integrados e marcado com os tipos correspondentes. O conjunto de rótulos é compartilhado por toda a sua equipe." +labelsHeading = "Rótulos de classificação" + [portal.policies.wizard.errors] noTools = "Ative pelo menos uma ferramenta no fluxo de trabalho primeiro." saveFailed = "Não foi possível salvar a política. Tente novamente." @@ -7548,73 +8376,118 @@ setUp = "Configurar política de {{category}}" [portal.policies.wizard.workflow] description = "Escolha o que esta política faz com cada documento que processa." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} ativas" -noRule = "Nenhuma regra aplicada ainda" -subtitle = "Automações permanentes pelas quais todos os documentos passam, independentemente de qual pipeline os manipula." -title = "O que é executado nos seus PDFs" - [portal.policySummary.action] -comingSoon = "Fazer upgrade para Enterprise" -configure = "Configurar" setUp = "Configurar" -[portal.policySummary.column] -activeRule = "Regra ativa" -policy = "Política" -status = "Status" +[portal.processorFlow] +footnote = "As contagens são das últimas 24 horas. A velocidade do fluxo é ilustrativa." +liveBadge = "Ao vivo" +stats = "{{connected}} conectados · {{processed}} PDFs processados" +title = "Processador PDF" -[portal.policySummary.empty] -description = "Assim que as políticas forem configuradas, as categorias aparecerão aqui." -title = "Nenhuma política ainda" +[portal.processorFlow.lens] +ariaLabel = "Modo de visualização" +flow = "Fluxo" +sankey = "Sankey" -[portal.policySummary.state] -active = "Ativa" -off = "Desativada" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Falhou" +heading = "Resultados" +success = "Entregue" + +[portal.processorFlow.policies] +activeCount = "{{n}} ativos" +count = "{{n}} · 24h" +heading = "Políticas" +setUp = "Configurar" soon = "Em breve" -[portal.processingStatus] -managePlan = "Gerenciar plano" -volumeSuffix = "PDFs processados · últimos 30 dias" +[portal.processorFlow.sankey] +waist = "Políticas · {{n}} ativas" + +[portal.processorFlow.sankey.empty] +description = "Conecte uma fonte e ative uma política para ver documentos fluírem pelo processador." +title = "Ainda não há fluxo" + +[portal.processorFlow.sources] +comingSoonTag = "Conectar" +editor = "Editor Stirling PDF" +heading = "Fontes" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Entrada de email" [portal.procurement] -reset = "Redefinir compras (demo)" subtitle = "Faça sua equipe ser avaliada, contratada e integrada. Todos os documentos em um só lugar." title = "Compras" -[portal.procurement.action] -download = "Download" -pay = "Pagar agora" -request = "Solicitar" -sign = "Revisar e assinar" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Concordar e assinar" +confidential = "Confidencial" confirm = "Li e concordo com o Acordo Stirling Enterprise." -eyebrow = "Acordo" -intro = "Um único acordo combinado cobre sua negociação: Contrato Mestre de Serviços, Formulário de Pedido, EULA e Acordo de Processamento de Dados. Revise-o e depois concorde para aceitar a cotação em uma assinatura comprometida." -title = "Revise seu acordo enterprise" +docName = "Contrato Stirling Enterprise" +docSub = "MSA, Formulário de Pedido, EULA e DPA, combinados em uma assinatura." +download = "Baixar" +downloadDraftError = "Não foi possível gerar o PDF do contrato - o renderizador de documentos está indisponível. Tente novamente em breve ou entre em contato com o suporte." +downloadError = "Não foi possível baixar o contrato assinado. Tente novamente." +legalName = "Nome da entidade legal" +legalNamePlaceholder = "A entidade legal que assinará" +loadError = "Não foi possível carregar o contrato. Tente novamente." +loading = "Carregando o contrato..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Solicitar alterações" +scrollHint = "Role para revisar" +signatory = "Nome do signatário" +signatoryPlaceholder = "Nome completo" +signatoryTitle = "Cargo do signatário" +signatoryTitlePlaceholder = "ex.: Diretor Jurídico" +signError = "Não foi possível registrar sua assinatura. Tente novamente." [portal.procurement.builder] addons = "Complementos" +addressLine1 = "Linha de endereço 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Linha de endereço 2" +addressLine2Placeholder = "Sala, andar, edifício" back = "Voltar" businessName = "Nome da empresa" businessNamePlaceholder = "Sua empresa" +city = "Cidade" +cityPlaceholder = "San Francisco" +completeRequired = "Preencha os campos obrigatórios (marcados com *) com um email válido antes de gerar a cotação." +contactEmail = "Email de contato" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nome do contato" +contactNamePlaceholder = "Jane Doe" continue = "Continuar" -country = "País" -countryEuro = "Zona do euro (EUR €)" -countryUK = "Reino Unido (GBP £)" -countryUS = "Estados Unidos (USD $)" +done = "Concluído" eula = "Li e concordo com a EULA Stirling Enterprise. Ela rege o acordo gerado a partir desta cotação." generate = "Gerar cotação" -included = "Incluído" indemnification = "Indenização por propriedade intelectual" indemnificationSub = "Defendemos reivindicações de propriedade intelectual qualificadas, conforme a EULA" -offlineLicense = "Licença offline / air-gapped" -offlineLicenseSub = "Um arquivo de licença baixável para uma instância self-hosted air-gapped" +paperEyebrow = "Cotação Enterprise" +paperFor = "Preparado para" +pdfSize = "Tamanho do PDF" +poNumber = "Número do pedido de compra" +poNumberPlaceholder = "Opcional" +postalCode = "Código postal" +postalCodePlaceholder = "94105" +posture = "Governança" +posture_count = "~{{count}} políticas" +posture_essentials = "Essenciais" +posture_essentialsSub = "Classificação + Compartilhamento — os padrões" +posture_governed = "Governado" +posture_governedSub = "Adiciona Segurança e Roteamento" +posture_regulated = "Regulado" +posture_regulatedSub = "Todas as categorias — Conformidade, Retenção, Ingestão" qbr = "Revisões de negócios trimestrais" qbrSub = "Seu SE revisa uso e roadmap a cada trimestre" +region = "Estado / região" +regionPlaceholder = "Califórnia" running = "{{annual}} / ano · {{years}} anos {{tcv}}" s1Sub = "Sua equipe e os PDFs que você espera executar a cada ano." s1Title = "Quanto você vai processar?" @@ -7623,6 +8496,12 @@ s2Title = "Compromisso e serviço" s3Sub = "Para a cotação e o acordo que ela gera." s3Title = "Seus dados" serviceLevel = "Nível de serviço" +size_compact = "Compacto" +size_compactSub = "Principalmente texto, abaixo de 1 MB" +size_heavy = "Pesado" +size_heavySub = "Digitalizado ou com muitas imagens, 5 MB+" +size_standard = "Padrão" +size_standardSub = "Texto e imagens mistos, 1 a 5 MB" slDedicated = "Dedicado" slDedicatedSub = "4 horas úteis · gerente de conta dedicado · +30%" slPriority = "Prioritário" @@ -7630,6 +8509,8 @@ slPrioritySub = "Mesmo dia útil · CSM nomeado · +15%" slStandard = "Padrão" slStandardSub = "Próximo dia útil · CSM compartilhado · incluído" stepOf = "Etapa {{n}} de {{total}}" +taxId = "VAT / ID fiscal" +taxIdPlaceholder = "Opcional" term = "Prazo" termDiscount = "Desconto de {{pct}}% por compromisso de vários anos aplicado" title = "Monte sua cotação" @@ -7637,6 +8518,7 @@ training = "Integração e treinamento" trainingSub = "Sessões ao vivo para colocar sua equipe em funcionamento" users = "Total de usuários" usersPlaceholder = "ex.: 250" +viewEula = "Ler" volEstimated = "Estimado a partir de {{count}} usuários (~2.000 PDFs cada, incluindo automação). Edite se souber melhor." volManual = "Usando o valor informado. Reestime pelo tamanho da sua equipe a qualquer momento." volNoUsers = "Não tem certeza? Informe o tamanho da sua equipe e vamos estimar." @@ -7645,55 +8527,55 @@ volumePlaceholder = "ex.: 1.000.000" years_one = "{{count}} ano" years_other = "{{count}} anos" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Concluído" -here = "Você está aqui" -hide = "Ocultar" -optional = "Opcional" -paidAddon = "Complemento pago" -show = "Mostrar" -subtitle = "Tudo que você precisa em cada etapa da jornada, exibido conforme a negociação avança." -supportingSubtitle = "SOC 2, revisões de segurança, formulários fiscais e mais, prontos quando sua equipe de segurança ou compras solicitar. Alguns têm taxa única." -supportingTitle = "Apoiando sua avaliação" +[portal.procurement.documents] +agreement = "Contrato Enterprise" +agreementSub = "MSA, Formulário de Pedido e DPA em uma assinatura." +download = "Baixar" +eula = "EULA e Termos Comerciais" +eulaSub = "O software e os termos comerciais self-service." +invoice = "Fatura" +invoiceSub = "Sua primeira fatura de assinatura." +laterInvoice = "Disponível depois que você aceitar" +laterQuote = "Disponível quando sua cotação for emitida" +quote = "Cotação" +quoteSub = "Sua cotação Enterprise detalhada." +sla = "Anexo SLA" +slaSub = "Níveis de suporte e metas de resposta." +subprocessors = "Subprocessadores" +subprocessorsSub = "Terceiros que processam dados em seu nome." +subtitle = "Sua documentação do negócio, disponível a qualquer momento." title = "Documentos" -upcoming = "Em breve" +view = "Ver" [portal.procurement.error] title = "Algo deu errado" [portal.procurement.hero] -company = "Sua negociação enterprise" +barAria = "Etapa {{current}} de {{total}}" ctaAgreement = "Revisar e assinar acordo" -ctaLive = "Você está no ar" -ctaPayment = "Adicionar pagamento" +ctaExploring = "Configurar seu teste" ctaQuote = "Revisar sua cotação" +ctaReviewQuote = "Revisar cotação" ctaTrial = "Montar sua cotação" +documents = "Documentos" eyebrow = "Compras Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Convidar colegas de equipe" -keyDocs = "Documentos principais" -nextStep = "Próxima etapa: {{action}}" -notStarted = "Não iniciado" +licenseKey = "Chave de licença" +liveSub = "Sua organização está provisionada e sua licença está ativa." +liveTitle = "Você está ativo no Stirling Enterprise" +next = "Próximo: {{stage}}" open = "Abrir compras" scheduleCall = "Agendar uma chamada" -setup1Sub = "Convide seus colegas de equipe" -setup1Title = "Implantar o PDF Editor" -setup2Sub = "Ative o processamento entre editores e outras fontes" -setup2Title = "Conectar o PDF Processor" -setup3Sub = "Ative Segurança, Conformidade, Roteamento ou Retenção quando precisar" -setup3Title = "Adicionar políticas recomendadas" +sentenceAgreement = "revisar e assinar o contrato" +sentenceLive = "provisionamento e rollout" +sentencePayment = "pedido de compra e pagamento" +sentenceQuote = "revisar e aceitar sua cotação" +sentenceTrial = "configurar seu workspace de teste" [portal.procurement.journey] daysLeft_one = "{{count}} dia restante" daysLeft_other = "{{count}} dias restantes" -engineerLabel = "Seu engenheiro de soluções" -eyebrow = "Sua implantação" -live = "Você está no ar no Stirling Enterprise" -nextStep = "Próxima etapa: {{action}}" -subtitle = "Seu engenheiro de soluções acompanha cada etapa. Uma próxima ação por vez; a lista completa está abaixo." -title = "Do teste ao go-live, um caminho guiado" -trialTitle = "Teste Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Uma assinatura cobre MSA, formulário de pedido, EULA e DPA." @@ -7727,6 +8609,9 @@ downloadError = "Ainda não foi possível gerar o arquivo de licença offline downloadOffline = "Baixar licença offline (.lic)" hint = "Cole esta chave em uma instância self-hosted para ativá-la ou guarde-a para seus registros. Mantenha-a segura." label = "Sua chave de licença" +subtitle = "Ative uma instância self-hosted com esta chave ou guarde-a para seus registros." +title = "Sua chave de licença" +trialFileHint = "Este é o arquivo de licença do seu teste. Assim que seu contrato estiver em vigor, volte e baixe-o novamente — ao contrário da chave de licença online, o arquivo .lic não será atualizado sozinho." [portal.procurement.link] cta = "Vincular conta" @@ -7739,65 +8624,62 @@ description = "Sua assinatura está ativa e sua licença foi emitida. Sua equipe eyebrow = "No ar" title = "Você está no ar no Stirling Enterprise" -[portal.procurement.locked] -description = "Chaves de teste, cotações por volume comprometido, o acordo de assinatura única, pagamento e seu registro de documentos ficam todos aqui quando você inicia uma avaliação enterprise." -eyebrow = "Somente Enterprise" -talkToSales = "Falar com vendas" -title = "A trilha de compras abre com Enterprise" - [portal.procurement.milestone] -accept = "Aceitar e continuar" -description = "Baixe o PDF para compartilhá-lo com sua equipe, volte para aceitar quando estiver pronto ou faça alterações." -download = "Baixar PDF" downloadError = "Ainda não foi possível baixar o PDF da cotação — tente novamente em instantes." -edit = "Editar cotação" -eyebrow = "Cotação {{number}}" -perYear = " / ano" -preparedFor = "Preparado para {{company}}" -tcv = "{{value}} de valor total do contrato" -title = "Sua cotação está pronta" - -[portal.procurement.modal] -cancel = "Cancelar" -chooseFile = "Escolher arquivo" -close = "Fechar" -downloadBody = "Seu download começará em breve." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Nenhum arquivo selecionado" -payBody = "Pague seu contrato comprometido com cartão ou transferência bancária pelo Stripe. Seu workspace é provisionado assim que o pagamento for compensado." -payCta = "Continuar para Stripe" -payTitle = "Confirmar pagamento" -requestBodyFree = "Geramos isso sob demanda. Confirme e seu engenheiro de soluções o enviará em breve." -requestBodyPaid = "Este é um complemento pago. Confirme e seu engenheiro de soluções avaliará o escopo e enviará a documentação." -requestCta = "Solicitar" -requestTitle = "Solicitar este documento" -signBody = "Abre o Acordo Stirling Enterprise para assinatura eletrônica: uma assinatura cobre o MSA, formulário de pedido, EULA e DPA. Assinamos automaticamente como contraparte e você avança para o pagamento." -signCta = "Abrir para assinatura" -signTitle = "Revisar e assinar seu acordo" -uploadBody = "Envie-nos sua PO e faturamos contra ela conforme seus termos. Arraste o PDF ou escolha um arquivo abaixo." -uploadCta = "Fazer upload da ordem de compra" -uploadTitle = "Fazer upload da sua ordem de compra" [portal.procurement.payment] description = "Sua cotação foi aceita e sua licença já está ativa — sua equipe pode começar imediatamente. Pague a primeira fatura quando estiver pronto; você pode pagar ou baixá-la aqui, sem precisar de email." +downloadAgreement = "Baixar contrato assinado" downloadInvoice = "Baixar fatura" -simulate = "Simular pagamento recebido (demo)" +eyebrow = "Pagamento" title = "Assinatura criada" viewInvoice = "Ver e pagar fatura" +[portal.procurement.review] +acceptCta = "Aceitar cotação" +annual = "Taxa anual (ano 1)" +downloadCta = "Baixar cotação" +poNumber = "Pedido de compra: {{po}}" +renewal = "Renova a {{amount}}/ano após o prazo (+{{pct}}% CPI)" +tcv = "Prazo de {{years}} anos, pago integralmente antecipado: {{tcv}}" +validUntil = "Válido até {{date}}" + [portal.procurement.schedule] fallback = "Não foi possível carregar o agendador." fallbackLink = "Abrir agendamento em uma nova aba" +loading = "Carregando agendador..." subtitle = "Seu engenheiro de soluções guiará sua equipe durante a implantação. Escolha um horário que seja adequado para você." title = "Agendar uma chamada" -[portal.procurement.status] -action = "Ação necessária" -available = "Disponível" -complete = "Concluído" -pending = "Pendente" -request = "Sob solicitação" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Selado, sem saída" +back = "Voltar" +businessName = "Nome da empresa" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gerenciado, início mais rápido" +continue = "Continuar" +deployment = "Onde deve ser implantado?" +eula = "Eu concordo com o Stirling EULA e os Termos Comerciais." +fullName = "Nome completo" +fullNamePlaceholder = "Seu nome" +invites = "Convidar colegas de equipe · opcional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Agendar uma chamada" +seats = "Pessoas testando" +seatsPlaceholder = "ex.: 15" +selfhost = "Self-hosted" +selfhostSub = "Docker ou Kubernetes" +start = "Iniciar teste" +stepOf = "Etapa {{n}} de {{total}}" +subtitle = "Grátis por 14 dias, sem cartão necessário." +subtitleDetails = "Alguns detalhes para sua cotação e contrato." +talkFirst = "Prefere conversar primeiro?" +title = "Configure seu teste" +viewEula = "Ler" +workEmail = "Email corporativo" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Estender adiciona 7 dias e notifica seu engenheiro de soluções." @@ -7808,109 +8690,113 @@ maxed = "Esgotado" subtitle = "Seu teste gratuito vai até {{date}}. Não é necessário cartão." title = "Teste Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Preços por volume comprometido, SSO + SCIM + RBAC em toda a organização, auditoria imutável de 90 dias e um SE dedicado." -homeCta = "Iniciar teste →" -homeHeadline = "Processe milhões de PDFs." - -[portal.recentActivity] -title = "Atividade recente" -viewAll = "Ver tudo" - -[portal.recentActivity.empty] -description = "Execuções de pipeline, implantações e eventos de agentes aparecerão aqui." -title = "Nada aqui ainda" - -[portal.search] -ariaLabel = "Pesquisar" -placeholder = "Pesquisar no Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Ações rápidas aparecerão aqui assim que estiverem disponíveis." -noActionsTitle = "Nenhuma ação rápida" -noMatches = "Nenhuma correspondência para \"{{query}}\"" -noMatchesDescription = "Tente uma palavra-chave diferente ou navegue pelo catálogo." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Vínculo de conta" -[portal.shell.header] -accountFallback = "Conta" -accountMenu = "Menu da conta" -darkMode = "Modo escuro" -lightMode = "Modo claro" -search = "Pesquisar" -searchPlaceholder = "Pesquisar…" -signOut = "Sair" -switchToDark = "Alternar para tema escuro" -switchToLight = "Alternar para tema claro" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Vincular conta Stirling" -planEditor = "Plano Editor" -planEnterprise = "Plano Enterprise" -planProcessor = "Plano Processor" primaryNav = "Navegação principal" switchApp = "Alternar app" +[portal.shell.topbar] +closeNav = "Fechar navegação" +openNav = "Abrir navegação" +search = "Pesquisar" + [portal.sources] subtitle = "Conexões de entrada reutilizáveis que alimentam documentos no Stirling. Configure uma conexão uma vez e depois referencie-a em qualquer número de políticas. Clique em uma linha para ver sua configuração e quais políticas a usam." title = "Fontes" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Conectar fonte" +[portal.sources.builder] +back = "Voltar para fontes" +backToTypes = "Todos os tipos de fonte" +cancel = "Cancelar" +chooseHint = "Escolha de onde vêm os documentos. Conectores esmaecidos estão a caminho." +comingSoon = "Em breve" +comingSoonHeading = "Em breve" +create = "Criar fonte" +createTitle = "Conectar uma fonte" +delete = "Excluir" +editTitle = "Editar fonte" +enabled = "Ativado" +save = "Salvar alterações" + +[portal.sources.builder.folderAccess] +description = "Automações de pasta só podem usar pastas que um administrador permitiu. Adicione-a nas configurações de Acesso a Pastas e tente novamente." +openSettings = "Configurações de Acesso a Pastas" +title = "Esta pasta não é permitida" + [portal.sources.delete] body = "Excluir \"{{name}}\"? Isso não pode ser desfeito. Políticas que a referenciam precisarão ser atualizadas." cancel = "Cancelar" confirm = "Excluir" title = "Excluir fonte?" -[portal.sources.detail] -closeAriaLabel = "Fechar detalhes" -delete = "Excluir fonte" -docs24h = "Últimas 24h" -docs30d = "Últimos 30 dias" -docsTotal = "Total visto" -docsTrend = "Documentos nos últimos 30 dias" -documents = "Documentos" -edit = "Editar" -notReferenced = "Não referenciada por nenhuma política, então é seguro excluí-la." -pause = "Pausar" -resume = "Retomar" -subtitle = "{{type}} · {{status}}" -usedBy = "Usada por" - -[portal.sources.empty] -description = "Conecte uma pasta (e, em breve, armazenamento em nuvem) para que suas políticas tenham de onde puxar documentos." -title = "Nenhuma fonte conectada ainda" - [portal.sources.kpi] inUse = "Em uso" total = "Conexões" unused = "Não usadas" +[portal.sources.networkFields.connection] +helperText = "A conexão armazenada com o endereço do servidor e as credenciais. Reutilizada por toda fonte que a referencia." +label = "Conexão" + +[portal.sources.networkFields.directory] +helperText = "Pasta no servidor a ser sondada, relativa ao diretório inicial do login ou à raiz do compartilhamento. Deixe em branco para a raiz." +label = "Pasta" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consumir remove cada arquivo do servidor assim que todas as políticas o processarem." +label = "Modo de leitura" + +[portal.sources.networkFields.mode.options] +consume = "Consumir: processar cada arquivo uma vez" +snapshot = "Snapshot: reler a pasta a cada execução" + +[portal.sources.networkFields.recursive] +label = "Profundidade da pasta" + +[portal.sources.networkFields.recursive.options] +all = "Incluir subpastas" +top = "Somente nível superior" + [portal.sources.status] active = "Ativa" disabled = "Desativada" unused = "Não usada" [portal.sources.table] +documents = "Documentos" source = "Fonte" status = "Status" +type = "Type" usedBy = "Políticas" +[portal.sources.types.box] +description = "Puxe documentos de pastas do Box que sua equipe já compartilha." +label = "Box" + +[portal.sources.types.dropbox] +description = "Puxe documentos de uma pasta compartilhada do Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documentos que sua equipe processou no editor, em execuções de políticas e AI." label = "Editor" +noPolicies = "Nenhuma política executada a partir do editor ainda." + +[portal.sources.types.email] +description = "Transforme anexos enviados para uma caixa de entrada dedicada em documentos." +label = "Caixa de entrada de email" [portal.sources.types.folder] description = "Monitore um diretório no servidor em busca de novos documentos." @@ -7943,25 +8829,80 @@ label = "Profundidade da pasta" all = "Incluir subpastas" top = "Somente nível superior" +[portal.sources.types.ftp] +description = "Sonde uma pasta de servidor FTP ou FTPS em busca de novos documentos." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Puxe documentos de pastas compartilhadas do Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Monitore um compartilhamento de rede SMB em busca de novos documentos." +label = "Drive de rede" + +[portal.sources.types.onedrive] +description = "Monitore pastas do OneDrive em busca de novos documentos." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Puxe documentos de um bucket Amazon S3 ou compatível com S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "A conexão armazenada que contém o bucket e as credenciais. Reutilizada por toda fonte e saída de pipeline que a referencia." +label = "Conexão" + +[portal.sources.types.s3.fields.mode] +helperText = "Consumir remove cada objeto do bucket assim que todas as políticas o processarem." +label = "Modo de leitura" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consumir: processar cada objeto uma vez" +snapshot = "Snapshot: reler o bucket a cada execução" + +[portal.sources.types.s3.fields.prefix] +helperText = "Somente objetos cujas chaves começam com este prefixo são processados." +label = "Prefixo da chave" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Sonde uma pasta de entrega SFTP em busca de novos documentos." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Puxe documentos de bibliotecas de documentos do SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Fonte" +[portal.sources.types.webhook] +createNote = "Uma URL de entrega e um segredo de assinatura são gerados quando você cria este webhook. Remetentes enviam documentos por POST para essa URL e as políticas de referência são executadas na chegada." +description = "Receba documentos por HTTP POST assinado de qualquer sistema externo." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de entrega" +secretNote = "O segredo de assinatura é mostrado uma vez, quando o webhook é criado. Recrie a fonte para rotacioná-lo." + +[portal.sources.types.webhook.reveal] +copy = "Copiar" +done = "Concluído" +secret = "Segredo de assinatura" +secretHelp = "Assine o corpo bruto de cada entrega com esta chave (HMAC-SHA256) e envie-o como o cabeçalho X-Stirling-Signature." +secretWarning = "Copie o segredo de assinatura agora. Para sua segurança, ele é mostrado apenas uma vez e não poderá ser recuperado posteriormente." +title = "Webhook criado" +url = "URL de entrega" +usage = "Envie cada documento por POST como o corpo bruto da requisição para a URL de entrega com um tipo de conteúdo binário (application/pdf ou application/octet-stream). As políticas de referência são executadas automaticamente na chegada." + [portal.sources.wizard] -back = "Voltar" -cancel = "Cancelar" -continue = "Continuar" -editTitle = "Editar fonte" name = "Nome" namePlaceholder = "ex.: Recebimento de sinistros" -save = "Salvar alterações" -subtitle = "Etapa {{current}} de {{total}} · {{label}}" -title = "Conectar uma fonte" type = "Tipo" -[portal.sources.wizard.steps] -chooseType = "Escolher tipo" -configure = "Configurar" -review = "Revisar e conectar" +[portal.stepModal] +close = "Fechar" [portal.usage] managePayment = "Gerenciar pagamento" @@ -8021,16 +8962,9 @@ summary = "Possui uma equipe — gerencia os recursos e configurações comparti 2 = "Acesso ao portal pela política padrão" 3 = "Tudo que o Membro pode fazer" -[portal.welcome] -ariaLabel = "Bem-vindo ao Stirling PDF" -badge = "Open-source" -installEditor = "Instalar o Editor" -inviteTeammates = "Convidar colegas de equipe" -openInBrowser = "Abrir no navegador" -perks = "Grátis para sempre · Self-hostable" -subtitle = "O PDF Editor mais seguro do mundo é gratuito para equipes de todos os tamanhos. Inclui mais de 60 operações de PDF e SSO." -title = "Bem-vindo ao" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Ilimitado" [printFile] title = "Imprimir arquivo" @@ -8613,6 +9547,14 @@ text = "Rotate your PDF pages clockwise or anticlockwise in 90-degree increments [rotate.tooltip.header] title = "Rotate Settings Overview" +[ruler] +clearAll = "Limpar tudo" +hideAllLabels = "Ocultar todos os rótulos" +hideSmallLabels = "Ocultar rótulos pequenos" +physicalValues = "Físico" +scaled = "Escalado" +showAllLabels = "Mostrar todos os rótulos" + [sanitize] desc = "Remove potentially harmful elements from PDF files." sanitizationResults = "Sanitisation Results" @@ -8654,31 +9596,45 @@ label = "Remove XMP Metadata" [sanitize.steps] settings = "Settings" -[sanitizePdf] -tags = "clean,secure,safe,remove-threats" - [sanitizePDF] title = "Sanitise PDF" +[sanitizePdf] +tags = "clean,secure,safe,remove-threats" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Adjust page-scale" +[scaleSettings] +activeScale = "Escala ativa" +apply = "Aplicar escala" +applyCalibration = "Aplicar calibração" +calculatedScale = "Escala calculada" +calibrate = "Calibrar" +calibrating = "Calibrando" +calibrationDistanceRequired = "Insira uma distância do mundo real maior que zero" +calibrationInvalid = "Não foi possível calcular a escala a partir desta medição" +calibrationPaperDistance = "Distância medida na página: {{distance}}" +calibrationTitle = "Calibrar escala" +calibrationTooltip = "Meça uma distância conhecida para calcular a escala automaticamente" +customScale = "Escala personalizada" +noneSet = "Nenhuma escala personalizada definida" +presets = "Escalas predefinidas" +ratio = "Proporção da escala" +ratioHelp = "Proporção: 1 unidade da página = X unidades do mundo real" +ratioPlaceholder = "ex.: 100" +ratioPositive = "A proporção deve ser maior que zero" +ratioRequired = "A proporção é obrigatória" +realDistance = "Distância do mundo real" +realDistancePlaceholder = "ex.: 5" +reset = "Redefinir para os padrões" +unit = "Unidade" +unitInvalid = "Unidade inválida: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Angle Threshold:" -10 = "Extra padding (in pixels) around each saved photo so edges aren't cut." -2 = "Tilt (in degrees) needed before we auto-straighten a photo." -3 = "Tolerance:" -4 = "How closely a colour must match the page background to count as background. Higher = looser, lower = stricter." -5 = "Minimum Area:" -6 = "Smallest photo size (in pixels²) we'll keep to avoid tiny fragments." -7 = "Minimum Contour Area:" -8 = "Smallest edge/shape we consider when finding photos (filters dust and specks)." -9 = "Border Size:" - [scannerImageSplit] submit = "Extract Image Scans" tags = "separate,auto-detect,scans,multi-photo,organise" @@ -8710,6 +9666,18 @@ whatThisDoes = "What this does" whatThisDoesDesc = "Automatically finds and extracts each photo from a scanned page or composite image-no manual cropping." whenToUse = "When to use" +[ScannerImageSplit.selectText] +1 = "Angle Threshold:" +10 = "Extra padding (in pixels) around each saved photo so edges aren't cut." +2 = "Tilt (in degrees) needed before we auto-straighten a photo." +3 = "Tolerance:" +4 = "How closely a colour must match the page background to count as background. Higher = looser, lower = stricter." +5 = "Minimum Area:" +6 = "Smallest photo size (in pixels²) we'll keep to avoid tiny fragments." +7 = "Minimum Contour Area:" +8 = "Smallest edge/shape we consider when finding photos (filters dust and specks)." +9 = "Border Size:" + [search] noResults = "No results found" placeholder = "Enter search term..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint} [session] expired = "Your session has expired. Please refresh the page and try again." -[sessionManagement.tooltip] -header = "Managing Signing Sessions" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "New participants added to the end of signing order" -bullet2 = "Cannot add participants after session is finalised" -bullet3 = "Each participant receives a notification when it's their turn" -description = "You can add more participants to an active session at any time before finalisation." -title = "Adding Participants" - -[sessionManagement.tooltip.finalization] -bullet1 = "Full finalisation: All participants have signed" -bullet2 = "Partial finalisation: Some participants haven't signed yet" -bullet3 = "Unsigned participants will be excluded from the final document" -bullet4 = "Once finalised, you can load the signed PDF into active files" -description = "Finalisation combines all signatures into a single signed PDF. This action cannot be undone." -title = "Session Finalisation" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Cannot remove participants who have already signed" -bullet2 = "Removed participants no longer receive notifications" -bullet3 = "Signing order adjusts automatically" -description = "Participants can be removed from sessions before they sign." -title = "Removing Participants" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Each signature is applied sequentially to the PDF" -bullet2 = "Later signers can see earlier signatures" -bullet3 = "Critical for approval workflows and legal chains of custody" -description = "The order you specify when creating the session determines who signs first." -title = "Signature Order" - [settings] close = "Close" +[settings.ai] +documents = "Documentos e RAG" +general = "Geral" +limits = "Limites e desempenho" +models = "Modelos e provedores" +title = "IA" + [settings.configuration] advanced = "Advanced" database = "Database" endpoints = "Endpoints" features = "Features" +folderAccess = "Acesso a pastas" mcp = "MCP Server" storageSharing = "File Storage & Sharing" systemSettings = "System Settings" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Default PDF editor" defaultPdfEditorActive = "Stirling PDF is your default PDF editor" defaultPdfEditorChecking = "Checking..." defaultPdfEditorInactive = "Another application is set as default" +defaultPdfEditorRemind = "Lembrar-me de definir como padrão" +defaultPdfEditorRemindDescription = "Mostrar um banner quando Stirling PDF não for seu aplicativo PDF padrão." defaultPdfEditorSet = "Already Default" defaultStartupView = "Default view on launch" defaultStartupViewDescription = "Choose which view is active when the app starts" @@ -8817,9 +9763,18 @@ languageDescription = "Choose the display language" logout = "Log out" setAsDefault = "Set as Default" theme = "Theme" +themeAccent = "Cor de destaque" +themeAccentCustom = "Cor personalizada" +themeAccentCustomHint = "Personalizada (padrões recomendados)" +themeAccentDark = "Cor de destaque do modo escuro" +themeAccentDefault = "Padrão" +themeAccentDefaultHint = "Tema padrão (recomendado)" +themeAccentDescription = "Botões, links e destaques a seguem, e ela colore sutilmente o app. Os modos claro e escuro têm cada um a sua. O sistema usa a que estiver ativa." +themeAccentLight = "Cor de destaque do modo claro" themeDark = "Dark" themeDescription = "Switch between light and dark mode" themeLight = "Light" +themeReset = "Restaurar tema para o padrão" themeSystem = "System" title = "General" user = "User" @@ -8833,6 +9788,12 @@ intro = "Enable user authentication, team management, and workspace features for learnMore = "Learn more in documentation" title = "For System Administrators" +[settings.general.loginLanding] +description = "Escolha qual app abre quando você entra no Stirling Cloud." +editor = "Editor" +processor = "Processador" +title = "Após entrar" + [settings.general.mode] fullscreen = "Fullscreen" sidebar = "Sidebar" @@ -9013,9 +9974,6 @@ title = "Policies & Privacy" [settings.preferences] title = "Preferences" -[settings.search] -placeholder = "Search settings pages..." - [settings.security] description = "Update your password to keep your account secure." title = "Security" @@ -9041,6 +9999,11 @@ security = "Security" telegram = "Telegram" title = "Security & Authentication" +[settings.signOut] +confirm = "Tem certeza de que deseja sair?" +submit = "Sair" +title = "Sair" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Last page" maintainRatio = "Toggle maintain aspect ratio" next = "Next page" noSavedSigs = "No saved signatures found" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personal Signatures" previous = "Previous page" redo = "Redo" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Background removal failed" hint = "Upload a PNG or JPG image of your signature" label = "Upload signature image" placeholder = "Select image file" +previewAlt = "Current image signature" processing = "Processing image..." removeBackground = "Remove white background (make transparent)" @@ -9207,6 +10172,17 @@ saved = "Select a saved signature above, then click anywhere on the PDF to place text = "After entering your name above, click anywhere on the PDF to place your signature." title = "How to add signature" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Move Signature" pause = "Pause placement" @@ -9653,10 +10629,30 @@ title = "Upload to Server" updateButton = "Update on Server" uploadButton = "Upload to Server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "clique aqui" +fallback = "Se não abriu automaticamente," +opening = "Abrindo Swagger UI em uma nova aba..." tags = "api,documentation,swagger,endpoints,development" title = "API Documentation" @@ -9764,12 +10760,39 @@ selectFilesHint = "Select files in Active Files to run this tool" singleFileScope = "Only applying to: {{fileName}}" viewerMode = "Switch to the file editor to add multiple files." +[toolFormat] +ANY = "qualquer arquivo" +CBR = "um arquivo de quadrinhos (CBR)" +CBZ = "um arquivo de quadrinhos (CBZ)" +CSV = "um arquivo CSV" +EBOOK = "um e-book" +EMAIL = "um e-mail" +EXCEL = "uma planilha" +HTML = "uma página da web" +IMAGE = "imagens" +JAVASCRIPT = "um arquivo JavaScript" +JSON = "um arquivo JSON" +MARKDOWN = "um arquivo Markdown" +NONE = "nenhum arquivo" +PCL = "um arquivo de impressão PCL" +PDF = "um PDF" +PDF_ENCRYPTED = "um PDF protegido por senha" +POSTSCRIPT = "um arquivo PostScript" +PPT = "uma apresentação" +TEXT = "um arquivo de texto" +VIDEO = "um vídeo" +WORD = "um documento Word" +XML = "um arquivo XML" +XPS = "um documento XPS" +ZIP = "um arquivo ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Back to all tools" collapse = "Collapse panel" expand = "Expand panel" goBack = "Go back" +pdfTools = "Ferramentas PDF" placeholder = "Choose a tool to get started" premiumFeature = "Premium feature:" toolsHeader = "Tools" @@ -9806,11 +10829,6 @@ noToolsFound = "No tools found" quickAccess = "QUICK ACCESS" searchPlaceholder = "Search tools..." -[toolPicker.categories] -advancedTools = "Advanced Tools" -recommendedTools = "Recommended Tools" -standardTools = "Standard Tools" - [toolPicker.subcategories] advancedFormatting = "Advanced Formatting" automation = "Automation" @@ -9824,6 +10842,9 @@ removal = "Removal" signing = "Signing" verification = "Verification" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "No tools found" noTools = "No tools available" @@ -9950,28 +10971,53 @@ title = "Usuários" you = "(você)" [users.action] +cancelInvite = "Cancelar convite" deleteTeam = "Excluir equipe" disableMfa = "Redefinir MFA" move = "Mover para equipe" reinstate = "Reintegrar" remove = "Remover da organização" +removeTeam = "Remover da equipe" rename = "Renomear equipe" resetPw = "Redefinir senha" suspend = "Suspender" unlock = "Desbloquear conta" +[users.activity] +daysAgo = "há {{count}} d" +hoursAgo = "há {{count}} h" +justNow = "Agora mesmo" +minutesAgo = "há {{count}} min" +monthsAgo_one = "há {{count}} mês" +monthsAgo_other = "há {{count}} meses" +never = "Nunca" +weeksAgo_one = "há {{count}} semana" +weeksAgo_other = "há {{count}} semanas" +yearsAgo_one = "há {{count}} ano" +yearsAgo_other = "há {{count}} anos" + [users.cap] addProcessor = "+ Processor" approver = "Aprova política" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Cancelar o convite para {{email}}? A pessoa não poderá entrar com o link atual." +cancelInviteTitle = "Cancelar convite" deleteTeamBody = "Excluir a equipe {{name}}? A equipe deve estar vazia primeiro - mova seus membros para outra equipe, e ela não pode ainda possuir nenhuma configuração de integração." deleteTeamTitle = "Excluir equipe" disableMfaBody = "Remover o cadastro de MFA de {{name}}? A pessoa configurará novamente no próximo login, se necessário." disableMfaTitle = "Redefinir MFA" removeBody = "Remover permanentemente {{name}} da organização? Isso não pode ser desfeito." +removeTeamBody = "Remover {{name}} da equipe? A pessoa manterá a conta, mas perderá o acesso aos recursos desta equipe." removeTitle = "Remover membro" [users.empty] @@ -9982,10 +11028,8 @@ title = "Nenhum membro ainda" addToTeam = "Adicionar à equipe" guestCount = "{{count}} convidado" guests = "Convidados" -guestsDesc = "Colaboradores externos, limitados ao que você compartilhou. Somente Editor." ledBy = "liderada por {{owner}}" org = "Organização" -orgDesc = "Proprietários com autoridade em toda a organização e aprovação de políticas" owners = "{{count}} proprietário" team = "Equipe {{name}}" teamMeta = "{{count}} pessoas" @@ -10022,6 +11066,18 @@ username = "Nome de usuário" usernameError = "O nome de usuário deve ter pelo menos 3 caracteres" usernamePlaceholder = "jsmith" +[users.invites] +by = "Convidado por {{who}}" +cancel = "Cancelar" +expiresInDays_one = "Expira em {{count}} dia" +expiresInDays_other = "Expira em {{count}} dias" +expiresToday = "Expira hoje" +title = "Convites pendentes" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Algo deu errado ao acessar o backend, ou você não tem acesso. Tente novamente." title = "Não foi possível carregar os membros" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Search attachments" title = "Attachments" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Título do marcador" +bookmarkTitleRequired = "O título do marcador é obrigatório" closeSidebar = "Fechar barra lateral de marcadores" collapseAll = "Recolher todos os marcadores" +empty = "No bookmarks in this document" expandAll = "Expandir todos os marcadores" +searchPlaceholder = "Pesquisar marcadores" +title = "Bookmarks" [viewer.comments] addComment = "Add comment" @@ -10236,10 +11297,12 @@ locateAnnotation = "Locate in document" moreActions = "More actions" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Page {{page}}" placingHint = "Clique em uma página para posicionar… (cancelar)" removeCommentOnly = "Remove comment only" saveReply = "Save reply" +searchPlaceholder = "Search comments" title = "Comments" typeComment = "Comment" typeInsertText = "Insert Text" @@ -10262,6 +11325,7 @@ unsavedDesc = "You have unsaved changes" closeSidebar = "Fechar barra lateral de camadas" hideAll = "Ocultar todas as camadas" showAll = "Mostrar todas as camadas" +title = "Layers" [viewer.link] delete = "Delete link" @@ -10605,30 +11669,6 @@ title = "Watermark Text" image = "Image" text = "Text" -[wetSignature.tooltip] -header = "Signature Creation Methods" - -[wetSignature.tooltip.draw] -bullet1 = "Customise pen colour and thickness" -bullet2 = "Clear and redraw until satisfied" -bullet3 = "Works on touch devices (tablets, phones)" -description = "Create a handwritten signature using your mouse or touchscreen. Best for personal, authentic signatures." -title = "Draw Signature" - -[wetSignature.tooltip.type] -bullet1 = "Choose from multiple fonts" -bullet2 = "Customise text size and colour" -bullet3 = "Perfect for standardised signatures" -description = "Generate a signature from typed text. Fast and consistent, suitable for business documents." -title = "Type Signature" - -[wetSignature.tooltip.upload] -bullet1 = "Supports PNG, JPG, and other image formats" -bullet2 = "Transparent backgrounds recommended for best results" -bullet3 = "Image will be resized to fit signature area" -description = "Upload a pre-created signature image. Ideal if you have a scanned signature or company logo." -title = "Upload Signature Image" - [workbenchBar] activeFiles = "Active Files" annotations = "Annotations" @@ -10642,6 +11682,7 @@ exitRedaction = "Exit Redaction Mode" exportAll = "Export PDF" exportSelected = "Export Selected Pages" formFill = "Fill Form" +hideToolbar = "Hide toolbar" multiTool = "Multi-Tool" panMode = "Pan Mode" print = "Print PDF" @@ -10652,6 +11693,7 @@ redact = "Redact" rotateLeft = "Rotate Left" rotateRight = "Rotate Right" ruler = "Ruler / Measure" +rulerSettings = "Configurações de escala" save = "Save" saveAll = "Save All" saveAs = "Save As" @@ -10661,6 +11703,7 @@ selectAll = "Select All" selectByNumber = "Select by Page Numbers" selectLanguage = "Select language" share = "Compartilhar" +showToolbar = "Show toolbar" toggleAnnotations = "Toggle Annotations Visibility" toggleAttachments = "Toggle Attachments" toggleBookmarks = "Toggle Bookmarks" diff --git a/frontend/editor/public/locales/pt-PT/translation.toml b/frontend/editor/public/locales/pt-PT/translation.toml index 565fe39088..159dd5439d 100644 --- a/frontend/editor/public/locales/pt-PT/translation.toml +++ b/frontend/editor/public/locales/pt-PT/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabeto" apply = "Aplicar" applyAndContinue = "Guardar e sair" -areYouSure = "Tem a certeza de que pretende sair?" back = "Voltar" black = "Preto" blue = "Azul" @@ -46,10 +45,8 @@ filesSelected = "ficheiros selecionados" font = "Tipo de letra" fontSizeTooltip = "Tamanho do texto do número de página em pontos. Números maiores criam texto maior." fontTypeTooltip = "Família de fontes para os números de página. Escolha com base no estilo do seu documento." -genericSubmit = "Submeter" green = "Verde" help = "Ajuda" -imgPrompt = "Selecione Imagem(ns)" incorrectPasswordMessage = "A palavra-passe atual está incorreta." info = "Informação" invalidUndoData = "Não é possível desfazer: dados de operação inválidos" @@ -62,7 +59,6 @@ loginAgreementTitle = "Acordo de início de sessão" logOut = "Terminar sessão" marginTooltip = "Distância entre o número de página e a margem da página." moreOptions = "Mais opções" -multiPdfPrompt = "Selecione PDFs (2+)" never = "Nunca" no = "Não" noFileSelected = "Nenhum ficheiro selecionado. Carregue um." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordem Personalizada de Páginas (Insira uma lista de números pages = "Páginas" pageSelectionPrompt = "Seleção Personalizada de Páginas (Insira uma lista de números de página separados por vírgulas 1,5,6 ou Funções como 2n+1):" password = "Palavra-passe" -pdfPrompt = "Selecione PDF(s)" pin = "Fixar ficheiro (manter ativo após executar a ferramenta)" pro = "Pro" processingComplete = "O seu ficheiro está pronto." @@ -110,6 +105,7 @@ undoSuccess = "Operação desfeita com sucesso" unknown = "Desconhecido" unpin = "Desafixar ficheiro (substituir após executar a ferramenta)" unsavedChanges = "Tem alterações não guardadas no seu PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Alterações não guardadas" unsupported = "Não suportado" username = "Nome de utilizador" @@ -356,6 +352,7 @@ text = "Estas permissões controlam o que os utilizadores podem fazer com o PDF. title = "Alterar Permissões" [addStamp] +preview = "Pré-visualizar carimbo" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Transferências manuais: clique nas ligações e coloque os fiche noLanguages = "Não foram encontrados idiomas tessdata no diretório configurado." permissionNotice = "O caminho tessdata não é gravável. As transferências serão abertas no navegador; guarde manualmente os ficheiros .traineddata na pasta tessdata." +[admin.settings.ai.documents] +description = "Configure o modelo de embeddings e as definições de recuperação usadas para responder a perguntas sobre documentos. Aplicado ao motor de IA quando guardado." +title = "Documentos e RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Deixe em branco para usar a credencial de ambiente do próprio motor. Aplica-se a implementações autoalojadas com um único motor." +generic = "Chave API de embeddings" +openai = "Chave API da OpenAI" +setPlaceholder = "Guardada - deixe em branco para manter a chave atual" +voyageai = "Chave API da VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL base do endpoint de embeddings compatível com OpenAI / Ollama, por exemplo, http://ollama:11434/v1. Deve apontar para um endpoint interno fidedigno (sensível a SSRF)." +label = "URL base de embeddings" + +[admin.settings.ai.documents.embeddingModel] +description = "Nome do modelo de embeddings. Texto livre; as sugestões são apenas indicações." +label = "Modelo de embeddings" + +[admin.settings.ai.documents.embeddingProvider] +description = "Fornecedor usado para transformar o texto dos documentos em embeddings vetoriais." +label = "Fornecedor de embeddings" + +[admin.settings.ai.documents.maxSearches] +description = "Número máximo de pesquisas de recuperação que o agente pode executar por pedido." +label = "Máx. de pesquisas" + +[admin.settings.ai.documents.reindexNote] +body = "Alterar o modelo de embeddings produz efeito imediatamente, mas os documentos indexados com o modelo anterior têm de ser reindexados para a pesquisa devolver resultados corretos." +title = "Reindexação necessária" + +[admin.settings.ai.documents.saved] +reindexNote = "Se alterou o modelo de embeddings, reindexe os documentos existentes para que a pesquisa use o novo modelo." + +[admin.settings.ai.documents.topK] +description = "Número dos fragmentos mais relevantes recuperados por pesquisa." +label = "Top K" + +[admin.settings.ai.general] +description = "Ligue o Stirling ao motor de IA Python e escolha que capacidades de IA são disponibilizadas. As alterações aplicam-se ao reiniciar." +title = "Motor de IA" + +[admin.settings.ai.general.capabilities] +description = "Ative ou desative funcionalidades individuais de IA. As funcionalidades desativadas ficam ocultas na aplicação." +title = "Capacidades" + +[admin.settings.ai.general.enabled] +description = "Interruptor principal. Quando desativado, não estão disponíveis ferramentas de IA, agentes ou chamadas ao motor." +label = "Ativar IA" + +[admin.settings.ai.general.features.chat] +description = "Assistente de conversação para trabalhar com PDFs." +label = "Assistente de chat" + +[admin.settings.ai.general.features.classify] +description = "Categorize automaticamente documentos por tipo ou conteúdo." +label = "Classificação de documentos" + +[admin.settings.ai.general.features.createPdf] +description = "Gere um novo PDF (por exemplo, a partir de HTML) através de um agente de IA." +label = "Criar PDF a partir de prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Faça perguntas e obtenha respostas baseadas num documento carregado." +label = "Perguntas sobre documentos" + +[admin.settings.ai.general.features.mathAuditor] +description = "Reveja documentos quanto a erros matemáticos e numéricos." +label = "Auditor matemático" + +[admin.settings.ai.general.features.pdfComment] +description = "Adicione comentários de revisão e anotações escritos por IA a um PDF." +label = "Agente de comentários PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Tempo limite para operações de agente mais pesadas, como geração de documentos." +label = "Tempo limite prolongado (segundos)" + +[admin.settings.ai.general.note] +body = "O motor de IA é executado como um serviço separado. O respetivo segredo partilhado" +body2 = "é definido através de uma variável de ambiente do contentor. As chaves API dos fornecedores podem ser introduzidas nestas páginas ou fornecidas como variáveis de ambiente do motor; as chaves introduzidas aqui são enviadas para o motor quando guardadas." +title = "Sobre o motor de IA" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Tempo limite para respostas de chat transmitidas (token a token)." +label = "Tempo limite de stream (segundos)" + +[admin.settings.ai.general.test] +button = "Testar ligação" +failBody = "O motor de IA não respondeu. Verifique o URL, se o contentor do motor está em execução e se a IA está ativada (é necessário reiniciar após ativar)." +failTitle = "Motor de IA inacessível" +okBody = "O motor de IA respondeu a uma verificação de estado." +okTitle = "Motor de IA acessível" + +[admin.settings.ai.general.timeoutSeconds] +description = "Tempo limite para pedidos de IA padrão ao motor." +label = "Tempo limite do pedido (segundos)" + +[admin.settings.ai.general.url] +description = "URL interno do motor de IA Python, por exemplo, http://stirling-pdf-engine:5001." +label = "URL do motor de IA" + +[admin.settings.ai.limits] +description = "Limites de proteção para quanto trabalho os pedidos de IA podem fazer e quantos são executados em simultâneo. Aplicado ao motor de IA quando guardado." +title = "Limites e desempenho" + +[admin.settings.ai.limits.maxCharacters] +description = "Limite de proteção: rejeitar pedidos de IA cujo texto extraído exceda este comprimento." +label = "Máx. de caracteres por pedido" + +[admin.settings.ai.limits.maxPages] +description = "Limite de proteção: rejeitar pedidos de IA com mais do que este número de páginas PDF." +label = "Máx. de páginas por pedido" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Máximo de chamadas de modelo simultâneas em curso em todo o motor." +label = "Concorrência máx. do modelo" + +[admin.settings.ai.models] +description = "Escolha o fornecedor de LLM e os modelos inteligente/rápido que o motor de IA usa. Aplicado ao motor de IA quando guardado." +title = "Modelos e fornecedores" + +[admin.settings.ai.models.apiKey] +anthropic = "Chave API da Anthropic" +description = "Deixe em branco para usar a credencial de ambiente do próprio motor. Aplica-se a implementações autoalojadas com um único motor." +generic = "Chave API" +openai = "Chave API da OpenAI" +setPlaceholder = "Guardada - deixe em branco para manter a chave atual" + +[admin.settings.ai.models.baseUrl] +description = "URL base do endpoint compatível com OpenAI / Ollama, por exemplo, http://ollama:11434/v1." +label = "URL base do fornecedor" +warning = "O URL base deve apontar para um endpoint interno fidedigno. O motor fará pedidos do lado do servidor para esse URL, por isso um valor não fidedigno é sensível a SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Máximo de tokens de saída para o modelo rápido." +label = "Máx. de tokens do modelo rápido" + +[admin.settings.ai.models.fastModel] +description = "Modelo mais barato e rápido para tarefas leves. Texto livre; as sugestões são apenas indicações." +label = "Modelo rápido" + +[admin.settings.ai.models.provider] +description = "Fornecedor de LLM com o qual o motor comunica." +label = "Fornecedor" + +[admin.settings.ai.models.smartMaxTokens] +description = "Máximo de tokens de saída para o modelo inteligente." +label = "Máx. de tokens do modelo inteligente" + +[admin.settings.ai.models.smartModel] +description = "Modelo de alta capacidade para raciocínio complexo. Texto livre; as sugestões são apenas indicações." +label = "Modelo inteligente" + +[admin.settings.ai.saved] +body = "As alterações são enviadas automaticamente para o motor de IA." +bodyNoPush = "Definições guardadas. Serão aplicadas da próxima vez que o motor de IA recolher a sua configuração." +title = "Definições de IA guardadas" + [admin.settings.badge] clickToUpgrade = "Clique para ver os detalhes do plano" @@ -938,6 +1094,25 @@ label = "Regenerar no arranque" description = "Número de dias durante os quais o certificado será válido" label = "Validade do certificado (dias)" +[admin.settings.folderAccess] +description = "Diretórios dos quais as origens de pastas e as saídas de pastas podem ler e nos quais podem escrever. Isto é um limite de segurança: as automatizações nunca podem apontar para um caminho do servidor fora desta lista." +securityNote = "Deixe isto vazio para desativar completamente as origens e saídas de pastas. O diretório de configuração do próprio Stirling está sempre interdito." +title = "Acesso a pastas" + +[admin.settings.folderAccess.implied] +description = "Estes diretórios geridos pelo Stirling são sempre permitidos e não podem ser alterados aqui." +serverStorage = "Armazenamento de ficheiros do servidor" +title = "Sempre permitido" +watchedFolder = "Pasta vigiada da pipeline" + +[admin.settings.folderAccess.roots] +add = "Adicionar" +empty = "Nenhuma pasta permitida. As origens e saídas de pastas estão atualmente desativadas." +hint = "Introduza caminhos absolutos, por exemplo /data/inbox." +label = "Raízes de pastas permitidas" +placeholder = "/data/inbox" +remove = "Remover raiz da pasta" + [admin.settings.general] description = "Configurar definições de aplicação a nível do sistema, incluindo marca e comportamento predefinido." system = "Sistema" @@ -1704,6 +1879,9 @@ width = "Largura" [app] description = "A alternativa gratuita ao Adobe Acrobat (10M+ transferências)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Converter para PDF/A-3b" convertToPdfA3bDescription = "Cria um PDF de arquivo com anexos incorporados" @@ -1842,6 +2020,15 @@ export = "Exportar" accessDenied = "Acesso negado" insufficientPermissions = "Não tem permissão para executar esta ação." +[auth.callback] +completing = "A concluir autenticação" +invalidToken = "O início de sessão OAuth falhou - token inválido." +missingToken = "O início de sessão OAuth falhou - nenhum token recebido." +oauthFailed = "O início de sessão OAuth falhou. Tente novamente." +pleaseWait = "Aguarde enquanto concluímos o seu início de sessão." +signedOut = "A sua sessão foi terminada. Inicie sessão novamente." +windowMayClose = "Pode fechar esta janela quando terminar." + [auth.displayName] guest = "Convidado" user = "Utilizador" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Protege documentos PDF removendo conteúdo potencialmente description = "Esta ferramenta renomeia automaticamente ficheiros PDF com base no seu conteúdo. Analisa o documento para encontrar o título mais adequado a partir do texto." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rotação automática" +tags = "rotação automática,corrigir orientação,de cabeça para baixo,na horizontal,endireitar,orientar,detetar orientação,corrigir rotação,orientação automática,digitalizado,OSD" + +[autoRotate.confidenceThreshold] +title = "Limiar de confiança" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Apenas OCR" +text = "Apenas texto" +title = "Método de deteção" + +[autoRotate.error] +failed = "Ocorreu um erro ao rodar automaticamente o PDF." + +[autoRotate.inferUndetected] +title = "Rodar páginas com baixa confiança para corresponder" + +[autoRotate.report] +applied = "{{degrees}}° no sentido horário" +confidenceValue = "Confiança {{value}}" +noChange = "Sem alteração" +pageLabel = "Página {{number}}" +summary = "{{rotated}} de {{total}} páginas rodadas, {{unchanged}} deixadas como estão" +title = "Relatório de deteção" + +[autoRotate.report.method] +inferred = "Correspondente" +none = "Ignorado" +osd = "OCR" +text = "Texto" + +[autoRotate.report.note] +belowThreshold = "Abaixo do limiar" +blankPage = "A página está em branco" +inferredFromDocument = "Igual às outras páginas" +noDominantDirection = "Direções de texto mistas" +osdFailed = "Não foi encontrado texto legível" +osdNoVerdict = "Não foi possível determinar a orientação" +tesseractUnavailable = "OCR não instalado" +tooFewGlyphs = "Texto insuficiente" + +[autoRotate.results] +title = "Resultados da rotação automática" + +[autoRotate.settings] +title = "Definições" + +[autoRotate.tooltip.confidenceThreshold] +text = "Quão certa a verificação tem de estar antes de uma página ser rodada. Baixe para rodar mais páginas; aumente para deixar mais páginas intactas." +title = "Limiar de confiança" + +[autoRotate.tooltip.description] +text = "Determina a orientação correta de cada página e roda-a para a posição certa. As páginas que já estão direitas não são alteradas." + +[autoRotate.tooltip.detectionMode] +text = "Auto lê primeiro o texto da página e depois analisa a página como imagem se isso não for suficiente. Apenas texto ignora a verificação de imagem, que é a opção mais rápida para documentos que nunca foram digitalizados. Apenas OCR trata todas as páginas como imagem." +title = "Método de deteção" + +[autoRotate.tooltip.header] +title = "Rotação automática" + +[autoRotate.tooltip.inferUndetected] +text = "Páginas em branco ou quase em branco não podem ser verificadas isoladamente. Quando todas as páginas que puderam ser lidas precisam da mesma rotação, estas páginas são rodadas para corresponder." +title = "Rodar páginas com baixa confiança para corresponder" + [autoSizeSplitPDF] tags = "pdf,dividir,documento,organização" @@ -2189,33 +2443,6 @@ keywords = "Palavras-chave: odd, even" numbers = "Números/intervalos: 5, 10-20" progressions = "Progressões: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipos de certificado" - -[certificateChoice.tooltip.organization] -bullet1 = "Gerido pelos administradores do sistema" -bullet2 = "Partilhado entre utilizadores autorizados" -bullet3 = "Representa a identidade da empresa, não do indivíduo" -bullet4 = "Ideal para: documentos oficiais, assinaturas de equipa" -description = "Um certificado partilhado fornecido pela sua organização. Usado para autoridade de assinatura a nível da empresa." -title = "Certificado da Organização" - -[certificateChoice.tooltip.personal] -bullet1 = "Gerado automaticamente na primeira utilização" -bullet2 = "Associado à sua conta de utilizador" -bullet3 = "Não pode ser partilhado com outros utilizadores" -bullet4 = "Ideal para: documentos pessoais, responsabilidade individual" -description = "Um certificado gerado automaticamente, exclusivo da sua conta de utilizador. Adequado para assinaturas individuais." -title = "Certificado Pessoal" - -[certificateChoice.tooltip.upload] -bullet1 = "Requer ficheiro P12/PFX e palavra-passe" -bullet2 = "Pode ser emitido por Autoridades Certificadoras externas" -bullet3 = "Maior nível de confiança para documentos legais" -bullet4 = "Ideal para: contratos legalmente vinculativos, validação externa" -description = "Utilize o seu próprio ficheiro de certificado PKCS#12. Fornece controlo total sobre as propriedades do certificado." -title = "Carregar P12 personalizado" - [certSign] awaitingSignatures = "A aguardar assinaturas" chooseCertificate = "Escolher ficheiro de certificado" @@ -2542,34 +2769,6 @@ server = "Servidor" stepTitle = "Origem do certificado" upload = "Carregar" -[certSign.tooltip.header] -title = "Sobre a Gestão de Assinaturas" - -[certSign.tooltip.overview] -bullet1 = "Verificar assinaturas existentes e a sua validade" -bullet2 = "Ver detalhes sobre signatários e certificados" -bullet3 = "Adicionar novas assinaturas digitais para proteger os seus documentos" -bullet4 = "Vários ficheiros suportados com navegação simples" -text = "Esta ferramenta permite verificar se os seus PDFs estão assinados digitalmente e adicionar novas assinaturas digitais. As assinaturas digitais provam quem criou ou aprovou um documento e mostram se foi alterado desde a assinatura." -title = "O que pode esta ferramenta fazer?" - -[certSign.tooltip.signing] -bullet1 = "Suporta formatos de certificado PEM, PKCS12, JKS e do servidor" -bullet2 = "Opção para mostrar ou ocultar a assinatura no PDF" -bullet3 = "Adicionar motivo, localização e nome do signatário" -bullet4 = "Escolher em que página colocar assinaturas visíveis" -bullet5 = "Usar o certificado do servidor para a opção simples 'Assinar com Stirling-PDF'" -text = "Para assinar um PDF, precisa de um certificado digital (como PEM, PKCS12 ou JKS). Pode optar por tornar a assinatura visível no documento ou mantê-la invisível apenas para efeitos de segurança." -title = "Adicionar assinaturas" - -[certSign.tooltip.validation] -bullet1 = "Mostra se as assinaturas são válidas ou inválidas" -bullet2 = "Mostra informações do signatário e data de assinatura" -bullet3 = "Verifica se o documento foi modificado após a assinatura" -bullet4 = "Pode usar certificados personalizados para verificação" -text = "Ao verificar assinaturas, a ferramenta informa se são válidas, quem assinou o documento, quando foi assinado e se o documento foi alterado desde a assinatura." -title = "Verificação de Assinaturas" - [changeCreds] changeUsername = "Atualize o seu nome de utilizador. Será terminada a sua sessão após a atualização." credsUpdated = "Conta atualizada" @@ -2737,12 +2936,16 @@ text = "Para tornar estas permissões imutáveis, use a ferramenta Adicionar pal [chat.actions] copy = "Copiar mensagem" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opções do agente Stirling" +assistant = "AI Assistant" clearChat = "Limpar chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Esta operação irá usar os seus créditos cloud" tooltip = "Escolher cor do ecrã" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Escolher cor" [common] +actions = "Actions" back = "Voltar" cancel = "Cancelar" close = "Fechar" +codeSample = "Code sample" collapse = "Recolher" confirm = "Confirmar" continue = "Continuar" copied = "Copiado!" copy = "Copiar" done = "Concluído" +enterValue = "Enter value" error = "Erro" expand = "Expandir" loading = "A carregar..." next = "Seguinte" +open = "Open" preview = "Pré-visualizar" previous = "Anterior" refresh = "Atualizar" +remaining = "Restantes" retry = "Tentar novamente" save = "Guardar" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Limpar selecionados" @@ -2988,6 +3199,7 @@ title = "Método de compressão" [compress.settings] desiredSize = "Tamanho de ficheiro pretendido" desiredSizePlaceholder = "Introduzir tamanho" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "A compressão é uma forma simples de reduzir o tamanho do seu ficheiro. Escolha Tamanho do ficheiro para introduzir um tamanho alvo e deixarmos ajustar a qualidade por si. Escolha Qualidade para definir manualmente a intensidade da compressão." @@ -3190,6 +3402,24 @@ pdfOptions = "Opções de PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Selecione primeiro um formato de origem" settings = "Definições" single = "Único" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitorização avançada" -allInOneWorkspace = "Workspace PDF tudo-em-um (visualizador, ferramentas e agente)" -apiSandbox = "Sandbox de API" -dedicatedSupportSlas = "Suporte dedicado e SLAs" -fasterThroughput = "Débito 10x mais rápido" -fullyPrivateFiles = "Ficheiros totalmente privados" -largeFileProcessing = "Processamento de ficheiros grandes" -orgWideAccess = "Controlos de acesso para toda a organização" -premiumAiModels = "Modelos de IA premium" -prioritySupport = "Suporte prioritário" -privateDocCloud = "Nuvem de Documentos Privada" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Acesso seguro à API" -standardThroughput = "Débito padrão" -unlimitedApiAccess = "Acesso ilimitado à API" -unlimitedSeats = "Lugares ilimitados" - [crop] autoCrop = "Recortar automaticamente espaços em branco" noFileSelected = "Selecione um ficheiro PDF para começar a recortar" @@ -3346,6 +3558,7 @@ message = "Falha ao definir a aplicação de PDF predefinida" title = "Erro" [defaultApp.prompt] +dontRemind = "Não voltar a lembrar-me" message = "Torne o Stirling PDF a sua aplicação predefinida para abrir ficheiros PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Pesquisar..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Editar" @@ -3644,11 +3861,13 @@ shareSelected = "Partilhar selecionados" sharing = "Partilha" showAll = "Mostrar tudo" showHistory = "Mostrar histórico" +sortBy = "Sort files" sortByDate = "Ordenar por data" sortByName = "Ordenar por nome" sortBySize = "Ordenar por tamanho" storage = "Armazenamento" storageState = "Armazenamento" +storageUsed = "Storage used" synced = "Sincronizado" title = "Carregar ficheiros PDF" toolChain = "Ferramentas aplicadas" @@ -3694,13 +3913,17 @@ addFiles = "Adicionar ficheiros" addingFiles = "A adicionar ficheiros…" collapse = "Recolher barra lateral" customizeGroups = "Personalizar grupos" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Abra ficheiros para começar" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expandir barra lateral" -files = "Ficheiros" googleDrive = "Google Drive" googleDriveDisabled = "O Google Drive não está configurado" leaveMyFiles = "Sair de Os meus ficheiros" +library = "Biblioteca de PDF" myFiles = "Os meus ficheiros" noFiles = "Ainda sem ficheiros" openFileManager = "Abrir gestor de ficheiros" @@ -3708,37 +3931,36 @@ openFromComputer = "Abrir do computador" openSettings = "Abrir definições" other = "Outro" recent = "Recente" -search = "Pesquisar" -searchPlaceholder = "Pesquisar ficheiros..." viewAll = "Ver todos os {{count}} ficheiros" [fileSidebar.fileItem] closeViewer = "Fechar visualizador" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Abrir no visualizador" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Adicionar" -addLabel = "Adicionar uma etiqueta…" -categoryIconAria = "Escolha um ícone para {{name}}" -createCategory = "Criar categoria" -delete = "Eliminar categoria" done = "Concluído" -hide = "Ocultar categoria" -newCategory = "Novo nome da categoria…" -removeLabel = "Remover {{name}}" -rename = "Mudar nome" reset = "Repor predefinições" -search = "Pesquisar etiquetas…" -show = "Mostrar categoria" subtitle = "Agrupe os seus ficheiros em categorias principais. Adicione etiquetas existentes ou novas a uma categoria, mude o nome ou crie a sua própria. Ficheiros que não estejam em nenhuma das suas categorias aparecem em “Outro”." title = "Categorias da barra lateral" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Adicionar ao espaço de trabalho" addToWorkspaceCount = "Adicionar {{count}} ao espaço de trabalho" @@ -3749,7 +3971,6 @@ backToMyFiles = "Voltar a Os meus ficheiros" breadcrumbs = "Caminho da pasta" cancel = "Cancelar" classification = "Classificação" -clearSearch = "Limpar pesquisa" clearSelection = "Limpar seleção" closeDetails = "Fechar detalhes" create = "Criar" @@ -3781,6 +4002,7 @@ downloadAll = "Transferir todos" downloadVersion = "Transferir esta versão" dropOverlay = "Largar ficheiros para carregar" dropOverlaySub = "Os ficheiros começam em Local. Use 'Mover para' ou 'Guardar na nuvem' para os organizar numa pasta." +duplicate = "Duplicate" file = "Ficheiro" fileInfo = "File info" fileMenu = "Ações do ficheiro" @@ -3808,13 +4030,12 @@ refresh = "Atualizar do servidor" remove = "Eliminar" removeVersion = "Remover esta versão" rename = "Renomear" +renamed = "Renomeado" renameFolder = "Renomear pasta" resizeFolderTree = "Redimensionar árvore de pastas (teclas de seta, Shift para passos maiores; duplo clique para ajustar automaticamente)" save = "Guardar" saveToServer = "Guardar no servidor" saveToServerDisabledHint = "Guardar no servidor não está ativado neste servidor. Peça ao seu administrador para o ativar." -search = "Pesquisar" -searchPlaceholder = "Pesquisar nesta pasta e subpastas" selectAll = "Selecionar tudo" selectAllHint = "Clique para selecionar tudo. Dica: mantenha Ctrl (ou Cmd) para adicionar ficheiros um a um, Shift para selecionar um intervalo." selectedCount = "{{count}} selecionados" @@ -3874,6 +4095,10 @@ title = "Ainda não há ficheiros na nuvem" hint = "Os ficheiros guardados sem carregar ficam aqui. Largue um ficheiro para adicionar." title = "Sem ficheiros apenas locais" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Os ficheiros que abrir ou editar aparecerão aqui." title = "Ainda nada modificado" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Não foi possível eliminar a pasta." deleteFolderFailedDetail = "Não foi possível eliminar a pasta: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Não foi possível atualizar a aparência da pasta." folderAppearanceFailedDetail = "Não foi possível atualizar a aparência da pasta: {{message}}" moveFilesFailed = "Não foi possível mover os ficheiros." @@ -3910,9 +4137,7 @@ count = "Ficheiros" folder = "Pasta" labels = "Etiquetas" modified = "Modificado" -name = "Nome" size = "Tamanho" -toolHistoryAtVersion = "Cadeia de ferramentas cumulativa" totalSize = "Tamanho total" type = "Tipo" versionHistory = "Percurso da versão" @@ -3945,6 +4170,11 @@ localHint = "Apenas armazenado neste navegador" shared = "Partilhado" sharedHint = "Partilhado consigo via ligação" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Mais antigos primeiro" modifiedDesc = "Mais recentes primeiro" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Sem ficheiros disponíveis no armazenamento. Abra alguns open = "Abrir" openFile = "Abrir ficheiro" openFiles = "Abrir ficheiros" +selectFile = "Select {{name}}" selectFromStorage = "Selecionar do armazenamento" upload = "Carregar" uploadFile = "Carregar ficheiro" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tudo guardado" +analyzingFields = "Analysing form fields..." +extractCsvError = "Falha ao extrair CSV" +extractXlsxError = "Falha ao extrair XLSX" +filled = "filled" flattenAfterFilling = "Achatar após o preenchimento" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obrig." +requiredFieldsError = "Preencha todos os campos obrigatórios" rescanFields = "Reanalisar campos" rescanFormFields = "Reanalisar campos do formulário" save = "Guardar" +saveShortcut = "Ctrl+S para guardar" +unsavedChanges = "Alterações não guardadas" [formFill.sidebar] close = "Fechar barra lateral" @@ -4309,6 +4552,11 @@ desc = "Renomeia automaticamente um ficheiro PDF com base no cabeçalho detetado tags = "deteção automática,baseado em cabeçalho,organizar,renomear" title = "Auto-renomear PDF" +[home.autoRotate] +desc = "Detete a orientação de cada página e rode-a automaticamente para ficar direita." +tags = "rotação automática,corrigir orientação,de cabeça para baixo,na horizontal,endireitar,orientar,detetar orientação,corrigir rotação,orientação automática,digitalizado,OSD" +title = "Rotação automática" + [home.autoSizeSplitPDF] tags = "auto,dividir,tamanho" title = "Auto-dividir por tamanho/n.º" @@ -4608,15 +4856,12 @@ hint = "Carregue um ficheiro de imagem PNG, JPG, SVG ou outro para colocar no PD label = "Carregar imagem" placeholder = "Selecionar ficheiro de imagem" -[imageToPdf] -tags = "conversão,img,jpg,imagem,foto" - [imageToPDF] header = "Imagem para PDF" title = "Imagem para PDF" -[infoBanner] -dismiss = "Ignorar" +[imageToPdf] +tags = "conversão,img,jpg,imagem,foto" [invite] acceptError = "Falha ao criar conta" @@ -4647,11 +4892,9 @@ welcomeTitle = "Foi convidado!" [landing] addFiles = "Adicionar ficheiros" -heroSubtitle = "Largue ou adicione um PDF existente para começar." mobileUpload = "Carregar do telemóvel" openFromComputer = "Abrir do computador" uploadFromComputer = "Carregar do computador" -workbenchEmptyStateHero = "Largue um PDF em qualquer lugar" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Introduza o nome de utilizador" failedToSignIn = "Falha ao iniciar sessão com {{provider}}: {{message}}" forgotPassword = "Esqueceu-se da palavra-passe?" loggingIn = "A iniciar sessão..." -logIn = "Iniciar sessão" login = "Início de sessão" magicLinkSent = "Ligação mágica enviada para {{email}}! Verifique o seu email e clique na ligação para iniciar sessão." mfaCode = "Código de autenticação" @@ -4715,26 +4957,10 @@ signInWith = "Iniciar sessão com" title = "Iniciar sessão" unexpectedError = "Erro inesperado: {{message}}" updatePassword = "Atualizar palavra-passe" -useEmailInstead = "Iniciar sessão com email" useMagicLink = "Usar ligação mágica" username = "Nome de utilizador" youAreLoggedIn = "Sessão iniciada!" -[login.slides.edit] -alt = "Editar PDFs" -subtitle = "Com mais de uma dúzia de ferramentas para o ajudar a ocultar, assinar, ler e manipular PDFs, encontrará certamente o que procura." -title = "Edite PDFs para mostrar/proteger a informação que pretende" - -[login.slides.overview] -alt = "Visão geral do Stirling PDF" -subtitle = "Uma suite cloud com foco na privacidade para PDFs que permite converter, assinar, ocultar e gerir documentos, juntamente com mais de 50 outras ferramentas poderosas." -title = "A sua solução tudo-em-um para todas as suas necessidades de PDF." - -[login.slides.secure] -alt = "Proteger PDFs" -subtitle = "Adicione palavras-passe, oculte conteúdo e gere certificados com facilidade." -title = "Proteja informações sensíveis nos seus PDFs" - [margin] large = "Grande" medium = "Médio" @@ -4818,6 +5044,34 @@ uploadSuccess = "Carregamento concluído!" uploadSuccessMessage = "As suas imagens foram enviadas." validating = "A validar a sessão..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Digitalize para carregar fotos. As imagens convertem-se automaticamente em PDF." descriptionNoConvert = "Digitalize para carregar fotos a partir do seu dispositivo móvel." @@ -4836,8 +5090,13 @@ title = "Carregar do telemóvel" tags = "Multi Ferramenta,Multi operação,UI,clicar arrastar,front end,lado cliente,interativo,interagível,mover,eliminar,migrar,dividir" title = "Multi Ferramenta PDF" -[navbar] -search = "Pesquisar" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "A autenticação não foi bem-sucedida. Pode fechar esta janela e tentar novamente." @@ -4966,6 +5225,7 @@ activeFiles = "A vista Ficheiros ativos mostra todos os PDFs qu allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Fechar" cropSettings = "Agora que selecionámos o ficheiro que queremos recortar, podemos configurar a ferramenta Cortar para escolher a área para a qual queremos recortar o PDF." +dialogLabel = "Onboarding" fileCheckbox = "Clicar num dos ficheiros seleciona-o para processamento. Pode selecionar múltiplos ficheiros para operações em lote." fileReplacement = "O ficheiro modificado substituirá automaticamente o ficheiro original na Área de trabalho, permitindo-lhe executá-lo facilmente em mais ferramentas." filesButton = "O botão Ficheiros na barra de Acesso Rápido permite-lhe carregar PDFs para usar as ferramentas." @@ -4976,6 +5236,7 @@ pinButton = "Pode usar o botão Afixar se preferir que os seus results = "Após a ferramenta terminar, o passo de Revisão mostrará uma pré-visualização dos resultados neste painel e permitirá desfazer a operação ou transferir o ficheiro." runButton = "Depois de configurar a ferramenta, este botão permite executar a ferramenta em todos os PDFs selecionados." selectCropTool = "Vamos selecionar a ferramenta Cortar para demonstrar como usar uma das ferramentas." +stepOf = "Passo {{current}} de {{total}}" toolInterface = "Esta é a interface da ferramenta Cortar. Como pode ver, não há muito porque ainda não adicionámos ficheiros PDF com que trabalhar." workbench = "Esta é a Área de trabalho - a área principal onde visualiza e edita os seus PDFs." wrapUp = "Está tudo pronto! Aprendeu as principais áreas da aplicação e como usá-las. Clique no botão Ajuda sempre que quiser para ver novamente esta visita." @@ -4988,6 +5249,26 @@ showMeAround = "Mostre-me" skipForNow = "Saltar por agora" skipTheTour = "Saltar a visita guiada" +[onboarding.checklist] +dismiss = "Dispensar" +title = "Configurar o Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Execute o Stirling nativamente no seu computador" +title = "Transferir o Stirling para Desktop" + +[onboarding.checklist.inviteTeam] +description = "Colabore com a sua equipa" +title = "Convidar membros da equipa" + +[onboarding.checklist.shareAnalytics] +description = "Ajude a melhorar o Stirling" +title = "Partilhar dados de utilização anónimos" + +[onboarding.checklist.takeTour] +description = "Veja como o Stirling funciona numa visita guiada rápida" +title = "Fazer a visita guiada" + [onboarding.desktopInstall] body = "O Stirling funciona melhor como aplicação de desktop. Pode usá-lo offline, aceder a documentos mais rapidamente e fazer edições localmente no seu computador." selectOs = "Selecione o sistema operativo" @@ -4996,7 +5277,21 @@ titleWithOs = "Transferir para {{osLabel}}" [onboarding.mfa] authenticationCode = "Código de autenticação" +description = "Proteja a sua conta associando uma aplicação de autenticação. Digitalize o código QR ou introduza a chave de configuração e depois confirme o código de 6 dígitos para terminar." +enable = "Ativar MFA" +enableError = "Não foi possível ativar a autenticação de dois fatores. Verifique o código e tente novamente." +enterCode = "Introduza o código de 6 dígitos da sua aplicação." +enterCodeError = "Introduza o código de autenticação para continuar." +logout = "Terminar sessão" +openAuthenticator = "Abra o Google Authenticator, Authy ou 1Password." qrCodeLoading = "A gerar o seu código QR…" +regenerateQrCode = "Regenerar código QR" +scanQrCode = "Digitalize o código QR ou introduza a chave de configuração abaixo." +setupError = "Não foi possível iniciar a configuração de dois fatores. Tente novamente." +setupKey = "Chave de configuração (entrada manual)" +stepByStep = "Passo a passo" +success = "A MFA foi ativada. Pode agora continuar." +title = "Configuração de autenticação multifator" [onboarding.planOverview] adminBodyLoginDisabled = "Depois de ativar o modo de login, pode gerir utilizadores, configurar definições e monitorizar a saúde do servidor. As primeiras {{freeTierLimit}} pessoas no seu servidor podem usar o Stirling gratuitamente." @@ -5005,6 +5300,11 @@ adminTitle = "Visão geral do administrador" userBody = "Convide colegas de equipa, atribua funções e mantenha os documentos organizados num espaço de trabalho seguro. Ative o modo de login quando estiver pronto para crescer além do uso a solo." userTitle = "Visão geral do plano" +[onboarding.processorIntro] +body = "O Stirling agora executa Políticas — regras automatizadas que classificam, protegem e processam todos os documentos quando chegam. Configure-as e monitorize as execuções no Processor." +cta = "Conheça o Processor" +title = "Conheça o Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Ano pré-pago" + +[payg.prepaid.banner] +exhausted = "A sua capacidade pré-paga foi esgotada; a faturação medida foi retomada. Recarregue para manter o desconto." +expiring_one = "O seu ano pré-pago termina dentro de {{count}} dia. A capacidade não utilizada expira — recarregue para iniciar um novo ano." +expiring_other = "O seu ano pré-pago termina dentro de {{count}} dias. A capacidade não utilizada expira — recarregue para iniciar um novo ano." +low = "Está com {{pct}}% da sua capacidade pré-paga restante. Recarregue a qualquer momento para manter o processamento sem interrupções." + +[payg.prepaid.card] +subtitle = "Usada antes da faturação medida e fora do seu limite de gastos." +title = "Capacidade pré-paga" + +[payg.prepaid.meter] +capSuffix = "de {{total}} créditos pré-pagos" +expires = "Expira em {{date}}" + +[payg.prepaid.state] +exhausted = "Esgotada" +healthy = "Bastante restante" +low = "A ficar baixa" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF Para PDF/A" tags = "arquivo,longo prazo,padrão,conversão,armazenamento,preservação" title = "PDF Para PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "impressão,padrão,conversão,produção,pré-impressão,arquivo" title = "PDF para PDF/X" @@ -5822,6 +6151,7 @@ small = "500 créditos" xsmall = "100 créditos" [plan.availablePlans] +currency = "Billing currency" subtitle = "Escolha o plano que se adapta às suas necessidades" title = "Planos disponíveis" @@ -5937,49 +6267,12 @@ keyDescription = "Cole a chave de licença do seu e-mail" success = "Licença ativada!" successMessage = "A sua licença foi ativada com sucesso. Pode fechar esta janela." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "aplicada" -enforcing = "A aplicar..." -failed = "A aplicação falhou" outputsUnavailable = "Os resultados da política já não estão disponíveis para transferência." partialOutputsUnavailable = "Alguns resultados da política já não estão disponíveis para transferência." -retrying = "Ocupado, a tentar novamente..." runNotFound = "A execução da aplicação já não pôde ser encontrada." -step = "passo {{current}}/{{total}}" timedOut = "A aplicação excedeu o tempo limite antes de a execução terminar." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Fechar" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "A aplicar {{names}}" applyingProgress = "A aplicar {{names}} ({{done}} de {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} de {{total}} ficheiro(s) não puderam ser processado failureTitle = "Exportado sem aplicação completa" printPolicyAppliedBody = "Este PDF foi atualizado para cumprir uma política. Reveja as alterações e imprima novamente." printPolicyAppliedTitle = "Política aplicada antes da impressão" -queued = "+{{count}} na fila" successTitle = "{{names}} aplicadas" summaryMore = "{{first}}, {{second}} e mais {{more}}" summaryTwo = "{{first}} e {{second}}" -[policies.enforcement.triggerVerb] -convert = "A aplicar antes de converter" -default = "A aplicar" -export = "A aplicar antes de exportar" -input = "A aplicar ao importar" -print = "A aplicar antes de imprimir" - -[policies.field] -accessLog = "Registo de acesso" -archiveAfter = "Arquivar após" -auditTrail = "Histórico de auditoria" -belowThreshold = "Abaixo do limite" -destination = "Destino" -frameworks = "Frameworks" -immutableHold = "Retenção imutável" -keepFor = "Manter durante" -minConfidence = "Confiança mín." -notify = "Notificar no encaminhamento" -onViolation = "Quando não conforme" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Adicionar" -addPlaceholder = "Adicionar uma etiqueta…" -customNote = "Personalizado para a sua equipa." -defaultNote = "A utilizar a predefinição incorporada, partilhada com a sua equipa." -duplicate = "\"{{name}}\" já existe." -edit = "Editar etiquetas" -empty = "Ainda não existem etiquetas." -export = "Exportar JSON" -iconAria = "Escolha um ícone para {{name}}" -import = "Importar JSON" -importError = "Não foi possível importar esse ficheiro." -managedNote = "As etiquetas da equipa são geridas pelo líder da sua equipa." -modalSubtitle = "Partilhado com toda a sua equipa. O classificador escolhe as etiquetas que se aplicam a cada documento." -modalTitle = "Etiquetas de classificação" -pickIcon = "Utilizar ícone de {{label}}" -removeAria = "Remover {{name}}" -resetToDefault = "Repor predefinição" -saveForTeam = "Guardar para a equipa" -saving = "A guardar…" -sectionLabel = "Etiquetas de classificação" -startFromScratch = "Começar do zero" -teamCount = "etiquetas da equipa" -tooLong = "As etiquetas podem ter, no máximo, {{max}} caracteres." -ungrouped = "Sem grupo" -view = "Ver etiquetas" +categoryCount = "categorias" +hideCategory = "Ocultar categoria" +labelCount = "etiquetas" +sharedNote = "Estas etiquetas são integradas e partilhadas por toda a sua equipa." +showCategory = "Mostrar categoria" [policies.pii] account = "Números de conta (com rótulo)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Números de encaminhamento dos EUA (ABA)" ssn = "Números de Segurança Social" -[policies.settings] -noneExport = "Atualmente, nenhuma política é executada na exportação." -noneUpload = "Atualmente, nenhuma política é executada no carregamento." -onExport = "Na exportação" -onUpload = "No carregamento" -reorderHandle = "Arraste para reordenar" -runOrderDesc = "Quando mais do que uma política é executada no mesmo acionador, são executadas por esta ordem — cada uma sobre a saída da política anterior. Arraste para reordenar." -title = "Definições de políticas" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opções de políticas" -policySettings = "Definições de políticas" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Tentar novamente políticas falhadas ({{count}})" -rowProgress = "{{completed}} de {{total}} ficheiros processados" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "O que é uma política?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Localiza e oculta automaticamente informações sensíveis — como números de Segurança Social e de cartões — para que não possam ser lidas no documento." -sanitize = "Remove JavaScript oculto do ficheiro, para que nada seja executado automaticamente quando alguém o abrir." -watermark = "Aplica uma marca visível (p. ex. \"Confidencial\") em todas as páginas." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Fechar" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} em aplicação..." badgeRan = "A política {{name}} foi executada neste ficheiro" +badgeRunning = "{{name}} em execução..." blockingAction = "{{action}} bloqueada enquanto a política é aplicada, aguarde..." dismiss = "Dispensar sobreposição" enforcingTitle = "A aplicar política..." @@ -6151,6 +6316,7 @@ viewAnyway = "Ver ficheiro (política ainda em aplicação)" [policyOption] autoRedactPhi = "Ocultar automaticamente PHI" blockExport = "Bloquear exportação" +ccpa = "CCPA" contracts = "Contratos" documents = "Documentos" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plano Editor" subscribed = "Plano Processor" unlinked = "Não associado" -[portal.agentBuilder] -bootstrapFromDocument = "Inicializar a partir de documento" -sectionsAriaLabel = "Secções do criador de agentes" -selectorAriaLabel = "Agentes" -subtitle = "Conceba, teste e disponibilize os agentes de IA que classificam, extraem e encaminham os seus documentos. Defina cenários, limite o acesso a ferramentas, execute um conjunto dourado e publique uma versão." -title = "Criador de agentes" - -[portal.agentBuilder.bootstrap] -cancel = "Cancelar" -dropzoneText = "Escolha um documento de exemplo (PDF ou imagem)" -lead = "Largue um documento de exemplo e iremos propor cenários e um esquema de extração que pode refinar. Nada é publicado até o rever." -submit = "Inicializar agente" -subtitle = "Iniciar um novo agente a partir de um ficheiro representativo" -title = "Inicializar a partir de um documento" - -[portal.agentBuilder.empty] -description = "Inicialize um agente a partir de um documento de exemplo para iniciar os respetivos cenários e esquema de extração; depois refine e publique." -title = "Ainda não existem agentes" - -[portal.agentBuilder.evals] -casesPassing = "Casos aprovados" -columnCase = "Caso de avaliação" -columnLatency = "Latência" -columnResult = "Resultado" -fail = "falha" -goldenSetPassRate = "Taxa de aprovação do conjunto dourado" -latencyMs = "{{ms}} ms" -notRun = "não executado" -pass = "aprovado" -passRate = "Taxa de aprovação" -runEvals = "Executar avaliações" - -[portal.agentBuilder.evals.empty] -description = "As avaliações transformam os seus cenários num conjunto dourado repetível. Atualize para registar a taxa de aprovação ao longo do tempo e condicionar publicações com base nela." -title = "Ainda não existe conjunto dourado" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "entre conjuntos dourados" -activeAgents = "Agentes ativos" -avgPassRate = "Taxa média de aprovação das avaliações" -fleetWide = "em toda a frota" -latestPublished = "Última publicada" -scenarios = "Cenários" -testCases = "casos de teste" -totalDescription_one = "{{count}} no total" -totalDescription_other = "{{count}} no total" - -[portal.agentBuilder.scenarios] -add = "Adicionar" -addScenario = "Adicionar cenário" -enable = "Ativar" -expectationLabel = "Comportamento esperado" -expectationPlaceholder = "O que o agente deve fazer" -inEval = "em avaliação" -mute = "Silenciar" -muted = "silenciado" -nameLabel = "Nome" -namePlaceholder = "ex.: Escalonamento de conformidade" - -[portal.agentBuilder.tabs] -evals = "Avaliações" -scenarios = "Cenários" -tools = "Ferramentas" -versions = "Versões" - -[portal.agentBuilder.tools] -broadAccess = "Acesso amplo" -deniedHint = "As ferramentas selecionadas estão bloqueadas. Tudo o resto permanece invocável." -deniedTools = "Ferramentas negadas" -governanceGate = "A governação de ferramentas está disponível no plano Enterprise." -restricted = "Restrito" -restrictedAccess = "Acesso restrito a ferramentas" -restrictedDescription = "Permitir todas as ferramentas exceto as que negar abaixo." - -[portal.agentBuilder.versions] -current = "atual" -historyGate = "O histórico completo de versões e a reposição estão disponíveis no plano Enterprise." -publish = "Publicar" -rollBack = "Reverter" - [portal.assistant] close = "Fechar assistente" error = "Não foi possível contactar o assistente." @@ -6365,9 +6451,21 @@ typing = "A escrever" loading = "A carregar" redirectingToEditor = "A redirecionar para o editor..." +[portal.billing.activation] +maybeLater = "Talvez mais tarde" +title = "Ativar o Processor" + +[portal.billing.activation.payg] +sub = "Defina um limite mensal. Pague pelo que executar." +title = "Pagar conforme utiliza" + +[portal.billing.activation.prepay] +badge = "2 meses grátis" +sub = "Uma fatura, cartão ou transferência bancária." +title = "Pré-pagar um ano" + [portal.billing.checkout] noClientSecret = "A função Edge não devolveu client_secret." -subtitle = "Adicione um cartão para continuar além da sua oferta gratuita do plano Editor. O Stripe trata do resto." title = "Ativar o plano Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "O pagamento foi efetuado com sucesso, mas a ativação está a demorar u close = "Fechar" title = "Quase concluído" +[portal.billing.checkout.cap] +amountAria = "Limite mensal de gastos" +back = "Voltar" +continue = "Continuar para pagamento" +error = "Não foi possível definir o seu limite de gastos" +finePrint = "As faturas são emitidas mensalmente. Cancele a qualquer momento." +noLimit = "Sem limite" +note = "Nunca será faturado para além do seu limite. O processamento apenas fica em pausa." +perMonth = "/ mês" +title = "Defina o seu limite de gastos" + +[portal.billing.checkout.card] +fields = "Número do cartão · MM / AA · CVC · ZIP" +label = "Dados do cartão" +note = "Dados do cartão recolhidos pela Stripe. O Stirling nunca armazena PAN ou CVC." + [portal.billing.checkout.error] title = "Não foi possível iniciar o checkout" @@ -6383,10 +6497,9 @@ body = "O pagamento foi efetuado. Estamos a ativar o processamento medido nas su hint = "Mantenha esta janela aberta." title = "A ativar o seu plano Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "no env do portal para ativar o checkout na aplicação." -bodyBefore = "Defina" -title = "Stripe não configurado" +[portal.billing.checkout.pay] +limit = "Limite mensal de gastos {{amount}}/mês. O processamento fica em pausa no limite." +limitNoCap = "Sem limite de gastos. O processamento não ficará em pausa. As faturas são emitidas mensalmente." [portal.billing.enterpriseUpsell] cta = "Criar o seu orçamento Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plano atual" freeForever = "Gratuito para sempre" noTeamResolved = "Ainda não existe uma equipa definida na sua carteira — atualize e tente novamente." ownerOnly = "Apenas o proprietário da equipa pode ativar o plano Processor." +payInvoice = "Pagar fatura para concluir" planName = "Editor" ssoIncluded = "SSO incluído" switchOnProcessor = "Ativar o Processor →" unlimitedUsers = "Utilizadores ilimitados" +viewQuote = "Ver orçamento" [portal.billing.invoices] columnAmount = "Valor" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Pedidos API diretos" segmentApiLabel = "API" segmentAutomationDesc = "Automações e pipelines" segmentAutomationLabel = "Automação" +sizeMultiplier = "{{formatted}} PDFs usaram um multiplicador de tamanho" +summary = "{{unique}} únicos · {{units}} unidades de medição · média {{avg}} por PDF" +summaryNoRate = "{{unique}} únicos · {{units}} unidades de medição" unit = "PDF medidos" +unitsPending_one = "{{units}} unidade de medição pendente de sincronização a partir de instâncias ligadas" +unitsPending_other = "{{units}} unidades de medição pendentes de sincronização a partir de instâncias ligadas" + +[portal.billing.prepaid] +capSuffix = "de {{total}} créditos pré-pagos" +expires = "Expira em {{date}}" +eyebrow = "Capacidade pré-paga" +note = "Usada antes da faturação medida e fora do seu limite de gastos." +topUp = "Recarregar" + +[portal.billing.prepaid.buy] +back = "Voltar" +calcTitle = "Calcule o seu pagamento anual" +cancel = "Cancelar" +cancelPurchase = "Cancelar compra" +close = "Fechar" +continueToPayment = "Continuar para pagamento" +finalise = "Finalizar" +invoiceErrorTitle = "Não foi possível gerar a fatura" +loadingTitle = "A carregar o seu orçamento" +payTitle = "Pague o seu ano" +step = "Passo {{current}} de {{total}}" + +[portal.billing.prepaid.calc] +change = "Alterar" +deployRow = "Implementação" +done = "Concluído" +enterpriseHint = "Isto é escala empresarial. Com este volume, as tarifas empresariais superam qualquer desconto self-service. Preço fixo, termos e orçamento em minutos." +governanceRow = "Governação" +governanceValue = "{{label}} · {{count}} políticas" +handlesLabel = "O seu Processor" +handlesValue = "processa {{volume}} créditos / mês" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} políticas" +poolCaption = "Um conjunto de {{credits}} créditos para o ano. Meses pesados recorrem aos meses leves." +priceLabel = "O seu ano · 12 meses pelo preço de 10" +rateUnknown = "Mostraremos o preço exato no checkout." +savings = "Poupa {{amount}} · 2 meses grátis." +sizingRow = "Dimensionamento" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Total de utilizadores" + +[portal.billing.prepaid.consent] +label = "Compreendo que, quando a minha capacidade pré-paga for esgotada ou expirar após 12 meses, o processamento continua automaticamente à tarifa medida padrão de pagamento conforme utilização (até ao meu limite de gastos), salvo se eu cancelar, e que posso cancelar a qualquer momento no portal de faturação." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gerido pelo Stirling. Em funcionamento em minutos." +cloudValue = "Stirling Cloud · Gerido" +selfhost = "Autoalojado" +selfhostDesc = "Executado em infraestrutura privada. Mesma tarifa." +selfhostValue = "Autoalojado · Infraestrutura privada" + +[portal.billing.prepaid.offer] +cta = "Rever oferta" +subtitle = "Pré-pague um ano de processamento de PDF e obtenha dois meses grátis, usados antes da faturação medida, fora do seu limite de gastos." +title = "Obtenha 12 meses pelo preço de 10" + +[portal.billing.prepaid.pay] +accountName = "Nome do titular da conta" +accountNamePlaceholder = "por exemplo, Jane Smith" +companyName = "Nome da empresa (opcional)" +companyNamePlaceholder = "por exemplo, Acme Inc." +downloadInvoice = "Transferir fatura" +payOnline = "Pagar online" +poNumber = "N.º de encomenda (opcional)" +poNumberPlaceholder = "por exemplo, PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avançado" +advancedDesc = "As pipelines impulsionam a maior parte do seu processamento." +none = "Nenhum" +noneDesc = "Ainda não está a executar pipelines. Ative-as a qualquer momento." +standard = "Padrão" +standardDesc = "Algumas pipelines reprocessam PDFs recebidos." + +[portal.billing.prepaid.posture] +essentials = "Essenciais" +essentialsDesc = "Classificação, a predefinição, mais Partilha" +governed = "Governado" +governedDesc = "Adiciona Segurança e Encaminhamento" +regulated = "Regulado" +regulatedDesc = "Todas as categorias, incl. Conformidade, Retenção, Ingestão" + +[portal.billing.prepaid.review] +download = "Transferir orçamento (PDF)" +downloading = "A preparar…" +share = "partilhar para aprovação" + +[portal.billing.prepaid.size] +compact = "Compacto" +compactDesc = "Ficheiros abaixo de 25 MB, sem custos de dados" +heavy = "Pesado" +heavyDesc = "Digitalizados ou com muitas imagens, frequentemente 50 MB+" +standard = "Padrão" +standardDesc = "Maioritariamente pequenos, algumas digitalizações acima de 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Esgotada" +healthy = "Bastante restante" +low = "A ficar baixa" [portal.billing.spendLimit] adjustLimit = "Ajustar limite" @@ -6494,6 +6713,8 @@ label = "Previsto exceder." [portal.billing.spendThisMonth] eyebrow = "Gastos este mês" +freeRemaining_one = "{{formatted}} PDF gratuito restante" +freeRemaining_other = "{{formatted}} PDFs gratuitos restantes" processed_one = "{{formattedCount}} PDF processado." processed_other = "{{formattedCount}} PDF processados." processedWithRate_one = "{{formattedCount}} PDF processado, a {{rate}} cada." @@ -6510,6 +6731,7 @@ reachedTitle = "Limite mensal de gastos atingido" title = "Não foi possível abrir o portal Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "de {{allowance}} PDF gratuitos utilizados" capSuffix_other = "de {{allowance}} PDF gratuitos utilizados" eyebrow = "Avaliação do Processor" @@ -6534,6 +6756,376 @@ signature = "assinatura" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nova ligação" +createTitleFor = "Ligar {{name}}" +delete = "Eliminar" +edit = "Editar" +editTitle = "Editar ligação" +unsupportedType = "Este tipo de ligação não pode ser editado aqui." + +[portal.connections.categories.advanced] +description = "Para um serviço que ainda não suportamos." +label = "Avançado" + +[portal.connections.categories.audit] +description = "Registe o que aconteceu a um documento, onde os seus auditores já consultam." +label = "Registo de auditoria e conformidade" + +[portal.connections.categories.notify] +description = "Informe outro sistema de que algo aconteceu." +label = "Notificação e fluxo de trabalho" + +[portal.connections.categories.security] +description = "Analise ou etiquete um documento antes de este avançar." +label = "Segurança e classificação" + +[portal.connections.categories.signing] +description = "Envie um documento para assinatura ou autenticação notarial." +label = "Assinatura eletrónica e autenticação notarial" + +[portal.connections.categories.storage] +description = "Onde os documentos processados ficam - um bucket, uma drive, um ticket ou uma página." +label = "Ficheiro e anexar" + +[portal.connections.commonFields.accessToken] +helperText = "O Stirling armazena isto encriptado e nunca volta a mostrá-lo." +label = "Token de acesso" + +[portal.connections.commonFields.apiKey] +label = "Chave API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Os passos que usam esta ligação só podem chamar caminhos sob este URL, nunca outro host." +label = "URL do servidor" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Palavra-passe" + +[portal.connections.commonFields.port] +label = "Porta" + +[portal.connections.commonFields.username] +label = "Nome de utilizador" + +[portal.connections.commonFields.webhookUrl] +helperText = "Cole o URL que o serviço lhe forneceu. Apenas caminhos sob ele podem ser chamados." +label = "URL do Webhook" + +[portal.connections.fields] +name = "Nome da ligação" +namePlaceholder = "por exemplo, Produção" + +[portal.connections.picker] +cancel = "Cancelar" +createNew = "Nova ligação..." +placeholder = "Selecione uma ligação" +save = "Guardar ligação" + +[portal.connections.picker2] +back = "Alterar integração" +heading = "O que pretende ligar?" +noResultsBody = "Ainda não suportamos isso - fale-nos sobre isso e veremos se o podemos adicionar." +noResultsTitle = "Nada corresponde a \"{{query}}\"" +searchPlaceholder = "Pesquisar integrações - experimente \"sign\", \"ocr\" ou o nome de um produto" + +[portal.connections.types.api] +description = "Chame qualquer HTTP API a partir de uma política. Escolhe o URL, a autenticação e o payload." +label = "API personalizada" + +[portal.connections.types.api.fields.authType] +label = "Autenticação" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nome de utilizador e palavra-passe" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Token Bearer" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token num cabeçalho personalizado" + +[portal.connections.types.api.fields.authType.options.none] +label = "Nenhuma" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Iniciar sessão para obter um token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Os passos que usam esta ligação só podem chamar caminhos sob este URL, nunca outro host." +label = "URL base" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nome do cabeçalho" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Caminho de início de sessão" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Palavra-passe" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Se a API responder com uma ligação de transferência, apenas estes hosts podem ser obtidos. Separe com vírgulas. Deixe em branco para permitir apenas o host do próprio URL base." +label = "Hosts de resultado permitidos" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Enviar o token como este cabeçalho" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "O cabeçalho de resposta no qual a chamada de início de sessão devolve o token, por exemplo, X-Auth-Token." +label = "Cabeçalho de resposta do token" + +[portal.connections.types.api.fields.username] +label = "Nome de utilizador" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Analise documentos quanto a malware num servidor que controla - nada sai da sua rede." +label = "ClamAV (autoalojado)" + +[portal.connections.types.cloudmersive] +description = "Análise de vírus e conteúdo para documentos numa política." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Bloqueie documentos que contenham macros, executáveis, scripts ou objetos incorporados - não apenas malware conhecido." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Anexe documentos processados a uma página Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Envie documentos para o ConsignO Cloud (Notarius) para assinatura eletrónica e autenticação notarial." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "O seu ambiente ConsignO Cloud, incluindo /api/v1." +label = "URL do servidor" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Chave API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Segredo API" + +[portal.connections.types.consigno.fields.password] +label = "Palavra-passe" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Necessário apenas se a sua organização usar tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nome de utilizador" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publique uma mensagem no Discord quando uma política processar um documento." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexe um evento de auditoria no Elasticsearch sempre que uma política trata um documento." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Consulte periodicamente um servidor FTP ou FTPS para novos documentos." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Modo de ligação" + +[portal.connections.types.ftp.fields.passive.options] +active = "Ativo" +passive = "Passivo (recomendado)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP simples envia credenciais e ficheiros sem encriptação; escolha FTPS se o servidor o suportar." +label = "Encriptação" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explícito / AUTH TLS)" +implicit = "FTPS (implícito)" +none = "Nenhuma (FTP simples)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publique uma mensagem num espaço Google Chat quando uma política for executada." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Anexe documentos processados a uma issue do Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Envie um documento ou uma notificação por email quando uma política for executada." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Arquive documentos processados no Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Palavra-passe da aplicação" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detete dados pessoais no texto de documentos usando um servidor Presidio que controla." +label = "Presidio (autoalojado)" + +[portal.connections.types.purview] +description = "Leia e aplique etiquetas de sensibilidade Microsoft Purview em PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opcional. Necessário apenas para escolher etiquetas por nome; a etiquetagem funciona sem isto." +label = "ID da aplicação (cliente)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Segredo do cliente" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Carimbado em todas as etiquetas como o respetivo SiteId, para que as ferramentas compatíveis com Purview saibam que organização a aplicou." +label = "ID do diretório (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Um bucket S3 do qual as origens podem ler e no qual as saídas de pipelines podem escrever." +label = "Armazenamento S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID da chave de acesso" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Deixe em branco para AWS. Defina isto para MinIO ou outro armazenamento compatível com S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Região" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Chave de acesso secreta" + +[portal.connections.types.sendgrid] +description = "Envie um documento ou uma notificação por email quando uma política for executada." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Consulte periodicamente um servidor SFTP (SSH) para novos documentos." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opcional. A impressão digital SHA-256 da chave de host do servidor (de `ssh-keyscan host | ssh-keygen -lf -`). Quando definida, apenas é aceite um servidor que apresente esta chave; quando em branco, a chave vista na primeira ligação é fixada e alterações posteriores são recusadas." +label = "Impressão digital da chave de host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frase-passe da chave" + +[portal.connections.types.sftp.fields.password] +helperText = "Deixe em branco se autenticar com uma chave privada em vez disso." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opcional. Cole uma chave privada OpenSSH para autenticar com uma chave em vez de palavra-passe." +label = "Chave privada" + +[portal.connections.types.slack] +description = "Publique uma mensagem no Slack quando uma política processar um documento." +label = "Slack" + +[portal.connections.types.smb] +description = "Vigie uma partilha de rede SMB / CIFS (Windows ou Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Partilha de rede" + +[portal.connections.types.smb.fields.domain] +helperText = "Opcional. O domínio ou grupo de trabalho Windows, se o seu servidor usar um." +label = "Domínio" + +[portal.connections.types.smb.fields.share] +label = "Partilha" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Envie um evento de auditoria para o Splunk sempre que uma política trata um documento." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Envie um evento de auditoria para um coletor HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publique uma mensagem num canal Teams quando uma política for executada." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publique cada documento, ou apenas os detalhes, para um URL à sua escolha." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Acione um cenário Zap ou Make a partir de uma política." +label = "Zapier e Make" + +[portal.docs] +browse = "Navegar na documentação" +viewSource = "Ver fonte no GitHub" + [portal.docs.authentication] codeCaption = "cada pedido" eyebrow = "INTRODUÇÃO" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Capacidades incorporadas e nomeadas que o seu agente invoca como uma única ferramenta. Cada skill é uma cadeia de operações determinística com avaliações associadas." title = "Skills de agente" +[portal.docs.toc] +title = "Nesta página" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERÊNCIA DA API" @@ -6697,6 +7292,8 @@ type = "Tipo" user = "Utilizador" [portal.documents.queue.empty] +connectSource = "Ligar uma origem" +createPipeline = "Criar uma pipeline" description = "À medida que as fontes enviarem documentos para as suas pipelines, estes aparecerão aqui para revisão." title = "Não há documentos na fila" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Nenhum documento corresponde a este filtro." rowActions = "Ações da linha" sensitiveLabel = "Sensível" -sensitiveTitle = "Sensível — acesso necessário" [portal.documents.table.columns] action = "Pipeline / Ação" document = "Documento" +labels = "Labels" product = "Produto" status = "Estado" time = "Hora" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Visto pela última vez" region = "Região" status = "Estado" +target = "Target" version = "Versão" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Esta vista encontrou um erro inesperado. Tente novamente ou escol retry = "Tentar novamente" title = "Algo correu mal nesta página" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Voltar" +body = "Transfira a aplicação de desktop ou autoaloje o servidor." +done = "Concluído" +guide = "Abrir o guia {{name}}" +openInBrowser = "Abrir no navegador em vez disso" +sectionDesktop = "Aplicação de desktop" +sectionSelfHosted = "Autoalojado" +title = "Instalar o Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Execute a imagem oficial no seu servidor." +note = "Abre em :8080. Início de sessão predefinido: admin / stirling." +tagline = "Execute o servidor num contentor" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Padrão" + +[portal.home.download.kubernetes] +detailBody = "Instale o Helm chart oficial." +note = "Adicione um PVC para /configs para persistir as definições." +tagline = "Implemente num cluster com Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Compilações nativas para a sua distribuição." +guideBtn = "Ver opções de instalação para Linux" +note = "Estão disponíveis compilações AppImage, .deb e .rpm para distribuições comuns." +tagline = "Aplicação de ambiente de trabalho nativa para a sua distribuição" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ou via Homebrew" +detailBody = "Execute o instalador ou use Homebrew." +downloadBtn = "Transferir para macOS" +tagline = "Aplicação nativa para Apple silicon e Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Execute o .jar num servidor bare metal." +downloadBtn = "Transferir o .jar" +note = "Requer Java 21+. Abre em :8080; início de sessão predefinido admin / stirling." +runLabel = "Depois execute-o" +tagline = "Execute o .jar num servidor bare metal" +title = "Configuração manual do servidor" + +[portal.home.download.windows] +altLabel = "Ou via winget" +detailBody = "Execute o instalador ou use um gestor de pacotes." +downloadBtn = "Transferir instalador" +tagline = "Aplicação nativa para Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} ativos" -invite = "Convidar colegas de equipa" +activeOfDeployed = "{{active}} de {{total}} ativos este mês" name = "Stirling PDF Editor" open = "Abrir no navegador" updated = "atualizado {{time}}" +[portal.home.editor.deploy] +finish = "Concluir implementação" +options = "Ver opções de implementação" +start = "Implementar o Editor" + [portal.home.editor.target] cloud = "Cloud Gerida" docker = "Autoalojado · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Boa tarde" evening = "Boa noite" morning = "Bom dia" -[portal.home.onboarding] -dismiss = "Ignorar configuração" -notStarted = "Não iniciado" -progress = "{{done}} de {{total}} concluídos" -title = "Concluir configuração" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC em toda a organização, preços por volume comprometido e implementação isolada da rede." -cta = "Iniciar avaliação" -ctaQuote = "Obter orçamento" -lead = "Para mais de 250 colaboradores." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Implemente a aplicação de desktop, grátis para sempre." -title = "Transferir o PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} ativas · {{recommended}} recomendadas" -chip = "{{active}} ativas" -title = "Ativar políticas" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} ligadas · todos os PDF governados onde chegam" -chip = "{{connected}} ligadas" -title = "Ligar as suas fontes" - -[portal.home.quickActions] -subtitle = "Principais tarefas para hoje" -title = "Ações rápidas" - -[portal.home.quickActions.buildPipeline] -blurb = "Compositor em 3 passos sobre a biblioteca de operações tipadas" -title = "Criar uma pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agentes, webhooks, pastas monitorizadas" -title = "Ligar uma fonte" - -[portal.home.quickActions.issueApiKey] -blurb = "Chave com âmbito, limites de taxa e lista de permissões de IP" -title = "Emitir uma chave API" - [portal.infrastructure] manageEditorDeployment = "Gerir implementação do Editor" sectionsAriaLabel = "Secções de infraestrutura" @@ -6878,13 +7541,9 @@ heading = "Chaves API" subheading = "Credenciais com âmbito, limites de taxa por chave, permissões e listas de permissões de IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IPs permitidos" -anyIp = "Qualquer IP (sem lista de permissões)" created = "Criado" lastUsed = "Usado pela última vez" -permissions = "Permissões" -rateLimit = "Limite de taxa" -rateLimitValue = "{{value}} pedidos/min" +revoke = "Revogar chave" usageMonth = "Utilização este mês" usageToday = "Utilização hoje" @@ -6892,6 +7551,15 @@ usageToday = "Utilização hoje" description = "Crie uma chave com âmbito para começar a chamar a Stirling API." title = "Ainda não há chaves API" +[portal.infrastructure.apiKeys.error] +load = "Não foi possível carregar as suas chaves API. Tente novamente." + +[portal.infrastructure.apiKeys.revoke] +body = "Revogar \"{{name}}\"? Qualquer integração que ainda use esta chave começará imediatamente a receber erros 401. Isto não pode ser anulado." +cancel = "Cancelar" +confirm = "Revogar chave" +title = "Revogar chave API" + [portal.infrastructure.audit] filterAriaLabel = "Filtrar eventos de auditoria por categoria" heading = "Registos de auditoria" @@ -6935,6 +7603,7 @@ all = "Todos" auth = "Autenticação" config = "Configuração" elevation = "Elevação" +policy = "Política" processing = "Processamento" security = "Segurança" @@ -6944,19 +7613,30 @@ title = "Não tem acesso" [portal.infrastructure.audit.metrics] config = "Configuração" -elevation = "Elevação" +policy = "Política" processing = "Processamento" totalEvents = "Total de eventos · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autenticação" +config = "Configuração" +elevation = "Elevação" +policy = "Política" +processing = "Processamento" +security = "Segurança" + +[portal.infrastructure.auditStatusLabel] +danger = "Erro" +info = "Info" +success = "Sucesso" +warning = "Aviso" + [portal.infrastructure.createKey] cancel = "Cancelar" createKey = "Criar chave" done = "Concluído" -ipAllowlistHelper = "Intervalos CIDR separados por vírgulas. Deixe em branco para permitir qualquer IP." -ipAllowlistLabel = "Lista de permissões de IP" keyNameLabel = "Nome da chave" keyNamePlaceholder = "ex.: Produção · ingestão" -permissionsLabel = "Permissões" secretKeyCaption = "Chave secreta" secretWarning = "Guarde isto num gestor de segredos. O Stirling armazena apenas um hash — não há forma de o recuperar mais tarde." subtitle = "Limite a chave ao mínimo de que necessita. Pode rodá-la ou revogá-la a qualquer momento." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copie este segredo agora — não será mostrado novamente." title = "Criar chave API" titleCreated = "Chave criada" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Implementado por" -environment = "Ambiente" -product = "Produto" -status = "Estado" -version = "Versão" -when = "Quando" - -[portal.infrastructure.deployments.recent] -heading = "Implementações recentes" -subheading = "Os rollouts mais recentes em produtos e ambientes." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instâncias" -latency = "Latência" -load = "Carga" -p99 = "P99" -region = "Região" -status = "Estado" -throughput = "Débito" -uptime = "Tempo de atividade" -version = "Versão" - -[portal.infrastructure.deployments.regions] -heading = "Regiões" -subheading = "Estado em tempo real de cada região Stirling implementada — latência, carga e versão do rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "As regiões implementadas aparecem aqui assim que o seu espaço de trabalho for aprovisionado." -title = "Nenhuma região implementada" - -[portal.infrastructure.models] -heading = "Modelos" -msValue = "{{value}} ms" -subheading = "O catálogo de modelos e encaminhamento que alimenta o processamento de documentos em todo o seu espaço de trabalho." - -[portal.infrastructure.models.byom] -description = "Registe um modelo on-prem ou autoalojado e fixe-o a uma região para processamento vinculado à residência dos dados." -title = "Traga o seu próprio modelo" - -[portal.infrastructure.models.catalogue] -heading = "Catálogo" -sub = "Modelos geridos disponíveis para o seu espaço de trabalho, com latência e custo em tempo real." -subEnterprise = "Modelos geridos, próprios e on-prem — com fixação por região disponível." - -[portal.infrastructure.models.catalogue.empty] -description = "Os modelos no catálogo do seu espaço de trabalho aparecem aqui." -title = "Não há modelos disponíveis" - -[portal.infrastructure.models.columns] -cost = "Custo" -latency = "Latência" -load = "Carga" -model = "Modelo" -status = "Estado" -type = "Tipo" -version = "Versão" - -[portal.infrastructure.models.metrics] -activeModels = "Modelos ativos" -avgLatency = "Latência média" -included = "Incluído" -monthlySpend = "Gasto mensal com modelos" - -[portal.infrastructure.models.routing] -empty = "Não há regras de encaminhamento configuradas." -heading = "Regras de encaminhamento" -sub = "Qual o modelo que trata cada operação. A predefinição aplica-se quando nenhuma regra mais específica corresponde." -subLocked = "Encaminhe operações para modelos específicos — disponível em planos pagos." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Atualize para Pro para controlar qual o modelo que trata cada operação e tipo de documento." -title = "O encaminhamento de modelos é uma funcionalidade paga" - -[portal.infrastructure.models.routingColumns] -default = "Predefinição" -docType = "Tipo de documento" -modelForAria = "Modelo para {{operation}}" -operation = "Operação" -routedTo = "Encaminhado para" - -[portal.infrastructure.security.access.byok] -description = "Forneça uma chave do seu próprio KMS. O Stirling cifra com ela, mas ainda consegue ler." -label = "Traga a sua própria chave (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "As chaves nunca saem do seu KMS. O Stirling mantém apenas texto cifrado." -label = "Mantenha a sua própria chave (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "O Stirling gere as chaves de cifra. O mais simples — zero operações de chaves do seu lado." -label = "Chaves detidas pelo Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Política de acesso a documentos" -subheading = "Controla quem pode decifrar documentos processados em repouso." - -[portal.infrastructure.security.attestations] -heading = "Atestados de conformidade" -noReport = "Nenhum relatório disponível" -subheading = "Postura de auditoria por framework, com relatórios disponíveis sobre controlos atestados." -viewReport = "Ver relatório →" - -[portal.infrastructure.security.compliance] -heading = "Conformidade" -subheading = "Atestados e certificações que abrangem a plataforma Stirling." - -[portal.infrastructure.security.empty] -description = "A configuração de segurança do seu espaço de trabalho aparecerá aqui." -title = "Postura de segurança indisponível" - -[portal.infrastructure.security.hyokBanner] -description = "Com HYOK, as chaves de cifra nunca saem do seu KMS. O Stirling armazena e processa apenas texto cifrado que pode revogar a qualquer momento." -title = "O Stirling não consegue decifrar os seus documentos" - -[portal.infrastructure.security.ipAllowlist] -empty = "Não há intervalos de IP configurados — todos os IPs são permitidos." -heading = "Lista de permissões de IP" -sub = "O acesso à API está restrito a estes intervalos CIDR." -subLocked = "Restrinja o acesso à API a intervalos de IP conhecidos — disponível em planos pagos." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Atualize para Pro para restringir o acesso à API a redes específicas." -title = "A lista de permissões de IP é uma funcionalidade paga" - -[portal.infrastructure.security.ipColumns] -added = "Adicionado" -addedBy = "Adicionado por" -cidr = "CIDR" -label = "Etiqueta" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmo" -heading = "Gestão de chaves de cifra" -keyId = "Identificador da chave" -lastRotated = "Rodada pela última vez" -rotateKey = "Rodar chave" -rotationPolicy = "Política de rotação" -subheading = "Custódia das chaves que cifram documentos em repouso — quem pode decifrar e como as chaves rodam." - -[portal.infrastructure.security.managedBanner] -description = "A custódia traga-a-sua-própria-chave (BYOK) e mantenha-a-sua-própria-chave (HYOK) está disponível no Enterprise. Atualize para fornecer chaves do seu próprio KMS." -title = "As chaves são geridas pelo Stirling no seu plano" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Ásia-Pacífico" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · limite de dados GDPR" -label = "União Europeia" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Estados Unidos" - -[portal.infrastructure.security.residencyHeader] -heading = "Residência dos dados" -subheading = "Onde os documentos são armazenados e processados." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} usado" - -[portal.infrastructure.storage.empty] -description = "O armazenamento ligado e a utilização aparecem aqui." -title = "Nenhum armazenamento configurado" - -[portal.infrastructure.storage.lifecycle] -active = "Ativo" -activeRange = "0–{{value}}d" -archived = "Arquivado" -coldStorage = "armazenamento frio" -deleted = "Eliminado" -never = "nunca" -purged = "purgado" - -[portal.infrastructure.storage.providers] -connect = "Ligar" -connected = "Ligado" -heading = "Fornecedores ligados" -subheading = "Onde os artefactos processados são escritos." - -[portal.infrastructure.storage.retention] -heading = "Retenção" -subheading = "Durante quanto tempo os artefactos são mantidos antes da eliminação do ciclo de vida." -windowLabel = "Janela de retenção predefinida" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dia" -days_other = "{{count}} dias" -never = "Nunca eliminar" - -[portal.infrastructure.storage.totalUsage] -heading = "Utilização total" -progressLabel = "Armazenamento usado" -subheading = "Armazenamento consumido em todos os fornecedores ligados." +[portal.infrastructure.keyLabel] +active = "Ativa" +revoked = "Revogada" [portal.infrastructure.tabs] apiKeys = "Chaves API" @@ -7174,6 +7656,49 @@ models = "Modelos" security = "Segurança" storage = "Armazenamento" +[portal.integrations] +availableHeading = "Disponíveis" +comingSoonHeading = "Brevemente" +connect = "Ligar" +connectionCount_one = "{{count}} ligação" +connectionCount_other = "{{count}} ligações" +customApi = "API personalizada" +searchPlaceholder = "Pesquisar integrações" +subtitle = "Tudo com que o Stirling pode comunicar. As credenciais são criadas aqui uma vez e depois referenciadas por fontes, políticas e pipelines." +title = "Integrações" +typedName = "Nome de {{tool}}" + +[portal.integrations.filters] +all = "Todas" +audit = "Auditoria" +connected = "Ligadas" +notify = "Notificar" +security = "Segurança" +signing = "Assinatura" +storage = "Armazenamento" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Ligada" + +[portal.integrations.table] +integration = "Integração" +worksWith = "Funciona com" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Políticas" +sources = "Fontes" + +[portal.legal] +draft = "{{label}} · rascunho" +loadError = "Não foi possível carregar este documento. Tente novamente." +loading = "A carregar…" +title = "Documento legal" + [portal.nav] agent-builder = "Criador de agentes" components = "Componentes" @@ -7182,33 +7707,17 @@ documents = "Documentos" editor = "Editor" home = "Início" infrastructure = "Infraestrutura" +integrations = "Integrações" pipelines = "Pipelines" policies = "Políticas" procurement = "Aquisições" -settings = "Definições" sources = "Fontes" usage = "Utilização e faturação" users = "Utilizadores" -[portal.notifications] -markAllRead = "Marcar tudo como lido" -title = "Notificações" -viewAll = "Ver tudo" - -[portal.notifications.ariaLabel] -none = "Notificações, nenhuma por ler" -unread_one = "Notificações, {{count}} por ler" -unread_other = "Notificações, {{count}} por ler" - -[portal.notifications.count] -allRead = "todas lidas" -loading = "a carregar" -new_one = "{{count}} nova" -new_other = "{{count}} novas" - -[portal.notifications.empty] -description = "Não há novas notificações." -title = "Está tudo em dia" +[portal.nav.section] +platform = "Plataforma PDF" +processor = "Processador PDF" [portal.pipelines] subtitle = "Todas as pipelines de documentos automatizadas no backend: uma cadeia ordenada de operações sobre um conjunto de fontes, executada por um acionador. Clique numa linha para ver os respetivos passos e fontes." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Nova pipeline" [portal.pipelines.builder] -addStep = "Adicionar ferramenta" +activate = "Activate" back = "Voltar às pipelines" +cannotFollow = "Não é possível receber {{produced}}" +chooseAccount = "Escolha uma conta" +chooseDestination = "Escolha um destino" +chooseOperation = "Escolha o que este passo faz" +chooseSource = "Escolha uma fonte" discard = "Descartar alterações" -enabled = "Ativada" +inputs = "Entrada" +inputSource = "Fonte de entrada" +inputTrigger = "Acionador" keepEditing = "Continuar a editar" -needsUpload = "Precisa de um ficheiro carregado" +moreActions = "More actions" +needsConfiguring = "Precisa de configuração" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nenhuma ferramenta corresponde à sua pesquisa." -pipelineSettings = "Definições da pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Pesquisar ferramentas" -selectToolBody = "Adicione uma ferramenta para criar a sua pipeline." -selectToolTitle = "Ainda não há ferramentas" -toolSettings = "Definições da ferramenta" +sendToSystem = "Enviar para outro sistema" +stepsIncompatible = "Estes passos não podem ser executados sobre o que o passo anterior produz: {{tools}}." +stepsNeedSetup = "Estes passos ainda precisam de configuração antes de guardar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operação não reconhecida, mantida tal como está." unsavedBody = "Tem alterações não guardadas. Guarde-as antes de sair ou descarte-as?" unsavedTitle = "Alterações não guardadas" -uploadUnsupported = "Os ficheiros carregados ainda não são suportados em pipelines, por isso estes passos não podem ser guardados: {{tools}}." usesDefaults = "Executa com definições predefinidas" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combina todos os ficheiros do passo anterior" +fan-out = "Executa uma vez por ficheiro do passo anterior" +format-mismatch = "Precisa de {{accepts}}, mas o passo anterior produz {{produced}}" +output-uncertain = "Pode não ser executado: a saída do passo anterior depende da forma como está configurado" +source-mismatch = "Precisa de {{accepts}}, mas a entrada desta pipeline é {{produced}}" +undeclared-operation = "Não é possível verificar o que este passo aceita" [portal.pipelines.composer] addTool = "Adicionar ferramenta" -cancel = "Cancelar" -chainEmpty = "Adicione uma ferramenta para começar a criar a sua pipeline." create = "Criar pipeline" -directory = "Pasta de saída" -directoryHelp = "Caminho absoluto no servidor. Tem de estar dentro das pastas permitidas configuradas." +createPaused = "Create paused" editingUnsupported = "A apresentação destes parâmetros da ferramenta para edição ainda não é suportada." -moveDown = "Mover para baixo" -moveUp = "Mover para cima" +editSource = "Edit source" name = "Nome" namePlaceholder = "ex.: Varredura de redação" -noSources = "Ainda não há fontes ligadas. A pipeline ainda pode ser executada em ficheiros fornecidos diretamente." noToolSettings = "Esta ferramenta não tem definições configuráveis." -operations_one = "Operação ({{count}})" -operations_other = "Operações ({{count}})" output = "Saída" -removeStep = "Remover operação" save = "Guardar alterações" scheduleEvery = "Executar a cada" -sources = "Fontes" -sourcesLoading = "A carregar fontes..." trigger = "Acionador" triggerManual = "Apenas manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dias" hours = "horas" minutes = "minutos" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Eliminar \"{{name}}\"? Esta ação não pode ser anulada." cancel = "Cancelar" @@ -7273,28 +7815,62 @@ confirm = "Eliminar" title = "Eliminar pipeline?" [portal.pipelines.detail] +clearHistory = "Limpar histórico" delete = "Eliminar pipeline" run = "Executar agora" [portal.pipelines.empty] action = "Criar uma pipeline" +connectSource = "Ligar uma fonte" description = "Crie a sua primeira pipeline: escolha as fontes sobre as quais é executada, encadeie as operações e escolha para onde vai a saída." title = "Ainda não há pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Ativas" paused = "Em pausa" total = "Pipelines" -[portal.pipelines.output] -folder = "Escrever na pasta" -inline = "Devolver ficheiros" - [portal.pipelines.run] +allProcessed_one = "Nada a executar: o {{count}} documento da fonte já foi processado." +allProcessed_other = "Nada a executar: todos os {{count}} documentos nas fontes já foram processados." completed_one = "Execução concluída." completed_other = "Todas as {{count}} execuções foram concluídas." empty = "Nada para executar: as fontes não tinham documentos para processar." failed = "Falha na execução: {{error}}" +inFlight = "Nada de novo a executar: os documentos ainda estão a ser processados a partir de uma execução anterior." +parked_one = "Nada a executar: {{count}} documento falhou anteriormente e está estacionado. Corrija a causa e depois limpe o histórico para o tentar novamente." +parked_other = "Nada a executar: {{count}} documentos falharam anteriormente e estão estacionados. Corrija a causa e depois limpe o histórico para os tentar novamente." running = "Execução iniciada; ainda em curso." timeout = "A execução está a demorar mais do que o esperado; poderá ainda terminar em segundo plano." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Fontes" status = "Estado" steps = "Passos" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Monitorização de pasta" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Agendado" [portal.policies] +defaultName = "Política de {{category}}" subtitle = "Automatizações permanentes que aplicam uma pipeline de ferramentas a todos os documentos. Cada política é acionada no carregamento ou exportação, executa a sua cadeia de ferramentas e guarda a versão aplicada juntamente com o original." title = "Políticas" [portal.policies.card] comingSoon = "Atualizar para Enterprise" notSetUp = "Não configurado" +requiresAiEngine = "Requer motor de AI" + +[portal.policies.categories.classification] +desc = "Identifique o tipo de cada documento no carregamento e marque os seus metadados para arquivo e pesquisa." +label = "Classificação" [portal.policies.categories.compliance] desc = "Aplique requisitos HIPAA, GDPR, SOC 2 ou FedRAMP a todos os documentos." @@ -7344,6 +7927,13 @@ label = "Segurança" [portal.policies.config] scopeAll = "Todos os documentos" +[portal.policies.config.classification] +summary = "Classifica todos os documentos carregados em relação às etiquetas da sua equipa e marca os respetivos metadados." + +[portal.policies.config.classification.rules] +0 = "Classificar" +1 = "Marcar metadados" + [portal.policies.config.compliance] summary = "Valida documentos em relação a frameworks regulamentares antes de saírem do sistema." @@ -7371,6 +7961,29 @@ minConfidence = "Confiança mín." 2 = "Nomear" 3 = "Normalizar" +[portal.policies.config.purview] +label = "Aplicar uma etiqueta de sensibilidade do Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Inquilino Purview" +labelId = "ID da etiqueta" +labelIdHelp = "O GUID da etiqueta, do portal Purview. Aplicar uma etiqueta grava os respetivos metadados no PDF; não pode encriptar, o que requer o cliente Microsoft." +labelName = "Nome da etiqueta (opcional)" +labelNameHelp = "Registado juntamente com o ID para que pessoas e ferramentas possam reconhecer a etiqueta." + +[portal.policies.config.purview.fields.method] +help = "O Purview distingue uma etiqueta aplicada por uma política de uma escolhida por uma pessoa." +label = "Como a etiqueta é registada" +privileged = "Como escolhida por uma pessoa" +standard = "Como aplicada automaticamente" + +[portal.policies.config.purviewRead] +label = "Ler uma etiqueta do Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Inquilino Purview" +connectionHelp = "Lê a etiqueta que o documento já contém, para que passos posteriores possam atuar sobre ela." + [portal.policies.config.retention] summary = "Impõe durante quanto tempo os documentos são mantidos, quando arquivar e quando eliminar." @@ -7438,6 +8051,7 @@ title = "Ainda sem atividade" [portal.policies.endpoints] addWatermark = "Marca de água" autoRedact = "Ocultar PII" +classifyAndLabel = "Classificar" compressPdf = "Comprimir" flatten = "Achatar" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "As suas políticas estão guardadas e aparecerão assim que a lig retry = "Tentar novamente" title = "Backend indisponível" +[portal.policies.operations] +change = "Alterar o que este passo faz" +noResults = "Nenhum passo corresponde a isso. Experimente um nome de produto, ou \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Pesquisar passos - experimente \"scan\", \"attach\" ou um nome de produto" + +[portal.policies.operations.bodyMode] +binary = "Bytes brutos do ficheiro" +json = "Dentro de uma carga JSON" +multipart = "Como carregamento de ficheiro (multipart)" + +[portal.policies.operations.clamavScan] +description = "O seu próprio servidor ClamAV verifica o documento; nada sai da sua rede." +label = "Procurar vírus (autoalojado)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Rejeita documentos que contenham macros, executáveis, scripts ou objetos incorporados." +label = "Bloquear conteúdo ativo" + +[portal.policies.operations.cloudmersiveScan] +description = "O Cloudmersive verifica o documento e a execução para se não estiver limpo." +label = "Procurar vírus" + +[portal.policies.operations.confluenceAttach] +description = "Arquiva o documento processado numa página." +label = "Anexar a uma página do Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Submete o documento ao ConsignO Cloud para assinatura." +label = "Enviar para assinatura (ConsignO)" +note = "Submete o documento. A obtenção da cópia assinada ainda não é suportada, porque um passo não consegue passar um resultado a um passo posterior." + +[portal.policies.operations.customApiCall] +description = "Crie a chamada: caminho, método, corpo e tratamento da resposta." +label = "Chamar uma API personalizada" + +[portal.policies.operations.discordNotify] +description = "Informa um canal de que a política tratou um documento." +label = "Publicar uma mensagem no Discord" + +[portal.policies.operations.elasticIndex] +description = "Regista o que a política fez como um documento pesquisável." +label = "Indexar um evento de auditoria no Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart adequa-se a API de carregamento; JSON adequa-se a API que querem o ficheiro dentro de uma carga; binário envia os bytes brutos." +label = "Como enviar o documento" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Um corpo JSON enviado tal como está. {{document.base64}} contém o ficheiro; {{document.*}} e {{run.*}} são preenchidos por documento." +label = "Corpo JSON" + +[portal.policies.operations.fields.connection] +label = "Conta" + +[portal.policies.operations.fields.domain] +label = "Domínio Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "O campo do formulário sob o qual a API espera o documento." +label = "Nome do campo do ficheiro" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Um objeto JSON de pares nome/valor de cabeçalho. A credencial da ligação é adicionada por si." +label = "Cabeçalhos" + +[portal.policies.operations.fields.index] +label = "Índice" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Chave da incidência" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Enviado como corpo da mensagem. {{document.*}} e {{run.*}} são preenchidos por documento." +label = "Mensagem" + +[portal.policies.operations.fields.method] +label = "Método" + +[portal.policies.operations.fields.pageId] +label = "ID da página" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Anexado ao URL base da ligação. Nunca pode chegar a outro anfitrião." +label = "Caminho" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Onde gravá-lo na drive. {{document.filename}} é preenchido por documento." +label = "Caminho de destino" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Relatório deixa o documento intacto. Substituir troca-o pelo que vier na resposta." +label = "O que fazer com a resposta" + +[portal.policies.operations.fields.signerEmail] +label = "Email do signatário" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Assunto" +placeholder = "Processado: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "O Presidio analisa este texto. Use {{document.title}} ou cole o texto que quer verificar." +label = "Texto a verificar" + +[portal.policies.operations.fields.to] +label = "Para" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nome de utilizador Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Informa um espaço de que a política tratou um documento." +label = "Publicar uma mensagem no Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arquiva o documento processado numa incidência." +label = "Anexar a uma incidência Jira" + +[portal.policies.operations.mailgunEmail] +description = "Envia o documento processado como anexo." +label = "Enviar o documento por email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Grava o documento processado numa pasta." +label = "Carregar para Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "O seu próprio servidor Presidio comunica os dados pessoais que encontra." +label = "Detetar dados pessoais" +note = "O Presidio lê texto, não ficheiros. Forneça o texto que quer verificar — o documento em si não é carregado." + +[portal.policies.operations.responseMode] +replace = "Substituir o documento pela resposta" +report = "Manter o documento, registar a resposta" + +[portal.policies.operations.sendgridEmail] +description = "Envia o documento processado como anexo." +label = "Enviar o documento por email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Informa um canal de que a política tratou um documento." +label = "Publicar uma mensagem no Slack" + +[portal.policies.operations.splunkEvent] +description = "Regista o que a política fez, onde os seus auditores já consultam." +label = "Enviar um evento de auditoria para Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Regista o que a política fez num coletor HTTP." +label = "Enviar um evento de auditoria para Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Informa um canal de que a política tratou um documento." +label = "Publicar uma mensagem no Teams" + +[portal.policies.operations.webhookPost] +description = "Envia o próprio documento para um URL à sua escolha." +label = "Publicar o documento num webhook" + +[portal.policies.operations.zapierNotify] +description = "Entrega os detalhes da execução à sua automação." +label = "Acionar um cenário Zap ou Make" + [portal.policies.stats] activeFor = "Ativa" dataProcessed = "Dados processados" @@ -7473,6 +8282,13 @@ label = "Docs aplicados" description = "Configuradas mas sem execução" label = "Pausadas" +[portal.policies.table] +appliesTo = "Aplica-se a" +docs = "Docs aplicados" +enforces = "Impõe" +policy = "Política" +status = "Estado" + [portal.policies.wizard.actions] back = "Voltar" cancel = "Cancelar" @@ -7480,6 +8296,10 @@ continue = "Continuar" enablePolicy = "Ativar política" saveChanges = "Guardar alterações" +[portal.policies.wizard.capability.classify] +desc = "Identifica o tipo do documento a partir das etiquetas da sua equipa e marca-o, para que seja arquivado e pesquisado por categoria." +label = "Classificar o documento" + [portal.policies.wizard.capability.compress] desc = "Comprime o documento para um tamanho de ficheiro menor." label = "Reduzir tamanho do ficheiro" @@ -7500,10 +8320,18 @@ label = "Ocultar informações sensíveis" desc = "Remove JavaScript oculto para que nada possa ser executado automaticamente quando o documento é aberto." label = "Remover conteúdo ativo" +[portal.policies.wizard.capability.timestampPdf] +desc = "Prova que o documento existia nesta forma exata num determinado momento, usando uma autoridade de carimbo temporal independente. Apenas é enviado um hash - o documento nunca sai do seu servidor." +label = "Adicionar um carimbo temporal de confiança" + [portal.policies.wizard.capability.watermark] desc = "Aplica uma marca visível (por exemplo, “Confidencial”) em todas as páginas." label = "Aplicar uma marca de água" +[portal.policies.wizard.classification] +description = "Cada documento carregado é classificado em relação às etiquetas incorporadas e marcado com os tipos correspondentes. O conjunto de etiquetas é partilhado por toda a sua equipa." +labelsHeading = "Etiquetas de classificação" + [portal.policies.wizard.errors] noTools = "Ative primeiro pelo menos uma ferramenta no fluxo de trabalho." saveFailed = "Não foi possível guardar a política. Tente novamente." @@ -7548,73 +8376,118 @@ setUp = "Configurar política de {{category}}" [portal.policies.wizard.workflow] description = "Escolha o que esta política faz a cada documento que processa." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} ativas" -noRule = "Ainda não foi aplicada nenhuma regra" -subtitle = "Automatizações permanentes pelas quais todos os documentos passam, independentemente da pipeline que os trata." -title = "O que é executado nos seus PDFs" - [portal.policySummary.action] -comingSoon = "Atualizar para Enterprise" -configure = "Configurar" setUp = "Configurar" -[portal.policySummary.column] -activeRule = "Regra ativa" -policy = "Política" -status = "Estado" +[portal.processorFlow] +footnote = "As contagens referem-se às últimas 24 horas. A velocidade do fluxo é ilustrativa." +liveBadge = "Em direto" +stats = "{{connected}} ligadas · {{processed}} PDFs processados" +title = "Processador PDF" -[portal.policySummary.empty] -description = "Assim que as políticas forem configuradas, as categorias aparecerão aqui." -title = "Ainda sem políticas" +[portal.processorFlow.lens] +ariaLabel = "Modo de visualização" +flow = "Fluxo" +sankey = "Sankey" -[portal.policySummary.state] -active = "Ativa" -off = "Desligada" -soon = "Em breve" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Falhou" +heading = "Resultados" +success = "Entregue" -[portal.processingStatus] -managePlan = "Gerir plano" -volumeSuffix = "PDFs processados · últimos 30 dias" +[portal.processorFlow.policies] +activeCount = "{{n}} ativas" +count = "{{n}} · 24h" +heading = "Políticas" +setUp = "Configurar" +soon = "Brevemente" + +[portal.processorFlow.sankey] +waist = "Políticas · {{n}} ativas" + +[portal.processorFlow.sankey.empty] +description = "Ligue uma fonte e ative uma política para ver os documentos fluírem pelo processador." +title = "Ainda sem fluxo" + +[portal.processorFlow.sources] +comingSoonTag = "Ligar" +editor = "Editor Stirling PDF" +heading = "Fontes" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Entrada de email" [portal.procurement] -reset = "Repor aprovisionamento (demo)" subtitle = "Faça a avaliação, contratação e integração da sua equipa. Todos os documentos num só lugar." title = "Aprovisionamento" -[portal.procurement.action] -download = "Download" -pay = "Pagar agora" -request = "Pedir" -sign = "Rever e assinar" -upload = "Upload" - [portal.procurement.agreement] agreeCta = "Aceitar e subscrever" +confidential = "Confidencial" confirm = "Li e aceito o Acordo Stirling Enterprise." -eyebrow = "Acordo" -intro = "Um acordo combinado cobre a sua proposta: Contrato de Prestação de Serviços, Formulário de Encomenda, EULA e Acordo de Tratamento de Dados. Reveja-o e aceite para converter a proposta numa subscrição assumida." -title = "Reveja o seu acordo empresarial" +docName = "Contrato Stirling Enterprise" +docSub = "MSA, Formulário de Encomenda, EULA e DPA, combinados numa assinatura." +download = "Transferir" +downloadDraftError = "Não foi possível gerar o PDF do contrato - o renderizador de documentos está indisponível. Tente novamente em breve ou contacte o suporte." +downloadError = "Não foi possível transferir o contrato assinado. Tente novamente." +legalName = "Nome da entidade legal" +legalNamePlaceholder = "A entidade legal que irá assinar" +loadError = "Não foi possível carregar o contrato. Tente novamente." +loading = "A carregar o contrato..." +ref = "Ref. {{ref}} · {{version}}" +requestChanges = "Solicitar alterações" +scrollHint = "Desloque para rever" +signatory = "Nome do signatário" +signatoryPlaceholder = "Nome completo" +signatoryTitle = "Cargo do signatário" +signatoryTitlePlaceholder = "p. ex., Consultor Jurídico Geral" +signError = "Não foi possível registar a sua assinatura. Tente novamente." [portal.procurement.builder] addons = "Extras" +addressLine1 = "Linha de endereço 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Linha de endereço 2" +addressLine2Placeholder = "Suite, piso, edifício" back = "Voltar" businessName = "Nome da empresa" businessNamePlaceholder = "A sua empresa" +city = "Cidade" +cityPlaceholder = "São Francisco" +completeRequired = "Preencha os campos obrigatórios (marcados com *) com um email válido antes de gerar o orçamento." +contactEmail = "Email de contacto" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nome do contacto" +contactNamePlaceholder = "Jane Doe" continue = "Continuar" -country = "País" -countryEuro = "Zona Euro (EUR €)" -countryUK = "Reino Unido (GBP £)" -countryUS = "Estados Unidos (USD $)" +done = "Concluído" eula = "Li e aceito o EULA do Stirling Enterprise. Este rege o acordo gerado a partir desta proposta." generate = "Gerar proposta" -included = "Incluído" indemnification = "Indemnização de PI" indemnificationSub = "Defendemos reclamações de PI qualificadas, conforme o EULA" -offlineLicense = "Licença offline / air-gapped" -offlineLicenseSub = "Um ficheiro de licença descarregável para uma instância self-hosted air-gapped" +paperEyebrow = "Orçamento Enterprise" +paperFor = "Preparado para" +pdfSize = "Tamanho do PDF" +poNumber = "Número de PO" +poNumberPlaceholder = "Opcional" +postalCode = "Código postal" +postalCodePlaceholder = "94105" +posture = "Governação" +posture_count = "~{{count}} políticas" +posture_essentials = "Essenciais" +posture_essentialsSub = "Classificação + Partilha — as predefinições" +posture_governed = "Governado" +posture_governedSub = "Adiciona Segurança e Encaminhamento" +posture_regulated = "Regulado" +posture_regulatedSub = "Todas as categorias — Conformidade, Retenção, Ingestão" qbr = "Revisões trimestrais de negócio" qbrSub = "O seu SE revê a utilização e o roadmap a cada trimestre" +region = "Estado / região" +regionPlaceholder = "Califórnia" running = "{{annual}} / ano · {{years}} anos {{tcv}}" s1Sub = "A sua equipa e os PDFs que espera processar a cada ano." s1Title = "Quanto irá processar?" @@ -7623,6 +8496,12 @@ s2Title = "Compromisso e serviço" s3Sub = "Para a proposta e o acordo que esta gera." s3Title = "Os seus dados" serviceLevel = "Nível de serviço" +size_compact = "Compacto" +size_compactSub = "Principalmente texto, abaixo de 1 MB" +size_heavy = "Pesado" +size_heavySub = "Digitalizado ou com muitas imagens, 5 MB+" +size_standard = "Padrão" +size_standardSub = "Texto e imagens mistos, 1 a 5 MB" slDedicated = "Dedicado" slDedicatedSub = "4 horas úteis · gestor de conta dedicado · +30%" slPriority = "Prioritário" @@ -7630,6 +8509,8 @@ slPrioritySub = "Mesmo dia útil · CSM nomeado · +15%" slStandard = "Standard" slStandardSub = "Próximo dia útil · CSM partilhado · incluído" stepOf = "Passo {{n}} de {{total}}" +taxId = "IVA / NIF" +taxIdPlaceholder = "Opcional" term = "Prazo" termDiscount = "Desconto de {{pct}}% por compromisso plurianual aplicado" title = "Crie a sua proposta" @@ -7637,6 +8518,7 @@ training = "Onboarding e formação" trainingSub = "Sessões ao vivo para pôr a sua equipa a funcionar" users = "Total de utilizadores" usersPlaceholder = "por exemplo, 250" +viewEula = "Ler" volEstimated = "Estimado a partir de {{count}} utilizadores (~2.000 PDFs cada, incluindo automação). Edite se tiver uma estimativa melhor." volManual = "A usar o valor que indicou. Reestime a partir do tamanho da sua equipa a qualquer momento." volNoUsers = "Não tem a certeza? Introduza o tamanho da sua equipa e faremos uma estimativa." @@ -7645,55 +8527,55 @@ volumePlaceholder = "por exemplo, 1.000.000" years_one = "{{count}} ano" years_other = "{{count}} anos" -[portal.procurement.docs] -count_one = "{{count}} doc" -count_other = "{{count}} docs" -done = "Concluído" -here = "Está aqui" -hide = "Ocultar" -optional = "Opcional" -paidAddon = "Extra pago" -show = "Mostrar" -subtitle = "Tudo o que precisa em cada passo do percurso, apresentado à medida que a proposta avança." -supportingSubtitle = "SOC 2, revisões de segurança, formulários fiscais e mais, prontos quando a sua equipa de segurança ou aprovisionamento pedir. Alguns têm uma taxa única." -supportingTitle = "A apoiar a sua avaliação" +[portal.procurement.documents] +agreement = "Contrato Enterprise" +agreementSub = "MSA, Formulário de Encomenda e DPA numa assinatura." +download = "Transferir" +eula = "EULA e Termos Comerciais" +eulaSub = "Os termos comerciais do software e de autosserviço." +invoice = "Fatura" +invoiceSub = "A sua primeira fatura de subscrição." +laterInvoice = "Disponível depois de aceitar" +laterQuote = "Disponível assim que o seu orçamento for emitido" +quote = "Orçamento" +quoteSub = "O seu orçamento empresarial detalhado." +sla = "Anexo SLA" +slaSub = "Níveis de suporte e objetivos de resposta." +subprocessors = "Subprocessadores" +subprocessorsSub = "Terceiros que processam dados em seu nome." +subtitle = "A documentação do seu acordo, disponível a qualquer momento." title = "Documentos" -upcoming = "Em breve" +view = "Ver" [portal.procurement.error] title = "Algo correu mal" [portal.procurement.hero] -company = "A sua proposta empresarial" +barAria = "Etapa {{current}} de {{total}}" ctaAgreement = "Rever e assinar acordo" -ctaLive = "Está ativo" -ctaPayment = "Adicionar pagamento" +ctaExploring = "Configurar a sua avaliação" ctaQuote = "Rever a sua proposta" +ctaReviewQuote = "Rever orçamento" ctaTrial = "Criar a sua proposta" +documents = "Documentos" eyebrow = "Aprovisionamento Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Convidar colegas de equipa" -keyDocs = "Documentos-chave" -nextStep = "Próximo passo: {{action}}" -notStarted = "Não iniciado" +licenseKey = "Chave de licença" +liveSub = "A sua organização está provisionada e a sua licença está ativa." +liveTitle = "Está em direto no Stirling Enterprise" +next = "Seguinte: {{stage}}" open = "Abrir aprovisionamento" scheduleCall = "Agendar uma chamada" -setup1Sub = "Convide os seus colegas de equipa" -setup1Title = "Implemente o Editor de PDF" -setup2Sub = "Ative o processamento em editores e outras origens" -setup2Title = "Ligue o Processador de PDF" -setup3Sub = "Ative Segurança, Conformidade, Encaminhamento ou Retenção quando precisar" -setup3Title = "Adicionar políticas recomendadas" +sentenceAgreement = "rever e assinar o contrato" +sentenceLive = "provisionamento e implementação" +sentencePayment = "ordem de compra e pagamento" +sentenceQuote = "rever e aceitar o seu orçamento" +sentenceTrial = "configurar o seu espaço de avaliação" [portal.procurement.journey] daysLeft_one = "Falta {{count}} dia" daysLeft_other = "Faltam {{count}} dias" -engineerLabel = "O seu engenheiro de soluções" -eyebrow = "A sua implementação" -live = "Está ativo no Stirling Enterprise" -nextStep = "Próximo passo: {{action}}" -subtitle = "O seu engenheiro de soluções acompanha todos os passos. Uma ação seguinte de cada vez; a lista de verificação completa está abaixo." -title = "Do teste à ativação, um percurso guiado" -trialTitle = "Teste Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Uma assinatura cobre MSA, formulário de encomenda, EULA e DPA." @@ -7727,6 +8609,9 @@ downloadError = "Ainda não foi possível gerar o ficheiro de licença offline downloadOffline = "Download da licença offline (.lic)" hint = "Cole esta chave numa instância self-hosted para a ativar ou guarde-a para os seus registos. Mantenha-a em segurança." label = "A sua chave de licença" +subtitle = "Ative uma instância autoalojada com esta chave, ou guarde-a para os seus registos." +title = "A sua chave de licença" +trialFileHint = "Este é o seu ficheiro de licença de avaliação. Assim que o seu contrato estiver em vigor, volte e transfira-o novamente — ao contrário da chave de licença online, o ficheiro .lic não será atualizado automaticamente." [portal.procurement.link] cta = "Associar conta" @@ -7739,65 +8624,62 @@ description = "A sua subscrição está ativa e a sua licença foi emitida. A su eyebrow = "Ativo" title = "Está ativo no Stirling Enterprise" -[portal.procurement.locked] -description = "Chaves de teste, propostas por volume assumido, o acordo de assinatura única, pagamento e o seu registo de documentos ficam todos aqui assim que iniciar uma avaliação Enterprise." -eyebrow = "Apenas Enterprise" -talkToSales = "Falar com vendas" -title = "O percurso de aprovisionamento abre com Enterprise" - [portal.procurement.milestone] -accept = "Aceitar e continuar" -description = "Faça download do PDF para o partilhar com a sua equipa, volte para aceitar quando estiver pronto ou faça alterações." -download = "Download do PDF" downloadError = "Ainda não foi possível fazer download do PDF da proposta — tente novamente dentro de momentos." -edit = "Editar proposta" -eyebrow = "Proposta {{number}}" -perYear = " / ano" -preparedFor = "Preparada para {{company}}" -tcv = "{{value}} valor total do contrato" -title = "A sua proposta está pronta" - -[portal.procurement.modal] -cancel = "Cancelar" -chooseFile = "Escolher ficheiro" -close = "Fechar" -downloadBody = "O seu download começará em breve." -downloadCta = "Download" -downloadTitle = "Download" -noFile = "Nenhum ficheiro selecionado" -payBody = "Pague o seu contrato assumido por cartão ou transferência bancária através da Stripe. O seu workspace é provisionado assim que o pagamento for confirmado." -payCta = "Continuar para a Stripe" -payTitle = "Confirmar pagamento" -requestBodyFree = "Geramos isto a pedido. Confirme e o seu engenheiro de soluções enviá-lo-á em breve." -requestBodyPaid = "Este é um extra pago. Confirme e o seu engenheiro de soluções irá definir o âmbito e enviar a documentação." -requestCta = "Pedir" -requestTitle = "Pedir este documento" -signBody = "Abre o Acordo Stirling Enterprise para assinatura eletrónica: uma assinatura cobre o MSA, o formulário de encomenda, o EULA e o DPA. Nós contra-assinamos automaticamente e avança para o pagamento." -signCta = "Abrir para assinatura" -signTitle = "Rever e assinar o seu acordo" -uploadBody = "Envie-nos a sua PO e faturamos de acordo com os seus termos. Arraste o PDF ou escolha um ficheiro abaixo." -uploadCta = "Upload da ordem de compra" -uploadTitle = "Upload da sua ordem de compra" [portal.procurement.payment] description = "A sua proposta foi aceite e a sua licença já está ativa — a sua equipa pode começar imediatamente. Pague a primeira fatura quando estiver pronto; pode pagá-la ou fazer download aqui, sem precisar de email." +downloadAgreement = "Transferir contrato assinado" downloadInvoice = "Download da fatura" -simulate = "Simular pagamento recebido (demo)" +eyebrow = "Pagamento" title = "Subscrição criada" viewInvoice = "Ver e pagar fatura" +[portal.procurement.review] +acceptCta = "Aceitar orçamento" +annual = "Taxa anual (ano 1)" +downloadCta = "Transferir orçamento" +poNumber = "Ordem de compra: {{po}}" +renewal = "Renova a {{amount}}/ano após o prazo (+{{pct}}% IPC)" +tcv = "Prazo de {{years}} anos, pago integralmente adiantado: {{tcv}}" +validUntil = "Válido até {{date}}" + [portal.procurement.schedule] fallback = "Não foi possível carregar o agendador." fallbackLink = "Abrir agendamento num novo separador" +loading = "A carregar agendador..." subtitle = "O seu engenheiro de soluções guiará a sua equipa pela implementação. Escolha uma hora conveniente." title = "Agendar uma chamada" -[portal.procurement.status] -action = "Ação necessária" -available = "Disponível" -complete = "Concluído" -pending = "Pendente" -request = "A pedido" +[portal.procurement.setup] +airgap = "Isolado" +airgapSub = "Selado, sem saída" +back = "Voltar" +businessName = "Nome da empresa" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gerido, início mais rápido" +continue = "Continuar" +deployment = "Onde deve ser implementado?" +eula = "Concordo com o EULA e os Termos Comerciais do Stirling." +fullName = "Nome completo" +fullNamePlaceholder = "O seu nome" +invites = "Convidar colegas de equipa · opcional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Agendar uma chamada" +seats = "Pessoas a testar" +seatsPlaceholder = "p. ex., 15" +selfhost = "Autoalojado" +selfhostSub = "Docker ou Kubernetes" +start = "Iniciar avaliação" +stepOf = "Passo {{n}} de {{total}}" +subtitle = "Gratuito durante 14 dias, sem cartão necessário." +subtitleDetails = "Alguns detalhes para o seu orçamento e contrato." +talkFirst = "Prefere falar primeiro?" +title = "Configure a sua avaliação" +viewEula = "Ler" +workEmail = "Email profissional" +workEmailPlaceholder = "voce@empresa.com" [portal.procurement.trial] body = "A extensão adiciona 7 dias e notifica o seu engenheiro de soluções." @@ -7808,109 +8690,113 @@ maxed = "Esgotado" subtitle = "O seu teste gratuito decorre até {{date}}. Não é necessário cartão." title = "Teste Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Preços por volume assumido, SSO + SCIM + RBAC para toda a organização, auditoria imutável de 90 dias e um SE dedicado." -homeCta = "Iniciar teste →" -homeHeadline = "Processe milhões de PDFs." - -[portal.recentActivity] -title = "Atividade recente" -viewAll = "Ver tudo" - -[portal.recentActivity.empty] -description = "Execuções de pipelines, implementações e eventos de agentes aparecerão aqui." -title = "Ainda não há nada aqui" - -[portal.search] -ariaLabel = "Pesquisar" -placeholder = "Pesquisar no Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "As ações rápidas aparecerão aqui assim que estiverem disponíveis." -noActionsTitle = "Sem ações rápidas" -noMatches = "Sem resultados para \"{{query}}\"" -noMatchesDescription = "Experimente uma palavra-chave diferente ou navegue pelo catálogo." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Ligação da conta" -[portal.shell.header] -accountFallback = "Conta" -accountMenu = "Menu da conta" -darkMode = "Modo escuro" -lightMode = "Modo claro" -search = "Pesquisar" -searchPlaceholder = "Pesquisar…" -signOut = "Terminar sessão" -switchToDark = "Mudar para tema escuro" -switchToLight = "Mudar para tema claro" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processador" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} docs" linkAccount = "Associar conta Stirling" -planEditor = "Plano Editor" -planEnterprise = "Plano Enterprise" -planProcessor = "Plano Processador" primaryNav = "Navegação principal" switchApp = "Mudar de aplicação" +[portal.shell.topbar] +closeNav = "Fechar navegação" +openNav = "Abrir navegação" +search = "Pesquisar" + [portal.sources] subtitle = "Ligações de entrada reutilizáveis que alimentam documentos no Stirling. Configure uma ligação uma vez e depois referencie-a em qualquer número de políticas. Clique numa linha para ver a respetiva configuração e que políticas a usam." title = "Origens" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Ligar origem" +[portal.sources.builder] +back = "Voltar às fontes" +backToTypes = "Todos os tipos de fonte" +cancel = "Cancelar" +chooseHint = "Escolha de onde vêm os documentos. Os conectores a cinzento estão a caminho." +comingSoon = "Brevemente" +comingSoonHeading = "Brevemente" +create = "Criar fonte" +createTitle = "Ligar uma fonte" +delete = "Eliminar" +editTitle = "Editar fonte" +enabled = "Ativada" +save = "Guardar alterações" + +[portal.sources.builder.folderAccess] +description = "As automações de pastas só podem usar pastas permitidas por um administrador. Adicione-a nas definições de Acesso a Pastas e tente novamente." +openSettings = "Definições de Acesso a Pastas" +title = "Esta pasta não é permitida" + [portal.sources.delete] body = "Eliminar \"{{name}}\"? Esta ação não pode ser anulada. As políticas que a referenciam terão de ser atualizadas." cancel = "Cancelar" confirm = "Eliminar" title = "Eliminar origem?" -[portal.sources.detail] -closeAriaLabel = "Fechar detalhe" -delete = "Eliminar origem" -docs24h = "Últimas 24 h" -docs30d = "Últimos 30 dias" -docsTotal = "Total visto" -docsTrend = "Documentos nos últimos 30 dias" -documents = "Documentos" -edit = "Editar" -notReferenced = "Não é referenciada por nenhuma política, por isso é seguro eliminá-la." -pause = "Pausar" -resume = "Retomar" -subtitle = "{{type}} · {{status}}" -usedBy = "Usada por" - -[portal.sources.empty] -description = "Ligue uma pasta (e, em breve, armazenamento na cloud) para que as suas políticas tenham de onde obter documentos." -title = "Ainda não há origens ligadas" - [portal.sources.kpi] inUse = "Em utilização" total = "Ligações" unused = "Não utilizadas" +[portal.sources.networkFields.connection] +helperText = "A ligação guardada com o endereço do servidor e as credenciais. Reutilizada por todas as fontes que a referenciam." +label = "Ligação" + +[portal.sources.networkFields.directory] +helperText = "Pasta no servidor a consultar, relativa à pasta inicial de início de sessão ou à raiz da partilha. Deixe em branco para a raiz." +label = "Pasta" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consumir remove cada ficheiro do servidor assim que todas as políticas o tenham processado." +label = "Modo de leitura" + +[portal.sources.networkFields.mode.options] +consume = "Consumir: processar cada ficheiro uma vez" +snapshot = "Snapshot: reler a pasta a cada execução" + +[portal.sources.networkFields.recursive] +label = "Profundidade da pasta" + +[portal.sources.networkFields.recursive.options] +all = "Incluir subpastas" +top = "Apenas nível superior" + [portal.sources.status] active = "Ativa" disabled = "Desativada" unused = "Não utilizada" [portal.sources.table] +documents = "Documentos" source = "Origem" status = "Estado" +type = "Type" usedBy = "Políticas" +[portal.sources.types.box] +description = "Obtenha documentos de pastas Box que a sua equipa já partilha." +label = "Box" + +[portal.sources.types.dropbox] +description = "Obtenha documentos de uma pasta Dropbox partilhada." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documentos que a sua equipa processou no editor, em execuções de políticas e AI." label = "Editor" +noPolicies = "Ainda não há políticas executadas a partir do editor." + +[portal.sources.types.email] +description = "Transforme anexos enviados para uma caixa de correio dedicada em documentos." +label = "Caixa de entrada de email" [portal.sources.types.folder] description = "Monitoriza um diretório no servidor para novos documentos." @@ -7943,25 +8829,80 @@ label = "Profundidade da pasta" all = "Incluir subpastas" top = "Apenas nível superior" +[portal.sources.types.ftp] +description = "Consulte uma pasta de servidor FTP ou FTPS para novos documentos." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Obtenha documentos de pastas partilhadas do Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Monitorize uma partilha de rede SMB para novos documentos." +label = "Unidade de rede" + +[portal.sources.types.onedrive] +description = "Monitorize pastas OneDrive para novos documentos." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Obtenha documentos de um bucket Amazon S3 ou compatível com S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "A ligação guardada que contém o bucket e as credenciais. Reutilizada por todas as fontes e saídas de pipeline que a referenciam." +label = "Ligação" + +[portal.sources.types.s3.fields.mode] +helperText = "Consumir remove cada objeto do bucket assim que todas as políticas o tenham processado." +label = "Modo de leitura" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consumir: processar cada objeto uma vez" +snapshot = "Snapshot: reler o bucket a cada execução" + +[portal.sources.types.s3.fields.prefix] +helperText = "Apenas são processados objetos cujas chaves comecem com este prefixo." +label = "Prefixo da chave" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Consulte uma pasta de entrega SFTP para novos documentos." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Obtenha documentos de bibliotecas de documentos SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Origem" +[portal.sources.types.webhook] +createNote = "Um URL de entrega e um segredo de assinatura são gerados quando cria este webhook. Os remetentes fazem POST de documentos para esse URL e as políticas que o referenciam são executadas à chegada." +description = "Receba documentos por HTTP POST assinado de qualquer sistema externo." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de entrega" +secretNote = "O segredo de assinatura é mostrado uma vez, quando o webhook é criado. Recrie a fonte para o rodar." + +[portal.sources.types.webhook.reveal] +copy = "Copiar" +done = "Concluído" +secret = "Segredo de assinatura" +secretHelp = "Assine o corpo bruto de cada entrega com esta chave (HMAC-SHA256) e envie-o como cabeçalho X-Stirling-Signature." +secretWarning = "Copie agora o segredo de assinatura. Para sua segurança, é mostrado apenas uma vez e não pode ser recuperado mais tarde." +title = "Webhook criado" +url = "URL de entrega" +usage = "Faça POST de cada documento como corpo bruto do pedido para o URL de entrega com um tipo de conteúdo binário (application/pdf ou application/octet-stream). As políticas que o referenciam são executadas automaticamente à chegada." + [portal.sources.wizard] -back = "Voltar" -cancel = "Cancelar" -continue = "Continuar" -editTitle = "Editar origem" name = "Nome" namePlaceholder = "por exemplo, Entrada de reclamações" -save = "Guardar alterações" -subtitle = "Passo {{current}} de {{total}} · {{label}}" -title = "Ligar uma origem" type = "Tipo" -[portal.sources.wizard.steps] -chooseType = "Escolher tipo" -configure = "Configurar" -review = "Rever e ligar" +[portal.stepModal] +close = "Fechar" [portal.usage] managePayment = "Gerir pagamento" @@ -8021,16 +8962,9 @@ summary = "Detém uma equipa — gere os recursos dos seus membros e configuraç 2 = "Acesso ao portal através da política predefinida" 3 = "Tudo o que o Membro pode fazer" -[portal.welcome] -ariaLabel = "Bem-vindo ao Stirling PDF" -badge = "Open-source" -installEditor = "Instalar o Editor" -inviteTeammates = "Convidar colegas de equipa" -openInBrowser = "Abrir no navegador" -perks = "Gratuito para sempre · Self-hostable" -subtitle = "O Editor de PDF mais seguro do mundo é gratuito para equipas de todos os tamanhos. Inclui mais de 60 operações PDF e SSO." -title = "Bem-vindo ao" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Ilimitado" [printFile] title = "Imprimir Ficheiro" @@ -8613,6 +9547,14 @@ text = "Rode as páginas do seu PDF no sentido horário ou anti-horário em incr [rotate.tooltip.header] title = "Visão geral das definições de rotação" +[ruler] +clearAll = "Limpar tudo" +hideAllLabels = "Ocultar todas as etiquetas" +hideSmallLabels = "Ocultar etiquetas pequenas" +physicalValues = "Físico" +scaled = "Escalado" +showAllLabels = "Mostrar todas as etiquetas" + [sanitize] desc = "Remover elementos potencialmente nocivos dos ficheiros PDF." sanitizationResults = "Resultados da sanitização" @@ -8654,31 +9596,45 @@ label = "Remover metadados XMP" [sanitize.steps] settings = "Definições" -[sanitizePdf] -tags = "limpar,seguro,segurança,remover-ameaças" - [sanitizePDF] title = "Sanitizar PDF" +[sanitizePdf] +tags = "limpar,seguro,segurança,remover-ameaças" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Ajustar escala de página" +[scaleSettings] +activeScale = "Escala ativa" +apply = "Aplicar escala" +applyCalibration = "Aplicar calibração" +calculatedScale = "Escala calculada" +calibrate = "Calibrar" +calibrating = "A calibrar" +calibrationDistanceRequired = "Introduza uma distância real maior do que zero" +calibrationInvalid = "Não foi possível calcular a escala a partir desta medição" +calibrationPaperDistance = "Distância medida na página: {{distance}}" +calibrationTitle = "Calibrar escala" +calibrationTooltip = "Meça uma distância conhecida para calcular a escala automaticamente" +customScale = "Escala personalizada" +noneSet = "Nenhuma escala personalizada definida" +presets = "Escalas predefinidas" +ratio = "Proporção da escala" +ratioHelp = "Proporção: 1 unidade da página = X unidades reais" +ratioPlaceholder = "por exemplo, 100" +ratioPositive = "A proporção deve ser maior do que zero" +ratioRequired = "A proporção é obrigatória" +realDistance = "Distância real" +realDistancePlaceholder = "por exemplo, 5" +reset = "Repor predefinições" +unit = "Unidade" +unitInvalid = "Unidade inválida: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Limiar de Ângulo:" -10 = "Define o tamanho da borda adicionada e removida para prevenir bordas brancas na saída (predefinição: 1)." -2 = "Define o ângulo absoluto mínimo necessário para a imagem ser rodada (predefinição: 10)." -3 = "Tolerância:" -4 = "Determina o intervalo de variação de cor em torno da cor de fundo estimada (predefinição: 30)." -5 = "Área Mínima:" -6 = "Define o limiar de área mínima para uma foto (predefinição: 10000)." -7 = "Área Mínima de Contorno:" -8 = "Define o limiar de área mínima de contorno para uma foto" -9 = "Tamanho da Borda:" - [scannerImageSplit] submit = "Extrair Digitalizações de Imagem" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "O que isto faz" whatThisDoesDesc = "Encontra e extrai automaticamente cada foto de uma página digitalizada ou imagem composta—sem recorte manual." whenToUse = "Quando usar" +[ScannerImageSplit.selectText] +1 = "Limiar de Ângulo:" +10 = "Define o tamanho da borda adicionada e removida para prevenir bordas brancas na saída (predefinição: 1)." +2 = "Define o ângulo absoluto mínimo necessário para a imagem ser rodada (predefinição: 10)." +3 = "Tolerância:" +4 = "Determina o intervalo de variação de cor em torno da cor de fundo estimada (predefinição: 30)." +5 = "Área Mínima:" +6 = "Define o limiar de área mínima para uma foto (predefinição: 10000)." +7 = "Área Mínima de Contorno:" +8 = "Define o limiar de área mínima de contorno para uma foto" +9 = "Tamanho da Borda:" + [search] noResults = "Nenhum resultado encontrado" placeholder = "Introduza o termo de pesquisa..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "O seu servidor Stirling-PDF está offline e \"{{endpo [session] expired = "A sua sessão expirou. Por favor atualize a página e tente novamente." -[sessionManagement.tooltip] -header = "Gestão de sessões de assinatura" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Novos participantes são adicionados ao fim da ordem de assinatura" -bullet2 = "Não é possível adicionar participantes após a finalização da sessão" -bullet3 = "Cada participante recebe uma notificação quando chega a sua vez" -description = "Pode adicionar mais participantes a uma sessão ativa a qualquer momento antes da finalização." -title = "Adicionar participantes" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalização total: Todos os participantes assinaram" -bullet2 = "Finalização parcial: Alguns participantes ainda não assinaram" -bullet3 = "Os participantes não assinados serão excluídos do documento final" -bullet4 = "Depois de finalizada, pode carregar o PDF assinado nos ficheiros ativos" -description = "A finalização combina todas as assinaturas num único PDF assinado. Esta ação não pode ser anulada." -title = "Finalização da sessão" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Não é possível remover participantes que já assinaram" -bullet2 = "Os participantes removidos deixam de receber notificações" -bullet3 = "A ordem de assinatura ajusta-se automaticamente" -description = "Os participantes podem ser removidos das sessões antes de assinarem." -title = "Remover participantes" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Cada assinatura é aplicada sequencialmente ao PDF" -bullet2 = "Os signatários posteriores podem ver assinaturas anteriores" -bullet3 = "Crítico para fluxos de aprovação e cadeias legais de custódia" -description = "A ordem que especificar ao criar a sessão determina quem assina primeiro." -title = "Ordem de assinatura" - [settings] close = "Fechar" +[settings.ai] +documents = "Documentos e RAG" +general = "Geral" +limits = "Limites e desempenho" +models = "Modelos e fornecedores" +title = "IA" + [settings.configuration] advanced = "Avançado" database = "Base de dados" endpoints = "Endpoints" features = "Funcionalidades" +folderAccess = "Acesso a pastas" mcp = "MCP Server" storageSharing = "Armazenamento de Ficheiros e Partilha" systemSettings = "Definições do sistema" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Editor PDF predefinido" defaultPdfEditorActive = "O Stirling PDF é o seu editor PDF predefinido" defaultPdfEditorChecking = "A verificar..." defaultPdfEditorInactive = "Outra aplicação está definida como predefinida" +defaultPdfEditorRemind = "Lembrar-me de definir como predefinição" +defaultPdfEditorRemindDescription = "Mostrar uma faixa quando o Stirling PDF não for a sua aplicação PDF predefinida." defaultPdfEditorSet = "Já é predefinido" defaultStartupView = "Vista predefinida no arranque" defaultStartupViewDescription = "Escolha qual o separador ativo na coluna esquerda quando a aplicação inicia" @@ -8817,9 +9763,18 @@ languageDescription = "Escolha o idioma de apresentação" logout = "Terminar sessão" setAsDefault = "Definir como predefinido" theme = "Tema" +themeAccent = "Cor de destaque" +themeAccentCustom = "Cor personalizada" +themeAccentCustomHint = "Personalizada (predefinições recomendadas)" +themeAccentDark = "Cor de destaque do modo escuro" +themeAccentDefault = "Predefinição" +themeAccentDefaultHint = "Tema predefinido (recomendado)" +themeAccentDescription = "Botões, ligações e destaques seguem-na, e esta aplica uma tonalidade subtil à aplicação. O modo claro e o modo escuro têm cada um a sua. O sistema usa a que estiver ativa." +themeAccentLight = "Cor de destaque do modo claro" themeDark = "Escuro" themeDescription = "Alternar entre modo claro e escuro" themeLight = "Claro" +themeReset = "Restaurar tema predefinido" themeSystem = "System" title = "Geral" user = "Utilizador" @@ -8833,6 +9788,12 @@ intro = "Ativar autenticação de utilizadores, gestão de equipas e funcionalid learnMore = "Saiba mais na documentação" title = "Para administradores do sistema" +[settings.general.loginLanding] +description = "Escolha que aplicação abre quando inicia sessão no Stirling Cloud." +editor = "Editor" +processor = "Processador" +title = "Após iniciar sessão" + [settings.general.mode] fullscreen = "Ecrã inteiro" sidebar = "Barra lateral" @@ -9013,9 +9974,6 @@ title = "Políticas e Privacidade" [settings.preferences] title = "Preferências" -[settings.search] -placeholder = "Procurar páginas de definições..." - [settings.security] description = "Atualize a sua palavra-passe para manter a sua conta segura." title = "Segurança" @@ -9041,6 +9999,11 @@ security = "Segurança" telegram = "Telegram" title = "Segurança e Autenticação" +[settings.signOut] +confirm = "Tem a certeza de que pretende terminar sessão?" +submit = "Terminar sessão" +title = "Terminar sessão" + [settings.team] title = "Equipa" @@ -9156,6 +10119,7 @@ last = "Última página" maintainRatio = "Alternar manter proporção" next = "Próxima página" noSavedSigs = "Nenhuma assinatura guardada encontrada" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Assinaturas Pessoais" previous = "Página anterior" redo = "Refazer" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Falha na remoção do fundo" hint = "Carregue uma imagem PNG ou JPG da sua assinatura" label = "Carregar imagem da assinatura" placeholder = "Selecione ficheiro de imagem" +previewAlt = "Current image signature" processing = "A processar imagem..." removeBackground = "Remover fundo branco (tornar transparente)" @@ -9207,6 +10172,17 @@ saved = "Selecione uma assinatura guardada acima e depois clique em qualquer par text = "Depois de introduzir o seu nome acima, clique em qualquer lugar no PDF para colocar a sua assinatura." title = "Como adicionar a assinatura" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Mover Assinatura" pause = "Pausar colocação" @@ -9653,10 +10629,30 @@ title = "Carregar para o servidor" updateButton = "Atualizar no servidor" uploadButton = "Carregar para o servidor" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "clique aqui" +fallback = "Se não abriu automaticamente," +opening = "A abrir Swagger UI num novo separador..." tags = "api,documentação,swagger,endpoints,desenvolvimento" title = "Documentação da API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Selecione ficheiros em Ficheiros Ativos para executar esta fe singleFileScope = "A aplicar apenas a: {{fileName}}" viewerMode = "Altere para o editor de ficheiros para selecionar vários ficheiros." +[toolFormat] +ANY = "qualquer ficheiro" +CBR = "um arquivo de banda desenhada (CBR)" +CBZ = "um arquivo de banda desenhada (CBZ)" +CSV = "um ficheiro CSV" +EBOOK = "um livro digital" +EMAIL = "um email" +EXCEL = "uma folha de cálculo" +HTML = "uma página web" +IMAGE = "imagens" +JAVASCRIPT = "um ficheiro JavaScript" +JSON = "um ficheiro JSON" +MARKDOWN = "um ficheiro Markdown" +NONE = "nenhum ficheiro" +PCL = "um ficheiro de impressão PCL" +PDF = "um PDF" +PDF_ENCRYPTED = "um PDF protegido por palavra-passe" +POSTSCRIPT = "um ficheiro PostScript" +PPT = "uma apresentação" +TEXT = "um ficheiro de texto" +VIDEO = "um vídeo" +WORD = "um documento Word" +XML = "um ficheiro XML" +XPS = "um documento XPS" +ZIP = "um arquivo ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Voltar a todas as ferramentas" collapse = "Recolher painel" expand = "Expandir painel" goBack = "Voltar" +pdfTools = "Ferramentas PDF" placeholder = "Escolha uma ferramenta para começar" premiumFeature = "Funcionalidade premium:" toolsHeader = "Ferramentas" @@ -9806,11 +10829,6 @@ noToolsFound = "Nenhuma ferramenta encontrada" quickAccess = "ACESSO RÁPIDO" searchPlaceholder = "Pesquisar ferramentas..." -[toolPicker.categories] -advancedTools = "Ferramentas avançadas" -recommendedTools = "Ferramentas recomendadas" -standardTools = "Ferramentas padrão" - [toolPicker.subcategories] advancedFormatting = "Formatação avançada" automation = "Automação" @@ -9824,6 +10842,9 @@ removal = "Remoção" signing = "Assinatura" verification = "Verificação" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nenhuma ferramenta encontrada" noTools = "Nenhuma ferramenta disponível" @@ -9950,28 +10971,53 @@ title = "Utilizadores" you = "(você)" [users.action] +cancelInvite = "Cancelar convite" deleteTeam = "Eliminar equipa" disableMfa = "Repor MFA" move = "Mover para equipa" reinstate = "Reintegrar" remove = "Remover da organização" +removeTeam = "Remover da equipa" rename = "Renomear equipa" resetPw = "Repor palavra-passe" suspend = "Suspender" unlock = "Desbloquear conta" +[users.activity] +daysAgo = "há {{count}} d" +hoursAgo = "há {{count}} h" +justNow = "Agora mesmo" +minutesAgo = "há {{count}} min" +monthsAgo_one = "há {{count}} mês" +monthsAgo_other = "há {{count}} meses" +never = "Nunca" +weeksAgo_one = "há {{count}} semana" +weeksAgo_other = "há {{count}} semanas" +yearsAgo_one = "há {{count}} ano" +yearsAgo_other = "há {{count}} anos" + [users.cap] addProcessor = "+ Processador" approver = "Aprova política" editor = "Editor" processor = "Processador" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Cancelar o convite para {{email}}? A pessoa não poderá aderir com a ligação atual." +cancelInviteTitle = "Cancelar convite" deleteTeamBody = "Eliminar a equipa {{name}}? A equipa tem de estar vazia primeiro — mova os seus membros para outra equipa e esta não pode continuar a ser proprietária de quaisquer configurações de integração." deleteTeamTitle = "Eliminar equipa" disableMfaBody = "Remover a inscrição MFA de {{name}}? Será configurada novamente no próximo início de sessão, se necessário." disableMfaTitle = "Repor MFA" removeBody = "Remover permanentemente {{name}} da organização? Esta ação não pode ser anulada." +removeTeamBody = "Remover {{name}} da equipa? A pessoa mantém a conta, mas perde o acesso aos recursos desta equipa." removeTitle = "Remover membro" [users.empty] @@ -9982,10 +11028,8 @@ title = "Ainda sem membros" addToTeam = "Adicionar à equipa" guestCount = "{{count}} convidado" guests = "Convidados" -guestsDesc = "Colaboradores externos, limitados ao que partilhou. Apenas Editor." ledBy = "liderada por {{owner}}" org = "Organização" -orgDesc = "Proprietários com autoridade em toda a organização e aprovação de políticas" owners = "{{count}} proprietário" team = "Equipa {{name}}" teamMeta = "{{count}} pessoas" @@ -10022,6 +11066,18 @@ username = "Nome de utilizador" usernameError = "O nome de utilizador deve ter pelo menos 3 caracteres" usernamePlaceholder = "jsmith" +[users.invites] +by = "Convidado por {{who}}" +cancel = "Cancelar" +expiresInDays_one = "Expira em {{count}} dia" +expiresInDays_other = "Expira em {{count}} dias" +expiresToday = "Expira hoje" +title = "Convites pendentes" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Ocorreu um erro ao contactar o backend, ou não tem acesso. Tente novamente." title = "Não foi possível carregar os membros" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Pesquisar anexos" title = "Anexos" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Título do marcador" +bookmarkTitleRequired = "O título do marcador é obrigatório" closeSidebar = "Fechar barra lateral de marcadores" collapseAll = "Recolher todos os marcadores" +empty = "No bookmarks in this document" expandAll = "Expandir todos os marcadores" +searchPlaceholder = "Pesquisar marcadores" +title = "Bookmarks" [viewer.comments] addComment = "Adicionar comentário" @@ -10236,10 +11297,12 @@ locateAnnotation = "Localizar no documento" moreActions = "Mais ações" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Página {{page}}" placingHint = "Clique numa página para colocar… (cancelar)" removeCommentOnly = "Remover apenas o comentário" saveReply = "Guardar resposta" +searchPlaceholder = "Search comments" title = "Comentários" typeComment = "Comentário" typeInsertText = "Inserir texto" @@ -10262,6 +11325,7 @@ unsavedDesc = "Tem alterações por guardar" closeSidebar = "Fechar barra lateral de camadas" hideAll = "Ocultar todas as camadas" showAll = "Mostrar todas as camadas" +title = "Layers" [viewer.link] delete = "Eliminar ligação" @@ -10605,30 +11669,6 @@ title = "Texto da Marca de Água" image = "Imagem" text = "Texto" -[wetSignature.tooltip] -header = "Métodos de criação de assinatura" - -[wetSignature.tooltip.draw] -bullet1 = "Personalize a cor e a espessura da caneta" -bullet2 = "Limpe e volte a desenhar até ficar satisfeito" -bullet3 = "Funciona em dispositivos táteis (tablets, telemóveis)" -description = "Crie uma assinatura manuscrita usando o rato ou o ecrã tátil. Ideal para assinaturas pessoais e autênticas." -title = "Desenhar Assinatura" - -[wetSignature.tooltip.type] -bullet1 = "Escolha entre vários tipos de letra" -bullet2 = "Personalize o tamanho e a cor do texto" -bullet3 = "Perfeito para assinaturas normalizadas" -description = "Gere uma assinatura a partir de texto escrito. Rápido e consistente, adequado para documentos empresariais." -title = "Escrever Assinatura" - -[wetSignature.tooltip.upload] -bullet1 = "Suporta PNG, JPG e outros formatos de imagem" -bullet2 = "Recomendam-se fundos transparentes para melhores resultados" -bullet3 = "A imagem será redimensionada para ajustar à área da assinatura" -description = "Carregue uma imagem de assinatura pré-criada. Ideal se tiver uma assinatura digitalizada ou o logótipo da empresa." -title = "Carregar Imagem da Assinatura" - [workbenchBar] activeFiles = "Ficheiros ativos" annotations = "Anotações" @@ -10642,6 +11682,7 @@ exitRedaction = "Sair do modo de expurgo" exportAll = "Exportar PDF" exportSelected = "Exportar páginas selecionadas" formFill = "Preencher formulário" +hideToolbar = "Hide toolbar" multiTool = "Multi-ferramenta" panMode = "Modo de deslocação" print = "Imprimir PDF" @@ -10652,6 +11693,7 @@ redact = "Expurgar" rotateLeft = "Rodar à esquerda" rotateRight = "Rodar à direita" ruler = "Régua / Medir" +rulerSettings = "Definições de escala" save = "Guardar" saveAll = "Guardar tudo" saveAs = "Guardar como" @@ -10661,6 +11703,7 @@ selectAll = "Selecionar tudo" selectByNumber = "Selecionar por números de página" selectLanguage = "Selecionar idioma" share = "Partilhar" +showToolbar = "Show toolbar" toggleAnnotations = "Alternar visibilidade das anotações" toggleAttachments = "Alternar anexos" toggleBookmarks = "Alternar marcadores" diff --git a/frontend/editor/public/locales/ro-RO/translation.toml b/frontend/editor/public/locales/ro-RO/translation.toml index 507215aaea..721c051539 100644 --- a/frontend/editor/public/locales/ro-RO/translation.toml +++ b/frontend/editor/public/locales/ro-RO/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Aplicare" applyAndContinue = "Salvați și ieșiți" -areYouSure = "Sigur doriți să părăsiți?" back = "Înapoi" black = "Negru" blue = "Albastru" @@ -46,10 +45,8 @@ filesSelected = "fișiere selectate" font = "Font" fontSizeTooltip = "Dimensiunea textului numărului de pagină în puncte. Numere mai mari creează text mai mare." fontTypeTooltip = "Familia de font pentru numerele de pagină. Alegeți în funcție de stilul documentului." -genericSubmit = "Trimite" green = "Verde" help = "Ajutor" -imgPrompt = "Selectează imagini" incorrectPasswordMessage = "Parola curentă este incorectă." info = "Informații" invalidUndoData = "Nu se poate anula: date de operațiune nevalide" @@ -62,7 +59,6 @@ loginAgreementTitle = "Acord de autentificare" logOut = "Deconectare" marginTooltip = "Distanța dintre numărul paginii și marginea paginii." moreOptions = "Mai multe opțiuni" -multiPdfPrompt = "Selectează mai multe fișiere PDF (2+)" never = "Niciodată" no = "Nu" noFileSelected = "Niciun fișier selectat. Vă rugăm să încărcați unul." @@ -80,7 +76,6 @@ pageOrderPrompt = "Ordinea paginilor (Introdu o listă separată prin virgulă d pages = "Pagini" pageSelectionPrompt = "Selecție Personalizată de Pagini (Introduceți o listă separată prin virgule a numerelor de pagini 1,5,6 sau funcții precum 2n+1) :" password = "Parolă" -pdfPrompt = "Selectează fișiere PDF" pin = "Fixare fișier (rămâne activ după rularea instrumentului)" pro = "Pro" processingComplete = "Fișierul dvs. este gata." @@ -110,6 +105,7 @@ undoSuccess = "Operațiune anulată cu succes" unknown = "Necunoscut" unpin = "Anulare fixare fișier (înlocuiește după rularea instrumentului)" unsavedChanges = "Aveți modificări nesalvate în PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Modificări nesalvate" unsupported = "Nesuportat" username = "Nume de utilizator" @@ -356,6 +352,7 @@ text = "Aceste permisiuni controlează ce pot face utilizatorii cu PDF-ul. Sunt title = "Schimbă Permisiunile" [addStamp] +preview = "Previzualizare ștampilă" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Descărcări manuale: faceți clic pe linkuri și plasați fișie noLanguages = "Nu s-au găsit limbi tessdata în directorul configurat." permissionNotice = "Calea tessdata nu are permisiuni de scriere. Descărcările vor fi deschise în browser; salvați manual fișierele .traineddata în folderul tessdata." +[admin.settings.ai.documents] +description = "Configurați modelul de embedding și setările de recuperare folosite pentru a răspunde la întrebări despre documente. Se aplică motorului AI la salvare." +title = "Documente și RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Lăsați necompletat pentru a folosi credențialul de mediu propriu al motorului. Se aplică implementărilor self-hosted cu un singur motor." +generic = "Cheie API pentru embedding" +openai = "Cheie API OpenAI" +setPlaceholder = "Salvată - lăsați necompletat pentru a păstra cheia curentă" +voyageai = "Cheie API VoyageAI" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "URL de bază al endpoint-ului de embeddings compatibil OpenAI / Ollama, de ex. http://ollama:11434/v1. Trebuie să indice către un endpoint intern de încredere (sensibil la SSRF)." +label = "URL de bază pentru embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Numele modelului de embedding. Text liber; sugestiile sunt doar indicii." +label = "Model de embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Furnizor folosit pentru a transforma textul documentului în embeddings vectoriale." +label = "Furnizor de embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Numărul maxim de căutări de recuperare pe care agentul le poate rula per solicitare." +label = "Căutări maxime" + +[admin.settings.ai.documents.reindexNote] +body = "Schimbarea modelului de embedding intră în vigoare imediat, dar documentele indexate cu modelul anterior trebuie reindexate pentru ca rezultatele căutării să fie corecte." +title = "Reindexare necesară" + +[admin.settings.ai.documents.saved] +reindexNote = "Dacă ați schimbat modelul de embedding, reindexați documentele existente pentru ca căutarea să folosească noul model." + +[admin.settings.ai.documents.topK] +description = "Numărul celor mai relevante fragmente recuperate per căutare." +label = "Top K" + +[admin.settings.ai.general] +description = "Conectați Stirling la motorul Python AI și alegeți ce capabilități AI sunt expuse. Modificările se aplică la repornire." +title = "Motor AI" + +[admin.settings.ai.general.capabilities] +description = "Activați sau dezactivați funcții AI individuale. Funcțiile dezactivate sunt ascunse în aplicație." +title = "Capabilități" + +[admin.settings.ai.general.enabled] +description = "Comutator principal. Când este dezactivat, nu sunt disponibile instrumente AI, agenți sau apeluri către motor." +label = "Activați AI" + +[admin.settings.ai.general.features.chat] +description = "Asistent conversațional pentru lucrul cu PDFs." +label = "Asistent chat" + +[admin.settings.ai.general.features.classify] +description = "Clasificați automat documentele după tip sau conținut." +label = "Clasificarea documentelor" + +[admin.settings.ai.general.features.createPdf] +description = "Generați un PDF nou (de ex. din HTML) printr-un agent AI." +label = "Creați PDF din prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Puneți întrebări și primiți răspunsuri bazate pe un document încărcat." +label = "Întrebări despre documente" + +[admin.settings.ai.general.features.mathAuditor] +description = "Revizuiți documentele pentru erori matematice și numerice." +label = "Auditor matematic" + +[admin.settings.ai.general.features.pdfComment] +description = "Adăugați comentarii de revizuire și adnotări generate de AI la un PDF." +label = "Agent de comentarii PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout pentru operațiuni de agent mai complexe, cum ar fi generarea documentelor." +label = "Timeout pentru rulări lungi (secunde)" + +[admin.settings.ai.general.note] +body = "Motorul AI rulează ca serviciu separat. Secretul său partajat" +body2 = "este setat printr-o variabilă de mediu a containerului. Cheile API ale furnizorilor pot fi introduse pe aceste pagini sau furnizate ca variabile de mediu ale motorului; cheile introduse aici sunt trimise către motor la salvare." +title = "Despre motorul AI" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout pentru răspunsurile chat transmise în flux (token cu token)." +label = "Timeout stream (secunde)" + +[admin.settings.ai.general.test] +button = "Testați conexiunea" +failBody = "Motorul AI nu a răspuns. Verificați URL-ul, dacă containerul motorului rulează și dacă AI este activat (este necesară o repornire după activare)." +failTitle = "Motor AI inaccesibil" +okBody = "Motorul AI a răspuns la o verificare de sănătate." +okTitle = "Motor AI accesibil" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout pentru solicitările AI standard către motor." +label = "Timeout solicitare (secunde)" + +[admin.settings.ai.general.url] +description = "URL intern al motorului Python AI, de ex. http://stirling-pdf-engine:5001." +label = "URL motor AI" + +[admin.settings.ai.limits] +description = "Măsuri de control pentru cât de mult lucru pot face solicitările AI și câte rulează concomitent. Se aplică motorului AI la salvare." +title = "Limite și performanță" + +[admin.settings.ai.limits.maxCharacters] +description = "Măsură de control: respinge solicitările AI al căror text extras depășește această lungime." +label = "Număr maxim de caractere per solicitare" + +[admin.settings.ai.limits.maxPages] +description = "Măsură de control: respinge solicitările AI peste acest număr de pagini PDF." +label = "Număr maxim de pagini per solicitare" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Numărul maxim de apeluri simultane către model în desfășurare în întregul motor." +label = "Concurență maximă model" + +[admin.settings.ai.models] +description = "Alegeți furnizorul LLM și modelele inteligente/rapide folosite de motorul AI. Se aplică motorului AI la salvare." +title = "Modele și furnizori" + +[admin.settings.ai.models.apiKey] +anthropic = "Cheie API Anthropic" +description = "Lăsați necompletat pentru a folosi credențialul de mediu propriu al motorului. Se aplică implementărilor self-hosted cu un singur motor." +generic = "Cheie API" +openai = "Cheie API OpenAI" +setPlaceholder = "Salvată - lăsați necompletat pentru a păstra cheia curentă" + +[admin.settings.ai.models.baseUrl] +description = "URL de bază al endpoint-ului compatibil OpenAI / Ollama, de ex. http://ollama:11434/v1." +label = "URL de bază furnizor" +warning = "URL-ul de bază trebuie să indice către un endpoint intern de încredere. Motorul va face solicitări server-side către acesta, deci o valoare nefiabilă este sensibilă la SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Numărul maxim de token-uri de ieșire pentru modelul rapid." +label = "Token-uri maxime model rapid" + +[admin.settings.ai.models.fastModel] +description = "Model mai ieftin și mai rapid pentru sarcini ușoare. Text liber; sugestiile sunt doar indicii." +label = "Model rapid" + +[admin.settings.ai.models.provider] +description = "Cu ce furnizor LLM comunică motorul." +label = "Furnizor" + +[admin.settings.ai.models.smartMaxTokens] +description = "Numărul maxim de token-uri de ieșire pentru modelul inteligent." +label = "Token-uri maxime model inteligent" + +[admin.settings.ai.models.smartModel] +description = "Model cu capabilități ridicate pentru raționament complex. Text liber; sugestiile sunt doar indicii." +label = "Model inteligent" + +[admin.settings.ai.saved] +body = "Modificările sunt trimise automat către motorul AI." +bodyNoPush = "Setări salvate. Acestea se vor aplica data viitoare când motorul AI își preia configurația." +title = "Setări AI salvate" + [admin.settings.badge] clickToUpgrade = "Faceți clic pentru a vedea detaliile planului" @@ -938,6 +1094,25 @@ label = "Regenerare la pornire" description = "Numărul de zile pentru care certificatul va fi valabil" label = "Valabilitate certificat (zile)" +[admin.settings.folderAccess] +description = "Directoare din care sursele de folder și ieșirile de folder au permisiunea să citească și în care să scrie. Aceasta este o limită de securitate: automatizările nu pot fi niciodată direcționate către o cale de server din afara acestei liste." +securityNote = "Lăsați necompletat pentru a dezactiva complet sursele și ieșirile de folder. Directorul de configurare propriu al Stirling este întotdeauna interzis." +title = "Acces la foldere" + +[admin.settings.folderAccess.implied] +description = "Aceste directoare gestionate de Stirling sunt întotdeauna permise și nu pot fi modificate aici." +serverStorage = "Stocare fișiere server" +title = "Permise întotdeauna" +watchedFolder = "Folder monitorizat pentru pipeline" + +[admin.settings.folderAccess.roots] +add = "Adăugați" +empty = "Nu este permis niciun folder. Sursele și ieșirile de folder sunt dezactivate în prezent." +hint = "Introduceți căi absolute, de exemplu /data/inbox." +label = "Rădăcini de foldere permise" +placeholder = "/data/inbox" +remove = "Eliminați rădăcina folderului" + [admin.settings.general] description = "Configurați setările aplicației la nivel de sistem, inclusiv branding și comportament prestabilit." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Lățime" [app] description = "Alternativa gratuită la Adobe Acrobat (10M+ descărcări)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Convertește la PDF/A-3b" convertToPdfA3bDescription = "Creează un PDF pentru arhivare cu atașamente încorporate" @@ -1842,6 +2020,15 @@ export = "Export" accessDenied = "Acces refuzat" insufficientPermissions = "Nu aveți permisiunea de a efectua această acțiune." +[auth.callback] +completing = "Se finalizează autentificarea" +invalidToken = "Autentificarea OAuth a eșuat - token nevalid." +missingToken = "Autentificarea OAuth a eșuat - nu s-a primit niciun token." +oauthFailed = "Autentificarea OAuth a eșuat. Încercați din nou." +pleaseWait = "Așteptați cât timp finalizăm autentificarea." +signedOut = "Ați fost deconectat. Autentificați-vă din nou." +windowMayClose = "Puteți închide această fereastră după finalizare." + [auth.displayName] guest = "Invitat" user = "Utilizator" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Asigură documentele PDF prin eliminarea conținutului pot description = "Acest instrument va redenumi automat fișierele PDF pe baza conținutului lor. Analizează documentul pentru a găsi cel mai potrivit titlu din text." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rotire automată" +tags = "rotire automată,remediere orientare,cu susul în jos,lateral,îndreptare,orientare,detectare orientare,corectare rotire,orientare automată,scanat,OSD" + +[autoRotate.confidenceThreshold] +title = "Prag de încredere" + +[autoRotate.detectionMode] +auto = "Automat" +osd = "Doar OCR" +text = "Doar text" +title = "Metodă de detectare" + +[autoRotate.error] +failed = "A apărut o eroare la rotirea automată a PDF-ului." + +[autoRotate.inferUndetected] +title = "Rotiți paginile cu încredere scăzută pentru potrivire" + +[autoRotate.report] +applied = "{{degrees}}° CW" +confidenceValue = "Încredere {{value}}" +noChange = "Nicio modificare" +pageLabel = "Pagina {{number}}" +summary = "{{rotated}} din {{total}} pagini rotite, {{unchanged}} lăsate neschimbate" +title = "Raport de detectare" + +[autoRotate.report.method] +inferred = "Potrivit" +none = "Omis" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Sub prag" +blankPage = "Pagina este goală" +inferredFromDocument = "La fel ca celelalte pagini" +noDominantDirection = "Direcții de text mixte" +osdFailed = "Nu s-a găsit text lizibil" +osdNoVerdict = "Nu s-a putut determina orientarea corectă" +tesseractUnavailable = "OCR nu este instalat" +tooFewGlyphs = "Prea puțin text" + +[autoRotate.results] +title = "Rezultate rotire automată" + +[autoRotate.settings] +title = "Setări" + +[autoRotate.tooltip.confidenceThreshold] +text = "Cât de sigură trebuie să fie verificarea înainte ca o pagină să fie rotită. Reduceți pentru a roti mai multe pagini, creșteți pentru a lăsa mai multe pagini neatinse." +title = "Prag de încredere" + +[autoRotate.tooltip.description] +text = "Determină orientarea corectă a fiecărei pagini și o rotește corespunzător. Paginile care sunt deja drepte sunt lăsate neschimbate." + +[autoRotate.tooltip.detectionMode] +text = "Automat citește mai întâi textul de pe pagină, apoi analizează pagina ca imagine dacă nu este suficient. Doar text omite verificarea imaginii, fiind cel mai rapid pentru documentele care nu au fost scanate niciodată. Doar OCR tratează fiecare pagină ca imagine." +title = "Metodă de detectare" + +[autoRotate.tooltip.header] +title = "Rotire automată" + +[autoRotate.tooltip.inferUndetected] +text = "Paginile goale sau aproape goale nu pot fi verificate singure. Când toate paginile care au putut fi citite necesită aceeași rotire, aceste pagini sunt rotite pentru a se potrivi cu ele." +title = "Rotiți paginile cu încredere scăzută pentru potrivire" + [autoSizeSplitPDF] tags = "pdf,împarte,document,organizare" @@ -2189,33 +2443,6 @@ keywords = "Cuvinte cheie: odd, even" numbers = "Numere/intervale: 5, 10-20" progressions = "Progresii: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipuri de certificate" - -[certificateChoice.tooltip.organization] -bullet1 = "Gestionat de administratorii de sistem" -bullet2 = "Partajat între utilizatorii autorizați" -bullet3 = "Reprezintă identitatea companiei, nu pe cea individuală" -bullet4 = "Ideal pentru: Documente oficiale, semnături de echipă" -description = "Un certificat partajat furnizat de organizația dvs. Utilizat pentru autoritate de semnare la nivel de companie." -title = "Certificat al organizației" - -[certificateChoice.tooltip.personal] -bullet1 = "Generat automat la prima utilizare" -bullet2 = "Asociat contului dvs. de utilizator" -bullet3 = "Nu poate fi partajat cu alți utilizatori" -bullet4 = "Ideal pentru: Documente personale, responsabilitate individuală" -description = "Un certificat generat automat, unic pentru contul dvs. de utilizator. Potrivit pentru semnături individuale." -title = "Certificat personal" - -[certificateChoice.tooltip.upload] -bullet1 = "Necesită fișier P12/PFX și parolă" -bullet2 = "Poate fi emis de Autorități de Certificare externe" -bullet3 = "Nivel de încredere mai ridicat pentru documente legale" -bullet4 = "Ideal pentru: Contracte cu valoare legală, validare externă" -description = "Utilizați propriul fișier de certificat PKCS#12. Oferă control complet asupra proprietăților certificatului." -title = "Încărcați P12 personalizat" - [certSign] awaitingSignatures = "În așteptarea semnăturilor" chooseCertificate = "Alegeți fișierul certificatului" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Sursa certificatului" upload = "Încărcare" -[certSign.tooltip.header] -title = "Despre gestionarea semnăturilor" - -[certSign.tooltip.overview] -bullet1 = "Verificați semnăturile existente și valabilitatea lor" -bullet2 = "Vizualizați informații detaliate despre semnatari și certificate" -bullet3 = "Adăugați semnături digitale noi pentru a vă securiza documentele" -bullet4 = "Suport pentru fișiere multiple, cu navigare facilă" -text = "Acest instrument vă permite să verificați dacă PDF-urile sunt semnate digital și să adăugați semnături digitale noi. Semnăturile digitale dovedesc cine a creat sau aprobat un document și arată dacă a fost modificat după semnare." -title = "Ce poate face acest instrument?" - -[certSign.tooltip.signing] -bullet1 = "Suportă formatele PEM, PKCS12, JKS și certificat de server" -bullet2 = "Opțiune de afișare sau ascundere a semnăturii în PDF" -bullet3 = "Adăugați motiv, locație și numele semnatarului" -bullet4 = "Alegeți pagina pe care să plasați semnăturile vizibile" -bullet5 = "Folosiți certificatul serverului pentru opțiunea simplă „Semnează cu Stirling-PDF”" -text = "Pentru a semna un PDF, aveți nevoie de un certificat digital (precum PEM, PKCS12 sau JKS). Puteți alege ca semnătura să fie vizibilă în document sau invizibilă doar pentru securitate." -title = "Adăugarea semnăturilor" - -[certSign.tooltip.validation] -bullet1 = "Afișează dacă semnăturile sunt valide sau invalide" -bullet2 = "Afișează informații despre semnatar și data semnării" -bullet3 = "Verifică dacă documentul a fost modificat după semnare" -bullet4 = "Poate folosi certificate personalizate pentru verificare" -text = "Când verificați semnăturile, instrumentul vă spune dacă sunt valide, cine a semnat documentul, când a fost semnat și dacă documentul a fost schimbat după semnare." -title = "Verificarea semnăturilor" - [changeCreds] changeUsername = "Actualizați numele de utilizator. Veți fi deconectat(ă) după actualizare." credsUpdated = "Cont actualizat" @@ -2737,12 +2936,16 @@ text = "Pentru ca aceste permisiuni să nu poată fi schimbate, utilizați instr [chat.actions] copy = "Copiați mesajul" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opțiuni agent Stirling" +assistant = "AI Assistant" clearChat = "Goliți chatul" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Această operație va folosi creditele din cloud" tooltip = "Alegeți culoarea de pe ecran" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Alege culoarea" [common] +actions = "Actions" back = "Înapoi" cancel = "Anulați" close = "Închide" +codeSample = "Code sample" collapse = "Restrânge" confirm = "Confirmă" continue = "Continuă" copied = "Copiat!" copy = "Copiați" done = "Gata" +enterValue = "Enter value" error = "Eroare" expand = "Extinde" loading = "Se încarcă..." next = "Înainte" +open = "Open" preview = "Previzualizare" previous = "Înapoi" refresh = "Reîmprospătați" +remaining = "Rămas" retry = "Reîncercați" save = "Salvează" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Șterge selecția" @@ -2988,6 +3199,7 @@ title = "Metodă de comprimare" [compress.settings] desiredSize = "Dimensiunea dorită a fișierului" desiredSizePlaceholder = "Introduceți dimensiunea" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Comprimarea este o modalitate ușoară de a reduce dimensiunea fișierului. Alegeți Dimensiunea fișierului pentru a introduce o dimensiune țintă și vom ajusta noi calitatea. Alegeți Calitate pentru a seta manual intensitatea comprimării." @@ -3190,6 +3402,24 @@ pdfOptions = "Opțiuni PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Selectați mai întâi un format sursă" settings = "Setări" single = "Unic" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Monitorizare avansată" -allInOneWorkspace = "Spațiu de lucru PDF all-in-one (vizualizator, instrumente și agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Asistență dedicată și SLA-uri" -fasterThroughput = "Debit de procesare de 10x mai rapid" -fullyPrivateFiles = "Fișiere complet private" -largeFileProcessing = "Procesare fișiere mari" -orgWideAccess = "Control de acces la nivel de organizație" -premiumAiModels = "Modele AI Premium" -prioritySupport = "Asistență prioritară" -privateDocCloud = "Cloud de documente privat" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Acces API securizat" -standardThroughput = "Debit standard" -unlimitedApiAccess = "Acces API nelimitat" -unlimitedSeats = "Locuri nelimitate" - [crop] autoCrop = "Decupare automată a spațiilor albe" noFileSelected = "Selectați un fișier PDF pentru a începe decuparea" @@ -3346,6 +3558,7 @@ message = "Nu s-a putut seta gestionarul PDF implicit" title = "Eroare" [defaultApp.prompt] +dontRemind = "Nu-mi mai reaminti" message = "Fă din Stirling PDF aplicația implicită pentru deschiderea fișierelor PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Căutați..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Editați" @@ -3644,11 +3861,13 @@ shareSelected = "Partajați elementele selectate" sharing = "Partajare" showAll = "Afișați tot" showHistory = "Afișați istoricul" +sortBy = "Sort files" sortByDate = "Sortează după dată" sortByName = "Sortează după nume" sortBySize = "Sortează după dimensiune" storage = "Stocare" storageState = "Stocare" +storageUsed = "Storage used" synced = "Sincronizat" title = "Încărcați fișiere PDF" toolChain = "Instrumente aplicate" @@ -3694,13 +3913,17 @@ addFiles = "Adaugă fișiere" addingFiles = "Se adaugă fișiere…" collapse = "Restrânge bara laterală" customizeGroups = "Personalizează grupurile" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Deschide fișiere pentru a începe" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Extinde bara laterală" -files = "Fișiere" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive nu este configurat" leaveMyFiles = "Părăsiți Fișierele mele" +library = "Bibliotecă PDF" myFiles = "Fișierele mele" noFiles = "Încă nu există fișiere" openFileManager = "Deschide managerul de fișiere" @@ -3708,37 +3931,36 @@ openFromComputer = "Deschide de pe calculator" openSettings = "Deschide setările" other = "Altele" recent = "Recente" -search = "Căutare" -searchPlaceholder = "Caută fișiere..." viewAll = "Vezi toate cele {{count}} fișiere" [fileSidebar.fileItem] closeViewer = "Închideți vizualizatorul" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Deschideți în vizualizator" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Adaugă" -addLabel = "Adaugă o etichetă…" -categoryIconAria = "Alege o pictogramă pentru {{name}}" -createCategory = "Creează categorie" -delete = "Șterge categoria" done = "Gata" -hide = "Ascunde categoria" -newCategory = "Nume categorie nouă…" -removeLabel = "Elimină {{name}}" -rename = "Redenumește" reset = "Resetează la valorile implicite" -search = "Caută etichete…" -show = "Afișează categoria" subtitle = "Grupează fișierele în categorii părinte. Adaugă etichete existente sau noi într-o categorie, redenumește-o sau creează una proprie. Fișierele care nu se află în niciuna dintre categoriile tale apar sub „Altele”." title = "Categorii bară laterală" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Adăugați la spațiul de lucru" addToWorkspaceCount = "Adăugați {{count}} la spațiul de lucru" @@ -3749,7 +3971,6 @@ backToMyFiles = "Înapoi la Fișierele mele" breadcrumbs = "Calea folderului" cancel = "Anulați" classification = "Clasificare" -clearSearch = "Ștergeți căutarea" clearSelection = "Goliți selecția" closeDetails = "Închideți detaliile" create = "Creați" @@ -3781,6 +4002,7 @@ downloadAll = "Descărcați tot" downloadVersion = "Descărcați această versiune" dropOverlay = "Plasați fișierele pentru a încărca" dropOverlaySub = "Fișierele pornesc în Local. Folosiți „Mută în” sau „Salvează în cloud” pentru a le organiza într-un folder." +duplicate = "Duplicate" file = "Fișier" fileInfo = "File info" fileMenu = "Acțiuni fișier" @@ -3808,13 +4030,12 @@ refresh = "Reîmprospătați de pe server" remove = "Ștergeți" removeVersion = "Eliminați această versiune" rename = "Redenumiți" +renamed = "Redenumit" renameFolder = "Redenumiți folderul" resizeFolderTree = "Redimensionați arborele de foldere (tastele săgeți, Shift pentru pași mai mari; dublu-clic pentru autofit)" save = "Salvați" saveToServer = "Salvați pe server" saveToServerDisabledHint = "Salvarea pe server nu este activată pe acest server. Rugați administratorul să o activeze." -search = "Căutați" -searchPlaceholder = "Căutați în acest folder și subfoldere" selectAll = "Selectați tot" selectAllHint = "Faceți clic pentru a selecta tot. Sfat: țineți apăsat Ctrl (sau Cmd) pentru a adăuga fișiere pe rând, Shift pentru a selecta un interval." selectedCount = "{{count}} selectate" @@ -3874,6 +4095,10 @@ title = "Încă nu există fișiere în cloud" hint = "Fișierele salvate fără încărcare rămân aici. Plasați un fișier pentru a adăuga unul." title = "Niciun fișier doar local" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Fișierele pe care le deschideți sau editați vor apărea aici." title = "Nimic modificat încă" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nu s-a putut șterge folderul." deleteFolderFailedDetail = "Nu s-a putut șterge folderul: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nu s-a putut actualiza aspectul folderului." folderAppearanceFailedDetail = "Nu s-a putut actualiza aspectul folderului: {{message}}" moveFilesFailed = "Nu s-au putut muta fișierele." @@ -3910,9 +4137,7 @@ count = "Fișiere" folder = "Folder" labels = "Etichete" modified = "Modificat" -name = "Nume" size = "Dimensiune" -toolHistoryAtVersion = "Lanț de instrumente cumulativ" totalSize = "Dimensiune totală" type = "Tip" versionHistory = "Evoluție versiuni" @@ -3945,6 +4170,11 @@ localHint = "Stocat doar în acest browser" shared = "Partajat" sharedHint = "Partajat cu dvs. prin link" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Cele mai vechi mai întâi" modifiedDesc = "Cele mai recente mai întâi" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Nu există fișiere în stocare. Deschide mai întâi c open = "Deschide" openFile = "Deschide fișier" openFiles = "Deschide fișiere" +selectFile = "Select {{name}}" selectFromStorage = "Selectați din stocare" upload = "Încărcați" uploadFile = "Încărcați fișier" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Totul salvat" +analyzingFields = "Analysing form fields..." +extractCsvError = "Extragerea CSV a eșuat" +extractXlsxError = "Extragerea XLSX a eșuat" +filled = "filled" flattenAfterFilling = "Aplatizați după completare" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl" +requiredFieldsError = "Completați toate câmpurile obligatorii" rescanFields = "Rescanați câmpurile" rescanFormFields = "Rescanați câmpurile formularului" save = "Salvați" +saveShortcut = "Ctrl+S pentru salvare" +unsavedChanges = "Modificări nesalvate" [formFill.sidebar] close = "Închideți bara laterală" @@ -4309,6 +4552,11 @@ desc = "Redenumește automat un fișier PDF pe baza antetului detectat" tags = "auto-detectare,bazat-pe-antet,organizează,re-etichetează" title = "Redenumire automată fișier PDF" +[home.autoRotate] +desc = "Detectați orientarea fiecărei pagini și rotiți-o automat în poziție dreaptă." +tags = "rotire automată,remediere orientare,cu susul în jos,lateral,îndreptare,orientare,detectare orientare,corectare rotire,orientare automată,scanat,OSD" +title = "Rotire automată" + [home.autoSizeSplitPDF] tags = "auto,împarte,dimensiune" title = "Auto-împărțire mărime/pagini" @@ -4608,15 +4856,12 @@ hint = "Încărcați un fișier imagine PNG, JPG, SVG sau altul pentru a-l plasa label = "Încărcați imagine" placeholder = "Selectați fișierul imagine" -[imageToPdf] -tags = "conversie,img,jpg,poză,fotografie" - [imageToPDF] header = "Imagine în PDF" title = "Imagine în PDF" -[infoBanner] -dismiss = "Închideți" +[imageToPdf] +tags = "conversie,img,jpg,poză,fotografie" [invite] acceptError = "Crearea contului a eșuat" @@ -4647,11 +4892,9 @@ welcomeTitle = "Ați fost invitat!" [landing] addFiles = "Adăugați fișiere" -heroSubtitle = "Trageți și plasați sau adăugați un PDF existent pentru a începe." mobileUpload = "Încărcare de pe mobil" openFromComputer = "Deschide de pe calculator" uploadFromComputer = "Încărcați de pe calculator" -workbenchEmptyStateHero = "Plasează un PDF oriunde" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Introduceți numele de utilizator" failedToSignIn = "Autentificarea cu {{provider}} a eșuat: {{message}}" forgotPassword = "Ați uitat parola?" loggingIn = "Autentificare..." -logIn = "Autentificare" login = "Autentificare" magicLinkSent = "Linkul magic a fost trimis la {{email}}! Verificați emailul și faceți clic pe link pentru a vă autentifica." mfaCode = "Cod de autentificare" @@ -4715,26 +4957,10 @@ signInWith = "Autentificați-vă cu" title = "Autentificare" unexpectedError = "Eroare neașteptată: {{message}}" updatePassword = "Actualizați parola" -useEmailInstead = "Autentificare cu email" useMagicLink = "Folosiți link magic în schimb" username = "Nume utilizator" youAreLoggedIn = "Sunteți autentificat!" -[login.slides.edit] -alt = "Editați PDF-uri" -subtitle = "Cu peste o duzină de instrumente pentru a redacta, semna, citi și manipula PDF-uri, veți găsi cu siguranță ceea ce căutați." -title = "Editați PDF-uri pentru a afișa/proteja informațiile dorite" - -[login.slides.overview] -alt = "Prezentare generală Stirling PDF" -subtitle = "O suită cloud axată pe confidențialitate pentru PDF-uri, care vă permite să convertiți, semnați, redactați și gestionați documente, plus peste 50 de alte instrumente puternice." -title = "Un centru unic pentru toate nevoile PDF." - -[login.slides.secure] -alt = "Securizați PDF-uri" -subtitle = "Adăugați parole, redactați conținut și gestionați certificate cu ușurință." -title = "Protejați informațiile sensibile din PDF-urile dvs." - [margin] large = "Mare" medium = "Mediu" @@ -4818,6 +5044,34 @@ uploadSuccess = "Încărcare reușită!" uploadSuccessMessage = "Imaginile au fost transferate." validating = "Se validează sesiunea..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Scanați pentru a încărca fotografii. Imaginile se convertesc automat în PDF." descriptionNoConvert = "Scanați pentru a încărca fotografii de pe dispozitivul mobil." @@ -4836,8 +5090,13 @@ title = "Încărcare de pe mobil" tags = "Instrument Multiplu,Operație multiplă,UI,clic tragere,front end,client side" title = "Instrument PDF multiplu" -[navbar] -search = "Căutare" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentificarea nu a reușit. Puteți închide această fereastră și să încercați din nou." @@ -4966,6 +5225,7 @@ activeFiles = "Vizualizarea Fișiere active arată toate PDF-ur allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Închide" cropSettings = "Acum că am selectat fișierul pe care dorim să-l decupăm, putem configura instrumentul Decupare pentru a alege zona la care vrem să decupăm PDF-ul." +dialogLabel = "Onboarding" fileCheckbox = "Un clic pe unul dintre fișiere îl selectează pentru procesare. Puteți selecta mai multe fișiere pentru operațiuni în lot." fileReplacement = "Fișierul modificat va înlocui automat fișierul original în Bancul de lucru, permițându-vă să îl treceți ușor prin mai multe instrumente." filesButton = "Butonul Fișiere din bara de acces rapid vă permite să încărcați PDF-uri pe care să folosiți instrumentele." @@ -4976,6 +5236,7 @@ pinButton = "Puteți folosi butonul Fixează dacă preferați c results = "După ce instrumentul a terminat rularea, pasul de Revizuire va afișa o previzualizare a rezultatelor în acest panou și vă va permite să anulați operațiunea sau să descărcați fișierul." runButton = "După ce instrumentul a fost configurat, acest buton vă permite să rulați instrumentul pe toate PDF-urile selectate." selectCropTool = "Să selectăm instrumentul Decupare pentru a demonstra cum se utilizează unul dintre instrumente." +stepOf = "Pasul {{current}} din {{total}}" toolInterface = "Aceasta este interfața instrumentului Decupare. După cum vedeți, nu este prea mult acolo deoarece încă nu am adăugat fișiere PDF cu care să lucrăm." workbench = "Acesta este Bancul de lucru - zona principală în care vizualizați și editați PDF-urile." wrapUp = "Totul este pregătit! Ați învățat despre zonele principale ale aplicației și cum să le utilizați. Faceți clic pe butonul Ajutor oricând doriți pentru a vedea din nou acest tur." @@ -4988,6 +5249,26 @@ showMeAround = "Arată-mi" skipForNow = "Sari peste deocamdată" skipTheTour = "Sari peste tur" +[onboarding.checklist] +dismiss = "Închideți" +title = "Configurați Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Rulați Stirling nativ pe computerul dvs." +title = "Descărcați Stirling pentru Desktop" + +[onboarding.checklist.inviteTeam] +description = "Colaborați cu echipa dvs." +title = "Invitați membri ai echipei" + +[onboarding.checklist.shareAnalytics] +description = "Ajutați la îmbunătățirea Stirling" +title = "Partajați date anonime de utilizare" + +[onboarding.checklist.takeTour] +description = "Vedeți cum funcționează Stirling într-un tur rapid" +title = "Faceți turul" + [onboarding.desktopInstall] body = "Stirling funcționează cel mai bine ca aplicație desktop. O poți folosi offline, accesezi documentele mai rapid și faci editări local pe computer." selectOs = "Selectează sistemul de operare" @@ -4996,7 +5277,21 @@ titleWithOs = "Descărcare pentru {{osLabel}}" [onboarding.mfa] authenticationCode = "Cod de autentificare" +description = "Securizați-vă contul conectând o aplicație de autentificare. Scanați codul QR sau introduceți cheia de configurare, apoi confirmați codul din 6 cifre pentru a finaliza." +enable = "Activați MFA" +enableError = "Nu se poate activa autentificarea cu doi factori. Verificați codul și încercați din nou." +enterCode = "Introduceți codul din 6 cifre din aplicația dvs." +enterCodeError = "Introduceți codul de autentificare pentru a continua." +logout = "Deconectare" +openAuthenticator = "Deschideți Google Authenticator, Authy sau 1Password." qrCodeLoading = "Se generează codul QR…" +regenerateQrCode = "Regenerați codul QR" +scanQrCode = "Scanați codul QR sau introduceți cheia de configurare de mai jos." +setupError = "Nu se poate porni configurarea cu doi factori. Încercați din nou." +setupKey = "Cheie de configurare (introducere manuală)" +stepByStep = "Pas cu pas" +success = "MFA a fost activat. Acum puteți continua." +title = "Configurare autentificare multi-factor" [onboarding.planOverview] adminBodyLoginDisabled = "După ce activezi modul de autentificare, poți gestiona utilizatori, configura setări și monitoriza starea serverului. Primele {{freeTierLimit}} persoane de pe serverul tău folosesc Stirling gratuit." @@ -5005,6 +5300,11 @@ adminTitle = "Prezentare admin" userBody = "Invite colegi, atribuie roluri și ține documentele organizate într-un spațiu de lucru sigur. Activează modul de autentificare când ești pregătit să treci dincolo de utilizarea solo." userTitle = "Prezentare plan" +[onboarding.processorIntro] +body = "Stirling rulează acum Politici — reguli automate care clasifică, securizează și procesează fiecare document pe măsură ce sosește. Configurați-le și monitorizați rulările în Processor." +cta = "Consultați Processor" +title = "Consultați Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "An plătit în avans" + +[payg.prepaid.banner] +exhausted = "Capacitatea dvs. plătită în avans este epuizată; facturarea contorizată a fost reluată. Reîncărcați pentru a păstra reducerea." +expiring_one = "Anul dvs. plătit în avans se încheie peste {{count}} zi. Capacitatea neutilizată expiră — reîncărcați pentru a începe un an nou." +expiring_other = "Anul dvs. plătit în avans se încheie peste {{count}} zile. Capacitatea neutilizată expiră — reîncărcați pentru a începe un an nou." +low = "Ați ajuns la {{pct}}% din capacitatea plătită în avans. Reîncărcați oricând pentru a menține procesarea neîntreruptă." + +[payg.prepaid.card] +subtitle = "Folosită înainte de facturarea contorizată și în afara plafonului dvs. de cheltuieli." +title = "Capacitate plătită în avans" + +[payg.prepaid.meter] +capSuffix = "din {{total}} credite plătite în avans" +expires = "Expiră {{date}}" + +[payg.prepaid.state] +exhausted = "Epuizată" +healthy = "Mai este suficient" +low = "Se epuizează" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF către PDF/A" tags = "arhivă,termen-lung,standard,conversie,stocare,conservare" title = "PDF către PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tipărire,standard,conversie,producție,pregătire pentru tipar,arhivă" title = "PDF în PDF/X" @@ -5822,6 +6151,7 @@ small = "500 de credite" xsmall = "100 de credite" [plan.availablePlans] +currency = "Billing currency" subtitle = "Alegeți planul care se potrivește nevoilor dvs." title = "Planuri disponibile" @@ -5937,49 +6267,12 @@ keyDescription = "Lipiți cheia de licență din e-mail" success = "Licență activată!" successMessage = "Licența a fost activată cu succes. Puteți închide această fereastră." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "aplicată" -enforcing = "Se aplică..." -failed = "Aplicarea a eșuat" outputsUnavailable = "Rezultatele politicii nu mai sunt disponibile pentru descărcare." partialOutputsUnavailable = "Unele rezultate ale politicii nu mai sunt disponibile pentru descărcare." -retrying = "Ocupat, se reîncearcă..." runNotFound = "Rularea aplicării nu a mai putut fi găsită." -step = "pas {{current}}/{{total}}" timedOut = "Aplicarea a expirat înainte ca rularea să se finalizeze." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Închide" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Se aplică {{names}}" applyingProgress = "Se aplică {{names}} ({{done}} din {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} din {{total}} fișier(e) nu au putut fi procesate ș failureTitle = "Exportat fără aplicare completă" printPolicyAppliedBody = "Acest PDF a fost actualizat pentru a respecta o politică. Revizuiți modificările, apoi imprimați din nou." printPolicyAppliedTitle = "Politică aplicată înainte de imprimare" -queued = "+{{count}} în așteptare" successTitle = "{{names}} aplicate" summaryMore = "{{first}}, {{second}} și încă {{more}}" summaryTwo = "{{first}} și {{second}}" -[policies.enforcement.triggerVerb] -convert = "Se aplică înainte de conversie" -default = "Se aplică" -export = "Se aplică înainte de export" -input = "Se aplică la import" -print = "Se aplică înainte de imprimare" - -[policies.field] -accessLog = "Jurnal de acces" -archiveAfter = "Arhivează după" -auditTrail = "Pistă de audit" -belowThreshold = "Sub prag" -destination = "Destinație" -frameworks = "Frameworks" -immutableHold = "Reținere imuabilă" -keepFor = "Păstrează timp de" -minConfidence = "Încredere minimă" -notify = "Notifică la rutare" -onViolation = "Când nu este conform" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Adaugă" -addPlaceholder = "Adaugă o etichetă…" -customNote = "Personalizat pentru echipa ta." -defaultNote = "Se folosește valoarea implicită integrată, partajată cu echipa ta." -duplicate = "„{{name}}” există deja." -edit = "Editează etichetele" -empty = "Încă nu există etichete." -export = "Exportă JSON" -iconAria = "Alege o pictogramă pentru {{name}}" -import = "Importă JSON" -importError = "Nu s-a putut importa acel fișier." -managedNote = "Etichetele echipei sunt gestionate de liderul echipei." -modalSubtitle = "Partajate cu întreaga ta echipă. Clasificatorul alege etichetele care se potrivesc fiecărui document." -modalTitle = "Etichete de clasificare" -pickIcon = "Folosește pictograma {{label}}" -removeAria = "Elimină {{name}}" -resetToDefault = "Resetează la implicit" -saveForTeam = "Salvează pentru echipă" -saving = "Se salvează…" -sectionLabel = "Etichete de clasificare" -startFromScratch = "Începe de la zero" -teamCount = "etichete de echipă" -tooLong = "Etichetele pot avea cel mult {{max}} caractere." -ungrouped = "Negrupate" -view = "Vezi etichetele" +categoryCount = "categorii" +hideCategory = "Ascundeți categoria" +labelCount = "etichete" +sharedNote = "Aceste etichete sunt integrate și partajate în întreaga echipă." +showCategory = "Afișați categoria" [policies.pii] account = "Numere de cont (etichetate)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Numere de rutare din SUA (ABA)" ssn = "Numere de securitate socială" -[policies.settings] -noneExport = "În prezent, nu se execută politici la export." -noneUpload = "În prezent, nu se execută politici la upload." -onExport = "La export" -onUpload = "La upload" -reorderHandle = "Trage pentru a reordona" -runOrderDesc = "Când mai multe politici rulează pe același declanșator, acestea rulează în această ordine — fiecare pe rezultatul politicii anterioare. Trage pentru a reordona." -title = "Setări politici" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opțiuni politici" -policySettings = "Setări politici" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Reîncearcă politicile eșuate ({{count}})" -rowProgress = "{{completed}} din {{total}} fișiere procesate" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Ce este o politică?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Găsește automat și acoperă cu negru detalii sensibile — precum numerele de securitate socială și de card — astfel încât să nu poată fi citite în document." -sanitize = "Elimină JavaScript ascuns din fișier, astfel încât nimic să nu ruleze automat când cineva îl deschide." -watermark = "Aplică o marcă vizibilă (de ex. \"Confidențial\") pe fiecare pagină." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Închide" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} se aplică..." badgeRan = "Politica {{name}} a rulat pe acest fișier" +badgeRunning = "{{name}} rulează..." blockingAction = "{{action}} blocată în timpul aplicării politicii, te rugăm să aștepți..." dismiss = "Închide suprapunerea" enforcingTitle = "Se aplică politica..." @@ -6151,6 +6316,7 @@ viewAnyway = "Vezi fișierul (politica încă se aplică)" [policyOption] autoRedactPhi = "Anonimizează automat PHI" blockExport = "Blochează exportul" +ccpa = "CCPA" contracts = "Contracte" documents = "Documente" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plan Editor" subscribed = "Plan Processor" unlinked = "Neconectat" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap din document" -sectionsAriaLabel = "Secțiuni Agent Builder" -selectorAriaLabel = "Agenți" -subtitle = "Proiectează, testează și livrează agenții AI care clasifică, extrag din și rutează documentele tale. Definește scenarii, limitează accesul la instrumente, rulează un set de referință și publică o versiune." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Anulează" -dropzoneText = "Alege un document exemplu (PDF sau imagine)" -lead = "Plasează un document exemplu și vom propune scenarii și o schemă de extragere pe care o poți rafina. Nimic nu este publicat până nu îl revizuiești." -submit = "Bootstrap agent" -subtitle = "Inițializează un agent nou dintr-un fișier reprezentativ" -title = "Bootstrap dintr-un document" - -[portal.agentBuilder.empty] -description = "Fă bootstrap unui agent dintr-un document exemplu pentru a-i inițializa scenariile și schema de extragere, apoi rafinează și publică." -title = "Încă nu există agenți" - -[portal.agentBuilder.evals] -casesPassing = "Cazuri reușite" -columnCase = "Caz eval" -columnLatency = "Latență" -columnResult = "Rezultat" -fail = "eșec" -goldenSetPassRate = "Rată de reușită set de referință" -latencyMs = "{{ms}} ms" -notRun = "nerulat" -pass = "reușit" -passRate = "Rată de reușită" -runEvals = "Rulează evals" - -[portal.agentBuilder.evals.empty] -description = "Evals îți transformă scenariile într-un set de referință repetabil. Fă upgrade pentru a captura rata de reușită în timp și a condiționa publicările de aceasta." -title = "Încă nu există set de referință" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "în seturile de referință" -activeAgents = "Agenți activi" -avgPassRate = "Rată medie de reușită eval" -fleetWide = "la nivelul întregii flote" -latestPublished = "Ultima publicare" -scenarios = "Scenarii" -testCases = "cazuri de test" -totalDescription_one = "{{count}} în total" -totalDescription_other = "{{count}} în total" - -[portal.agentBuilder.scenarios] -add = "Adaugă" -addScenario = "Adaugă scenariu" -enable = "Activează" -expectationLabel = "Comportament așteptat" -expectationPlaceholder = "Ce ar trebui să facă agentul" -inEval = "în eval" -mute = "Dezactivează sunetul" -muted = "fără sunet" -nameLabel = "Nume" -namePlaceholder = "de ex. Escaladare conformitate" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarii" -tools = "Instrumente" -versions = "Versiuni" - -[portal.agentBuilder.tools] -broadAccess = "Acces larg" -deniedHint = "Instrumentele selectate sunt blocate. Toate celelalte rămân apelabile." -deniedTools = "Instrumente refuzate" -governanceGate = "Guvernanța instrumentelor este disponibilă în planul Enterprise." -restricted = "Restricționat" -restrictedAccess = "Acces restricționat la instrumente" -restrictedDescription = "Permite fiecare instrument, cu excepția celor pe care le refuzi mai jos." - -[portal.agentBuilder.versions] -current = "curentă" -historyGate = "Istoricul complet al versiunilor și revenirea sunt disponibile în planul Enterprise." -publish = "Publică" -rollBack = "Revino" - [portal.assistant] close = "Închide asistentul" error = "Nu s-a putut contacta asistentul." @@ -6365,9 +6451,21 @@ typing = "Tastează" loading = "Se încarcă" redirectingToEditor = "Se redirecționează către editor..." +[portal.billing.activation] +maybeLater = "Poate mai târziu" +title = "Activați Processor" + +[portal.billing.activation.payg] +sub = "Setați o limită lunară. Plătiți pentru ceea ce rulați." +title = "Plătiți pe măsură ce utilizați" + +[portal.billing.activation.prepay] +badge = "2 luni gratuite" +sub = "O singură factură, card sau transfer bancar." +title = "Plătiți un an în avans" + [portal.billing.checkout] noClientSecret = "Funcția Edge nu a returnat client_secret." -subtitle = "Adaugă un card pentru a continua după alocarea gratuită a planului Editor. Stripe se ocupă de restul." title = "Activează planul Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Plata ta a reușit, dar activarea durează puțin mai mult decât de obi close = "Închide" title = "Aproape gata" +[portal.billing.checkout.cap] +amountAria = "Limită lunară de cheltuieli" +back = "Înapoi" +continue = "Continuați către plată" +error = "Nu s-a putut seta limita de cheltuieli" +finePrint = "Facturile se emit lunar. Anulați oricând." +noLimit = "Fără limită" +note = "Nu sunteți facturat niciodată peste limita dvs. Procesarea doar se întrerupe." +perMonth = "/ lună" +title = "Setați limita de cheltuieli" + +[portal.billing.checkout.card] +fields = "Număr card · LL / AA · CVC · ZIP" +label = "Detalii card" +note = "Detaliile cardului sunt colectate de Stripe. Stirling nu stochează niciodată PAN sau CVC." + [portal.billing.checkout.error] title = "Nu s-a putut porni checkout-ul" @@ -6383,10 +6497,9 @@ body = "Plata ta a fost procesată. Activăm procesarea contorizată pentru inst hint = "Te rugăm să păstrezi această fereastră deschisă." title = "Se activează planul Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "în portal env pentru a activa checkout-ul în aplicație." -bodyBefore = "Setează" -title = "Stripe nu este configurat" +[portal.billing.checkout.pay] +limit = "Limită lunară de cheltuieli {{amount}}/lună. Procesarea se întrerupe la atingerea limitei." +limitNoCap = "Fără limită de cheltuieli. Procesarea nu se va întrerupe. Facturile se emit lunar." [portal.billing.enterpriseUpsell] cta = "Creează oferta ta Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Plan curent" freeForever = "Gratuit pentru totdeauna" noTeamResolved = "Încă nu este rezolvată nicio echipă în portofelul tău — reîmprospătează și încearcă din nou." ownerOnly = "Doar proprietarul echipei poate activa planul Processor." +payInvoice = "Plătiți factura pentru finalizare" planName = "Editor" ssoIncluded = "SSO inclus" switchOnProcessor = "Activează Processor →" unlimitedUsers = "Utilizatori nelimitați" +viewQuote = "Vizualizați oferta" [portal.billing.invoices] columnAmount = "Sumă" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Solicitări API directe" segmentApiLabel = "API" segmentAutomationDesc = "Automatizări și pipelines" segmentAutomationLabel = "Automatizare" +sizeMultiplier = "{{formatted}} PDFs au folosit un multiplicator de dimensiune" +summary = "{{unique}} unice · {{units}} unități contor · {{avg}} medie per PDF" +summaryNoRate = "{{unique}} unice · {{units}} unități contor" unit = "PDF-uri contorizate" +unitsPending_one = "{{units}} unitate contor în așteptare pentru sincronizare din instanțe conectate" +unitsPending_other = "{{units}} unități contor în așteptare pentru sincronizare din instanțe conectate" + +[portal.billing.prepaid] +capSuffix = "din {{total}} credite plătite în avans" +expires = "Expiră {{date}}" +eyebrow = "Capacitate plătită în avans" +note = "Folosită înainte de facturarea contorizată și în afara limitei dvs. de cheltuieli." +topUp = "Reîncărcați" + +[portal.billing.prepaid.buy] +back = "Înapoi" +calcTitle = "Calculați plata anuală" +cancel = "Anulați" +cancelPurchase = "Anulați achiziția" +close = "Închideți" +continueToPayment = "Continuați către plată" +finalise = "Finalizați" +invoiceErrorTitle = "Nu s-a putut genera factura" +loadingTitle = "Se încarcă oferta" +payTitle = "Plătiți pentru anul dvs." +step = "Pasul {{current}} din {{total}}" + +[portal.billing.prepaid.calc] +change = "Schimbați" +deployRow = "Implementare" +done = "Gata" +enterpriseHint = "Aceasta este scară enterprise. La acest volum, tarifele enterprise depășesc orice reducere self-serve. Blocarea tarifului, termeni și o ofertă în câteva minute." +governanceRow = "Guvernanță" +governanceValue = "{{label}} · {{count}} politici" +handlesLabel = "Processor-ul dvs." +handlesValue = "gestionează {{volume}} credite / lună" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} politici" +poolCaption = "Un singur pool de {{credits}} credite pentru an. Lunile aglomerate împrumută de la cele ușoare." +priceLabel = "Anul dvs. · 12 luni la preț de 10" +rateUnknown = "Vom afișa prețul exact la checkout." +savings = "Economisiți {{amount}} · 2 luni gratuite." +sizingRow = "Dimensionare" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Total utilizatori" + +[portal.billing.prepaid.consent] +label = "Înțeleg că atunci când capacitatea mea plătită în avans este epuizată sau expiră după 12 luni, procesarea continuă automat la tariful standard contorizat pay-as-you-go (până la limita mea de cheltuieli), cu excepția cazului în care anulez, și că pot anula oricând din portalul de facturare." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Gestionat de Stirling. Activ în câteva minute." +cloudValue = "Stirling Cloud · Gestionat" +selfhost = "Self-hosted" +selfhostDesc = "Rulează pe infrastructură privată. Același tarif." +selfhostValue = "Self-hosted · Infrastructură privată" + +[portal.billing.prepaid.offer] +cta = "Revizuiți oferta" +subtitle = "Plătiți în avans un an de procesare PDF și primiți două luni gratuite, folosite înainte de facturarea contorizată, în afara limitei dvs. de cheltuieli." +title = "Obțineți 12 luni la preț de 10" + +[portal.billing.prepaid.pay] +accountName = "Numele titularului contului" +accountNamePlaceholder = "de ex. Jane Smith" +companyName = "Numele companiei (opțional)" +companyNamePlaceholder = "de ex. Acme Inc." +downloadInvoice = "Descărcați factura" +payOnline = "Plătiți online" +poNumber = "Număr PO (opțional)" +poNumberPlaceholder = "de ex. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avansat" +advancedDesc = "Pipelines gestionează cea mai mare parte a procesării dvs." +none = "Niciunul" +noneDesc = "Nu rulați încă pipelines. Activați-le oricând." +standard = "Standard" +standardDesc = "Câteva pipelines reprocesază PDFs sosite." + +[portal.billing.prepaid.posture] +essentials = "Esențiale" +essentialsDesc = "Clasificare, implicit, plus Partajare" +governed = "Guvernat" +governedDesc = "Adaugă Securitate și Rutare" +regulated = "Reglementat" +regulatedDesc = "Fiecare categorie, incl. Conformitate, Retenție, Ingestie" + +[portal.billing.prepaid.review] +download = "Descărcați oferta (PDF)" +downloading = "Se pregătește…" +share = "partajați pentru aprobare" + +[portal.billing.prepaid.size] +compact = "Compact" +compactDesc = "Fișiere sub 25 MB, fără taxe de date" +heavy = "Greu" +heavyDesc = "Scanat sau bogat în imagini, frecvent 50 MB+" +standard = "Standard" +standardDesc = "În principal mici, unele scanări peste 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Epuizată" +healthy = "Mai este suficient" +low = "Se epuizează" [portal.billing.spendLimit] adjustLimit = "Ajustează limita" @@ -6494,6 +6713,8 @@ label = "Se estimează că va fi depășit." [portal.billing.spendThisMonth] eyebrow = "Cheltuieli luna aceasta" +freeRemaining_one = "{{formatted}} PDF gratuit rămas" +freeRemaining_other = "{{formatted}} PDFs gratuite rămase" processed_one = "{{formattedCount}} PDF procesat." processed_other = "{{formattedCount}} PDF-uri procesate." processedWithRate_one = "{{formattedCount}} PDF procesat, la {{rate}} fiecare." @@ -6510,6 +6731,7 @@ reachedTitle = "Limita lunară de cheltuieli a fost atinsă" title = "Nu s-a putut deschide portalul Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "din {{allowance}} PDF-uri gratuite folosite" capSuffix_other = "din {{allowance}} PDF-uri gratuite folosite" eyebrow = "Perioadă de încercare Processor" @@ -6534,6 +6756,376 @@ signature = "semnătură" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Conexiune nouă" +createTitleFor = "Conectați {{name}}" +delete = "Ștergeți" +edit = "Editați" +editTitle = "Editați conexiunea" +unsupportedType = "Acest tip de conexiune nu poate fi editat aici." + +[portal.connections.categories.advanced] +description = "Pentru un serviciu pe care încă nu îl suportăm." +label = "Avansat" + +[portal.connections.categories.audit] +description = "Înregistrați ce s-a întâmplat cu un document, acolo unde auditorii dvs. se uită deja." +label = "Jurnalizare audit și conformitate" + +[portal.connections.categories.notify] +description = "Anunțați alt sistem că s-a întâmplat ceva." +label = "Notificare și flux de lucru" + +[portal.connections.categories.security] +description = "Scanați sau etichetați un document înainte să meargă mai departe." +label = "Securitate și clasificare" + +[portal.connections.categories.signing] +description = "Trimiteți un document pentru semnare sau notarizare." +label = "Semnătură electronică și notarizare" + +[portal.connections.categories.storage] +description = "Unde ajung documentele procesate - un bucket, un drive, un tichet sau o pagină." +label = "Fișier și atașare" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling stochează acest lucru criptat și nu îl mai afișează niciodată." +label = "Token de acces" + +[portal.connections.commonFields.apiKey] +label = "Cheie API" + +[portal.connections.commonFields.apiToken] +label = "Token API" + +[portal.connections.commonFields.baseUrl] +helperText = "Pașii care folosesc această conexiune pot apela doar căi sub acest URL, niciodată alt host." +label = "URL server" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Parolă" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Nume utilizator" + +[portal.connections.commonFields.webhookUrl] +helperText = "Lipiți URL-ul pe care vi l-a dat serviciul. Pot fi apelate doar căile de sub acesta." +label = "Webhook URL" + +[portal.connections.fields] +name = "Numele conexiunii" +namePlaceholder = "de ex. Producție" + +[portal.connections.picker] +cancel = "Anulați" +createNew = "Conexiune nouă..." +placeholder = "Selectați o conexiune" +save = "Salvați conexiunea" + +[portal.connections.picker2] +back = "Schimbați integrarea" +heading = "Ce doriți să conectați?" +noResultsBody = "Încă nu îl suportăm - spuneți-ne despre el și vom analiza adăugarea sa." +noResultsTitle = "Nimic nu corespunde \"{{query}}\"" +searchPlaceholder = "Căutați integrări - încercați \"sign\", \"ocr\" sau un nume de produs" + +[portal.connections.types.api] +description = "Apelați orice HTTP API dintr-o politică. Alegeți URL-ul, autentificarea și payload-ul." +label = "API personalizat" + +[portal.connections.types.api.fields.authType] +label = "Autentificare" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Nume utilizator și parolă" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token într-un header personalizat" + +[portal.connections.types.api.fields.authType.options.none] +label = "Niciuna" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Autentificare pentru un token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Pașii care folosesc această conexiune pot apela doar căi sub acest URL, niciodată alt host." +label = "URL de bază" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Nume header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Cale de autentificare" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Parolă" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Dacă API răspunde cu un link de descărcare, doar aceste host-uri pot fi preluate. Separați cu virgule. Lăsați necompletat pentru a permite doar host-ul propriu al URL-ului de bază." +label = "Host-uri de rezultat permise" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Trimiteți token-ul ca acest header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Header-ul de răspuns în care apelul de autentificare returnează token-ul, de ex. X-Auth-Token." +label = "Header răspuns token" + +[portal.connections.types.api.fields.username] +label = "Nume utilizator" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Scanați documentele pentru malware pe un server pe care îl rulați - nimic nu părăsește rețeaua dvs." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Scanare de viruși și conținut pentru documente într-o politică." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blocați documentele care conțin macrocomenzi, executabile, scripturi sau obiecte încorporate - nu doar malware cunoscut." +label = "Scanare avansată Cloudmersive" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Atașați documentele procesate la o pagină Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Trimiteți documente către ConsignO Cloud (Notarius) pentru semnătură electronică și notarizare." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Mediul dvs. ConsignO Cloud, incluzând /api/v1." +label = "URL server" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "Cheie API" + +[portal.connections.types.consigno.fields.clientSecret] +label = "Secret API" + +[portal.connections.types.consigno.fields.password] +label = "Parolă" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Necesar doar dacă organizația dvs. folosește tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Nume utilizator" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Postați un mesaj pe Discord când o politică procesează un document." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexați un eveniment de audit în Elasticsearch de fiecare dată când o politică gestionează un document." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Interogați un server FTP sau FTPS pentru documente noi." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Mod conexiune" + +[portal.connections.types.ftp.fields.passive.options] +active = "Activ" +passive = "Pasiv (recomandat)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP simplu trimite credențiale și fișiere necriptate; alegeți FTPS dacă serverul îl suportă." +label = "Criptare" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Niciuna (FTP simplu)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Postați un mesaj într-un spațiu Google Chat când rulează o politică." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Atașați documentele procesate la o problemă Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Trimiteți prin email un document sau o notificare când rulează o politică." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Încărcați documentele procesate în Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Parolă aplicație" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detectați date personale în textul documentului folosind un server Presidio pe care îl rulați." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Citiți și aplicați etichete de sensibilitate Microsoft Purview pe PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opțional. Necesar doar pentru a selecta etichete după nume; etichetarea funcționează fără acesta." +label = "ID aplicație (client)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Secret client" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Ștampilat pe fiecare etichetă ca SiteId, astfel încât instrumentele compatibile Purview să știe ce organizație a aplicat-o." +label = "ID director (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Un bucket S3 din care sursele pot citi și în care ieșirile pipeline pot scrie." +label = "Stocare S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID cheie de acces" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Lăsați necompletat pentru AWS. Setați acest câmp pentru MinIO sau altă stocare compatibilă S3." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Regiune" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Cheie secretă de acces" + +[portal.connections.types.sendgrid] +description = "Trimiteți prin email un document sau o notificare când rulează o politică." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Interogați un server SFTP (SSH) pentru documente noi." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opțional. Amprenta SHA-256 a cheii de host a serverului (din `ssh-keyscan host | ssh-keygen -lf -`). Când este setată, este acceptat doar un server care prezintă această cheie; când este necompletată, cheia văzută la prima conectare este fixată și modificările ulterioare sunt refuzate." +label = "Amprentă cheie host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Frază de acces cheie" + +[portal.connections.types.sftp.fields.password] +helperText = "Lăsați necompletat dacă vă autentificați cu o cheie privată în schimb." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opțional. Lipiți o cheie privată OpenSSH pentru autentificare cu cheie în loc de parolă." +label = "Cheie privată" + +[portal.connections.types.slack] +description = "Postați un mesaj pe Slack când o politică procesează un document." +label = "Slack" + +[portal.connections.types.smb] +description = "Monitorizați o partajare de rețea SMB / CIFS (Windows sau Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Partajare de rețea" + +[portal.connections.types.smb.fields.domain] +helperText = "Opțional. Domeniul sau grupul de lucru Windows, dacă serverul dvs. folosește unul." +label = "Domeniu" + +[portal.connections.types.smb.fields.share] +label = "Partajare" +placeholder = "documente" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Trimiteți un eveniment de audit către Splunk de fiecare dată când o politică gestionează un document." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Trimiteți un eveniment de audit către un colector HTTP Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Postați un mesaj într-un canal Teams când rulează o politică." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Postați fiecare document sau doar detaliile către un URL ales de dvs." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Declanșați un scenariu Zap sau Make dintr-o politică." +label = "Zapier & Make" + +[portal.docs] +browse = "Răsfoiți documentația" +viewSource = "Vizualizați sursa pe GitHub" + [portal.docs.authentication] codeCaption = "fiecare solicitare" eyebrow = "ÎNCEPUT" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Capabilități incluse, denumite, pe care agentul tău le invocă drept un singur instrument. Fiecare skill este un lanț determinist de operațiuni, cu evals atașate." title = "Skill-uri agent" +[portal.docs.toc] +title = "Pe această pagină" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "REFERINȚĂ API" @@ -6697,6 +7292,8 @@ type = "Tip" user = "Utilizator" [portal.documents.queue.empty] +connectSource = "Conectați o sursă" +createPipeline = "Creați un pipeline" description = "Pe măsură ce sursele trimit documente în pipeline-urile dvs., acestea vor apărea aici pentru revizuire." title = "Nu există documente în coadă" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Niciun document nu corespunde acestui filtru." rowActions = "Acțiuni rând" sensitiveLabel = "Sensibil" -sensitiveTitle = "Sensibil — acces necesar" [portal.documents.table.columns] action = "Pipeline / Acțiune" document = "Document" +labels = "Labels" product = "Produs" status = "Stare" time = "Timp" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Văzut ultima dată" region = "Regiune" status = "Stare" +target = "Target" version = "Versiune" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Această vizualizare a întâmpinat o eroare neașteptată. Înce retry = "Încercați din nou" title = "Ceva nu a mers bine pe această pagină" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Înapoi" +body = "Descărcați aplicația desktop sau găzduiți serverul self-hosted." +done = "Gata" +guide = "Deschideți ghidul {{name}}" +openInBrowser = "Deschideți în browser în schimb" +sectionDesktop = "Aplicație desktop" +sectionSelfHosted = "Self-hosted" +title = "Instalați Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Rulați imaginea oficială pe serverul dvs." +note = "Se deschide la :8080. Autentificare implicită: admin / stirling." +tagline = "Rulați serverul într-un container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instalați chart-ul Helm oficial." +note = "Adăugați un PVC pentru /configs pentru a păstra setările." +tagline = "Implementați într-un cluster cu Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Build-uri native pentru distribuția dvs." +guideBtn = "Vedeți opțiunile de instalare Linux" +note = "Build-uri AppImage, .deb și .rpm sunt disponibile pentru distribuțiile uzuale." +tagline = "Aplicație desktop nativă pentru distribuția dvs." +title = "Linux" + +[portal.home.download.mac] +altLabel = "Sau prin Homebrew" +detailBody = "Rulați instalatorul sau folosiți Homebrew." +downloadBtn = "Descărcați pentru macOS" +tagline = "Aplicație nativă pentru Apple silicon și Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Rulați fișierul .jar pe un server bare metal." +downloadBtn = "Descărcați fișierul .jar" +note = "Necesită Java 21+. Se deschide la :8080; autentificare implicită admin / stirling." +runLabel = "Apoi rulați-l" +tagline = "Rulați fișierul .jar pe un server bare metal" +title = "Configurare manuală server" + +[portal.home.download.windows] +altLabel = "Sau prin winget" +detailBody = "Rulați instalatorul sau folosiți un manager de pachete." +downloadBtn = "Descărcați instalatorul" +tagline = "Aplicație nativă pentru Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} activi" -invite = "Invitați colegi" +activeOfDeployed = "{{active}} din {{total}} active luna aceasta" name = "Stirling PDF Editor" open = "Deschideți în browser" updated = "actualizat {{time}}" +[portal.home.editor.deploy] +finish = "Finalizați implementarea" +options = "Vedeți opțiunile de implementare" +start = "Implementați Editorul" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Bună ziua" evening = "Bună seara" morning = "Bună dimineața" -[portal.home.onboarding] -dismiss = "Închideți configurarea" -notStarted = "Neînceput" -progress = "{{done}} din {{total}} finalizate" -title = "Finalizați configurarea" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC la nivel de organizație, prețuri pentru volum angajat și implementare air-gapped." -cta = "Începeți perioada de probă" -ctaQuote = "Obțineți ofertă" -lead = "Pentru 250+ angajați." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Implementați aplicația desktop, gratuită pentru totdeauna." -title = "Descărcați PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} active · {{recommended}} recomandate" -chip = "{{active}} active" -title = "Activați politicile" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} conectate · fiecare PDF guvernat acolo unde ajunge" -chip = "{{connected}} conectate" -title = "Conectați sursele" - -[portal.home.quickActions] -subtitle = "Sarcinile principale pentru astăzi" -title = "Acțiuni rapide" - -[portal.home.quickActions.buildPipeline] -blurb = "Composer în 3 pași peste biblioteca de operații tipizate" -title = "Construiți un pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenți, webhook-uri, foldere monitorizate" -title = "Conectați o sursă" - -[portal.home.quickActions.issueApiKey] -blurb = "Cheie cu domeniu limitat, limite de rată și listă de permisiuni IP" -title = "Emiteți o cheie API" - [portal.infrastructure] manageEditorDeployment = "Gestionați implementarea Editorului" sectionsAriaLabel = "Secțiuni de infrastructură" @@ -6878,13 +7541,9 @@ heading = "Chei API" subheading = "Credențiale cu domeniu limitat, limite de rată per cheie, permisiuni și liste de permisiuni IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IP-uri permise" -anyIp = "Orice IP (fără listă de permisiuni)" created = "Creat" lastUsed = "Utilizat ultima dată" -permissions = "Permisiuni" -rateLimit = "Limită de rată" -rateLimitValue = "{{value}} cereri/min" +revoke = "Revocați cheia" usageMonth = "Utilizare luna aceasta" usageToday = "Utilizare astăzi" @@ -6892,6 +7551,15 @@ usageToday = "Utilizare astăzi" description = "Creați o cheie cu domeniu limitat pentru a începe să apelați Stirling API." title = "Nu există încă chei API" +[portal.infrastructure.apiKeys.error] +load = "Nu s-au putut încărca cheile dvs. API. Încercați din nou." + +[portal.infrastructure.apiKeys.revoke] +body = "Revocați \"{{name}}\"? Orice integrare care încă folosește această cheie va începe imediat să primească erori 401. Această acțiune nu poate fi anulată." +cancel = "Anulați" +confirm = "Revocați cheia" +title = "Revocați cheia API" + [portal.infrastructure.audit] filterAriaLabel = "Filtrați evenimentele de audit după categorie" heading = "Jurnale de audit" @@ -6935,6 +7603,7 @@ all = "Toate" auth = "Autentificare" config = "Configurare" elevation = "Elevare" +policy = "Politică" processing = "Procesare" security = "Securitate" @@ -6944,19 +7613,30 @@ title = "Nu aveți acces" [portal.infrastructure.audit.metrics] config = "Configurare" -elevation = "Elevare" +policy = "Politică" processing = "Procesare" totalEvents = "Evenimente totale · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Config" +elevation = "Elevare" +policy = "Politică" +processing = "Procesare" +security = "Securitate" + +[portal.infrastructure.auditStatusLabel] +danger = "Eroare" +info = "Info" +success = "Succes" +warning = "Avertisment" + [portal.infrastructure.createKey] cancel = "Anulați" createKey = "Creați cheie" done = "Gata" -ipAllowlistHelper = "Intervale CIDR separate prin virgulă. Lăsați necompletat pentru a permite orice IP." -ipAllowlistLabel = "Listă de permisiuni IP" keyNameLabel = "Nume cheie" keyNamePlaceholder = "de ex. Producție · ingestie" -permissionsLabel = "Permisiuni" secretKeyCaption = "Cheie secretă" secretWarning = "Stocați aceasta într-un manager de secrete. Stirling stochează doar un hash — nu există nicio modalitate de a o recupera ulterior." subtitle = "Limitați cheia la minimul necesar. O puteți roti sau revoca oricând." @@ -6964,207 +7644,9 @@ subtitleCreated = "Copiați acest secret acum — nu va mai fi afișat." title = "Creați cheie API" titleCreated = "Cheie creată" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Implementat de" -environment = "Mediu" -product = "Produs" -status = "Stare" -version = "Versiune" -when = "Când" - -[portal.infrastructure.deployments.recent] -heading = "Implementări recente" -subheading = "Cele mai recente lansări pe produse și medii." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanțe" -latency = "Latență" -load = "Încărcare" -p99 = "P99" -region = "Regiune" -status = "Stare" -throughput = "Debit" -uptime = "Timp de funcționare" -version = "Versiune" - -[portal.infrastructure.deployments.regions] -heading = "Regiuni" -subheading = "Sănătate în timp real pentru fiecare regiune Stirling implementată — latență, încărcare și versiune de lansare." - -[portal.infrastructure.deployments.regions.empty] -description = "Regiunile implementate apar aici după ce spațiul dvs. de lucru este provisionat." -title = "Nu există regiuni implementate" - -[portal.infrastructure.models] -heading = "Modele" -msValue = "{{value}} ms" -subheading = "Catalogul de modele și rutarea care susțin procesarea documentelor în spațiul dvs. de lucru." - -[portal.infrastructure.models.byom] -description = "Înregistrați un model on-prem sau self-hosted și fixați-l într-o regiune pentru procesare cu rezidență a datelor." -title = "Aduceți propriul model" - -[portal.infrastructure.models.catalogue] -heading = "Catalog" -sub = "Modele gestionate disponibile pentru spațiul dvs. de lucru, cu latență și cost în timp real." -subEnterprise = "Modele gestionate, proprii și on-prem — cu fixare per regiune disponibilă." - -[portal.infrastructure.models.catalogue.empty] -description = "Modelele din catalogul spațiului dvs. de lucru apar aici." -title = "Nu există modele disponibile" - -[portal.infrastructure.models.columns] -cost = "Cost" -latency = "Latență" -load = "Încărcare" -model = "Model" -status = "Stare" -type = "Tip" -version = "Versiune" - -[portal.infrastructure.models.metrics] -activeModels = "Modele active" -avgLatency = "Latență medie" -included = "Inclus" -monthlySpend = "Cheltuială lunară pentru modele" - -[portal.infrastructure.models.routing] -empty = "Nu sunt configurate reguli de rutare." -heading = "Reguli de rutare" -sub = "Ce model gestionează fiecare operațiune. Valoarea implicită se aplică atunci când nu se potrivește nicio regulă mai restrânsă." -subLocked = "Rutați operațiunile către modele specifice — disponibil în planurile plătite." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Actualizați la Pro pentru a controla ce model gestionează fiecare operațiune și tip de document." -title = "Rutarea modelelor este o funcție plătită" - -[portal.infrastructure.models.routingColumns] -default = "Implicit" -docType = "Tip document" -modelForAria = "Model pentru {{operation}}" -operation = "Operațiune" -routedTo = "Rutat către" - -[portal.infrastructure.security.access.byok] -description = "Furnizați o cheie din propriul KMS. Stirling criptează cu ea, dar poate încă citi." -label = "Aduceți propria cheie (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Cheile nu părăsesc niciodată KMS-ul dvs. Stirling deține doar ciphertext." -label = "Păstrați propria cheie (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling gestionează cheile de criptare. Cel mai simplu — zero operațiuni cu chei din partea dvs." -label = "Chei deținute de Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Politică de acces la documente" -subheading = "Controlează cine poate decripta documentele procesate în repaus." - -[portal.infrastructure.security.attestations] -heading = "Atestări de conformitate" -noReport = "Niciun raport disponibil" -subheading = "Postură de audit cadru cu cadru, cu rapoarte disponibile pentru controalele atestate." -viewReport = "Vizualizați raportul →" - -[portal.infrastructure.security.compliance] -heading = "Conformitate" -subheading = "Atestări și certificări care acoperă platforma Stirling." - -[portal.infrastructure.security.empty] -description = "Configurația de securitate a spațiului dvs. de lucru va apărea aici." -title = "Postura de securitate indisponibilă" - -[portal.infrastructure.security.hyokBanner] -description = "Cu HYOK, cheile de criptare nu părăsesc niciodată KMS-ul dvs. Stirling stochează și procesează doar ciphertext pe care îl puteți revoca oricând." -title = "Stirling nu poate decripta documentele dvs." - -[portal.infrastructure.security.ipAllowlist] -empty = "Nu sunt configurate intervale IP — toate IP-urile sunt permise." -heading = "Listă de permisiuni IP" -sub = "Accesul API este restricționat la aceste intervale CIDR." -subLocked = "Restricționați accesul API la intervale IP cunoscute — disponibil în planurile plătite." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Actualizați la Pro pentru a restricționa accesul API la anumite rețele." -title = "Lista de permisiuni IP este o funcție plătită" - -[portal.infrastructure.security.ipColumns] -added = "Adăugat" -addedBy = "Adăugat de" -cidr = "CIDR" -label = "Etichetă" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritm" -heading = "Gestionarea cheilor de criptare" -keyId = "Identificator cheie" -lastRotated = "Rotită ultima dată" -rotateKey = "Rotiți cheia" -rotationPolicy = "Politică de rotație" -subheading = "Custodia cheilor care criptează documentele în repaus — cine poate decripta și cum se rotesc cheile." - -[portal.infrastructure.security.managedBanner] -description = "Custodia bring-your-own-key (BYOK) și hold-your-own-key (HYOK) este disponibilă în Enterprise. Actualizați pentru a furniza chei din propriul KMS." -title = "Cheile sunt gestionate de Stirling în planul dvs." - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asia Pacific" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · limită de date GDPR" -label = "Uniunea Europeană" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Statele Unite" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezidența datelor" -subheading = "Unde sunt stocate și procesate documentele." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} utilizat" - -[portal.infrastructure.storage.empty] -description = "Stocarea conectată și utilizarea apar aici." -title = "Nu este configurată nicio stocare" - -[portal.infrastructure.storage.lifecycle] -active = "Activ" -activeRange = "0–{{value}}d" -archived = "Arhivat" -coldStorage = "stocare rece" -deleted = "Șters" -never = "niciodată" -purged = "epurat" - -[portal.infrastructure.storage.providers] -connect = "Conectați" -connected = "Conectat" -heading = "Furnizori conectați" -subheading = "Unde sunt scrise artefactele procesate." - -[portal.infrastructure.storage.retention] -heading = "Păstrare" -subheading = "Cât timp sunt păstrate artefactele înainte de ștergerea prin ciclu de viață." -windowLabel = "Fereastră implicită de păstrare" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} zi" -days_other = "{{count}} zile" -never = "Nu ștergeți niciodată" - -[portal.infrastructure.storage.totalUsage] -heading = "Utilizare totală" -progressLabel = "Stocare utilizată" -subheading = "Stocare consumată pe toți furnizorii conectați." +[portal.infrastructure.keyLabel] +active = "Activă" +revoked = "Revocată" [portal.infrastructure.tabs] apiKeys = "Chei API" @@ -7174,6 +7656,49 @@ models = "Modele" security = "Securitate" storage = "Stocare" +[portal.integrations] +availableHeading = "Disponibile" +comingSoonHeading = "În curând" +connect = "Conectați" +connectionCount_one = "{{count}} conexiune" +connectionCount_other = "{{count}} conexiuni" +customApi = "API personalizat" +searchPlaceholder = "Căutați integrări" +subtitle = "Tot cu ce poate comunica Stirling. Credențialele sunt create o singură dată aici, apoi referențiate de surse, politici și pipeline-uri." +title = "Integrări" +typedName = "Nume {{tool}}" + +[portal.integrations.filters] +all = "Toate" +audit = "Audit" +connected = "Conectate" +notify = "Notificare" +security = "Securitate" +signing = "Semnare" +storage = "Stocare" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Conectat" + +[portal.integrations.table] +integration = "Integrare" +worksWith = "Funcționează cu" + +[portal.integrations.worksWith] +pipelines = "Pipeline-uri" +policies = "Politici" +sources = "Surse" + +[portal.legal] +draft = "{{label}} · ciornă" +loadError = "Nu s-a putut încărca acest document. Încercați din nou." +loading = "Se încarcă…" +title = "Document juridic" + [portal.nav] agent-builder = "Constructor de agenți" components = "Componente" @@ -7182,33 +7707,17 @@ documents = "Documente" editor = "Editor" home = "Acasă" infrastructure = "Infrastructură" +integrations = "Integrări" pipelines = "Pipeline-uri" policies = "Politici" procurement = "Achiziții" -settings = "Setări" sources = "Surse" usage = "Utilizare și facturare" users = "Utilizatori" -[portal.notifications] -markAllRead = "Marcați toate ca citite" -title = "Notificări" -viewAll = "Vizualizați toate" - -[portal.notifications.ariaLabel] -none = "Notificări, niciuna necitită" -unread_one = "Notificări, {{count}} necitită" -unread_other = "Notificări, {{count}} necitite" - -[portal.notifications.count] -allRead = "toate citite" -loading = "se încarcă" -new_one = "{{count}} nouă" -new_other = "{{count}} noi" - -[portal.notifications.empty] -description = "Nu există notificări noi." -title = "Sunteți la zi" +[portal.nav.section] +platform = "Platformă PDF" +processor = "Procesor PDF" [portal.pipelines] subtitle = "Fiecare pipeline automatizat de documente pe backend: un lanț ordonat de operațiuni peste un set de surse, rulat la un declanșator. Faceți clic pe un rând pentru pașii și sursele sale." @@ -7218,54 +7727,87 @@ title = "Pipeline-uri" newPipeline = "Pipeline nou" [portal.pipelines.builder] -addStep = "Adăugați instrument" +activate = "Activate" back = "Înapoi la pipeline-uri" +cannotFollow = "Nu poate prelua {{produced}}" +chooseAccount = "Alegeți un cont" +chooseDestination = "Alegeți o destinație" +chooseOperation = "Alegeți ce face acest pas" +chooseSource = "Alegeți o sursă" discard = "Renunțați la modificări" -enabled = "Activat" +inputs = "Intrare" +inputSource = "Sursă de intrare" +inputTrigger = "Declanșator" keepEditing = "Continuați editarea" -needsUpload = "Necesită un fișier încărcat" +moreActions = "More actions" +needsConfiguring = "Necesită configurare" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Niciun instrument nu corespunde căutării dvs." -pipelineSettings = "Setări pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Căutați instrumente" -selectToolBody = "Adăugați un instrument pentru a construi pipeline-ul." -selectToolTitle = "Nu există încă instrumente" -toolSettings = "Setări instrument" +sendToSystem = "Trimiteți către alt sistem" +stepsIncompatible = "Acești pași nu pot rula pe ceea ce produce pasul lor anterior: {{tools}}." +stepsNeedSetup = "Acești pași necesită încă configurare înainte de salvare: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Operațiune nerecunoscută, păstrată ca atare." unsavedBody = "Aveți modificări nesalvate. Salvați-le înainte de a pleca sau renunțați la ele?" unsavedTitle = "Modificări nesalvate" -uploadUnsupported = "Fișierele încărcate nu sunt încă acceptate în pipeline-uri, deci acești pași nu pot fi salvați: {{tools}}." usesDefaults = "Rulează cu setările implicite" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Combină fiecare fișier din pasul anterior" +fan-out = "Rulează o dată pentru fiecare fișier din pasul anterior" +format-mismatch = "Necesită {{accepts}}, dar pasul anterior produce {{produced}}" +output-uncertain = "Este posibil să nu ruleze: ieșirea pasului anterior depinde de modul în care este configurat" +source-mismatch = "Necesită {{accepts}}, dar intrarea acestui pipeline este {{produced}}" +undeclared-operation = "Nu se poate verifica ce acceptă acest pas" [portal.pipelines.composer] addTool = "Adăugați instrument" -cancel = "Anulați" -chainEmpty = "Adăugați un instrument pentru a începe să construiți pipeline-ul." create = "Creați pipeline" -directory = "Folder de ieșire" -directoryHelp = "Cale absolută pe server. Trebuie să fie în folderele permise configurate." +createPaused = "Create paused" editingUnsupported = "Afișarea acestor parametri ai instrumentului pentru editare nu este încă acceptată." -moveDown = "Mutați în jos" -moveUp = "Mutați în sus" +editSource = "Edit source" name = "Nume" namePlaceholder = "de ex. Scanare de redactare" -noSources = "Nu sunt conectate încă surse. Pipeline-ul poate rula în continuare pe fișiere furnizate direct către acesta." noToolSettings = "Acest instrument nu are setări configurabile." -operations_one = "Operațiune ({{count}})" -operations_other = "Operațiuni ({{count}})" output = "Ieșire" -removeStep = "Eliminați operațiunea" save = "Salvați modificările" scheduleEvery = "Rulați la fiecare" -sources = "Surse" -sourcesLoading = "Se încarcă sursele..." trigger = "Declanșator" triggerManual = "Doar manual" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "zile" hours = "ore" minutes = "minute" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Ștergeți „{{name}}”? Această acțiune nu poate fi anulată." cancel = "Anulați" @@ -7273,28 +7815,62 @@ confirm = "Ștergeți" title = "Ștergeți pipeline-ul?" [portal.pipelines.detail] +clearHistory = "Ștergeți istoricul" delete = "Ștergeți pipeline-ul" run = "Rulați acum" [portal.pipelines.empty] action = "Creați un pipeline" +connectSource = "Conectați o sursă" description = "Creați primul pipeline: alegeți sursele peste care rulează, înlănțuiți operațiunile și alegeți unde ajunge ieșirea." title = "Nu există încă pipeline-uri" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Active" paused = "În pauză" total = "Pipeline-uri" -[portal.pipelines.output] -folder = "Scrieți în folder" -inline = "Returnați fișiere" - [portal.pipelines.run] +allProcessed_one = "Nimic de rulat: {{count}} document al sursei a fost deja procesat." +allProcessed_other = "Nimic de rulat: toate cele {{count}} documente din surse au fost deja procesate." completed_one = "Rulare finalizată." completed_other = "Toate cele {{count}} rulări au fost finalizate." empty = "Nimic de rulat: sursele nu au avut documente de procesat." failed = "Rularea a eșuat: {{error}}" +inFlight = "Nimic nou de rulat: documentele sunt încă procesate dintr-o rulare anterioară." +parked_one = "Nimic de rulat: {{count}} document a eșuat anterior și este parcat. Remediați cauza, apoi ștergeți istoricul pentru a-l reîncerca." +parked_other = "Nimic de rulat: {{count}} documente au eșuat anterior și sunt parcate. Remediați cauza, apoi ștergeți istoricul pentru a le reîncerca." running = "Rularea a început; este încă în desfășurare." timeout = "Rularea durează mai mult decât era de așteptat; se poate finaliza în continuare în fundal." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Surse" status = "Stare" steps = "Pași" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Monitorizare folder" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "Programat" [portal.policies] +defaultName = "Politică {{category}}" subtitle = "Automatizări permanente care aplică un pipeline de instrumente fiecărui document. Fiecare politică se declanșează la încărcare sau export, rulează lanțul de instrumente și salvează versiunea aplicată alături de original." title = "Politici" [portal.policies.card] comingSoon = "Actualizați la Enterprise" notSetUp = "Neconfigurat" +requiresAiEngine = "Necesită motor AI" + +[portal.policies.categories.classification] +desc = "Identificați tipul fiecărui document la încărcare și etichetați-i metadatele pentru arhivare și căutare." +label = "Clasificare" [portal.policies.categories.compliance] desc = "Aplicați cerințele HIPAA, GDPR, SOC 2 sau FedRAMP fiecărui document." @@ -7344,6 +7927,13 @@ label = "Securitate" [portal.policies.config] scopeAll = "Toate documentele" +[portal.policies.config.classification] +summary = "Clasifică fiecare document încărcat în funcție de etichetele echipei dvs. și îi etichetează metadatele." + +[portal.policies.config.classification.rules] +0 = "Clasifică" +1 = "Etichetează metadatele" + [portal.policies.config.compliance] summary = "Validează documentele față de cadrele de reglementare înainte ca acestea să părăsească sistemul." @@ -7371,6 +7961,29 @@ minConfidence = "Încredere minimă" 2 = "Denumiți" 3 = "Normalizați" +[portal.policies.config.purview] +label = "Aplicați o etichetă de sensibilitate Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Tenant Purview" +labelId = "ID etichetă" +labelIdHelp = "GUID-ul etichetei, din portalul Purview. Aplicarea unei etichete scrie metadatele acesteia pe PDF; nu poate cripta, pentru asta este necesar clientul Microsoft." +labelName = "Nume etichetă (opțional)" +labelNameHelp = "Înregistrat alături de ID, astfel încât oamenii și instrumentele să poată recunoaște eticheta." + +[portal.policies.config.purview.fields.method] +help = "Purview distinge o etichetă aplicată de o politică de una aleasă de o persoană." +label = "Cum este înregistrată eticheta" +privileged = "Ca aleasă de o persoană" +standard = "Ca aplicată automat" + +[portal.policies.config.purviewRead] +label = "Citiți o etichetă Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Tenant Purview" +connectionHelp = "Citește eticheta pe care documentul o poartă deja, astfel încât pașii următori să poată acționa în funcție de ea." + [portal.policies.config.retention] summary = "Aplică durata de păstrare a documentelor, momentul arhivării și momentul ștergerii." @@ -7438,6 +8051,7 @@ title = "Încă nu există activitate" [portal.policies.endpoints] addWatermark = "Filigran" autoRedact = "Redactează PII" +classifyAndLabel = "Clasifică" compressPdf = "Comprimă" flatten = "Aplatizează" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Politicile tale sunt salvate și vor apărea după restabilirea c retry = "Reîncearcă" title = "Backend indisponibil" +[portal.policies.operations] +change = "Schimbați ce face acest pas" +noResults = "Niciun pas nu corespunde. Încercați un nume de produs sau \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Căutați pași - încercați \"scan\", \"attach\" sau un nume de produs" + +[portal.policies.operations.bodyMode] +binary = "Octeți bruti ai fișierului" +json = "În interiorul unui payload JSON" +multipart = "Ca încărcare de fișier (multipart)" + +[portal.policies.operations.clamavScan] +description = "Propriul dvs. server ClamAV verifică documentul; nimic nu părăsește rețeaua dvs." +label = "Scanați pentru viruși (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Respinge documentele care conțin macrocomenzi, executabile, scripturi sau obiecte încorporate." +label = "Blocați conținutul activ" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive verifică documentul, iar rularea se oprește dacă acesta nu este curat." +label = "Scanați pentru viruși" + +[portal.policies.operations.confluenceAttach] +description = "Arhivează documentul procesat pe o pagină." +label = "Atașați la o pagină Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Trimite documentul către ConsignO Cloud pentru semnare." +label = "Trimiteți pentru semnare (ConsignO)" +note = "Trimite documentul. Preluarea copiei semnate nu este încă acceptată, deoarece un pas nu poate transmite un rezultat unui pas ulterior." + +[portal.policies.operations.customApiCall] +description = "Construiți apelul singur: cale, metodă, corp și gestionarea răspunsului." +label = "Apelați un API personalizat" + +[portal.policies.operations.discordNotify] +description = "Anunță un canal că politica a gestionat un document." +label = "Postați un mesaj pe Discord" + +[portal.policies.operations.elasticIndex] +description = "Înregistrează ce a făcut politica sub forma unui document ce poate fi căutat." +label = "Indexați un eveniment de audit în Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart se potrivește API-urilor de încărcare; JSON se potrivește API-urilor care vor fișierul într-un payload; binary trimite octeții bruti." +label = "Cum se trimite documentul" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Un corp JSON trimis ca atare. {{document.base64}} conține fișierul; {{document.*}} și {{run.*}} sunt completate pentru fiecare document." +label = "Corp JSON" + +[portal.policies.operations.fields.connection] +label = "Cont" + +[portal.policies.operations.fields.domain] +label = "Domeniu Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Câmpul de formular sub care API așteaptă documentul." +label = "Numele câmpului de fișier" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "De la" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Un obiect JSON cu perechi nume/valoare pentru anteturi. Credențiala conexiunii este adăugată automat." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Cheie problemă" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Trimis ca mesaj. {{document.*}} și {{run.*}} sunt completate pentru fiecare document." +label = "Mesaj" + +[portal.policies.operations.fields.method] +label = "Metodă" + +[portal.policies.operations.fields.pageId] +label = "ID pagină" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Adăugat la URL-ul de bază al conexiunii. Nu poate ajunge niciodată la alt host." +label = "Cale" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Locul din drive unde se scrie. {{document.filename}} este completat pentru fiecare document." +label = "Cale destinație" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Raportul lasă documentul neatins. Înlocuirea îl schimbă cu orice se întoarce." +label = "Ce se face cu răspunsul" + +[portal.policies.operations.fields.signerEmail] +label = "Email semnatar" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Subiect" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizează acest text. Folosiți {{document.title}} sau lipiți textul pe care doriți să îl verificați." +label = "Text de verificat" + +[portal.policies.operations.fields.to] +label = "Către" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nume utilizator Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Anunță un spațiu că politica a gestionat un document." +label = "Postați un mesaj pe Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arhivează documentul procesat pe o problemă." +label = "Atașați la o problemă Jira" + +[portal.policies.operations.mailgunEmail] +description = "Trimite documentul procesat ca atașament." +label = "Trimiteți documentul prin email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Scrie documentul procesat într-un folder." +label = "Încărcați în Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Propriul dvs. server Presidio raportează datele personale pe care le găsește." +label = "Detectați date personale" +note = "Presidio citește text, nu fișiere. Dați-i textul pe care doriți să îl verificați — documentul în sine nu este încărcat." + +[portal.policies.operations.responseMode] +replace = "Înlocuiți documentul cu răspunsul" +report = "Păstrați documentul, înregistrați răspunsul" + +[portal.policies.operations.sendgridEmail] +description = "Trimite documentul procesat ca atașament." +label = "Trimiteți documentul prin email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Anunță un canal că politica a gestionat un document." +label = "Postați un mesaj pe Slack" + +[portal.policies.operations.splunkEvent] +description = "Înregistrează ce a făcut politica acolo unde auditorii dvs. caută deja." +label = "Trimiteți un eveniment de audit către Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Înregistrează ce a făcut politica într-un colector HTTP." +label = "Trimiteți un eveniment de audit către Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Anunță un canal că politica a gestionat un document." +label = "Postați un mesaj pe Teams" + +[portal.policies.operations.webhookPost] +description = "Trimite documentul în sine către un URL ales de dvs." +label = "Postați documentul către un webhook" + +[portal.policies.operations.zapierNotify] +description = "Predă detaliile rulării automatizării dvs." +label = "Declanșați un scenariu Zap sau Make" + [portal.policies.stats] activeFor = "Activ" dataProcessed = "Date procesate" @@ -7473,6 +8282,13 @@ label = "Documente aplicate" description = "Configurate, dar nu se declanșează" label = "În pauză" +[portal.policies.table] +appliesTo = "Se aplică la" +docs = "Documente aplicate" +enforces = "Aplică" +policy = "Politică" +status = "Stare" + [portal.policies.wizard.actions] back = "Înapoi" cancel = "Anulează" @@ -7480,6 +8296,10 @@ continue = "Continuă" enablePolicy = "Activează politica" saveChanges = "Salvează modificările" +[portal.policies.wizard.capability.classify] +desc = "Identifică tipul documentului din etichetele echipei dvs. și îl etichetează, astfel încât să fie arhivat și căutat după categorie." +label = "Clasificați documentul" + [portal.policies.wizard.capability.compress] desc = "Comprimă documentul la o dimensiune mai mică a fișierului." label = "Reduce dimensiunea fișierului" @@ -7500,10 +8320,18 @@ label = "Redactează informațiile sensibile" desc = "Elimină JavaScript ascuns, astfel încât nimic să nu poată rula automat când documentul este deschis." label = "Elimină conținutul activ" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dovedește că documentul a existat în această formă exactă la un moment dat, folosind o autoritate independentă de timestamp. Se trimite doar un hash - documentul nu părăsește niciodată serverul dvs." +label = "Adăugați un timestamp de încredere" + [portal.policies.wizard.capability.watermark] desc = "Aplică un marcaj vizibil (de ex. „Confidențial”) pe fiecare pagină." label = "Aplică un filigran" +[portal.policies.wizard.classification] +description = "Fiecare document încărcat este clasificat în funcție de etichetele încorporate și etichetat cu tipurile potrivite. Setul de etichete este partajat în întreaga echipă." +labelsHeading = "Etichete de clasificare" + [portal.policies.wizard.errors] noTools = "Activează mai întâi cel puțin un instrument în fluxul de lucru." saveFailed = "Politica nu a putut fi salvată. Încearcă din nou." @@ -7548,73 +8376,118 @@ setUp = "Configurează politica {{category}}" [portal.policies.wizard.workflow] description = "Alege ce face această politică fiecărui document pe care îl procesează." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} active" -noRule = "Încă nu se aplică nicio regulă" -subtitle = "Automatizări permanente prin care trece fiecare document, indiferent de pipeline-ul care îl gestionează." -title = "Ce rulează pe PDF-urile tale" - [portal.policySummary.action] -comingSoon = "Fă upgrade la Enterprise" -configure = "Configurează" setUp = "Configurează" -[portal.policySummary.column] -activeRule = "Regulă activă" -policy = "Politică" -status = "Stare" +[portal.processorFlow] +footnote = "Numărătorile sunt pentru ultimele 24 de ore. Viteza fluxului este ilustrativă." +liveBadge = "Live" +stats = "{{connected}} conectate · {{processed}} PDF-uri procesate" +title = "Procesor PDF" -[portal.policySummary.empty] -description = "După configurarea politicilor, categoriile apar aici." -title = "Încă nu există politici" +[portal.processorFlow.lens] +ariaLabel = "Mod de vizualizare" +flow = "Flux" +sankey = "Sankey" -[portal.policySummary.state] -active = "Activ" -off = "Oprit" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Eșuate" +heading = "Rezultate" +success = "Livrate" + +[portal.processorFlow.policies] +activeCount = "{{n}} active" +count = "{{n}} · 24h" +heading = "Politici" +setUp = "Configurați" soon = "În curând" -[portal.processingStatus] -managePlan = "Gestionează planul" -volumeSuffix = "PDF-uri procesate · ultimele 30 de zile" +[portal.processorFlow.sankey] +waist = "Politici · {{n}} active" + +[portal.processorFlow.sankey.empty] +description = "Conectați o sursă și activați o politică pentru a urmări documentele cum trec prin procesor." +title = "Încă nu există flux" + +[portal.processorFlow.sources] +comingSoonTag = "Conectați" +editor = "Stirling PDF Editor" +heading = "Surse" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Preluare email" [portal.procurement] -reset = "Resetează achizițiile (demo)" subtitle = "Parcurge evaluarea, contractarea și integrarea echipei tale. Fiecare document într-un singur loc." title = "Achiziții" -[portal.procurement.action] -download = "Descarcă" -pay = "Plătește acum" -request = "Solicită" -sign = "Revizuiește și semnează" -upload = "Încarcă" - [portal.procurement.agreement] agreeCta = "Acceptă și abonează-te" +confidential = "Confidențial" confirm = "Am citit și sunt de acord cu Stirling Enterprise Agreement." -eyebrow = "Acord" -intro = "Un acord combinat acoperă tranzacția ta: Master Service Agreement, Order Form, EULA și Data Processing Agreement. Revizuiește-l, apoi acceptă pentru a transforma oferta într-un abonament angajat." -title = "Revizuiește acordul enterprise" +docName = "Acord Stirling Enterprise" +docSub = "MSA, Formular de comandă, EULA și DPA, combinate într-o singură semnătură." +download = "Descărcați" +downloadDraftError = "Nu s-a putut genera acordul PDF - rendererul de documente nu este disponibil. Încercați din nou în scurt timp sau contactați suportul." +downloadError = "Nu s-a putut descărca acordul semnat. Încercați din nou." +legalName = "Numele entității juridice" +legalNamePlaceholder = "Entitatea juridică ce va semna" +loadError = "Nu s-a putut încărca acordul. Încercați din nou." +loading = "Se încarcă acordul..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Solicitați modificări" +scrollHint = "Derulați pentru a revizui" +signatory = "Nume semnatar" +signatoryPlaceholder = "Nume complet" +signatoryTitle = "Titlu semnatar" +signatoryTitlePlaceholder = "de ex. Consilier juridic general" +signError = "Nu s-a putut înregistra semnătura dvs. Încercați din nou." [portal.procurement.builder] addons = "Suplimente" +addressLine1 = "Linia de adresă 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Linia de adresă 2" +addressLine2Placeholder = "Suită, etaj, clădire" back = "Înapoi" businessName = "Numele firmei" businessNamePlaceholder = "Compania ta" +city = "Oraș" +cityPlaceholder = "San Francisco" +completeRequired = "Completați câmpurile obligatorii (marcate cu *) cu un email valid înainte de a genera oferta." +contactEmail = "Email contact" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Nume contact" +contactNamePlaceholder = "Jane Doe" continue = "Continuă" -country = "Țară" -countryEuro = "Zona euro (EUR €)" -countryUK = "Regatul Unit (GBP £)" -countryUS = "Statele Unite (USD $)" +done = "Gata" eula = "Am citit și sunt de acord cu Stirling Enterprise EULA. Aceasta guvernează acordul generat din această ofertă." generate = "Generează oferta" -included = "Inclus" indemnification = "Despăgubire IP" indemnificationSub = "Apărăm revendicările IP eligibile, conform EULA" -offlineLicense = "Licență offline / izolată de rețea" -offlineLicenseSub = "Un fișier de licență descărcabil pentru o instanță self-hosted izolată de rețea" +paperEyebrow = "Ofertă Enterprise" +paperFor = "Pregătită pentru" +pdfSize = "Dimensiune PDF" +poNumber = "Număr PO" +poNumberPlaceholder = "Opțional" +postalCode = "Cod poștal" +postalCodePlaceholder = "94105" +posture = "Guvernanță" +posture_count = "~{{count}} politici" +posture_essentials = "Esențiale" +posture_essentialsSub = "Clasificare + Partajare — valorile implicite" +posture_governed = "Guvernat" +posture_governedSub = "Adaugă Securitate și Rutare" +posture_regulated = "Reglementat" +posture_regulatedSub = "Fiecare categorie — Conformitate, Retenție, Ingestie" qbr = "Evaluări trimestriale de business" qbrSub = "SE-ul tău revizuiește utilizarea și foaia de parcurs în fiecare trimestru" +region = "Stat / regiune" +regionPlaceholder = "California" running = "{{annual}} / an · {{years}} ani {{tcv}}" s1Sub = "Echipa ta și PDF-urile pe care te aștepți să le rulezi în fiecare an." s1Title = "Cât vei procesa?" @@ -7623,6 +8496,12 @@ s2Title = "Angajament și serviciu" s3Sub = "Pentru oferta și acordul pe care îl generează." s3Title = "Detaliile tale" serviceLevel = "Nivel de serviciu" +size_compact = "Compact" +size_compactSub = "În principal text, sub 1 MB" +size_heavy = "Greu" +size_heavySub = "Scanat sau bogat în imagini, 5 MB+" +size_standard = "Standard" +size_standardSub = "Text și imagini mixte, 1 până la 5 MB" slDedicated = "Dedicat" slDedicatedSub = "4 ore lucrătoare · manager de cont dedicat · +30%" slPriority = "Prioritar" @@ -7630,6 +8509,8 @@ slPrioritySub = "Aceeași zi lucrătoare · CSM nominalizat · +15%" slStandard = "Standard" slStandardSub = "Următoarea zi lucrătoare · CSM partajat · inclus" stepOf = "Pasul {{n}} din {{total}}" +taxId = "TVA / ID fiscal" +taxIdPlaceholder = "Opțional" term = "Termen" termDiscount = "Reducere de {{pct}}% aplicată pentru angajament pe mai mulți ani" title = "Construiește oferta" @@ -7637,6 +8518,7 @@ training = "Onboarding și instruire" trainingSub = "Sesiuni live pentru a-ți pune echipa în mișcare" users = "Total utilizatori" usersPlaceholder = "de ex. 250" +viewEula = "Citiți-l" volEstimated = "Estimat din {{count}} utilizatori (~2.000 PDF-uri fiecare, inclusiv automatizarea). Editează dacă știi mai bine." volManual = "Se folosește valoarea ta. Reestimează din dimensiunea echipei oricând." volNoUsers = "Nu ești sigur? Introdu dimensiunea echipei și o vom estima." @@ -7645,55 +8527,55 @@ volumePlaceholder = "de ex. 1.000.000" years_one = "{{count}} an" years_other = "{{count}} ani" -[portal.procurement.docs] -count_one = "{{count}} doc." -count_other = "{{count}} doc." -done = "Gata" -here = "Ești aici" -hide = "Ascunde" -optional = "Opțional" -paidAddon = "Supliment plătit" -show = "Afișează" -subtitle = "Tot ce ai nevoie la fiecare pas al parcursului, afișat pe măsură ce tranzacția avansează." -supportingSubtitle = "SOC 2, evaluări de securitate, formulare fiscale și altele, pregătite când echipa ta de securitate sau achiziții le cere. Unele au o taxă unică." -supportingTitle = "Susținerea evaluării tale" +[portal.procurement.documents] +agreement = "Acord Enterprise" +agreementSub = "MSA, Formular de comandă și DPA într-o singură semnătură." +download = "Descărcați" +eula = "EULA și Termeni comerciali" +eulaSub = "Termenii software și comerciali self-serve." +invoice = "Factură" +invoiceSub = "Prima dvs. factură de abonament." +laterInvoice = "Disponibilă după ce acceptați" +laterQuote = "Disponibilă după emiterea ofertei" +quote = "Ofertă" +quoteSub = "Oferta dvs. Enterprise detaliată." +sla = "Anexă SLA" +slaSub = "Niveluri de suport și obiective de răspuns." +subprocessors = "Subprocesatori" +subprocessorsSub = "Terți care procesează date în numele dvs." +subtitle = "Documentele tranzacției dvs., disponibile oricând." title = "Documente" -upcoming = "Urmează" +view = "Vizualizați" [portal.procurement.error] title = "Ceva nu a mers bine" [portal.procurement.hero] -company = "Tranzacția ta enterprise" +barAria = "Etapa {{current}} din {{total}}" ctaAgreement = "Revizuiește și semnează acordul" -ctaLive = "Ești activ" -ctaPayment = "Adaugă plata" +ctaExploring = "Configurați perioada de probă" ctaQuote = "Revizuiește oferta" +ctaReviewQuote = "Revizuiți oferta" ctaTrial = "Construiește oferta" +documents = "Documente" eyebrow = "Achiziții enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Invită coechipieri" -keyDocs = "Documente cheie" -nextStep = "Pasul următor: {{action}}" -notStarted = "Neînceput" +licenseKey = "Cheie de licență" +liveSub = "Organizația dvs. este provisionată și licența dvs. este activă." +liveTitle = "Sunteți live pe Stirling Enterprise" +next = "Următorul: {{stage}}" open = "Deschide achizițiile" scheduleCall = "Programează un apel" -setup1Sub = "Invită-ți coechipierii" -setup1Title = "Implementează PDF Editor" -setup2Sub = "Activează procesarea în editori și alte surse" -setup2Title = "Conectează PDF Processor" -setup3Sub = "Activează Security, Compliance, Routing sau Retention când ai nevoie de ele" -setup3Title = "Adaugă politici recomandate" +sentenceAgreement = "revizuiți și semnați acordul" +sentenceLive = "provisionare și lansare" +sentencePayment = "ordin de achiziție și plată" +sentenceQuote = "revizuiți și acceptați oferta" +sentenceTrial = "configurați spațiul de lucru de probă" [portal.procurement.journey] daysLeft_one = "{{count}} zi rămasă" daysLeft_other = "{{count}} zile rămase" -engineerLabel = "Inginerul tău de soluții" -eyebrow = "Lansarea ta" -live = "Ești activ pe Stirling Enterprise" -nextStep = "Pasul următor: {{action}}" -subtitle = "Inginerul tău de soluții este prezent la fiecare pas. O singură acțiune următoare pe rând; lista completă este mai jos." -title = "De la trial la activ, un parcurs ghidat" -trialTitle = "Trial Enterprise" [portal.procurement.journeySteps.agreement] blurb = "O singură semnătură acoperă MSA, formularul de comandă, EULA și DPA." @@ -7727,6 +8609,9 @@ downloadError = "Fișierul de licență offline nu a putut fi generat încă — downloadOffline = "Descarcă licența offline (.lic)" hint = "Lipește această cheie într-o instanță self-hosted pentru a o activa sau păstreaz-o pentru evidențele tale. Păstreaz-o în siguranță." label = "Cheia ta de licență" +subtitle = "Activați o instanță self-hosted cu această cheie sau păstrați-o pentru evidențele dvs." +title = "Cheia dvs. de licență" +trialFileHint = "Acesta este fișierul dvs. de licență de probă. După ce acordul este în vigoare, reveniți și descărcați-l din nou — spre deosebire de cheia de licență online, fișierul .lic nu se actualizează singur." [portal.procurement.link] cta = "Conectează contul" @@ -7739,65 +8624,62 @@ description = "Abonamentul tău este activ și licența ta a fost emisă. Echipa eyebrow = "Activ" title = "Ești activ pe Stirling Enterprise" -[portal.procurement.locked] -description = "Cheile de trial, ofertele pentru volum angajat, acordul cu o singură semnătură, plata și registrul documentelor tale se află toate aici după ce începi o evaluare enterprise." -eyebrow = "Doar Enterprise" -talkToSales = "Discută cu vânzările" -title = "Traseul de achiziții se deschide cu Enterprise" - [portal.procurement.milestone] -accept = "Acceptă și continuă" -description = "Descarcă PDF-ul pentru a-l partaja cu echipa ta, revino să accepți când ești pregătit sau fă modificări." -download = "Descarcă PDF" downloadError = "Oferta PDF nu a putut fi descărcată încă — încearcă din nou peste câteva momente." -edit = "Editează oferta" -eyebrow = "Oferta {{number}}" -perYear = " / an" -preparedFor = "Pregătită pentru {{company}}" -tcv = "{{value}} valoare totală a contractului" -title = "Oferta ta este gata" - -[portal.procurement.modal] -cancel = "Anulează" -chooseFile = "Alege fișierul" -close = "Închide" -downloadBody = "Descărcarea ta va începe în scurt timp." -downloadCta = "Descarcă" -downloadTitle = "Descarcă" -noFile = "Niciun fișier selectat" -payBody = "Plătește contractul angajat cu cardul sau prin transfer bancar prin Stripe. Spațiul tău de lucru se provisionează imediat ce plata este confirmată." -payCta = "Continuă către Stripe" -payTitle = "Confirmă plata" -requestBodyFree = "Generăm acest document la cerere. Confirmă, iar inginerul tău de soluții îl va trimite în scurt timp." -requestBodyPaid = "Acesta este un supliment plătit. Confirmă, iar inginerul tău de soluții îl va dimensiona și va trimite documentele." -requestCta = "Solicită" -requestTitle = "Solicită acest document" -signBody = "Deschide Stirling Enterprise Agreement pentru semnătură electronică: o singură semnătură acoperă MSA, formularul de comandă, EULA și DPA. Contrasemnăm automat și treci la plată." -signCta = "Deschide pentru semnare" -signTitle = "Revizuiește și semnează acordul" -uploadBody = "Trimite-ne PO-ul tău și facturăm în baza lui, în termenii tăi. Trage PDF-ul aici sau alege un fișier mai jos." -uploadCta = "Încarcă comanda de achiziție" -uploadTitle = "Încarcă comanda ta de achiziție" [portal.procurement.payment] description = "Oferta ta este acceptată și licența este deja activă — echipa ta poate începe imediat. Plătește prima factură când ești pregătit; o poți plăti sau descărca aici, fără email." +downloadAgreement = "Descărcați acordul semnat" downloadInvoice = "Descarcă factura" -simulate = "Simulează plata primită (demo)" +eyebrow = "Plată" title = "Abonament creat" viewInvoice = "Vezi și plătește factura" +[portal.procurement.review] +acceptCta = "Acceptați oferta" +annual = "Taxă anuală (anul 1)" +downloadCta = "Descărcați oferta" +poNumber = "Ordin de achiziție: {{po}}" +renewal = "Se reînnoiește la {{amount}}/an după termen (+{{pct}}% CPI)" +tcv = "Termen de {{years}} ani, plătit integral în avans: {{tcv}}" +validUntil = "Valabil până la {{date}}" + [portal.procurement.schedule] fallback = "Programatorul nu a putut fi încărcat." fallbackLink = "Deschide programarea într-o filă nouă" +loading = "Se încarcă programatorul..." subtitle = "Inginerul tău de soluții îți va ghida echipa prin lansare. Alege o oră care ți se potrivește." title = "Programează un apel" -[portal.procurement.status] -action = "Acțiune necesară" -available = "Disponibil" -complete = "Complet" -pending = "În așteptare" -request = "La cerere" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Sigilat, fără ieșire" +back = "Înapoi" +businessName = "Nume companie" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Gestionat, cel mai rapid start" +continue = "Continuați" +deployment = "Unde ar trebui implementat?" +eula = "Sunt de acord cu Stirling EULA și Termenii comerciali." +fullName = "Nume complet" +fullNamePlaceholder = "Numele dvs." +invites = "Invitați colegi · opțional" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Programați un apel" +seats = "Persoane care testează" +seatsPlaceholder = "de ex. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker sau Kubernetes" +start = "Începeți perioada de probă" +stepOf = "Pasul {{n}} din {{total}}" +subtitle = "Gratuit 14 zile, fără card necesar." +subtitleDetails = "Câteva detalii pentru oferta și acordul dvs." +talkFirst = "Preferați să discutăm mai întâi?" +title = "Configurați perioada de probă" +viewEula = "Citiți-l" +workEmail = "Email de serviciu" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Extinderea adaugă 7 zile și notifică inginerul tău de soluții." @@ -7808,109 +8690,113 @@ maxed = "Limită atinsă" subtitle = "Trialul tău gratuit rulează până la {{date}}. Nu este necesar cardul." title = "Trial Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Preț pentru volum angajat, SSO + SCIM + RBAC la nivel de org, audit imuabil de 90 de zile și un SE dedicat." -homeCta = "Începe trialul →" -homeHeadline = "Procesează milioane de PDF-uri." - -[portal.recentActivity] -title = "Activitate recentă" -viewAll = "Vezi tot" - -[portal.recentActivity.empty] -description = "Rulările de pipeline, implementările și evenimentele agenților vor apărea aici." -title = "Încă nu este nimic aici" - -[portal.search] -ariaLabel = "Caută" -placeholder = "Caută în Stirling — endpoint-uri, pipeline-uri, documente…" - -[portal.search.empty] -noActionsDescription = "Acțiunile rapide vor apărea aici după ce devin disponibile." -noActionsTitle = "Nicio acțiune rapidă" -noMatches = "Nicio potrivire pentru \"{{query}}\"" -noMatchesDescription = "Încearcă un alt cuvânt cheie sau răsfoiește catalogul." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Conectarea contului" -[portal.shell.header] -accountFallback = "Cont" -accountMenu = "Meniu cont" -darkMode = "Mod întunecat" -lightMode = "Mod luminos" -search = "Caută" -searchPlaceholder = "Caută…" -signOut = "Deconectează-te" -switchToDark = "Comută la tema întunecată" -switchToLight = "Comută la tema luminoasă" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} doc." linkAccount = "Conectează contul Stirling" -planEditor = "Plan Editor" -planEnterprise = "Plan Enterprise" -planProcessor = "Plan Processor" primaryNav = "Navigare principală" switchApp = "Comută aplicația" +[portal.shell.topbar] +closeNav = "Închideți navigarea" +openNav = "Deschideți navigarea" +search = "Căutați" + [portal.sources] subtitle = "Conexiuni de intrare reutilizabile care alimentează documente în Stirling. Configurează o conexiune o singură dată, apoi referențiaz-o din orice număr de politici. Fă clic pe un rând pentru configurarea sa și politicile care o folosesc." title = "Surse" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Conectează sursa" +[portal.sources.builder] +back = "Înapoi la surse" +backToTypes = "Toate tipurile de surse" +cancel = "Anulați" +chooseHint = "Alegeți de unde vin documentele. Conectorii gri sunt în pregătire." +comingSoon = "În curând" +comingSoonHeading = "În curând" +create = "Creați sursa" +createTitle = "Conectați o sursă" +delete = "Ștergeți" +editTitle = "Editați sursa" +enabled = "Activată" +save = "Salvați modificările" + +[portal.sources.builder.folderAccess] +description = "Automatizările de folder pot folosi doar foldere pe care un administrator le-a permis. Adăugați-l în setările Acces folder, apoi încercați din nou." +openSettings = "Setări Acces folder" +title = "Acest folder nu este permis" + [portal.sources.delete] body = "Ștergi \"{{name}}\"? Această acțiune nu poate fi anulată. Politicile care o referențiază ar trebui actualizate." cancel = "Anulează" confirm = "Șterge" title = "Ștergi sursa?" -[portal.sources.detail] -closeAriaLabel = "Închide detaliile" -delete = "Șterge sursa" -docs24h = "Ultimele 24h" -docs30d = "Ultimele 30 de zile" -docsTotal = "Total văzute" -docsTrend = "Documente în ultimele 30 de zile" -documents = "Documente" -edit = "Editează" -notReferenced = "Nu este referențiată de nicio politică, deci poate fi ștearsă în siguranță." -pause = "Pauză" -resume = "Reia" -subtitle = "{{type}} · {{status}}" -usedBy = "Folosită de" - -[portal.sources.empty] -description = "Conectează un folder (și, în curând, stocare cloud) astfel încât politicile tale să aibă de unde prelua documente." -title = "Încă nu există surse conectate" - [portal.sources.kpi] inUse = "În uz" total = "Conexiuni" unused = "Nefolosite" +[portal.sources.networkFields.connection] +helperText = "Conexiunea stocată cu adresa serverului și credențialele. Reutilizată de fiecare sursă care o referențiază." +label = "Conexiune" + +[portal.sources.networkFields.directory] +helperText = "Folder de pe server de interogat, relativ la home-ul de autentificare sau la rădăcina share-ului. Lăsați gol pentru rădăcină." +label = "Folder" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume elimină fiecare fișier de pe server după ce fiecare politică l-a procesat." +label = "Mod de citire" + +[portal.sources.networkFields.mode.options] +consume = "Consume: procesați fiecare fișier o singură dată" +snapshot = "Snapshot: recitiți folderul la fiecare rulare" + +[portal.sources.networkFields.recursive] +label = "Adâncime folder" + +[portal.sources.networkFields.recursive.options] +all = "Include subfoldere" +top = "Doar nivelul superior" + [portal.sources.status] active = "Activ" disabled = "Dezactivat" unused = "Nefolosit" [portal.sources.table] +documents = "Documente" source = "Sursă" status = "Stare" +type = "Type" usedBy = "Politici" +[portal.sources.types.box] +description = "Preluați documente din foldere Box pe care echipa dvs. le partajează deja." +label = "Box" + +[portal.sources.types.dropbox] +description = "Preluați documente dintr-un folder Dropbox partajat." +label = "Dropbox" + [portal.sources.types.editor] +description = "Documente pe care echipa dvs. le-a procesat în editor, în rulări de politici și AI." label = "Editor" +noPolicies = "Încă nu a rulat nicio politică din editor." + +[portal.sources.types.email] +description = "Transformați atașamentele trimise către o căsuță poștală dedicată în documente." +label = "Inbox email" [portal.sources.types.folder] description = "Monitorizează un director de pe server pentru documente noi." @@ -7943,25 +8829,80 @@ label = "Adâncime folder" all = "Include subfoldere" top = "Doar nivelul superior" +[portal.sources.types.ftp] +description = "Interogați un folder de server FTP sau FTPS pentru documente noi." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Preluați documente din foldere Google Drive partajate." +label = "Google Drive" + +[portal.sources.types.network] +description = "Monitorizați un share de rețea SMB pentru documente noi." +label = "Drive de rețea" + +[portal.sources.types.onedrive] +description = "Monitorizați foldere OneDrive pentru documente noi." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Preluați documente dintr-un bucket Amazon S3 sau compatibil S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Conexiunea stocată care conține bucket-ul și credențialele. Reutilizată de fiecare sursă și ieșire de pipeline care o referențiază." +label = "Conexiune" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume elimină fiecare obiect din bucket după ce fiecare politică l-a procesat." +label = "Mod de citire" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: procesați fiecare obiect o singură dată" +snapshot = "Snapshot: recitiți bucket-ul la fiecare rulare" + +[portal.sources.types.s3.fields.prefix] +helperText = "Sunt procesate doar obiectele ale căror chei încep cu acest prefix." +label = "Prefix cheie" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Interogați un folder drop SFTP pentru documente noi." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Preluați documente din bibliotecile de documente SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Sursă" +[portal.sources.types.webhook] +createNote = "Un URL de livrare și un secret de semnare sunt generate când creați acest webhook. Expeditorii trimit documente prin POST către acel URL, iar politicile care îl referențiază rulează la sosire." +description = "Primiți documente prin HTTP POST semnat de la orice sistem extern." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL de livrare" +secretNote = "Secretul de semnare este afișat o singură dată, când webhook-ul este creat. Recreați sursa pentru a-l roti." + +[portal.sources.types.webhook.reveal] +copy = "Copiați" +done = "Gata" +secret = "Secret de semnare" +secretHelp = "Semnați corpul brut al fiecărei livrări cu această cheie (HMAC-SHA256) și trimiteți-l ca header X-Stirling-Signature." +secretWarning = "Copiați acum secretul de semnare. Pentru securitatea dvs., este afișat o singură dată și nu poate fi recuperat ulterior." +title = "Webhook creat" +url = "URL de livrare" +usage = "Trimiteți fiecare document prin POST ca body brut al cererii către URL-ul de livrare, cu un tip de conținut binar (application/pdf sau application/octet-stream). Politicile care îl referențiază rulează automat la sosire." + [portal.sources.wizard] -back = "Înapoi" -cancel = "Anulează" -continue = "Continuă" -editTitle = "Editează sursa" name = "Nume" namePlaceholder = "de ex. Primire cereri" -save = "Salvează modificările" -subtitle = "Pasul {{current}} din {{total}} · {{label}}" -title = "Conectează o sursă" type = "Tip" -[portal.sources.wizard.steps] -chooseType = "Alege tipul" -configure = "Configurează" -review = "Revizuiește și conectează" +[portal.stepModal] +close = "Închideți" [portal.usage] managePayment = "Gestionează plata" @@ -8021,16 +8962,9 @@ summary = "Deține o echipă — gestionează resursele membrilor și configură 2 = "Acces la portal prin politica implicită" 3 = "Tot ce poate face un Membru" -[portal.welcome] -ariaLabel = "Bun venit la Stirling PDF" -badge = "Open-source" -installEditor = "Instalează Editorul" -inviteTeammates = "Invită coechipieri" -openInBrowser = "Deschide în browser" -perks = "Gratuit pentru totdeauna · Self-hostable" -subtitle = "Cel mai sigur PDF Editor din lume este gratuit pentru echipe de toate dimensiunile. Include peste 60 de operațiuni PDF și SSO." -title = "Bun venit la" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Nelimitat" [printFile] title = "Tipărește Fișier" @@ -8613,6 +9547,14 @@ text = "Rotiți paginile PDF în sens orar sau antiorar în trepte de 90 de grad [rotate.tooltip.header] title = "Prezentare setări de rotație" +[ruler] +clearAll = "Ștergeți tot" +hideAllLabels = "Ascundeți toate etichetele" +hideSmallLabels = "Ascundeți etichetele mici" +physicalValues = "Fizic" +scaled = "Scalat" +showAllLabels = "Afișați toate etichetele" + [sanitize] desc = "Eliminați elementele potențial dăunătoare din fișierele PDF." sanitizationResults = "Rezultatele sanitizării" @@ -8654,31 +9596,45 @@ label = "Eliminați metadatele XMP" [sanitize.steps] settings = "Setări" -[sanitizePdf] -tags = "curăță,securizează,sigur,elimină-amenințări" - [sanitizePDF] title = "Igienizează PDF" +[sanitizePdf] +tags = "curăță,securizează,sigur,elimină-amenințări" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Ajustează scala paginii" +[scaleSettings] +activeScale = "Scară activă" +apply = "Aplicați scara" +applyCalibration = "Aplicați calibrarea" +calculatedScale = "Scară calculată" +calibrate = "Calibrați" +calibrating = "Se calibrează" +calibrationDistanceRequired = "Introduceți o distanță reală mai mare decât zero" +calibrationInvalid = "Nu se poate calcula scara din această măsurătoare" +calibrationPaperDistance = "Distanța măsurată pe pagină: {{distance}}" +calibrationTitle = "Calibrare scară" +calibrationTooltip = "Măsurați o distanță cunoscută pentru a calcula automat scara" +customScale = "Scară personalizată" +noneSet = "Nu este setată nicio scară personalizată" +presets = "Scări presetate" +ratio = "Raport de scară" +ratioHelp = "Raport: 1 unitate de pagină = X unități reale" +ratioPlaceholder = "de ex., 100" +ratioPositive = "Raportul trebuie să fie mai mare decât zero" +ratioRequired = "Raportul este obligatoriu" +realDistance = "Distanță reală" +realDistancePlaceholder = "de ex., 5" +reset = "Resetează la valorile implicite" +unit = "Unitate" +unitInvalid = "Unitate nevalidă: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Prag unghi:" -10 = "Stabilește mărimea marginii adăugate și eliminate pentru a evita marginile albe în rezultat (implicit: 1)." -2 = "Stabilește unghiul absolut minim necesar pentru ca imaginea să fie rotită (implicit: 5)." -3 = "Toleranță:" -4 = "Determină intervalul de variație a culorii în jurul culorii de fundal estimate (implicit: 20)." -5 = "Arie minimă:" -6 = "Stabilește pragul minim de arie pentru o fotografie (implicit: 8000)." -7 = "Arie minimă a conturului:" -8 = "Stabilește pragul minim de arie a conturului pentru o fotografie." -9 = "Mărimea marginii:" - [scannerImageSplit] submit = "Extrage scanări de imagini" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Ce face" whatThisDoesDesc = "Găsește și extrage automat fiecare fotografie dintr-o pagină scanată sau imagine compozit—fără decupare manuală." whenToUse = "Când se folosește" +[ScannerImageSplit.selectText] +1 = "Prag unghi:" +10 = "Stabilește mărimea marginii adăugate și eliminate pentru a evita marginile albe în rezultat (implicit: 1)." +2 = "Stabilește unghiul absolut minim necesar pentru ca imaginea să fie rotită (implicit: 5)." +3 = "Toleranță:" +4 = "Determină intervalul de variație a culorii în jurul culorii de fundal estimate (implicit: 20)." +5 = "Arie minimă:" +6 = "Stabilește pragul minim de arie pentru o fotografie (implicit: 8000)." +7 = "Arie minimă a conturului:" +8 = "Stabilește pragul minim de arie a conturului pentru o fotografie." +9 = "Mărimea marginii:" + [search] noResults = "Nu s-au găsit rezultate" placeholder = "Introduceți termenul de căutare..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Serverul dvs. Stirling-PDF este offline, iar \"{{endp [session] expired = "Sesiunea dvs. a expirat. Reîmprospătați pagina și încercați din nou." -[sessionManagement.tooltip] -header = "Gestionarea sesiunilor de semnare" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Noii participanți sunt adăugați la finalul ordinii de semnare" -bullet2 = "Nu se pot adăuga participanți după finalizarea sesiunii" -bullet3 = "Fiecare participant primește o notificare când îi vine rândul" -description = "Puteți adăuga mai mulți participanți la o sesiune activă oricând înainte de finalizare." -title = "Adăugarea participanților" - -[sessionManagement.tooltip.finalization] -bullet1 = "Finalizare completă: Toți participanții au semnat" -bullet2 = "Finalizare parțială: Unii participanți nu au semnat încă" -bullet3 = "Participanții fără semnătură vor fi excluși din documentul final" -bullet4 = "După finalizare, puteți încărca PDF-ul semnat în fișierele active" -description = "Finalizarea combină toate semnăturile într-un singur PDF semnat. Această acțiune nu poate fi anulată." -title = "Finalizarea sesiunii" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Nu pot fi eliminați participanții care au semnat deja" -bullet2 = "Participanții eliminați nu mai primesc notificări" -bullet3 = "Ordinea de semnare se ajustează automat" -description = "Participanții pot fi eliminați din sesiuni înainte să semneze." -title = "Eliminarea participanților" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Fiecare semnătură este aplicată secvențial pe PDF" -bullet2 = "Semnatarii ulteriori pot vedea semnăturile anterioare" -bullet3 = "Critic pentru fluxuri de aprobare și lanțuri legale de custodie" -description = "Ordinea specificată la crearea sesiunii determină cine semnează primul." -title = "Ordinea semnăturilor" - [settings] close = "Închideți" +[settings.ai] +documents = "Documente și RAG" +general = "General" +limits = "Limite și performanță" +models = "Modele și furnizori" +title = "AI" + [settings.configuration] advanced = "Avansat" database = "Bază de date" endpoints = "Endpoint-uri" features = "Funcții" +folderAccess = "Acces la foldere" mcp = "MCP Server" storageSharing = "Stocare fișiere și partajare" systemSettings = "Setări sistem" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Editor PDF implicit" defaultPdfEditorActive = "Stirling PDF este editorul tău PDF implicit" defaultPdfEditorChecking = "Se verifică..." defaultPdfEditorInactive = "O altă aplicație este setată ca implicită" +defaultPdfEditorRemind = "Amintește-mi să setez ca implicit" +defaultPdfEditorRemindDescription = "Afișează un banner când Stirling PDF nu este aplicația implicită pentru PDF." defaultPdfEditorSet = "Deja implicit" defaultStartupView = "Vizualizare implicită la pornire" defaultStartupViewDescription = "Alegeți care filă este activă în coloana din stânga la pornirea aplicației" @@ -8817,9 +9763,18 @@ languageDescription = "Alege limba afișată" logout = "Deconectare" setAsDefault = "Setează ca implicit" theme = "Temă" +themeAccent = "Culoare de accent" +themeAccentCustom = "Culoare personalizată" +themeAccentCustomHint = "Personalizat (valorile implicite sunt recomandate)" +themeAccentDark = "Culoare de accent pentru modul întunecat" +themeAccentDefault = "Implicit" +themeAccentDefaultHint = "Temă implicită (recomandată)" +themeAccentDescription = "Butoanele, linkurile și evidențierile folosesc această culoare, iar aceasta nuanțează subtil aplicația. Modurile luminos și întunecat au fiecare propria culoare. Sistemul folosește culoarea activă." +themeAccentLight = "Culoare de accent pentru modul luminos" themeDark = "Întunecată" themeDescription = "Comută între modul deschis și întunecat" themeLight = "Deschisă" +themeReset = "Restabilește tema implicită" themeSystem = "System" title = "General" user = "Utilizator" @@ -8833,6 +9788,12 @@ intro = "Activați autentificarea utilizatorilor, gestionarea echipei și funcț learnMore = "Aflați mai multe în documentație" title = "Pentru administratorii de sistem" +[settings.general.loginLanding] +description = "Alegeți ce aplicație se deschide când vă autentificați în Stirling Cloud." +editor = "Editor" +processor = "Procesator" +title = "După autentificare" + [settings.general.mode] fullscreen = "Ecran complet" sidebar = "Bară laterală" @@ -9013,9 +9974,6 @@ title = "Politici și confidențialitate" [settings.preferences] title = "Preferințe" -[settings.search] -placeholder = "Căutați în paginile de setări..." - [settings.security] description = "Actualizați parola pentru a vă menține contul în siguranță." title = "Securitate" @@ -9041,6 +9999,11 @@ security = "Securitate" telegram = "Telegram" title = "Securitate și autentificare" +[settings.signOut] +confirm = "Sigur doriți să vă deconectați?" +submit = "Deconectare" +title = "Deconectare" + [settings.team] title = "Echipă" @@ -9156,6 +10119,7 @@ last = "Ultima pagină" maintainRatio = "Comută menținerea raportului de aspect" next = "Pagina următoare" noSavedSigs = "Nu s-au găsit semnături salvate" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Semnături personale" previous = "Pagina anterioară" redo = "Refă" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Eliminarea fundalului a eșuat" hint = "Încărcați o imagine PNG sau JPG a semnăturii dvs." label = "Încarcă imaginea semnăturii" placeholder = "Selectați fișierul imagine" +previewAlt = "Current image signature" processing = "Se procesează imaginea..." removeBackground = "Elimină fundalul alb (transparentizează)" @@ -9207,6 +10172,17 @@ saved = "Selectează o semnătură salvată mai sus, apoi fă clic oriunde pe PD text = "După ce introduceți numele mai sus, faceți clic oriunde pe PDF pentru a plasa semnătura." title = "Cum să adăugați semnătura" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Mută semnătura" pause = "Pauză plasare" @@ -9653,10 +10629,30 @@ title = "Încărcați pe server" updateButton = "Actualizați pe server" uploadButton = "Încărcați pe server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "faceți clic aici" +fallback = "Dacă nu s-a deschis automat," +opening = "Se deschide Swagger UI într-o filă nouă..." tags = "api,documentație,swagger,endpoint-uri,dezvoltare" title = "Documentație API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Selectați fișiere din Fișiere active pentru a rula acest i singleFileScope = "Se aplică numai pentru: {{fileName}}" viewerMode = "Comutați la editorul de fișiere pentru a selecta mai multe fișiere." +[toolFormat] +ANY = "orice fișier" +CBR = "o arhivă de benzi desenate (CBR)" +CBZ = "o arhivă de benzi desenate (CBZ)" +CSV = "un fișier CSV" +EBOOK = "o carte electronică" +EMAIL = "un e-mail" +EXCEL = "o foaie de calcul" +HTML = "o pagină web" +IMAGE = "imagini" +JAVASCRIPT = "un fișier JavaScript" +JSON = "un fișier JSON" +MARKDOWN = "un fișier Markdown" +NONE = "niciun fișier" +PCL = "un fișier de imprimare PCL" +PDF = "un PDF" +PDF_ENCRYPTED = "un PDF protejat cu parolă" +POSTSCRIPT = "un fișier PostScript" +PPT = "o prezentare" +TEXT = "un fișier text" +VIDEO = "un videoclip" +WORD = "un document Word" +XML = "un fișier XML" +XPS = "un document XPS" +ZIP = "o arhivă ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Înapoi la toate instrumentele" collapse = "Restrânge panoul" expand = "Extinde panoul" goBack = "Înapoi" +pdfTools = "Instrumente PDF" placeholder = "Alegeți un instrument pentru a începe" premiumFeature = "Funcție premium:" toolsHeader = "Instrumente" @@ -9806,11 +10829,6 @@ noToolsFound = "Nu s-au găsit instrumente" quickAccess = "ACCES RAPID" searchPlaceholder = "Căutați instrumente..." -[toolPicker.categories] -advancedTools = "Instrumente avansate" -recommendedTools = "Instrumente recomandate" -standardTools = "Instrumente standard" - [toolPicker.subcategories] advancedFormatting = "Formatare avansată" automation = "Automatizare" @@ -9824,6 +10842,9 @@ removal = "Eliminare" signing = "Semnare" verification = "Verificare" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nu s-au găsit instrumente" noTools = "Niciun instrument disponibil" @@ -9950,28 +10971,53 @@ title = "Utilizatori" you = "(tu)" [users.action] +cancelInvite = "Anulează invitația" deleteTeam = "Șterge echipa" disableMfa = "Resetează MFA" move = "Mută în echipă" reinstate = "Reactivează" remove = "Elimină din org" +removeTeam = "Elimină din echipă" rename = "Redenumește echipa" resetPw = "Resetează parola" suspend = "Suspendă" unlock = "Deblochează contul" +[users.activity] +daysAgo = "{{count}} z în urmă" +hoursAgo = "{{count}} h în urmă" +justNow = "Chiar acum" +minutesAgo = "{{count}} min în urmă" +monthsAgo_one = "{{count}} lună în urmă" +monthsAgo_other = "{{count}} luni în urmă" +never = "Niciodată" +weeksAgo_one = "{{count}} săptămână în urmă" +weeksAgo_other = "{{count}} săptămâni în urmă" +yearsAgo_one = "{{count}} an în urmă" +yearsAgo_other = "{{count}} ani în urmă" + [users.cap] addProcessor = "+ Processor" approver = "Aprobă politica" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Anulați invitația către {{email}}? Nu va putea să se alăture cu linkul actual." +cancelInviteTitle = "Anulează invitația" deleteTeamBody = "Ștergi echipa {{name}}? Echipa trebuie să fie mai întâi goală - mută-i membrii într-o altă echipă și nu trebuie să mai dețină configurări de integrare." deleteTeamTitle = "Șterge echipa" disableMfaBody = "Elimini înscrierea MFA a lui {{name}}? O va configura din nou la următoarea autentificare, dacă este necesar." disableMfaTitle = "Resetează MFA" removeBody = "Elimini definitiv pe {{name}} din organizație? Această acțiune nu poate fi anulată." +removeTeamBody = "Eliminați pe {{name}} din echipă? Își păstrează contul, dar pierde accesul la resursele acestei echipe." removeTitle = "Elimină membrul" [users.empty] @@ -9982,10 +11028,8 @@ title = "Încă nu există membri" addToTeam = "Adaugă în echipă" guestCount = "{{count}} invitat" guests = "Invitați" -guestsDesc = "Colaboratori externi, limitați la ce ai partajat. Doar Editor." ledBy = "condusă de {{owner}}" org = "Organizație" -orgDesc = "Proprietari cu autoritate la nivel de org și aprobare de politici" owners = "{{count}} proprietar" team = "Echipa {{name}}" teamMeta = "{{count}} persoane" @@ -10022,6 +11066,18 @@ username = "Nume de utilizator" usernameError = "Numele de utilizator trebuie să aibă cel puțin 3 caractere" usernamePlaceholder = "jsmith" +[users.invites] +by = "Invitat de {{who}}" +cancel = "Anulează" +expiresInDays_one = "Expiră peste {{count}} zi" +expiresInDays_other = "Expiră peste {{count}} zile" +expiresToday = "Expiră astăzi" +title = "Invitații în așteptare" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Ceva nu a mers bine la conectarea cu backend-ul sau nu aveți acces. Încercați din nou." title = "Nu s-au putut încărca membrii" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Caută în atașamente" title = "Atașamente" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Titlul marcajului" +bookmarkTitleRequired = "Titlul marcajului este obligatoriu" closeSidebar = "Închide bara laterală a marcajelor" collapseAll = "Restrânge toate marcajele" +empty = "No bookmarks in this document" expandAll = "Extinde toate marcajele" +searchPlaceholder = "Caută marcaje" +title = "Bookmarks" [viewer.comments] addComment = "Adăugați comentariu" @@ -10236,10 +11297,12 @@ locateAnnotation = "Localizați în document" moreActions = "Mai multe acțiuni" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Pagina {{page}}" placingHint = "Faceți clic pe o pagină pentru a plasa… (anulare)" removeCommentOnly = "Eliminați doar comentariul" saveReply = "Salvați răspunsul" +searchPlaceholder = "Search comments" title = "Comentarii" typeComment = "Comentariu" typeInsertText = "Inserare text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Ai modificări nesalvate" closeSidebar = "Închide bara laterală a straturilor" hideAll = "Ascunde toate straturile" showAll = "Afișează toate straturile" +title = "Layers" [viewer.link] delete = "Ștergeți linkul" @@ -10605,30 +11669,6 @@ title = "Text filigran" image = "Imagine" text = "Text" -[wetSignature.tooltip] -header = "Metode de creare a semnăturii" - -[wetSignature.tooltip.draw] -bullet1 = "Personalizați culoarea și grosimea liniei" -bullet2 = "Ștergeți și redesenați până când sunteți mulțumit" -bullet3 = "Funcționează pe dispozitive cu ecran tactil (tablete, telefoane)" -description = "Creați o semnătură olografă folosind mouse-ul sau ecranul tactil. Ideală pentru semnături personale, autentice." -title = "Desenați semnătura" - -[wetSignature.tooltip.type] -bullet1 = "Alegeți dintre mai multe fonturi" -bullet2 = "Personalizați dimensiunea și culoarea textului" -bullet3 = "Perfect pentru semnături standardizate" -description = "Generați o semnătură din text tastat. Rapidă și consecventă, potrivită pentru documente de afaceri." -title = "Tastați semnătura" - -[wetSignature.tooltip.upload] -bullet1 = "Acceptă PNG, JPG și alte formate de imagine" -bullet2 = "Fundalurile transparente sunt recomandate pentru rezultate optime" -bullet3 = "Imaginea va fi redimensionată pentru a se potrivi zonei semnăturii" -description = "Încărcați o imagine de semnătură preexistentă. Ideală dacă aveți o semnătură scanată sau un logo al companiei." -title = "Încărcați imaginea semnăturii" - [workbenchBar] activeFiles = "Fișiere active" annotations = "Adnotări" @@ -10642,6 +11682,7 @@ exitRedaction = "Ieși din modul de redactare" exportAll = "Exportă PDF" exportSelected = "Exportă paginile selectate" formFill = "Completează formularul" +hideToolbar = "Hide toolbar" multiTool = "Instrument multiplu" panMode = "Mod panoramare" print = "Tipărește PDF" @@ -10652,6 +11693,7 @@ redact = "Redactează" rotateLeft = "Rotește la stânga" rotateRight = "Rotește la dreapta" ruler = "Riglă / Măsurare" +rulerSettings = "Setări scară" save = "Salvează" saveAll = "Salvează tot" saveAs = "Salvează ca" @@ -10661,6 +11703,7 @@ selectAll = "Selectează tot" selectByNumber = "Selectează după numerele paginilor" selectLanguage = "Selectează limba" share = "Partajează" +showToolbar = "Show toolbar" toggleAnnotations = "Comută vizibilitatea adnotărilor" toggleAttachments = "Comută atașamentele" toggleBookmarks = "Comută semnele de carte" diff --git a/frontend/editor/public/locales/ru-RU/translation.toml b/frontend/editor/public/locales/ru-RU/translation.toml index 289e93e965..9f6321bc64 100644 --- a/frontend/editor/public/locales/ru-RU/translation.toml +++ b/frontend/editor/public/locales/ru-RU/translation.toml @@ -1,7 +1,6 @@ alphabet = "Алфавит" apply = "Применить" applyAndContinue = "Применить и продолжить" -areYouSure = "Вы уверены, что хотите уйти?" back = "Назад" black = "Черный" blue = "Синий" @@ -46,10 +45,8 @@ filesSelected = "файлов выбрано" font = "Шрифт" fontSizeTooltip = "Размер текста номера страницы в пунктах. Большие числа — больше текст." fontTypeTooltip = "Семейство шрифтов для номеров страниц. Выберите в соответствии со стилем документа." -genericSubmit = "Отправить" green = "Зеленый" help = "Помощь" -imgPrompt = "Выберите изображение(я)" incorrectPasswordMessage = "Текущий пароль неверен." info = "Информация" invalidUndoData = "Нельзя отменить: неверные данные операции" @@ -62,7 +59,6 @@ loginAgreementTitle = "Соглашение при входе" logOut = "Выйти" marginTooltip = "Расстояние между номером страницы и краем страницы." moreOptions = "Больше параметров" -multiPdfPrompt = "Выберите PDF-файлы (2+)" never = "Никогда" no = "Нет" noFileSelected = "Файл не выбран. Пожалуйста, загрузите его." @@ -80,7 +76,6 @@ pageOrderPrompt = "Пользовательский порядок страни pages = "Страницы" pageSelectionPrompt = "Выбор страниц (Введите список номеров страниц через запятую 1,5,6 или функции типа 2n+1):" password = "Пароль" -pdfPrompt = "Выберите PDF-файл(ы)" pin = "Закрепить файл (оставить активным после выполнения инструмента)" pro = "Pro" processingComplete = "Ваш файл готов." @@ -110,6 +105,7 @@ undoSuccess = "Операция успешно отменена" unknown = "Неизвестно" unpin = "Открепить файл (заменить после выполнения инструмента)" unsavedChanges = "У вас есть несохранённые изменения в вашем PDF. Что вы хотите сделать?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Несохранённые изменения" unsupported = "Не поддерживается" username = "Имя пользователя" @@ -356,6 +352,7 @@ text = "Эти разрешения контролируют, что польз title = "Изменить разрешения" [addStamp] +preview = "Предпросмотр штампа" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ручные загрузки: нажмите на ссылки noLanguages = "В настроенном каталоге не найдено языков tessdata." permissionNotice = "Путь tessdata недоступен для записи. Загрузки будут открываться в браузере; сохраните файлы .traineddata вручную в папку tessdata." +[admin.settings.ai.documents] +description = "Настройте модель эмбеддингов и параметры извлечения, используемые для ответов на вопросы по документам. Применяется к AI engine при сохранении." +title = "Документы и RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Оставьте пустым, чтобы использовать собственные учетные данные окружения engine. Применяется к самостоятельно размещенным развертываниям с одним engine." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Сохранено — оставьте пустым, чтобы сохранить текущий ключ" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Базовый URL OpenAI-совместимой / Ollama конечной точки эмбеддингов, например http://ollama:11434/v1. Должен указывать на доверенную внутреннюю конечную точку (чувствительно к SSRF)." +label = "Базовый URL эмбеддингов" + +[admin.settings.ai.documents.embeddingModel] +description = "Название модели эмбеддингов. Свободный текст; предложения носят только рекомендательный характер." +label = "Модель эмбеддингов" + +[admin.settings.ai.documents.embeddingProvider] +description = "Поставщик, используемый для преобразования текста документа в векторные эмбеддинги." +label = "Поставщик эмбеддингов" + +[admin.settings.ai.documents.maxSearches] +description = "Максимальное количество поисков извлечения, которые агент может выполнить за один запрос." +label = "Макс. поисков" + +[admin.settings.ai.documents.reindexNote] +body = "Изменение модели эмбеддингов вступает в силу немедленно, но документы, проиндексированные предыдущей моделью, необходимо переиндексировать, чтобы поиск возвращал корректные результаты." +title = "Требуется переиндексация" + +[admin.settings.ai.documents.saved] +reindexNote = "Если вы изменили модель эмбеддингов, переиндексируйте существующие документы, чтобы поиск использовал новую модель." + +[admin.settings.ai.documents.topK] +description = "Количество наиболее релевантных фрагментов, извлекаемых за один поиск." +label = "Top K" + +[admin.settings.ai.general] +description = "Подключите Stirling к Python AI engine и выберите, какие возможности AI будут доступны. Изменения применяются при перезапуске." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Включайте или отключайте отдельные функции AI. Отключенные функции скрываются в приложении." +title = "Возможности" + +[admin.settings.ai.general.enabled] +description = "Главный переключатель. Если выключен, инструменты AI, агенты и вызовы engine недоступны." +label = "Включить AI" + +[admin.settings.ai.general.features.chat] +description = "Разговорный помощник для работы с PDF." +label = "Чат-помощник" + +[admin.settings.ai.general.features.classify] +description = "Автоматически классифицируйте документы по типу или содержимому." +label = "Классификация документов" + +[admin.settings.ai.general.features.createPdf] +description = "Создайте новый PDF (например, из HTML) через AI-агента." +label = "Создать PDF из запроса" + +[admin.settings.ai.general.features.documentQuestions] +description = "Задавайте вопросы и получайте ответы, основанные на загруженном документе." +label = "Вопросы по документу" + +[admin.settings.ai.general.features.mathAuditor] +description = "Проверяйте документы на математические и числовые ошибки." +label = "Математический аудитор" + +[admin.settings.ai.general.features.pdfComment] +description = "Добавляйте в PDF комментарии и аннотации для проверки, созданные AI." +label = "Агент комментариев PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Тайм-аут для более тяжелых операций агента, таких как создание документов." +label = "Тайм-аут длительных операций (секунды)" + +[admin.settings.ai.general.note] +body = "AI engine работает как отдельная служба. Его общий секрет" +body2 = "задается через переменную окружения контейнера. API keys поставщиков можно ввести на этих страницах или предоставить как переменные окружения engine; ключи, введенные здесь, отправляются в engine при сохранении." +title = "Об AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Тайм-аут для потоковых (токен за токеном) ответов чата." +label = "Тайм-аут потока (секунды)" + +[admin.settings.ai.general.test] +button = "Проверить подключение" +failBody = "AI engine не ответил. Проверьте URL, что контейнер engine запущен и что AI включен (после включения требуется перезапуск)." +failTitle = "AI engine недоступен" +okBody = "AI engine ответил на проверку работоспособности." +okTitle = "AI engine доступен" + +[admin.settings.ai.general.timeoutSeconds] +description = "Тайм-аут для стандартных AI-запросов к engine." +label = "Тайм-аут запроса (секунды)" + +[admin.settings.ai.general.url] +description = "Внутренний URL Python AI engine, например http://stirling-pdf-engine:5001." +label = "URL AI engine" + +[admin.settings.ai.limits] +description = "Ограничения для объема работы AI-запросов и количества одновременно выполняемых запросов. Применяется к AI engine при сохранении." +title = "Ограничения и производительность" + +[admin.settings.ai.limits.maxCharacters] +description = "Ограничение: отклонять AI-запросы, извлеченный текст которых превышает эту длину." +label = "Макс. символов на запрос" + +[admin.settings.ai.limits.maxPages] +description = "Ограничение: отклонять AI-запросы с количеством страниц PDF больше указанного." +label = "Макс. страниц на запрос" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Максимальное количество одновременных активных вызовов модели во всем engine." +label = "Макс. параллелизм модели" + +[admin.settings.ai.models] +description = "Выберите поставщика LLM и умную/быструю модели, которые использует AI engine. Применяется к AI engine при сохранении." +title = "Модели и поставщики" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Оставьте пустым, чтобы использовать собственные учетные данные окружения engine. Применяется к самостоятельно размещенным развертываниям с одним engine." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Сохранено — оставьте пустым, чтобы сохранить текущий ключ" + +[admin.settings.ai.models.baseUrl] +description = "Базовый URL OpenAI-совместимой / Ollama конечной точки, например http://ollama:11434/v1." +label = "Базовый URL поставщика" +warning = "Базовый URL должен указывать на доверенную внутреннюю конечную точку. Engine будет выполнять к ней серверные запросы, поэтому недоверенное значение чувствительно к SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Максимальное количество выходных токенов для быстрой модели." +label = "Макс. токенов быстрой модели" + +[admin.settings.ai.models.fastModel] +description = "Более дешевая и быстрая модель для легких задач. Свободный текст; предложения носят только рекомендательный характер." +label = "Быстрая модель" + +[admin.settings.ai.models.provider] +description = "С каким поставщиком LLM взаимодействует engine." +label = "Поставщик" + +[admin.settings.ai.models.smartMaxTokens] +description = "Максимальное количество выходных токенов для умной модели." +label = "Макс. токенов умной модели" + +[admin.settings.ai.models.smartModel] +description = "Модель с высокими возможностями для сложных рассуждений. Свободный текст; предложения носят только рекомендательный характер." +label = "Умная модель" + +[admin.settings.ai.saved] +body = "Изменения автоматически отправлены в AI engine." +bodyNoPush = "Настройки сохранены. Они применятся, когда AI engine в следующий раз получит свою конфигурацию." +title = "Настройки AI сохранены" + [admin.settings.badge] clickToUpgrade = "Нажмите, чтобы посмотреть детали плана" @@ -938,6 +1094,25 @@ label = "Пересоздавать при запуске" description = "Количество дней действия сертификата" label = "Срок действия сертификата (дни)" +[admin.settings.folderAccess] +description = "Каталоги, из которых источникам папок и выходным папкам разрешено читать и в которые разрешено записывать. Это граница безопасности: автоматизации никогда нельзя указать путь на сервере вне этого списка." +securityNote = "Оставьте пустым, чтобы полностью отключить источники папок и выходные папки. Собственный каталог конфигурации Stirling всегда недоступен." +title = "Доступ к папкам" + +[admin.settings.folderAccess.implied] +description = "Эти каталоги, управляемые Stirling, всегда разрешены и не могут быть изменены здесь." +serverStorage = "Файловое хранилище сервера" +title = "Всегда разрешено" +watchedFolder = "Отслеживаемая папка конвейера" + +[admin.settings.folderAccess.roots] +add = "Добавить" +empty = "Нет разрешенных папок. Источники папок и выходные папки сейчас отключены." +hint = "Введите абсолютные пути, например /data/inbox." +label = "Разрешенные корневые папки" +placeholder = "/data/inbox" +remove = "Удалить корневую папку" + [admin.settings.general] description = "Настройте системные параметры приложения, включая брендинг и поведение по умолчанию." system = "Система" @@ -1704,6 +1879,9 @@ width = "Ширина" [app] description = "Бесплатная альтернатива Adobe Acrobat (10M+ загрузок)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Преобразовать в PDF/A-3b" convertToPdfA3bDescription = "Создаёт архивный PDF со встроенными вложениями" @@ -1842,6 +2020,15 @@ export = "Экспорт" accessDenied = "Доступ запрещен" insufficientPermissions = "У вас нет прав для выполнения этого действия." +[auth.callback] +completing = "Завершение аутентификации" +invalidToken = "Вход через OAuth не удался — недействительный токен." +missingToken = "Вход через OAuth не удался — токен не получен." +oauthFailed = "Вход через OAuth не удался. Повторите попытку." +pleaseWait = "Подождите, пока мы завершим вход." +signedOut = "Вы вышли из системы. Войдите снова." +windowMayClose = "Вы можете закрыть это окно после завершения." + [auth.displayName] guest = "Гость" user = "Пользователь" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Защищает PDF-документы, удаляя по description = "Этот инструмент автоматически переименует PDF-файлы на основе их содержимого. Он анализирует документ, чтобы найти наиболее подходящий заголовок из текста." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Автоповорот" +tags = "автоповорот,исправить ориентацию,вверх ногами,боком,выпрямить,ориентация,определить ориентацию,исправить поворот,автоориентация,сканированный,OSD" + +[autoRotate.confidenceThreshold] +title = "Порог уверенности" + +[autoRotate.detectionMode] +auto = "Авто" +osd = "Только OCR" +text = "Только текст" +title = "Метод определения" + +[autoRotate.error] +failed = "Произошла ошибка при автоповороте PDF." + +[autoRotate.inferUndetected] +title = "Повернуть страницы с низкой уверенностью для соответствия" + +[autoRotate.report] +applied = "{{degrees}}° по часовой стрелке" +confidenceValue = "Уверенность {{value}}" +noChange = "Без изменений" +pageLabel = "Страница {{number}}" +summary = "Повернуто {{rotated}} из {{total}} страниц, {{unchanged}} оставлено как есть" +title = "Отчет об определении" + +[autoRotate.report.method] +inferred = "Сопоставлено" +none = "Пропущено" +osd = "OCR" +text = "Текст" + +[autoRotate.report.note] +belowThreshold = "Ниже порога" +blankPage = "Страница пустая" +inferredFromDocument = "Как остальные страницы" +noDominantDirection = "Смешанные направления текста" +osdFailed = "Читаемый текст не найден" +osdNoVerdict = "Не удалось определить ориентацию" +tesseractUnavailable = "OCR не установлен" +tooFewGlyphs = "Слишком мало текста" + +[autoRotate.results] +title = "Результаты автоповорота" + +[autoRotate.settings] +title = "Настройки" + +[autoRotate.tooltip.confidenceThreshold] +text = "Насколько надежной должна быть проверка, прежде чем страница будет повернута. Уменьшите значение, чтобы поворачивать больше страниц, или увеличьте, чтобы оставлять больше страниц без изменений." +title = "Порог уверенности" + +[autoRotate.tooltip.description] +text = "Определяет, как должна быть ориентирована каждая страница, и поворачивает ее в правильное положение. Страницы, которые уже расположены правильно, остаются без изменений." + +[autoRotate.tooltip.detectionMode] +text = "Авто сначала читает текст на странице, затем, если этого недостаточно, анализирует страницу как изображение. Только текст пропускает проверку изображения — это быстрее всего для документов, которые никогда не сканировались. Только OCR обрабатывает каждую страницу как изображение." +title = "Метод определения" + +[autoRotate.tooltip.header] +title = "Автоповорот" + +[autoRotate.tooltip.inferUndetected] +text = "Пустые или почти пустые страницы нельзя проверить отдельно. Когда все страницы, которые удалось прочитать, требуют одинакового поворота, эти страницы поворачиваются в соответствии с ними." +title = "Повернуть страницы с низкой уверенностью для соответствия" + [autoSizeSplitPDF] tags = "pdf,разделение,документ,организация" @@ -2189,33 +2443,6 @@ keywords = "Ключевые слова: odd, even" numbers = "Числа/диапазоны: 5, 10–20" progressions = "Прогрессии: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Типы сертификатов" - -[certificateChoice.tooltip.organization] -bullet1 = "Управляется системными администраторами" -bullet2 = "Доступен авторизованным пользователям" -bullet3 = "Представляет компанию, а не конкретного человека" -bullet4 = "Лучше всего для: официальных документов, командных подписей" -description = "Общий сертификат вашей организации. Используется для корпоративной подписи." -title = "Сертификат организации" - -[certificateChoice.tooltip.personal] -bullet1 = "Генерируется автоматически при первом использовании" -bullet2 = "Привязан к вашей учётной записи" -bullet3 = "Нельзя поделиться с другими пользователями" -bullet4 = "Лучше всего для: личных документов, индивидуальной ответственности" -description = "Автоматически сгенерированный сертификат, уникальный для вашей учётной записи. Подходит для индивидуальных подписей." -title = "Личный сертификат" - -[certificateChoice.tooltip.upload] -bullet1 = "Требуются файл P12/PFX и пароль" -bullet2 = "Может быть выдан внешними центрами сертификации" -bullet3 = "Более высокий уровень доверия для юридических документов" -bullet4 = "Лучше всего для: юридически значимых контрактов, внешней валидации" -description = "Используйте собственный сертификат PKCS#12. Предоставляет полный контроль над свойствами сертификата." -title = "Загрузить собственный P12" - [certSign] awaitingSignatures = "Ожидание подписей" chooseCertificate = "Выберите файл сертификата" @@ -2542,34 +2769,6 @@ server = "Сервер" stepTitle = "Источник сертификата" upload = "Загрузить" -[certSign.tooltip.header] -title = "Управление подписями" - -[certSign.tooltip.overview] -bullet1 = "Проверяйте существующие подписи и их валидность" -bullet2 = "Просматривайте сведения о подписантах и сертификатах" -bullet3 = "Добавляйте новые цифровые подписи для защиты документов" -bullet4 = "Поддержка нескольких файлов с удобной навигацией" -text = "Этот инструмент позволяет проверить, подписаны ли ваши PDF, и добавить новые цифровые подписи. Цифровые подписи подтверждают автора/согласование и показывают, изменялся ли документ после подписания." -title = "Что умеет инструмент?" - -[certSign.tooltip.signing] -bullet1 = "Поддерживаются форматы PEM, PKCS12, JKS и серверные сертификаты" -bullet2 = "Можно показывать или скрывать подпись в PDF" -bullet3 = "Добавьте причину, местоположение и имя подписанта" -bullet4 = "Выберите страницу для видимых подписей" -bullet5 = "Используйте серверный сертификат для простого варианта «Подписать со Stirling-PDF»" -text = "Для подписи PDF нужен цифровой сертификат (например, PEM, PKCS12 или JKS). Вы можете сделать подпись видимой в документе или оставить невидимой — только для безопасности." -title = "Добавление подписей" - -[certSign.tooltip.validation] -bullet1 = "Показывает, действительны подписи или нет" -bullet2 = "Отображает информацию о подписанте и дату подписания" -bullet3 = "Проверяет, изменялся ли документ после подписания" -bullet4 = "Можно использовать пользовательские сертификаты для проверки" -text = "При проверке подписей инструмент сообщает, действительны ли они, кто подписал документ, когда он был подписан и менялся ли после подписания." -title = "Проверка подписей" - [changeCreds] changeUsername = "Обновите имя пользователя. После обновления вы будете выведены из системы." credsUpdated = "Учётная запись обновлена" @@ -2737,12 +2936,16 @@ text = "Чтобы сделать эти разрешения неизменяе [chat.actions] copy = "Копировать сообщение" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Параметры агента Stirling" +assistant = "AI Assistant" clearChat = "Очистить чат" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Эта операция использует ваши облачны tooltip = "Выбрать цвет с экрана" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Выберите цвет" [common] +actions = "Actions" back = "Назад" cancel = "Отмена" close = "Закрыть" +codeSample = "Code sample" collapse = "Свернуть" confirm = "Подтвердить" continue = "Продолжить" copied = "Скопировано!" copy = "Копировать" done = "Готово" +enterValue = "Enter value" error = "Ошибка" expand = "Развернуть" loading = "Загрузка..." next = "Далее" +open = "Open" preview = "Предпросмотр" previous = "Предыдущий" refresh = "Обновить" +remaining = "Осталось" retry = "Повторить" save = "Сохранить" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Очистить выбранное" @@ -2988,6 +3199,7 @@ title = "Метод сжатия" [compress.settings] desiredSize = "Желаемый размер файла" desiredSizePlaceholder = "Введите размер" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Сжатие — простой способ уменьшить размер файла. Выберите «Размер файла», чтобы указать целевой размер, а мы подберём качество. Выберите «Качество», чтобы задать силу сжатия вручную." @@ -3190,6 +3402,24 @@ pdfOptions = "Параметры PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Сначала выберите исходный формат" settings = "Настройки" single = "Один" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Расширенный мониторинг" -allInOneWorkspace = "Единое рабочее пространство PDF (просмотрщик, инструменты и агент)" -apiSandbox = "Песочница API" -dedicatedSupportSlas = "Выделенная поддержка и SLA" -fasterThroughput = "Пропускная способность в 10 раз выше" -fullyPrivateFiles = "Полностью приватные файлы" -largeFileProcessing = "Обработка больших файлов" -orgWideAccess = "Контроль доступа на уровне организации" -premiumAiModels = "Премиальные AI-модели" -prioritySupport = "Приоритетная поддержка" -privateDocCloud = "Приватное облако документов" -ragFineTuning = "RAG + дообучение" -secureApiAccess = "Безопасный доступ к API" -standardThroughput = "Стандартная пропускная способность" -unlimitedApiAccess = "Неограниченный доступ к API" -unlimitedSeats = "Неограниченное число пользователей" - [crop] autoCrop = "Автообрезка пустых полей" noFileSelected = "Выберите PDF-файл, чтобы начать обрезку" @@ -3346,6 +3558,7 @@ message = "Не удалось назначить обработчик PDF по title = "Ошибка" [defaultApp.prompt] +dontRemind = "Больше не напоминать" message = "Сделайте Stirling PDF приложением по умолчанию для открытия файлов PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Поиск..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Изменить" @@ -3644,11 +3861,13 @@ shareSelected = "Поделиться выбранными" sharing = "Общий доступ" showAll = "Показать все" showHistory = "Показать историю" +sortBy = "Sort files" sortByDate = "Сортировать по дате" sortByName = "Сортировать по имени" sortBySize = "Сортировать по размеру" storage = "Хранилище" storageState = "Хранилище" +storageUsed = "Storage used" synced = "Синхронизировано" title = "Загрузить PDF-файлы" toolChain = "Применённые инструменты" @@ -3694,13 +3913,17 @@ addFiles = "Добавить файлы" addingFiles = "Добавление файлов…" collapse = "Свернуть боковую панель" customizeGroups = "Настроить группы" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Откройте файлы, чтобы начать" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Развернуть боковую панель" -files = "Файлы" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive не настроен" leaveMyFiles = "Выйти из Моих файлов" +library = "Библиотека PDF" myFiles = "Мои файлы" noFiles = "Файлов пока нет" openFileManager = "Открыть файловый менеджер" @@ -3708,37 +3931,36 @@ openFromComputer = "Открыть с компьютера" openSettings = "Открыть настройки" other = "Другое" recent = "Недавние" -search = "Поиск" -searchPlaceholder = "Поиск файлов..." viewAll = "Показать все {{count}} файлов" [fileSidebar.fileItem] closeViewer = "Закрыть просмотрщик" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Открыть в просмотрщике" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Добавить" -addLabel = "Добавить метку…" -categoryIconAria = "Выберите значок для {{name}}" -createCategory = "Создать категорию" -delete = "Удалить категорию" done = "Готово" -hide = "Скрыть категорию" -newCategory = "Название новой категории…" -removeLabel = "Удалить {{name}}" -rename = "Переименовать" reset = "Сбросить к значениям по умолчанию" -search = "Искать метки…" -show = "Показать категорию" subtitle = "Группируйте файлы по родительским категориям. Добавляйте существующие или новые метки в категорию, переименовывайте её или создавайте свою. Файлы, не входящие ни в одну из ваших категорий, отображаются в разделе «Другое»." title = "Категории боковой панели" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Добавить в рабочую область" addToWorkspaceCount = "Добавить {{count}} в рабочую область" @@ -3749,7 +3971,6 @@ backToMyFiles = "Назад к Моим файлам" breadcrumbs = "Путь к папке" cancel = "Отмена" classification = "Классификация" -clearSearch = "Очистить поиск" clearSelection = "Очистить выбор" closeDetails = "Закрыть сведения" create = "Создать" @@ -3781,6 +4002,7 @@ downloadAll = "Скачать все" downloadVersion = "Скачать эту версию" dropOverlay = "Перетащите файлы для загрузки" dropOverlaySub = "Файлы сначала попадают в Локальные. Используйте «Переместить в» или «Сохранить в облако», чтобы разместить их в папке." +duplicate = "Duplicate" file = "Файл" fileInfo = "File info" fileMenu = "Действия с файлом" @@ -3808,13 +4030,12 @@ refresh = "Обновить с сервера" remove = "Удалить" removeVersion = "Удалить эту версию" rename = "Переименовать" +renamed = "Переименовано" renameFolder = "Переименовать папку" resizeFolderTree = "Изменить размер дерева папок (стрелки, Shift — большие шаги; двойной щелчок — автоподбор)" save = "Сохранить" saveToServer = "Сохранить на сервер" saveToServerDisabledHint = "Сохранение на сервер не включено на этом сервере. Попросите администратора включить его." -search = "Поиск" -searchPlaceholder = "Искать в этой папке и подпапках" selectAll = "Выбрать всё" selectAllHint = "Нажмите, чтобы выбрать всё. Совет: удерживайте Ctrl (или Cmd), чтобы добавлять файлы по одному, Shift — для выбора диапазона." selectedCount = "Выбрано: {{count}}" @@ -3874,6 +4095,10 @@ title = "Пока нет облачных файлов" hint = "Файлы, сохранённые без загрузки, остаются здесь. Перетащите файл, чтобы добавить его." title = "Нет локальных файлов" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Здесь будут отображаться файлы, которые вы открывали или редактировали." title = "Пока ничего не изменено" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Не удалось удалить папку." deleteFolderFailedDetail = "Не удалось удалить папку: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Не удалось обновить оформление папки." folderAppearanceFailedDetail = "Не удалось обновить оформление папки: {{message}}" moveFilesFailed = "Не удалось переместить файлы." @@ -3910,9 +4137,7 @@ count = "Файлы" folder = "Папка" labels = "Метки" modified = "Изменено" -name = "Имя" size = "Размер" -toolHistoryAtVersion = "Совокупная цепочка инструментов" totalSize = "Общий размер" type = "Тип" versionHistory = "Путь версий" @@ -3945,6 +4170,11 @@ localHint = "Хранится только в этом браузере" shared = "Общий доступ" sharedHint = "Предоставлен по ссылке" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Сначала старые" modifiedDesc = "Сначала новые" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "В хранилище нет файлов. Сначал open = "Открыть" openFile = "Открыть файл" openFiles = "Открыть файлы" +selectFile = "Select {{name}}" selectFromStorage = "Выбрать из хранилища" upload = "Загрузить" uploadFile = "Загрузить файл" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Все сохранено" +analyzingFields = "Analysing form fields..." +extractCsvError = "Не удалось извлечь CSV" +extractXlsxError = "Не удалось извлечь XLSX" +filled = "filled" flattenAfterFilling = "Сплющить после заполнения" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "обяз." +requiredFieldsError = "Заполните все обязательные поля" rescanFields = "Пересканировать поля" rescanFormFields = "Пересканировать поля формы" save = "Сохранить" +saveShortcut = "Ctrl+S для сохранения" +unsavedChanges = "Несохраненные изменения" [formFill.sidebar] close = "Закрыть боковую панель" @@ -4309,6 +4552,11 @@ desc = "Автоматически переименовывает PDF по об tags = "автообнаружение,заголовок,организация,переименование" title = "Автопереим. PDF" +[home.autoRotate] +desc = "Определяйте ориентацию каждой страницы и автоматически поворачивайте ее вертикально." +tags = "автоповорот,исправить ориентацию,вверх ногами,боком,выпрямить,ориентация,определить ориентацию,исправить поворот,автоориентация,сканированный,OSD" +title = "Автоповорот" + [home.autoSizeSplitPDF] tags = "авто,разделить,размер" title = "Авторазбить по размеру/стр." @@ -4608,15 +4856,12 @@ hint = "Загрузите PNG, JPG, SVG или другой файл изобр label = "Загрузить изображение" placeholder = "Выберите файл изображения" -[imageToPdf] -tags = "конвертация,изображение,jpg,картинка,фото" - [imageToPDF] header = "Изображение в PDF" title = "Изображение в PDF" -[infoBanner] -dismiss = "Закрыть" +[imageToPdf] +tags = "конвертация,изображение,jpg,картинка,фото" [invite] acceptError = "Не удалось создать учетную запись" @@ -4647,11 +4892,9 @@ welcomeTitle = "Вас пригласили!" [landing] addFiles = "Добавить файлы" -heroSubtitle = "Перетащите или добавьте существующий PDF, чтобы начать." mobileUpload = "Загрузка с мобильного" openFromComputer = "Открыть с компьютера" uploadFromComputer = "Загрузить с компьютера" -workbenchEmptyStateHero = "Перетащите PDF куда угодно" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Введите имя пользователя" failedToSignIn = "Не удалось войти через {{provider}}: {{message}}" forgotPassword = "Забыли пароль?" loggingIn = "Вход..." -logIn = "Войти" login = "Войти" magicLinkSent = "Магическая ссылка отправлена на {{email}}! Проверьте почту и перейдите по ссылке для входа." mfaCode = "Код аутентификации" @@ -4715,26 +4957,10 @@ signInWith = "Войти с помощью" title = "Вход" unexpectedError = "Непредвиденная ошибка: {{message}}" updatePassword = "Обновить пароль" -useEmailInstead = "Войти по электронной почте" useMagicLink = "Использовать магическую ссылку" username = "Имя пользователя" youAreLoggedIn = "Вы вошли в систему!" -[login.slides.edit] -alt = "Редактирование PDF" -subtitle = "С более чем дюжиной инструментов для скрытия данных, подписания, чтения и обработки PDF вы точно найдёте нужное." -title = "Редактируйте PDF, чтобы показывать и защищать нужную информацию" - -[login.slides.overview] -alt = "Обзор Stirling PDF" -subtitle = "Облачный набор с приоритетом приватности для работы с PDF: конвертируйте, подписывайте, скрывайте данные и управляйте документами, а также 50+ других мощных инструментов." -title = "Всё для работы с PDF в одном месте." - -[login.slides.secure] -alt = "Защита PDF" -subtitle = "Легко добавляйте пароли, скрывайте содержание и управляйте сертификатами." -title = "Защитите конфиденциальные данные в ваших PDF" - [margin] large = "Большой" medium = "Средний" @@ -4818,6 +5044,34 @@ uploadSuccess = "Загрузка выполнена!" uploadSuccessMessage = "Ваши изображения переданы." validating = "Проверка сеанса..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Сканируйте для загрузки фото. Изображения автоматически преобразуются в PDF." descriptionNoConvert = "Сканируйте, чтобы загрузить фото с мобильного устройства." @@ -4836,8 +5090,13 @@ title = "Загрузка с мобильного" tags = "Мультиинструмент,Многооперационный,UI,перетаскивание,клиентская часть,интерактивный" title = "Мультиинструмент PDF" -[navbar] -search = "Поиск" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Аутентификация не выполнена. Закройте окно и попробуйте снова." @@ -4966,6 +5225,7 @@ activeFiles = "Вид Активные файлы показы allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Закрыть" cropSettings = "Теперь, когда мы выбрали файл для обрезки, можно настроить инструмент «Обрезка», чтобы выбрать область, до которой нужно обрезать PDF." +dialogLabel = "Onboarding" fileCheckbox = "Щелчок по файлу выбирает его для обработки. Можно выбрать несколько файлов для пакетных операций." fileReplacement = "Измененный файл автоматически заменит исходный файл в Рабочей области, что позволит легко прогнать его через другие инструменты." filesButton = "Кнопка Файлы на панели быстрого доступа позволяет загружать PDF, с которыми будут работать инструменты." @@ -4976,6 +5236,7 @@ pinButton = "Вы можете использовать кнопку З results = "После завершения работы инструмента шаг Проверка покажет предпросмотр результатов в этой панели и позволит отменить операцию или скачать файл." runButton = "После настройки инструмента эта кнопка позволяет запустить инструмент для всех выбранных PDF." selectCropTool = "Давайте выберем инструмент Обрезка, чтобы показать, как пользоваться одним из инструментов." +stepOf = "Шаг {{current}} из {{total}}" toolInterface = "Это интерфейс инструмента Обрезка. Как видите, здесь пока немного элементов, потому что мы еще не добавили PDF-файлы для работы." workbench = "Это Рабочая область — основная зона, где вы просматриваете и редактируете свои PDF." wrapUp = "Готово! Вы узнали об основных областях приложения и о том, как ими пользоваться. Нажимайте кнопку Справка в любое время, чтобы снова посмотреть этот тур." @@ -4988,6 +5249,26 @@ showMeAround = "Показать обзор" skipForNow = "Пока пропустить" skipTheTour = "Пропустить тур" +[onboarding.checklist] +dismiss = "Закрыть" +title = "Настройте Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Запустите Stirling нативно на своем компьютере" +title = "Скачать Stirling для Desktop" + +[onboarding.checklist.inviteTeam] +description = "Работайте совместно со своей командой" +title = "Пригласить участников команды" + +[onboarding.checklist.shareAnalytics] +description = "Помогите улучшить Stirling" +title = "Делиться анонимными данными использования" + +[onboarding.checklist.takeTour] +description = "Посмотрите, как работает Stirling, в коротком обзоре" +title = "Пройти обзор" + [onboarding.desktopInstall] body = "Stirling лучше всего работает как настольное приложение. Можно работать офлайн, быстрее открывать документы и редактировать их локально на компьютере." selectOs = "Выберите операционную систему" @@ -4996,7 +5277,21 @@ titleWithOs = "Скачать для {{osLabel}}" [onboarding.mfa] authenticationCode = "Код аутентификации" +description = "Защитите свой аккаунт, подключив приложение-аутентификатор. Отсканируйте QR code или введите ключ настройки, затем подтвердите 6-значный код для завершения." +enable = "Включить MFA" +enableError = "Не удалось включить двухфакторную аутентификацию. Проверьте код и повторите попытку." +enterCode = "Введите 6-значный код из вашего приложения." +enterCodeError = "Введите код аутентификации, чтобы продолжить." +logout = "Выйти" +openAuthenticator = "Откройте Google Authenticator, Authy или 1Password." qrCodeLoading = "Генерация вашего QR‑кода…" +regenerateQrCode = "Создать QR code заново" +scanQrCode = "Отсканируйте QR code или введите ключ настройки ниже." +setupError = "Не удалось начать настройку двухфакторной аутентификации. Повторите попытку." +setupKey = "Ключ настройки (ручной ввод)" +stepByStep = "Пошагово" +success = "MFA включена. Теперь вы можете продолжить." +title = "Настройка многофакторной аутентификации" [onboarding.planOverview] adminBodyLoginDisabled = "После включения режима входа вы сможете управлять пользователями, настраивать параметры и мониторить состояние сервера. Первые {{freeTierLimit}} человек на вашем сервере используют Stirling бесплатно." @@ -5005,6 +5300,11 @@ adminTitle = "Обзор для админа" userBody = "Приглашайте коллег, назначайте роли и держите документы в порядке в одном безопасном рабочем пространстве. Включите режим входа, когда будете готовы выйти за рамки сольного использования." userTitle = "Обзор тарифа" +[onboarding.processorIntro] +body = "Stirling теперь выполняет политики — автоматизированные правила, которые классифицируют, защищают и обрабатывают каждый документ по мере его поступления. Настраивайте их и отслеживайте выполнения в Процессоре." +cta = "Посмотреть Процессор" +title = "Посмотрите Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Предоплаченный год" + +[payg.prepaid.banner] +exhausted = "Ваш предоплаченный объем исчерпан; тарификация по счетчику возобновлена. Пополните, чтобы сохранить скидку." +expiring_one = "Ваш предоплаченный год заканчивается через {{count}} день. Неиспользованный объем истекает — пополните, чтобы начать новый год." +expiring_other = "Ваш предоплаченный год заканчивается через {{count}} дней. Неиспользованный объем истекает — пополните, чтобы начать новый год." +low = "У вас осталось {{pct}}% предоплаченного объема. Пополните в любое время, чтобы обработка не прерывалась." + +[payg.prepaid.card] +subtitle = "Используется до тарификации по счетчику и вне вашего лимита расходов." +title = "Предоплаченный объем" + +[payg.prepaid.meter] +capSuffix = "из {{total}} предоплаченных кредитов" +expires = "Истекает {{date}}" + +[payg.prepaid.state] +exhausted = "Исчерпано" +healthy = "Осталось много" +low = "Заканчивается" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF в PDF/A" tags = "архив,долгосрочный,стандарт,конвертация,хранение,сохранение" title = "PDF в PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "печать,стандарт,конвертация,производство,допечатная подготовка,архив" title = "PDF в PDF/X" @@ -5822,6 +6151,7 @@ small = "500 кредитов" xsmall = "100 кредитов" [plan.availablePlans] +currency = "Billing currency" subtitle = "Выберите план, подходящий вашим потребностям" title = "Доступные планы" @@ -5937,49 +6267,12 @@ keyDescription = "Вставьте лицензионный ключ из пис success = "Лицензия активирована!" successMessage = "Ваша лицензия успешно активирована. Теперь можно закрыть это окно." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "применено" -enforcing = "Применение политики..." -failed = "Сбой применения" outputsUnavailable = "Результаты политики больше недоступны для скачивания." partialOutputsUnavailable = "Некоторые результаты политики больше недоступны для скачивания." -retrying = "Занято, повторяем попытку..." runNotFound = "Не удалось найти запуск применения." -step = "шаг {{current}}/{{total}}" timedOut = "Время ожидания истекло до завершения запуска." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Закрыть" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Применение {{names}}" applyingProgress = "Применение {{names}} ({{done}} из {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} из {{total}} файлов не удалось об failureTitle = "Экспортировано без полного применения политики" printPolicyAppliedBody = "Этот PDF был обновлён в соответствии с политикой. Проверьте изменения и распечатайте снова." printPolicyAppliedTitle = "Политика применена перед печатью" -queued = "+{{count}} в очереди" successTitle = "Применено: {{names}}" summaryMore = "{{first}}, {{second}} и ещё {{more}}" summaryTwo = "{{first}} и {{second}}" -[policies.enforcement.triggerVerb] -convert = "Применение политики перед преобразованием" -default = "Применение политики" -export = "Применение политики перед экспортом" -input = "Применение политики при импорте" -print = "Применение политики перед печатью" - -[policies.field] -accessLog = "Журнал доступа" -archiveAfter = "Архивировать через" -auditTrail = "Аудиторский след" -belowThreshold = "Ниже порога" -destination = "Назначение" -frameworks = "Фреймворки" -immutableHold = "Неизменяемое удержание" -keepFor = "Хранить в течение" -minConfidence = "Мин. уверенность" -notify = "Уведомлять при маршрутизации" -onViolation = "При несоответствии" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Добавить" -addPlaceholder = "Добавить метку…" -customNote = "Настроено для вашей команды." -defaultNote = "Используется встроенное значение по умолчанию, общее для вашей команды." -duplicate = "\"{{name}}\" уже существует." -edit = "Редактировать метки" -empty = "Меток пока нет." -export = "Экспорт JSON" -iconAria = "Выберите значок для {{name}}" -import = "Импорт JSON" -importError = "Не удалось импортировать этот файл." -managedNote = "Метки команды управляются руководителем вашей команды." -modalSubtitle = "Общие для всей вашей команды. Классификатор выбирает метки, подходящие для каждого документа." -modalTitle = "Метки классификации" -pickIcon = "Использовать значок {{label}}" -removeAria = "Удалить {{name}}" -resetToDefault = "Сбросить к значению по умолчанию" -saveForTeam = "Сохранить для команды" -saving = "Сохранение…" -sectionLabel = "Метки классификации" -startFromScratch = "Начать с нуля" -teamCount = "метки команды" -tooLong = "Метки могут содержать не более {{max}} символов." -ungrouped = "Без группы" -view = "Показать метки" +categoryCount = "категории" +hideCategory = "Скрыть категорию" +labelCount = "метки" +sharedNote = "Эти метки встроены и используются всей вашей командой." +showCategory = "Показать категорию" [policies.pii] account = "Номера счетов (с пометкой)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Маршрутные номера США (ABA)" ssn = "Номера социального страхования" -[policies.settings] -noneExport = "Сейчас при экспорте не выполняются политики." -noneUpload = "Сейчас при загрузке не выполняются политики." -onExport = "При экспорте" -onUpload = "При загрузке" -reorderHandle = "Перетащите, чтобы изменить порядок" -runOrderDesc = "Когда для одного триггера выполняется несколько политик, они выполняются в этом порядке — каждая с результатом предыдущей политики. Перетащите, чтобы изменить порядок." -title = "Настройки политик" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Параметры политики" -policySettings = "Настройки политик" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Повторить неудачные политики ({{count}})" -rowProgress = "Обработано {{completed}} из {{total}} файлов" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Что такое политика?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Автоматически находит и замазывает конфиденциальные данные — например, номера социального страхования и карт — чтобы их нельзя было прочитать в документе." -sanitize = "Удаляет скрытый JavaScript из файла, чтобы при открытии ничего не запускалось автоматически." -watermark = "Наносит видимую отметку (например, «Confidential») на каждую страницу." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Закрыть" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} применяется..." badgeRan = "Политика {{name}} выполнена для этого файла" +badgeRunning = "{{name}} выполняется..." blockingAction = "{{action}} заблокировано на время применения политики, подождите..." dismiss = "Закрыть оверлей" enforcingTitle = "Применение политики..." @@ -6151,6 +6316,7 @@ viewAnyway = "Показать файл (политика всё ещё прим [policyOption] autoRedactPhi = "Автоматически редактировать PHI" blockExport = "Заблокировать экспорт" +ccpa = "CCPA" contracts = "Контракты" documents = "Документы" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "План Editor" subscribed = "План Processor" unlinked = "Не привязан" -[portal.agentBuilder] -bootstrapFromDocument = "Создать из документа" -sectionsAriaLabel = "Разделы конструктора агентов" -selectorAriaLabel = "Агенты" -subtitle = "Проектируйте, тестируйте и выпускайте AI-агентов, которые классифицируют, извлекают данные из документов и маршрутизируют их. Определяйте сценарии, ограничивайте доступ к инструментам, запускайте эталонный набор и публикуйте версию." -title = "Конструктор агентов" - -[portal.agentBuilder.bootstrap] -cancel = "Отмена" -dropzoneText = "Выберите пример документа (PDF или изображение)" -lead = "Перетащите пример документа, и мы предложим сценарии и схему извлечения, которые можно уточнить. Ничего не публикуется, пока вы это не проверите." -submit = "Создать агента" -subtitle = "Создайте нового агента на основе одного репрезентативного файла" -title = "Создать из документа" - -[portal.agentBuilder.empty] -description = "Создайте агента из примера документа, чтобы сформировать его сценарии и схему извлечения, затем доработайте и опубликуйте." -title = "Агентов пока нет" - -[portal.agentBuilder.evals] -casesPassing = "Пройденные случаи" -columnCase = "Случай eval" -columnLatency = "Задержка" -columnResult = "Результат" -fail = "не пройдено" -goldenSetPassRate = "Процент прохождения golden set" -latencyMs = "{{ms}} ms" -notRun = "не запускалось" -pass = "пройдено" -passRate = "Процент прохождения" -runEvals = "Запустить evals" - -[portal.agentBuilder.evals.empty] -description = "Evals превращают ваши сценарии в повторяемый golden set. Обновите план, чтобы отслеживать процент прохождения со временем и ограничивать публикации по нему." -title = "Golden set пока нет" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "по golden sets" -activeAgents = "Активные агенты" -avgPassRate = "Средний процент прохождения eval" -fleetWide = "по всему парку" -latestPublished = "Последняя опубликованная" -scenarios = "Сценарии" -testCases = "тестовые случаи" -totalDescription_one = "{{count}} всего" -totalDescription_other = "{{count}} всего" - -[portal.agentBuilder.scenarios] -add = "Добавить" -addScenario = "Добавить сценарий" -enable = "Включить" -expectationLabel = "Ожидаемое поведение" -expectationPlaceholder = "Что должен делать агент" -inEval = "в eval" -mute = "Отключить" -muted = "отключено" -nameLabel = "Название" -namePlaceholder = "например, Эскалация соответствия" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Сценарии" -tools = "Инструменты" -versions = "Версии" - -[portal.agentBuilder.tools] -broadAccess = "Широкий доступ" -deniedHint = "Выбранные инструменты заблокированы. Всё остальное остаётся доступным для вызова." -deniedTools = "Запрещённые инструменты" -governanceGate = "Управление инструментами доступно на плане Enterprise." -restricted = "Ограничено" -restrictedAccess = "Ограниченный доступ к инструментам" -restrictedDescription = "Разрешить все инструменты, кроме запрещённых ниже." - -[portal.agentBuilder.versions] -current = "текущая" -historyGate = "Полная история версий и откат доступны на плане Enterprise." -publish = "Опубликовать" -rollBack = "Откатить" - [portal.assistant] close = "Закрыть ассистента" error = "Не удалось связаться с ассистентом." @@ -6365,9 +6451,21 @@ typing = "Печатает" loading = "Загрузка" redirectingToEditor = "Перенаправление в редактор..." +[portal.billing.activation] +maybeLater = "Может быть позже" +title = "Включить Процессор" + +[portal.billing.activation.payg] +sub = "Установите месячный лимит. Платите за то, что используете." +title = "Оплата по факту" + +[portal.billing.activation.prepay] +badge = "2 месяца бесплатно" +sub = "Один счет, карта или банковский перевод." +title = "Предоплатить год" + [portal.billing.checkout] noClientSecret = "Edge function не вернула client_secret." -subtitle = "Добавьте карту, чтобы продолжить после бесплатного лимита плана Editor. Остальное обработает Stripe." title = "Включить план Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Платёж прошёл успешно, но активация зан close = "Закрыть" title = "Почти готово" +[portal.billing.checkout.cap] +amountAria = "Месячный лимит расходов" +back = "Назад" +continue = "Перейти к оплате" +error = "Не удалось установить лимит расходов" +finePrint = "Счета выставляются ежемесячно. Отменить можно в любое время." +noLimit = "Без лимита" +note = "С вас никогда не спишут больше лимита. Обработка просто приостановится." +perMonth = "/ мес." +title = "Установите лимит расходов" + +[portal.billing.checkout.card] +fields = "Номер карты · MM / YY · CVC · ZIP" +label = "Данные карты" +note = "Данные карты собираются Stripe. Stirling никогда не хранит PAN или CVC." + [portal.billing.checkout.error] title = "Не удалось начать оформление" @@ -6383,10 +6497,9 @@ body = "Ваш платёж прошёл. Мы включаем тарифици hint = "Пожалуйста, не закрывайте это окно." title = "Активация вашего плана Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "в env портала, чтобы включить оформление в приложении." -bodyBefore = "Установите" -title = "Stripe не настроен" +[portal.billing.checkout.pay] +limit = "Месячный лимит расходов {{amount}}/мес. Обработка приостанавливается при достижении лимита." +limitNoCap = "Без лимита расходов. Обработка не будет приостанавливаться. Счета выставляются ежемесячно." [portal.billing.enterpriseUpsell] cta = "Собрать предложение Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Текущий план" freeForever = "Бесплатно навсегда" noTeamResolved = "Для вашего кошелька ещё не определена команда — обновите страницу и попробуйте снова." ownerOnly = "Только владелец команды может включить план Processor." +payInvoice = "Оплатить счет для завершения" planName = "Editor" ssoIncluded = "SSO включён" switchOnProcessor = "Включить Processor →" unlimitedUsers = "Неограниченное число пользователей" +viewQuote = "Посмотреть предложение" [portal.billing.invoices] columnAmount = "Сумма" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Прямые API-запросы" segmentApiLabel = "API" segmentAutomationDesc = "Автоматизации и конвейеры" segmentAutomationLabel = "Автоматизация" +sizeMultiplier = "{{formatted}} PDF использовали множитель размера" +summary = "{{unique}} уникальных · {{units}} единиц счетчика · {{avg}} в среднем на PDF" +summaryNoRate = "{{unique}} уникальных · {{units}} единиц счетчика" unit = "тарифицируемые PDF" +unitsPending_one = "{{units}} единица счетчика ожидает синхронизации из связанных экземпляров" +unitsPending_other = "{{units}} единиц счетчика ожидают синхронизации из связанных экземпляров" + +[portal.billing.prepaid] +capSuffix = "из {{total}} предоплаченных кредитов" +expires = "Истекает {{date}}" +eyebrow = "Предоплаченный объем" +note = "Используется до тарификации по счетчику и вне вашего лимита расходов." +topUp = "Пополнить" + +[portal.billing.prepaid.buy] +back = "Назад" +calcTitle = "Рассчитайте годовой платеж" +cancel = "Отмена" +cancelPurchase = "Отменить покупку" +close = "Закрыть" +continueToPayment = "Перейти к оплате" +finalise = "Завершить" +invoiceErrorTitle = "Не удалось создать счет" +loadingTitle = "Загрузка вашего предложения" +payTitle = "Оплатите свой год" +step = "Шаг {{current}} из {{total}}" + +[portal.billing.prepaid.calc] +change = "Изменить" +deployRow = "Развертывание" +done = "Готово" +enterpriseHint = "Это корпоративный масштаб. При таком объеме корпоративные тарифы выгоднее любой скидки самообслуживания. Фиксация тарифа, условия и предложение за минуты." +governanceRow = "Управление" +governanceValue = "{{label}} · {{count}} политик" +handlesLabel = "Ваш Процессор" +handlesValue = "обрабатывает {{volume}} кредитов / мес." +pipelinesLabel = "Конвейеры" +policiesMeta = "{{count}} политик" +poolCaption = "Один пул из {{credits}} кредитов на год. Тяжелые месяцы используют остаток легких." +priceLabel = "Ваш год · 12 месяцев по цене 10" +rateUnknown = "Мы покажем точную цену при оформлении заказа." +savings = "Вы экономите {{amount}} · 2 месяца бесплатно." +sizingRow = "Размер" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Всего пользователей" + +[portal.billing.prepaid.consent] +label = "Я понимаю, что когда мой предоплаченный объем будет исчерпан или истечет через 12 месяцев, обработка автоматически продолжится по стандартному тарифу оплаты по факту со счетчиком (в пределах моего лимита расходов), если я не отменю, и что я могу отменить в любое время из billing portal." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Управляется Stirling. Запуск за минуты." +cloudValue = "Stirling Cloud · Управляемый" +selfhost = "Самостоятельно размещенный" +selfhostDesc = "Работает на частной инфраструктуре. Тот же тариф." +selfhostValue = "Самостоятельно размещенный · Частная инфраструктура" + +[portal.billing.prepaid.offer] +cta = "Посмотреть предложение" +subtitle = "Предоплатите год обработки PDF и получите два месяца бесплатно; используется до тарификации по счетчику и вне вашего лимита расходов." +title = "Получите 12 месяцев по цене 10" + +[portal.billing.prepaid.pay] +accountName = "Имя владельца аккаунта" +accountNamePlaceholder = "например Jane Smith" +companyName = "Название компании (необязательно)" +companyNamePlaceholder = "например Acme Inc." +downloadInvoice = "Скачать счет" +payOnline = "Оплатить онлайн" +poNumber = "Номер PO (необязательно)" +poNumberPlaceholder = "например PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Расширенные" +advancedDesc = "Конвейеры выполняют большую часть вашей обработки." +none = "Нет" +noneDesc = "Конвейеры пока не запущены. Включите их в любое время." +standard = "Стандартные" +standardDesc = "Несколько конвейеров повторно обрабатывают поступающие PDF." + +[portal.billing.prepaid.posture] +essentials = "Основное" +essentialsDesc = "Классификация, по умолчанию, плюс общий доступ" +governed = "Управляемое" +governedDesc = "Добавляет безопасность и маршрутизацию" +regulated = "Регулируемое" +regulatedDesc = "Каждая категория, включая соответствие требованиям, хранение, прием" + +[portal.billing.prepaid.review] +download = "Скачать предложение (PDF)" +downloading = "Подготовка…" +share = "поделиться для согласования" + +[portal.billing.prepaid.size] +compact = "Компактный" +compactDesc = "Файлы менее 25 MB, без платы за данные" +heavy = "Тяжелый" +heavyDesc = "Сканированные или насыщенные изображениями, обычно 50 MB+" +standard = "Стандартный" +standardDesc = "В основном небольшие, некоторые сканы более 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Исчерпано" +healthy = "Осталось много" +low = "Заканчивается" [portal.billing.spendLimit] adjustLimit = "Изменить лимит" @@ -6494,6 +6713,8 @@ label = "Прогнозируется превышение." [portal.billing.spendThisMonth] eyebrow = "Расходы в этом месяце" +freeRemaining_one = "Остался {{formatted}} бесплатный PDF" +freeRemaining_other = "Осталось {{formatted}} бесплатных PDF" processed_one = "Обработано {{formattedCount}} PDF." processed_other = "Обработано {{formattedCount}} PDF." processedWithRate_one = "Обработано {{formattedCount}} PDF, по {{rate}} за каждый." @@ -6510,6 +6731,7 @@ reachedTitle = "Месячный лимит расходов достигнут" title = "Не удалось открыть портал Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "из {{allowance}} бесплатных PDF использовано" capSuffix_other = "из {{allowance}} бесплатных PDF использовано" eyebrow = "Пробный период Processor" @@ -6534,6 +6756,376 @@ signature = "подпись" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Новое подключение" +createTitleFor = "Подключить {{name}}" +delete = "Удалить" +edit = "Редактировать" +editTitle = "Редактировать подключение" +unsupportedType = "Этот тип подключения нельзя редактировать здесь." + +[portal.connections.categories.advanced] +description = "Для сервиса, который мы пока не поддерживаем." +label = "Расширенные" + +[portal.connections.categories.audit] +description = "Записывайте, что произошло с документом, там, где ваши аудиторы уже проверяют." +label = "Журнал аудита и соответствия требованиям" + +[portal.connections.categories.notify] +description = "Сообщите другой системе, что что-то произошло." +label = "Уведомления и рабочий процесс" + +[portal.connections.categories.security] +description = "Сканируйте или помечайте документ перед его дальнейшей передачей." +label = "Безопасность и классификация" + +[portal.connections.categories.signing] +description = "Отправьте документ на подпись или нотариальное заверение." +label = "Электронная подпись и нотариальное заверение" + +[portal.connections.categories.storage] +description = "Куда попадают обработанные документы — в bucket, диск, ticket или страницу." +label = "Файлы и вложения" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling хранит это в зашифрованном виде и больше никогда не показывает." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Шаги, использующие это подключение, могут вызывать только пути в рамках этого URL, но не другой host." +label = "URL сервера" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Пароль" + +[portal.connections.commonFields.port] +label = "Порт" + +[portal.connections.commonFields.username] +label = "Имя пользователя" + +[portal.connections.commonFields.webhookUrl] +helperText = "Вставьте URL, который предоставил сервис. Можно вызывать только пути в его рамках." +label = "Webhook URL" + +[portal.connections.fields] +name = "Название подключения" +namePlaceholder = "например Production" + +[portal.connections.picker] +cancel = "Отмена" +createNew = "Новое подключение..." +placeholder = "Выберите подключение" +save = "Сохранить подключение" + +[portal.connections.picker2] +back = "Изменить интеграцию" +heading = "Что вы хотите подключить?" +noResultsBody = "Мы пока это не поддерживаем — расскажите нам об этом, и мы рассмотрим возможность добавления." +noResultsTitle = "Нет совпадений для \"{{query}}\"" +searchPlaceholder = "Ищите интеграции — попробуйте \"sign\", \"ocr\" или название продукта" + +[portal.connections.types.api] +description = "Вызывайте любой HTTP API из политики. Вы выбираете URL, аутентификацию и полезную нагрузку." +label = "Пользовательский API" + +[portal.connections.types.api.fields.authType] +label = "Аутентификация" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Имя пользователя и пароль" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Токен в пользовательском заголовке" + +[portal.connections.types.api.fields.authType.options.none] +label = "Нет" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Войти для получения токена" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Шаги, использующие это подключение, могут вызывать только пути в рамках этого URL, но не другой host." +label = "Базовый URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Имя заголовка" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Путь входа" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Пароль" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Если API отвечает ссылкой для скачивания, получать данные можно только с этих hosts. Разделяйте запятыми. Оставьте пустым, чтобы разрешить только собственный host базового URL." +label = "Разрешенные hosts результата" + +[portal.connections.types.api.fields.token] +label = "Токен" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Отправлять токен как этот заголовок" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Заголовок ответа, в котором вызов входа возвращает токен, например X-Auth-Token." +label = "Заголовок ответа с токеном" + +[portal.connections.types.api.fields.username] +label = "Имя пользователя" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Сканируйте документы на вредоносное ПО на сервере, которым вы управляете — ничего не покидает вашу сеть." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Проверка документов на вирусы и содержимое в политике." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Блокируйте документы, содержащие макросы, исполняемые файлы, скрипты или встроенные объекты, — не только известное вредоносное ПО." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Прикрепляйте обработанные документы к странице Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Отправляйте документы в ConsignO Cloud (Notarius) для электронной подписи и нотариального заверения." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Ваша среда ConsignO Cloud, включая /api/v1." +label = "URL сервера" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Пароль" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Нужно только если ваша организация использует tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Имя пользователя" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Публикуйте сообщение в Discord, когда политика обрабатывает документ." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Индексируйте событие аудита в Elasticsearch каждый раз, когда политика обрабатывает документ." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Опрашивайте FTP или FTPS сервер на наличие новых документов." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Режим подключения" + +[portal.connections.types.ftp.fields.passive.options] +active = "Активный" +passive = "Пассивный (рекомендуется)" + +[portal.connections.types.ftp.fields.security] +helperText = "Обычный FTP отправляет учетные данные и файлы без шифрования; выберите FTPS, если сервер его поддерживает." +label = "Шифрование" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (явный / AUTH TLS)" +implicit = "FTPS (неявный)" +none = "Нет (обычный FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Публикуйте сообщение в пространстве Google Chat, когда выполняется политика." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Прикрепляйте обработанные документы к задаче Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Отправляйте документ или уведомление по Email, когда выполняется политика." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Сохраняйте обработанные документы в Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Пароль приложения" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Обнаруживайте персональные данные в тексте документа с помощью сервера Presidio, которым вы управляете." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Читайте и применяйте метки конфиденциальности Microsoft Purview к PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Необязательно. Нужно только для выбора меток по имени; нанесение меток работает и без этого." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Проставляется на каждой метке как SiteId, чтобы инструменты, совместимые с Purview, знали, какая организация ее применила." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, из которого источники могут читать и в который выходные данные конвейера могут записывать." +label = "Хранилище S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Оставьте пустым для AWS. Установите это для MinIO или другого S3-совместимого хранилища." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Регион" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Отправляйте документ или уведомление по Email, когда выполняется политика." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Опрашивайте SFTP (SSH) сервер на наличие новых документов." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Необязательно. SHA-256 fingerprint ключа host сервера (из `ssh-keyscan host | ssh-keygen -lf -`). Если задано, принимается только сервер, предоставляющий этот ключ; если пусто, ключ, увиденный при первом подключении, закрепляется, а последующие изменения отклоняются." +label = "Fingerprint ключа host" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Парольная фраза ключа" + +[portal.connections.types.sftp.fields.password] +helperText = "Оставьте пустым, если вместо этого вы проходите аутентификацию с private key." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Необязательно. Вставьте OpenSSH private key для аутентификации с ключом вместо пароля." +label = "Private key" + +[portal.connections.types.slack] +description = "Публикуйте сообщение в Slack, когда политика обрабатывает документ." +label = "Slack" + +[portal.connections.types.smb] +description = "Отслеживайте SMB / CIFS сетевую папку (Windows или Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / сетевой ресурс" + +[portal.connections.types.smb.fields.domain] +helperText = "Необязательно. Домен Windows или рабочая группа, если ваш сервер их использует." +label = "Домен" + +[portal.connections.types.smb.fields.share] +label = "Общий ресурс" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Отправляйте событие аудита в Splunk каждый раз, когда политика обрабатывает документ." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Отправляйте событие аудита в HTTP collector Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Публикуйте сообщение в канале Teams, когда выполняется политика." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Отправляйте каждый документ или только сведения о нем на выбранный вами URL." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Запускайте сценарий Zap или Make из политики." +label = "Zapier & Make" + +[portal.docs] +browse = "Просмотреть документацию" +viewSource = "Посмотреть исходный код на GitHub" + [portal.docs.authentication] codeCaption = "каждый запрос" eyebrow = "GETTING STARTED" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Встроенные именованные возможности, которые ваш агент вызывает как единый инструмент. Каждый skill — детерминированная цепочка операций с привязанными evals." title = "Навыки агента" +[portal.docs.toc] +title = "На этой странице" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Тип" user = "Пользователь" [portal.documents.queue.empty] +connectSource = "Подключить источник" +createPipeline = "Создать конвейер" description = "Когда источники будут отправлять документы в ваши pipelines, они появятся здесь для проверки." title = "В очереди нет документов" @@ -6712,11 +7309,11 @@ editorAction = "Редактор" empty = "Нет документов, соответствующих этому фильтру." rowActions = "Действия со строкой" sensitiveLabel = "Конфиденциально" -sensitiveTitle = "Конфиденциально — требуется доступ" [portal.documents.table.columns] action = "Pipeline / Действие" document = "Документ" +labels = "Labels" product = "Продукт" status = "Статус" time = "Время" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Последний сеанс" region = "Регион" status = "Статус" +target = "Target" version = "Версия" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "В этом представлении произошла неп retry = "Попробовать еще раз" title = "На этой странице что-то пошло не так" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Назад" +body = "Скачайте настольное приложение или самостоятельно разместите сервер." +done = "Готово" +guide = "Открыть руководство {{name}}" +openInBrowser = "Открыть в браузере вместо этого" +sectionDesktop = "Настольное приложение" +sectionSelfHosted = "Самостоятельное размещение" +title = "Установите Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Запустите официальный образ на своем сервере." +note = "Открывается на :8080. Вход по умолчанию: admin / stirling." +tagline = "Запустите сервер в контейнере" +title = "Docker" +variantFat = "Полная" +variantLite = "Ultra-Lite" +variantStandard = "Стандартная" + +[portal.home.download.kubernetes] +detailBody = "Установите официальный Helm chart." +note = "Добавьте PVC для /configs, чтобы сохранять настройки." +tagline = "Разверните в кластере с помощью Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Нативные сборки для вашего дистрибутива." +guideBtn = "Посмотреть варианты установки для Linux" +note = "Для распространенных дистрибутивов доступны сборки AppImage, .deb и .rpm." +tagline = "Нативное настольное приложение для вашего дистрибутива" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Или через Homebrew" +detailBody = "Запустите установщик или используйте Homebrew." +downloadBtn = "Скачать для macOS" +tagline = "Нативное приложение для Apple silicon и Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Запустите .jar на физическом сервере." +downloadBtn = "Скачать .jar" +note = "Требуется Java 21+. Открывается на :8080; логин по умолчанию: admin / stirling." +runLabel = "Затем запустите" +tagline = "Запустите .jar на физическом сервере" +title = "Ручная настройка сервера" + +[portal.home.download.windows] +altLabel = "Или через winget" +detailBody = "Запустите установщик или используйте менеджер пакетов." +downloadBtn = "Скачать установщик" +tagline = "Нативное приложение для Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} активных" -invite = "Пригласить участников" +activeOfDeployed = "{{active}} из {{total}} активны в этом месяце" name = "Stirling PDF Editor" open = "Открыть в браузере" updated = "обновлено {{time}}" +[portal.home.editor.deploy] +finish = "Завершить развертывание" +options = "Посмотреть варианты развертывания" +start = "Развернуть редактор" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Самостоятельно размещенный · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Добрый день" evening = "Добрый вечер" morning = "Доброе утро" -[portal.home.onboarding] -dismiss = "Скрыть настройку" -notStarted = "Не начато" -progress = "Выполнено {{done}} из {{total}}" -title = "Завершите настройку" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC для всей организации, цены за согласованный объем и развертывание в изолированной среде." -cta = "Начать пробный период" -ctaQuote = "Получить расчет" -lead = "Для 250+ сотрудников." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Разверните desktop app, навсегда бесплатно." -title = "Скачать PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} активных · {{recommended}} рекомендовано" -chip = "{{active}} активных" -title = "Включить политики" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} подключено · каждый PDF управляется там, где появляется" -chip = "{{connected}} подключено" -title = "Подключить источники" - -[portal.home.quickActions] -subtitle = "Главные задачи на сегодня" -title = "Быстрые действия" - -[portal.home.quickActions.buildPipeline] -blurb = "3-шаговый компоновщик поверх типизированной библиотеки операций" -title = "Создать pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, агенты, webhooks, отслеживаемые папки" -title = "Подключить источник" - -[portal.home.quickActions.issueApiKey] -blurb = "Ограниченный по области ключ с лимитами скорости и IP allowlist" -title = "Выпустить ключ API" - [portal.infrastructure] manageEditorDeployment = "Управлять развертыванием Editor" sectionsAriaLabel = "Разделы инфраструктуры" @@ -6878,13 +7541,9 @@ heading = "Ключи API" subheading = "Ограниченные учетные данные с лимитами скорости, разрешениями и IP allowlists для каждого ключа." [portal.infrastructure.apiKeys.card] -allowedIps = "Разрешенные IP" -anyIp = "Любой IP (без allowlist)" created = "Создан" lastUsed = "Последнее использование" -permissions = "Разрешения" -rateLimit = "Лимит скорости" -rateLimitValue = "{{value}} запр./мин" +revoke = "Отозвать ключ" usageMonth = "Использование за этот месяц" usageToday = "Использование сегодня" @@ -6892,6 +7551,15 @@ usageToday = "Использование сегодня" description = "Создайте ограниченный по области ключ, чтобы начать вызывать Stirling API." title = "Ключей API пока нет" +[portal.infrastructure.apiKeys.error] +load = "Не удалось загрузить ваши API ключи. Попробуйте еще раз." + +[portal.infrastructure.apiKeys.revoke] +body = "Отозвать \"{{name}}\"? Любая интеграция, которая все еще использует этот ключ, сразу начнет получать ошибки 401. Это действие нельзя отменить." +cancel = "Отмена" +confirm = "Отозвать ключ" +title = "Отозвать API ключ" + [portal.infrastructure.audit] filterAriaLabel = "Фильтровать события аудита по категории" heading = "Журналы аудита" @@ -6935,6 +7603,7 @@ all = "Все" auth = "Аутентификация" config = "Конфигурация" elevation = "Повышение доступа" +policy = "Политика" processing = "Обработка" security = "Безопасность" @@ -6944,19 +7613,30 @@ title = "У вас нет доступа" [portal.infrastructure.audit.metrics] config = "Конфигурация" -elevation = "Повышение доступа" +policy = "Политика" processing = "Обработка" totalEvents = "Всего событий · 24 ч" +[portal.infrastructure.auditCatLabel] +auth = "Аутентификация" +config = "Конфигурация" +elevation = "Повышение прав" +policy = "Политика" +processing = "Обработка" +security = "Безопасность" + +[portal.infrastructure.auditStatusLabel] +danger = "Ошибка" +info = "Информация" +success = "Успешно" +warning = "Предупреждение" + [portal.infrastructure.createKey] cancel = "Отмена" createKey = "Создать ключ" done = "Готово" -ipAllowlistHelper = "Диапазоны CIDR, разделенные запятыми. Оставьте пустым, чтобы разрешить любой IP." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Имя ключа" keyNamePlaceholder = "например, Production · ingest" -permissionsLabel = "Разрешения" secretKeyCaption = "Секретный ключ" secretWarning = "Сохраните это в менеджере секретов. Stirling всегда хранит только hash — восстановить его позже невозможно." subtitle = "Ограничьте ключ минимально необходимой областью. Вы можете сменить или отозвать его в любое время." @@ -6964,207 +7644,9 @@ subtitleCreated = "Скопируйте этот секрет сейчас — title = "Создать ключ API" titleCreated = "Ключ создан" -[portal.infrastructure.deployments] -msValue = "{{value}} мс" -throughputValue = "{{value}}/мин" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Развернул" -environment = "Среда" -product = "Продукт" -status = "Статус" -version = "Версия" -when = "Когда" - -[portal.infrastructure.deployments.recent] -heading = "Недавние развертывания" -subheading = "Последние выпуски по продуктам и средам." - -[portal.infrastructure.deployments.regionColumns] -instances = "Экземпляры" -latency = "Задержка" -load = "Нагрузка" -p99 = "P99" -region = "Регион" -status = "Статус" -throughput = "Пропускная способность" -uptime = "Uptime" -version = "Версия" - -[portal.infrastructure.deployments.regions] -heading = "Регионы" -subheading = "Текущее состояние каждого развернутого региона Stirling — задержка, нагрузка и версия выпуска." - -[portal.infrastructure.deployments.regions.empty] -description = "Развернутые регионы появятся здесь после подготовки вашего рабочего пространства." -title = "Нет развернутых регионов" - -[portal.infrastructure.models] -heading = "Модели" -msValue = "{{value}} мс" -subheading = "Каталог моделей и маршрутизация, обеспечивающие обработку документов во всем вашем рабочем пространстве." - -[portal.infrastructure.models.byom] -description = "Зарегистрируйте локальную или самостоятельно размещенную модель и закрепите ее за регионом для обработки с учетом резидентности данных." -title = "Используйте свою модель" - -[portal.infrastructure.models.catalogue] -heading = "Каталог" -sub = "Управляемые модели, доступные вашему рабочему пространству, с текущей задержкой и стоимостью." -subEnterprise = "Управляемые, собственные и локальные модели — с возможностью закрепления по регионам." - -[portal.infrastructure.models.catalogue.empty] -description = "Модели из каталога вашего рабочего пространства появятся здесь." -title = "Нет доступных моделей" - -[portal.infrastructure.models.columns] -cost = "Стоимость" -latency = "Задержка" -load = "Нагрузка" -model = "Модель" -status = "Статус" -type = "Тип" -version = "Версия" - -[portal.infrastructure.models.metrics] -activeModels = "Активные модели" -avgLatency = "Средняя задержка" -included = "Включено" -monthlySpend = "Ежемесячные расходы на модели" - -[portal.infrastructure.models.routing] -empty = "Правила маршрутизации не настроены." -heading = "Правила маршрутизации" -sub = "Какая модель обрабатывает каждую операцию. Значение по умолчанию применяется, когда нет более узкого подходящего правила." -subLocked = "Направляйте операции к определенным моделям — доступно на платных планах." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Перейдите на Pro, чтобы управлять тем, какая модель обрабатывает каждую операцию и тип документа." -title = "Маршрутизация моделей — платная функция" - -[portal.infrastructure.models.routingColumns] -default = "По умолчанию" -docType = "Тип документа" -modelForAria = "Модель для {{operation}}" -operation = "Операция" -routedTo = "Направлено в" - -[portal.infrastructure.security.access.byok] -description = "Предоставьте ключ из собственного KMS. Stirling шифрует им, но все еще может читать." -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ключи никогда не покидают ваш KMS. Stirling хранит только ciphertext." -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling управляет ключами шифрования. Самый простой вариант — нулевые операции с ключами с вашей стороны." -label = "Ключи хранятся в Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Политика доступа к документам" -subheading = "Управляет тем, кто может расшифровывать обработанные документы в состоянии покоя." - -[portal.infrastructure.security.attestations] -heading = "Подтверждения соответствия" -noReport = "Отчет недоступен" -subheading = "Состояние аудита по каждому фреймворку, с отчетами по подтвержденным контролям." -viewReport = "Просмотреть отчет →" - -[portal.infrastructure.security.compliance] -heading = "Соответствие требованиям" -subheading = "Подтверждения и сертификации, охватывающие платформу Stirling." - -[portal.infrastructure.security.empty] -description = "Конфигурация безопасности вашего рабочего пространства появится здесь." -title = "Состояние безопасности недоступно" - -[portal.infrastructure.security.hyokBanner] -description = "С HYOK ключи шифрования никогда не покидают ваш KMS. Stirling хранит и обрабатывает только ciphertext, который вы можете отозвать в любое время." -title = "Stirling не может расшифровать ваши документы" - -[portal.infrastructure.security.ipAllowlist] -empty = "Диапазоны IP не настроены — разрешены все IP." -heading = "IP allowlist" -sub = "Доступ к API ограничен этими диапазонами CIDR." -subLocked = "Ограничьте доступ к API известными диапазонами IP — доступно на платных планах." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Перейдите на Pro, чтобы ограничить доступ к API определенными сетями." -title = "IP allowlisting — платная функция" - -[portal.infrastructure.security.ipColumns] -added = "Добавлено" -addedBy = "Добавил" -cidr = "CIDR" -label = "Метка" - -[portal.infrastructure.security.keyManagement] -algorithm = "Алгоритм" -heading = "Управление ключами шифрования" -keyId = "Идентификатор ключа" -lastRotated = "Последняя смена" -rotateKey = "Сменить ключ" -rotationPolicy = "Политика смены" -subheading = "Хранение ключей, шифрующих документы в состоянии покоя — кто может расшифровать и как меняются ключи." - -[portal.infrastructure.security.managedBanner] -description = "Хранение ключей Bring-your-own-key (BYOK) и hold-your-own-key (HYOK) доступно в Enterprise. Улучшите план, чтобы предоставлять ключи из собственного KMS." -title = "На вашем плане ключами управляет Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Азиатско-Тихоокеанский регион" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · граница данных GDPR" -label = "Европейский союз" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "США" - -[portal.infrastructure.security.residencyHeader] -heading = "Резидентность данных" -subheading = "Где документы хранятся и обрабатываются." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "Использовано {{value}}" - -[portal.infrastructure.storage.empty] -description = "Подключенное хранилище и использование появятся здесь." -title = "Хранилище не настроено" - -[portal.infrastructure.storage.lifecycle] -active = "Активно" -activeRange = "0–{{value}}d" -archived = "В архиве" -coldStorage = "холодное хранилище" -deleted = "Удалено" -never = "никогда" -purged = "очищено" - -[portal.infrastructure.storage.providers] -connect = "Подключить" -connected = "Подключено" -heading = "Подключенные провайдеры" -subheading = "Куда записываются обработанные артефакты." - -[portal.infrastructure.storage.retention] -heading = "Хранение" -subheading = "Как долго артефакты хранятся перед удалением по жизненному циклу." -windowLabel = "Окно хранения по умолчанию" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} день" -days_other = "{{count}} дней" -never = "Никогда не удалять" - -[portal.infrastructure.storage.totalUsage] -heading = "Общее использование" -progressLabel = "Использовано хранилища" -subheading = "Хранилище, использованное во всех подключенных провайдерах." +[portal.infrastructure.keyLabel] +active = "Активен" +revoked = "Отозван" [portal.infrastructure.tabs] apiKeys = "Ключи API" @@ -7174,6 +7656,49 @@ models = "Модели" security = "Безопасность" storage = "Хранилище" +[portal.integrations] +availableHeading = "Доступно" +comingSoonHeading = "Скоро" +connect = "Подключить" +connectionCount_one = "{{count}} подключение" +connectionCount_other = "{{count}} подключений" +customApi = "Пользовательский API" +searchPlaceholder = "Поиск интеграций" +subtitle = "Все, с чем Stirling может взаимодействовать. Учетные данные создаются здесь один раз, а затем используются источниками, политиками и конвейерами." +title = "Интеграции" +typedName = "Название {{tool}}" + +[portal.integrations.filters] +all = "Все" +audit = "Аудит" +connected = "Подключено" +notify = "Уведомления" +security = "Безопасность" +signing = "Подписание" +storage = "Хранилище" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Подключено" + +[portal.integrations.table] +integration = "Интеграция" +worksWith = "Работает с" + +[portal.integrations.worksWith] +pipelines = "Конвейеры" +policies = "Политики" +sources = "Источники" + +[portal.legal] +draft = "{{label}} · черновик" +loadError = "Не удалось загрузить этот документ. Попробуйте еще раз." +loading = "Загрузка…" +title = "Юридический документ" + [portal.nav] agent-builder = "Конструктор агентов" components = "Компоненты" @@ -7182,33 +7707,17 @@ documents = "Документы" editor = "Редактор" home = "Главная" infrastructure = "Инфраструктура" +integrations = "Интеграции" pipelines = "Pipelines" policies = "Политики" procurement = "Закупки" -settings = "Настройки" sources = "Источники" usage = "Использование и оплата" users = "Пользователи" -[portal.notifications] -markAllRead = "Отметить все как прочитанные" -title = "Уведомления" -viewAll = "Просмотреть все" - -[portal.notifications.ariaLabel] -none = "Уведомления, нет непрочитанных" -unread_one = "Уведомления, {{count}} непрочитанное" -unread_other = "Уведомления, {{count}} непрочитанных" - -[portal.notifications.count] -allRead = "все прочитано" -loading = "загрузка" -new_one = "{{count}} новое" -new_other = "{{count}} новых" - -[portal.notifications.empty] -description = "Нет новых уведомлений." -title = "Все актуально" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Каждый автоматизированный pipeline документов на backend: упорядоченная цепочка операций над набором источников, запускаемая по триггеру. Нажмите строку, чтобы увидеть ее шаги и источники." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Новый pipeline" [portal.pipelines.builder] -addStep = "Добавить инструмент" +activate = "Activate" back = "Назад к pipelines" +cannotFollow = "Не удается принять {{produced}}" +chooseAccount = "Выберите учетную запись" +chooseDestination = "Выберите место назначения" +chooseOperation = "Выберите, что делает этот шаг" +chooseSource = "Выберите источник" discard = "Отменить изменения" -enabled = "Включено" +inputs = "Ввод" +inputSource = "Источник ввода" +inputTrigger = "Триггер" keepEditing = "Продолжить редактирование" -needsUpload = "Требуется загруженный файл" +moreActions = "More actions" +needsConfiguring = "Требуется настройка" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Нет инструментов, соответствующих вашему поиску." -pipelineSettings = "Настройки pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Искать инструменты" -selectToolBody = "Добавьте инструмент, чтобы создать свой pipeline." -selectToolTitle = "Инструментов пока нет" -toolSettings = "Настройки инструмента" +sendToSystem = "Отправить в другую систему" +stepsIncompatible = "Эти шаги не могут работать с тем, что производит предыдущий шаг: {{tools}}." +stepsNeedSetup = "Эти шаги все еще нужно настроить перед сохранением: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Нераспознанная операция, сохранена как есть." unsavedBody = "У вас есть несохраненные изменения. Сохранить их перед выходом или отменить?" unsavedTitle = "Несохраненные изменения" -uploadUnsupported = "Загруженные файлы пока не поддерживаются в pipelines, поэтому эти шаги нельзя сохранить: {{tools}}." usesDefaults = "Запускается с настройками по умолчанию" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Объединяет каждый файл из предыдущего шага" +fan-out = "Запускается один раз для каждого файла из предыдущего шага" +format-mismatch = "Требуется {{accepts}}, но предыдущий шаг производит {{produced}}" +output-uncertain = "Может не запуститься: вывод предыдущего шага зависит от его настройки" +source-mismatch = "Требуется {{accepts}}, но ввод этого конвейера — {{produced}}" +undeclared-operation = "Не удается проверить, что принимает этот шаг" [portal.pipelines.composer] addTool = "Добавить инструмент" -cancel = "Отмена" -chainEmpty = "Добавьте инструмент, чтобы начать создавать свой pipeline." create = "Создать pipeline" -directory = "Выходная папка" -directoryHelp = "Абсолютный путь на сервере. Должен находиться в пределах настроенных разрешенных папок." +createPaused = "Create paused" editingUnsupported = "Отображение этих параметров инструмента для редактирования пока не поддерживается." -moveDown = "Переместить вниз" -moveUp = "Переместить вверх" +editSource = "Edit source" name = "Имя" namePlaceholder = "например, проверка редактирования" -noSources = "Источники пока не подключены. Pipeline все равно может запускаться на файлах, переданных ему напрямую." noToolSettings = "У этого инструмента нет настраиваемых параметров." -operations_one = "Операция ({{count}})" -operations_other = "Операции ({{count}})" output = "Вывод" -removeStep = "Удалить операцию" save = "Сохранить изменения" scheduleEvery = "Запускать каждые" -sources = "Источники" -sourcesLoading = "Загрузка источников..." trigger = "Триггер" triggerManual = "Только вручную" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "дней" hours = "часов" minutes = "минут" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Удалить \"{{name}}\"? Это действие нельзя отменить." cancel = "Отмена" @@ -7273,28 +7815,62 @@ confirm = "Удалить" title = "Удалить pipeline?" [portal.pipelines.detail] +clearHistory = "Очистить историю" delete = "Удалить pipeline" run = "Запустить сейчас" [portal.pipelines.empty] action = "Создать pipeline" +connectSource = "Подключить источник" description = "Создайте первый pipeline: выберите источники, над которыми он запускается, свяжите операции и выберите, куда отправлять вывод." title = "Pipelines пока нет" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Активные" paused = "Приостановленные" total = "Pipelines" -[portal.pipelines.output] -folder = "Записать в папку" -inline = "Вернуть файлы" - [portal.pipelines.run] +allProcessed_one = "Нечего запускать: {{count}} документ источника уже обработан." +allProcessed_other = "Нечего запускать: все {{count}} документов в источниках уже обработаны." completed_one = "Запуск завершен." completed_other = "Все {{count}} запусков завершены." empty = "Нечего запускать: в источниках не было документов для обработки." failed = "Запуск не удался: {{error}}" +inFlight = "Нет ничего нового для запуска: документы все еще обрабатываются после предыдущего запуска." +parked_one = "Нечего запускать: {{count}} документ ранее завершился с ошибкой и отложен. Устраните причину, затем очистите историю, чтобы повторить попытку." +parked_other = "Нечего запускать: {{count}} документов ранее завершились с ошибкой и отложены. Устраните причину, затем очистите историю, чтобы повторить попытку." running = "Запуск начат; все еще выполняется." timeout = "Запуск занимает больше времени, чем ожидалось; он может завершиться в фоновом режиме." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Источники" status = "Статус" steps = "Шаги" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Отслеживание папки" @@ -7314,12 +7891,18 @@ manual = "Вручную" schedule = "По расписанию" [portal.policies] +defaultName = "Политика {{category}}" subtitle = "Постоянные автоматизации, применяющие pipeline инструментов к каждому документу. Каждая политика срабатывает при загрузке или экспорте, запускает свою цепочку инструментов и сохраняет принудительно обработанную версию рядом с оригиналом." title = "Политики" [portal.policies.card] comingSoon = "Перейти на Enterprise" notSetUp = "Не настроено" +requiresAiEngine = "Требуется AI engine" + +[portal.policies.categories.classification] +desc = "Определяйте тип каждого документа при загрузке и помечайте его метаданные для хранения и поиска." +label = "Классификация" [portal.policies.categories.compliance] desc = "Обеспечьте соблюдение требований HIPAA, GDPR, SOC 2 или FedRAMP для каждого документа." @@ -7344,6 +7927,13 @@ label = "Безопасность" [portal.policies.config] scopeAll = "Все документы" +[portal.policies.config.classification] +summary = "Классифицирует каждый загруженный документ по меткам вашей команды и помечает его метаданные." + +[portal.policies.config.classification.rules] +0 = "Классифицировать" +1 = "Пометить метаданные" + [portal.policies.config.compliance] summary = "Проверяет документы на соответствие нормативным фреймворкам перед выходом из системы." @@ -7371,6 +7961,29 @@ minConfidence = "Мин. уверенность" 2 = "Имя" 3 = "Нормализовать" +[portal.policies.config.purview] +label = "Применить метку конфиденциальности Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Тенант Purview" +labelId = "ID метки" +labelIdHelp = "GUID метки из портала Purview. Применение метки записывает ее метаданные в PDF; шифровать она не может — для этого нужен клиент Microsoft." +labelName = "Название метки (необязательно)" +labelNameHelp = "Записывается вместе с ID, чтобы пользователи и инструменты могли распознать метку." + +[portal.policies.config.purview.fields.method] +help = "Purview отличает метку, примененную политикой, от метки, выбранной человеком." +label = "Как записывается метка" +privileged = "Как выбранная человеком" +standard = "Как примененная автоматически" + +[portal.policies.config.purviewRead] +label = "Прочитать метку Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Тенант Purview" +connectionHelp = "Считывает метку, которая уже есть у документа, чтобы последующие шаги могли действовать на ее основе." + [portal.policies.config.retention] summary = "Обеспечивает, как долго документы хранятся, когда архивируются и когда удаляются." @@ -7438,6 +8051,7 @@ title = "Пока нет активности" [portal.policies.endpoints] addWatermark = "Водяной знак" autoRedact = "Скрыть персональные данные" +classifyAndLabel = "Классифицировать" compressPdf = "Сжать" flatten = "Свести" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Ваши политики сохранены и появятся retry = "Повторить" title = "Backend недоступен" +[portal.policies.operations] +change = "Изменить, что делает этот шаг" +noResults = "Нет подходящего шага. Попробуйте название продукта или \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Поиск шагов — попробуйте \"scan\", \"attach\" или название продукта" + +[portal.policies.operations.bodyMode] +binary = "Сырые байты файла" +json = "Внутри полезной нагрузки JSON" +multipart = "Как загрузка файла (multipart)" + +[portal.policies.operations.clamavScan] +description = "Ваш собственный сервер ClamAV проверяет документ; ничто не покидает вашу сеть." +label = "Проверить на вирусы (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Отклоняет документы с макросами, исполняемыми файлами, скриптами или встроенными объектами." +label = "Блокировать активное содержимое" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive проверяет документ, и запуск останавливается, если он не чист." +label = "Проверить на вирусы" + +[portal.policies.operations.confluenceAttach] +description = "Помещает обработанный документ на страницу." +label = "Прикрепить к странице Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Отправляет документ в ConsignO Cloud на подпись." +label = "Отправить на подпись (ConsignO)" +note = "Отправляет документ. Получение подписанной копии пока не поддерживается, потому что шаг не может передать результат последующему шагу." + +[portal.policies.operations.customApiCall] +description = "Составьте вызов самостоятельно: путь, метод, тело и обработка ответа." +label = "Вызвать пользовательский API" + +[portal.policies.operations.discordNotify] +description = "Сообщает каналу, что политика обработала документ." +label = "Опубликовать сообщение в Discord" + +[portal.policies.operations.elasticIndex] +description = "Записывает действия политики как документ, доступный для поиска." +label = "Индексировать событие аудита в Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart подходит для API загрузки; JSON подходит для API, которым нужен файл внутри полезной нагрузки; binary отправляет сырые байты." +label = "Как отправить документ" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Тело JSON, отправляемое как есть. {{document.base64}} содержит файл; {{document.*}} и {{run.*}} заполняются для каждого документа." +label = "Тело JSON" + +[portal.policies.operations.fields.connection] +label = "Учетная запись" + +[portal.policies.operations.fields.domain] +label = "Домен Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Поле формы, в котором API ожидает документ." +label = "Имя поля файла" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "От" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Объект JSON с парами имя/значение заголовков. Учетные данные подключения добавляются автоматически." +label = "Заголовки" + +[portal.policies.operations.fields.index] +label = "Индекс" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ключ задачи" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Отправляется как тело сообщения. {{document.*}} и {{run.*}} заполняются для каждого документа." +label = "Сообщение" + +[portal.policies.operations.fields.method] +label = "Метод" + +[portal.policies.operations.fields.pageId] +label = "ID страницы" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Добавляется к базовому URL подключения. Он никогда не сможет обратиться к другому хосту." +label = "Путь" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Куда записать файл на диске. {{document.filename}} заполняется для каждого документа." +label = "Путь назначения" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Отчет оставляет документ без изменений. Замена подставляет вместо него то, что вернется в ответе." +label = "Что делать с ответом" + +[portal.policies.operations.fields.signerEmail] +label = "Email подписанта" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Тема" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio анализирует этот текст. Используйте {{document.title}} или вставьте текст, который нужно проверить." +label = "Текст для проверки" + +[portal.policies.operations.fields.to] +label = "Кому" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Имя пользователя Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Сообщает пространству, что политика обработала документ." +label = "Опубликовать сообщение в Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Помещает обработанный документ в задачу." +label = "Прикрепить к задаче Jira" + +[portal.policies.operations.mailgunEmail] +description = "Отправляет обработанный документ как вложение." +label = "Отправить документ по email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Записывает обработанный документ в папку." +label = "Загрузить в Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Ваш собственный сервер Presidio сообщает о найденных персональных данных." +label = "Обнаружить персональные данные" +note = "Presidio читает текст, а не файлы. Укажите текст, который нужно проверить — сам документ не загружается." + +[portal.policies.operations.responseMode] +replace = "Заменить документ ответом" +report = "Сохранить документ, записать ответ" + +[portal.policies.operations.sendgridEmail] +description = "Отправляет обработанный документ как вложение." +label = "Отправить документ по email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Сообщает каналу, что политика обработала документ." +label = "Опубликовать сообщение в Slack" + +[portal.policies.operations.splunkEvent] +description = "Записывает действия политики там, где ваши аудиторы уже их просматривают." +label = "Отправить событие аудита в Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Записывает действия политики в HTTP collector." +label = "Отправить событие аудита в Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Сообщает каналу, что политика обработала документ." +label = "Опубликовать сообщение в Teams" + +[portal.policies.operations.webhookPost] +description = "Отправляет сам документ на выбранный вами URL." +label = "Опубликовать документ в webhook" + +[portal.policies.operations.zapierNotify] +description = "Передает сведения о запуске в вашу автоматизацию." +label = "Запустить сценарий Zap или Make" + [portal.policies.stats] activeFor = "Активна" dataProcessed = "Обработано данных" @@ -7473,6 +8282,13 @@ label = "Документов обработано" description = "Настроены, но не запускаются" label = "Приостановлены" +[portal.policies.table] +appliesTo = "Применяется к" +docs = "Документы под контролем" +enforces = "Обеспечивает" +policy = "Политика" +status = "Статус" + [portal.policies.wizard.actions] back = "Назад" cancel = "Отмена" @@ -7480,6 +8296,10 @@ continue = "Продолжить" enablePolicy = "Включить политику" saveChanges = "Сохранить изменения" +[portal.policies.wizard.capability.classify] +desc = "Определяет тип документа по меткам вашей команды и помечает его, чтобы он сохранялся и искался по категории." +label = "Классифицировать документ" + [portal.policies.wizard.capability.compress] desc = "Сжимает документ до меньшего размера файла." label = "Уменьшить размер файла" @@ -7500,10 +8320,18 @@ label = "Скрыть конфиденциальную информацию" desc = "Удаляет скрытый JavaScript, чтобы ничто не могло запускаться автоматически при открытии документа." label = "Удалить активное содержимое" +[portal.policies.wizard.capability.timestampPdf] +desc = "Доказывает, что документ существовал именно в этой форме в определенный момент времени, используя независимый timestamp authority. Отправляется только hash — документ никогда не покидает ваш сервер." +label = "Добавить доверенную временную метку" + [portal.policies.wizard.capability.watermark] desc = "Ставит видимую отметку (например, «Конфиденциально») на каждой странице." label = "Применить водяной знак" +[portal.policies.wizard.classification] +description = "Каждый загруженный документ классифицируется по встроенным меткам и помечается подходящими типами. Набор меток общий для всей вашей команды." +labelsHeading = "Метки классификации" + [portal.policies.wizard.errors] noTools = "Сначала включите хотя бы один инструмент в рабочем процессе." saveFailed = "Не удалось сохранить политику. Повторите попытку." @@ -7548,73 +8376,118 @@ setUp = "Настроить политику {{category}}" [portal.policies.wizard.workflow] description = "Выберите, что эта политика делает с каждым обрабатываемым документом." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} активны" -noRule = "Пока нет применяемого правила" -subtitle = "Постоянные автоматизации, через которые проходит каждый документ, независимо от того, какой конвейер его обрабатывает." -title = "Что выполняется для ваших PDF" - [portal.policySummary.action] -comingSoon = "Перейти на Enterprise" -configure = "Настроить" setUp = "Настроить" -[portal.policySummary.column] -activeRule = "Активное правило" -policy = "Политика" -status = "Статус" +[portal.processorFlow] +footnote = "Подсчеты приведены за последние 24 часа. Скорость потока показана для примера." +liveBadge = "Работает" +stats = "{{connected}} подключено · {{processed}} PDF обработано" +title = "PDF Processor" -[portal.policySummary.empty] -description = "После настройки политик категории появятся здесь." -title = "Политик пока нет" +[portal.processorFlow.lens] +ariaLabel = "Режим просмотра" +flow = "Поток" +sankey = "Sankey" -[portal.policySummary.state] -active = "Активна" -off = "Выкл." +[portal.processorFlow.outcomes] +count = "{{n}} · 24 ч" +failed = "Ошибка" +heading = "Результаты" +success = "Доставлено" + +[portal.processorFlow.policies] +activeCount = "{{n}} активны" +count = "{{n}} · 24 ч" +heading = "Политики" +setUp = "Настроить" soon = "Скоро" -[portal.processingStatus] -managePlan = "Управлять планом" -volumeSuffix = "PDF обработано · последние 30 дней" +[portal.processorFlow.sankey] +waist = "Политики · {{n}} активны" + +[portal.processorFlow.sankey.empty] +description = "Подключите источник и включите политику, чтобы увидеть, как документы проходят через обработчик." +title = "Потока пока нет" + +[portal.processorFlow.sources] +comingSoonTag = "Подключить" +editor = "Редактор Stirling PDF" +heading = "Источники" +perDay = "{{n}} / 24 ч" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Прием email" [portal.procurement] -reset = "Сбросить закупку (demo)" subtitle = "Проведите оценку, заключение договора и подключение вашей команды. Каждый документ в одном месте." title = "Закупка" -[portal.procurement.action] -download = "Скачать" -pay = "Оплатить сейчас" -request = "Запросить" -sign = "Проверить и подписать" -upload = "Загрузить" - [portal.procurement.agreement] agreeCta = "Согласиться и подписаться" +confidential = "Конфиденциально" confirm = "Я прочитал(а) и принимаю Stirling Enterprise Agreement." -eyebrow = "Соглашение" -intro = "Одно объединённое соглашение покрывает вашу сделку: Master Service Agreement, Order Form, EULA и Data Processing Agreement. Проверьте его, затем согласитесь, чтобы принять коммерческое предложение в рамках обязательной подписки." -title = "Проверьте ваше корпоративное соглашение" +docName = "Соглашение Stirling Enterprise" +docSub = "MSA, Order Form, EULA и DPA, объединенные в одну подпись." +download = "Скачать" +downloadDraftError = "Не удалось создать PDF соглашения — средство рендеринга документов недоступно. Попробуйте еще раз позже или обратитесь в поддержку." +downloadError = "Не удалось скачать подписанное соглашение. Попробуйте еще раз." +legalName = "Название юридического лица" +legalNamePlaceholder = "Юридическое лицо, которое будет подписывать" +loadError = "Не удалось загрузить соглашение. Попробуйте еще раз." +loading = "Загрузка соглашения..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Запросить изменения" +scrollHint = "Прокрутите для просмотра" +signatory = "Имя подписанта" +signatoryPlaceholder = "Полное имя" +signatoryTitle = "Должность подписанта" +signatoryTitlePlaceholder = "например, General Counsel" +signError = "Не удалось записать вашу подпись. Попробуйте еще раз." [portal.procurement.builder] addons = "Дополнения" +addressLine1 = "Адрес, строка 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Адрес, строка 2" +addressLine2Placeholder = "Офис, этаж, здание" back = "Назад" businessName = "Название компании" businessNamePlaceholder = "Ваша компания" +city = "Город" +cityPlaceholder = "San Francisco" +completeRequired = "Заполните обязательные поля (помечены *) и укажите действительный email перед созданием предложения." +contactEmail = "Контактный email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Имя контактного лица" +contactNamePlaceholder = "Jane Doe" continue = "Продолжить" -country = "Страна" -countryEuro = "Еврозона (EUR €)" -countryUK = "Великобритания (GBP £)" -countryUS = "США (USD $)" +done = "Готово" eula = "Я прочитал(а) и принимаю Stirling Enterprise EULA. Он регулирует соглашение, созданное на основе этого предложения." generate = "Сформировать предложение" -included = "Включено" indemnification = "Компенсация по претензиям IP" indemnificationSub = "Мы защищаем от соответствующих претензий IP согласно EULA" -offlineLicense = "Офлайн-лицензия / для изолированной среды" -offlineLicenseSub = "Скачиваемый файл лицензии для изолированного self-hosted экземпляра" +paperEyebrow = "Enterprise предложение" +paperFor = "Подготовлено для" +pdfSize = "Размер PDF" +poNumber = "Номер PO" +poNumberPlaceholder = "Необязательно" +postalCode = "Почтовый индекс" +postalCodePlaceholder = "94105" +posture = "Управление" +posture_count = "~{{count}} политик" +posture_essentials = "Essentials" +posture_essentialsSub = "Классификация + совместный доступ — настройки по умолчанию" +posture_governed = "Governed" +posture_governedSub = "Добавляет безопасность и маршрутизацию" +posture_regulated = "Regulated" +posture_regulatedSub = "Каждая категория — Compliance, Retention, Ingestion" qbr = "Ежеквартальные бизнес-обзоры" qbrSub = "Ваш инженер по решениям каждый квартал анализирует использование и roadmap" +region = "Штат / регион" +regionPlaceholder = "California" running = "{{annual}} / год · {{years}}-летний {{tcv}}" s1Sub = "Ваша команда и PDF, которые вы планируете обрабатывать каждый год." s1Title = "Сколько вы будете обрабатывать?" @@ -7623,6 +8496,12 @@ s2Title = "Обязательство и обслуживание" s3Sub = "Для предложения и соглашения, которое оно сформирует." s3Title = "Ваши данные" serviceLevel = "Уровень обслуживания" +size_compact = "Компактный" +size_compactSub = "В основном текст, менее 1 MB" +size_heavy = "Тяжелый" +size_heavySub = "Сканированные или с большим количеством изображений, 5 MB+" +size_standard = "Стандартный" +size_standardSub = "Текст и изображения, от 1 до 5 MB" slDedicated = "Выделенный" slDedicatedSub = "4 рабочих часа · выделенный менеджер аккаунта · +30%" slPriority = "Приоритетный" @@ -7630,6 +8509,8 @@ slPrioritySub = "В тот же рабочий день · назначенны slStandard = "Стандартный" slStandardSub = "Следующий рабочий день · общий CSM · включено" stepOf = "Шаг {{n}} из {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Необязательно" term = "Срок" termDiscount = "Применена скидка {{pct}}% за многолетнее обязательство" title = "Соберите ваше предложение" @@ -7637,6 +8518,7 @@ training = "Онбординг и обучение" trainingSub = "Живые сессии, чтобы ваша команда быстро начала работу" users = "Всего пользователей" usersPlaceholder = "например, 250" +viewEula = "Прочитать" volEstimated = "Оценено по {{count}} пользователям (~2 000 PDF на каждого, включая автоматизацию). Измените, если знаете точнее." volManual = "Используется ваше значение. В любой момент можно переоценить по размеру команды." volNoUsers = "Не уверены? Введите размер команды, и мы оценим объём." @@ -7645,55 +8527,55 @@ volumePlaceholder = "например, 1 000 000" years_one = "{{count}} год" years_other = "{{count}} лет" -[portal.procurement.docs] -count_one = "{{count}} документ" -count_other = "{{count}} документов" -done = "Готово" -here = "Вы здесь" -hide = "Скрыть" -optional = "Необязательно" -paidAddon = "Платное дополнение" -show = "Показать" -subtitle = "Всё, что нужно на каждом этапе пути, появляется по мере продвижения сделки." -supportingSubtitle = "SOC 2, проверки безопасности, налоговые формы и многое другое — готовы, когда их запросит ваша служба безопасности или закупок. За некоторые взимается разовая плата." -supportingTitle = "Поддержка вашей оценки" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form и DPA в одной подписи." +download = "Скачать" +eula = "EULA и коммерческие условия" +eulaSub = "Программное обеспечение и коммерческие условия самообслуживания." +invoice = "Счет" +invoiceSub = "Ваш первый счет за подписку." +laterInvoice = "Доступно после принятия" +laterQuote = "Доступно после выпуска вашего предложения" +quote = "Предложение" +quoteSub = "Ваше детализированное enterprise предложение." +sla = "Приложение SLA" +slaSub = "Уровни поддержки и целевые сроки ответа." +subprocessors = "Субпроцессоры" +subprocessorsSub = "Третьи стороны, обрабатывающие данные от вашего имени." +subtitle = "Документы по вашей сделке доступны в любое время." title = "Документы" -upcoming = "Предстоит" +view = "Просмотреть" [portal.procurement.error] title = "Что-то пошло не так" [portal.procurement.hero] -company = "Ваша корпоративная сделка" +barAria = "Этап {{current}} из {{total}}" ctaAgreement = "Проверить и подписать соглашение" -ctaLive = "Вы запущены" -ctaPayment = "Добавить оплату" +ctaExploring = "Настроить пробный период" ctaQuote = "Проверить ваше предложение" +ctaReviewQuote = "Просмотреть предложение" ctaTrial = "Собрать ваше предложение" +documents = "Документы" eyebrow = "Корпоративная закупка" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Пригласить коллег" -keyDocs = "Ключевые документы" -nextStep = "Следующий шаг: {{action}}" -notStarted = "Не начато" +licenseKey = "Лицензионный ключ" +liveSub = "Ваша организация подготовлена, и ваша лицензия активна." +liveTitle = "Вы работаете в Stirling Enterprise" +next = "Далее: {{stage}}" open = "Открыть закупку" scheduleCall = "Запланировать звонок" -setup1Sub = "Пригласите коллег" -setup1Title = "Разверните PDF Editor" -setup2Sub = "Включите обработку в редакторах и других источниках" -setup2Title = "Подключите PDF Processor" -setup3Sub = "Включайте Security, Compliance, Routing или Retention, когда они понадобятся" -setup3Title = "Добавьте рекомендуемые политики" +sentenceAgreement = "просмотр и подписание соглашения" +sentenceLive = "подготовка и развертывание" +sentencePayment = "заказ на покупку и оплата" +sentenceQuote = "просмотр и принятие вашего предложения" +sentenceTrial = "настройка пробного рабочего пространства" [portal.procurement.journey] daysLeft_one = "Остался {{count}} день" daysLeft_other = "Осталось {{count}} дней" -engineerLabel = "Ваш инженер по решениям" -eyebrow = "Ваш запуск" -live = "Stirling Enterprise уже работает у вас" -nextStep = "Следующий шаг: {{action}}" -subtitle = "Ваш инженер по решениям сопровождает каждый шаг. По одному следующему действию за раз; полный контрольный список ниже." -title = "От пробного периода до запуска — один понятный путь" -trialTitle = "Пробный период Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Одна подпись покрывает MSA, order form, EULA и DPA." @@ -7727,6 +8609,9 @@ downloadError = "Пока не удалось создать офлайн-фай downloadOffline = "Скачать офлайн-лицензию (.lic)" hint = "Вставьте этот ключ в self-hosted экземпляр, чтобы активировать его, или сохраните для своих записей. Храните его в безопасности." label = "Ваш лицензионный ключ" +subtitle = "Активируйте self-hosted экземпляр с помощью этого ключа или сохраните его для учета." +title = "Ваш лицензионный ключ" +trialFileHint = "Это ваш файл пробной лицензии. После заключения соглашения вернитесь и скачайте его снова — в отличие от онлайн-лицензионного ключа, файл .lic не обновляется сам." [portal.procurement.link] cta = "Привязать аккаунт" @@ -7739,65 +8624,62 @@ description = "Ваша подписка активна, и лицензия в eyebrow = "Запущено" title = "Stirling Enterprise уже работает у вас" -[portal.procurement.locked] -description = "Ключи для пробного периода, предложения с обязательным объёмом, соглашение с одной подписью, оплата и журнал документов появятся здесь после начала корпоративной оценки." -eyebrow = "Только Enterprise" -talkToSales = "Связаться с отделом продаж" -title = "Процесс закупки открывается с Enterprise" - [portal.procurement.milestone] -accept = "Принять и продолжить" -description = "Скачайте PDF, чтобы поделиться им с командой, вернитесь и примите, когда будете готовы, или внесите изменения." -download = "Скачать PDF" downloadError = "Пока не удалось скачать PDF с предложением — повторите попытку через минуту." -edit = "Изменить предложение" -eyebrow = "Предложение {{number}}" -perYear = " / год" -preparedFor = "Подготовлено для {{company}}" -tcv = "{{value}} общая стоимость контракта" -title = "Ваше предложение готово" - -[portal.procurement.modal] -cancel = "Отмена" -chooseFile = "Выбрать файл" -close = "Закрыть" -downloadBody = "Скачивание скоро начнётся." -downloadCta = "Скачать" -downloadTitle = "Скачать" -noFile = "Файл не выбран" -payBody = "Оплатите ваш обязательный контракт картой или банковским переводом через Stripe. Ваше рабочее пространство будет подготовлено, как только платёж пройдёт." -payCta = "Перейти к Stripe" -payTitle = "Подтвердить оплату" -requestBodyFree = "Мы создаём это по запросу. Подтвердите, и ваш инженер по решениям скоро отправит документ." -requestBodyPaid = "Это платное дополнение. Подтвердите, и ваш инженер по решениям оценит объём и отправит документы." -requestCta = "Запросить" -requestTitle = "Запросить этот документ" -signBody = "Открывает Stirling Enterprise Agreement для электронной подписи: одна подпись покрывает MSA, order form, EULA и DPA. Мы автоматически подпишем со своей стороны, и вы перейдёте к оплате." -signCta = "Открыть для подписи" -signTitle = "Проверьте и подпишите соглашение" -uploadBody = "Отправьте нам ваш заказ на закупку, и мы выставим счёт по вашим условиям. Перетащите PDF или выберите файл ниже." -uploadCta = "Загрузить заказ на закупку" -uploadTitle = "Загрузите ваш заказ на закупку" [portal.procurement.payment] description = "Ваше предложение принято, и лицензия уже активна — ваша команда может начать сразу. Оплатите первый счёт, когда будете готовы; оплатить или скачать его можно здесь, email не нужен." +downloadAgreement = "Скачать подписанное соглашение" downloadInvoice = "Скачать счёт" -simulate = "Симулировать получение оплаты (demo)" +eyebrow = "Оплата" title = "Подписка создана" viewInvoice = "Посмотреть и оплатить счёт" +[portal.procurement.review] +acceptCta = "Принять предложение" +annual = "Годовая плата (1-й год)" +downloadCta = "Скачать предложение" +poNumber = "Заказ на покупку: {{po}}" +renewal = "Продление по {{amount}}/год после срока (+{{pct}}% CPI)" +tcv = "Срок {{years}} лет, полная предоплата: {{tcv}}" +validUntil = "Действительно до {{date}}" + [portal.procurement.schedule] fallback = "Не удалось загрузить планировщик." fallbackLink = "Открыть планирование в новой вкладке" +loading = "Загрузка планировщика..." subtitle = "Ваш инженер по решениям проведёт вашу команду через запуск. Выберите удобное время." title = "Запланировать звонок" -[portal.procurement.status] -action = "Требуется действие" -available = "Доступно" -complete = "Завершено" -pending = "В ожидании" -request = "По запросу" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Изолировано, без исходящих подключений" +back = "Назад" +businessName = "Название компании" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Управляемое, самый быстрый старт" +continue = "Продолжить" +deployment = "Где развернуть?" +eula = "Я принимаю Stirling EULA и коммерческие условия." +fullName = "Полное имя" +fullNamePlaceholder = "Ваше имя" +invites = "Пригласить коллег · необязательно" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Запланировать звонок" +seats = "Люди для тестирования" +seatsPlaceholder = "например, 15" +selfhost = "Self-hosted" +selfhostSub = "Docker или Kubernetes" +start = "Начать пробный период" +stepOf = "Шаг {{n}} из {{total}}" +subtitle = "Бесплатно 14 дней, карта не требуется." +subtitleDetails = "Несколько деталей для вашего предложения и соглашения." +talkFirst = "Хотите сначала поговорить?" +title = "Настройте пробный период" +viewEula = "Прочитать" +workEmail = "Рабочий email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Продление добавляет 7 дней и уведомляет вашего инженера по решениям." @@ -7808,109 +8690,113 @@ maxed = "Лимит исчерпан" subtitle = "Ваш бесплатный пробный период действует до {{date}}. Карта не требуется." title = "Пробный период Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Цены по обязательному объёму, SSO + SCIM + RBAC для всей организации, неизменяемый аудит за 90 дней и выделенный SE." -homeCta = "Начать пробный период →" -homeHeadline = "Обрабатывайте миллионы PDF." - -[portal.recentActivity] -title = "Недавняя активность" -viewAll = "Посмотреть всё" - -[portal.recentActivity.empty] -description = "Здесь появятся запуски конвейеров, развёртывания и события агентов." -title = "Здесь пока ничего нет" - -[portal.search] -ariaLabel = "Поиск" -placeholder = "Поиск в Stirling — endpoints, конвейеры, документы…" - -[portal.search.empty] -noActionsDescription = "Быстрые действия появятся здесь, когда станут доступны." -noActionsTitle = "Нет быстрых действий" -noMatches = "Нет совпадений для «{{query}}»" -noMatchesDescription = "Попробуйте другое ключевое слово или просмотрите каталог." - [portal.settings.groups] admin = "Администратор" [portal.settings.sections] account-link = "Привязка аккаунта" -[portal.shell.header] -accountFallback = "Аккаунт" -accountMenu = "Меню аккаунта" -darkMode = "Тёмный режим" -lightMode = "Светлый режим" -search = "Поиск" -searchPlaceholder = "Поиск…" -signOut = "Выйти" -switchToDark = "Переключиться на тёмную тему" -switchToLight = "Переключиться на светлую тему" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} документов" linkAccount = "Привязать аккаунт Stirling" -planEditor = "План Editor" -planEnterprise = "План Enterprise" -planProcessor = "План Processor" primaryNav = "Основная навигация" switchApp = "Переключить приложение" +[portal.shell.topbar] +closeNav = "Закрыть навигацию" +openNav = "Открыть навигацию" +search = "Поиск" + [portal.sources] subtitle = "Многоразовые входные подключения, которые передают документы в Stirling. Настройте подключение один раз, затем используйте его в любом количестве политик. Нажмите строку, чтобы увидеть её конфигурацию и политики, которые её используют." title = "Источники" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Подключить источник" +[portal.sources.builder] +back = "Назад к источникам" +backToTypes = "Все типы источников" +cancel = "Отмена" +chooseHint = "Выберите, откуда поступают документы. Неактивные коннекторы уже в разработке." +comingSoon = "Скоро" +comingSoonHeading = "Скоро" +create = "Создать источник" +createTitle = "Подключить источник" +delete = "Удалить" +editTitle = "Редактировать источник" +enabled = "Включено" +save = "Сохранить изменения" + +[portal.sources.builder.folderAccess] +description = "Автоматизации папок могут использовать только папки, разрешенные администратором. Добавьте ее в настройках доступа к папкам, затем попробуйте снова." +openSettings = "Настройки доступа к папкам" +title = "Эта папка не разрешена" + [portal.sources.delete] body = "Удалить «{{name}}»? Это действие нельзя отменить. Политики, которые ссылаются на него, нужно будет обновить." cancel = "Отмена" confirm = "Удалить" title = "Удалить источник?" -[portal.sources.detail] -closeAriaLabel = "Закрыть детали" -delete = "Удалить источник" -docs24h = "Последние 24 ч" -docs30d = "Последние 30 дней" -docsTotal = "Всего обнаружено" -docsTrend = "Документы за последние 30 дней" -documents = "Документы" -edit = "Изменить" -notReferenced = "Не используется ни одной политикой, поэтому его можно безопасно удалить." -pause = "Приостановить" -resume = "Возобновить" -subtitle = "{{type}} · {{status}}" -usedBy = "Используется" - -[portal.sources.empty] -description = "Подключите папку (а скоро и облачное хранилище), чтобы вашим политикам было откуда получать документы." -title = "Источники пока не подключены" - [portal.sources.kpi] inUse = "Используются" total = "Подключения" unused = "Не используются" +[portal.sources.networkFields.connection] +helperText = "Сохраненное подключение с адресом сервера и учетными данными. Повторно используется каждым источником, который на него ссылается." +label = "Подключение" + +[portal.sources.networkFields.directory] +helperText = "Папка на сервере для опроса, относительно домашней папки входа или корня общего ресурса. Оставьте пустым для корня." +label = "Папка" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Потребление удаляет каждый файл с сервера после того, как каждая политика его обработает." +label = "Режим чтения" + +[portal.sources.networkFields.mode.options] +consume = "Потребление: обработать каждый файл один раз" +snapshot = "Снимок: перечитывать папку при каждом запуске" + +[portal.sources.networkFields.recursive] +label = "Глубина папки" + +[portal.sources.networkFields.recursive.options] +all = "Включать подпапки" +top = "Только верхний уровень" + [portal.sources.status] active = "Активен" disabled = "Отключён" unused = "Не используется" [portal.sources.table] +documents = "Документы" source = "Источник" status = "Статус" +type = "Type" usedBy = "Политики" +[portal.sources.types.box] +description = "Получайте документы из папок Box, к которым ваша команда уже предоставляет доступ." +label = "Box" + +[portal.sources.types.dropbox] +description = "Получайте документы из общей папки Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Документы, обработанные вашей командой в редакторе, по политикам и запускам AI." label = "Editor" +noPolicies = "Из редактора еще не запускались политики." + +[portal.sources.types.email] +description = "Преобразуйте вложения, отправленные в выделенный почтовый ящик, в документы." +label = "Email inbox" [portal.sources.types.folder] description = "Отслеживать новые документы в каталоге на сервере." @@ -7943,25 +8829,80 @@ label = "Глубина папки" all = "Включать подпапки" top = "Только верхний уровень" +[portal.sources.types.ftp] +description = "Опрос папки сервера FTP или FTPS на наличие новых документов." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Получайте документы из общих папок Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Отслеживайте сетевой ресурс SMB на наличие новых документов." +label = "Сетевой диск" + +[portal.sources.types.onedrive] +description = "Отслеживайте папки OneDrive на наличие новых документов." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Получайте документы из Amazon S3 или S3-совместимого bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Сохраненное подключение с bucket и учетными данными. Повторно используется каждым источником и выходом конвейера, который на него ссылается." +label = "Подключение" + +[portal.sources.types.s3.fields.mode] +helperText = "Потребление удаляет каждый объект из bucket после того, как каждая политика его обработает." +label = "Режим чтения" + +[portal.sources.types.s3.fields.mode.options] +consume = "Потребление: обработать каждый объект один раз" +snapshot = "Снимок: перечитывать bucket при каждом запуске" + +[portal.sources.types.s3.fields.prefix] +helperText = "Обрабатываются только объекты, ключи которых начинаются с этого префикса." +label = "Префикс ключа" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Опрос папки приема SFTP на наличие новых документов." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Получайте документы из библиотек документов SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Источник" +[portal.sources.types.webhook] +createNote = "Delivery URL и секрет подписи создаются при создании этого webhook. Отправители отправляют документы POST на этот URL, а ссылающиеся политики запускаются при поступлении." +description = "Получайте документы через подписанный HTTP POST из любой внешней системы." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Секрет подписи показывается один раз при создании webhook. Создайте источник заново, чтобы обновить его." + +[portal.sources.types.webhook.reveal] +copy = "Копировать" +done = "Готово" +secret = "Секрет подписи" +secretHelp = "Подписывайте сырое тело каждой доставки этим ключом (HMAC-SHA256) и отправляйте его в заголовке X-Stirling-Signature." +secretWarning = "Скопируйте секрет подписи сейчас. В целях безопасности он показывается только один раз и не может быть получен позже." +title = "Webhook создан" +url = "Delivery URL" +usage = "Отправляйте каждый документ POST как сырое тело запроса на Delivery URL с бинарным типом содержимого (application/pdf или application/octet-stream). Ссылающиеся политики автоматически запускаются при поступлении." + [portal.sources.wizard] -back = "Назад" -cancel = "Отмена" -continue = "Продолжить" -editTitle = "Изменить источник" name = "Название" namePlaceholder = "например, приём заявок" -save = "Сохранить изменения" -subtitle = "Шаг {{current}} из {{total}} · {{label}}" -title = "Подключить источник" type = "Тип" -[portal.sources.wizard.steps] -chooseType = "Выбрать тип" -configure = "Настроить" -review = "Проверить и подключить" +[portal.stepModal] +close = "Закрыть" [portal.usage] managePayment = "Управление оплатой" @@ -8021,16 +8962,9 @@ summary = "Владеет командой — управляет ресурса 2 = "Доступ к порталу через политику по умолчанию" 3 = "Всё, что может участник" -[portal.welcome] -ariaLabel = "Добро пожаловать в Stirling PDF" -badge = "Открытый исходный код" -installEditor = "Установить Editor" -inviteTeammates = "Пригласить коллег" -openInBrowser = "Открыть в браузере" -perks = "Бесплатно навсегда · Можно self-host" -subtitle = "Самый безопасный в мире PDF Editor бесплатен для команд любого размера. Включает 60+ операций с PDF и SSO." -title = "Добро пожаловать в" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Без ограничений" [printFile] title = "Печать файла" @@ -8613,6 +9547,14 @@ text = "Поворачивайте страницы PDF по или против [rotate.tooltip.header] title = "Обзор настроек поворота" +[ruler] +clearAll = "Очистить все" +hideAllLabels = "Скрыть все метки" +hideSmallLabels = "Скрыть мелкие метки" +physicalValues = "Физические" +scaled = "Масштабированные" +showAllLabels = "Показать все метки" + [sanitize] desc = "Удаление потенциально вредных элементов из PDF-файлов." sanitizationResults = "Результаты санитизации" @@ -8654,31 +9596,45 @@ label = "Удалить XMP-метаданные" [sanitize.steps] settings = "Настройки" -[sanitizePdf] -tags = "очистка,безопасность,защита,удаление угроз" - [sanitizePDF] title = "Очистить PDF" +[sanitizePdf] +tags = "очистка,безопасность,защита,удаление угроз" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Настройка масштаба страницы" +[scaleSettings] +activeScale = "Активный масштаб" +apply = "Применить масштаб" +applyCalibration = "Применить калибровку" +calculatedScale = "Рассчитанный масштаб" +calibrate = "Калибровать" +calibrating = "Калибровка" +calibrationDistanceRequired = "Введите реальное расстояние больше нуля" +calibrationInvalid = "Не удалось рассчитать масштаб по этому измерению" +calibrationPaperDistance = "Измеренное расстояние на странице: {{distance}}" +calibrationTitle = "Калибровать масштаб" +calibrationTooltip = "Измерьте известное расстояние, чтобы автоматически рассчитать масштаб" +customScale = "Пользовательский масштаб" +noneSet = "Пользовательский масштаб не задан" +presets = "Предустановленные масштабы" +ratio = "Коэффициент масштаба" +ratioHelp = "Соотношение: 1 единица страницы = X реальных единиц" +ratioPlaceholder = "например, 100" +ratioPositive = "Коэффициент должен быть больше нуля" +ratioRequired = "Коэффициент обязателен" +realDistance = "Реальное расстояние" +realDistancePlaceholder = "например, 5" +reset = "Сбросить к значениям по умолчанию" +unit = "Единица измерения" +unitInvalid = "Недопустимая единица измерения: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Пороговый угол:" -10 = "Устанавливает размер добавляемой и удаляемой границы для предотвращения белых границ в выводе (по умолчанию: 1)." -2 = "Устанавливает минимальный абсолютный угол, необходимый для поворота изображения (по умолчанию: 10)." -3 = "Допуск:" -4 = "Определяет диапазон изменения цвета вокруг предполагаемого фонового цвета (по умолчанию: 30)." -5 = "Минимальная площадь:" -6 = "Устанавливает минимальный порог площади для фотографии (по умолчанию: 10000)." -7 = "Минимальная площадь контура:" -8 = "Устанавливает минимальный порог площади контура для фотографии" -9 = "Размер границы:" - [scannerImageSplit] submit = "Извлечь отсканированные изображения" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Что делает инструмент" whatThisDoesDesc = "Автоматически находит и извлекает каждую фотографию со отсканированной страницы или составного изображения — без ручной обрезки." whenToUse = "Когда использовать" +[ScannerImageSplit.selectText] +1 = "Пороговый угол:" +10 = "Устанавливает размер добавляемой и удаляемой границы для предотвращения белых границ в выводе (по умолчанию: 1)." +2 = "Устанавливает минимальный абсолютный угол, необходимый для поворота изображения (по умолчанию: 10)." +3 = "Допуск:" +4 = "Определяет диапазон изменения цвета вокруг предполагаемого фонового цвета (по умолчанию: 30)." +5 = "Минимальная площадь:" +6 = "Устанавливает минимальный порог площади для фотографии (по умолчанию: 10000)." +7 = "Минимальная площадь контура:" +8 = "Устанавливает минимальный порог площади контура для фотографии" +9 = "Размер границы:" + [search] noResults = "Ничего не найдено" placeholder = "Введите поисковый запрос..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Ваш сервер Stirling-PDF офлайн, и \"{ [session] expired = "Ваша сессия истекла. Пожалуйста, обновите страницу и попробуйте снова." -[sessionManagement.tooltip] -header = "Управление сессиями подписания" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Новые участники добавляются в конец порядка подписания" -bullet2 = "Нельзя добавлять участников после завершения сессии" -bullet3 = "Каждый участник получает уведомление, когда наступает его очередь" -description = "Вы можете добавлять участников в активную сессию в любое время до завершения." -title = "Добавление участников" - -[sessionManagement.tooltip.finalization] -bullet1 = "Полное завершение: все участники подписали" -bullet2 = "Частичное завершение: некоторые участники ещё не подписали" -bullet3 = "Неподписавшие участники будут исключены из финального документа" -bullet4 = "После завершения вы можете загрузить подписанный PDF в активные файлы" -description = "Завершение объединяет все подписи в один подписанный PDF. Это действие нельзя отменить." -title = "Завершение сессии" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Нельзя удалить участников, которые уже подписали" -bullet2 = "Удалённые участники больше не получают уведомления" -bullet3 = "Порядок подписания корректируется автоматически" -description = "Участников можно удалять из сессий до того, как они подпишут." -title = "Удаление участников" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Каждая подпись применяется к PDF последовательно" -bullet2 = "Поздние подписанты видят более ранние подписи" -bullet3 = "Критично для процессов утверждения и юридической цепочки хранения" -description = "Порядок, который вы задаёте при создании сессии, определяет, кто подписывает первым." -title = "Порядок подписей" - [settings] close = "Закрыть" +[settings.ai] +documents = "Документы и RAG" +general = "Общие" +limits = "Ограничения и производительность" +models = "Модели и поставщики" +title = "ИИ" + [settings.configuration] advanced = "Дополнительно" database = "База данных" endpoints = "Конечные точки" features = "Функции" +folderAccess = "Доступ к папкам" mcp = "MCP Server" storageSharing = "Хранение файлов и общий доступ" systemSettings = "Системные настройки" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Редактор PDF по умолчанию" defaultPdfEditorActive = "Stirling PDF — ваш редактор PDF по умолчанию" defaultPdfEditorChecking = "Проверка..." defaultPdfEditorInactive = "Другое приложение назначено по умолчанию" +defaultPdfEditorRemind = "Напоминать установить по умолчанию" +defaultPdfEditorRemindDescription = "Показывать баннер, когда Stirling PDF не является вашим приложением PDF по умолчанию." defaultPdfEditorSet = "Уже по умолчанию" defaultStartupView = "Вид по умолчанию при запуске" defaultStartupViewDescription = "Выберите, какая вкладка в левой колонке будет активной при запуске приложения" @@ -8817,9 +9763,18 @@ languageDescription = "Выберите язык отображения" logout = "Выйти" setAsDefault = "Сделать по умолчанию" theme = "Тема" +themeAccent = "Акцентный цвет" +themeAccentCustom = "Пользовательский цвет" +themeAccentCustomHint = "Пользовательский (рекомендуются значения по умолчанию)" +themeAccentDark = "Акцентный цвет темного режима" +themeAccentDefault = "По умолчанию" +themeAccentDefaultHint = "Тема по умолчанию (рекомендуется)" +themeAccentDescription = "Кнопки, ссылки и выделения используют его, а также он слегка окрашивает приложение. У светлого и темного режимов свои настройки. Системный режим использует активный вариант." +themeAccentLight = "Акцентный цвет светлого режима" themeDark = "Темная" themeDescription = "Переключение между светлым и темным режимом" themeLight = "Светлая" +themeReset = "Восстановить тему по умолчанию" themeSystem = "System" title = "Общие" user = "Пользователь" @@ -8833,6 +9788,12 @@ intro = "Включите аутентификацию пользователе learnMore = "Подробнее в документации" title = "Для системных администраторов" +[settings.general.loginLanding] +description = "Выберите, какое приложение будет открываться при входе в Stirling Cloud." +editor = "Редактор" +processor = "Обработчик" +title = "После входа" + [settings.general.mode] fullscreen = "Полноэкранный" sidebar = "Боковая панель" @@ -9013,9 +9974,6 @@ title = "Политики и конфиденциальность" [settings.preferences] title = "Настройки" -[settings.search] -placeholder = "Поиск по страницам настроек..." - [settings.security] description = "Обновите пароль, чтобы защитить свою учётную запись." title = "Безопасность" @@ -9041,6 +9999,11 @@ security = "Безопасность" telegram = "Telegram" title = "Безопасность и аутентификация" +[settings.signOut] +confirm = "Вы уверены, что хотите выйти?" +submit = "Выйти" +title = "Выход" + [settings.team] title = "Команда" @@ -9156,6 +10119,7 @@ last = "Последняя страница" maintainRatio = "Переключить сохранение пропорций" next = "Следующая страница" noSavedSigs = "Сохраненные подписи не найдены" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Личные подписи" previous = "Предыдущая страница" redo = "Повторить" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Не удалось удалить фон" hint = "Загрузите PNG или JPG с вашей подписью" label = "Загрузить изображение подписи" placeholder = "Выберите файл изображения" +previewAlt = "Current image signature" processing = "Обработка изображения..." removeBackground = "Удалить белый фон (сделать прозрачным)" @@ -9207,6 +10172,17 @@ saved = "Выберите сохранённую подпись выше, зат text = "После ввода вашего имени выше щелкните в любом месте PDF, чтобы поставить подпись." title = "Как добавить подпись" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Переместить подпись" pause = "Пауза размещения" @@ -9653,10 +10629,30 @@ title = "Загрузить на сервер" updateButton = "Обновить на сервере" uploadButton = "Загрузить на сервер" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "нажмите здесь" +fallback = "Если он не открылся автоматически," +opening = "Открытие Swagger UI в новой вкладке..." tags = "api,документация,swagger,эндпоинты,разработка" title = "Документация API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Выберите файлы в «Активных файла singleFileScope = "Применяется только к: {{fileName}}" viewerMode = "Переключитесь в редактор файлов, чтобы выбрать несколько файлов." +[toolFormat] +ANY = "любой файл" +CBR = "архив комиксов (CBR)" +CBZ = "архив комиксов (CBZ)" +CSV = "CSV-файл" +EBOOK = "электронная книга" +EMAIL = "электронное письмо" +EXCEL = "электронная таблица" +HTML = "веб-страница" +IMAGE = "изображения" +JAVASCRIPT = "JavaScript-файл" +JSON = "JSON-файл" +MARKDOWN = "Markdown-файл" +NONE = "нет файла" +PCL = "печатный файл PCL" +PDF = "PDF" +PDF_ENCRYPTED = "защищенный паролем PDF" +POSTSCRIPT = "PostScript-файл" +PPT = "презентация" +TEXT = "текстовый файл" +VIDEO = "видео" +WORD = "документ Word" +XML = "XML-файл" +XPS = "документ XPS" +ZIP = "ZIP-архив" + [toolPanel] alpha = "Альфа" backToAllTools = "Назад ко всем инструментам" collapse = "Свернуть панель" expand = "Развернуть панель" goBack = "Вернуться" +pdfTools = "Инструменты PDF" placeholder = "Выберите инструмент, чтобы начать" premiumFeature = "Премиум‑функция:" toolsHeader = "Инструменты" @@ -9806,11 +10829,6 @@ noToolsFound = "Инструменты не найдены" quickAccess = "БЫСТРЫЙ ДОСТУП" searchPlaceholder = "Поиск инструментов..." -[toolPicker.categories] -advancedTools = "Расширенные инструменты" -recommendedTools = "Рекомендуемые инструменты" -standardTools = "Стандартные инструменты" - [toolPicker.subcategories] advancedFormatting = "Расширенное форматирование" automation = "Автоматизация" @@ -9824,6 +10842,9 @@ removal = "Удаление" signing = "Подписание" verification = "Проверка" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Инструменты не найдены" noTools = "Нет доступных инструментов" @@ -9950,28 +10971,53 @@ title = "Пользователи" you = "(вы)" [users.action] +cancelInvite = "Отменить приглашение" deleteTeam = "Удалить команду" disableMfa = "Сбросить MFA" move = "Переместить в команду" reinstate = "Восстановить" remove = "Удалить из организации" +removeTeam = "Удалить из команды" rename = "Переименовать команду" resetPw = "Сбросить пароль" suspend = "Приостановить" unlock = "Разблокировать аккаунт" +[users.activity] +daysAgo = "{{count}} дн. назад" +hoursAgo = "{{count}} ч назад" +justNow = "Только что" +minutesAgo = "{{count}} мин назад" +monthsAgo_one = "{{count}} месяц назад" +monthsAgo_other = "{{count}} мес. назад" +never = "Никогда" +weeksAgo_one = "{{count}} неделю назад" +weeksAgo_other = "{{count}} нед. назад" +yearsAgo_one = "{{count}} год назад" +yearsAgo_other = "{{count}} г. назад" + [users.cap] addProcessor = "+ Processor" approver = "Утверждает политику" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Отменить приглашение для {{email}}? Они не смогут присоединиться по текущей ссылке." +cancelInviteTitle = "Отменить приглашение" deleteTeamBody = "Удалить команду {{name}}? Сначала команда должна быть пустой — переместите её участников в другую команду, и у неё не должно оставаться конфигураций интеграций." deleteTeamTitle = "Удалить команду" disableMfaBody = "Удалить регистрацию MFA для {{name}}? При необходимости он(а) настроит её снова при следующем входе." disableMfaTitle = "Сбросить MFA" removeBody = "Навсегда удалить {{name}} из организации? Это действие нельзя отменить." +removeTeamBody = "Удалить {{name}} из команды? У них останется учетная запись, но они потеряют доступ к ресурсам этой команды." removeTitle = "Удалить участника" [users.empty] @@ -9982,10 +11028,8 @@ title = "Участников пока нет" addToTeam = "Добавить в команду" guestCount = "{{count}} гость" guests = "Гости" -guestsDesc = "Внешние участники с доступом только к тому, чем вы поделились. Только Editor." ledBy = "руководит {{owner}}" org = "Организация" -orgDesc = "Владельцы с полномочиями на уровне всей организации и утверждением политик" owners = "{{count}} владельцев" team = "Команда {{name}}" teamMeta = "{{count}} человек" @@ -10022,6 +11066,18 @@ username = "Имя пользователя" usernameError = "Имя пользователя должно содержать не менее 3 символов" usernamePlaceholder = "jsmith" +[users.invites] +by = "Пригласил(а) {{who}}" +cancel = "Отменить" +expiresInDays_one = "Истекает через {{count}} день" +expiresInDays_other = "Истекает через {{count}} дн." +expiresToday = "Истекает сегодня" +title = "Ожидающие приглашения" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Что-то пошло не так при обращении к серверной части, или у вас нет доступа. Попробуйте снова." title = "Не удалось загрузить участников" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Поиск вложений" title = "Вложения" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Название закладки" +bookmarkTitleRequired = "Название закладки обязательно" closeSidebar = "Закрыть боковую панель закладок" collapseAll = "Свернуть все закладки" +empty = "No bookmarks in this document" expandAll = "Развернуть все закладки" +searchPlaceholder = "Поиск закладок" +title = "Bookmarks" [viewer.comments] addComment = "Добавить комментарий" @@ -10236,10 +11297,12 @@ locateAnnotation = "Найти в документе" moreActions = "Другие действия" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Страница {{page}}" placingHint = "Щёлкните по странице, чтобы разместить… (отмена)" removeCommentOnly = "Удалить только комментарий" saveReply = "Сохранить ответ" +searchPlaceholder = "Search comments" title = "Комментарии" typeComment = "Комментарий" typeInsertText = "Вставить текст" @@ -10262,6 +11325,7 @@ unsavedDesc = "У вас есть несохранённые изменения" closeSidebar = "Закрыть боковую панель слоев" hideAll = "Скрыть все слои" showAll = "Показать все слои" +title = "Layers" [viewer.link] delete = "Удалить ссылку" @@ -10605,30 +11669,6 @@ title = "Текст водяного знака" image = "Изображение" text = "Текст" -[wetSignature.tooltip] -header = "Способы создания подписи" - -[wetSignature.tooltip.draw] -bullet1 = "Настройте цвет и толщину пера" -bullet2 = "Стирайте и перерисовывайте, пока не будете довольны" -bullet3 = "Работает на сенсорных устройствах (планшеты, телефоны)" -description = "Создайте рукописную подпись с помощью мыши или сенсорного экрана. Лучше всего для личных, аутентичных подписей." -title = "Нарисовать подпись" - -[wetSignature.tooltip.type] -bullet1 = "Выберите из нескольких шрифтов" -bullet2 = "Настройте размер и цвет текста" -bullet3 = "Идеально для стандартизированных подписей" -description = "Создайте подпись из введенного текста. Быстро и единообразно, подходит для деловых документов." -title = "Ввести подпись" - -[wetSignature.tooltip.upload] -bullet1 = "Поддерживаются PNG, JPG и другие форматы изображений" -bullet2 = "Для наилучших результатов рекомендуется прозрачный фон" -bullet3 = "Изображение будет масштабировано под область подписи" -description = "Загрузите заранее созданное изображение подписи. Идеально, если у вас есть скан подписи или логотип компании." -title = "Загрузить изображение подписи" - [workbenchBar] activeFiles = "Активные файлы" annotations = "Аннотации" @@ -10642,6 +11682,7 @@ exitRedaction = "Выйти из режима скрытия данных" exportAll = "Экспортировать PDF" exportSelected = "Экспортировать выбранные страницы" formFill = "Заполнить форму" +hideToolbar = "Hide toolbar" multiTool = "Мультиинструмент" panMode = "Режим перемещения" print = "Печать PDF" @@ -10652,6 +11693,7 @@ redact = "Скрыть данные" rotateLeft = "Повернуть влево" rotateRight = "Повернуть вправо" ruler = "Линейка / Измерение" +rulerSettings = "Настройки масштаба" save = "Сохранить" saveAll = "Сохранить все" saveAs = "Сохранить как" @@ -10661,6 +11703,7 @@ selectAll = "Выбрать все" selectByNumber = "Выбрать по номерам страниц" selectLanguage = "Выбрать язык" share = "Поделиться" +showToolbar = "Show toolbar" toggleAnnotations = "Показать/скрыть аннотации" toggleAttachments = "Показать/скрыть вложения" toggleBookmarks = "Показать/скрыть закладки" diff --git a/frontend/editor/public/locales/sk-SK/translation.toml b/frontend/editor/public/locales/sk-SK/translation.toml index 1da4a88095..fc43937505 100644 --- a/frontend/editor/public/locales/sk-SK/translation.toml +++ b/frontend/editor/public/locales/sk-SK/translation.toml @@ -1,7 +1,6 @@ alphabet = "Abeceda" apply = "Použiť" applyAndContinue = "Uložiť a odísť" -areYouSure = "Naozaj chcete odísť?" back = "Späť" black = "Čierna" blue = "Modrá" @@ -46,10 +45,8 @@ filesSelected = "vybrané súbory" font = "Písmo" fontSizeTooltip = "Veľkosť textu čísla strany v bodoch. Väčšie čísla vytvoria väčší text." fontTypeTooltip = "Rodina písma pre čísla strán. Vyberte podľa štýlu vášho dokumentu." -genericSubmit = "Odoslať" green = "Zelená" help = "Pomoc" -imgPrompt = "Vyberte obrázok(y)" incorrectPasswordMessage = "Aktuálne heslo je nesprávne." info = "Info" invalidUndoData = "Nemožno vrátiť: neplatné údaje operácie" @@ -62,7 +59,6 @@ loginAgreementTitle = "Dohoda pri prihlásení" logOut = "Odhlásiť sa" marginTooltip = "Vzdialenosť medzi číslom strany a okrajom strany." moreOptions = "Ďalšie možnosti" -multiPdfPrompt = "Vyberte PDF súbory (2+)" never = "Nikdy" no = "Nie" noFileSelected = "Nie je vybraný žiadny súbor. Nahrajte ho, prosím." @@ -80,7 +76,6 @@ pageOrderPrompt = "Vlastné poradie stránok (Zadajte zoznam čísel stránok od pages = "Strany" pageSelectionPrompt = "Vlastný výber stránok (Zadajte zoznam čísel stránok oddelených čiarkou 1,5,6 alebo funkcie ako 2n+1):" password = "Heslo" -pdfPrompt = "Vyberte PDF súbor(y)" pin = "Pripnúť súbor (ponechať aktívny po spustení nástroja)" pro = "Pro" processingComplete = "Váš súbor je pripravený." @@ -110,6 +105,7 @@ undoSuccess = "Operácia bola úspešne vrátená" unknown = "Neznáme" unpin = "Zrušiť pripnutie súboru (nahradiť po spustení nástroja)" unsavedChanges = "Máte neuložené zmeny v PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Neuložené zmeny" unsupported = "Nepodporované" username = "Používateľské meno" @@ -356,6 +352,7 @@ text = "Tieto povolenia riadia, čo môžu používatelia s PDF robiť. Najefekt title = "Zmeniť povolenia" [addStamp] +preview = "Náhľad pečiatky" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manuálne sťahovanie: kliknite na odkazy a uložte súbory do pr noLanguages = "V nakonfigurovanom adresári sa nenašli žiadne jazyky tessdata." permissionNotice = "Cesta k tessdata nie je zapisovateľná. Sťahovania sa otvoria v prehliadači; súbory .traineddata prosím uložte manuálne do priečinka tessdata." +[admin.settings.ai.documents] +description = "Nakonfigurujte model embeddingov a nastavenia vyhľadávania používané na odpovedanie na otázky nad dokumentmi. Po uložení sa použije v AI engine." +title = "Dokumenty a RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Nechajte prázdne, ak chcete použiť vlastné prihlasovacie údaje prostredia engine. Platí pre self-hosted nasadenia s jedným engine." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Uložené – nechajte prázdne, ak chcete ponechať aktuálny kľúč" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Základná URL OpenAI-kompatibilného / Ollama endpointu pre embeddingy, napr. http://ollama:11434/v1. Musí smerovať na dôveryhodný interný endpoint (citlivé na SSRF)." +label = "Základná URL embeddingov" + +[admin.settings.ai.documents.embeddingModel] +description = "Názov modelu embeddingov. Voľný text; návrhy sú iba pomôcky." +label = "Model embeddingov" + +[admin.settings.ai.documents.embeddingProvider] +description = "Poskytovateľ používaný na prevod textu dokumentu na vektorové embeddingy." +label = "Poskytovateľ embeddingov" + +[admin.settings.ai.documents.maxSearches] +description = "Maximálny počet vyhľadávaní, ktoré môže agent spustiť na jednu požiadavku." +label = "Max. počet vyhľadávaní" + +[admin.settings.ai.documents.reindexNote] +body = "Zmena modelu embeddingov sa prejaví okamžite, ale dokumenty indexované predchádzajúcim modelom musia byť znovu indexované, aby vyhľadávanie vracalo správne výsledky." +title = "Vyžaduje sa opätovné indexovanie" + +[admin.settings.ai.documents.saved] +reindexNote = "Ak ste zmenili model embeddingov, znovu indexujte existujúce dokumenty, aby vyhľadávanie používalo nový model." + +[admin.settings.ai.documents.topK] +description = "Počet najrelevantnejších častí načítaných pri každom vyhľadávaní." +label = "Top K" + +[admin.settings.ai.general] +description = "Pripojte Stirling k Python AI engine a vyberte, ktoré AI funkcie budú dostupné. Zmeny sa použijú po reštarte." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Zapnite alebo vypnite jednotlivé AI funkcie. Vypnuté funkcie sú v aplikácii skryté." +title = "Možnosti" + +[admin.settings.ai.general.enabled] +description = "Hlavný prepínač. Keď je vypnutý, nie sú dostupné žiadne AI nástroje, agenti ani volania engine." +label = "Povoliť AI" + +[admin.settings.ai.general.features.chat] +description = "Konverzačný asistent na prácu s PDF." +label = "Chat asistent" + +[admin.settings.ai.general.features.classify] +description = "Automaticky kategorizujte dokumenty podľa typu alebo obsahu." +label = "Klasifikácia dokumentov" + +[admin.settings.ai.general.features.createPdf] +description = "Vygenerujte nový PDF (napr. z HTML) cez AI agenta." +label = "Vytvoriť PDF z promptu" + +[admin.settings.ai.general.features.documentQuestions] +description = "Pýtajte sa otázky a získavajte odpovede založené na nahranom dokumente." +label = "Otázky k dokumentu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Skontrolujte dokumenty na matematické a číselné chyby." +label = "Matematický audítor" + +[admin.settings.ai.general.features.pdfComment] +description = "Pridajte do PDF komentáre a anotácie na kontrolu vytvorené AI." +label = "Agent komentárov PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Časový limit pre náročnejšie operácie agenta, ako je generovanie dokumentov." +label = "Časový limit dlhých operácií (sekundy)" + +[admin.settings.ai.general.note] +body = "AI engine beží ako samostatná služba. Jej zdieľané tajomstvo" +body2 = "sa nastavuje cez premennú prostredia kontajnera. Provider API keys je možné zadať na týchto stránkach alebo dodať ako premenné prostredia engine; kľúče zadané tu sa po uložení odošlú do engine." +title = "O AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Časový limit pre streamované (token po tokene) odpovede chatu." +label = "Časový limit streamu (sekundy)" + +[admin.settings.ai.general.test] +button = "Otestovať pripojenie" +failBody = "AI engine neodpovedal. Skontrolujte URL, či beží kontajner engine a či je AI povolené (po povolení je potrebný reštart)." +failTitle = "AI engine je nedostupný" +okBody = "AI engine odpovedal na kontrolu stavu." +okTitle = "AI engine je dostupný" + +[admin.settings.ai.general.timeoutSeconds] +description = "Časový limit pre štandardné AI požiadavky na engine." +label = "Časový limit požiadavky (sekundy)" + +[admin.settings.ai.general.url] +description = "Interná URL Python AI engine, napr. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Ochranné limity určujúce, koľko práce môžu AI požiadavky vykonať a koľko ich môže bežať súčasne. Po uložení sa použijú v AI engine." +title = "Limity a výkon" + +[admin.settings.ai.limits.maxCharacters] +description = "Ochranný limit: odmietnuť AI požiadavky, ktorých extrahovaný text prekročí túto dĺžku." +label = "Max. počet znakov na požiadavku" + +[admin.settings.ai.limits.maxPages] +description = "Ochranný limit: odmietnuť AI požiadavky nad tento počet strán PDF." +label = "Max. počet strán na požiadavku" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximálny počet súčasne prebiehajúcich volaní modelu v celom engine." +label = "Max. súbežnosť modelu" + +[admin.settings.ai.models] +description = "Vyberte poskytovateľa LLM a inteligentné/rýchle modely, ktoré AI engine používa. Po uložení sa použije v AI engine." +title = "Modely a poskytovatelia" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Nechajte prázdne, ak chcete použiť vlastné prihlasovacie údaje prostredia engine. Platí pre self-hosted nasadenia s jedným engine." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Uložené – nechajte prázdne, ak chcete ponechať aktuálny kľúč" + +[admin.settings.ai.models.baseUrl] +description = "Základná URL OpenAI-kompatibilného / Ollama endpointu, napr. http://ollama:11434/v1." +label = "Základná URL poskytovateľa" +warning = "Základná URL musí smerovať na dôveryhodný interný endpoint. Engine na ňu bude odosielať požiadavky zo servera, takže nedôveryhodná hodnota je citlivá na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximálny počet výstupných tokenov pre rýchly model." +label = "Max. tokeny rýchleho modelu" + +[admin.settings.ai.models.fastModel] +description = "Lacnejší a rýchlejší model pre jednoduché úlohy. Voľný text; návrhy sú iba pomôcky." +label = "Rýchly model" + +[admin.settings.ai.models.provider] +description = "S ktorým poskytovateľom LLM engine komunikuje." +label = "Poskytovateľ" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximálny počet výstupných tokenov pre inteligentný model." +label = "Max. tokeny inteligentného modelu" + +[admin.settings.ai.models.smartModel] +description = "Vysokovýkonný model pre komplexné uvažovanie. Voľný text; návrhy sú iba pomôcky." +label = "Inteligentný model" + +[admin.settings.ai.saved] +body = "Zmeny sa automaticky odosielajú do AI engine." +bodyNoPush = "Nastavenia boli uložené. Použijú sa pri najbližšom načítaní konfigurácie AI engine." +title = "AI nastavenia uložené" + [admin.settings.badge] clickToUpgrade = "Kliknite pre zobrazenie podrobností plánu" @@ -938,6 +1094,25 @@ label = "Znovu vygenerovať pri štarte" description = "Počet dní, počas ktorých bude certifikát platný" label = "Platnosť certifikátu (dni)" +[admin.settings.folderAccess] +description = "Adresáre, z ktorých môžu zdroje priečinkov a výstupy priečinkov čítať a do ktorých môžu zapisovať. Ide o bezpečnostnú hranicu: automatizácie nikdy nemožno nasmerovať na serverovú cestu mimo tohto zoznamu." +securityNote = "Nechajte prázdne, ak chcete úplne vypnúť zdroje a výstupy priečinkov. Vlastný konfiguračný adresár Stirling je vždy zakázaný." +title = "Prístup k priečinkom" + +[admin.settings.folderAccess.implied] +description = "Tieto adresáre spravované Stirling sú vždy povolené a tu ich nemožno zmeniť." +serverStorage = "Ukladanie súborov na serveri" +title = "Vždy povolené" +watchedFolder = "Sledovaný priečinok Pipeline" + +[admin.settings.folderAccess.roots] +add = "Pridať" +empty = "Nie sú povolené žiadne priečinky. Zdroje a výstupy priečinkov sú momentálne vypnuté." +hint = "Zadajte absolútne cesty, napríklad /data/inbox." +label = "Povolené koreňové priečinky" +placeholder = "/data/inbox" +remove = "Odstrániť koreňový priečinok" + [admin.settings.general] description = "Nakonfigurujte celosystémové nastavenia aplikácie vrátane značky a predvoleného správania." system = "Systém" @@ -1704,6 +1879,9 @@ width = "Šírka" [app] description = "Bezplatná alternatíva k Adobe Acrobat (10M+ stiahnutí)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konvertovať na PDF/A-3b" convertToPdfA3bDescription = "Vytvorí archívne PDF s vloženými prílohami" @@ -1842,6 +2020,15 @@ export = "Export" accessDenied = "Prístup zamietnutý" insufficientPermissions = "Nemáte oprávnenie vykonať túto akciu." +[auth.callback] +completing = "Dokončuje sa overenie" +invalidToken = "OAuth prihlásenie zlyhalo – neplatný token." +missingToken = "OAuth prihlásenie zlyhalo – nebol prijatý žiadny token." +oauthFailed = "OAuth prihlásenie zlyhalo. Skúste to znova." +pleaseWait = "Počkajte, kým dokončíme vaše prihlásenie." +signedOut = "Boli ste odhlásení. Prihláste sa znova." +windowMayClose = "Po dokončení môžete toto okno zavrieť." + [auth.displayName] guest = "Hosť" user = "Používateľ" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Zabezpečí dokumenty PDF odstránením potenciálne škod description = "Tento nástroj automaticky premenuje súbory PDF na základe ich obsahu. Analyzuje dokument, aby našiel najvhodnejší názov z textu." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automaticky otočiť" +tags = "automatické otočenie,opraviť orientáciu,hore nohami,nabok,vyrovnať,orientovať,zistiť orientáciu,opraviť otočenie,automatická orientácia,naskenované,OSD" + +[autoRotate.confidenceThreshold] +title = "Prah spoľahlivosti" + +[autoRotate.detectionMode] +auto = "Automaticky" +osd = "Iba OCR" +text = "Iba text" +title = "Metóda detekcie" + +[autoRotate.error] +failed = "Pri automatickom otáčaní PDF sa vyskytla chyba." + +[autoRotate.inferUndetected] +title = "Otočiť stránky s nízkou spoľahlivosťou podľa ostatných" + +[autoRotate.report] +applied = "{{degrees}}° v smere hodinových ručičiek" +confidenceValue = "Spoľahlivosť {{value}}" +noChange = "Bez zmeny" +pageLabel = "Strana {{number}}" +summary = "Otočených {{rotated}} z {{total}} strán, {{unchanged}} ponechaných bez zmeny" +title = "Správa o detekcii" + +[autoRotate.report.method] +inferred = "Zhodné" +none = "Preskočené" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Pod prahom" +blankPage = "Strana je prázdna" +inferredFromDocument = "Rovnako ako ostatné strany" +noDominantDirection = "Zmiešané smery textu" +osdFailed = "Nenašiel sa čitateľný text" +osdNoVerdict = "Nepodarilo sa určiť správnu orientáciu" +tesseractUnavailable = "OCR nie je nainštalované" +tooFewGlyphs = "Príliš málo textu" + +[autoRotate.results] +title = "Výsledky automatického otočenia" + +[autoRotate.settings] +title = "Nastavenia" + +[autoRotate.tooltip.confidenceThreshold] +text = "Ako istá musí byť kontrola, než sa stránka otočí. Znížte hodnotu, ak chcete otočiť viac strán, zvýšte ju, ak chcete ponechať viac strán nedotknutých." +title = "Prah spoľahlivosti" + +[autoRotate.tooltip.description] +text = "Zistí, ktorou stranou hore má byť každá stránka, a otočí ju správnym smerom. Stránky, ktoré už sú správne orientované, ponechá bez zmeny." + +[autoRotate.tooltip.detectionMode] +text = "Automaticky najprv prečíta text na stránke a potom, ak to nestačí, sa pozrie na stránku ako na obrázok. Iba text preskočí kontrolu obrázka, čo je najrýchlejšie pre dokumenty, ktoré nikdy neboli skenované. Iba OCR zaobchádza s každou stránkou ako s obrázkom." +title = "Metóda detekcie" + +[autoRotate.tooltip.header] +title = "Automatické otočenie" + +[autoRotate.tooltip.inferUndetected] +text = "Prázdne alebo takmer prázdne stránky nemožno spoľahlivo skontrolovať samostatne. Keď každá čitateľná stránka vyžaduje rovnaké otočenie, tieto stránky sa otočia podľa nich." +title = "Otočiť stránky s nízkou spoľahlivosťou podľa ostatných" + [autoSizeSplitPDF] tags = "pdf, rozdelenie, dokument, organizácia" @@ -2189,33 +2443,6 @@ keywords = "Kľúčové slová: odd, even" numbers = "Čísla/rozsahy: 5, 10-20" progressions = "Postupnosti: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Typy certifikátov" - -[certificateChoice.tooltip.organization] -bullet1 = "Spravované systémovými administrátormi" -bullet2 = "Zdieľané medzi autorizovanými používateľmi" -bullet3 = "Reprezentuje identitu spoločnosti, nie jednotlivca" -bullet4 = "Vhodné pre: oficiálne dokumenty, tímové podpisy" -description = "Zdieľaný certifikát poskytovaný vašou organizáciou. Používa sa na celofiremné podpisovanie." -title = "Organizačný certifikát" - -[certificateChoice.tooltip.personal] -bullet1 = "Generuje sa automaticky pri prvom použití" -bullet2 = "Viazaný na váš používateľský účet" -bullet3 = "Nie je možné zdieľať s inými používateľmi" -bullet4 = "Vhodné pre: osobné dokumenty, individuálnu zodpovednosť" -description = "Automaticky generovaný certifikát jedinečný pre váš používateľský účet. Vhodný pre individuálne podpisy." -title = "Osobný certifikát" - -[certificateChoice.tooltip.upload] -bullet1 = "Vyžaduje súbor P12/PFX a heslo" -bullet2 = "Môže byť vydaný externými certifikačnými autoritami" -bullet3 = "Vyššia úroveň dôvery pre právne dokumenty" -bullet4 = "Vhodné pre: právne záväzné zmluvy, externé overenie" -description = "Použite vlastný súbor certifikátu PKCS#12. Poskytuje plnú kontrolu nad vlastnosťami certifikátu." -title = "Nahrať vlastný P12" - [certSign] awaitingSignatures = "Čaká sa na podpisy" chooseCertificate = "Vybrať súbor certifikátu" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Zdroj certifikátu" upload = "Nahrať" -[certSign.tooltip.header] -title = "O správe podpisov" - -[certSign.tooltip.overview] -bullet1 = "Skontrolujte existujúce podpisy a ich platnosť" -bullet2 = "Zobrazte podrobné informácie o podpisujúcich a certifikátoch" -bullet3 = "Pridajte nové digitálne podpisy na zabezpečenie dokumentov" -bullet4 = "Podpora viacerých súborov s jednoduchou navigáciou" -text = "Tento nástroj vám umožňuje zistiť, či sú vaše PDF digitálne podpísané, a pridávať nové digitálne podpisy. Digitálne podpisy dokazujú, kto dokument vytvoril alebo schválil, a ukazujú, či bol po podpise zmenený." -title = "Čo tento nástroj dokáže?" - -[certSign.tooltip.signing] -bullet1 = "Podporuje formáty PEM, PKCS12, JKS a serverový certifikát" -bullet2 = "Možnosť zobraziť alebo skryť podpis v PDF" -bullet3 = "Pridajte dôvod, miesto a meno podpisujúceho" -bullet4 = "Zvoľte, na ktorej strane umiestniť viditeľné podpisy" -bullet5 = "Použite serverový certifikát pre jednoduchú možnosť 'Sign with Stirling-PDF'" -text = "Na podpis PDF potrebujete digitálny certifikát (napr. PEM, PKCS12 alebo JKS). Môžete zvoliť, či bude podpis v dokumente viditeľný, alebo ho ponechať neviditeľný len pre bezpečnosť." -title = "Pridávanie podpisov" - -[certSign.tooltip.validation] -bullet1 = "Zobrazuje, či sú podpisy platné alebo neplatné" -bullet2 = "Zobrazuje informácie o podpisujúcom a dátum podpisu" -bullet3 = "Kontroluje, či bol dokument po podpise upravený" -bullet4 = "Môže použiť vlastné certifikáty na overenie" -text = "Pri kontrole podpisov vám nástroj oznámi, či sú platné, kto dokument podpísal, kedy bol podpísaný a či bol dokument po podpise zmenený." -title = "Kontrola podpisov" - [changeCreds] changeUsername = "Aktualizujte svoje používateľské meno. Po aktualizácii budete odhlásení." credsUpdated = "Účet aktualizovaný" @@ -2737,12 +2936,16 @@ text = "Aby boli tieto povolenia nezmeniteľné, použite nástroj Add Password [chat.actions] copy = "Kopírovať správu" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Možnosti agenta Stirling" +assistant = "AI Assistant" clearChat = "Vyčistiť chat" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Táto operácia použije vaše cloudové kredity" tooltip = "Vybrať farbu z obrazovky" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Vyberte farbu" [common] +actions = "Actions" back = "Späť" cancel = "Zrušiť" close = "Zavrieť" +codeSample = "Code sample" collapse = "Zbaliť" confirm = "Potvrdiť" continue = "Pokračovať" copied = "Skopírované!" copy = "Kopírovať" done = "Hotovo" +enterValue = "Enter value" error = "Chyba" expand = "Rozbaliť" loading = "Načítava sa..." next = "Ďalej" +open = "Open" preview = "Náhľad" previous = "Predchádzajúce" refresh = "Obnoviť" +remaining = "Zostáva" retry = "Skúsiť znova" save = "Uložiť" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Vymazať výber" @@ -2988,6 +3199,7 @@ title = "Metóda kompresie" [compress.settings] desiredSize = "Požadovaná veľkosť súboru" desiredSizePlaceholder = "Zadajte veľkosť" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Kompresia je jednoduchý spôsob, ako zmenšiť veľkosť súboru. Zvoľte Veľkosť súboru a zadajte cieľovú veľkosť, pričom kvalitu upravíme za vás. Zvoľte Kvalita a nastavte silu kompresie manuálne." @@ -3190,6 +3402,24 @@ pdfOptions = "Možnosti PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Najprv vyberte zdrojový formát" settings = "Nastavenia" single = "Jeden" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Pokročilé monitorovanie" -allInOneWorkspace = "Všetko v jednom: pracovné prostredie pre PDF (prehliadač, nástroje a agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Vyhradená podpora a SLA" -fasterThroughput = "10× rýchlejšia priepustnosť" -fullyPrivateFiles = "Plne súkromné súbory" -largeFileProcessing = "Spracovanie veľkých súborov" -orgWideAccess = "Celopodnikové riadenie prístupu" -premiumAiModels = "Prémiové modely AI" -prioritySupport = "Prioritná podpora" -privateDocCloud = "Súkromný cloud dokumentov" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Zabezpečený prístup k API" -standardThroughput = "Štandardná priepustnosť" -unlimitedApiAccess = "Neobmedzený prístup k API" -unlimitedSeats = "Neobmedzený počet používateľov" - [crop] autoCrop = "Automaticky orezať biele okraje" noFileSelected = "Vyberte súbor PDF a začnite orezávať" @@ -3346,6 +3558,7 @@ message = "Nepodarilo sa nastaviť predvolenú aplikáciu PDF" title = "Chyba" [defaultApp.prompt] +dontRemind = "Už mi to nepripomínať" message = "Nastavte Stirling PDF ako predvolenú aplikáciu na otváranie PDF súborov." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Hľadať..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Upraviť" @@ -3644,11 +3861,13 @@ shareSelected = "Zdieľať vybrané" sharing = "Zdieľanie" showAll = "Zobraziť všetko" showHistory = "Zobraziť históriu" +sortBy = "Sort files" sortByDate = "Zoradiť podľa dátumu" sortByName = "Zoradiť podľa názvu" sortBySize = "Zoradiť podľa veľkosti" storage = "Úložisko" storageState = "Úložisko" +storageUsed = "Storage used" synced = "Synchronizované" title = "Nahrať súbory PDF" toolChain = "Použité nástroje" @@ -3694,13 +3913,17 @@ addFiles = "Pridať súbory" addingFiles = "Pridávajú sa súbory…" collapse = "Zbaliť bočný panel" customizeGroups = "Prispôsobiť skupiny" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Ak chcete začať, otvorte súbory" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Rozbaliť bočný panel" -files = "Súbory" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive nie je nakonfigurovaný" leaveMyFiles = "Opustiť Moje súbory" +library = "Knižnica PDF" myFiles = "Moje súbory" noFiles = "Zatiaľ žiadne súbory" openFileManager = "Otvoriť správcu súborov" @@ -3708,37 +3931,36 @@ openFromComputer = "Otvoriť z počítača" openSettings = "Otvoriť nastavenia" other = "Ostatné" recent = "Nedávne" -search = "Hľadať" -searchPlaceholder = "Hľadať súbory..." viewAll = "Zobraziť všetkých {{count}} súborov" [fileSidebar.fileItem] closeViewer = "Zavrieť prehliadač" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Otvoriť v prehliadači" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Pridať" -addLabel = "Pridať štítok…" -categoryIconAria = "Vybrať ikonu pre {{name}}" -createCategory = "Vytvoriť kategóriu" -delete = "Odstrániť kategóriu" done = "Hotovo" -hide = "Skryť kategóriu" -newCategory = "Nový názov kategórie…" -removeLabel = "Odstrániť {{name}}" -rename = "Premenovať" reset = "Obnoviť predvolené nastavenia" -search = "Hľadať štítky…" -show = "Zobraziť kategóriu" subtitle = "Zoskupte svoje súbory do nadradených kategórií. Pridajte existujúce alebo nové štítky do kategórie, premenujte ju alebo si vytvorte vlastnú. Súbory, ktoré nie sú v žiadnej z vašich kategórií, sa zobrazia v časti „Ostatné“." title = "Kategórie bočného panela" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Pridať do pracovného priestoru" addToWorkspaceCount = "Pridať {{count}} do pracovného priestoru" @@ -3749,7 +3971,6 @@ backToMyFiles = "Späť na Moje súbory" breadcrumbs = "Cesta k priečinku" cancel = "Zrušiť" classification = "Klasifikácia" -clearSearch = "Vymazať vyhľadávanie" clearSelection = "Zrušiť výber" closeDetails = "Zavrieť podrobnosti" create = "Vytvoriť" @@ -3781,6 +4002,7 @@ downloadAll = "Stiahnuť všetko" downloadVersion = "Stiahnuť túto verziu" dropOverlay = "Pustite súbory sem na nahratie" dropOverlaySub = "Súbory začnú v Lokálne. Použite 'Presunúť do' alebo 'Uložiť do cloudu' na ich usporiadanie do priečinka." +duplicate = "Duplicate" file = "Súbor" fileInfo = "File info" fileMenu = "Akcie súboru" @@ -3808,13 +4030,12 @@ refresh = "Obnoviť zo servera" remove = "Odstrániť" removeVersion = "Odstrániť túto verziu" rename = "Premenovať" +renamed = "Premenované" renameFolder = "Premenovať priečinok" resizeFolderTree = "Zmeniť veľkosť stromu priečinkov (šípkami, Shift pre väčšie kroky; dvojklik pre automatické prispôsobenie)" save = "Uložiť" saveToServer = "Uložiť na server" saveToServerDisabledHint = "Ukladanie na server nie je na tomto serveri povolené. Požiadajte správcu, aby ho povolil." -search = "Hľadať" -searchPlaceholder = "Hľadať v tomto priečinku a podpriečinkoch" selectAll = "Vybrať všetko" selectAllHint = "Kliknutím vyberiete všetko. Tip: podržte Ctrl (alebo Cmd) na pridávanie súborov po jednom, Shift na výber rozsahu." selectedCount = "Vybrané: {{count}}" @@ -3874,6 +4095,10 @@ title = "Zatiaľ žiadne cloudové súbory" hint = "Súbory uložené bez nahrania zostanú tu. Pretiahnite súbor, aby ste pridali nový." title = "Žiadne iba lokálne súbory" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Súbory, ktoré otvoríte alebo upravíte, sa zobrazia tu." title = "Zatiaľ nič upravené" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Priečinok sa nepodarilo odstrániť." deleteFolderFailedDetail = "Priečinok sa nepodarilo odstrániť: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nepodarilo sa aktualizovať vzhľad priečinka." folderAppearanceFailedDetail = "Nepodarilo sa aktualizovať vzhľad priečinka: {{message}}" moveFilesFailed = "Nepodarilo sa presunúť súbory." @@ -3910,9 +4137,7 @@ count = "Súbory" folder = "Priečinok" labels = "Štítky" modified = "Zmenené" -name = "Názov" size = "Veľkosť" -toolHistoryAtVersion = "Kumulatívny reťazec nástrojov" totalSize = "Celková veľkosť" type = "Typ" versionHistory = "Vývoj verzií" @@ -3945,6 +4170,11 @@ localHint = "Uložené len v tomto prehliadači" shared = "Zdieľané" sharedHint = "Zdieľané s vami cez odkaz" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Najstaršie ako prvé" modifiedDesc = "Najnovšie ako prvé" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "V úložisku nie sú dostupné žiadne súbory. Najprv o open = "Otvoriť" openFile = "Otvoriť súbor" openFiles = "Otvoriť súbory" +selectFile = "Select {{name}}" selectFromStorage = "Vybrať z úložiska" upload = "Nahrať" uploadFile = "Nahrať súbor" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Všetko uložené" +analyzingFields = "Analysing form fields..." +extractCsvError = "Nepodarilo sa extrahovať CSV" +extractXlsxError = "Nepodarilo sa extrahovať XLSX" +filled = "filled" flattenAfterFilling = "Spľoštiť po vyplnení" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "pov" +requiredFieldsError = "Vyplňte všetky povinné polia" rescanFields = "Znovu naskenovať polia" rescanFormFields = "Znovu naskenovať polia formulára" save = "Uložiť" +saveShortcut = "Ctrl+S na uloženie" +unsavedChanges = "Neuložené zmeny" [formFill.sidebar] close = "Zavrieť bočný panel" @@ -4309,6 +4552,11 @@ desc = "Automaticky premenuje PDF súbor podľa zistenej hlavičky" tags = "automaticky zistiť,na základe hlavičky,organizovať,premenovať" title = "Auto premenovať PDF" +[home.autoRotate] +desc = "Zistite orientáciu každej strany a automaticky ju otočte správne." +tags = "automatické otočenie,opraviť orientáciu,hore nohami,nabok,vyrovnať,orientovať,zistiť orientáciu,opraviť otočenie,automatická orientácia,naskenované,OSD" +title = "Automatické otočenie" + [home.autoSizeSplitPDF] tags = "auto,rozdeliť,veľkosť" title = "Auto rozdeliť veľkosť/počet" @@ -4608,15 +4856,12 @@ hint = "Nahrajte PNG, JPG, SVG alebo iný obrázkový súbor, ktorý umiestnite label = "Nahrať obrázok" placeholder = "Vybrať súbor s obrázkom" -[imageToPdf] -tags = "konverzia,img,jpg,obrázok,fotografia" - [imageToPDF] header = "Obrázok na PDF" title = "Obrázok na PDF" -[infoBanner] -dismiss = "Zavrieť" +[imageToPdf] +tags = "konverzia,img,jpg,obrázok,fotografia" [invite] acceptError = "Nepodarilo sa vytvoriť účet" @@ -4647,11 +4892,9 @@ welcomeTitle = "Boli ste pozvaní!" [landing] addFiles = "Pridať súbory" -heroSubtitle = "Pretiahnite alebo pridajte existujúci súbor PDF a začnite." mobileUpload = "Nahratie z mobilu" openFromComputer = "Otvoriť z počítača" uploadFromComputer = "Nahrať z počítača" -workbenchEmptyStateHero = "Presuňte PDF kamkoľvek" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Zadajte používateľské meno" failedToSignIn = "Prihlásenie cez {{provider}} zlyhalo: {{message}}" forgotPassword = "Zabudli ste heslo?" loggingIn = "Prihlasovanie..." -logIn = "Prihlásiť sa" login = "Prihlásenie" magicLinkSent = "Magický odkaz bol odoslaný na {{email}}! Skontrolujte si e-mail a kliknite na odkaz na prihlásenie." mfaCode = "Overovací kód" @@ -4715,26 +4957,10 @@ signInWith = "Prihlásiť sa pomocou" title = "Prihlásenie" unexpectedError = "Neočakávaná chyba: {{message}}" updatePassword = "Aktualizovať heslo" -useEmailInstead = "Prihlásiť sa pomocou e-mailu" useMagicLink = "Namiesto toho použiť magický odkaz" username = "Používateľské meno" youAreLoggedIn = "Ste prihlásení!" -[login.slides.edit] -alt = "Úprava PDF" -subtitle = "S viac než tuctom nástrojov na začierňovanie, podpisovanie, čítanie a prácu s PDF určite nájdete, čo hľadáte." -title = "Upravujte PDF na zobrazenie/ochranu požadovaných informácií" - -[login.slides.overview] -alt = "Prehľad Stirling PDF" -subtitle = "Cloudová sada s dôrazom na súkromie pre PDF: konvertovanie, podpisovanie, začierňovanie a správa dokumentov, a 50+ ďalších výkonných nástrojov." -title = "Všetko pre vaše PDF na jednom mieste." - -[login.slides.secure] -alt = "Zabezpečenie PDF" -subtitle = "Jednoducho pridávajte heslá, začierňujte obsah a spravujte certifikáty." -title = "Chráňte citlivé informácie v PDF" - [margin] large = "Veľké" medium = "Stredné" @@ -4818,6 +5044,34 @@ uploadSuccess = "Nahrávanie úspešné!" uploadSuccessMessage = "Vaše obrázky boli prenesené." validating = "Overuje sa relácia..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Naskenujte na nahratie fotiek. Obrázky sa automaticky konvertujú na PDF." descriptionNoConvert = "Naskenujte na nahratie fotiek z mobilného zariadenia." @@ -4836,8 +5090,13 @@ title = "Nahratie z mobilu" tags = "Multi Tool,Multi operácie,UI,klik drag,front end,beží na klientovi,interaktívne,intraktívne,posunúť" title = "PDF Multi Nástroj" -[navbar] -search = "Hľadať" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Overenie nebolo úspešné. Môžete zavrieť toto okno a skúsiť znova." @@ -4966,6 +5225,7 @@ activeFiles = "Zobrazenie Aktívne súbory zobrazuje všetky PD allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zavrieť" cropSettings = "Keď sme vybrali súbor, ktorý chceme orezať, môžeme nakonfigurovať nástroj Orez tak, aby sme zvolili oblasť, na ktorú chceme PDF orezať." +dialogLabel = "Onboarding" fileCheckbox = "Kliknutím na niektorý zo súborov ho vyberiete na spracovanie. Môžete vybrať viac súborov pre hromadné operácie." fileReplacement = "Upravený súbor automaticky nahradí pôvodný súbor na pracovnej ploche, čo vám umožní ľahko ho spracovať ďalšími nástrojmi." filesButton = "Tlačidlo Súbory na paneli rýchleho prístupu umožňuje nahrať PDF, na ktorých môžete nástroje používať." @@ -4976,6 +5236,7 @@ pinButton = "Môžete použiť tlačidlo Pripnúť, ak chcete, results = "Po dokončení behu nástroja krok Kontrola zobrazí v tomto paneli náhľad výsledkov a umožní vrátiť operáciu alebo stiahnuť súbor. " runButton = "Keď je nástroj nakonfigurovaný, toto tlačidlo umožní spustiť ho na všetkých vybraných PDF." selectCropTool = "Vyberme nástroj Orez, aby sme ukázali, ako používať jeden z nástrojov." +stepOf = "Krok {{current}} z {{total}}" toolInterface = "Toto je rozhranie nástroja Orez. Ako vidíte, zatiaľ tu toho veľa nie je, pretože sme ešte nepridali žiadne súbory PDF, s ktorými by sme pracovali." workbench = "Toto je Pracovná plocha - hlavná oblasť, kde si prezeráte a upravujete svoje PDF." wrapUp = "Všetko pripravené! Dozvedeli ste sa o hlavných častiach aplikácie a ich použití. Kedykoľvek chcete, kliknite na tlačidlo Pomoc a prehliadku si zobrazte znova." @@ -4988,6 +5249,26 @@ showMeAround = "Ukážte mi to" skipForNow = "Preskočiť zatiaľ" skipTheTour = "Preskočiť prehliadku" +[onboarding.checklist] +dismiss = "Zavrieť" +title = "Nastaviť Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Spustite Stirling natívne na svojom počítači" +title = "Stiahnuť Stirling pre Desktop" + +[onboarding.checklist.inviteTeam] +description = "Spolupracujte so svojím tímom" +title = "Pozvať členov tímu" + +[onboarding.checklist.shareAnalytics] +description = "Pomôžte zlepšiť Stirling" +title = "Zdieľať anonymné údaje o používaní" + +[onboarding.checklist.takeTour] +description = "Pozrite si v krátkej prehliadke, ako Stirling funguje" +title = "Absolvovať prehliadku" + [onboarding.desktopInstall] body = "Stirling najlepšie funguje ako desktopová aplikácia. Môžete ju používať offline, pristupovať k dokumentom rýchlejšie a upravovať lokálne na počítači." selectOs = "Vyberte operačný systém" @@ -4996,7 +5277,21 @@ titleWithOs = "Stiahnuť pre {{osLabel}}" [onboarding.mfa] authenticationCode = "Overovací kód" +description = "Zabezpečte svoj účet prepojením s aplikáciou autentifikátora. Naskenujte QR kód alebo zadajte nastavovací kľúč a potom potvrďte 6-miestny kód na dokončenie." +enable = "Povoliť MFA" +enableError = "Nepodarilo sa povoliť dvojfaktorové overenie. Skontrolujte kód a skúste to znova." +enterCode = "Zadajte 6-miestny kód zo svojej aplikácie." +enterCodeError = "Zadajte overovací kód, aby ste mohli pokračovať." +logout = "Odhlásiť sa" +openAuthenticator = "Otvorte Google Authenticator, Authy alebo 1Password." qrCodeLoading = "Generuje sa váš QR kód…" +regenerateQrCode = "Znovu vygenerovať QR kód" +scanQrCode = "Naskenujte QR kód alebo zadajte nastavovací kľúč nižšie." +setupError = "Nepodarilo sa spustiť nastavenie dvojfaktorového overenia. Skúste to znova." +setupKey = "Nastavovací kľúč (ručné zadanie)" +stepByStep = "Krok za krokom" +success = "MFA bolo povolené. Teraz môžete pokračovať." +title = "Nastavenie viacfaktorového overenia" [onboarding.planOverview] adminBodyLoginDisabled = "Keď zapnete režim prihlásenia, môžete spravovať používateľov, konfigurovať nastavenia a monitorovať stav servera. Prvých {{freeTierLimit}} ľudí na vašom serveri môže používať Stirling bezplatne." @@ -5005,6 +5300,11 @@ adminTitle = "Prehľad pre admina" userBody = "Pozvite kolegov, priraďujte roly a udržujte dokumenty organizované v jednom bezpečnom pracovnom priestore. Režim prihlásenia zapnite, keď ste pripravení rásť nad sólo použitie." userTitle = "Prehľad plánu" +[onboarding.processorIntro] +body = "Stirling teraz spúšťa Pravidlá — automatizované pravidlá, ktoré klasifikujú, zabezpečujú a spracúvajú každý dokument pri jeho príchode. Nastavte ich a sledujte behy v Procesore." +cta = "Pozrieť si Procesor" +title = "Pozrite si Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Predplatený rok" + +[payg.prepaid.banner] +exhausted = "Vaša predplatená kapacita je vyčerpaná; účtovanie podľa spotreby bolo obnovené. Dobite kredit, aby ste si zachovali zľavu." +expiring_one = "Váš predplatený rok končí o {{count}} deň. Nevyužitá kapacita vyprší — dobite kredit a začnite nový rok." +expiring_other = "Váš predplatený rok končí o {{count}} dní. Nevyužitá kapacita vyprší — dobite kredit a začnite nový rok." +low = "Zostáva vám {{pct}}% predplatenej kapacity. Kredit môžete kedykoľvek dobiť, aby spracovanie pokračovalo bez prerušenia." + +[payg.prepaid.card] +subtitle = "Používa sa pred účtovaním podľa spotreby a mimo vášho limitu výdavkov." +title = "Predplatená kapacita" + +[payg.prepaid.meter] +capSuffix = "z {{total}} predplatených kreditov" +expires = "Vyprší {{date}}" + +[payg.prepaid.state] +exhausted = "Vyčerpané" +healthy = "Dostatok zostáva" +low = "Dochádza" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF na PDF/A" tags = "archív,dĺhodobé,štandard,konverzia,uchovanie" title = "PDF na PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tlač,štandard,konverzia,produkcia,predtlačová príprava,archív" title = "PDF na PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kreditov" xsmall = "100 kreditov" [plan.availablePlans] +currency = "Billing currency" subtitle = "Vyberte si plán, ktorý vám vyhovuje" title = "Dostupné plány" @@ -5937,49 +6267,12 @@ keyDescription = "Vložte licenčný kľúč z e‑mailu" success = "Licencia aktivovaná!" successMessage = "Vaša licencia bola úspešne aktivovaná. Teraz môžete toto okno zatvoriť." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "vynútené" -enforcing = "Prebieha vynucovanie..." -failed = "Vynucovanie zlyhalo" outputsUnavailable = "Výstupy zásad už nie sú dostupné na stiahnutie." partialOutputsUnavailable = "Niektoré výstupy zásad už nie sú dostupné na stiahnutie." -retrying = "Zaneprázdnené, skúšam znova..." runNotFound = "Spustenie vynucovania sa už nedá nájsť." -step = "krok {{current}}/{{total}}" timedOut = "Časový limit vynucovania vypršal skôr, než sa spustenie dokončilo." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zavrieť" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Uplatňuje sa {{names}}" applyingProgress = "Uplatňuje sa {{names}} ({{done}} z {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} z {{total}} súborov nebolo možné spracovať a bol failureTitle = "Exportované bez úplného vynucovania" printPolicyAppliedBody = "Tento PDF súbor bol upravený, aby spĺňal zásadu. Skontrolujte zmeny a potom vytlačte znova." printPolicyAppliedTitle = "Zásada uplatnená pred tlačou" -queued = "+{{count}} v poradí" successTitle = "{{names}} uplatnené" summaryMore = "{{first}}, {{second}} a ďalších {{more}}" summaryTwo = "{{first}} a {{second}}" -[policies.enforcement.triggerVerb] -convert = "Vynucovanie pred konverziou" -default = "Vynucovanie" -export = "Vynucovanie pred exportom" -input = "Vynucovanie pri importe" -print = "Vynucovanie pred tlačou" - -[policies.field] -accessLog = "Protokol prístupu" -archiveAfter = "Archivovať po" -auditTrail = "Auditná stopa" -belowThreshold = "Pod prahom" -destination = "Cieľ" -frameworks = "Rámce" -immutableHold = "Nemenné pozdržanie" -keepFor = "Uchovať na" -minConfidence = "Min. spoľahlivosť" -notify = "Upozorniť pri smerovaní" -onViolation = "Pri nesúlade" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Pridať" -addPlaceholder = "Pridať štítok…" -customNote = "Prispôsobené pre váš tím." -defaultNote = "Používa sa vstavané predvolené nastavenie zdieľané s vaším tímom." -duplicate = "„{{name}}“ už existuje." -edit = "Upraviť štítky" -empty = "Zatiaľ žiadne štítky." -export = "Exportovať JSON" -iconAria = "Vybrať ikonu pre {{name}}" -import = "Importovať JSON" -importError = "Tento súbor sa nepodarilo importovať." -managedNote = "Tímové štítky spravuje vedúci vášho tímu." -modalSubtitle = "Zdieľané s celým vaším tímom. Klasifikátor vyberie štítky, ktoré sa hodia ku každému dokumentu." -modalTitle = "Klasifikačné štítky" -pickIcon = "Použiť ikonu {{label}}" -removeAria = "Odstrániť {{name}}" -resetToDefault = "Obnoviť predvolené" -saveForTeam = "Uložiť pre tím" -saving = "Ukladá sa…" -sectionLabel = "Klasifikačné štítky" -startFromScratch = "Začať od začiatku" -teamCount = "tímové štítky" -tooLong = "Štítky môžu mať najviac {{max}} znakov." -ungrouped = "Nezoskupené" -view = "Zobraziť štítky" +categoryCount = "kategórií" +hideCategory = "Skryť kategóriu" +labelCount = "štítkov" +sharedNote = "Tieto štítky sú vstavané a zdieľané v celom vašom tíme." +showCategory = "Zobraziť kategóriu" [policies.pii] account = "Čísla účtov (označené)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "US smerovacie čísla (ABA)" ssn = "Čísla sociálneho zabezpečenia" -[policies.settings] -noneExport = "Pri exporte sa momentálne nespúšťajú žiadne zásady." -noneUpload = "Pri nahrávaní sa momentálne nespúšťajú žiadne zásady." -onExport = "Pri exporte" -onUpload = "Pri nahrávaní" -reorderHandle = "Potiahnutím zmeniť poradie" -runOrderDesc = "Keď sa pri rovnakom spúšťači spustí viac než jedna zásada, spustia sa v tomto poradí — každá na výstupe predchádzajúcej zásady. Potiahnutím zmeníte poradie." -title = "Nastavenia zásad" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Možnosti zásad" -policySettings = "Nastavenia zásad" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Znova skúsiť neúspešné zásady ({{count}})" -rowProgress = "Spracovaných {{completed}} z {{total}} súborov" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Čo je zásada?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automaticky nájde a zatemní citlivé údaje — napríklad čísla sociálneho zabezpečenia a čísla kariet — aby nebolo možné ich v dokumente prečítať." -sanitize = "Odstráni skrytý JavaScript zo súboru, aby sa pri otvorení nič nespúšťalo automaticky." -watermark = "Vloží viditeľnú značku (napr. „Dôverné“) na každú stranu." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zavrieť" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} sa vynucuje..." badgeRan = "Zásada {{name}} sa spustila na tomto súbore" +badgeRunning = "{{name}} beží..." blockingAction = "{{action}} zablokované počas vynucovania zásady, čakajte prosím..." dismiss = "Zavrieť prekrytie" enforcingTitle = "Vynucuje sa zásada..." @@ -6151,6 +6316,7 @@ viewAnyway = "Zobraziť súbor (zásada sa stále vynucuje)" [policyOption] autoRedactPhi = "Automaticky začierniť PHI" blockExport = "Blokovať export" +ccpa = "CCPA" contracts = "Zmluvy" documents = "Dokumenty" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Plán Editor" subscribed = "Plán Processor" unlinked = "Neprepojené" -[portal.agentBuilder] -bootstrapFromDocument = "Inicializovať z dokumentu" -sectionsAriaLabel = "Sekcie tvorcu agentov" -selectorAriaLabel = "Agenti" -subtitle = "Navrhujte, testujte a nasadzujte AI agentov, ktorí klasifikujú, extrahujú z vašich dokumentov a smerujú ich. Definujte scenáre, obmedzte prístup k nástrojom, spustite golden set a publikujte verziu." -title = "Tvorca agentov" - -[portal.agentBuilder.bootstrap] -cancel = "Zrušiť" -dropzoneText = "Vyberte vzorový dokument (PDF alebo obrázok)" -lead = "Presuňte vzorový dokument a navrhneme scenáre a schému extrakcie, ktoré môžete doladiť. Nič sa nepublikuje, kým to neskontrolujete." -submit = "Inicializovať agenta" -subtitle = "Vytvoriť základ nového agenta z jedného reprezentatívneho súboru" -title = "Inicializovať z dokumentu" - -[portal.agentBuilder.empty] -description = "Inicializujte agenta zo vzorového dokumentu, aby sa vytvoril základ scenárov a schémy extrakcie, potom ho dolaďte a publikujte." -title = "Zatiaľ žiadni agenti" - -[portal.agentBuilder.evals] -casesPassing = "Úspešné prípady" -columnCase = "Eval prípad" -columnLatency = "Latencia" -columnResult = "Výsledok" -fail = "neúspech" -goldenSetPassRate = "Miera úspešnosti golden-set" -latencyMs = "{{ms}} ms" -notRun = "nespustené" -pass = "úspech" -passRate = "Miera úspešnosti" -runEvals = "Spustiť evals" - -[portal.agentBuilder.evals.empty] -description = "Evals premieňajú vaše scenáre na opakovateľný golden set. Prejdite na vyšší plán, aby ste mohli sledovať mieru úspešnosti v čase a podmieniť ňou publikovanie." -title = "Zatiaľ žiadny golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "naprieč golden setmi" -activeAgents = "Aktívni agenti" -avgPassRate = "Priemerná úspešnosť eval" -fleetWide = "v celej flotile" -latestPublished = "Naposledy publikované" -scenarios = "Scenáre" -testCases = "testovacie prípady" -totalDescription_one = "{{count}} celkom" -totalDescription_other = "{{count}} celkom" - -[portal.agentBuilder.scenarios] -add = "Pridať" -addScenario = "Pridať scenár" -enable = "Povoliť" -expectationLabel = "Očakávané správanie" -expectationPlaceholder = "Čo má agent urobiť" -inEval = "v eval" -mute = "Stlmiť" -muted = "stlmené" -nameLabel = "Názov" -namePlaceholder = "napr. Eskalácia súladu" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenáre" -tools = "Nástroje" -versions = "Verzie" - -[portal.agentBuilder.tools] -broadAccess = "Široký prístup" -deniedHint = "Vybrané nástroje sú blokované. Všetko ostatné zostáva vyvolateľné." -deniedTools = "Zakázané nástroje" -governanceGate = "Správa nástrojov je dostupná v pláne Enterprise." -restricted = "Obmedzené" -restrictedAccess = "Obmedzený prístup k nástrojom" -restrictedDescription = "Povoliť každý nástroj okrem tých, ktoré nižšie zakážete." - -[portal.agentBuilder.versions] -current = "aktuálna" -historyGate = "Úplná história verzií a rollback sú dostupné v pláne Enterprise." -publish = "Publikovať" -rollBack = "Vrátiť späť" - [portal.assistant] close = "Zavrieť asistenta" error = "Asistenta sa nepodarilo kontaktovať." @@ -6365,9 +6451,21 @@ typing = "Píše" loading = "Načítava sa" redirectingToEditor = "Presmerováva sa do editora..." +[portal.billing.activation] +maybeLater = "Možno neskôr" +title = "Zapnúť Procesor" + +[portal.billing.activation.payg] +sub = "Nastavte mesačný limit. Plaťte za to, čo spustíte." +title = "Plaťte podľa spotreby" + +[portal.billing.activation.prepay] +badge = "2 mesiace zadarmo" +sub = "Jedna faktúra, karta alebo bankový prevod." +title = "Predplatiť rok" + [portal.billing.checkout] noClientSecret = "Edge funkcia nevrátila client_secret." -subtitle = "Pridajte kartu, aby ste mohli pokračovať po vyčerpaní bezplatného grantu plánu Editor. Stripe sa postará o zvyšok." title = "Zapnúť plán Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Vaša platba bola úspešná, ale aktivácia trvá o niečo dlhšie než close = "Zavrieť" title = "Už takmer hotovo" +[portal.billing.checkout.cap] +amountAria = "Mesačný limit výdavkov" +back = "Späť" +continue = "Pokračovať k platbe" +error = "Nepodarilo sa nastaviť limit výdavkov" +finePrint = "Faktúry sa vystavujú mesačne. Zrušiť môžete kedykoľvek." +noLimit = "Bez limitu" +note = "Nikdy vám nebude účtované viac než váš limit. Spracovanie sa iba pozastaví." +perMonth = "/ mes." +title = "Nastavte svoj limit výdavkov" + +[portal.billing.checkout.card] +fields = "Číslo karty · MM / RR · CVC · ZIP" +label = "Údaje o karte" +note = "Údaje o karte zhromažďuje Stripe. Stirling nikdy neukladá PAN ani CVC." + [portal.billing.checkout.error] title = "Checkout sa nepodarilo spustiť" @@ -6383,10 +6497,9 @@ body = "Vaša platba prešla. Zapíname merané spracovanie naprieč vašimi pre hint = "Nechajte toto okno otvorené." title = "Aktivuje sa váš plán Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "v env portálu na povolenie checkoutu v aplikácii." -bodyBefore = "Nastavte" -title = "Stripe nie je nakonfigurovaný" +[portal.billing.checkout.pay] +limit = "Mesačný limit výdavkov {{amount}}/mes. Spracovanie sa pri limite pozastaví." +limitNoCap = "Bez limitu výdavkov. Spracovanie sa nepozastaví. Faktúry sa vystavujú mesačne." [portal.billing.enterpriseUpsell] cta = "Vytvoriť vašu ponuku Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Aktuálny plán" freeForever = "Navždy zadarmo" noTeamResolved = "Vo vašej peňaženke zatiaľ nie je vyriešený žiadny tím — obnovte stránku a skúste to znova." ownerOnly = "Plán Processor môže zapnúť iba vlastník tímu." +payInvoice = "Zaplatiť faktúru na dokončenie" planName = "Editor" ssoIncluded = "SSO zahrnuté" switchOnProcessor = "Zapnúť Processor →" unlimitedUsers = "Neobmedzení používatelia" +viewQuote = "Zobraziť cenovú ponuku" [portal.billing.invoices] columnAmount = "Suma" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Priame API požiadavky" segmentApiLabel = "API" segmentAutomationDesc = "Automatizácie a pipeline" segmentAutomationLabel = "Automatizácia" +sizeMultiplier = "{{formatted}} PDF použilo násobiteľ veľkosti" +summary = "{{unique}} jedinečných · {{units}} jednotiek merania · {{avg}} priemer na PDF" +summaryNoRate = "{{unique}} jedinečných · {{units}} jednotiek merania" unit = "merané PDF" +unitsPending_one = "{{units}} jednotka merania čaká na synchronizáciu z prepojených inštancií" +unitsPending_other = "{{units}} jednotiek merania čaká na synchronizáciu z prepojených inštancií" + +[portal.billing.prepaid] +capSuffix = "z {{total}} predplatených kreditov" +expires = "Vyprší {{date}}" +eyebrow = "Predplatená kapacita" +note = "Používa sa pred účtovaním podľa spotreby a mimo vášho limitu výdavkov." +topUp = "Dobiť" + +[portal.billing.prepaid.buy] +back = "Späť" +calcTitle = "Vypočítajte svoju ročnú platbu" +cancel = "Zrušiť" +cancelPurchase = "Zrušiť nákup" +close = "Zavrieť" +continueToPayment = "Pokračovať k platbe" +finalise = "Dokončiť" +invoiceErrorTitle = "Nepodarilo sa vygenerovať faktúru" +loadingTitle = "Načítava sa vaša ponuka" +payTitle = "Zaplaťte za svoj rok" +step = "Krok {{current}} z {{total}}" + +[portal.billing.prepaid.calc] +change = "Zmeniť" +deployRow = "Nasadenie" +done = "Hotovo" +enterpriseHint = "Toto je podniková úroveň. Pri tomto objeme sú enterprise sadzby výhodnejšie než akákoľvek samoobslužná zľava. Uzamknutie sadzby, podmienky a cenová ponuka za pár minút." +governanceRow = "Riadenie" +governanceValue = "{{label}} · {{count}} pravidiel" +handlesLabel = "Váš Procesor" +handlesValue = "spracuje {{volume}} kreditov / mes." +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} pravidiel" +poolCaption = "Jeden fond {{credits}} kreditov na rok. Náročné mesiace si požičiavajú z ľahších." +priceLabel = "Váš rok · 12 mesiacov za cenu 10" +rateUnknown = "Presnú cenu zobrazíme pri pokladni." +savings = "Ušetríte {{amount}} · 2 mesiace zadarmo." +sizingRow = "Veľkosť" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Celkový počet používateľov" + +[portal.billing.prepaid.consent] +label = "Rozumiem, že keď sa moja predplatená kapacita vyčerpá alebo vyprší po 12 mesiacoch, spracovanie bude automaticky pokračovať za štandardnú meranú sadzbu pay-as-you-go (až do môjho limitu výdavkov), pokiaľ ho nezruším, a že ho môžem kedykoľvek zrušiť z billing portálu." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Spravuje Stirling. Spustené za pár minút." +cloudValue = "Stirling Cloud · Spravované" +selfhost = "Self-hosted" +selfhostDesc = "Beží na súkromnej infraštruktúre. Rovnaká sadzba." +selfhostValue = "Self-hosted · Súkromná infraštruktúra" + +[portal.billing.prepaid.offer] +cta = "Skontrolovať ponuku" +subtitle = "Predplaťte si rok spracovania PDF a získajte dva mesiace zadarmo, použité pred účtovaním podľa spotreby, mimo vášho limitu výdavkov." +title = "Získajte 12 mesiacov za cenu 10" + +[portal.billing.prepaid.pay] +accountName = "Meno majiteľa účtu" +accountNamePlaceholder = "napr. Jana Nováková" +companyName = "Názov spoločnosti (voliteľné)" +companyNamePlaceholder = "napr. Acme Inc." +downloadInvoice = "Stiahnuť faktúru" +payOnline = "Zaplatiť online" +poNumber = "Číslo objednávky (voliteľné)" +poNumberPlaceholder = "napr. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Pokročilé" +advancedDesc = "Pipelines zabezpečujú väčšinu vášho spracovania." +none = "Žiadne" +noneDesc = "Zatiaľ nespúšťate pipelines. Zapnúť ich môžete kedykoľvek." +standard = "Štandardné" +standardDesc = "Niekoľko pipelines znovu spracúva prichádzajúce PDF." + +[portal.billing.prepaid.posture] +essentials = "Základy" +essentialsDesc = "Klasifikácia, predvolené nastavenie, plus zdieľanie" +governed = "Riadené" +governedDesc = "Pridáva zabezpečenie a smerovanie" +regulated = "Regulované" +regulatedDesc = "Každá kategória vrátane dodržiavania predpisov, uchovávania a prijímania" + +[portal.billing.prepaid.review] +download = "Stiahnuť ponuku (PDF)" +downloading = "Pripravuje sa…" +share = "zdieľať na schválenie" + +[portal.billing.prepaid.size] +compact = "Kompaktné" +compactDesc = "Súbory pod 25 MB, bez poplatkov za dáta" +heavy = "Ťažké" +heavyDesc = "Skenované alebo s množstvom obrázkov, bežne 50 MB+" +standard = "Štandardné" +standardDesc = "Väčšinou malé, niektoré skeny nad 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Vyčerpané" +healthy = "Dostatok zostáva" +low = "Dochádza" [portal.billing.spendLimit] adjustLimit = "Upraviť limit" @@ -6494,6 +6713,8 @@ label = "Predpokladá sa prekročenie." [portal.billing.spendThisMonth] eyebrow = "Výdavky tento mesiac" +freeRemaining_one = "Zostáva {{formatted}} bezplatný PDF" +freeRemaining_other = "Zostáva {{formatted}} bezplatných PDF" processed_one = "Spracované {{formattedCount}} PDF." processed_other = "Spracovaných {{formattedCount}} PDF." processedWithRate_one = "Spracované {{formattedCount}} PDF, po {{rate}} za kus." @@ -6510,6 +6731,7 @@ reachedTitle = "Mesačný limit výdavkov bol dosiahnutý" title = "Portál Stripe sa nepodarilo otvoriť" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "z {{allowance}} bezplatných PDF použitých" capSuffix_other = "z {{allowance}} bezplatných PDF použitých" eyebrow = "Skúšobná verzia Processor" @@ -6534,6 +6756,376 @@ signature = "podpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nové pripojenie" +createTitleFor = "Pripojiť {{name}}" +delete = "Odstrániť" +edit = "Upraviť" +editTitle = "Upraviť pripojenie" +unsupportedType = "Tento typ pripojenia tu nemožno upraviť." + +[portal.connections.categories.advanced] +description = "Pre službu, ktorú zatiaľ nepodporujeme." +label = "Pokročilé" + +[portal.connections.categories.audit] +description = "Zaznamenajte, čo sa stalo s dokumentom, tam, kde už vaši audítori hľadajú." +label = "Audit a záznam dodržiavania predpisov" + +[portal.connections.categories.notify] +description = "Informujte iný systém, že sa niečo stalo." +label = "Upozornenia a workflow" + +[portal.connections.categories.security] +description = "Naskenujte alebo označte dokument predtým, než sa posunie ďalej." +label = "Zabezpečenie a klasifikácia" + +[portal.connections.categories.signing] +description = "Odošlite dokument na podpis alebo notárske overenie." +label = "E-signature a notárske overenie" + +[portal.connections.categories.storage] +description = "Kam spracované dokumenty skončia – bucket, disk, ticket alebo stránka." +label = "Súbor a príloha" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling to ukladá šifrovane a už to nikdy nezobrazí." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Kroky používajúce toto pripojenie môžu volať iba cesty pod touto URL, nikdy iného hostiteľa." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Heslo" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Používateľské meno" + +[portal.connections.commonFields.webhookUrl] +helperText = "Vložte URL, ktorú vám služba poskytla. Volať možno iba cesty pod ňou." +label = "Webhook URL" + +[portal.connections.fields] +name = "Názov pripojenia" +namePlaceholder = "napr. Produkcia" + +[portal.connections.picker] +cancel = "Zrušiť" +createNew = "Nové pripojenie..." +placeholder = "Vyberte pripojenie" +save = "Uložiť pripojenie" + +[portal.connections.picker2] +back = "Zmeniť integráciu" +heading = "Čo chcete pripojiť?" +noResultsBody = "Zatiaľ to nepodporujeme – povedzte nám o tom a pozrieme sa na pridanie podpory." +noResultsTitle = "Nič nezodpovedá \"{{query}}\"" +searchPlaceholder = "Hľadať integrácie – skúste \"sign\", \"ocr\" alebo názov produktu" + +[portal.connections.types.api] +description = "Volajte ľubovoľné HTTP API z pravidla. Vyberiete URL, overenie a payload." +label = "Vlastné API" + +[portal.connections.types.api.fields.authType] +label = "Overenie" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Používateľské meno a heslo" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token vo vlastnej hlavičke" + +[portal.connections.types.api.fields.authType.options.none] +label = "Žiadne" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Prihlásiť sa pre token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Kroky používajúce toto pripojenie môžu volať iba cesty pod touto URL, nikdy iného hostiteľa." +label = "Základná URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Názov hlavičky" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Cesta prihlásenia" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Heslo" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Ak API odpovie odkazom na stiahnutie, načítať sa môžu iba títo hostitelia. Oddeľte čiarkami. Nechajte prázdne, ak chcete povoliť iba vlastného hostiteľa základnej URL." +label = "Povolení hostitelia výsledkov" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Odoslať token ako túto hlavičku" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Hlavička odpovede, v ktorej volanie prihlásenia vráti token, napr. X-Auth-Token." +label = "Hlavička odpovede s tokenom" + +[portal.connections.types.api.fields.username] +label = "Používateľské meno" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skenujte dokumenty na malvér na serveri, ktorý prevádzkujete – nič neopustí vašu sieť." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Skenovanie vírusov a obsahu dokumentov v pravidle." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokujte dokumenty obsahujúce makrá, spustiteľné súbory, skripty alebo vložené objekty – nielen známy malvér." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Priložte spracované dokumenty na stránku Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Odošlite dokumenty do ConsignO Cloud (Notarius) na elektronický podpis a notárske overenie." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Vaše prostredie ConsignO Cloud vrátane /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Heslo" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potrebné iba vtedy, ak vaša organizácia používa tenantov." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Používateľské meno" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Odošlite správu do Discord, keď pravidlo spracuje dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexujte auditnú udalosť v Elasticsearch vždy, keď pravidlo spracuje dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Kontrolujte FTP alebo FTPS server na nové dokumenty." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Režim pripojenia" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktívny" +passive = "Pasívny (odporúčané)" + +[portal.connections.types.ftp.fields.security] +helperText = "Obyčajné FTP odosiela prihlasovacie údaje a súbory nešifrovane; vyberte FTPS, ak ho server podporuje." +label = "Šifrovanie" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Žiadne (obyčajné FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Odošlite správu do priestoru Google Chat, keď sa spustí pravidlo." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Priložte spracované dokumenty k Jira issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Odošlite dokument alebo upozornenie e-mailom, keď sa spustí pravidlo." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Uložte spracované dokumenty do Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Heslo aplikácie" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Detegujte osobné údaje v texte dokumentu pomocou servera Presidio, ktorý prevádzkujete." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Čítajte a aplikujte štítky citlivosti Microsoft Purview na PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Voliteľné. Potrebné iba na výber štítkov podľa názvu; označovanie funguje aj bez toho." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Odtlačí sa do každého štítka ako jeho SiteId, aby nástroje kompatibilné s Purview vedeli, ktorá organizácia ho použila." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, z ktorého môžu zdroje čítať a do ktorého môžu výstupy pipeline zapisovať." +label = "S3 úložisko" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Nechajte prázdne pre AWS. Nastavte pre MinIO alebo iné S3-kompatibilné úložisko." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Región" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Odošlite dokument alebo upozornenie e-mailom, keď sa spustí pravidlo." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Kontrolujte SFTP (SSH) server na nové dokumenty." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Voliteľné. Odtlačok SHA-256 hostiteľského kľúča servera (z `ssh-keyscan host | ssh-keygen -lf -`). Ak je nastavený, akceptuje sa iba server predkladajúci tento kľúč; ak je prázdny, kľúč videný pri prvom pripojení sa pripne a neskoršie zmeny sa odmietnu." +label = "Odtlačok hostiteľského kľúča" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Passphrase kľúča" + +[portal.connections.types.sftp.fields.password] +helperText = "Nechajte prázdne, ak sa namiesto hesla overujete súkromným kľúčom." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Voliteľné. Vložte súkromný kľúč OpenSSH na overenie kľúčom namiesto hesla." +label = "Súkromný kľúč" + +[portal.connections.types.slack] +description = "Odošlite správu do Slack, keď pravidlo spracuje dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Sledujte sieťové zdieľanie SMB / CIFS (Windows alebo Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Sieťové zdieľanie" + +[portal.connections.types.smb.fields.domain] +helperText = "Voliteľné. Windows doména alebo pracovná skupina, ak ju váš server používa." +label = "Doména" + +[portal.connections.types.smb.fields.share] +label = "Zdieľanie" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Odošlite auditnú udalosť do Splunk vždy, keď pravidlo spracuje dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Odošlite auditnú udalosť do HTTP kolektora Sumo Logic." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Odošlite správu do kanála Teams, keď sa spustí pravidlo." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Odošlite každý dokument alebo iba podrobnosti na URL, ktorú si vyberiete." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Spustite Zap alebo Make scenár z pravidla." +label = "Zapier a Make" + +[portal.docs] +browse = "Prehliadať dokumentáciu" +viewSource = "Zobraziť zdroj na GitHub" + [portal.docs.authentication] codeCaption = "každá požiadavka" eyebrow = "ZAČÍNAME" @@ -6624,6 +7216,9 @@ eyebrow = "ZRUČNOSTI" lead = "Zabalené pomenované schopnosti, ktoré váš agent vyvoláva ako jeden nástroj. Každá zručnosť je deterministický reťazec operácií s pripojenými evals." title = "Zručnosti agentov" +[portal.docs.toc] +title = "Na tejto stránke" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCIA" @@ -6697,6 +7292,8 @@ type = "Typ" user = "Používateľ" [portal.documents.queue.empty] +connectSource = "Pripojiť zdroj" +createPipeline = "Vytvoriť pipeline" description = "Keď zdroje odošlú dokumenty do vašich pipeline, zobrazia sa tu na kontrolu." title = "Vo fronte nie sú žiadne dokumenty" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Tomuto filtru nezodpovedajú žiadne dokumenty." rowActions = "Akcie riadka" sensitiveLabel = "Citlivé" -sensitiveTitle = "Citlivé — vyžaduje sa prístup" [portal.documents.table.columns] action = "Pipeline / Akcia" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Stav" time = "Čas" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Naposledy videné" region = "Región" status = "Stav" +target = "Target" version = "Verzia" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "V tomto zobrazení nastala neočakávaná chyba. Skúste to znova retry = "Skúsiť znova" title = "Na tejto stránke sa niečo pokazilo" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Späť" +body = "Stiahnite si desktopovú aplikáciu alebo self-hostujte server." +done = "Hotovo" +guide = "Otvoriť príručku {{name}}" +openInBrowser = "Namiesto toho otvoriť v prehliadači" +sectionDesktop = "Desktopová aplikácia" +sectionSelfHosted = "Self-hosted" +title = "Nainštalovať Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Spustite oficiálny image na svojom serveri." +note = "Otvára sa na :8080. Predvolené prihlásenie: admin / stirling." +tagline = "Spustite server v kontajneri" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Nainštalujte oficiálny Helm chart." +note = "Pridajte PVC pre /configs, aby sa nastavenia zachovali." +tagline = "Nasaďte do klastra pomocou Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Natívne zostavenia pre vašu distribúciu." +guideBtn = "Zobraziť možnosti inštalácie pre Linux" +note = "Pre bežné distribúcie sú dostupné zostavenia AppImage, .deb a .rpm." +tagline = "Natívna desktopová aplikácia pre vašu distribúciu" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Alebo cez Homebrew" +detailBody = "Spustite inštalátor alebo použite Homebrew." +downloadBtn = "Stiahnuť pre macOS" +tagline = "Natívna aplikácia pre Apple silicon a Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Spustite .jar na fyzickom serveri." +downloadBtn = "Stiahnuť .jar" +note = "Vyžaduje Java 21+. Otvára sa na :8080; predvolené prihlásenie admin / stirling." +runLabel = "Potom ho spustite" +tagline = "Spustite .jar na fyzickom serveri" +title = "Manuálne nastavenie servera" + +[portal.home.download.windows] +altLabel = "Alebo cez winget" +detailBody = "Spustite inštalátor alebo použite správcu balíkov." +downloadBtn = "Stiahnuť inštalátor" +tagline = "Natívna aplikácia pre Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktívnych" -invite = "Pozvať členov tímu" +activeOfDeployed = "{{active}} z {{total}} aktívnych tento mesiac" name = "Stirling PDF Editor" open = "Otvoriť v prehliadači" updated = "aktualizované {{time}}" +[portal.home.editor.deploy] +finish = "Dokončiť nasadenie" +options = "Zobraziť možnosti nasadenia" +start = "Nasadiť Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dobré popoludnie" evening = "Dobrý večer" morning = "Dobré ráno" -[portal.home.onboarding] -dismiss = "Zavrieť nastavenie" -notStarted = "Nespustené" -progress = "Dokončené {{done}} z {{total}}" -title = "Dokončite nastavenie" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC pre celú organizáciu, dohodnuté objemové ceny a nasadenie v izolovanom prostredí." -cta = "Spustiť skúšobnú verziu" -ctaQuote = "Získať ponuku" -lead = "Pre 250+ zamestnancov." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Nasaďte desktopovú aplikáciu, navždy zadarmo." -title = "Stiahnuť PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktívnych · {{recommended}} odporúčaných" -chip = "{{active}} aktívnych" -title = "Zapnúť politiky" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} pripojených · každý PDF je riadený tam, kde sa objaví" -chip = "{{connected}} pripojených" -title = "Pripojte svoje zdroje" - -[portal.home.quickActions] -subtitle = "Najdôležitejšie úlohy na dnes" -title = "Rýchle akcie" - -[portal.home.quickActions.buildPipeline] -blurb = "3-krokový kompozér nad knižnicou typovaných operácií" -title = "Vytvoriť pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooky, sledované priečinky" -title = "Pripojiť zdroj" - -[portal.home.quickActions.issueApiKey] -blurb = "Kľúč s rozsahom, limitmi rýchlosti a IP allowlistom" -title = "Vydať API kľúč" - [portal.infrastructure] manageEditorDeployment = "Spravovať nasadenie Editoru" sectionsAriaLabel = "Sekcie infraštruktúry" @@ -6878,13 +7541,9 @@ heading = "API kľúče" subheading = "Prihlasovacie údaje s rozsahom, limitmi rýchlosti pre každý kľúč, oprávneniami a IP allowlistami." [portal.infrastructure.apiKeys.card] -allowedIps = "Povolené IP adresy" -anyIp = "Ľubovoľná IP adresa (bez allowlistu)" created = "Vytvorené" lastUsed = "Naposledy použité" -permissions = "Oprávnenia" -rateLimit = "Limit rýchlosti" -rateLimitValue = "{{value}} pož./min" +revoke = "Odvolať kľúč" usageMonth = "Použitie tento mesiac" usageToday = "Použitie dnes" @@ -6892,6 +7551,15 @@ usageToday = "Použitie dnes" description = "Vytvorte kľúč s rozsahom a začnite volať Stirling API." title = "Zatiaľ žiadne API kľúče" +[portal.infrastructure.apiKeys.error] +load = "Nepodarilo sa načítať vaše API kľúče. Skúste to znova." + +[portal.infrastructure.apiKeys.revoke] +body = "Odvolať „{{name}}“? Každá integrácia, ktorá tento kľúč stále používa, začne okamžite dostávať chyby 401. Túto akciu nie je možné vrátiť späť." +cancel = "Zrušiť" +confirm = "Odvolať kľúč" +title = "Odvolať API kľúč" + [portal.infrastructure.audit] filterAriaLabel = "Filtrovať auditné udalosti podľa kategórie" heading = "Auditné záznamy" @@ -6935,6 +7603,7 @@ all = "Všetko" auth = "Autentifikácia" config = "Konfigurácia" elevation = "Zvýšenie oprávnení" +policy = "Politika" processing = "Spracovanie" security = "Bezpečnosť" @@ -6944,19 +7613,30 @@ title = "Nemáte prístup" [portal.infrastructure.audit.metrics] config = "Konfigurácia" -elevation = "Zvýšenie oprávnení" +policy = "Politika" processing = "Spracovanie" totalEvents = "Celkový počet udalostí · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autentifikácia" +config = "Konfigurácia" +elevation = "Zvýšenie oprávnení" +policy = "Politika" +processing = "Spracovanie" +security = "Zabezpečenie" + +[portal.infrastructure.auditStatusLabel] +danger = "Chyba" +info = "Info" +success = "Úspech" +warning = "Upozornenie" + [portal.infrastructure.createKey] cancel = "Zrušiť" createKey = "Vytvoriť kľúč" done = "Hotovo" -ipAllowlistHelper = "Rozsahy CIDR oddelené čiarkami. Nechajte prázdne, ak chcete povoliť ľubovoľnú IP adresu." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Názov kľúča" keyNamePlaceholder = "napr. Produkcia · ingest" -permissionsLabel = "Oprávnenia" secretKeyCaption = "Tajný kľúč" secretWarning = "Uložte ho v správcovi tajomstiev. Stirling vždy ukladá iba hash — neskôr ho nebude možné obnoviť." subtitle = "Obmedzte rozsah kľúča na minimum, ktoré potrebuje. Kedykoľvek ho môžete rotovať alebo zrušiť." @@ -6964,207 +7644,9 @@ subtitleCreated = "Skopírujte tento tajný kľúč teraz — znova sa už nezob title = "Vytvoriť API kľúč" titleCreated = "Kľúč vytvorený" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Nasadil" -environment = "Prostredie" -product = "Produkt" -status = "Stav" -version = "Verzia" -when = "Kedy" - -[portal.infrastructure.deployments.recent] -heading = "Nedávne nasadenia" -subheading = "Najnovšie nasadenia naprieč produktmi a prostrediami." - -[portal.infrastructure.deployments.regionColumns] -instances = "Inštancie" -latency = "Latencia" -load = "Zaťaženie" -p99 = "P99" -region = "Región" -status = "Stav" -throughput = "Priepustnosť" -uptime = "Dostupnosť" -version = "Verzia" - -[portal.infrastructure.deployments.regions] -heading = "Regióny" -subheading = "Živý stav pre každý nasadený región Stirling — latencia, zaťaženie a verzia rollout." - -[portal.infrastructure.deployments.regions.empty] -description = "Nasadené regióny sa tu zobrazia po zriadení vášho pracovného priestoru." -title = "Žiadne nasadené regióny" - -[portal.infrastructure.models] -heading = "Modely" -msValue = "{{value}} ms" -subheading = "Katalóg modelov a smerovanie, ktoré poháňa spracovanie dokumentov vo vašom pracovnom priestore." - -[portal.infrastructure.models.byom] -description = "Zaregistrujte on-prem alebo self-hosted model a pripnite ho k regiónu na spracovanie viazané na rezidenciu dát." -title = "Prineste si vlastný model" - -[portal.infrastructure.models.catalogue] -heading = "Katalóg" -sub = "Spravované modely dostupné pre váš pracovný priestor, so živou latenciou a nákladmi." -subEnterprise = "Spravované, vlastné a on-prem modely — s dostupným pripnutím podľa regiónu." - -[portal.infrastructure.models.catalogue.empty] -description = "Modely v katalógu vášho pracovného priestoru sa zobrazia tu." -title = "Nie sú dostupné žiadne modely" - -[portal.infrastructure.models.columns] -cost = "Náklady" -latency = "Latencia" -load = "Zaťaženie" -model = "Model" -status = "Stav" -type = "Typ" -version = "Verzia" - -[portal.infrastructure.models.metrics] -activeModels = "Aktívne modely" -avgLatency = "Priem. latencia" -included = "Zahrnuté" -monthlySpend = "Mesačné náklady na modely" - -[portal.infrastructure.models.routing] -empty = "Nie sú nakonfigurované žiadne pravidlá smerovania." -heading = "Pravidlá smerovania" -sub = "Ktorý model obsluhuje jednotlivé operácie. Predvolené pravidlo sa použije, keď sa nezhoduje žiadne užšie pravidlo." -subLocked = "Smerujte operácie na konkrétne modely — dostupné v platených plánoch." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Inovujte na Pro a ovládajte, ktorý model obsluhuje jednotlivé operácie a typy dokumentov." -title = "Smerovanie modelov je platená funkcia" - -[portal.infrastructure.models.routingColumns] -default = "Predvolené" -docType = "Typ dokumentu" -modelForAria = "Model pre {{operation}}" -operation = "Operácia" -routedTo = "Smerované na" - -[portal.infrastructure.security.access.byok] -description = "Dodajte kľúč z vlastného KMS. Stirling ním šifruje, ale stále môže čítať." -label = "Prineste si vlastný kľúč (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Kľúče nikdy neopustia váš KMS. Stirling uchováva iba ciphertext." -label = "Držte si vlastný kľúč (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling spravuje šifrovacie kľúče. Najjednoduchšie — žiadne operácie s kľúčmi na vašej strane." -label = "Kľúče držané službou Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Politika prístupu k dokumentom" -subheading = "Riadi, kto môže dešifrovať spracované dokumenty v pokoji." - -[portal.infrastructure.security.attestations] -heading = "Potvrdenia zhody" -noReport = "Nie je dostupná žiadna správa" -subheading = "Auditný stav podľa jednotlivých rámcov, so správami dostupnými pre potvrdené kontroly." -viewReport = "Zobraziť správu →" - -[portal.infrastructure.security.compliance] -heading = "Zhoda" -subheading = "Potvrdenia a certifikácie pokrývajúce platformu Stirling." - -[portal.infrastructure.security.empty] -description = "Bezpečnostná konfigurácia vášho pracovného priestoru sa zobrazí tu." -title = "Bezpečnostný stav nie je dostupný" - -[portal.infrastructure.security.hyokBanner] -description = "S HYOK šifrovacie kľúče nikdy neopustia váš KMS. Stirling ukladá a spracúva iba ciphertext, ktorý môžete kedykoľvek odvolať." -title = "Stirling nemôže dešifrovať vaše dokumenty" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nie sú nakonfigurované žiadne rozsahy IP adries — všetky IP adresy sú povolené." -heading = "IP allowlist" -sub = "Prístup k API je obmedzený na tieto rozsahy CIDR." -subLocked = "Obmedzte prístup k API na známe rozsahy IP adries — dostupné v platených plánoch." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Inovujte na Pro a obmedzte prístup k API na konkrétne siete." -title = "IP allowlisting je platená funkcia" - -[portal.infrastructure.security.ipColumns] -added = "Pridané" -addedBy = "Pridal" -cidr = "CIDR" -label = "Štítok" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritmus" -heading = "Správa šifrovacích kľúčov" -keyId = "Identifikátor kľúča" -lastRotated = "Naposledy rotované" -rotateKey = "Rotovať kľúč" -rotationPolicy = "Politika rotácie" -subheading = "Správa kľúčov, ktoré šifrujú dokumenty v pokoji — kto môže dešifrovať a ako sa kľúče rotujú." - -[portal.infrastructure.security.managedBanner] -description = "Správa kľúčov bring-your-own-key (BYOK) a hold-your-own-key (HYOK) je dostupná v Enterprise. Inovujte a dodajte kľúče z vlastného KMS." -title = "Kľúče vo vašom pláne spravuje Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Ázia a Tichomorie" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · dátová hranica GDPR" -label = "Európska únia" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Spojené štáty" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezidencia dát" -subheading = "Kde sa dokumenty ukladajú a spracúvajú." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} použité" - -[portal.infrastructure.storage.empty] -description = "Pripojené úložisko a použitie sa zobrazia tu." -title = "Nie je nakonfigurované žiadne úložisko" - -[portal.infrastructure.storage.lifecycle] -active = "Aktívne" -activeRange = "0–{{value}} d" -archived = "Archivované" -coldStorage = "studené úložisko" -deleted = "Odstránené" -never = "nikdy" -purged = "vyčistené" - -[portal.infrastructure.storage.providers] -connect = "Pripojiť" -connected = "Pripojené" -heading = "Pripojení poskytovatelia" -subheading = "Kam sa zapisujú spracované artefakty." - -[portal.infrastructure.storage.retention] -heading = "Uchovávanie" -subheading = "Ako dlho sa artefakty uchovávajú pred odstránením v životnom cykle." -windowLabel = "Predvolené okno uchovávania" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} deň" -days_other = "{{count}} dní" -never = "Nikdy neodstraňovať" - -[portal.infrastructure.storage.totalUsage] -heading = "Celkové použitie" -progressLabel = "Použité úložisko" -subheading = "Úložisko spotrebované naprieč všetkými pripojenými poskytovateľmi." +[portal.infrastructure.keyLabel] +active = "Aktívny" +revoked = "Odvolaný" [portal.infrastructure.tabs] apiKeys = "API kľúče" @@ -7174,6 +7656,49 @@ models = "Modely" security = "Bezpečnosť" storage = "Úložisko" +[portal.integrations] +availableHeading = "Dostupné" +comingSoonHeading = "Čoskoro" +connect = "Pripojiť" +connectionCount_one = "{{count}} pripojenie" +connectionCount_other = "{{count}} pripojení" +customApi = "Vlastné API" +searchPlaceholder = "Hľadať integrácie" +subtitle = "Všetko, s čím Stirling dokáže komunikovať. Prihlasovacie údaje sa tu vytvoria raz a potom sa na ne odkazuje zo zdrojov, politík a pipeline." +title = "Integrácie" +typedName = "Názov {{tool}}" + +[portal.integrations.filters] +all = "Všetko" +audit = "Audit" +connected = "Pripojené" +notify = "Oznámenia" +security = "Zabezpečenie" +signing = "Podpisovanie" +storage = "Úložisko" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Pripojené" + +[portal.integrations.table] +integration = "Integrácia" +worksWith = "Funguje s" + +[portal.integrations.worksWith] +pipelines = "Pipeline" +policies = "Politiky" +sources = "Zdroje" + +[portal.legal] +draft = "{{label}} · návrh" +loadError = "Tento dokument sa nepodarilo načítať. Skúste to znova." +loading = "Načítava sa…" +title = "Právny dokument" + [portal.nav] agent-builder = "Agent Builder" components = "Komponenty" @@ -7182,33 +7707,17 @@ documents = "Dokumenty" editor = "Editor" home = "Domov" infrastructure = "Infraštruktúra" +integrations = "Integrácie" pipelines = "Pipeline" policies = "Politiky" procurement = "Obstarávanie" -settings = "Nastavenia" sources = "Zdroje" usage = "Používanie a fakturácia" users = "Používatelia" -[portal.notifications] -markAllRead = "Označiť všetko ako prečítané" -title = "Oznámenia" -viewAll = "Zobraziť všetko" - -[portal.notifications.ariaLabel] -none = "Oznámenia, žiadne neprečítané" -unread_one = "Oznámenia, {{count}} neprečítané" -unread_other = "Oznámenia, {{count}} neprečítaných" - -[portal.notifications.count] -allRead = "všetko prečítané" -loading = "načítava sa" -new_one = "{{count}} nové" -new_other = "{{count}} nových" - -[portal.notifications.empty] -description = "Žiadne nové oznámenia." -title = "Všetko máte vybavené" +[portal.nav.section] +platform = "PDF platforma" +processor = "PDF Processor" [portal.pipelines] subtitle = "Každý automatizovaný dokumentový pipeline na backende: usporiadaný reťazec operácií nad množinou zdrojov, spustený triggerom. Kliknite na riadok pre jeho kroky a zdroje." @@ -7218,54 +7727,87 @@ title = "Pipeline" newPipeline = "Nový pipeline" [portal.pipelines.builder] -addStep = "Pridať nástroj" +activate = "Activate" back = "Späť na pipeline" +cannotFollow = "Nie je možné prevziať {{produced}}" +chooseAccount = "Vyberte účet" +chooseDestination = "Vyberte cieľ" +chooseOperation = "Vyberte, čo tento krok vykoná" +chooseSource = "Vyberte zdroj" discard = "Zahodiť zmeny" -enabled = "Povolené" +inputs = "Vstup" +inputSource = "Vstupný zdroj" +inputTrigger = "Spúšťač" keepEditing = "Pokračovať v úpravách" -needsUpload = "Vyžaduje nahraný súbor" +moreActions = "More actions" +needsConfiguring = "Vyžaduje nastavenie" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Vašemu vyhľadávaniu nezodpovedajú žiadne nástroje." -pipelineSettings = "Nastavenia pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Hľadať nástroje" -selectToolBody = "Pridajte nástroj na vytvorenie pipeline." -selectToolTitle = "Zatiaľ žiadne nástroje" -toolSettings = "Nastavenia nástroja" +sendToSystem = "Odoslať do iného systému" +stepsIncompatible = "Tieto kroky nemožno spustiť na tom, čo vytvára ich predchádzajúci krok: {{tools}}." +stepsNeedSetup = "Tieto kroky je pred uložením ešte potrebné nastaviť: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Nerozpoznaná operácia, ponechaná bez zmeny." unsavedBody = "Máte neuložené zmeny. Uložte ich pred odchodom alebo ich zahodiť?" unsavedTitle = "Neuložené zmeny" -uploadUnsupported = "Nahrané súbory ešte nie sú v pipeline podporované, takže tieto kroky sa nedajú uložiť: {{tools}}." usesDefaults = "Spúšťa sa s predvolenými nastaveniami" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinuje každý súbor z predchádzajúceho kroku" +fan-out = "Spustí sa raz pre každý súbor z predchádzajúceho kroku" +format-mismatch = "Vyžaduje {{accepts}}, ale predchádzajúci krok vytvára {{produced}}" +output-uncertain = "Nemusí sa spustiť: výstup predchádzajúceho kroku závisí od jeho nastavenia" +source-mismatch = "Vyžaduje {{accepts}}, ale vstup tejto pipeline je {{produced}}" +undeclared-operation = "Nie je možné skontrolovať, čo tento krok prijíma" [portal.pipelines.composer] addTool = "Pridať nástroj" -cancel = "Zrušiť" -chainEmpty = "Pridajte nástroj a začnite vytvárať pipeline." create = "Vytvoriť pipeline" -directory = "Výstupný priečinok" -directoryHelp = "Absolútna cesta na serveri. Musí byť v rámci nakonfigurovaných povolených priečinkov." +createPaused = "Create paused" editingUnsupported = "Zobrazenie týchto parametrov nástroja na úpravu zatiaľ nie je podporované." -moveDown = "Presunúť nadol" -moveUp = "Presunúť nahor" +editSource = "Edit source" name = "Názov" namePlaceholder = "napr. Prechod redakciou" -noSources = "Zatiaľ nie sú pripojené žiadne zdroje. Pipeline sa stále môže spustiť na súboroch dodaných priamo doň." noToolSettings = "Tento nástroj nemá žiadne konfigurovateľné nastavenia." -operations_one = "Operácia ({{count}})" -operations_other = "Operácie ({{count}})" output = "Výstup" -removeStep = "Odstrániť operáciu" save = "Uložiť zmeny" scheduleEvery = "Spúšťať každých" -sources = "Zdroje" -sourcesLoading = "Načítavajú sa zdroje..." trigger = "Trigger" triggerManual = "Iba manuálne" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dní" hours = "hodín" minutes = "minút" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Odstrániť \"{{name}}\"? Túto akciu nie je možné vrátiť späť." cancel = "Zrušiť" @@ -7273,28 +7815,62 @@ confirm = "Odstrániť" title = "Odstrániť pipeline?" [portal.pipelines.detail] +clearHistory = "Vymazať históriu" delete = "Odstrániť pipeline" run = "Spustiť teraz" [portal.pipelines.empty] action = "Vytvoriť pipeline" +connectSource = "Pripojiť zdroj" description = "Vytvorte svoj prvý pipeline: vyberte zdroje, nad ktorými beží, spojte operácie do reťazca a vyberte, kam pôjde výstup." title = "Zatiaľ žiadne pipeline" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktívne" paused = "Pozastavené" total = "Pipeline" -[portal.pipelines.output] -folder = "Zapísať do priečinka" -inline = "Vrátiť súbory" - [portal.pipelines.run] +allProcessed_one = "Nie je čo spustiť: {{count}} dokument zdroja už bol spracovaný." +allProcessed_other = "Nie je čo spustiť: všetkých {{count}} dokumentov v zdrojoch už bolo spracovaných." completed_one = "Spustenie dokončené." completed_other = "Všetkých {{count}} spustení dokončených." empty = "Nie je čo spustiť: zdroje nemali žiadne dokumenty na spracovanie." failed = "Spustenie zlyhalo: {{error}}" +inFlight = "Nie je nič nové na spustenie: dokumenty sa stále spracúvajú z predchádzajúceho spustenia." +parked_one = "Nie je čo spustiť: {{count}} dokument predtým zlyhal a je pozastavený. Opravte príčinu a potom vymažte históriu, aby ste ho skúsili znova." +parked_other = "Nie je čo spustiť: {{count}} dokumentov predtým zlyhalo a sú pozastavené. Opravte príčinu a potom vymažte históriu, aby ste ich skúsili znova." running = "Spustenie začalo; stále prebieha." timeout = "Spustenie trvá dlhšie, než sa očakávalo; môže sa stále dokončiť na pozadí." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Zdroje" status = "Stav" steps = "Kroky" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Sledovanie priečinka" @@ -7314,12 +7891,18 @@ manual = "Manuálne" schedule = "Naplánované" [portal.policies] +defaultName = "Politika {{category}}" subtitle = "Trvalé automatizácie, ktoré vynucujú pipeline nástrojov na každom dokumente. Každá politika sa spustí pri nahraní alebo exporte, vykoná svoj reťazec nástrojov a uloží vynútenú verziu vedľa originálu." title = "Politiky" [portal.policies.card] comingSoon = "Inovovať na Enterprise" notSetUp = "Nenastavené" +requiresAiEngine = "Vyžaduje AI engine" + +[portal.policies.categories.classification] +desc = "Identifikuje typ každého dokumentu pri nahraní a označí jeho metadáta na zaradenie a vyhľadávanie." +label = "Klasifikácia" [portal.policies.categories.compliance] desc = "Vynucujte požiadavky HIPAA, GDPR, SOC 2 alebo FedRAMP na každom dokumente." @@ -7344,6 +7927,13 @@ label = "Bezpečnosť" [portal.policies.config] scopeAll = "Všetky dokumenty" +[portal.policies.config.classification] +summary = "Klasifikuje každý nahraný dokument podľa štítkov vášho tímu a označí jeho metadáta." + +[portal.policies.config.classification.rules] +0 = "Klasifikovať" +1 = "Označiť metadáta" + [portal.policies.config.compliance] summary = "Overuje dokumenty podľa regulačných rámcov predtým, než opustia systém." @@ -7371,6 +7961,29 @@ minConfidence = "Min. spoľahlivosť" 2 = "Pomenovať" 3 = "Normalizovať" +[portal.policies.config.purview] +label = "Použiť štítok citlivosti Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "ID štítka" +labelIdHelp = "GUID štítka z portálu Purview. Použitie štítka zapíše jeho metadáta do PDF; nedokáže ho zašifrovať, na to je potrebný klient Microsoft." +labelName = "Názov štítka (voliteľné)" +labelNameHelp = "Zaznamenané spolu s ID, aby ľudia a nástroje vedeli štítok rozpoznať." + +[portal.policies.config.purview.fields.method] +help = "Purview rozlišuje štítok použitý politikou od štítka, ktorý vybrala osoba." +label = "Ako sa štítok zaznamená" +privileged = "Ako vybrané osobou" +standard = "Ako automaticky použité" + +[portal.policies.config.purviewRead] +label = "Prečítať štítok Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Prečíta štítok, ktorý dokument už nesie, aby naň mohli neskoršie kroky reagovať." + [portal.policies.config.retention] summary = "Vynucuje, ako dlho sa dokumenty uchovávajú, kedy sa archivujú a kedy sa odstraňujú." @@ -7438,6 +8051,7 @@ title = "Zatiaľ žiadna aktivita" [portal.policies.endpoints] addWatermark = "Vodoznak" autoRedact = "Redigovať PII" +classifyAndLabel = "Klasifikovať" compressPdf = "Komprimovať" flatten = "Zlúčiť vrstvy" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vaše politiky sú uložené a zobrazia sa po obnovení pripojeni retry = "Skúsiť znova" title = "Backend nie je dostupný" +[portal.policies.operations] +change = "Zmeniť, čo tento krok vykoná" +noResults = "Žiadny krok tomu nezodpovedá. Skúste názov produktu alebo „scan“, „notify“, „attach“." +searchPlaceholder = "Hľadať kroky – skúste „scan“, „attach“ alebo názov produktu" + +[portal.policies.operations.bodyMode] +binary = "Surové bajty súboru" +json = "Vnútri JSON payload" +multipart = "Ako nahratie súboru (multipart)" + +[portal.policies.operations.clamavScan] +description = "Váš vlastný server ClamAV skontroluje dokument; nič neopustí vašu sieť." +label = "Skenovať na vírusy (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Odmietne dokumenty obsahujúce makrá, spustiteľné súbory, skripty alebo vložené objekty." +label = "Blokovať aktívny obsah" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive skontroluje dokument a spustenie sa zastaví, ak nie je čistý." +label = "Skenovať na vírusy" + +[portal.policies.operations.confluenceAttach] +description = "Uloží spracovaný dokument na stránku." +label = "Pripojiť k stránke Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Odošle dokument do ConsignO Cloud na podpis." +label = "Odoslať na podpis (ConsignO)" +note = "Odošle dokument. Získanie podpísanej kópie zatiaľ nie je podporované, pretože krok nemôže odovzdať výsledok neskoršiemu kroku." + +[portal.policies.operations.customApiCall] +description = "Vytvorte volanie sami: cesta, metóda, telo a spracovanie odpovede." +label = "Zavolať vlastné API" + +[portal.policies.operations.discordNotify] +description = "Oznámi kanálu, že politika spracovala dokument." +label = "Uverejniť správu na Discord" + +[portal.policies.operations.elasticIndex] +description = "Zaznamená, čo politika vykonala, ako vyhľadateľný dokument." +label = "Indexovať auditnú udalosť v Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart sa hodí pre upload API; JSON sa hodí pre API, ktoré chcú súbor v payload; binary odošle surové bajty." +label = "Ako odoslať dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON telo odoslané bez úprav. {{document.base64}} obsahuje súbor; {{document.*}} a {{run.*}} sa vyplnia pre každý dokument." +label = "JSON telo" + +[portal.policies.operations.fields.connection] +label = "Účet" + +[portal.policies.operations.fields.domain] +label = "Mailgun doména" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Pole formulára, pod ktorým API očakáva dokument." +label = "Názov poľa súboru" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON objekt dvojíc názov/hodnota hlavičky. Prihlasovací údaj pripojenia sa pridá za vás." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Kľúč issue" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Odoslané ako telo správy. {{document.*}} a {{run.*}} sa vyplnia pre každý dokument." +label = "Správa" + +[portal.policies.operations.fields.method] +label = "Metóda" + +[portal.policies.operations.fields.pageId] +label = "ID stránky" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Pripojí sa k základnej URL pripojenia. Nikdy nemôže dosiahnuť iného hostiteľa." +label = "Cesta" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Kam sa má v úložisku zapísať. {{document.filename}} sa vyplní pre každý dokument." +label = "Cieľová cesta" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report ponechá dokument bez zmeny. Replace ho nahradí tým, čo sa vráti." +label = "Čo urobiť s odpoveďou" + +[portal.policies.operations.fields.signerEmail] +label = "Email podpisujúceho" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Predmet" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyzuje tento text. Použite {{document.title}} alebo vložte text, ktorý chcete skontrolovať." +label = "Text na kontrolu" + +[portal.policies.operations.fields.to] +label = "Komu" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Používateľské meno Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Oznámi priestoru, že politika spracovala dokument." +label = "Uverejniť správu na Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Uloží spracovaný dokument k issue." +label = "Pripojiť k Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "Odošle spracovaný dokument ako prílohu." +label = "Odoslať dokument emailom (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Zapíše spracovaný dokument do priečinka." +label = "Nahrať do Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Váš vlastný server Presidio nahlási osobné údaje, ktoré nájde." +label = "Zistiť osobné údaje" +note = "Presidio číta text, nie súbory. Poskytnite mu text, ktorý chcete skontrolovať — samotný dokument sa nenahráva." + +[portal.policies.operations.responseMode] +replace = "Nahradiť dokument odpoveďou" +report = "Ponechať dokument, zaznamenať odpoveď" + +[portal.policies.operations.sendgridEmail] +description = "Odošle spracovaný dokument ako prílohu." +label = "Odoslať dokument emailom (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Oznámi kanálu, že politika spracovala dokument." +label = "Uverejniť správu na Slack" + +[portal.policies.operations.splunkEvent] +description = "Zaznamená, čo politika vykonala, tam, kde to už sledujú vaši audítori." +label = "Odoslať auditnú udalosť do Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Zaznamená, čo politika vykonala, do HTTP kolektora." +label = "Odoslať auditnú udalosť do Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Oznámi kanálu, že politika spracovala dokument." +label = "Uverejniť správu na Teams" + +[portal.policies.operations.webhookPost] +description = "Odošle samotný dokument na URL, ktorú vyberiete." +label = "Odoslať dokument na webhook" + +[portal.policies.operations.zapierNotify] +description = "Odovzdá podrobnosti spustenia vašej automatizácii." +label = "Spustiť Zap alebo Make scenár" + [portal.policies.stats] activeFor = "Aktívne" dataProcessed = "Spracované údaje" @@ -7473,6 +8282,13 @@ label = "Vynútené dokumenty" description = "Nakonfigurované, ale nespúšťa sa" label = "Pozastavené" +[portal.policies.table] +appliesTo = "Vzťahuje sa na" +docs = "Vynútené dokumenty" +enforces = "Vynucuje" +policy = "Politika" +status = "Stav" + [portal.policies.wizard.actions] back = "Späť" cancel = "Zrušiť" @@ -7480,6 +8296,10 @@ continue = "Pokračovať" enablePolicy = "Povoliť politiku" saveChanges = "Uložiť zmeny" +[portal.policies.wizard.capability.classify] +desc = "Identifikuje typ dokumentu podľa štítkov vášho tímu a označí ho, aby sa zaraďoval a vyhľadával podľa kategórie." +label = "Klasifikovať dokument" + [portal.policies.wizard.capability.compress] desc = "Komprimuje dokument na menšiu veľkosť súboru." label = "Zmenšiť veľkosť súboru" @@ -7500,10 +8320,18 @@ label = "Redigovať citlivé informácie" desc = "Odstráni skrytý JavaScript, aby sa pri otvorení dokumentu nič nespustilo automaticky." label = "Odstrániť aktívny obsah" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dokazuje, že dokument existoval presne v tejto podobe v konkrétnom čase, pomocou nezávislej autority časových pečiatok. Odosiela sa iba hash - dokument nikdy neopustí váš server." +label = "Pridať dôveryhodnú časovú pečiatku" + [portal.policies.wizard.capability.watermark] desc = "Označí každú stránku viditeľnou značkou (napr. „Dôverné“)." label = "Použiť vodoznak" +[portal.policies.wizard.classification] +description = "Každý nahraný dokument sa klasifikuje podľa vstavaných štítkov a označí typmi, ktoré zodpovedajú. Sada štítkov je zdieľaná naprieč celým vaším tímom." +labelsHeading = "Klasifikačné štítky" + [portal.policies.wizard.errors] noTools = "Najprv vo workflow povoľte aspoň jeden nástroj." saveFailed = "Politiku sa nepodarilo uložiť. Skúste to znova." @@ -7548,73 +8376,118 @@ setUp = "Nastaviť politiku {{category}}" [portal.policies.wizard.workflow] description = "Vyberte, čo táto politika urobí s každým dokumentom, ktorý spracuje." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktívnych" -noRule = "Zatiaľ sa nevynucuje žiadne pravidlo" -subtitle = "Trvalé automatizácie, ktorými prechádza každý dokument bez ohľadu na to, ktorá pipeline ho spracúva." -title = "Čo sa spúšťa na vašich PDF" - [portal.policySummary.action] -comingSoon = "Upgradovať na Enterprise" -configure = "Konfigurovať" setUp = "Nastaviť" -[portal.policySummary.column] -activeRule = "Aktívne pravidlo" -policy = "Politika" -status = "Stav" +[portal.processorFlow] +footnote = "Počty sú za posledných 24 hodín. Rýchlosť toku je ilustračná." +liveBadge = "Naživo" +stats = "{{connected}} pripojených · {{processed}} PDF spracovaných" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Po nakonfigurovaní politík sa tu zobrazia kategórie." -title = "Zatiaľ žiadne politiky" +[portal.processorFlow.lens] +ariaLabel = "Režim zobrazenia" +flow = "Tok" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktívne" -off = "Vypnuté" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Zlyhalo" +heading = "Výsledky" +success = "Doručené" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktívnych" +count = "{{n}} · 24h" +heading = "Politiky" +setUp = "Nastaviť" soon = "Čoskoro" -[portal.processingStatus] -managePlan = "Spravovať plán" -volumeSuffix = "Spracované PDF · posledných 30 dní" +[portal.processorFlow.sankey] +waist = "Politiky · {{n}} aktívnych" + +[portal.processorFlow.sankey.empty] +description = "Pripojte zdroj a zapnite politiku, aby ste videli, ako dokumenty prechádzajú processorom." +title = "Zatiaľ žiadny tok" + +[portal.processorFlow.sources] +comingSoonTag = "Pripojiť" +editor = "Stirling PDF Editor" +heading = "Zdroje" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Príjem emailov" [portal.procurement] -reset = "Resetovať obstarávanie (demo)" subtitle = "Nechajte svoj tím vyhodnotiť, zazmluvniť a zaučiť. Každý dokument na jednom mieste." title = "Obstarávanie" -[portal.procurement.action] -download = "Stiahnuť" -pay = "Zaplatiť teraz" -request = "Vyžiadať" -sign = "Skontrolovať a podpísať" -upload = "Nahrať" - [portal.procurement.agreement] agreeCta = "Súhlasiť a predplatiť" +confidential = "Dôverné" confirm = "Prečítal/-a som si Stirling Enterprise Agreement a súhlasím s ňou." -eyebrow = "Zmluva" -intro = "Jedna kombinovaná zmluva pokrýva vašu dohodu: Master Service Agreement, Order Form, EULA a Data Processing Agreement. Skontrolujte ju a potom súhlasom prijmite ponuku ako záväzné predplatné." -title = "Skontrolujte svoju enterprise zmluvu" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA a DPA, spojené do jedného podpisu." +download = "Stiahnuť" +downloadDraftError = "Nepodarilo sa vygenerovať agreement PDF - renderer dokumentov nie je dostupný. Skúste to znova o chvíľu alebo kontaktujte podporu." +downloadError = "Nepodarilo sa stiahnuť podpísanú zmluvu. Skúste to znova." +legalName = "Názov právnickej osoby" +legalNamePlaceholder = "Právnická osoba, ktorá bude podpisovať" +loadError = "Nepodarilo sa načítať zmluvu. Skúste to znova." +loading = "Načítava sa zmluva..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Požiadať o zmeny" +scrollHint = "Posuňte na kontrolu" +signatory = "Meno podpisujúceho" +signatoryPlaceholder = "Celé meno" +signatoryTitle = "Funkcia podpisujúceho" +signatoryTitlePlaceholder = "napr. hlavný právnik" +signError = "Nepodarilo sa zaznamenať váš podpis. Skúste to znova." [portal.procurement.builder] addons = "Doplnky" +addressLine1 = "Riadok adresy 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Riadok adresy 2" +addressLine2Placeholder = "Apartmán, poschodie, budova" back = "Späť" businessName = "Názov firmy" businessNamePlaceholder = "Vaša spoločnosť" +city = "Mesto" +cityPlaceholder = "San Francisco" +completeRequired = "Pred vygenerovaním cenovej ponuky vyplňte povinné polia (označené *) platným emailom." +contactEmail = "Kontaktný email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktné meno" +contactNamePlaceholder = "Jane Doe" continue = "Pokračovať" -country = "Krajina" -countryEuro = "Eurozóna (EUR €)" -countryUK = "Spojené kráľovstvo (GBP £)" -countryUS = "Spojené štáty (USD $)" +done = "Hotovo" eula = "Prečítal/-a som si Stirling Enterprise EULA a súhlasím s ňou. Riadi zmluvu vygenerovanú z tejto ponuky." generate = "Vygenerovať ponuku" -included = "Zahrnuté" indemnification = "Odškodnenie za IP" indemnificationSub = "Obhajujeme oprávnené nároky týkajúce sa IP podľa EULA" -offlineLicense = "Offline / izolovaná licencia" -offlineLicenseSub = "Stiahnuteľný licenčný súbor pre izolovanú self-hosted inštanciu" +paperEyebrow = "Enterprise cenová ponuka" +paperFor = "Pripravené pre" +pdfSize = "Veľkosť PDF" +poNumber = "Číslo PO" +poNumberPlaceholder = "Voliteľné" +postalCode = "PSČ" +postalCodePlaceholder = "94105" +posture = "Riadenie" +posture_count = "~{{count}} politík" +posture_essentials = "Základy" +posture_essentialsSub = "Klasifikácia + Zdieľanie — predvolené hodnoty" +posture_governed = "Riadené" +posture_governedSub = "Pridáva Zabezpečenie a Smerovanie" +posture_regulated = "Regulované" +posture_regulatedSub = "Každá kategória — Compliance, Uchovávanie, Ingestion" qbr = "Štvrťročné obchodné hodnotenia" qbrSub = "Váš SE každý štvrťrok prehodnotí používanie a plán rozvoja" +region = "Štát / región" +regionPlaceholder = "California" running = "{{annual}} / rok · {{years}}-ročné {{tcv}}" s1Sub = "Váš tím a PDF, ktoré očakávate spracovať každý rok." s1Title = "Koľko budete spracúvať?" @@ -7623,6 +8496,12 @@ s2Title = "Záväzok a služba" s3Sub = "Pre ponuku a zmluvu, ktorú vygeneruje." s3Title = "Vaše údaje" serviceLevel = "Úroveň služby" +size_compact = "Kompaktné" +size_compactSub = "Väčšinou text, menej ako 1 MB" +size_heavy = "Veľké" +size_heavySub = "Skenované alebo s množstvom obrázkov, 5 MB+" +size_standard = "Štandardné" +size_standardSub = "Kombinácia textu a obrázkov, 1 až 5 MB" slDedicated = "Vyhradená" slDedicatedSub = "4 pracovné hodiny · vyhradený account manager · +30 %" slPriority = "Prioritná" @@ -7630,6 +8509,8 @@ slPrioritySub = "Ten istý pracovný deň · pridelený CSM · +15 %" slStandard = "Štandardná" slStandardSub = "Nasledujúci pracovný deň · zdieľaný CSM · zahrnuté" stepOf = "Krok {{n}} z {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Voliteľné" term = "Obdobie" termDiscount = "Uplatnená {{pct}}% zľava za viacročný záväzok" title = "Vytvorte svoju ponuku" @@ -7637,6 +8518,7 @@ training = "Onboarding a školenie" trainingSub = "Živé relácie, aby váš tím začal pracovať" users = "Celkový počet používateľov" usersPlaceholder = "napr. 250" +viewEula = "Prečítať" volEstimated = "Odhadnuté z {{count}} používateľov (~2 000 PDF na každého vrátane automatizácie). Upravte, ak máte presnejší údaj." volManual = "Používa sa váš údaj. Kedykoľvek môžete znova odhadnúť podľa veľkosti tímu." volNoUsers = "Nie ste si istí? Zadajte veľkosť tímu a my to odhadneme." @@ -7645,55 +8527,55 @@ volumePlaceholder = "napr. 1 000 000" years_one = "{{count}} rok" years_other = "{{count}} roky" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumenty" -done = "Hotovo" -here = "Ste tu" -hide = "Skryť" -optional = "Voliteľné" -paidAddon = "Platený doplnok" -show = "Zobraziť" -subtitle = "Všetko, čo potrebujete v každom kroku cesty, zobrazené podľa toho, ako sa dohoda posúva ďalej." -supportingSubtitle = "SOC 2, bezpečnostné kontroly, daňové formuláre a ďalšie, pripravené, keď si ich vyžiada váš bezpečnostný alebo obstarávací tím. Niektoré majú jednorazový poplatok." -supportingTitle = "Podpora vášho hodnotenia" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form a DPA v jednom podpise." +download = "Stiahnuť" +eula = "EULA & Commercial Terms" +eulaSub = "Softvér a samoobslužné obchodné podmienky." +invoice = "Faktúra" +invoiceSub = "Vaša prvá faktúra za predplatné." +laterInvoice = "Dostupné po prijatí" +laterQuote = "Dostupné po vystavení vašej cenovej ponuky" +quote = "Cenová ponuka" +quoteSub = "Vaša položková enterprise cenová ponuka." +sla = "SLA Exhibit" +slaSub = "Úrovne podpory a ciele odozvy." +subprocessors = "Subprocesori" +subprocessorsSub = "Tretie strany, ktoré spracúvajú údaje vo vašom mene." +subtitle = "Vaše obchodné dokumenty, dostupné kedykoľvek." title = "Dokumenty" -upcoming = "Nadchádzajúce" +view = "Zobraziť" [portal.procurement.error] title = "Niečo sa pokazilo" [portal.procurement.hero] -company = "Vaša enterprise dohoda" +barAria = "Fáza {{current}} z {{total}}" ctaAgreement = "Skontrolovať a podpísať zmluvu" -ctaLive = "Ste live" -ctaPayment = "Pridať platbu" +ctaExploring = "Nastaviť skúšobnú verziu" ctaQuote = "Skontrolovať ponuku" +ctaReviewQuote = "Skontrolovať cenovú ponuku" ctaTrial = "Vytvoriť ponuku" +documents = "Dokumenty" eyebrow = "Enterprise obstarávanie" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Pozvať spoluhráčov" -keyDocs = "Kľúčové dokumenty" -nextStep = "Ďalší krok: {{action}}" -notStarted = "Nezačaté" +licenseKey = "Licenčný kľúč" +liveSub = "Vaša organizácia je zriadená a vaša licencia je aktívna." +liveTitle = "Ste naživo na Stirling Enterprise" +next = "Ďalej: {{stage}}" open = "Otvoriť obstarávanie" scheduleCall = "Naplánovať hovor" -setup1Sub = "Pozvite svojich spoluhráčov" -setup1Title = "Nasadiť PDF Editor" -setup2Sub = "Zapnite spracovanie naprieč editormi a ďalšími zdrojmi" -setup2Title = "Pripojiť PDF Processor" -setup3Sub = "Zapnite Security, Compliance, Routing alebo Retention, keď ich budete potrebovať" -setup3Title = "Pridať odporúčané politiky" +sentenceAgreement = "skontrolujte a podpíšte zmluvu" +sentenceLive = "zriadenie a nasadenie" +sentencePayment = "objednávka a platba" +sentenceQuote = "skontrolujte a prijmite svoju cenovú ponuku" +sentenceTrial = "nastavte si skúšobný pracovný priestor" [portal.procurement.journey] daysLeft_one = "Zostáva {{count}} deň" daysLeft_other = "Zostávajú {{count}} dni" -engineerLabel = "Váš solutions engineer" -eyebrow = "Vaše zavedenie" -live = "Ste live na Stirling Enterprise" -nextStep = "Ďalší krok: {{action}}" -subtitle = "Váš solutions engineer je pri každom kroku. Naraz jedna ďalšia akcia; celý kontrolný zoznam je nižšie." -title = "Od skúšobnej verzie po live, jedna vedená cesta" -trialTitle = "Enterprise skúšobná verzia" [portal.procurement.journeySteps.agreement] blurb = "Jeden podpis pokrýva MSA, objednávkový formulár, EULA a DPA." @@ -7727,6 +8609,9 @@ downloadError = "Offline licenčný súbor zatiaľ nebolo možné vygenerovať downloadOffline = "Stiahnuť offline licenciu (.lic)" hint = "Vložte tento kľúč do self-hosted inštancie na jej aktiváciu alebo si ho ponechajte pre svoje záznamy. Uchovajte ho bezpečne." label = "Váš licenčný kľúč" +subtitle = "Aktivujte self-hosted inštanciu týmto kľúčom alebo si ho ponechajte pre svoje záznamy." +title = "Váš licenčný kľúč" +trialFileHint = "Toto je váš súbor skúšobnej licencie. Keď bude vaša zmluva uzavretá, vráťte sa a stiahnite si ho znova — na rozdiel od online licenčného kľúča sa súbor .lic neaktualizuje sám." [portal.procurement.link] cta = "Prepojiť účet" @@ -7739,65 +8624,62 @@ description = "Vaše predplatné je aktívne a licencia je vydaná. Váš tím j eyebrow = "Live" title = "Ste live na Stirling Enterprise" -[portal.procurement.locked] -description = "Skúšobné kľúče, ponuky na záväzný objem, zmluva na jeden podpis, platba a register dokumentov budú po spustení enterprise hodnotenia všetky tu." -eyebrow = "Iba Enterprise" -talkToSales = "Kontaktovať predaj" -title = "Obstarávacia cesta sa otvára s Enterprise" - [portal.procurement.milestone] -accept = "Prijať a pokračovať" -description = "Stiahnite si PDF a zdieľajte ho so svojím tímom, vráťte sa a prijmite ho, keď budete pripravení, alebo vykonajte zmeny." -download = "Stiahnuť PDF" downloadError = "PDF ponuky sa zatiaľ nepodarilo stiahnuť — skúste to znova o chvíľu." -edit = "Upraviť ponuku" -eyebrow = "Ponuka {{number}}" -perYear = " / rok" -preparedFor = "Pripravené pre {{company}}" -tcv = "{{value}} celková hodnota zmluvy" -title = "Vaša ponuka je pripravená" - -[portal.procurement.modal] -cancel = "Zrušiť" -chooseFile = "Vybrať súbor" -close = "Zavrieť" -downloadBody = "Sťahovanie sa čoskoro začne." -downloadCta = "Stiahnuť" -downloadTitle = "Stiahnuť" -noFile = "Nie je vybraný žiadny súbor" -payBody = "Zaplaťte záväznú zmluvu kartou alebo bankovým prevodom cez Stripe. Váš workspace sa zriadi hneď po pripísaní platby." -payCta = "Pokračovať na Stripe" -payTitle = "Potvrdiť platbu" -requestBodyFree = "Vygenerujeme to na požiadanie. Potvrďte a váš solutions engineer vám to čoskoro pošle." -requestBodyPaid = "Toto je platený doplnok. Potvrďte a váš solutions engineer ho nacení a pošle dokumenty." -requestCta = "Vyžiadať" -requestTitle = "Vyžiadať tento dokument" -signBody = "Otvorí Stirling Enterprise Agreement na elektronický podpis: jeden podpis pokrýva MSA, objednávkový formulár, EULA a DPA. Automaticky spolupodpíšeme a posuniete sa k platbe." -signCta = "Otvoriť na podpis" -signTitle = "Skontrolujte a podpíšte svoju zmluvu" -uploadBody = "Pošlite nám svoju objednávku a my ju vyfakturujeme podľa vašich podmienok. Presuňte PDF sem alebo vyberte súbor nižšie." -uploadCta = "Nahrať objednávku" -uploadTitle = "Nahrať svoju objednávku" [portal.procurement.payment] description = "Vaša ponuka je prijatá a licencia je už aktívna — váš tím môže začať hneď. Zaplaťte prvú faktúru, keď budete pripravení; môžete ju zaplatiť alebo stiahnuť tu, bez potreby emailu." +downloadAgreement = "Stiahnuť podpísanú zmluvu" downloadInvoice = "Stiahnuť faktúru" -simulate = "Simulovať prijatie platby (demo)" +eyebrow = "Platba" title = "Predplatné vytvorené" viewInvoice = "Zobraziť a zaplatiť faktúru" +[portal.procurement.review] +acceptCta = "Prijať cenovú ponuku" +annual = "Ročný poplatok (rok 1)" +downloadCta = "Stiahnuť cenovú ponuku" +poNumber = "Objednávka: {{po}}" +renewal = "Obnovuje sa za {{amount}}/rok po skončení obdobia (+{{pct}}% CPI)" +tcv = "{{years}}-ročné obdobie, zaplatené celé vopred: {{tcv}}" +validUntil = "Platné do {{date}}" + [portal.procurement.schedule] fallback = "Plánovač sa nepodarilo načítať." fallbackLink = "Otvoriť plánovanie na novej karte" +loading = "Načítava sa plánovač..." subtitle = "Váš solutions engineer prevedie váš tím zavedením. Vyberte si čas, ktorý vám vyhovuje." title = "Naplánovať hovor" -[portal.procurement.status] -action = "Vyžaduje sa akcia" -available = "Dostupné" -complete = "Dokončené" -pending = "Čaká sa" -request = "Na požiadanie" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Uzavreté, bez odchádzajúceho pripojenia" +back = "Späť" +businessName = "Názov firmy" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Spravované, najrýchlejší štart" +continue = "Pokračovať" +deployment = "Kam sa má nasadiť?" +eula = "Súhlasím s Stirling EULA & Commercial Terms." +fullName = "Celé meno" +fullNamePlaceholder = "Vaše meno" +invites = "Pozvať spoluhráčov · voliteľné" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Naplánovať hovor" +seats = "Testujúce osoby" +seatsPlaceholder = "napr. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker alebo Kubernetes" +start = "Spustiť skúšobnú verziu" +stepOf = "Krok {{n}} z {{total}}" +subtitle = "Zadarmo na 14 dní, bez karty." +subtitleDetails = "Niekoľko údajov pre vašu cenovú ponuku a zmluvu." +talkFirst = "Chcete sa najprv porozprávať?" +title = "Nastavte si skúšobnú verziu" +viewEula = "Prečítať" +workEmail = "Pracovný email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Predĺženie pridá 7 dní a upozorní vášho solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Maximum dosiahnuté" subtitle = "Vaša bezplatná skúšobná verzia trvá do {{date}}. Karta nie je potrebná." title = "Enterprise skúšobná verzia" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Ceny za záväzný objem, SSO + SCIM + RBAC pre celú organizáciu, 90-dňový nemenný audit a vyhradený SE." -homeCta = "Spustiť skúšobnú verziu →" -homeHeadline = "Spracujte milióny PDF." - -[portal.recentActivity] -title = "Nedávna aktivita" -viewAll = "Zobraziť všetko" - -[portal.recentActivity.empty] -description = "Spustenia pipeline, nasadenia a udalosti agentov sa zobrazia tu." -title = "Zatiaľ tu nič nie je" - -[portal.search] -ariaLabel = "Hľadať" -placeholder = "Hľadať v Stirling — endpointy, pipeline, dokumenty…" - -[portal.search.empty] -noActionsDescription = "Rýchle akcie sa tu zobrazia, keď budú dostupné." -noActionsTitle = "Žiadne rýchle akcie" -noMatches = "Žiadne zhody pre „{{query}}“" -noMatchesDescription = "Skúste iné kľúčové slovo alebo prejdite katalóg." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Prepojenie účtu" -[portal.shell.header] -accountFallback = "Účet" -accountMenu = "Menu účtu" -darkMode = "Tmavý režim" -lightMode = "Svetlý režim" -search = "Hľadať" -searchPlaceholder = "Hľadať…" -signOut = "Odhlásiť sa" -switchToDark = "Prepnúť na tmavú tému" -switchToLight = "Prepnúť na svetlú tému" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumentov" linkAccount = "Prepojiť účet Stirling" -planEditor = "Plán Editor" -planEnterprise = "Plán Enterprise" -planProcessor = "Plán Processor" primaryNav = "Primárna navigácia" switchApp = "Prepnúť aplikáciu" +[portal.shell.topbar] +closeNav = "Zavrieť navigáciu" +openNav = "Otvoriť navigáciu" +search = "Hľadať" + [portal.sources] subtitle = "Opakovane použiteľné vstupné pripojenia, ktoré privádzajú dokumenty do Stirling. Nakonfigurujte pripojenie raz a potom ho odkazujte z ľubovoľného počtu politík. Kliknutím na riadok zobrazíte jeho konfiguráciu a politiky, ktoré ho používajú." title = "Zdroje" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Pripojiť zdroj" +[portal.sources.builder] +back = "Späť na zdroje" +backToTypes = "Všetky typy zdrojov" +cancel = "Zrušiť" +chooseHint = "Vyberte, odkiaľ dokumenty pochádzajú. Sivé konektory sú na ceste." +comingSoon = "Čoskoro" +comingSoonHeading = "Čoskoro" +create = "Vytvoriť zdroj" +createTitle = "Pripojiť zdroj" +delete = "Odstrániť" +editTitle = "Upraviť zdroj" +enabled = "Povolené" +save = "Uložiť zmeny" + +[portal.sources.builder.folderAccess] +description = "Automatizácie priečinkov môžu používať iba priečinky, ktoré povolil správca. Pridajte ho v nastaveniach Folder Access a potom to skúste znova." +openSettings = "Nastavenia Folder Access" +title = "Tento priečinok nie je povolený" + [portal.sources.delete] body = "Odstrániť „{{name}}“? Túto akciu nie je možné vrátiť späť. Politiky, ktoré naň odkazujú, bude potrebné aktualizovať." cancel = "Zrušiť" confirm = "Odstrániť" title = "Odstrániť zdroj?" -[portal.sources.detail] -closeAriaLabel = "Zavrieť detail" -delete = "Odstrániť zdroj" -docs24h = "Posledných 24 h" -docs30d = "Posledných 30 dní" -docsTotal = "Celkovo zistené" -docsTrend = "Dokumenty za posledných 30 dní" -documents = "Dokumenty" -edit = "Upraviť" -notReferenced = "Nepoužíva ho žiadna politika, takže ho možno bezpečne odstrániť." -pause = "Pozastaviť" -resume = "Pokračovať" -subtitle = "{{type}} · {{status}}" -usedBy = "Používa" - -[portal.sources.empty] -description = "Pripojte priečinok (a čoskoro aj cloudové úložisko), aby mali vaše politiky odkiaľ získavať dokumenty." -title = "Zatiaľ nie sú pripojené žiadne zdroje" - [portal.sources.kpi] inUse = "Používa sa" total = "Pripojenia" unused = "Nepoužité" +[portal.sources.networkFields.connection] +helperText = "Uložené pripojenie s adresou servera a prihlasovacími údajmi. Opätovne sa používa každým zdrojom, ktorý naň odkazuje." +label = "Pripojenie" + +[portal.sources.networkFields.directory] +helperText = "Priečinok na serveri na kontrolu, relatívne k domovskému adresáru prihlásenia alebo koreňu zdieľania. Nechajte prázdne pre koreň." +label = "Priečinok" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume odstráni každý súbor zo servera, keď ho spracuje každá politika." +label = "Režim čítania" + +[portal.sources.networkFields.mode.options] +consume = "Consume: spracovať každý súbor raz" +snapshot = "Snapshot: znova prečítať priečinok pri každom spustení" + +[portal.sources.networkFields.recursive] +label = "Hĺbka priečinka" + +[portal.sources.networkFields.recursive.options] +all = "Zahrnúť podpriečinky" +top = "Iba najvyššia úroveň" + [portal.sources.status] active = "Aktívne" disabled = "Zakázané" unused = "Nepoužité" [portal.sources.table] +documents = "Dokumenty" source = "Zdroj" status = "Stav" +type = "Type" usedBy = "Politiky" +[portal.sources.types.box] +description = "Načítavajte dokumenty z priečinkov Box, ktoré už váš tím zdieľa." +label = "Box" + +[portal.sources.types.dropbox] +description = "Načítavajte dokumenty zo zdieľaného priečinka Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenty, ktoré váš tím spracoval v editore, naprieč spusteniami politík a AI." label = "Editor" +noPolicies = "Z editora zatiaľ neboli spustené žiadne politiky." + +[portal.sources.types.email] +description = "Premeňte prílohy odoslané do vyhradenej poštovej schránky na dokumenty." +label = "Emailová schránka" [portal.sources.types.folder] description = "Sledujte adresár na serveri pre nové dokumenty." @@ -7943,25 +8829,80 @@ label = "Hĺbka priečinka" all = "Zahrnúť podpriečinky" top = "Iba najvyššia úroveň" +[portal.sources.types.ftp] +description = "Kontrolujte priečinok servera FTP alebo FTPS na nové dokumenty." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Načítavajte dokumenty zo zdieľaných priečinkov Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Sledujte sieťové zdieľanie SMB pre nové dokumenty." +label = "Sieťový disk" + +[portal.sources.types.onedrive] +description = "Sledujte priečinky OneDrive pre nové dokumenty." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Načítavajte dokumenty z Amazon S3 alebo S3-kompatibilného bucketu." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Uložené pripojenie obsahujúce bucket a prihlasovacie údaje. Opätovne sa používa každým zdrojom a výstupom pipeline, ktorý naň odkazuje." +label = "Pripojenie" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume odstráni každý objekt z bucketu, keď ho spracuje každá politika." +label = "Režim čítania" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: spracovať každý objekt raz" +snapshot = "Snapshot: znova prečítať bucket pri každom spustení" + +[portal.sources.types.s3.fields.prefix] +helperText = "Spracujú sa iba objekty, ktorých kľúče začínajú týmto prefixom." +label = "Prefix kľúča" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Kontrolujte SFTP drop priečinok na nové dokumenty." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Načítavajte dokumenty z knižníc dokumentov SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Zdroj" +[portal.sources.types.webhook] +createNote = "Delivery URL a tajný kľúč na podpisovanie sa vygenerujú pri vytvorení tohto webhooku. Odosielatelia POSTujú dokumenty na túto URL a odkazujúce politiky sa spustia pri prijatí." +description = "Prijímajte dokumenty cez podpísaný HTTP POST z akéhokoľvek externého systému." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Tajný kľúč na podpisovanie sa zobrazí raz pri vytvorení webhooku. Ak ho chcete obnoviť, znovu vytvorte zdroj." + +[portal.sources.types.webhook.reveal] +copy = "Kopírovať" +done = "Hotovo" +secret = "Tajný kľúč na podpisovanie" +secretHelp = "Podpíšte surové telo každej delivery týmto kľúčom (HMAC-SHA256) a odošlite ho ako header X-Stirling-Signature." +secretWarning = "Skopírujte si tajný kľúč na podpisovanie teraz. Z bezpečnostných dôvodov sa zobrazí iba raz a neskôr ho nebude možné získať." +title = "Webhook vytvorený" +url = "Delivery URL" +usage = "POSTujte každý dokument ako surové telo požiadavky na delivery URL s binárnym typom obsahu (application/pdf alebo application/octet-stream). Odkazujúce politiky sa automaticky spustia pri prijatí." + [portal.sources.wizard] -back = "Späť" -cancel = "Zrušiť" -continue = "Pokračovať" -editTitle = "Upraviť zdroj" name = "Názov" namePlaceholder = "napr. Príjem poistných nárokov" -save = "Uložiť zmeny" -subtitle = "Krok {{current}} z {{total}} · {{label}}" -title = "Pripojiť zdroj" type = "Typ" -[portal.sources.wizard.steps] -chooseType = "Vybrať typ" -configure = "Konfigurovať" -review = "Skontrolovať a pripojiť" +[portal.stepModal] +close = "Zavrieť" [portal.usage] managePayment = "Spravovať platbu" @@ -8021,16 +8962,9 @@ summary = "Vlastní tím — spravuje zdroje jeho členov a zdieľané konfigur 2 = "Prístup k portálu cez predvolenú politiku" 3 = "Všetko, čo môže robiť člen" -[portal.welcome] -ariaLabel = "Vitajte v Stirling PDF" -badge = "Open-source" -installEditor = "Nainštalovať Editor" -inviteTeammates = "Pozvať spoluhráčov" -openInBrowser = "Otvoriť v prehliadači" -perks = "Navždy zadarmo · Self-hostable" -subtitle = "Najbezpečnejší PDF Editor na svete je zadarmo pre tímy všetkých veľkostí. Zahŕňa viac ako 60 PDF operácií a SSO." -title = "Vitajte v" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Neobmedzené" [printFile] title = "Vytlačiť súbor" @@ -8613,6 +9547,14 @@ text = "Otočte svoje strany PDF v smere alebo proti smeru hodinových ručičie [rotate.tooltip.header] title = "Prehľad nastavení otáčania" +[ruler] +clearAll = "Vymazať všetko" +hideAllLabels = "Skryť všetky štítky" +hideSmallLabels = "Skryť malé štítky" +physicalValues = "Fyzické" +scaled = "V mierke" +showAllLabels = "Zobraziť všetky štítky" + [sanitize] desc = "Odstráňte potenciálne škodlivé prvky zo súborov PDF." sanitizationResults = "Výsledky očistenia" @@ -8654,31 +9596,45 @@ label = "Odstrániť XMP metadáta" [sanitize.steps] settings = "Nastavenia" -[sanitizePdf] -tags = "čistiť, zabezpečiť, bezpečné, odstrániť hrozby" - [sanitizePDF] title = "Vyčistiť PDF" +[sanitizePdf] +tags = "čistiť, zabezpečiť, bezpečné, odstrániť hrozby" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Upraviť mierku stránky" +[scaleSettings] +activeScale = "Aktívna mierka" +apply = "Použiť mierku" +applyCalibration = "Použiť kalibráciu" +calculatedScale = "Vypočítaná mierka" +calibrate = "Kalibrovať" +calibrating = "Kalibruje sa" +calibrationDistanceRequired = "Zadajte skutočnú vzdialenosť väčšiu ako nula" +calibrationInvalid = "Z tohto merania sa nepodarilo vypočítať mierku" +calibrationPaperDistance = "Nameraná vzdialenosť na stránke: {{distance}}" +calibrationTitle = "Kalibrovať mierku" +calibrationTooltip = "Zmerajte známu vzdialenosť na automatický výpočet mierky" +customScale = "Vlastná mierka" +noneSet = "Nie je nastavená žiadna vlastná mierka" +presets = "Prednastavené mierky" +ratio = "Pomer mierky" +ratioHelp = "Pomer: 1 jednotka stránky = X skutočných jednotiek" +ratioPlaceholder = "napr. 100" +ratioPositive = "Pomer musí byť väčší ako nula" +ratioRequired = "Pomer je povinný" +realDistance = "Skutočná vzdialenosť" +realDistancePlaceholder = "napr. 5" +reset = "Obnoviť predvolené hodnoty" +unit = "Jednotka" +unitInvalid = "Neplatná jednotka: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Prahová hodnota uhla:" -10 = "Nastaví veľkosť okraja pridaného a odstráneného, aby sa zabránilo bielym okrajom vo výstupe (predvolené: 1)." -2 = "Nastaví minimálny absolútny uhol potrebný na otočenie obrázka (predvolené: 10)." -3 = "Tolerancia:" -4 = "Určuje rozsah farebnej variácie okolo odhadovanej farby pozadia (predvolené: 30)." -5 = "Minimálna plocha:" -6 = "Nastaví minimálnu prahovú hodnotu plochy pre fotografiu (predvolené: 10000)." -7 = "Minimálna plocha obrysu:" -8 = "Nastaví minimálnu prahovú hodnotu plochy obrysu pre fotografiu" -9 = "Veľkosť okraja:" - [scannerImageSplit] submit = "Extrahovať naskenované obrázky" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Čo to robí" whatThisDoesDesc = "Automaticky nájde a extrahuje každú fotografiu zo skenovanej strany alebo zloženého obrázka—bez ručného orezávania." whenToUse = "Kedy použiť" +[ScannerImageSplit.selectText] +1 = "Prahová hodnota uhla:" +10 = "Nastaví veľkosť okraja pridaného a odstráneného, aby sa zabránilo bielym okrajom vo výstupe (predvolené: 1)." +2 = "Nastaví minimálny absolútny uhol potrebný na otočenie obrázka (predvolené: 10)." +3 = "Tolerancia:" +4 = "Určuje rozsah farebnej variácie okolo odhadovanej farby pozadia (predvolené: 30)." +5 = "Minimálna plocha:" +6 = "Nastaví minimálnu prahovú hodnotu plochy pre fotografiu (predvolené: 10000)." +7 = "Minimálna plocha obrysu:" +8 = "Nastaví minimálnu prahovú hodnotu plochy obrysu pre fotografiu" +9 = "Veľkosť okraja:" + [search] noResults = "Nenašli sa žiadne výsledky" placeholder = "Zadajte hľadaný výraz..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Váš server Stirling-PDF je offline a \"{{endpoint}} [session] expired = "Vaša relácia vypršala. Obnovte stránku a skúste znova." -[sessionManagement.tooltip] -header = "Správa podpisových relácií" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Noví účastníci sú pridaní na koniec poradia podpisovania" -bullet2 = "Po finalizácii relácie už nemožno pridávať účastníkov" -bullet3 = "Každý účastník dostane upozornenie, keď je na rade" -description = "Do aktívnej relácie môžete do finalizácie kedykoľvek pridať ďalších účastníkov." -title = "Pridávanie účastníkov" - -[sessionManagement.tooltip.finalization] -bullet1 = "Plná finalizácia: všetci účastníci podpísali" -bullet2 = "Čiastočná finalizácia: niektorí účastníci ešte nepodpísali" -bullet3 = "Nepodpísaní účastníci budú vylúčení z finálneho dokumentu" -bullet4 = "Po finalizácii môžete načítať podpísané PDF do aktívnych súborov" -description = "Finalizácia spojí všetky podpisy do jedného podpísaného PDF. Túto akciu nemožno vrátiť späť." -title = "Finalizácia relácie" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Účastníkov, ktorí už podpísali, nemožno odstrániť" -bullet2 = "Odstránení účastníci už nedostávajú upozornenia" -bullet3 = "Poradie podpisovania sa automaticky upraví" -description = "Účastníkov možno z relácie odstrániť predtým, ako podpíšu." -title = "Odstraňovanie účastníkov" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Každý podpis sa aplikuje na PDF postupne" -bullet2 = "Neskorší signatári môžu vidieť skoršie podpisy" -bullet3 = "Kritické pre schvaľovacie postupy a právny reťazec držby" -description = "Poradie, ktoré určíte pri vytváraní relácie, určuje, kto podpisuje ako prvý." -title = "Poradie podpisov" - [settings] close = "Zavrieť" +[settings.ai] +documents = "Dokumenty a RAG" +general = "Všeobecné" +limits = "Limity a výkon" +models = "Modely a poskytovatelia" +title = "AI" + [settings.configuration] advanced = "Pokročilé" database = "Databáza" endpoints = "Endpointy" features = "Funkcie" +folderAccess = "Prístup k priečinkom" mcp = "MCP Server" storageSharing = "Úložisko súborov a zdieľanie" systemSettings = "Systémové nastavenia" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Predvolený editor PDF" defaultPdfEditorActive = "Stirling PDF je váš predvolený editor PDF" defaultPdfEditorChecking = "Kontroluje sa..." defaultPdfEditorInactive = "Iná aplikácia je nastavená ako predvolená" +defaultPdfEditorRemind = "Pripomenúť mi nastavenie ako predvolené" +defaultPdfEditorRemindDescription = "Zobraziť banner, keď Stirling PDF nie je vašou predvolenou aplikáciou pre PDF." defaultPdfEditorSet = "Už je predvolené" defaultStartupView = "Predvolené zobrazenie pri spustení" defaultStartupViewDescription = "Vyberte, ktorá karta bude aktívna v ľavom stĺpci pri spustení aplikácie" @@ -8817,9 +9763,18 @@ languageDescription = "Vyberte jazyk rozhrania" logout = "Odhlásiť sa" setAsDefault = "Nastaviť ako predvolené" theme = "Téma" +themeAccent = "Farba zvýraznenia" +themeAccentCustom = "Vlastná farba" +themeAccentCustomHint = "Vlastná (odporúčajú sa predvolené hodnoty)" +themeAccentDark = "Farba zvýraznenia v tmavom režime" +themeAccentDefault = "Predvolená" +themeAccentDefaultHint = "Predvolený motív (odporúčané)" +themeAccentDescription = "Tlačidlá, odkazy a zvýraznenia ju používajú a jemne zafarbuje aplikáciu. Svetlý a tmavý režim majú každý svoju vlastnú. Systém použije tú, ktorá je aktívna." +themeAccentLight = "Farba zvýraznenia vo svetlom režime" themeDark = "Tmavá" themeDescription = "Prepínať medzi svetlým a tmavým režimom" themeLight = "Svetlá" +themeReset = "Obnoviť motív na predvolený" themeSystem = "System" title = "Všeobecné" user = "Používateľ" @@ -8833,6 +9788,12 @@ intro = "Povoľte overovanie používateľov, správu tímov a funkcie pracovné learnMore = "Viac v dokumentácii" title = "Pre správcov systému" +[settings.general.loginLanding] +description = "Vyberte, ktorá aplikácia sa otvorí po prihlásení do Stirling Cloud." +editor = "Editor" +processor = "Procesor" +title = "Po prihlásení" + [settings.general.mode] fullscreen = "Celá obrazovka" sidebar = "Bočný panel" @@ -9013,9 +9974,6 @@ title = "Zásady a súkromie" [settings.preferences] title = "Predvoľby" -[settings.search] -placeholder = "Hľadať na stránkach nastavení..." - [settings.security] description = "Aktualizujte svoje heslo, aby bol váš účet v bezpečí." title = "Zabezpečenie" @@ -9041,6 +9999,11 @@ security = "Zabezpečenie" telegram = "Telegram" title = "Zabezpečenie a autentifikácia" +[settings.signOut] +confirm = "Naozaj sa chcete odhlásiť?" +submit = "Odhlásiť sa" +title = "Odhlásenie" + [settings.team] title = "Tím" @@ -9156,6 +10119,7 @@ last = "Posledná strana" maintainRatio = "Prepnúť zachovanie pomeru strán" next = "Ďalšia strana" noSavedSigs = "Nenašli sa žiadne uložené podpisy" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Osobné podpisy" previous = "Predchádzajúca strana" redo = "Znova" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Odstránenie pozadia zlyhalo" hint = "Nahrajte obrázok vášho podpisu vo formáte PNG alebo JPG" label = "Nahrať obrázok podpisu" placeholder = "Vyberte súbor obrázka" +previewAlt = "Current image signature" processing = "Spracovanie obrázka..." removeBackground = "Odstrániť biele pozadie (spriehľadniť)" @@ -9207,6 +10172,17 @@ saved = "Vyberte uložený podpis vyššie, potom kliknite kdekoľvek do PDF na text = "Po zadaní svojho mena vyššie kliknite kdekoľvek do PDF, aby ste umiestnili svoj podpis." title = "Ako pridať podpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Presúvať podpis" pause = "Pozastaviť umiestňovanie" @@ -9653,10 +10629,30 @@ title = "Nahrať na server" updateButton = "Aktualizovať na serveri" uploadButton = "Nahrať na server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kliknite sem" +fallback = "Ak sa neotvorilo automaticky," +opening = "Otvára sa Swagger UI na novej karte..." tags = "api,documentation,swagger,endpoints,development" title = "Dokumentácia API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Na spustenie tohto nástroja vyberte súbory v časti Aktívn singleFileScope = "Použije sa len na: {{fileName}}" viewerMode = "Ak chcete vybrať viac súborov, prepnite do editora súborov." +[toolFormat] +ANY = "ľubovoľný súbor" +CBR = "archív komiksu (CBR)" +CBZ = "archív komiksu (CBZ)" +CSV = "súbor CSV" +EBOOK = "e-kniha" +EMAIL = "e-mail" +EXCEL = "tabuľka" +HTML = "webová stránka" +IMAGE = "obrázky" +JAVASCRIPT = "súbor JavaScript" +JSON = "súbor JSON" +MARKDOWN = "súbor Markdown" +NONE = "žiadny súbor" +PCL = "tlačový súbor PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF chránený heslom" +POSTSCRIPT = "súbor PostScript" +PPT = "prezentácia" +TEXT = "textový súbor" +VIDEO = "video" +WORD = "dokument Word" +XML = "súbor XML" +XPS = "dokument XPS" +ZIP = "archív ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Späť na všetky nástroje" collapse = "Zbaliť panel" expand = "Rozbaliť panel" goBack = "Prejsť späť" +pdfTools = "Nástroje PDF" placeholder = "Vyberte nástroj, aby ste začali" premiumFeature = "Prémiová funkcia:" toolsHeader = "Nástroje" @@ -9806,11 +10829,6 @@ noToolsFound = "Nenašli sa žiadne nástroje" quickAccess = "RÝCHLY PRÍSTUP" searchPlaceholder = "Hľadať nástroje..." -[toolPicker.categories] -advancedTools = "Pokročilé nástroje" -recommendedTools = "Odporúčané nástroje" -standardTools = "Štandardné nástroje" - [toolPicker.subcategories] advancedFormatting = "Pokročilé formátovanie" automation = "Automatizácia" @@ -9824,6 +10842,9 @@ removal = "Odstraňovanie" signing = "Podpisovanie" verification = "Overovanie" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nenašli sa žiadne nástroje" noTools = "Nie sú dostupné žiadne nástroje" @@ -9950,28 +10971,53 @@ title = "Používatelia" you = "(vy)" [users.action] +cancelInvite = "Zrušiť pozvanie" deleteTeam = "Odstrániť tím" disableMfa = "Resetovať MFA" move = "Presunúť do tímu" reinstate = "Obnoviť" remove = "Odstrániť z organizácie" +removeTeam = "Odstrániť z tímu" rename = "Premenovať tím" resetPw = "Resetovať heslo" suspend = "Pozastaviť" unlock = "Odomknúť účet" +[users.activity] +daysAgo = "pred {{count}} d" +hoursAgo = "pred {{count}} h" +justNow = "Práve teraz" +minutesAgo = "pred {{count}} min" +monthsAgo_one = "pred {{count}} mesiacom" +monthsAgo_other = "pred {{count}} mesiacmi" +never = "Nikdy" +weeksAgo_one = "pred {{count}} týždňom" +weeksAgo_other = "pred {{count}} týždňami" +yearsAgo_one = "pred {{count}} rokom" +yearsAgo_other = "pred {{count}} rokmi" + [users.cap] addProcessor = "+ Processor" approver = "Schvaľuje politiku" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Zrušiť pozvanie pre {{email}}? S aktuálnym odkazom sa nebudú môcť pripojiť." +cancelInviteTitle = "Zrušiť pozvanie" deleteTeamBody = "Odstrániť tím {{name}}? Tím musí byť najprv prázdny - presuňte jeho členov do iného tímu a nesmie stále vlastniť žiadne integračné konfigurácie." deleteTeamTitle = "Odstrániť tím" disableMfaBody = "Odstrániť MFA registráciu používateľa {{name}}? Ak je to vyžadované, nastaví si ju znova pri ďalšom prihlásení." disableMfaTitle = "Resetovať MFA" removeBody = "Natrvalo odstrániť {{name}} z organizácie? Túto akciu nie je možné vrátiť späť." +removeTeamBody = "Odstrániť {{name}} z tímu? Účet zostane zachovaný, ale stratia prístup k zdrojom tohto tímu." removeTitle = "Odstrániť člena" [users.empty] @@ -9982,10 +11028,8 @@ title = "Zatiaľ žiadni členovia" addToTeam = "Pridať do tímu" guestCount = "{{count}} hosť" guests = "Hostia" -guestsDesc = "Externí spolupracovníci, obmedzení na to, čo ste zdieľali. Iba Editor." ledBy = "vedie {{owner}}" org = "Organizácia" -orgDesc = "Vlastníci s celofiremnou autoritou a schvaľovaním politík" owners = "{{count}} vlastník" team = "Tím {{name}}" teamMeta = "{{count}} ľudí" @@ -10022,6 +11066,18 @@ username = "Používateľské meno" usernameError = "Používateľské meno musí mať aspoň 3 znaky" usernamePlaceholder = "jsmith" +[users.invites] +by = "Pozvanie od {{who}}" +cancel = "Zrušiť" +expiresInDays_one = "Platnosť vyprší o {{count}} deň" +expiresInDays_other = "Platnosť vyprší o {{count}} dní" +expiresToday = "Platnosť vyprší dnes" +title = "Čakajúce pozvania" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Pri pripájaní k backendu sa niečo pokazilo alebo nemáte prístup. Skúste to znova." title = "Nepodarilo sa načítať členov" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Hľadať prílohy" title = "Prílohy" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Názov záložky" +bookmarkTitleRequired = "Názov záložky je povinný" closeSidebar = "Zavrieť bočný panel záložiek" collapseAll = "Zbaliť všetky záložky" +empty = "No bookmarks in this document" expandAll = "Rozbaliť všetky záložky" +searchPlaceholder = "Hľadať záložky" +title = "Bookmarks" [viewer.comments] addComment = "Pridať komentár" @@ -10236,10 +11297,12 @@ locateAnnotation = "Vyhľadať v dokumente" moreActions = "Ďalšie akcie" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Strana {{page}}" placingHint = "Kliknite na stránku a umiestnite… (zrušiť)" removeCommentOnly = "Odstrániť iba komentár" saveReply = "Uložiť odpoveď" +searchPlaceholder = "Search comments" title = "Komentáre" typeComment = "Komentár" typeInsertText = "Vložiť text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Máte neuložené zmeny" closeSidebar = "Zavrieť bočný panel vrstiev" hideAll = "Skryť všetky vrstvy" showAll = "Zobraziť všetky vrstvy" +title = "Layers" [viewer.link] delete = "Odstrániť odkaz" @@ -10605,30 +11669,6 @@ title = "Text vodoznaku" image = "Obrázok" text = "Text" -[wetSignature.tooltip] -header = "Spôsoby vytvorenia podpisu" - -[wetSignature.tooltip.draw] -bullet1 = "Prispôsobte farbu a hrúbku pera" -bullet2 = "Mažte a kreslite znova, kým nebudete spokojní" -bullet3 = "Funguje na dotykových zariadeniach (tablety, telefóny)" -description = "Vytvorte vlastnoručný podpis pomocou myši alebo dotykovej obrazovky. Najlepšie pre osobné, autentické podpisy." -title = "Nakresliť podpis" - -[wetSignature.tooltip.type] -bullet1 = "Vyberte si z viacerých písiem" -bullet2 = "Prispôsobte veľkosť a farbu textu" -bullet3 = "Ideálne pre štandardizované podpisy" -description = "Vygenerujte podpis zo zadaného textu. Rýchle a konzistentné, vhodné pre obchodné dokumenty." -title = "Napísať podpis" - -[wetSignature.tooltip.upload] -bullet1 = "Podporuje PNG, JPG a iné formáty obrázkov" -bullet2 = "Pre najlepší výsledok sa odporúča priehľadné pozadie" -bullet3 = "Obrázok bude zmenený, aby sa prispôsobil oblasti podpisu" -description = "Nahrajte vopred vytvorený obrázok podpisu. Ideálne, ak máte naskenovaný podpis alebo logo spoločnosti." -title = "Nahrať obrázok podpisu" - [workbenchBar] activeFiles = "Aktívne súbory" annotations = "Anotácie" @@ -10642,6 +11682,7 @@ exitRedaction = "Ukončiť režim začiernenia" exportAll = "Exportovať PDF" exportSelected = "Exportovať vybrané strany" formFill = "Vyplniť formulár" +hideToolbar = "Hide toolbar" multiTool = "Multinástroj" panMode = "Režim posunu" print = "Tlačiť PDF" @@ -10652,6 +11693,7 @@ redact = "Začierniť" rotateLeft = "Otočiť doľava" rotateRight = "Otočiť doprava" ruler = "Pravítko / Meranie" +rulerSettings = "Nastavenia mierky" save = "Uložiť" saveAll = "Uložiť všetko" saveAs = "Uložiť ako" @@ -10661,6 +11703,7 @@ selectAll = "Vybrať všetko" selectByNumber = "Vybrať podľa čísel strán" selectLanguage = "Vybrať jazyk" share = "Zdieľať" +showToolbar = "Show toolbar" toggleAnnotations = "Prepnúť viditeľnosť anotácií" toggleAttachments = "Prepnúť prílohy" toggleBookmarks = "Prepnúť záložky" diff --git a/frontend/editor/public/locales/sl-SI/translation.toml b/frontend/editor/public/locales/sl-SI/translation.toml index 18e194a4d4..dda187e356 100644 --- a/frontend/editor/public/locales/sl-SI/translation.toml +++ b/frontend/editor/public/locales/sl-SI/translation.toml @@ -1,7 +1,6 @@ alphabet = "Abeceda" apply = "Uporabi" applyAndContinue = "Shrani in zapusti" -areYouSure = "Ali ste prepričani, da želite oditi?" back = "Nazaj" black = "črna" blue = "modra" @@ -46,10 +45,8 @@ filesSelected = "izbrane datoteke" font = "Pisava" fontSizeTooltip = "Velikost besedila številke strani v točkah. Večje številke ustvarijo večje besedilo." fontTypeTooltip = "Družina pisave za številke strani. Izberite glede na slog dokumenta." -genericSubmit = "Pošlji" green = "zelena" help = "Pomoč" -imgPrompt = "Izberite sliko(e)" incorrectPasswordMessage = "Trenutno geslo ni pravilno." info = "Podatki" invalidUndoData = "Razveljavitev ni mogoča: neveljavni podatki o operaciji" @@ -62,7 +59,6 @@ loginAgreementTitle = "Sporazum ob prijavi" logOut = "Odjava" marginTooltip = "Razdalja med številko strani in robom strani." moreOptions = "Več možnosti" -multiPdfPrompt = "Izberi PDF (2+)" never = "Nikoli" no = "Ne" noFileSelected = "Ni izbrane datoteke. Naložite eno." @@ -80,7 +76,6 @@ pageOrderPrompt = "Vrstni red strani po meri (Vnesite z vejicami ločen seznam pages = "Strani" pageSelectionPrompt = "Izbira strani po meri (Vnesite z vejicami ločen seznam številk strani 1,5,6 ali funkcije, kot je 2n+1) :" password = "Geslo" -pdfPrompt = "Izberi PDF(e)" pin = "Pripni datoteko (ostane aktivna po zagonu orodja)" pro = "Pro" processingComplete = "Vaša datoteka je pripravljena." @@ -110,6 +105,7 @@ undoSuccess = "Operacija uspešno razveljavljena" unknown = "Neznano" unpin = "Odpni datoteko (zamenjaj po zagonu orodja)" unsavedChanges = "Imate neshranjene spremembe v vašem PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Neshranjene spremembe" unsupported = "Nepodprto" username = "Uporabniško ime" @@ -356,6 +352,7 @@ text = "Ta dovoljenja nadzorujejo, kaj lahko uporabniki počnejo s PDF-jem. Najb title = "Spremeni dovoljenja" [addStamp] +preview = "Predogled žiga" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ročni prenosi: kliknite povezave in datoteke postavite v mapo te noLanguages = "V konfiguriranem imeniku ni najdenih jezikov tessdata." permissionNotice = "Pot tessdata ni zapisljiva. Prenosi se bodo odprli v brskalniku; datoteke .traineddata ročno shranite v mapo tessdata." +[admin.settings.ai.documents] +description = "Konfigurirajte model za vdelave in nastavitve pridobivanja, ki se uporabljajo za odgovarjanje na vprašanja o dokumentih. Ob shranjevanju se uporabi za AI pogon." +title = "Dokumenti in RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Pustite prazno za uporabo lastne poverilnice okolja pogona. Velja za samogostovane uvedbe z enim pogonom." +generic = "API ključ za vdelave" +openai = "OpenAI API ključ" +setPlaceholder = "Shranjeno – pustite prazno, da ohranite trenutni ključ" +voyageai = "VoyageAI API ključ" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Osnovni URL končne točke za vdelave, združljive z OpenAI / Ollama, npr. http://ollama:11434/v1. Kazati mora na zaupanja vredno interno končno točko (občutljivo na SSRF)." +label = "Osnovni URL vdelav" + +[admin.settings.ai.documents.embeddingModel] +description = "Ime modela za vdelave. Prosto besedilo; predlogi so samo namigi." +label = "Model za vdelave" + +[admin.settings.ai.documents.embeddingProvider] +description = "Ponudnik, ki se uporablja za pretvorbo besedila dokumenta v vektorske vdelave." +label = "Ponudnik vdelav" + +[admin.settings.ai.documents.maxSearches] +description = "Največje število iskanj za pridobivanje, ki jih lahko agent izvede na zahtevo." +label = "Največ iskanj" + +[admin.settings.ai.documents.reindexNote] +body = "Sprememba modela za vdelave začne veljati takoj, vendar je treba dokumente, indeksirane s prejšnjim modelom, ponovno indeksirati, da iskanje vrne pravilne rezultate." +title = "Potrebno je ponovno indeksiranje" + +[admin.settings.ai.documents.saved] +reindexNote = "Če ste spremenili model za vdelave, ponovno indeksirajte obstoječe dokumente, da bo iskanje uporabljalo novi model." + +[admin.settings.ai.documents.topK] +description = "Število najpomembnejših odsekov, pridobljenih pri vsakem iskanju." +label = "Top K" + +[admin.settings.ai.general] +description = "Povežite Stirling s Python AI pogonom in izberite, katere AI zmožnosti so izpostavljene. Spremembe se uporabijo ob ponovnem zagonu." +title = "AI pogon" + +[admin.settings.ai.general.capabilities] +description = "Vklopite ali izklopite posamezne AI funkcije. Onemogočene funkcije so v aplikaciji skrite." +title = "Zmožnosti" + +[admin.settings.ai.general.enabled] +description = "Glavno stikalo. Ko je izklopljeno, niso na voljo nobena AI orodja, agenti ali klici pogona." +label = "Omogoči AI" + +[admin.settings.ai.general.features.chat] +description = "Pogovorni pomočnik za delo z datotekami PDF." +label = "Pogovorni pomočnik" + +[admin.settings.ai.general.features.classify] +description = "Samodejno razvrstite dokumente po vrsti ali vsebini." +label = "Razvrščanje dokumentov" + +[admin.settings.ai.general.features.createPdf] +description = "Ustvarite nov PDF (npr. iz HTML) prek AI agenta." +label = "Ustvari PDF iz poziva" + +[admin.settings.ai.general.features.documentQuestions] +description = "Postavljajte vprašanja in pridobite odgovore, utemeljene na naloženem dokumentu." +label = "Vprašanja o dokumentu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Preglejte dokumente glede matematičnih in številskih napak." +label = "Matematični revizor" + +[admin.settings.ai.general.features.pdfComment] +description = "Dodajte AI komentarje za pregled in opombe v PDF." +label = "Agent za komentarje PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Časovna omejitev za zahtevnejše operacije agentov, kot je ustvarjanje dokumentov." +label = "Časovna omejitev dolgotrajnih opravil (sekunde)" + +[admin.settings.ai.general.note] +body = "AI pogon deluje kot ločena storitev. Njegova skupna skrivnost" +body2 = "je nastavljena prek okoljske spremenljivke vsebnika. API ključe ponudnikov lahko vnesete na teh straneh ali jih zagotovite kot okoljske spremenljivke pogona; ključi, vneseni tukaj, se ob shranjevanju potisnejo v pogon." +title = "O AI pogonu" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Časovna omejitev za pretočne (token za tokenom) odgovore klepeta." +label = "Časovna omejitev pretoka (sekunde)" + +[admin.settings.ai.general.test] +button = "Preizkusi povezavo" +failBody = "AI pogon se ni odzval. Preverite URL, ali vsebnik pogona deluje in ali je AI omogočen (po omogočanju je potreben ponovni zagon)." +failTitle = "AI pogon ni dosegljiv" +okBody = "AI pogon se je odzval na preverjanje stanja." +okTitle = "AI pogon je dosegljiv" + +[admin.settings.ai.general.timeoutSeconds] +description = "Časovna omejitev za standardne AI zahteve do pogona." +label = "Časovna omejitev zahteve (sekunde)" + +[admin.settings.ai.general.url] +description = "Interni URL Python AI pogona, npr. http://stirling-pdf-engine:5001." +label = "URL AI pogona" + +[admin.settings.ai.limits] +description = "Varovala za količino dela, ki ga lahko opravijo AI zahteve, in koliko jih teče hkrati. Ob shranjevanju se uporabijo za AI pogon." +title = "Omejitve in zmogljivost" + +[admin.settings.ai.limits.maxCharacters] +description = "Varovalo: zavrni AI zahteve, katerih izvlečeno besedilo presega to dolžino." +label = "Največ znakov na zahtevo" + +[admin.settings.ai.limits.maxPages] +description = "Varovalo: zavrni AI zahteve z več kot toliko stranmi PDF." +label = "Največ strani na zahtevo" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Največje število hkratnih aktivnih klicev modela v celotnem pogonu." +label = "Največja sočasnost modela" + +[admin.settings.ai.models] +description = "Izberite ponudnika LLM ter pametne/hitre modele, ki jih uporablja AI pogon. Ob shranjevanju se uporabi za AI pogon." +title = "Modeli in ponudniki" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API ključ" +description = "Pustite prazno za uporabo lastne poverilnice okolja pogona. Velja za samogostovane uvedbe z enim pogonom." +generic = "API ključ" +openai = "OpenAI API ključ" +setPlaceholder = "Shranjeno – pustite prazno, da ohranite trenutni ključ" + +[admin.settings.ai.models.baseUrl] +description = "Osnovni URL končne točke, združljive z OpenAI / Ollama, npr. http://ollama:11434/v1." +label = "Osnovni URL ponudnika" +warning = "Osnovni URL mora kazati na zaupanja vredno interno končno točko. Pogon bo izvajal strežniške zahteve do nje, zato je nezaupanja vredna vrednost občutljiva na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Največje število izhodnih tokenov za hitri model." +label = "Največ tokenov hitrega modela" + +[admin.settings.ai.models.fastModel] +description = "Cenejši, hitrejši model za lahka opravila. Prosto besedilo; predlogi so samo namigi." +label = "Hitri model" + +[admin.settings.ai.models.provider] +description = "S katerim ponudnikom LLM komunicira pogon." +label = "Ponudnik" + +[admin.settings.ai.models.smartMaxTokens] +description = "Največje število izhodnih tokenov za pametni model." +label = "Največ tokenov pametnega modela" + +[admin.settings.ai.models.smartModel] +description = "Model z visoko zmogljivostjo za kompleksno sklepanje. Prosto besedilo; predlogi so samo namigi." +label = "Pametni model" + +[admin.settings.ai.saved] +body = "Spremembe se samodejno potisnejo v AI pogon." +bodyNoPush = "Nastavitve so shranjene. Uporabljene bodo, ko AI pogon naslednjič prevzame svojo konfiguracijo." +title = "AI nastavitve shranjene" + [admin.settings.badge] clickToUpgrade = "Kliknite za ogled podrobnosti paketa" @@ -938,6 +1094,25 @@ label = "Znova ustvari ob zagonu" description = "Število dni veljavnosti potrdila" label = "Veljavnost potrdila (dni)" +[admin.settings.folderAccess] +description = "Imeniki, iz katerih lahko viri map in izhodi map berejo ter v katere lahko pišejo. To je varnostna meja: avtomatizacij nikoli ni mogoče usmeriti na strežniško pot zunaj tega seznama." +securityNote = "Pustite prazno, da v celoti onemogočite vire map in izhode. Lastni konfiguracijski imenik Stirling je vedno nedostopen." +title = "Dostop do map" + +[admin.settings.folderAccess.implied] +description = "Ti imeniki, ki jih upravlja Stirling, so vedno dovoljeni in jih tukaj ni mogoče spremeniti." +serverStorage = "Strežniška hramba datotek" +title = "Vedno dovoljeno" +watchedFolder = "Nadzorovana mapa cevovoda" + +[admin.settings.folderAccess.roots] +add = "Dodaj" +empty = "Nobena mapa ni dovoljena. Viri map in izhodi so trenutno onemogočeni." +hint = "Vnesite absolutne poti, na primer /data/inbox." +label = "Dovoljeni koreni map" +placeholder = "/data/inbox" +remove = "Odstrani koren mape" + [admin.settings.general] description = "Konfigurirajte sistemske nastavitve aplikacije, vključno z blagovno znamko in privzetim vedenjem." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Širina" [app] description = "Brezplačna alternativa Adobe Acrobat (10M+ prenosov)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Pretvori v PDF/A-3b" convertToPdfA3bDescription = "Ustvari arhivski PDF z vdelanimi prilogami" @@ -1842,6 +2020,15 @@ export = "Izvoz" accessDenied = "Dostop zavrnjen" insufficientPermissions = "Nimate dovoljenja za izvedbo tega dejanja." +[auth.callback] +completing = "Dokončevanje avtentikacije" +invalidToken = "Prijava OAuth ni uspela – neveljaven token." +missingToken = "Prijava OAuth ni uspela – token ni bil prejet." +oauthFailed = "Prijava OAuth ni uspela. Poskusite znova." +pleaseWait = "Počakajte, da dokončamo vašo prijavo." +signedOut = "Odjavljeni ste bili. Prijavite se znova." +windowMayClose = "To okno lahko zaprete, ko se postopek konča." + [auth.displayName] guest = "Gost" user = "Uporabnik" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Zavaruje PDF dokumente tako, da odstrani potencialno zlona description = "To orodje bo samodejno preimenovalo PDF datoteke na podlagi njihove vsebine. Analizira dokument, da iz besedila najde najbolj ustrezen naslov." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Samodejno zasukaj" +tags = "samodejni zasuk,popravi usmerjenost,na glavo,postrani,poravnaj,usmeri,zaznaj usmerjenost,popravi zasuk,samodejna usmeritev,skenirano,OSD" + +[autoRotate.confidenceThreshold] +title = "Prag zaupanja" + +[autoRotate.detectionMode] +auto = "Samodejno" +osd = "Samo OCR" +text = "Samo besedilo" +title = "Način zaznavanja" + +[autoRotate.error] +failed = "Pri samodejnem zasuku PDF je prišlo do napake." + +[autoRotate.inferUndetected] +title = "Zasukaj strani z nizkim zaupanjem za ujemanje" + +[autoRotate.report] +applied = "{{degrees}}° v smeri urinega kazalca" +confidenceValue = "Zaupanje {{value}}" +noChange = "Brez spremembe" +pageLabel = "Stran {{number}}" +summary = "Zasukano {{rotated}} od {{total}} strani, {{unchanged}} jih je ostalo nespremenjenih" +title = "Poročilo zaznavanja" + +[autoRotate.report.method] +inferred = "Usklajeno" +none = "Preskočeno" +osd = "OCR" +text = "Besedilo" + +[autoRotate.report.note] +belowThreshold = "Pod pragom" +blankPage = "Stran je prazna" +inferredFromDocument = "Enako kot druge strani" +noDominantDirection = "Mešane smeri besedila" +osdFailed = "Ni najdenega berljivega besedila" +osdNoVerdict = "Ni bilo mogoče določiti pravilne usmerjenosti" +tesseractUnavailable = "OCR ni nameščen" +tooFewGlyphs = "Premalo besedila" + +[autoRotate.results] +title = "Rezultati samodejnega zasuka" + +[autoRotate.settings] +title = "Nastavitve" + +[autoRotate.tooltip.confidenceThreshold] +text = "Kako gotova mora biti preverba, preden se stran obrne. Znižajte vrednost za zasuk več strani ali jo zvišajte, da več strani ostane nespremenjenih." +title = "Prag zaupanja" + +[autoRotate.tooltip.description] +text = "Ugotovi, kako mora biti vsaka stran usmerjena, in jo obrne v pravo smer. Strani, ki so že pokončne, ostanejo nespremenjene." + +[autoRotate.tooltip.detectionMode] +text = "Samodejno najprej prebere besedilo na strani, nato pa stran preveri kot sliko, če to ni dovolj. Samo besedilo preskoči preverjanje slike, kar je najhitreje za dokumente, ki niso bili nikoli skenirani. Samo OCR obravnava vsako stran kot sliko." +title = "Način zaznavanja" + +[autoRotate.tooltip.header] +title = "Samodejni zasuk" + +[autoRotate.tooltip.inferUndetected] +text = "Praznih ali skoraj praznih strani ni mogoče preveriti samostojno. Ko vse berljive strani potrebujejo enak zasuk, se te strani zasukajo, da se ujemajo z njimi." +title = "Zasukaj strani z nizkim zaupanjem za ujemanje" + [autoSizeSplitPDF] tags = "pdf,razdelitev,dokument,organizacija" @@ -2189,33 +2443,6 @@ keywords = "Ključne besede: odd, even" numbers = "Številke/obsegi: 5, 10-20" progressions = "Zaporedja: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Vrste potrdil" - -[certificateChoice.tooltip.organization] -bullet1 = "Upravljajo skrbniki sistema" -bullet2 = "V skupni rabi med pooblaščenimi uporabniki" -bullet3 = "Predstavlja identiteto podjetja, ne posameznika" -bullet4 = "Najbolj primerno za: uradne dokumente, podpise ekipe" -description = "Skupno potrdilo, ki ga zagotovi vaša organizacija. Uporablja se za podpisovanje v imenu podjetja." -title = "Organizacijsko potrdilo" - -[certificateChoice.tooltip.personal] -bullet1 = "Samodejno ustvarjeno ob prvi uporabi" -bullet2 = "Povezano z vašim uporabniškim računom" -bullet3 = "Ni ga mogoče deliti z drugimi uporabniki" -bullet4 = "Najbolj primerno za: osebne dokumente, individualno odgovornost" -description = "Samodejno ustvarjeno potrdilo, edinstveno za vaš uporabniški račun. Primerno za posamezne podpise." -title = "Osebno potrdilo" - -[certificateChoice.tooltip.upload] -bullet1 = "Zahteva datoteko P12/PFX in geslo" -bullet2 = "Lahko ga izdajajo zunanji overitelji potrdil" -bullet3 = "Višja raven zaupanja za pravne dokumente" -bullet4 = "Najbolj primerno za: pravno zavezujoče pogodbe, zunanjo overitev" -description = "Uporabite svojo datoteko potrdila PKCS#12. Omogoča popoln nadzor nad lastnostmi potrdila." -title = "Naloži lastni P12" - [certSign] awaitingSignatures = "Čakanje na podpise" chooseCertificate = "Izberite datoteko potrdila" @@ -2542,34 +2769,6 @@ server = "Strežnik" stepTitle = "Vir potrdila" upload = "Naloži" -[certSign.tooltip.header] -title = "O upravljanju podpisov" - -[certSign.tooltip.overview] -bullet1 = "Preverite obstoječe podpise in njihovo veljavnost" -bullet2 = "Oglejte si podrobne informacije o podpisnikih in potrdilih" -bullet3 = "Dodajte nove digitalne podpise za zaščito dokumentov" -bullet4 = "Podprte so več datotek z enostavnim krmarjenjem" -text = "To orodje vam omogoča preveriti, ali so vaši PDF-ji digitalno podpisani, in dodati nove digitalne podpise. Digitalni podpisi dokazujejo, kdo je dokument ustvaril ali odobril, in pokažejo, ali je bil po podpisu spremenjen." -title = "Kaj lahko to orodje naredi?" - -[certSign.tooltip.signing] -bullet1 = "Podpira formate PEM, PKCS12, JKS in strežniško potrdilo" -bullet2 = "Možnost prikaza ali skritja podpisa na PDF" -bullet3 = "Dodajte razlog, lokacijo in ime podpisnika" -bullet4 = "Izberite, na kateri strani postaviti vidne podpise" -bullet5 = "Uporabite strežniško potrdilo za preprosto možnost »Sign with Stirling-PDF«" -text = "Za podpis PDF potrebujete digitalno potrdilo (kot PEM, PKCS12 ali JKS). Podpis lahko na dokumentu prikažete ali ga ohranite nevidnega zgolj zaradi varnosti." -title = "Dodajanje podpisov" - -[certSign.tooltip.validation] -bullet1 = "Prikaže, ali so podpisi veljavni ali neveljavni" -bullet2 = "Prikaže informacije o podpisniku in datum podpisa" -bullet3 = "Preveri, ali je bil dokument po podpisu spremenjen" -bullet4 = "Za preverjanje lahko uporabi prilagojena potrdila" -text = "Ko preverite podpise, vam orodje pove, ali so veljavni, kdo je dokument podpisal, kdaj je bil podpisan in ali je bil dokument po podpisu spremenjen." -title = "Preverjanje podpisov" - [changeCreds] changeUsername = "Posodobite uporabniško ime. Po posodobitvi boste odjavljeni." credsUpdated = "Račun posodobljen" @@ -2737,12 +2936,16 @@ text = "Da bodo ta dovoljenja nespremenljiva, uporabite orodje Dodaj geslo za na [chat.actions] copy = "Kopiraj sporočilo" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Možnosti agenta Stirling" +assistant = "AI Assistant" clearChat = "Počisti klepet" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ta operacija bo uporabila vaše kredite v oblaku" tooltip = "Izberi barvo z zaslona" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Izberite barvo" [common] +actions = "Actions" back = "Nazaj" cancel = "Prekliči" close = "Zapri" +codeSample = "Code sample" collapse = "Strni" confirm = "Potrdi" continue = "Nadaljuj" copied = "Kopirano!" copy = "Kopiraj" done = "Končano" +enterValue = "Enter value" error = "Napaka" expand = "Razširi" loading = "Nalaganje..." next = "Naslednje" +open = "Open" preview = "Predogled" previous = "Prejšnje" refresh = "Osveži" +remaining = "Preostalo" retry = "Poskusi znova" save = "Shrani" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Počisti izbrano" @@ -2988,6 +3199,7 @@ title = "Način stiskanja" [compress.settings] desiredSize = "Želena velikost datoteke" desiredSizePlaceholder = "Vnesite velikost" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Stiskanje je enostaven način za zmanjšanje velikosti datoteke. Izberite Velikost datoteke za vnos ciljne velikosti in mi bomo prilagodili kakovost namesto vas. Izberite Kakovost, če želite ročno nastaviti moč stiskanja." @@ -3190,6 +3402,24 @@ pdfOptions = "Možnosti PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Najprej izberite izvorni format" settings = "Nastavitve" single = "Posamezna" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Napredno spremljanje" -allInOneWorkspace = "Vse-v-enem delovni prostor za PDF (pregledovalnik, orodja in agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Namenska podpora in SLA-ji" -fasterThroughput = "10x hitrejša prepustnost" -fullyPrivateFiles = "Popolnoma zasebne datoteke" -largeFileProcessing = "Obdelava velikih datotek" -orgWideAccess = "Organizacijski nadzori dostopa" -premiumAiModels = "Premium AI modeli" -prioritySupport = "Prednostna podpora" -privateDocCloud = "Zasebni oblak dokumentov" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Varni dostop do API" -standardThroughput = "Standardna prepustnost" -unlimitedApiAccess = "Neomejen dostop do API" -unlimitedSeats = "Neomejeno število uporabnikov" - [crop] autoCrop = "Samodejno obreži prazne robove" noFileSelected = "Izberite datoteko PDF za začetek obrezovanja" @@ -3346,6 +3558,7 @@ message = "Privzetega upravljalnika PDF ni bilo mogoče nastaviti" title = "Napaka" [defaultApp.prompt] +dontRemind = "Ne opomni me več" message = "Nastavite Stirling PDF kot privzeto aplikacijo za odpiranje datotek PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Išči..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Uredi" @@ -3644,11 +3861,13 @@ shareSelected = "Daj v skupno rabo izbrano" sharing = "Skupna raba" showAll = "Prikaži vse" showHistory = "Prikaži zgodovino" +sortBy = "Sort files" sortByDate = "Razvrsti po datumu" sortByName = "Razvrsti po imenu" sortBySize = "Razvrsti po velikosti" storage = "Shramba" storageState = "Shramba" +storageUsed = "Storage used" synced = "Sinhronizirano" title = "Naloži datoteke PDF" toolChain = "Uporabljena orodja" @@ -3694,13 +3913,17 @@ addFiles = "Dodaj datoteke" addingFiles = "Dodajanje datotek…" collapse = "Strni stransko vrstico" customizeGroups = "Prilagodi skupine" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Odprite datoteke, da začnete" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Razširi stransko vrstico" -files = "Datoteke" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive ni konfiguriran" leaveMyFiles = "Zapusti Moje datoteke" +library = "Knjižnica PDF" myFiles = "Moje datoteke" noFiles = "Še ni datotek" openFileManager = "Odpri upravitelja datotek" @@ -3708,37 +3931,36 @@ openFromComputer = "Odpri iz računalnika" openSettings = "Odpri nastavitve" other = "Drugo" recent = "Nedavno" -search = "Išči" -searchPlaceholder = "Išči datoteke..." viewAll = "Prikaži vseh {{count}} datotek" [fileSidebar.fileItem] closeViewer = "Zapri pregledovalnik" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Odpri v pregledovalniku" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Dodaj" -addLabel = "Dodaj oznako…" -categoryIconAria = "Izberite ikono za {{name}}" -createCategory = "Ustvari kategorijo" -delete = "Izbriši kategorijo" done = "Končano" -hide = "Skrij kategorijo" -newCategory = "Ime nove kategorije…" -removeLabel = "Odstrani {{name}}" -rename = "Preimenuj" reset = "Ponastavi na privzeto" -search = "Išči oznake…" -show = "Prikaži kategorijo" subtitle = "Združite svoje datoteke v nadrejene kategorije. V kategorijo dodajte obstoječe ali nove oznake, jo preimenujte ali ustvarite svojo. Datoteke, ki niso v nobeni od vaših kategorij, se prikažejo pod »Drugo«." title = "Kategorije stranske vrstice" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Dodaj v delovni prostor" addToWorkspaceCount = "Dodaj {{count}} v delovni prostor" @@ -3749,7 +3971,6 @@ backToMyFiles = "Nazaj v Moje datoteke" breadcrumbs = "Pot mape" cancel = "Prekliči" classification = "Klasifikacija" -clearSearch = "Počisti iskanje" clearSelection = "Počisti izbiro" closeDetails = "Zapri podrobnosti" create = "Ustvari" @@ -3781,6 +4002,7 @@ downloadAll = "Prenesi vse" downloadVersion = "Prenesi to različico" dropOverlay = "Spustite datoteke, da jih naložite" dropOverlaySub = "Datoteke se najprej shranijo v Lokalno. Uporabite 'Premakni v' ali 'Shrani v oblak' za organizacijo v mape." +duplicate = "Duplicate" file = "Datoteka" fileInfo = "File info" fileMenu = "Dejanja z datoteko" @@ -3808,13 +4030,12 @@ refresh = "Osveži s strežnika" remove = "Izbriši" removeVersion = "Odstrani to različico" rename = "Preimenuj" +renamed = "Preimenovano" renameFolder = "Preimenuj mapo" resizeFolderTree = "Spremeni velikost drevesa map (smernimi tipkami, Shift za večje korake; dvoklik za samodejno prilagoditev)" save = "Shrani" saveToServer = "Shrani na strežnik" saveToServerDisabledHint = "Shranjevanje na strežnik ni omogočeno na tem strežniku. Prosite skrbnika, naj to omogoči." -search = "Iskanje" -searchPlaceholder = "Iščite po tej mapi in podmapah" selectAll = "Izberi vse" selectAllHint = "Kliknite za izbiro vseh. Namig: držite Ctrl (ali Cmd) za dodajanje datotek posamično, Shift za izbiro obsega." selectedCount = "Izbranih {{count}}" @@ -3874,6 +4095,10 @@ title = "Še ni oblačnih datotek" hint = "Datoteke, shranjene brez nalaganja, ostanejo tukaj. Spustite datoteko za dodajanje." title = "Ni lokalnih datotek" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Datoteke, ki jih odprete ali uredite, se bodo pojavile tukaj." title = "Še nič ni spremenjeno" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Mape ni bilo mogoče izbrisati." deleteFolderFailedDetail = "Mape ni bilo mogoče izbrisati: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Ni bilo mogoče posodobiti videza mape." folderAppearanceFailedDetail = "Ni bilo mogoče posodobiti videza mape: {{message}}" moveFilesFailed = "Ni bilo mogoče premakniti datotek." @@ -3910,9 +4137,7 @@ count = "Datoteke" folder = "Mapa" labels = "Oznake" modified = "Spremenjeno" -name = "Ime" size = "Velikost" -toolHistoryAtVersion = "Kumulativna veriga orodij" totalSize = "Skupna velikost" type = "Vrsta" versionHistory = "Zgodovina različic" @@ -3945,6 +4170,11 @@ localHint = "Shranjeno samo v tem brskalniku" shared = "V skupni rabi" sharedHint = "V skupni rabi z vami prek povezave" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Najstarejše najprej" modifiedDesc = "Najnovejše najprej" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "V shrambi ni datotek. Najprej odprite nekaj datotek." open = "Odpri" openFile = "Odpri datoteko" openFiles = "Odpri datoteke" +selectFile = "Select {{name}}" selectFromStorage = "Izberi iz shrambe" upload = "Naloži" uploadFile = "Naloži datoteko" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Vse shranjeno" +analyzingFields = "Analysing form fields..." +extractCsvError = "Izvlečenje CSV ni uspelo" +extractXlsxError = "Izvlečenje XLSX ni uspelo" +filled = "filled" flattenAfterFilling = "Splošči po izpolnjevanju" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obv." +requiredFieldsError = "Izpolnite vsa obvezna polja" rescanFields = "Znova preglej polja" rescanFormFields = "Znova preglej polja obrazca" save = "Shrani" +saveShortcut = "Ctrl+S za shranjevanje" +unsavedChanges = "Neshranjene spremembe" [formFill.sidebar] close = "Zapri stransko vrstico" @@ -4309,6 +4552,11 @@ desc = "Samodejno preimenuje datoteko PDF na podlagi zaznane glave" tags = "samodejno zaznaj,na osnovi glave,organizira,preimenuj" title = "Samodejno preimenuj PDF" +[home.autoRotate] +desc = "Zaznajte usmerjenost vsake strani in jo samodejno zasukajte pokonci." +tags = "samodejni zasuk,popravi usmerjenost,na glavo,postrani,poravnaj,usmeri,zaznaj usmerjenost,popravi zasuk,samodejna usmeritev,skenirano,OSD" +title = "Samodejni zasuk" + [home.autoSizeSplitPDF] tags = "samodejno,razdeli,velikost" title = "Samodejno razdeli po vel./št." @@ -4608,15 +4856,12 @@ hint = "Naložite PNG, JPG, SVG ali drugo slikovno datoteko, da jo postavite na label = "Naloži sliko" placeholder = "Izberite slikovno datoteko" -[imageToPdf] -tags = "pretvorba,img,jpg,slika,fotografija" - [imageToPDF] header = "Slika v PDF" title = "Slika v PDF" -[infoBanner] -dismiss = "Opusti" +[imageToPdf] +tags = "pretvorba,img,jpg,slika,fotografija" [invite] acceptError = "Računa ni bilo mogoče ustvariti" @@ -4647,11 +4892,9 @@ welcomeTitle = "Povabljeni ste!" [landing] addFiles = "Dodaj datoteke" -heroSubtitle = "Spustite sem ali dodajte obstoječi PDF, da začnete." mobileUpload = "Naloži iz mobilne naprave" openFromComputer = "Odpri iz računalnika" uploadFromComputer = "Naloži z računalnika" -workbenchEmptyStateHero = "Spustite PDF kamor koli" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Vnesite uporabniško ime" failedToSignIn = "Prijava s ponudnikom {{provider}} ni uspela: {{message}}" forgotPassword = "Ste pozabili geslo?" loggingIn = "Prijavljanje..." -logIn = "Prijava" login = "Prijava" magicLinkSent = "Čarobna povezava je poslana na {{email}}! Preverite e-pošto in kliknite povezavo za prijavo." mfaCode = "Koda za preverjanje pristnosti" @@ -4715,26 +4957,10 @@ signInWith = "Prijavite se z" title = "Prijava" unexpectedError = "Nepričakovana napaka: {{message}}" updatePassword = "Posodobi geslo" -useEmailInstead = "Prijava z e-pošto" useMagicLink = "Namesto tega uporabite čarobno povezavo" username = "Uporabniško ime" youAreLoggedIn = "Prijavljeni ste!" -[login.slides.edit] -alt = "Urejanje PDF-jev" -subtitle = "Z več kot ducatom orodij za začrnjevanje, podpisovanje, branje in obdelavo PDF-jev boste gotovo našli, kar potrebujete." -title = "Uredite PDF-je za prikaz ali zaščito želenih informacij" - -[login.slides.overview] -alt = "Pregled Stirling PDF" -subtitle = "Paket v oblaku, ki postavlja zasebnost na prvo mesto: pretvarjajte, podpisujte, začrnjajte in upravljajte dokumente ter uporabite več kot 50 drugih zmogljivih orodij." -title = "Vse na enem mestu za vse vaše potrebe s PDF" - -[login.slides.secure] -alt = "Zavarovanje PDF-jev" -subtitle = "Preprosto dodajte gesla, začrnite vsebino in upravljajte potrdila." -title = "Zaščitite občutljive podatke v svojih PDF-jih" - [margin] large = "Velika" medium = "Srednja" @@ -4818,6 +5044,34 @@ uploadSuccess = "Nalaganje uspešno!" uploadSuccessMessage = "Vaše slike so bile prenesene." validating = "Preverjanje seje..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skenirajte za nalaganje fotografij. Slike se samodejno pretvorijo v PDF." descriptionNoConvert = "Skenirajte za nalaganje fotografij z vaše mobilne naprave." @@ -4836,8 +5090,13 @@ title = "Naloži iz mobilne naprave" tags = "Več orodij, več operacij, uporabniški vmesnik, klik povleci, sprednji del, odjemalska stran, interaktivno, nepremagljivo, premakni, izbriši, preseli, razdeli" title = "PDF večnamensko orodje" -[navbar] -search = "Išči" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Overjanje ni bilo uspešno. Okno lahko zaprete in poskusite znova." @@ -4966,6 +5225,7 @@ activeFiles = "Pogled Aktivne datoteke prikaže vse PDF-je, ki allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zapri" cropSettings = "Zdaj ko smo izbrali datoteko, ki jo želimo obrezati, lahko nastavimo orodje Obreži in izberemo območje, na katerega želimo obrezati PDF." +dialogLabel = "Onboarding" fileCheckbox = "S klikom na eno izmed datotek jo izberete za obdelavo. Izberete lahko več datotek za paketne operacije." fileReplacement = "Spremenjena datoteka bo samodejno zamenjala izvirno datoteko na Delovni mizi, kar omogoča, da jo zlahka poženete skozi dodatna orodja." filesButton = "Gumb Datoteke v vrstici hitrega dostopa omogoča nalaganje PDF-jev, na katerih želite uporabljati orodja." @@ -4976,6 +5236,7 @@ pinButton = "Če želite, da vaše datoteke ostanejo aktivne tudi po zagonu orod results = "Ko orodje zaključi, korak Pregled pokaže predogled rezultatov v tem podoknu in omogoča razveljavitev operacije ali prenos datoteke. " runButton = "Ko je orodje nastavljeno, ta gumb omogoča zagon orodja na vseh izbranih PDF-jih." selectCropTool = "Izberimo orodje Obreži, da pokažemo, kako uporabiti eno izmed orodij." +stepOf = "Korak {{current}} od {{total}}" toolInterface = "To je vmesnik orodja Obreži. Kot vidite, ni veliko prikazanega, ker še nismo dodali nobenih PDF datotek za delo." workbench = "To je Delovna miza - glavno območje, kjer si ogledujete in urejate svoje PDF-je." wrapUp = "Vse je pripravljeno! Spoznali ste glavna področja aplikacije in kako jih uporabljati. Kadarkoli kliknite gumb Pomoč, da ponovno odprete ta vodnik." @@ -4988,6 +5249,26 @@ showMeAround = "Predstavi okolje" skipForNow = "Preskoči za zdaj" skipTheTour = "Preskoči ogled" +[onboarding.checklist] +dismiss = "Opusti" +title = "Nastavite Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Zaženite Stirling izvorno na svojem računalniku" +title = "Prenesite Stirling za namizje" + +[onboarding.checklist.inviteTeam] +description = "Sodelujte s svojo ekipo" +title = "Povabite člane ekipe" + +[onboarding.checklist.shareAnalytics] +description = "Pomagajte izboljšati Stirling" +title = "Deli anonimne podatke o uporabi" + +[onboarding.checklist.takeTour] +description = "Oglejte si, kako Stirling deluje, v kratkem vodenem ogledu" +title = "Oglejte si predstavitev" + [onboarding.desktopInstall] body = "Stirling najbolje deluje kot namizna aplikacija. Uporabljate ga lahko brez povezave, hitreje dostopate do dokumentov in urejate lokalno na računalniku." selectOs = "Izberite operacijski sistem" @@ -4996,7 +5277,21 @@ titleWithOs = "Prenesi za {{osLabel}}" [onboarding.mfa] authenticationCode = "Avtentikacijska koda" +description = "Zavarujte svoj račun s povezavo aplikacije za avtentikacijo. Skenirajte QR kodo ali vnesite nastavitveni ključ, nato potrdite 6-mestno kodo za dokončanje." +enable = "Omogoči MFA" +enableError = "Dvofaktorske avtentikacije ni mogoče omogočiti. Preverite kodo in poskusite znova." +enterCode = "Vnesite 6-mestno kodo iz svoje aplikacije." +enterCodeError = "Za nadaljevanje vnesite kodo za avtentikacijo." +logout = "Odjava" +openAuthenticator = "Odprite Google Authenticator, Authy ali 1Password." qrCodeLoading = "Ustvarjanje vaše QR kode…" +regenerateQrCode = "Ponovno ustvari QR kodo" +scanQrCode = "Skenirajte QR kodo ali spodaj vnesite nastavitveni ključ." +setupError = "Dvofaktorske nastavitve ni mogoče začeti. Poskusite znova." +setupKey = "Nastavitveni ključ (ročni vnos)" +stepByStep = "Korak za korakom" +success = "MFA je omogočen. Zdaj lahko nadaljujete." +title = "Nastavitev večfaktorske avtentikacije" [onboarding.planOverview] adminBodyLoginDisabled = "Ko omogočite način prijave, lahko upravljate uporabnike, nastavite možnosti in spremljate stanje strežnika. Prvih {{freeTierLimit}} oseb na vašem strežniku lahko Stirling uporablja brezplačno." @@ -5005,6 +5300,11 @@ adminTitle = "Pregled skrbnika" userBody = "Povabite sodelavce, dodelite vloge in ohranite dokumente organizirane v enem varnem delovnem prostoru. Omogočite način prijave, ko ste pripravljeni prerasti samostojno rabo." userTitle = "Pregled načrta" +[onboarding.processorIntro] +body = "Stirling zdaj izvaja Pravila — avtomatizirana pravila, ki razvrstijo, zaščitijo in obdelajo vsak dokument ob prihodu. Nastavite jih in spremljajte izvajanja v Processor." +cta = "Oglejte si Processor" +title = "Oglejte si Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Predplačniško leto" + +[payg.prepaid.banner] +exhausted = "Vaša predplačniška zmogljivost je porabljena; obračun po porabi se je nadaljeval. Napolnite, da ohranite popust." +expiring_one = "Vaše predplačniško leto se konča čez {{count}} dan. Neuporabljena zmogljivost poteče — napolnite za začetek novega leta." +expiring_other = "Vaše predplačniško leto se konča čez {{count}} dni. Neuporabljena zmogljivost poteče — napolnite za začetek novega leta." +low = "Preostalo vam je {{pct}}% predplačniške zmogljivosti. Napolnite kadar koli, da obdelava ostane neprekinjena." + +[payg.prepaid.card] +subtitle = "Uporablja se pred obračunom po porabi in zunaj vaše omejitve porabe." +title = "Predplačniška zmogljivost" + +[payg.prepaid.meter] +capSuffix = "od {{total}} predplačniških kreditov" +expires = "Poteče {{date}}" + +[payg.prepaid.state] +exhausted = "Porabljeno" +healthy = "Veliko preostalo" +low = "Zmanjkuje" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF v PDF/A" tags = "arhiviranje,dolgoročno,standard,pretvorba,shranjevanje,hramba" title = "PDF v PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tiskanje,standard,pretvorba,produkcija,predtisk,arhiv" title = "PDF v PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kreditov" xsmall = "100 kreditov" [plan.availablePlans] +currency = "Billing currency" subtitle = "Izberite paket, ki ustreza vašim potrebam" title = "Razpoložljivi paketi" @@ -5937,49 +6267,12 @@ keyDescription = "Prilepite licenčni ključ iz e-pošte" success = "Licenca aktivirana!" successMessage = "Vaša licenca je bila uspešno aktivirana. To okno lahko zaprete." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "uveljavljeno" -enforcing = "Uveljavljanje..." -failed = "Uveljavljanje ni uspelo" outputsUnavailable = "Rezultati politike niso več na voljo za prenos." partialOutputsUnavailable = "Nekateri rezultati politike niso več na voljo za prenos." -retrying = "Zasedeno, ponovno poskušam..." runNotFound = "Izvedbe uveljavljanja ni bilo več mogoče najti." -step = "korak {{current}}/{{total}}" timedOut = "Čas za uveljavljanje je potekel, preden se je izvajanje zaključilo." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zapri" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Uveljavljanje {{names}}" applyingProgress = "Uveljavljanje {{names}} ({{done}} od {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} od {{total}} datotek ni bilo mogoče obdelati in so failureTitle = "Izvoženo brez popolnega uveljavljanja" printPolicyAppliedBody = "Ta PDF je bil posodobljen, da ustreza politiki. Preglejte spremembe, nato znova natisnite." printPolicyAppliedTitle = "Politika uveljavljena pred tiskanjem" -queued = "+{{count}} v čakalni vrsti" successTitle = "{{names}} uveljavljeno" summaryMore = "{{first}}, {{second}} in še {{more}}" summaryTwo = "{{first}} in {{second}}" -[policies.enforcement.triggerVerb] -convert = "Uveljavljanje pred pretvorbo" -default = "Uveljavljanje" -export = "Uveljavljanje pred izvozom" -input = "Uveljavljanje ob uvozu" -print = "Uveljavljanje pred tiskanjem" - -[policies.field] -accessLog = "Dnevnik dostopa" -archiveAfter = "Arhiviraj po" -auditTrail = "Revizijska sled" -belowThreshold = "Pod pragom" -destination = "Cilj" -frameworks = "Okviri" -immutableHold = "Nespremenljivo zadržanje" -keepFor = "Hrani za" -minConfidence = "Najm. zaupanje" -notify = "Obvesti ob usmerjanju" -onViolation = "Ob neskladnosti" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Dodaj" -addPlaceholder = "Dodaj oznako…" -customNote = "Prilagojeno za vašo ekipo." -defaultNote = "Uporablja se vgrajena privzeta nastavitev, deljena z vašo ekipo." -duplicate = "\"{{name}}\" že obstaja." -edit = "Uredi oznake" -empty = "Oznak še ni." -export = "Izvozi JSON" -iconAria = "Izberite ikono za {{name}}" -import = "Uvozi JSON" -importError = "Te datoteke ni bilo mogoče uvoziti." -managedNote = "Ekipne oznake upravlja vodja vaše ekipe." -modalSubtitle = "Deljeno z vso vašo ekipo. Klasifikator izbere oznake, ki ustrezajo vsakemu dokumentu." -modalTitle = "Klasifikacijske oznake" -pickIcon = "Uporabi ikono {{label}}" -removeAria = "Odstrani {{name}}" -resetToDefault = "Ponastavi na privzeto" -saveForTeam = "Shrani za ekipo" -saving = "Shranjevanje…" -sectionLabel = "Klasifikacijske oznake" -startFromScratch = "Začni od začetka" -teamCount = "ekipne oznake" -tooLong = "Oznake imajo lahko največ {{max}} znakov." -ungrouped = "Nerazvrščeno" -view = "Prikaži oznake" +categoryCount = "kategorij" +hideCategory = "Skrij kategorijo" +labelCount = "oznak" +sharedNote = "Te oznake so vgrajene in v skupni rabi za celotno ekipo." +showCategory = "Pokaži kategorijo" [policies.pii] account = "Številke računov (označene)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Ameriške bančne usmerjevalne številke (ABA)" ssn = "Številke socialnega zavarovanja" -[policies.settings] -noneExport = "Ob izvozu se trenutno ne izvaja noben pravilnik." -noneUpload = "Ob nalaganju se trenutno ne izvaja noben pravilnik." -onExport = "Ob izvozu" -onUpload = "Ob nalaganju" -reorderHandle = "Povlecite za prerazporeditev" -runOrderDesc = "Ko se na isti sprožilec izvede več pravilnikov, se izvedejo v tem vrstnem redu — vsak na izhodu prejšnjega pravilnika. Povlecite za prerazporeditev." -title = "Nastavitve pravilnikov" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Možnosti pravilnikov" -policySettings = "Nastavitve pravilnikov" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Znova poskusi neuspele pravilnike ({{count}})" -rowProgress = "Obdelanih {{completed}} od {{total}} datotek" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Kaj je pravilnik?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Samodejno najde in prečrni občutljive podatke — kot so številke socialnega zavarovanja in kartic — tako da jih v dokumentu ni mogoče prebrati." -sanitize = "Odstrani skriti JavaScript iz datoteke, tako da se ob odprtju nič ne zažene samodejno, ko jo nekdo odpre." -watermark = "Odtisne viden znak (npr. \"Zaupno\") čez vsako stran." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zapri" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} se uveljavlja..." badgeRan = "Pravilnik {{name}} se je izvedel za to datoteko" +badgeRunning = "{{name}} se izvaja..." blockingAction = "{{action}} je blokirano med uveljavljanjem pravilnika, počakajte..." dismiss = "Opusti prekrivanje" enforcingTitle = "Uveljavljanje pravilnika..." @@ -6151,6 +6316,7 @@ viewAnyway = "Prikaži datoteko (pravilnik se še uveljavlja)" [policyOption] autoRedactPhi = "Samodejno prekrij PHI" blockExport = "Blokiraj izvoz" +ccpa = "CCPA" contracts = "Pogodbe" documents = "Dokumenti" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Paket Editor" subscribed = "Paket Processor" unlinked = "Ni povezano" -[portal.agentBuilder] -bootstrapFromDocument = "Inicializiraj iz dokumenta" -sectionsAriaLabel = "Razdelki graditelja agentov" -selectorAriaLabel = "Agenti" -subtitle = "Oblikujte, preizkusite in uvedite AI agente, ki klasificirajo, izločajo iz dokumentov in jih usmerjajo. Določite scenarije, omejite dostop do orodij, zaženite zlati nabor in objavite različico." -title = "Graditelj agentov" - -[portal.agentBuilder.bootstrap] -cancel = "Prekliči" -dropzoneText = "Izberite vzorčni dokument (PDF ali slika)" -lead = "Spustite vzorčni dokument in predlagali bomo scenarije ter shemo izločanja, ki jo lahko izpopolnite. Nič ni objavljeno, dokler tega ne pregledate." -submit = "Inicializiraj agenta" -subtitle = "Ustvarite začetno različico novega agenta iz ene reprezentativne datoteke" -title = "Inicializacija iz dokumenta" - -[portal.agentBuilder.empty] -description = "Inicializirajte agenta iz vzorčnega dokumenta, da ustvarite začetne scenarije in shemo izločanja, nato ga izpopolnite in objavite." -title = "Agentov še ni" - -[portal.agentBuilder.evals] -casesPassing = "Uspešni primeri" -columnCase = "Eval primer" -columnLatency = "Zakasnitev" -columnResult = "Rezultat" -fail = "neuspeh" -goldenSetPassRate = "Stopnja uspešnosti zlatega nabora" -latencyMs = "{{ms}} ms" -notRun = "ni zagnano" -pass = "uspeh" -passRate = "Stopnja uspešnosti" -runEvals = "Zaženi evalvacije" - -[portal.agentBuilder.evals.empty] -description = "Evalvacije spremenijo vaše scenarije v ponovljiv zlati nabor. Nadgradite za spremljanje stopnje uspešnosti skozi čas in pogojevanje objav z njo." -title = "Zlatega nabora še ni" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "po zlatih naborih" -activeAgents = "Aktivni agenti" -avgPassRate = "Povp. stopnja uspešnosti evalvacij" -fleetWide = "v celotni floti" -latestPublished = "Nazadnje objavljeno" -scenarios = "Scenariji" -testCases = "testni primeri" -totalDescription_one = "skupaj {{count}}" -totalDescription_other = "skupaj {{count}}" - -[portal.agentBuilder.scenarios] -add = "Dodaj" -addScenario = "Dodaj scenarij" -enable = "Omogoči" -expectationLabel = "Pričakovano vedenje" -expectationPlaceholder = "Kaj naj agent naredi" -inEval = "v evalvaciji" -mute = "Utišaj" -muted = "utišano" -nameLabel = "Ime" -namePlaceholder = "npr. eskalacija skladnosti" - -[portal.agentBuilder.tabs] -evals = "Evalvacije" -scenarios = "Scenariji" -tools = "Orodja" -versions = "Različice" - -[portal.agentBuilder.tools] -broadAccess = "Širok dostop" -deniedHint = "Izbrana orodja so blokirana. Vse drugo ostane klicljivo." -deniedTools = "Zavrnjena orodja" -governanceGate = "Upravljanje orodij je na voljo v paketu Enterprise." -restricted = "Omejeno" -restrictedAccess = "Omejen dostop do orodij" -restrictedDescription = "Dovolite vsa orodja razen tistih, ki jih spodaj zavrnete." - -[portal.agentBuilder.versions] -current = "trenutno" -historyGate = "Celotna zgodovina različic in povrnitev sta na voljo v paketu Enterprise." -publish = "Objavi" -rollBack = "Povrni" - [portal.assistant] close = "Zapri pomočnika" error = "Pomočnika ni bilo mogoče doseči." @@ -6365,9 +6451,21 @@ typing = "Tipkanje" loading = "Nalaganje" redirectingToEditor = "Preusmerjanje v urejevalnik..." +[portal.billing.activation] +maybeLater = "Morda pozneje" +title = "Vklopite Processor" + +[portal.billing.activation.payg] +sub = "Nastavite mesečno omejitev. Plačajte za izvedeno." +title = "Plačilo po porabi" + +[portal.billing.activation.prepay] +badge = "2 meseca brezplačno" +sub = "En račun, kartica ali bančno nakazilo." +title = "Predplačajte leto" + [portal.billing.checkout] noClientSecret = "Edge function ni vrnila client_secret." -subtitle = "Dodajte kartico, da nadaljujete po brezplačni dodelitvi paketa Editor. Stripe poskrbi za ostalo." title = "Vklopite paket Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Vaše plačilo je uspelo, vendar aktivacija traja nekoliko dlje kot obi close = "Zapri" title = "Skoraj smo tam" +[portal.billing.checkout.cap] +amountAria = "Mesečna omejitev porabe" +back = "Nazaj" +continue = "Nadaljuj na plačilo" +error = "Omejitve porabe ni bilo mogoče nastaviti" +finePrint = "Računi se izdajajo mesečno. Prekličete lahko kadar koli." +noLimit = "Brez omejitve" +note = "Nikoli vam ne zaračunamo prek vaše omejitve. Obdelava se samo začasno ustavi." +perMonth = "/ mes." +title = "Nastavite omejitev porabe" + +[portal.billing.checkout.card] +fields = "Številka kartice · MM / LL · CVC · ZIP" +label = "Podatki kartice" +note = "Podatke kartice zbira Stripe. Stirling nikoli ne shranjuje PAN ali CVC." + [portal.billing.checkout.error] title = "Checkouta ni bilo mogoče začeti" @@ -6383,10 +6497,9 @@ body = "Vaše plačilo je bilo izvedeno. Vklapljamo merjeno obdelavo v vseh vaš hint = "Pustite to okno odprto." title = "Aktiviranje vašega paketa Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "v portal env za omogočanje checkouta v aplikaciji." -bodyBefore = "Nastavite" -title = "Stripe ni konfiguriran" +[portal.billing.checkout.pay] +limit = "Mesečna omejitev porabe {{amount}}/mes. Obdelava se ustavi pri omejitvi." +limitNoCap = "Brez omejitve porabe. Obdelava se ne bo ustavila. Računi se izdajajo mesečno." [portal.billing.enterpriseUpsell] cta = "Ustvarite svojo ponudbo Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Trenutni paket" freeForever = "Brezplačno za vedno" noTeamResolved = "V vaši denarnici še ni razrešene ekipe — osvežite in poskusite znova." ownerOnly = "Samo lastnik ekipe lahko vklopi paket Processor." +payInvoice = "Plačaj račun za dokončanje" planName = "Editor" ssoIncluded = "SSO vključen" switchOnProcessor = "Vklopi Processor →" unlimitedUsers = "Neomejeno uporabnikov" +viewQuote = "Ogled ponudbe" [portal.billing.invoices] columnAmount = "Znesek" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Neposredne API zahteve" segmentApiLabel = "API" segmentAutomationDesc = "Avtomatizacije in cevovodi" segmentAutomationLabel = "Avtomatizacija" +sizeMultiplier = "{{formatted}} datotek PDF je uporabilo množitelj velikosti" +summary = "{{unique}} edinstvenih · {{units}} merskih enot · {{avg}} povprečno na PDF" +summaryNoRate = "{{unique}} edinstvenih · {{units}} merskih enot" unit = "merjeni PDF-ji" +unitsPending_one = "{{units}} merska enota čaka na sinhronizacijo iz povezanih instanc" +unitsPending_other = "{{units}} merskih enot čaka na sinhronizacijo iz povezanih instanc" + +[portal.billing.prepaid] +capSuffix = "od {{total}} predplačniških kreditov" +expires = "Poteče {{date}}" +eyebrow = "Predplačniška zmogljivost" +note = "Uporablja se pred obračunom po porabi in zunaj vaše omejitve porabe." +topUp = "Napolni" + +[portal.billing.prepaid.buy] +back = "Nazaj" +calcTitle = "Izračunajte svoje letno plačilo" +cancel = "Prekliči" +cancelPurchase = "Prekliči nakup" +close = "Zapri" +continueToPayment = "Nadaljuj na plačilo" +finalise = "Dokončaj" +invoiceErrorTitle = "Računa ni bilo mogoče ustvariti" +loadingTitle = "Nalaganje vaše ponudbe" +payTitle = "Plačajte za svoje leto" +step = "Korak {{current}} od {{total}}" + +[portal.billing.prepaid.calc] +change = "Spremeni" +deployRow = "Uvedba" +done = "Končano" +enterpriseHint = "To je raven za podjetja. Pri tem obsegu podjetniške cene presegajo vsak samopostrežni popust. Zaklep cene, pogoji in ponudba v nekaj minutah." +governanceRow = "Upravljanje" +governanceValue = "{{label}} · {{count}} pravil" +handlesLabel = "Vaš Processor" +handlesValue = "obdela {{volume}} kreditov / mes." +pipelinesLabel = "Cevovodi" +policiesMeta = "{{count}} pravil" +poolCaption = "En sklad {{credits}} kreditov za leto. Težki meseci si izposodijo od lažjih." +priceLabel = "Vaše leto · 12 mesecev za ceno 10" +rateUnknown = "Točno ceno bomo prikazali ob zaključku nakupa." +savings = "Prihranite {{amount}} · 2 meseca brezplačno." +sizingRow = "Velikost" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Skupaj uporabnikov" + +[portal.billing.prepaid.consent] +label = "Razumem, da se ob porabi moje predplačniške zmogljivosti ali njenem poteku po 12 mesecih obdelava samodejno nadaljuje po standardni merjeni tarifi plačila po porabi (do moje omejitve porabe), razen če prekličem, in da lahko kadar koli prekličem v obračunskem portalu." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Upravlja Stirling. Deluje v nekaj minutah." +cloudValue = "Stirling Cloud · Upravljano" +selfhost = "Samogostovano" +selfhostDesc = "Deluje na zasebni infrastrukturi. Enaka tarifa." +selfhostValue = "Samogostovano · Zasebna infrastruktura" + +[portal.billing.prepaid.offer] +cta = "Preglej ponudbo" +subtitle = "Predplačajte leto obdelave PDF in prejmite dva meseca brezplačno, uporabljeno pred obračunom po porabi, zunaj vaše omejitve porabe." +title = "Pridobite 12 mesecev za ceno 10" + +[portal.billing.prepaid.pay] +accountName = "Ime imetnika računa" +accountNamePlaceholder = "npr. Jane Smith" +companyName = "Ime podjetja (neobvezno)" +companyNamePlaceholder = "npr. Acme Inc." +downloadInvoice = "Prenesi račun" +payOnline = "Plačaj prek spleta" +poNumber = "Številka naročilnice (neobvezno)" +poNumberPlaceholder = "npr. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Napredno" +advancedDesc = "Cevovodi poganjajo večino vaše obdelave." +none = "Brez" +noneDesc = "Cevovodov še ne izvajate. Vklopite jih lahko kadar koli." +standard = "Standardno" +standardDesc = "Nekaj cevovodov ponovno obdela prispele datoteke PDF." + +[portal.billing.prepaid.posture] +essentials = "Osnove" +essentialsDesc = "Razvrščanje, privzeto, plus skupna raba" +governed = "Upravljano" +governedDesc = "Doda varnost in usmerjanje" +regulated = "Regulirano" +regulatedDesc = "Vsaka kategorija, vklj. skladnost, hrambo, zajem" + +[portal.billing.prepaid.review] +download = "Prenesi ponudbo (PDF)" +downloading = "Pripravljanje…" +share = "deli za odobritev" + +[portal.billing.prepaid.size] +compact = "Kompaktno" +compactDesc = "Datoteke pod 25 MB, brez stroškov podatkov" +heavy = "Težko" +heavyDesc = "Skenirano ali z veliko slikami, običajno 50 MB+" +standard = "Standardno" +standardDesc = "Večinoma majhno, nekaj skenov nad 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Porabljeno" +healthy = "Veliko preostalo" +low = "Zmanjkuje" [portal.billing.spendLimit] adjustLimit = "Prilagodi omejitev" @@ -6494,6 +6713,8 @@ label = "Predvidena prekoračitev." [portal.billing.spendThisMonth] eyebrow = "Poraba ta mesec" +freeRemaining_one = "Preostala je {{formatted}} brezplačna datoteka PDF" +freeRemaining_other = "Preostalo je {{formatted}} brezplačnih datotek PDF" processed_one = "Obdelan {{formattedCount}} PDF." processed_other = "Obdelanih {{formattedCount}} PDF-jev." processedWithRate_one = "Obdelan {{formattedCount}} PDF, po {{rate}} vsak." @@ -6510,6 +6731,7 @@ reachedTitle = "Mesečna omejitev porabe dosežena" title = "Portala Stripe ni bilo mogoče odpreti" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "od {{allowance}} brezplačnih PDF-jev porabljenih" capSuffix_other = "od {{allowance}} brezplačnih PDF-jev porabljenih" eyebrow = "Preizkus Processor" @@ -6534,6 +6756,376 @@ signature = "podpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nova povezava" +createTitleFor = "Poveži {{name}}" +delete = "Izbriši" +edit = "Uredi" +editTitle = "Uredi povezavo" +unsupportedType = "Te vrste povezave tukaj ni mogoče urejati." + +[portal.connections.categories.advanced] +description = "Za storitev, ki je še ne podpiramo." +label = "Napredno" + +[portal.connections.categories.audit] +description = "Zabeležite, kaj se je zgodilo z dokumentom, tam, kjer vaši revizorji že preverjajo." +label = "Revizijsko in skladnostno beleženje" + +[portal.connections.categories.notify] +description = "Obvestite drug sistem, da se je nekaj zgodilo." +label = "Obvestila in potek dela" + +[portal.connections.categories.security] +description = "Preglejte ali označite dokument, preden se premakne naprej." +label = "Varnost in razvrščanje" + +[portal.connections.categories.signing] +description = "Pošljite dokument v podpis ali notarsko overitev." +label = "E-podpis in notarska overitev" + +[portal.connections.categories.storage] +description = "Kam končajo obdelani dokumenti – v bucket, pogon, zahtevek ali stran." +label = "Datoteke in priloge" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling to shrani šifrirano in nikoli več ne prikaže." +label = "Dostopni token" + +[portal.connections.commonFields.apiKey] +label = "API ključ" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Koraki, ki uporabljajo to povezavo, lahko kličejo samo poti pod tem URL, nikoli drugega gostitelja." +label = "URL strežnika" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Gostitelj" + +[portal.connections.commonFields.password] +label = "Geslo" + +[portal.connections.commonFields.port] +label = "Vrata" + +[portal.connections.commonFields.username] +label = "Uporabniško ime" + +[portal.connections.commonFields.webhookUrl] +helperText = "Prilepite URL, ki vam ga je dala storitev. Klicati je mogoče samo poti pod njim." +label = "Webhook URL" + +[portal.connections.fields] +name = "Ime povezave" +namePlaceholder = "npr. Produkcija" + +[portal.connections.picker] +cancel = "Prekliči" +createNew = "Nova povezava..." +placeholder = "Izberite povezavo" +save = "Shrani povezavo" + +[portal.connections.picker2] +back = "Spremeni integracijo" +heading = "Kaj želite povezati?" +noResultsBody = "Tega še ne podpiramo – povejte nam o tem in razmislili bomo o dodajanju." +noResultsTitle = "Nič se ne ujema z \"{{query}}\"" +searchPlaceholder = "Išči integracije – poskusite \"sign\", \"ocr\" ali ime izdelka" + +[portal.connections.types.api] +description = "Kličite kateri koli HTTP API iz pravila. Izberete URL, avtentikacijo in payload." +label = "API po meri" + +[portal.connections.types.api.fields.authType] +label = "Avtentikacija" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Uporabniško ime in geslo" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token v glavi po meri" + +[portal.connections.types.api.fields.authType.options.none] +label = "Brez" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Prijava za token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Koraki, ki uporabljajo to povezavo, lahko kličejo samo poti pod tem URL, nikoli drugega gostitelja." +label = "Osnovni URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Ime glave" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Pot za prijavo" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Geslo" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Če API odgovori s povezavo za prenos, je mogoče pridobiti samo te gostitelje. Ločite z vejicami. Pustite prazno, da dovolite samo lastnega gostitelja osnovnega URL." +label = "Dovoljeni gostitelji rezultatov" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Pošlji token kot to glavo" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Glava odgovora, v kateri klic za prijavo vrne token, npr. X-Auth-Token." +label = "Glava odgovora tokena" + +[portal.connections.types.api.fields.username] +label = "Uporabniško ime" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Preglejte dokumente za zlonamerno programsko opremo na strežniku, ki ga upravljate – nič ne zapusti vašega omrežja." +label = "ClamAV (samogostovano)" + +[portal.connections.types.cloudmersive] +description = "Pregled virusov in vsebine za dokumente v pravilu." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokirajte dokumente, ki vsebujejo makre, izvršljive datoteke, skripte ali vdelane predmete – ne samo znane zlonamerne programske opreme." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Priložite obdelane dokumente strani Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Pošljite dokumente v ConsignO Cloud (Notarius) za elektronski podpis in notarsko overitev." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Vaše okolje ConsignO Cloud, vključno z /api/v1." +label = "URL strežnika" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API ključ" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API skrivnost" + +[portal.connections.types.consigno.fields.password] +label = "Geslo" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potrebno samo, če vaša organizacija uporablja tenant-e." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Uporabniško ime" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Objavite sporočilo v Discord, ko pravilo obdela dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indeksirajte revizijski dogodek v Elasticsearch vsakič, ko pravilo obdela dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Preverjajte FTP ali FTPS strežnik za nove dokumente." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Način povezave" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktivno" +passive = "Pasivno (priporočeno)" + +[portal.connections.types.ftp.fields.security] +helperText = "Navadni FTP pošilja poverilnice in datoteke nešifrirano; izberite FTPS, če ga strežnik podpira." +label = "Šifriranje" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (eksplicitno / AUTH TLS)" +implicit = "FTPS (implicitno)" +none = "Brez (navadni FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Objavite sporočilo v prostor Google Chat, ko se pravilo izvede." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Priložite obdelane dokumente zadevi Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Pošljite dokument ali obvestilo po Email, ko se pravilo izvede." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Shranjujte obdelane dokumente v Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Geslo aplikacije" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Zaznajte osebne podatke v besedilu dokumenta z uporabo strežnika Presidio, ki ga upravljate." +label = "Presidio (samogostovano)" + +[portal.connections.types.purview] +description = "Preberite in uporabite oznake občutljivosti Microsoft Purview na datotekah PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Neobvezno. Potrebno samo za izbiro oznak po imenu; označevanje deluje tudi brez tega." +label = "ID aplikacije (odjemalca)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Skrivnost odjemalca" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Vtisnjeno na vsako oznako kot njen SiteId, da orodja, ki podpirajo Purview, vedo, katera organizacija jo je uporabila." +label = "ID imenika (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, iz katerega lahko viri berejo in v katerega lahko izhodi cevovodov pišejo." +label = "Shramba S3" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID dostopnega ključa" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Pustite prazno za AWS. Nastavite to za MinIO ali drugo shrambo, združljivo s S3." +label = "Končna točka" + +[portal.connections.types.s3.fields.region] +label = "Regija" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Skrivni dostopni ključ" + +[portal.connections.types.sendgrid] +description = "Pošljite dokument ali obvestilo po Email, ko se pravilo izvede." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Preverjajte SFTP (SSH) strežnik za nove dokumente." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Neobvezno. SHA-256 prstni odtis gostiteljskega ključa strežnika (iz `ssh-keyscan host | ssh-keygen -lf -`). Ko je nastavljen, je sprejet samo strežnik, ki predstavi ta ključ; ko je prazno, se ključ, viden ob prvi povezavi, pripne in poznejše spremembe so zavrnjene." +label = "Prstni odtis gostiteljskega ključa" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Geselska fraza ključa" + +[portal.connections.types.sftp.fields.password] +helperText = "Pustite prazno, če se namesto tega avtenticirate z zasebnim ključem." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Neobvezno. Prilepite zasebni ključ OpenSSH za avtentikacijo s ključem namesto z geslom." +label = "Zasebni ključ" + +[portal.connections.types.slack] +description = "Objavite sporočilo v Slack, ko pravilo obdela dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Spremljajte omrežno skupno rabo SMB / CIFS (Windows ali Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Omrežna skupna raba" + +[portal.connections.types.smb.fields.domain] +helperText = "Neobvezno. Domena Windows ali delovna skupina, če jo vaš strežnik uporablja." +label = "Domena" + +[portal.connections.types.smb.fields.share] +label = "Skupna raba" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Pošljite revizijski dogodek v Splunk vsakič, ko pravilo obdela dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Pošljite revizijski dogodek v Sumo Logic HTTP zbiralnik." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Objavite sporočilo v kanal Teams, ko se pravilo izvede." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Objavite vsak dokument ali samo podrobnosti na URL, ki ga izberete." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Sprožite scenarij Zap ali Make iz pravila." +label = "Zapier in Make" + +[portal.docs] +browse = "Prebrskaj dokumentacijo" +viewSource = "Ogled izvorne kode na GitHub" + [portal.docs.authentication] codeCaption = "vsaka zahteva" eyebrow = "ZAČETEK" @@ -6624,6 +7216,9 @@ eyebrow = "VEŠČINE" lead = "Vključene, poimenovane zmožnosti, ki jih vaš agent prikliče kot eno orodje. Vsaka veščina je deterministična veriga operacij s priloženimi evalvacijami." title = "Veščine agenta" +[portal.docs.toc] +title = "Na tej strani" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCA" @@ -6697,6 +7292,8 @@ type = "Vrsta" user = "Uporabnik" [portal.documents.queue.empty] +connectSource = "Poveži vir" +createPipeline = "Ustvari cevovod" description = "Ko viri dovajajo dokumente v vaše pipeline, se bodo tukaj prikazali za pregled." title = "V čakalni vrsti ni dokumentov" @@ -6712,11 +7309,11 @@ editorAction = "Urejevalnik" empty = "Noben dokument se ne ujema s tem filtrom." rowActions = "Dejanja vrstice" sensitiveLabel = "Občutljivo" -sensitiveTitle = "Občutljivo — potreben je dostop" [portal.documents.table.columns] action = "Pipeline / Dejanje" document = "Dokument" +labels = "Labels" product = "Izdelek" status = "Stanje" time = "Čas" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Nazadnje videno" region = "Regija" status = "Stanje" +target = "Target" version = "Različica" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "V tem pogledu je prišlo do nepričakovane napake. Poskusite znov retry = "Poskusite znova" title = "Na tej strani je šlo nekaj narobe" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Nazaj" +body = "Prenesite namizno aplikacijo ali samogostujte strežnik." +done = "Končano" +guide = "Odprite vodnik {{name}}" +openInBrowser = "Namesto tega odpri v brskalniku" +sectionDesktop = "Namizna aplikacija" +sectionSelfHosted = "Samogostovano" +title = "Namestite Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Zaženite uradno sliko na svojem strežniku." +note = "Odpre se na :8080. Privzeta prijava: admin / stirling." +tagline = "Zaženite strežnik v vsebniku" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Namestite uradni Helm chart." +note = "Dodajte PVC za /configs, da ohranite nastavitve." +tagline = "Uvedite v gručo s Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Izvorne gradnje za vašo distribucijo." +guideBtn = "Ogled možnosti namestitve za Linux" +note = "Gradnje AppImage, .deb in .rpm so na voljo za pogoste distribucije." +tagline = "Izvorna namizna aplikacija za vašo distribucijo" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ali prek Homebrew" +detailBody = "Zaženite namestitveni program ali uporabite Homebrew." +downloadBtn = "Prenesi za macOS" +tagline = "Izvorna aplikacija za Apple silicon in Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Zaženite .jar na fizičnem strežniku." +downloadBtn = "Prenesi .jar" +note = "Zahteva Java 21+. Odpre se na :8080; privzeta prijava admin / stirling." +runLabel = "Nato ga zaženite" +tagline = "Zaženite .jar na fizičnem strežniku" +title = "Ročna nastavitev strežnika" + +[portal.home.download.windows] +altLabel = "Ali prek winget" +detailBody = "Zaženite namestitveni program ali uporabite upravitelja paketov." +downloadBtn = "Prenesi namestitveni program" +tagline = "Izvorna aplikacija za Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktivnih" -invite = "Povabite sodelavce" +activeOfDeployed = "{{active}} od {{total}} aktivnih ta mesec" name = "Stirling PDF Editor" open = "Odpri v brskalniku" updated = "posodobljeno {{time}}" +[portal.home.editor.deploy] +finish = "Dokončaj uvedbo" +options = "Ogled možnosti uvedbe" +start = "Uvedi Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Samogostovano · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dober dan" evening = "Dober večer" morning = "Dobro jutro" -[portal.home.onboarding] -dismiss = "Opusti nastavitev" -notStarted = "Ni začeto" -progress = "Dokončano {{done}} od {{total}}" -title = "Dokončajte nastavitev" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC za celotno organizacijo, zavezane cene glede na količino in air-gapped namestitev." -cta = "Začnite preizkus" -ctaQuote = "Pridobite ponudbo" -lead = "Za 250+ zaposlenih." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Namestite namizno aplikacijo, za vedno brezplačno." -title = "Prenesite PDF urejevalnik" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktivnih · {{recommended}} priporočenih" -chip = "{{active}} aktivnih" -title = "Vklopite pravilnike" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} povezanih · vsak PDF upravljan tam, kjer pristane" -chip = "{{connected}} povezanih" -title = "Povežite svoje vire" - -[portal.home.quickActions] -subtitle = "Najpomembnejša opravila za danes" -title = "Hitra dejanja" - -[portal.home.quickActions.buildPipeline] -blurb = "3-koračni sestavljalnik nad tipizirano knjižnico operacij" -title = "Zgradite pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooks, nadzorovane mape" -title = "Povežite vir" - -[portal.home.quickActions.issueApiKey] -blurb = "Omejen ključ z omejitvami hitrosti in seznamom dovoljenih IP" -title = "Izdajte API ključ" - [portal.infrastructure] manageEditorDeployment = "Upravljaj namestitev urejevalnika" sectionsAriaLabel = "Razdelki infrastrukture" @@ -6878,13 +7541,9 @@ heading = "API ključi" subheading = "Omejene poverilnice z omejitvami hitrosti po ključu, dovoljenji in seznami dovoljenih IP." [portal.infrastructure.apiKeys.card] -allowedIps = "Dovoljeni IP-ji" -anyIp = "Kateri koli IP (brez seznama dovoljenih)" created = "Ustvarjeno" lastUsed = "Nazadnje uporabljeno" -permissions = "Dovoljenja" -rateLimit = "Omejitev hitrosti" -rateLimitValue = "{{value}} zahtev/min" +revoke = "Prekliči ključ" usageMonth = "Uporaba ta mesec" usageToday = "Uporaba danes" @@ -6892,6 +7551,15 @@ usageToday = "Uporaba danes" description = "Ustvarite omejen ključ, da začnete klicati Stirling API." title = "API ključev še ni" +[portal.infrastructure.apiKeys.error] +load = "Vaših API ključev ni bilo mogoče naložiti. Poskusite znova." + +[portal.infrastructure.apiKeys.revoke] +body = "Preklicati \"{{name}}\"? Vsaka integracija, ki še uporablja ta ključ, bo takoj začela prejemati napake 401. Tega ni mogoče razveljaviti." +cancel = "Prekliči" +confirm = "Prekliči ključ" +title = "Prekliči API ključ" + [portal.infrastructure.audit] filterAriaLabel = "Filtriraj revizijske dogodke po kategoriji" heading = "Revizijski dnevniki" @@ -6935,6 +7603,7 @@ all = "Vse" auth = "Overjanje" config = "Konfiguracija" elevation = "Povišanje dostopa" +policy = "Pravilnik" processing = "Obdelava" security = "Varnost" @@ -6944,19 +7613,30 @@ title = "Nimate dostopa" [portal.infrastructure.audit.metrics] config = "Konfiguracija" -elevation = "Povišanje dostopa" +policy = "Pravilnik" processing = "Obdelava" totalEvents = "Skupaj dogodkov · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Konfiguracija" +elevation = "Povišanje" +policy = "Pravilnik" +processing = "Obdelava" +security = "Varnost" + +[portal.infrastructure.auditStatusLabel] +danger = "Napaka" +info = "Info" +success = "Uspeh" +warning = "Opozorilo" + [portal.infrastructure.createKey] cancel = "Prekliči" createKey = "Ustvari ključ" done = "Končano" -ipAllowlistHelper = "Z vejicami ločeni razponi CIDR. Pustite prazno, da dovolite kateri koli IP." -ipAllowlistLabel = "Seznam dovoljenih IP" keyNameLabel = "Ime ključa" keyNamePlaceholder = "npr. Produkcija · zajem" -permissionsLabel = "Dovoljenja" secretKeyCaption = "Skrivni ključ" secretWarning = "Shranite to v upravljalnik skrivnosti. Stirling vedno shrani samo hash — pozneje ga ni mogoče obnoviti." subtitle = "Omejite ključ na najmanjši obseg, ki ga potrebuje. Kadar koli ga lahko zavrtite ali prekličete." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopirajte to skrivnost zdaj — ne bo več prikazana." title = "Ustvari API ključ" titleCreated = "Ključ ustvarjen" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Namestil" -environment = "Okolje" -product = "Izdelek" -status = "Stanje" -version = "Različica" -when = "Kdaj" - -[portal.infrastructure.deployments.recent] -heading = "Nedavne namestitve" -subheading = "Najnovejše uvedbe po izdelkih in okoljih." - -[portal.infrastructure.deployments.regionColumns] -instances = "Primerki" -latency = "Zakasnitev" -load = "Obremenitev" -p99 = "P99" -region = "Regija" -status = "Stanje" -throughput = "Prepustnost" -uptime = "Čas delovanja" -version = "Različica" - -[portal.infrastructure.deployments.regions] -heading = "Regije" -subheading = "Stanje v živo za vsako nameščeno regijo Stirling — zakasnitev, obremenitev in različica uvedbe." - -[portal.infrastructure.deployments.regions.empty] -description = "Nameščene regije se prikažejo tukaj, ko je vaš delovni prostor pripravljen." -title = "Ni nameščenih regij" - -[portal.infrastructure.models] -heading = "Modeli" -msValue = "{{value}} ms" -subheading = "Katalog modelov in usmerjanje, ki poganja obdelavo dokumentov v vašem delovnem prostoru." - -[portal.infrastructure.models.byom] -description = "Registrirajte on-prem ali samogostovan model in ga pripnite na regijo za obdelavo, vezano na rezidentnost podatkov." -title = "Prinesite svoj model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Upravljani modeli, ki so na voljo vašemu delovnemu prostoru, z zakasnitvijo in stroški v živo." -subEnterprise = "Upravljani, lastni in on-prem modeli — z možnostjo pripenjanja po regijah." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeli v katalogu vašega delovnega prostora se prikažejo tukaj." -title = "Ni razpoložljivih modelov" - -[portal.infrastructure.models.columns] -cost = "Strošek" -latency = "Zakasnitev" -load = "Obremenitev" -model = "Model" -status = "Stanje" -type = "Vrsta" -version = "Različica" - -[portal.infrastructure.models.metrics] -activeModels = "Aktivni modeli" -avgLatency = "Povp. zakasnitev" -included = "Vključeno" -monthlySpend = "Mesečna poraba za modele" - -[portal.infrastructure.models.routing] -empty = "Ni konfiguriranih pravil usmerjanja." -heading = "Pravila usmerjanja" -sub = "Kateri model obravnava posamezno operacijo. Privzeto velja, kadar se ne ujema nobeno ožje pravilo." -subLocked = "Usmerite operacije na določene modele — na voljo v plačljivih paketih." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Nadgradite na Pro, da nadzirate, kateri model obravnava posamezno operacijo in vrsto dokumenta." -title = "Usmerjanje modelov je plačljiva funkcija" - -[portal.infrastructure.models.routingColumns] -default = "Privzeto" -docType = "Vrsta dokumenta" -modelForAria = "Model za {{operation}}" -operation = "Operacija" -routedTo = "Usmerjeno na" - -[portal.infrastructure.security.access.byok] -description = "Zagotovite ključ iz svojega KMS. Stirling z njim šifrira, vendar lahko še vedno bere." -label = "Prinesite svoj ključ (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ključi nikoli ne zapustijo vašega KMS. Stirling hrani samo šifrirano besedilo." -label = "Obdržite svoj ključ (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling upravlja šifrirne ključe. Najpreprosteje — brez operacij s ključi na vaši strani." -label = "Ključi pri Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Pravilnik dostopa do dokumentov" -subheading = "Nadzira, kdo lahko dešifrira obdelane dokumente v mirovanju." - -[portal.infrastructure.security.attestations] -heading = "Potrdila o skladnosti" -noReport = "Poročilo ni na voljo" -subheading = "Revizijska drža po posameznih okvirjih, s poročili, ki so na voljo za potrjene kontrole." -viewReport = "Ogled poročila →" - -[portal.infrastructure.security.compliance] -heading = "Skladnost" -subheading = "Potrdila in certifikati, ki pokrivajo platformo Stirling." - -[portal.infrastructure.security.empty] -description = "Varnostna konfiguracija vašega delovnega prostora se bo prikazala tukaj." -title = "Varnostna drža ni na voljo" - -[portal.infrastructure.security.hyokBanner] -description = "Pri HYOK šifrirni ključi nikoli ne zapustijo vašega KMS. Stirling shranjuje in obdeluje samo šifrirano besedilo, ki ga lahko kadar koli prekličete." -title = "Stirling ne more dešifrirati vaših dokumentov" - -[portal.infrastructure.security.ipAllowlist] -empty = "Ni konfiguriranih razponov IP — vsi IP-ji so dovoljeni." -heading = "Seznam dovoljenih IP" -sub = "API dostop je omejen na te razpone CIDR." -subLocked = "Omejite API dostop na znane razpone IP — na voljo v plačljivih paketih." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Nadgradite na Pro, da omejite API dostop na določena omrežja." -title = "Seznami dovoljenih IP so plačljiva funkcija" - -[portal.infrastructure.security.ipColumns] -added = "Dodano" -addedBy = "Dodal" -cidr = "CIDR" -label = "Oznaka" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritem" -heading = "Upravljanje šifrirnih ključev" -keyId = "Identifikator ključa" -lastRotated = "Nazadnje zavrteno" -rotateKey = "Zavrti ključ" -rotationPolicy = "Pravilnik vrtenja" -subheading = "Skrbništvo nad ključi, ki šifrirajo dokumente v mirovanju — kdo lahko dešifrira in kako se ključi vrtijo." - -[portal.infrastructure.security.managedBanner] -description = "Skrbništvo bring-your-own-key (BYOK) in hold-your-own-key (HYOK) je na voljo v Enterprise. Nadgradite, da zagotovite ključe iz svojega KMS." -title = "Ključe v vašem paketu upravlja Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Azija-Pacifik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR meja podatkov" -label = "Evropska unija" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Združene države" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezidentnost podatkov" -subheading = "Kje so dokumenti shranjeni in obdelani." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} uporabljeno" - -[portal.infrastructure.storage.empty] -description = "Povezana shramba in uporaba se prikažeta tukaj." -title = "Shramba ni konfigurirana" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktivno" -activeRange = "0–{{value}}d" -archived = "Arhivirano" -coldStorage = "hladna shramba" -deleted = "Izbrisano" -never = "nikoli" -purged = "trajno izbrisano" - -[portal.infrastructure.storage.providers] -connect = "Poveži" -connected = "Povezano" -heading = "Povezani ponudniki" -subheading = "Kam se zapisujejo obdelani artefakti." - -[portal.infrastructure.storage.retention] -heading = "Hramba" -subheading = "Kako dolgo se artefakti hranijo pred brisanjem v življenjskem ciklu." -windowLabel = "Privzeto obdobje hrambe" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dan" -days_other = "{{count}} dni" -never = "Nikoli ne izbriši" - -[portal.infrastructure.storage.totalUsage] -heading = "Skupna uporaba" -progressLabel = "Uporabljena shramba" -subheading = "Shramba, porabljena pri vseh povezanih ponudnikih." +revoked = "Preklicano" [portal.infrastructure.tabs] apiKeys = "API ključi" @@ -7174,6 +7656,49 @@ models = "Modeli" security = "Varnost" storage = "Shramba" +[portal.integrations] +availableHeading = "Na voljo" +comingSoonHeading = "Kmalu na voljo" +connect = "Poveži" +connectionCount_one = "{{count}} povezava" +connectionCount_other = "{{count}} povezav" +customApi = "API po meri" +searchPlaceholder = "Išči integracije" +subtitle = "Vse, s čimer lahko Stirling komunicira. Poverilnice se tukaj ustvarijo enkrat, nato pa jih uporabljajo viri, pravilniki in cevovodi." +title = "Integracije" +typedName = "Ime za {{tool}}" + +[portal.integrations.filters] +all = "Vse" +audit = "Revizija" +connected = "Povezano" +notify = "Obvesti" +security = "Varnost" +signing = "Podpisovanje" +storage = "Shramba" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Povezano" + +[portal.integrations.table] +integration = "Integracija" +worksWith = "Deluje z" + +[portal.integrations.worksWith] +pipelines = "Cevovodi" +policies = "Pravilniki" +sources = "Viri" + +[portal.legal] +draft = "{{label}} · osnutek" +loadError = "Tega dokumenta ni bilo mogoče naložiti. Poskusite znova." +loading = "Nalaganje…" +title = "Pravni dokument" + [portal.nav] agent-builder = "Graditelj agentov" components = "Komponente" @@ -7182,33 +7707,17 @@ documents = "Dokumenti" editor = "Urejevalnik" home = "Domov" infrastructure = "Infrastruktura" +integrations = "Integracije" pipelines = "Pipelines" policies = "Pravilniki" procurement = "Nabava" -settings = "Nastavitve" sources = "Viri" usage = "Uporaba in obračunavanje" users = "Uporabniki" -[portal.notifications] -markAllRead = "Označi vse kot prebrano" -title = "Obvestila" -viewAll = "Prikaži vse" - -[portal.notifications.ariaLabel] -none = "Obvestila, brez neprebranih" -unread_one = "Obvestila, {{count}} neprebrano" -unread_other = "Obvestila, {{count}} neprebranih" - -[portal.notifications.count] -allRead = "vse prebrano" -loading = "nalaganje" -new_one = "{{count}} novo" -new_other = "{{count}} novih" - -[portal.notifications.empty] -description = "Ni novih obvestil." -title = "Vse je urejeno" +[portal.nav.section] +platform = "PDF Platform" +processor = "PDF Processor" [portal.pipelines] subtitle = "Vsak samodejni pipeline dokumentov na backendu: urejena veriga operacij nad naborom virov, zagnana s sprožilcem. Kliknite vrstico za njegove korake in vire." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Nov pipeline" [portal.pipelines.builder] -addStep = "Dodaj orodje" +activate = "Activate" back = "Nazaj na pipelines" +cannotFollow = "Ne more sprejeti {{produced}}" +chooseAccount = "Izberite račun" +chooseDestination = "Izberite cilj" +chooseOperation = "Izberite, kaj naredi ta korak" +chooseSource = "Izberite vir" discard = "Zavrzi spremembe" -enabled = "Omogočeno" +inputs = "Vhod" +inputSource = "Vhodni vir" +inputTrigger = "Sprožilec" keepEditing = "Nadaljuj urejanje" -needsUpload = "Potrebuje naloženo datoteko" +moreActions = "More actions" +needsConfiguring = "Potrebna je nastavitev" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nobeno orodje se ne ujema z vašim iskanjem." -pipelineSettings = "Nastavitve pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Išči orodja" -selectToolBody = "Dodajte orodje za gradnjo pipeline." -selectToolTitle = "Orodij še ni" -toolSettings = "Nastavitve orodja" +sendToSystem = "Pošlji v drug sistem" +stepsIncompatible = "Ti koraki se ne morejo zagnati na tem, kar proizvede njihov prejšnji korak: {{tools}}." +stepsNeedSetup = "Te korake je treba pred shranjevanjem še nastaviti: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Neprepoznana operacija, ohranjena takšna, kot je." unsavedBody = "Imate neshranjene spremembe. Jih želite shraniti pred odhodom ali jih zavreči?" unsavedTitle = "Neshranjene spremembe" -uploadUnsupported = "Naložene datoteke v pipelines še niso podprte, zato teh korakov ni mogoče shraniti: {{tools}}." usesDefaults = "Zažene se s privzetimi nastavitvami" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Združi vsako datoteko iz prejšnjega koraka" +fan-out = "Zažene se enkrat na datoteko iz prejšnjega koraka" +format-mismatch = "Potrebuje {{accepts}}, vendar prejšnji korak proizvede {{produced}}" +output-uncertain = "Morda se ne bo zagnalo: izhod prejšnjega koraka je odvisen od njegove nastavitve" +source-mismatch = "Potrebuje {{accepts}}, vendar je vhod tega cevovoda {{produced}}" +undeclared-operation = "Ni mogoče preveriti, kaj ta korak sprejme" [portal.pipelines.composer] addTool = "Dodaj orodje" -cancel = "Prekliči" -chainEmpty = "Dodajte orodje, da začnete graditi svoj pipeline." create = "Ustvari pipeline" -directory = "Izhodna mapa" -directoryHelp = "Absolutna pot na strežniku. Biti mora znotraj konfiguriranih dovoljenih map." +createPaused = "Create paused" editingUnsupported = "Prikaz teh parametrov orodja za urejanje še ni podprt." -moveDown = "Premakni dol" -moveUp = "Premakni gor" +editSource = "Edit source" name = "Ime" namePlaceholder = "npr. Pregled redakcij" -noSources = "Viri še niso povezani. Pipeline se lahko še vedno zažene na datotekah, ki so mu posredovane neposredno." noToolSettings = "To orodje nima nastavljivih možnosti." -operations_one = "Operacija ({{count}})" -operations_other = "Operacije ({{count}})" output = "Izhod" -removeStep = "Odstrani operacijo" save = "Shrani spremembe" scheduleEvery = "Zaženi vsakih" -sources = "Viri" -sourcesLoading = "Nalaganje virov..." trigger = "Sprožilec" triggerManual = "Samo ročno" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dni" hours = "ur" minutes = "minut" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Izbrisati \"{{name}}\"? Tega ni mogoče razveljaviti." cancel = "Prekliči" @@ -7273,28 +7815,62 @@ confirm = "Izbriši" title = "Izbrisati pipeline?" [portal.pipelines.detail] +clearHistory = "Počisti zgodovino" delete = "Izbriši pipeline" run = "Zaženi zdaj" [portal.pipelines.empty] action = "Ustvari pipeline" +connectSource = "Poveži vir" description = "Ustvarite svoj prvi pipeline: izberite vire, na katerih se izvaja, povežite operacije in izberite, kam gre izhod." title = "Pipelines še ni" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktivni" paused = "Zaustavljeni" total = "Pipelines" -[portal.pipelines.output] -folder = "Zapiši v mapo" -inline = "Vrni datoteke" - [portal.pipelines.run] +allProcessed_one = "Ni ničesar za zagon: {{count}} dokument vira je že bil obdelan." +allProcessed_other = "Ni ničesar za zagon: vseh {{count}} dokumentov v virih je že bilo obdelanih." completed_one = "Zagon končan." completed_other = "Vseh {{count}} zagonov je končanih." empty = "Ničesar ni za zagnati: viri niso imeli dokumentov za obdelavo." failed = "Zagon ni uspel: {{error}}" +inFlight = "Ni nič novega za zagon: dokumenti se še vedno obdelujejo iz prejšnjega zagona." +parked_one = "Ni ničesar za zagon: {{count}} dokument je prej spodletel in je parkiran. Odpravite vzrok, nato počistite zgodovino, da poskusite znova." +parked_other = "Ni ničesar za zagon: {{count}} dokumentov je prej spodletelo in so parkirani. Odpravite vzrok, nato počistite zgodovino, da poskusite znova." running = "Zagon se je začel; še vedno poteka." timeout = "Zagon traja dlje od pričakovanega; morda se bo še vedno dokončal v ozadju." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Viri" status = "Stanje" steps = "Koraki" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Nadzor mape" @@ -7314,12 +7891,18 @@ manual = "Ročno" schedule = "Načrtovano" [portal.policies] +defaultName = "Pravilnik {{category}}" subtitle = "Stalne avtomatizacije, ki uveljavljajo pipeline orodij na vsakem dokumentu. Vsak pravilnik se sproži ob nalaganju ali izvozu, zažene svojo verigo orodij in shrani uveljavljeno različico poleg izvirnika." title = "Pravilniki" [portal.policies.card] comingSoon = "Nadgradite na Enterprise" notSetUp = "Ni nastavljeno" +requiresAiEngine = "Zahteva AI pogon" + +[portal.policies.categories.classification] +desc = "Ob nalaganju prepozna vrsto vsakega dokumenta in označi njegove metapodatke za arhiviranje in iskanje." +label = "Klasifikacija" [portal.policies.categories.compliance] desc = "Uveljavite zahteve HIPAA, GDPR, SOC 2 ali FedRAMP za vsak dokument." @@ -7344,6 +7927,13 @@ label = "Varnost" [portal.policies.config] scopeAll = "Vsi dokumenti" +[portal.policies.config.classification] +summary = "Klasificira vsak naložen dokument glede na oznake vaše ekipe in označi njegove metapodatke." + +[portal.policies.config.classification.rules] +0 = "Klasificiraj" +1 = "Označi metapodatke" + [portal.policies.config.compliance] summary = "Preveri dokumente glede na regulativne okvire, preden zapustijo sistem." @@ -7371,6 +7961,29 @@ minConfidence = "Najm. zanesljivost" 2 = "Poimenuj" 3 = "Normaliziraj" +[portal.policies.config.purview] +label = "Uporabi oznako občutljivosti Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Najemnik Purview" +labelId = "ID oznake" +labelIdHelp = "GUID oznake iz portala Purview. Uporaba oznake zapiše njene metapodatke v PDF; ne more šifrirati, za kar je potreben Microsoft odjemalec." +labelName = "Ime oznake (neobvezno)" +labelNameHelp = "Zabeleženo skupaj z ID-jem, da lahko ljudje in orodja prepoznajo oznako." + +[portal.policies.config.purview.fields.method] +help = "Purview razlikuje med oznako, ki jo je uporabil pravilnik, in oznako, ki jo je izbrala oseba." +label = "Kako je oznaka zabeležena" +privileged = "Kot jo je izbrala oseba" +standard = "Kot uporabljeno samodejno" + +[portal.policies.config.purviewRead] +label = "Preberi oznako Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Najemnik Purview" +connectionHelp = "Prebere oznako, ki jo dokument že nosi, da lahko poznejši koraki ukrepajo na podlagi nje." + [portal.policies.config.retention] summary = "Uveljavlja, kako dolgo se dokumenti hranijo, kdaj se arhivirajo in kdaj izbrišejo." @@ -7438,6 +8051,7 @@ title = "Ni še dejavnosti" [portal.policies.endpoints] addWatermark = "Vodni žig" autoRedact = "Prekrij PII" +classifyAndLabel = "Klasificiraj" compressPdf = "Stisni" flatten = "Splošči" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vaši pravilniki so shranjeni in se bodo prikazali, ko bo povezav retry = "Poskusi znova" title = "Backend ni na voljo" +[portal.policies.operations] +change = "Spremeni, kaj naredi ta korak" +noResults = "Noben korak se ne ujema s tem. Poskusite ime izdelka ali \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Išči korake – poskusite \"scan\", \"attach\" ali ime izdelka" + +[portal.policies.operations.bodyMode] +binary = "Surovi bajti datoteke" +json = "Znotraj JSON tovora" +multipart = "Kot nalaganje datoteke (multipart)" + +[portal.policies.operations.clamavScan] +description = "Vaš lastni strežnik ClamAV preveri dokument; nič ne zapusti vašega omrežja." +label = "Preglej za viruse (samostojno gostovano)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Zavrne dokumente, ki vsebujejo makre, izvedljive datoteke, skripte ali vdelane objekte." +label = "Blokiraj aktivno vsebino" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive preveri dokument in zagon se ustavi, če ni čist." +label = "Preglej za viruse" + +[portal.policies.operations.confluenceAttach] +description = "Shrani obdelani dokument na stran." +label = "Priloži strani Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Pošlje dokument v ConsignO Cloud za podpisovanje." +label = "Pošlji v podpis (ConsignO)" +note = "Pošlje dokument. Pridobivanje podpisane kopije še ni podprto, ker korak ne more posredovati rezultata poznejšemu koraku." + +[portal.policies.operations.customApiCall] +description = "Klic sestavite sami: pot, metoda, telo in obravnava odgovora." +label = "Pokliči API po meri" + +[portal.policies.operations.discordNotify] +description = "Obvesti kanal, da je pravilnik obdelal dokument." +label = "Objavi sporočilo v Discord" + +[portal.policies.operations.elasticIndex] +description = "Zabeleži, kaj je pravilnik naredil, kot iskalni dokument." +label = "Indeksiraj revizijski dogodek v Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart ustreza API-jem za nalaganje; JSON ustreza API-jem, ki želijo datoteko znotraj tovora; binary pošlje surove bajte." +label = "Kako poslati dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON telo, poslano takšno, kot je. {{document.base64}} nosi datoteko; {{document.*}} in {{run.*}} se izpolnita za vsak dokument." +label = "JSON telo" + +[portal.policies.operations.fields.connection] +label = "Račun" + +[portal.policies.operations.fields.domain] +label = "Domena Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Polje obrazca, pod katerim API pričakuje dokument." +label = "Ime polja datoteke" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON objekt parov ime/vrednost glave. Poverilnica povezave je dodana namesto vas." +label = "Glave" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ključ zadeve" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Poslano kot telo sporočila. {{document.*}} in {{run.*}} se izpolnita za vsak dokument." +label = "Sporočilo" + +[portal.policies.operations.fields.method] +label = "Metoda" + +[portal.policies.operations.fields.pageId] +label = "ID strani" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Dodano osnovnemu URL povezave. Nikoli ne more doseči drugega gostitelja." +label = "Pot" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Kam na disku naj se zapiše. {{document.filename}} se izpolni za vsak dokument." +label = "Ciljna pot" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Poročilo pusti dokument nespremenjen. Zamenjava ga zamenja s tem, kar se vrne." +label = "Kaj storiti z odgovorom" + +[portal.policies.operations.fields.signerEmail] +label = "E-pošta podpisnika" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Zadeva" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizira to besedilo. Uporabite {{document.title}} ali prilepite besedilo, ki ga želite preveriti." +label = "Besedilo za preverjanje" + +[portal.policies.operations.fields.to] +label = "Za" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Uporabniško ime Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Obvesti prostor, da je pravilnik obdelal dokument." +label = "Objavi sporočilo v Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Shrani obdelani dokument v zadevo." +label = "Priloži zadevi Jira" + +[portal.policies.operations.mailgunEmail] +description = "Pošlje obdelani dokument kot prilogo." +label = "Pošlji dokument po e-pošti (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Zapiše obdelani dokument v mapo." +label = "Naloži v Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Vaš lastni strežnik Presidio poroča o osebnih podatkih, ki jih najde." +label = "Zaznaj osebne podatke" +note = "Presidio bere besedilo, ne datotek. Podajte besedilo, ki ga želite preveriti — sam dokument se ne naloži." + +[portal.policies.operations.responseMode] +replace = "Zamenjaj dokument z odgovorom" +report = "Obdrži dokument, zabeleži odgovor" + +[portal.policies.operations.sendgridEmail] +description = "Pošlje obdelani dokument kot prilogo." +label = "Pošlji dokument po e-pošti (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Obvesti kanal, da je pravilnik obdelal dokument." +label = "Objavi sporočilo v Slack" + +[portal.policies.operations.splunkEvent] +description = "Zabeleži, kaj je pravilnik naredil, tam, kjer vaši revizorji že iščejo." +label = "Pošlji revizijski dogodek v Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Zabeleži, kaj je pravilnik naredil, v HTTP zbiralnik." +label = "Pošlji revizijski dogodek v Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Obvesti kanal, da je pravilnik obdelal dokument." +label = "Objavi sporočilo v Teams" + +[portal.policies.operations.webhookPost] +description = "Pošlje sam dokument na URL, ki ga izberete." +label = "Objavi dokument v webhook" + +[portal.policies.operations.zapierNotify] +description = "Podrobnosti zagona preda vaši avtomatizaciji." +label = "Sproži scenarij Zap ali Make" + [portal.policies.stats] activeFor = "Aktivno" dataProcessed = "Obdelani podatki" @@ -7473,6 +8282,13 @@ label = "Uveljavljeni dokumenti" description = "Konfigurirano, vendar se ne sproži" label = "Začasno ustavljeno" +[portal.policies.table] +appliesTo = "Velja za" +docs = "Uveljavljeni dokumenti" +enforces = "Uveljavlja" +policy = "Pravilnik" +status = "Stanje" + [portal.policies.wizard.actions] back = "Nazaj" cancel = "Prekliči" @@ -7480,6 +8296,10 @@ continue = "Nadaljuj" enablePolicy = "Omogoči pravilnik" saveChanges = "Shrani spremembe" +[portal.policies.wizard.capability.classify] +desc = "Prepozna vrsto dokumenta iz oznak vaše ekipe in ga označi, da se arhivira in išče po kategoriji." +label = "Klasificiraj dokument" + [portal.policies.wizard.capability.compress] desc = "Stisne dokument na manjšo velikost datoteke." label = "Zmanjšaj velikost datoteke" @@ -7500,10 +8320,18 @@ label = "Prekrij občutljive podatke" desc = "Odstrani skriti JavaScript, da se ob odprtju dokumenta nič ne more samodejno zagnati." label = "Odstrani aktivno vsebino" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dokazuje, da je dokument obstajal v tej točni obliki v določenem trenutku z uporabo neodvisnega organa za časovni žig. Pošlje se samo zgoščena vrednost – dokument nikoli ne zapusti vašega strežnika." +label = "Dodaj zaupanja vreden časovni žig" + [portal.policies.wizard.capability.watermark] desc = "Odtisne viden znak (npr. »Zaupno«) čez vsako stran." label = "Uporabi vodni žig" +[portal.policies.wizard.classification] +description = "Vsak naložen dokument je klasificiran glede na vgrajene oznake in označen z vrstami, ki ustrezajo. Nabor oznak je v skupni rabi celotne ekipe." +labelsHeading = "Klasifikacijske oznake" + [portal.policies.wizard.errors] noTools = "Najprej omogočite vsaj eno orodje v poteku dela." saveFailed = "Pravilnika ni bilo mogoče shraniti. Poskusite znova." @@ -7548,73 +8376,118 @@ setUp = "Nastavi pravilnik {{category}}" [portal.policies.wizard.workflow] description = "Izberite, kaj ta pravilnik naredi z vsakim dokumentom, ki ga obdela." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktivno" -noRule = "Nobeno pravilo še ni uveljavljeno" -subtitle = "Stalne avtomatizacije, skozi katere gre vsak dokument, ne glede na to, kateri cevovod ga obravnava." -title = "Kaj se izvaja na vaših PDF-jih" - [portal.policySummary.action] -comingSoon = "Nadgradite na Enterprise" -configure = "Konfiguriraj" setUp = "Nastavi" -[portal.policySummary.column] -activeRule = "Aktivno pravilo" -policy = "Pravilnik" -status = "Stanje" +[portal.processorFlow] +footnote = "Števci veljajo za zadnjih 24 ur. Hitrost toka je ponazoritvena." +liveBadge = "V živo" +stats = "{{connected}} povezanih · {{processed}} PDF obdelanih" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Ko so pravilniki konfigurirani, se kategorije prikažejo tukaj." -title = "Ni še pravilnikov" +[portal.processorFlow.lens] +ariaLabel = "Način pogleda" +flow = "Tok" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktivno" -off = "Izklopljeno" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Neuspešno" +heading = "Rezultati" +success = "Dostavljeno" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktivnih" +count = "{{n}} · 24 h" +heading = "Pravilniki" +setUp = "Nastavi" soon = "Kmalu" -[portal.processingStatus] -managePlan = "Upravljaj paket" -volumeSuffix = "Obdelani PDF-ji · zadnjih 30 dni" +[portal.processorFlow.sankey] +waist = "Pravilniki · {{n}} aktivnih" + +[portal.processorFlow.sankey.empty] +description = "Povežite vir in vklopite pravilnik, da opazujete tok dokumentov skozi procesor." +title = "Toka še ni" + +[portal.processorFlow.sources] +comingSoonTag = "Poveži" +editor = "Stirling PDF Editor" +heading = "Viri" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Vnos e-pošte" [portal.procurement] -reset = "Ponastavi nabavo (demo)" subtitle = "Poskrbite za evalvacijo, pogodbo in uvajanje svoje ekipe. Vsak dokument na enem mestu." title = "Nabava" -[portal.procurement.action] -download = "Prenesi" -pay = "Plačaj zdaj" -request = "Zahtevaj" -sign = "Preglej in podpiši" -upload = "Naloži" - [portal.procurement.agreement] agreeCta = "Strinjam se in se naročam" +confidential = "Zaupno" confirm = "Prebral/-a sem in se strinjam s pogodbo Stirling Enterprise Agreement." -eyebrow = "Pogodba" -intro = "Ena združena pogodba pokriva vaš dogovor: Master Service Agreement, Order Form, EULA in Data Processing Agreement. Preglejte jo, nato se strinjajte, da sprejmete ponudbo v zavezujočo naročnino." -title = "Preglejte svojo pogodbo Enterprise" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, naročilnica, EULA in DPA, združeni v en podpis." +download = "Prenesi" +downloadDraftError = "PDF pogodbe ni bilo mogoče ustvariti – upodabljalnik dokumentov ni na voljo. Poskusite znova kmalu ali se obrnite na podporo." +downloadError = "Podpisane pogodbe ni bilo mogoče prenesti. Poskusite znova." +legalName = "Ime pravne osebe" +legalNamePlaceholder = "Pravna oseba, ki bo podpisala" +loadError = "Pogodbe ni bilo mogoče naložiti. Poskusite znova." +loading = "Nalaganje pogodbe..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Zahtevaj spremembe" +scrollHint = "Pomaknite se za pregled" +signatory = "Ime podpisnika" +signatoryPlaceholder = "Polno ime" +signatoryTitle = "Naziv podpisnika" +signatoryTitlePlaceholder = "npr. General Counsel" +signError = "Vašega podpisa ni bilo mogoče zabeležiti. Poskusite znova." [portal.procurement.builder] addons = "Dodatki" +addressLine1 = "Naslovna vrstica 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Naslovna vrstica 2" +addressLine2Placeholder = "Suita, nadstropje, stavba" back = "Nazaj" businessName = "Ime podjetja" businessNamePlaceholder = "Vaše podjetje" +city = "Mesto" +cityPlaceholder = "San Francisco" +completeRequired = "Pred ustvarjanjem ponudbe izpolnite zahtevana polja (označena z *) z veljavnim e-poštnim naslovom." +contactEmail = "Kontaktna e-pošta" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Ime kontakta" +contactNamePlaceholder = "Jane Doe" continue = "Nadaljuj" -country = "Država" -countryEuro = "Evroobmočje (EUR €)" -countryUK = "Združeno kraljestvo (GBP £)" -countryUS = "Združene države (USD $)" +done = "Končano" eula = "Prebral/-a sem in se strinjam z Stirling Enterprise EULA. Ta ureja pogodbo, ustvarjeno iz te ponudbe." generate = "Ustvari ponudbo" -included = "Vključeno" indemnification = "Odškodnina za IP" indemnificationSub = "Branimo upravičene zahtevke IP v skladu z EULA" -offlineLicense = "Licenca za offline / air-gapped okolje" -offlineLicenseSub = "Prenosljiva licenčna datoteka za air-gapped samogostujočo instanco" +paperEyebrow = "Podjetniška ponudba" +paperFor = "Pripravljeno za" +pdfSize = "Velikost PDF" +poNumber = "Številka naročilnice" +poNumberPlaceholder = "Neobvezno" +postalCode = "Poštna številka" +postalCodePlaceholder = "94105" +posture = "Upravljanje" +posture_count = "~{{count}} pravilnikov" +posture_essentials = "Osnove" +posture_essentialsSub = "Klasifikacija + deljenje — privzete nastavitve" +posture_governed = "Upravljano" +posture_governedSub = "Doda varnost in usmerjanje" +posture_regulated = "Regulirano" +posture_regulatedSub = "Vsaka kategorija — skladnost, hramba, zajem" qbr = "Četrtletni poslovni pregledi" qbrSub = "Vaš SE vsako četrtletje pregleda uporabo in časovni načrt" +region = "Država / regija" +regionPlaceholder = "California" running = "{{annual}} / leto · {{years}}-letni {{tcv}}" s1Sub = "Vaša ekipa in PDF-ji, ki jih pričakujete obdelati vsako leto." s1Title = "Koliko boste obdelali?" @@ -7623,6 +8496,12 @@ s2Title = "Zaveza in storitev" s3Sub = "Za ponudbo in pogodbo, ki jo ustvari." s3Title = "Vaši podatki" serviceLevel = "Raven storitve" +size_compact = "Kompaktno" +size_compactSub = "Večinoma besedilo, pod 1 MB" +size_heavy = "Težko" +size_heavySub = "Skenirano ali z veliko slikami, 5 MB+" +size_standard = "Standardno" +size_standardSub = "Mešano besedilo in slike, 1 do 5 MB" slDedicated = "Namenska" slDedicatedSub = "4 delovne ure · namenski skrbnik računa · +30%" slPriority = "Prednostna" @@ -7630,6 +8509,8 @@ slPrioritySub = "Isti delovni dan · imenovani CSM · +15%" slStandard = "Standardna" slStandardSub = "Naslednji delovni dan · skupni CSM · vključeno" stepOf = "Korak {{n}} od {{total}}" +taxId = "DDV / davčna ID" +taxIdPlaceholder = "Neobvezno" term = "Obdobje" termDiscount = "Uporabljen {{pct}}% popust za večletno zavezo" title = "Sestavite svojo ponudbo" @@ -7637,6 +8518,7 @@ training = "Uvajanje in usposabljanje" trainingSub = "Seje v živo, da vaša ekipa začne delati" users = "Skupno uporabnikov" usersPlaceholder = "npr. 250" +viewEula = "Preberi" volEstimated = "Ocenjeno iz {{count}} uporabnikov (~2.000 PDF-jev vsak, vključno z avtomatizacijo). Uredite, če veste bolje." volManual = "Uporablja se vaša številka. Kadar koli znova ocenite glede na velikost ekipe." volNoUsers = "Niste prepričani? Vnesite velikost ekipe in ocenili bomo." @@ -7645,55 +8527,55 @@ volumePlaceholder = "npr. 1.000.000" years_one = "{{count}} leto" years_other = "{{count}} let" -[portal.procurement.docs] -count_one = "{{count}} dokument" -count_other = "{{count}} dokumentov" -done = "Končano" -here = "Tukaj ste" -hide = "Skrij" -optional = "Izbirno" -paidAddon = "Plačljiv dodatek" -show = "Prikaži" -subtitle = "Vse, kar potrebujete na vsakem koraku poti, prikazano, ko se dogovor premika naprej." -supportingSubtitle = "SOC 2, varnostni pregledi, davčni obrazci in več, pripravljeno, ko vaša varnostna ali nabavna ekipa vpraša. Nekateri imajo enkratno pristojbino." -supportingTitle = "Podpora vaši evalvaciji" +[portal.procurement.documents] +agreement = "Podjetniška pogodba" +agreementSub = "MSA, naročilnica in DPA v enem podpisu." +download = "Prenesi" +eula = "EULA in komercialni pogoji" +eulaSub = "Programska oprema in samopostrežni komercialni pogoji." +invoice = "Račun" +invoiceSub = "Vaš prvi naročniški račun." +laterInvoice = "Na voljo po sprejemu" +laterQuote = "Na voljo, ko je vaša ponudba izdana" +quote = "Ponudba" +quoteSub = "Vaša razčlenjena podjetniška ponudba." +sla = "Priloga SLA" +slaSub = "Ravni podpore in ciljni odzivni časi." +subprocessors = "Podobdelovalci" +subprocessorsSub = "Tretje osebe, ki obdelujejo podatke v vašem imenu." +subtitle = "Vaša dokumentacija posla, na voljo kadar koli." title = "Dokumenti" -upcoming = "Prihajajoče" +view = "Ogled" [portal.procurement.error] title = "Nekaj je šlo narobe" [portal.procurement.hero] -company = "Vaš Enterprise dogovor" +barAria = "Faza {{current}} od {{total}}" ctaAgreement = "Preglej in podpiši pogodbo" -ctaLive = "Ste v živo" -ctaPayment = "Dodaj plačilo" +ctaExploring = "Nastavite preskus" ctaQuote = "Preglejte svojo ponudbo" +ctaReviewQuote = "Preglej ponudbo" ctaTrial = "Sestavite svojo ponudbo" +documents = "Dokumenti" eyebrow = "Enterprise nabava" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Povabi člane ekipe" -keyDocs = "Ključni dokumenti" -nextStep = "Naslednji korak: {{action}}" -notStarted = "Ni začeto" +licenseKey = "Licenčni ključ" +liveSub = "Vaša organizacija je pripravljena in vaša licenca je aktivna." +liveTitle = "Stirling Enterprise je pri vas v živo" +next = "Naslednje: {{stage}}" open = "Odpri nabavo" scheduleCall = "Načrtuj klic" -setup1Sub = "Povabite člane ekipe" -setup1Title = "Uvedite PDF Editor" -setup2Sub = "Vklopite obdelavo v urejevalnikih in drugih virih" -setup2Title = "Povežite PDF Processor" -setup3Sub = "Vklopite varnost, skladnost, usmerjanje ali hrambo, ko jih potrebujete" -setup3Title = "Dodajte priporočene pravilnike" +sentenceAgreement = "pregled in podpis pogodbe" +sentenceLive = "zagotavljanje in uvedba" +sentencePayment = "naročilnica in plačilo" +sentenceQuote = "pregled in sprejem vaše ponudbe" +sentenceTrial = "nastavitev preskusnega delovnega prostora" [portal.procurement.journey] daysLeft_one = "Preostane {{count}} dan" daysLeft_other = "Preostane {{count}} dni" -engineerLabel = "Vaš inženir rešitev" -eyebrow = "Vaša uvedba" -live = "V Stirling Enterprise ste v živo" -nextStep = "Naslednji korak: {{action}}" -subtitle = "Vaš inženir rešitev je prisoten pri vsakem koraku. Eno naslednje dejanje naenkrat; celoten kontrolni seznam je spodaj." -title = "Od preizkusa do produkcije, ena vodena pot" -trialTitle = "Enterprise preizkus" [portal.procurement.journeySteps.agreement] blurb = "En podpis pokriva MSA, naročilnico, EULA in DPA." @@ -7727,6 +8609,9 @@ downloadError = "Datoteke offline licence trenutno še ni bilo mogoče ustvariti downloadOffline = "Prenesi offline licenco (.lic)" hint = "Prilepite ta ključ v samogostujočo instanco, da jo aktivirate, ali ga shranite za svojo evidenco. Varno ga hranite." label = "Vaš licenčni ključ" +subtitle = "S tem ključem aktivirajte samostojno gostovano instanco ali ga shranite za svoje evidence." +title = "Vaš licenčni ključ" +trialFileHint = "To je vaša preskusna licenčna datoteka. Ko je vaša pogodba sklenjena, se vrnite in jo znova prenesite — za razliko od spletnega licenčnega ključa se datoteka .lic ne posodobi sama." [portal.procurement.link] cta = "Poveži račun" @@ -7739,65 +8624,62 @@ description = "Vaša naročnina je aktivna in vaša licenca je izdana. Vaša eki eyebrow = "V živo" title = "V Stirling Enterprise ste v živo" -[portal.procurement.locked] -description = "Preizkusni ključi, ponudbe z zavezanim obsegom, pogodba z enim podpisom, plačilo in vaša knjiga dokumentov so tukaj, ko začnete Enterprise evalvacijo." -eyebrow = "Samo Enterprise" -talkToSales = "Pogovorite se s prodajo" -title = "Nabavna pot se odpre z Enterprise" - [portal.procurement.milestone] -accept = "Sprejmi in nadaljuj" -description = "Prenesite PDF, da ga delite s svojo ekipo, vrnite se in sprejmite, ko boste pripravljeni, ali naredite spremembe." -download = "Prenesi PDF" downloadError = "Ponudbenega PDF-ja trenutno še ni bilo mogoče prenesti — poskusite znova čez trenutek." -edit = "Uredi ponudbo" -eyebrow = "Ponudba {{number}}" -perYear = " / leto" -preparedFor = "Pripravljeno za {{company}}" -tcv = "{{value}} skupna pogodbena vrednost" -title = "Vaša ponudba je pripravljena" - -[portal.procurement.modal] -cancel = "Prekliči" -chooseFile = "Izberi datoteko" -close = "Zapri" -downloadBody = "Vaš prenos se bo kmalu začel." -downloadCta = "Prenesi" -downloadTitle = "Prenos" -noFile = "Ni izbrane datoteke" -payBody = "Plačajte svojo zavezano pogodbo s kartico ali bančnim nakazilom prek Stripe. Vaš delovni prostor se pripravi takoj, ko je plačilo poravnano." -payCta = "Nadaljuj na Stripe" -payTitle = "Potrdi plačilo" -requestBodyFree = "To ustvarimo na zahtevo. Potrdite in vaš inženir rešitev ga bo kmalu poslal." -requestBodyPaid = "To je plačljiv dodatek. Potrdite in vaš inženir rešitev bo določil obseg ter poslal dokumentacijo." -requestCta = "Zahtevaj" -requestTitle = "Zahtevaj ta dokument" -signBody = "Odpre Stirling Enterprise Agreement za e-podpis: en podpis pokriva MSA, naročilnico, EULA in DPA. Samodejno sopodpišemo in napredujete na plačilo." -signCta = "Odpri za podpis" -signTitle = "Preglejte in podpišite svojo pogodbo" -uploadBody = "Pošljite nam svojo PO in izdali bomo račun po vaših pogojih. Povlecite PDF ali spodaj izberite datoteko." -uploadCta = "Naloži naročilnico" -uploadTitle = "Naložite svojo naročilnico" [portal.procurement.payment] description = "Vaša ponudba je sprejeta in vaša licenca je že aktivna — vaša ekipa lahko začne takoj. Prvi račun plačajte, ko boste pripravljeni; tukaj ga lahko plačate ali prenesete, brez e-pošte." +downloadAgreement = "Prenesi podpisano pogodbo" downloadInvoice = "Prenesi račun" -simulate = "Simuliraj prejeto plačilo (demo)" +eyebrow = "Plačilo" title = "Naročnina ustvarjena" viewInvoice = "Ogled in plačilo računa" +[portal.procurement.review] +acceptCta = "Sprejmi ponudbo" +annual = "Letna pristojbina (1. leto)" +downloadCta = "Prenesi ponudbo" +poNumber = "Naročilnica: {{po}}" +renewal = "Obnovi se po {{amount}}/leto po izteku obdobja (+{{pct}}% CPI)" +tcv = "{{years}}-letno obdobje, plačano v celoti vnaprej: {{tcv}}" +validUntil = "Veljavno do {{date}}" + [portal.procurement.schedule] fallback = "Razporejevalnika ni bilo mogoče naložiti." fallbackLink = "Odpri načrtovanje v novem zavihku" +loading = "Nalaganje razporejevalnika..." subtitle = "Vaš inženir rešitev bo vašo ekipo vodil skozi uvedbo. Izberite čas, ki vam ustreza." title = "Načrtuj klic" -[portal.procurement.status] -action = "Potrebno dejanje" -available = "Na voljo" -complete = "Dokončano" -pending = "V teku" -request = "Na zahtevo" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Zaprto, brez odhodnih povezav" +back = "Nazaj" +businessName = "Ime podjetja" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Upravljano, najhitrejši začetek" +continue = "Nadaljuj" +deployment = "Kam naj se uvede?" +eula = "Strinjam se s Stirling EULA in komercialnimi pogoji." +fullName = "Polno ime" +fullNamePlaceholder = "Vaše ime" +invites = "Povabi sodelavce · neobvezno" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Načrtuj klic" +seats = "Osebe, ki testirajo" +seatsPlaceholder = "npr. 15" +selfhost = "Samostojno gostovano" +selfhostSub = "Docker ali Kubernetes" +start = "Začni preskus" +stepOf = "Korak {{n}} od {{total}}" +subtitle = "Brezplačno 14 dni, kartica ni potrebna." +subtitleDetails = "Nekaj podrobnosti za vašo ponudbo in pogodbo." +talkFirst = "Se raje najprej pogovorite?" +title = "Nastavite preskus" +viewEula = "Preberi" +workEmail = "Službena e-pošta" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Podaljšanje doda 7 dni in obvesti vašega inženirja rešitev." @@ -7808,109 +8690,113 @@ maxed = "Dosežena omejitev" subtitle = "Vaš brezplačni preizkus traja do {{date}}. Kartica ni potrebna." title = "Enterprise preizkus" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Cene za zavezani obseg, SSO + SCIM + RBAC za celotno organizacijo, 90-dnevna nespremenljiva revizija in namenski SE." -homeCta = "Začni preizkus →" -homeHeadline = "Obdelajte milijone PDF-jev." - -[portal.recentActivity] -title = "Nedavna dejavnost" -viewAll = "Prikaži vse" - -[portal.recentActivity.empty] -description = "Zagoni cevovodov, uvedbe in dogodki agentov se bodo prikazali tukaj." -title = "Tukaj še ni ničesar" - -[portal.search] -ariaLabel = "Iskanje" -placeholder = "Išči Stirling — endpointi, cevovodi, dokumenti…" - -[portal.search.empty] -noActionsDescription = "Hitra dejanja se bodo prikazala tukaj, ko bodo na voljo." -noActionsTitle = "Ni hitrih dejanj" -noMatches = "Ni zadetkov za \"{{query}}\"" -noMatchesDescription = "Poskusite z drugo ključno besedo ali prebrskajte katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Povezava računa" -[portal.shell.header] -accountFallback = "Račun" -accountMenu = "Meni računa" -darkMode = "Temni način" -lightMode = "Svetli način" -search = "Iskanje" -searchPlaceholder = "Išči…" -signOut = "Odjava" -switchToDark = "Preklopi na temno temo" -switchToLight = "Preklopi na svetlo temo" - [portal.shell.sidebar] appEditor = "Urejevalnik" appProcessor = "Procesor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dokumentov" linkAccount = "Poveži račun Stirling" -planEditor = "Paket Editor" -planEnterprise = "Paket Enterprise" -planProcessor = "Paket Processor" primaryNav = "Primarna navigacija" switchApp = "Preklopi aplikacijo" +[portal.shell.topbar] +closeNav = "Zapri navigacijo" +openNav = "Odpri navigacijo" +search = "Išči" + [portal.sources] subtitle = "Ponovno uporabne vhodne povezave, ki dovajajo dokumente v Stirling. Povezavo konfigurirajte enkrat, nato se nanjo sklicujte iz poljubnega števila pravilnikov. Kliknite vrstico za njeno konfiguracijo in pravilnike, ki jo uporabljajo." title = "Viri" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Poveži vir" +[portal.sources.builder] +back = "Nazaj na vire" +backToTypes = "Vse vrste virov" +cancel = "Prekliči" +chooseHint = "Izberite, od kod prihajajo dokumenti. Zasivljeni konektorji so na poti." +comingSoon = "Kmalu na voljo" +comingSoonHeading = "Kmalu na voljo" +create = "Ustvari vir" +createTitle = "Poveži vir" +delete = "Izbriši" +editTitle = "Uredi vir" +enabled = "Omogočeno" +save = "Shrani spremembe" + +[portal.sources.builder.folderAccess] +description = "Avtomatizacije map lahko uporabljajo samo mape, ki jih je dovolil skrbnik. Dodajte jo v nastavitvah dostopa do map, nato poskusite znova." +openSettings = "Nastavitve dostopa do map" +title = "Ta mapa ni dovoljena" + [portal.sources.delete] body = "Izbrisati \"{{name}}\"? Tega ni mogoče razveljaviti. Pravilnike, ki se nanj sklicujejo, bo treba posodobiti." cancel = "Prekliči" confirm = "Izbriši" title = "Izbriši vir?" -[portal.sources.detail] -closeAriaLabel = "Zapri podrobnosti" -delete = "Izbriši vir" -docs24h = "Zadnjih 24 h" -docs30d = "Zadnjih 30 dni" -docsTotal = "Skupno videnih" -docsTrend = "Dokumenti v zadnjih 30 dneh" -documents = "Dokumenti" -edit = "Uredi" -notReferenced = "Nanj se ne sklicuje noben pravilnik, zato ga je varno izbrisati." -pause = "Začasno ustavi" -resume = "Nadaljuj" -subtitle = "{{type}} · {{status}}" -usedBy = "Uporablja" - -[portal.sources.empty] -description = "Povežite mapo (in kmalu shrambo v oblaku), da bodo vaši pravilniki imeli od kod pridobivati dokumente." -title = "Ni še povezanih virov" - [portal.sources.kpi] inUse = "V uporabi" total = "Povezave" unused = "Neuporabljeno" +[portal.sources.networkFields.connection] +helperText = "Shranjena povezava z naslovom strežnika in poverilnicami. Ponovno jo uporabi vsak vir, ki se nanjo sklicuje." +label = "Povezava" + +[portal.sources.networkFields.directory] +helperText = "Mapa na strežniku za preverjanje, relativno glede na domačo mapo prijave ali koren skupne rabe. Pustite prazno za koren." +label = "Mapa" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Poraba odstrani vsako datoteko s strežnika, ko jo obdela vsak pravilnik." +label = "Način branja" + +[portal.sources.networkFields.mode.options] +consume = "Poraba: vsako datoteko obdelaj enkrat" +snapshot = "Posnetek: ob vsakem zagonu ponovno preberi mapo" + +[portal.sources.networkFields.recursive] +label = "Globina mape" + +[portal.sources.networkFields.recursive.options] +all = "Vključi podmape" +top = "Samo najvišja raven" + [portal.sources.status] active = "Aktivno" disabled = "Onemogočeno" unused = "Neuporabljeno" [portal.sources.table] +documents = "Dokumenti" source = "Vir" status = "Stanje" +type = "Type" usedBy = "Pravilniki" +[portal.sources.types.box] +description = "Povlecite dokumente iz map Box, ki jih vaša ekipa že deli." +label = "Box" + +[portal.sources.types.dropbox] +description = "Povlecite dokumente iz deljene mape Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenti, ki jih je vaša ekipa obdelala v urejevalniku, prek pravilnikov in AI zagonov." label = "Urejevalnik" +noPolicies = "Iz urejevalnika še ni bil zagnan noben pravilnik." + +[portal.sources.types.email] +description = "Spremenite priloge, poslane v namenski nabiralnik, v dokumente." +label = "E-poštni nabiralnik" [portal.sources.types.folder] description = "Spremljajte imenik na strežniku za nove dokumente." @@ -7943,25 +8829,80 @@ label = "Globina mape" all = "Vključi podmape" top = "Samo zgornja raven" +[portal.sources.types.ftp] +description = "Preverjajte mapo strežnika FTP ali FTPS za nove dokumente." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Povlecite dokumente iz deljenih map Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Spremljajte omrežno skupno rabo SMB za nove dokumente." +label = "Omrežni pogon" + +[portal.sources.types.onedrive] +description = "Spremljajte mape OneDrive za nove dokumente." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Povlecite dokumente iz vedra Amazon S3 ali S3-združljivega vedra." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Shranjena povezava, ki vsebuje vedro in poverilnice. Ponovno jo uporabi vsak vir in izhod cevovoda, ki se nanjo sklicuje." +label = "Povezava" + +[portal.sources.types.s3.fields.mode] +helperText = "Poraba odstrani vsak objekt iz vedra, ko ga obdela vsak pravilnik." +label = "Način branja" + +[portal.sources.types.s3.fields.mode.options] +consume = "Poraba: vsak objekt obdelaj enkrat" +snapshot = "Posnetek: ob vsakem zagonu ponovno preberi vedro" + +[portal.sources.types.s3.fields.prefix] +helperText = "Obdelajo se samo objekti, katerih ključi se začnejo s to predpono." +label = "Predpona ključa" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Preverjajte odložno mapo SFTP za nove dokumente." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Povlecite dokumente iz knjižnic dokumentov SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Vir" +[portal.sources.types.webhook] +createNote = "URL za dostavo in podpisna skrivnost se ustvarita, ko ustvarite ta webhook. Pošiljatelji pošljejo dokumente s POST na ta URL, sklicujoči se pravilniki pa se zaženejo ob prispetju." +description = "Prejmite dokumente s podpisanim HTTP POST iz katerega koli zunanjega sistema." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL za dostavo" +secretNote = "Podpisna skrivnost je prikazana enkrat, ko je webhook ustvarjen. Če jo želite zamenjati, ponovno ustvarite vir." + +[portal.sources.types.webhook.reveal] +copy = "Kopiraj" +done = "Končano" +secret = "Podpisna skrivnost" +secretHelp = "Podpišite surovo telo vsake dostave s tem ključem (HMAC-SHA256) in ga pošljite kot glavo X-Stirling-Signature." +secretWarning = "Zdaj kopirajte podpisno skrivnost. Zaradi vaše varnosti je prikazana samo enkrat in je pozneje ni mogoče pridobiti." +title = "Webhook ustvarjen" +url = "URL za dostavo" +usage = "Pošljite vsak dokument s POST kot surovo telo zahteve na URL za dostavo z binarno vrsto vsebine (application/pdf ali application/octet-stream). Sklicujoči se pravilniki se samodejno zaženejo ob prispetju." + [portal.sources.wizard] -back = "Nazaj" -cancel = "Prekliči" -continue = "Nadaljuj" -editTitle = "Uredi vir" name = "Ime" namePlaceholder = "npr. Sprejem zahtevkov" -save = "Shrani spremembe" -subtitle = "Korak {{current}} od {{total}} · {{label}}" -title = "Poveži vir" type = "Vrsta" -[portal.sources.wizard.steps] -chooseType = "Izberi vrsto" -configure = "Konfiguriraj" -review = "Preglej in poveži" +[portal.stepModal] +close = "Zapri" [portal.usage] managePayment = "Upravljaj plačilo" @@ -8021,16 +8962,9 @@ summary = "Ima ekipo — upravlja vire njenih članov in deljene konfiguracije." 2 = "Dostop do portala prek privzetega pravilnika" 3 = "Vse, kar lahko naredi član" -[portal.welcome] -ariaLabel = "Dobrodošli v Stirling PDF" -badge = "Open-source" -installEditor = "Namesti urejevalnik" -inviteTeammates = "Povabi člane ekipe" -openInBrowser = "Odpri v brskalniku" -perks = "Za vedno brezplačno · Samogostovanje možno" -subtitle = "Najvarnejši PDF Editor na svetu je brezplačen za ekipe vseh velikosti. Vključuje več kot 60 PDF operacij in SSO." -title = "Dobrodošli v" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Neomejeno" [printFile] title = "Natisni datoteko" @@ -8613,6 +9547,14 @@ text = "Zavrtite strani PDF v smeri urinega kazalca ali nasprotno v 90-stopinjsk [rotate.tooltip.header] title = "Pregled nastavitev vrtenja" +[ruler] +clearAll = "Počisti vse" +hideAllLabels = "Skrij vse oznake" +hideSmallLabels = "Skrij majhne oznake" +physicalValues = "Fizično" +scaled = "Skalirano" +showAllLabels = "Prikaži vse oznake" + [sanitize] desc = "Odstranite potencialno škodljive elemente iz datotek PDF." sanitizationResults = "Rezultati saniranja" @@ -8654,31 +9596,45 @@ label = "Odstrani metapodatke XMP" [sanitize.steps] settings = "Nastavitve" -[sanitizePdf] -tags = "očisti,varno,varnost,odstrani-grožnje" - [sanitizePDF] title = "Prečisti PDF" +[sanitizePdf] +tags = "očisti,varno,varnost,odstrani-grožnje" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Prilagodi velikost strani" +[scaleSettings] +activeScale = "Aktivno merilo" +apply = "Uporabi merilo" +applyCalibration = "Uporabi umerjanje" +calculatedScale = "Izračunano merilo" +calibrate = "Umeri" +calibrating = "Umerjanje" +calibrationDistanceRequired = "Vnesite dejansko razdaljo, večjo od nič" +calibrationInvalid = "Iz te meritve ni mogoče izračunati merila" +calibrationPaperDistance = "Izmerjena razdalja na strani: {{distance}}" +calibrationTitle = "Umeri merilo" +calibrationTooltip = "Izmerite znano razdaljo za samodejni izračun merila" +customScale = "Merilo po meri" +noneSet = "Merilo po meri ni nastavljeno" +presets = "Prednastavljena merila" +ratio = "Razmerje merila" +ratioHelp = "Razmerje: 1 enota strani = X dejanskih enot" +ratioPlaceholder = "npr. 100" +ratioPositive = "Razmerje mora biti večje od nič" +ratioRequired = "Razmerje je obvezno" +realDistance = "Dejanska razdalja" +realDistancePlaceholder = "npr. 5" +reset = "Ponastavi na privzeto" +unit = "Enota" +unitInvalid = "Neveljavna enota: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Prag kota:" -10 = "Nastavi velikost dodane in odstranjene obrobe, da prepreči bele obrobe v izpisu (privzeto: 1)." -2 = "Nastavi najmanjši absolutni kot, potreben za vrtenje slike (privzeto: 10)." -3 = "Toleranca:" -4 = "Določi razpon barvne variacije okoli ocenjene barve ozadja (privzeto: 30)." -5 = "Najmanjša površina:" -6 = "Nastavi minimalni prag površine za fotografijo (privzeto: 10000)." -7 = "Najmanjša konturna površina:" -8 = "Nastavi najmanjši prag konturne površine za fotografijo" -9 = "Velikost obrobe:" - [scannerImageSplit] submit = "Izvleci skenirane slike" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Kaj počne" whatThisDoesDesc = "Samodejno najde in izvleče vsako fotografijo s skenirane strani ali sestavljene slike — brez ročnega obrezovanja." whenToUse = "Kdaj uporabiti" +[ScannerImageSplit.selectText] +1 = "Prag kota:" +10 = "Nastavi velikost dodane in odstranjene obrobe, da prepreči bele obrobe v izpisu (privzeto: 1)." +2 = "Nastavi najmanjši absolutni kot, potreben za vrtenje slike (privzeto: 10)." +3 = "Toleranca:" +4 = "Določi razpon barvne variacije okoli ocenjene barve ozadja (privzeto: 30)." +5 = "Najmanjša površina:" +6 = "Nastavi minimalni prag površine za fotografijo (privzeto: 10000)." +7 = "Najmanjša konturna površina:" +8 = "Nastavi najmanjši prag konturne površine za fotografijo" +9 = "Velikost obrobe:" + [search] noResults = "Ni zadetkov" placeholder = "Vnesite iskani izraz..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Vaš strežnik Stirling-PDF je brez povezave in \"{{e [session] expired = "Vaša seja je potekla. Osvežite stran in poskusite znova." -[sessionManagement.tooltip] -header = "Upravljanje sej podpisovanja" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Novi udeleženci se dodajo na konec vrstnega reda podpisovanja" -bullet2 = "Udeležencev ni mogoče dodajati po zaključku seje" -bullet3 = "Vsak udeleženec prejme obvestilo, ko je na vrsti" -description = "Dodatne udeležence lahko dodate kadar koli pred zaključkom aktivne seje." -title = "Dodajanje udeležencev" - -[sessionManagement.tooltip.finalization] -bullet1 = "Poln zaključek: Vsi udeleženci so podpisali" -bullet2 = "Delni zaključek: Nekateri udeleženci še niso podpisali" -bullet3 = "Nepodpisani udeleženci bodo izključeni iz končnega dokumenta" -bullet4 = "Ko je zaključeno, lahko naložite podpisan PDF v aktivne datoteke" -description = "Zaključek združi vse podpise v en sam podpisan PDF. Tega dejanja ni mogoče razveljaviti." -title = "Zaključek seje" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Udeležencev, ki so že podpisali, ni mogoče odstraniti" -bullet2 = "Odstranjeni udeleženci ne bodo več prejemali obvestil" -bullet3 = "Vrstni red podpisovanja se samodejno prilagodi" -description = "Udeležence je mogoče odstraniti iz sej pred njihovim podpisom." -title = "Odstranjevanje udeležencev" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Vsak podpis se uporabi zaporedno na PDF" -bullet2 = "Poznejši podpisniki lahko vidijo prejšnje podpise" -bullet3 = "Ključno za potrditvene poteke in pravno sledljivost" -description = "Vrstni red, ki ga določite ob ustvarjanju seje, določa, kdo podpisuje prvi." -title = "Vrstni red podpisov" - [settings] close = "Zapri" +[settings.ai] +documents = "Dokumenti in RAG" +general = "Splošno" +limits = "Omejitve in zmogljivost" +models = "Modeli in ponudniki" +title = "AI" + [settings.configuration] advanced = "Napredno" database = "Podatkovna baza" endpoints = "Končne točke" features = "Funkcije" +folderAccess = "Dostop do map" mcp = "MCP Server" storageSharing = "Shramba datotek in skupna raba" systemSettings = "Sistemske nastavitve" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Privzeti urejevalnik PDF" defaultPdfEditorActive = "Stirling PDF je vaš privzeti urejevalnik PDF" defaultPdfEditorChecking = "Preverjanje..." defaultPdfEditorInactive = "Druga aplikacija je nastavljena kot privzeta" +defaultPdfEditorRemind = "Opomni me, naj nastavim kot privzeto" +defaultPdfEditorRemindDescription = "Prikaži pasico, ko Stirling PDF ni vaša privzeta aplikacija za PDF." defaultPdfEditorSet = "Že privzeto" defaultStartupView = "Privzet pogled ob zagonu" defaultStartupViewDescription = "Izberite, kateri zavihek je aktiven v levem stolpcu ob zagonu aplikacije" @@ -8817,9 +9763,18 @@ languageDescription = "Izberite jezik prikaza" logout = "Odjava" setAsDefault = "Nastavi kot privzeto" theme = "Tema" +themeAccent = "Poudarjena barva" +themeAccentCustom = "Barva po meri" +themeAccentCustomHint = "Po meri (priporočene privzete nastavitve)" +themeAccentDark = "Poudarjena barva v temnem načinu" +themeAccentDefault = "Privzeto" +themeAccentDefaultHint = "Privzeta tema (priporočeno)" +themeAccentDescription = "Gumbi, povezave in poudarki ji sledijo, aplikacijo pa subtilno obarva. Svetli in temni način imata vsak svojo. Sistem uporabi tisto, ki je aktivna." +themeAccentLight = "Poudarjena barva v svetlem načinu" themeDark = "Temna" themeDescription = "Preklapljajte med svetlim in temnim načinom" themeLight = "Svetla" +themeReset = "Obnovi privzeto temo" themeSystem = "System" title = "Splošno" user = "Uporabnik" @@ -8833,6 +9788,12 @@ intro = "Omogočite preverjanje pristnosti uporabnikov, upravljanje ekip in funk learnMore = "Več v dokumentaciji" title = "Za skrbnike sistema" +[settings.general.loginLanding] +description = "Izberite, katera aplikacija se odpre, ko se prijavite v Stirling Cloud." +editor = "Urejevalnik" +processor = "Obdelovalnik" +title = "Po prijavi" + [settings.general.mode] fullscreen = "Celozaslonski" sidebar = "Stranska vrstica" @@ -9013,9 +9974,6 @@ title = "Politike in zasebnost" [settings.preferences] title = "Nastavitve" -[settings.search] -placeholder = "Išči po straneh nastavitev..." - [settings.security] description = "Posodobite geslo, da ostane vaš račun varen." title = "Varnost" @@ -9041,6 +9999,11 @@ security = "Varnost" telegram = "Telegram" title = "Varnost in avtentikacija" +[settings.signOut] +confirm = "Ali ste prepričani, da se želite odjaviti?" +submit = "Odjava" +title = "Odjava" + [settings.team] title = "Ekipa" @@ -9156,6 +10119,7 @@ last = "Zadnja stran" maintainRatio = "Preklopi ohranjanje razmerja stranic" next = "Naslednja stran" noSavedSigs = "Ni shranjenih podpisov" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Osebni podpisi" previous = "Prejšnja stran" redo = "Ponovi" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Odstranjevanje ozadja ni uspelo" hint = "Naložite sliko podpisa v PNG ali JPG" label = "Naložite sliko podpisa" placeholder = "Izberite slikovno datoteko" +previewAlt = "Current image signature" processing = "Obdelava slike..." removeBackground = "Odstrani belo ozadje (naredi prozorno)" @@ -9207,6 +10172,17 @@ saved = "Zgoraj izberite shranjen podpis, nato kliknite kjer koli na PDF-ju, da text = "Ko zgoraj vnesete svoje ime, kliknite kjerkoli v PDF, da postavite podpis." title = "Kako dodati podpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Premakni podpis" pause = "Začasno ustavi postavljanje" @@ -9653,10 +10629,30 @@ title = "Naloži na strežnik" updateButton = "Posodobi na strežniku" uploadButton = "Naloži na strežnik" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kliknite tukaj" +fallback = "Če se ni odprlo samodejno," +opening = "Odpiranje Swagger UI v novem zavihku..." tags = "api,dokumentacija,swagger,končne-točke,razvoj" title = "Dokumentacija API" @@ -9764,12 +10760,39 @@ selectFilesHint = "V razdelku Aktivne datoteke izberite datoteke za zagon tega o singleFileScope = "Se uporablja samo za: {{fileName}}" viewerMode = "Preklopite v urejevalnik datotek, da izberete več datotek." +[toolFormat] +ANY = "katera koli datoteka" +CBR = "arhiv stripov (CBR)" +CBZ = "arhiv stripov (CBZ)" +CSV = "datoteka CSV" +EBOOK = "e-knjiga" +EMAIL = "e-pošta" +EXCEL = "preglednica" +HTML = "spletna stran" +IMAGE = "slike" +JAVASCRIPT = "datoteka JavaScript" +JSON = "datoteka JSON" +MARKDOWN = "datoteka Markdown" +NONE = "brez datoteke" +PCL = "datoteka za tiskanje PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF, zaščiten z geslom" +POSTSCRIPT = "datoteka PostScript" +PPT = "predstavitev" +TEXT = "besedilna datoteka" +VIDEO = "video" +WORD = "dokument Word" +XML = "datoteka XML" +XPS = "dokument XPS" +ZIP = "arhiv ZIP" + [toolPanel] alpha = "Alfa" backToAllTools = "Nazaj na vsa orodja" collapse = "Strni ploščo" expand = "Razširi ploščo" goBack = "Pojdi nazaj" +pdfTools = "Orodja PDF" placeholder = "Izberite orodje za začetek" premiumFeature = "Premium funkcija:" toolsHeader = "Orodja" @@ -9806,11 +10829,6 @@ noToolsFound = "Ni najdenih orodij" quickAccess = "HITER DOSTOP" searchPlaceholder = "Išči orodja..." -[toolPicker.categories] -advancedTools = "Napredna orodja" -recommendedTools = "Priporočena orodja" -standardTools = "Standardna orodja" - [toolPicker.subcategories] advancedFormatting = "Napredno oblikovanje" automation = "Avtomatizacija" @@ -9824,6 +10842,9 @@ removal = "Odstranjevanje" signing = "Podpisovanje" verification = "Preverjanje" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Ni najdenih orodij" noTools = "Ni razpoložljivih orodij" @@ -9950,28 +10971,53 @@ title = "Uporabniki" you = "(vi)" [users.action] +cancelInvite = "Prekliči povabilo" deleteTeam = "Izbriši ekipo" disableMfa = "Ponastavi MFA" move = "Premakni v ekipo" reinstate = "Ponovno vzpostavi" remove = "Odstrani iz organizacije" +removeTeam = "Odstrani iz ekipe" rename = "Preimenuj ekipo" resetPw = "Ponastavi geslo" suspend = "Suspendiraj" unlock = "Odkleni račun" +[users.activity] +daysAgo = "pred {{count}} d" +hoursAgo = "pred {{count}} h" +justNow = "Pravkar" +minutesAgo = "pred {{count}} min" +monthsAgo_one = "pred {{count}} mesecem" +monthsAgo_other = "pred {{count}} meseci" +never = "Nikoli" +weeksAgo_one = "pred {{count}} tednom" +weeksAgo_other = "pred {{count}} tedni" +yearsAgo_one = "pred {{count}} letom" +yearsAgo_other = "pred {{count}} leti" + [users.cap] addProcessor = "+ Processor" approver = "Odobri pravilnik" editor = "Urejevalnik" processor = "Procesor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Prekličem povabilo za {{email}}? S trenutno povezavo se ne bodo mogli pridružiti." +cancelInviteTitle = "Prekliči povabilo" deleteTeamBody = "Izbrisati ekipo {{name}}? Ekipa mora biti najprej prazna - premaknite njene člane v drugo ekipo in ne sme več imeti nobenih integracijskih konfiguracij." deleteTeamTitle = "Izbriši ekipo" disableMfaBody = "Odstraniti vpis MFA za {{name}}? Če je zahtevano, ga bodo ob naslednji prijavi znova nastavili." disableMfaTitle = "Ponastavi MFA" removeBody = "Trajno odstraniti {{name}} iz organizacije? Tega ni mogoče razveljaviti." +removeTeamBody = "Odstranim {{name}} iz ekipe? Ohranili bodo svoj račun, vendar bodo izgubili dostop do virov te ekipe." removeTitle = "Odstrani člana" [users.empty] @@ -9982,10 +11028,8 @@ title = "Ni še članov" addToTeam = "Dodaj v ekipo" guestCount = "{{count}} gost" guests = "Gostje" -guestsDesc = "Zunanji sodelavci, omejeni na to, kar ste delili. Samo urejevalnik." ledBy = "vodi {{owner}}" org = "Organizacija" -orgDesc = "Lastniki z avtoriteto za celotno organizacijo in odobritvijo pravilnikov" owners = "{{count}} lastnik" team = "Ekipa {{name}}" teamMeta = "{{count}} oseb" @@ -10022,6 +11066,18 @@ username = "Uporabniško ime" usernameError = "Uporabniško ime mora imeti vsaj 3 znake" usernamePlaceholder = "jsmith" +[users.invites] +by = "Povabil/-a {{who}}" +cancel = "Prekliči" +expiresInDays_one = "Poteče čez {{count}} dan" +expiresInDays_other = "Poteče čez {{count}} dni" +expiresToday = "Poteče danes" +title = "Povabila na čakanju" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Pri dostopu do zalednega sistema je prišlo do napake ali pa nimate dostopa. Poskusite znova." title = "Članov ni bilo mogoče naložiti" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Išči priloge" title = "Priloge" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Naslov zaznamka" +bookmarkTitleRequired = "Naslov zaznamka je obvezen" closeSidebar = "Zapri stransko vrstico zaznamkov" collapseAll = "Strni vse zaznamke" +empty = "No bookmarks in this document" expandAll = "Razširi vse zaznamke" +searchPlaceholder = "Iskanje zaznamkov" +title = "Bookmarks" [viewer.comments] addComment = "Dodaj komentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Najdi v dokumentu" moreActions = "Več dejanj" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Stran {{page}}" placingHint = "Kliknite stran za postavitev… (prekliči)" removeCommentOnly = "Odstrani samo komentar" saveReply = "Shrani odgovor" +searchPlaceholder = "Search comments" title = "Komentarji" typeComment = "Komentar" typeInsertText = "Vstavi besedilo" @@ -10262,6 +11325,7 @@ unsavedDesc = "Imate neshranjene spremembe" closeSidebar = "Zapri stransko vrstico plasti" hideAll = "Skrij vse plasti" showAll = "Prikaži vse plasti" +title = "Layers" [viewer.link] delete = "Izbriši povezavo" @@ -10605,30 +11669,6 @@ title = "Besedilo vodnega žiga" image = "Slika" text = "Besedilo" -[wetSignature.tooltip] -header = "Načini ustvarjanja podpisa" - -[wetSignature.tooltip.draw] -bullet1 = "Prilagodite barvo in debelino peresa" -bullet2 = "Brišite in znova narišite, dokler niste zadovoljni" -bullet3 = "Deluje na napravah na dotik (tablice, telefoni)" -description = "Ustvarite ročni podpis z miško ali zaslonom na dotik. Najbolj primerno za osebne, verodostojne podpise." -title = "Nariši podpis" - -[wetSignature.tooltip.type] -bullet1 = "Izberite med več pisavami" -bullet2 = "Prilagodite velikost in barvo besedila" -bullet3 = "Odlično za standardizirane podpise" -description = "Ustvarite podpis iz vnesenega besedila. Hitro in dosledno, primerno za poslovne dokumente." -title = "Vnesi podpis" - -[wetSignature.tooltip.upload] -bullet1 = "Podpira PNG, JPG in druge slikovne formate" -bullet2 = "Za najboljše rezultate so priporočena prosojna ozadja" -bullet3 = "Velikost slike bo prilagojena območju podpisa" -description = "Naložite vnaprej ustvarjeno sliko podpisa. Idealno, če imate skeniran podpis ali logotip podjetja." -title = "Naloži sliko podpisa" - [workbenchBar] activeFiles = "Aktivne datoteke" annotations = "Anotacije" @@ -10642,6 +11682,7 @@ exitRedaction = "Izhod iz načina redakcije" exportAll = "Izvozi PDF" exportSelected = "Izvozi izbrane strani" formFill = "Izpolni obrazec" +hideToolbar = "Hide toolbar" multiTool = "Večnamensko orodje" panMode = "Način premikanja" print = "Natisni PDF" @@ -10652,6 +11693,7 @@ redact = "Redakcija" rotateLeft = "Zavrti levo" rotateRight = "Zavrti desno" ruler = "Ravnilo / Merjenje" +rulerSettings = "Nastavitve merila" save = "Shrani" saveAll = "Shrani vse" saveAs = "Shrani kot" @@ -10661,6 +11703,7 @@ selectAll = "Izberi vse" selectByNumber = "Izberi po številkah strani" selectLanguage = "Izberi jezik" share = "Delite" +showToolbar = "Show toolbar" toggleAnnotations = "Preklopi vidnost anotacij" toggleAttachments = "Preklopi priloge" toggleBookmarks = "Preklopi zaznamke" diff --git a/frontend/editor/public/locales/sr-LATN-RS/translation.toml b/frontend/editor/public/locales/sr-LATN-RS/translation.toml index 1c13814719..fb1ec724ae 100644 --- a/frontend/editor/public/locales/sr-LATN-RS/translation.toml +++ b/frontend/editor/public/locales/sr-LATN-RS/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Primeni" applyAndContinue = "Sačuvaj i napusti" -areYouSure = "Da li ste sigurni da želite da napustite?" back = "Nazad" black = "Crno" blue = "Plavo" @@ -46,10 +45,8 @@ filesSelected = "odabrani fajlovi" font = "Font" fontSizeTooltip = "Veličina teksta broja stranice u tačkama. Veće vrednosti daju veći tekst." fontTypeTooltip = "Porodica fonta za brojeve stranica. Izaberite prema stilu dokumenta." -genericSubmit = "Prihvatiti" green = "Zeleno" help = "Pomoć" -imgPrompt = "Odaberi sliku (slike)" incorrectPasswordMessage = "Trenutna šifra je netačna." info = "Informacije" invalidUndoData = "Nije moguće poništiti: nevažeći podaci o operaciji" @@ -62,7 +59,6 @@ loginAgreementTitle = "Sporazum o prijavi" logOut = "Odjavi se" marginTooltip = "Udaljenost između broja stranice i ivice strane." moreOptions = "Još opcija" -multiPdfPrompt = "Odaberi PDF-ove (2+)" never = "Nikada" no = "Ne" noFileSelected = "Datoteka nije izabrana. Otpremi jednu." @@ -80,7 +76,6 @@ pageOrderPrompt = "Prilagođeni redosled stranica (unesi listu brojeva stranica pages = "Strane" pageSelectionPrompt = "Prilagođeni redosled stranica (unesi listu brojeva stranica 1,5,6 ili funkcija, kao što je 2n+1, razdvojenih zarezima) :" password = "Šifra" -pdfPrompt = "Odaberi PDF(ove)" pin = "Zakači datoteku (zadrži aktivnom nakon pokretanja alata)" pro = "Pro" processingComplete = "Vaša datoteka je spremna." @@ -110,6 +105,7 @@ undoSuccess = "Operacija uspešno poništena" unknown = "Nepoznato" unpin = "Otkači datoteku (zameni nakon pokretanja alata)" unsavedChanges = "Imate nesačuvane promene u svom PDF-u." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Nesačuvane promene" unsupported = "Nije podržano" username = "Korisničko ime" @@ -356,6 +352,7 @@ text = "Ove dozvole kontrolišu šta korisnici mogu da rade sa PDF-om. Najefikas title = "Promeni dozvole" [addStamp] +preview = "Pregled pečata" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ručno preuzimanje: kliknite na linkove i smestite datoteke u tes noLanguages = "U podešenom direktorijumu nisu pronađeni tessdata jezici." permissionNotice = "Putanja tessdata nije upisiva. Preuzimanja će biti otvorena u pregledaču; sačuvajte .traineddata datoteke ručno u tessdata folder." +[admin.settings.ai.documents] +description = "Konfigurišite model za embedding i podešavanja pretrage koja se koriste za odgovaranje na pitanja nad dokumentima. Primenjuje se na AI mehanizam nakon čuvanja." +title = "Dokumenti i RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Ostavite prazno da biste koristili sopstveni kredencijal okruženja mehanizma. Primenjuje se na self-hosted implementacije sa jednim mehanizmom." +generic = "Embedding API ključ" +openai = "OpenAI API ključ" +setPlaceholder = "Sačuvano - ostavite prazno da zadržite trenutni ključ" +voyageai = "VoyageAI API ključ" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Osnovni URL OpenAI-kompatibilne / Ollama krajnje tačke za embeddings, npr. http://ollama:11434/v1. Mora da upućuje na pouzdanu internu krajnju tačku (osetljivo na SSRF)." +label = "Osnovni URL za embedding" + +[admin.settings.ai.documents.embeddingModel] +description = "Naziv modela za embedding. Slobodan tekst; predlozi su samo smernice." +label = "Model za embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Provajder koji se koristi za pretvaranje teksta dokumenta u vektorske embeddings." +label = "Provajder za embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Maksimalan broj pretraga za dobavljanje koje agent može da pokrene po zahtevu." +label = "Maks. pretraga" + +[admin.settings.ai.documents.reindexNote] +body = "Promena modela za embedding stupa na snagu odmah, ali dokumenti indeksirani prethodnim modelom moraju biti ponovo indeksirani da bi pretraga vratila ispravne rezultate." +title = "Potrebno ponovno indeksiranje" + +[admin.settings.ai.documents.saved] +reindexNote = "Ako ste promenili model za embedding, ponovo indeksirajte postojeće dokumente kako bi pretraga koristila novi model." + +[admin.settings.ai.documents.topK] +description = "Broj najrelevantnijih delova dobijenih po pretrazi." +label = "Top K" + +[admin.settings.ai.general] +description = "Povežite Stirling sa Python AI mehanizmom i izaberite koje AI mogućnosti su dostupne. Promene se primenjuju pri ponovnom pokretanju." +title = "AI mehanizam" + +[admin.settings.ai.general.capabilities] +description = "Uključite ili isključite pojedinačne AI funkcije. Onemogućene funkcije su skrivene u aplikaciji." +title = "Mogućnosti" + +[admin.settings.ai.general.enabled] +description = "Glavni prekidač. Kada je isključen, AI alati, agenti ili pozivi mehanizma nisu dostupni." +label = "Omogući AI" + +[admin.settings.ai.general.features.chat] +description = "Konverzacijski asistent za rad sa PDF-ovima." +label = "Chat asistent" + +[admin.settings.ai.general.features.classify] +description = "Automatski kategorizujte dokumente po tipu ili sadržaju." +label = "Klasifikacija dokumenata" + +[admin.settings.ai.general.features.createPdf] +description = "Generišite novi PDF (npr. iz HTML-a) putem AI agenta." +label = "Kreiraj PDF iz upita" + +[admin.settings.ai.general.features.documentQuestions] +description = "Postavljajte pitanja i dobijajte odgovore zasnovane na otpremljenom dokumentu." +label = "Pitanja o dokumentu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Pregledajte dokumente radi matematičkih i numeričkih grešaka." +label = "Matematički auditor" + +[admin.settings.ai.general.features.pdfComment] +description = "Dodajte komentare pregleda i napomene koje je napisao AI u PDF." +label = "PDF agent za komentare" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Vremensko ograničenje za zahtevnije operacije agenta, kao što je generisanje dokumenata." +label = "Vremensko ograničenje dugotrajnih operacija (sekunde)" + +[admin.settings.ai.general.note] +body = "AI mehanizam radi kao zasebna usluga. Njegova deljena tajna" +body2 = "podešava se putem promenljive okruženja kontejnera. API ključevi provajdera mogu se uneti na ovim stranicama ili dostaviti kao promenljive okruženja mehanizma; ključevi uneti ovde šalju se mehanizmu nakon čuvanja." +title = "O AI mehanizmu" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Vremensko ograničenje za streamovane (token-po-token) chat odgovore." +label = "Vremensko ograničenje streama (sekunde)" + +[admin.settings.ai.general.test] +button = "Testiraj vezu" +failBody = "AI mehanizam nije odgovorio. Proverite URL, da li je kontejner mehanizma pokrenut i da li je AI omogućen (potrebno je ponovno pokretanje nakon omogućavanja)." +failTitle = "AI mehanizam nedostupan" +okBody = "AI mehanizam je odgovorio na proveru stanja." +okTitle = "AI mehanizam dostupan" + +[admin.settings.ai.general.timeoutSeconds] +description = "Vremensko ograničenje za standardne AI zahteve ka mehanizmu." +label = "Vremensko ograničenje zahteva (sekunde)" + +[admin.settings.ai.general.url] +description = "Interni URL Python AI mehanizma, npr. http://stirling-pdf-engine:5001." +label = "URL AI mehanizma" + +[admin.settings.ai.limits] +description = "Zaštitna ograničenja za količinu posla koju AI zahtevi mogu da izvrše i koliko ih se pokreće istovremeno. Primenjuje se na AI mehanizam nakon čuvanja." +title = "Ograničenja i performanse" + +[admin.settings.ai.limits.maxCharacters] +description = "Zaštitno ograničenje: odbij AI zahteve čiji izdvojeni tekst premašuje ovu dužinu." +label = "Maks. znakova po zahtevu" + +[admin.settings.ai.limits.maxPages] +description = "Zaštitno ograničenje: odbij AI zahteve preko ovoliko PDF stranica." +label = "Maks. stranica po zahtevu" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maksimalan broj istovremenih poziva modela u toku preko celog mehanizma." +label = "Maks. konkurentnost modela" + +[admin.settings.ai.models] +description = "Izaberite LLM provajdera i pametne/brze modele koje AI mehanizam koristi. Primenjuje se na AI mehanizam nakon čuvanja." +title = "Modeli i provajderi" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API ključ" +description = "Ostavite prazno da biste koristili sopstveni kredencijal okruženja mehanizma. Primenjuje se na self-hosted implementacije sa jednim mehanizmom." +generic = "API ključ" +openai = "OpenAI API ključ" +setPlaceholder = "Sačuvano - ostavite prazno da zadržite trenutni ključ" + +[admin.settings.ai.models.baseUrl] +description = "Osnovni URL OpenAI-kompatibilne / Ollama krajnje tačke, npr. http://ollama:11434/v1." +label = "Osnovni URL provajdera" +warning = "Osnovni URL mora da upućuje na pouzdanu internu krajnju tačku. Mehanizam će upućivati serverske zahteve ka njoj, pa je nepouzdana vrednost osetljiva na SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maksimalan broj izlaznih tokena za brzi model." +label = "Maks. tokena brzog modela" + +[admin.settings.ai.models.fastModel] +description = "Jeftiniji, brži model za lagane zadatke. Slobodan tekst; predlozi su samo smernice." +label = "Brzi model" + +[admin.settings.ai.models.provider] +description = "Sa kojim LLM provajderom mehanizam komunicira." +label = "Provajder" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maksimalan broj izlaznih tokena za pametni model." +label = "Maks. tokena pametnog modela" + +[admin.settings.ai.models.smartModel] +description = "Model visoke sposobnosti za složeno rezonovanje. Slobodan tekst; predlozi su samo smernice." +label = "Pametni model" + +[admin.settings.ai.saved] +body = "Promene se automatski šalju AI mehanizmu." +bodyNoPush = "Podešavanja su sačuvana. Primenjuju se sledeći put kada AI mehanizam preuzme svoju konfiguraciju." +title = "AI podešavanja sačuvana" + [admin.settings.badge] clickToUpgrade = "Kliknite da vidite detalje plana" @@ -938,6 +1094,25 @@ label = "Ponovo generiši pri pokretanju" description = "Broj dana tokom kojih će sertifikat važiti" label = "Važenje sertifikata (dani)" +[admin.settings.folderAccess] +description = "Direktorijumi iz kojih izvori fascikli i izlazi fascikli smeju da čitaju i u koje smeju da pišu. Ovo je bezbednosna granica: automatizacije nikada ne mogu biti usmerene na serversku putanju van ove liste." +securityNote = "Ostavite ovo prazno da biste potpuno onemogućili izvore i izlaze fascikli. Stirling-ov sopstveni konfiguracioni direktorijum je uvek zabranjen." +title = "Pristup fasciklama" + +[admin.settings.folderAccess.implied] +description = "Ovi direktorijumi kojima upravlja Stirling su uvek dozvoljeni i ovde se ne mogu menjati." +serverStorage = "Skladište datoteka na serveru" +title = "Uvek dozvoljeno" +watchedFolder = "Nadzirana fascikla pipeline-a" + +[admin.settings.folderAccess.roots] +add = "Dodaj" +empty = "Nema dozvoljenih fascikli. Izvori i izlazi fascikli su trenutno onemogućeni." +hint = "Unesite apsolutne putanje, na primer /data/inbox." +label = "Dozvoljeni koreni fascikli" +placeholder = "/data/inbox" +remove = "Ukloni koren fascikle" + [admin.settings.general] description = "Podesite sistemska podešavanja aplikacije uključujući brendiranje i podrazumevano ponašanje." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Širina" [app] description = "Besplatna alternativa Adobe Acrobat‑u (10M+ preuzimanja)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konvertuj u PDF/A-3b" convertToPdfA3bDescription = "Kreira arhivski PDF sa ugrađenim prilozima" @@ -1842,6 +2020,15 @@ export = "Izvoz" accessDenied = "Pristup odbijen" insufficientPermissions = "Nemate dozvolu da izvršite ovu radnju." +[auth.callback] +completing = "Završavanje autentifikacije" +invalidToken = "OAuth prijava nije uspela - nevažeći token." +missingToken = "OAuth prijava nije uspela - token nije primljen." +oauthFailed = "OAuth prijava nije uspela. Pokušajte ponovo." +pleaseWait = "Sačekajte dok završimo vašu prijavu." +signedOut = "Odjavljeni ste. Prijavite se ponovo." +windowMayClose = "Možete zatvoriti ovaj prozor kada se završi." + [auth.displayName] guest = "Gost" user = "Korisnik" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Obezbeđuje PDF dokumente uklanjanjem potencijalno zloname description = "Ovaj alat će automatski preimenovati PDF fajlove na osnovu njihovog sadržaja. Analizira dokument kako bi iz teksta pronašao najprikladniji naslov." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Automatski rotiraj" +tags = "automatsko rotiranje,ispravi orijentaciju,naopako,bočno,ispravi,orijentiši,otkrij orijentaciju,ispravi rotaciju,automatski orijentiši,skenirano,OSD" + +[autoRotate.confidenceThreshold] +title = "Prag pouzdanosti" + +[autoRotate.detectionMode] +auto = "Automatski" +osd = "Samo OCR" +text = "Samo tekst" +title = "Metod detekcije" + +[autoRotate.error] +failed = "Došlo je do greške pri automatskom rotiranju PDF-a." + +[autoRotate.inferUndetected] +title = "Rotiraj stranice niske pouzdanosti da se poklope" + +[autoRotate.report] +applied = "{{degrees}}° u smeru kazaljke" +confidenceValue = "Pouzdanost {{value}}" +noChange = "Bez promene" +pageLabel = "Stranica {{number}}" +summary = "{{rotated}} od {{total}} stranica rotirano, {{unchanged}} ostavljeno kako jeste" +title = "Izveštaj detekcije" + +[autoRotate.report.method] +inferred = "Usklađeno" +none = "Preskočeno" +osd = "OCR" +text = "Tekst" + +[autoRotate.report.note] +belowThreshold = "Ispod praga" +blankPage = "Stranica je prazna" +inferredFromDocument = "Isto kao ostale stranice" +noDominantDirection = "Pomešani smerovi teksta" +osdFailed = "Nije pronađen čitljiv tekst" +osdNoVerdict = "Nije moguće utvrditi koja strana je gore" +tesseractUnavailable = "OCR nije instaliran" +tooFewGlyphs = "Premalo teksta" + +[autoRotate.results] +title = "Rezultati automatskog rotiranja" + +[autoRotate.settings] +title = "Podešavanja" + +[autoRotate.tooltip.confidenceThreshold] +text = "Koliko provera mora biti sigurna pre nego što se stranica okrene. Smanjite da biste rotirali više stranica, povećajte da biste više stranica ostavili netaknuto." +title = "Prag pouzdanosti" + +[autoRotate.tooltip.description] +text = "Utvrđuje kako svaka stranica treba da bude okrenuta i okreće je u pravi položaj. Stranice koje su već uspravne ostaju nepromenjene." + +[autoRotate.tooltip.detectionMode] +text = "Automatski prvo čita tekst na stranici, zatim posmatra stranicu kao sliku ako to nije dovoljno. Samo tekst preskače proveru slike, što je najbrže za dokumente koji nikada nisu skenirani. Samo OCR tretira svaku stranicu kao sliku." +title = "Metod detekcije" + +[autoRotate.tooltip.header] +title = "Automatsko rotiranje" + +[autoRotate.tooltip.inferUndetected] +text = "Prazne ili skoro prazne stranice ne mogu se proveriti same za sebe. Kada svaka stranica koja se mogla pročitati zahteva isto okretanje, ove stranice se okreću da se poklope sa njima." +title = "Rotiraj stranice niske pouzdanosti da se poklope" + [autoSizeSplitPDF] tags = "pdf,delenje,dokumenti,organizacija" @@ -2189,33 +2443,6 @@ keywords = "Ključne reči: odd, even" numbers = "Brojevi/opsezi: 5, 10-20" progressions = "Progresije: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Tipovi sertifikata" - -[certificateChoice.tooltip.organization] -bullet1 = "Upravlja sistemski administrator" -bullet2 = "Deljen među ovlašćenim korisnicima" -bullet3 = "Predstavlja identitet kompanije, ne pojedinca" -bullet4 = "Najbolje za: Zvanična dokumenta, timske potpise" -description = "Deljeni sertifikat koji obezbeđuje vaša organizacija. Koristi se za ovlašćeno potpisivanje u ime kompanije." -title = "Sertifikat organizacije" - -[certificateChoice.tooltip.personal] -bullet1 = "Generiše se automatski pri prvoj upotrebi" -bullet2 = "Povezan sa vašim korisničkim nalogom" -bullet3 = "Ne može se deliti sa drugim korisnicima" -bullet4 = "Najbolje za: Lična dokumenta, individualnu odgovornost" -description = "Automatski generisan sertifikat jedinstven za vaš korisnički nalog. Pogodan za individualne potpise." -title = "Lični sertifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Zahteva P12/PFX datoteku i lozinku" -bullet2 = "Može biti izdat od strane eksternih sertifikacionih tela" -bullet3 = "Viši nivo poverenja za pravna dokumenta" -bullet4 = "Najbolje za: Pravno obavezujuće ugovore, eksternu verifikaciju" -description = "Koristite svoju PKCS#12 datoteku sertifikata. Obezbeđuje potpunu kontrolu nad svojstvima sertifikata." -title = "Otpremi prilagođeni P12" - [certSign] awaitingSignatures = "Čeka se na potpise" chooseCertificate = "Izaberite fajl sertifikata" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Izvor sertifikata" upload = "Otpremi" -[certSign.tooltip.header] -title = "O upravljanju potpisima" - -[certSign.tooltip.overview] -bullet1 = "Proverite postojeće potpise i njihovu validnost" -bullet2 = "Prikažite detaljne informacije o potpisnicima i sertifikatima" -bullet3 = "Dodajte nove digitalne potpise radi zaštite dokumenata" -bullet4 = "Podržani su višestruki fajlovi uz jednostavnu navigaciju" -text = "Ovaj alat vam omogućava da proverite da li su vaši PDF-ovi digitalno potpisani i da dodate nove digitalne potpise. Digitalni potpisi dokazuju ko je kreirao ili odobrio dokument i pokazuju da li je promenjen nakon potpisivanja." -title = "Šta može ovaj alat?" - -[certSign.tooltip.signing] -bullet1 = "Podržava PEM, PKCS12, JKS i formate serverskog sertifikata" -bullet2 = "Opcija da prikažete ili sakrijete potpis na PDF-u" -bullet3 = "Dodajte razlog, lokaciju i ime potpisnika" -bullet4 = "Izaberite na kojoj strani da postavite vidljive potpise" -bullet5 = "Koristite serverski sertifikat za jednostavnu opciju 'Potpiši sa Stirling-PDF'" -text = "Za potpisivanje PDF-a potreban je digitalni sertifikat (kao PEM, PKCS12 ili JKS). Možete izabrati da potpis bude vidljiv na dokumentu ili da bude nevidljiv samo radi bezbednosti." -title = "Dodavanje potpisa" - -[certSign.tooltip.validation] -bullet1 = "Prikazuje da li su potpisi važeći ili nevažeći" -bullet2 = "Prikazuje informacije o potpisniku i datum potpisivanja" -bullet3 = "Proverava da li je dokument izmenjen nakon potpisivanja" -bullet4 = "Može koristiti prilagođene sertifikate za verifikaciju" -text = "Kada proveravate potpise, alat prikazuje da li su važeći, ko je potpisao dokument, kada je potpisan i da li je dokument promenjen nakon potpisivanja." -title = "Provera potpisa" - [changeCreds] changeUsername = "Ažurirajte korisničko ime. Bićete odjavljeni nakon ažuriranja." credsUpdated = "Nalog ažuriran" @@ -2737,12 +2936,16 @@ text = "Da biste ove dozvole učinili neizmenjivim, koristite alat Dodaj lozinku [chat.actions] copy = "Kopiraj poruku" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Opcije Stirling agenta" +assistant = "AI Assistant" clearChat = "Obriši čet" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ova operacija će potrošiti vaše cloud kredite" tooltip = "Izaberi boju sa ekrana" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Izaberite boju" [common] +actions = "Actions" back = "Nazad" cancel = "Otkaži" close = "Zatvori" +codeSample = "Code sample" collapse = "Skupi" confirm = "Potvrdi" continue = "Nastavi" copied = "Kopirano!" copy = "Kopiraj" done = "Gotovo" +enterValue = "Enter value" error = "Greška" expand = "Proširi" loading = "Učitavanje..." next = "Sledeće" +open = "Open" preview = "Pregled" previous = "Prethodno" refresh = "Osveži" +remaining = "Preostalo" retry = "Pokušaj ponovo" save = "Sačuvaj" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Obriši izabrano" @@ -2988,6 +3199,7 @@ title = "Metod kompresije" [compress.settings] desiredSize = "Željena veličina fajla" desiredSizePlaceholder = "Unesite veličinu" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Kompresija je lak način da smanjite veličinu fajla. Izaberite Veličina fajla da unesete ciljnu veličinu i da mi prilagodimo kvalitet za vas. Izaberite Kvalitet da ručno podesite jačinu kompresije." @@ -3190,6 +3402,24 @@ pdfOptions = "Opcije za PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Prvo izaberite izvorni format" settings = "Podešavanja" single = "Jedan" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog analitika" scarf = "Scarf piksel" -[credits.modal] -advancedMonitoring = "Napredno nadgledanje" -allInOneWorkspace = "Sve-u-jednom PDF radni prostor (prikazivač, alati i agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Posvećena podrška i SLA-ovi" -fasterThroughput = "10x brži protok" -fullyPrivateFiles = "Potpuno privatne datoteke" -largeFileProcessing = "Obrada velikih datoteka" -orgWideAccess = "Kontrole pristupa na nivou organizacije" -premiumAiModels = "Premium AI modeli" -prioritySupport = "Prioritetna podrška" -privateDocCloud = "Privatni oblak dokumenata" -ragFineTuning = "RAG + fino podešavanje" -secureApiAccess = "Bezbedan API pristup" -standardThroughput = "Standardni protok" -unlimitedApiAccess = "Neograničen API pristup" -unlimitedSeats = "Neograničen broj korisnika" - [crop] autoCrop = "Automatski iseci beline" noFileSelected = "Izaberite PDF fajl da biste započeli isecanje" @@ -3346,6 +3558,7 @@ message = "Neuspešno postavljanje podrazumevanog PDF rukovaoca" title = "Greška" [defaultApp.prompt] +dontRemind = "Ne podsećaj me ponovo" message = "Postavite Stirling PDF kao podrazumevanu aplikaciju za otvaranje PDF fajlova." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Pretraga..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Uredi" @@ -3644,11 +3861,13 @@ shareSelected = "Podeli izabrano" sharing = "Deljenje" showAll = "Prikaži sve" showHistory = "Prikaži istoriju" +sortBy = "Sort files" sortByDate = "Sortiraj po datumu" sortByName = "Sortiraj po nazivu" sortBySize = "Sortiraj po veličini" storage = "Skladište" storageState = "Skladište" +storageUsed = "Storage used" synced = "Sinhronizovano" title = "Otpremi PDF datoteke" toolChain = "Primеnjeni alati" @@ -3694,13 +3913,17 @@ addFiles = "Dodaj datoteke" addingFiles = "Dodavanje fajlova…" collapse = "Skupi bočnu traku" customizeGroups = "Prilagodi grupe" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Otvorite datoteke da biste započeli" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Proširi bočnu traku" -files = "Datoteke" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive nije konfigurisan" leaveMyFiles = "Napusti Moje fajlove" +library = "PDF biblioteka" myFiles = "Moji fajlovi" noFiles = "Još nema datoteka" openFileManager = "Otvori menadžer datoteka" @@ -3708,37 +3931,36 @@ openFromComputer = "Otvori sa računara" openSettings = "Otvori podešavanja" other = "Ostalo" recent = "Nedavno" -search = "Pretraga" -searchPlaceholder = "Pretraži datoteke..." viewAll = "Prikaži svih {{count}} fajlova" [fileSidebar.fileItem] closeViewer = "Zatvori pregledač" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Otvori u pregledaču" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Dodaj" -addLabel = "Dodaj oznaku…" -categoryIconAria = "Izaberite ikonu za {{name}}" -createCategory = "Kreiraj kategoriju" -delete = "Izbriši kategoriju" done = "Gotovo" -hide = "Sakrij kategoriju" -newCategory = "Naziv nove kategorije…" -removeLabel = "Ukloni {{name}}" -rename = "Preimenuj" reset = "Vrati na podrazumevano" -search = "Pretraži oznake…" -show = "Prikaži kategoriju" subtitle = "Grupišite fajlove u nadređene kategorije. Dodajte postojeće ili nove oznake u kategoriju, preimenujte je ili kreirajte svoju. Fajlovi koji nisu ni u jednoj kategoriji prikazuju se pod „Ostalo”." title = "Kategorije bočne trake" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Dodaj u radni prostor" addToWorkspaceCount = "Dodaj {{count}} u radni prostor" @@ -3749,7 +3971,6 @@ backToMyFiles = "Nazad na Moje fajlove" breadcrumbs = "Putanja fascikle" cancel = "Otkaži" classification = "Klasifikacija" -clearSearch = "Obriši pretragu" clearSelection = "Poništi izbor" closeDetails = "Zatvori detalje" create = "Kreiraj" @@ -3781,6 +4002,7 @@ downloadAll = "Preuzmi sve" downloadVersion = "Preuzmi ovu verziju" dropOverlay = "Ispustite fajlove za otpremanje" dropOverlaySub = "Fajlovi počinju u Lokalno. Koristite 'Premesti u' ili 'Sačuvaj u oblaku' da ih organizujete u fasciklu." +duplicate = "Duplicate" file = "Fajl" fileInfo = "File info" fileMenu = "Radnje nad fajlom" @@ -3808,13 +4030,12 @@ refresh = "Osveži sa servera" remove = "Obriši" removeVersion = "Ukloni ovu verziju" rename = "Preimenuj" +renamed = "Preimenovano" renameFolder = "Preimenuj fasciklu" resizeFolderTree = "Promeni veličinu stabla fascikli (tasteri sa strelicama, Shift za veće korake; dvoklik za automatsko uklapanje)" save = "Sačuvaj" saveToServer = "Sačuvaj na server" saveToServerDisabledHint = "Čuvanje na serveru nije omogućeno na ovom serveru. Zamolite administratora da ga uključi." -search = "Pretraga" -searchPlaceholder = "Pretraži ovu fasciklu i potfascikle" selectAll = "Izaberi sve" selectAllHint = "Kliknite da izaberete sve. Savet: držite Ctrl (ili Cmd) da dodajete fajlove jedan po jedan, Shift za izbor opsega." selectedCount = "{{count}} izabrano" @@ -3874,6 +4095,10 @@ title = "Još nema cloud fajlova" hint = "Fajlovi sačuvani bez otpremanja ostaju ovde. Ispustite fajl da dodate jedan." title = "Nema samo-lokalnih fajlova" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Fajlovi koje otvorite ili izmenite pojaviće se ovde." title = "Još ništa nije izmenjeno" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Nije moguće obrisati fasciklu." deleteFolderFailedDetail = "Nije moguće obrisati fasciklu: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Nije moguće ažurirati izgled fascikle." folderAppearanceFailedDetail = "Nije moguće ažurirati izgled fascikle: {{message}}" moveFilesFailed = "Nije moguće premestiti fajlove." @@ -3910,9 +4137,7 @@ count = "Fajlova" folder = "Fascikla" labels = "Oznake" modified = "Izmenjeno" -name = "Naziv" size = "Veličina" -toolHistoryAtVersion = "Kumulativni lanac alata" totalSize = "Ukupna veličina" type = "Tip" versionHistory = "Istorija verzija" @@ -3945,6 +4170,11 @@ localHint = "Samo skladišteno u ovom pregledaču" shared = "Podeljeno" sharedHint = "Podeljeno sa vama putem linka" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Najstarije prvo" modifiedDesc = "Najnovije prvo" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Nema fajlova u skladištu. Prvo otvorite neke fajlove." open = "Otvori" openFile = "Otvori fajl" openFiles = "Otvori fajlove" +selectFile = "Select {{name}}" selectFromStorage = "Izaberi iz skladišta" upload = "Otpremi" uploadFile = "Otpremi datoteku" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Sve je sačuvano" +analyzingFields = "Analysing form fields..." +extractCsvError = "Izdvajanje CSV-a nije uspelo" +extractXlsxError = "Izdvajanje XLSX-a nije uspelo" +filled = "filled" flattenAfterFilling = "Zapečati nakon popunjavanja" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obav" +requiredFieldsError = "Popunite sva obavezna polja" rescanFields = "Ponovo skeniraj polja" rescanFormFields = "Ponovo skeniraj polja obrasca" save = "Sačuvaj" +saveShortcut = "Ctrl+S za čuvanje" +unsavedChanges = "Nesačuvane promene" [formFill.sidebar] close = "Zatvori bočnu traku" @@ -4309,6 +4552,11 @@ desc = "Automatski preimenuje PDF datoteku na osnovu otkrivenog zaglavlja" tags = "automatsko otkrivanje,na osnovu zaglavlja,organizuj,preimenuj" title = "Automatski preimenuj PDF datoteku" +[home.autoRotate] +desc = "Otkrijte orijentaciju svake stranice i automatski je rotirajte uspravno." +tags = "automatsko rotiranje,ispravi orijentaciju,naopako,bočno,ispravi,orijentiši,otkrij orijentaciju,ispravi rotaciju,automatski orijentiši,skenirano,OSD" +title = "Automatsko rotiranje" + [home.autoSizeSplitPDF] tags = "automatski,podeli,veličina" title = "Automatsko deljenje po veličini/broju" @@ -4608,15 +4856,12 @@ hint = "Otpremite PNG, JPG, SVG ili drugi fajl slike da ga postavite na PDF. SVG label = "Otpremi sliku" placeholder = "Izaberite fajl slike" -[imageToPdf] -tags = "konverzija,img,jpg,slika,foto" - [imageToPDF] header = "Slika u PDF" title = "Slika u PDF" -[infoBanner] -dismiss = "Odbaci" +[imageToPdf] +tags = "konverzija,img,jpg,slika,foto" [invite] acceptError = "Kreiranje naloga nije uspelo" @@ -4647,11 +4892,9 @@ welcomeTitle = "Pozvani ste!" [landing] addFiles = "Dodaj datoteke" -heroSubtitle = "Prevucite ili dodajte postojeći PDF da biste počeli." mobileUpload = "Otpremi sa mobilnog" openFromComputer = "Otvori sa računara" uploadFromComputer = "Otpremi sa računara" -workbenchEmptyStateHero = "Ispustite PDF bilo gde" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Unesite korisničko ime" failedToSignIn = "Neuspešna prijava putem {{provider}}: {{message}}" forgotPassword = "Zaboravili ste lozinku?" loggingIn = "Prijavljivanje..." -logIn = "Prijava" login = "Prijava" magicLinkSent = "Magični link je poslat na {{email}}! Proverite e-mail i kliknite na link da biste se prijavili." mfaCode = "Autentifikacioni kod" @@ -4715,26 +4957,10 @@ signInWith = "Prijavite se pomoću" title = "Prijavite se" unexpectedError = "Neočekivana greška: {{message}}" updatePassword = "Ažuriraj lozinku" -useEmailInstead = "Prijava pomoću e-maila" useMagicLink = "Umesto toga koristite magični link" username = "Korisničko ime" youAreLoggedIn = "Prijavljeni ste!" -[login.slides.edit] -alt = "Uređivanje PDF dokumenata" -subtitle = "Uz više od desetak alata za zatamnjivanje, potpisivanje, čitanje i manipulaciju PDF dokumentima, sigurno ćete naći šta tražite." -title = "Uredite PDF dokumente da prikažete/zaštitite informacije koje želite" - -[login.slides.overview] -alt = "Pregled Stirling PDF" -subtitle = "Cloud paket sa privatnošću na prvom mestu za PDF dokumente koji omogućava konvertovanje, potpisivanje, zatamnjivanje i upravljanje dokumentima, uz 50+ drugih moćnih alata." -title = "Sve na jednom mestu za sve vaše PDF potrebe." - -[login.slides.secure] -alt = "Zaštita PDF" -subtitle = "Dodajte lozinke, zatamnite sadržaj i lako upravljajte sertifikatima." -title = "Zaštitite osetljive informacije u vašim PDF dokumentima" - [margin] large = "Veliko" medium = "Srednje" @@ -4818,6 +5044,34 @@ uploadSuccess = "Otpremanje uspešno!" uploadSuccessMessage = "Vaše slike su prenete." validating = "Provera sesije..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skenirajte da otpremite fotografije. Slike se automatski konvertuju u PDF." descriptionNoConvert = "Skenirajte da otpremite fotografije sa mobilnog uređaja." @@ -4836,8 +5090,13 @@ title = "Otpremi sa mobilnog" tags = "Multi Alat,Multi operacija,Korisnički interfejs,klik i povuci,front end,klijentska strana,interaktivno,pomera" title = "PDF Multi Alatka" -[navbar] -search = "Pretraga" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentikacija nije uspela. Možete zatvoriti ovaj prozor i pokušati ponovo." @@ -4966,6 +5225,7 @@ activeFiles = "Prikaz Active Files pokazuje sve PDF-ove koje st allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Zatvori" cropSettings = "Sada kada smo izabrali fajl koji želimo da isečemo, možemo podesiti alat Crop da odaberemo oblast na koju želimo da isečemo PDF." +dialogLabel = "Onboarding" fileCheckbox = "Klikom na neki fajl birate ga za obradu. Možete izabrati više fajlova za grupne operacije." fileReplacement = "Izmenjeni fajl će automatski zameniti original u Workbench-u, što vam omogućava da ga lako obradite kroz dodatne alate." filesButton = "Dugme Files na traci brzog pristupa omogućava otpremanje PDF-ova na kojima ćete koristiti alate." @@ -4976,6 +5236,7 @@ pinButton = "Možete koristiti dugme Pin ako želite da vaši f results = "Nakon što alat završi, korak Review će prikazati pregled rezultata u ovom panelu i omogućiti vam da poništite operaciju ili preuzmete fajl. " runButton = "Kada je alat podešen, ovo dugme omogućava pokretanje alata na svim izabranim PDF-ovima." selectCropTool = "Hajde da izaberemo alat Crop da pokažemo kako se koristi jedan od alata." +stepOf = "Korak {{current}} od {{total}}" toolInterface = "Ovo je interfejs alata Crop. Kao što vidite, nema mnogo toga jer još nismo dodali nijedan PDF fajl za rad." workbench = "Ovo je Workbench - glavno područje gde pregledate i uređujete svoje PDF-ove." wrapUp = "Spremni ste! Naučili ste o glavnim delovima aplikacije i kako da ih koristite. Kliknite na dugme Pomoć kada god želite da ponovo vidite ovu turu." @@ -4988,6 +5249,26 @@ showMeAround = "Provedi me kroz aplikaciju" skipForNow = "Preskoči za sada" skipTheTour = "Preskoči obilazak" +[onboarding.checklist] +dismiss = "Odbaci" +title = "Podesite Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Pokrenite Stirling nativno na svom računaru" +title = "Preuzmite Stirling za desktop" + +[onboarding.checklist.inviteTeam] +description = "Sarađujte sa svojim timom" +title = "Pozovite članove tima" + +[onboarding.checklist.shareAnalytics] +description = "Pomozite da se Stirling poboljša" +title = "Delite anonimne podatke o korišćenju" + +[onboarding.checklist.takeTour] +description = "Pogledajte kako Stirling radi u kratkom obilasku" +title = "Pokrenite obilazak" + [onboarding.desktopInstall] body = "Stirling najbolje radi kao desktop aplikacija. Možete je koristiti offline, brže pristupati dokumentima i praviti izmene lokalno na računaru." selectOs = "Izaberite operativni sistem" @@ -4996,7 +5277,21 @@ titleWithOs = "Preuzmi za {{osLabel}}" [onboarding.mfa] authenticationCode = "Autentifikacioni kod" +description = "Zaštitite svoj nalog povezivanjem aplikacije za autentifikaciju. Skenirajte QR kod ili unesite ključ za podešavanje, zatim potvrdite 6-cifreni kod da završite." +enable = "Omogući MFA" +enableError = "Nije moguće omogućiti dvofaktorsku autentifikaciju. Proverite kod i pokušajte ponovo." +enterCode = "Unesite 6-cifreni kod iz svoje aplikacije." +enterCodeError = "Unesite kod za autentifikaciju da nastavite." +logout = "Odjava" +openAuthenticator = "Otvorite Google Authenticator, Authy ili 1Password." qrCodeLoading = "Generisanje vašeg QR koda…" +regenerateQrCode = "Ponovo generiši QR kod" +scanQrCode = "Skenirajte QR kod ili unesite ključ za podešavanje ispod." +setupError = "Nije moguće pokrenuti podešavanje dvofaktorske autentifikacije. Pokušajte ponovo." +setupKey = "Ključ za podešavanje (ručni unos)" +stepByStep = "Korak po korak" +success = "MFA je omogućen. Sada možete da nastavite." +title = "Podešavanje višefaktorske autentifikacije" [onboarding.planOverview] adminBodyLoginDisabled = "Kada omogućite režim prijave, možete upravljati korisnicima, podešavati opcije i pratiti stanje servera. Prvih {{freeTierLimit}} ljudi na vašem serveru koristi Stirling besplatno." @@ -5005,6 +5300,11 @@ adminTitle = "Admin pregled" userBody = "Pozovite kolege, dodeljujte uloge i držite dokumente organizovanim u jednom bezbednom prostoru. Omogućite režim prijave kad god ste spremni da pređete preko solo upotrebe." userTitle = "Pregled plana" +[onboarding.processorIntro] +body = "Stirling sada pokreće Pravila — automatizovana pravila koja klasifikuju, štite i obrađuju svaki dokument po dolasku. Podesite ih i pratite izvršavanja u Procesoru." +cta = "Pogledajte Procesor" +title = "Pogledajte Stirling Procesor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Unapred plaćena godina" + +[payg.prepaid.banner] +exhausted = "Vaš unapred plaćeni kapacitet je potrošen; obračun po potrošnji je nastavljen. Dopunite da zadržite popust." +expiring_one = "Vaša unapred plaćena godina se završava za {{count}} dan. Neiskorišćeni kapacitet ističe — dopunite da započnete novu godinu." +expiring_other = "Vaša unapred plaćena godina se završava za {{count}} dana. Neiskorišćeni kapacitet ističe — dopunite da započnete novu godinu." +low = "Preostalo vam je {{pct}}% unapred plaćenog kapaciteta. Dopunite bilo kada da obrada ostane neprekinuta." + +[payg.prepaid.card] +subtitle = "Koristi se pre obračuna po potrošnji i izvan vašeg limita potrošnje." +title = "Unapred plaćeni kapacitet" + +[payg.prepaid.meter] +capSuffix = "od {{total}} unapred plaćenih kredita" +expires = "Ističe {{date}}" + +[payg.prepaid.state] +exhausted = "Potrošeno" +healthy = "Dovoljno preostalo" +low = "Pri kraju" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF u PDF/A" tags = "arhiva,dugoročno,standard,konverzija,čuvanje,čuvanje" title = "PDF u PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "štampa,standard,konverzija,produkcija,pretštampa,arhiva" title = "PDF u PDF/X" @@ -5822,6 +6151,7 @@ small = "500 kredita" xsmall = "100 kredita" [plan.availablePlans] +currency = "Billing currency" subtitle = "Izaberite paket koji odgovara vašim potrebama" title = "Dostupni paketi" @@ -5937,49 +6267,12 @@ keyDescription = "Nalepite licencni ključ iz e-pošte" success = "Licenca aktivirana!" successMessage = "Vaša licenca je uspešno aktivirana. Sada možete zatvoriti ovaj prozor." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "primenjeno" -enforcing = "Sprovodi se..." -failed = "Primena nije uspela" outputsUnavailable = "Rezultati politike više nisu dostupni za preuzimanje." partialOutputsUnavailable = "Neki rezultati politike više nisu dostupni za preuzimanje." -retrying = "Zauzeto, pokušavam ponovo..." runNotFound = "Izvršavanje primene više nije moguće pronaći." -step = "korak {{current}}/{{total}}" timedOut = "Vreme za primenu je isteklo pre nego što je izvršavanje moglo da se završi." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Zatvori" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Primena {{names}}" applyingProgress = "Primena {{names}} ({{done}} od {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "Obrada {{failures}} od {{total}} datoteka nije uspela, pa su izve failureTitle = "Izvezeno bez potpune primene" printPolicyAppliedBody = "Ovaj PDF je ažuriran da bi ispunio politiku. Pregledajte izmene, zatim ponovo odštampajte." printPolicyAppliedTitle = "Politika primenjena pre štampanja" -queued = "+{{count}} na čekanju" successTitle = "{{names}} primenjeno" summaryMore = "{{first}}, {{second}} i još {{more}}" summaryTwo = "{{first}} i {{second}}" -[policies.enforcement.triggerVerb] -convert = "Primena pre konverzije" -default = "Primena" -export = "Primena pre izvoza" -input = "Primena pri uvozu" -print = "Primena pre štampanja" - -[policies.field] -accessLog = "Dnevnik pristupa" -archiveAfter = "Arhiviraj posle" -auditTrail = "Revizioni trag" -belowThreshold = "Ispod praga" -destination = "Odredište" -frameworks = "Okviri" -immutableHold = "Nepromeljivo zadržavanje" -keepFor = "Čuvaj" -minConfidence = "Min. pouzdanost" -notify = "Obavesti pri usmeravanju" -onViolation = "Kada nije usklađeno" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Dodaj" -addPlaceholder = "Dodaj oznaku…" -customNote = "Prilagođeno vašem timu." -defaultNote = "Koristi se ugrađena podrazumevana vrednost, deljena sa vašim timom." -duplicate = "\"{{name}}\" već postoji." -edit = "Uredi oznake" -empty = "Još nema oznaka." -export = "Izvezi JSON" -iconAria = "Izaberite ikonu za {{name}}" -import = "Uvezi JSON" -importError = "Nije moguće uvesti taj fajl." -managedNote = "Timskim oznakama upravlja vođa vašeg tima." -modalSubtitle = "Deljeno sa celim vašim timom. Klasifikator bira oznake koje odgovaraju svakom dokumentu." -modalTitle = "Oznake klasifikacije" -pickIcon = "Koristi ikonu {{label}}" -removeAria = "Ukloni {{name}}" -resetToDefault = "Vrati na podrazumevano" -saveForTeam = "Sačuvaj za tim" -saving = "Čuvanje…" -sectionLabel = "Oznake klasifikacije" -startFromScratch = "Počni od nule" -teamCount = "timske oznake" -tooLong = "Oznake mogu imati najviše {{max}} znakova." -ungrouped = "Negrupisano" -view = "Prikaži oznake" +categoryCount = "kategorije" +hideCategory = "Sakrij kategoriju" +labelCount = "oznake" +sharedNote = "Ove oznake su ugrađene i dele se u celom vašem timu." +showCategory = "Prikaži kategoriju" [policies.pii] account = "Brojevi računa (označeni)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Brojevi za rutiranje u SAD (ABA)" ssn = "Brojevi socijalnog osiguranja" -[policies.settings] -noneExport = "Trenutno se nijedna politika ne pokreće pri izvozu." -noneUpload = "Trenutno se nijedna politika ne pokreće pri otpremanju." -onExport = "Pri izvozu" -onUpload = "Pri otpremanju" -reorderHandle = "Prevucite za promenu redosleda" -runOrderDesc = "Kada se više od jedne politike pokreće na isti okidač, pokreću se ovim redosledom — svaka na izlazu prethodne politike. Prevucite za promenu redosleda." -title = "Podešavanja politika" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Opcije politike" -policySettings = "Podešavanja politika" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Ponovi neuspele politike ({{count}})" -rowProgress = "Obrađeno {{completed}} od {{total}} fajlova" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Šta je politika?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Automatski pronalazi i zatamnjuje osetljive podatke — kao što su brojevi socijalnog osiguranja i brojevi kartica — kako ne bi mogli da se čitaju u dokumentu." -sanitize = "Uklanja skriveni JavaScript iz datoteke, tako da se ništa ne pokreće automatski kada je neko otvori." -watermark = "Postavlja vidljivu oznaku (npr. \"Poverljivo\") preko svake stranice." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Zatvori" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} se primenjuje..." badgeRan = "Politika {{name}} je pokrenuta na ovom fajlu" +badgeRunning = "{{name}} se izvršava..." blockingAction = "{{action}} blokirano dok se politika primenjuje, sačekajte..." dismiss = "Zatvori preklop" enforcingTitle = "Primena politike..." @@ -6151,6 +6316,7 @@ viewAnyway = "Prikaži fajl (politika se i dalje primenjuje)" [policyOption] autoRedactPhi = "Automatski rediguj PHI" blockExport = "Blokiraj izvoz" +ccpa = "CCPA" contracts = "Ugovori" documents = "Dokumenti" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor plan" subscribed = "Processor plan" unlinked = "Nije povezano" -[portal.agentBuilder] -bootstrapFromDocument = "Kreiraj iz dokumenta" -sectionsAriaLabel = "Odeljci za izradu agenta" -selectorAriaLabel = "Agenti" -subtitle = "Dizajnirajte, testirajte i isporučite AI agente koji klasifikuju, izdvajaju iz i usmeravaju vaše dokumente. Definišite scenarije, ograničite pristup alatima, pokrenite zlatni skup i objavite verziju." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Otkaži" -dropzoneText = "Izaberite primer dokumenta (PDF ili slika)" -lead = "Ispustite primer dokumenta i predložićemo scenarije i šemu ekstrakcije koje možete doraditi. Ništa se ne objavljuje dok to ne pregledate." -submit = "Kreiraj agenta" -subtitle = "Započnite novog agenta iz jednog reprezentativnog fajla" -title = "Kreiraj iz dokumenta" - -[portal.agentBuilder.empty] -description = "Kreirajte agenta iz primera dokumenta da biste započeli njegove scenarije i šemu ekstrakcije, zatim doradite i objavite." -title = "Još nema agenata" - -[portal.agentBuilder.evals] -casesPassing = "Slučajevi prolaze" -columnCase = "Eval slučaj" -columnLatency = "Latencija" -columnResult = "Rezultat" -fail = "neuspeh" -goldenSetPassRate = "Stopa prolaznosti zlatnog skupa" -latencyMs = "{{ms}} ms" -notRun = "nije pokrenuto" -pass = "prolaz" -passRate = "Stopa prolaznosti" -runEvals = "Pokreni evals" - -[portal.agentBuilder.evals.empty] -description = "Evals pretvaraju vaše scenarije u ponovljiv zlatni skup. Nadogradite da biste pratili stopu prolaznosti tokom vremena i uslovili objavljivanja njome." -title = "Još nema zlatnog skupa" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "preko zlatnih skupova" -activeAgents = "Aktivni agenti" -avgPassRate = "Prosečna stopa prolaznosti eval" -fleetWide = "na nivou flote" -latestPublished = "Poslednje objavljeno" -scenarios = "Scenariji" -testCases = "test slučajevi" -totalDescription_one = "ukupno {{count}}" -totalDescription_other = "ukupno {{count}}" - -[portal.agentBuilder.scenarios] -add = "Dodaj" -addScenario = "Dodaj scenario" -enable = "Omogući" -expectationLabel = "Očekivano ponašanje" -expectationPlaceholder = "Šta agent treba da uradi" -inEval = "u eval" -mute = "Utišaj" -muted = "utišano" -nameLabel = "Naziv" -namePlaceholder = "npr. Eskalacija usklađenosti" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenariji" -tools = "Alati" -versions = "Verzije" - -[portal.agentBuilder.tools] -broadAccess = "Širok pristup" -deniedHint = "Izabrani alati su blokirani. Sve ostalo ostaje dostupno za pozivanje." -deniedTools = "Zabranjeni alati" -governanceGate = "Upravljanje alatima je dostupno na Enterprise planu." -restricted = "Ograničeno" -restrictedAccess = "Ograničen pristup alatima" -restrictedDescription = "Dozvolite svaki alat osim onih koje zabranite ispod." - -[portal.agentBuilder.versions] -current = "trenutna" -historyGate = "Puna istorija verzija i vraćanje su dostupni na Enterprise planu." -publish = "Objavi" -rollBack = "Vrati nazad" - [portal.assistant] close = "Zatvori asistenta" error = "Nije moguće doći do asistenta." @@ -6365,9 +6451,21 @@ typing = "Kuca" loading = "Učitavanje" redirectingToEditor = "Preusmeravanje u editor..." +[portal.billing.activation] +maybeLater = "Možda kasnije" +title = "Uključite Procesor" + +[portal.billing.activation.payg] +sub = "Postavite mesečni limit. Plaćajte ono što pokrenete." +title = "Plaćanje po potrošnji" + +[portal.billing.activation.prepay] +badge = "2 meseca besplatno" +sub = "Jedna faktura, kartica ili bankovni transfer." +title = "Platite godinu unapred" + [portal.billing.checkout] noClientSecret = "Edge funkcija nije vratila client_secret." -subtitle = "Dodajte karticu da biste nastavili nakon besplatnog Editor plana. Stripe obrađuje ostalo." title = "Uključite Processor plan" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Vaša uplata je uspela, ali aktivacija traje malo duže nego obično. Uk close = "Zatvori" title = "Skoro gotovo" +[portal.billing.checkout.cap] +amountAria = "Mesečni limit potrošnje" +back = "Nazad" +continue = "Nastavi na plaćanje" +error = "Nije moguće postaviti limit potrošnje" +finePrint = "Fakture se izdaju mesečno. Otkažite bilo kada." +noLimit = "Bez limita" +note = "Nikada vam se ne naplaćuje preko limita. Obrada se samo pauzira." +perMonth = "/ mes" +title = "Postavite limit potrošnje" + +[portal.billing.checkout.card] +fields = "Broj kartice · MM / GG · CVC · ZIP" +label = "Podaci kartice" +note = "Podatke kartice prikuplja Stripe. Stirling nikada ne čuva PAN ili CVC." + [portal.billing.checkout.error] title = "Nije moguće pokrenuti checkout" @@ -6383,10 +6497,9 @@ body = "Vaša uplata je prošla. Uključujemo mereno procesiranje na vašim pove hint = "Držite ovaj prozor otvoren." title = "Aktiviranje vašeg Processor plana..." -[portal.billing.checkout.notConfigured] -bodyAfter = "u portal env da biste omogućili checkout u aplikaciji." -bodyBefore = "Podesite" -title = "Stripe nije konfigurisan" +[portal.billing.checkout.pay] +limit = "Mesečni limit potrošnje {{amount}}/mes. Obrada se pauzira na limitu." +limitNoCap = "Bez limita potrošnje. Obrada se neće pauzirati. Fakture se izdaju mesečno." [portal.billing.enterpriseUpsell] cta = "Sastavite svoju Enterprise ponudu" @@ -6409,10 +6522,12 @@ currentPlan = "Trenutni plan" freeForever = "Besplatno zauvek" noTeamResolved = "Još nije razrešen tim na vašem novčaniku — osvežite i pokušajte ponovo." ownerOnly = "Samo vlasnik tima može da uključi Processor plan." +payInvoice = "Platite fakturu za završetak" planName = "Editor" ssoIncluded = "SSO uključen" switchOnProcessor = "Uključi Processor →" unlimitedUsers = "Neograničen broj korisnika" +viewQuote = "Prikaži ponudu" [portal.billing.invoices] columnAmount = "Iznos" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direktni API zahtevi" segmentApiLabel = "API" segmentAutomationDesc = "Automatizacije i pipeline-ovi" segmentAutomationLabel = "Automatizacija" +sizeMultiplier = "{{formatted}} PDF-ova je koristilo množilac veličine" +summary = "{{unique}} jedinstvenih · {{units}} obračunskih jedinica · {{avg}} prosek po PDF-u" +summaryNoRate = "{{unique}} jedinstvenih · {{units}} obračunskih jedinica" unit = "mereni PDF dokumenti" +unitsPending_one = "{{units}} obračunska jedinica čeka sinhronizaciju iz povezanih instanci" +unitsPending_other = "{{units}} obračunskih jedinica čeka sinhronizaciju iz povezanih instanci" + +[portal.billing.prepaid] +capSuffix = "od {{total}} unapred plaćenih kredita" +expires = "Ističe {{date}}" +eyebrow = "Unapred plaćeni kapacitet" +note = "Koristi se pre obračuna po potrošnji i izvan vašeg limita potrošnje." +topUp = "Dopuni" + +[portal.billing.prepaid.buy] +back = "Nazad" +calcTitle = "Izračunajte godišnju uplatu" +cancel = "Otkaži" +cancelPurchase = "Otkaži kupovinu" +close = "Zatvori" +continueToPayment = "Nastavi na plaćanje" +finalise = "Završi" +invoiceErrorTitle = "Nije moguće generisati fakturu" +loadingTitle = "Učitavanje vaše ponude" +payTitle = "Platite svoju godinu" +step = "Korak {{current}} od {{total}}" + +[portal.billing.prepaid.calc] +change = "Promeni" +deployRow = "Implementacija" +done = "Gotovo" +enterpriseHint = "Ovo je enterprise nivo. Pri ovom obimu, enterprise cene nadmašuju svaki self-serve popust. Zaključavanje cene, uslovi i ponuda za nekoliko minuta." +governanceRow = "Upravljanje" +governanceValue = "{{label}} · {{count}} pravila" +handlesLabel = "Vaš Procesor" +handlesValue = "obrađuje {{volume}} kredita / mes" +pipelinesLabel = "Tokovi obrade" +policiesMeta = "{{count}} pravila" +poolCaption = "Jedan skup od {{credits}} kredita za godinu. Teži meseci pozajmljuju od lakših." +priceLabel = "Vaša godina · 12 meseci po ceni 10" +rateUnknown = "Tačnu cenu ćemo prikazati pri plaćanju." +savings = "Štedite {{amount}} · 2 meseca besplatno." +sizingRow = "Dimenzionisanje" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Ukupno korisnika" + +[portal.billing.prepaid.consent] +label = "Razumem da, kada se moj unapred plaćeni kapacitet potroši ili istekne posle 12 meseci, obrada automatski nastavlja po standardnoj stopi plaćanja po potrošnji (do mog limita potrošnje) osim ako ne otkažem, i da mogu da otkažem bilo kada iz portala za naplatu." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Upravlja Stirling. Uživo za nekoliko minuta." +cloudValue = "Stirling Cloud · Upravljano" +selfhost = "Self-hosted" +selfhostDesc = "Radi na privatnoj infrastrukturi. Ista cena." +selfhostValue = "Self-hosted · Privatna infrastruktura" + +[portal.billing.prepaid.offer] +cta = "Pregledaj ponudu" +subtitle = "Platite unapred godinu obrade PDF-ova i dobijte dva meseca besplatno, koristi se pre obračuna po potrošnji, izvan vašeg limita potrošnje." +title = "Dobijte 12 meseci po ceni 10" + +[portal.billing.prepaid.pay] +accountName = "Ime vlasnika naloga" +accountNamePlaceholder = "npr. Jane Smith" +companyName = "Naziv kompanije (opciono)" +companyNamePlaceholder = "npr. Acme Inc." +downloadInvoice = "Preuzmi fakturu" +payOnline = "Plati online" +poNumber = "PO broj (opciono)" +poNumberPlaceholder = "npr. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Napredno" +advancedDesc = "Tokovi obrade pokreću većinu vaše obrade." +none = "Nijedan" +noneDesc = "Još ne pokrećete tokove obrade. Uključite ih bilo kada." +standard = "Standardno" +standardDesc = "Nekoliko tokova obrade ponovo obrađuje pristigle PDF-ove." + +[portal.billing.prepaid.posture] +essentials = "Osnove" +essentialsDesc = "Klasifikacija, podrazumevano, plus Deljenje" +governed = "Upravljano" +governedDesc = "Dodaje Bezbednost i Usmeravanje" +regulated = "Regulisano" +regulatedDesc = "Svaka kategorija, uklj. Usklađenost, Zadržavanje, Unos" + +[portal.billing.prepaid.review] +download = "Preuzmi ponudu (PDF)" +downloading = "Priprema…" +share = "podeli za odobrenje" + +[portal.billing.prepaid.size] +compact = "Kompaktno" +compactDesc = "Datoteke ispod 25 MB, bez troškova za podatke" +heavy = "Teško" +heavyDesc = "Skenirano ili sa mnogo slika, redovno 50 MB+" +standard = "Standardno" +standardDesc = "Uglavnom malo, neki skenovi preko 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Potrošeno" +healthy = "Dovoljno preostalo" +low = "Pri kraju" [portal.billing.spendLimit] adjustLimit = "Podesi limit" @@ -6494,6 +6713,8 @@ label = "Projektovano prekoračenje." [portal.billing.spendThisMonth] eyebrow = "Potrošnja ovog meseca" +freeRemaining_one = "Preostaje {{formatted}} besplatan PDF" +freeRemaining_other = "Preostaje {{formatted}} besplatnih PDF-ova" processed_one = "Obrađen {{formattedCount}} PDF dokument." processed_other = "Obrađeno {{formattedCount}} PDF dokumenata." processedWithRate_one = "Obrađen {{formattedCount}} PDF dokument, po {{rate}} svaki." @@ -6510,6 +6731,7 @@ reachedTitle = "Dostignut je mesečni limit potrošnje" title = "Nije moguće otvoriti Stripe portal" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "od {{allowance}} besplatnih PDF dokumenata iskorišćeno" capSuffix_other = "od {{allowance}} besplatnih PDF dokumenata iskorišćeno" eyebrow = "Processor probni period" @@ -6534,6 +6756,376 @@ signature = "potpis" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Nova veza" +createTitleFor = "Poveži {{name}}" +delete = "Izbriši" +edit = "Uredi" +editTitle = "Uredi vezu" +unsupportedType = "Ovaj tip veze se ne može uređivati ovde." + +[portal.connections.categories.advanced] +description = "Za uslugu koju još ne podržavamo." +label = "Napredno" + +[portal.connections.categories.audit] +description = "Zabeležite šta se dogodilo sa dokumentom tamo gde vaši auditori već gledaju." +label = "Audit i evidencija usklađenosti" + +[portal.connections.categories.notify] +description = "Obavestite drugi sistem da se nešto dogodilo." +label = "Obaveštenja i tok rada" + +[portal.connections.categories.security] +description = "Skenirajte ili označite dokument pre nego što nastavi dalje." +label = "Bezbednost i klasifikacija" + +[portal.connections.categories.signing] +description = "Pošaljite dokument na potpisivanje ili notarizaciju." +label = "E-potpis i notarizacija" + +[portal.connections.categories.storage] +description = "Gde obrađeni dokumenti završavaju - bucket, drive, tiket ili stranica." +label = "Datoteke i prilozi" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling ovo čuva šifrovano i nikada ga više ne prikazuje." +label = "Pristupni token" + +[portal.connections.commonFields.apiKey] +label = "API ključ" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Koraci koji koriste ovu vezu mogu pozivati samo putanje ispod ovog URL-a, nikada drugi host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Lozinka" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Korisničko ime" + +[portal.connections.commonFields.webhookUrl] +helperText = "Nalepite URL koji vam je usluga dala. Mogu se pozivati samo putanje ispod njega." +label = "Webhook URL" + +[portal.connections.fields] +name = "Naziv veze" +namePlaceholder = "npr. Produkcija" + +[portal.connections.picker] +cancel = "Otkaži" +createNew = "Nova veza..." +placeholder = "Izaberite vezu" +save = "Sačuvaj vezu" + +[portal.connections.picker2] +back = "Promeni integraciju" +heading = "Šta želite da povežete?" +noResultsBody = "Još to ne podržavamo - recite nam o tome i razmotrićemo dodavanje." +noResultsTitle = "Ništa se ne poklapa sa \"{{query}}\"" +searchPlaceholder = "Pretražite integracije - probajte \"sign\", \"ocr\" ili naziv proizvoda" + +[portal.connections.types.api] +description = "Pozovite bilo koji HTTP API iz pravila. Birate URL, autentifikaciju i payload." +label = "Prilagođeni API" + +[portal.connections.types.api.fields.authType] +label = "Autentifikacija" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Korisničko ime i lozinka" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token u prilagođenom header-u" + +[portal.connections.types.api.fields.authType.options.none] +label = "Nijedna" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Prijava za token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Koraci koji koriste ovu vezu mogu pozivati samo putanje ispod ovog URL-a, nikada drugi host." +label = "Osnovni URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Naziv header-a" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Putanja za prijavu" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Lozinka" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Ako API odgovori linkom za preuzimanje, mogu se preuzeti samo ovi hostovi. Razdvojite zarezima. Ostavite prazno da dozvolite samo sopstveni host osnovnog URL-a." +label = "Dozvoljeni hostovi rezultata" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Pošalji token kao ovaj header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Response header u kojem login poziv vraća token, npr. X-Auth-Token." +label = "Header odgovora tokena" + +[portal.connections.types.api.fields.username] +label = "Korisničko ime" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skenirajte dokumente na malware na serveru koji pokrećete - ništa ne napušta vašu mrežu." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Skeniranje virusa i sadržaja za dokumente u pravilu." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blokirajte dokumente koji sadrže makroe, izvršne datoteke, skripte ili ugrađene objekte - ne samo poznati malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Priložite obrađene dokumente Confluence stranici." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Pošaljite dokumente u ConsignO Cloud (Notarius) za elektronski potpis i notarizaciju." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Vaše ConsignO Cloud okruženje, uključujući /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API ključ" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API tajna" + +[portal.connections.types.consigno.fields.password] +label = "Lozinka" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Potrebno samo ako vaša organizacija koristi tenant-e." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Korisničko ime" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Objavite poruku na Discord kada pravilo obradi dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indeksirajte audit događaj u Elasticsearch svaki put kada pravilo obradi dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Proveravajte FTP ili FTPS server za nove dokumente." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Režim veze" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktivno" +passive = "Pasivno (preporučeno)" + +[portal.connections.types.ftp.fields.security] +helperText = "Običan FTP šalje kredencijale i datoteke nešifrovano; izaberite FTPS ako server to podržava." +label = "Šifrovanje" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (eksplicitno / AUTH TLS)" +implicit = "FTPS (implicitno)" +none = "Nijedno (običan FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Objavite poruku u Google Chat prostoru kada se pravilo pokrene." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Priložite obrađene dokumente Jira issue-u." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Pošaljite dokument ili obaveštenje email-om kada se pravilo pokrene." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Sačuvajte obrađene dokumente u Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Lozinka aplikacije" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Otkrijte lične podatke u tekstu dokumenta koristeći Presidio server koji pokrećete." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Čitajte i primenjujte Microsoft Purview oznake osetljivosti na PDF-ove." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Opciono. Potrebno samo za izbor oznaka po nazivu; označavanje radi i bez toga." +label = "ID aplikacije (klijenta)" + +[portal.connections.types.purview.fields.clientSecret] +label = "Tajna klijenta" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Utiskuje se na svaku oznaku kao njen SiteId, kako bi alati svesni Purview-a znali koja organizacija ju je primenila." +label = "ID direktorijuma (tenant)" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket iz kojeg izvori mogu čitati i u koji izlazi pipeline-a mogu pisati." +label = "S3 skladište" + +[portal.connections.types.s3.fields.accessKeyId] +label = "ID pristupnog ključa" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Ostavite prazno za AWS. Podesite ovo za MinIO ili drugo S3-kompatibilno skladište." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Tajni pristupni ključ" + +[portal.connections.types.sendgrid] +description = "Pošaljite dokument ili obaveštenje email-om kada se pravilo pokrene." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Proveravajte SFTP (SSH) server za nove dokumente." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Opciono. SHA-256 otisak host ključa servera (iz `ssh-keyscan host | ssh-keygen -lf -`). Kada je podešen, prihvata se samo server koji predstavlja ovaj ključ; kada je prazno, ključ viđen pri prvom povezivanju se pinuje i kasnije promene se odbijaju." +label = "Otisak host ključa" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Passphrase ključa" + +[portal.connections.types.sftp.fields.password] +helperText = "Ostavite prazno ako se autentifikujete privatnim ključem umesto toga." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Opciono. Nalepite OpenSSH privatni ključ za autentifikaciju ključem umesto lozinkom." +label = "Privatni ključ" + +[portal.connections.types.slack] +description = "Objavite poruku na Slack kada pravilo obradi dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Nadzirite SMB / CIFS mrežno deljenje (Windows ili Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Mrežno deljenje" + +[portal.connections.types.smb.fields.domain] +helperText = "Opciono. Windows domen ili radna grupa, ako vaš server koristi jedno od toga." +label = "Domen" + +[portal.connections.types.smb.fields.share] +label = "Deljenje" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Pošaljite audit događaj u Splunk svaki put kada pravilo obradi dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Pošaljite audit događaj Sumo Logic HTTP collector-u." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Objavite poruku na Teams kanalu kada se pravilo pokrene." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Objavite svaki dokument, ili samo detalje, na URL koji izaberete." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Pokrenite Zap ili Make scenario iz pravila." +label = "Zapier i Make" + +[portal.docs] +browse = "Pregledaj dokumentaciju" +viewSource = "Prikaži izvor na GitHub-u" + [portal.docs.authentication] codeCaption = "svaki zahtev" eyebrow = "POČETAK" @@ -6624,6 +7216,9 @@ eyebrow = "VEŠTINE" lead = "Ugrađene, imenovane mogućnosti koje vaš agent poziva kao jedan alat. Svaka veština je deterministički lanac operacija sa priloženim evals." title = "Veštine agenta" +[portal.docs.toc] +title = "Na ovoj stranici" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCA" @@ -6697,6 +7292,8 @@ type = "Tip" user = "Korisnik" [portal.documents.queue.empty] +connectSource = "Povežite izvor" +createPipeline = "Kreirajte tok obrade" description = "Kako izvori budu slali dokumente u vaše procesne tokove, pojavljivaće se ovde za proveru." title = "Nema dokumenata u redu" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "Nijedan dokument ne odgovara ovom filteru." rowActions = "Radnje reda" sensitiveLabel = "Osetljivo" -sensitiveTitle = "Osetljivo — potreban pristup" [portal.documents.table.columns] action = "Procesni tok / Radnja" document = "Dokument" +labels = "Labels" product = "Proizvod" status = "Status" time = "Vreme" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Poslednji put viđeno" region = "Region" status = "Status" +target = "Target" version = "Verzija" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Ovaj prikaz je naišao na neočekivanu grešku. Pokušajte ponovo retry = "Pokušajte ponovo" title = "Nešto je pošlo naopako na ovoj stranici" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Nazad" +body = "Preuzmite desktop aplikaciju ili self-hostujte server." +done = "Gotovo" +guide = "Otvori {{name}} vodič" +openInBrowser = "Otvori u pregledaču umesto toga" +sectionDesktop = "Desktop aplikacija" +sectionSelfHosted = "Self-hosted" +title = "Instalirajte Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Pokrenite zvaničnu sliku na svom serveru." +note = "Otvara se na :8080. Podrazumevana prijava: admin / stirling." +tagline = "Pokrenite server u kontejneru" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Instalirajte zvanični Helm chart." +note = "Dodajte PVC za /configs da bi se podešavanja sačuvala." +tagline = "Implementirajte u klaster pomoću Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Nativne verzije za vašu distribuciju." +guideBtn = "Prikaži opcije instalacije za Linux" +note = "AppImage, .deb i .rpm verzije su dostupne za uobičajene distribucije." +tagline = "Nativna desktop aplikacija za vašu distribuciju" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Ili preko Homebrew" +detailBody = "Pokrenite instalacioni program ili koristite Homebrew." +downloadBtn = "Preuzmi za macOS" +tagline = "Nativna aplikacija za Apple silicon i Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Pokrenite .jar na bare metal serveru." +downloadBtn = "Preuzmi .jar" +note = "Zahteva Java 21+. Otvara se na :8080; podrazumevana prijava admin / stirling." +runLabel = "Zatim ga pokrenite" +tagline = "Pokrenite .jar na bare metal serveru" +title = "Ručno podešavanje servera" + +[portal.home.download.windows] +altLabel = "Ili preko winget" +detailBody = "Pokrenite instalacioni program ili koristite menadžer paketa." +downloadBtn = "Preuzmi instalacioni program" +tagline = "Nativna aplikacija za Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktivnih" -invite = "Pozovi članove tima" +activeOfDeployed = "{{active}} od {{total}} aktivno ovog meseca" name = "Stirling PDF Editor" open = "Otvori u pregledaču" updated = "ažurirano {{time}}" +[portal.home.editor.deploy] +finish = "Završi implementaciju" +options = "Prikaži opcije implementacije" +start = "Implementiraj Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Dobar dan" evening = "Dobro veče" morning = "Dobro jutro" -[portal.home.onboarding] -dismiss = "Odbaci podešavanje" -notStarted = "Nije započeto" -progress = "{{done}} od {{total}} završeno" -title = "Završite podešavanje" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC za celu organizaciju, ugovorene cene po obimu i postavljanje u izolovanoj mreži." -cta = "Započni probu" -ctaQuote = "Zatraži ponudu" -lead = "Za 250+ zaposlenih." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Postavite desktop aplikaciju, zauvek besplatno." -title = "Preuzmite PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktivno · {{recommended}} preporučeno" -chip = "{{active}} aktivno" -title = "Uključite politike" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} povezano · svaki PDF pod upravljanjem gde god da stigne" -chip = "{{connected}} povezano" -title = "Povežite svoje izvore" - -[portal.home.quickActions] -subtitle = "Najvažniji zadaci za danas" -title = "Brze radnje" - -[portal.home.quickActions.buildPipeline] -blurb = "Kompozitor u 3 koraka preko tipizirane biblioteke operacija" -title = "Napravite procesni tok" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenti, webhooks, nadgledani folderi" -title = "Povežite izvor" - -[portal.home.quickActions.issueApiKey] -blurb = "Ključ sa opsegom, ograničenjima stope i IP allowlist" -title = "Izdajte API ključ" - [portal.infrastructure] manageEditorDeployment = "Upravljajte postavljanjem Editora" sectionsAriaLabel = "Odeljci infrastrukture" @@ -6878,13 +7541,9 @@ heading = "API ključevi" subheading = "Akreditivi sa opsegom, ograničenjima stope po ključu, dozvolama i IP allowlist." [portal.infrastructure.apiKeys.card] -allowedIps = "Dozvoljene IP adrese" -anyIp = "Bilo koja IP adresa (bez allowlist)" created = "Kreirano" lastUsed = "Poslednje korišćeno" -permissions = "Dozvole" -rateLimit = "Ograničenje stope" -rateLimitValue = "{{value}} zahteva/min" +revoke = "Opozovi ključ" usageMonth = "Korišćenje ovog meseca" usageToday = "Korišćenje danas" @@ -6892,6 +7551,15 @@ usageToday = "Korišćenje danas" description = "Kreirajte ključ sa opsegom da biste počeli da pozivate Stirling API." title = "Još nema API ključeva" +[portal.infrastructure.apiKeys.error] +load = "Nije moguće učitati vaše API ključeve. Pokušajte ponovo." + +[portal.infrastructure.apiKeys.revoke] +body = "Opozvati „{{name}}”? Svaka integracija koja i dalje koristi ovaj ključ odmah će početi da dobija 401 greške. Ovo se ne može opozvati." +cancel = "Otkaži" +confirm = "Opozovi ključ" +title = "Opozovi API ključ" + [portal.infrastructure.audit] filterAriaLabel = "Filtriraj audit događaje po kategoriji" heading = "Audit zapisi" @@ -6935,6 +7603,7 @@ all = "Sve" auth = "Auth" config = "Konfiguracija" elevation = "Povišenje pristupa" +policy = "Politika" processing = "Obrada" security = "Bezbednost" @@ -6944,19 +7613,30 @@ title = "Nemate pristup" [portal.infrastructure.audit.metrics] config = "Konfiguracija" -elevation = "Povišenje pristupa" +policy = "Politika" processing = "Obrada" totalEvents = "Ukupno događaja · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Autentikacija" +config = "Konfiguracija" +elevation = "Povećanje privilegija" +policy = "Politika" +processing = "Obrada" +security = "Bezbednost" + +[portal.infrastructure.auditStatusLabel] +danger = "Greška" +info = "Informacija" +success = "Uspeh" +warning = "Upozorenje" + [portal.infrastructure.createKey] cancel = "Otkaži" createKey = "Kreiraj ključ" done = "Gotovo" -ipAllowlistHelper = "CIDR opsezi odvojeni zarezima. Ostavite prazno da dozvolite bilo koju IP adresu." -ipAllowlistLabel = "IP allowlist" keyNameLabel = "Naziv ključa" keyNamePlaceholder = "npr. Produkcija · unos" -permissionsLabel = "Dozvole" secretKeyCaption = "Tajni ključ" secretWarning = "Sačuvajte ovo u menadžeru tajni. Stirling uvek čuva samo hash — nema načina da se kasnije oporavi." subtitle = "Ograničite opseg ključa na minimum koji mu je potreban. Možete ga rotirati ili opozvati u bilo kom trenutku." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopirajte ovu tajnu sada — neće biti ponovo prikazana." title = "Kreiraj API ključ" titleCreated = "Ključ je kreiran" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Postavio" -environment = "Okruženje" -product = "Proizvod" -status = "Status" -version = "Verzija" -when = "Kada" - -[portal.infrastructure.deployments.recent] -heading = "Nedavna postavljanja" -subheading = "Najnovija uvođenja kroz proizvode i okruženja." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instance" -latency = "Latencija" -load = "Opterećenje" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Propusnost" -uptime = "Vreme rada" -version = "Verzija" - -[portal.infrastructure.deployments.regions] -heading = "Regioni" -subheading = "Uživo stanje svakog postavljenog Stirling regiona — latencija, opterećenje i verzija uvođenja." - -[portal.infrastructure.deployments.regions.empty] -description = "Postavljeni regioni će se ovde pojaviti kada vaš radni prostor bude obezbeđen." -title = "Nema postavljenih regiona" - -[portal.infrastructure.models] -heading = "Modeli" -msValue = "{{value}} ms" -subheading = "Katalog modela i rutiranje koje pokreće obradu dokumenata u vašem radnom prostoru." - -[portal.infrastructure.models.byom] -description = "Registrujte on-prem ili self-hosted model i vežite ga za region radi obrade u skladu sa rezidentnošću podataka." -title = "Donesite sopstveni model" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Upravljani modeli dostupni vašem radnom prostoru, sa latencijom i cenom uživo." -subEnterprise = "Upravljani, sopstveni i on-prem modeli — sa dostupnim vezivanjem po regionu." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeli u katalogu vašeg radnog prostora pojavljuju se ovde." -title = "Nema dostupnih modela" - -[portal.infrastructure.models.columns] -cost = "Cena" -latency = "Latencija" -load = "Opterećenje" -model = "Model" -status = "Status" -type = "Tip" -version = "Verzija" - -[portal.infrastructure.models.metrics] -activeModels = "Aktivni modeli" -avgLatency = "Prosečna latencija" -included = "Uključeno" -monthlySpend = "Mesečna potrošnja za modele" - -[portal.infrastructure.models.routing] -empty = "Nema konfigurisanih pravila rutiranja." -heading = "Pravila rutiranja" -sub = "Koji model obrađuje svaku operaciju. Podrazumevano se primenjuje kada se ne poklopi uže pravilo." -subLocked = "Rutirajte operacije ka određenim modelima — dostupno na plaćenim planovima." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Nadogradite na Pro da kontrolišete koji model obrađuje svaku operaciju i tip dokumenta." -title = "Rutiranje modela je plaćena funkcija" - -[portal.infrastructure.models.routingColumns] -default = "Podrazumevano" -docType = "Tip dokumenta" -modelForAria = "Model za {{operation}}" -operation = "Operacija" -routedTo = "Rutirano ka" - -[portal.infrastructure.security.access.byok] -description = "Obezbedite ključ iz sopstvenog KMS. Stirling šifruje njime, ali i dalje može da čita." -label = "Donesite sopstveni ključ (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ključevi nikada ne napuštaju vaš KMS. Stirling čuva samo šifrovani tekst." -label = "Zadržite sopstveni ključ (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling upravlja ključevima za šifrovanje. Najjednostavnije — bez upravljanja ključevima sa vaše strane." -label = "Ključevi koje drži Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Politika pristupa dokumentima" -subheading = "Kontroliše ko može da dešifruje obrađene dokumente u mirovanju." - -[portal.infrastructure.security.attestations] -heading = "Potvrde usklađenosti" -noReport = "Nema dostupnog izveštaja" -subheading = "Audit položaj po okvirima, sa izveštajima dostupnim za potvrđene kontrole." -viewReport = "Prikaži izveštaj →" - -[portal.infrastructure.security.compliance] -heading = "Usklađenost" -subheading = "Potvrde i sertifikati koji pokrivaju Stirling platformu." - -[portal.infrastructure.security.empty] -description = "Bezbednosna konfiguracija vašeg radnog prostora će se ovde pojaviti." -title = "Bezbednosni položaj nije dostupan" - -[portal.infrastructure.security.hyokBanner] -description = "Uz HYOK, ključevi za šifrovanje nikada ne napuštaju vaš KMS. Stirling čuva i obrađuje samo šifrovani tekst koji možete opozvati u bilo kom trenutku." -title = "Stirling ne može da dešifruje vaše dokumente" - -[portal.infrastructure.security.ipAllowlist] -empty = "Nema konfigurisanih IP opsega — sve IP adrese su dozvoljene." -heading = "IP allowlist" -sub = "API pristup je ograničen na ove CIDR opsege." -subLocked = "Ograničite API pristup na poznate IP opsege — dostupno na plaćenim planovima." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Nadogradite na Pro da ograničite API pristup na određene mreže." -title = "IP allowlisting je plaćena funkcija" - -[portal.infrastructure.security.ipColumns] -added = "Dodato" -addedBy = "Dodao" -cidr = "CIDR" -label = "Oznaka" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Upravljanje ključevima za šifrovanje" -keyId = "Identifikator ključa" -lastRotated = "Poslednja rotacija" -rotateKey = "Rotiraj ključ" -rotationPolicy = "Politika rotacije" -subheading = "Starateljstvo nad ključevima koji šifruju dokumente u mirovanju — ko može da dešifruje i kako se ključevi rotiraju." - -[portal.infrastructure.security.managedBanner] -description = "Starateljstvo po principu donesite sopstveni ključ (BYOK) i zadržite sopstveni ključ (HYOK) dostupno je na Enterprise planu. Nadogradite da obezbedite ključeve iz sopstvenog KMS." -title = "Ključevima na vašem planu upravlja Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Azijsko-pacifički region" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR granica podataka" -label = "Evropska unija" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Sjedinjene Države" - -[portal.infrastructure.security.residencyHeader] -heading = "Rezidentnost podataka" -subheading = "Gde se dokumenti čuvaju i obrađuju." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} iskorišćeno" - -[portal.infrastructure.storage.empty] -description = "Povezano skladište i korišćenje će se ovde pojaviti." -title = "Nema konfigurisanog skladišta" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktivno" -activeRange = "0–{{value}}d" -archived = "Arhivirano" -coldStorage = "hladno skladište" -deleted = "Obrisano" -never = "nikada" -purged = "trajno obrisano" - -[portal.infrastructure.storage.providers] -connect = "Poveži" -connected = "Povezano" -heading = "Povezani pružaoci" -subheading = "Gde se upisuju obrađeni artefakti." - -[portal.infrastructure.storage.retention] -heading = "Zadržavanje" -subheading = "Koliko dugo se artefakti čuvaju pre brisanja tokom životnog ciklusa." -windowLabel = "Podrazumevani period zadržavanja" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dan" -days_other = "{{count}} dana" -never = "Nikada ne briši" - -[portal.infrastructure.storage.totalUsage] -heading = "Ukupno korišćenje" -progressLabel = "Iskorišćeno skladište" -subheading = "Skladište potrošeno kroz sve povezane pružaoce." +revoked = "Opozvano" [portal.infrastructure.tabs] apiKeys = "API ključevi" @@ -7174,6 +7656,49 @@ models = "Modeli" security = "Bezbednost" storage = "Skladište" +[portal.integrations] +availableHeading = "Dostupno" +comingSoonHeading = "Uskoro" +connect = "Poveži" +connectionCount_one = "{{count}} veza" +connectionCount_other = "{{count}} veza" +customApi = "Prilagođeni API" +searchPlaceholder = "Pretraži integracije" +subtitle = "Sve sa čim Stirling može da komunicira. Akreditivi se ovde kreiraju jednom, a zatim ih koriste izvori, politike i tokovi obrade." +title = "Integracije" +typedName = "Naziv za {{tool}}" + +[portal.integrations.filters] +all = "Sve" +audit = "Revizija" +connected = "Povezano" +notify = "Obaveštavanje" +security = "Bezbednost" +signing = "Potpisivanje" +storage = "Skladište" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Povezano" + +[portal.integrations.table] +integration = "Integracija" +worksWith = "Radi sa" + +[portal.integrations.worksWith] +pipelines = "Tokovi obrade" +policies = "Politike" +sources = "Izvori" + +[portal.legal] +draft = "{{label}} · nacrt" +loadError = "Nije moguće učitati ovaj dokument. Pokušajte ponovo." +loading = "Učitavanje…" +title = "Pravni dokument" + [portal.nav] agent-builder = "Kreator agenata" components = "Komponente" @@ -7182,33 +7707,17 @@ documents = "Dokumenti" editor = "Editor" home = "Početna" infrastructure = "Infrastruktura" +integrations = "Integracije" pipelines = "Procesni tokovi" policies = "Politike" procurement = "Nabavka" -settings = "Podešavanja" sources = "Izvori" usage = "Korišćenje i naplata" users = "Korisnici" -[portal.notifications] -markAllRead = "Označi sve kao pročitano" -title = "Obaveštenja" -viewAll = "Prikaži sve" - -[portal.notifications.ariaLabel] -none = "Obaveštenja, nema nepročitanih" -unread_one = "Obaveštenja, {{count}} nepročitano" -unread_other = "Obaveštenja, {{count}} nepročitanih" - -[portal.notifications.count] -allRead = "sve pročitano" -loading = "učitavanje" -new_one = "{{count}} novo" -new_other = "{{count}} novih" - -[portal.notifications.empty] -description = "Nema novih obaveštenja." -title = "Sve ste završili" +[portal.nav.section] +platform = "PDF platforma" +processor = "PDF procesor" [portal.pipelines] subtitle = "Svaki automatizovani procesni tok dokumenata na backendu: uređeni lanac operacija nad skupom izvora, pokrenut okidačem. Kliknite na red za njegove korake i izvore." @@ -7218,54 +7727,87 @@ title = "Procesni tokovi" newPipeline = "Novi procesni tok" [portal.pipelines.builder] -addStep = "Dodaj alat" +activate = "Activate" back = "Nazad na procesne tokove" +cannotFollow = "Ne može da prihvati {{produced}}" +chooseAccount = "Izaberite nalog" +chooseDestination = "Izaberite odredište" +chooseOperation = "Izaberite šta ovaj korak radi" +chooseSource = "Izaberite izvor" discard = "Odbaci izmene" -enabled = "Omogućeno" +inputs = "Unos" +inputSource = "Izvor unosa" +inputTrigger = "Okidač" keepEditing = "Nastavi uređivanje" -needsUpload = "Potrebna je otpremljena datoteka" +moreActions = "More actions" +needsConfiguring = "Potrebno je podešavanje" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Nijedan alat ne odgovara vašoj pretrazi." -pipelineSettings = "Podešavanja procesnog toka" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Pretraži alate" -selectToolBody = "Dodajte alat da napravite procesni tok." -selectToolTitle = "Još nema alata" -toolSettings = "Podešavanja alata" +sendToSystem = "Pošalji u drugi sistem" +stepsIncompatible = "Ovi koraci ne mogu da se pokrenu na onome što njihov prethodni korak proizvodi: {{tools}}." +stepsNeedSetup = "Ove korake i dalje treba podesiti pre čuvanja: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Neprepoznata operacija, zadržana kakva jeste." unsavedBody = "Imate nesačuvane izmene. Sačuvajte ih pre napuštanja ili ih odbacite?" unsavedTitle = "Nesačuvane izmene" -uploadUnsupported = "Otpremljene datoteke još nisu podržane u procesnim tokovima, pa ovi koraci ne mogu biti sačuvani: {{tools}}." usesDefaults = "Pokreće se sa podrazumevanim podešavanjima" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinuje svaku datoteku iz prethodnog koraka" +fan-out = "Pokreće se jednom po datoteci iz prethodnog koraka" +format-mismatch = "Zahteva {{accepts}}, ali prethodni korak proizvodi {{produced}}" +output-uncertain = "Možda se neće pokrenuti: izlaz prethodnog koraka zavisi od njegovog podešavanja" +source-mismatch = "Zahteva {{accepts}}, ali unos ovog toka obrade je {{produced}}" +undeclared-operation = "Nije moguće proveriti šta ovaj korak prihvata" [portal.pipelines.composer] addTool = "Dodaj alat" -cancel = "Otkaži" -chainEmpty = "Dodajte alat da počnete da pravite procesni tok." create = "Kreiraj procesni tok" -directory = "Izlazni folder" -directoryHelp = "Apsolutna putanja na serveru. Mora biti unutar konfigurisanih dozvoljenih foldera." +createPaused = "Create paused" editingUnsupported = "Prikaz ovih parametara alata za uređivanje još nije podržan." -moveDown = "Pomeri dole" -moveUp = "Pomeri gore" +editSource = "Edit source" name = "Naziv" namePlaceholder = "npr. Prolaz za redakciju" -noSources = "Još nema povezanih izvora. Procesni tok i dalje može da se pokreće nad datotekama koje mu se direktno dostave." noToolSettings = "Ovaj alat nema podesiva podešavanja." -operations_one = "Operacija ({{count}})" -operations_other = "Operacije ({{count}})" output = "Izlaz" -removeStep = "Ukloni operaciju" save = "Sačuvaj izmene" scheduleEvery = "Pokreni svakih" -sources = "Izvori" -sourcesLoading = "Učitavanje izvora..." trigger = "Okidač" triggerManual = "Samo ručno" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dana" hours = "sati" minutes = "minuta" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Obrisati \"{{name}}\"? Ovo se ne može opozvati." cancel = "Otkaži" @@ -7273,28 +7815,62 @@ confirm = "Obriši" title = "Obrisati procesni tok?" [portal.pipelines.detail] +clearHistory = "Obriši istoriju" delete = "Obriši procesni tok" run = "Pokreni sada" [portal.pipelines.empty] action = "Kreiraj procesni tok" +connectSource = "Poveži izvor" description = "Kreirajte svoj prvi procesni tok: izaberite izvore nad kojima se pokreće, povežite operacije u lanac i izaberite gde izlaz ide." title = "Još nema procesnih tokova" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktivni" paused = "Pauzirani" total = "Procesni tokovi" -[portal.pipelines.output] -folder = "Upiši u folder" -inline = "Vrati datoteke" - [portal.pipelines.run] +allProcessed_one = "Nema šta da se pokrene: {{count}} dokument iz izvora je već obrađen." +allProcessed_other = "Nema šta da se pokrene: svih {{count}} dokumenata u izvorima je već obrađeno." completed_one = "Pokretanje je završeno." completed_other = "Svih {{count}} pokretanja je završeno." empty = "Nema ničega za pokretanje: izvori nisu imali dokumente za obradu." failed = "Pokretanje nije uspelo: {{error}}" +inFlight = "Nema ničeg novog za pokretanje: dokumenti se još obrađuju iz ranijeg pokretanja." +parked_one = "Nema šta da se pokrene: {{count}} dokument prethodno nije uspeo i parkiran je. Otklonite uzrok, zatim obrišite istoriju da biste pokušali ponovo." +parked_other = "Nema šta da se pokrene: {{count}} dokumenata prethodno nije uspelo i parkirani su. Otklonite uzrok, zatim obrišite istoriju da biste pokušali ponovo." running = "Pokretanje je započeto; još je u toku." timeout = "Pokretanje traje duže od očekivanog; možda će se i dalje završiti u pozadini." @@ -7307,6 +7883,7 @@ name = "Procesni tok" sources = "Izvori" status = "Status" steps = "Koraci" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Nadzor foldera" @@ -7314,12 +7891,18 @@ manual = "Ručno" schedule = "Zakazano" [portal.policies] +defaultName = "Politika za {{category}}" subtitle = "Stalne automatizacije koje primenjuju procesni tok alata na svaki dokument. Svaka politika se aktivira pri otpremanju ili izvozu, pokreće svoj lanac alata i čuva primenjenu verziju pored originala." title = "Politike" [portal.policies.card] comingSoon = "Nadogradite na Enterprise" notSetUp = "Nije podešeno" +requiresAiEngine = "Zahteva AI engine" + +[portal.policies.categories.classification] +desc = "Identifikujte tip svakog dokumenta pri otpremanju i označite njegove metapodatke radi arhiviranja i pretrage." +label = "Klasifikacija" [portal.policies.categories.compliance] desc = "Sprovedite HIPAA, GDPR, SOC 2 ili FedRAMP zahteve na svakom dokumentu." @@ -7344,6 +7927,13 @@ label = "Bezbednost" [portal.policies.config] scopeAll = "Svi dokumenti" +[portal.policies.config.classification] +summary = "Klasifikuje svaki otpremljeni dokument prema oznakama vašeg tima i označava njegove metapodatke." + +[portal.policies.config.classification.rules] +0 = "Klasifikuj" +1 = "Označi metapodatke" + [portal.policies.config.compliance] summary = "Validira dokumente prema regulatornim okvirima pre nego što napuste sistem." @@ -7371,6 +7961,29 @@ minConfidence = "Min. pouzdanost" 2 = "Imenuj" 3 = "Normalizuj" +[portal.policies.config.purview] +label = "Primeni Microsoft Purview oznaku osetljivosti" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "ID oznake" +labelIdHelp = "GUID oznake, sa Purview portala. Primena oznake upisuje njene metapodatke u PDF; ne može da šifruje, za šta je potreban Microsoft klijent." +labelName = "Naziv oznake (opciono)" +labelNameHelp = "Beleži se uz ID kako bi ljudi i alati mogli da prepoznaju oznaku." + +[portal.policies.config.purview.fields.method] +help = "Purview razlikuje oznaku koju je primenila politika od one koju je izabrala osoba." +label = "Kako se oznaka beleži" +privileged = "Kao što je izabrala osoba" +standard = "Kao automatski primenjeno" + +[portal.policies.config.purviewRead] +label = "Pročitaj Microsoft Purview oznaku" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Čita oznaku koju dokument već nosi, kako bi kasniji koraci mogli da deluju na osnovu nje." + [portal.policies.config.retention] summary = "Sprovodi koliko dugo se dokumenti čuvaju, kada se arhiviraju i kada se brišu." @@ -7438,6 +8051,7 @@ title = "Još nema aktivnosti" [portal.policies.endpoints] addWatermark = "Vodeni žig" autoRedact = "Rediguj PII" +classifyAndLabel = "Klasifikuj" compressPdf = "Kompresuj" flatten = "Poravnaj" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Vaše politike su sačuvane i pojaviće se kada se veza obnovi." retry = "Pokušaj ponovo" title = "Backend nije dostupan" +[portal.policies.operations] +change = "Promeni šta ovaj korak radi" +noResults = "Nijedan korak se ne poklapa sa tim. Pokušajte sa nazivom proizvoda ili „scan”, „notify”, „attach”." +searchPlaceholder = "Pretraži korake - pokušajte „scan”, „attach” ili naziv proizvoda" + +[portal.policies.operations.bodyMode] +binary = "Sirovi bajtovi datoteke" +json = "Unutar JSON payload-a" +multipart = "Kao otpremanje datoteke (multipart)" + +[portal.policies.operations.clamavScan] +description = "Vaš sopstveni ClamAV server proverava dokument; ništa ne napušta vašu mrežu." +label = "Skeniraj na viruse (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Odbija dokumente koji nose makroe, izvršne datoteke, skripte ili ugrađene objekte." +label = "Blokiraj aktivni sadržaj" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive proverava dokument i pokretanje se zaustavlja ako nije čist." +label = "Skeniraj na viruse" + +[portal.policies.operations.confluenceAttach] +description = "Smešta obrađeni dokument na stranicu." +label = "Priloži na Confluence stranicu" + +[portal.policies.operations.consignoSubmit] +description = "Šalje dokument u ConsignO Cloud na potpisivanje." +label = "Pošalji na potpisivanje (ConsignO)" +note = "Šalje dokument. Preuzimanje potpisane kopije još nije podržano, jer korak ne može da prosledi rezultat kasnijem koraku." + +[portal.policies.operations.customApiCall] +description = "Sami definišite poziv: putanju, metod, telo i obradu odgovora." +label = "Pozovi prilagođeni API" + +[portal.policies.operations.discordNotify] +description = "Obaveštava kanal da je politika obradila dokument." +label = "Objavi poruku na Discord" + +[portal.policies.operations.elasticIndex] +description = "Beleži šta je politika uradila kao pretraživ dokument." +label = "Indeksiraj događaj revizije u Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart odgovara API-jima za otpremanje; JSON odgovara API-jima koji žele datoteku unutar payload-a; binary šalje sirove bajtove." +label = "Kako poslati dokument" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON telo se šalje takvo kakvo jeste. {{document.base64}} sadrži datoteku; {{document.*}} i {{run.*}} se popunjavaju po dokumentu." +label = "JSON telo" + +[portal.policies.operations.fields.connection] +label = "Nalog" + +[portal.policies.operations.fields.domain] +label = "Mailgun domen" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Polje obrasca pod kojim API očekuje dokument." +label = "Naziv polja datoteke" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Od" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON objekat parova naziv/vrednost zaglavlja. Akreditiv veze se dodaje za vas." +label = "Zaglavlja" + +[portal.policies.operations.fields.index] +label = "Indeks" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ključ zadatka" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Šalje se kao telo poruke. {{document.*}} i {{run.*}} se popunjavaju po dokumentu." +label = "Poruka" + +[portal.policies.operations.fields.method] +label = "Metod" + +[portal.policies.operations.fields.pageId] +label = "ID stranice" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Dodaje se na osnovni URL veze. Nikada ne može da dosegne drugi host." +label = "Putanja" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Gde na disku treba upisati dokument. {{document.filename}} se popunjava po dokumentu." +label = "Odredišna putanja" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Izveštaj ostavlja dokument neizmenjenim. Zamena ga menja onim što stigne nazad." +label = "Šta uraditi sa odgovorom" + +[portal.policies.operations.fields.signerEmail] +label = "Email potpisnika" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Naslov" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analizira ovaj tekst. Koristite {{document.title}} ili nalepite tekst koji želite da proverite." +label = "Tekst za proveru" + +[portal.policies.operations.fields.to] +label = "Za" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud korisničko ime" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Obaveštava prostor da je politika obradila dokument." +label = "Objavi poruku na Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Smešta obrađeni dokument u zadatak." +label = "Priloži uz Jira zadatak" + +[portal.policies.operations.mailgunEmail] +description = "Šalje obrađeni dokument kao prilog." +label = "Pošalji dokument email-om (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Upisuje obrađeni dokument u fasciklu." +label = "Otpremi u Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Vaš sopstveni Presidio server prijavljuje lične podatke koje pronađe." +label = "Otkrij lične podatke" +note = "Presidio čita tekst, ne datoteke. Dajte mu tekst koji želite da proverite — sam dokument se ne otprema." + +[portal.policies.operations.responseMode] +replace = "Zameni dokument odgovorom" +report = "Zadrži dokument, zabeleži odgovor" + +[portal.policies.operations.sendgridEmail] +description = "Šalje obrađeni dokument kao prilog." +label = "Pošalji dokument email-om (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Obaveštava kanal da je politika obradila dokument." +label = "Objavi poruku na Slack" + +[portal.policies.operations.splunkEvent] +description = "Beleži šta je politika uradila, tamo gde vaši revizori već gledaju." +label = "Pošalji događaj revizije u Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Beleži šta je politika uradila u HTTP kolektor." +label = "Pošalji događaj revizije u Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Obaveštava kanal da je politika obradila dokument." +label = "Objavi poruku na Teams" + +[portal.policies.operations.webhookPost] +description = "Šalje sam dokument na URL koji izaberete." +label = "Pošalji dokument na webhook" + +[portal.policies.operations.zapierNotify] +description = "Predaje detalje pokretanja vašoj automatizaciji." +label = "Pokreni Zap ili Make scenario" + [portal.policies.stats] activeFor = "Aktivno" dataProcessed = "Obrađeni podaci" @@ -7473,6 +8282,13 @@ label = "Primenjeno na dokumente" description = "Konfigurisano, ali se ne pokreće" label = "Pauzirano" +[portal.policies.table] +appliesTo = "Primenjuje se na" +docs = "Dokumenti pod pravilima" +enforces = "Sprovodi" +policy = "Politika" +status = "Status" + [portal.policies.wizard.actions] back = "Nazad" cancel = "Otkaži" @@ -7480,6 +8296,10 @@ continue = "Nastavi" enablePolicy = "Omogući politiku" saveChanges = "Sačuvaj izmene" +[portal.policies.wizard.capability.classify] +desc = "Identifikuje tip dokumenta iz oznaka vašeg tima i označava ga, kako bi se arhivirao i pretraživao po kategoriji." +label = "Klasifikuj dokument" + [portal.policies.wizard.capability.compress] desc = "Kompresuje dokument na manju veličinu datoteke." label = "Smanji veličinu datoteke" @@ -7500,10 +8320,18 @@ label = "Rediguj osetljive informacije" desc = "Uklanja skriveni JavaScript tako da ništa ne može automatski da se pokrene kada se dokument otvori." label = "Ukloni aktivni sadržaj" +[portal.policies.wizard.capability.timestampPdf] +desc = "Dokazuje da je dokument postojao u ovom tačnom obliku u određenom trenutku, koristeći nezavisni autoritet za vremenski žig. Šalje se samo hash - dokument nikada ne napušta vaš server." +label = "Dodaj pouzdani vremenski žig" + [portal.policies.wizard.capability.watermark] desc = "Stavlja vidljivu oznaku (npr. „Poverljivo“) preko svake stranice." label = "Primeni vodeni žig" +[portal.policies.wizard.classification] +description = "Svaki otpremljeni dokument se klasifikuje prema ugrađenim oznakama i označava tipovima koji mu odgovaraju. Skup oznaka se deli u celom vašem timu." +labelsHeading = "Oznake klasifikacije" + [portal.policies.wizard.errors] noTools = "Prvo omogućite bar jedan alat u toku rada." saveFailed = "Nije moguće sačuvati politiku. Pokušajte ponovo." @@ -7548,73 +8376,118 @@ setUp = "Podesi {{category}} politiku" [portal.policies.wizard.workflow] description = "Izaberite šta ova politika radi svakom dokumentu koji obrađuje." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktivno" -noRule = "Još nije primenjeno nijedno pravilo" -subtitle = "Stalne automatizacije kroz koje prolazi svaki dokument, bez obzira na to koji tok obrade ga obrađuje." -title = "Šta se pokreće na vašim PDF-ovima" - [portal.policySummary.action] -comingSoon = "Nadogradite na Enterprise" -configure = "Konfiguriši" setUp = "Podesi" -[portal.policySummary.column] -activeRule = "Aktivno pravilo" -policy = "Politika" -status = "Status" +[portal.processorFlow] +footnote = "Brojevi su za poslednja 24 sata. Brzina toka je ilustrativna." +liveBadge = "Uživo" +stats = "{{connected}} povezano · {{processed}} PDF-ova obrađeno" +title = "PDF procesor" -[portal.policySummary.empty] -description = "Kada se politike konfigurišu, kategorije će se pojaviti ovde." -title = "Još nema politika" +[portal.processorFlow.lens] +ariaLabel = "Režim prikaza" +flow = "Tok" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktivno" -off = "Isključeno" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Neuspešno" +heading = "Ishodi" +success = "Isporučeno" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktivnih" +count = "{{n}} · 24h" +heading = "Politike" +setUp = "Podesi" soon = "Uskoro" -[portal.processingStatus] -managePlan = "Upravljaj planom" -volumeSuffix = "PDF-ovi obrađeni · poslednjih 30 dana" +[portal.processorFlow.sankey] +waist = "Politike · {{n}} aktivnih" + +[portal.processorFlow.sankey.empty] +description = "Povežite izvor i uključite politiku da biste videli kako dokumenti prolaze kroz procesor." +title = "Još nema toka" + +[portal.processorFlow.sources] +comingSoonTag = "Poveži" +editor = "Stirling PDF Editor" +heading = "Izvori" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Prijem email-a" [portal.procurement] -reset = "Resetuj nabavku (demo)" subtitle = "Obavite procenu, ugovaranje i uvođenje tima. Svaki dokument na jednom mestu." title = "Nabavka" -[portal.procurement.action] -download = "Preuzmi" -pay = "Plati sada" -request = "Zatraži" -sign = "Pregledaj i potpiši" -upload = "Otpremi" - [portal.procurement.agreement] agreeCta = "Prihvati i pretplati se" +confidential = "Poverljivo" confirm = "Pročitao/la sam i prihvatam Stirling Enterprise Agreement." -eyebrow = "Ugovor" -intro = "Jedan objedinjeni ugovor pokriva vaš dogovor: Master Service Agreement, Order Form, EULA i Data Processing Agreement. Pregledajte ga, a zatim prihvatite ponudu kao obaveznu pretplatu." -title = "Pregledajte svoj enterprise ugovor" +docName = "Stirling Enterprise ugovor" +docSub = "MSA, narudžbeni obrazac, EULA i DPA, objedinjeni u jedan potpis." +download = "Preuzmi" +downloadDraftError = "Nije moguće generisati PDF ugovora - renderer dokumenta nije dostupan. Pokušajte ponovo uskoro ili kontaktirajte podršku." +downloadError = "Nije moguće preuzeti potpisani ugovor. Pokušajte ponovo." +legalName = "Naziv pravnog lica" +legalNamePlaceholder = "Pravno lice koje će potpisati" +loadError = "Nije moguće učitati ugovor. Pokušajte ponovo." +loading = "Učitavanje ugovora..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Zatraži izmene" +scrollHint = "Pomerite da pregledate" +signatory = "Ime potpisnika" +signatoryPlaceholder = "Puno ime" +signatoryTitle = "Funkcija potpisnika" +signatoryTitlePlaceholder = "npr. glavni pravni savetnik" +signError = "Nije moguće zabeležiti vaš potpis. Pokušajte ponovo." [portal.procurement.builder] addons = "Dodaci" +addressLine1 = "Adresa, red 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adresa, red 2" +addressLine2Placeholder = "Apartman, sprat, zgrada" back = "Nazad" businessName = "Naziv preduzeća" businessNamePlaceholder = "Vaša kompanija" +city = "Grad" +cityPlaceholder = "San Francisco" +completeRequired = "Popunite obavezna polja (označena *) važećim email-om pre generisanja ponude." +contactEmail = "Kontakt email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Ime kontakta" +contactNamePlaceholder = "Jane Doe" continue = "Nastavi" -country = "Zemlja" -countryEuro = "Evrozona (EUR €)" -countryUK = "Ujedinjeno Kraljevstvo (GBP £)" -countryUS = "Sjedinjene Države (USD $)" +done = "Gotovo" eula = "Pročitao/la sam i prihvatam Stirling Enterprise EULA. Ona uređuje ugovor generisan iz ove ponude." generate = "Generiši ponudu" -included = "Uključeno" indemnification = "IP obeštećenje" indemnificationSub = "Branimo kvalifikovane IP zahteve, prema EULA" -offlineLicense = "Offline / air-gapped licenca" -offlineLicenseSub = "Licencna datoteka za preuzimanje za air-gapped self-hosted instancu" +paperEyebrow = "Enterprise ponuda" +paperFor = "Pripremljeno za" +pdfSize = "Veličina PDF-a" +poNumber = "Broj narudžbenice" +poNumberPlaceholder = "Opciono" +postalCode = "Poštanski broj" +postalCodePlaceholder = "94105" +posture = "Upravljanje" +posture_count = "~{{count}} politika" +posture_essentials = "Osnovno" +posture_essentialsSub = "Klasifikacija + deljenje — podrazumevano" +posture_governed = "Upravljano" +posture_governedSub = "Dodaje bezbednost i rutiranje" +posture_regulated = "Regulisano" +posture_regulatedSub = "Svaka kategorija — usklađenost, zadržavanje, unos" qbr = "Kvartalni poslovni pregledi" qbrSub = "Vaš SE pregleda korišćenje i roadmap svakog kvartala" +region = "Država / region" +regionPlaceholder = "California" running = "{{annual}} / god · {{years}}-god {{tcv}}" s1Sub = "Vaš tim i PDF-ovi koje očekujete da obrađujete svake godine." s1Title = "Koliko ćete obrađivati?" @@ -7623,6 +8496,12 @@ s2Title = "Obaveza i usluga" s3Sub = "Za ponudu i ugovor koji ona generiše." s3Title = "Vaši podaci" serviceLevel = "Nivo usluge" +size_compact = "Kompaktno" +size_compactSub = "Uglavnom tekst, ispod 1 MB" +size_heavy = "Teško" +size_heavySub = "Skenirano ili sa mnogo slika, 5 MB+" +size_standard = "Standard" +size_standardSub = "Mešavina teksta i slika, 1 do 5 MB" slDedicated = "Dedicated" slDedicatedSub = "4 radna sata · namenski menadžer naloga · +30%" slPriority = "Priority" @@ -7630,6 +8509,8 @@ slPrioritySub = "Isti radni dan · imenovani CSM · +15%" slStandard = "Standard" slStandardSub = "Sledeći radni dan · deljeni CSM · uključeno" stepOf = "Korak {{n}} od {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Opciono" term = "Rok" termDiscount = "Primenjen je {{pct}}% popust za višegodišnju obavezu" title = "Napravite svoju ponudu" @@ -7637,6 +8518,7 @@ training = "Uvođenje i obuka" trainingSub = "Sesije uživo da vaš tim počne da radi" users = "Ukupno korisnika" usersPlaceholder = "npr. 250" +viewEula = "Pročitaj" volEstimated = "Procenjeno na osnovu {{count}} korisnika (~2.000 PDF-ova po korisniku, uključujući automatizaciju). Izmenite ako znate bolje." volManual = "Koristi se vaša vrednost. U svakom trenutku možete ponovo proceniti na osnovu veličine tima." volNoUsers = "Niste sigurni? Unesite veličinu tima i mi ćemo proceniti." @@ -7645,55 +8527,55 @@ volumePlaceholder = "npr. 1.000.000" years_one = "{{count}} godina" years_other = "{{count}} godina" -[portal.procurement.docs] -count_one = "{{count}} dok." -count_other = "{{count}} dok." -done = "Gotovo" -here = "Ovde ste" -hide = "Sakrij" -optional = "Opciono" -paidAddon = "Plaćeni dodatak" -show = "Prikaži" -subtitle = "Sve što vam je potrebno u svakom koraku puta, prikazano kako dogovor napreduje." -supportingSubtitle = "SOC 2, bezbednosne provere, poreski obrasci i još mnogo toga, spremno kada vaš tim za bezbednost ili nabavku zatraži. Neki nose jednokratnu naknadu." -supportingTitle = "Podrška vašoj proceni" +[portal.procurement.documents] +agreement = "Enterprise ugovor" +agreementSub = "MSA, narudžbeni obrazac i DPA u jednom potpisu." +download = "Preuzmi" +eula = "EULA i komercijalni uslovi" +eulaSub = "Softver i self-serve komercijalni uslovi." +invoice = "Faktura" +invoiceSub = "Vaša prva faktura za pretplatu." +laterInvoice = "Dostupno nakon što prihvatite" +laterQuote = "Dostupno kada vaša ponuda bude izdata" +quote = "Ponuda" +quoteSub = "Vaša detaljna enterprise ponuda." +sla = "SLA prilog" +slaSub = "Nivoi podrške i ciljevi vremena odgovora." +subprocessors = "Podobrađivači" +subprocessorsSub = "Treće strane koje obrađuju podatke u vaše ime." +subtitle = "Vaša dokumentacija za ugovor, dostupna u svakom trenutku." title = "Dokumenti" -upcoming = "Predstojeće" +view = "Prikaži" [portal.procurement.error] title = "Nešto je pošlo naopako" [portal.procurement.hero] -company = "Vaš enterprise dogovor" +barAria = "Faza {{current}} od {{total}}" ctaAgreement = "Pregledaj i potpiši ugovor" -ctaLive = "Uživo ste" -ctaPayment = "Dodaj plaćanje" +ctaExploring = "Podesite probni period" ctaQuote = "Pregledajte svoju ponudu" +ctaReviewQuote = "Pregledaj ponudu" ctaTrial = "Napravite svoju ponudu" +documents = "Dokumenti" eyebrow = "Enterprise nabavka" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Pozovite članove tima" -keyDocs = "Ključni dokumenti" -nextStep = "Sledeći korak: {{action}}" -notStarted = "Nije započeto" +licenseKey = "Licencni ključ" +liveSub = "Vaša organizacija je provisionirana i vaša licenca je aktivna." +liveTitle = "Uživo ste na Stirling Enterprise" +next = "Sledeće: {{stage}}" open = "Otvori nabavku" scheduleCall = "Zakaži poziv" -setup1Sub = "Pozovite članove svog tima" -setup1Title = "Postavite PDF Editor" -setup2Sub = "Uključite obradu kroz editore i druge izvore" -setup2Title = "Povežite PDF Processor" -setup3Sub = "Uključite Security, Compliance, Routing ili Retention kada vam zatrebaju" -setup3Title = "Dodajte preporučene politike" +sentenceAgreement = "pregledajte i potpišite ugovor" +sentenceLive = "provisioniranje i uvođenje" +sentencePayment = "narudžbenica i plaćanje" +sentenceQuote = "pregledajte i prihvatite ponudu" +sentenceTrial = "podesite probni radni prostor" [portal.procurement.journey] daysLeft_one = "Preostao je {{count}} dan" daysLeft_other = "Preostalo je {{count}} dana" -engineerLabel = "Vaš solutions engineer" -eyebrow = "Vaše uvođenje" -live = "Uživo ste na Stirling Enterprise" -nextStep = "Sledeći korak: {{action}}" -subtitle = "Vaš solutions engineer je uz vas u svakom koraku. Jedna sledeća radnja u datom trenutku; potpuna kontrolna lista je ispod." -title = "Od probnog perioda do rada uživo, jedan vođeni put" -trialTitle = "Enterprise probni period" [portal.procurement.journeySteps.agreement] blurb = "Jedan potpis pokriva MSA, order form, EULA i DPA." @@ -7727,6 +8609,9 @@ downloadError = "Još nije moguće generisati offline licencnu datoteku — poku downloadOffline = "Preuzmi offline licencu (.lic)" hint = "Nalepite ovaj ključ u self-hosted instancu da biste je aktivirali ili ga sačuvajte za svoju evidenciju. Čuvajte ga na sigurnom." label = "Vaš licencni ključ" +subtitle = "Aktivirajte self-hosted instancu ovim ključem ili ga sačuvajte za svoju evidenciju." +title = "Vaš licencni ključ" +trialFileHint = "Ovo je vaša probna licencna datoteka. Kada ugovor bude uspostavljen, vratite se i ponovo je preuzmite — za razliku od online licencnog ključa, .lic datoteka se neće sama ažurirati." [portal.procurement.link] cta = "Poveži nalog" @@ -7739,65 +8624,62 @@ description = "Vaša pretplata je aktivna i licenca je izdata. Vaš tim je obezb eyebrow = "Uživo" title = "Uživo ste na Stirling Enterprise" -[portal.procurement.locked] -description = "Probni ključevi, ponude za obavezni obim, ugovor sa jednim potpisom, plaćanje i vaša evidencija dokumenata nalaze se ovde kada započnete enterprise procenu." -eyebrow = "Samo Enterprise" -talkToSales = "Razgovarajte sa prodajom" -title = "Proces nabavke se otvara uz Enterprise" - [portal.procurement.milestone] -accept = "Prihvati i nastavi" -description = "Preuzmite PDF da biste ga podelili sa timom, vratite se da prihvatite kada budete spremni ili napravite izmene." -download = "Preuzmi PDF" downloadError = "Još nije moguće preuzeti PDF ponude — pokušajte ponovo za trenutak." -edit = "Izmeni ponudu" -eyebrow = "Ponuda {{number}}" -perYear = " / god" -preparedFor = "Pripremljeno za {{company}}" -tcv = "{{value}} ukupna vrednost ugovora" -title = "Vaša ponuda je spremna" - -[portal.procurement.modal] -cancel = "Otkaži" -chooseFile = "Izaberi datoteku" -close = "Zatvori" -downloadBody = "Vaše preuzimanje će uskoro početi." -downloadCta = "Preuzmi" -downloadTitle = "Preuzimanje" -noFile = "Nije izabrana datoteka" -payBody = "Platite svoj obavezni ugovor karticom ili bankovnim transferom preko Stripe. Vaš radni prostor se obezbeđuje čim se plaćanje proknjiži." -payCta = "Nastavi na Stripe" -payTitle = "Potvrdi plaćanje" -requestBodyFree = "Ovo generišemo na zahtev. Potvrdite i vaš solutions engineer će ga uskoro poslati." -requestBodyPaid = "Ovo je plaćeni dodatak. Potvrdite i vaš solutions engineer će definisati obim i poslati dokumentaciju." -requestCta = "Zatraži" -requestTitle = "Zatraži ovaj dokument" -signBody = "Otvara Stirling Enterprise Agreement za e-potpis: jedan potpis pokriva MSA, order form, EULA i DPA. Automatski supotpisujemo i prelazite na plaćanje." -signCta = "Otvori za potpis" -signTitle = "Pregledajte i potpišite svoj ugovor" -uploadBody = "Pošaljite nam svoju PO i fakturišemo prema njoj pod vašim uslovima. Prevucite PDF ili izaberite datoteku ispod." -uploadCta = "Otpremi narudžbenicu" -uploadTitle = "Otpremite svoju narudžbenicu" [portal.procurement.payment] description = "Vaša ponuda je prihvaćena i licenca je već aktivna — vaš tim može odmah da počne. Platite prvu fakturu kada budete spremni; možete je platiti ili preuzeti ovde, bez email-a." +downloadAgreement = "Preuzmi potpisani ugovor" downloadInvoice = "Preuzmi fakturu" -simulate = "Simuliraj primljenu uplatu (demo)" +eyebrow = "Plaćanje" title = "Pretplata je kreirana" viewInvoice = "Prikaži i plati fakturu" +[portal.procurement.review] +acceptCta = "Prihvati ponudu" +annual = "Godišnja naknada (1. godina)" +downloadCta = "Preuzmi ponudu" +poNumber = "Narudžbenica: {{po}}" +renewal = "Obnavlja se po {{amount}}/godina nakon isteka perioda (+{{pct}}% CPI)" +tcv = "Period od {{years}} godina, plaćeno u celosti unapred: {{tcv}}" +validUntil = "Važi do {{date}}" + [portal.procurement.schedule] fallback = "Nije moguće učitati planer." fallbackLink = "Otvori zakazivanje u novoj kartici" +loading = "Učitavanje planera..." subtitle = "Vaš solutions engineer će provesti vaš tim kroz uvođenje. Izaberite vreme koje vam odgovara." title = "Zakaži poziv" -[portal.procurement.status] -action = "Potrebna radnja" -available = "Dostupno" -complete = "Završeno" -pending = "Na čekanju" -request = "Na zahtev" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Izolovano, bez odlaznog saobraćaja" +back = "Nazad" +businessName = "Naziv preduzeća" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Upravljano, najbrži početak" +continue = "Nastavi" +deployment = "Gde treba implementirati?" +eula = "Slažem se sa Stirling EULA i komercijalnim uslovima." +fullName = "Puno ime" +fullNamePlaceholder = "Vaše ime" +invites = "Pozovite članove tima · opciono" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Zakaži poziv" +seats = "Osobe koje testiraju" +seatsPlaceholder = "npr. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker ili Kubernetes" +start = "Započni probni period" +stepOf = "Korak {{n}} od {{total}}" +subtitle = "Besplatno 14 dana, kartica nije potrebna." +subtitleDetails = "Nekoliko detalja za vašu ponudu i ugovor." +talkFirst = "Radije biste prvo razgovarali?" +title = "Podesite probni period" +viewEula = "Pročitaj" +workEmail = "Poslovni email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Produženje dodaje 7 dana i obaveštava vašeg solutions engineer-a." @@ -7808,109 +8690,113 @@ maxed = "Maksimalno iskorišćeno" subtitle = "Vaš besplatni probni period traje do {{date}}. Kartica nije potrebna." title = "Enterprise probni period" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Cena za obavezni obim, SSO + SCIM + RBAC na nivou organizacije, 90-dnevni nepromenljivi audit i namenski SE." -homeCta = "Započni probni period →" -homeHeadline = "Obradite milione PDF-ova." - -[portal.recentActivity] -title = "Nedavna aktivnost" -viewAll = "Prikaži sve" - -[portal.recentActivity.empty] -description = "Pokretanja tokova obrade, deploy-ovi i događaji agenata pojaviće se ovde." -title = "Ovde još nema ničega" - -[portal.search] -ariaLabel = "Pretraga" -placeholder = "Pretražite Stirling — endpoint-i, tokovi obrade, dokumenti…" - -[portal.search.empty] -noActionsDescription = "Brze radnje će se pojaviti ovde kada budu dostupne." -noActionsTitle = "Nema brzih radnji" -noMatches = "Nema poklapanja za \"{{query}}\"" -noMatchesDescription = "Pokušajte sa drugom ključnom reči ili pregledajte katalog." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Povezivanje naloga" -[portal.shell.header] -accountFallback = "Nalog" -accountMenu = "Meni naloga" -darkMode = "Tamni režim" -lightMode = "Svetli režim" -search = "Pretraga" -searchPlaceholder = "Pretraga…" -signOut = "Odjavi se" -switchToDark = "Prebaci na tamnu temu" -switchToLight = "Prebaci na svetlu temu" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dok." linkAccount = "Poveži Stirling nalog" -planEditor = "Editor plan" -planEnterprise = "Enterprise plan" -planProcessor = "Processor plan" primaryNav = "Primarna navigacija" switchApp = "Promeni aplikaciju" +[portal.shell.topbar] +closeNav = "Zatvori navigaciju" +openNav = "Otvori navigaciju" +search = "Pretraga" + [portal.sources] subtitle = "Ponovo upotrebljive ulazne veze koje dostavljaju dokumente u Stirling. Konfigurišite vezu jednom, a zatim je referencirajte iz bilo kog broja politika. Kliknite na red za njenu konfiguraciju i politike koje je koriste." title = "Izvori" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Poveži izvor" +[portal.sources.builder] +back = "Nazad na izvore" +backToTypes = "Svi tipovi izvora" +cancel = "Otkaži" +chooseHint = "Izaberite odakle dokumenti dolaze. Zasivljeni konektori stižu uskoro." +comingSoon = "Uskoro" +comingSoonHeading = "Uskoro" +create = "Kreiraj izvor" +createTitle = "Poveži izvor" +delete = "Izbriši" +editTitle = "Uredi izvor" +enabled = "Omogućeno" +save = "Sačuvaj izmene" + +[portal.sources.builder.folderAccess] +description = "Automatizacije fascikli mogu koristiti samo fascikle koje je administrator dozvolio. Dodajte je u podešavanjima pristupa fasciklama, zatim pokušajte ponovo." +openSettings = "Podešavanja pristupa fasciklama" +title = "Ova fascikla nije dozvoljena" + [portal.sources.delete] body = "Izbrisati \"{{name}}\"? Ovo se ne može opozvati. Politike koje ga referenciraju moraće da se ažuriraju." cancel = "Otkaži" confirm = "Izbriši" title = "Izbrisati izvor?" -[portal.sources.detail] -closeAriaLabel = "Zatvori detalje" -delete = "Izbriši izvor" -docs24h = "Poslednja 24 h" -docs30d = "Poslednjih 30 dana" -docsTotal = "Ukupno viđeno" -docsTrend = "Dokumenti tokom poslednjih 30 dana" -documents = "Dokumenti" -edit = "Izmeni" -notReferenced = "Ne referencira ga nijedna politika, pa je bezbedno za brisanje." -pause = "Pauziraj" -resume = "Nastavi" -subtitle = "{{type}} · {{status}}" -usedBy = "Koristi ga" - -[portal.sources.empty] -description = "Povežite fasciklu (a uskoro i cloud skladište) kako bi vaše politike imale odakle da preuzimaju dokumente." -title = "Još nema povezanih izvora" - [portal.sources.kpi] inUse = "U upotrebi" total = "Veze" unused = "Neiskorišćeno" +[portal.sources.networkFields.connection] +helperText = "Sačuvana veza sa adresom servera i akreditivima. Ponovo je koristi svaki izvor koji se na nju poziva." +label = "Veza" + +[portal.sources.networkFields.directory] +helperText = "Fascikla na serveru koja se proverava, relativno na početnu fasciklu za prijavu ili koren deljenja. Ostavite prazno za koren." +label = "Fascikla" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Obrada jednom uklanja svaku datoteku sa servera kada je svaka politika obradi." +label = "Režim čitanja" + +[portal.sources.networkFields.mode.options] +consume = "Obrada jednom: obradi svaku datoteku jednom" +snapshot = "Snimak: ponovo pročitaj fasciklu pri svakom pokretanju" + +[portal.sources.networkFields.recursive] +label = "Dubina fascikle" + +[portal.sources.networkFields.recursive.options] +all = "Uključi podfascikle" +top = "Samo najviši nivo" + [portal.sources.status] active = "Aktivno" disabled = "Onemogućeno" unused = "Neiskorišćeno" [portal.sources.table] +documents = "Dokumenti" source = "Izvor" status = "Status" +type = "Type" usedBy = "Politike" +[portal.sources.types.box] +description = "Preuzimajte dokumente iz Box fascikli koje vaš tim već deli." +label = "Box" + +[portal.sources.types.dropbox] +description = "Preuzimajte dokumente iz deljene Dropbox fascikle." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokumenti koje je vaš tim obradio u editoru, kroz pokretanja politika i AI." label = "Editor" +noPolicies = "Još nema politika pokrenutih iz editora." + +[portal.sources.types.email] +description = "Pretvorite priloge poslate u namensko poštansko sanduče u dokumente." +label = "Email prijemno sanduče" [portal.sources.types.folder] description = "Prati direktorijum na serveru za nove dokumente." @@ -7943,25 +8829,80 @@ label = "Dubina fascikle" all = "Uključi podfascikle" top = "Samo najviši nivo" +[portal.sources.types.ftp] +description = "Proverava fasciklu FTP ili FTPS servera za nove dokumente." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Preuzimajte dokumente iz deljenih Google Drive fascikli." +label = "Google Drive" + +[portal.sources.types.network] +description = "Prati SMB mrežno deljenje za nove dokumente." +label = "Mrežni disk" + +[portal.sources.types.onedrive] +description = "Prati OneDrive fascikle za nove dokumente." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Preuzimajte dokumente iz Amazon S3 ili S3-kompatibilnog bucket-a." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Sačuvana veza koja sadrži bucket i akreditive. Ponovo je koristi svaki izvor i izlaz toka obrade koji se na nju poziva." +label = "Veza" + +[portal.sources.types.s3.fields.mode] +helperText = "Obrada jednom uklanja svaki objekat iz bucket-a kada ga svaka politika obradi." +label = "Režim čitanja" + +[portal.sources.types.s3.fields.mode.options] +consume = "Obrada jednom: obradi svaki objekat jednom" +snapshot = "Snimak: ponovo pročitaj bucket pri svakom pokretanju" + +[portal.sources.types.s3.fields.prefix] +helperText = "Obrađuju se samo objekti čiji ključevi počinju ovim prefiksom." +label = "Prefiks ključa" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Proverava SFTP drop fasciklu za nove dokumente." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Preuzimajte dokumente iz SharePoint biblioteka dokumenata." +label = "SharePoint" + [portal.sources.types.unknown] label = "Izvor" +[portal.sources.types.webhook] +createNote = "URL za isporuku i tajna za potpisivanje generišu se kada kreirate ovaj webhook. Pošiljaoci šalju dokumente POST metodom na taj URL i politike koje se na njega pozivaju pokreću se po prijemu." +description = "Primajte dokumente putem potpisanog HTTP POST iz bilo kog spoljnog sistema." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "URL za isporuku" +secretNote = "Tajna za potpisivanje prikazuje se jednom, kada se webhook kreira. Ponovo kreirajte izvor da biste je rotirali." + +[portal.sources.types.webhook.reveal] +copy = "Kopiraj" +done = "Gotovo" +secret = "Tajna za potpisivanje" +secretHelp = "Potpišite sirovo telo svake isporuke ovim ključem (HMAC-SHA256) i pošaljite ga kao X-Stirling-Signature zaglavlje." +secretWarning = "Kopirajte tajnu za potpisivanje sada. Radi vaše bezbednosti prikazuje se samo jednom i ne može se kasnije preuzeti." +title = "Webhook kreiran" +url = "URL za isporuku" +usage = "Pošaljite svaki dokument POST metodom kao sirovo telo zahteva na URL za isporuku sa binarnim tipom sadržaja (application/pdf ili application/octet-stream). Politike koje se na njega pozivaju automatski se pokreću po prijemu." + [portal.sources.wizard] -back = "Nazad" -cancel = "Otkaži" -continue = "Nastavi" -editTitle = "Izmeni izvor" name = "Naziv" namePlaceholder = "npr. Prijem zahteva" -save = "Sačuvaj izmene" -subtitle = "Korak {{current}} od {{total}} · {{label}}" -title = "Poveži izvor" type = "Tip" -[portal.sources.wizard.steps] -chooseType = "Izaberite tip" -configure = "Konfiguriši" -review = "Pregledaj i poveži" +[portal.stepModal] +close = "Zatvori" [portal.usage] managePayment = "Upravljaj plaćanjem" @@ -8021,16 +8962,9 @@ summary = "Vlasnik tima — upravlja resursima njegovih članova i deljenim konf 2 = "Pristup portalu putem podrazumevane politike" 3 = "Sve što Član može da uradi" -[portal.welcome] -ariaLabel = "Dobro došli u Stirling PDF" -badge = "Open-source" -installEditor = "Instaliraj Editor" -inviteTeammates = "Pozovite članove tima" -openInBrowser = "Otvori u pregledaču" -perks = "Besplatno zauvek · Self-hostable" -subtitle = "Najbezbedniji PDF Editor na svetu je besplatan za timove svih veličina. Uključuje 60+ PDF operacija i SSO." -title = "Dobro došli u" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Neograničeno" [printFile] title = "Odštampaj datoteku" @@ -8613,6 +9547,14 @@ text = "Rotirajte stranice PDF-a u smeru ili suprotno od kazaljke na satu u kora [rotate.tooltip.header] title = "Pregled podešavanja rotacije" +[ruler] +clearAll = "Obriši sve" +hideAllLabels = "Sakrij sve oznake" +hideSmallLabels = "Sakrij male oznake" +physicalValues = "Fizičke" +scaled = "Skalirano" +showAllLabels = "Prikaži sve oznake" + [sanitize] desc = "Uklonite potencijalno štetne elemente iz PDF datoteka." sanitizationResults = "Rezultati sanitizacije" @@ -8654,31 +9596,45 @@ label = "Ukloni XMP metapodatke" [sanitize.steps] settings = "Podešavanja" -[sanitizePdf] -tags = "čišćenje,bezbednost,bezbedno,ukloni-pretnje" - [sanitizePDF] title = "Sanitizacija PDF-a" +[sanitizePdf] +tags = "čišćenje,bezbednost,bezbedno,ukloni-pretnje" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Podesi razmeru stranica" +[scaleSettings] +activeScale = "Aktivna razmera" +apply = "Primeni razmeru" +applyCalibration = "Primeni kalibraciju" +calculatedScale = "Izračunata razmera" +calibrate = "Kalibriši" +calibrating = "Kalibrisanje" +calibrationDistanceRequired = "Unesite stvarnu udaljenost veću od nule" +calibrationInvalid = "Nije moguće izračunati razmeru iz ovog merenja" +calibrationPaperDistance = "Izmerena udaljenost na stranici: {{distance}}" +calibrationTitle = "Kalibracija razmere" +calibrationTooltip = "Izmerite poznatu udaljenost da biste automatski izračunali razmeru" +customScale = "Prilagođena razmera" +noneSet = "Nije podešena prilagođena razmera" +presets = "Unapred podešene razmere" +ratio = "Odnos razmere" +ratioHelp = "Odnos: 1 jedinica stranice = X stvarnih jedinica" +ratioPlaceholder = "npr. 100" +ratioPositive = "Odnos mora biti veći od nule" +ratioRequired = "Odnos je obavezan" +realDistance = "Stvarna udaljenost" +realDistancePlaceholder = "npr. 5" +reset = "Vrati na podrazumevano" +unit = "Jedinica" +unitInvalid = "Nevažeća jedinica: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Ugao praga:" -10 = "Postavlja veličinu ivice dodate i uklonjene kako bi se sprečile bele ivice u izlazu (podrazumevano: 1)." -2 = "Postavlja minimalni apsolutni ugao potreban za rotiranje slike (podrazumevano: 10)." -3 = "Tolerancija:" -4 = "Određuje opseg varijacije boja oko procenjene boje pozadine (podrazumevano: 30)." -5 = "Minimalna površina:" -6 = "Postavlja minimalni prag površine za fotografiju (podrazumevano: 10000)." -7 = "Minimalna površina konture:" -8 = "Postavlja minimalni prag površine konture za fotografiju" -9 = "Veličina ivice:" - [scannerImageSplit] submit = "Izdvoji skenove slika" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Šta radi" whatThisDoesDesc = "Automatski pronalazi i izdvaja svaku fotografiju sa skenirane stranice ili kompozitne slike—bez ručnog isecanja." whenToUse = "Kada koristiti" +[ScannerImageSplit.selectText] +1 = "Ugao praga:" +10 = "Postavlja veličinu ivice dodate i uklonjene kako bi se sprečile bele ivice u izlazu (podrazumevano: 1)." +2 = "Postavlja minimalni apsolutni ugao potreban za rotiranje slike (podrazumevano: 10)." +3 = "Tolerancija:" +4 = "Određuje opseg varijacije boja oko procenjene boje pozadine (podrazumevano: 30)." +5 = "Minimalna površina:" +6 = "Postavlja minimalni prag površine za fotografiju (podrazumevano: 10000)." +7 = "Minimalna površina konture:" +8 = "Postavlja minimalni prag površine konture za fotografiju" +9 = "Veličina ivice:" + [search] noResults = "Nema rezultata" placeholder = "Unesite pojam pretrage..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Vaš Stirling-PDF server je van mreže i \"{{endpoint [session] expired = "Istekla ti je sesija. Osveži stranicu i pokušaj ponovo." -[sessionManagement.tooltip] -header = "Upravljanje sesijama potpisivanja" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Novi učesnici se dodaju na kraj redosleda potpisivanja" -bullet2 = "Ne možete dodavati učesnike nakon finalizacije sesije" -bullet3 = "Svaki učesnik dobija obaveštenje kada dođe na red" -description = "Možete dodati još učesnika u aktivnu sesiju u bilo kom trenutku pre finalizacije." -title = "Dodavanje učesnika" - -[sessionManagement.tooltip.finalization] -bullet1 = "Potpuna finalizacija: Svi učesnici su potpisali" -bullet2 = "Delimična finalizacija: Neki učesnici još nisu potpisali" -bullet3 = "Učesnici koji nisu potpisali biće isključeni iz konačnog dokumenta" -bullet4 = "Nakon finalizacije možete učitati potpisani PDF u aktivne datoteke" -description = "Finalizacija kombinuje sve potpise u jedan potpisani PDF. Ova radnja se ne može opozvati." -title = "Finalizacija sesije" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Ne možete ukloniti učesnike koji su već potpisali" -bullet2 = "Uklonjeni učesnici više ne dobijaju obaveštenja" -bullet3 = "Redosled potpisivanja se automatski prilagođava" -description = "Učesnici se mogu ukloniti iz sesije pre nego što potpišu." -title = "Uklanjanje učesnika" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Svaki potpis se primenjuje sekvencijalno na PDF" -bullet2 = "Kasniji potpisnici mogu videti ranije potpise" -bullet3 = "Ključno za tokove odobravanja i pravni lanac čuvanja" -description = "Redosled koji navedete pri kreiranju sesije određuje ko potpisuje prvi." -title = "Redosled potpisivanja" - [settings] close = "Zatvori" +[settings.ai] +documents = "Dokumenti i RAG" +general = "Opšte" +limits = "Ograničenja i performanse" +models = "Modeli i provajderi" +title = "AI" + [settings.configuration] advanced = "Napredno" database = "Baza podataka" endpoints = "Krajnje tačke" features = "Funkcije" +folderAccess = "Pristup fasciklama" mcp = "MCP Server" storageSharing = "Skladištenje datoteka i deljenje" systemSettings = "Sistemska podešavanja" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Podrazumevani PDF uređivač" defaultPdfEditorActive = "Stirling PDF je vaš podrazumevani PDF uređivač" defaultPdfEditorChecking = "Provera..." defaultPdfEditorInactive = "Druga aplikacija je postavljena kao podrazumevana" +defaultPdfEditorRemind = "Podseti me da postavim kao podrazumevano" +defaultPdfEditorRemindDescription = "Prikaži baner kada Stirling PDF nije vaša podrazumevana PDF aplikacija." defaultPdfEditorSet = "Već podrazumevano" defaultStartupView = "Podrazumevani prikaz pri pokretanju" defaultStartupViewDescription = "Izaberite koja je kartica aktivna u levoj koloni kada se aplikacija pokrene" @@ -8817,9 +9763,18 @@ languageDescription = "Izaberite jezik prikaza" logout = "Odjavi se" setAsDefault = "Postavi kao podrazumevano" theme = "Tema" +themeAccent = "Akcentna boja" +themeAccentCustom = "Prilagođena boja" +themeAccentCustomHint = "Prilagođeno (preporučuju se podrazumevana podešavanja)" +themeAccentDark = "Akcentna boja tamnog režima" +themeAccentDefault = "Podrazumevano" +themeAccentDefaultHint = "Podrazumevana tema (preporučeno)" +themeAccentDescription = "Dugmad, linkovi i istaknuti elementi je prate, a aplikaciji daje suptilnu nijansu. Svetli i tamni režim imaju zasebne. Sistem koristi onu koja je aktivna." +themeAccentLight = "Akcentna boja svetlog režima" themeDark = "Tamna" themeDescription = "Prebacujte između svetlog i tamnog režima" themeLight = "Svetla" +themeReset = "Vrati temu na podrazumevano" themeSystem = "System" title = "Opšte" user = "Korisnik" @@ -8833,6 +9788,12 @@ intro = "Omogućite autentifikaciju korisnika, upravljanje timom i funkcije radn learnMore = "Saznaj više u dokumentaciji" title = "Za sistemske administratore" +[settings.general.loginLanding] +description = "Izaberite koja se aplikacija otvara kada se prijavite u Stirling Cloud." +editor = "Uređivač" +processor = "Procesor" +title = "Nakon prijavljivanja" + [settings.general.mode] fullscreen = "Ceo ekran" sidebar = "Bočna traka" @@ -9013,9 +9974,6 @@ title = "Politike i privatnost" [settings.preferences] title = "Preferencije" -[settings.search] -placeholder = "Pretražite stranice podešavanja..." - [settings.security] description = "Ažurirajte lozinku da biste svoj nalog održali bezbednim." title = "Bezbednost" @@ -9041,6 +9999,11 @@ security = "Bezbednost" telegram = "Telegram" title = "Bezbednost i autentikacija" +[settings.signOut] +confirm = "Da li ste sigurni da želite da se odjavite?" +submit = "Odjavi se" +title = "Odjava" + [settings.team] title = "Tim" @@ -9156,6 +10119,7 @@ last = "Poslednja strana" maintainRatio = "Uključi/isključi zadržavanje proporcija" next = "Sledeća strana" noSavedSigs = "Nema snimljenih potpisa" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Lični potpisi" previous = "Prethodna strana" redo = "Ponovi" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Uklanjanje pozadine nije uspelo" hint = "Otpremite PNG ili JPG sliku svog potpisa" label = "Otpremite sliku potpisa" placeholder = "Izaberite slikovnu datoteku" +previewAlt = "Current image signature" processing = "Obrada slike..." removeBackground = "Ukloni belu pozadinu (učini providnom)" @@ -9207,6 +10172,17 @@ saved = "Izaberite sačuvani potpis iznad, zatim kliknite bilo gde na PDF da ga text = "Nakon što unesete svoje ime iznad, kliknite bilo gde na PDF da postavite svoj potpis." title = "Kako dodati potpis" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Pomeraj potpis" pause = "Pauziraj postavljanje" @@ -9653,10 +10629,30 @@ title = "Otpremi na server" updateButton = "Ažuriraj na serveru" uploadButton = "Otpremi na server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "kliknite ovde" +fallback = "Ako se nije automatski otvorilo," +opening = "Otvaranje Swagger UI u novoj kartici..." tags = "api,documentation,swagger,endpoints,development" title = "API dokumentacija" @@ -9764,12 +10760,39 @@ selectFilesHint = "Izaberite datoteke u Aktivne datoteke da biste pokrenuli ovaj singleFileScope = "Primena samo na: {{fileName}}" viewerMode = "Prebacite se u uređivač datoteka da biste izabrali više datoteka." +[toolFormat] +ANY = "bilo koja datoteka" +CBR = "arhiva stripa (CBR)" +CBZ = "arhiva stripa (CBZ)" +CSV = "CSV datoteka" +EBOOK = "e-knjiga" +EMAIL = "e-poruka" +EXCEL = "proračunska tabela" +HTML = "web stranica" +IMAGE = "slike" +JAVASCRIPT = "JavaScript datoteka" +JSON = "JSON datoteka" +MARKDOWN = "Markdown datoteka" +NONE = "nema datoteke" +PCL = "PCL datoteka za štampu" +PDF = "PDF" +PDF_ENCRYPTED = "PDF zaštićen lozinkom" +POSTSCRIPT = "PostScript datoteka" +PPT = "prezentacija" +TEXT = "tekstualna datoteka" +VIDEO = "video" +WORD = "Word dokument" +XML = "XML datoteka" +XPS = "XPS dokument" +ZIP = "ZIP arhiva" + [toolPanel] alpha = "Alfa" backToAllTools = "Nazad na sve alate" collapse = "Skupi panel" expand = "Proširi panel" goBack = "Idi nazad" +pdfTools = "PDF alati" placeholder = "Izaberite alat da biste započeli" premiumFeature = "Premium funkcija:" toolsHeader = "Alati" @@ -9806,11 +10829,6 @@ noToolsFound = "Nema pronađenih alata" quickAccess = "BRZI PRISTUP" searchPlaceholder = "Pretraži alate..." -[toolPicker.categories] -advancedTools = "Napredni alati" -recommendedTools = "Preporučeni alati" -standardTools = "Standardni alati" - [toolPicker.subcategories] advancedFormatting = "Napredno formatiranje" automation = "Automatizacija" @@ -9824,6 +10842,9 @@ removal = "Uklanjanje" signing = "Potpisivanje" verification = "Verifikacija" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Nema pronađenih alata" noTools = "Nema dostupnih alata" @@ -9950,28 +10971,53 @@ title = "Korisnici" you = "(vi)" [users.action] +cancelInvite = "Otkaži poziv" deleteTeam = "Izbriši tim" disableMfa = "Resetuj MFA" move = "Premesti u tim" reinstate = "Vrati" remove = "Ukloni iz organizacije" +removeTeam = "Ukloni iz tima" rename = "Preimenuj tim" resetPw = "Resetuj lozinku" suspend = "Suspenduj" unlock = "Otključaj nalog" +[users.activity] +daysAgo = "Pre {{count}} d" +hoursAgo = "Pre {{count}} h" +justNow = "Upravo sada" +minutesAgo = "Pre {{count}} min" +monthsAgo_one = "Pre {{count}} mesec" +monthsAgo_other = "Pre {{count}} meseci" +never = "Nikada" +weeksAgo_one = "Pre {{count}} nedelju" +weeksAgo_other = "Pre {{count}} nedelja" +yearsAgo_one = "Pre {{count}} godinu" +yearsAgo_other = "Pre {{count}} godina" + [users.cap] addProcessor = "+ Processor" approver = "Odobrava politiku" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Otkažite poziv za {{email}}? Ova osoba neće moći da se pridruži pomoću trenutnog linka." +cancelInviteTitle = "Otkaži poziv" deleteTeamBody = "Izbrisati tim {{name}}? Tim prvo mora biti prazan - premestite njegove članove u drugi tim, i ne sme još uvek da poseduje konfiguracije integracija." deleteTeamTitle = "Izbriši tim" disableMfaBody = "Ukloniti MFA registraciju za {{name}}? Ponovo će je podesiti pri sledećoj prijavi ako je potrebna." disableMfaTitle = "Resetuj MFA" removeBody = "Trajno ukloniti {{name}} iz organizacije? Ovo se ne može opozvati." +removeTeamBody = "Ukloniti {{name}} iz tima? Zadržaće nalog, ali će izgubiti pristup resursima ovog tima." removeTitle = "Ukloni člana" [users.empty] @@ -9982,10 +11028,8 @@ title = "Još nema članova" addToTeam = "Dodaj u tim" guestCount = "{{count}} gost" guests = "Gosti" -guestsDesc = "Spoljni saradnici, ograničeni na ono što ste podelili. Samo Editor." ledBy = "vodi {{owner}}" org = "Organizacija" -orgDesc = "Vlasnici sa ovlašćenjima na nivou organizacije i odobravanjem politika" owners = "{{count}} vlasnik" team = "{{name}} tim" teamMeta = "{{count}} osoba" @@ -10022,6 +11066,18 @@ username = "Korisničko ime" usernameError = "Korisničko ime mora imati najmanje 3 karaktera" usernamePlaceholder = "jsmith" +[users.invites] +by = "Pozvao/la {{who}}" +cancel = "Otkaži" +expiresInDays_one = "Ističe za {{count}} dan" +expiresInDays_other = "Ističe za {{count}} dana" +expiresToday = "Ističe danas" +title = "Pozivi na čekanju" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Nešto je pošlo naopako pri kontaktiranju backend-a ili nemate pristup. Pokušajte ponovo." title = "Nije moguće učitati članove" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Pretraži priloge" title = "Prilozi" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Naslov obeleživača" +bookmarkTitleRequired = "Naslov obeleživača je obavezan" closeSidebar = "Zatvori bočnu traku obeleživača" collapseAll = "Skupi sve obeleživače" +empty = "No bookmarks in this document" expandAll = "Proširi sve obeleživače" +searchPlaceholder = "Pretraži obeleživače" +title = "Bookmarks" [viewer.comments] addComment = "Dodaj komentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Pronađi u dokumentu" moreActions = "Više radnji" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Stranica {{page}}" placingHint = "Kliknite na stranicu da postavite… (otkaži)" removeCommentOnly = "Ukloni samo komentar" saveReply = "Sačuvaj odgovor" +searchPlaceholder = "Search comments" title = "Komentari" typeComment = "Komentar" typeInsertText = "Umetni tekst" @@ -10262,6 +11325,7 @@ unsavedDesc = "Imate nesačuvane promene" closeSidebar = "Zatvori bočnu traku slojeva" hideAll = "Sakrij sve slojeve" showAll = "Prikaži sve slojeve" +title = "Layers" [viewer.link] delete = "Obriši link" @@ -10605,30 +11669,6 @@ title = "Tekst vodenog žiga" image = "Slika" text = "Tekst" -[wetSignature.tooltip] -header = "Metode kreiranja potpisa" - -[wetSignature.tooltip.draw] -bullet1 = "Prilagodite boju i debljinu olovke" -bullet2 = "Brišite i crtajte ponovo dok ne budete zadovoljni" -bullet3 = "Radi na uređajima na dodir (tableti, telefoni)" -description = "Napravite rukom pisani potpis pomoću miša ili ekrana osetljivog na dodir. Najbolje za lične, autentične potpise." -title = "Nacrtaj potpis" - -[wetSignature.tooltip.type] -bullet1 = "Izaberite među više fontova" -bullet2 = "Prilagodite veličinu i boju teksta" -bullet3 = "Idealno za standardizovane potpise" -description = "Generišite potpis iz unetog teksta. Brzo i dosledno, pogodno za poslovna dokumenta." -title = "Unesi potpis" - -[wetSignature.tooltip.upload] -bullet1 = "Podržava PNG, JPG i druge formate slika" -bullet2 = "Za najbolje rezultate preporučuju se providne pozadine" -bullet3 = "Slika će biti prilagođena veličini oblasti potpisa" -description = "Otpremite unapred napravljenu sliku potpisa. Idealno ako imate skenirani potpis ili logo kompanije." -title = "Otpremi sliku potpisa" - [workbenchBar] activeFiles = "Aktivne datoteke" annotations = "Anotacije" @@ -10642,6 +11682,7 @@ exitRedaction = "Izađi iz režima zacrnjivanja" exportAll = "Izvezi PDF" exportSelected = "Izvezi izabrane stranice" formFill = "Popuni obrazac" +hideToolbar = "Hide toolbar" multiTool = "Višenamenski alat" panMode = "Režim pomeranja" print = "Štampaj PDF" @@ -10652,6 +11693,7 @@ redact = "Zacrni" rotateLeft = "Rotiraj ulevo" rotateRight = "Rotiraj udesno" ruler = "Lenjir / Merenje" +rulerSettings = "Podešavanja razmere" save = "Sačuvaj" saveAll = "Sačuvaj sve" saveAs = "Sačuvaj kao" @@ -10661,6 +11703,7 @@ selectAll = "Izaberi sve" selectByNumber = "Izaberi po brojevima stranica" selectLanguage = "Izaberi jezik" share = "Podeli" +showToolbar = "Show toolbar" toggleAnnotations = "Uključi/isključi vidljivost anotacija" toggleAttachments = "Uključi/isključi priloge" toggleBookmarks = "Uključi/isključi obeleživače" diff --git a/frontend/editor/public/locales/sv-SE/translation.toml b/frontend/editor/public/locales/sv-SE/translation.toml index c8ae1fed37..2576b3bd96 100644 --- a/frontend/editor/public/locales/sv-SE/translation.toml +++ b/frontend/editor/public/locales/sv-SE/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabet" apply = "Verkställ" applyAndContinue = "Spara och lämna" -areYouSure = "Är du säker på att du vill lämna?" back = "Tillbaka" black = "Svart" blue = "Blå" @@ -46,10 +45,8 @@ filesSelected = "filer valda" font = "Teckensnitt" fontSizeTooltip = "Storlek på sidnummertext i punkter. Större tal ger större text." fontTypeTooltip = "Typsnittsfamilj för sidnummer. Välj utifrån dokumentets stil." -genericSubmit = "Skicka" green = "Grön" help = "Hjälp" -imgPrompt = "Välj bild(er)" incorrectPasswordMessage = "Nuvarande lösenord är felaktigt." info = "Info" invalidUndoData = "Kan inte ångra: ogiltiga åtgärdsdata" @@ -62,7 +59,6 @@ loginAgreementTitle = "Inloggningsavtal" logOut = "Logga ut" marginTooltip = "Avstånd mellan sidnumret och sidans kant." moreOptions = "Fler alternativ" -multiPdfPrompt = "Välj PDF-filer (2+)" never = "Aldrig" no = "Nej" noFileSelected = "Ingen fil vald. Ladda upp en." @@ -80,7 +76,6 @@ pageOrderPrompt = "Sidordning (Ange en kommaseparerad lista med sidnummer) :" pages = "Sidor" pageSelectionPrompt = "Anpassat sidval (Ange en kommaseparerad lista med sidnummer 1,5,6 eller funktioner som 2n+1) :" password = "Lösenord" -pdfPrompt = "Välj PDF(er)" pin = "Fäst fil (behåll aktiv efter verktygskörning)" pro = "Pro" processingComplete = "Din fil är klar." @@ -110,6 +105,7 @@ undoSuccess = "Åtgärden ångrades" unknown = "Okänt" unpin = "Lossa fil (ersätt efter verktygskörning)" unsavedChanges = "Du har osparade ändringar i din PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Osparade ändringar" unsupported = "Stöds inte" username = "Användarnamn" @@ -356,6 +352,7 @@ text = "Dessa behörigheter styr vad användare kan göra med PDF-filen. Mest ef title = "Ändra behörigheter" [addStamp] +preview = "Förhandsgranska stämpel" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manuella nedladdningar: klicka på länkarna och placera filerna noLanguages = "Inga tessdata-språk hittades i den konfigurerade katalogen." permissionNotice = "tessdata-sökvägen är inte skrivbar. Nedladdningar öppnas i webbläsaren; spara .traineddata-filerna manuellt i tessdata-mappen." +[admin.settings.ai.documents] +description = "Konfigurera inbäddningsmodellen och hämtningsinställningarna som används för att besvara frågor om dokument. Tillämpas på AI-motorn när de sparas." +title = "Dokument och RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Lämna tomt för att använda motorns egna miljöuppgifter. Gäller för självhostade distributioner med en enda motor." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Sparad – lämna tomt för att behålla den aktuella nyckeln" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL för den OpenAI-kompatibla / Ollama-embeddings-slutpunkten, t.ex. http://ollama:11434/v1. Måste peka på en betrodd intern slutpunkt (SSRF-känsligt)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Namn på inbäddningsmodell. Fritext; förslag är endast tips." +label = "Inbäddningsmodell" + +[admin.settings.ai.documents.embeddingProvider] +description = "Leverantör som används för att omvandla dokumenttext till vektorinbäddningar." +label = "Inbäddningsleverantör" + +[admin.settings.ai.documents.maxSearches] +description = "Maximalt antal hämtningssökningar som agenten får köra per begäran." +label = "Max antal sökningar" + +[admin.settings.ai.documents.reindexNote] +body = "Ändring av inbäddningsmodellen börjar gälla direkt, men dokument som indexerats med den tidigare modellen måste indexeras om för att sökningen ska ge korrekta resultat." +title = "Omindexering krävs" + +[admin.settings.ai.documents.saved] +reindexNote = "Om du ändrade inbäddningsmodellen, indexera om befintliga dokument så att sökningen använder den nya modellen." + +[admin.settings.ai.documents.topK] +description = "Antal mest relevanta segment som hämtas per sökning." +label = "Top K" + +[admin.settings.ai.general] +description = "Anslut Stirling till Python AI-motorn och välj vilka AI-funktioner som ska exponeras. Ändringar tillämpas vid omstart." +title = "AI-motor" + +[admin.settings.ai.general.capabilities] +description = "Slå på eller av enskilda AI-funktioner. Inaktiverade funktioner döljs i appen." +title = "Funktioner" + +[admin.settings.ai.general.enabled] +description = "Huvudbrytare. När den är avstängd är inga AI-verktyg, agenter eller motoranrop tillgängliga." +label = "Aktivera AI" + +[admin.settings.ai.general.features.chat] +description = "Konversationsassistent för arbete med PDFs." +label = "Chattassistent" + +[admin.settings.ai.general.features.classify] +description = "Kategorisera dokument automatiskt efter typ eller innehåll." +label = "Dokumentklassificering" + +[admin.settings.ai.general.features.createPdf] +description = "Generera en ny PDF (t.ex. från HTML) via en AI-agent." +label = "Skapa PDF från prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Ställ frågor och få svar baserade på ett uppladdat dokument." +label = "Dokumentfrågor" + +[admin.settings.ai.general.features.mathAuditor] +description = "Granska dokument för matematiska och numeriska fel." +label = "Matematikgranskare" + +[admin.settings.ai.general.features.pdfComment] +description = "Lägg till AI-skrivna granskningskommentarer och anteckningar i en PDF." +label = "PDF-kommentaragent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Timeout för tyngre agentåtgärder, till exempel dokumentgenerering." +label = "Timeout för långvariga åtgärder (sekunder)" + +[admin.settings.ai.general.note] +body = "AI-motorn körs som en separat tjänst. Dess delade hemlighet" +body2 = "ställs in via en containermiljövariabel. Leverantörers API keys kan anges på dessa sidor eller tillhandahållas som motorns miljövariabler; nycklar som anges här skickas till motorn när de sparas." +title = "Om AI-motorn" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Timeout för strömmade chatt-svar (token för token)." +label = "Timeout för ström (sekunder)" + +[admin.settings.ai.general.test] +button = "Testa anslutning" +failBody = "AI-motorn svarade inte. Kontrollera URL, att motorcontainern körs och att AI är aktiverat (en omstart krävs efter aktivering)." +failTitle = "AI-motorn kan inte nås" +okBody = "AI-motorn svarade på en hälsokontroll." +okTitle = "AI-motorn kan nås" + +[admin.settings.ai.general.timeoutSeconds] +description = "Timeout för standard AI-begäranden till motorn." +label = "Timeout för begäran (sekunder)" + +[admin.settings.ai.general.url] +description = "Intern URL för Python AI-motorn, t.ex. http://stirling-pdf-engine:5001." +label = "AI-motor-URL" + +[admin.settings.ai.limits] +description = "Skyddsräcken för hur mycket arbete AI-begäranden får göra och hur många som körs samtidigt. Tillämpas på AI-motorn när de sparas." +title = "Gränser och prestanda" + +[admin.settings.ai.limits.maxCharacters] +description = "Skyddsräcke: avvisa AI-begäranden vars extraherade text överskrider denna längd." +label = "Max tecken per begäran" + +[admin.settings.ai.limits.maxPages] +description = "Skyddsräcke: avvisa AI-begäranden över detta antal PDF-sidor." +label = "Max sidor per begäran" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Maximalt antal samtidiga pågående modellanrop i hela motorn." +label = "Max samtidighet för modell" + +[admin.settings.ai.models] +description = "Välj LLM-leverantör och de smarta/snabba modeller som AI-motorn använder. Tillämpas på AI-motorn när de sparas." +title = "Modeller och leverantörer" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Lämna tomt för att använda motorns egna miljöuppgifter. Gäller för självhostade distributioner med en enda motor." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Sparad – lämna tomt för att behålla den aktuella nyckeln" + +[admin.settings.ai.models.baseUrl] +description = "Base URL för den OpenAI-kompatibla / Ollama-slutpunkten, t.ex. http://ollama:11434/v1." +label = "Leverantörens base URL" +warning = "Base URL måste peka på en betrodd intern slutpunkt. Motorn gör server-side-begäranden till den, så ett obetrott värde är SSRF-känsligt." + +[admin.settings.ai.models.fastMaxTokens] +description = "Maximalt antal utdata-tokens för den snabba modellen." +label = "Max tokens för snabb modell" + +[admin.settings.ai.models.fastModel] +description = "Billigare, snabbare modell för lättare uppgifter. Fritext; förslag är endast tips." +label = "Snabb modell" + +[admin.settings.ai.models.provider] +description = "Vilken LLM-leverantör motorn kommunicerar med." +label = "Leverantör" + +[admin.settings.ai.models.smartMaxTokens] +description = "Maximalt antal utdata-tokens för den smarta modellen." +label = "Max tokens för smart modell" + +[admin.settings.ai.models.smartModel] +description = "Högkapabel modell för komplexa resonemang. Fritext; förslag är endast tips." +label = "Smart modell" + +[admin.settings.ai.saved] +body = "Ändringar skickas automatiskt till AI-motorn." +bodyNoPush = "Inställningarna har sparats. De tillämpas nästa gång AI-motorn hämtar sin konfiguration." +title = "AI-inställningar sparade" + [admin.settings.badge] clickToUpgrade = "Klicka för att visa plandetaljer" @@ -938,6 +1094,25 @@ label = "Återskapa vid start" description = "Antal dagar certifikatet ska vara giltigt" label = "Certifikatets giltighet (dagar)" +[admin.settings.folderAccess] +description = "Kataloger som mappkällor och mappmål får läsa från och skriva till. Detta är en säkerhetsgräns: automatiseringar kan aldrig pekas mot en serversökväg utanför denna lista." +securityNote = "Lämna detta tomt för att inaktivera mappkällor och mappmål helt. Stirlings egen konfigurationskatalog är alltid förbjuden." +title = "Mappåtkomst" + +[admin.settings.folderAccess.implied] +description = "Dessa Stirling-hanterade kataloger är alltid tillåtna och kan inte ändras här." +serverStorage = "Serverfillagring" +title = "Alltid tillåtet" +watchedFolder = "Övervakad pipeline-mapp" + +[admin.settings.folderAccess.roots] +add = "Lägg till" +empty = "Inga mappar tillåtna. Mappkällor och mappmål är för närvarande inaktiverade." +hint = "Ange absoluta sökvägar, till exempel /data/inbox." +label = "Tillåtna mapprötter" +placeholder = "/data/inbox" +remove = "Ta bort mapprot" + [admin.settings.general] description = "Konfigurera programinställningar på systemnivå inklusive varumärke och standardbeteende." system = "System" @@ -1704,6 +1879,9 @@ width = "Bredd" [app] description = "Det kostnadsfria alternativet till Adobe Acrobat (10M+ nedladdningar)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Konvertera till PDF/A-3b" convertToPdfA3bDescription = "Skapar en arkiv-PDF med inbäddade bilagor" @@ -1842,6 +2020,15 @@ export = "Exportera" accessDenied = "Åtkomst nekad" insufficientPermissions = "Du har inte behörighet att utföra denna åtgärd." +[auth.callback] +completing = "Slutför autentisering" +invalidToken = "OAuth-inloggning misslyckades – ogiltig token." +missingToken = "OAuth-inloggning misslyckades – ingen token mottogs." +oauthFailed = "OAuth-inloggning misslyckades. Försök igen." +pleaseWait = "Vänta medan vi slutför din inloggning." +signedOut = "Du har loggats ut. Logga in igen." +windowMayClose = "Du kan stänga detta fönster när det är klart." + [auth.displayName] guest = "Gäst" user = "Användare" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Säkrar PDF-dokument genom att ta bort potentiellt skadlig description = "Detta verktyg byter automatiskt namn på PDF-filer baserat på deras innehåll. Det analyserar dokumentet för att hitta den mest lämpliga titeln från texten." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Rotera automatiskt" +tags = "automatisk rotering,fixa orientering,upp och ner,sidledes,räta upp,orientera,identifiera orientering,korrigera rotation,automatisk orientering,skannad,OSD" + +[autoRotate.confidenceThreshold] +title = "Konfidensgräns" + +[autoRotate.detectionMode] +auto = "Auto" +osd = "Endast OCR" +text = "Endast text" +title = "Identifieringsmetod" + +[autoRotate.error] +failed = "Ett fel uppstod när PDF:en roterades automatiskt." + +[autoRotate.inferUndetected] +title = "Rotera sidor med låg konfidens så att de matchar" + +[autoRotate.report] +applied = "{{degrees}}° medurs" +confidenceValue = "Konfidens {{value}}" +noChange = "Ingen ändring" +pageLabel = "Sida {{number}}" +summary = "{{rotated}} av {{total}} sidor roterades, {{unchanged}} lämnades som de är" +title = "Identifieringsrapport" + +[autoRotate.report.method] +inferred = "Matchade" +none = "Hoppades över" +osd = "OCR" +text = "Text" + +[autoRotate.report.note] +belowThreshold = "Under gränsvärdet" +blankPage = "Sidan är tom" +inferredFromDocument = "Samma som de andra sidorna" +noDominantDirection = "Blandade textriktningar" +osdFailed = "Ingen läsbar text hittades" +osdNoVerdict = "Kunde inte avgöra vilken sida som är upp" +tesseractUnavailable = "OCR inte installerat" +tooFewGlyphs = "För lite text" + +[autoRotate.results] +title = "Resultat för automatisk rotering" + +[autoRotate.settings] +title = "Inställningar" + +[autoRotate.tooltip.confidenceThreshold] +text = "Hur säker kontrollen måste vara innan en sida roteras. Sänk den för att rotera fler sidor, höj den för att lämna fler sidor orörda." +title = "Konfidensgräns" + +[autoRotate.tooltip.description] +text = "Tar reda på vilken sida upp varje sida ska ha och vrider den rätt. Sidor som redan är upprätta lämnas orörda." + +[autoRotate.tooltip.detectionMode] +text = "Auto läser texten på sidan först och tittar sedan på sidan som en bild om det inte räcker. Endast text hoppar över bildkontrollen, vilket är snabbast för dokument som aldrig skannats. Endast OCR behandlar varje sida som en bild." +title = "Identifieringsmetod" + +[autoRotate.tooltip.header] +title = "Rotera automatiskt" + +[autoRotate.tooltip.inferUndetected] +text = "Tomma eller nästan tomma sidor kan inte kontrolleras på egen hand. När alla sidor som kunde läsas behöver samma vridning vrids dessa sidor för att matcha dem." +title = "Rotera sidor med låg konfidens så att de matchar" + [autoSizeSplitPDF] tags = "pdf,dela,dokument,organisation" @@ -2189,33 +2443,6 @@ keywords = "Nyckelord: odd, even" numbers = "Tal/intervall: 5, 10-20" progressions = "Progressioner: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Certifikattyper" - -[certificateChoice.tooltip.organization] -bullet1 = "Hanteras av systemadministratörer" -bullet2 = "Delas mellan auktoriserade användare" -bullet3 = "Representerar företagets identitet, inte individen" -bullet4 = "Bäst för: Officiella dokument, teamsignaturer" -description = "Ett delat certifikat tillhandahållet av din organisation. Används för företagets signeringsbehörighet." -title = "Organisationscertifikat" - -[certificateChoice.tooltip.personal] -bullet1 = "Genereras automatiskt vid första användning" -bullet2 = "Kopplat till ditt användarkonto" -bullet3 = "Kan inte delas med andra användare" -bullet4 = "Bäst för: Personliga dokument, individuellt ansvar" -description = "Ett automatiskt genererat certifikat unikt för ditt användarkonto. Lämpligt för individuella signaturer." -title = "Personligt certifikat" - -[certificateChoice.tooltip.upload] -bullet1 = "Kräver P12/PFX-fil och lösenord" -bullet2 = "Kan utfärdas av externa certifikatutfärdare" -bullet3 = "Högre tillitsnivå för juridiska dokument" -bullet4 = "Bäst för: Juridiskt bindande avtal, extern validering" -description = "Använd din egen PKCS#12-certifikatfil. Ger full kontroll över certifikategenskaper." -title = "Ladda upp eget P12" - [certSign] awaitingSignatures = "Väntar på signaturer" chooseCertificate = "Välj certifikatfil" @@ -2542,34 +2769,6 @@ server = "Server" stepTitle = "Certifikatkälla" upload = "Ladda upp" -[certSign.tooltip.header] -title = "Om hantering av signaturer" - -[certSign.tooltip.overview] -bullet1 = "Kontrollera befintliga signaturer och deras giltighet" -bullet2 = "Visa detaljerad information om undertecknare och certifikat" -bullet3 = "Lägg till nya digitala signaturer för att säkra dina dokument" -bullet4 = "Flera filer stöds med enkel navigering" -text = "Det här verktyget låter dig kontrollera om dina PDF:er är digitalt signerade och lägga till nya digitala signaturer. Digitala signaturer visar vem som skapat eller godkänt ett dokument och om det har ändrats efter signering." -title = "Vad kan detta verktyg göra?" - -[certSign.tooltip.signing] -bullet1 = "Stöd för PEM, PKCS12, JKS och servercertifikatformat" -bullet2 = "Alternativ att visa eller dölja signatur på PDF:en" -bullet3 = "Lägg till anledning, plats och undertecknarens namn" -bullet4 = "Välj vilken sida som synliga signaturer ska placeras på" -bullet5 = "Använd servercertifikat för enkelt alternativ 'Sign with Stirling-PDF'" -text = "För att signera en PDF behöver du ett digitalt certifikat (som PEM, PKCS12 eller JKS). Du kan välja att göra signaturen synlig i dokumentet eller hålla den osynlig endast för säkerhet." -title = "Lägga till signaturer" - -[certSign.tooltip.validation] -bullet1 = "Visar om signaturer är giltiga eller ogiltiga" -bullet2 = "Visar information om undertecknare och signeringsdatum" -bullet3 = "Kontrollerar om dokumentet ändrats efter signering" -bullet4 = "Kan använda anpassade certifikat för verifiering" -text = "När du kontrollerar signaturer berättar verktyget om de är giltiga, vem som undertecknat dokumentet, när det signerades och om dokumentet har ändrats efter signering." -title = "Kontrollerar signaturer" - [changeCreds] changeUsername = "Uppdatera ditt användarnamn. Du loggas ut efter uppdateringen." credsUpdated = "Konto uppdaterat" @@ -2737,12 +2936,16 @@ text = "För att göra dessa behörigheter oföränderliga, använd verktyget L [chat.actions] copy = "Kopiera meddelande" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Alternativ för Stirling-agent" +assistant = "AI Assistant" clearChat = "Rensa chatt" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Denna åtgärd kommer att använda dina molnkrediter" tooltip = "Välj färg från skärmen" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Välj färg" [common] +actions = "Actions" back = "Tillbaka" cancel = "Avbryt" close = "Stäng" +codeSample = "Code sample" collapse = "Fäll ihop" confirm = "Bekräfta" continue = "Fortsätt" copied = "Kopierat!" copy = "Kopiera" done = "Klar" +enterValue = "Enter value" error = "Fel" expand = "Fäll ut" loading = "Läser in..." next = "Nästa" +open = "Open" preview = "Förhandsgranska" previous = "Föregående" refresh = "Uppdatera" +remaining = "Återstår" retry = "Försök igen" save = "Spara" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Rensa valda" @@ -2988,6 +3199,7 @@ title = "Komprimeringsmetod" [compress.settings] desiredSize = "Önskad filstorlek" desiredSizePlaceholder = "Ange storlek" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Komprimering är ett enkelt sätt att minska filstorleken. Välj Filstorlek för att ange en målstorlek så justerar vi kvaliteten åt dig. Välj Kvalitet för att ställa in komprimeringsstyrkan manuellt." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF‑alternativ" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Välj först ett källformat" settings = "Inställningar" single = "En" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Avancerad övervakning" -allInOneWorkspace = "Allt-i-ett PDF-arbetsyta (visare, verktyg och agent)" -apiSandbox = "API-sandbox" -dedicatedSupportSlas = "Dedikerad support och SLA:er" -fasterThroughput = "10x snabbare genomströmning" -fullyPrivateFiles = "Helt privata filer" -largeFileProcessing = "Hantering av stora filer" -orgWideAccess = "Organisationstäckande åtkomstkontroller" -premiumAiModels = "Premium AI-modeller" -prioritySupport = "Prioriterad support" -privateDocCloud = "Privat dokumentmoln" -ragFineTuning = "RAG + finjustering" -secureApiAccess = "Säker API-åtkomst" -standardThroughput = "Standardgenomströmning" -unlimitedApiAccess = "Obegränsad API-åtkomst" -unlimitedSeats = "Obegränsat antal platser" - [crop] autoCrop = "Auto-beskär tomrum" noFileSelected = "Välj en PDF-fil för att börja beskära" @@ -3346,6 +3558,7 @@ message = "Det gick inte att ange standard-PDF-hanterare" title = "Fel" [defaultApp.prompt] +dontRemind = "Påminn mig inte igen" message = "Gör Stirling PDF till din standardapp för att öppna PDF-filer." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Sök..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Redigera" @@ -3644,11 +3861,13 @@ shareSelected = "Dela valda" sharing = "Delning" showAll = "Visa alla" showHistory = "Visa historik" +sortBy = "Sort files" sortByDate = "Sortera efter datum" sortByName = "Sortera efter namn" sortBySize = "Sortera efter storlek" storage = "Lagring" storageState = "Lagring" +storageUsed = "Storage used" synced = "Synkroniserad" title = "Ladda upp PDF-filer" toolChain = "Tillämpade verktyg" @@ -3694,13 +3913,17 @@ addFiles = "Lägg till filer" addingFiles = "Lägger till filer…" collapse = "Fäll ihop sidofält" customizeGroups = "Anpassa grupper" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Öppna filer för att komma igång" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Expandera sidofält" -files = "Filer" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive är inte konfigurerat" leaveMyFiles = "Lämna Mina filer" +library = "PDF-bibliotek" myFiles = "Mina filer" noFiles = "Inga filer ännu" openFileManager = "Öppna filhanteraren" @@ -3708,37 +3931,36 @@ openFromComputer = "Öppna från dator" openSettings = "Öppna inställningar" other = "Annat" recent = "Senaste" -search = "Sök" -searchPlaceholder = "Sök filer..." viewAll = "Visa alla {{count}} filer" [fileSidebar.fileItem] closeViewer = "Stäng visare" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Öppna i visare" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Lägg till" -addLabel = "Lägg till en etikett…" -categoryIconAria = "Välj en ikon för {{name}}" -createCategory = "Skapa kategori" -delete = "Ta bort kategori" done = "Klart" -hide = "Dölj kategori" -newCategory = "Nytt kategorinamn…" -removeLabel = "Ta bort {{name}}" -rename = "Byt namn" reset = "Återställ till standard" -search = "Sök etiketter…" -show = "Visa kategori" subtitle = "Gruppera dina filer i överordnade kategorier. Lägg till befintliga eller nya etiketter i en kategori, byt namn på den eller skapa en egen. Filer som inte finns i någon av dina kategorier visas under ”Annat”." title = "Sidofältskategorier" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Lägg till i arbetsyta" addToWorkspaceCount = "Lägg till {{count}} i arbetsyta" @@ -3749,7 +3971,6 @@ backToMyFiles = "Tillbaka till Mina filer" breadcrumbs = "Mappsökväg" cancel = "Avbryt" classification = "Klassificering" -clearSearch = "Rensa sökning" clearSelection = "Rensa markering" closeDetails = "Stäng detaljer" create = "Skapa" @@ -3781,6 +4002,7 @@ downloadAll = "Ladda ner alla" downloadVersion = "Ladda ner den här versionen" dropOverlay = "Släpp filer för att ladda upp" dropOverlaySub = "Filer börjar i Lokalt. Använd 'Flytta till' eller 'Spara till molnet' för att organisera dem i en mapp." +duplicate = "Duplicate" file = "Fil" fileInfo = "File info" fileMenu = "Filåtgärder" @@ -3808,13 +4030,12 @@ refresh = "Uppdatera från servern" remove = "Ta bort" removeVersion = "Ta bort den här versionen" rename = "Byt namn" +renamed = "Omdöpt" renameFolder = "Byt namn på mapp" resizeFolderTree = "Ändra storlek på mappträdet (piltangenter, Skift för större steg; dubbelklicka för att autoanpassa)" save = "Spara" saveToServer = "Spara till servern" saveToServerDisabledHint = "Spara till servern är inte aktiverat på denna server. Be din administratör att aktivera det." -search = "Sök" -searchPlaceholder = "Sök i den här mappen och undermappar" selectAll = "Markera alla" selectAllHint = "Klicka för att markera alla. Tips: håll Ctrl (eller Cmd) för att lägga till filer en i taget, Skift för att markera ett intervall." selectedCount = "{{count}} markerade" @@ -3874,6 +4095,10 @@ title = "Inga molnfiler ännu" hint = "Filer som sparas utan uppladdning stannar här. Släpp en fil för att lägga till en." title = "Inga lokala filer" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Filer du öppnar eller redigerar visas här." title = "Inget ändrat ännu" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Kunde inte ta bort mappen." deleteFolderFailedDetail = "Kunde inte ta bort mappen: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Kunde inte uppdatera mappens utseende." folderAppearanceFailedDetail = "Kunde inte uppdatera mappens utseende: {{message}}" moveFilesFailed = "Kunde inte flytta filer." @@ -3910,9 +4137,7 @@ count = "Filer" folder = "Mapp" labels = "Etiketter" modified = "Ändrad" -name = "Namn" size = "Storlek" -toolHistoryAtVersion = "Samlad verktygskedja" totalSize = "Total storlek" type = "Typ" versionHistory = "Versionsresa" @@ -3945,6 +4170,11 @@ localHint = "Endast lagrad i denna webbläsare" shared = "Delad" sharedHint = "Delad med dig via länk" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Äldst först" modifiedDesc = "Senaste först" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Inga filer tillgängliga i lagringen. Öppna några file open = "Öppna" openFile = "Öppna fil" openFiles = "Öppna filer" +selectFile = "Select {{name}}" selectFromStorage = "Välj från lagring" upload = "Ladda upp" uploadFile = "Ladda upp fil" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Allt sparat" +analyzingFields = "Analysing form fields..." +extractCsvError = "Kunde inte extrahera CSV" +extractXlsxError = "Kunde inte extrahera XLSX" +filled = "filled" flattenAfterFilling = "Platta ut efter ifyllning" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "obl" +requiredFieldsError = "Fyll i alla obligatoriska fält" rescanFields = "Skanna fält igen" rescanFormFields = "Skanna formulärfält igen" save = "Spara" +saveShortcut = "Ctrl+S för att spara" +unsavedChanges = "Osparade ändringar" [formFill.sidebar] close = "Stäng sidofält" @@ -4309,6 +4552,11 @@ desc = "Byter automatiskt namn på en PDF baserat på detekterad rubrik" tags = "autodetektera,huvudbaserad,organisera,etikettera om" title = "Byt namn på PDF automatiskt" +[home.autoRotate] +desc = "Identifiera varje sidas orientering och rotera den automatiskt rättvänd." +tags = "automatisk rotering,fixa orientering,upp och ner,sidledes,räta upp,orientera,identifiera orientering,korrigera rotation,automatisk orientering,skannad,OSD" +title = "Rotera automatiskt" + [home.autoSizeSplitPDF] tags = "auto,dela,storlek" title = "Auto-dela efter storlek/antal" @@ -4608,15 +4856,12 @@ hint = "Ladda upp en PNG, JPG, SVG eller annan bildfil att placera på PDF:en. S label = "Ladda upp bild" placeholder = "Välj bildfil" -[imageToPdf] -tags = "konvertering,img,jpg,bild,foto" - [imageToPDF] header = "Bild till PDF" title = "Bild till PDF" -[infoBanner] -dismiss = "Avfärda" +[imageToPdf] +tags = "konvertering,img,jpg,bild,foto" [invite] acceptError = "Misslyckades med att skapa konto" @@ -4647,11 +4892,9 @@ welcomeTitle = "Du har bjudits in!" [landing] addFiles = "Lägg till filer" -heroSubtitle = "Släpp in eller lägg till en befintlig PDF för att komma igång." mobileUpload = "Ladda upp från mobil" openFromComputer = "Öppna från dator" uploadFromComputer = "Ladda upp från datorn" -workbenchEmptyStateHero = "Släpp en PDF var som helst" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Ange användarnamn" failedToSignIn = "Det gick inte att logga in med {{provider}}: {{message}}" forgotPassword = "Glömt lösenordet?" loggingIn = "Loggar in..." -logIn = "Logga in" login = "Inloggning" magicLinkSent = "Magisk länk skickad till {{email}}! Kontrollera din e-post och klicka på länken för att logga in." mfaCode = "Autentiseringskod" @@ -4715,26 +4957,10 @@ signInWith = "Logga in med" title = "Logga in" unexpectedError = "Oväntat fel: {{message}}" updatePassword = "Uppdatera lösenord" -useEmailInstead = "Logga in med e-post" useMagicLink = "Använd magisk länk i stället" username = "Användarnamn" youAreLoggedIn = "Du är inloggad!" -[login.slides.edit] -alt = "Redigera PDF-filer" -subtitle = "Med över ett dussin verktyg för att maskera, signera, läsa och manipulera PDF-filer hittar du garanterat det du söker." -title = "Redigera PDF-filer för att visa/säkra den information du vill" - -[login.slides.overview] -alt = "Översikt över Stirling PDF" -subtitle = "En integritetsfokuserad molnsuite för PDF som låter dig konvertera, signera, maskera och hantera dokument, plus 50+ andra kraftfulla verktyg." -title = "Din allt-i-ett-lösning för alla dina PDF-behov." - -[login.slides.secure] -alt = "Säkra PDF-filer" -subtitle = "Lägg till lösenord, maskera innehåll och hantera certifikat enkelt." -title = "Skydda känslig information i dina PDF-filer" - [margin] large = "Stor" medium = "Mellan" @@ -4818,6 +5044,34 @@ uploadSuccess = "Uppladdning lyckades!" uploadSuccessMessage = "Dina bilder har överförts." validating = "Validerar session..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Skanna för att ladda upp foton. Bilder konverteras automatiskt till PDF." descriptionNoConvert = "Skanna för att ladda upp foton från din mobila enhet." @@ -4836,8 +5090,13 @@ title = "Ladda upp från mobil" tags = "Multiverktyg,Multioperation,UI,klicka dra,front end,klientsida" title = "PDF-multiverktyg" -[navbar] -search = "Sök" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Autentiseringen lyckades inte. Du kan stänga detta fönster och försöka igen." @@ -4966,6 +5225,7 @@ activeFiles = "Vyn Aktiva filer visar alla PDF:er du har laddat allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Stäng" cropSettings = "Nu när vi har valt filen vi vill beskära kan vi konfigurera Beskär-verktyget för att välja området som vi vill beskära PDF-filen till." +dialogLabel = "Onboarding" fileCheckbox = "Att klicka på en av filerna markerar den för bearbetning. Du kan välja flera filer för batchåtgärder." fileReplacement = "Den ändrade filen ersätter originalfilen i Arbetsbänken automatiskt, vilket gör det enkelt att köra den genom fler verktyg." filesButton = "Knappen Filer på snabbåtkomstfältet låter dig ladda upp PDF-filer att använda verktygen på." @@ -4976,6 +5236,7 @@ pinButton = "Du kan använda knappen Fäst om du vill att dina results = "Efter att verktyget har körts klart visar steget Granska en förhandsvisning av resultaten i denna panel och låter dig ångra åtgärden eller ladda ner filen. " runButton = "När verktyget har konfigurerats låter denna knapp dig köra verktyget på alla valda PDF:er." selectCropTool = "Låt oss välja verktyget Beskär för att visa hur man använder ett av verktygen." +stepOf = "Steg {{current}} av {{total}}" toolInterface = "Detta är gränssnittet för verktyget Beskär. Som du ser finns det inte mycket där eftersom vi ännu inte har lagt till några PDF-filer att arbeta med." workbench = "Detta är Arbetsbänken - huvudområdet där du visar och redigerar dina PDF:er." wrapUp = "Allt klart! Du har lärt dig om appens huvudområden och hur du använder dem. Klicka på knappen Hjälp när du vill för att se denna rundtur igen." @@ -4988,6 +5249,26 @@ showMeAround = "Visa mig runt" skipForNow = "Hoppa över nu" skipTheTour = "Hoppa över rundturen" +[onboarding.checklist] +dismiss = "Avfärda" +title = "Konfigurera Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Kör Stirling lokalt på din dator" +title = "Ladda ner Stirling för Desktop" + +[onboarding.checklist.inviteTeam] +description = "Samarbeta med ditt team" +title = "Bjud in teammedlemmar" + +[onboarding.checklist.shareAnalytics] +description = "Hjälp till att förbättra Stirling" +title = "Dela anonym användningsdata" + +[onboarding.checklist.takeTour] +description = "Se hur Stirling fungerar i en snabb genomgång" +title = "Ta rundturen" + [onboarding.desktopInstall] body = "Stirling fungerar bäst som ett skrivbordsprogram. Du kan använda det offline, komma åt dokument snabbare och göra redigeringar lokalt på din dator." selectOs = "Välj operativsystem" @@ -4996,7 +5277,21 @@ titleWithOs = "Ladda ner för {{osLabel}}" [onboarding.mfa] authenticationCode = "Autentiseringskod" +description = "Säkra ditt konto genom att länka en autentiseringsapp. Skanna QR-koden eller ange konfigurationsnyckeln och bekräfta sedan den 6-siffriga koden för att slutföra." +enable = "Aktivera MFA" +enableError = "Det gick inte att aktivera tvåfaktorsautentisering. Kontrollera koden och försök igen." +enterCode = "Ange den 6-siffriga koden från din app." +enterCodeError = "Ange autentiseringskoden för att fortsätta." +logout = "Logga ut" +openAuthenticator = "Öppna Google Authenticator, Authy eller 1Password." qrCodeLoading = "Genererar din QR-kod…" +regenerateQrCode = "Generera QR-kod på nytt" +scanQrCode = "Skanna QR-koden eller ange konfigurationsnyckeln nedan." +setupError = "Det gick inte att starta tvåfaktorskonfigurationen. Försök igen." +setupKey = "Konfigurationsnyckel (manuell inmatning)" +stepByStep = "Steg för steg" +success = "MFA har aktiverats. Du kan nu fortsätta." +title = "Konfigurera multifaktorautentisering" [onboarding.planOverview] adminBodyLoginDisabled = "När du aktiverar inloggningsläge kan du hantera användare, konfigurera inställningar och övervaka serverhälsa. De första {{freeTierLimit}} personerna på din server får använda Stirling gratis." @@ -5005,6 +5300,11 @@ adminTitle = "Adminöversikt" userBody = "Bjud in teammedlemmar, tilldela roller och håll dina dokument organiserade i en säker arbetsyta. Aktivera inloggningsläge när du är redo att växa bortom sololäge." userTitle = "Planöversikt" +[onboarding.processorIntro] +body = "Stirling kör nu policyer — automatiserade regler som klassificerar, säkrar och bearbetar varje dokument när det kommer in. Konfigurera dem och övervaka körningar i Processor." +cta = "Kolla in Processor" +title = "Kolla in Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Förbetalt år" + +[payg.prepaid.banner] +exhausted = "Din förbetalda kapacitet är förbrukad; debitering efter användning har återupptagits. Fyll på för att behålla rabatten." +expiring_one = "Ditt förbetalda år slutar om {{count}} dag. Oanvänd kapacitet förfaller – fyll på för att starta ett nytt år." +expiring_other = "Ditt förbetalda år slutar om {{count}} dagar. Oanvänd kapacitet förfaller – fyll på för att starta ett nytt år." +low = "Du är nere på {{pct}}% av din förbetalda kapacitet. Fyll på när som helst för att fortsätta bearbeta utan avbrott." + +[payg.prepaid.card] +subtitle = "Används före debitering efter användning och utanför ditt utgiftstak." +title = "Förbetald kapacitet" + +[payg.prepaid.meter] +capSuffix = "av {{total}} förbetalda krediter" +expires = "Förfaller {{date}}" + +[payg.prepaid.state] +exhausted = "Förbrukad" +healthy = "Gott om kvar" +low = "Börjar ta slut" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF till PDF/A" tags = "arkiv,långtids,standard,konvertering,lagring,bevarande" title = "PDF till PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "tryck,standard,konvertering,produktion,prepress,arkiv" title = "PDF till PDF/X" @@ -5822,6 +6151,7 @@ small = "500 krediter" xsmall = "100 krediter" [plan.availablePlans] +currency = "Billing currency" subtitle = "Välj den plan som passar dina behov" title = "Tillgängliga planer" @@ -5937,49 +6267,12 @@ keyDescription = "Klistra in licensnyckeln från ditt e‑postmeddelande" success = "Licens aktiverad!" successMessage = "Din licens har aktiverats. Du kan nu stänga detta fönster." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "tillämpad" -enforcing = "Tillämpas..." -failed = "Tillämpningen misslyckades" outputsUnavailable = "Policyutdata är inte längre tillgängliga att ladda ned." partialOutputsUnavailable = "Vissa policyutdata är inte längre tillgängliga att ladda ned." -retrying = "Upptagen, försöker igen..." runNotFound = "Tillämpningskörningen kunde inte längre hittas." -step = "steg {{current}}/{{total}}" timedOut = "Tillämpningen överskred tidsgränsen innan körningen kunde slutföras." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Stäng" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Tillämpning av {{names}}" applyingProgress = "Tillämpning av {{names}} ({{done}} av {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} av {{total}} fil(er) kunde inte bearbetas och export failureTitle = "Exporterades utan fullständig tillämpning" printPolicyAppliedBody = "Denna PDF uppdaterades för att uppfylla en policy. Granska ändringarna och skriv sedan ut igen." printPolicyAppliedTitle = "Policy tillämpad före utskrift" -queued = "+{{count}} i kö" successTitle = "{{names}} har tillämpats" summaryMore = "{{first}}, {{second}} och {{more}} till" summaryTwo = "{{first}} och {{second}}" -[policies.enforcement.triggerVerb] -convert = "Tillämpa före konvertering" -default = "Tillämpa" -export = "Tillämpa före export" -input = "Tillämpa vid import" -print = "Tillämpa före utskrift" - -[policies.field] -accessLog = "Åtkomstlogg" -archiveAfter = "Arkivera efter" -auditTrail = "Revisionsspår" -belowThreshold = "Under tröskelvärde" -destination = "Destination" -frameworks = "Ramverk" -immutableHold = "Oföränderlig spärr" -keepFor = "Behåll i" -minConfidence = "Minsta konfidens" -notify = "Avisera vid dirigering" -onViolation = "Vid bristande efterlevnad" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Lägg till" -addPlaceholder = "Lägg till en etikett…" -customNote = "Anpassat för ditt team." -defaultNote = "Använder den inbyggda standarden, delad med ditt team." -duplicate = "\"{{name}}\" finns redan." -edit = "Redigera etiketter" -empty = "Inga etiketter ännu." -export = "Exportera JSON" -iconAria = "Välj en ikon för {{name}}" -import = "Importera JSON" -importError = "Det gick inte att importera den filen." -managedNote = "Teametiketter hanteras av din teamledare." -modalSubtitle = "Delas med hela ditt team. Klassificeraren väljer de etiketter som passar varje dokument." -modalTitle = "Klassificeringsetiketter" -pickIcon = "Använd ikonen {{label}}" -removeAria = "Ta bort {{name}}" -resetToDefault = "Återställ till standard" -saveForTeam = "Spara för teamet" -saving = "Sparar…" -sectionLabel = "Klassificeringsetiketter" -startFromScratch = "Börja från början" -teamCount = "teametiketter" -tooLong = "Etiketter får vara högst {{max}} tecken." -ungrouped = "Ogrupperade" -view = "Visa etiketter" +categoryCount = "kategorier" +hideCategory = "Dölj kategori" +labelCount = "etiketter" +sharedNote = "Dessa etiketter är inbyggda och delas inom hela ditt team." +showCategory = "Visa kategori" [policies.pii] account = "Kontonummer (etiketterade)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "USA-routingnummer (ABA)" ssn = "Social Security-nummer" -[policies.settings] -noneExport = "Inga policyer körs för närvarande vid export." -noneUpload = "Inga policyer körs för närvarande vid uppladdning." -onExport = "Vid export" -onUpload = "Vid uppladdning" -reorderHandle = "Dra för att ändra ordning" -runOrderDesc = "När fler än en policy körs på samma utlösare körs de i den här ordningen — var och en på föregående policys utdata. Dra för att ändra ordning." -title = "Policyinställningar" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Policyalternativ" -policySettings = "Policyinställningar" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Försök igen med misslyckade policyer ({{count}})" -rowProgress = "{{completed}} av {{total}} filer bearbetade" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Vad är en policy?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Hittar och svartmarkerar automatiskt känsliga uppgifter — som Social Security- och kortnummer — så att de inte kan läsas i dokumentet." -sanitize = "Tar bort dold JavaScript-kod från filen, så att inget kan köras automatiskt när någon öppnar den." -watermark = "Stämplar en synlig markering (t.ex. \"Confidential\") över varje sida." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Stäng" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} tillämpas..." badgeRan = "Policyn {{name}} kördes på den här filen" +badgeRunning = "{{name}} körs..." blockingAction = "{{action}} blockerades medan policy tillämpas, vänta..." dismiss = "Stäng överlägg" enforcingTitle = "Tillämpar policy..." @@ -6151,6 +6316,7 @@ viewAnyway = "Visa fil (policy tillämpas fortfarande)" [policyOption] autoRedactPhi = "Automatisk maskering av PHI" blockExport = "Blockera export" +ccpa = "CCPA" contracts = "Avtal" documents = "Dokument" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor-plan" subscribed = "Processor-plan" unlinked = "Inte länkat" -[portal.agentBuilder] -bootstrapFromDocument = "Initiera från dokument" -sectionsAriaLabel = "Agent Builder-sektioner" -selectorAriaLabel = "Agenter" -subtitle = "Designa, testa och leverera AI-agenter som klassificerar, extraherar från och dirigerar dina dokument. Definiera scenarier, avgränsa verktygsåtkomst, kör ett golden set och publicera en version." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Avbryt" -dropzoneText = "Välj ett exempeldokument (PDF eller bild)" -lead = "Släpp ett exempeldokument så föreslår vi scenarier och ett extraktionsschema som du kan förfina. Ingenting publiceras förrän du har granskat det." -submit = "Initiera agent" -subtitle = "Starta en ny agent från en representativ fil" -title = "Initiera från ett dokument" - -[portal.agentBuilder.empty] -description = "Initiera en agent från ett exempeldokument för att starta dess scenarier och extraktionsschema, förfina sedan och publicera." -title = "Inga agenter ännu" - -[portal.agentBuilder.evals] -casesPassing = "Fall som godkänns" -columnCase = "Eval-fall" -columnLatency = "Latens" -columnResult = "Resultat" -fail = "misslyckas" -goldenSetPassRate = "Godkännandefrekvens för golden set" -latencyMs = "{{ms}} ms" -notRun = "inte kört" -pass = "godkänt" -passRate = "Godkännandefrekvens" -runEvals = "Kör evals" - -[portal.agentBuilder.evals.empty] -description = "Evals gör dina scenarier till ett repeterbart golden set. Uppgradera för att registrera godkännandefrekvens över tid och spärra publiceringar baserat på den." -title = "Inget golden set ännu" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "över golden sets" -activeAgents = "Aktiva agenter" -avgPassRate = "Genomsnittlig godkännandefrekvens för eval" -fleetWide = "för hela flottan" -latestPublished = "Senast publicerad" -scenarios = "Scenarier" -testCases = "testfall" -totalDescription_one = "{{count}} totalt" -totalDescription_other = "{{count}} totalt" - -[portal.agentBuilder.scenarios] -add = "Lägg till" -addScenario = "Lägg till scenario" -enable = "Aktivera" -expectationLabel = "Förväntat beteende" -expectationPlaceholder = "Vad agenten ska göra" -inEval = "i eval" -mute = "Tysta" -muted = "tystad" -nameLabel = "Namn" -namePlaceholder = "t.ex. eskalering för efterlevnad" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarier" -tools = "Verktyg" -versions = "Versioner" - -[portal.agentBuilder.tools] -broadAccess = "Bred åtkomst" -deniedHint = "Valda verktyg är blockerade. Allt annat förblir anropbart." -deniedTools = "Nekade verktyg" -governanceGate = "Verktygsstyrning är tillgänglig i Enterprise-planen." -restricted = "Begränsad" -restrictedAccess = "Begränsad verktygsåtkomst" -restrictedDescription = "Tillåt alla verktyg utom de du nekar nedan." - -[portal.agentBuilder.versions] -current = "aktuell" -historyGate = "Full versionshistorik och återställning är tillgängliga i Enterprise-planen." -publish = "Publicera" -rollBack = "Återställ" - [portal.assistant] close = "Stäng assistenten" error = "Det gick inte att nå assistenten." @@ -6365,9 +6451,21 @@ typing = "Skriver" loading = "Läser in" redirectingToEditor = "Omdirigerar till redigeraren..." +[portal.billing.activation] +maybeLater = "Kanske senare" +title = "Slå på Processor" + +[portal.billing.activation.payg] +sub = "Ange en månadsgräns. Betala för det du kör." +title = "Betala per användning" + +[portal.billing.activation.prepay] +badge = "2 månader gratis" +sub = "En faktura, kort eller banköverföring." +title = "Förbetala ett år" + [portal.billing.checkout] noClientSecret = "Edge-funktionen returnerade ingen client_secret." -subtitle = "Lägg till ett kort för att fortsätta efter din kostnadsfria tilldelning i Editor-planen. Stripe hanterar resten." title = "Aktivera Processor-planen" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Din betalning lyckades, men aktiveringen tar lite längre tid än vanlig close = "Stäng" title = "Nästan klart" +[portal.billing.checkout.cap] +amountAria = "Månatlig utgiftsgräns" +back = "Tillbaka" +continue = "Fortsätt till betalning" +error = "Kunde inte ange din utgiftsgräns" +finePrint = "Fakturor skickas månadsvis. Avbryt när som helst." +noLimit = "Ingen gräns" +note = "Du debiteras aldrig över din gräns. Bearbetningen pausas bara." +perMonth = "/ mån" +title = "Ange din utgiftsgräns" + +[portal.billing.checkout.card] +fields = "Kortnummer · MM / ÅÅ · CVC · ZIP" +label = "Kortuppgifter" +note = "Kortuppgifter samlas in av Stripe. Stirling lagrar aldrig PAN eller CVC." + [portal.billing.checkout.error] title = "Det gick inte att starta kassan" @@ -6383,10 +6497,9 @@ body = "Din betalning gick igenom. Vi aktiverar nu mätt bearbetning över dina hint = "Låt det här fönstret vara öppet." title = "Aktiverar din Processor-plan..." -[portal.billing.checkout.notConfigured] -bodyAfter = "i portal env för att aktivera kassa i appen." -bodyBefore = "Ange" -title = "Stripe är inte konfigurerat" +[portal.billing.checkout.pay] +limit = "Månatlig utgiftsgräns {{amount}}/mån. Bearbetningen pausas vid gränsen." +limitNoCap = "Ingen utgiftsgräns. Bearbetningen pausas inte. Fakturor skickas månadsvis." [portal.billing.enterpriseUpsell] cta = "Skapa din Enterprise-offert" @@ -6409,10 +6522,12 @@ currentPlan = "Aktuell plan" freeForever = "Gratis för alltid" noTeamResolved = "Inget team har kopplats till din plånbok ännu — uppdatera och försök igen." ownerOnly = "Endast teamägaren kan aktivera Processor-planen." +payInvoice = "Betala faktura för att slutföra" planName = "Editor" ssoIncluded = "SSO ingår" switchOnProcessor = "Aktivera Processor →" unlimitedUsers = "Obegränsat antal användare" +viewQuote = "Visa offert" [portal.billing.invoices] columnAmount = "Belopp" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Direkta API-förfrågningar" segmentApiLabel = "API" segmentAutomationDesc = "Automatiseringar och pipelines" segmentAutomationLabel = "Automatisering" +sizeMultiplier = "{{formatted}} PDFs använde en storleksmultiplikator" +summary = "{{unique}} unika · {{units}} mätarenheter · {{avg}} i genomsnitt per PDF" +summaryNoRate = "{{unique}} unika · {{units}} mätarenheter" unit = "mätta PDF:er" +unitsPending_one = "{{units}} mätarenhet väntar på synk från länkade instanser" +unitsPending_other = "{{units}} mätarenheter väntar på synk från länkade instanser" + +[portal.billing.prepaid] +capSuffix = "av {{total}} förbetalda krediter" +expires = "Förfaller {{date}}" +eyebrow = "Förbetald kapacitet" +note = "Används före debitering efter användning och utanför din utgiftsgräns." +topUp = "Fyll på" + +[portal.billing.prepaid.buy] +back = "Tillbaka" +calcTitle = "Beräkna din årsbetalning" +cancel = "Avbryt" +cancelPurchase = "Avbryt köp" +close = "Stäng" +continueToPayment = "Fortsätt till betalning" +finalise = "Slutför" +invoiceErrorTitle = "Kunde inte generera fakturan" +loadingTitle = "Läser in din offert" +payTitle = "Betala för ditt år" +step = "Steg {{current}} av {{total}}" + +[portal.billing.prepaid.calc] +change = "Ändra" +deployRow = "Distribution" +done = "Klart" +enterpriseHint = "Detta är företagsnivå. Vid den här volymen är företagspriser bättre än någon självbetjäningsrabatt. Prislåsning, villkor och en offert på några minuter." +governanceRow = "Styrning" +governanceValue = "{{label}} · {{count}} policyer" +handlesLabel = "Din Processor" +handlesValue = "hanterar {{volume}} krediter / mån" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policyer" +poolCaption = "En pool med {{credits}} krediter för året. Tunga månader lånar från lätta." +priceLabel = "Ditt år · 12 månader till priset av 10" +rateUnknown = "Vi visar det exakta priset i kassan." +savings = "Du sparar {{amount}} · 2 månader gratis." +sizingRow = "Storleksanpassning" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Totalt antal användare" + +[portal.billing.prepaid.consent] +label = "Jag förstår att när min förbetalda kapacitet är förbrukad eller förfaller efter 12 månader fortsätter bearbetningen automatiskt till standardpriset för debitering efter användning (upp till min utgiftsgräns) om jag inte avbryter, och att jag kan avbryta när som helst från faktureringsportalen." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Hanteras av Stirling. Live på några minuter." +cloudValue = "Stirling Cloud · Hanterad" +selfhost = "Självhostad" +selfhostDesc = "Körs på privat infrastruktur. Samma pris." +selfhostValue = "Självhostad · Privat infrastruktur" + +[portal.billing.prepaid.offer] +cta = "Granska erbjudande" +subtitle = "Förbetala ett år av PDF-bearbetning och få två månader gratis, används före debitering efter användning, utanför din utgiftsgräns." +title = "Få 12 månader till priset av 10" + +[portal.billing.prepaid.pay] +accountName = "Kontoinnehavarens namn" +accountNamePlaceholder = "t.ex. Jane Smith" +companyName = "Företagsnamn (valfritt)" +companyNamePlaceholder = "t.ex. Acme Inc." +downloadInvoice = "Ladda ner faktura" +payOnline = "Betala online" +poNumber = "PO-nummer (valfritt)" +poNumberPlaceholder = "t.ex. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Avancerad" +advancedDesc = "Pipelines driver det mesta av din bearbetning." +none = "Inga" +noneDesc = "Kör inga pipelines ännu. Slå på dem när som helst." +standard = "Standard" +standardDesc = "Några pipelines ombearbetar inkommande PDFs." + +[portal.billing.prepaid.posture] +essentials = "Grundläggande" +essentialsDesc = "Klassificering, standarden, plus Delning" +governed = "Styrd" +governedDesc = "Lägger till Säkerhet och Routing" +regulated = "Reglerad" +regulatedDesc = "Varje kategori, inkl. Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "Ladda ner offert (PDF)" +downloading = "Förbereder…" +share = "dela för godkännande" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "Filer under 25 MB, inga dataavgifter" +heavy = "Tung" +heavyDesc = "Skannad eller bildtung, rutinmässigt 50 MB+" +standard = "Standard" +standardDesc = "Mestadels små, vissa skanningar över 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Förbrukad" +healthy = "Gott om kvar" +low = "Börjar ta slut" [portal.billing.spendLimit] adjustLimit = "Justera gräns" @@ -6494,6 +6713,8 @@ label = "Prognos att överskridas." [portal.billing.spendThisMonth] eyebrow = "Utgifter denna månad" +freeRemaining_one = "{{formatted}} gratis PDF återstår" +freeRemaining_other = "{{formatted}} gratis PDFs återstår" processed_one = "{{formattedCount}} PDF bearbetad." processed_other = "{{formattedCount}} PDF:er bearbetade." processedWithRate_one = "{{formattedCount}} PDF bearbetad, med {{rate}} styck." @@ -6510,6 +6731,7 @@ reachedTitle = "Månatlig utgiftsgräns nådd" title = "Det gick inte att öppna Stripe-portalen" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "av {{allowance}} gratis PDF:er använda" capSuffix_other = "av {{allowance}} gratis PDF:er använda" eyebrow = "Processor-provperiod" @@ -6534,6 +6756,376 @@ signature = "signatur" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Ny anslutning" +createTitleFor = "Anslut {{name}}" +delete = "Ta bort" +edit = "Redigera" +editTitle = "Redigera anslutning" +unsupportedType = "Den här anslutningstypen kan inte redigeras här." + +[portal.connections.categories.advanced] +description = "För en tjänst som vi inte stöder ännu." +label = "Avancerat" + +[portal.connections.categories.audit] +description = "Registrera vad som hände med ett dokument, där dina revisorer redan tittar." +label = "Granskning och efterlevnadsloggning" + +[portal.connections.categories.notify] +description = "Berätta för ett annat system att något har hänt." +label = "Avisering och arbetsflöde" + +[portal.connections.categories.security] +description = "Skanna eller etikettera ett dokument innan det går vidare." +label = "Säkerhet och klassificering" + +[portal.connections.categories.signing] +description = "Skicka ut ett dokument för signering eller notarisering." +label = "E-signatur och notarisering" + +[portal.connections.categories.storage] +description = "Där bearbetade dokument hamnar – en bucket, en enhet, ett ärende eller en sida." +label = "Fil och bifoga" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling lagrar detta krypterat och visar det aldrig igen." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Steg som använder denna anslutning kan bara anropa sökvägar under denna URL, aldrig en annan host." +label = "Server-URL" + +[portal.connections.commonFields.email] +label = "E-post" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Lösenord" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Användarnamn" + +[portal.connections.commonFields.webhookUrl] +helperText = "Klistra in URL som tjänsten gav dig. Endast sökvägar under den kan anropas." +label = "Webhook URL" + +[portal.connections.fields] +name = "Anslutningsnamn" +namePlaceholder = "t.ex. Produktion" + +[portal.connections.picker] +cancel = "Avbryt" +createNew = "Ny anslutning..." +placeholder = "Välj en anslutning" +save = "Spara anslutning" + +[portal.connections.picker2] +back = "Ändra integration" +heading = "Vad vill du ansluta?" +noResultsBody = "Vi stöder det inte ännu – berätta om det så tittar vi på att lägga till det." +noResultsTitle = "Inget matchar \"{{query}}\"" +searchPlaceholder = "Sök integrationer – prova \"sign\", \"ocr\" eller ett produktnamn" + +[portal.connections.types.api] +description = "Anropa valfritt HTTP API från en policy. Du väljer URL, autentisering och payload." +label = "Anpassat API" + +[portal.connections.types.api.fields.authType] +label = "Autentisering" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Användarnamn och lösenord" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token i en anpassad header" + +[portal.connections.types.api.fields.authType.options.none] +label = "Ingen" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Logga in för en token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Steg som använder denna anslutning kan bara anropa sökvägar under denna URL, aldrig en annan host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header-namn" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Inloggningssökväg" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Lösenord" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Om API svarar med en nedladdningslänk får endast dessa hosts hämtas. Separera med kommatecken. Lämna tomt för att endast tillåta base URL:ens egen host." +label = "Tillåtna resultathosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Skicka token som denna header" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Svarsheadern som inloggningsanropet returnerar token i, t.ex. X-Auth-Token." +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "Användarnamn" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Skanna dokument efter skadlig kod på en server du kör – inget lämnar ditt nätverk." +label = "ClamAV (självhostad)" + +[portal.connections.types.cloudmersive] +description = "Virus- och innehållsskanning för dokument i en policy." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Blockera dokument som innehåller makron, körbara filer, skript eller inbäddade objekt – inte bara känd skadlig kod." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Bifoga bearbetade dokument till en Confluence-sida." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Skicka dokument till ConsignO Cloud (Notarius) för elektronisk signatur och notarisering." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Din ConsignO Cloud-miljö, inklusive /api/v1." +label = "Server-URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Lösenord" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Behövs bara om din organisation använder tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Användarnamn" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Publicera ett meddelande till Discord när en policy bearbetar ett dokument." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Indexera en granskningshändelse i Elasticsearch varje gång en policy hanterar ett dokument." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Hämta nya dokument från en FTP- eller FTPS-server." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Anslutningsläge" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktiv" +passive = "Passiv (rekommenderas)" + +[portal.connections.types.ftp.fields.security] +helperText = "Vanlig FTP skickar inloggningsuppgifter och filer okrypterat; välj FTPS om servern stöder det." +label = "Kryptering" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Ingen (vanlig FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Publicera ett meddelande till ett Google Chat-utrymme när en policy körs." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Bifoga bearbetade dokument till ett Jira-ärende." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Skicka ett dokument eller en avisering via e-post när en policy körs." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Lagra bearbetade dokument i Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Applösenord" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Identifiera personuppgifter i dokumenttext med en Presidio-server som du kör." +label = "Presidio (självhostad)" + +[portal.connections.types.purview] +description = "Läs och tillämpa Microsoft Purview-känslighetsetiketter på PDFs." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Valfritt. Behövs bara för att välja etiketter efter namn; etikettering fungerar utan det." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Stämplas på varje etikett som dess SiteId, så Purview-medvetna verktyg vet vilken organisation som tillämpade den." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "En S3 bucket som källor kan läsa från och pipeline-utdata kan skriva till." +label = "S3-lagring" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Lämna tomt för AWS. Ange detta för MinIO eller en annan S3-kompatibel lagring." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Skicka ett dokument eller en avisering via e-post när en policy körs." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Hämta nya dokument från en SFTP (SSH)-server." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Valfritt. Serverns SHA-256 host key fingerprint (från `ssh-keyscan host | ssh-keygen -lf -`). När den anges accepteras endast en server som presenterar denna nyckel; när den lämnas tom fästs nyckeln som ses vid första anslutningen och senare ändringar nekas." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Lämna tomt om du autentiserar med en privat nyckel istället." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Valfritt. Klistra in en privat OpenSSH-nyckel för att autentisera med en nyckel istället för ett lösenord." +label = "Privat nyckel" + +[portal.connections.types.slack] +description = "Publicera ett meddelande till Slack när en policy bearbetar ett dokument." +label = "Slack" + +[portal.connections.types.smb] +description = "Övervaka en SMB / CIFS-nätverksresurs (Windows eller Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Nätverksresurs" + +[portal.connections.types.smb.fields.domain] +helperText = "Valfritt. Windows-domänen eller arbetsgruppen, om din server använder en." +label = "Domän" + +[portal.connections.types.smb.fields.share] +label = "Resurs" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Skicka en granskningshändelse till Splunk varje gång en policy hanterar ett dokument." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Skicka en granskningshändelse till en Sumo Logic HTTP-insamlare." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Publicera ett meddelande till en Teams-kanal när en policy körs." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Publicera varje dokument, eller bara detaljerna, till en URL du väljer." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Utlös ett Zap- eller Make-scenario från en policy." +label = "Zapier och Make" + +[portal.docs] +browse = "Bläddra i dokumentation" +viewSource = "Visa källa på GitHub" + [portal.docs.authentication] codeCaption = "varje förfrågan" eyebrow = "KOM IGÅNG" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Medföljande, namngivna förmågor som din agent anropar som ett enda verktyg. Varje skill är en deterministisk åtgärdskedja med kopplade evals." title = "Agent-skills" +[portal.docs.toc] +title = "På denna sida" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API-REFERENS" @@ -6697,6 +7292,8 @@ type = "Typ" user = "Användare" [portal.documents.queue.empty] +connectSource = "Anslut en källa" +createPipeline = "Skapa en pipeline" description = "När källor matar in dokument i dina pipelines visas de här för granskning." title = "Inga dokument i kön" @@ -6712,11 +7309,11 @@ editorAction = "Redigerare" empty = "Inga dokument matchar det här filtret." rowActions = "Radåtgärder" sensitiveLabel = "Känsligt" -sensitiveTitle = "Känsligt — åtkomst krävs" [portal.documents.table.columns] action = "Pipeline / Åtgärd" document = "Dokument" +labels = "Labels" product = "Produkt" status = "Status" time = "Tid" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Senast sedd" region = "Region" status = "Status" +target = "Target" version = "Version" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Den här vyn stötte på ett oväntat fel. Försök igen eller v retry = "Försök igen" title = "Något gick fel på den här sidan" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Tillbaka" +body = "Ladda ner desktop-appen eller självhosta servern." +done = "Klart" +guide = "Öppna guiden för {{name}}" +openInBrowser = "Öppna i webbläsaren istället" +sectionDesktop = "Desktop-app" +sectionSelfHosted = "Självhostad" +title = "Installera Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Kör den officiella imagen på din server." +note = "Öppnas på :8080. Standardinloggning: admin / stirling." +tagline = "Kör servern i en container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Installera det officiella Helm chart." +note = "Lägg till en PVC för /configs för att spara inställningar." +tagline = "Distribuera till ett kluster med Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Inbyggda byggen för din distribution." +guideBtn = "Visa installationsalternativ för Linux" +note = "AppImage-, .deb- och .rpm-byggen är tillgängliga för vanliga distributioner." +tagline = "Inbyggd skrivbordsapp för din distribution" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Eller via Homebrew" +detailBody = "Kör installationsprogrammet eller använd Homebrew." +downloadBtn = "Ladda ner för macOS" +tagline = "Inbyggd app för Apple silicon och Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Kör .jar på en bare metal-server." +downloadBtn = "Ladda ner .jar" +note = "Kräver Java 21+. Öppnas på :8080; standardinloggning admin / stirling." +runLabel = "Kör den sedan" +tagline = "Kör .jar på en bare metal-server" +title = "Manuell serverkonfiguration" + +[portal.home.download.windows] +altLabel = "Eller via winget" +detailBody = "Kör installationsprogrammet eller använd en pakethanterare." +downloadBtn = "Ladda ner installationsprogram" +tagline = "Inbyggd app för Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktiva" -invite = "Bjud in teammedlemmar" +activeOfDeployed = "{{active}} av {{total}} aktiva denna månad" name = "Stirling PDF Editor" open = "Öppna i webbläsare" updated = "uppdaterad {{time}}" +[portal.home.editor.deploy] +finish = "Slutför distribution" +options = "Visa distributionsalternativ" +start = "Distribuera editorn" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Självhostad · Docker" @@ -6823,49 +7529,6 @@ afternoon = "God eftermiddag" evening = "God kväll" morning = "God morgon" -[portal.home.onboarding] -dismiss = "Avvisa konfiguration" -notStarted = "Inte startad" -progress = "{{done}} av {{total}} klara" -title = "Slutför konfigureringen" - -[portal.home.onboarding.enterprise] -body = "Organisationsomfattande SSO + SCIM + RBAC, avtalad volymprissättning och distribution i isolerad miljö." -cta = "Starta provperiod" -ctaQuote = "Få offert" -lead = "För 250+ anställda." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Distribuera skrivbordsappen, gratis för alltid." -title = "Ladda ner PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktiva · {{recommended}} rekommenderade" -chip = "{{active}} aktiva" -title = "Aktivera policyer" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} anslutna · varje PDF styrs där den hamnar" -chip = "{{connected}} anslutna" -title = "Anslut dina källor" - -[portal.home.quickActions] -subtitle = "Viktigaste uppgifterna för idag" -title = "Snabbåtgärder" - -[portal.home.quickActions.buildPipeline] -blurb = "3-stegsbyggare över det typade åtgärdsbiblioteket" -title = "Bygg en pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agenter, webhooks, bevakade mappar" -title = "Anslut en källa" - -[portal.home.quickActions.issueApiKey] -blurb = "Avgränsad nyckel med hastighetsgränser och IP-tillåtelselista" -title = "Utfärda en API-nyckel" - [portal.infrastructure] manageEditorDeployment = "Hantera Editor-distribution" sectionsAriaLabel = "Infrastrukturavsnitt" @@ -6878,13 +7541,9 @@ heading = "API-nycklar" subheading = "Avgränsade autentiseringsuppgifter med hastighetsgränser per nyckel, behörigheter och IP-tillåtelselistor." [portal.infrastructure.apiKeys.card] -allowedIps = "Tillåtna IP-adresser" -anyIp = "Valfri IP (ingen tillåtelselista)" created = "Skapad" lastUsed = "Senast använd" -permissions = "Behörigheter" -rateLimit = "Hastighetsgräns" -rateLimitValue = "{{value}} beg/min" +revoke = "Återkalla nyckel" usageMonth = "Användning denna månad" usageToday = "Användning idag" @@ -6892,6 +7551,15 @@ usageToday = "Användning idag" description = "Skapa en avgränsad nyckel för att börja anropa Stirling API." title = "Inga API-nycklar ännu" +[portal.infrastructure.apiKeys.error] +load = "Kunde inte läsa in dina API-nycklar. Försök igen." + +[portal.infrastructure.apiKeys.revoke] +body = "Återkalla \"{{name}}\"? Alla integrationer som fortfarande använder den här nyckeln kommer omedelbart att börja få 401-fel. Detta kan inte ångras." +cancel = "Avbryt" +confirm = "Återkalla nyckel" +title = "Återkalla API-nyckel" + [portal.infrastructure.audit] filterAriaLabel = "Filtrera granskningshändelser efter kategori" heading = "Granskningsloggar" @@ -6935,6 +7603,7 @@ all = "Alla" auth = "Auth" config = "Konfiguration" elevation = "Åtkomsthöjning" +policy = "Policy" processing = "Bearbetning" security = "Säkerhet" @@ -6944,19 +7613,30 @@ title = "Du har inte åtkomst" [portal.infrastructure.audit.metrics] config = "Konfiguration" -elevation = "Åtkomsthöjning" +policy = "Policy" processing = "Bearbetning" totalEvents = "Totalt antal händelser · 24 h" +[portal.infrastructure.auditCatLabel] +auth = "Autentisering" +config = "Konfiguration" +elevation = "Behörighetshöjning" +policy = "Policy" +processing = "Bearbetning" +security = "Säkerhet" + +[portal.infrastructure.auditStatusLabel] +danger = "Fel" +info = "Info" +success = "Klart" +warning = "Varning" + [portal.infrastructure.createKey] cancel = "Avbryt" createKey = "Skapa nyckel" done = "Klar" -ipAllowlistHelper = "Kommaseparerade CIDR-intervall. Lämna tomt för att tillåta valfri IP." -ipAllowlistLabel = "IP-tillåtelselista" keyNameLabel = "Nyckelnamn" keyNamePlaceholder = "t.ex. Produktion · ingest" -permissionsLabel = "Behörigheter" secretKeyCaption = "Hemlig nyckel" secretWarning = "Lagra detta i en hemlighetshanterare. Stirling lagrar endast en hash — det går inte att återställa den senare." subtitle = "Avgränsa nyckeln till det minsta den behöver. Du kan rotera eller återkalla den när som helst." @@ -6964,207 +7644,9 @@ subtitleCreated = "Kopiera den här hemligheten nu — den visas inte igen." title = "Skapa API-nyckel" titleCreated = "Nyckel skapad" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Distribuerad av" -environment = "Miljö" -product = "Produkt" -status = "Status" -version = "Version" -when = "När" - -[portal.infrastructure.deployments.recent] -heading = "Senaste distributioner" -subheading = "De senaste utrullningarna över produkter och miljöer." - -[portal.infrastructure.deployments.regionColumns] -instances = "Instanser" -latency = "Latens" -load = "Belastning" -p99 = "P99" -region = "Region" -status = "Status" -throughput = "Genomströmning" -uptime = "Drifttid" -version = "Version" - -[portal.infrastructure.deployments.regions] -heading = "Regioner" -subheading = "Livehälsa för varje distribuerad Stirling-region — latens, belastning och utrullningsversion." - -[portal.infrastructure.deployments.regions.empty] -description = "Distribuerade regioner visas här när din arbetsyta har provisionerats." -title = "Inga regioner distribuerade" - -[portal.infrastructure.models] -heading = "Modeller" -msValue = "{{value}} ms" -subheading = "Modellkatalogen och routingen som driver dokumentbearbetning i din arbetsyta." - -[portal.infrastructure.models.byom] -description = "Registrera en lokal eller självhostad modell och fäst den vid en region för behandling som är bunden till dataresidens." -title = "Ta med din egen modell" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Hanterade modeller tillgängliga för din arbetsyta, med live-latens och kostnad." -subEnterprise = "Hanterade, egna och lokala modeller — med fästning per region tillgänglig." - -[portal.infrastructure.models.catalogue.empty] -description = "Modeller i din arbetsytas katalog visas här." -title = "Inga modeller tillgängliga" - -[portal.infrastructure.models.columns] -cost = "Kostnad" -latency = "Latens" -load = "Belastning" -model = "Modell" -status = "Status" -type = "Typ" -version = "Version" - -[portal.infrastructure.models.metrics] -activeModels = "Aktiva modeller" -avgLatency = "Genomsnittlig latens" -included = "Inkluderat" -monthlySpend = "Månatlig modellkostnad" - -[portal.infrastructure.models.routing] -empty = "Inga routingregler konfigurerade." -heading = "Routingregler" -sub = "Vilken modell som hanterar varje åtgärd. Standardvärdet används när ingen smalare regel matchar." -subLocked = "Routa åtgärder till specifika modeller — tillgängligt med betalplaner." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Uppgradera till Pro för att styra vilken modell som hanterar varje åtgärd och dokumenttyp." -title = "Modellrouting är en betalfunktion" - -[portal.infrastructure.models.routingColumns] -default = "Standard" -docType = "Dokumenttyp" -modelForAria = "Modell för {{operation}}" -operation = "Åtgärd" -routedTo = "Routas till" - -[portal.infrastructure.security.access.byok] -description = "Tillhandahåll en nyckel från din egen KMS. Stirling krypterar med den men kan fortfarande läsa." -label = "Ta med din egen nyckel (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Nycklar lämnar aldrig din KMS. Stirling lagrar endast chiffertext." -label = "Behåll din egen nyckel (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling hanterar krypteringsnycklar. Enklast — ingen nyckeldrift på din sida." -label = "Nycklar som hålls av Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Policy för dokumentåtkomst" -subheading = "Styr vem som kan dekryptera bearbetade dokument i vila." - -[portal.infrastructure.security.attestations] -heading = "Efterlevnadsintyg" -noReport = "Ingen rapport tillgänglig" -subheading = "Granskningsläge per ramverk, med rapporter tillgängliga för intygade kontroller." -viewReport = "Visa rapport →" - -[portal.infrastructure.security.compliance] -heading = "Efterlevnad" -subheading = "Intyg och certifieringar som täcker Stirling-plattformen." - -[portal.infrastructure.security.empty] -description = "Din arbetsytas säkerhetskonfiguration visas här." -title = "Säkerhetsläge inte tillgängligt" - -[portal.infrastructure.security.hyokBanner] -description = "Med HYOK lämnar krypteringsnycklar aldrig din KMS. Stirling lagrar och bearbetar endast chiffertext som du kan återkalla när som helst." -title = "Stirling kan inte dekryptera dina dokument" - -[portal.infrastructure.security.ipAllowlist] -empty = "Inga IP-intervall konfigurerade — alla IP-adresser tillåtna." -heading = "IP-tillåtelselista" -sub = "API-åtkomst är begränsad till dessa CIDR-intervall." -subLocked = "Begränsa API-åtkomst till kända IP-intervall — tillgängligt med betalplaner." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Uppgradera till Pro för att begränsa API-åtkomst till specifika nätverk." -title = "IP-tillåtelselista är en betalfunktion" - -[portal.infrastructure.security.ipColumns] -added = "Tillagd" -addedBy = "Tillagd av" -cidr = "CIDR" -label = "Etikett" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritm" -heading = "Hantering av krypteringsnycklar" -keyId = "Nyckelidentifierare" -lastRotated = "Senast roterad" -rotateKey = "Rotera nyckel" -rotationPolicy = "Rotationspolicy" -subheading = "Förvaring av nycklarna som krypterar dokument i vila — vem som kan dekryptera och hur nycklar roteras." - -[portal.infrastructure.security.managedBanner] -description = "Förvaring med bring-your-own-key (BYOK) och hold-your-own-key (HYOK) är tillgänglig i Enterprise. Uppgradera för att tillhandahålla nycklar från din egen KMS." -title = "Nycklar hanteras av Stirling i din plan" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asien och Stillahavsområdet" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR-datagräns" -label = "Europeiska unionen" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "USA" - -[portal.infrastructure.security.residencyHeader] -heading = "Dataresidens" -subheading = "Var dokument lagras och bearbetas." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} används" - -[portal.infrastructure.storage.empty] -description = "Ansluten lagring och användning visas här." -title = "Ingen lagring konfigurerad" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktiv" -activeRange = "0–{{value}} d" -archived = "Arkiverad" -coldStorage = "kallagring" -deleted = "Borttagen" -never = "aldrig" -purged = "rensad" - -[portal.infrastructure.storage.providers] -connect = "Anslut" -connected = "Ansluten" -heading = "Anslutna leverantörer" -subheading = "Där bearbetade artefakter skrivs." - -[portal.infrastructure.storage.retention] -heading = "Lagringstid" -subheading = "Hur länge artefakter behålls innan livscykelborttagning." -windowLabel = "Standardfönster för lagringstid" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} dag" -days_other = "{{count}} dagar" -never = "Ta aldrig bort" - -[portal.infrastructure.storage.totalUsage] -heading = "Total användning" -progressLabel = "Lagring använd" -subheading = "Lagring som förbrukats över alla anslutna leverantörer." +revoked = "Återkallad" [portal.infrastructure.tabs] apiKeys = "API-nycklar" @@ -7174,6 +7656,49 @@ models = "Modeller" security = "Säkerhet" storage = "Lagring" +[portal.integrations] +availableHeading = "Tillgängliga" +comingSoonHeading = "Kommer snart" +connect = "Anslut" +connectionCount_one = "{{count}} anslutning" +connectionCount_other = "{{count}} anslutningar" +customApi = "Anpassat API" +searchPlaceholder = "Sök integrationer" +subtitle = "Allt som Stirling kan kommunicera med. Inloggningsuppgifter skapas en gång här och refereras sedan av källor, policies och pipelines." +title = "Integrationer" +typedName = "{{tool}}-namn" + +[portal.integrations.filters] +all = "Alla" +audit = "Granskning" +connected = "Anslutna" +notify = "Notifiera" +security = "Säkerhet" +signing = "Signering" +storage = "Lagring" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Ansluten" + +[portal.integrations.table] +integration = "Integration" +worksWith = "Fungerar med" + +[portal.integrations.worksWith] +pipelines = "Pipelines" +policies = "Policies" +sources = "Källor" + +[portal.legal] +draft = "{{label}} · utkast" +loadError = "Kunde inte läsa in det här dokumentet. Försök igen." +loading = "Läser in…" +title = "Juridiskt dokument" + [portal.nav] agent-builder = "Agentbyggare" components = "Komponenter" @@ -7182,33 +7707,17 @@ documents = "Dokument" editor = "Editor" home = "Hem" infrastructure = "Infrastruktur" +integrations = "Integrationer" pipelines = "Pipelines" policies = "Policyer" procurement = "Inköp" -settings = "Inställningar" sources = "Källor" usage = "Användning och fakturering" users = "Användare" -[portal.notifications] -markAllRead = "Markera alla som lästa" -title = "Aviseringar" -viewAll = "Visa alla" - -[portal.notifications.ariaLabel] -none = "Aviseringar, inga olästa" -unread_one = "Aviseringar, {{count}} oläst" -unread_other = "Aviseringar, {{count}} olästa" - -[portal.notifications.count] -allRead = "alla lästa" -loading = "läser in" -new_one = "{{count}} ny" -new_other = "{{count}} nya" - -[portal.notifications.empty] -description = "Inga nya aviseringar." -title = "Du är helt uppdaterad" +[portal.nav.section] +platform = "PDF-plattform" +processor = "PDF-processor" [portal.pipelines] subtitle = "Varje automatiserad dokumentpipeline på backend: en ordnad kedja av åtgärder över en uppsättning källor, körd vid en trigger. Klicka på en rad för dess steg och källor." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Ny pipeline" [portal.pipelines.builder] -addStep = "Lägg till verktyg" +activate = "Activate" back = "Tillbaka till pipelines" +cannotFollow = "Kan inte ta emot {{produced}}" +chooseAccount = "Välj ett konto" +chooseDestination = "Välj en destination" +chooseOperation = "Välj vad det här steget gör" +chooseSource = "Välj en källa" discard = "Kassera ändringar" -enabled = "Aktiverad" +inputs = "Indata" +inputSource = "Indatakälla" +inputTrigger = "Utlösare" keepEditing = "Fortsätt redigera" -needsUpload = "Behöver en uppladdad fil" +moreActions = "More actions" +needsConfiguring = "Behöver konfigureras" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Inga verktyg matchar din sökning." -pipelineSettings = "Pipeline-inställningar" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Sök verktyg" -selectToolBody = "Lägg till ett verktyg för att bygga din pipeline." -selectToolTitle = "Inga verktyg ännu" -toolSettings = "Verktygsinställningar" +sendToSystem = "Skicka till ett annat system" +stepsIncompatible = "Dessa steg kan inte köras på det som deras föregående steg producerar: {{tools}}." +stepsNeedSetup = "Dessa steg behöver fortfarande konfigureras innan du sparar: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Okänd åtgärd, behålls som den är." unsavedBody = "Du har osparade ändringar. Spara dem innan du lämnar, eller kassera dem?" unsavedTitle = "Osparade ändringar" -uploadUnsupported = "Uppladdade filer stöds inte i pipelines ännu, så de här stegen kan inte sparas: {{tools}}." usesDefaults = "Körs med standardinställningar" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kombinerar varje fil från föregående steg" +fan-out = "Körs en gång per fil från föregående steg" +format-mismatch = "Behöver {{accepts}}, men föregående steg producerar {{produced}}" +output-uncertain = "Kanske inte körs: föregående stegs utdata beror på hur det är konfigurerat" +source-mismatch = "Behöver {{accepts}}, men denna pipelines indata är {{produced}}" +undeclared-operation = "Kan inte kontrollera vad det här steget accepterar" [portal.pipelines.composer] addTool = "Lägg till verktyg" -cancel = "Avbryt" -chainEmpty = "Lägg till ett verktyg för att börja bygga din pipeline." create = "Skapa pipeline" -directory = "Utdatamapp" -directoryHelp = "Absolut sökväg på servern. Måste vara inom de konfigurerade tillåtna mapparna." +createPaused = "Create paused" editingUnsupported = "Visning av dessa verktygsparametrar för redigering stöds inte ännu." -moveDown = "Flytta ned" -moveUp = "Flytta upp" +editSource = "Edit source" name = "Namn" namePlaceholder = "t.ex. Maskeringsgenomgång" -noSources = "Inga källor anslutna ännu. Pipelinen kan fortfarande köras på filer som tillhandahålls direkt till den." noToolSettings = "Det här verktyget har inga konfigurerbara inställningar." -operations_one = "Åtgärd ({{count}})" -operations_other = "Åtgärder ({{count}})" output = "Utdata" -removeStep = "Ta bort åtgärd" save = "Spara ändringar" scheduleEvery = "Kör varje" -sources = "Källor" -sourcesLoading = "Läser in källor..." trigger = "Trigger" triggerManual = "Endast manuellt" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "dagar" hours = "timmar" minutes = "minuter" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Ta bort \"{{name}}\"? Detta kan inte ångras." cancel = "Avbryt" @@ -7273,28 +7815,62 @@ confirm = "Ta bort" title = "Ta bort pipeline?" [portal.pipelines.detail] +clearHistory = "Rensa historik" delete = "Ta bort pipeline" run = "Kör nu" [portal.pipelines.empty] action = "Skapa en pipeline" +connectSource = "Anslut en källa" description = "Skapa din första pipeline: välj källorna den körs över, kedja åtgärderna och välj vart utdata ska skickas." title = "Inga pipelines ännu" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktiva" paused = "Pausade" total = "Pipelines" -[portal.pipelines.output] -folder = "Skriv till mapp" -inline = "Returnera filer" - [portal.pipelines.run] +allProcessed_one = "Inget att köra: källans {{count}} dokument har redan bearbetats." +allProcessed_other = "Inget att köra: alla {{count}} dokument i källorna har redan bearbetats." completed_one = "Körning slutförd." completed_other = "Alla {{count}} körningar slutförda." empty = "Inget att köra: källorna hade inga dokument att bearbeta." failed = "Körning misslyckades: {{error}}" +inFlight = "Inget nytt att köra: dokument bearbetas fortfarande från en tidigare körning." +parked_one = "Inget att köra: {{count}} dokument misslyckades tidigare och är parkerat. Åtgärda orsaken och rensa sedan historiken för att försöka igen." +parked_other = "Inget att köra: {{count}} dokument misslyckades tidigare och är parkerade. Åtgärda orsaken och rensa sedan historiken för att försöka igen." running = "Körning startad; pågår fortfarande." timeout = "Körningen tar längre tid än väntat; den kan fortfarande slutföras i bakgrunden." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Källor" status = "Status" steps = "Steg" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Mappbevakning" @@ -7314,12 +7891,18 @@ manual = "Manuell" schedule = "Schemalagd" [portal.policies] +defaultName = "{{category}}-policy" subtitle = "Stående automatiseringar som tillämpar en verktygspipeline på varje dokument. Varje policy utlöses vid uppladdning eller export, kör sin verktygskedja och sparar den tillämpade versionen bredvid originalet." title = "Policyer" [portal.policies.card] comingSoon = "Uppgradera till Enterprise" notSetUp = "Inte konfigurerad" +requiresAiEngine = "Kräver AI-motor" + +[portal.policies.categories.classification] +desc = "Identifiera varje dokuments typ vid uppladdning och tagga dess metadata för arkivering och sökning." +label = "Klassificering" [portal.policies.categories.compliance] desc = "Tillämpa HIPAA-, GDPR-, SOC 2- eller FedRAMP-krav på varje dokument." @@ -7344,6 +7927,13 @@ label = "Säkerhet" [portal.policies.config] scopeAll = "Alla dokument" +[portal.policies.config.classification] +summary = "Klassificerar varje uppladdat dokument mot ditt teams etiketter och taggar dess metadata." + +[portal.policies.config.classification.rules] +0 = "Klassificera" +1 = "Tagga metadata" + [portal.policies.config.compliance] summary = "Validerar dokument mot regelverksramverk innan de lämnar systemet." @@ -7371,6 +7961,29 @@ minConfidence = "Minsta konfidens" 2 = "Namnge" 3 = "Normalisera" +[portal.policies.config.purview] +label = "Tillämpa en känslighetsetikett från Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview-klientorganisation" +labelId = "Etikett-ID" +labelIdHelp = "Etikettens GUID från Purview-portalen. När en etikett tillämpas skrivs dess metadata till PDF:en; den kan inte kryptera, vilket kräver Microsoft-klienten." +labelName = "Etikettnamn (valfritt)" +labelNameHelp = "Sparas tillsammans med ID så att personer och verktyg kan känna igen etiketten." + +[portal.policies.config.purview.fields.method] +help = "Purview skiljer en etikett som en policy tillämpade från en som en person valde." +label = "Hur etiketten sparas" +privileged = "Som vald av en person" +standard = "Som tillämpad automatiskt" + +[portal.policies.config.purviewRead] +label = "Läs en Microsoft Purview-etikett" + +[portal.policies.config.purviewRead.fields] +connection = "Purview-klientorganisation" +connectionHelp = "Läser etiketten som dokumentet redan har, så att senare steg kan agera på den." + [portal.policies.config.retention] summary = "Tillämpar hur länge dokument behålls, när de arkiveras och när de tas bort." @@ -7438,6 +8051,7 @@ title = "Ingen aktivitet än" [portal.policies.endpoints] addWatermark = "Vattenstämpel" autoRedact = "Maskera PII" +classifyAndLabel = "Klassificera" compressPdf = "Komprimera" flatten = "Platta ut" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Dina policyer sparas och visas när anslutningen har återställt retry = "Försök igen" title = "Backend inte tillgänglig" +[portal.policies.operations] +change = "Ändra vad det här steget gör" +noResults = "Inget steg matchar det. Prova ett produktnamn eller \"skanna\", \"notifiera\", \"bifoga\"." +searchPlaceholder = "Sök steg – prova \"skanna\", \"bifoga\" eller ett produktnamn" + +[portal.policies.operations.bodyMode] +binary = "Råa filbytes" +json = "Inuti en JSON-payload" +multipart = "Som en filuppladdning (multipart)" + +[portal.policies.operations.clamavScan] +description = "Din egen ClamAV-server kontrollerar dokumentet; ingenting lämnar ditt nätverk." +label = "Skanna efter virus (egenhostad)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Avvisar dokument som innehåller makron, körbara filer, skript eller inbäddade objekt." +label = "Blockera aktivt innehåll" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive kontrollerar dokumentet och körningen stoppas om det inte är rent." +label = "Skanna efter virus" + +[portal.policies.operations.confluenceAttach] +description = "Arkiverar det bearbetade dokumentet på en sida." +label = "Bifoga till en Confluence-sida" + +[portal.policies.operations.consignoSubmit] +description = "Skickar dokumentet till ConsignO Cloud för signering." +label = "Skicka för signatur (ConsignO)" +note = "Skickar dokumentet. Hämtning av den signerade kopian stöds inte ännu, eftersom ett steg inte kan skicka ett resultat till ett senare steg." + +[portal.policies.operations.customApiCall] +description = "Skapa anropet själv: sökväg, metod, body och svarshantering." +label = "Anropa ett anpassat API" + +[portal.policies.operations.discordNotify] +description = "Meddelar en kanal att policyn hanterade ett dokument." +label = "Publicera ett meddelande på Discord" + +[portal.policies.operations.elasticIndex] +description = "Registrerar vad policyn gjorde som ett sökbart dokument." +label = "Indexera en granskningshändelse i Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart passar uppladdnings-API:er; JSON passar API:er som vill ha filen i en payload; binary skickar de råa byten." +label = "Hur dokumentet ska skickas" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "En JSON-body som skickas som den är. {{document.base64}} innehåller filen; {{document.*}} och {{run.*}} fylls i per dokument." +label = "JSON-body" + +[portal.policies.operations.fields.connection] +label = "Konto" + +[portal.policies.operations.fields.domain] +label = "Mailgun-domän" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Formulärfältet som API:et förväntar sig dokumentet i." +label = "Filfältsnamn" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Från" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Ett JSON-objekt med par av header-namn/värde. Anslutningens inloggningsuppgift läggs till åt dig." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ärendenyckel" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Skickas som meddelandets body. {{document.*}} och {{run.*}} fylls i per dokument." +label = "Meddelande" + +[portal.policies.operations.fields.method] +label = "Metod" + +[portal.policies.operations.fields.pageId] +label = "Sid-ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Läggs till efter anslutningens bas-URL. Den kan aldrig nå en annan värd." +label = "Sökväg" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Var i enheten den ska skrivas. {{document.filename}} fylls i per dokument." +label = "Destinationssökväg" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapport lämnar dokumentet orört. Ersätt byter ut det mot det som kommer tillbaka." +label = "Vad som ska göras med svaret" + +[portal.policies.operations.fields.signerEmail] +label = "Signerarens e-post" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Ämne" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio analyserar den här texten. Använd {{document.title}} eller klistra in texten du vill kontrollera." +label = "Text att kontrollera" + +[portal.policies.operations.fields.to] +label = "Till" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud-användarnamn" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Meddelar ett utrymme att policyn hanterade ett dokument." +label = "Publicera ett meddelande på Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Arkiverar det bearbetade dokumentet på ett ärende." +label = "Bifoga till ett Jira-ärende" + +[portal.policies.operations.mailgunEmail] +description = "Skickar det bearbetade dokumentet som en bilaga." +label = "Skicka dokumentet via e-post (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Skriver det bearbetade dokumentet till en mapp." +label = "Ladda upp till Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Din egen Presidio-server rapporterar de personuppgifter den hittar." +label = "Identifiera personuppgifter" +note = "Presidio läser text, inte filer. Ge den texten du vill kontrollera — själva dokumentet laddas inte upp." + +[portal.policies.operations.responseMode] +replace = "Ersätt dokumentet med svaret" +report = "Behåll dokumentet, registrera svaret" + +[portal.policies.operations.sendgridEmail] +description = "Skickar det bearbetade dokumentet som en bilaga." +label = "Skicka dokumentet via e-post (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Meddelar en kanal att policyn hanterade ett dokument." +label = "Publicera ett meddelande på Slack" + +[portal.policies.operations.splunkEvent] +description = "Registrerar vad policyn gjorde där dina revisorer redan tittar." +label = "Skicka en granskningshändelse till Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Registrerar vad policyn gjorde till en HTTP-insamlare." +label = "Skicka en granskningshändelse till Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Meddelar en kanal att policyn hanterade ett dokument." +label = "Publicera ett meddelande på Teams" + +[portal.policies.operations.webhookPost] +description = "Skickar själva dokumentet till en URL som du väljer." +label = "Posta dokumentet till en webhook" + +[portal.policies.operations.zapierNotify] +description = "Skickar körningens detaljer till din automatisering." +label = "Utlös ett Zap- eller Make-scenario" + [portal.policies.stats] activeFor = "Aktiv" dataProcessed = "Data bearbetad" @@ -7473,6 +8282,13 @@ label = "Dokument tillämpade" description = "Konfigurerade men körs inte" label = "Pausade" +[portal.policies.table] +appliesTo = "Gäller för" +docs = "Dokument tillämpade" +enforces = "Tvingar igenom" +policy = "Policy" +status = "Status" + [portal.policies.wizard.actions] back = "Tillbaka" cancel = "Avbryt" @@ -7480,6 +8296,10 @@ continue = "Fortsätt" enablePolicy = "Aktivera policy" saveChanges = "Spara ändringar" +[portal.policies.wizard.capability.classify] +desc = "Identifierar dokumentets typ utifrån ditt teams etiketter och taggar det, så att det arkiveras och söks efter kategori." +label = "Klassificera dokumentet" + [portal.policies.wizard.capability.compress] desc = "Komprimerar dokumentet till en mindre filstorlek." label = "Minska filstorlek" @@ -7500,10 +8320,18 @@ label = "Maskera känslig information" desc = "Tar bort dolt JavaScript så att inget kan köras automatiskt när dokumentet öppnas." label = "Ta bort aktivt innehåll" +[portal.policies.wizard.capability.timestampPdf] +desc = "Bevisar att dokumentet fanns i exakt denna form vid en viss tidpunkt, med hjälp av en oberoende tidsstämpelutfärdare. Endast en hash skickas – dokumentet lämnar aldrig din server." +label = "Lägg till en betrodd tidsstämpel" + [portal.policies.wizard.capability.watermark] desc = "Stämplar en synlig markering (t.ex. ”Konfidentiellt”) över varje sida." label = "Lägg till en vattenstämpel" +[portal.policies.wizard.classification] +description = "Varje uppladdat dokument klassificeras mot de inbyggda etiketterna och taggas med de typer som passar. Etikettuppsättningen delas i hela ditt team." +labelsHeading = "Klassificeringsetiketter" + [portal.policies.wizard.errors] noTools = "Aktivera minst ett verktyg i arbetsflödet först." saveFailed = "Det gick inte att spara policyn. Försök igen." @@ -7548,73 +8376,118 @@ setUp = "Konfigurera policy för {{category}}" [portal.policies.wizard.workflow] description = "Välj vad denna policy gör med varje dokument den bearbetar." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktiva" -noRule = "Ingen regel tillämpas än" -subtitle = "Stående automatiseringar som varje dokument passerar genom, oavsett vilken pipeline som hanterar det." -title = "Vad som körs på dina PDF:er" - [portal.policySummary.action] -comingSoon = "Uppgradera till Enterprise" -configure = "Konfigurera" setUp = "Konfigurera" -[portal.policySummary.column] -activeRule = "Aktiv regel" -policy = "Policy" -status = "Status" +[portal.processorFlow] +footnote = "Antal avser de senaste 24 timmarna. Flödeshastigheten är illustrativ." +liveBadge = "Live" +stats = "{{connected}} anslutna · {{processed}} PDF-dokument bearbetade" +title = "PDF-processor" -[portal.policySummary.empty] -description = "När policyer har konfigurerats visas kategorierna här." -title = "Inga policyer än" +[portal.processorFlow.lens] +ariaLabel = "Visningsläge" +flow = "Flöde" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktiv" -off = "Av" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 h" +failed = "Misslyckades" +heading = "Resultat" +success = "Levererat" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktiva" +count = "{{n}} · 24 h" +heading = "Policies" +setUp = "Konfigurera" soon = "Snart" -[portal.processingStatus] -managePlan = "Hantera plan" -volumeSuffix = "PDF:er bearbetade · senaste 30 dagarna" +[portal.processorFlow.sankey] +waist = "Policies · {{n}} aktiva" + +[portal.processorFlow.sankey.empty] +description = "Anslut en källa och slå på en policy för att se dokument flöda genom processorn." +title = "Inget flöde ännu" + +[portal.processorFlow.sources] +comingSoonTag = "Anslut" +editor = "Stirling PDF Editor" +heading = "Källor" +perDay = "{{n}} / 24 h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-postintag" [portal.procurement] -reset = "Återställ inköp (demo)" subtitle = "Få ditt team utvärderat, kontrakterat och ombordat. Varje dokument på ett ställe." title = "Inköp" -[portal.procurement.action] -download = "Ladda ner" -pay = "Betala nu" -request = "Begär" -sign = "Granska och signera" -upload = "Ladda upp" - [portal.procurement.agreement] agreeCta = "Godkänn och prenumerera" +confidential = "Konfidentiellt" confirm = "Jag har läst och godkänner Stirling Enterprise Agreement." -eyebrow = "Avtal" -intro = "Ett samlat avtal täcker din affär: Master Service Agreement, beställningsformulär, EULA och databehandlingsavtal. Granska det och godkänn sedan för att acceptera offerten som en bindande prenumeration." -title = "Granska ditt företagsavtal" +docName = "Stirling Enterprise-avtal" +docSub = "MSA, Order Form, EULA och DPA, samlat för en signatur." +download = "Ladda ner" +downloadDraftError = "Kunde inte generera avtalets PDF – dokumentrenderaren är inte tillgänglig. Försök igen snart eller kontakta supporten." +downloadError = "Kunde inte ladda ner det signerade avtalet. Försök igen." +legalName = "Juridiskt enhetsnamn" +legalNamePlaceholder = "Den juridiska enhet som ska signera" +loadError = "Kunde inte läsa in avtalet. Försök igen." +loading = "Läser in avtalet..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Begär ändringar" +scrollHint = "Rulla för att granska" +signatory = "Signerarens namn" +signatoryPlaceholder = "Fullständigt namn" +signatoryTitle = "Signerarens titel" +signatoryTitlePlaceholder = "t.ex. chefsjurist" +signError = "Kunde inte registrera din signatur. Försök igen." [portal.procurement.builder] addons = "Tillägg" +addressLine1 = "Adressrad 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adressrad 2" +addressLine2Placeholder = "Svit, våning, byggnad" back = "Tillbaka" businessName = "Företagsnamn" businessNamePlaceholder = "Ditt företag" +city = "Ort" +cityPlaceholder = "San Francisco" +completeRequired = "Fyll i de obligatoriska fälten (markerade med *) med en giltig e-postadress innan offerten genereras." +contactEmail = "Kontakt-e-post" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Kontaktnamn" +contactNamePlaceholder = "Jane Doe" continue = "Fortsätt" -country = "Land" -countryEuro = "Eurozonen (EUR €)" -countryUK = "Storbritannien (GBP £)" -countryUS = "USA (USD $)" +done = "Klart" eula = "Jag har läst och godkänner Stirling Enterprise EULA. Den styr avtalet som genereras från denna offert." generate = "Generera offert" -included = "Ingår" indemnification = "IP-skadeslöshet" indemnificationSub = "Vi försvarar kvalificerande IP-anspråk enligt EULA" -offlineLicense = "Offline-/air-gapped-licens" -offlineLicenseSub = "En nedladdningsbar licensfil för en air-gapped egenhostad instans" +paperEyebrow = "Enterprise-offert" +paperFor = "Förberedd för" +pdfSize = "PDF-storlek" +poNumber = "PO-nummer" +poNumberPlaceholder = "Valfritt" +postalCode = "Postnummer" +postalCodePlaceholder = "94105" +posture = "Styrning" +posture_count = "~{{count}} policies" +posture_essentials = "Grundläggande" +posture_essentialsSub = "Klassificering + delning — standardinställningarna" +posture_governed = "Styrd" +posture_governedSub = "Lägger till säkerhet och routning" +posture_regulated = "Reglerad" +posture_regulatedSub = "Varje kategori — efterlevnad, lagring, inmatning" qbr = "Kvartalsvisa affärsgenomgångar" qbrSub = "Din SE går igenom användning och roadmap varje kvartal" +region = "Delstat / region" +regionPlaceholder = "Kalifornien" running = "{{annual}} / år · {{years}} år {{tcv}}" s1Sub = "Ditt team och de PDF:er du förväntar dig att köra varje år." s1Title = "Hur mycket kommer du att bearbeta?" @@ -7623,6 +8496,12 @@ s2Title = "Åtagande och service" s3Sub = "För offerten och avtalet den genererar." s3Title = "Dina uppgifter" serviceLevel = "Servicenivå" +size_compact = "Kompakt" +size_compactSub = "Mestadels text, under 1 MB" +size_heavy = "Tung" +size_heavySub = "Skannad eller bildtung, 5 MB+" +size_standard = "Standard" +size_standardSub = "Blandad text och bilder, 1 till 5 MB" slDedicated = "Dedikerad" slDedicatedSub = "4 arbetstimmar · dedikerad kontoansvarig · +30%" slPriority = "Prioriterad" @@ -7630,6 +8509,8 @@ slPrioritySub = "Samma arbetsdag · namngiven CSM · +15%" slStandard = "Standard" slStandardSub = "Nästa arbetsdag · delad CSM · ingår" stepOf = "Steg {{n}} av {{total}}" +taxId = "VAT / skatte-ID" +taxIdPlaceholder = "Valfritt" term = "Period" termDiscount = "{{pct}}% rabatt för flerårigt åtagande tillämpad" title = "Bygg din offert" @@ -7637,6 +8518,7 @@ training = "Onboarding och utbildning" trainingSub = "Livesessioner för att få igång ditt team" users = "Totalt antal användare" usersPlaceholder = "t.ex. 250" +viewEula = "Läs den" volEstimated = "Uppskattat från {{count}} användare (~2 000 PDF:er vardera, inklusive automatisering). Redigera om du vet bättre." volManual = "Använder din siffra. Uppskatta om från teamstorleken när som helst." volNoUsers = "Osäker? Ange din teamstorlek så uppskattar vi det." @@ -7645,55 +8527,55 @@ volumePlaceholder = "t.ex. 1 000 000" years_one = "{{count}} år" years_other = "{{count}} år" -[portal.procurement.docs] -count_one = "{{count}} dok" -count_other = "{{count}} dok" -done = "Klart" -here = "Du är här" -hide = "Dölj" -optional = "Valfritt" -paidAddon = "Betalt tillägg" -show = "Visa" -subtitle = "Allt du behöver vid varje steg på resan, lyfts fram när affären går vidare." -supportingSubtitle = "SOC 2, säkerhetsgranskningar, skatteformulär och mer, redo när ditt säkerhets- eller inköpsteam frågar. Vissa har en engångsavgift." -supportingTitle = "Stöd för din utvärdering" +[portal.procurement.documents] +agreement = "Enterprise-avtal" +agreementSub = "MSA, Order Form och DPA i en signatur." +download = "Ladda ner" +eula = "EULA och kommersiella villkor" +eulaSub = "Programvaran och kommersiella villkor för självbetjäning." +invoice = "Faktura" +invoiceSub = "Din första prenumerationsfaktura." +laterInvoice = "Tillgänglig när du har accepterat" +laterQuote = "Tillgänglig när din offert har utfärdats" +quote = "Offert" +quoteSub = "Din specificerade Enterprise-offert." +sla = "SLA-bilaga" +slaSub = "Supportnivåer och svarsmål." +subprocessors = "Underbiträden" +subprocessorsSub = "Tredje parter som behandlar data för din räkning." +subtitle = "Dina avtalsdokument, tillgängliga när som helst." title = "Dokument" -upcoming = "Kommande" +view = "Visa" [portal.procurement.error] title = "Något gick fel" [portal.procurement.hero] -company = "Din Enterprise-affär" +barAria = "Steg {{current}} av {{total}}" ctaAgreement = "Granska och signera avtal" -ctaLive = "Du är live" -ctaPayment = "Lägg till betalning" +ctaExploring = "Konfigurera din testperiod" ctaQuote = "Granska din offert" +ctaReviewQuote = "Granska offert" ctaTrial = "Bygg din offert" +documents = "Dokument" eyebrow = "Enterprise-inköp" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Bjud in teammedlemmar" -keyDocs = "Viktiga dokument" -nextStep = "Nästa steg: {{action}}" -notStarted = "Inte startad" +licenseKey = "Licensnyckel" +liveSub = "Din organisation är provisionerad och din licens är aktiv." +liveTitle = "Du är live på Stirling Enterprise" +next = "Nästa: {{stage}}" open = "Öppna inköp" scheduleCall = "Boka ett samtal" -setup1Sub = "Bjud in dina teammedlemmar" -setup1Title = "Distribuera PDF Editor" -setup2Sub = "Aktivera bearbetning över redigerare och andra källor" -setup2Title = "Anslut PDF Processor" -setup3Sub = "Aktivera säkerhet, efterlevnad, routing eller lagring när du behöver dem" -setup3Title = "Lägg till rekommenderade policyer" +sentenceAgreement = "granska och signera avtalet" +sentenceLive = "provisionering och utrullning" +sentencePayment = "inköpsorder och betalning" +sentenceQuote = "granska och acceptera din offert" +sentenceTrial = "konfigurera din testarbetsyta" [portal.procurement.journey] daysLeft_one = "{{count}} dag kvar" daysLeft_other = "{{count}} dagar kvar" -engineerLabel = "Din lösningsingenjör" -eyebrow = "Din utrullning" -live = "Du är live på Stirling Enterprise" -nextStep = "Nästa steg: {{action}}" -subtitle = "Din lösningsingenjör är med i varje steg. En nästa åtgärd i taget; hela checklistan finns nedan." -title = "Från testperiod till live, en guidad väg" -trialTitle = "Enterprise-testperiod" [portal.procurement.journeySteps.agreement] blurb = "En signatur täcker MSA, beställningsformulär, EULA och DPA." @@ -7727,6 +8609,9 @@ downloadError = "Det gick inte att generera offline-licensfilen just nu — för downloadOffline = "Ladda ner offline-licens (.lic)" hint = "Klistra in den här nyckeln i en egenhostad instans för att aktivera den, eller spara den för dina register. Förvara den säkert." label = "Din licensnyckel" +subtitle = "Aktivera en egenhostad instans med den här nyckeln, eller spara den för dina register." +title = "Din licensnyckel" +trialFileHint = "Detta är din testlicensfil. När ditt avtal är på plats kan du komma tillbaka och ladda ner den igen — till skillnad från licensnyckeln online uppdateras .lic-filen inte av sig själv." [portal.procurement.link] cta = "Länka konto" @@ -7739,65 +8624,62 @@ description = "Din prenumeration är aktiv och din licens är utfärdad. Ditt te eyebrow = "Live" title = "Du är live på Stirling Enterprise" -[portal.procurement.locked] -description = "Testnycklar, offerter med bunden volym, avtalet med en signatur, betalning och din dokumentliggare finns alla här när du startar en Enterprise-utvärdering." -eyebrow = "Endast Enterprise" -talkToSales = "Prata med försäljning" -title = "Inköpsspåret öppnas med Enterprise" - [portal.procurement.milestone] -accept = "Acceptera och fortsätt" -description = "Ladda ner PDF:en för att dela den med ditt team, kom tillbaka för att acceptera när du är redo, eller gör ändringar." -download = "Ladda ner PDF" downloadError = "Det gick inte att ladda ner offert-PDF:en just nu — försök igen om en stund." -edit = "Redigera offert" -eyebrow = "Offert {{number}}" -perYear = " / år" -preparedFor = "Förberedd för {{company}}" -tcv = "{{value}} totalt kontraktsvärde" -title = "Din offert är klar" - -[portal.procurement.modal] -cancel = "Avbryt" -chooseFile = "Välj fil" -close = "Stäng" -downloadBody = "Din nedladdning börjar inom kort." -downloadCta = "Ladda ner" -downloadTitle = "Ladda ner" -noFile = "Ingen fil vald" -payBody = "Betala ditt bundna avtal med kort eller banköverföring via Stripe. Din arbetsyta provisioneras så snart betalningen är klar." -payCta = "Fortsätt till Stripe" -payTitle = "Bekräfta betalning" -requestBodyFree = "Vi genererar detta på begäran. Bekräfta så skickar din lösningsingenjör det inom kort." -requestBodyPaid = "Detta är ett betalt tillägg. Bekräfta så avgränsar din lösningsingenjör det och skickar dokumenten." -requestCta = "Begär" -requestTitle = "Begär detta dokument" -signBody = "Öppnar Stirling Enterprise Agreement för e-signatur: en signatur täcker MSA, beställningsformulär, EULA och DPA. Vi motsignerar automatiskt och du går vidare till betalning." -signCta = "Öppna för signatur" -signTitle = "Granska och signera ditt avtal" -uploadBody = "Skicka oss din inköpsorder så fakturerar vi mot den enligt dina villkor. Dra in PDF:en eller välj en fil nedan." -uploadCta = "Ladda upp inköpsorder" -uploadTitle = "Ladda upp din inköpsorder" [portal.procurement.payment] description = "Din offert är accepterad och din licens är redan aktiv — ditt team kan börja direkt. Betala den första fakturan när du är redo; du kan betala eller ladda ner den här, ingen e-post behövs." +downloadAgreement = "Ladda ner signerat avtal" downloadInvoice = "Ladda ner faktura" -simulate = "Simulera mottagen betalning (demo)" +eyebrow = "Betalning" title = "Prenumeration skapad" viewInvoice = "Visa och betala faktura" +[portal.procurement.review] +acceptCta = "Acceptera offert" +annual = "Årsavgift (år 1)" +downloadCta = "Ladda ner offert" +poNumber = "Inköpsorder: {{po}}" +renewal = "Förnyas till {{amount}}/år efter perioden (+{{pct}} % KPI)" +tcv = "{{years}}-årsperiod, betald i sin helhet i förskott: {{tcv}}" +validUntil = "Giltig till {{date}}" + [portal.procurement.schedule] fallback = "Det gick inte att läsa in schemaläggaren." fallbackLink = "Öppna schemaläggning i en ny flik" +loading = "Läser in schemaläggaren..." subtitle = "Din lösningsingenjör går igenom utrullningen med ditt team. Välj en tid som passar dig." title = "Boka ett samtal" -[portal.procurement.status] -action = "Åtgärd krävs" -available = "Tillgänglig" -complete = "Slutförd" -pending = "Väntar" -request = "På begäran" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Isolerad, ingen utgående trafik" +back = "Tillbaka" +businessName = "Företagsnamn" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Hanterad, snabbast start" +continue = "Fortsätt" +deployment = "Var ska den distribueras?" +eula = "Jag godkänner Stirling EULA och kommersiella villkor." +fullName = "Fullständigt namn" +fullNamePlaceholder = "Ditt namn" +invites = "Bjud in teammedlemmar · valfritt" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Boka ett samtal" +seats = "Personer som testar" +seatsPlaceholder = "t.ex. 15" +selfhost = "Egenhostad" +selfhostSub = "Docker eller Kubernetes" +start = "Starta testperiod" +stepOf = "Steg {{n}} av {{total}}" +subtitle = "Gratis i 14 dagar, inget kort krävs." +subtitleDetails = "Några uppgifter för din offert och ditt avtal." +talkFirst = "Vill du prata först?" +title = "Konfigurera din testperiod" +viewEula = "Läs den" +workEmail = "Arbets-e-post" +workEmailPlaceholder = "du@företag.com" [portal.procurement.trial] body = "Förlängning lägger till 7 dagar och meddelar din lösningsingenjör." @@ -7808,109 +8690,113 @@ maxed = "Maximalt utnyttjad" subtitle = "Din kostnadsfria testperiod pågår till {{date}}. Inget kort krävs." title = "Enterprise-testperiod" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Prissättning för bunden volym, organisationsomfattande SSO + SCIM + RBAC, 90 dagars oföränderlig granskning och en dedikerad SE." -homeCta = "Starta testperiod →" -homeHeadline = "Bearbeta miljontals PDF:er." - -[portal.recentActivity] -title = "Senaste aktivitet" -viewAll = "Visa alla" - -[portal.recentActivity.empty] -description = "Pipeline-körningar, distributioner och agenthändelser visas här." -title = "Inget här än" - -[portal.search] -ariaLabel = "Sök" -placeholder = "Sök i Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Snabbåtgärder visas här när de är tillgängliga." -noActionsTitle = "Inga snabbåtgärder" -noMatches = "Inga träffar för \"{{query}}\"" -noMatchesDescription = "Prova ett annat sökord eller bläddra i katalogen." - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "Kontolänk" -[portal.shell.header] -accountFallback = "Konto" -accountMenu = "Kontomeny" -darkMode = "Mörkt läge" -lightMode = "Ljust läge" -search = "Sök" -searchPlaceholder = "Sök…" -signOut = "Logga ut" -switchToDark = "Byt till mörkt tema" -switchToLight = "Byt till ljust tema" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} dok" linkAccount = "Länka Stirling-konto" -planEditor = "Editor-plan" -planEnterprise = "Enterprise-plan" -planProcessor = "Processor-plan" primaryNav = "Primär navigering" switchApp = "Byt app" +[portal.shell.topbar] +closeNav = "Stäng navigering" +openNav = "Öppna navigering" +search = "Sök" + [portal.sources] subtitle = "Återanvändbara indataanslutningar som matar dokument till Stirling. Konfigurera en anslutning en gång och referera sedan till den från valfritt antal policyer. Klicka på en rad för dess konfiguration och vilka policyer som använder den." title = "Källor" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Anslut källa" +[portal.sources.builder] +back = "Tillbaka till källor" +backToTypes = "Alla källtyper" +cancel = "Avbryt" +chooseHint = "Välj var dokument kommer ifrån. Gråmarkerade kopplingar är på väg." +comingSoon = "Kommer snart" +comingSoonHeading = "Kommer snart" +create = "Skapa källa" +createTitle = "Anslut en källa" +delete = "Ta bort" +editTitle = "Redigera källa" +enabled = "Aktiverad" +save = "Spara ändringar" + +[portal.sources.builder.folderAccess] +description = "Mappautomatiseringar kan bara använda mappar som en administratör har tillåtit. Lägg till den under inställningarna för mappåtkomst och försök sedan igen." +openSettings = "Inställningar för mappåtkomst" +title = "Den här mappen är inte tillåten" + [portal.sources.delete] body = "Ta bort \"{{name}}\"? Detta kan inte ångras. Policyer som refererar till den behöver uppdateras." cancel = "Avbryt" confirm = "Ta bort" title = "Ta bort källa?" -[portal.sources.detail] -closeAriaLabel = "Stäng detaljvy" -delete = "Ta bort källa" -docs24h = "Senaste 24 h" -docs30d = "Senaste 30 dagarna" -docsTotal = "Totalt sedda" -docsTrend = "Dokument under de senaste 30 dagarna" -documents = "Dokument" -edit = "Redigera" -notReferenced = "Refereras inte av någon policy, så den är säker att ta bort." -pause = "Pausa" -resume = "Återuppta" -subtitle = "{{type}} · {{status}}" -usedBy = "Används av" - -[portal.sources.empty] -description = "Anslut en mapp (och snart molnlagring) så att dina policyer har någonstans att hämta dokument från." -title = "Inga källor anslutna än" - [portal.sources.kpi] inUse = "Används" total = "Anslutningar" unused = "Oanvända" +[portal.sources.networkFields.connection] +helperText = "Den lagrade anslutningen med serveradressen och inloggningsuppgifterna. Återanvänds av varje källa som refererar till den." +label = "Anslutning" + +[portal.sources.networkFields.directory] +helperText = "Mapp på servern att kontrollera, relativt till inloggningshemmet eller delningens rot. Lämna tomt för roten." +label = "Mapp" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Förbruka tar bort varje fil från servern när varje policy har bearbetat den." +label = "Läsläge" + +[portal.sources.networkFields.mode.options] +consume = "Förbruka: bearbeta varje fil en gång" +snapshot = "Ögonblicksbild: läs om mappen vid varje körning" + +[portal.sources.networkFields.recursive] +label = "Mappdjup" + +[portal.sources.networkFields.recursive.options] +all = "Inkludera undermappar" +top = "Endast översta nivån" + [portal.sources.status] active = "Aktiv" disabled = "Inaktiverad" unused = "Oanvänd" [portal.sources.table] +documents = "Dokument" source = "Källa" status = "Status" +type = "Type" usedBy = "Policyer" +[portal.sources.types.box] +description = "Hämta dokument från Box-mappar som ditt team redan delar." +label = "Box" + +[portal.sources.types.dropbox] +description = "Hämta dokument från en delad Dropbox-mapp." +label = "Dropbox" + [portal.sources.types.editor] +description = "Dokument som ditt team har bearbetat i editorn, över policy- och AI-körningar." label = "Editor" +noPolicies = "Inga policies har körts från editorn ännu." + +[portal.sources.types.email] +description = "Gör bilagor som skickas till en dedikerad postlåda till dokument." +label = "E-postinkorg" [portal.sources.types.folder] description = "Bevaka en katalog på servern för nya dokument." @@ -7943,25 +8829,80 @@ label = "Mappdjup" all = "Inkludera undermappar" top = "Endast toppnivå" +[portal.sources.types.ftp] +description = "Kontrollera regelbundet en FTP- eller FTPS-servermapp för nya dokument." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Hämta dokument från delade Google Drive-mappar." +label = "Google Drive" + +[portal.sources.types.network] +description = "Bevaka en SMB-nätverksresurs för nya dokument." +label = "Nätverksenhet" + +[portal.sources.types.onedrive] +description = "Bevaka OneDrive-mappar för nya dokument." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Hämta dokument från en Amazon S3- eller S3-kompatibel bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Den lagrade anslutningen som innehåller bucket och inloggningsuppgifter. Återanvänds av varje källa och pipeline-utdata som refererar till den." +label = "Anslutning" + +[portal.sources.types.s3.fields.mode] +helperText = "Förbruka tar bort varje objekt från bucket när varje policy har bearbetat det." +label = "Läsläge" + +[portal.sources.types.s3.fields.mode.options] +consume = "Förbruka: bearbeta varje objekt en gång" +snapshot = "Ögonblicksbild: läs om bucket vid varje körning" + +[portal.sources.types.s3.fields.prefix] +helperText = "Endast objekt vars nycklar börjar med detta prefix bearbetas." +label = "Nyckelprefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Kontrollera regelbundet en SFTP-drop-mapp för nya dokument." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Hämta dokument från SharePoint-dokumentbibliotek." +label = "SharePoint" + [portal.sources.types.unknown] label = "Källa" +[portal.sources.types.webhook] +createNote = "En leverans-URL och signeringshemlighet genereras när du skapar denna webhook. Avsändare POST:ar dokument till den URL:en och de refererande policies körs vid ankomst." +description = "Ta emot dokument via signerad HTTP POST från valfritt externt system." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Leverans-URL" +secretNote = "Signeringshemligheten visas en gång, när webhooken skapas. Skapa om källan för att rulla den." + +[portal.sources.types.webhook.reveal] +copy = "Kopiera" +done = "Klart" +secret = "Signeringshemlighet" +secretHelp = "Signera varje leverans råa body med den här nyckeln (HMAC-SHA256) och skicka den som X-Stirling-Signature-header." +secretWarning = "Kopiera signeringshemligheten nu. För din säkerhet visas den bara en gång och kan inte hämtas senare." +title = "Webhook skapad" +url = "Leverans-URL" +usage = "POST:a varje dokument som rå request body till leverans-URL:en med en binär content type (application/pdf eller application/octet-stream). Refererande policies körs automatiskt vid ankomst." + [portal.sources.wizard] -back = "Tillbaka" -cancel = "Avbryt" -continue = "Fortsätt" -editTitle = "Redigera källa" name = "Namn" namePlaceholder = "t.ex. skadeanmälningar" -save = "Spara ändringar" -subtitle = "Steg {{current}} av {{total}} · {{label}}" -title = "Anslut en källa" type = "Typ" -[portal.sources.wizard.steps] -chooseType = "Välj typ" -configure = "Konfigurera" -review = "Granska och anslut" +[portal.stepModal] +close = "Stäng" [portal.usage] managePayment = "Hantera betalning" @@ -8021,16 +8962,9 @@ summary = "Äger ett team — hanterar dess medlemmars resurser och delade konfi 2 = "Portalåtkomst via standardpolicyn" 3 = "Allt som medlemmen kan göra" -[portal.welcome] -ariaLabel = "Välkommen till Stirling PDF" -badge = "Open-source" -installEditor = "Installera Editor" -inviteTeammates = "Bjud in teammedlemmar" -openInBrowser = "Öppna i webbläsare" -perks = "Gratis för alltid · Kan egenhostas" -subtitle = "Världens säkraste PDF Editor är gratis för team av alla storlekar. Inkluderar 60+ PDF-åtgärder och SSO." -title = "Välkommen till" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Obegränsat" [printFile] title = "Skriv ut fil" @@ -8613,6 +9547,14 @@ text = "Rotera dina PDF‑sidor medurs eller moturs i steg om 90 grader. Alla si [rotate.tooltip.header] title = "Översikt över rotationsinställningar" +[ruler] +clearAll = "Rensa alla" +hideAllLabels = "Dölj alla etiketter" +hideSmallLabels = "Dölj små etiketter" +physicalValues = "Fysisk" +scaled = "Skalad" +showAllLabels = "Visa alla etiketter" + [sanitize] desc = "Ta bort potentiellt skadliga element från PDF-filer." sanitizationResults = "Saneringsresultat" @@ -8654,31 +9596,45 @@ label = "Ta bort XMP-metadata" [sanitize.steps] settings = "Inställningar" -[sanitizePdf] -tags = "rengöra,säkra,säker,ta bort hot" - [sanitizePDF] title = "Sanera PDF" +[sanitizePdf] +tags = "rengöra,säkra,säker,ta bort hot" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Justera sidskala" +[scaleSettings] +activeScale = "Aktiv skala" +apply = "Tillämpa skala" +applyCalibration = "Tillämpa kalibrering" +calculatedScale = "Beräknad skala" +calibrate = "Kalibrera" +calibrating = "Kalibrerar" +calibrationDistanceRequired = "Ange ett verkligt avstånd större än noll" +calibrationInvalid = "Det gick inte att beräkna skalan från denna mätning" +calibrationPaperDistance = "Uppmätt sidavstånd: {{distance}}" +calibrationTitle = "Kalibrera skala" +calibrationTooltip = "Mät ett känt avstånd för att beräkna skalan automatiskt" +customScale = "Anpassad skala" +noneSet = "Ingen anpassad skala angiven" +presets = "Förinställda skalor" +ratio = "Skalförhållande" +ratioHelp = "Förhållande: 1 sidenhet = X verkliga enheter" +ratioPlaceholder = "t.ex. 100" +ratioPositive = "Förhållandet måste vara större än noll" +ratioRequired = "Förhållande krävs" +realDistance = "Verkligt avstånd" +realDistancePlaceholder = "t.ex. 5" +reset = "Återställ till standardvärden" +unit = "Enhet" +unitInvalid = "Ogiltig enhet: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Vinkeltröskel:" -10 = "Ställer in storleken på kanten som läggs till och tas bort för att förhindra vita kanter i utdata (standard: 1)." -2 = "Ställer in den minsta absoluta vinkeln som krävs för att bilden ska roteras (standard: 10)." -3 = "Tolerans:" -4 = "Bestämmer intervallet för färgvariation runt den uppskattade bakgrundsfärgen (standard: 30)." -5 = "Minsta area:" -6 = "Ställer in minsta areatröskel för ett foto (standard: 10000)." -7 = "Minsta konturarea:" -8 = "Ställer in minsta tröskelvärde för konturarea för ett foto" -9 = "Kantstorlek:" - [scannerImageSplit] submit = "Extrahera bildskanningar" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Vad detta gör" whatThisDoesDesc = "Hittar och extraherar automatiskt varje foto från en skannad sida eller sammansatt bild—ingen manuell beskärning." whenToUse = "När den ska användas" +[ScannerImageSplit.selectText] +1 = "Vinkeltröskel:" +10 = "Ställer in storleken på kanten som läggs till och tas bort för att förhindra vita kanter i utdata (standard: 1)." +2 = "Ställer in den minsta absoluta vinkeln som krävs för att bilden ska roteras (standard: 10)." +3 = "Tolerans:" +4 = "Bestämmer intervallet för färgvariation runt den uppskattade bakgrundsfärgen (standard: 30)." +5 = "Minsta area:" +6 = "Ställer in minsta areatröskel för ett foto (standard: 10000)." +7 = "Minsta konturarea:" +8 = "Ställer in minsta tröskelvärde för konturarea för ett foto" +9 = "Kantstorlek:" + [search] noResults = "Inga resultat hittades" placeholder = "Ange sökterm..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Din Stirling-PDF-server är offline och \"{{endpoint} [session] expired = "Din session har löpt ut. Uppdatera sidan och försök igen." -[sessionManagement.tooltip] -header = "Hantera signeringssessioner" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Nya deltagare läggs till sist i signeringsordningen" -bullet2 = "Deltagare kan inte läggas till efter att sessionen har slutförts" -bullet3 = "Varje deltagare får en avisering när det är deras tur" -description = "Du kan lägga till fler deltagare i en aktiv session när som helst före slutförandet." -title = "Lägga till deltagare" - -[sessionManagement.tooltip.finalization] -bullet1 = "Fullständig slutföring: Alla deltagare har signerat" -bullet2 = "Delvis slutföring: Vissa deltagare har inte signerat ännu" -bullet3 = "Osignerade deltagare utesluts från det slutliga dokumentet" -bullet4 = "När den är slutförd kan du ladda in den signerade PDF:en i aktiva filer" -description = "Slutförande kombinerar alla signaturer till en enda signerad PDF. Denna åtgärd kan inte ångras." -title = "Slutförande av session" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Deltagare som redan har signerat kan inte tas bort" -bullet2 = "Borttagna deltagare får inte längre aviseringar" -bullet3 = "Signeringsordningen justeras automatiskt" -description = "Deltagare kan tas bort från sessioner innan de signerar." -title = "Ta bort deltagare" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Varje signatur tillämpas sekventiellt på PDF:en" -bullet2 = "Senare undertecknare kan se tidigare signaturer" -bullet3 = "Kritiskt för godkännandeprocesser och juridisk spårbarhet" -description = "Den ordning du anger när du skapar sessionen avgör vem som signerar först." -title = "Signeringsordning" - [settings] close = "Stäng" +[settings.ai] +documents = "Dokument och RAG" +general = "Allmänt" +limits = "Gränser och prestanda" +models = "Modeller och leverantörer" +title = "AI" + [settings.configuration] advanced = "Avancerat" database = "Databas" endpoints = "Slutpunkter" features = "Funktioner" +folderAccess = "Mappåtkomst" mcp = "MCP Server" storageSharing = "Fillagring och delning" systemSettings = "Systeminställningar" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Standard-PDF-redigerare" defaultPdfEditorActive = "Stirling PDF är din standard-PDF-redigerare" defaultPdfEditorChecking = "Kontrollerar..." defaultPdfEditorInactive = "En annan app är standard" +defaultPdfEditorRemind = "Påminn mig om att ange som standard" +defaultPdfEditorRemindDescription = "Visa en banner när Stirling PDF inte är ditt standardprogram för PDF." defaultPdfEditorSet = "Redan standard" defaultStartupView = "Standardvy vid start" defaultStartupViewDescription = "Välj vilken flik som är aktiv i vänstra kolumnen när appen startar" @@ -8817,9 +9763,18 @@ languageDescription = "Välj visningsspråk" logout = "Logga ut" setAsDefault = "Ange som standard" theme = "Tema" +themeAccent = "Accentfärg" +themeAccentCustom = "Anpassad färg" +themeAccentCustomHint = "Anpassad (standard rekommenderas)" +themeAccentDark = "Accentfärg för mörkt läge" +themeAccentDefault = "Standard" +themeAccentDefaultHint = "Standardtema (rekommenderas)" +themeAccentDescription = "Knappar, länkar och markeringar följer den, och den tonar appen subtilt. Ljust och mörkt läge har varsin. System använder den som är aktiv." +themeAccentLight = "Accentfärg för ljust läge" themeDark = "Mörkt" themeDescription = "Växla mellan ljust och mörkt läge" themeLight = "Ljust" +themeReset = "Återställ temat till standard" themeSystem = "System" title = "Allmänt" user = "Användare" @@ -8833,6 +9788,12 @@ intro = "Aktivera användarautentisering, teamhantering och arbetsytefunktioner learnMore = "Läs mer i dokumentationen" title = "För systemadministratörer" +[settings.general.loginLanding] +description = "Välj vilken app som öppnas när du loggar in på Stirling Cloud." +editor = "Redigerare" +processor = "Processor" +title = "Efter inloggning" + [settings.general.mode] fullscreen = "Helskärm" sidebar = "Sidopanel" @@ -9013,9 +9974,6 @@ title = "Policyer och integritet" [settings.preferences] title = "Preferenser" -[settings.search] -placeholder = "Sök i inställningar..." - [settings.security] description = "Uppdatera ditt lösenord för att hålla ditt konto säkert." title = "Säkerhet" @@ -9041,6 +9999,11 @@ security = "Säkerhet" telegram = "Telegram" title = "Säkerhet och autentisering" +[settings.signOut] +confirm = "Är du säker på att du vill logga ut?" +submit = "Logga ut" +title = "Logga ut" + [settings.team] title = "Team" @@ -9156,6 +10119,7 @@ last = "Sista sidan" maintainRatio = "Växla bibehållet bildförhållande" next = "Nästa sida" noSavedSigs = "Inga sparade signaturer hittades" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Personliga signaturer" previous = "Föregående sida" redo = "Gör om" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Borttagning av bakgrund misslyckades" hint = "Ladda upp en PNG- eller JPG-bild av din signatur" label = "Ladda upp signaturbild" placeholder = "Välj bildfil" +previewAlt = "Current image signature" processing = "Bearbetar bild..." removeBackground = "Ta bort vit bakgrund (gör transparent)" @@ -9207,6 +10172,17 @@ saved = "Välj en sparad signatur ovan, klicka sedan var som helst i PDF:en för text = "När du har angett ditt namn ovan, klicka var som helst i PDF:en för att placera din signatur." title = "Så här lägger du till signatur" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Flytta signatur" pause = "Pausa placering" @@ -9653,10 +10629,30 @@ title = "Ladda upp till server" updateButton = "Uppdatera på servern" uploadButton = "Ladda upp till server" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "klicka här" +fallback = "Om det inte öppnades automatiskt," +opening = "Öppnar Swagger UI i en ny flik..." tags = "api,dokumentation,swagger,ändpunkter,utveckling" title = "API-dokumentation" @@ -9764,12 +10760,39 @@ selectFilesHint = "Välj filer i Aktiva filer för att köra detta verktyg" singleFileScope = "Gäller endast: {{fileName}}" viewerMode = "Byt till filredigeraren för att välja flera filer." +[toolFormat] +ANY = "valfri fil" +CBR = "ett serietidningsarkiv (CBR)" +CBZ = "ett serietidningsarkiv (CBZ)" +CSV = "en CSV fil" +EBOOK = "en e-bok" +EMAIL = "ett e-postmeddelande" +EXCEL = "ett kalkylblad" +HTML = "en webbsida" +IMAGE = "bilder" +JAVASCRIPT = "en JavaScript fil" +JSON = "en JSON fil" +MARKDOWN = "en Markdown fil" +NONE = "ingen fil" +PCL = "en PCL utskriftsfil" +PDF = "en PDF" +PDF_ENCRYPTED = "en lösenordsskyddad PDF" +POSTSCRIPT = "en PostScript fil" +PPT = "en presentation" +TEXT = "en textfil" +VIDEO = "en video" +WORD = "ett Word-dokument" +XML = "en XML fil" +XPS = "ett XPS-dokument" +ZIP = "ett ZIP arkiv" + [toolPanel] alpha = "Alfa" backToAllTools = "Tillbaka till alla verktyg" collapse = "Fäll ihop panel" expand = "Expandera panel" goBack = "Gå tillbaka" +pdfTools = "PDF-verktyg" placeholder = "Välj ett verktyg för att komma igång" premiumFeature = "Premiumfunktion:" toolsHeader = "Verktyg" @@ -9806,11 +10829,6 @@ noToolsFound = "Inga verktyg hittades" quickAccess = "SNABBÅTKOMST" searchPlaceholder = "Sök verktyg..." -[toolPicker.categories] -advancedTools = "Avancerade verktyg" -recommendedTools = "Rekommenderade verktyg" -standardTools = "Standardverktyg" - [toolPicker.subcategories] advancedFormatting = "Avancerad formatering" automation = "Automatisering" @@ -9824,6 +10842,9 @@ removal = "Borttagning" signing = "Signering" verification = "Verifiering" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Inga verktyg hittades" noTools = "Inga verktyg tillgängliga" @@ -9950,28 +10971,53 @@ title = "Användare" you = "(du)" [users.action] +cancelInvite = "Avbryt inbjudan" deleteTeam = "Ta bort team" disableMfa = "Återställ MFA" move = "Flytta till team" reinstate = "Återinsätt" remove = "Ta bort från organisationen" +removeTeam = "Ta bort från team" rename = "Byt namn på team" resetPw = "Återställ lösenord" suspend = "Stäng av" unlock = "Lås upp konto" +[users.activity] +daysAgo = "för {{count}} d sedan" +hoursAgo = "för {{count}} h sedan" +justNow = "Nyss" +minutesAgo = "för {{count}} min sedan" +monthsAgo_one = "för {{count}} månad sedan" +monthsAgo_other = "för {{count}} månader sedan" +never = "Aldrig" +weeksAgo_one = "för {{count}} vecka sedan" +weeksAgo_other = "för {{count}} veckor sedan" +yearsAgo_one = "för {{count}} år sedan" +yearsAgo_other = "för {{count}} år sedan" + [users.cap] addProcessor = "+ Processor" approver = "Godkänner policy" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Avbryt inbjudan till {{email}}? De kommer inte att kunna gå med via den aktuella länken." +cancelInviteTitle = "Avbryt inbjudan" deleteTeamBody = "Ta bort teamet {{name}}? Teamet måste vara tomt först - flytta dess medlemmar till ett annat team, och det får inte fortfarande äga några integrationskonfigurationer." deleteTeamTitle = "Ta bort team" disableMfaBody = "Ta bort MFA-registreringen för {{name}}? De konfigurerar den igen vid nästa inloggning om det krävs." disableMfaTitle = "Återställ MFA" removeBody = "Ta bort {{name}} permanent från organisationen? Detta kan inte ångras." +removeTeamBody = "Ta bort {{name}} från teamet? De behåller sitt konto men förlorar åtkomst till teamets resurser." removeTitle = "Ta bort medlem" [users.empty] @@ -9982,10 +11028,8 @@ title = "Inga medlemmar än" addToTeam = "Lägg till i team" guestCount = "{{count}} gäst" guests = "Gäster" -guestsDesc = "Externa samarbetspartner, begränsade till det du har delat. Endast Editor." ledBy = "ledd av {{owner}}" org = "Organisation" -orgDesc = "Ägare med organisationsövergripande behörighet och policygodkännande" owners = "{{count}} ägare" team = "{{name}}-team" teamMeta = "{{count}} personer" @@ -10022,6 +11066,18 @@ username = "Användarnamn" usernameError = "Användarnamnet måste vara minst 3 tecken" usernamePlaceholder = "jsmith" +[users.invites] +by = "Inbjuden av {{who}}" +cancel = "Avbryt" +expiresInDays_one = "Upphör om {{count}} dag" +expiresInDays_other = "Upphör om {{count}} dagar" +expiresToday = "Upphör idag" +title = "Väntande inbjudningar" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Något gick fel vid kontakt med backend, eller så har du inte åtkomst. Försök igen." title = "Kunde inte läsa in medlemmar" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Sök bilagor" title = "Bilagor" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Bokmärkestitel" +bookmarkTitleRequired = "Bokmärkestitel krävs" closeSidebar = "Stäng sidopanel för bokmärken" collapseAll = "Fäll ihop alla bokmärken" +empty = "No bookmarks in this document" expandAll = "Expandera alla bokmärken" +searchPlaceholder = "Sök bokmärken" +title = "Bookmarks" [viewer.comments] addComment = "Lägg till kommentar" @@ -10236,10 +11297,12 @@ locateAnnotation = "Hitta i dokumentet" moreActions = "Fler åtgärder" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Sida {{page}}" placingHint = "Klicka på en sida för att placera… (avbryt)" removeCommentOnly = "Ta bort endast kommentaren" saveReply = "Spara svar" +searchPlaceholder = "Search comments" title = "Kommentarer" typeComment = "Kommentar" typeInsertText = "Infoga text" @@ -10262,6 +11325,7 @@ unsavedDesc = "Du har osparade ändringar" closeSidebar = "Stäng sidopanel för lager" hideAll = "Dölj alla lager" showAll = "Visa alla lager" +title = "Layers" [viewer.link] delete = "Ta bort länk" @@ -10605,30 +11669,6 @@ title = "Vattenstämpeltext" image = "Bild" text = "Text" -[wetSignature.tooltip] -header = "Metoder för signaturskapande" - -[wetSignature.tooltip.draw] -bullet1 = "Anpassa pennfärg och tjocklek" -bullet2 = "Rensa och rita om tills du är nöjd" -bullet3 = "Fungerar på pekskärmsenheter (surfplattor, telefoner)" -description = "Skapa en handskriven signatur med musen eller pekskärmen. Bäst för personliga, autentiska signaturer." -title = "Rita signatur" - -[wetSignature.tooltip.type] -bullet1 = "Välj mellan flera typsnitt" -bullet2 = "Anpassa textstorlek och färg" -bullet3 = "Perfekt för standardiserade signaturer" -description = "Skapa en signatur från skriven text. Snabb och konsekvent, lämplig för affärsdokument." -title = "Skriv signatur" - -[wetSignature.tooltip.upload] -bullet1 = "Stöder PNG, JPG och andra bildformat" -bullet2 = "Genomskinliga bakgrunder rekommenderas för bästa resultat" -bullet3 = "Bilden kommer att skalas för att passa signaturområdet" -description = "Ladda upp en i förväg skapad signaturbild. Idealiskt om du har en skannad signatur eller företagslogotyp." -title = "Ladda upp signaturbild" - [workbenchBar] activeFiles = "Aktiva filer" annotations = "Anteckningar" @@ -10642,6 +11682,7 @@ exitRedaction = "Avsluta maskeringsläge" exportAll = "Exportera PDF" exportSelected = "Exportera valda sidor" formFill = "Fyll i formulär" +hideToolbar = "Hide toolbar" multiTool = "Multiverktyg" panMode = "Panoreringsläge" print = "Skriv ut PDF" @@ -10652,6 +11693,7 @@ redact = "Maskera" rotateLeft = "Rotera åt vänster" rotateRight = "Rotera åt höger" ruler = "Linjal / Mät" +rulerSettings = "Skalinställningar" save = "Spara" saveAll = "Spara alla" saveAs = "Spara som" @@ -10661,6 +11703,7 @@ selectAll = "Markera alla" selectByNumber = "Markera efter sidnummer" selectLanguage = "Välj språk" share = "Dela" +showToolbar = "Show toolbar" toggleAnnotations = "Visa/dölj anteckningar" toggleAttachments = "Visa/dölj bilagor" toggleBookmarks = "Visa/dölj bokmärken" diff --git a/frontend/editor/public/locales/th-TH/translation.toml b/frontend/editor/public/locales/th-TH/translation.toml index 936411b021..4d194d6e06 100644 --- a/frontend/editor/public/locales/th-TH/translation.toml +++ b/frontend/editor/public/locales/th-TH/translation.toml @@ -1,7 +1,6 @@ alphabet = "ตัวอักษร" apply = "ใช้" applyAndContinue = "บันทึกและออก" -areYouSure = "แน่ใจหรือไม่ว่าต้องการออก?" back = "ย้อนกลับ" black = "ดำ" blue = "น้ำเงิน" @@ -46,10 +45,8 @@ filesSelected = "ไฟล์ที่เลือก" font = "ฟอนต์" fontSizeTooltip = "ขนาดข้อความหมายเลขหน้าเป็นหน่วยพอยต์ ตัวเลขใหญ่ทำให้ข้อความใหญ่ขึ้น" fontTypeTooltip = "ตระกูลฟอนต์สำหรับหมายเลขหน้า เลือกให้เหมาะกับสไตล์เอกสารของคุณ" -genericSubmit = "ส่ง" green = "เขียว" help = "ช่วยเหลือ" -imgPrompt = "เลือกภาพ" incorrectPasswordMessage = "รหัสผ่านปัจจุบันไม่ถูกต้อง" info = "ข้อมูล" invalidUndoData = "ไม่สามารถยกเลิก: ข้อมูลการดำเนินการไม่ถูกต้อง" @@ -62,7 +59,6 @@ loginAgreementTitle = "ข้อตกลงการเข้าสู่ระ logOut = "ออกจากระบบ" marginTooltip = "ระยะห่างระหว่างหมายเลขหน้ากับขอบกระดาษ" moreOptions = "ตัวเลือกเพิ่มเติม" -multiPdfPrompt = "เลือก PDF หลายไฟล์ (2 ขึ้นไป)" never = "ไม่เคย" no = "ไม่" noFileSelected = "ยังไม่ได้เลือกไฟล์ โปรดอัปโหลดไฟล์หนึ่งไฟล์" @@ -80,7 +76,6 @@ pageOrderPrompt = "เรียงลำดับหน้าตามควา pages = "หน้า" pageSelectionPrompt = "เลือกหน้าตามความต้องการ (ป้อนหมายเลขหน้าแยกด้วยเครื่องหมายจุลภาค เช่น 1,5,6 หรือฟังก์ชัน เช่น 2n+1) :" password = "รหัสผ่าน" -pdfPrompt = "เลือก PDF" pin = "ปักหมุดไฟล์ (คงไว้หลังรันเครื่องมือ)" pro = "โปร" processingComplete = "ไฟล์ของคุณพร้อมแล้ว" @@ -110,6 +105,7 @@ undoSuccess = "ยกเลิกการดำเนินการเรี unknown = "ไม่ทราบ" unpin = "เอาปักหมุดไฟล์ออก (แทนที่หลังรันเครื่องมือ)" unsavedChanges = "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกใน PDF" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "การเปลี่ยนแปลงที่ยังไม่ได้บันทึก" unsupported = "ไม่รองรับ" username = "ชื่อผู้ใช้" @@ -356,6 +352,7 @@ text = "สิทธิ์เหล่านี้ควบคุมสิ่ง title = "เปลี่ยนสิทธิ์" [addStamp] +preview = "ดูตัวอย่างตราประทับ" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "ดาวน์โหลดด้วยตนเอง: คลิ noLanguages = "ไม่พบภาษา tessdata ในไดเรกทอรีที่กำหนดค่าไว้" permissionNotice = "เส้นทาง tessdata ไม่สามารถเขียนได้ การดาวน์โหลดจะถูกเปิดในเบราว์เซอร์ โปรดบันทึกไฟล์ .traineddata ลงในโฟลเดอร์ tessdata ด้วยตนเอง" +[admin.settings.ai.documents] +description = "กำหนดค่าโมเดล embedding และการตั้งค่าการดึงข้อมูลที่ใช้ตอบคำถามเกี่ยวกับเอกสาร จะนำไปใช้กับ AI engine เมื่อบันทึก" +title = "เอกสารและ RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "เว้นว่างเพื่อใช้ข้อมูลรับรองสภาพแวดล้อมของ engine เอง ใช้กับการติดตั้งแบบ self-hosted single-engine" +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "บันทึกแล้ว - เว้นว่างเพื่อคง key ปัจจุบันไว้" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL ของ endpoint embeddings ที่เข้ากันได้กับ OpenAI / Ollama เช่น http://ollama:11434/v1 ต้องชี้ไปยัง endpoint ภายในที่เชื่อถือได้ (ไวต่อ SSRF)" +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "ชื่อโมเดล embedding ข้อความอิสระ; คำแนะนำเป็นเพียงแนวทางเท่านั้น" +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "ผู้ให้บริการที่ใช้แปลงข้อความเอกสารเป็น vector embeddings" +label = "Embedding provider" + +[admin.settings.ai.documents.maxSearches] +description = "จำนวนสูงสุดของการค้นหาแบบดึงข้อมูลที่ agent สามารถเรียกใช้ได้ต่อคำขอ" +label = "การค้นหาสูงสุด" + +[admin.settings.ai.documents.reindexNote] +body = "การเปลี่ยนโมเดล embedding จะมีผลทันที แต่เอกสารที่จัดทำดัชนีด้วยโมเดลก่อนหน้าต้องจัดทำดัชนีใหม่เพื่อให้การค้นหาส่งคืนผลลัพธ์ที่ถูกต้อง" +title = "ต้องจัดทำดัชนีใหม่" + +[admin.settings.ai.documents.saved] +reindexNote = "หากคุณเปลี่ยนโมเดล embedding ให้จัดทำดัชนีเอกสารที่มีอยู่ใหม่เพื่อให้การค้นหาใช้โมเดลใหม่" + +[admin.settings.ai.documents.topK] +description = "จำนวนชิ้นส่วนที่เกี่ยวข้องที่สุดที่ดึงมาต่อการค้นหา" +label = "Top K" + +[admin.settings.ai.general] +description = "เชื่อมต่อ Stirling กับ Python AI engine และเลือกความสามารถ AI ที่จะแสดง การเปลี่ยนแปลงจะมีผลเมื่อรีสตาร์ท" +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "เปิดหรือปิดฟีเจอร์ AI แต่ละรายการ ฟีเจอร์ที่ปิดจะถูกซ่อนในแอป" +title = "ความสามารถ" + +[admin.settings.ai.general.enabled] +description = "สวิตช์หลัก เมื่อปิด จะไม่มีเครื่องมือ AI, agents หรือการเรียก engine ให้ใช้งาน" +label = "เปิดใช้ AI" + +[admin.settings.ai.general.features.chat] +description = "ผู้ช่วยสนทนาสำหรับทำงานกับ PDFs" +label = "ผู้ช่วยแชท" + +[admin.settings.ai.general.features.classify] +description = "จัดหมวดหมู่เอกสารโดยอัตโนมัติตามประเภทหรือเนื้อหา" +label = "การจัดหมวดหมู่เอกสาร" + +[admin.settings.ai.general.features.createPdf] +description = "สร้าง PDF ใหม่ (เช่น จาก HTML) ผ่าน AI agent" +label = "สร้าง PDF จาก prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "ถามคำถามและรับคำตอบที่อ้างอิงจากเอกสารที่อัปโหลด" +label = "คำถามเกี่ยวกับเอกสาร" + +[admin.settings.ai.general.features.mathAuditor] +description = "ตรวจสอบเอกสารเพื่อหาข้อผิดพลาดทางคณิตศาสตร์และตัวเลข" +label = "ผู้ตรวจสอบคณิตศาสตร์" + +[admin.settings.ai.general.features.pdfComment] +description = "เพิ่มความคิดเห็นตรวจทานและคำอธิบายประกอบที่เขียนโดย AI ลงใน PDF" +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "หมดเวลาสำหรับการทำงานของ agent ที่หนักขึ้น เช่น การสร้างเอกสาร" +label = "หมดเวลาการทำงานนาน (วินาที)" + +[admin.settings.ai.general.note] +body = "AI engine ทำงานเป็นบริการแยกต่างหาก shared secret ของมัน" +body2 = "ถูกตั้งค่าผ่านตัวแปรสภาพแวดล้อมของ container Provider API keys สามารถป้อนในหน้าเหล่านี้หรือจัดเตรียมเป็นตัวแปรสภาพแวดล้อมของ engine; keys ที่ป้อนที่นี่จะถูกส่งไปยัง engine เมื่อบันทึก" +title = "เกี่ยวกับ AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "หมดเวลาสำหรับคำตอบแชทแบบสตรีม (ทีละ token)" +label = "หมดเวลา stream (วินาที)" + +[admin.settings.ai.general.test] +button = "ทดสอบการเชื่อมต่อ" +failBody = "AI engine ไม่ตอบสนอง ตรวจสอบ URL ว่า engine container กำลังทำงานอยู่ และเปิดใช้ AI แล้ว (ต้องรีสตาร์ทหลังจากเปิดใช้)" +failTitle = "เข้าถึง AI engine ไม่ได้" +okBody = "AI engine ตอบสนองต่อการตรวจสอบสถานะ" +okTitle = "เข้าถึง AI engine ได้" + +[admin.settings.ai.general.timeoutSeconds] +description = "หมดเวลาสำหรับคำขอ AI มาตรฐานไปยัง engine" +label = "หมดเวลาคำขอ (วินาที)" + +[admin.settings.ai.general.url] +description = "URL ภายในของ Python AI engine เช่น http://stirling-pdf-engine:5001" +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "กรอบจำกัดปริมาณงานที่คำขอ AI สามารถทำได้และจำนวนที่ทำงานพร้อมกัน นำไปใช้กับ AI engine เมื่อบันทึก" +title = "ขีดจำกัดและประสิทธิภาพ" + +[admin.settings.ai.limits.maxCharacters] +description = "กรอบจำกัด: ปฏิเสธคำขอ AI ที่ข้อความที่ดึงออกมายาวเกินค่านี้" +label = "จำนวนอักขระสูงสุดต่อคำขอ" + +[admin.settings.ai.limits.maxPages] +description = "กรอบจำกัด: ปฏิเสธคำขอ AI ที่เกินจำนวนหน้า PDF นี้" +label = "จำนวนหน้าสูงสุดต่อคำขอ" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "จำนวนสูงสุดของการเรียกโมเดลที่กำลังดำเนินการพร้อมกันทั่วทั้ง engine" +label = "การทำงานพร้อมกันสูงสุดของโมเดล" + +[admin.settings.ai.models] +description = "เลือกผู้ให้บริการ LLM และโมเดล smart/fast ที่ AI engine ใช้ นำไปใช้กับ AI engine เมื่อบันทึก" +title = "โมเดลและผู้ให้บริการ" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "เว้นว่างเพื่อใช้ข้อมูลรับรองสภาพแวดล้อมของ engine เอง ใช้กับการติดตั้งแบบ self-hosted single-engine" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "บันทึกแล้ว - เว้นว่างเพื่อคง key ปัจจุบันไว้" + +[admin.settings.ai.models.baseUrl] +description = "Base URL ของ endpoint ที่เข้ากันได้กับ OpenAI / Ollama เช่น http://ollama:11434/v1" +label = "Provider base URL" +warning = "Base URL ต้องชี้ไปยัง endpoint ภายในที่เชื่อถือได้ engine จะส่งคำขอฝั่งเซิร์ฟเวอร์ไปยังค่านั้น ดังนั้นค่าที่ไม่น่าเชื่อถือจึงไวต่อ SSRF" + +[admin.settings.ai.models.fastMaxTokens] +description = "จำนวน output tokens สูงสุดสำหรับโมเดล fast" +label = "Fast model max tokens" + +[admin.settings.ai.models.fastModel] +description = "โมเดลที่ถูกกว่าและเร็วกว่า สำหรับงานเบา ข้อความอิสระ; คำแนะนำเป็นเพียงแนวทางเท่านั้น" +label = "Fast model" + +[admin.settings.ai.models.provider] +description = "ผู้ให้บริการ LLM ที่ engine สื่อสารด้วย" +label = "ผู้ให้บริการ" + +[admin.settings.ai.models.smartMaxTokens] +description = "จำนวน output tokens สูงสุดสำหรับโมเดล smart" +label = "Smart model max tokens" + +[admin.settings.ai.models.smartModel] +description = "โมเดลความสามารถสูงสำหรับการให้เหตุผลที่ซับซ้อน ข้อความอิสระ; คำแนะนำเป็นเพียงแนวทางเท่านั้น" +label = "Smart model" + +[admin.settings.ai.saved] +body = "การเปลี่ยนแปลงจะถูกส่งไปยัง AI engine โดยอัตโนมัติ" +bodyNoPush = "บันทึกการตั้งค่าแล้ว จะมีผลในครั้งถัดไปที่ AI engine รับการกำหนดค่าของมัน" +title = "บันทึกการตั้งค่า AI แล้ว" + [admin.settings.badge] clickToUpgrade = "คลิกเพื่อดูรายละเอียดแผน" @@ -938,6 +1094,25 @@ label = "สร้างใหม่เมื่อเริ่มต้น" description = "จำนวนวันที่ใบรับรองจะยังคงใช้ได้" label = "อายุใบรับรอง (วัน)" +[admin.settings.folderAccess] +description = "ไดเรกทอรีที่แหล่งที่มาของโฟลเดอร์และเอาต์พุตโฟลเดอร์ได้รับอนุญาตให้อ่านและเขียน นี่คือขอบเขตความปลอดภัย: automations ไม่สามารถชี้ไปยังเส้นทางเซิร์ฟเวอร์นอกเหนือจากรายการนี้ได้" +securityNote = "เว้นว่างไว้เพื่อปิดใช้งานแหล่งที่มาของโฟลเดอร์และเอาต์พุตทั้งหมด ไดเรกทอรีการกำหนดค่าของ Stirling เองจะถูกห้ามใช้เสมอ" +title = "การเข้าถึงโฟลเดอร์" + +[admin.settings.folderAccess.implied] +description = "ไดเรกทอรีที่ Stirling จัดการเหล่านี้ได้รับอนุญาตเสมอและไม่สามารถเปลี่ยนได้ที่นี่" +serverStorage = "ที่เก็บไฟล์บนเซิร์ฟเวอร์" +title = "อนุญาตเสมอ" +watchedFolder = "โฟลเดอร์ที่ pipeline เฝ้าดู" + +[admin.settings.folderAccess.roots] +add = "เพิ่ม" +empty = "ไม่มีโฟลเดอร์ที่อนุญาต แหล่งที่มาของโฟลเดอร์และเอาต์พุตถูกปิดใช้งานอยู่ในขณะนี้" +hint = "ป้อนเส้นทางแบบ absolute เช่น /data/inbox" +label = "รากโฟลเดอร์ที่อนุญาต" +placeholder = "/data/inbox" +remove = "ลบรากโฟลเดอร์" + [admin.settings.general] description = "กำหนดค่าการตั้งค่าแอปพลิเคชันทั่วทั้งระบบ รวมถึงการสร้างแบรนด์และพฤติกรรมเริ่มต้น" system = "ระบบ" @@ -1704,6 +1879,9 @@ width = "ความกว้าง" [app] description = "ทางเลือกแทน Adobe Acrobat ฟรี (ดาวน์โหลด 10M+)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "แปลงเป็น PDF/A-3b" convertToPdfA3bDescription = "สร้าง PDF สำหรับเก็บถาวรพร้อมฝังไฟล์แนบ" @@ -1842,6 +2020,15 @@ export = "ส่งออก" accessDenied = "ปฏิเสธการเข้าถึง" insufficientPermissions = "คุณไม่มีสิทธิ์ในการดำเนินการนี้" +[auth.callback] +completing = "กำลังดำเนินการยืนยันตัวตนให้เสร็จสิ้น" +invalidToken = "เข้าสู่ระบบ OAuth ล้มเหลว - token ไม่ถูกต้อง" +missingToken = "เข้าสู่ระบบ OAuth ล้มเหลว - ไม่ได้รับ token" +oauthFailed = "เข้าสู่ระบบ OAuth ล้มเหลว โปรดลองอีกครั้ง" +pleaseWait = "โปรดรอขณะที่เราดำเนินการเข้าสู่ระบบให้เสร็จสิ้น" +signedOut = "คุณออกจากระบบแล้ว โปรดเข้าสู่ระบบอีกครั้ง" +windowMayClose = "คุณสามารถปิดหน้าต่างนี้ได้เมื่อดำเนินการเสร็จสิ้น" + [auth.displayName] guest = "ผู้เยี่ยมชม" user = "ผู้ใช้" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "ทำให้เอกสาร PDF ปลอดภั description = "เครื่องมือนี้จะเปลี่ยนชื่อไฟล์ PDF โดยอัตโนมัติตามเนื้อหา มันวิเคราะห์เอกสารเพื่อค้นหาชื่อเรื่องที่เหมาะสมที่สุดจากข้อความ" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "หมุนอัตโนมัติ" +tags = "หมุนอัตโนมัติ,แก้ไขการวางแนว,กลับหัว,ตะแคง,ปรับให้ตรง,จัดแนว,ตรวจจับการวางแนว,แก้ไขการหมุน,จัดแนวอัตโนมัติ,สแกน,OSD" + +[autoRotate.confidenceThreshold] +title = "เกณฑ์ความมั่นใจ" + +[autoRotate.detectionMode] +auto = "อัตโนมัติ" +osd = "OCR เท่านั้น" +text = "ข้อความเท่านั้น" +title = "วิธีตรวจจับ" + +[autoRotate.error] +failed = "เกิดข้อผิดพลาดขณะหมุน PDF อัตโนมัติ" + +[autoRotate.inferUndetected] +title = "หมุนหน้าที่มีความมั่นใจต่ำให้ตรงกัน" + +[autoRotate.report] +applied = "{{degrees}}° ตามเข็มนาฬิกา" +confidenceValue = "ความมั่นใจ {{value}}" +noChange = "ไม่มีการเปลี่ยนแปลง" +pageLabel = "หน้า {{number}}" +summary = "หมุนแล้ว {{rotated}} จาก {{total}} หน้า, คงไว้เหมือนเดิม {{unchanged}} หน้า" +title = "รายงานการตรวจจับ" + +[autoRotate.report.method] +inferred = "จับคู่แล้ว" +none = "ข้ามแล้ว" +osd = "OCR" +text = "ข้อความ" + +[autoRotate.report.note] +belowThreshold = "ต่ำกว่าเกณฑ์" +blankPage = "หน้านี้ว่าง" +inferredFromDocument = "เหมือนกับหน้าอื่น ๆ" +noDominantDirection = "ทิศทางข้อความปะปนกัน" +osdFailed = "ไม่พบข้อความที่อ่านได้" +osdNoVerdict = "ไม่สามารถบอกได้ว่าด้านใดคือด้านบน" +tesseractUnavailable = "ไม่ได้ติดตั้ง OCR" +tooFewGlyphs = "ข้อความน้อยเกินไป" + +[autoRotate.results] +title = "ผลลัพธ์การหมุนอัตโนมัติ" + +[autoRotate.settings] +title = "การตั้งค่า" + +[autoRotate.tooltip.confidenceThreshold] +text = "กำหนดว่าการตรวจสอบต้องมั่นใจเพียงใดก่อนที่จะหมุนหน้า ลดค่าเพื่อหมุนหน้ามากขึ้น เพิ่มค่าเพื่อปล่อยหน้าไว้โดยไม่แตะต้องมากขึ้น" +title = "เกณฑ์ความมั่นใจ" + +[autoRotate.tooltip.description] +text = "ตรวจหาว่าแต่ละหน้าควรหันด้านใดขึ้น และหมุนให้ถูกทาง หน้าที่ตั้งตรงอยู่แล้วจะถูกปล่อยไว้เหมือนเดิม" + +[autoRotate.tooltip.detectionMode] +text = "อัตโนมัติจะอ่านข้อความบนหน้าก่อน แล้วจึงดูหน้าเป็นรูปภาพหากยังไม่เพียงพอ ข้อความเท่านั้นจะข้ามการตรวจสอบรูปภาพ ซึ่งเร็วที่สุดสำหรับเอกสารที่ไม่เคยถูกสแกน OCR เท่านั้นจะถือว่าทุกหน้าเป็นรูปภาพ" +title = "วิธีตรวจจับ" + +[autoRotate.tooltip.header] +title = "หมุนอัตโนมัติ" + +[autoRotate.tooltip.inferUndetected] +text = "หน้าว่างหรือเกือบว่างไม่สามารถตรวจสอบได้ด้วยตัวเอง เมื่อทุกหน้าที่อ่านได้ต้องหมุนในทิศทางเดียวกัน หน้าเหล่านี้จะถูกหมุนให้ตรงกัน" +title = "หมุนหน้าที่มีความมั่นใจต่ำให้ตรงกัน" + [autoSizeSplitPDF] tags = "pdf, แยก, เอกสาร, การจัดระเบียบ" @@ -2189,33 +2443,6 @@ keywords = "คีย์เวิร์ด: odd, even" numbers = "ตัวเลข/ช่วง: 5, 10-20" progressions = "ลำดับขั้น: 3n, 4n+1" -[certificateChoice.tooltip] -header = "ประเภทของใบรับรอง" - -[certificateChoice.tooltip.organization] -bullet1 = "จัดการโดยผู้ดูแลระบบของระบบ" -bullet2 = "ใช้ร่วมกันระหว่างผู้ใช้ที่ได้รับอนุญาต" -bullet3 = "แสดงตัวตนของบริษัท ไม่ใช่บุคคล" -bullet4 = "เหมาะสำหรับ: เอกสารทางการ ลายเซ็นของทีม" -description = "ใบรับรองที่ใช้ร่วมกันซึ่งจัดเตรียมโดยองค์กรของคุณ ใช้สำหรับอำนาจการลงนามในระดับบริษัท" -title = "ใบรับรองขององค์กร" - -[certificateChoice.tooltip.personal] -bullet1 = "สร้างโดยอัตโนมัติเมื่อใช้ครั้งแรก" -bullet2 = "ผูกกับบัญชีผู้ใช้ของคุณ" -bullet3 = "ไม่สามารถใช้ร่วมกับผู้ใช้อื่นได้" -bullet4 = "เหมาะสำหรับ: เอกสารส่วนบุคคล ความรับผิดชอบรายบุคคล" -description = "ใบรับรองที่สร้างอัตโนมัติเฉพาะบัญชีผู้ใช้ของคุณ เหมาะสำหรับลายเซ็นส่วนบุคคล" -title = "ใบรับรองส่วนบุคคล" - -[certificateChoice.tooltip.upload] -bullet1 = "ต้องมีไฟล์ P12/PFX และรหัสผ่าน" -bullet2 = "สามารถออกโดยหน่วยงานออกใบรับรองภายนอก" -bullet3 = "ระดับความน่าเชื่อถือสูงสำหรับเอกสารทางกฎหมาย" -bullet4 = "เหมาะสำหรับ: สัญญาที่มีผลผูกพันตามกฎหมาย การตรวจสอบจากภายนอก" -description = "ใช้ไฟล์ใบรับรอง PKCS#12 ของคุณเอง ให้การควบคุมคุณสมบัติใบรับรองอย่างเต็มที่" -title = "อัปโหลด P12 แบบกำหนดเอง" - [certSign] awaitingSignatures = "รอการลงนาม" chooseCertificate = "เลือกไฟล์ใบรับรอง" @@ -2542,34 +2769,6 @@ server = "เซิร์ฟเวอร์" stepTitle = "แหล่งที่มาของใบรับรอง" upload = "อัปโหลด" -[certSign.tooltip.header] -title = "เกี่ยวกับการจัดการลายเซ็น" - -[certSign.tooltip.overview] -bullet1 = "ตรวจสอบลายเซ็นที่มีอยู่และความถูกต้อง" -bullet2 = "ดูข้อมูลรายละเอียดเกี่ยวกับผู้ลงนามและใบรับรอง" -bullet3 = "เพิ่มลายเซ็นดิจิทัลใหม่เพื่อปกป้องเอกสารของคุณ" -bullet4 = "รองรับหลายไฟล์พร้อมการนำทางที่ง่าย" -text = "เครื่องมือนี้ช่วยให้คุณตรวจสอบว่า PDF ของคุณมีการลงนามดิจิทัลหรือไม่ และเพิ่มลายเซ็นดิจิทัลใหม่ได้ ลายเซ็นดิจิทัลยืนยันตัวตนของผู้สร้างหรือผู้อนุมัติเอกสาร และแสดงว่าเอกสารถูกเปลี่ยนแปลงหลังจากลงนามหรือไม่" -title = "เครื่องมือนี้ทำอะไรได้บ้าง?" - -[certSign.tooltip.signing] -bullet1 = "รองรับรูปแบบ PEM, PKCS12, JKS และใบรับรองของเซิร์ฟเวอร์" -bullet2 = "เลือกแสดงหรือซ่อนลายเซ็นบน PDF ได้" -bullet3 = "เพิ่มเหตุผล สถานที่ และชื่อผู้ลงนาม" -bullet4 = "เลือกหน้าที่จะวางลายเซ็นที่มองเห็นได้" -bullet5 = "ใช้ใบรับรองของเซิร์ฟเวอร์สำหรับตัวเลือก 'Sign with Stirling-PDF' ที่ง่าย" -text = "ในการลงนาม PDF คุณต้องมีใบรับรองดิจิทัล (เช่น PEM, PKCS12 หรือ JKS) คุณสามารถเลือกให้ลายเซ็นมองเห็นได้บนเอกสารหรือให้มองไม่เห็นเพื่อเน้นด้านความปลอดภัยเท่านั้น" -title = "การเพิ่มลายเซ็น" - -[certSign.tooltip.validation] -bullet1 = "แสดงว่าลายเซ็นถูกต้องหรือไม่ถูกต้อง" -bullet2 = "แสดงข้อมูลผู้ลงนามและวันที่ลงนาม" -bullet3 = "ตรวจสอบว่าเอกสารถูกแก้ไขหลังการลงนามหรือไม่" -bullet4 = "สามารถใช้ใบรับรองแบบกำหนดเองเพื่อการตรวจสอบได้" -text = "เมื่อคุณตรวจสอบลายเซ็น เครื่องมือจะแจ้งว่าถูกต้องหรือไม่ ใครเป็นผู้ลงนาม ลงนามเมื่อใด และเอกสารถูกเปลี่ยนแปลงหลังการลงนามหรือไม่" -title = "การตรวจสอบลายเซ็น" - [changeCreds] changeUsername = "อัปเดตชื่อผู้ใช้ของคุณ คุณจะถูกออกจากระบบหลังจากอัปเดต" credsUpdated = "อัปเดตบัญชีแล้ว" @@ -2737,12 +2936,16 @@ text = "เพื่อให้สิทธิ์เหล่านี้ไม [chat.actions] copy = "คัดลอกข้อความ" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "ตัวเลือกเอเจนต์ของ Stirling" +assistant = "AI Assistant" clearChat = "ล้างแชต" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "การดำเนินการนี้จะใช้เคร tooltip = "เลือกสีจากหน้าจอ" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "เลือกสี" [common] +actions = "Actions" back = "ย้อนกลับ" cancel = "ยกเลิก" close = "ปิด" +codeSample = "Code sample" collapse = "ยุบ" confirm = "ยืนยัน" continue = "ดำเนินการต่อ" copied = "คัดลอกแล้ว!" copy = "คัดลอก" done = "เสร็จสิ้น" +enterValue = "Enter value" error = "ข้อผิดพลาด" expand = "ขยาย" loading = "กำลังโหลด..." next = "ถัดไป" +open = "Open" preview = "แสดงตัวอย่าง" previous = "ก่อนหน้า" refresh = "รีเฟรช" +remaining = "คงเหลือ" retry = "ลองใหม่" save = "บันทึก" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "ล้างที่เลือก" @@ -2988,6 +3199,7 @@ title = "วิธีการบีบอัด" [compress.settings] desiredSize = "ขนาดไฟล์ที่ต้องการ" desiredSizePlaceholder = "ป้อนขนาด" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "การบีบอัดเป็นวิธีง่าย ๆ ในการลดขนาดไฟล์ เลือก ขนาดไฟล์ เพื่อป้อนขนาดเป้าหมายและให้เราปรับคุณภาพให้คุณ เลือก คุณภาพ เพื่อกำหนดความแรงการบีบอัดด้วยตนเอง" @@ -3190,6 +3402,24 @@ pdfOptions = "ตัวเลือก PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "เลือกประเภทต้นทางก่อน" settings = "การตั้งค่า" single = "เดี่ยว" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "การมอนิเตอร์ขั้นสูง" -allInOneWorkspace = "พื้นที่ทำงาน PDF แบบครบวงจร (ตัวดูไฟล์ เครื่องมือ และเอเจนต์)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "การสนับสนุนเฉพาะทางและ SLA" -fasterThroughput = "อัตราการประมวลผลเร็วขึ้น 10 เท่า" -fullyPrivateFiles = "ไฟล์เป็นส่วนตัวอย่างเต็มรูปแบบ" -largeFileProcessing = "ประมวลผลไฟล์ขนาดใหญ่" -orgWideAccess = "การควบคุมการเข้าถึงทั่วทั้งองค์กร" -premiumAiModels = "โมเดล AI ระดับพรีเมียม" -prioritySupport = "การสนับสนุนแบบเร่งด่วน" -privateDocCloud = "Private Document Cloud" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "การเข้าถึง API อย่างปลอดภัย" -standardThroughput = "อัตราการประมวลผลมาตรฐาน" -unlimitedApiAccess = "การเข้าถึง API แบบไม่จำกัด" -unlimitedSeats = "ผู้ใช้ไม่จำกัด" - [crop] autoCrop = "ครอบตัดช่องว่างอัตโนมัติ" noFileSelected = "เลือกไฟล์ PDF เพื่อเริ่มการครอบตัด" @@ -3346,6 +3558,7 @@ message = "ตั้งตัวจัดการ PDF เริ่มต้น title = "ข้อผิดพลาด" [defaultApp.prompt] +dontRemind = "ไม่ต้องเตือนฉันอีก" message = "ทำให้ Stirling PDF เป็นแอปเริ่มต้นสำหรับเปิดไฟล์ PDF" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "ค้นหา..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "แก้ไข" @@ -3644,11 +3861,13 @@ shareSelected = "แชร์ที่เลือก" sharing = "การแชร์" showAll = "แสดงทั้งหมด" showHistory = "แสดงประวัติ" +sortBy = "Sort files" sortByDate = "เรียงตามวันที่" sortByName = "เรียงตามชื่อ" sortBySize = "เรียงตามขนาด" storage = "ที่เก็บ" storageState = "ที่เก็บข้อมูล" +storageUsed = "Storage used" synced = "ซิงค์แล้ว" title = "อัปโหลดไฟล์ PDF" toolChain = "เครื่องมือที่ใช้" @@ -3694,13 +3913,17 @@ addFiles = "เพิ่มไฟล์" addingFiles = "กำลังเพิ่มไฟล์…" collapse = "ยุบแถบข้าง" customizeGroups = "ปรับแต่งกลุ่ม" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "เปิดไฟล์เพื่อเริ่มต้น" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "ขยายแถบข้าง" -files = "ไฟล์" googleDrive = "Google Drive" googleDriveDisabled = "ยังไม่ได้กำหนดค่า Google Drive" leaveMyFiles = "ออกจากไฟล์ของฉัน" +library = "ไลบรารี PDF" myFiles = "ไฟล์ของฉัน" noFiles = "ยังไม่มีไฟล์" openFileManager = "เปิดตัวจัดการไฟล์" @@ -3708,37 +3931,36 @@ openFromComputer = "เปิดจากคอมพิวเตอร์" openSettings = "เปิดการตั้งค่า" other = "อื่นๆ" recent = "ล่าสุด" -search = "ค้นหา" -searchPlaceholder = "ค้นหาไฟล์..." viewAll = "ดูไฟล์ทั้งหมด {{count}} ไฟล์" [fileSidebar.fileItem] closeViewer = "ปิดตัวแสดงผล" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "เปิดในตัวแสดงผล" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "เพิ่ม" -addLabel = "เพิ่มป้ายกำกับ…" -categoryIconAria = "เลือกไอคอนสำหรับ {{name}}" -createCategory = "สร้างหมวดหมู่" -delete = "ลบหมวดหมู่" done = "เสร็จสิ้น" -hide = "ซ่อนหมวดหมู่" -newCategory = "ชื่อหมวดหมู่ใหม่…" -removeLabel = "ลบ {{name}}" -rename = "เปลี่ยนชื่อ" reset = "รีเซ็ตเป็นค่าเริ่มต้น" -search = "ค้นหาป้ายกำกับ…" -show = "แสดงหมวดหมู่" subtitle = "จัดกลุ่มไฟล์ของคุณเป็นหมวดหมู่หลัก เพิ่มป้ายกำกับที่มีอยู่หรือป้ายกำกับใหม่ไปยังหมวดหมู่ เปลี่ยนชื่อ หรือสร้างของคุณเอง ไฟล์ที่ไม่อยู่ในหมวดหมู่ใดของคุณจะแสดงใต้ “อื่นๆ”" title = "หมวดหมู่แถบด้านข้าง" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "เพิ่มไปยังพื้นที่ทำงาน" addToWorkspaceCount = "เพิ่ม {{count}} ไปยังพื้นที่ทำงาน" @@ -3749,7 +3971,6 @@ backToMyFiles = "ย้อนกลับไปยังไฟล์ของฉ breadcrumbs = "เส้นทางโฟลเดอร์" cancel = "ยกเลิก" classification = "การจัดประเภท" -clearSearch = "ล้างการค้นหา" clearSelection = "ล้างการเลือก" closeDetails = "ปิดรายละเอียด" create = "สร้าง" @@ -3781,6 +4002,7 @@ downloadAll = "ดาวน์โหลดทั้งหมด" downloadVersion = "ดาวน์โหลดเวอร์ชันนี้" dropOverlay = "ปล่อยไฟล์เพื่ออัปโหลด" dropOverlaySub = "ไฟล์จะเริ่มใน ในเครื่อง ใช้ 'ย้ายไปยัง' หรือ 'บันทึกไปยังคลาวด์' เพื่อจัดระเบียบลงในโฟลเดอร์." +duplicate = "Duplicate" file = "ไฟล์" fileInfo = "File info" fileMenu = "การดำเนินการไฟล์" @@ -3808,13 +4030,12 @@ refresh = "รีเฟรชจากเซิร์ฟเวอร์" remove = "ลบ" removeVersion = "เอาเวอร์ชันนี้ออก" rename = "เปลี่ยนชื่อ" +renamed = "เปลี่ยนชื่อแล้ว" renameFolder = "เปลี่ยนชื่อโฟลเดอร์" resizeFolderTree = "ปรับขนาดต้นไม้โฟลเดอร์ (ปุ่มลูกศร, กด Shift เพื่อก้าวใหญ่ขึ้น; ดับเบิลคลิกเพื่อปรับอัตโนมัติ)" save = "บันทึก" saveToServer = "บันทึกไปยังเซิร์ฟเวอร์" saveToServerDisabledHint = "การบันทึกไปยังเซิร์ฟเวอร์ไม่ได้เปิดใช้งานบนเซิร์ฟเวอร์นี้ โปรดขอให้ผู้ดูแลระบบเปิดใช้งาน." -search = "ค้นหา" -searchPlaceholder = "ค้นหาโฟลเดอร์นี้และโฟลเดอร์ย่อย" selectAll = "เลือกทั้งหมด" selectAllHint = "คลิกเพื่อเลือกทั้งหมด เคล็ดลับ: กด Ctrl (หรือ Cmd) เพื่อเพิ่มทีละไฟล์ กด Shift เพื่อเลือกช่วง." selectedCount = "เลือกแล้ว {{count}}" @@ -3874,6 +4095,10 @@ title = "ยังไม่มีไฟล์คลาวด์" hint = "ไฟล์ที่บันทึกโดยไม่อัปโหลดจะอยู่ที่นี่ ปล่อยไฟล์เพื่อเพิ่ม." title = "ไม่มีไฟล์เฉพาะในเครื่อง" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "ไฟล์ที่คุณเปิดหรือแก้ไขจะปรากฏที่นี่." title = "ยังไม่มีการแก้ไข" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "ไม่สามารถลบโฟลเดอร์ได้." deleteFolderFailedDetail = "ไม่สามารถลบโฟลเดอร์ได้: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "ไม่สามารถอัปเดตลักษณะที่ปรากฏของโฟลเดอร์ได้." folderAppearanceFailedDetail = "ไม่สามารถอัปเดตลักษณะที่ปรากฏของโฟลเดอร์ได้: {{message}}" moveFilesFailed = "ไม่สามารถย้ายไฟล์ได้." @@ -3910,9 +4137,7 @@ count = "ไฟล์" folder = "โฟลเดอร์" labels = "ป้ายกำกับ" modified = "แก้ไขเมื่อ" -name = "ชื่อ" size = "ขนาด" -toolHistoryAtVersion = "ชุดเครื่องมือสะสม" totalSize = "ขนาดรวม" type = "ชนิด" versionHistory = "เส้นทางเวอร์ชัน" @@ -3945,6 +4170,11 @@ localHint = "เก็บไว้เฉพาะในเบราว์เซ shared = "แชร์" sharedHint = "แชร์กับคุณผ่านลิงก์" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "เก่าสุดก่อน" modifiedDesc = "ล่าสุดก่อน" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "ไม่มีไฟล์ในที่เก็บ open = "เปิด" openFile = "เปิดไฟล์" openFiles = "เปิดไฟล์หลายไฟล์" +selectFile = "Select {{name}}" selectFromStorage = "เลือกจากที่เก็บ" upload = "อัปโหลด" uploadFile = "อัปโหลดไฟล์" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "บันทึกทั้งหมดแล้ว" +analyzingFields = "Analysing form fields..." +extractCsvError = "ดึง CSV ไม่สำเร็จ" +extractXlsxError = "ดึง XLSX ไม่สำเร็จ" +filled = "filled" flattenAfterFilling = "ทำให้แบนหลังจากกรอก" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "จำเป็น" +requiredFieldsError = "โปรดกรอกช่องที่จำเป็นทั้งหมด" rescanFields = "สแกนฟิลด์อีกครั้ง" rescanFormFields = "สแกนฟิลด์ฟอร์มอีกครั้ง" save = "บันทึก" +saveShortcut = "Ctrl+S เพื่อบันทึก" +unsavedChanges = "การเปลี่ยนแปลงที่ยังไม่ได้บันทึก" [formFill.sidebar] close = "ปิดแถบด้านข้าง" @@ -4309,6 +4552,11 @@ desc = "เปลี่ยนชื่อไฟล์ PDF อัตโนมั tags = "ตรวจจับอัตโนมัติ,จากหัวกระดาษ,จัดระเบียบ,เปลี่ยนชื่อ" title = "เปลี่ยนชื่อไฟล์ PDF อัตโนมัติ" +[home.autoRotate] +desc = "ตรวจจับการวางแนวของแต่ละหน้าและหมุนให้ตั้งตรงโดยอัตโนมัติ" +tags = "หมุนอัตโนมัติ,แก้ไขการวางแนว,กลับหัว,ตะแคง,ปรับให้ตรง,จัดแนว,ตรวจจับการวางแนว,แก้ไขการหมุน,จัดแนวอัตโนมัติ,สแกน,OSD" +title = "หมุนอัตโนมัติ" + [home.autoSizeSplitPDF] tags = "อัตโนมัติ,แยก,ขนาด" title = "แยกตามขนาด/จำนวน" @@ -4608,15 +4856,12 @@ hint = "อัปโหลดไฟล์รูปภาพ PNG, JPG, SVG หร label = "อัปโหลดรูปภาพ" placeholder = "เลือกไฟล์รูปภาพ" -[imageToPdf] -tags = "การแปลง, รูปภาพ, JPG, ภาพ, รูปถ่าย" - [imageToPDF] header = "รูปภาพเป็น PDF" title = "รูปภาพเป็น PDF" -[infoBanner] -dismiss = "ปิด" +[imageToPdf] +tags = "การแปลง, รูปภาพ, JPG, ภาพ, รูปถ่าย" [invite] acceptError = "ไม่สามารถสร้างบัญชี" @@ -4647,11 +4892,9 @@ welcomeTitle = "คุณได้รับคำเชิญ!" [landing] addFiles = "เพิ่มไฟล์" -heroSubtitle = "ลากมาวางหรือเพิ่ม PDF ที่มีอยู่เพื่อเริ่มต้น" mobileUpload = "อัปโหลดจากมือถือ" openFromComputer = "เปิดจากคอมพิวเตอร์" uploadFromComputer = "อัปโหลดจากคอมพิวเตอร์" -workbenchEmptyStateHero = "วาง PDF ได้ทุกที่" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "ป้อนชื่อผู้ใช้" failedToSignIn = "ไม่สามารถลงชื่อเข้าใช้ด้วย {{provider}}: {{message}}" forgotPassword = "ลืมรหัสผ่านหรือไม่?" loggingIn = "กำลังเข้าสู่ระบบ..." -logIn = "ลงชื่อเข้าใช้" login = "เข้าสู่ระบบ" magicLinkSent = "ส่ง Magic Link ไปยัง {{email}} แล้ว! ตรวจสอบอีเมลของคุณและคลิกลิงก์เพื่อเข้าสู่ระบบ" mfaCode = "รหัสยืนยันตัวตน" @@ -4715,26 +4957,10 @@ signInWith = "ลงชื่อเข้าใช้ด้วย" title = "ลงชื่อเข้าใช้" unexpectedError = "ข้อผิดพลาดที่ไม่คาดคิด: {{message}}" updatePassword = "อัปเดตรหัสผ่าน" -useEmailInstead = "เข้าสู่ระบบด้วยอีเมล" useMagicLink = "ใช้ Magic Link แทน" username = "ชื่อผู้ใช้" youAreLoggedIn = "คุณได้เข้าสู่ระบบแล้ว!" -[login.slides.edit] -alt = "แก้ไข PDF" -subtitle = "ด้วยเครื่องมือกว่าหนึ่งโหลที่ช่วยคุณปกปิดข้อมูล ลงนาม อ่าน และจัดการ PDF คุณจะพบสิ่งที่ต้องการแน่นอน" -title = "แก้ไข PDF เพื่อแสดง/ปกป้องข้อมูลที่คุณต้องการ" - -[login.slides.overview] -alt = "ภาพรวม Stirling PDF" -subtitle = "ชุดเครื่องมือบนคลาวด์ที่ให้ความสำคัญกับความเป็นส่วนตัว ช่วยให้คุณแปลงไฟล์ ลงนาม ปกปิดข้อมูล และจัดการเอกสาร พร้อมเครื่องมือทรงพลังกว่า 50 รายการ" -title = "ทุกสิ่งสำหรับงาน PDF ของคุณในที่เดียว" - -[login.slides.secure] -alt = "ปกป้อง PDF" -subtitle = "เพิ่มรหัสผ่าน ปกปิดเนื้อหา และจัดการใบรับรองได้อย่างง่ายดาย" -title = "ปกป้องข้อมูลสำคัญใน PDF ของคุณ" - [margin] large = "ใหญ่" medium = "กลาง" @@ -4818,6 +5044,34 @@ uploadSuccess = "อัปโหลดสำเร็จ!" uploadSuccessMessage = "ถ่ายโอนรูปภาพของคุณเรียบร้อยแล้ว" validating = "กำลังตรวจสอบเซสชัน..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "สแกนเพื่ออัปโหลดรูปภาพ รูปภาพจะแปลงเป็น PDF อัตโนมัติ" descriptionNoConvert = "สแกนเพื่ออัปโหลดรูปภาพจากอุปกรณ์มือถือของคุณ" @@ -4836,8 +5090,13 @@ title = "อัปโหลดจากมือถือ" tags = "เครื่องมือหลายตัว, หลายการดำเนินการ, UI, คลิกและลาก, ส่วนหน้า, ฝั่งไคลเอนต์, อินเตอร์แอคทีฟ, เคลื่อนย้าย" title = "เครื่องมือ PDF หลายตัว" -[navbar] -search = "ค้นหา" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "การยืนยันตัวตนไม่สำเร็จ คุณสามารถปิดหน้าต่างนี้และลองอีกครั้ง" @@ -4966,6 +5225,7 @@ activeFiles = "มุมมอง ไฟล์ที่ใช้งา allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "ปิด" cropSettings = "เมื่อเราเลือกไฟล์ที่ต้องการครอบตัดแล้ว เราสามารถกำหนดค่าเครื่องมือครอบตัดเพื่อเลือกพื้นที่ที่ต้องการครอบตัด PDF" +dialogLabel = "Onboarding" fileCheckbox = "การคลิกที่หนึ่งในไฟล์จะเลือกไฟล์นั้นเพื่อประมวลผล คุณสามารถเลือกหลายไฟล์เพื่อทำแบบชุดได้" fileReplacement = "ไฟล์ที่แก้ไขจะมาแทนที่ไฟล์เดิมใน Workbench โดยอัตโนมัติ ช่วยให้คุณรันเครื่องมืออื่นต่อได้ง่าย" filesButton = "ปุ่ม ไฟล์ ในแถบเข้าถึงด่วนช่วยให้คุณอัปโหลด PDF เพื่อใช้เครื่องมือต่างๆ ได้" @@ -4976,6 +5236,7 @@ pinButton = "คุณสามารถใช้ปุ่ม ปัก results = "หลังจากเครื่องมือทำงานเสร็จ ขั้นตอน ตรวจทาน จะแสดงตัวอย่างผลลัพธ์ในพาเนลนี้ และให้คุณยกเลิกการดำเนินการหรือดาวน์โหลดไฟล์" runButton = "เมื่อกำหนดค่าเครื่องมือเรียบร้อยแล้ว ปุ่มนี้จะให้คุณรันเครื่องมือนี้กับ PDF ที่เลือกทั้งหมด" selectCropTool = "มาลองเลือกเครื่องมือ ครอบตัด เพื่อสาธิตการใช้งานเครื่องมือหนึ่ง" +stepOf = "ขั้นตอนที่ {{current}} จาก {{total}}" toolInterface = "นี่คืออินเทอร์เฟซของเครื่องมือ ครอบตัด จะเห็นว่ายังไม่มีอะไรมากเพราะเรายังไม่ได้เพิ่มไฟล์ PDF เพื่อทำงานด้วย" workbench = "นี่คือ Workbench - พื้นที่หลักที่คุณใช้ดูและแก้ไข PDF ของคุณ" wrapUp = "เสร็จเรียบร้อย! คุณได้เรียนรู้พื้นที่หลักของแอปและวิธีใช้งานแล้ว คลิกปุ่ม ช่วยเหลือ เมื่อใดก็ได้เพื่อดูทัวร์นี้อีกครั้ง" @@ -4988,6 +5249,26 @@ showMeAround = "พาชมรอบๆ" skipForNow = "ข้ามไปก่อน" skipTheTour = "ข้ามทัวร์" +[onboarding.checklist] +dismiss = "ปิด" +title = "ตั้งค่า Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "เรียกใช้ Stirling แบบเนทีฟบนเครื่องของคุณ" +title = "ดาวน์โหลด Stirling สำหรับ Desktop" + +[onboarding.checklist.inviteTeam] +description = "ทำงานร่วมกับทีมของคุณ" +title = "เชิญสมาชิกทีม" + +[onboarding.checklist.shareAnalytics] +description = "ช่วยปรับปรุง Stirling" +title = "แชร์ข้อมูลการใช้งานแบบไม่ระบุตัวตน" + +[onboarding.checklist.takeTour] +description = "ดูวิธีการทำงานของ Stirling ในคำแนะนำแบบรวดเร็ว" +title = "เริ่มทัวร์" + [onboarding.desktopInstall] body = "Stirling ใช้งานได้ดีที่สุดแบบแอปเดสก์ท็อป คุณสามารถใช้งานออฟไลน์ เข้าถึงเอกสารได้เร็วขึ้น และแก้ไขบนคอมพิวเตอร์ของคุณโดยตรง" selectOs = "เลือกระบบปฏิบัติการ" @@ -4996,7 +5277,21 @@ titleWithOs = "ดาวน์โหลดสำหรับ {{osLabel}}" [onboarding.mfa] authenticationCode = "รหัสยืนยันตัวตน" +description = "รักษาความปลอดภัยบัญชีของคุณโดยเชื่อมโยงแอปยืนยันตัวตน สแกน QR code หรือป้อน setup key จากนั้นยืนยันรหัส 6 หลักเพื่อเสร็จสิ้น" +enable = "เปิดใช้ MFA" +enableError = "ไม่สามารถเปิดใช้การยืนยันตัวตนสองปัจจัยได้ ตรวจสอบรหัสและลองอีกครั้ง" +enterCode = "ป้อนรหัส 6 หลักจากแอปของคุณ" +enterCodeError = "ป้อนรหัสยืนยันตัวตนเพื่อดำเนินการต่อ" +logout = "ออกจากระบบ" +openAuthenticator = "เปิด Google Authenticator, Authy หรือ 1Password" qrCodeLoading = "กำลังสร้างรหัส QR ของคุณ…" +regenerateQrCode = "สร้าง QR code ใหม่" +scanQrCode = "สแกน QR code หรือป้อน setup key ด้านล่าง" +setupError = "ไม่สามารถเริ่มการตั้งค่าการยืนยันตัวตนสองปัจจัยได้ โปรดลองอีกครั้ง" +setupKey = "Setup key (ป้อนด้วยตนเอง)" +stepByStep = "ทีละขั้นตอน" +success = "เปิดใช้ MFA แล้ว คุณสามารถดำเนินการต่อได้" +title = "การตั้งค่าการยืนยันตัวตนหลายปัจจัย" [onboarding.planOverview] adminBodyLoginDisabled = "เมื่อเปิดใช้โหมดล็อกอิน คุณจะสามารถจัดการผู้ใช้ กำหนดค่าต่างๆ และติดตามสุขภาพเซิร์ฟเวอร์ ผู้ใช้ {{freeTierLimit}} คนแรกบนเซิร์ฟเวอร์ของคุณใช้งาน Stirling ได้ฟรี" @@ -5005,6 +5300,11 @@ adminTitle = "ภาพรวมผู้ดูแล" userBody = "เชิญเพื่อนร่วมทีม กำหนดบทบาท และจัดระเบียบเอกสารในพื้นที่ทำงานเดียวที่ปลอดภัย เปิดโหมดล็อกอินเมื่อคุณพร้อมเติบโตเกินการใช้งานคนเดียว" userTitle = "ภาพรวมแพ็กเกจ" +[onboarding.processorIntro] +body = "ตอนนี้ Stirling เรียกใช้ Policies — กฎอัตโนมัติที่จัดหมวดหมู่ รักษาความปลอดภัย และประมวลผลเอกสารทุกฉบับเมื่อมาถึง ตั้งค่าและติดตามการทำงานใน Processor" +cta = "ดู Processor" +title = "ดู Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "ชำระล่วงหน้ารายปี" + +[payg.prepaid.banner] +exhausted = "ความจุแบบชำระล่วงหน้าของคุณถูกใช้หมดแล้ว; การเรียกเก็บเงินตามมิเตอร์กลับมาทำงานแล้ว เติมเงินเพื่อคงส่วนลดไว้" +expiring_one = "ปีแบบชำระล่วงหน้าของคุณจะสิ้นสุดใน {{count}} วัน ความจุที่ไม่ได้ใช้จะหมดอายุ — เติมเงินเพื่อเริ่มปีใหม่" +expiring_other = "ปีแบบชำระล่วงหน้าของคุณจะสิ้นสุดใน {{count}} วัน ความจุที่ไม่ได้ใช้จะหมดอายุ — เติมเงินเพื่อเริ่มปีใหม่" +low = "คุณเหลือ {{pct}}% ของความจุแบบชำระล่วงหน้า เติมเงินได้ทุกเมื่อเพื่อให้การประมวลผลไม่สะดุด" + +[payg.prepaid.card] +subtitle = "ใช้ก่อนการเรียกเก็บเงินตามมิเตอร์และอยู่นอกขีดจำกัดค่าใช้จ่ายของคุณ" +title = "ความจุแบบชำระล่วงหน้า" + +[payg.prepaid.meter] +capSuffix = "จาก {{total}} เครดิตแบบชำระล่วงหน้า" +expires = "หมดอายุ {{date}}" + +[payg.prepaid.state] +exhausted = "ใช้หมดแล้ว" +healthy = "เหลืออีกมาก" +low = "ใกล้หมด" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF เป็น PDF/A" tags = "การจัดเก็บ, ระยะยาว, มาตรฐาน, การแปลง, การเก็บรักษา" title = "PDF เป็น PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "พิมพ์,มาตรฐาน,แปลง,การผลิต,ก่อนพิมพ์,เก็บถาวร" title = "แปลง PDF เป็น PDF/X" @@ -5822,6 +6151,7 @@ small = "500 เครดิต" xsmall = "100 เครดิต" [plan.availablePlans] +currency = "Billing currency" subtitle = "เลือกแผนที่เหมาะกับความต้องการของคุณ" title = "แผนที่มีให้" @@ -5937,49 +6267,12 @@ keyDescription = "วางคีย์ไลเซนส์จากอีเ success = "เปิดใช้งานไลเซนส์แล้ว!" successMessage = "เปิดใช้งานไลเซนส์ของคุณเรียบร้อยแล้ว ตอนนี้คุณสามารถปิดหน้าต่างนี้ได้" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "บังคับใช้แล้ว" -enforcing = "กำลังบังคับใช้..." -failed = "การบังคับใช้ล้มเหลว" outputsUnavailable = "ผลลัพธ์ของนโยบายไม่สามารถดาวน์โหลดได้อีกต่อไป" partialOutputsUnavailable = "ผลลัพธ์ของนโยบายบางรายการไม่สามารถดาวน์โหลดได้อีกต่อไป" -retrying = "ระบบไม่ว่าง กำลังลองใหม่..." runNotFound = "ไม่พบการรันการบังคับใช้นี้แล้ว" -step = "ขั้นตอน {{current}}/{{total}}" timedOut = "การบังคับใช้หมดเวลาก่อนที่การรันจะเสร็จสิ้น" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "ปิด" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "กำลังนำ {{names}} ไปใช้" applyingProgress = "กำลังนำ {{names}} ไปใช้ ({{done}} จาก {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "ไฟล์ {{failures}} จาก {{total}} ไฟล์ไม failureTitle = "ส่งออกโดยไม่ได้บังคับใช้อย่างครบถ้วน" printPolicyAppliedBody = "PDF นี้ถูกอัปเดตให้เป็นไปตามนโยบาย โปรดตรวจสอบการเปลี่ยนแปลง จากนั้นพิมพ์อีกครั้ง" printPolicyAppliedTitle = "นำนโยบายไปใช้ก่อนพิมพ์" -queued = "+{{count}} อยู่ในคิว" successTitle = "นำ {{names}} ไปใช้แล้ว" summaryMore = "{{first}}, {{second}} และอีก {{more}} รายการ" summaryTwo = "{{first}} และ {{second}}" -[policies.enforcement.triggerVerb] -convert = "บังคับใช้ก่อนการแปลง" -default = "กำลังบังคับใช้" -export = "บังคับใช้ก่อนการส่งออก" -input = "บังคับใช้ระหว่างการนำเข้า" -print = "บังคับใช้ก่อนการพิมพ์" - -[policies.field] -accessLog = "บันทึกการเข้าถึง" -archiveAfter = "เก็บถาวรหลังจาก" -auditTrail = "ร่องรอยการตรวจสอบ" -belowThreshold = "ต่ำกว่าเกณฑ์" -destination = "ปลายทาง" -frameworks = "Frameworks" -immutableHold = "การระงับแบบเปลี่ยนแปลงไม่ได้" -keepFor = "เก็บไว้เป็นเวลา" -minConfidence = "ความเชื่อมั่นขั้นต่ำ" -notify = "แจ้งเตือนเมื่อกำหนดเส้นทาง" -onViolation = "เมื่อไม่เป็นไปตามข้อกำหนด" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "เพิ่ม" -addPlaceholder = "เพิ่มป้ายกำกับ…" -customNote = "ปรับแต่งสำหรับทีมของคุณแล้ว" -defaultNote = "ใช้ค่าเริ่มต้นในตัว ซึ่งแชร์กับทีมของคุณ" -duplicate = "\"{{name}}\" มีอยู่แล้ว" -edit = "แก้ไขป้ายกำกับ" -empty = "ยังไม่มีป้ายกำกับ" -export = "ส่งออก JSON" -iconAria = "เลือกไอคอนสำหรับ {{name}}" -import = "นำเข้า JSON" -importError = "ไม่สามารถนำเข้าไฟล์นั้นได้" -managedNote = "ป้ายกำกับทีมได้รับการจัดการโดยหัวหน้าทีมของคุณ" -modalSubtitle = "แชร์กับทั้งทีมของคุณ ตัวจัดประเภทจะเลือกป้ายกำกับที่เหมาะกับแต่ละเอกสาร" -modalTitle = "ป้ายกำกับการจัดประเภท" -pickIcon = "ใช้ไอคอน {{label}}" -removeAria = "ลบ {{name}}" -resetToDefault = "รีเซ็ตเป็นค่าเริ่มต้น" -saveForTeam = "บันทึกสำหรับทีม" -saving = "กำลังบันทึก…" -sectionLabel = "ป้ายกำกับการจัดประเภท" -startFromScratch = "เริ่มจากศูนย์" -teamCount = "ป้ายกำกับทีม" -tooLong = "ป้ายกำกับต้องมีความยาวไม่เกิน {{max}} อักขระ" -ungrouped = "ไม่ได้จัดกลุ่ม" -view = "ดูป้ายกำกับ" +categoryCount = "หมวดหมู่" +hideCategory = "ซ่อนหมวดหมู่" +labelCount = "ป้ายกำกับ" +sharedNote = "ป้ายกำกับเหล่านี้มีมาให้ในระบบและแชร์ทั่วทั้งทีมของคุณ" +showCategory = "แสดงหมวดหมู่" [policies.pii] account = "หมายเลขบัญชี (มีป้ายกำกับ)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "หมายเลข Routing ของสหรัฐฯ (ABA)" ssn = "หมายเลขประกันสังคม" -[policies.settings] -noneExport = "ไม่มีนโยบายที่ทำงานเมื่อส่งออกในขณะนี้" -noneUpload = "ไม่มีนโยบายที่ทำงานเมื่ออัปโหลดในขณะนี้" -onExport = "เมื่อส่งออก" -onUpload = "เมื่ออัปโหลด" -reorderHandle = "ลากเพื่อจัดลำดับใหม่" -runOrderDesc = "เมื่อมีนโยบายมากกว่าหนึ่งรายการทำงานบนทริกเกอร์เดียวกัน นโยบายจะทำงานตามลำดับนี้ — แต่ละรายการทำงานกับผลลัพธ์ของนโยบายก่อนหน้า ลากเพื่อจัดลำดับใหม่" -title = "การตั้งค่านโยบาย" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "ตัวเลือกนโยบาย" -policySettings = "การตั้งค่านโยบาย" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "ลองนโยบายที่ล้มเหลวอีกครั้ง ({{count}})" -rowProgress = "ประมวลผลแล้ว {{completed}} จาก {{total}} ไฟล์" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "นโยบายคืออะไร?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "ค้นหาและปกปิดข้อมูลที่อ่อนไหวด้วยสีดำโดยอัตโนมัติ — เช่น หมายเลขประกันสังคมและหมายเลขบัตร — เพื่อไม่ให้สามารถอ่านได้ในเอกสาร" -sanitize = "ลบ JavaScript ที่ซ่อนอยู่จากไฟล์ เพื่อไม่ให้มีสิ่งใดทำงานโดยอัตโนมัติเมื่อมีผู้เปิด" -watermark = "ประทับเครื่องหมายที่มองเห็นได้ (เช่น \"Confidential\") บนทุกหน้า" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "ปิด" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} กำลังบังคับใช้..." badgeRan = "นโยบาย {{name}} ทำงานกับไฟล์นี้แล้ว" +badgeRunning = "{{name}} กำลังทำงาน..." blockingAction = "{{action}} ถูกบล็อกขณะบังคับใช้นโยบาย โปรดรอ..." dismiss = "ปิดโอเวอร์เลย์" enforcingTitle = "กำลังบังคับใช้นโยบาย..." @@ -6151,6 +6316,7 @@ viewAnyway = "ดูไฟล์ (นโยบายยังคงบังค [policyOption] autoRedactPhi = "ปกปิด PHI อัตโนมัติ" blockExport = "บล็อกการส่งออก" +ccpa = "CCPA" contracts = "สัญญา" documents = "เอกสาร" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "แผน Editor" subscribed = "แผน Processor" unlinked = "ยังไม่ได้เชื่อมโยง" -[portal.agentBuilder] -bootstrapFromDocument = "เริ่มต้นจากเอกสาร" -sectionsAriaLabel = "ส่วนของ Agent builder" -selectorAriaLabel = "เอเจนต์" -subtitle = "ออกแบบ ทดสอบ และเผยแพร่เอเจนต์ AI ที่จัดประเภท ดึงข้อมูลจาก และกำหนดเส้นทางเอกสารของคุณ กำหนดสถานการณ์ จำกัดการเข้าถึงเครื่องมือ รัน golden set และเผยแพร่เวอร์ชัน" -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "ยกเลิก" -dropzoneText = "เลือกเอกสารตัวอย่าง (PDF หรือรูปภาพ)" -lead = "วางเอกสารตัวอย่าง แล้วเราจะเสนอสถานการณ์และสคีมาการดึงข้อมูลที่คุณสามารถปรับปรุงได้ จะไม่มีการเผยแพร่จนกว่าคุณจะตรวจสอบ" -submit = "เริ่มต้นเอเจนต์" -subtitle = "เริ่มเอเจนต์ใหม่จากไฟล์ตัวแทนหนึ่งไฟล์" -title = "เริ่มต้นจากเอกสาร" - -[portal.agentBuilder.empty] -description = "เริ่มต้นเอเจนต์จากเอกสารตัวอย่างเพื่อสร้างสถานการณ์และสคีมาการดึงข้อมูล จากนั้นปรับปรุงและเผยแพร่" -title = "ยังไม่มีเอเจนต์" - -[portal.agentBuilder.evals] -casesPassing = "กรณีที่ผ่าน" -columnCase = "กรณีการประเมิน" -columnLatency = "เวลาแฝง" -columnResult = "ผลลัพธ์" -fail = "ไม่ผ่าน" -goldenSetPassRate = "อัตราผ่านของ golden-set" -latencyMs = "{{ms}} ms" -notRun = "ยังไม่ได้รัน" -pass = "ผ่าน" -passRate = "อัตราผ่าน" -runEvals = "รันการประเมิน" - -[portal.agentBuilder.evals.empty] -description = "การประเมินจะเปลี่ยนสถานการณ์ของคุณให้เป็น golden set ที่ทำซ้ำได้ อัปเกรดเพื่อบันทึกอัตราผ่านตามเวลาและใช้เป็นเงื่อนไขในการเผยแพร่" -title = "ยังไม่มี golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "ใน golden sets" -activeAgents = "เอเจนต์ที่ใช้งานอยู่" -avgPassRate = "อัตราผ่านการประเมินเฉลี่ย" -fleetWide = "ทั้งระบบ" -latestPublished = "เผยแพร่ล่าสุด" -scenarios = "สถานการณ์" -testCases = "กรณีทดสอบ" -totalDescription_one = "ทั้งหมด {{count}}" -totalDescription_other = "ทั้งหมด {{count}}" - -[portal.agentBuilder.scenarios] -add = "เพิ่ม" -addScenario = "เพิ่มสถานการณ์" -enable = "เปิดใช้งาน" -expectationLabel = "พฤติกรรมที่คาดหวัง" -expectationPlaceholder = "สิ่งที่เอเจนต์ควรทำ" -inEval = "อยู่ในการประเมิน" -mute = "ปิดเสียง" -muted = "ปิดเสียงแล้ว" -nameLabel = "ชื่อ" -namePlaceholder = "เช่น การยกระดับการปฏิบัติตามข้อกำหนด" - -[portal.agentBuilder.tabs] -evals = "การประเมิน" -scenarios = "สถานการณ์" -tools = "เครื่องมือ" -versions = "เวอร์ชัน" - -[portal.agentBuilder.tools] -broadAccess = "การเข้าถึงแบบกว้าง" -deniedHint = "เครื่องมือที่เลือกถูกบล็อก เครื่องมืออื่นทั้งหมดยังคงเรียกใช้ได้" -deniedTools = "เครื่องมือที่ถูกปฏิเสธ" -governanceGate = "การกำกับดูแลเครื่องมือมีให้ใช้ในแผน Enterprise" -restricted = "ถูกจำกัด" -restrictedAccess = "การเข้าถึงเครื่องมือแบบจำกัด" -restrictedDescription = "อนุญาตทุกเครื่องมือ ยกเว้นรายการที่คุณปฏิเสธด้านล่าง" - -[portal.agentBuilder.versions] -current = "ปัจจุบัน" -historyGate = "ประวัติเวอร์ชันแบบเต็มและการย้อนกลับมีให้ใช้ในแผน Enterprise" -publish = "เผยแพร่" -rollBack = "ย้อนกลับ" - [portal.assistant] close = "ปิดผู้ช่วย" error = "ไม่สามารถเชื่อมต่อผู้ช่วยได้" @@ -6365,9 +6451,21 @@ typing = "กำลังพิมพ์" loading = "กำลังโหลด" redirectingToEditor = "กำลังเปลี่ยนเส้นทางไปยังตัวแก้ไข..." +[portal.billing.activation] +maybeLater = "ไว้ทีหลัง" +title = "เปิดใช้งาน Processor" + +[portal.billing.activation.payg] +sub = "ตั้งขีดจำกัดรายเดือน จ่ายตามที่ใช้" +title = "จ่ายตามการใช้งาน" + +[portal.billing.activation.prepay] +badge = "ฟรี 2 เดือน" +sub = "ใบแจ้งหนี้เดียว ชำระด้วยบัตรหรือโอนผ่านธนาคาร" +title = "ชำระล่วงหน้าหนึ่งปี" + [portal.billing.checkout] noClientSecret = "Edge function ไม่ได้ส่งคืน client_secret" -subtitle = "เพิ่มบัตรเพื่อใช้งานต่อหลังจากสิทธิ์ฟรีในแผน Editor ของคุณ Stripe จะจัดการส่วนที่เหลือ" title = "เปิดใช้แผน Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "การชำระเงินของคุณสำเร็จแ close = "ปิด" title = "เกือบเสร็จแล้ว" +[portal.billing.checkout.cap] +amountAria = "ขีดจำกัดค่าใช้จ่ายรายเดือน" +back = "กลับ" +continue = "ไปต่อที่การชำระเงิน" +error = "ไม่สามารถตั้งขีดจำกัดค่าใช้จ่ายของคุณได้" +finePrint = "ออกใบแจ้งหนี้รายเดือน ยกเลิกได้ทุกเมื่อ" +noLimit = "ไม่จำกัด" +note = "คุณจะไม่ถูกเรียกเก็บเกินขีดจำกัดของคุณ การประมวลผลจะหยุดชั่วคราวเท่านั้น" +perMonth = "/ เดือน" +title = "ตั้งขีดจำกัดค่าใช้จ่ายของคุณ" + +[portal.billing.checkout.card] +fields = "หมายเลขบัตร · MM / YY · CVC · ZIP" +label = "รายละเอียดบัตร" +note = "รายละเอียดบัตรเก็บรวบรวมโดย Stripe Stirling ไม่เคยจัดเก็บ PAN หรือ CVC" + [portal.billing.checkout.error] title = "ไม่สามารถเริ่มชำระเงินได้" @@ -6383,10 +6497,9 @@ body = "การชำระเงินของคุณสำเร็จแ hint = "โปรดเปิดหน้าต่างนี้ค้างไว้" title = "กำลังเปิดใช้งานแผน Processor ของคุณ..." -[portal.billing.checkout.notConfigured] -bodyAfter = "ใน portal env เพื่อเปิดใช้งานการชำระเงินในแอป" -bodyBefore = "ตั้งค่า" -title = "ยังไม่ได้กำหนดค่า Stripe" +[portal.billing.checkout.pay] +limit = "ขีดจำกัดค่าใช้จ่ายรายเดือน {{amount}}/เดือน การประมวลผลจะหยุดชั่วคราวเมื่อถึงขีดจำกัด" +limitNoCap = "ไม่มีขีดจำกัดค่าใช้จ่าย การประมวลผลจะไม่หยุดชั่วคราว ออกใบแจ้งหนี้รายเดือน" [portal.billing.enterpriseUpsell] cta = "สร้างใบเสนอราคา Enterprise ของคุณ" @@ -6409,10 +6522,12 @@ currentPlan = "แผนปัจจุบัน" freeForever = "ฟรีตลอดไป" noTeamResolved = "ยังไม่พบทีมในกระเป๋าเงินของคุณ — รีเฟรชแล้วลองอีกครั้ง" ownerOnly = "เฉพาะเจ้าของทีมเท่านั้นที่สามารถเปิดใช้แผน Processor ได้" +payInvoice = "ชำระใบแจ้งหนี้เพื่อดำเนินการให้เสร็จ" planName = "Editor" ssoIncluded = "รวม SSO" switchOnProcessor = "เปิดใช้ Processor →" unlimitedUsers = "ผู้ใช้ไม่จำกัด" +viewQuote = "ดูใบเสนอราคา" [portal.billing.invoices] columnAmount = "จำนวนเงิน" @@ -6465,7 +6580,111 @@ segmentApiDesc = "คำขอ API โดยตรง" segmentApiLabel = "API" segmentAutomationDesc = "ระบบอัตโนมัติและ pipelines" segmentAutomationLabel = "ระบบอัตโนมัติ" +sizeMultiplier = "{{formatted}} PDFs ใช้ตัวคูณขนาด" +summary = "{{unique}} ไม่ซ้ำ · {{units}} หน่วยมิเตอร์ · เฉลี่ย {{avg}} ต่อ PDF" +summaryNoRate = "{{unique}} ไม่ซ้ำ · {{units}} หน่วยมิเตอร์" unit = "PDFs ตามปริมาณการใช้งาน" +unitsPending_one = "{{units}} หน่วยมิเตอร์รอซิงค์จากอินสแตนซ์ที่เชื่อมโยง" +unitsPending_other = "{{units}} หน่วยมิเตอร์รอซิงค์จากอินสแตนซ์ที่เชื่อมโยง" + +[portal.billing.prepaid] +capSuffix = "จาก {{total}} เครดิตแบบชำระล่วงหน้า" +expires = "หมดอายุ {{date}}" +eyebrow = "ความจุแบบชำระล่วงหน้า" +note = "ใช้ก่อนการเรียกเก็บเงินตามมิเตอร์และอยู่นอกขีดจำกัดค่าใช้จ่ายของคุณ" +topUp = "เติมเงิน" + +[portal.billing.prepaid.buy] +back = "กลับ" +calcTitle = "คำนวณการชำระเงินรายปีของคุณ" +cancel = "ยกเลิก" +cancelPurchase = "ยกเลิกการซื้อ" +close = "ปิด" +continueToPayment = "ไปต่อที่การชำระเงิน" +finalise = "ดำเนินการให้เสร็จสิ้น" +invoiceErrorTitle = "ไม่สามารถสร้างใบแจ้งหนี้ได้" +loadingTitle = "กำลังโหลดใบเสนอราคาของคุณ" +payTitle = "ชำระเงินสำหรับปีของคุณ" +step = "ขั้นตอนที่ {{current}} จาก {{total}}" + +[portal.billing.prepaid.calc] +change = "เปลี่ยน" +deployRow = "การปรับใช้" +done = "เสร็จสิ้น" +enterpriseHint = "นี่คือระดับ enterprise ที่ปริมาณนี้ อัตรา enterprise ดีกว่าส่วนลดแบบบริการตนเองใด ๆ ล็อกอัตรา เงื่อนไข และใบเสนอราคาได้ในไม่กี่นาที" +governanceRow = "การกำกับดูแล" +governanceValue = "{{label}} · {{count}} policies" +handlesLabel = "Processor ของคุณ" +handlesValue = "รองรับ {{volume}} เครดิต / เดือน" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} policies" +poolCaption = "กองเครดิตเดียวจำนวน {{credits}} เครดิตสำหรับทั้งปี เดือนที่ใช้งานหนักยืมจากเดือนที่ใช้งานเบาได้" +priceLabel = "ปีของคุณ · 12 เดือนในราคา 10 เดือน" +rateUnknown = "เราจะแสดงราคาที่แน่นอนตอนชำระเงิน" +savings = "คุณประหยัด {{amount}} · ฟรี 2 เดือน" +sizingRow = "การกำหนดขนาด" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "ผู้ใช้ทั้งหมด" + +[portal.billing.prepaid.consent] +label = "ฉันเข้าใจว่าเมื่อความจุแบบชำระล่วงหน้าของฉันถูกใช้หมดหรือหมดอายุหลังจาก 12 เดือน การประมวลผลจะดำเนินต่อโดยอัตโนมัติในอัตรามาตรฐานแบบคิดตามมิเตอร์จ่ายตามการใช้งาน (ไม่เกินขีดจำกัดค่าใช้จ่ายของฉัน) เว้นแต่ฉันจะยกเลิก และฉันสามารถยกเลิกได้ทุกเมื่อจาก billing portal" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "จัดการโดย Stirling ใช้งานได้ในไม่กี่นาที" +cloudValue = "Stirling Cloud · จัดการแล้ว" +selfhost = "Self-hosted" +selfhostDesc = "ทำงานบนโครงสร้างพื้นฐานส่วนตัว อัตราเดียวกัน" +selfhostValue = "Self-hosted · โครงสร้างพื้นฐานส่วนตัว" + +[portal.billing.prepaid.offer] +cta = "ตรวจสอบข้อเสนอ" +subtitle = "ชำระล่วงหน้าหนึ่งปีสำหรับการประมวลผล PDF และรับฟรีสองเดือน ใช้ก่อนการเรียกเก็บเงินตามมิเตอร์ และอยู่นอกขีดจำกัดค่าใช้จ่ายของคุณ" +title = "รับ 12 เดือนในราคา 10 เดือน" + +[portal.billing.prepaid.pay] +accountName = "ชื่อเจ้าของบัญชี" +accountNamePlaceholder = "เช่น Jane Smith" +companyName = "ชื่อบริษัท (ไม่บังคับ)" +companyNamePlaceholder = "เช่น Acme Inc." +downloadInvoice = "ดาวน์โหลดใบแจ้งหนี้" +payOnline = "ชำระออนไลน์" +poNumber = "หมายเลข PO (ไม่บังคับ)" +poNumberPlaceholder = "เช่น PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "ขั้นสูง" +advancedDesc = "Pipelines ขับเคลื่อนการประมวลผลส่วนใหญ่ของคุณ" +none = "ไม่มี" +noneDesc = "ยังไม่ได้เรียกใช้ pipelines เปิดได้ทุกเมื่อ" +standard = "มาตรฐาน" +standardDesc = "Pipelines บางรายการประมวลผล PDFs ที่เข้ามาซ้ำ" + +[portal.billing.prepaid.posture] +essentials = "พื้นฐาน" +essentialsDesc = "การจัดหมวดหมู่ ค่าเริ่มต้น รวมถึงการแชร์" +governed = "มีการกำกับดูแล" +governedDesc = "เพิ่มความปลอดภัยและการกำหนดเส้นทาง" +regulated = "มีการควบคุม" +regulatedDesc = "ทุกหมวดหมู่ รวมถึง Compliance, Retention, Ingestion" + +[portal.billing.prepaid.review] +download = "ดาวน์โหลดใบเสนอราคา (PDF)" +downloading = "กำลังเตรียม…" +share = "แชร์เพื่อขออนุมัติ" + +[portal.billing.prepaid.size] +compact = "กะทัดรัด" +compactDesc = "ไฟล์ต่ำกว่า 25 MB ไม่มีค่าใช้จ่ายข้อมูล" +heavy = "หนัก" +heavyDesc = "สแกนหรือมีรูปภาพจำนวนมาก โดยปกติ 50 MB+" +standard = "มาตรฐาน" +standardDesc = "ส่วนใหญ่เป็นไฟล์เล็ก มีบางสแกนเกิน 25 MB" + +[portal.billing.prepaid.state] +exhausted = "ใช้หมดแล้ว" +healthy = "เหลืออีกมาก" +low = "ใกล้หมด" [portal.billing.spendLimit] adjustLimit = "ปรับขีดจำกัด" @@ -6494,6 +6713,8 @@ label = "คาดว่าจะเกิน" [portal.billing.spendThisMonth] eyebrow = "ค่าใช้จ่ายเดือนนี้" +freeRemaining_one = "เหลือ PDF ฟรี {{formatted}} ไฟล์" +freeRemaining_other = "เหลือ PDFs ฟรี {{formatted}} ไฟล์" processed_one = "ประมวลผล {{formattedCount}} PDF แล้ว" processed_other = "ประมวลผล {{formattedCount}} PDFs แล้ว" processedWithRate_one = "ประมวลผล {{formattedCount}} PDF แล้ว ที่ {{rate}} ต่อรายการ" @@ -6510,6 +6731,7 @@ reachedTitle = "ถึงขีดจำกัดค่าใช้จ่าย title = "ไม่สามารถเปิดพอร์ทัล Stripe ได้" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "จาก {{allowance}} PDFs ฟรีถูกใช้แล้ว" capSuffix_other = "จาก {{allowance}} PDFs ฟรีถูกใช้แล้ว" eyebrow = "ทดลองใช้ Processor" @@ -6534,6 +6756,376 @@ signature = "ลายเซ็น" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "การเชื่อมต่อใหม่" +createTitleFor = "เชื่อมต่อ {{name}}" +delete = "ลบ" +edit = "แก้ไข" +editTitle = "แก้ไขการเชื่อมต่อ" +unsupportedType = "ไม่สามารถแก้ไขประเภทการเชื่อมต่อนี้ที่นี่ได้" + +[portal.connections.categories.advanced] +description = "สำหรับบริการที่เรายังไม่รองรับ" +label = "ขั้นสูง" + +[portal.connections.categories.audit] +description = "บันทึกสิ่งที่เกิดขึ้นกับเอกสาร ในที่ที่ผู้ตรวจสอบของคุณดูอยู่แล้ว" +label = "บันทึกการตรวจสอบและการปฏิบัติตามข้อกำหนด" + +[portal.connections.categories.notify] +description = "แจ้งระบบอื่นว่ามีบางอย่างเกิดขึ้น" +label = "การแจ้งเตือนและเวิร์กโฟลว์" + +[portal.connections.categories.security] +description = "สแกนหรือติดป้ายกำกับเอกสารก่อนที่จะย้ายต่อ" +label = "ความปลอดภัยและการจัดหมวดหมู่" + +[portal.connections.categories.signing] +description = "ส่งเอกสารออกไปเพื่อลงนามหรือรับรองเอกสาร" +label = "ลายเซ็นอิเล็กทรอนิกส์และการรับรองเอกสาร" + +[portal.connections.categories.storage] +description = "ปลายทางของเอกสารที่ประมวลผลแล้ว - bucket, drive, ticket หรือ page" +label = "ไฟล์และแนบ" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling จัดเก็บข้อมูลนี้แบบเข้ารหัสและจะไม่แสดงอีก" +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "ขั้นตอนที่ใช้การเชื่อมต่อนี้สามารถเรียกได้เฉพาะ paths ภายใต้ URL นี้เท่านั้น ไม่ใช่ host อื่น" +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "รหัสผ่าน" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "ชื่อผู้ใช้" + +[portal.connections.commonFields.webhookUrl] +helperText = "วาง URL ที่บริการให้คุณ สามารถเรียกได้เฉพาะ paths ภายใต้ URL นั้นเท่านั้น" +label = "Webhook URL" + +[portal.connections.fields] +name = "ชื่อการเชื่อมต่อ" +namePlaceholder = "เช่น Production" + +[portal.connections.picker] +cancel = "ยกเลิก" +createNew = "การเชื่อมต่อใหม่..." +placeholder = "เลือกการเชื่อมต่อ" +save = "บันทึกการเชื่อมต่อ" + +[portal.connections.picker2] +back = "เปลี่ยน integration" +heading = "คุณต้องการเชื่อมต่ออะไร?" +noResultsBody = "เรายังไม่รองรับ - บอกเราเกี่ยวกับสิ่งนี้ แล้วเราจะพิจารณาเพิ่ม" +noResultsTitle = "ไม่มีรายการที่ตรงกับ \"{{query}}\"" +searchPlaceholder = "ค้นหา integrations - ลอง \"sign\", \"ocr\" หรือชื่อผลิตภัณฑ์" + +[portal.connections.types.api] +description = "เรียก HTTP API ใดก็ได้จาก policy คุณเลือก URL, การยืนยันตัวตน และ payload" +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "การยืนยันตัวตน" + +[portal.connections.types.api.fields.authType.options.basic] +label = "ชื่อผู้ใช้และรหัสผ่าน" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token ใน header แบบกำหนดเอง" + +[portal.connections.types.api.fields.authType.options.none] +label = "ไม่มี" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "เข้าสู่ระบบเพื่อรับ token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "ขั้นตอนที่ใช้การเชื่อมต่อนี้สามารถเรียกได้เฉพาะ paths ภายใต้ URL นี้เท่านั้น ไม่ใช่ host อื่น" +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "ชื่อ Header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Login path" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "รหัสผ่าน" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "หาก API ตอบกลับด้วยลิงก์ดาวน์โหลด จะดึงข้อมูลได้เฉพาะ hosts เหล่านี้เท่านั้น คั่นด้วยจุลภาค เว้นว่างเพื่ออนุญาตเฉพาะ host ของ Base URL เอง" +label = "Result hosts ที่อนุญาต" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "ส่ง token เป็น header นี้" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Response header ที่การเรียกเข้าสู่ระบบส่งคืน token เช่น X-Auth-Token" +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "ชื่อผู้ใช้" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "สแกนเอกสารเพื่อหามัลแวร์บนเซิร์ฟเวอร์ที่คุณเรียกใช้ - ไม่มีสิ่งใดออกจากเครือข่ายของคุณ" +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "การสแกนไวรัสและเนื้อหาสำหรับเอกสารใน policy" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "บล็อกเอกสารที่มี macros, executables, scripts หรือ embedded objects - ไม่ใช่แค่มัลแวร์ที่รู้จัก" +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "แนบเอกสารที่ประมวลผลแล้วไปยังหน้า Confluence" +label = "Confluence" + +[portal.connections.types.consigno] +description = "ส่งเอกสารไปยัง ConsignO Cloud (Notarius) เพื่อลงลายมือชื่ออิเล็กทรอนิกส์และรับรองเอกสาร" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "สภาพแวดล้อม ConsignO Cloud ของคุณ รวมถึง /api/v1" +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "รหัสผ่าน" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "จำเป็นเฉพาะเมื่อองค์กรของคุณใช้ tenants" +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "ชื่อผู้ใช้" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "โพสต์ข้อความไปยัง Discord เมื่อ policy ประมวลผลเอกสาร" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "จัดทำดัชนี audit event ใน Elasticsearch ทุกครั้งที่ policy จัดการเอกสาร" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "ตรวจสอบ FTP หรือ FTPS server เพื่อหาเอกสารใหม่" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "โหมดการเชื่อมต่อ" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (แนะนำ)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP แบบธรรมดาส่งข้อมูลรับรองและไฟล์โดยไม่เข้ารหัส; เลือก FTPS หาก server รองรับ" +label = "การเข้ารหัส" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "ไม่มี (FTP ธรรมดา)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "โพสต์ข้อความไปยังพื้นที่ Google Chat เมื่อ policy ทำงาน" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "แนบเอกสารที่ประมวลผลแล้วไปยัง issue ของ Jira" +label = "Jira" + +[portal.connections.types.mailgun] +description = "ส่งอีเมลเอกสารหรือการแจ้งเตือนเมื่อ policy ทำงาน" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "จัดเก็บเอกสารที่ประมวลผลแล้วลงใน Nextcloud" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "ตรวจจับข้อมูลส่วนบุคคลในข้อความเอกสารโดยใช้ Presidio server ที่คุณเรียกใช้" +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "อ่านและใช้ Microsoft Purview sensitivity labels บน PDFs" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "ไม่บังคับ จำเป็นเฉพาะเพื่อเลือก labels ตามชื่อ; การติดป้ายกำกับทำงานได้แม้ไม่มี" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "ประทับลงบนทุก label เป็น SiteId เพื่อให้เครื่องมือที่รองรับ Purview รู้ว่าองค์กรใดเป็นผู้ใช้" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket ที่ sources สามารถอ่านได้และ pipeline outputs สามารถเขียนได้" +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "เว้นว่างสำหรับ AWS ตั้งค่านี้สำหรับ MinIO หรือที่เก็บอื่นที่เข้ากันได้กับ S3" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "ส่งอีเมลเอกสารหรือการแจ้งเตือนเมื่อ policy ทำงาน" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "ตรวจสอบ SFTP (SSH) server เพื่อหาเอกสารใหม่" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "ไม่บังคับ fingerprint ของ SHA-256 host key ของ server (จาก `ssh-keyscan host | ssh-keygen -lf -`) เมื่อตั้งค่า จะยอมรับเฉพาะ server ที่แสดง key นี้เท่านั้น; เมื่อเว้นว่าง key ที่เห็นในการเชื่อมต่อครั้งแรกจะถูก pin และการเปลี่ยนแปลงภายหลังจะถูกปฏิเสธ" +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "เว้นว่างหากคุณยืนยันตัวตนด้วย private key แทน" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "ไม่บังคับ วาง OpenSSH private key เพื่อยืนยันตัวตนด้วย key แทนรหัสผ่าน" +label = "Private key" + +[portal.connections.types.slack] +description = "โพสต์ข้อความไปยัง Slack เมื่อ policy ประมวลผลเอกสาร" +label = "Slack" + +[portal.connections.types.smb] +description = "เฝ้าดู SMB / CIFS network share (Windows หรือ Samba)" +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "ไม่บังคับ Windows domain หรือ workgroup หาก server ของคุณใช้อยู่" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "ส่ง audit event ไปยัง Splunk ทุกครั้งที่ policy จัดการเอกสาร" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "ส่ง audit event ไปยัง Sumo Logic HTTP collector" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "โพสต์ข้อความไปยังช่อง Teams เมื่อ policy ทำงาน" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "โพสต์เอกสารแต่ละฉบับ หรือเพียงรายละเอียด ไปยัง URL ที่คุณเลือก" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "ทริกเกอร์ Zap หรือ Make scenario จาก policy" +label = "Zapier & Make" + +[portal.docs] +browse = "เรียกดูเอกสาร" +viewSource = "ดู source บน GitHub" + [portal.docs.authentication] codeCaption = "ทุกคำขอ" eyebrow = "เริ่มต้นใช้งาน" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "ความสามารถที่รวมมาและตั้งชื่อไว้ ซึ่งเอเจนต์ของคุณเรียกใช้เป็นเครื่องมือเดียว แต่ละ skill เป็นสายการดำเนินการแบบกำหนดแน่นอนพร้อมการประเมินแนบอยู่" title = "ทักษะเอเจนต์" +[portal.docs.toc] +title = "บนหน้านี้" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "ประเภท" user = "ผู้ใช้" [portal.documents.queue.empty] +connectSource = "เชื่อมต่อ source" +createPipeline = "สร้าง pipeline" description = "เมื่อแหล่งที่มาส่งเอกสารเข้าสู่ pipelines ของคุณ เอกสารจะแสดงที่นี่เพื่อให้ตรวจสอบ" title = "ไม่มีเอกสารในคิว" @@ -6712,11 +7309,11 @@ editorAction = "Editor" empty = "ไม่มีเอกสารที่ตรงกับตัวกรองนี้" rowActions = "การดำเนินการของแถว" sensitiveLabel = "ละเอียดอ่อน" -sensitiveTitle = "ละเอียดอ่อน — ต้องมีสิทธิ์เข้าถึง" [portal.documents.table.columns] action = "Pipeline / การดำเนินการ" document = "เอกสาร" +labels = "Labels" product = "ผลิตภัณฑ์" status = "สถานะ" time = "เวลา" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "เห็นล่าสุด" region = "ภูมิภาค" status = "สถานะ" +target = "Target" version = "เวอร์ชัน" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "มุมมองนี้พบข้อผิดพลาด retry = "ลองอีกครั้ง" title = "มีบางอย่างผิดพลาดในหน้านี้" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "กลับ" +body = "ดาวน์โหลดแอป desktop หรือ self-host server" +done = "เสร็จสิ้น" +guide = "เปิดคู่มือ {{name}}" +openInBrowser = "เปิดในเบราว์เซอร์แทน" +sectionDesktop = "แอป Desktop" +sectionSelfHosted = "Self-hosted" +title = "ติดตั้ง Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "เรียกใช้ image อย่างเป็นทางการบน server ของคุณ" +note = "เปิดที่ :8080 ข้อมูลเข้าสู่ระบบเริ่มต้น: admin / stirling" +tagline = "เรียกใช้ server ใน container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "ติดตั้ง Helm chart อย่างเป็นทางการ" +note = "เพิ่ม PVC สำหรับ /configs เพื่อคงการตั้งค่าไว้" +tagline = "Deploy ไปยังคลัสเตอร์ด้วย Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "บิลด์แบบ Native สำหรับดิสทริบิวชันของคุณ" +guideBtn = "ดูตัวเลือกการติดตั้ง Linux" +note = "มีบิลด์ AppImage, .deb และ .rpm สำหรับดิสโทรทั่วไป" +tagline = "แอปเดสก์ท็อปแบบ Native สำหรับดิสโทรของคุณ" +title = "Linux" + +[portal.home.download.mac] +altLabel = "หรือผ่าน Homebrew" +detailBody = "รันตัวติดตั้ง หรือใช้ Homebrew" +downloadBtn = "ดาวน์โหลดสำหรับ macOS" +tagline = "แอปแบบ Native สำหรับ Apple silicon และ Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "รัน .jar บนเซิร์ฟเวอร์แบบ bare metal" +downloadBtn = "ดาวน์โหลด .jar" +note = "ต้องใช้ Java 21+ เปิดที่ :8080; ล็อกอินเริ่มต้น admin / stirling" +runLabel = "จากนั้นรัน" +tagline = "รัน .jar บนเซิร์ฟเวอร์แบบ bare metal" +title = "ตั้งค่าเซิร์ฟเวอร์ด้วยตนเอง" + +[portal.home.download.windows] +altLabel = "หรือผ่าน winget" +detailBody = "รันตัวติดตั้ง หรือใช้ package manager" +downloadBtn = "ดาวน์โหลดตัวติดตั้ง" +tagline = "แอปแบบ Native สำหรับ Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} ใช้งานอยู่" -invite = "เชิญเพื่อนร่วมทีม" +activeOfDeployed = "{{active}} จาก {{total}} ใช้งานอยู่ในเดือนนี้" name = "Stirling PDF Editor" open = "เปิดในเบราว์เซอร์" updated = "อัปเดตเมื่อ {{time}}" +[portal.home.editor.deploy] +finish = "เสร็จสิ้นการ deployment" +options = "ดูตัวเลือก deployment" +start = "Deploy Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "โฮสต์เอง · Docker" @@ -6823,49 +7529,6 @@ afternoon = "สวัสดีตอนบ่าย" evening = "สวัสดีตอนเย็น" morning = "สวัสดีตอนเช้า" -[portal.home.onboarding] -dismiss = "ปิดการตั้งค่า" -notStarted = "ยังไม่เริ่ม" -progress = "เสร็จแล้ว {{done}} จาก {{total}}" -title = "ตั้งค่าให้เสร็จสิ้น" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC ทั่วทั้งองค์กร, ราคาตามปริมาณที่ตกลงไว้ และการปรับใช้แบบ air-gapped" -cta = "เริ่มทดลองใช้ฟรี" -ctaQuote = "ขอใบเสนอราคา" -lead = "สำหรับพนักงาน 250+ คน" -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "ปรับใช้แอปเดสก์ท็อป ฟรีตลอดไป" -title = "ดาวน์โหลด PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "ใช้งานอยู่ {{active}} · แนะนำ {{recommended}}" -chip = "ใช้งานอยู่ {{active}}" -title = "เปิดใช้นโยบาย" - -[portal.home.onboarding.steps.sources] -blurb = "เชื่อมต่อแล้ว {{connected}} · PDF ทุกไฟล์ถูกควบคุมในตำแหน่งที่ลงจอด" -chip = "เชื่อมต่อแล้ว {{connected}}" -title = "เชื่อมต่อแหล่งที่มาของคุณ" - -[portal.home.quickActions] -subtitle = "งานสำคัญสำหรับวันนี้" -title = "การดำเนินการด่วน" - -[portal.home.quickActions.buildPipeline] -blurb = "ตัวประกอบ 3 ขั้นตอนบนไลบรารี op แบบมีชนิด" -title = "สร้าง pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agents, webhooks, โฟลเดอร์ที่เฝ้าดู" -title = "เชื่อมต่อแหล่งที่มา" - -[portal.home.quickActions.issueApiKey] -blurb = "คีย์แบบกำหนดขอบเขตพร้อม rate limits และ IP allowlist" -title = "ออก API key" - [portal.infrastructure] manageEditorDeployment = "จัดการการปรับใช้ Editor" sectionsAriaLabel = "ส่วนโครงสร้างพื้นฐาน" @@ -6878,13 +7541,9 @@ heading = "API keys" subheading = "ข้อมูลประจำตัวแบบกำหนดขอบเขตพร้อม rate limits ต่อคีย์ สิทธิ์ และ IP allowlists" [portal.infrastructure.apiKeys.card] -allowedIps = "IP ที่อนุญาต" -anyIp = "IP ใดก็ได้ (ไม่มี allowlist)" created = "สร้างเมื่อ" lastUsed = "ใช้ล่าสุด" -permissions = "สิทธิ์" -rateLimit = "Rate limit" -rateLimitValue = "{{value}} คำขอ/นาที" +revoke = "เพิกถอนคีย์" usageMonth = "การใช้งานเดือนนี้" usageToday = "การใช้งานวันนี้" @@ -6892,6 +7551,15 @@ usageToday = "การใช้งานวันนี้" description = "สร้างคีย์แบบกำหนดขอบเขตเพื่อเริ่มเรียกใช้ Stirling API" title = "ยังไม่มี API keys" +[portal.infrastructure.apiKeys.error] +load = "ไม่สามารถโหลด API keys ของคุณได้ โปรดลองอีกครั้ง" + +[portal.infrastructure.apiKeys.revoke] +body = "เพิกถอน \"{{name}}\" หรือไม่? Integration ใดที่ยังใช้คีย์นี้จะเริ่มได้รับข้อผิดพลาด 401 ทันที การดำเนินการนี้ย้อนกลับไม่ได้" +cancel = "ยกเลิก" +confirm = "เพิกถอนคีย์" +title = "เพิกถอน API key" + [portal.infrastructure.audit] filterAriaLabel = "กรองเหตุการณ์ตรวจสอบตามหมวดหมู่" heading = "Audit logs" @@ -6935,6 +7603,7 @@ all = "ทั้งหมด" auth = "Auth" config = "การกำหนดค่า" elevation = "การยกระดับสิทธิ์" +policy = "นโยบาย" processing = "การประมวลผล" security = "ความปลอดภัย" @@ -6944,19 +7613,30 @@ title = "คุณไม่มีสิทธิ์เข้าถึง" [portal.infrastructure.audit.metrics] config = "การกำหนดค่า" -elevation = "การยกระดับสิทธิ์" +policy = "นโยบาย" processing = "การประมวลผล" totalEvents = "เหตุการณ์ทั้งหมด · 24 ชม." +[portal.infrastructure.auditCatLabel] +auth = "การยืนยันตัวตน" +config = "การกำหนดค่า" +elevation = "การยกระดับสิทธิ์" +policy = "นโยบาย" +processing = "การประมวลผล" +security = "ความปลอดภัย" + +[portal.infrastructure.auditStatusLabel] +danger = "ข้อผิดพลาด" +info = "ข้อมูล" +success = "สำเร็จ" +warning = "คำเตือน" + [portal.infrastructure.createKey] cancel = "ยกเลิก" createKey = "สร้างคีย์" done = "เสร็จสิ้น" -ipAllowlistHelper = "ช่วง CIDR คั่นด้วยจุลภาค เว้นว่างเพื่ออนุญาต IP ใดก็ได้" -ipAllowlistLabel = "IP allowlist" keyNameLabel = "ชื่อคีย์" keyNamePlaceholder = "เช่น Production · ingest" -permissionsLabel = "สิทธิ์" secretKeyCaption = "Secret key" secretWarning = "จัดเก็บสิ่งนี้ใน secrets manager Stirling จัดเก็บเฉพาะ hash เท่านั้น — ไม่มีวิธีกู้คืนในภายหลัง" subtitle = "กำหนดขอบเขตคีย์ให้เหลือน้อยที่สุดเท่าที่จำเป็น คุณสามารถหมุนเวียนหรือเพิกถอนได้ทุกเมื่อ" @@ -6964,207 +7644,9 @@ subtitleCreated = "คัดลอก secret นี้ตอนนี้ — จ title = "สร้าง API key" titleCreated = "สร้างคีย์แล้ว" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/นาที" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "ปรับใช้โดย" -environment = "Environment" -product = "ผลิตภัณฑ์" -status = "สถานะ" -version = "เวอร์ชัน" -when = "เมื่อใด" - -[portal.infrastructure.deployments.recent] -heading = "การปรับใช้ล่าสุด" -subheading = "การ rollout ล่าสุดในผลิตภัณฑ์และ environments" - -[portal.infrastructure.deployments.regionColumns] -instances = "อินสแตนซ์" -latency = "เวลาแฝง" -load = "โหลด" -p99 = "P99" -region = "ภูมิภาค" -status = "สถานะ" -throughput = "Throughput" -uptime = "Uptime" -version = "เวอร์ชัน" - -[portal.infrastructure.deployments.regions] -heading = "ภูมิภาค" -subheading = "สุขภาพแบบสดสำหรับทุกภูมิภาค Stirling ที่ปรับใช้ — เวลาแฝง โหลด และเวอร์ชัน rollout" - -[portal.infrastructure.deployments.regions.empty] -description = "ภูมิภาคที่ปรับใช้จะแสดงที่นี่เมื่อ workspace ของคุณได้รับการจัดสรรแล้ว" -title = "ยังไม่มีภูมิภาคที่ปรับใช้" - -[portal.infrastructure.models] -heading = "Models" -msValue = "{{value}} ms" -subheading = "แค็ตตาล็อก model และ routing ที่ขับเคลื่อนการประมวลผลเอกสารทั่ว workspace ของคุณ" - -[portal.infrastructure.models.byom] -description = "ลงทะเบียน model แบบ on-prem หรือโฮสต์เอง และปักหมุดกับภูมิภาคสำหรับการประมวลผลที่ผูกกับถิ่นที่อยู่ของข้อมูล" -title = "นำ model ของคุณมาเอง" - -[portal.infrastructure.models.catalogue] -heading = "แค็ตตาล็อก" -sub = "Managed models ที่พร้อมใช้งานสำหรับ workspace ของคุณ พร้อมเวลาแฝงและค่าใช้จ่ายแบบสด" -subEnterprise = "Managed, bring-your-own และ on-prem models — พร้อมการปักหมุดรายภูมิภาค" - -[portal.infrastructure.models.catalogue.empty] -description = "Models ในแค็ตตาล็อกของ workspace ของคุณจะแสดงที่นี่" -title = "ไม่มี models ที่พร้อมใช้งาน" - -[portal.infrastructure.models.columns] -cost = "ค่าใช้จ่าย" -latency = "เวลาแฝง" -load = "โหลด" -model = "Model" -status = "สถานะ" -type = "ประเภท" -version = "เวอร์ชัน" - -[portal.infrastructure.models.metrics] -activeModels = "Models ที่ใช้งานอยู่" -avgLatency = "เวลาแฝงเฉลี่ย" -included = "รวมแล้ว" -monthlySpend = "ค่าใช้จ่าย model รายเดือน" - -[portal.infrastructure.models.routing] -empty = "ยังไม่ได้กำหนดค่ากฎ routing" -heading = "กฎ routing" -sub = "Model ใดจัดการแต่ละการดำเนินการ ค่าเริ่มต้นจะใช้เมื่อไม่มีกฎที่แคบกว่าตรงกัน" -subLocked = "Route การดำเนินการไปยัง models เฉพาะ — พร้อมใช้งานในแผนแบบชำระเงิน" - -[portal.infrastructure.models.routing.lockedBanner] -description = "อัปเกรดเป็น Pro เพื่อควบคุมว่า model ใดจัดการแต่ละการดำเนินการและประเภทเอกสาร" -title = "Model routing เป็นฟีเจอร์แบบชำระเงิน" - -[portal.infrastructure.models.routingColumns] -default = "ค่าเริ่มต้น" -docType = "ประเภทเอกสาร" -modelForAria = "Model สำหรับ {{operation}}" -operation = "การดำเนินการ" -routedTo = "Route ไปยัง" - -[portal.infrastructure.security.access.byok] -description = "ระบุคีย์จาก KMS ของคุณเอง Stirling เข้ารหัสด้วยคีย์นั้นแต่ยังอ่านได้" -label = "Bring your own key (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "คีย์ไม่ออกจาก KMS ของคุณ Stirling ถือเฉพาะ ciphertext" -label = "Hold your own key (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling จัดการคีย์เข้ารหัส ง่ายที่สุด — ไม่ต้องทำ key ops ฝั่งคุณ" -label = "คีย์ที่ Stirling ถือไว้" - -[portal.infrastructure.security.accessPolicy] -heading = "นโยบายการเข้าถึงเอกสาร" -subheading = "ควบคุมว่าใครสามารถถอดรหัสเอกสารที่ประมวลผลแล้วเมื่อจัดเก็บอยู่ได้" - -[portal.infrastructure.security.attestations] -heading = "การรับรองการปฏิบัติตามข้อกำหนด" -noReport = "ไม่มีรายงาน" -subheading = "สถานะการตรวจสอบตามแต่ละ framework พร้อมรายงานสำหรับ controls ที่ได้รับการรับรอง" -viewReport = "ดูรายงาน →" - -[portal.infrastructure.security.compliance] -heading = "การปฏิบัติตามข้อกำหนด" -subheading = "การรับรองและใบรับรองที่ครอบคลุมแพลตฟอร์ม Stirling" - -[portal.infrastructure.security.empty] -description = "การกำหนดค่าความปลอดภัยของ workspace ของคุณจะแสดงที่นี่" -title = "ไม่พร้อมใช้งานสถานะความปลอดภัย" - -[portal.infrastructure.security.hyokBanner] -description = "ด้วย HYOK คีย์เข้ารหัสจะไม่ออกจาก KMS ของคุณ Stirling จัดเก็บและประมวลผลเฉพาะ ciphertext ที่คุณสามารถเพิกถอนได้ทุกเมื่อ" -title = "Stirling ไม่สามารถถอดรหัสเอกสารของคุณได้" - -[portal.infrastructure.security.ipAllowlist] -empty = "ยังไม่ได้กำหนดค่าช่วง IP — อนุญาตทุก IP" -heading = "IP allowlist" -sub = "การเข้าถึง API ถูกจำกัดไว้ที่ช่วง CIDR เหล่านี้" -subLocked = "จำกัดการเข้าถึง API ให้กับช่วง IP ที่รู้จัก — พร้อมใช้งานในแผนแบบชำระเงิน" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "อัปเกรดเป็น Pro เพื่อจำกัดการเข้าถึง API ให้กับเครือข่ายเฉพาะ" -title = "IP allowlisting เป็นฟีเจอร์แบบชำระเงิน" - -[portal.infrastructure.security.ipColumns] -added = "เพิ่มเมื่อ" -addedBy = "เพิ่มโดย" -cidr = "CIDR" -label = "ป้ายกำกับ" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "การจัดการคีย์เข้ารหัส" -keyId = "ตัวระบุคีย์" -lastRotated = "หมุนเวียนล่าสุด" -rotateKey = "หมุนเวียนคีย์" -rotationPolicy = "นโยบายการหมุนเวียน" -subheading = "การดูแลคีย์ที่เข้ารหัสเอกสารเมื่อจัดเก็บอยู่ — ใครสามารถถอดรหัสได้ และคีย์หมุนเวียนอย่างไร" - -[portal.infrastructure.security.managedBanner] -description = "การดูแลคีย์แบบ bring-your-own-key (BYOK) และ hold-your-own-key (HYOK) พร้อมใช้งานใน Enterprise อัปเกรดเพื่อระบุคีย์จาก KMS ของคุณเอง" -title = "คีย์ถูกจัดการโดย Stirling ในแผนของคุณ" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "เอเชียแปซิฟิก" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR data boundary" -label = "สหภาพยุโรป" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "สหรัฐอเมริกา" - -[portal.infrastructure.security.residencyHeader] -heading = "ถิ่นที่อยู่ของข้อมูล" -subheading = "ตำแหน่งที่เอกสารถูกจัดเก็บและประมวลผล" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "ใช้แล้ว {{value}}" - -[portal.infrastructure.storage.empty] -description = "พื้นที่จัดเก็บที่เชื่อมต่อและการใช้งานจะแสดงที่นี่" -title = "ยังไม่ได้กำหนดค่าพื้นที่จัดเก็บ" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "ใช้งานอยู่" -activeRange = "0–{{value}}d" -archived = "เก็บถาวรแล้ว" -coldStorage = "cold storage" -deleted = "ลบแล้ว" -never = "ไม่เคย" -purged = "ล้างแล้ว" - -[portal.infrastructure.storage.providers] -connect = "เชื่อมต่อ" -connected = "เชื่อมต่อแล้ว" -heading = "ผู้ให้บริการที่เชื่อมต่อ" -subheading = "ตำแหน่งที่เขียน artifacts ที่ประมวลผลแล้ว" - -[portal.infrastructure.storage.retention] -heading = "การเก็บรักษา" -subheading = "ระยะเวลาที่เก็บ artifacts ก่อนการลบตาม lifecycle" -windowLabel = "หน้าต่างการเก็บรักษาเริ่มต้น" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} วัน" -days_other = "{{count}} วัน" -never = "ไม่ลบ" - -[portal.infrastructure.storage.totalUsage] -heading = "การใช้งานรวม" -progressLabel = "พื้นที่จัดเก็บที่ใช้" -subheading = "พื้นที่จัดเก็บที่ใช้รวมทุกผู้ให้บริการที่เชื่อมต่อ" +revoked = "ถูกเพิกถอน" [portal.infrastructure.tabs] apiKeys = "API Keys" @@ -7174,6 +7656,49 @@ models = "Models" security = "ความปลอดภัย" storage = "พื้นที่จัดเก็บ" +[portal.integrations] +availableHeading = "พร้อมใช้งาน" +comingSoonHeading = "เร็ว ๆ นี้" +connect = "เชื่อมต่อ" +connectionCount_one = "การเชื่อมต่อ {{count}} รายการ" +connectionCount_other = "การเชื่อมต่อ {{count}} รายการ" +customApi = "API แบบกำหนดเอง" +searchPlaceholder = "ค้นหา Integrations" +subtitle = "ทุกอย่างที่ Stirling สามารถสื่อสารด้วยได้ ข้อมูลประจำตัวจะถูกสร้างที่นี่ครั้งเดียว แล้วอ้างอิงโดยแหล่งที่มา นโยบาย และไปป์ไลน์" +title = "Integrations" +typedName = "ชื่อ {{tool}}" + +[portal.integrations.filters] +all = "ทั้งหมด" +audit = "Audit" +connected = "เชื่อมต่อแล้ว" +notify = "แจ้งเตือน" +security = "ความปลอดภัย" +signing = "การลงนาม" +storage = "พื้นที่จัดเก็บ" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "เชื่อมต่อแล้ว" + +[portal.integrations.table] +integration = "Integration" +worksWith = "ใช้งานร่วมกับ" + +[portal.integrations.worksWith] +pipelines = "ไปป์ไลน์" +policies = "นโยบาย" +sources = "แหล่งที่มา" + +[portal.legal] +draft = "{{label}} · ร่าง" +loadError = "ไม่สามารถโหลดเอกสารนี้ได้ โปรดลองอีกครั้ง" +loading = "กำลังโหลด…" +title = "เอกสารทางกฎหมาย" + [portal.nav] agent-builder = "Agent Builder" components = "Components" @@ -7182,33 +7707,17 @@ documents = "เอกสาร" editor = "Editor" home = "หน้าแรก" infrastructure = "โครงสร้างพื้นฐาน" +integrations = "Integrations" pipelines = "Pipelines" policies = "นโยบาย" procurement = "การจัดซื้อ" -settings = "การตั้งค่า" sources = "แหล่งที่มา" usage = "การใช้งานและการเรียกเก็บเงิน" users = "ผู้ใช้" -[portal.notifications] -markAllRead = "ทำเครื่องหมายว่าอ่านทั้งหมด" -title = "การแจ้งเตือน" -viewAll = "ดูทั้งหมด" - -[portal.notifications.ariaLabel] -none = "การแจ้งเตือน ไม่มีที่ยังไม่ได้อ่าน" -unread_one = "การแจ้งเตือน, ยังไม่ได้อ่าน {{count}} รายการ" -unread_other = "การแจ้งเตือน, ยังไม่ได้อ่าน {{count}} รายการ" - -[portal.notifications.count] -allRead = "อ่านทั้งหมดแล้ว" -loading = "กำลังโหลด" -new_one = "ใหม่ {{count}} รายการ" -new_other = "ใหม่ {{count}} รายการ" - -[portal.notifications.empty] -description = "ไม่มีการแจ้งเตือนใหม่" -title = "คุณอัปเดตครบแล้ว" +[portal.nav.section] +platform = "แพลตฟอร์ม PDF" +processor = "ตัวประมวลผล PDF" [portal.pipelines] subtitle = "Pipeline เอกสารอัตโนมัติทุกรายการบน backend: ลำดับการดำเนินการที่เรียงไว้เหนือชุดแหล่งที่มา ทำงานเมื่อมี trigger คลิกแถวเพื่อดูขั้นตอนและแหล่งที่มา" @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Pipeline ใหม่" [portal.pipelines.builder] -addStep = "เพิ่มเครื่องมือ" +activate = "Activate" back = "กลับไปที่ pipelines" +cannotFollow = "รับ {{produced}} ไม่ได้" +chooseAccount = "เลือกบัญชี" +chooseDestination = "เลือกปลายทาง" +chooseOperation = "เลือกสิ่งที่ขั้นตอนนี้ทำ" +chooseSource = "เลือกแหล่งที่มา" discard = "ละทิ้งการเปลี่ยนแปลง" -enabled = "เปิดใช้งาน" +inputs = "อินพุต" +inputSource = "แหล่งที่มาของอินพุต" +inputTrigger = "Trigger" keepEditing = "แก้ไขต่อ" -needsUpload = "ต้องมีไฟล์ที่อัปโหลด" +moreActions = "More actions" +needsConfiguring = "ต้องตั้งค่า" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "ไม่มีเครื่องมือที่ตรงกับการค้นหาของคุณ" -pipelineSettings = "การตั้งค่า pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "ค้นหาเครื่องมือ" -selectToolBody = "เพิ่มเครื่องมือเพื่อสร้าง pipeline ของคุณ" -selectToolTitle = "ยังไม่มีเครื่องมือ" -toolSettings = "การตั้งค่าเครื่องมือ" +sendToSystem = "ส่งไปยังระบบอื่น" +stepsIncompatible = "ขั้นตอนเหล่านี้ไม่สามารถทำงานกับผลลัพธ์จากขั้นตอนก่อนหน้าได้: {{tools}}" +stepsNeedSetup = "ขั้นตอนเหล่านี้ยังต้องตั้งค่าก่อนบันทึก: {{tools}}" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "การดำเนินการที่ไม่รู้จัก เก็บไว้ตามเดิม" unsavedBody = "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก บันทึกก่อนออก หรือจะละทิ้ง?" unsavedTitle = "การเปลี่ยนแปลงที่ยังไม่ได้บันทึก" -uploadUnsupported = "ไฟล์ที่อัปโหลดยังไม่รองรับใน pipelines ดังนั้นขั้นตอนเหล่านี้จึงไม่สามารถบันทึกได้: {{tools}}" usesDefaults = "ทำงานด้วยการตั้งค่าเริ่มต้น" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "รวมทุกไฟล์จากขั้นตอนก่อนหน้า" +fan-out = "รันหนึ่งครั้งต่อไฟล์จากขั้นตอนก่อนหน้า" +format-mismatch = "ต้องใช้ {{accepts}} แต่ขั้นตอนก่อนหน้าสร้าง {{produced}}" +output-uncertain = "อาจไม่รัน: ผลลัพธ์ของขั้นตอนก่อนหน้าขึ้นอยู่กับวิธีตั้งค่า" +source-mismatch = "ต้องใช้ {{accepts}} แต่อินพุตของไปป์ไลน์นี้คือ {{produced}}" +undeclared-operation = "ตรวจสอบไม่ได้ว่าขั้นตอนนี้รับอะไรได้บ้าง" [portal.pipelines.composer] addTool = "เพิ่มเครื่องมือ" -cancel = "ยกเลิก" -chainEmpty = "เพิ่มเครื่องมือเพื่อเริ่มสร้าง pipeline ของคุณ" create = "สร้าง pipeline" -directory = "โฟลเดอร์ผลลัพธ์" -directoryHelp = "พาธแบบสัมบูรณ์บนเซิร์ฟเวอร์ ต้องอยู่ภายในโฟลเดอร์ที่อนุญาตซึ่งกำหนดค่าไว้" +createPaused = "Create paused" editingUnsupported = "ยังไม่รองรับการแสดง params ของเครื่องมือเหล่านี้เพื่อแก้ไข" -moveDown = "ย้ายลง" -moveUp = "ย้ายขึ้น" +editSource = "Edit source" name = "ชื่อ" namePlaceholder = "เช่น Redaction sweep" -noSources = "ยังไม่มีแหล่งที่มาที่เชื่อมต่อ Pipeline ยังสามารถทำงานกับไฟล์ที่ส่งให้โดยตรงได้" noToolSettings = "เครื่องมือนี้ไม่มีการตั้งค่าที่กำหนดค่าได้" -operations_one = "การดำเนินการ ({{count}})" -operations_other = "การดำเนินการ ({{count}})" output = "ผลลัพธ์" -removeStep = "ลบการดำเนินการ" save = "บันทึกการเปลี่ยนแปลง" scheduleEvery = "รันทุก" -sources = "แหล่งที่มา" -sourcesLoading = "กำลังโหลดแหล่งที่มา..." trigger = "Trigger" triggerManual = "Manual เท่านั้น" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "วัน" hours = "ชั่วโมง" minutes = "นาที" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "ลบ \"{{name}}\" หรือไม่? การดำเนินการนี้ย้อนกลับไม่ได้" cancel = "ยกเลิก" @@ -7273,28 +7815,62 @@ confirm = "ลบ" title = "ลบ pipeline?" [portal.pipelines.detail] +clearHistory = "ล้างประวัติ" delete = "ลบ pipeline" run = "รันตอนนี้" [portal.pipelines.empty] action = "สร้าง pipeline" +connectSource = "เชื่อมต่อแหล่งที่มา" description = "สร้าง pipeline แรกของคุณ: เลือกแหล่งที่มาที่จะรัน เชื่อมโยงการดำเนินการ และเลือกตำแหน่งผลลัพธ์" title = "ยังไม่มี pipelines" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "ใช้งานอยู่" paused = "หยุดชั่วคราว" total = "Pipelines" -[portal.pipelines.output] -folder = "เขียนไปยังโฟลเดอร์" -inline = "ส่งคืนไฟล์" - [portal.pipelines.run] +allProcessed_one = "ไม่มีอะไรให้รัน: เอกสาร {{count}} รายการของแหล่งที่มาได้รับการประมวลผลแล้ว" +allProcessed_other = "ไม่มีอะไรให้รัน: เอกสารทั้งหมด {{count}} รายการในแหล่งที่มาได้รับการประมวลผลแล้ว" completed_one = "การรันเสร็จสมบูรณ์" completed_other = "การรันทั้งหมด {{count}} รายการเสร็จสมบูรณ์" empty = "ไม่มีอะไรให้รัน: แหล่งที่มาไม่มีเอกสารให้ประมวลผล" failed = "การรันล้มเหลว: {{error}}" +inFlight = "ไม่มีรายการใหม่ให้รัน: เอกสารยังคงกำลังถูกประมวลผลจากการรันก่อนหน้า" +parked_one = "ไม่มีอะไรให้รัน: เอกสาร {{count}} รายการล้มเหลวก่อนหน้านี้และถูกพักไว้ แก้ไขสาเหตุ จากนั้นล้างประวัติเพื่อลองใหม่" +parked_other = "ไม่มีอะไรให้รัน: เอกสาร {{count}} รายการล้มเหลวก่อนหน้านี้และถูกพักไว้ แก้ไขสาเหตุ จากนั้นล้างประวัติเพื่อลองใหม่" running = "เริ่มการรันแล้ว; ยังดำเนินการอยู่" timeout = "การรันใช้เวลานานกว่าที่คาดไว้; อาจยังเสร็จสิ้นในเบื้องหลังได้" @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "แหล่งที่มา" status = "สถานะ" steps = "ขั้นตอน" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "เฝ้าดูโฟลเดอร์" @@ -7314,12 +7891,18 @@ manual = "Manual" schedule = "ตามกำหนดเวลา" [portal.policies] +defaultName = "นโยบาย {{category}}" subtitle = "ระบบอัตโนมัติถาวรที่บังคับใช้ tool pipeline กับทุกเอกสาร แต่ละนโยบายจะทำงานเมื่อ upload หรือ export รัน tool chain และบันทึกเวอร์ชันที่บังคับใช้ควบคู่กับต้นฉบับ" title = "นโยบาย" [portal.policies.card] comingSoon = "อัปเกรดเป็น Enterprise" notSetUp = "ยังไม่ได้ตั้งค่า" +requiresAiEngine = "ต้องใช้ AI engine" + +[portal.policies.categories.classification] +desc = "ระบุประเภทของเอกสารแต่ละฉบับเมื่ออัปโหลด และติดแท็ก metadata เพื่อการจัดเก็บและค้นหา" +label = "การจัดประเภท" [portal.policies.categories.compliance] desc = "บังคับใช้ข้อกำหนด HIPAA, GDPR, SOC 2 หรือ FedRAMP กับทุกเอกสาร" @@ -7344,6 +7927,13 @@ label = "ความปลอดภัย" [portal.policies.config] scopeAll = "เอกสารทั้งหมด" +[portal.policies.config.classification] +summary = "จัดประเภทเอกสารที่อัปโหลดทุกฉบับตามป้ายกำกับของทีมคุณ และติดแท็ก metadata" + +[portal.policies.config.classification.rules] +0 = "จัดประเภท" +1 = "ติดแท็ก metadata" + [portal.policies.config.compliance] summary = "ตรวจสอบเอกสารกับ regulatory frameworks ก่อนออกจากระบบ" @@ -7371,6 +7961,29 @@ minConfidence = "ความเชื่อมั่นขั้นต่ำ" 2 = "ตั้งชื่อ" 3 = "ปรับให้เป็นมาตรฐาน" +[portal.policies.config.purview] +label = "ใช้ป้ายกำกับความละเอียดอ่อนของ Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "GUID ของป้ายกำกับจาก Purview portal การใช้ป้ายกำกับจะเขียน metadata ลงใน PDF; ไม่สามารถเข้ารหัสได้ ซึ่งต้องใช้ Microsoft client" +labelName = "ชื่อป้ายกำกับ (ไม่บังคับ)" +labelNameHelp = "บันทึกไว้พร้อมกับ ID เพื่อให้ผู้คนและเครื่องมือรู้จักป้ายกำกับ" + +[portal.policies.config.purview.fields.method] +help = "Purview แยกความแตกต่างระหว่างป้ายกำกับที่นโยบายใช้กับป้ายกำกับที่บุคคลเลือก" +label = "วิธีบันทึกป้ายกำกับ" +privileged = "ตามที่บุคคลเลือก" +standard = "ตามที่ใช้โดยอัตโนมัติ" + +[portal.policies.config.purviewRead] +label = "อ่านป้ายกำกับ Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "อ่านป้ายกำกับที่เอกสารมีอยู่แล้ว เพื่อให้ขั้นตอนถัดไปดำเนินการกับป้ายกำกับนั้นได้" + [portal.policies.config.retention] summary = "บังคับใช้ระยะเวลาที่เก็บเอกสาร เวลาเก็บถาวร และเวลาลบ" @@ -7438,6 +8051,7 @@ title = "ยังไม่มีกิจกรรม" [portal.policies.endpoints] addWatermark = "ลายน้ำ" autoRedact = "ปกปิด PII" +classifyAndLabel = "จัดประเภท" compressPdf = "บีบอัด" flatten = "Flatten" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "นโยบายของคุณถูกบันทึก retry = "ลองอีกครั้ง" title = "Backend ไม่พร้อมใช้งาน" +[portal.policies.operations] +change = "เปลี่ยนสิ่งที่ขั้นตอนนี้ทำ" +noResults = "ไม่มีขั้นตอนที่ตรงกัน ลองใช้ชื่อผลิตภัณฑ์ หรือ \"scan\", \"notify\", \"attach\"" +searchPlaceholder = "ค้นหาขั้นตอน - ลอง \"scan\", \"attach\" หรือชื่อผลิตภัณฑ์" + +[portal.policies.operations.bodyMode] +binary = "ไบต์ไฟล์ดิบ" +json = "ภายใน payload แบบ JSON" +multipart = "เป็นการอัปโหลดไฟล์ (multipart)" + +[portal.policies.operations.clamavScan] +description = "เซิร์ฟเวอร์ ClamAV ของคุณเองจะตรวจสอบเอกสาร ไม่มีอะไรออกจากเครือข่ายของคุณ" +label = "สแกนไวรัส (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "ปฏิเสธเอกสารที่มี macros, executables, scripts หรือ embedded objects" +label = "บล็อกเนื้อหาที่ทำงานได้" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive ตรวจสอบเอกสารและหยุดการรันหากเอกสารไม่สะอาด" +label = "สแกนไวรัส" + +[portal.policies.operations.confluenceAttach] +description = "จัดเก็บเอกสารที่ประมวลผลแล้วไว้บนหน้า" +label = "แนบไปยังหน้า Confluence" + +[portal.policies.operations.consignoSubmit] +description = "ส่งเอกสารไปยัง ConsignO Cloud เพื่อลงนาม" +label = "ส่งเพื่อลงนาม (ConsignO)" +note = "ส่งเอกสาร ยังไม่รองรับการดึงสำเนาที่ลงนามแล้ว เนื่องจากขั้นตอนไม่สามารถส่งผลลัพธ์ไปยังขั้นตอนถัดไปได้" + +[portal.policies.operations.customApiCall] +description = "กำหนดการเรียกด้วยตนเอง: path, method, body และการจัดการ response" +label = "เรียก API แบบกำหนดเอง" + +[portal.policies.operations.discordNotify] +description = "แจ้งช่องว่า นโยบายได้จัดการเอกสารแล้ว" +label = "โพสต์ข้อความไปยัง Discord" + +[portal.policies.operations.elasticIndex] +description = "บันทึกสิ่งที่นโยบายทำเป็นเอกสารที่ค้นหาได้" +label = "ทำดัชนี audit event ใน Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart เหมาะกับ API สำหรับอัปโหลด; JSON เหมาะกับ API ที่ต้องการไฟล์อยู่ภายใน payload; binary ส่งไบต์ดิบ" +label = "วิธีส่งเอกสาร" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON body ที่ส่งตามเดิม {{document.base64}} มีไฟล์; {{document.*}} และ {{run.*}} จะถูกเติมต่อเอกสาร" +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "บัญชี" + +[portal.policies.operations.fields.domain] +label = "โดเมน Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "ฟิลด์ในฟอร์มที่ API คาดว่าจะรับเอกสาร" +label = "ชื่อฟิลด์ไฟล์" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "จาก" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "ออบเจ็กต์ JSON ของคู่ชื่อ/ค่า header ข้อมูลประจำตัวของการเชื่อมต่อจะถูกเพิ่มให้คุณ" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "ส่งเป็นเนื้อหาข้อความ {{document.*}} และ {{run.*}} จะถูกเติมต่อเอกสาร" +label = "ข้อความ" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "ต่อท้าย base URL ของการเชื่อมต่อ ไม่สามารถไปถึงโฮสต์อื่นได้" +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "ตำแหน่งในไดรฟ์ที่จะเขียนไฟล์ {{document.filename}} จะถูกเติมต่อเอกสาร" +label = "Path ปลายทาง" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "รายงานจะปล่อยเอกสารไว้ตามเดิม การแทนที่จะสลับเป็นสิ่งที่ตอบกลับมา" +label = "สิ่งที่จะทำกับการตอบกลับ" + +[portal.policies.operations.fields.signerEmail] +label = "อีเมลของผู้ลงนาม" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "หัวเรื่อง" +placeholder = "ประมวลผลแล้ว: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio วิเคราะห์ข้อความนี้ ใช้ {{document.title}} หรือวางข้อความที่คุณต้องการตรวจสอบ" +label = "ข้อความที่จะตรวจสอบ" + +[portal.policies.operations.fields.to] +label = "ถึง" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "ชื่อผู้ใช้ Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "แจ้ง space ว่า นโยบายได้จัดการเอกสารแล้ว" +label = "โพสต์ข้อความไปยัง Google Chat" + +[portal.policies.operations.jiraAttach] +description = "จัดเก็บเอกสารที่ประมวลผลแล้วไว้บน issue" +label = "แนบไปยัง Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "ส่งเอกสารที่ประมวลผลแล้วเป็นไฟล์แนบ" +label = "ส่งเอกสารทางอีเมล (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "เขียนเอกสารที่ประมวลผลแล้วลงในโฟลเดอร์" +label = "อัปโหลดไปยัง Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "เซิร์ฟเวอร์ Presidio ของคุณเองรายงานข้อมูลส่วนบุคคลที่พบ" +label = "ตรวจจับข้อมูลส่วนบุคคล" +note = "Presidio อ่านข้อความ ไม่ใช่ไฟล์ ให้ข้อความที่คุณต้องการตรวจสอบ — เอกสารเองจะไม่ถูกอัปโหลด" + +[portal.policies.operations.responseMode] +replace = "แทนที่เอกสารด้วยการตอบกลับ" +report = "เก็บเอกสารไว้ บันทึกการตอบกลับ" + +[portal.policies.operations.sendgridEmail] +description = "ส่งเอกสารที่ประมวลผลแล้วเป็นไฟล์แนบ" +label = "ส่งเอกสารทางอีเมล (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "แจ้งช่องว่า นโยบายได้จัดการเอกสารแล้ว" +label = "โพสต์ข้อความไปยัง Slack" + +[portal.policies.operations.splunkEvent] +description = "บันทึกสิ่งที่นโยบายทำ ในที่ที่ผู้ตรวจสอบของคุณดูอยู่แล้ว" +label = "ส่ง audit event ไปยัง Splunk" + +[portal.policies.operations.sumologicEvent] +description = "บันทึกสิ่งที่นโยบายทำไปยัง HTTP collector" +label = "ส่ง audit event ไปยัง Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "แจ้งช่องว่า นโยบายได้จัดการเอกสารแล้ว" +label = "โพสต์ข้อความไปยัง Teams" + +[portal.policies.operations.webhookPost] +description = "ส่งตัวเอกสารไปยัง URL ที่คุณเลือก" +label = "โพสต์เอกสารไปยัง webhook" + +[portal.policies.operations.zapierNotify] +description = "ส่งรายละเอียดการรันให้ระบบอัตโนมัติของคุณ" +label = "Trigger Zap หรือ Make scenario" + [portal.policies.stats] activeFor = "ใช้งานอยู่" dataProcessed = "ข้อมูลที่ประมวลผลแล้ว" @@ -7473,6 +8282,13 @@ label = "เอกสารที่บังคับใช้แล้ว" description = "กำหนดค่าแล้วแต่ยังไม่ทำงาน" label = "หยุดชั่วคราว" +[portal.policies.table] +appliesTo = "ใช้กับ" +docs = "เอกสารที่บังคับใช้" +enforces = "บังคับใช้" +policy = "นโยบาย" +status = "สถานะ" + [portal.policies.wizard.actions] back = "ย้อนกลับ" cancel = "ยกเลิก" @@ -7480,6 +8296,10 @@ continue = "ดำเนินการต่อ" enablePolicy = "เปิดใช้นโยบาย" saveChanges = "บันทึกการเปลี่ยนแปลง" +[portal.policies.wizard.capability.classify] +desc = "ระบุประเภทของเอกสารจากป้ายกำกับของทีมคุณและติดแท็ก เพื่อให้จัดเก็บและค้นหาตามหมวดหมู่ได้" +label = "จัดประเภทเอกสาร" + [portal.policies.wizard.capability.compress] desc = "บีบอัดเอกสารให้มีขนาดไฟล์เล็กลง" label = "ลดขนาดไฟล์" @@ -7500,10 +8320,18 @@ label = "ปกปิดข้อมูลที่ละเอียดอ่ desc = "ลบ JavaScript ที่ซ่อนอยู่เพื่อไม่ให้มีสิ่งใดทำงานอัตโนมัติเมื่อเปิดเอกสาร" label = "ลบเนื้อหาที่ทำงานได้" +[portal.policies.wizard.capability.timestampPdf] +desc = "พิสูจน์ว่าเอกสารมีอยู่ในรูปแบบนี้อย่างแน่นอน ณ เวลาหนึ่ง โดยใช้หน่วยงาน timestamp อิสระ ส่งเฉพาะ hash - เอกสารไม่ออกจากเซิร์ฟเวอร์ของคุณ" +label = "เพิ่ม timestamp ที่เชื่อถือได้" + [portal.policies.wizard.capability.watermark] desc = "ประทับเครื่องหมายที่มองเห็นได้ (เช่น “Confidential”) บนทุกหน้า" label = "ใส่ลายน้ำ" +[portal.policies.wizard.classification] +description = "เอกสารที่อัปโหลดทุกฉบับจะถูกจัดประเภทตามป้ายกำกับในตัวและติดแท็กด้วยประเภทที่เหมาะสม ชุดป้ายกำกับนี้ใช้ร่วมกันทั้งทีมของคุณ" +labelsHeading = "ป้ายกำกับการจัดประเภท" + [portal.policies.wizard.errors] noTools = "เปิดใช้อย่างน้อยหนึ่งเครื่องมือในเวิร์กโฟลว์ก่อน" saveFailed = "ไม่สามารถบันทึกนโยบายได้ โปรดลองอีกครั้ง" @@ -7548,73 +8376,118 @@ setUp = "ตั้งค่านโยบาย {{category}}" [portal.policies.wizard.workflow] description = "เลือกว่านโยบายนี้จะทำอะไรกับทุกเอกสารที่ประมวลผล" -[portal.policySummary] -activeSummary = "ใช้งานอยู่ {{active}} / {{total}}" -noRule = "ยังไม่มีกฎที่บังคับใช้" -subtitle = "ระบบอัตโนมัติถาวรที่ทุกเอกสารต้องผ่าน ไม่ว่าจะถูกจัดการโดย pipeline ใด" -title = "สิ่งที่ทำงานกับ PDF ของคุณ" - [portal.policySummary.action] -comingSoon = "อัปเกรดเป็น Enterprise" -configure = "กำหนดค่า" setUp = "ตั้งค่า" -[portal.policySummary.column] -activeRule = "กฎที่ใช้งานอยู่" -policy = "นโยบาย" -status = "สถานะ" +[portal.processorFlow] +footnote = "จำนวนเป็นของ 24 ชั่วโมงล่าสุด ความเร็วของ Flow เป็นเพียงภาพประกอบ" +liveBadge = "Live" +stats = "เชื่อมต่อ {{connected}} รายการ · ประมวลผล PDF แล้ว {{processed}} รายการ" +title = "ตัวประมวลผล PDF" -[portal.policySummary.empty] -description = "เมื่อกำหนดค่านโยบายแล้ว หมวดหมู่จะปรากฏที่นี่" -title = "ยังไม่มีนโยบาย" +[portal.processorFlow.lens] +ariaLabel = "โหมดการดู" +flow = "Flow" +sankey = "Sankey" -[portal.policySummary.state] -active = "ใช้งานอยู่" -off = "ปิด" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 ชม." +failed = "ล้มเหลว" +heading = "ผลลัพธ์" +success = "ส่งสำเร็จ" + +[portal.processorFlow.policies] +activeCount = "ใช้งานอยู่ {{n}} รายการ" +count = "{{n}} · 24 ชม." +heading = "นโยบาย" +setUp = "ตั้งค่า" soon = "เร็ว ๆ นี้" -[portal.processingStatus] -managePlan = "จัดการแผน" -volumeSuffix = "PDFs ที่ประมวลผล · 30 วันที่ผ่านมา" +[portal.processorFlow.sankey] +waist = "นโยบาย · ใช้งานอยู่ {{n}} รายการ" + +[portal.processorFlow.sankey.empty] +description = "เชื่อมต่อแหล่งที่มาและเปิดนโยบายเพื่อดูเอกสารไหลผ่านตัวประมวลผล" +title = "ยังไม่มี Flow" + +[portal.processorFlow.sources] +comingSoonTag = "เชื่อมต่อ" +editor = "Stirling PDF Editor" +heading = "แหล่งที่มา" +perDay = "{{n}} / 24 ชม." + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "รับ Email" [portal.procurement] -reset = "รีเซ็ตการจัดซื้อ (เดโม)" subtitle = "ให้ทีมของคุณได้รับการประเมิน ทำสัญญา และเริ่มใช้งาน ทุกเอกสารอยู่ในที่เดียว" title = "การจัดซื้อ" -[portal.procurement.action] -download = "ดาวน์โหลด" -pay = "ชำระเงินตอนนี้" -request = "ขอ" -sign = "ตรวจสอบและลงนาม" -upload = "อัปโหลด" - [portal.procurement.agreement] agreeCta = "ยอมรับและสมัครใช้งาน" +confidential = "ลับ" confirm = "ฉันได้อ่านและยอมรับ Stirling Enterprise Agreement แล้ว" -eyebrow = "ข้อตกลง" -intro = "ข้อตกลงรวมฉบับเดียวครอบคลุมดีลของคุณ: Master Service Agreement, Order Form, EULA และ Data Processing Agreement ตรวจสอบ จากนั้นยอมรับเพื่อรับใบเสนอราคาเข้าสู่การสมัครใช้งานที่มีข้อผูกพัน" -title = "ตรวจสอบข้อตกลง enterprise ของคุณ" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA และ DPA รวมไว้ในการลงนามครั้งเดียว" +download = "ดาวน์โหลด" +downloadDraftError = "ไม่สามารถสร้าง PDF ข้อตกลงได้ - ตัวเรนเดอร์เอกสารไม่พร้อมใช้งาน โปรดลองอีกครั้งในอีกสักครู่หรือติดต่อฝ่ายสนับสนุน" +downloadError = "ไม่สามารถดาวน์โหลดข้อตกลงที่ลงนามแล้วได้ โปรดลองอีกครั้ง" +legalName = "ชื่อนิติบุคคล" +legalNamePlaceholder = "นิติบุคคลที่จะลงนาม" +loadError = "ไม่สามารถโหลดข้อตกลงได้ โปรดลองอีกครั้ง" +loading = "กำลังโหลดข้อตกลง..." +ref = "อ้างอิง {{ref}} · {{version}}" +requestChanges = "ขอเปลี่ยนแปลง" +scrollHint = "เลื่อนเพื่อตรวจทาน" +signatory = "ชื่อผู้ลงนาม" +signatoryPlaceholder = "ชื่อเต็ม" +signatoryTitle = "ตำแหน่งผู้ลงนาม" +signatoryTitlePlaceholder = "เช่น General Counsel" +signError = "ไม่สามารถบันทึกลายเซ็นของคุณได้ โปรดลองอีกครั้ง" [portal.procurement.builder] addons = "ส่วนเสริม" +addressLine1 = "ที่อยู่บรรทัด 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "ที่อยู่บรรทัด 2" +addressLine2Placeholder = "ห้องชุด, ชั้น, อาคาร" back = "ย้อนกลับ" businessName = "ชื่อธุรกิจ" businessNamePlaceholder = "บริษัทของคุณ" +city = "เมือง" +cityPlaceholder = "San Francisco" +completeRequired = "โปรดกรอกฟิลด์ที่จำเป็น (ทำเครื่องหมาย *) พร้อมอีเมลที่ถูกต้องก่อนสร้างใบเสนอราคา" +contactEmail = "อีเมลติดต่อ" +contactEmailPlaceholder = "jane@acme.com" +contactName = "ชื่อผู้ติดต่อ" +contactNamePlaceholder = "Jane Doe" continue = "ดำเนินการต่อ" -country = "ประเทศ" -countryEuro = "Eurozone (EUR €)" -countryUK = "United Kingdom (GBP £)" -countryUS = "United States (USD $)" +done = "เสร็จสิ้น" eula = "ฉันได้อ่านและยอมรับ Stirling Enterprise EULA แล้ว ซึ่งควบคุมข้อตกลงที่สร้างจากใบเสนอราคานี้" generate = "สร้างใบเสนอราคา" -included = "รวมแล้ว" indemnification = "การชดเชยความเสียหายด้าน IP" indemnificationSub = "เราปกป้องการเรียกร้องด้าน IP ที่เข้าเกณฑ์ ตาม EULA" -offlineLicense = "licence แบบออฟไลน์ / air-gapped" -offlineLicenseSub = "ไฟล์ licence ที่ดาวน์โหลดได้สำหรับ instance แบบ self-hosted ที่เป็น air-gapped" +paperEyebrow = "ใบเสนอราคาสำหรับ Enterprise" +paperFor = "จัดเตรียมสำหรับ" +pdfSize = "ขนาด PDF" +poNumber = "หมายเลข PO" +poNumberPlaceholder = "ไม่บังคับ" +postalCode = "รหัสไปรษณีย์" +postalCodePlaceholder = "94105" +posture = "การกำกับดูแล" +posture_count = "~{{count}} นโยบาย" +posture_essentials = "Essentials" +posture_essentialsSub = "การจัดประเภท + การแชร์ — ค่าเริ่มต้น" +posture_governed = "Governed" +posture_governedSub = "เพิ่มความปลอดภัยและการกำหนดเส้นทาง" +posture_regulated = "Regulated" +posture_regulatedSub = "ทุกหมวดหมู่ — การปฏิบัติตามข้อกำหนด, การเก็บรักษา, การนำเข้า" qbr = "การทบทวนธุรกิจรายไตรมาส" qbrSub = "SE ของคุณจะทบทวนการใช้งานและ roadmap ทุกไตรมาส" +region = "รัฐ / ภูมิภาค" +regionPlaceholder = "California" running = "{{annual}} / ปี · {{years}} ปี {{tcv}}" s1Sub = "ทีมของคุณ และ PDFs ที่คุณคาดว่าจะเรียกใช้ในแต่ละปี" s1Title = "คุณจะประมวลผลมากแค่ไหน?" @@ -7623,6 +8496,12 @@ s2Title = "ข้อผูกพันและบริการ" s3Sub = "สำหรับใบเสนอราคาและข้อตกลงที่สร้างขึ้น" s3Title = "รายละเอียดของคุณ" serviceLevel = "ระดับบริการ" +size_compact = "Compact" +size_compactSub = "ส่วนใหญ่เป็นข้อความ ต่ำกว่า 1 MB" +size_heavy = "Heavy" +size_heavySub = "สแกนหรือมีรูปภาพจำนวนมาก, 5 MB+" +size_standard = "Standard" +size_standardSub = "ข้อความและรูปภาพผสมกัน, 1 ถึง 5 MB" slDedicated = "เฉพาะทาง" slDedicatedSub = "4 ชั่วโมงทำการ · ผู้จัดการบัญชีเฉพาะ · +30%" slPriority = "ลำดับความสำคัญ" @@ -7630,6 +8509,8 @@ slPrioritySub = "ภายในวันทำการเดียวกัน slStandard = "มาตรฐาน" slStandardSub = "วันทำการถัดไป · CSM ร่วม · รวมแล้ว" stepOf = "ขั้นตอน {{n}} จาก {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "ไม่บังคับ" term = "ระยะเวลา" termDiscount = "ใช้ส่วนลดข้อผูกพันหลายปี {{pct}}% แล้ว" title = "สร้างใบเสนอราคาของคุณ" @@ -7637,6 +8518,7 @@ training = "การเริ่มใช้งานและการฝึ trainingSub = "เซสชันสดเพื่อให้ทีมของคุณเริ่มทำงานได้" users = "ผู้ใช้ทั้งหมด" usersPlaceholder = "เช่น 250" +viewEula = "อ่าน" volEstimated = "ประมาณการจากผู้ใช้ {{count}} คน (~2,000 PDFs ต่อคน รวม automation) แก้ไขได้หากคุณมีตัวเลขที่ดีกว่า" volManual = "ใช้ตัวเลขของคุณ ประมาณการใหม่จากขนาดทีมของคุณได้ทุกเมื่อ" volNoUsers = "ไม่แน่ใจ? ป้อนขนาดทีมของคุณ แล้วเราจะประมาณการให้" @@ -7645,55 +8527,55 @@ volumePlaceholder = "เช่น 1,000,000" years_one = "{{count}} ปี" years_other = "{{count}} ปี" -[portal.procurement.docs] -count_one = "{{count}} เอกสาร" -count_other = "{{count}} เอกสาร" -done = "เสร็จสิ้น" -here = "คุณอยู่ที่นี่" -hide = "ซ่อน" -optional = "ไม่บังคับ" -paidAddon = "ส่วนเสริมแบบชำระเงิน" -show = "แสดง" -subtitle = "ทุกสิ่งที่คุณต้องใช้ในแต่ละขั้นตอนของเส้นทาง จะแสดงขึ้นเมื่อดีลดำเนินไป" -supportingSubtitle = "SOC 2, การทบทวนความปลอดภัย, แบบฟอร์มภาษี และอื่น ๆ พร้อมเมื่อทีมความปลอดภัยหรือจัดซื้อของคุณร้องขอ บางรายการมีค่าธรรมเนียมครั้งเดียว" -supportingTitle = "สนับสนุนการประเมินของคุณ" +[portal.procurement.documents] +agreement = "ข้อตกลง Enterprise" +agreementSub = "MSA, Order Form และ DPA ในการลงนามครั้งเดียว" +download = "ดาวน์โหลด" +eula = "EULA & Commercial Terms" +eulaSub = "เงื่อนไขซอฟต์แวร์และการค้าแบบ self-serve" +invoice = "ใบแจ้งหนี้" +invoiceSub = "ใบแจ้งหนี้การสมัครสมาชิกฉบับแรกของคุณ" +laterInvoice = "พร้อมใช้งานหลังจากคุณยอมรับ" +laterQuote = "พร้อมใช้งานเมื่อออกใบเสนอราคาของคุณแล้ว" +quote = "ใบเสนอราคา" +quoteSub = "ใบเสนอราคา Enterprise แบบแยกรายการของคุณ" +sla = "SLA Exhibit" +slaSub = "ระดับการสนับสนุนและเป้าหมายเวลาตอบกลับ" +subprocessors = "ผู้ประมวลผลย่อย" +subprocessorsSub = "บุคคลที่สามที่ประมวลผลข้อมูลในนามของคุณ" +subtitle = "เอกสารข้อตกลงของคุณ พร้อมใช้งานได้ทุกเมื่อ" title = "เอกสาร" -upcoming = "กำลังจะมาถึง" +view = "ดู" [portal.procurement.error] title = "มีบางอย่างผิดพลาด" [portal.procurement.hero] -company = "ดีล enterprise ของคุณ" +barAria = "ขั้นที่ {{current}} จาก {{total}}" ctaAgreement = "ตรวจสอบและลงนามข้อตกลง" -ctaLive = "คุณใช้งานจริงแล้ว" -ctaPayment = "เพิ่มการชำระเงิน" +ctaExploring = "ตั้งค่าการทดลองใช้ของคุณ" ctaQuote = "ตรวจสอบใบเสนอราคาของคุณ" +ctaReviewQuote = "ตรวจทานใบเสนอราคา" ctaTrial = "สร้างใบเสนอราคาของคุณ" +documents = "เอกสาร" eyebrow = "การจัดซื้อ Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "เชิญเพื่อนร่วมทีม" -keyDocs = "เอกสารสำคัญ" -nextStep = "ขั้นตอนถัดไป: {{action}}" -notStarted = "ยังไม่เริ่ม" +licenseKey = "คีย์ใบอนุญาต" +liveSub = "องค์กรของคุณได้รับการจัดเตรียมแล้ว และใบอนุญาตของคุณใช้งานอยู่" +liveTitle = "คุณใช้งานจริงบน Stirling Enterprise แล้ว" +next = "ถัดไป: {{stage}}" open = "เปิดการจัดซื้อ" scheduleCall = "นัดหมายการโทร" -setup1Sub = "เชิญเพื่อนร่วมทีมของคุณ" -setup1Title = "ปรับใช้ PDF Editor" -setup2Sub = "เปิดการประมวลผลใน editor และแหล่งที่มาอื่น ๆ" -setup2Title = "เชื่อมต่อ PDF Processor" -setup3Sub = "เปิด Security, Compliance, Routing หรือ Retention เมื่อคุณต้องการ" -setup3Title = "เพิ่มนโยบายที่แนะนำ" +sentenceAgreement = "ตรวจทานและลงนามข้อตกลง" +sentenceLive = "การจัดเตรียมและการเปิดใช้งาน" +sentencePayment = "ใบสั่งซื้อและการชำระเงิน" +sentenceQuote = "ตรวจทานและยอมรับใบเสนอราคาของคุณ" +sentenceTrial = "ตั้งค่าพื้นที่ทำงานทดลองใช้ของคุณ" [portal.procurement.journey] daysLeft_one = "เหลือ {{count}} วัน" daysLeft_other = "เหลือ {{count}} วัน" -engineerLabel = "solutions engineer ของคุณ" -eyebrow = "การเปิดใช้งานของคุณ" -live = "คุณใช้งานจริงบน Stirling Enterprise แล้ว" -nextStep = "ขั้นตอนถัดไป: {{action}}" -subtitle = "solutions engineer ของคุณอยู่ในทุกขั้นตอน ดำเนินการถัดไปทีละอย่าง; รายการตรวจสอบทั้งหมดอยู่ด้านล่าง" -title = "จาก trial ถึงใช้งานจริง ในเส้นทางที่มีคำแนะนำเดียว" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "ลายเซ็นเดียวครอบคลุม MSA, order form, EULA และ DPA" @@ -7727,6 +8609,9 @@ downloadError = "ยังไม่สามารถสร้างไฟล์ downloadOffline = "ดาวน์โหลด licence ออฟไลน์ (.lic)" hint = "วางคีย์นี้ลงใน instance แบบ self-hosted เพื่อเปิดใช้งาน หรือเก็บไว้เป็นบันทึกของคุณ เก็บรักษาให้ปลอดภัย" label = "คีย์ licence ของคุณ" +subtitle = "เปิดใช้งานอินสแตนซ์แบบ self-hosted ด้วยคีย์นี้ หรือเก็บไว้เป็นบันทึกของคุณ" +title = "คีย์ใบอนุญาตของคุณ" +trialFileHint = "นี่คือไฟล์ใบอนุญาตทดลองใช้ของคุณ เมื่อข้อตกลงของคุณพร้อมแล้ว ให้กลับมาดาวน์โหลดอีกครั้ง — ไฟล์ .lic จะไม่อัปเดตเอง ต่างจากคีย์ใบอนุญาตออนไลน์" [portal.procurement.link] cta = "เชื่อมโยงบัญชี" @@ -7739,65 +8624,62 @@ description = "การสมัครใช้งานของคุณใ eyebrow = "ใช้งานจริง" title = "คุณใช้งานจริงบน Stirling Enterprise แล้ว" -[portal.procurement.locked] -description = "trial keys, ใบเสนอราคาตามปริมาณที่มีข้อผูกพัน, ข้อตกลงลายเซ็นเดียว, การชำระเงิน และบัญชีเอกสารของคุณทั้งหมดอยู่ที่นี่เมื่อคุณเริ่มการประเมิน enterprise" -eyebrow = "Enterprise เท่านั้น" -talkToSales = "คุยกับฝ่ายขาย" -title = "เส้นทางการจัดซื้อเปิดด้วย Enterprise" - [portal.procurement.milestone] -accept = "ยอมรับและดำเนินการต่อ" -description = "ดาวน์โหลด PDF เพื่อแชร์กับทีมของคุณ กลับมายอมรับเมื่อคุณพร้อม หรือทำการเปลี่ยนแปลง" -download = "ดาวน์โหลด PDF" downloadError = "ยังไม่สามารถดาวน์โหลด PDF ใบเสนอราคาได้ — โปรดลองอีกครั้งในอีกสักครู่" -edit = "แก้ไขใบเสนอราคา" -eyebrow = "ใบเสนอราคา {{number}}" -perYear = " / ปี" -preparedFor = "จัดทำสำหรับ {{company}}" -tcv = "มูลค่าสัญญารวม {{value}}" -title = "ใบเสนอราคาของคุณพร้อมแล้ว" - -[portal.procurement.modal] -cancel = "ยกเลิก" -chooseFile = "เลือกไฟล์" -close = "ปิด" -downloadBody = "การดาวน์โหลดของคุณจะเริ่มในไม่ช้า" -downloadCta = "ดาวน์โหลด" -downloadTitle = "ดาวน์โหลด" -noFile = "ไม่ได้เลือกไฟล์" -payBody = "ชำระสัญญาที่มีข้อผูกพันของคุณด้วยบัตรหรือโอนเงินผ่านธนาคารผ่าน Stripe workspace ของคุณจะถูกจัดเตรียมทันทีที่การชำระเงินเคลียร์" -payCta = "ดำเนินการต่อไปยัง Stripe" -payTitle = "ยืนยันการชำระเงิน" -requestBodyFree = "เราสร้างสิ่งนี้ตามคำขอ ยืนยันแล้ว solutions engineer ของคุณจะส่งให้ในไม่ช้า" -requestBodyPaid = "นี่คือส่วนเสริมแบบชำระเงิน ยืนยันแล้ว solutions engineer ของคุณจะกำหนดขอบเขตและส่งเอกสารให้" -requestCta = "ขอ" -requestTitle = "ขอเอกสารนี้" -signBody = "เปิด Stirling Enterprise Agreement สำหรับ e-signature: ลายเซ็นเดียวครอบคลุม MSA, order form, EULA และ DPA เราจะลงนามคู่สัญญาโดยอัตโนมัติและคุณจะไปต่อยังการชำระเงิน" -signCta = "เปิดเพื่อลงนาม" -signTitle = "ตรวจสอบและลงนามข้อตกลงของคุณ" -uploadBody = "ส่ง PO ของคุณให้เรา แล้วเราจะออกใบแจ้งหนี้ตามเงื่อนไขของคุณ ลาก PDF เข้ามาหรือเลือกไฟล์ด้านล่าง" -uploadCta = "อัปโหลดใบสั่งซื้อ" -uploadTitle = "อัปโหลดใบสั่งซื้อของคุณ" [portal.procurement.payment] description = "ใบเสนอราคาของคุณได้รับการยอมรับแล้วและ licence ของคุณใช้งานอยู่แล้ว — ทีมของคุณเริ่มได้ทันที ชำระใบแจ้งหนี้แรกเมื่อคุณพร้อม; คุณสามารถชำระหรือดาวน์โหลดได้ที่นี่ ไม่ต้องใช้อีเมล" +downloadAgreement = "ดาวน์โหลดข้อตกลงที่ลงนามแล้ว" downloadInvoice = "ดาวน์โหลดใบแจ้งหนี้" -simulate = "จำลองว่าได้รับการชำระเงินแล้ว (เดโม)" +eyebrow = "การชำระเงิน" title = "สร้างการสมัครใช้งานแล้ว" viewInvoice = "ดูและชำระใบแจ้งหนี้" +[portal.procurement.review] +acceptCta = "ยอมรับใบเสนอราคา" +annual = "ค่าธรรมเนียมรายปี (ปีที่ 1)" +downloadCta = "ดาวน์โหลดใบเสนอราคา" +poNumber = "ใบสั่งซื้อ: {{po}}" +renewal = "ต่ออายุที่ {{amount}}/ปี หลังหมดระยะเวลา (+{{pct}}% CPI)" +tcv = "ระยะเวลา {{years}} ปี ชำระเต็มจำนวนล่วงหน้า: {{tcv}}" +validUntil = "ใช้ได้ถึง {{date}}" + [portal.procurement.schedule] fallback = "ไม่สามารถโหลดตัวจัดตารางเวลาได้" fallbackLink = "เปิดการจัดตารางเวลาในแท็บใหม่" +loading = "กำลังโหลด scheduler..." subtitle = "solutions engineer ของคุณจะแนะนำทีมของคุณตลอดการเปิดใช้งาน เลือกเวลาที่เหมาะกับคุณ" title = "นัดหมายการโทร" -[portal.procurement.status] -action = "ต้องดำเนินการ" -available = "พร้อมใช้งาน" -complete = "เสร็จสมบูรณ์" -pending = "รอดำเนินการ" -request = "ตามคำขอ" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "ปิดแยก ไม่มีการเชื่อมต่อขาออก" +back = "ย้อนกลับ" +businessName = "ชื่อธุรกิจ" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "จัดการให้ เริ่มต้นเร็วที่สุด" +continue = "ดำเนินการต่อ" +deployment = "ควร deploy ที่ไหน?" +eula = "ฉันยอมรับ Stirling EULA & Commercial Terms" +fullName = "ชื่อเต็ม" +fullNamePlaceholder = "ชื่อของคุณ" +invites = "เชิญเพื่อนร่วมทีม · ไม่บังคับ" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "นัดหมายการโทร" +seats = "ผู้ทดสอบ" +seatsPlaceholder = "เช่น 15" +selfhost = "Self-hosted" +selfhostSub = "Docker หรือ Kubernetes" +start = "เริ่มทดลองใช้" +stepOf = "ขั้นตอน {{n}} จาก {{total}}" +subtitle = "ฟรี 14 วัน ไม่ต้องใช้บัตร" +subtitleDetails = "รายละเอียดเล็กน้อยสำหรับใบเสนอราคาและข้อตกลงของคุณ" +talkFirst = "ต้องการพูดคุยก่อนหรือไม่?" +title = "ตั้งค่าการทดลองใช้ของคุณ" +viewEula = "อ่าน" +workEmail = "อีเมลที่ทำงาน" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "การขยายเวลาจะเพิ่ม 7 วันและแจ้ง solutions engineer ของคุณ" @@ -7808,109 +8690,113 @@ maxed = "ถึงขีดจำกัดแล้ว" subtitle = "trial ฟรีของคุณใช้งานได้ถึง {{date}} ไม่ต้องใช้บัตร" title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "ราคาตามปริมาณที่มีข้อผูกพัน, SSO + SCIM + RBAC ทั่วทั้งองค์กร, audit ที่แก้ไขไม่ได้ 90 วัน และ SE เฉพาะ" -homeCta = "เริ่ม Trial →" -homeHeadline = "ประมวลผล PDFs หลายล้านไฟล์" - -[portal.recentActivity] -title = "กิจกรรมล่าสุด" -viewAll = "ดูทั้งหมด" - -[portal.recentActivity.empty] -description = "การเรียกใช้ pipeline, การ deploy และเหตุการณ์ agent จะปรากฏที่นี่" -title = "ยังไม่มีอะไรที่นี่" - -[portal.search] -ariaLabel = "ค้นหา" -placeholder = "ค้นหา Stirling — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "การดำเนินการด่วนจะปรากฏที่นี่เมื่อพร้อมใช้งาน" -noActionsTitle = "ไม่มีการดำเนินการด่วน" -noMatches = "ไม่พบรายการที่ตรงกับ \"{{query}}\"" -noMatchesDescription = "ลองใช้คำค้นอื่นหรือเรียกดู catalogue" - [portal.settings.groups] admin = "Admin" [portal.settings.sections] account-link = "การเชื่อมโยงบัญชี" -[portal.shell.header] -accountFallback = "บัญชี" -accountMenu = "เมนูบัญชี" -darkMode = "โหมดมืด" -lightMode = "โหมดสว่าง" -search = "ค้นหา" -searchPlaceholder = "ค้นหา…" -signOut = "ออกจากระบบ" -switchToDark = "เปลี่ยนเป็นธีมมืด" -switchToLight = "เปลี่ยนเป็นธีมสว่าง" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} เอกสาร" linkAccount = "เชื่อมโยงบัญชี Stirling" -planEditor = "แผน Editor" -planEnterprise = "แผน Enterprise" -planProcessor = "แผน Processor" primaryNav = "การนำทางหลัก" switchApp = "สลับแอป" +[portal.shell.topbar] +closeNav = "ปิดการนำทาง" +openNav = "เปิดการนำทาง" +search = "ค้นหา" + [portal.sources] subtitle = "การเชื่อมต่ออินพุตที่ใช้ซ้ำได้ซึ่งส่งเอกสารเข้าสู่ Stirling กำหนดค่าการเชื่อมต่อครั้งเดียว แล้วอ้างอิงจากนโยบายกี่รายการก็ได้ คลิกแถวเพื่อดูการกำหนดค่าและนโยบายที่ใช้งาน" title = "แหล่งที่มา" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "เชื่อมต่อแหล่งที่มา" +[portal.sources.builder] +back = "กลับไปยังแหล่งที่มา" +backToTypes = "ประเภทแหล่งที่มาทั้งหมด" +cancel = "ยกเลิก" +chooseHint = "เลือกแหล่งที่มาของเอกสาร คอนเน็กเตอร์ที่เป็นสีเทากำลังจะมา" +comingSoon = "เร็ว ๆ นี้" +comingSoonHeading = "เร็ว ๆ นี้" +create = "สร้างแหล่งที่มา" +createTitle = "เชื่อมต่อแหล่งที่มา" +delete = "ลบ" +editTitle = "แก้ไขแหล่งที่มา" +enabled = "เปิดใช้งาน" +save = "บันทึกการเปลี่ยนแปลง" + +[portal.sources.builder.folderAccess] +description = "ระบบอัตโนมัติของโฟลเดอร์สามารถใช้ได้เฉพาะโฟลเดอร์ที่ผู้ดูแลระบบอนุญาต เพิ่มในส่วนการตั้งค่าการเข้าถึงโฟลเดอร์ แล้วลองอีกครั้ง" +openSettings = "การตั้งค่าการเข้าถึงโฟลเดอร์" +title = "โฟลเดอร์นี้ไม่ได้รับอนุญาต" + [portal.sources.delete] body = "ลบ \"{{name}}\" หรือไม่? การดำเนินการนี้ไม่สามารถย้อนกลับได้ นโยบายที่อ้างอิงถึงรายการนี้จะต้องได้รับการอัปเดต" cancel = "ยกเลิก" confirm = "ลบ" title = "ลบแหล่งที่มาหรือไม่?" -[portal.sources.detail] -closeAriaLabel = "ปิดรายละเอียด" -delete = "ลบแหล่งที่มา" -docs24h = "24 ชม. ล่าสุด" -docs30d = "30 วันที่ผ่านมา" -docsTotal = "พบทั้งหมด" -docsTrend = "เอกสารในช่วง 30 วันที่ผ่านมา" -documents = "เอกสาร" -edit = "แก้ไข" -notReferenced = "ไม่มีนโยบายใดอ้างอิงถึง จึงลบได้อย่างปลอดภัย" -pause = "หยุดชั่วคราว" -resume = "ทำต่อ" -subtitle = "{{type}} · {{status}}" -usedBy = "ใช้งานโดย" - -[portal.sources.empty] -description = "เชื่อมต่อโฟลเดอร์ (และเร็ว ๆ นี้ cloud storage) เพื่อให้นโยบายของคุณมีที่สำหรับดึงเอกสาร" -title = "ยังไม่มีแหล่งที่มาที่เชื่อมต่อ" - [portal.sources.kpi] inUse = "กำลังใช้งาน" total = "การเชื่อมต่อ" unused = "ไม่ได้ใช้งาน" +[portal.sources.networkFields.connection] +helperText = "การเชื่อมต่อที่จัดเก็บไว้พร้อมที่อยู่เซิร์ฟเวอร์และข้อมูลประจำตัว ใช้ซ้ำโดยทุกแหล่งที่มาที่อ้างอิงถึง" +label = "การเชื่อมต่อ" + +[portal.sources.networkFields.directory] +helperText = "โฟลเดอร์บนเซิร์ฟเวอร์ที่จะตรวจสอบ สัมพันธ์กับโฮมของการเข้าสู่ระบบหรือรูทของแชร์ เว้นว่างสำหรับรูท" +label = "โฟลเดอร์" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume จะลบแต่ละไฟล์ออกจากเซิร์ฟเวอร์เมื่อทุกนโยบายประมวลผลแล้ว" +label = "โหมดการอ่าน" + +[portal.sources.networkFields.mode.options] +consume = "Consume: ประมวลผลแต่ละไฟล์ครั้งเดียว" +snapshot = "Snapshot: อ่านโฟลเดอร์ซ้ำทุกครั้งที่รัน" + +[portal.sources.networkFields.recursive] +label = "ความลึกของโฟลเดอร์" + +[portal.sources.networkFields.recursive.options] +all = "รวมโฟลเดอร์ย่อย" +top = "เฉพาะระดับบนสุด" + [portal.sources.status] active = "ใช้งานอยู่" disabled = "ปิดใช้งาน" unused = "ไม่ได้ใช้งาน" [portal.sources.table] +documents = "เอกสาร" source = "แหล่งที่มา" status = "สถานะ" +type = "Type" usedBy = "นโยบาย" +[portal.sources.types.box] +description = "ดึงเอกสารจากโฟลเดอร์ Box ที่ทีมของคุณแชร์อยู่แล้ว" +label = "Box" + +[portal.sources.types.dropbox] +description = "ดึงเอกสารจากโฟลเดอร์ Dropbox ที่แชร์" +label = "Dropbox" + [portal.sources.types.editor] +description = "เอกสารที่ทีมของคุณประมวลผลใน editor ครอบคลุมการรันนโยบายและ AI" label = "Editor" +noPolicies = "ยังไม่มีนโยบายที่รันจาก editor" + +[portal.sources.types.email] +description = "เปลี่ยนไฟล์แนบที่ส่งไปยังกล่องจดหมายเฉพาะให้เป็นเอกสาร" +label = "กล่องจดหมาย Email" [portal.sources.types.folder] description = "เฝ้าดูไดเรกทอรีบนเซิร์ฟเวอร์สำหรับเอกสารใหม่" @@ -7943,25 +8829,80 @@ label = "ความลึกของโฟลเดอร์" all = "รวมโฟลเดอร์ย่อย" top = "เฉพาะระดับบนสุด" +[portal.sources.types.ftp] +description = "ตรวจสอบโฟลเดอร์เซิร์ฟเวอร์ FTP หรือ FTPS เพื่อหาเอกสารใหม่" +label = "FTP" + +[portal.sources.types.googledrive] +description = "ดึงเอกสารจากโฟลเดอร์ Google Drive ที่แชร์" +label = "Google Drive" + +[portal.sources.types.network] +description = "เฝ้าดู SMB network share เพื่อหาเอกสารใหม่" +label = "Network drive" + +[portal.sources.types.onedrive] +description = "เฝ้าดูโฟลเดอร์ OneDrive เพื่อหาเอกสารใหม่" +label = "OneDrive" + +[portal.sources.types.s3] +description = "ดึงเอกสารจาก Amazon S3 หรือ bucket ที่เข้ากันได้กับ S3" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "การเชื่อมต่อที่จัดเก็บไว้ซึ่งมี bucket และข้อมูลประจำตัว ใช้ซ้ำโดยทุกแหล่งที่มาและเอาต์พุตไปป์ไลน์ที่อ้างอิงถึง" +label = "การเชื่อมต่อ" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume จะลบแต่ละ object ออกจาก bucket เมื่อทุกนโยบายประมวลผลแล้ว" +label = "โหมดการอ่าน" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: ประมวลผลแต่ละ object ครั้งเดียว" +snapshot = "Snapshot: อ่าน bucket ซ้ำทุกครั้งที่รัน" + +[portal.sources.types.s3.fields.prefix] +helperText = "ประมวลผลเฉพาะ object ที่ key เริ่มต้นด้วย prefix นี้" +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "ตรวจสอบโฟลเดอร์รับส่ง SFTP เพื่อหาเอกสารใหม่" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "ดึงเอกสารจาก SharePoint document libraries" +label = "SharePoint" + [portal.sources.types.unknown] label = "แหล่งที่มา" +[portal.sources.types.webhook] +createNote = "Delivery URL และ signing secret จะถูกสร้างเมื่อคุณสร้าง webhook นี้ ผู้ส่ง POST เอกสารไปยัง URL นั้น และนโยบายที่อ้างอิงจะรันเมื่อมาถึง" +description = "รับเอกสารด้วย HTTP POST ที่ลงนามแล้วจากระบบภายนอกใดก็ได้" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Signing secret จะแสดงหนึ่งครั้งเมื่อสร้าง webhook สร้างแหล่งที่มาใหม่เพื่อหมุนเวียน" + +[portal.sources.types.webhook.reveal] +copy = "คัดลอก" +done = "เสร็จสิ้น" +secret = "Signing secret" +secretHelp = "ลงนาม raw body ของแต่ละ delivery ด้วยคีย์นี้ (HMAC-SHA256) แล้วส่งเป็น X-Stirling-Signature header" +secretWarning = "คัดลอก signing secret ตอนนี้ เพื่อความปลอดภัยของคุณ จะแสดงเพียงครั้งเดียวและไม่สามารถเรียกคืนได้ภายหลัง" +title = "สร้าง Webhook แล้ว" +url = "Delivery URL" +usage = "POST เอกสารแต่ละฉบับเป็น raw request body ไปยัง Delivery URL ด้วย binary content type (application/pdf หรือ application/octet-stream) นโยบายที่อ้างอิงจะรันโดยอัตโนมัติเมื่อมาถึง" + [portal.sources.wizard] -back = "ย้อนกลับ" -cancel = "ยกเลิก" -continue = "ดำเนินการต่อ" -editTitle = "แก้ไขแหล่งที่มา" name = "ชื่อ" namePlaceholder = "เช่น Claims intake" -save = "บันทึกการเปลี่ยนแปลง" -subtitle = "ขั้นตอน {{current}} จาก {{total}} · {{label}}" -title = "เชื่อมต่อแหล่งที่มา" type = "ประเภท" -[portal.sources.wizard.steps] -chooseType = "เลือกประเภท" -configure = "กำหนดค่า" -review = "ตรวจสอบและเชื่อมต่อ" +[portal.stepModal] +close = "ปิด" [portal.usage] managePayment = "จัดการการชำระเงิน" @@ -8021,16 +8962,9 @@ summary = "เป็นเจ้าของทีม — จัดการท 2 = "เข้าถึง portal ผ่านนโยบายเริ่มต้น" 3 = "ทุกอย่างที่ Member ทำได้" -[portal.welcome] -ariaLabel = "ยินดีต้อนรับสู่ Stirling PDF" -badge = "Open-source" -installEditor = "ติดตั้ง Editor" -inviteTeammates = "เชิญเพื่อนร่วมทีม" -openInBrowser = "เปิดในเบราว์เซอร์" -perks = "ฟรีตลอดไป · Self-hostable" -subtitle = "PDF Editor ที่ปลอดภัยที่สุดในโลกใช้ฟรีสำหรับทีมทุกขนาด รวมการดำเนินการ PDF มากกว่า 60 รายการและ SSO" -title = "ยินดีต้อนรับสู่" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · ไม่จำกัด" [printFile] title = "พิมพ์ไฟล์" @@ -8613,6 +9547,14 @@ text = "หมุนหน้าของ PDF ตามหรือทวนเ [rotate.tooltip.header] title = "ภาพรวมการตั้งค่าการหมุน" +[ruler] +clearAll = "ล้างทั้งหมด" +hideAllLabels = "ซ่อนป้ายกำกับทั้งหมด" +hideSmallLabels = "ซ่อนป้ายกำกับขนาดเล็ก" +physicalValues = "ค่าจริง" +scaled = "ปรับสเกลแล้ว" +showAllLabels = "แสดงป้ายกำกับทั้งหมด" + [sanitize] desc = "ลบองค์ประกอบที่อาจเป็นอันตรายออกจากไฟล์ PDF" sanitizationResults = "ผลการทำให้ปลอดภัย" @@ -8654,31 +9596,45 @@ label = "เอา XMP Metadata ออก" [sanitize.steps] settings = "การตั้งค่า" -[sanitizePdf] -tags = "ทำความสะอาด, ปลอดภัย, ปลอดภัย, ลบภัยคุกคาม" - [sanitizePDF] title = "ทำความสะอาด PDF" +[sanitizePdf] +tags = "ทำความสะอาด, ปลอดภัย, ปลอดภัย, ลบภัยคุกคาม" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "ปรับสเกลหน้า" +[scaleSettings] +activeScale = "สเกลที่ใช้งานอยู่" +apply = "ใช้สเกล" +applyCalibration = "ใช้การปรับเทียบ" +calculatedScale = "สเกลที่คำนวณได้" +calibrate = "ปรับเทียบ" +calibrating = "กำลังปรับเทียบ" +calibrationDistanceRequired = "ป้อนระยะทางจริงที่มากกว่าศูนย์" +calibrationInvalid = "ไม่สามารถคำนวณมาตราส่วนจากการวัดนี้ได้" +calibrationPaperDistance = "ระยะทางบนหน้าที่วัดได้: {{distance}}" +calibrationTitle = "ปรับเทียบมาตราส่วน" +calibrationTooltip = "วัดระยะทางที่ทราบเพื่อคำนวณมาตราส่วนโดยอัตโนมัติ" +customScale = "มาตราส่วนกำหนดเอง" +noneSet = "ยังไม่ได้ตั้งค่ามาตราส่วนกำหนดเอง" +presets = "มาตราส่วนที่ตั้งไว้ล่วงหน้า" +ratio = "อัตราส่วนมาตราส่วน" +ratioHelp = "อัตราส่วน: 1 หน่วยหน้า = X หน่วยจริง" +ratioPlaceholder = "เช่น 100" +ratioPositive = "อัตราส่วนต้องมากกว่าศูนย์" +ratioRequired = "จำเป็นต้องมีอัตราส่วน" +realDistance = "ระยะทางจริง" +realDistancePlaceholder = "เช่น 5" +reset = "รีเซ็ตเป็นค่าเริ่มต้น" +unit = "หน่วย" +unitInvalid = "หน่วยไม่ถูกต้อง: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "เกณฑ์มุม:" -10 = "ตั้งค่าขนาดขอบที่เพิ่มและลบเพื่อป้องกันขอบขาวในผลลัพธ์ (ค่าเริ่มต้น: 1)" -2 = "ตั้งค่ามุมขั้นต่ำที่จำเป็นสำหรับการหมุนภาพ (ค่าเริ่มต้น: 10)" -3 = "ความอดทน:" -4 = "กำหนดช่วงความแตกต่างของสีรอบสีพื้นหลังที่คาดคะเน (ค่าเริ่มต้น: 30)" -5 = "พื้นที่ขั้นต่ำ:" -6 = "ตั้งค่าเกณฑ์พื้นที่ขั้นต่ำสำหรับรูปภาพ (ค่าเริ่มต้น: 10000)" -7 = "พื้นที่เค้าโครงขั้นต่ำ:" -8 = "ตั้งค่าเกณฑ์พื้นที่เค้าโครงขั้นต่ำสำหรับรูปภาพ" -9 = "ขนาดขอบ:" - [scannerImageSplit] submit = "แยกรูปภาพสแกน" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "ทำอะไร" whatThisDoesDesc = "ค้นหาและดึงรูปถ่ายแต่ละรูปจากหน้าที่สแกนหรือภาพรวมโดยอัตโนมัติ—ไม่ต้องครอปเอง" whenToUse = "ควรใช้เมื่อใด" +[ScannerImageSplit.selectText] +1 = "เกณฑ์มุม:" +10 = "ตั้งค่าขนาดขอบที่เพิ่มและลบเพื่อป้องกันขอบขาวในผลลัพธ์ (ค่าเริ่มต้น: 1)" +2 = "ตั้งค่ามุมขั้นต่ำที่จำเป็นสำหรับการหมุนภาพ (ค่าเริ่มต้น: 10)" +3 = "ความอดทน:" +4 = "กำหนดช่วงความแตกต่างของสีรอบสีพื้นหลังที่คาดคะเน (ค่าเริ่มต้น: 30)" +5 = "พื้นที่ขั้นต่ำ:" +6 = "ตั้งค่าเกณฑ์พื้นที่ขั้นต่ำสำหรับรูปภาพ (ค่าเริ่มต้น: 10000)" +7 = "พื้นที่เค้าโครงขั้นต่ำ:" +8 = "ตั้งค่าเกณฑ์พื้นที่เค้าโครงขั้นต่ำสำหรับรูปภาพ" +9 = "ขนาดขอบ:" + [search] noResults = "ไม่พบผลลัพธ์" placeholder = "ป้อนคำค้นหา..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "เซิร์ฟเวอร์ Stirling-PDF ขอ [session] expired = "สถานะของคุณในระบบหมดอายุ กรุณารีเฟรชหน้าและลองใหม่อีกครั้ง" -[sessionManagement.tooltip] -header = "การจัดการเซสชันการลงนาม" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "ผู้เข้าร่วมใหม่จะถูกเพิ่มไว้ท้ายลำดับการลงนาม" -bullet2 = "ไม่สามารถเพิ่มผู้เข้าร่วมหลังจากสรุปผลเซสชันแล้ว" -bullet3 = "ผู้เข้าร่วมแต่ละคนจะได้รับการแจ้งเตือนเมื่อถึงคิวของตน" -description = "คุณสามารถเพิ่มผู้เข้าร่วมเพิ่มเติมในเซสชันที่ใช้งานได้ทุกเมื่อก่อนการสรุปผล" -title = "การเพิ่มผู้เข้าร่วม" - -[sessionManagement.tooltip.finalization] -bullet1 = "สรุปผลเต็มรูปแบบ: ผู้เข้าร่วมทั้งหมดได้ลงนามแล้ว" -bullet2 = "สรุปผลบางส่วน: บางคนยังไม่ได้ลงนาม" -bullet3 = "ผู้เข้าร่วมที่ยังไม่ลงนามจะถูกตัดออกจากเอกสารสุดท้าย" -bullet4 = "เมื่อสรุปผลแล้ว คุณสามารถโหลด PDF ที่ลงนามแล้วไปยังไฟล์ที่ใช้งานอยู่ได้" -description = "การสรุปผลจะรวมลายเซ็นทั้งหมดเข้าเป็น PDF ที่ลงนามฉบับเดียว การดำเนินการนี้ไม่สามารถย้อนกลับได้" -title = "การสรุปผลเซสชัน" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "ไม่สามารถลบผู้เข้าร่วมที่ได้ลงนามแล้ว" -bullet2 = "ผู้ที่ถูกลบจะไม่ได้รับการแจ้งเตือนอีก" -bullet3 = "ลำดับการลงนามจะปรับโดยอัตโนมัติ" -description = "สามารถลบผู้เข้าร่วมออกจากเซสชันได้ก่อนที่พวกเขาจะลงนาม" -title = "การลบผู้เข้าร่วม" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "แต่ละลายเซ็นจะถูกใช้กับ PDF ตามลำดับ" -bullet2 = "ผู้ลงนามภายหลังสามารถเห็นลายเซ็นก่อนหน้าได้" -bullet3 = "สำคัญสำหรับเวิร์กโฟลว์การอนุมัติและการส่งมอบตามสายโซ่ทางกฎหมาย" -description = "ลำดับที่คุณกำหนดเมื่อสร้างเซสชันจะกำหนดว่าใครลงนามก่อน" -title = "ลำดับลายเซ็น" - [settings] close = "ปิด" +[settings.ai] +documents = "เอกสารและ RAG" +general = "ทั่วไป" +limits = "ขีดจำกัดและประสิทธิภาพ" +models = "โมเดลและผู้ให้บริการ" +title = "AI" + [settings.configuration] advanced = "ขั้นสูง" database = "ฐานข้อมูล" endpoints = "Endpoints" features = "ฟีเจอร์" +folderAccess = "การเข้าถึงโฟลเดอร์" mcp = "MCP Server" storageSharing = "การจัดเก็บไฟล์และการแชร์" systemSettings = "การตั้งค่าระบบ" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "โปรแกรมแก้ไข PDF เริ่มต defaultPdfEditorActive = "Stirling PDF เป็นโปรแกรมแก้ไข PDF เริ่มต้นของคุณ" defaultPdfEditorChecking = "กำลังตรวจสอบ..." defaultPdfEditorInactive = "มีแอปอื่นตั้งเป็นค่าเริ่มต้น" +defaultPdfEditorRemind = "เตือนให้ตั้งเป็นค่าเริ่มต้น" +defaultPdfEditorRemindDescription = "แสดงแบนเนอร์เมื่อ Stirling PDF ไม่ใช่แอปพลิเคชัน PDF เริ่มต้นของคุณ" defaultPdfEditorSet = "เป็นค่าเริ่มต้นอยู่แล้ว" defaultStartupView = "มุมมองเริ่มต้นเมื่อเปิดแอป" defaultStartupViewDescription = "เลือกแท็บที่ใช้งานในคอลัมน์ซ้ายเมื่อแอปเริ่มทำงาน" @@ -8817,9 +9763,18 @@ languageDescription = "เลือกภาษาที่แสดง" logout = "ออกจากระบบ" setAsDefault = "ตั้งเป็นค่าเริ่มต้น" theme = "ธีม" +themeAccent = "สีเน้น" +themeAccentCustom = "สีกำหนดเอง" +themeAccentCustomHint = "กำหนดเอง (แนะนำค่าเริ่มต้น)" +themeAccentDark = "สีเน้นโหมดมืด" +themeAccentDefault = "ค่าเริ่มต้น" +themeAccentDefaultHint = "ธีมเริ่มต้น (แนะนำ)" +themeAccentDescription = "ปุ่ม ลิงก์ และไฮไลต์จะใช้สีนี้ และจะเติมโทนสีให้แอปอย่างละเอียดอ่อน โหมดสว่างและมืดมีสีของตนเอง ระบบจะใช้สีใดก็ตามที่ใช้งานอยู่" +themeAccentLight = "สีเน้นโหมดสว่าง" themeDark = "มืด" themeDescription = "สลับระหว่างโหมดสว่างและมืด" themeLight = "สว่าง" +themeReset = "คืนค่าธีมเป็นค่าเริ่มต้น" themeSystem = "System" title = "ทั่วไป" user = "ผู้ใช้" @@ -8833,6 +9788,12 @@ intro = "เปิดใช้การยืนยันตัวตนผู learnMore = "เรียนรู้เพิ่มเติมในเอกสาร" title = "สำหรับผู้ดูแลระบบ" +[settings.general.loginLanding] +description = "เลือกแอปที่จะเปิดเมื่อคุณลงชื่อเข้าใช้ Stirling Cloud" +editor = "ตัวแก้ไข" +processor = "ตัวประมวลผล" +title = "หลังจากลงชื่อเข้าใช้" + [settings.general.mode] fullscreen = "เต็มหน้าจอ" sidebar = "แถบด้านข้าง" @@ -9013,9 +9974,6 @@ title = "นโยบายและความเป็นส่วนตั [settings.preferences] title = "ค่ากำหนด" -[settings.search] -placeholder = "ค้นหาหน้าการตั้งค่า..." - [settings.security] description = "อัปเดตรหัสผ่านของคุณเพื่อให้บัญชีปลอดภัย" title = "ความปลอดภัย" @@ -9041,6 +9999,11 @@ security = "ความปลอดภัย" telegram = "Telegram" title = "ความปลอดภัยและการยืนยันตัวตน" +[settings.signOut] +confirm = "คุณแน่ใจหรือไม่ว่าต้องการลงชื่อออก?" +submit = "ลงชื่อออก" +title = "ลงชื่อออก" + [settings.team] title = "ทีม" @@ -9156,6 +10119,7 @@ last = "หน้าสุดท้าย" maintainRatio = "สลับคงอัตราส่วนภาพ" next = "หน้าถัดไป" noSavedSigs = "ไม่พบลายเซ็นที่บันทึกไว้" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "ลายเซ็นส่วนตัว" previous = "หน้าก่อนหน้า" redo = "ทำซ้ำ" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "ลบพื้นหลังไม่สำเ hint = "อัปโหลดภาพลายเซ็นเป็น PNG หรือ JPG" label = "อัปโหลดภาพลายเซ็น" placeholder = "เลือกไฟล์รูปภาพ" +previewAlt = "Current image signature" processing = "กำลังประมวลผลภาพ..." removeBackground = "ลบพื้นหลังสีขาว (ทำให้โปร่งใส)" @@ -9207,6 +10172,17 @@ saved = "เลือกหนึ่งลายเซ็นที่บัน text = "หลังจากป้อนชื่อของคุณด้านบนแล้ว ให้คลิกที่ใดก็ได้บน PDF เพื่อวางลายเซ็น" title = "วิธีเพิ่มลายเซ็น" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "ย้ายลายเซ็น" pause = "หยุดการวาง" @@ -9653,10 +10629,30 @@ title = "อัปโหลดไปยังเซิร์ฟเวอร์" updateButton = "อัปเดตบนเซิร์ฟเวอร์" uploadButton = "อัปโหลดไปยังเซิร์ฟเวอร์" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "คลิกที่นี่" +fallback = "หากไม่ได้เปิดโดยอัตโนมัติ" +opening = "กำลังเปิด Swagger UI ในแท็บใหม่..." tags = "api,documentation,swagger,endpoints,development" title = "เอกสาร API" @@ -9764,12 +10760,39 @@ selectFilesHint = "เลือกไฟล์ในไฟล์ที่ใช singleFileScope = "ใช้เฉพาะกับ: {{fileName}}" viewerMode = "สลับไปที่ตัวแก้ไขไฟล์เพื่อเลือกหลายไฟล์" +[toolFormat] +ANY = "ไฟล์ใดก็ได้" +CBR = "ไฟล์เก็บถาวรหนังสือการ์ตูน (CBR)" +CBZ = "ไฟล์เก็บถาวรหนังสือการ์ตูน (CBZ)" +CSV = "ไฟล์ CSV" +EBOOK = "อีบุ๊ก" +EMAIL = "อีเมล" +EXCEL = "สเปรดชีต" +HTML = "หน้าเว็บ" +IMAGE = "รูปภาพ" +JAVASCRIPT = "ไฟล์ JavaScript" +JSON = "ไฟล์ JSON" +MARKDOWN = "ไฟล์ Markdown" +NONE = "ไม่มีไฟล์" +PCL = "ไฟล์พิมพ์ PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF ที่ป้องกันด้วยรหัสผ่าน" +POSTSCRIPT = "ไฟล์ PostScript" +PPT = "งานนำเสนอ" +TEXT = "ไฟล์ข้อความ" +VIDEO = "วิดีโอ" +WORD = "เอกสาร Word" +XML = "ไฟล์ XML" +XPS = "เอกสาร XPS" +ZIP = "ไฟล์เก็บถาวร ZIP" + [toolPanel] alpha = "อัลฟา" backToAllTools = "กลับไปที่เครื่องมือทั้งหมด" collapse = "ยุบแผง" expand = "ขยายแผง" goBack = "ย้อนกลับ" +pdfTools = "เครื่องมือ PDF" placeholder = "เลือกเครื่องมือเพื่อเริ่มต้น" premiumFeature = "ฟีเจอร์พรีเมียม:" toolsHeader = "เครื่องมือ" @@ -9806,11 +10829,6 @@ noToolsFound = "ไม่พบเครื่องมือ" quickAccess = "เข้าถึงด่วน" searchPlaceholder = "ค้นหาเครื่องมือ..." -[toolPicker.categories] -advancedTools = "เครื่องมือขั้นสูง" -recommendedTools = "เครื่องมือแนะนำ" -standardTools = "เครื่องมือมาตรฐาน" - [toolPicker.subcategories] advancedFormatting = "การจัดรูปแบบขั้นสูง" automation = "การทำงานอัตโนมัติ" @@ -9824,6 +10842,9 @@ removal = "การลบ" signing = "การลงนาม" verification = "การตรวจสอบ" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "ไม่พบเครื่องมือ" noTools = "ไม่มีเครื่องมือให้ใช้" @@ -9950,28 +10971,53 @@ title = "ผู้ใช้" you = "(คุณ)" [users.action] +cancelInvite = "ยกเลิกคำเชิญ" deleteTeam = "ลบทีม" disableMfa = "รีเซ็ต MFA" move = "ย้ายไปยังทีม" reinstate = "คืนสถานะ" remove = "ลบออกจากองค์กร" +removeTeam = "นำออกจากทีม" rename = "เปลี่ยนชื่อทีม" resetPw = "รีเซ็ตรหัสผ่าน" suspend = "ระงับ" unlock = "ปลดล็อกบัญชี" +[users.activity] +daysAgo = "{{count}} วันก่อน" +hoursAgo = "{{count}} ชั่วโมงก่อน" +justNow = "เมื่อสักครู่" +minutesAgo = "{{count}} นาทีก่อน" +monthsAgo_one = "{{count}} เดือนก่อน" +monthsAgo_other = "{{count}} เดือนก่อน" +never = "ไม่เคย" +weeksAgo_one = "{{count}} สัปดาห์ก่อน" +weeksAgo_other = "{{count}} สัปดาห์ก่อน" +yearsAgo_one = "{{count}} ปีก่อน" +yearsAgo_other = "{{count}} ปีก่อน" + [users.cap] addProcessor = "+ Processor" approver = "อนุมัตินโยบาย" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "ยกเลิกคำเชิญไปยัง {{email}}? พวกเขาจะไม่สามารถเข้าร่วมด้วยลิงก์ปัจจุบันได้" +cancelInviteTitle = "ยกเลิกคำเชิญ" deleteTeamBody = "ลบทีม {{name}} หรือไม่? ทีมต้องว่างก่อน - ย้ายสมาชิกไปยังทีมอื่น และต้องไม่ยังเป็นเจ้าของการกำหนดค่า integration ใด ๆ" deleteTeamTitle = "ลบทีม" disableMfaBody = "ลบการลงทะเบียน MFA ของ {{name}} หรือไม่? พวกเขาจะตั้งค่าอีกครั้งในการเข้าสู่ระบบครั้งถัดไปหากจำเป็น" disableMfaTitle = "รีเซ็ต MFA" removeBody = "ลบ {{name}} ออกจากองค์กรอย่างถาวรหรือไม่? การดำเนินการนี้ไม่สามารถย้อนกลับได้" +removeTeamBody = "นำ {{name}} ออกจากทีมหรือไม่? ผู้ใช้จะยังคงมีบัญชีอยู่ แต่จะเสียสิทธิ์เข้าถึงทรัพยากรของทีมนี้" removeTitle = "ลบสมาชิก" [users.empty] @@ -9982,10 +11028,8 @@ title = "ยังไม่มีสมาชิก" addToTeam = "เพิ่มเข้าทีม" guestCount = "{{count}} guest" guests = "Guests" -guestsDesc = "ผู้ร่วมงานภายนอก จำกัดตามสิ่งที่คุณแชร์ Editor เท่านั้น" ledBy = "นำโดย {{owner}}" org = "องค์กร" -orgDesc = "เจ้าของที่มีอำนาจทั่วทั้งองค์กรและการอนุมัตินโยบาย" owners = "เจ้าของ {{count}} คน" team = "ทีม {{name}}" teamMeta = "{{count}} คน" @@ -10022,6 +11066,18 @@ username = "ชื่อผู้ใช้" usernameError = "ชื่อผู้ใช้ต้องมีอย่างน้อย 3 อักขระ" usernamePlaceholder = "jsmith" +[users.invites] +by = "เชิญโดย {{who}}" +cancel = "ยกเลิก" +expiresInDays_one = "หมดอายุใน {{count}} วัน" +expiresInDays_other = "หมดอายุใน {{count}} วัน" +expiresToday = "หมดอายุวันนี้" +title = "คำเชิญที่รอดำเนินการ" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "เกิดข้อผิดพลาดในการติดต่อ backend หรือคุณไม่มีสิทธิ์เข้าถึง ลองอีกครั้ง" title = "ไม่สามารถโหลดสมาชิกได้" @@ -10211,10 +11267,15 @@ searchPlaceholder = "ค้นหาไฟล์แนบ" title = "ไฟล์แนบ" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "ชื่อบุ๊กมาร์ก" +bookmarkTitleRequired = "จำเป็นต้องมีชื่อบุ๊กมาร์ก" closeSidebar = "ปิดแถบด้านข้างบุ๊กมาร์ก" collapseAll = "ยุบบุ๊กมาร์กทั้งหมด" +empty = "No bookmarks in this document" expandAll = "ขยายบุ๊กมาร์กทั้งหมด" +searchPlaceholder = "ค้นหาบุ๊กมาร์ก" +title = "Bookmarks" [viewer.comments] addComment = "เพิ่มความคิดเห็น" @@ -10236,10 +11297,12 @@ locateAnnotation = "ค้นหาตำแหน่งในเอกสาร moreActions = "การดำเนินการเพิ่มเติม" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "หน้า {{page}}" placingHint = "คลิกที่หน้ากระดาษเพื่อวาง… (ยกเลิก)" removeCommentOnly = "ลบเฉพาะความคิดเห็น" saveReply = "บันทึกการตอบกลับ" +searchPlaceholder = "Search comments" title = "ความคิดเห็น" typeComment = "ความคิดเห็น" typeInsertText = "แทรกข้อความ" @@ -10262,6 +11325,7 @@ unsavedDesc = "คุณมีการเปลี่ยนแปลงที closeSidebar = "ปิดแถบด้านข้างเลเยอร์" hideAll = "ซ่อนเลเยอร์ทั้งหมด" showAll = "แสดงเลเยอร์ทั้งหมด" +title = "Layers" [viewer.link] delete = "ลบลิงก์" @@ -10605,30 +11669,6 @@ title = "ข้อความลายน้ำ" image = "รูปภาพ" text = "ข้อความ" -[wetSignature.tooltip] -header = "วิธีการสร้างลายเซ็น" - -[wetSignature.tooltip.draw] -bullet1 = "ปรับแต่งสีและความหนาของปากกา" -bullet2 = "ลบและวาดใหม่ได้จนกว่าจะพอใจ" -bullet3 = "ใช้งานได้บนอุปกรณ์สัมผัส (แท็บเล็ต โทรศัพท์)" -description = "สร้างลายเซ็นลายมือโดยใช้เมาส์หรือหน้าจอสัมผัส เหมาะที่สุดสำหรับลายเซ็นส่วนบุคคลที่เป็นธรรมชาติ" -title = "วาดลายเซ็น" - -[wetSignature.tooltip.type] -bullet1 = "เลือกจากหลายฟอนต์" -bullet2 = "ปรับแต่งขนาดและสีของข้อความ" -bullet3 = "เหมาะสำหรับลายเซ็นแบบมาตรฐาน" -description = "สร้างลายเซ็นจากข้อความที่พิมพ์ รวดเร็วและสม่ำเสมอ เหมาะกับเอกสารทางธุรกิจ" -title = "พิมพ์ลายเซ็น" - -[wetSignature.tooltip.upload] -bullet1 = "รองรับ PNG, JPG และรูปแบบภาพอื่นๆ" -bullet2 = "แนะนำให้ใช้ภาพพื้นหลังโปร่งใสเพื่อผลลัพธ์ที่ดีที่สุด" -bullet3 = "รูปภาพจะถูกปรับขนาดให้พอดีกับพื้นที่ลายเซ็น" -description = "อัปโหลดรูปภาพลายเซ็นที่สร้างไว้แล้ว เหมาะอย่างยิ่งหากคุณมีลายเซ็นที่สแกนไว้หรือโลโก้บริษัท" -title = "อัปโหลดรูปภาพลายเซ็น" - [workbenchBar] activeFiles = "ไฟล์ที่ใช้งานอยู่" annotations = "คำอธิบายประกอบ" @@ -10642,6 +11682,7 @@ exitRedaction = "ออกจากโหมดปกปิดข้อมูล exportAll = "ส่งออก PDF" exportSelected = "ส่งออกหน้าที่เลือก" formFill = "กรอกแบบฟอร์ม" +hideToolbar = "Hide toolbar" multiTool = "เครื่องมือหลายฟังก์ชัน" panMode = "โหมดเลื่อน" print = "พิมพ์ PDF" @@ -10652,6 +11693,7 @@ redact = "ปกปิดข้อมูล" rotateLeft = "หมุนซ้าย" rotateRight = "หมุนขวา" ruler = "ไม้บรรทัด / วัดระยะ" +rulerSettings = "การตั้งค่ามาตราส่วน" save = "บันทึก" saveAll = "บันทึกทั้งหมด" saveAs = "บันทึกเป็น" @@ -10661,6 +11703,7 @@ selectAll = "เลือกทั้งหมด" selectByNumber = "เลือกตามหมายเลขหน้า" selectLanguage = "เลือกภาษา" share = "แชร์" +showToolbar = "Show toolbar" toggleAnnotations = "สลับการแสดงคำอธิบายประกอบ" toggleAttachments = "สลับไฟล์แนบ" toggleBookmarks = "สลับที่คั่นหน้า" diff --git a/frontend/editor/public/locales/tr-TR/translation.toml b/frontend/editor/public/locales/tr-TR/translation.toml index 6c9031be44..a1a1acd4f8 100644 --- a/frontend/editor/public/locales/tr-TR/translation.toml +++ b/frontend/editor/public/locales/tr-TR/translation.toml @@ -1,7 +1,6 @@ alphabet = "Alfabe" apply = "Uygula" applyAndContinue = "Kaydet ve Çık" -areYouSure = "Ayrılmak istediğinizden emin misiniz?" back = "Geri" black = "Siyah" blue = "Mavi" @@ -46,10 +45,8 @@ filesSelected = "dosya seçildi" font = "Yazı tipi" fontSizeTooltip = "Sayfa numarası metninin punto cinsinden boyutu. Daha büyük sayılar daha büyük metin oluşturur." fontTypeTooltip = "Sayfa numaraları için yazı tipi ailesi. Belgenizin stiline göre seçin." -genericSubmit = "Gönder" green = "Yeşil" help = "Yardım" -imgPrompt = "Resim(leri) seçin" incorrectPasswordMessage = "Mevcut şifre yanlış." info = "Bilgi" invalidUndoData = "Geri alma yapılamıyor: geçersiz işlem verisi" @@ -62,7 +59,6 @@ loginAgreementTitle = "Giriş Sözleşmesi" logOut = "Oturumu kapat" marginTooltip = "Sayfa numarası ile sayfa kenarı arasındaki mesafe." moreOptions = "Daha Fazla Seçenek" -multiPdfPrompt = "PDFleri seçin (2+)" never = "Asla" no = "Hayır" noFileSelected = "Hiçbir dosya seçilmedi. Lütfen bir dosya yükleyin." @@ -80,7 +76,6 @@ pageOrderPrompt = "Özel Sayfa Sırası (Virgülle ayrılmış sayfa numaraları pages = "Sayfalar" pageSelectionPrompt = "Özel Sayfa Seçimi (1,5,6 sayfa numaralarının virgülle ayrılmış bir listesini veya 2n+1 gibi bir fonksiyon girin) :" password = "Parola" -pdfPrompt = "PDF(leri) seçin" pin = "Dosyayı Sabitle (araç çalıştıktan sonra etkin tut)" pro = "Pro" processingComplete = "Dosyanız hazır." @@ -110,6 +105,7 @@ undoSuccess = "İşlem başarıyla geri alındı" unknown = "Bilinmeyen" unpin = "Dosya Sabitlemesini Kaldır (araç çalıştıktan sonra değiştir)" unsavedChanges = "PDF'inizde kaydedilmemiş değişiklikler var." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Kaydedilmemiş Değişiklikler" unsupported = "Desteklenmiyor" username = "Kullanıcı Adı" @@ -356,6 +352,7 @@ text = "Bu izinler, kullanıcıların PDF ile neler yapabileceğini kontrol eder title = "İzinleri Değiştir" [addStamp] +preview = "Damgayı Önizle" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Manuel indirmeler: bağlantılara tıklayın ve dosyaları tessda noLanguages = "Yapılandırılan dizinde tessdata dili bulunamadı." permissionNotice = "tessdata yolu yazılabilir değil. İndirmeler tarayıcıda açılacak; lütfen .traineddata dosyalarını elle tessdata klasörüne kaydedin." +[admin.settings.ai.documents] +description = "Belgeler üzerinden soruları yanıtlamak için kullanılan embedding modelini ve getirme ayarlarını yapılandırın. Kaydedildiğinde AI engine'e uygulanır." +title = "Belgeler ve RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Engine'in kendi ortam kimlik bilgisini kullanmak için boş bırakın. Kendi barındırılan tek engine dağıtımları için geçerlidir." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Kaydedildi - geçerli key'i korumak için boş bırakın" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI uyumlu / Ollama embeddings endpoint'inin temel URL'si, örn. http://ollama:11434/v1. Güvenilir bir dahili endpoint'e işaret etmelidir (SSRF açısından hassas)." +label = "Embedding temel URL'si" + +[admin.settings.ai.documents.embeddingModel] +description = "Embedding model adı. Serbest metin; öneriler yalnızca ipucudur." +label = "Embedding model" + +[admin.settings.ai.documents.embeddingProvider] +description = "Belge metnini vektör embedding'lere dönüştürmek için kullanılan sağlayıcı." +label = "Embedding sağlayıcısı" + +[admin.settings.ai.documents.maxSearches] +description = "Agent'ın istek başına çalıştırabileceği maksimum getirme araması sayısı." +label = "Maksimum arama" + +[admin.settings.ai.documents.reindexNote] +body = "Embedding modelini değiştirmek hemen etkili olur, ancak önceki modelle indekslenen belgelerin aramada doğru sonuçlar döndürmesi için yeniden indekslenmesi gerekir." +title = "Yeniden indeksleme gerekli" + +[admin.settings.ai.documents.saved] +reindexNote = "Embedding modelini değiştirdiyseniz, aramanın yeni modeli kullanması için mevcut belgeleri yeniden indeksleyin." + +[admin.settings.ai.documents.topK] +description = "Arama başına getirilen en alakalı parça sayısı." +label = "Top K" + +[admin.settings.ai.general] +description = "Stirling'i Python AI engine'e bağlayın ve hangi AI özelliklerinin sunulacağını seçin. Değişiklikler yeniden başlatmada uygulanır." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Tek tek AI özelliklerini açın veya kapatın. Devre dışı bırakılan özellikler uygulamada gizlenir." +title = "Yetenekler" + +[admin.settings.ai.general.enabled] +description = "Ana anahtar. Kapalıyken hiçbir AI aracı, agent veya engine çağrısı kullanılamaz." +label = "AI'ı etkinleştir" + +[admin.settings.ai.general.features.chat] +description = "PDF'lerle çalışmak için konuşmaya dayalı asistan." +label = "Sohbet asistanı" + +[admin.settings.ai.general.features.classify] +description = "Belgeleri türüne veya içeriğine göre otomatik olarak kategorize edin." +label = "Belge sınıflandırma" + +[admin.settings.ai.general.features.createPdf] +description = "Bir AI agent aracılığıyla yeni bir PDF oluşturun (örn. HTML'den)." +label = "Komuttan PDF oluştur" + +[admin.settings.ai.general.features.documentQuestions] +description = "Yüklenen bir belgeye dayalı sorular sorun ve yanıtlar alın." +label = "Belge soruları" + +[admin.settings.ai.general.features.mathAuditor] +description = "Belgeleri matematiksel ve sayısal hatalar açısından inceleyin." +label = "Matematik denetçisi" + +[admin.settings.ai.general.features.pdfComment] +description = "Bir PDF'ye AI tarafından yazılmış inceleme yorumları ve ek açıklamalar ekleyin." +label = "PDF yorum agent'ı" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Belge oluşturma gibi daha ağır agent işlemleri için zaman aşımı." +label = "Uzun süreli zaman aşımı (saniye)" + +[admin.settings.ai.general.note] +body = "AI engine ayrı bir hizmet olarak çalışır. Paylaşılan sırrı" +body2 = "bir container ortam değişkeni aracılığıyla ayarlanır. Sağlayıcı API key'leri bu sayfalara girilebilir veya engine ortam değişkenleri olarak sağlanabilir; buraya girilen key'ler kaydedildiğinde engine'e gönderilir." +title = "AI engine hakkında" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Akış halinde (token token) sohbet yanıtları için zaman aşımı." +label = "Akış zaman aşımı (saniye)" + +[admin.settings.ai.general.test] +button = "Bağlantıyı test et" +failBody = "AI engine yanıt vermedi. URL'yi, engine container'ının çalıştığını ve AI'ın etkin olduğunu kontrol edin (etkinleştirdikten sonra yeniden başlatma gerekir)." +failTitle = "AI engine'e ulaşılamıyor" +okBody = "AI engine sağlık kontrolüne yanıt verdi." +okTitle = "AI engine'e ulaşılabiliyor" + +[admin.settings.ai.general.timeoutSeconds] +description = "Engine'e yapılan standart AI istekleri için zaman aşımı." +label = "İstek zaman aşımı (saniye)" + +[admin.settings.ai.general.url] +description = "Python AI engine'in dahili URL'si, örn. http://stirling-pdf-engine:5001." +label = "AI engine URL'si" + +[admin.settings.ai.limits] +description = "AI isteklerinin ne kadar iş yapabileceği ve kaçının eş zamanlı çalışabileceği için koruma sınırları. Kaydedildiğinde AI engine'e uygulanır." +title = "Sınırlar ve Performans" + +[admin.settings.ai.limits.maxCharacters] +description = "Koruma sınırı: çıkarılan metni bu uzunluğu aşan AI isteklerini reddet." +label = "İstek başına maksimum karakter" + +[admin.settings.ai.limits.maxPages] +description = "Koruma sınırı: bu kadar PDF sayfasını aşan AI isteklerini reddet." +label = "İstek başına maksimum sayfa" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Tüm engine genelinde aynı anda devam eden maksimum model çağrısı sayısı." +label = "Model maksimum eş zamanlılık" + +[admin.settings.ai.models] +description = "AI engine'in kullandığı LLM sağlayıcısını ve akıllı/hızlı modelleri seçin. Kaydedildiğinde AI engine'e uygulanır." +title = "Modeller ve Sağlayıcılar" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Engine'in kendi ortam kimlik bilgisini kullanmak için boş bırakın. Kendi barındırılan tek engine dağıtımları için geçerlidir." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Kaydedildi - geçerli key'i korumak için boş bırakın" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI uyumlu / Ollama endpoint'inin temel URL'si, örn. http://ollama:11434/v1." +label = "Sağlayıcı temel URL'si" +warning = "Temel URL güvenilir bir dahili endpoint'e işaret etmelidir. Engine bu adrese sunucu tarafı istekleri yapacaktır, bu nedenle güvenilmeyen bir değer SSRF açısından hassastır." + +[admin.settings.ai.models.fastMaxTokens] +description = "Hızlı model için maksimum çıktı token'ları." +label = "Hızlı model maksimum token" + +[admin.settings.ai.models.fastModel] +description = "Hafif görevler için daha ucuz, daha hızlı model. Serbest metin; öneriler yalnızca ipucudur." +label = "Hızlı model" + +[admin.settings.ai.models.provider] +description = "Engine'in hangi LLM sağlayıcısıyla konuşacağı." +label = "Sağlayıcı" + +[admin.settings.ai.models.smartMaxTokens] +description = "Akıllı model için maksimum çıktı token'ları." +label = "Akıllı model maksimum token" + +[admin.settings.ai.models.smartModel] +description = "Karmaşık akıl yürütme için yüksek yetenekli model. Serbest metin; öneriler yalnızca ipucudur." +label = "Akıllı model" + +[admin.settings.ai.saved] +body = "Değişiklikler otomatik olarak AI engine'e gönderildi." +bodyNoPush = "Ayarlar kaydedildi. AI engine yapılandırmasını bir sonraki aldığında uygulanacak." +title = "AI ayarları kaydedildi" + [admin.settings.badge] clickToUpgrade = "Plan ayrıntılarını görmek için tıklayın" @@ -938,6 +1094,25 @@ label = "Başlangıçta Yeniden Oluştur" description = "Sertifikanın geçerli olacağı gün sayısı" label = "Sertifika Geçerliliği (gün)" +[admin.settings.folderAccess] +description = "Klasör kaynaklarının ve klasör çıktılarının okuma ve yazma izni olan dizinler. Bu bir güvenlik sınırıdır: otomasyonlar asla bu listenin dışındaki bir sunucu yoluna yönlendirilemez." +securityNote = "Klasör kaynaklarını ve çıktılarını tamamen devre dışı bırakmak için bunu boş bırakın. Stirling'in kendi yapılandırma dizini her zaman erişime kapalıdır." +title = "Klasör Erişimi" + +[admin.settings.folderAccess.implied] +description = "Stirling tarafından yönetilen bu dizinlere her zaman izin verilir ve burada değiştirilemez." +serverStorage = "Sunucu dosya depolama" +title = "Her zaman izinli" +watchedFolder = "Pipeline izlenen klasör" + +[admin.settings.folderAccess.roots] +add = "Ekle" +empty = "İzin verilen klasör yok. Klasör kaynakları ve çıktıları şu anda devre dışı." +hint = "Mutlak yollar girin, örneğin /data/inbox." +label = "İzin verilen klasör kökleri" +placeholder = "/data/inbox" +remove = "Klasör kökünü kaldır" + [admin.settings.general] description = "Markalaşma ve varsayılan davranış dahil olmak üzere sistem genelinde uygulama ayarlarını yapılandırın." system = "Sistem" @@ -1704,6 +1879,9 @@ width = "Genişlik" [app] description = "Ücretsiz Adobe Acrobat alternatifi (10M+ indirme)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "PDF/A-3b'ye dönüştür" convertToPdfA3bDescription = "Gömülü eklerle arşivlik bir PDF oluşturur" @@ -1842,6 +2020,15 @@ export = "Dışa Aktar" accessDenied = "Erişim Reddedildi" insufficientPermissions = "Bu işlemi gerçekleştirmek için izniniz yok." +[auth.callback] +completing = "Kimlik doğrulama tamamlanıyor" +invalidToken = "OAuth oturum açma başarısız - geçersiz token." +missingToken = "OAuth oturum açma başarısız - token alınmadı." +oauthFailed = "OAuth oturum açma başarısız. Lütfen tekrar deneyin." +pleaseWait = "Oturum açmanızı tamamlarken lütfen bekleyin." +signedOut = "Oturumunuz kapatıldı. Lütfen tekrar oturum açın." +windowMayClose = "Tamamlandığında bu pencereyi kapatabilirsiniz." + [auth.displayName] guest = "Konuk" user = "Kullanıcı" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "JavaScript ve gömülü dosyalar gibi potansiyel olarak k description = "Bu araç, PDF dosyalarını içeriklerine göre otomatik olarak yeniden adlandırır. Metinden en uygun başlığı bulmak için belgeyi analiz eder." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Otomatik Döndür" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "Güven eşiği" + +[autoRotate.detectionMode] +auto = "Otomatik" +osd = "Yalnızca OCR" +text = "Yalnızca metin" +title = "Algılama yöntemi" + +[autoRotate.error] +failed = "PDF otomatik döndürülürken bir hata oluştu." + +[autoRotate.inferUndetected] +title = "Düşük güvenli sayfaları eşleşecek şekilde döndür" + +[autoRotate.report] +applied = "{{degrees}}° saat yönünde" +confidenceValue = "Güven {{value}}" +noChange = "Değişiklik yok" +pageLabel = "Sayfa {{number}}" +summary = "{{total}} sayfanın {{rotated}} tanesi döndürüldü, {{unchanged}} tanesi olduğu gibi bırakıldı" +title = "Algılama raporu" + +[autoRotate.report.method] +inferred = "Eşleştirildi" +none = "Atlandı" +osd = "OCR" +text = "Metin" + +[autoRotate.report.note] +belowThreshold = "Eşiğin altında" +blankPage = "Sayfa boş" +inferredFromDocument = "Diğer sayfalarla aynı" +noDominantDirection = "Karışık metin yönleri" +osdFailed = "Okunabilir metin bulunamadı" +osdNoVerdict = "Hangi yönün yukarı olduğu belirlenemedi" +tesseractUnavailable = "OCR yüklü değil" +tooFewGlyphs = "Çok az metin" + +[autoRotate.results] +title = "Otomatik Döndürme Sonuçları" + +[autoRotate.settings] +title = "Ayarlar" + +[autoRotate.tooltip.confidenceThreshold] +text = "Bir sayfanın döndürülmeden önce kontrolün ne kadar emin olması gerektiği. Daha fazla sayfayı döndürmek için düşürün, daha fazla sayfayı dokunulmadan bırakmak için yükseltin." +title = "Güven eşiği" + +[autoRotate.tooltip.description] +text = "Her sayfanın hangi yönünün yukarı olması gerektiğini belirler ve doğru yöne döndürür. Zaten dik olan sayfalar olduğu gibi bırakılır." + +[autoRotate.tooltip.detectionMode] +text = "Otomatik önce sayfadaki metni okur, bu yeterli değilse sayfaya görüntü olarak bakar. Yalnızca metin, hiç taranmamış belgeler için en hızlı olan görüntü kontrolünü atlar. Yalnızca OCR her sayfayı görüntü olarak ele alır." +title = "Algılama yöntemi" + +[autoRotate.tooltip.header] +title = "Otomatik Döndür" + +[autoRotate.tooltip.inferUndetected] +text = "Boş veya neredeyse boş sayfalar tek başına kontrol edilemez. Okunabilen her sayfa aynı dönüşe ihtiyaç duyduğunda, bu sayfalar onlarla eşleşecek şekilde döndürülür." +title = "Düşük güvenli sayfaları eşleşecek şekilde döndür" + [autoSizeSplitPDF] tags = "pdf,bölme,belge,organizasyon" @@ -2189,33 +2443,6 @@ keywords = "Anahtar kelimeler: odd, even" numbers = "Sayılar/aralıklar: 5, 10-20" progressions = "İlerlemeler: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Sertifika Türleri" - -[certificateChoice.tooltip.organization] -bullet1 = "Sistem yöneticileri tarafından yönetilir" -bullet2 = "Yetkili kullanıcılar arasında paylaşılır" -bullet3 = "Şirket kimliğini temsil eder, bireyi değil" -bullet4 = "En uygun: Resmi belgeler, ekip imzaları" -description = "Kuruluşunuz tarafından sağlanan paylaşılan bir sertifika. Şirket çapında imzalama yetkisi için kullanılır." -title = "Kuruluş Sertifikası" - -[certificateChoice.tooltip.personal] -bullet1 = "İlk kullanımda otomatik oluşturulur" -bullet2 = "Kullanıcı hesabınıza bağlıdır" -bullet3 = "Diğer kullanıcılarla paylaşılamaz" -bullet4 = "En uygun: Kişisel belgeler, bireysel sorumluluk" -description = "Kullanıcı hesabınıza özgü otomatik oluşturulmuş bir sertifika. Bireysel imzalar için uygundur." -title = "Kişisel Sertifika" - -[certificateChoice.tooltip.upload] -bullet1 = "P12/PFX dosyası ve parola gerektirir" -bullet2 = "Harici Sertifika Otoriteleri tarafından verilebilir" -bullet3 = "Hukuki belgeler için daha yüksek güven seviyesi" -bullet4 = "En uygun: Hukuken bağlayıcı sözleşmeler, harici doğrulama" -description = "Kendi PKCS#12 sertifika dosyanızı kullanın. Sertifika özellikleri üzerinde tam kontrol sağlar." -title = "Özel P12 Yükle" - [certSign] awaitingSignatures = "İmzalar bekleniyor" chooseCertificate = "Sertifika Dosyası Seç" @@ -2542,34 +2769,6 @@ server = "Sunucu" stepTitle = "Sertifika kaynağı" upload = "Yükle" -[certSign.tooltip.header] -title = "İmzaları Yönetme Hakkında" - -[certSign.tooltip.overview] -bullet1 = "Mevcut imzaları ve geçerliliklerini kontrol edin" -bullet2 = "İmzalayanlar ve sertifikalar hakkında ayrıntılı bilgileri görüntüleyin" -bullet3 = "Belgelerinize yeni dijital imzalar ekleyerek güvence sağlayın" -bullet4 = "Kolay gezinme ile birden çok dosya desteklenir" -text = "Bu araç, PDF'lerinizin dijital olarak imzalı olup olmadığını kontrol etmenizi ve yeni dijital imzalar eklemenizi sağlar. Dijital imzalar, bir belgenin kimin tarafından oluşturulduğunu veya onaylandığını kanıtlar ve imzalandıktan sonra değiştirilip değiştirilmediğini gösterir." -title = "Bu araç ne yapabilir?" - -[certSign.tooltip.signing] -bullet1 = "PEM, PKCS12, JKS ve sunucu sertifika biçimlerini destekler" -bullet2 = "PDF üzerinde imzayı gösterme veya gizleme seçeneği" -bullet3 = "Neden, konum ve imzalayan adını ekleyin" -bullet4 = "Görünür imzaların hangi sayfaya yerleştirileceğini seçin" -bullet5 = "Basit 'Stirling PDF ile İmzala' seçeneği için sunucu sertifikasını kullanın" -text = "Bir PDF'yi imzalamak için dijital bir sertifikaya (PEM, PKCS12 veya JKS gibi) ihtiyacınız vardır. İmzayı belgede görünür yapmayı veya yalnızca güvenlik için görünmez tutmayı seçebilirsiniz." -title = "İmza Ekleme" - -[certSign.tooltip.validation] -bullet1 = "İmzaların geçerli mi geçersiz mi olduğunu gösterir" -bullet2 = "İmzalayan bilgilerini ve imzalama tarihini görüntüler" -bullet3 = "Belgenin imzadan sonra değiştirilip değiştirilmediğini kontrol eder" -bullet4 = "Doğrulama için özel sertifikalar kullanılabilir" -text = "İmzaları kontrol ettiğinizde, araç bunların geçerli olup olmadığını, belgenin kimin tarafından imzalandığını, ne zaman imzalandığını ve imzadan sonra belgenin değiştirilip değiştirilmediğini size bildirir." -title = "İmzaları Kontrol Etme" - [changeCreds] changeUsername = "Kullanıcı adınızı güncelleyin. Güncellemeden sonra oturumunuz kapatılacak." credsUpdated = "Hesap güncellendi" @@ -2737,12 +2936,16 @@ text = "Bu izinlerin değiştirilemez olmasını sağlamak için, sahip parolas [chat.actions] copy = "Mesajı kopyala" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling ajan seçenekleri" +assistant = "AI Assistant" clearChat = "Sohbeti temizle" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Bu işlem bulut kredilerinizi kullanacak" tooltip = "Ekrandan renk seç" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Renk seçin" [common] +actions = "Actions" back = "Geri" cancel = "İptal" close = "Kapat" +codeSample = "Code sample" collapse = "Daralt" confirm = "Onayla" continue = "Devam et" copied = "Kopyalandı!" copy = "Kopyala" done = "Bitti" +enterValue = "Enter value" error = "Hata" expand = "Genişlet" loading = "Yükleniyor..." next = "Sonraki" +open = "Open" preview = "Önizleme" previous = "Önceki" refresh = "Yenile" +remaining = "Kalan" retry = "Yeniden dene" save = "Kaydet" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Seçileni temizle" @@ -2988,6 +3199,7 @@ title = "Sıkıştırma Yöntemi" [compress.settings] desiredSize = "İstenen Dosya Boyutu" desiredSizePlaceholder = "Boyutu girin" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Sıkıştırma, dosya boyutunu azaltmanın kolay bir yoludur. Hedef boyut girmek ve kaliteyi bizim ayarlamamız için Dosya Boyutu'nu seçin. Sıkıştırma gücünü elle ayarlamak için Kalite'yi seçin." @@ -3190,6 +3402,24 @@ pdfOptions = "PDF Seçenekleri" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Önce bir kaynak biçimi seçin" settings = "Ayarlar" single = "Tek" @@ -3264,24 +3494,6 @@ description = "Bu çerezler, web sitesinin düzgün çalışabilmesi için gerek posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Gelişmiş izleme" -allInOneWorkspace = "Tümü bir arada PDF çalışma alanı (görüntüleyici, araçlar ve ajan)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Özel destek ve SLA'lar" -fasterThroughput = "10 kat daha hızlı işleme hızı" -fullyPrivateFiles = "Tamamen özel dosyalar" -largeFileProcessing = "Büyük dosya işleme" -orgWideAccess = "Kuruluş genelinde erişim kontrolleri" -premiumAiModels = "Premium AI modelleri" -prioritySupport = "Öncelikli destek" -privateDocCloud = "Özel Belge Bulutu" -ragFineTuning = "RAG + ince ayar" -secureApiAccess = "Güvenli API erişimi" -standardThroughput = "Standart işleme hızı" -unlimitedApiAccess = "Sınırsız API erişimi" -unlimitedSeats = "Sınırsız kullanıcı" - [crop] autoCrop = "Boşlukları otomatik kırp" noFileSelected = "Kırpmaya başlamak için bir PDF seçin" @@ -3346,6 +3558,7 @@ message = "Varsayılan PDF işleyicisi ayarlanamadı" title = "Hata" [defaultApp.prompt] +dontRemind = "Bana tekrar hatırlatma" message = "PDF dosyalarını açmak için varsayılan uygulamanız Stirling PDF olsun." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Ara..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Düzenle" @@ -3644,11 +3861,13 @@ shareSelected = "Seçileni Paylaş" sharing = "Paylaşım" showAll = "Tümünü Göster" showHistory = "Geçmişi Göster" +sortBy = "Sort files" sortByDate = "Tarihe Göre Sırala" sortByName = "Ada Göre Sırala" sortBySize = "Boyuta Göre Sırala" storage = "Depolama" storageState = "Depolama" +storageUsed = "Storage used" synced = "Eşitlendi" title = "PDF Dosyaları Yükle" toolChain = "Uygulanan Araçlar" @@ -3694,13 +3913,17 @@ addFiles = "Dosya ekle" addingFiles = "Dosyalar ekleniyor…" collapse = "Kenar çubuğunu daralt" customizeGroups = "Grupları özelleştir" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Başlamak için dosya açın" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Kenar çubuğunu genişlet" -files = "Dosyalar" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive yapılandırılmamış" leaveMyFiles = "Dosyalarımdan çık" +library = "PDF Kitaplığı" myFiles = "Dosyalarım" noFiles = "Henüz dosya yok" openFileManager = "Dosya yöneticisini aç" @@ -3708,37 +3931,36 @@ openFromComputer = "Bilgisayardan aç" openSettings = "Ayarları aç" other = "Diğer" recent = "Son" -search = "Ara" -searchPlaceholder = "Dosyalarda ara..." viewAll = "{{count}} dosyanın tümünü görüntüle" [fileSidebar.fileItem] closeViewer = "Görüntüleyiciyi kapat" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Görüntüleyicide aç" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Ekle" -addLabel = "Etiket ekle…" -categoryIconAria = "{{name}} için bir simge seç" -createCategory = "Kategori oluştur" -delete = "Kategoriyi sil" done = "Bitti" -hide = "Kategoriyi gizle" -newCategory = "Yeni kategori adı…" -removeLabel = "{{name}} etiketini kaldır" -rename = "Yeniden adlandır" reset = "Varsayılanlara sıfırla" -search = "Etiket ara…" -show = "Kategoriyi göster" subtitle = "Dosyalarınızı üst kategoriler halinde gruplandırın. Bir kategoriye mevcut veya yeni etiketler ekleyin, yeniden adlandırın ya da kendi kategorinizi oluşturun. Kategorilerinizin hiçbirinde olmayan dosyalar “Diğer” altında görünür." title = "Kenar çubuğu kategorileri" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Çalışma alanına ekle" addToWorkspaceCount = "{{count}} öğeyi çalışma alanına ekle" @@ -3749,7 +3971,6 @@ backToMyFiles = "Dosyalarıma geri dön" breadcrumbs = "Klasör yolu" cancel = "İptal" classification = "Sınıflandırma" -clearSearch = "Aramayı temizle" clearSelection = "Seçimi temizle" closeDetails = "Ayrıntıları kapat" create = "Oluştur" @@ -3781,6 +4002,7 @@ downloadAll = "Tümünü indir" downloadVersion = "Bu sürümü indir" dropOverlay = "Yüklemek için dosyaları bırakın" dropOverlaySub = "Dosyalar Yerel'de başlar. Onları bir klasöre düzenlemek için 'Taşı' veya 'Buluta kaydet'i kullanın." +duplicate = "Duplicate" file = "Dosya" fileInfo = "File info" fileMenu = "Dosya eylemleri" @@ -3808,13 +4030,12 @@ refresh = "Sunucudan yenile" remove = "Sil" removeVersion = "Bu sürümü sil" rename = "Yeniden adlandır" +renamed = "Yeniden adlandırıldı" renameFolder = "Klasörü yeniden adlandır" resizeFolderTree = "Klasör ağacını yeniden boyutlandır (yön tuşları, büyük adımlar için Shift; otomatik sığdırmak için çift tıklayın)" save = "Kaydet" saveToServer = "Sunucuya kaydet" saveToServerDisabledHint = "Bu sunucuda sunucuya kaydetme etkin değil. Yöneticinizden etkinleştirmesini isteyin." -search = "Ara" -searchPlaceholder = "Bu klasörde ve alt klasörlerde ara" selectAll = "Tümünü seç" selectAllHint = "Tümünü seçmek için tıklayın. İpucu: Tek tek dosya eklemek için Ctrl (veya Cmd) basılı tutun, aralık seçmek için Shift." selectedCount = "{{count}} seçildi" @@ -3874,6 +4095,10 @@ title = "Henüz bulut dosyası yok" hint = "Yüklemeden kaydedilen dosyalar burada kalır. Eklemek için bir dosya bırakın." title = "Yalnızca yerel dosya yok" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Açtığınız veya düzenlediğiniz dosyalar burada görünecek." title = "Henüz değiştirilen yok" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Klasör silinemedi." deleteFolderFailedDetail = "Klasör silinemedi: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Klasör görünümü güncellenemedi." folderAppearanceFailedDetail = "Klasör görünümü güncellenemedi: {{message}}" moveFilesFailed = "Dosyalar taşınamadı." @@ -3910,9 +4137,7 @@ count = "Dosyalar" folder = "Klasör" labels = "Etiketler" modified = "Değiştirildi" -name = "Ad" size = "Boyut" -toolHistoryAtVersion = "Kümülatif araç zinciri" totalSize = "Toplam boyut" type = "Tür" versionHistory = "Sürüm yolculuğu" @@ -3945,6 +4170,11 @@ localHint = "Sadece bu tarayıcıda depolanır" shared = "Paylaşılan" sharedHint = "Bağlantıyla sizinle paylaşılmış" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "En eskiden" modifiedDesc = "En yeniden" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Depolamada dosya yok. Önce bazı dosyaları açın." open = "Aç" openFile = "Dosya Aç" openFiles = "Dosyalar Aç" +selectFile = "Select {{name}}" selectFromStorage = "Depolamadan Seç" upload = "Yükle" uploadFile = "Dosya Yükle" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Tümü kaydedildi" +analyzingFields = "Analysing form fields..." +extractCsvError = "CSV çıkarılamadı" +extractXlsxError = "XLSX çıkarılamadı" +filled = "filled" flattenAfterFilling = "Doldurduktan sonra düzleştir" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "zor" +requiredFieldsError = "Lütfen tüm zorunlu alanları doldurun" rescanFields = "Alanları yeniden tara" rescanFormFields = "Form alanlarını yeniden tara" save = "Kaydet" +saveShortcut = "Kaydetmek için Ctrl+S" +unsavedChanges = "Kaydedilmemiş değişiklikler" [formFill.sidebar] close = "Kenar çubuğunu kapat" @@ -4309,6 +4552,11 @@ desc = "Algılanan başlığına göre bir PDF dosyasını otomatik olarak yenid tags = "otomatik_algılama,başlık_tabanlı,düzenle,yeniden_adlandır" title = "PDF'yi Otomatik Adlandır" +[home.autoRotate] +desc = "Her sayfanın yönünü algılayın ve otomatik olarak dik konuma döndürün." +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "Otomatik Döndür" + [home.autoSizeSplitPDF] tags = "otomatik,böl,boyut" title = "Otomatik Boyut/Sayıya Böl" @@ -4608,15 +4856,12 @@ hint = "PDF üzerine yerleştirmek için bir PNG, JPG, SVG veya başka bir görs label = "Görsel Yükle" placeholder = "Görsel dosyası seç" -[imageToPdf] -tags = "dönüşüm,img,jpg,fotoğraf,resim" - [imageToPDF] header = "Resimden PDF'e" title = "Resimden PDF'e" -[infoBanner] -dismiss = "Kapat" +[imageToPdf] +tags = "dönüşüm,img,jpg,fotoğraf,resim" [invite] acceptError = "Hesap oluşturulamadı" @@ -4647,11 +4892,9 @@ welcomeTitle = "Davet edildiniz!" [landing] addFiles = "Dosya Ekle" -heroSubtitle = "Başlamak için bir PDF bırakın veya mevcut bir PDF ekleyin." mobileUpload = "Mobil Cihazdan Yükle" openFromComputer = "Bilgisayardan aç" uploadFromComputer = "Bilgisayardan yükle" -workbenchEmptyStateHero = "Herhangi bir yere PDF bırakın" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Kullanıcı adını girin" failedToSignIn = "{{provider}} ile giriş yapılamadı: {{message}}" forgotPassword = "Şifrenizi mi unuttunuz?" loggingIn = "Giriş Yapılıyor..." -logIn = "Giriş Yap" login = "Giriş" magicLinkSent = "Sihirli bağlantı {{email}} adresine gönderildi! Giriş yapmak için e-postanızı kontrol edip bağlantıya tıklayın." mfaCode = "Kimlik doğrulama kodu" @@ -4715,26 +4957,10 @@ signInWith = "Şununla giriş yap" title = "Giriş Yap" unexpectedError = "Beklenmeyen hata: {{message}}" updatePassword = "Parolayı güncelle" -useEmailInstead = "E-posta ile giriş yap" useMagicLink = "Bunun yerine sihirli bağlantı kullan" username = "Kullanıcı adı" youAreLoggedIn = "Giriş yaptınız!" -[login.slides.edit] -alt = "PDF'leri düzenle" -subtitle = "PDF'leri sansürlemek, imzalamak, okumak ve işlemek için bir düzineden fazla araçla aradığınızı bulacaksınız." -title = "İstediğiniz bilgiyi göstermek/korumak için PDF'leri düzenleyin" - -[login.slides.overview] -alt = "Stirling PDF genel bakış" -subtitle = "PDF'ler için gizlilik odaklı bir bulut paketi; belgeleri dönüştürmenize, imzalamanıza, sansürlemenize ve yönetmenize olanak tanır, ayrıca 50+ başka güçlü araçla birlikte gelir." -title = "PDF ihtiyaçlarınızın tümü için tek adresiniz." - -[login.slides.secure] -alt = "PDF'leri güvenli hale getirin" -subtitle = "Parola ekleyin, içeriği sansürleyin ve sertifikaları kolayca yönetin." -title = "PDF'lerinizdeki hassas bilgileri koruyun" - [margin] large = "Büyük" medium = "Orta" @@ -4818,6 +5044,34 @@ uploadSuccess = "Yükleme Başarılı!" uploadSuccessMessage = "Görselleriniz aktarıldı." validating = "Oturum doğrulanıyor..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Fotoğrafları yüklemek için tarayın. Görseller otomatik olarak PDF'ye dönüştürülür." descriptionNoConvert = "Mobil cihazınızdan fotoğrafları yüklemek için tarayın." @@ -4836,8 +5090,13 @@ title = "Mobil Cihazdan Yükle" tags = "Çoklu Araç,Çoklu işlem,Arayüz,tıklama sürükleme,ön uç,istemci tarafı,etkileşimli,taşınabilir,taşı" title = "PDF Çoklu Araç" -[navbar] -search = "Ara" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Kimlik doğrulama başarılı olmadı. Bu pencereyi kapatıp tekrar deneyebilirsiniz." @@ -4966,6 +5225,7 @@ activeFiles = "Aktif Dosyalar görünümü, araca yüklediğini allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Kapat" cropSettings = "Artık kırpmak istediğimiz dosyayı seçtiğimize göre, PDF'nin kırpılacağı alanı seçmek için Kırp aracını yapılandırabiliriz." +dialogLabel = "Onboarding" fileCheckbox = "Dosyalardan birine tıklamak, onu işleme için seçer. Toplu işlemler için birden fazla dosya seçebilirsiniz." fileReplacement = "Değiştirilen dosya, Çalışma Alanı'nda orijinal dosyanın yerini otomatik olarak alır; böylece onu kolayca başka araçlardan da geçirebilirsiniz." filesButton = "Hızlı Erişim çubuğundaki Dosyalar düğmesi, araçları kullanmak üzere PDF yüklemenize olanak tanır." @@ -4976,6 +5236,7 @@ pinButton = "Dosyaların üzerinde araçları çalıştırdıktan sonra aktif ka results = "Araç çalışmayı bitirdikten sonra, Gözden Geçir adımı bu panelde sonuçların bir önizlemesini gösterir ve işlemi geri almanıza veya dosyayı indirmenize olanak tanır. " runButton = "Araç yapılandırıldıktan sonra bu düğme, aracı seçili tüm PDF'lerde çalıştırmanıza olanak tanır." selectCropTool = "Araçlardan birinin nasıl kullanılacağını göstermek için Kırp aracını seçelim." +stepOf = "Adım {{current}} / {{total}}" toolInterface = "Burası Kırp aracının arayüzü. Gördüğünüz gibi, üzerinde çalışacak PDF dosyaları henüz eklemediğimiz için pek bir şey yok." workbench = "Burası Çalışma Alanı - PDF'lerinizi görüntüleyip düzenlediğiniz ana alandır." wrapUp = "Her şey hazır! Uygulamanın ana alanlarını ve bunları nasıl kullanacağınızı öğrendiniz. Bu turu tekrar görmek istediğinizde istediğiniz zaman Yardım düğmesine tıklayın." @@ -4988,6 +5249,26 @@ showMeAround = "Bana etrafı göster" skipForNow = "Şimdilik atla" skipTheTour = "Turu atla" +[onboarding.checklist] +dismiss = "Kapat" +title = "Stirling PDF'yi kurun" + +[onboarding.checklist.downloadDesktop] +description = "Stirling'i makinenizde yerel olarak çalıştırın" +title = "Masaüstü için Stirling'i indir" + +[onboarding.checklist.inviteTeam] +description = "Ekibinizle iş birliği yapın" +title = "Ekip üyelerini davet et" + +[onboarding.checklist.shareAnalytics] +description = "Stirling'i geliştirmeye yardımcı olun" +title = "Anonim kullanım verilerini paylaş" + +[onboarding.checklist.takeTour] +description = "Kısa bir gezintiyle Stirling'in nasıl çalıştığını görün" +title = "Tura katıl" + [onboarding.desktopInstall] body = "Stirling, bir masaüstü uygulaması olarak en iyi şekilde çalışır. Çevrimdışı kullanabilir, belgelere daha hızlı erişebilir ve düzenlemeleri bilgisayarınızda yerel olarak yapabilirsiniz." selectOs = "İşletim sistemini seç" @@ -4996,7 +5277,21 @@ titleWithOs = "{{osLabel}} için indir" [onboarding.mfa] authenticationCode = "Kimlik doğrulama kodu" +description = "Bir kimlik doğrulama uygulamasını bağlayarak hesabınızı güvence altına alın. QR kodunu tarayın veya kurulum key'ini girin, ardından tamamlamak için 6 haneli kodu onaylayın." +enable = "MFA'yı etkinleştir" +enableError = "İki faktörlü kimlik doğrulama etkinleştirilemiyor. Kodu kontrol edip tekrar deneyin." +enterCode = "Uygulamanızdaki 6 haneli kodu girin." +enterCodeError = "Devam etmek için kimlik doğrulama kodunu girin." +logout = "Çıkış yap" +openAuthenticator = "Google Authenticator, Authy veya 1Password'ü açın." qrCodeLoading = "QR kodunuz oluşturuluyor…" +regenerateQrCode = "QR kodunu yeniden oluştur" +scanQrCode = "QR kodunu tarayın veya aşağıdaki kurulum key'ini girin." +setupError = "İki faktörlü kurulum başlatılamıyor. Lütfen tekrar deneyin." +setupKey = "Kurulum key'i (manuel giriş)" +stepByStep = "Adım adım" +success = "MFA etkinleştirildi. Şimdi devam edebilirsiniz." +title = "Çok Faktörlü Kimlik Doğrulama Kurulumu" [onboarding.planOverview] adminBodyLoginDisabled = "Oturum açma modunu etkinleştirdiğinizde kullanıcıları yönetebilir, ayarları yapılandırabilir ve sunucu sağlığını izleyebilirsiniz. Sunucunuzdaki ilk {{freeTierLimit}} kişi Stirling'i ücretsiz kullanır." @@ -5005,6 +5300,11 @@ adminTitle = "Yönetici Özeti" userBody = "Ekip arkadaşlarını davet edin, roller atayın ve belgelerinizi tek bir güvenli çalışma alanında düzenli tutun. Tek başına kullanımdan büyümeye hazır olduğunuzda oturum açma modunu etkinleştirin." userTitle = "Plan Özeti" +[onboarding.processorIntro] +body = "Stirling artık İlkeler çalıştırıyor — her belge geldiğinde onu sınıflandıran, güvenceye alan ve işleyen otomatik kurallar. Bunları ayarlayın ve çalışmaları Processor içinde izleyin." +cta = "Processor'ı incele" +title = "Stirling Processor'ı inceleyin" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Ön ödemeli yıl" + +[payg.prepaid.banner] +exhausted = "Ön ödemeli kapasiteniz tükendi; ölçümlü faturalandırma yeniden başladı. İndirimi korumak için yükleme yapın." +expiring_one = "Ön ödemeli yılınız {{count}} gün içinde sona eriyor. Kullanılmayan kapasite sona erer — yeni bir yıl başlatmak için yükleme yapın." +expiring_other = "Ön ödemeli yılınız {{count}} gün içinde sona eriyor. Kullanılmayan kapasite sona erer — yeni bir yıl başlatmak için yükleme yapın." +low = "Ön ödemeli kapasitenizin %{{pct}} seviyesine düştünüz. İşlemenin kesintisiz kalması için istediğiniz zaman yükleme yapın." + +[payg.prepaid.card] +subtitle = "Ölçümlü faturalandırmadan önce ve harcama sınırınızın dışında kullanılır." +title = "Ön ödemeli kapasite" + +[payg.prepaid.meter] +capSuffix = "{{total}} ön ödemeli krediden" +expires = "Sona erme {{date}}" + +[payg.prepaid.state] +exhausted = "Tükendi" +healthy = "Bol miktarda kaldı" +low = "Azalıyor" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF'den PDF/A'ya" tags = "arşiv,uzun vadeli,standart,dönüşüm,saklama,koruma" title = "PDF'den PDF/A'ya" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "yazdırma,standart,dönüştürme,üretim,baskı öncesi,arşiv" title = "PDF'den PDF/X'e" @@ -5822,6 +6151,7 @@ small = "500 Kredi" xsmall = "100 Kredi" [plan.availablePlans] +currency = "Billing currency" subtitle = "İhtiyaçlarınıza uygun planı seçin" title = "Mevcut Planlar" @@ -5937,49 +6267,12 @@ keyDescription = "E-postanızdaki lisans anahtarını yapıştırın" success = "Lisans Etkinleştirildi!" successMessage = "Lisansınız başarıyla etkinleştirildi. Bu pencereyi şimdi kapatabilirsiniz." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "uygulandı" -enforcing = "Uygulanıyor..." -failed = "Uygulama başarısız oldu" outputsUnavailable = "Politika çıktıları artık indirilemiyor." partialOutputsUnavailable = "Bazı politika çıktıları artık indirilemiyor." -retrying = "Meşgul, yeniden deneniyor..." runNotFound = "Uygulama çalışması artık bulunamıyor." -step = "adım {{current}}/{{total}}" timedOut = "İşlem tamamlanmadan önce uygulama zaman aşımına uğradı." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Kapat" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "{{names}} uygulanıyor" applyingProgress = "{{names}} uygulanıyor ({{done}} / {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} dosyadan {{failures}} işlenemedi ve olduğu gibi dış failureTitle = "Tam uygulama olmadan dışa aktarıldı" printPolicyAppliedBody = "Bu PDF bir politikayı karşılamak için güncellendi. Değişiklikleri gözden geçirin, ardından yeniden yazdırın." printPolicyAppliedTitle = "Yazdırmadan önce politika uygulandı" -queued = "+{{count}} sıraya alındı" successTitle = "{{names}} uygulandı" summaryMore = "{{first}}, {{second}} ve {{more}} daha" summaryTwo = "{{first}} ve {{second}}" -[policies.enforcement.triggerVerb] -convert = "Dönüştürmeden önce uygulanıyor" -default = "Uygulanıyor" -export = "Dışa aktarmadan önce uygulanıyor" -input = "İçe aktarırken uygulanıyor" -print = "Yazdırmadan önce uygulanıyor" - -[policies.field] -accessLog = "Erişim günlüğü" -archiveAfter = "Şundan sonra arşivle" -auditTrail = "Denetim izi" -belowThreshold = "Eşik altında" -destination = "Hedef" -frameworks = "Çerçeveler" -immutableHold = "Değiştirilemez tutma" -keepFor = "Şu kadar süre tut" -minConfidence = "Min güven" -notify = "Yönlendirmede bildir" -onViolation = "Uyumsuz olduğunda" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Ekle" -addPlaceholder = "Etiket ekle…" -customNote = "Ekibiniz için özelleştirildi." -defaultNote = "Ekibinizle paylaşılan yerleşik varsayılan kullanılıyor." -duplicate = "\"{{name}}\" zaten mevcut." -edit = "Etiketleri düzenle" -empty = "Henüz etiket yok." -export = "JSON dışa aktar" -iconAria = "{{name}} için bir simge seç" -import = "JSON içe aktar" -importError = "Bu dosya içe aktarılamadı." -managedNote = "Ekip etiketleri ekip lideriniz tarafından yönetilir." -modalSubtitle = "Tüm ekibinizle paylaşılır. Sınıflandırıcı her belgeye uyan etiketleri seçer." -modalTitle = "Sınıflandırma etiketleri" -pickIcon = "{{label}} simgesini kullan" -removeAria = "{{name}} etiketini kaldır" -resetToDefault = "Varsayılana sıfırla" -saveForTeam = "Ekip için kaydet" -saving = "Kaydediliyor…" -sectionLabel = "Sınıflandırma etiketleri" -startFromScratch = "Sıfırdan başla" -teamCount = "ekip etiketleri" -tooLong = "Etiketler en fazla {{max}} karakter olabilir." -ungrouped = "Gruplandırılmamış" -view = "Etiketleri görüntüle" +categoryCount = "kategori" +hideCategory = "Kategoriyi gizle" +labelCount = "etiket" +sharedNote = "Bu etiketler yerleşiktir ve tüm ekibiniz genelinde paylaşılır." +showCategory = "Kategoriyi göster" [policies.pii] account = "Hesap numaraları (etiketli)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "ABD yönlendirme numaraları (ABA)" ssn = "Sosyal Güvenlik numaraları" -[policies.settings] -noneExport = "Şu anda dışa aktarmada çalışan politika yok." -noneUpload = "Şu anda yüklemede çalışan politika yok." -onExport = "Dışa aktarmada" -onUpload = "Yüklemede" -reorderHandle = "Yeniden sıralamak için sürükle" -runOrderDesc = "Aynı tetikleyicide birden fazla politika çalıştığında, bu sırayla çalışırlar — her biri önceki politikanın çıktısı üzerinde. Yeniden sıralamak için sürükleyin." -title = "Politika ayarları" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Politika seçenekleri" -policySettings = "Politika ayarları" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Başarısız politikaları yeniden dene ({{count}})" -rowProgress = "{{total}} dosyadan {{completed}} tanesi işlendi" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Politika nedir?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Belgedeki hassas ayrıntıları — Sosyal Güvenlik ve kart numaraları gibi — otomatik olarak bulur ve okunamayacak şekilde karartır." -sanitize = "Dosyadan gizli JavaScript'i kaldırır; böylece biri açtığında hiçbir şey otomatik olarak çalışmaz." -watermark = "Her sayfaya görünür bir işaret (ör. \"Gizli\") basar." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Kapat" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} uygulanıyor..." badgeRan = "{{name}} politikası bu dosyada çalıştı" +badgeRunning = "{{name}} çalışıyor..." blockingAction = "Politika uygulanırken {{action}} engellendi, lütfen bekleyin..." dismiss = "Katmanı kapat" enforcingTitle = "Politika uygulanıyor..." @@ -6151,6 +6316,7 @@ viewAnyway = "Dosyayı görüntüle (politika hâlâ uygulanıyor)" [policyOption] autoRedactPhi = "PHI'yi otomatik sansürle" blockExport = "Dışa aktarmayı engelle" +ccpa = "CCPA" contracts = "Sözleşmeler" documents = "Belgeler" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor planı" subscribed = "Processor planı" unlinked = "Bağlı değil" -[portal.agentBuilder] -bootstrapFromDocument = "Belgeden başlat" -sectionsAriaLabel = "Agent Builder bölümleri" -selectorAriaLabel = "Ajanlar" -subtitle = "Belgelerinizi sınıflandıran, ayıklayan ve yönlendiren AI ajanlarını tasarlayın, test edin ve yayınlayın. Senaryoları tanımlayın, araç erişimini sınırlayın, bir altın set çalıştırın ve bir sürüm yayınlayın." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "İptal" -dropzoneText = "Örnek bir belge seçin (PDF veya görüntü)" -lead = "Örnek bir belge bırakın; geliştirebileceğiniz senaryolar ve bir ayıklama şeması önereceğiz. Siz inceleyene kadar hiçbir şey yayınlanmaz." -submit = "Ajanı başlat" -subtitle = "Temsili bir dosyadan yeni bir ajanı başlatın" -title = "Bir belgeden başlat" - -[portal.agentBuilder.empty] -description = "Senaryolarını ve ayıklama şemasını başlatmak için örnek bir belgeden bir ajan oluşturun, ardından geliştirin ve yayınlayın." -title = "Henüz ajan yok" - -[portal.agentBuilder.evals] -casesPassing = "Geçen vakalar" -columnCase = "Eval vakası" -columnLatency = "Gecikme" -columnResult = "Sonuç" -fail = "başarısız" -goldenSetPassRate = "Altın set geçme oranı" -latencyMs = "{{ms}} ms" -notRun = "çalıştırılmadı" -pass = "başarılı" -passRate = "Geçme oranı" -runEvals = "Eval'ları çalıştır" - -[portal.agentBuilder.evals.empty] -description = "Eval'lar senaryolarınızı tekrarlanabilir bir altın sete dönüştürür. Geçme oranını zaman içinde yakalamak ve yayınları buna göre kapılamak için yükseltin." -title = "Henüz altın set yok" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "altın setler genelinde" -activeAgents = "Aktif ajanlar" -avgPassRate = "Ort. eval geçme oranı" -fleetWide = "filo genelinde" -latestPublished = "Son yayınlanan" -scenarios = "Senaryolar" -testCases = "test vakaları" -totalDescription_one = "toplam {{count}}" -totalDescription_other = "toplam {{count}}" - -[portal.agentBuilder.scenarios] -add = "Ekle" -addScenario = "Senaryo ekle" -enable = "Etkinleştir" -expectationLabel = "Beklenen davranış" -expectationPlaceholder = "Ajanın ne yapması gerektiği" -inEval = "eval'da" -mute = "Sessize al" -muted = "sessize alındı" -nameLabel = "Ad" -namePlaceholder = "örn. Uyumluluk eskalasyonu" - -[portal.agentBuilder.tabs] -evals = "Eval'lar" -scenarios = "Senaryolar" -tools = "Araçlar" -versions = "Sürümler" - -[portal.agentBuilder.tools] -broadAccess = "Geniş erişim" -deniedHint = "Seçili araçlar engellenir. Diğer her şey çağrılabilir kalır." -deniedTools = "Reddedilen araçlar" -governanceGate = "Araç yönetişimi Enterprise planında mevcuttur." -restricted = "Kısıtlı" -restrictedAccess = "Kısıtlı araç erişimi" -restrictedDescription = "Aşağıda reddettikleriniz dışındaki tüm araçlara izin verin." - -[portal.agentBuilder.versions] -current = "geçerli" -historyGate = "Tam sürüm geçmişi ve geri alma Enterprise planında mevcuttur." -publish = "Yayınla" -rollBack = "Geri al" - [portal.assistant] close = "Asistanı kapat" error = "Asistana ulaşılamadı." @@ -6365,9 +6451,21 @@ typing = "Yazıyor" loading = "Yükleniyor" redirectingToEditor = "Editöre yönlendiriliyor..." +[portal.billing.activation] +maybeLater = "Belki sonra" +title = "Processor'ı aç" + +[portal.billing.activation.payg] +sub = "Aylık bir limit belirleyin. Çalıştırdığınız kadar ödeyin." +title = "Kullandıkça öde" + +[portal.billing.activation.prepay] +badge = "2 ay ücretsiz" +sub = "Tek fatura, kart veya banka transferi." +title = "Bir yıl için ön ödeme yap" + [portal.billing.checkout] noClientSecret = "Edge function client_secret döndürmedi." -subtitle = "Ücretsiz Editor planı hakkınızı aşarak devam etmek için bir kart ekleyin. Gerisini Stripe halleder." title = "Processor planını aç" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Ödemeniz başarılı oldu, ancak aktivasyon normalden biraz daha uzun s close = "Kapat" title = "Neredeyse hazır" +[portal.billing.checkout.cap] +amountAria = "Aylık harcama limiti" +back = "Geri" +continue = "Ödemeye devam et" +error = "Harcama limitiniz ayarlanamadı" +finePrint = "Faturalar aylık olarak düzenlenir. İstediğiniz zaman iptal edin." +noLimit = "Limit yok" +note = "Limitinizin ötesinde asla faturalandırılmazsınız. İşleme sadece duraklar." +perMonth = "/ ay" +title = "Harcama limitinizi belirleyin" + +[portal.billing.checkout.card] +fields = "Kart numarası · AA / YY · CVC · ZIP" +label = "Kart bilgileri" +note = "Kart bilgileri Stripe tarafından toplanır. Stirling hiçbir zaman PAN veya CVC saklamaz." + [portal.billing.checkout.error] title = "Checkout başlatılamadı" @@ -6383,10 +6497,9 @@ body = "Ödemeniz gerçekleşti. Bağlı örneklerinizde ölçümlü işlemeyi a hint = "Lütfen bu pencereyi açık tutun." title = "Processor planınız etkinleştiriliyor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "uygulama içi checkout'u etkinleştirmek için portal env içinde ayarlayın." -bodyBefore = "Ayarla" -title = "Stripe yapılandırılmamış" +[portal.billing.checkout.pay] +limit = "Aylık harcama limiti {{amount}}/ay. Limitte işleme duraklar." +limitNoCap = "Harcama limiti yok. İşleme duraklamaz. Faturalar aylık olarak düzenlenir." [portal.billing.enterpriseUpsell] cta = "Enterprise teklifinizi oluşturun" @@ -6409,10 +6522,12 @@ currentPlan = "Geçerli plan" freeForever = "Sonsuza kadar ücretsiz" noTeamResolved = "Cüzdanınızda henüz bir ekip çözümlenmedi — yenileyin ve tekrar deneyin." ownerOnly = "Processor planını yalnızca ekip sahibi açabilir." +payInvoice = "Tamamlamak için faturayı öde" planName = "Editor" ssoIncluded = "SSO dahil" switchOnProcessor = "Processor'ı aç →" unlimitedUsers = "Sınırsız kullanıcı" +viewQuote = "Teklifi görüntüle" [portal.billing.invoices] columnAmount = "Tutar" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Doğrudan API istekleri" segmentApiLabel = "API" segmentAutomationDesc = "Otomasyonlar ve pipeline'lar" segmentAutomationLabel = "Otomasyon" +sizeMultiplier = "{{formatted}} PDF bir boyut çarpanı kullandı" +summary = "{{unique}} benzersiz · {{units}} sayaç birimi · PDF başına ort. {{avg}}" +summaryNoRate = "{{unique}} benzersiz · {{units}} sayaç birimi" unit = "ölçümlü PDF'ler" +unitsPending_one = "{{units}} sayaç birimi bağlı örneklerden senkronizasyon bekliyor" +unitsPending_other = "{{units}} sayaç birimi bağlı örneklerden senkronizasyon bekliyor" + +[portal.billing.prepaid] +capSuffix = "{{total}} ön ödemeli krediden" +expires = "Sona erme {{date}}" +eyebrow = "Ön ödemeli kapasite" +note = "Ölçümlü faturalandırmadan önce ve harcama limitinizin dışında kullanılır." +topUp = "Yükleme yap" + +[portal.billing.prepaid.buy] +back = "Geri" +calcTitle = "Yıllık ödemenizi hesaplayın" +cancel = "İptal" +cancelPurchase = "Satın almayı iptal et" +close = "Kapat" +continueToPayment = "Ödemeye devam et" +finalise = "Sonlandır" +invoiceErrorTitle = "Fatura oluşturulamadı" +loadingTitle = "Teklifiniz yükleniyor" +payTitle = "Yılınız için ödeme yapın" +step = "Adım {{current}} / {{total}}" + +[portal.billing.prepaid.calc] +change = "Değiştir" +deployRow = "Dağıtım" +done = "Bitti" +enterpriseHint = "Bu kurumsal ölçektir. Bu hacimde kurumsal tarifeler tüm self servis indirimlerden daha avantajlıdır. Tarife kilidi, şartlar ve dakikalar içinde teklif." +governanceRow = "Yönetişim" +governanceValue = "{{label}} · {{count}} ilke" +handlesLabel = "Processor'ınız" +handlesValue = "{{volume}} kredi / ay işler" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} ilke" +poolCaption = "Yıl için {{credits}} kredilik tek bir havuz. Yoğun aylar hafif aylardan ödünç alır." +priceLabel = "Yılınız · 10 ay fiyatına 12 ay" +rateUnknown = "Kesin fiyatı ödeme sırasında göstereceğiz." +savings = "{{amount}} tasarruf edersiniz · 2 ay ücretsiz." +sizingRow = "Boyutlandırma" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Toplam kullanıcı" + +[portal.billing.prepaid.consent] +label = "Ön ödemeli kapasitem kullanıldığında veya 12 ay sonra sona erdiğinde, iptal etmediğim sürece işlemenin standart ölçümlü kullandıkça öde tarifesiyle (harcama limitime kadar) otomatik olarak devam edeceğini ve faturalandırma portalından istediğim zaman iptal edebileceğimi anlıyorum." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Stirling tarafından yönetilir. Dakikalar içinde canlı." +cloudValue = "Stirling Cloud · Yönetilen" +selfhost = "Kendi barındırılan" +selfhostDesc = "Özel altyapıda çalışır. Aynı tarife." +selfhostValue = "Kendi barındırılan · Özel altyapı" + +[portal.billing.prepaid.offer] +cta = "Teklifi incele" +subtitle = "Bir yıllık PDF işleme için ön ödeme yapın ve iki ay ücretsiz kazanın; ölçümlü faturalandırmadan önce, harcama limitinizin dışında kullanılır." +title = "10 ay fiyatına 12 ay alın" + +[portal.billing.prepaid.pay] +accountName = "Hesap sahibi adı" +accountNamePlaceholder = "örn. Jane Smith" +companyName = "Şirket adı (isteğe bağlı)" +companyNamePlaceholder = "örn. Acme Inc." +downloadInvoice = "Faturayı indir" +payOnline = "Online öde" +poNumber = "PO numarası (isteğe bağlı)" +poNumberPlaceholder = "örn. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Gelişmiş" +advancedDesc = "Pipelines işlemenizin çoğunu yürütür." +none = "Yok" +noneDesc = "Henüz pipelines çalıştırılmıyor. İstediğiniz zaman açın." +standard = "Standart" +standardDesc = "Birkaç pipeline gelen PDF'leri yeniden işler." + +[portal.billing.prepaid.posture] +essentials = "Temel" +essentialsDesc = "Sınıflandırma, varsayılan ve ayrıca Paylaşım" +governed = "Yönetilen" +governedDesc = "Güvenlik ve Yönlendirme ekler" +regulated = "Düzenlenmiş" +regulatedDesc = "Uyumluluk, Saklama, Alım dahil her kategori" + +[portal.billing.prepaid.review] +download = "Teklifi indir (PDF)" +downloading = "Hazırlanıyor…" +share = "onay için paylaş" + +[portal.billing.prepaid.size] +compact = "Kompakt" +compactDesc = "25 MB altı dosyalar, veri ücreti yok" +heavy = "Ağır" +heavyDesc = "Taranmış veya görüntü ağırlıklı, genellikle 50 MB+" +standard = "Standart" +standardDesc = "Çoğunlukla küçük, bazı taramalar 25 MB'ı aşar" + +[portal.billing.prepaid.state] +exhausted = "Tükendi" +healthy = "Bol miktarda kaldı" +low = "Azalıyor" [portal.billing.spendLimit] adjustLimit = "Limiti ayarla" @@ -6494,6 +6713,8 @@ label = "Aşması öngörülüyor." [portal.billing.spendThisMonth] eyebrow = "Bu ayki harcama" +freeRemaining_one = "{{formatted}} ücretsiz PDF kaldı" +freeRemaining_other = "{{formatted}} ücretsiz PDF kaldı" processed_one = "{{formattedCount}} PDF işlendi." processed_other = "{{formattedCount}} PDF işlendi." processedWithRate_one = "{{formattedCount}} PDF işlendi, her biri {{rate}}." @@ -6510,6 +6731,7 @@ reachedTitle = "Aylık harcama limitine ulaşıldı" title = "Stripe portalı açılamadı" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "{{allowance}} ücretsiz PDF hakkından kullanıldı" capSuffix_other = "{{allowance}} ücretsiz PDF hakkından kullanıldı" eyebrow = "Processor denemesi" @@ -6534,6 +6756,376 @@ signature = "imza" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Yeni bağlantı" +createTitleFor = "{{name}} bağla" +delete = "Sil" +edit = "Düzenle" +editTitle = "Bağlantıyı düzenle" +unsupportedType = "Bu bağlantı türü burada düzenlenemez." + +[portal.connections.categories.advanced] +description = "Henüz desteklemediğimiz bir hizmet için." +label = "Gelişmiş" + +[portal.connections.categories.audit] +description = "Bir belgeye ne olduğunu, denetçilerinizin zaten baktığı yerde kaydedin." +label = "Denetim ve uyumluluk günlükleme" + +[portal.connections.categories.notify] +description = "Başka bir sisteme bir şey olduğunu bildirin." +label = "Bildirim ve iş akışı" + +[portal.connections.categories.security] +description = "Bir belge taşınmadan önce tarayın veya etiketleyin." +label = "Güvenlik ve sınıflandırma" + +[portal.connections.categories.signing] +description = "Bir belgeyi imzalanması veya noter onayı için gönderin." +label = "E-imza ve noter onayı" + +[portal.connections.categories.storage] +description = "İşlenen belgelerin sona erdiği yer - bir bucket, drive, ticket veya sayfa." +label = "Dosya ve ek" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling bunu şifreli olarak saklar ve bir daha asla göstermez." +label = "Erişim token'ı" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Bu bağlantıyı kullanan adımlar yalnızca bu URL altındaki yolları çağırabilir, asla başka bir host'u değil." +label = "Sunucu URL'si" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Parola" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Kullanıcı adı" + +[portal.connections.commonFields.webhookUrl] +helperText = "Hizmetin size verdiği URL'yi yapıştırın. Yalnızca bunun altındaki yollar çağrılabilir." +label = "Webhook URL" + +[portal.connections.fields] +name = "Bağlantı adı" +namePlaceholder = "örn. Production" + +[portal.connections.picker] +cancel = "İptal" +createNew = "Yeni bağlantı..." +placeholder = "Bir bağlantı seçin" +save = "Bağlantıyı kaydet" + +[portal.connections.picker2] +back = "Entegrasyonu değiştir" +heading = "Neyi bağlamak istiyorsunuz?" +noResultsBody = "Bunu henüz desteklemiyoruz - bize bildirin, eklemeyi değerlendirelim." +noResultsTitle = "\"{{query}}\" ile eşleşen yok" +searchPlaceholder = "Entegrasyonlarda ara - \"sign\", \"ocr\" veya bir ürün adı deneyin" + +[portal.connections.types.api] +description = "Bir ilkeden herhangi bir HTTP API çağırın. URL'yi, kimlik doğrulamayı ve payload'ı siz seçersiniz." +label = "Özel API" + +[portal.connections.types.api.fields.authType] +label = "Kimlik doğrulama" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Kullanıcı adı ve parola" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Özel bir header içinde token" + +[portal.connections.types.api.fields.authType.options.none] +label = "Yok" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Token için oturum aç" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Bu bağlantıyı kullanan adımlar yalnızca bu URL altındaki yolları çağırabilir, asla başka bir host'u değil." +label = "Temel URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header adı" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Oturum açma yolu" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Parola" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "API bir indirme bağlantısıyla yanıt verirse, yalnızca bu host'lar getirilebilir. Virgülle ayırın. Yalnızca temel URL'nin kendi host'una izin vermek için boş bırakın." +label = "İzin verilen sonuç host'ları" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Token'ı bu header olarak gönder" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Oturum açma çağrısının token'ı döndürdüğü yanıt header'ı, örn. X-Auth-Token." +label = "Token yanıt header'ı" + +[portal.connections.types.api.fields.username] +label = "Kullanıcı adı" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Çalıştırdığınız bir sunucuda belgeleri kötü amaçlı yazılımlara karşı tarayın - ağınızdan hiçbir şey çıkmaz." +label = "ClamAV (kendi barındırılan)" + +[portal.connections.types.cloudmersive] +description = "Bir ilkede belgeler için virüs ve içerik taraması." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Yalnızca bilinen kötü amaçlı yazılımları değil; macro, yürütülebilir dosya, script veya gömülü nesne içeren belgeleri engelleyin." +label = "Cloudmersive Gelişmiş Tarama" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "İşlenen belgeleri bir Confluence sayfasına ekleyin." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Belgeleri elektronik imza ve noter onayı için ConsignO Cloud'a (Notarius) gönderin." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "/api/v1 dahil ConsignO Cloud ortamınız." +label = "Sunucu URL'si" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Parola" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Yalnızca kuruluşunuz tenant kullanıyorsa gereklidir." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Kullanıcı adı" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Bir ilke bir belgeyi işlediğinde Discord'a mesaj gönderin." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Bir ilke bir belgeyi her ele aldığında Elasticsearch'te bir denetim olayı indeksleyin." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Yeni belgeler için bir FTP veya FTPS sunucusunu yoklayın." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Bağlantı modu" + +[portal.connections.types.ftp.fields.passive.options] +active = "Aktif" +passive = "Pasif (önerilen)" + +[portal.connections.types.ftp.fields.security] +helperText = "Düz FTP kimlik bilgilerini ve dosyaları şifrelemeden gönderir; sunucu destekliyorsa FTPS seçin." +label = "Şifreleme" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Yok (düz FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Bir ilke çalıştığında bir Google Chat alanına mesaj gönderin." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "İşlenen belgeleri bir Jira issue'ya ekleyin." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Bir ilke çalıştığında bir belgeyi veya bildirimi email ile gönderin." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "İşlenen belgeleri Nextcloud'a dosyalayın." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "Uygulama parolası" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Çalıştırdığınız bir Presidio sunucusunu kullanarak belge metnindeki kişisel verileri algılayın." +label = "Presidio (kendi barındırılan)" + +[portal.connections.types.purview] +description = "PDF'lerde Microsoft Purview duyarlılık etiketlerini okuyun ve uygulayın." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "İsteğe bağlı. Yalnızca etiketleri ada göre seçmek için gereklidir; etiketleme onsuz çalışır." +label = "Uygulama (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Her etikete SiteId olarak damgalanır, böylece Purview uyumlu araçlar hangi kuruluşun uyguladığını bilir." +label = "Dizin (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Kaynakların okuyabileceği ve pipeline çıktılarının yazabileceği bir S3 bucket." +label = "S3 depolama" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Erişim key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS için boş bırakın. MinIO veya başka bir S3 uyumlu depo için bunu ayarlayın." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Bölge" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Bir ilke çalıştığında bir belgeyi veya bildirimi email ile gönderin." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Yeni belgeler için bir SFTP (SSH) sunucusunu yoklayın." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "İsteğe bağlı. Sunucunun SHA-256 host key fingerprint'i (`ssh-keyscan host | ssh-keygen -lf -` çıktısından). Ayarlandığında yalnızca bu key'i sunan bir sunucu kabul edilir; boş bırakıldığında ilk bağlantıda görülen key sabitlenir ve sonraki değişiklikler reddedilir." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Bunun yerine private key ile kimlik doğruluyorsanız boş bırakın." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "İsteğe bağlı. Parola yerine key ile kimlik doğrulamak için bir OpenSSH private key yapıştırın." +label = "Private key" + +[portal.connections.types.slack] +description = "Bir ilke bir belgeyi işlediğinde Slack'e mesaj gönderin." +label = "Slack" + +[portal.connections.types.smb] +description = "Bir SMB / CIFS ağ paylaşımını izleyin (Windows veya Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Ağ paylaşımı" + +[portal.connections.types.smb.fields.domain] +helperText = "İsteğe bağlı. Sunucunuz kullanıyorsa Windows domain'i veya çalışma grubu." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Paylaşım" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Bir ilke bir belgeyi her ele aldığında Splunk'a denetim olayı gönderin." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Bir Sumo Logic HTTP collector'a denetim olayı gönderin." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Bir ilke çalıştığında Teams kanalına mesaj gönderin." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Her belgeyi veya yalnızca ayrıntıları seçtiğiniz bir URL'ye gönderin." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Bir ilkeden Zap veya Make senaryosunu tetikleyin." +label = "Zapier ve Make" + +[portal.docs] +browse = "Dokümanlara göz at" +viewSource = "Kaynağı GitHub'da görüntüle" + [portal.docs.authentication] codeCaption = "her istek" eyebrow = "BAŞLANGIÇ" @@ -6624,6 +7216,9 @@ eyebrow = "BECERİLER" lead = "Ajanınızın tek bir araç olarak çağırdığı paketlenmiş, adlandırılmış yetenekler. Her beceri, eval'ları ekli deterministik bir op zinciridir." title = "Ajan becerileri" +[portal.docs.toc] +title = "Bu sayfada" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERANSI" @@ -6697,6 +7292,8 @@ type = "Tür" user = "Kullanıcı" [portal.documents.queue.empty] +connectSource = "Bir kaynak bağla" +createPipeline = "Bir pipeline oluştur" description = "Kaynaklar belgeleri pipeline'larınıza besledikçe, inceleme için burada görünürler." title = "Kuyrukta belge yok" @@ -6712,11 +7309,11 @@ editorAction = "Düzenleyici" empty = "Bu filtreyle eşleşen belge yok." rowActions = "Satır eylemleri" sensitiveLabel = "Hassas" -sensitiveTitle = "Hassas — erişim gerekli" [portal.documents.table.columns] action = "Pipeline / Eylem" document = "Belge" +labels = "Labels" product = "Ürün" status = "Durum" time = "Zaman" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Son görülme" region = "Bölge" status = "Durum" +target = "Target" version = "Sürüm" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Bu görünüm beklenmeyen bir hatayla karşılaştı. Tekrar dene retry = "Tekrar dene" title = "Bu sayfada bir şeyler ters gitti" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Geri" +body = "Masaüstü uygulamasını indirin veya sunucuyu kendi ortamınızda barındırın." +done = "Bitti" +guide = "{{name}} kılavuzunu aç" +openInBrowser = "Bunun yerine tarayıcıda aç" +sectionDesktop = "Masaüstü uygulaması" +sectionSelfHosted = "Kendi barındırılan" +title = "Stirling PDF Editor'ı yükleyin" + +[portal.home.download.docker] +detailBody = "Resmi image'ı sunucunuzda çalıştırın." +note = ":8080 üzerinde açılır. Varsayılan giriş: admin / stirling." +tagline = "Sunucuyu bir container içinde çalıştırın" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Resmi Helm chart'ını kurun." +note = "Ayarları kalıcı hale getirmek için /configs için bir PVC ekleyin." +tagline = "Helm ile bir cluster'a dağıtın" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Dağıtımınız için native derlemeler." +guideBtn = "Linux kurulum seçeneklerini görüntüle" +note = "Yaygın dağıtımlar için AppImage, .deb ve .rpm derlemeleri mevcuttur." +tagline = "Dağıtımınız için native masaüstü uygulaması" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Veya Homebrew ile" +detailBody = "Yükleyiciyi çalıştırın veya Homebrew kullanın." +downloadBtn = "macOS için indir" +tagline = "Apple silicon ve Intel için native uygulama" +title = "macOS" + +[portal.home.download.manual] +detailBody = ".jar dosyasını bare metal bir sunucuda çalıştırın." +downloadBtn = ".jar dosyasını indir" +note = "Java 21+ gerektirir. :8080'de açılır; varsayılan giriş admin / stirling." +runLabel = "Ardından çalıştırın" +tagline = ".jar dosyasını bare metal bir sunucuda çalıştırın" +title = "Manuel sunucu kurulumu" + +[portal.home.download.windows] +altLabel = "Veya winget ile" +detailBody = "Yükleyiciyi çalıştırın veya bir paket yöneticisi kullanın." +downloadBtn = "Yükleyiciyi indir" +tagline = "Microsoft Windows için native uygulama" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} aktif" -invite = "Ekip arkadaşlarını davet et" +activeOfDeployed = "Bu ay {{total}} içinden {{active}} aktif" name = "Stirling PDF Editor" open = "Tarayıcıda aç" updated = "{{time}} güncellendi" +[portal.home.editor.deploy] +finish = "Dağıtımı tamamla" +options = "Dağıtım seçeneklerini görüntüle" +start = "Editor'ı dağıt" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "İyi günler" evening = "İyi akşamlar" morning = "Günaydın" -[portal.home.onboarding] -dismiss = "Kurulumu kapat" -notStarted = "Başlatılmadı" -progress = "{{total}} öğeden {{done}} tamamlandı" -title = "Kurulumu tamamlayın" - -[portal.home.onboarding.enterprise] -body = "Kuruluş geneli SSO + SCIM + RBAC, taahhütlü hacim fiyatlandırması ve ağdan izole dağıtım." -cta = "Denemeyi Başlat" -ctaQuote = "Teklif Al" -lead = "250+ çalışan için." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Masaüstü uygulamasını dağıtın, sonsuza kadar ücretsiz." -title = "PDF Editor'ü indir" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} aktif · {{recommended}} önerilen" -chip = "{{active}} aktif" -title = "İlkeleri aç" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} bağlı · her PDF ulaştığı yerde yönetilir" -chip = "{{connected}} bağlı" -title = "Kaynaklarınızı bağlayın" - -[portal.home.quickActions] -subtitle = "Bugünün başlıca görevleri" -title = "Hızlı eylemler" - -[portal.home.quickActions.buildPipeline] -blurb = "Türlendirilmiş işlem kitaplığı üzerinde 3 adımlı oluşturucu" -title = "Bir pipeline oluştur" - -[portal.home.quickActions.connectSource] -blurb = "S3, ajanlar, webhooks, izlenen klasörler" -title = "Bir kaynak bağla" - -[portal.home.quickActions.issueApiKey] -blurb = "Hız sınırları ve IP izin listesi olan kapsamlı anahtar" -title = "Bir API anahtarı ver" - [portal.infrastructure] manageEditorDeployment = "Editor dağıtımını yönet" sectionsAriaLabel = "Altyapı bölümleri" @@ -6878,13 +7541,9 @@ heading = "API anahtarları" subheading = "Anahtar bazında hız sınırları, izinler ve IP izin listeleri olan kapsamlı kimlik bilgileri." [portal.infrastructure.apiKeys.card] -allowedIps = "İzin verilen IP'ler" -anyIp = "Herhangi bir IP (izin listesi yok)" created = "Oluşturuldu" lastUsed = "Son kullanım" -permissions = "İzinler" -rateLimit = "Hız sınırı" -rateLimitValue = "{{value}} istek/dk" +revoke = "Anahtarı iptal et" usageMonth = "Bu ay kullanım" usageToday = "Bugünkü kullanım" @@ -6892,6 +7551,15 @@ usageToday = "Bugünkü kullanım" description = "Stirling API'yi çağırmaya başlamak için kapsamlı bir anahtar oluşturun." title = "Henüz API anahtarı yok" +[portal.infrastructure.apiKeys.error] +load = "API anahtarlarınız yüklenemedi. Lütfen tekrar deneyin." + +[portal.infrastructure.apiKeys.revoke] +body = "\"{{name}}\" iptal edilsin mi? Bu anahtarı hâlâ kullanan tüm entegrasyonlar hemen 401 hataları almaya başlayacaktır. Bu işlem geri alınamaz." +cancel = "İptal" +confirm = "Anahtarı iptal et" +title = "API anahtarını iptal et" + [portal.infrastructure.audit] filterAriaLabel = "Denetim etkinliklerini kategoriye göre filtrele" heading = "Denetim günlükleri" @@ -6935,6 +7603,7 @@ all = "Tümü" auth = "Kimlik doğrulama" config = "Yapılandırma" elevation = "Yetki yükseltme" +policy = "Politika" processing = "İşleme" security = "Güvenlik" @@ -6944,19 +7613,30 @@ title = "Erişiminiz yok" [portal.infrastructure.audit.metrics] config = "Yapılandırma" -elevation = "Yetki yükseltme" +policy = "Politika" processing = "İşleme" totalEvents = "Toplam etkinlik · 24 sa" +[portal.infrastructure.auditCatLabel] +auth = "Kimlik doğrulama" +config = "Yapılandırma" +elevation = "Yükseltme" +policy = "Politika" +processing = "İşleme" +security = "Güvenlik" + +[portal.infrastructure.auditStatusLabel] +danger = "Hata" +info = "Bilgi" +success = "Başarılı" +warning = "Uyarı" + [portal.infrastructure.createKey] cancel = "İptal" createKey = "Anahtar oluştur" done = "Tamam" -ipAllowlistHelper = "Virgülle ayrılmış CIDR aralıkları. Herhangi bir IP'ye izin vermek için boş bırakın." -ipAllowlistLabel = "IP izin listesi" keyNameLabel = "Anahtar adı" keyNamePlaceholder = "örn. Üretim · içe alma" -permissionsLabel = "İzinler" secretKeyCaption = "Gizli anahtar" secretWarning = "Bunu bir secrets manager içinde saklayın. Stirling yalnızca bir hash saklar — daha sonra geri almanın yolu yoktur." subtitle = "Anahtarı ihtiyaç duyduğu minimum kapsamla sınırlayın. İstediğiniz zaman döndürebilir veya iptal edebilirsiniz." @@ -6964,207 +7644,9 @@ subtitleCreated = "Bu sırrı şimdi kopyalayın — tekrar gösterilmeyecek." title = "API anahtarı oluştur" titleCreated = "Anahtar oluşturuldu" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/dk" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Dağıtan" -environment = "Ortam" -product = "Ürün" -status = "Durum" -version = "Sürüm" -when = "Ne zaman" - -[portal.infrastructure.deployments.recent] -heading = "Son dağıtımlar" -subheading = "Ürünler ve ortamlar genelindeki en son kullanıma almalar." - -[portal.infrastructure.deployments.regionColumns] -instances = "Örnekler" -latency = "Gecikme" -load = "Yük" -p99 = "P99" -region = "Bölge" -status = "Durum" -throughput = "Aktarım hızı" -uptime = "Çalışma süresi" -version = "Sürüm" - -[portal.infrastructure.deployments.regions] -heading = "Bölgeler" -subheading = "Dağıtılmış her Stirling bölgesi için canlı sağlık — gecikme, yük ve rollout sürümü." - -[portal.infrastructure.deployments.regions.empty] -description = "Çalışma alanınız hazırlandıktan sonra dağıtılmış bölgeler burada görünür." -title = "Dağıtılmış bölge yok" - -[portal.infrastructure.models] -heading = "Modeller" -msValue = "{{value}} ms" -subheading = "Çalışma alanınız genelinde belge işlemeyi destekleyen model kataloğu ve yönlendirme." - -[portal.infrastructure.models.byom] -description = "On-prem veya self-hosted bir modeli kaydedin ve veri yerleşimiyle sınırlı işleme için bir bölgeye sabitleyin." -title = "Kendi modelinizi getirin" - -[portal.infrastructure.models.catalogue] -heading = "Katalog" -sub = "Çalışma alanınız için kullanılabilir yönetilen modeller, canlı gecikme ve maliyetle birlikte." -subEnterprise = "Yönetilen, kendi getirdiğiniz ve on-prem modeller — bölge bazında sabitleme kullanılabilir." - -[portal.infrastructure.models.catalogue.empty] -description = "Çalışma alanınızın kataloğundaki modeller burada görünür." -title = "Kullanılabilir model yok" - -[portal.infrastructure.models.columns] -cost = "Maliyet" -latency = "Gecikme" -load = "Yük" -model = "Model" -status = "Durum" -type = "Tür" -version = "Sürüm" - -[portal.infrastructure.models.metrics] -activeModels = "Aktif modeller" -avgLatency = "Ort. gecikme" -included = "Dahil" -monthlySpend = "Aylık model harcaması" - -[portal.infrastructure.models.routing] -empty = "Yapılandırılmış yönlendirme kuralı yok." -heading = "Yönlendirme kuralları" -sub = "Her işlemi hangi modelin yönettiği. Daha dar bir kural eşleşmediğinde varsayılan uygulanır." -subLocked = "İşlemleri belirli modellere yönlendirin — ücretli planlarda kullanılabilir." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Her işlemi ve belge türünü hangi modelin yöneteceğini kontrol etmek için Pro'ya yükseltin." -title = "Model yönlendirme ücretli bir özelliktir" - -[portal.infrastructure.models.routingColumns] -default = "Varsayılan" -docType = "Belge türü" -modelForAria = "{{operation}} için model" -operation = "İşlem" -routedTo = "Yönlendirildiği yer" - -[portal.infrastructure.security.access.byok] -description = "Kendi KMS'nizden bir anahtar sağlayın. Stirling bununla şifreler ancak yine de okuyabilir." -label = "Kendi anahtarınızı getirin (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Anahtarlar KMS'nizden asla ayrılmaz. Stirling yalnızca şifreli metni tutar." -label = "Kendi anahtarınızı tutun (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling şifreleme anahtarlarını yönetir. En basiti — sizin tarafınızda sıfır anahtar operasyonu." -label = "Stirling tarafından tutulan anahtarlar" - -[portal.infrastructure.security.accessPolicy] -heading = "Belge erişim ilkesi" -subheading = "İşlenen belgelerin beklemedeyken şifresini kimlerin çözebileceğini kontrol eder." - -[portal.infrastructure.security.attestations] -heading = "Uyumluluk beyanları" -noReport = "Rapor mevcut değil" -subheading = "Beyan edilmiş kontrollerde raporların bulunduğu, çerçeve bazında denetim duruşu." -viewReport = "Raporu görüntüle →" - -[portal.infrastructure.security.compliance] -heading = "Uyumluluk" -subheading = "Stirling platformunu kapsayan beyanlar ve sertifikasyonlar." - -[portal.infrastructure.security.empty] -description = "Çalışma alanınızın güvenlik yapılandırması burada görünür." -title = "Güvenlik duruşu kullanılamıyor" - -[portal.infrastructure.security.hyokBanner] -description = "HYOK ile şifreleme anahtarları KMS'nizden asla ayrılmaz. Stirling yalnızca istediğiniz zaman iptal edebileceğiniz şifreli metni saklar ve işler." -title = "Stirling belgelerinizin şifresini çözemez" - -[portal.infrastructure.security.ipAllowlist] -empty = "Yapılandırılmış IP aralığı yok — tüm IP'lere izin verilir." -heading = "IP izin listesi" -sub = "API erişimi bu CIDR aralıklarıyla sınırlandırılmıştır." -subLocked = "API erişimini bilinen IP aralıklarıyla sınırlandırın — ücretli planlarda kullanılabilir." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "API erişimini belirli ağlarla sınırlandırmak için Pro'ya yükseltin." -title = "IP izin listeleme ücretli bir özelliktir" - -[portal.infrastructure.security.ipColumns] -added = "Eklendi" -addedBy = "Ekleyen" -cidr = "CIDR" -label = "Etiket" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algoritma" -heading = "Şifreleme anahtarı yönetimi" -keyId = "Anahtar tanımlayıcısı" -lastRotated = "Son döndürme" -rotateKey = "Anahtarı döndür" -rotationPolicy = "Döndürme ilkesi" -subheading = "Beklemedeki belgeleri şifreleyen anahtarların saklama sorumluluğu — kim şifre çözebilir ve anahtarlar nasıl döndürülür." - -[portal.infrastructure.security.managedBanner] -description = "Kendi anahtarını getir (BYOK) ve kendi anahtarını tut (HYOK) saklama sorumluluğu Enterprise'da kullanılabilir. Kendi KMS'nizden anahtar sağlamak için yükseltin." -title = "Planınızda anahtarlar Stirling tarafından yönetilir" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Asya Pasifik" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR veri sınırı" -label = "Avrupa Birliği" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Amerika Birleşik Devletleri" - -[portal.infrastructure.security.residencyHeader] -heading = "Veri yerleşimi" -subheading = "Belgelerin depolandığı ve işlendiği yer." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} kullanıldı" - -[portal.infrastructure.storage.empty] -description = "Bağlı depolama ve kullanım burada görünür." -title = "Yapılandırılmış depolama yok" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Aktif" -activeRange = "0–{{value}}g" -archived = "Arşivlendi" -coldStorage = "soğuk depolama" -deleted = "Silindi" -never = "asla" -purged = "kalıcı olarak silindi" - -[portal.infrastructure.storage.providers] -connect = "Bağlan" -connected = "Bağlandı" -heading = "Bağlı sağlayıcılar" -subheading = "İşlenen çıktılarının yazıldığı yer." - -[portal.infrastructure.storage.retention] -heading = "Saklama" -subheading = "Yaşam döngüsü silinmesinden önce çıktıların ne kadar süre tutulacağı." -windowLabel = "Varsayılan saklama penceresi" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} gün" -days_other = "{{count}} gün" -never = "Asla silme" - -[portal.infrastructure.storage.totalUsage] -heading = "Toplam kullanım" -progressLabel = "Kullanılan depolama" -subheading = "Tüm bağlı sağlayıcılar genelinde tüketilen depolama." +revoked = "İptal edildi" [portal.infrastructure.tabs] apiKeys = "API Anahtarları" @@ -7174,6 +7656,49 @@ models = "Modeller" security = "Güvenlik" storage = "Depolama" +[portal.integrations] +availableHeading = "Kullanılabilir" +comingSoonHeading = "Yakında" +connect = "Bağlan" +connectionCount_one = "{{count}} bağlantı" +connectionCount_other = "{{count}} bağlantı" +customApi = "Özel API" +searchPlaceholder = "Entegrasyonlarda ara" +subtitle = "Stirling'in konuşabileceği her şey. Kimlik bilgileri burada bir kez oluşturulur, ardından kaynaklar, politikalar ve işlem hatları tarafından referans verilir." +title = "Entegrasyonlar" +typedName = "{{tool}} adı" + +[portal.integrations.filters] +all = "Tümü" +audit = "Denetim" +connected = "Bağlı" +notify = "Bildirim" +security = "Güvenlik" +signing = "İmzalama" +storage = "Depolama" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Bağlı" + +[portal.integrations.table] +integration = "Entegrasyon" +worksWith = "Birlikte çalışır" + +[portal.integrations.worksWith] +pipelines = "İşlem hatları" +policies = "Politikalar" +sources = "Kaynaklar" + +[portal.legal] +draft = "{{label}} · taslak" +loadError = "Bu belge yüklenemedi. Lütfen tekrar deneyin." +loading = "Yükleniyor…" +title = "Yasal belge" + [portal.nav] agent-builder = "Agent Builder" components = "Bileşenler" @@ -7182,33 +7707,17 @@ documents = "Belgeler" editor = "Editor" home = "Ana Sayfa" infrastructure = "Altyapı" +integrations = "Entegrasyonlar" pipelines = "Pipelines" policies = "İlkeler" procurement = "Tedarik" -settings = "Ayarlar" sources = "Kaynaklar" usage = "Kullanım ve Faturalandırma" users = "Kullanıcılar" -[portal.notifications] -markAllRead = "Tümünü okundu işaretle" -title = "Bildirimler" -viewAll = "Tümünü görüntüle" - -[portal.notifications.ariaLabel] -none = "Bildirimler, okunmamış yok" -unread_one = "Bildirimler, {{count}} okunmamış" -unread_other = "Bildirimler, {{count}} okunmamış" - -[portal.notifications.count] -allRead = "tümü okundu" -loading = "yükleniyor" -new_one = "{{count}} yeni" -new_other = "{{count}} yeni" - -[portal.notifications.empty] -description = "Yeni bildirim yok." -title = "Her şey tamam" +[portal.nav.section] +platform = "PDF Platformu" +processor = "PDF İşleyici" [portal.pipelines] subtitle = "Backend üzerindeki her otomatik belge pipeline'ı: bir tetikleyiciyle çalışan, bir kaynak kümesi üzerinde sıralı işlemler zinciri. Adımlarını ve kaynaklarını görmek için bir satıra tıklayın." @@ -7218,54 +7727,87 @@ title = "Pipelines" newPipeline = "Yeni pipeline" [portal.pipelines.builder] -addStep = "Araç ekle" +activate = "Activate" back = "Pipeline'lara dön" +cannotFollow = "{{produced}} alınamaz" +chooseAccount = "Bir hesap seçin" +chooseDestination = "Bir hedef seçin" +chooseOperation = "Bu adımın ne yapacağını seçin" +chooseSource = "Bir kaynak seçin" discard = "Değişiklikleri at" -enabled = "Etkin" +inputs = "Girdi" +inputSource = "Girdi kaynağı" +inputTrigger = "Tetikleyici" keepEditing = "Düzenlemeye devam et" -needsUpload = "Yüklenmiş bir dosya gerekiyor" +moreActions = "More actions" +needsConfiguring = "Kurulum gerekli" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Aramanızla eşleşen araç yok." -pipelineSettings = "Pipeline ayarları" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Araç ara" -selectToolBody = "Pipeline'ınızı oluşturmak için bir araç ekleyin." -selectToolTitle = "Henüz araç yok" -toolSettings = "Araç ayarları" +sendToSystem = "Başka bir sisteme gönder" +stepsIncompatible = "Bu adımlar, önceki adımlarının ürettiği üzerinde çalışamaz: {{tools}}." +stepsNeedSetup = "Kaydetmeden önce bu adımların hâlâ kurulması gerekiyor: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Tanınmayan işlem, olduğu gibi tutuldu." unsavedBody = "Kaydedilmemiş değişiklikleriniz var. Ayrılmadan önce kaydedin mi, yoksa atılsın mı?" unsavedTitle = "Kaydedilmemiş değişiklikler" -uploadUnsupported = "Yüklenen dosyalar henüz pipeline'larda desteklenmiyor, bu nedenle şu adımlar kaydedilemez: {{tools}}." usesDefaults = "Varsayılan ayarlarla çalışır" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Önceki adımdaki her dosyayı birleştirir" +fan-out = "Önceki adımdaki her dosya için bir kez çalışır" +format-mismatch = "{{accepts}} gerekiyor, ancak önceki adım {{produced}} üretiyor" +output-uncertain = "Çalışmayabilir: önceki adımın çıktısı nasıl kurulduğuna bağlıdır" +source-mismatch = "{{accepts}} gerekiyor, ancak bu işlem hattının girdisi {{produced}}" +undeclared-operation = "Bu adımın ne kabul ettiği denetlenemiyor" [portal.pipelines.composer] addTool = "Araç ekle" -cancel = "İptal" -chainEmpty = "Pipeline'ınızı oluşturmaya başlamak için bir araç ekleyin." create = "Pipeline oluştur" -directory = "Çıktı klasörü" -directoryHelp = "Sunucudaki mutlak yol. Yapılandırılmış izin verilen klasörlerin içinde olmalıdır." +createPaused = "Create paused" editingUnsupported = "Bu araç parametrelerinin düzenleme için görüntülenmesi henüz desteklenmiyor." -moveDown = "Aşağı taşı" -moveUp = "Yukarı taşı" +editSource = "Edit source" name = "Ad" namePlaceholder = "örn. Redaction taraması" -noSources = "Henüz bağlı kaynak yok. Pipeline, doğrudan kendisine sağlanan dosyalarda yine de çalışabilir." noToolSettings = "Bu aracın yapılandırılabilir ayarı yok." -operations_one = "İşlem ({{count}})" -operations_other = "İşlemler ({{count}})" output = "Çıktı" -removeStep = "İşlemi kaldır" save = "Değişiklikleri kaydet" scheduleEvery = "Şu aralıkla çalıştır" -sources = "Kaynaklar" -sourcesLoading = "Kaynaklar yükleniyor..." trigger = "Tetikleyici" triggerManual = "Yalnızca manuel" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "gün" hours = "saat" minutes = "dakika" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "\"{{name}}\" silinsin mi? Bu geri alınamaz." cancel = "İptal" @@ -7273,28 +7815,62 @@ confirm = "Sil" title = "Pipeline silinsin mi?" [portal.pipelines.detail] +clearHistory = "Geçmişi temizle" delete = "Pipeline sil" run = "Şimdi çalıştır" [portal.pipelines.empty] action = "Bir pipeline oluştur" +connectSource = "Bir kaynak bağla" description = "İlk pipeline'ınızı oluşturun: çalışacağı kaynakları seçin, işlemleri zincirleyin ve çıktının nereye gideceğini belirleyin." title = "Henüz pipeline yok" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Aktif" paused = "Duraklatıldı" total = "Pipelines" -[portal.pipelines.output] -folder = "Klasöre yaz" -inline = "Dosyaları döndür" - [portal.pipelines.run] +allProcessed_one = "Çalıştırılacak bir şey yok: kaynağın {{count}} belgesi zaten işlendi." +allProcessed_other = "Çalıştırılacak bir şey yok: kaynaklardaki {{count}} belgenin tümü zaten işlendi." completed_one = "Çalıştırma tamamlandı." completed_other = "{{count}} çalıştırmanın tümü tamamlandı." empty = "Çalıştırılacak bir şey yok: kaynaklarda işlenecek belge yoktu." failed = "Çalıştırma başarısız: {{error}}" +inFlight = "Çalıştırılacak yeni bir şey yok: belgeler önceki bir çalıştırmadan hâlâ işleniyor." +parked_one = "Çalıştırılacak bir şey yok: {{count}} belge daha önce başarısız oldu ve park edildi. Nedeni düzeltin, ardından yeniden denemek için geçmişi temizleyin." +parked_other = "Çalıştırılacak bir şey yok: {{count}} belge daha önce başarısız oldu ve park edildi. Nedeni düzeltin, ardından yeniden denemek için geçmişi temizleyin." running = "Çalıştırma başladı; hâlâ devam ediyor." timeout = "Çalıştırma beklenenden uzun sürüyor; arka planda yine de tamamlanabilir." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Kaynaklar" status = "Durum" steps = "Adımlar" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Klasör izleme" @@ -7314,12 +7891,18 @@ manual = "Manuel" schedule = "Zamanlanmış" [portal.policies] +defaultName = "{{category}} Politikası" subtitle = "Her belgeye bir araç pipeline'ı uygulayan kalıcı otomasyonlar. Her ilke yükleme veya dışa aktarma sırasında tetiklenir, araç zincirini çalıştırır ve zorunlu sürümü orijinalin yanında kaydeder." title = "İlkeler" [portal.policies.card] comingSoon = "Enterprise'a yükselt" notSetUp = "Kurulmadı" +requiresAiEngine = "AI motoru gerektirir" + +[portal.policies.categories.classification] +desc = "Yükleme sırasında her belgenin türünü tanımlayın ve dosyalama ile arama için meta verilerini etiketleyin." +label = "Sınıflandırma" [portal.policies.categories.compliance] desc = "Her belgede HIPAA, GDPR, SOC 2 veya FedRAMP gereksinimlerini zorunlu kılın." @@ -7344,6 +7927,13 @@ label = "Güvenlik" [portal.policies.config] scopeAll = "Tüm belgeler" +[portal.policies.config.classification] +summary = "Yüklenen her belgeyi ekibinizin etiketlerine göre sınıflandırır ve meta verilerini etiketler." + +[portal.policies.config.classification.rules] +0 = "Sınıflandır" +1 = "Meta verileri etiketle" + [portal.policies.config.compliance] summary = "Belgeler sistemden ayrılmadan önce düzenleyici çerçevelere göre doğrular." @@ -7371,6 +7961,29 @@ minConfidence = "Min güven" 2 = "Adlandır" 3 = "Normalleştir" +[portal.policies.config.purview] +label = "Bir Microsoft Purview duyarlılık etiketi uygula" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Etiket ID" +labelIdHelp = "Purview portalından etiketin GUID'si. Bir etiket uygulamak, meta verilerini PDF üzerine yazar; şifreleyemez, bunun için Microsoft client gerekir." +labelName = "Etiket adı (isteğe bağlı)" +labelNameHelp = "Kişilerin ve araçların etiketi tanıyabilmesi için ID ile birlikte kaydedilir." + +[portal.policies.config.purview.fields.method] +help = "Purview, bir politikanın uyguladığı etiketi bir kişinin seçtiği etiketten ayırır." +label = "Etiket nasıl kaydedilir" +privileged = "Bir kişi tarafından seçilmiş olarak" +standard = "Otomatik uygulanmış olarak" + +[portal.policies.config.purviewRead] +label = "Bir Microsoft Purview etiketi oku" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Belgenin zaten taşıdığı etiketi okur, böylece sonraki adımlar buna göre işlem yapabilir." + [portal.policies.config.retention] summary = "Belgelerin ne kadar süre tutulacağını, ne zaman arşivleneceğini ve ne zaman silineceğini zorunlu kılar." @@ -7438,6 +8051,7 @@ title = "Henüz etkinlik yok" [portal.policies.endpoints] addWatermark = "Filigran" autoRedact = "PII'yi karart" +classifyAndLabel = "Sınıflandır" compressPdf = "Sıkıştır" flatten = "Düzleştir" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "İlkeleriniz kaydedildi ve bağlantı yeniden kurulduğunda gör retry = "Yeniden dene" title = "Backend kullanılamıyor" +[portal.policies.operations] +change = "Bu adımın ne yapacağını değiştir" +noResults = "Bununla eşleşen adım yok. Bir ürün adı veya \"scan\", \"notify\", \"attach\" deneyin." +searchPlaceholder = "Adımlarda ara - \"scan\", \"attach\" veya bir ürün adı deneyin" + +[portal.policies.operations.bodyMode] +binary = "Ham dosya baytları" +json = "Bir JSON payload içinde" +multipart = "Dosya yükleme olarak (multipart)" + +[portal.policies.operations.clamavScan] +description = "Kendi ClamAV sunucunuz belgeyi denetler; ağınızdan hiçbir şey çıkmaz." +label = "Virüs tara (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Makro, yürütülebilir dosya, script veya gömülü nesne taşıyan belgeleri reddeder." +label = "Aktif içeriği engelle" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive belgeyi denetler ve temiz değilse çalıştırma durur." +label = "Virüs tara" + +[portal.policies.operations.confluenceAttach] +description = "İşlenen belgeyi bir sayfaya dosyalar." +label = "Bir Confluence sayfasına ekle" + +[portal.policies.operations.consignoSubmit] +description = "Belgeyi imzalama için ConsignO Cloud'a gönderir." +label = "İmza için gönder (ConsignO)" +note = "Belgeyi gönderir. İmzalı kopyayı alma henüz desteklenmiyor, çünkü bir adım sonucu sonraki adıma aktaramaz." + +[portal.policies.operations.customApiCall] +description = "Çağrıyı kendiniz oluşturun: path, method, body ve yanıt işleme." +label = "Özel bir API çağır" + +[portal.policies.operations.discordNotify] +description = "Politikanın bir belgeyi işlediğini bir kanala bildirir." +label = "Discord'a mesaj gönder" + +[portal.policies.operations.elasticIndex] +description = "Politikanın ne yaptığını aranabilir bir belge olarak kaydeder." +label = "Elasticsearch'te bir denetim olayını indeksle" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart, yükleme API'lerine uygundur; JSON, dosyayı bir payload içinde isteyen API'lere uygundur; binary ham baytları gönderir." +label = "Belge nasıl gönderilsin" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Olduğu gibi gönderilen bir JSON body. {{document.base64}} dosyayı taşır; {{document.*}} ve {{run.*}} belge başına doldurulur." +label = "JSON body" + +[portal.policies.operations.fields.connection] +label = "Hesap" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API'nin belgeyi altında beklediği form alanı." +label = "Dosya alanı adı" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Kimden" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Header ad/değer çiftlerinden oluşan bir JSON nesnesi. Bağlantının kimlik bilgisi sizin için eklenir." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue anahtarı" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Mesaj gövdesi olarak gönderilir. {{document.*}} ve {{run.*}} belge başına doldurulur." +label = "Mesaj" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Sayfa ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Bağlantının base URL'sine eklenir. Asla başka bir host'a ulaşamaz." +label = "Path" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Drive içinde nereye yazılacağı. {{document.filename}} belge başına doldurulur." +label = "Hedef path" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Rapor belgeye dokunmadan bırakır. Değiştir, gelen her neyse belgeyi onunla değiştirir." +label = "Yanıtla ne yapılsın" + +[portal.policies.operations.fields.signerEmail] +label = "İmzalayanın e-postası" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Konu" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio bu metni analiz eder. {{document.title}} kullanın veya denetlenmesini istediğiniz metni yapıştırın." +label = "Denetlenecek metin" + +[portal.policies.operations.fields.to] +label = "Kime" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud kullanıcı adı" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Politikanın bir belgeyi işlediğini bir alana bildirir." +label = "Google Chat'e mesaj gönder" + +[portal.policies.operations.jiraAttach] +description = "İşlenen belgeyi bir issue'ya dosyalar." +label = "Bir Jira issue'suna ekle" + +[portal.policies.operations.mailgunEmail] +description = "İşlenen belgeyi ek olarak gönderir." +label = "Belgeyi e-posta ile gönder (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "İşlenen belgeyi bir klasöre yazar." +label = "Nextcloud'a yükle" + +[portal.policies.operations.presidioAnalyze] +description = "Kendi Presidio sunucunuz bulduğu kişisel verileri raporlar." +label = "Kişisel verileri algıla" +note = "Presidio dosyaları değil, metni okur. Denetlenmesini istediğiniz metni verin — belgenin kendisi yüklenmez." + +[portal.policies.operations.responseMode] +replace = "Belgeyi yanıtla değiştir" +report = "Belgeyi koru, yanıtı kaydet" + +[portal.policies.operations.sendgridEmail] +description = "İşlenen belgeyi ek olarak gönderir." +label = "Belgeyi e-posta ile gönder (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Politikanın bir belgeyi işlediğini bir kanala bildirir." +label = "Slack'e mesaj gönder" + +[portal.policies.operations.splunkEvent] +description = "Politikanın ne yaptığını denetçilerinizin zaten baktığı yere kaydeder." +label = "Splunk'a denetim olayı gönder" + +[portal.policies.operations.sumologicEvent] +description = "Politikanın ne yaptığını bir HTTP collector'a kaydeder." +label = "Sumo Logic'e denetim olayı gönder" + +[portal.policies.operations.teamsNotify] +description = "Politikanın bir belgeyi işlediğini bir kanala bildirir." +label = "Teams'e mesaj gönder" + +[portal.policies.operations.webhookPost] +description = "Belgenin kendisini seçtiğiniz bir URL'ye gönderir." +label = "Belgeyi bir webhook'a gönder" + +[portal.policies.operations.zapierNotify] +description = "Çalıştırmanın ayrıntılarını otomasyonunuza aktarır." +label = "Bir Zap veya Make senaryosu tetikle" + [portal.policies.stats] activeFor = "Aktif" dataProcessed = "İşlenen veri" @@ -7473,6 +8282,13 @@ label = "Uygulanan belgeler" description = "Yapılandırıldı ancak tetiklenmiyor" label = "Duraklatıldı" +[portal.policies.table] +appliesTo = "Uygulandığı yer" +docs = "Zorunlu kılınan belgeler" +enforces = "Zorunlu kılar" +policy = "Politika" +status = "Durum" + [portal.policies.wizard.actions] back = "Geri" cancel = "İptal" @@ -7480,6 +8296,10 @@ continue = "Devam" enablePolicy = "İlkeyi etkinleştir" saveChanges = "Değişiklikleri kaydet" +[portal.policies.wizard.capability.classify] +desc = "Belgenin türünü ekibinizin etiketlerinden tanımlar ve etiketler; böylece kategoriye göre dosyalanır ve aranır." +label = "Belgeyi sınıflandır" + [portal.policies.wizard.capability.compress] desc = "Belgeyi daha küçük bir dosya boyutuna sıkıştırır." label = "Dosya boyutunu azalt" @@ -7500,10 +8320,18 @@ label = "Hassas bilgileri karart" desc = "Belge açıldığında hiçbir şeyin otomatik çalışmaması için gizli JavaScript'i kaldırır." label = "Aktif içeriği çıkar" +[portal.policies.wizard.capability.timestampPdf] +desc = "Bağımsız bir zaman damgası yetkilisi kullanarak belgenin belirli bir zamanda tam olarak bu biçimde var olduğunu kanıtlar. Yalnızca bir hash gönderilir - belge asla sunucunuzdan ayrılmaz." +label = "Güvenilir zaman damgası ekle" + [portal.policies.wizard.capability.watermark] desc = "Her sayfanın üzerine görünür bir işaret (örn. “Gizli”) basar." label = "Filigran uygula" +[portal.policies.wizard.classification] +description = "Yüklenen her belge yerleşik etiketlere göre sınıflandırılır ve uygun türlerle etiketlenir. Etiket kümesi tüm ekibiniz arasında paylaşılır." +labelsHeading = "Sınıflandırma etiketleri" + [portal.policies.wizard.errors] noTools = "Önce iş akışında en az bir aracı etkinleştirin." saveFailed = "İlke kaydedilemedi. Lütfen tekrar deneyin." @@ -7548,73 +8376,118 @@ setUp = "{{category}} ilkesini kur" [portal.policies.wizard.workflow] description = "Bu ilkenin işlediği her belgeye ne yapacağını seçin." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} aktif" -noRule = "Henüz uygulanan kural yok" -subtitle = "Hangi pipeline işlerse işlesin, her belgenin geçtiği kalıcı otomasyonlar." -title = "PDF'lerinizde çalışanlar" - [portal.policySummary.action] -comingSoon = "Enterprise'a yükselt" -configure = "Yapılandır" setUp = "Kur" -[portal.policySummary.column] -activeRule = "Aktif kural" -policy = "İlke" -status = "Durum" +[portal.processorFlow] +footnote = "Sayılar son 24 saate aittir. Akış hızı temsili amaçlıdır." +liveBadge = "Canlı" +stats = "{{connected}} bağlı · {{processed}} PDF işlendi" +title = "PDF İşleyici" -[portal.policySummary.empty] -description = "İlkeler yapılandırıldığında kategoriler burada görünür." -title = "Henüz ilke yok" +[portal.processorFlow.lens] +ariaLabel = "Görünüm modu" +flow = "Akış" +sankey = "Sankey" -[portal.policySummary.state] -active = "Aktif" -off = "Kapalı" +[portal.processorFlow.outcomes] +count = "{{n}} · 24s" +failed = "Başarısız" +heading = "Sonuçlar" +success = "Teslim edildi" + +[portal.processorFlow.policies] +activeCount = "{{n}} aktif" +count = "{{n}} · 24s" +heading = "Politikalar" +setUp = "Kur" soon = "Yakında" -[portal.processingStatus] -managePlan = "Planı yönet" -volumeSuffix = "İşlenen PDF'ler · son 30 gün" +[portal.processorFlow.sankey] +waist = "Politikalar · {{n}} aktif" + +[portal.processorFlow.sankey.empty] +description = "Belgelerin işleyiciden akışını izlemek için bir kaynak bağlayın ve bir politikayı açın." +title = "Henüz akış yok" + +[portal.processorFlow.sources] +comingSoonTag = "Bağlan" +editor = "Stirling PDF Editor" +heading = "Kaynaklar" +perDay = "{{n}} / 24s" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "E-posta alımı" [portal.procurement] -reset = "Satın almayı sıfırla (demo)" subtitle = "Ekibinizin değerlendirme, sözleşme ve katılım süreçlerini tamamlayın. Her belge tek yerde." title = "Satın alma" -[portal.procurement.action] -download = "İndir" -pay = "Şimdi öde" -request = "Talep et" -sign = "İncele ve imzala" -upload = "Yükle" - [portal.procurement.agreement] agreeCta = "Kabul et ve abone ol" +confidential = "Gizli" confirm = "Stirling Enterprise Agreement'ı okudum ve kabul ediyorum." -eyebrow = "Anlaşma" -intro = "Tek bir birleşik anlaşma, anlaşmanızı kapsar: Master Service Agreement, Order Form, EULA ve Data Processing Agreement. İnceleyin, ardından teklifi taahhütlü bir abonelik olarak kabul etmek için onaylayın." -title = "Enterprise anlaşmanızı inceleyin" +docName = "Stirling Kurumsal Sözleşmesi" +docSub = "MSA, Sipariş Formu, EULA ve DPA, tek bir imzada birleştirildi." +download = "İndir" +downloadDraftError = "Sözleşme PDF'i oluşturulamadı - belge renderer kullanılamıyor. Lütfen kısa süre sonra tekrar deneyin veya destekle iletişime geçin." +downloadError = "İmzalı sözleşme indirilemedi. Lütfen tekrar deneyin." +legalName = "Tüzel kişi adı" +legalNamePlaceholder = "İmzalayacak tüzel kişi" +loadError = "Sözleşme yüklenemedi. Lütfen tekrar deneyin." +loading = "Sözleşme yükleniyor..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Değişiklik iste" +scrollHint = "İncelemek için kaydırın" +signatory = "İmzalayan adı" +signatoryPlaceholder = "Tam ad" +signatoryTitle = "İmzalayan unvanı" +signatoryTitlePlaceholder = "örn. Genel Hukuk Müşaviri" +signError = "İmzanız kaydedilemedi. Lütfen tekrar deneyin." [portal.procurement.builder] addons = "Eklentiler" +addressLine1 = "Adres satırı 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Adres satırı 2" +addressLine2Placeholder = "Daire, kat, bina" back = "Geri" businessName = "İşletme adı" businessNamePlaceholder = "Şirketiniz" +city = "Şehir" +cityPlaceholder = "San Francisco" +completeRequired = "Lütfen teklif oluşturmadan önce gerekli alanları (* ile işaretli) geçerli bir e-posta ile doldurun." +contactEmail = "İletişim e-postası" +contactEmailPlaceholder = "jane@acme.com" +contactName = "İletişim adı" +contactNamePlaceholder = "Jane Doe" continue = "Devam" -country = "Ülke" -countryEuro = "Euro Bölgesi (EUR €)" -countryUK = "Birleşik Krallık (GBP £)" -countryUS = "Amerika Birleşik Devletleri (USD $)" +done = "Tamam" eula = "Stirling Enterprise EULA'yı okudum ve kabul ediyorum. Bu tekliften oluşturulan anlaşmayı yönetir." generate = "Teklif oluştur" -included = "Dahil" indemnification = "IP tazminatı" indemnificationSub = "EULA uyarınca uygun IP taleplerini savunuruz" -offlineLicense = "Çevrimdışı / air-gapped lisans" -offlineLicenseSub = "Air-gapped, kendi sunucusunda barındırılan bir örnek için indirilebilir lisans dosyası" +paperEyebrow = "Kurumsal teklif" +paperFor = "Hazırlanan" +pdfSize = "PDF boyutu" +poNumber = "PO numarası" +poNumberPlaceholder = "İsteğe bağlı" +postalCode = "Posta kodu" +postalCodePlaceholder = "94105" +posture = "Yönetişim" +posture_count = "~{{count}} politika" +posture_essentials = "Temel öğeler" +posture_essentialsSub = "Sınıflandırma + Paylaşım — varsayılanlar" +posture_governed = "Yönetilen" +posture_governedSub = "Güvenlik ve Yönlendirme ekler" +posture_regulated = "Düzenlemeye tabi" +posture_regulatedSub = "Her kategori — Uyumluluk, Saklama, Alım" qbr = "Üç aylık iş değerlendirmeleri" qbrSub = "SE'niz her çeyrek kullanım ve yol haritasını inceler" +region = "Eyalet / bölge" +regionPlaceholder = "California" running = "{{annual}} / yıl · {{years}} yıllık {{tcv}}" s1Sub = "Ekibiniz ve her yıl çalıştırmayı beklediğiniz PDF'ler." s1Title = "Ne kadar işlem yapacaksınız?" @@ -7623,6 +8496,12 @@ s2Title = "Taahhüt ve hizmet" s3Sub = "Teklif ve oluşturduğu anlaşma için." s3Title = "Bilgileriniz" serviceLevel = "Hizmet seviyesi" +size_compact = "Kompakt" +size_compactSub = "Çoğunlukla metin, 1 MB altında" +size_heavy = "Ağır" +size_heavySub = "Taranmış veya görsel ağırlıklı, 5 MB+" +size_standard = "Standart" +size_standardSub = "Karışık metin ve görseller, 1 ila 5 MB" slDedicated = "Özel" slDedicatedSub = "4 iş saati · özel hesap yöneticisi · +30%" slPriority = "Öncelikli" @@ -7630,6 +8509,8 @@ slPrioritySub = "Aynı iş günü · atanmış CSM · +15%" slStandard = "Standart" slStandardSub = "Sonraki iş günü · paylaşılan CSM · dahil" stepOf = "Adım {{n}} / {{total}}" +taxId = "VAT / Vergi ID" +taxIdPlaceholder = "İsteğe bağlı" term = "Vade" termDiscount = "{{pct}}% çok yıllı taahhüt indirimi uygulandı" title = "Teklifinizi oluşturun" @@ -7637,6 +8518,7 @@ training = "İlk kurulum ve eğitim" trainingSub = "Ekibinizi çalışır hale getirmek için canlı oturumlar" users = "Toplam kullanıcı" usersPlaceholder = "örn. 250" +viewEula = "Oku" volEstimated = "{{count}} kullanıcıdan tahmin edildi (her biri ~2.000 PDF, otomasyon dahil). Daha iyi biliyorsanız düzenleyin." volManual = "Kendi rakamınız kullanılıyor. İstediğiniz zaman ekip boyutunuzdan yeniden tahmin edin." volNoUsers = "Emin değil misiniz? Ekip boyutunuzu girin, biz tahmin edelim." @@ -7645,55 +8527,55 @@ volumePlaceholder = "örn. 1.000.000" years_one = "{{count}} yıl" years_other = "{{count}} yıl" -[portal.procurement.docs] -count_one = "{{count}} belge" -count_other = "{{count}} belge" -done = "Tamamlandı" -here = "Buradasınız" -hide = "Gizle" -optional = "İsteğe bağlı" -paidAddon = "Ücretli eklenti" -show = "Göster" -subtitle = "Yolculuğun her adımında ihtiyacınız olan her şey, anlaşma ilerledikçe görünür hale gelir." -supportingSubtitle = "SOC 2, güvenlik incelemeleri, vergi formları ve daha fazlası, güvenlik veya satın alma ekibiniz istediğinde hazır. Bazılarında tek seferlik ücret bulunur." -supportingTitle = "Değerlendirmenizi destekleme" +[portal.procurement.documents] +agreement = "Kurumsal Sözleşme" +agreementSub = "MSA, Sipariş Formu ve DPA tek imzada." +download = "İndir" +eula = "EULA ve Ticari Şartlar" +eulaSub = "Yazılım ve self-servis ticari şartlar." +invoice = "Fatura" +invoiceSub = "İlk abonelik faturanız." +laterInvoice = "Kabul ettikten sonra kullanılabilir" +laterQuote = "Teklifiniz düzenlendiğinde kullanılabilir" +quote = "Teklif" +quoteSub = "Kalemlere ayrılmış kurumsal teklifiniz." +sla = "SLA Eki" +slaSub = "Destek kademeleri ve yanıt hedefleri." +subprocessors = "Alt işleyiciler" +subprocessorsSub = "Sizin adınıza veri işleyen üçüncü taraflar." +subtitle = "Anlaşma evraklarınız her zaman kullanılabilir." title = "Belgeler" -upcoming = "Yaklaşan" +view = "Görüntüle" [portal.procurement.error] title = "Bir şeyler ters gitti" [portal.procurement.hero] -company = "Enterprise anlaşmanız" +barAria = "Aşama {{current}} / {{total}}" ctaAgreement = "Anlaşmayı incele ve imzala" -ctaLive = "Yayındasınız" -ctaPayment = "Ödeme ekle" +ctaExploring = "Denemenizi kurun" ctaQuote = "Teklifinizi inceleyin" +ctaReviewQuote = "Teklifi incele" ctaTrial = "Teklifinizi oluşturun" +documents = "Belgeler" eyebrow = "Enterprise satın alma" +eyebrowCompany = "{{company}} Kurumsal" inviteTeammates = "Takım arkadaşlarını davet et" -keyDocs = "Önemli belgeler" -nextStep = "Sonraki adım: {{action}}" -notStarted = "Başlatılmadı" +licenseKey = "Lisans anahtarı" +liveSub = "Kuruluşunuz provision edildi ve lisansınız aktif." +liveTitle = "Stirling Enterprise'ta canlıdasınız" +next = "Sonraki: {{stage}}" open = "Satın almayı aç" scheduleCall = "Görüşme planla" -setup1Sub = "Takım arkadaşlarınızı davet edin" -setup1Title = "PDF Editor'ı dağıtın" -setup2Sub = "Editor'lar ve diğer kaynaklar genelinde işlemeyi açın" -setup2Title = "PDF Processor'ı bağlayın" -setup3Sub = "İhtiyacınız olduğunda Güvenlik, Uyumluluk, Yönlendirme veya Saklama'yı açın" -setup3Title = "Önerilen ilkeleri ekleyin" +sentenceAgreement = "sözleşmeyi inceleyip imzalama" +sentenceLive = "provisioning ve kullanıma alma" +sentencePayment = "satın alma siparişi ve ödeme" +sentenceQuote = "teklifinizi inceleyip kabul etme" +sentenceTrial = "deneme çalışma alanınızı kurma" [portal.procurement.journey] daysLeft_one = "{{count}} gün kaldı" daysLeft_other = "{{count}} gün kaldı" -engineerLabel = "Çözüm mühendisiniz" -eyebrow = "Yayına alma süreciniz" -live = "Stirling Enterprise'ta yayındasınız" -nextStep = "Sonraki adım: {{action}}" -subtitle = "Çözüm mühendisiniz her adımda yanınızda. Her seferinde tek bir sonraki eylem; tam kontrol listesi aşağıdadır." -title = "Denemeden canlıya, rehberli tek yol" -trialTitle = "Enterprise denemesi" [portal.procurement.journeySteps.agreement] blurb = "Tek imza MSA, sipariş formu, EULA ve DPA'yı kapsar." @@ -7727,6 +8609,9 @@ downloadError = "Çevrimdışı lisans dosyası henüz oluşturulamadı — lüt downloadOffline = "Çevrimdışı lisansı indir (.lic)" hint = "Etkinleştirmek için bu anahtarı kendi sunucunuzda barındırılan bir örneğe yapıştırın veya kayıtlarınız için saklayın. Güvende tutun." label = "Lisans anahtarınız" +subtitle = "Kendi barındırdığınız bir instance'ı bu anahtarla etkinleştirin veya kayıtlarınız için saklayın." +title = "Lisans anahtarınız" +trialFileHint = "Bu, deneme lisans dosyanızdır. Sözleşmeniz yürürlüğe girdikten sonra geri dönüp tekrar indirin — çevrimiçi lisans anahtarının aksine, .lic dosyası kendiliğinden güncellenmez." [portal.procurement.link] cta = "Hesabı bağla" @@ -7739,65 +8624,62 @@ description = "Aboneliğiniz aktif ve lisansınız verildi. Ekibiniz hazırland eyebrow = "Canlı" title = "Stirling Enterprise'ta yayındasınız" -[portal.procurement.locked] -description = "Deneme anahtarları, taahhütlü hacim teklifleri, tek imzalı anlaşma, ödeme ve belge kayıt defteriniz, enterprise değerlendirmesine başladığınızda burada yer alır." -eyebrow = "Yalnızca Enterprise" -talkToSales = "Satış ekibiyle görüş" -title = "Satın alma süreci Enterprise ile açılır" - [portal.procurement.milestone] -accept = "Kabul et ve devam et" -description = "PDF'yi ekibinizle paylaşmak için indirin, hazır olduğunuzda kabul etmek için geri gelin veya değişiklik yapın." -download = "PDF indir" downloadError = "Teklif PDF'si henüz indirilemedi — lütfen birazdan tekrar deneyin." -edit = "Teklifi düzenle" -eyebrow = "Teklif {{number}}" -perYear = " / yıl" -preparedFor = "{{company}} için hazırlandı" -tcv = "{{value}} toplam sözleşme değeri" -title = "Teklifiniz hazır" - -[portal.procurement.modal] -cancel = "İptal" -chooseFile = "Dosya seç" -close = "Kapat" -downloadBody = "İndirmeniz kısa süre içinde başlayacak." -downloadCta = "İndir" -downloadTitle = "İndir" -noFile = "Dosya seçilmedi" -payBody = "Taahhütlü sözleşmenizi Stripe üzerinden kartla veya banka transferiyle ödeyin. Ödeme temizlenir temizlenmez çalışma alanınız hazırlanır." -payCta = "Stripe'a devam et" -payTitle = "Ödemeyi onayla" -requestBodyFree = "Bunu talep üzerine oluştururuz. Onaylayın, çözüm mühendisiniz kısa süre içinde gönderecek." -requestBodyPaid = "Bu ücretli bir eklentidir. Onaylayın, çözüm mühendisiniz kapsamını belirleyip evrakları gönderecek." -requestCta = "Talep et" -requestTitle = "Bu belgeyi talep et" -signBody = "Elektronik imza için Stirling Enterprise Agreement'ı açar: tek imza MSA, sipariş formu, EULA ve DPA'yı kapsar. Biz otomatik olarak karşı imza atarız ve ödemeye geçersiniz." -signCta = "İmza için aç" -signTitle = "Anlaşmanızı inceleyin ve imzalayın" -uploadBody = "PO'nuzu bize gönderin, biz de şartlarınıza göre faturalandıralım. PDF'yi sürükleyin veya aşağıdan bir dosya seçin." -uploadCta = "Satın alma emrini yükle" -uploadTitle = "Satın alma emrinizi yükleyin" [portal.procurement.payment] description = "Teklifiniz kabul edildi ve lisansınız zaten aktif — ekibiniz hemen başlayabilir. Hazır olduğunuzda ilk faturayı ödeyin; e-posta gerekmeden burada ödeyebilir veya indirebilirsiniz." +downloadAgreement = "İmzalı sözleşmeyi indir" downloadInvoice = "Faturayı indir" -simulate = "Ödeme alındı simülasyonu yap (demo)" +eyebrow = "Ödeme" title = "Abonelik oluşturuldu" viewInvoice = "Faturayı görüntüle ve öde" +[portal.procurement.review] +acceptCta = "Teklifi kabul et" +annual = "Yıllık ücret (1. yıl)" +downloadCta = "Teklifi indir" +poNumber = "Satın alma siparişi: {{po}}" +renewal = "Dönemden sonra {{amount}}/yıl üzerinden yenilenir (+{{pct}}% CPI)" +tcv = "{{years}} yıllık dönem, peşin tam ödemeli: {{tcv}}" +validUntil = "{{date}} tarihine kadar geçerli" + [portal.procurement.schedule] fallback = "Planlayıcı yüklenemedi." fallbackLink = "Planlamayı yeni sekmede aç" +loading = "Zamanlayıcı yükleniyor..." subtitle = "Çözüm mühendisiniz ekibinize yayına alma sürecinde rehberlik edecek. Size uygun bir zaman seçin." title = "Görüşme planla" -[portal.procurement.status] -action = "Eylem gerekli" -available = "Kullanılabilir" -complete = "Tamamlandı" -pending = "Beklemede" -request = "Talep üzerine" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Kapalı, dışa giden yok" +back = "Geri" +businessName = "İşletme adı" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Yönetilen, en hızlı başlangıç" +continue = "Devam et" +deployment = "Nereye dağıtılmalı?" +eula = "Stirling EULA ve Ticari Şartlar'ı kabul ediyorum." +fullName = "Tam ad" +fullNamePlaceholder = "Adınız" +invites = "Ekip arkadaşlarını davet et · isteğe bağlı" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Bir görüşme planla" +seats = "Test eden kişiler" +seatsPlaceholder = "örn. 15" +selfhost = "Self-hosted" +selfhostSub = "Docker veya Kubernetes" +start = "Denemeyi başlat" +stepOf = "Adım {{n}} / {{total}}" +subtitle = "14 gün ücretsiz, kart gerekmez." +subtitleDetails = "Teklifiniz ve sözleşmeniz için birkaç ayrıntı." +talkFirst = "Önce konuşmayı mı tercih edersiniz?" +title = "Denemenizi kurun" +viewEula = "Oku" +workEmail = "İş e-postası" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Uzatma 7 gün ekler ve çözüm mühendisinizi bilgilendirir." @@ -7808,109 +8690,113 @@ maxed = "Sınıra ulaşıldı" subtitle = "Ücretsiz denemeniz {{date}} tarihine kadar sürer. Kart gerekmez." title = "Enterprise denemesi" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Taahhütlü hacim fiyatlandırması, kuruluş genelinde SSO + SCIM + RBAC, 90 günlük değiştirilemez denetim ve özel bir SE." -homeCta = "Denemeyi Başlat →" -homeHeadline = "Milyonlarca PDF işleyin." - -[portal.recentActivity] -title = "Son etkinlik" -viewAll = "Tümünü görüntüle" - -[portal.recentActivity.empty] -description = "Pipeline çalıştırmaları, dağıtımlar ve aracı etkinlikleri burada görünecek." -title = "Henüz burada bir şey yok" - -[portal.search] -ariaLabel = "Ara" -placeholder = "Stirling'de ara — endpoints, pipelines, docs…" - -[portal.search.empty] -noActionsDescription = "Hızlı eylemler kullanılabilir olduğunda burada görünecek." -noActionsTitle = "Hızlı eylem yok" -noMatches = "\"{{query}}\" için eşleşme yok" -noMatchesDescription = "Farklı bir anahtar kelime deneyin veya kataloğa göz atın." - [portal.settings.groups] admin = "Yönetici" [portal.settings.sections] account-link = "Hesap bağlantısı" -[portal.shell.header] -accountFallback = "Hesap" -accountMenu = "Hesap menüsü" -darkMode = "Koyu mod" -lightMode = "Açık mod" -search = "Ara" -searchPlaceholder = "Ara…" -signOut = "Çıkış yap" -switchToDark = "Koyu temaya geç" -switchToLight = "Açık temaya geç" - [portal.shell.sidebar] appEditor = "Editor" appProcessor = "Processor" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} belge" linkAccount = "Stirling hesabını bağla" -planEditor = "Editor planı" -planEnterprise = "Enterprise planı" -planProcessor = "Processor planı" primaryNav = "Birincil gezinme" switchApp = "Uygulama değiştir" +[portal.shell.topbar] +closeNav = "Gezinmeyi kapat" +openNav = "Gezinmeyi aç" +search = "Ara" + [portal.sources] subtitle = "Belgeleri Stirling'e besleyen yeniden kullanılabilir giriş bağlantıları. Bir bağlantıyı bir kez yapılandırın, ardından istediğiniz sayıda ilkeden referans verin. Yapılandırması ve hangi ilkelerin kullandığı için bir satıra tıklayın." title = "Kaynaklar" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Kaynak bağla" +[portal.sources.builder] +back = "Kaynaklara dön" +backToTypes = "Tüm kaynak türleri" +cancel = "İptal" +chooseHint = "Belgelerin nereden geldiğini seçin. Gri görünen connector'lar yolda." +comingSoon = "Yakında" +comingSoonHeading = "Yakında" +create = "Kaynak oluştur" +createTitle = "Bir kaynak bağla" +delete = "Sil" +editTitle = "Kaynağı düzenle" +enabled = "Etkin" +save = "Değişiklikleri kaydet" + +[portal.sources.builder.folderAccess] +description = "Klasör otomasyonları yalnızca bir yöneticinin izin verdiği klasörleri kullanabilir. Klasör Erişimi ayarları altında ekleyin, ardından tekrar deneyin." +openSettings = "Klasör Erişimi ayarları" +title = "Bu klasöre izin verilmiyor" + [portal.sources.delete] body = "\"{{name}}\" silinsin mi? Bu işlem geri alınamaz. Ona referans veren ilkelerin güncellenmesi gerekir." cancel = "İptal" confirm = "Sil" title = "Kaynak silinsin mi?" -[portal.sources.detail] -closeAriaLabel = "Ayrıntıyı kapat" -delete = "Kaynağı sil" -docs24h = "Son 24 saat" -docs30d = "Son 30 gün" -docsTotal = "Toplam görülen" -docsTrend = "Son 30 gündeki belgeler" -documents = "Belgeler" -edit = "Düzenle" -notReferenced = "Hiçbir ilke tarafından referans verilmiyor, bu yüzden silmek güvenli." -pause = "Duraklat" -resume = "Sürdür" -subtitle = "{{type}} · {{status}}" -usedBy = "Kullanan" - -[portal.sources.empty] -description = "İlkelerinizin belgeleri alabileceği bir yer olması için bir klasör (ve yakında bulut depolama) bağlayın." -title = "Henüz bağlı kaynak yok" - [portal.sources.kpi] inUse = "Kullanımda" total = "Bağlantılar" unused = "Kullanılmıyor" +[portal.sources.networkFields.connection] +helperText = "Sunucu adresi ve kimlik bilgilerini içeren saklanan bağlantı. Ona referans veren her kaynak tarafından yeniden kullanılır." +label = "Bağlantı" + +[portal.sources.networkFields.directory] +helperText = "Sunucuda yoklanacak klasör, giriş ana dizinine veya share köküne göre göreli. Kök için boş bırakın." +label = "Klasör" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume, her politika işledikten sonra her dosyayı sunucudan kaldırır." +label = "Okuma modu" + +[portal.sources.networkFields.mode.options] +consume = "Consume: her dosyayı bir kez işle" +snapshot = "Snapshot: her çalıştırmada klasörü yeniden oku" + +[portal.sources.networkFields.recursive] +label = "Klasör derinliği" + +[portal.sources.networkFields.recursive.options] +all = "Alt klasörleri dahil et" +top = "Yalnızca üst seviye" + [portal.sources.status] active = "Aktif" disabled = "Devre dışı" unused = "Kullanılmıyor" [portal.sources.table] +documents = "Belgeler" source = "Kaynak" status = "Durum" +type = "Type" usedBy = "İlkeler" +[portal.sources.types.box] +description = "Ekibinizin zaten paylaştığı Box klasörlerinden belgeleri çekin." +label = "Box" + +[portal.sources.types.dropbox] +description = "Paylaşılan bir Dropbox klasöründen belgeleri çekin." +label = "Dropbox" + [portal.sources.types.editor] +description = "Ekibinizin editörde, politika ve AI çalıştırmaları genelinde işlediği belgeler." label = "Editor" +noPolicies = "Editörden henüz politika çalıştırılmadı." + +[portal.sources.types.email] +description = "Ayrılmış bir mailbox'a gönderilen ekleri belgelere dönüştürün." +label = "E-posta gelen kutusu" [portal.sources.types.folder] description = "Yeni belgeler için sunucudaki bir dizini izleyin." @@ -7943,25 +8829,80 @@ label = "Klasör derinliği" all = "Alt klasörleri dahil et" top = "Yalnızca üst seviye" +[portal.sources.types.ftp] +description = "Yeni belgeler için bir FTP veya FTPS sunucu klasörünü yoklayın." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Paylaşılan Google Drive klasörlerinden belgeleri çekin." +label = "Google Drive" + +[portal.sources.types.network] +description = "Yeni belgeler için bir SMB ağ paylaşımını izleyin." +label = "Ağ sürücüsü" + +[portal.sources.types.onedrive] +description = "Yeni belgeler için OneDrive klasörlerini izleyin." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Belgeleri Amazon S3 veya S3 uyumlu bir bucket'tan çekin." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Bucket ve kimlik bilgilerini tutan saklanan bağlantı. Ona referans veren her kaynak ve işlem hattı çıktısı tarafından yeniden kullanılır." +label = "Bağlantı" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume, her politika işledikten sonra her nesneyi bucket'tan kaldırır." +label = "Okuma modu" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: her nesneyi bir kez işle" +snapshot = "Snapshot: her çalıştırmada bucket'ı yeniden oku" + +[portal.sources.types.s3.fields.prefix] +helperText = "Yalnızca anahtarları bu prefix ile başlayan nesneler işlenir." +label = "Anahtar prefix'i" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Yeni belgeler için bir SFTP drop klasörünü yoklayın." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "SharePoint belge kitaplıklarından belgeleri çekin." +label = "SharePoint" + [portal.sources.types.unknown] label = "Kaynak" +[portal.sources.types.webhook] +createNote = "Bu webhook'u oluşturduğunuzda bir teslim URL'si ve imzalama secret'ı oluşturulur. Gönderenler belgeleri o URL'ye POST eder ve referans veren politikalar varışta çalışır." +description = "Herhangi bir harici sistemden imzalı HTTP POST ile belgeler alın." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Teslim URL'si" +secretNote = "İmzalama secret'ı webhook oluşturulduğunda bir kez gösterilir. Yenilemek için kaynağı yeniden oluşturun." + +[portal.sources.types.webhook.reveal] +copy = "Kopyala" +done = "Tamam" +secret = "İmzalama secret'ı" +secretHelp = "Her teslimatın ham body içeriğini bu anahtarla imzalayın (HMAC-SHA256) ve X-Stirling-Signature header'ı olarak gönderin." +secretWarning = "İmzalama secret'ını şimdi kopyalayın. Güvenliğiniz için yalnızca bir kez gösterilir ve daha sonra alınamaz." +title = "Webhook oluşturuldu" +url = "Teslim URL'si" +usage = "Her belgeyi binary content type (application/pdf veya application/octet-stream) ile ham request body olarak teslim URL'sine POST edin. Referans veren politikalar varışta otomatik olarak çalışır." + [portal.sources.wizard] -back = "Geri" -cancel = "İptal" -continue = "Devam" -editTitle = "Kaynağı düzenle" name = "Ad" namePlaceholder = "örn. Talep alımı" -save = "Değişiklikleri kaydet" -subtitle = "Adım {{current}} / {{total}} · {{label}}" -title = "Kaynak bağla" type = "Tür" -[portal.sources.wizard.steps] -chooseType = "Tür seç" -configure = "Yapılandır" -review = "İncele ve bağla" +[portal.stepModal] +close = "Kapat" [portal.usage] managePayment = "Ödemeyi yönet" @@ -8021,16 +8962,9 @@ summary = "Bir takımın sahibidir — takım üyelerinin kaynaklarını ve payl 2 = "Varsayılan ilke üzerinden portal erişimi" 3 = "Üyenin yapabildiği her şey" -[portal.welcome] -ariaLabel = "Stirling PDF'ye hoş geldiniz" -badge = "Açık kaynak" -installEditor = "Editor'ı yükle" -inviteTeammates = "Takım arkadaşlarını davet et" -openInBrowser = "Tarayıcıda aç" -perks = "Sonsuza kadar ücretsiz · Kendi sunucunuzda barındırılabilir" -subtitle = "Dünyanın en güvenli PDF Editor'ı, her boyuttaki takım için ücretsizdir. 60+ PDF işlemi ve SSO içerir." -title = "Hoş geldiniz" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Sınırsız" [printFile] title = "Dosya Yazdır" @@ -8613,6 +9547,14 @@ text = "PDF sayfalarınızı 90 derecelik artışlarla saat yönünde veya saat [rotate.tooltip.header] title = "Döndürme Ayarları Genel Bakış" +[ruler] +clearAll = "Tümünü temizle" +hideAllLabels = "Tüm etiketleri gizle" +hideSmallLabels = "Küçük etiketleri gizle" +physicalValues = "Fiziksel" +scaled = "Ölçekli" +showAllLabels = "Tüm etiketleri göster" + [sanitize] desc = "PDF dosyalarından potansiyel olarak zararlı öğeleri kaldırın." sanitizationResults = "Temizleme Sonuçları" @@ -8654,31 +9596,45 @@ label = "XMP Üstverisini Kaldır" [sanitize.steps] settings = "Ayarlar" -[sanitizePdf] -tags = "temizle,güvende,korunaklı,tehditleri-kaldır" - [sanitizePDF] title = "PDF'i Temizle" +[sanitizePdf] +tags = "temizle,güvende,korunaklı,tehditleri-kaldır" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Sayfa Ölçeğini Ayarla" +[scaleSettings] +activeScale = "Aktif Ölçek" +apply = "Ölçeği Uygula" +applyCalibration = "Kalibrasyonu Uygula" +calculatedScale = "Hesaplanan ölçek" +calibrate = "Kalibre et" +calibrating = "Kalibre ediliyor" +calibrationDistanceRequired = "Sıfırdan büyük bir gerçek dünya mesafesi girin" +calibrationInvalid = "Bu ölçümden ölçek hesaplanamıyor" +calibrationPaperDistance = "Ölçülen sayfa mesafesi: {{distance}}" +calibrationTitle = "Ölçeği Kalibre Et" +calibrationTooltip = "Ölçeği otomatik olarak hesaplamak için bilinen bir mesafeyi ölçün" +customScale = "Özel Ölçek" +noneSet = "Özel ölçek ayarlanmadı" +presets = "Hazır Ölçekler" +ratio = "Ölçek Oranı" +ratioHelp = "Oran: 1 sayfa birimi = X gerçek dünya birimi" +ratioPlaceholder = "örn., 100" +ratioPositive = "Oran sıfırdan büyük olmalıdır" +ratioRequired = "Oran gereklidir" +realDistance = "Gerçek dünya mesafesi" +realDistancePlaceholder = "örn., 5" +reset = "Varsayılanlara Sıfırla" +unit = "Birim" +unitInvalid = "Geçersiz birim: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Açı Eşiği:" -10 = "Çıktıda beyaz kenarların önlenmesi için eklenen ve kaldırılan kenarın boyutunu ayarlar (varsayılan: 1)." -2 = "Resmin döndürülmesi için gereken minimum mutlak açıyı ayarlar (varsayılan: 10)." -3 = "Tolerans:" -4 = "Tahmini arka plan rengi etrafındaki renk varyasyon aralığını belirler (varsayılan: 30)." -5 = "Minimum Alan:" -6 = "Bir fotoğraf için minimum alan eşiğini ayarlar (varsayılan: 10000)." -7 = "Minimum Kontur Alanı:" -8 = "Bir fotoğraf için minimum kontur alanı eşiğini ayarlar" -9 = "Kenar Boyutu:" - [scannerImageSplit] submit = "Görsel Taramalarını Çıkar" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Ne yapar" whatThisDoesDesc = "Taranmış bir sayfa veya bileşik görüntüdeki her fotoğrafı otomatik olarak bulur ve çıkarır—manuel kırpma yok." whenToUse = "Ne zaman kullanılır" +[ScannerImageSplit.selectText] +1 = "Açı Eşiği:" +10 = "Çıktıda beyaz kenarların önlenmesi için eklenen ve kaldırılan kenarın boyutunu ayarlar (varsayılan: 1)." +2 = "Resmin döndürülmesi için gereken minimum mutlak açıyı ayarlar (varsayılan: 10)." +3 = "Tolerans:" +4 = "Tahmini arka plan rengi etrafındaki renk varyasyon aralığını belirler (varsayılan: 30)." +5 = "Minimum Alan:" +6 = "Bir fotoğraf için minimum alan eşiğini ayarlar (varsayılan: 10000)." +7 = "Minimum Kontur Alanı:" +8 = "Bir fotoğraf için minimum kontur alanı eşiğini ayarlar" +9 = "Kenar Boyutu:" + [search] noResults = "Sonuç bulunamadı" placeholder = "Arama terimi girin..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Stirling-PDF sunucunuz çevrimdışı ve \"{{endpoint [session] expired = "Oturumunuzun süresi doldu. Lütfen sayfayı yenileyip tekrar deneyin." -[sessionManagement.tooltip] -header = "İmzalama Oturumlarını Yönetme" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Yeni katılımcılar imzalama sırasının sonuna eklenir" -bullet2 = "Oturum sonlandırıldıktan sonra katılımcı eklenemez" -bullet3 = "Her bir katılımcı sırası geldiğinde bir bildirim alır" -description = "Sonlandırmadan önce istediğiniz zaman aktif bir oturuma daha fazla katılımcı ekleyebilirsiniz." -title = "Katılımcı Ekleme" - -[sessionManagement.tooltip.finalization] -bullet1 = "Tam sonlandırma: Tüm katılımcılar imzaladı" -bullet2 = "Kısmi sonlandırma: Bazı katılımcılar henüz imzalamadı" -bullet3 = "İmzalamayan katılımcılar nihai belgeden hariç tutulur" -bullet4 = "Sonlandırdıktan sonra imzalı PDF'yi aktif dosyalara yükleyebilirsiniz" -description = "Sonlandırma, tüm imzaları tek bir imzalı PDF'de birleştirir. Bu işlem geri alınamaz." -title = "Oturumu Sonlandırma" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Zaten imzalamış katılımcılar kaldırılamaz" -bullet2 = "Kaldırılan katılımcılara artık bildirim gönderilmez" -bullet3 = "İmzalama sırası otomatik olarak ayarlanır" -description = "Katılımcılar imzalamadan önce oturumlardan kaldırılabilir." -title = "Katılımcıları Kaldırma" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Her imza PDF'ye sıralı olarak uygulanır" -bullet2 = "Sonraki imzalayanlar önceki imzaları görebilir" -bullet3 = "Onay iş akışları ve yasal teslim zincirleri için kritiktir" -description = "Oturumu oluştururken belirttiğiniz sıra, kimin önce imzalayacağını belirler." -title = "İmza Sırası" - [settings] close = "Kapat" +[settings.ai] +documents = "Belgeler ve RAG" +general = "Genel" +limits = "Sınırlar ve Performans" +models = "Modeller ve Sağlayıcılar" +title = "AI" + [settings.configuration] advanced = "Gelişmiş" database = "Veritabanı" endpoints = "Uç Noktalar" features = "Özellikler" +folderAccess = "Klasör Erişimi" mcp = "MCP Server" storageSharing = "Dosya Depolama ve Paylaşma" systemSettings = "Sistem Ayarları" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Varsayılan PDF düzenleyici" defaultPdfEditorActive = "Stirling PDF varsayılan PDF düzenleyiciniz" defaultPdfEditorChecking = "Kontrol ediliyor..." defaultPdfEditorInactive = "Varsayılan olarak başka bir uygulama ayarlı" +defaultPdfEditorRemind = "Varsayılan olarak ayarlamayı hatırlat" +defaultPdfEditorRemindDescription = "Stirling PDF varsayılan PDF uygulamanız olmadığında bir banner göster." defaultPdfEditorSet = "Zaten Varsayılan" defaultStartupView = "Başlangıçtaki varsayılan görünüm" defaultStartupViewDescription = "Uygulama başlarken sol sütunda hangi sekmenin etkin olacağını seçin" @@ -8817,9 +9763,18 @@ languageDescription = "Görüntüleme dilini seçin" logout = "Oturumu kapat" setAsDefault = "Varsayılan Yap" theme = "Tema" +themeAccent = "Vurgu rengi" +themeAccentCustom = "Özel renk" +themeAccentCustomHint = "Özel (varsayılanlar önerilir)" +themeAccentDark = "Koyu mod vurgu rengi" +themeAccentDefault = "Varsayılan" +themeAccentDefaultHint = "Varsayılan tema (önerilir)" +themeAccentDescription = "Düğmeler, bağlantılar ve vurgular bunu takip eder ve uygulamayı hafifçe renklendirir. Açık ve koyu modların her birinin kendine ait rengi vardır. Sistem, hangisi etkinse onu kullanır." +themeAccentLight = "Açık mod vurgu rengi" themeDark = "Koyu" themeDescription = "Açık ve koyu mod arasında geçiş yapın" themeLight = "Açık" +themeReset = "Temayı varsayılana geri yükle" themeSystem = "System" title = "Genel" user = "Kullanıcı" @@ -8833,6 +9788,12 @@ intro = "Kuruluşunuz için kullanıcı kimlik doğrulaması, ekip yönetimi ve learnMore = "Belgelerde daha fazla bilgi edinin" title = "Sistem Yöneticileri için" +[settings.general.loginLanding] +description = "Stirling Cloud'da oturum açtığınızda hangi uygulamanın açılacağını seçin." +editor = "Düzenleyici" +processor = "İşleyici" +title = "Oturum açtıktan sonra" + [settings.general.mode] fullscreen = "Tam ekran" sidebar = "Kenar çubuğu" @@ -9013,9 +9974,6 @@ title = "Politikalar ve Gizlilik" [settings.preferences] title = "Tercihler" -[settings.search] -placeholder = "Ayar sayfalarında ara..." - [settings.security] description = "Hesabınızı güvende tutmak için şifrenizi güncelleyin." title = "Güvenlik" @@ -9041,6 +9999,11 @@ security = "Güvenlik" telegram = "Telegram" title = "Güvenlik ve Kimlik Doğrulama" +[settings.signOut] +confirm = "Oturumu kapatmak istediğinizden emin misiniz?" +submit = "Oturumu kapat" +title = "Oturumu kapat" + [settings.team] title = "Takım" @@ -9156,6 +10119,7 @@ last = "Son sayfa" maintainRatio = "Oranı korumayı değiştir" next = "Sonraki sayfa" noSavedSigs = "Kayıtlı imza bulunamadı" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Kişisel İmzalar" previous = "Önceki sayfa" redo = "Yinele" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Arka plan kaldırılamadı" hint = "İmzanızın PNG veya JPG görselini yükleyin" label = "İmza görseli yükle" placeholder = "Görsel dosyası seç" +previewAlt = "Current image signature" processing = "Görsel işleniyor..." removeBackground = "Beyaz arka planı kaldır (şeffaf yap)" @@ -9207,6 +10172,17 @@ saved = "Yukarıdan kaydedilmiş bir imza seçin, ardından PDF üzerinde herhan text = "Adınızı yukarıya girdikten sonra, imzanızı PDF üzerinde herhangi bir yere tıklayarak yerleştirin." title = "İmza nasıl eklenir" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "İmzayı Taşı" pause = "Yerleştirmeyi duraklat" @@ -9653,10 +10629,30 @@ title = "Sunucuya Yükle" updateButton = "Sunucuda Güncelle" uploadButton = "Sunucuya Yükle" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "buraya tıklayın" +fallback = "Otomatik olarak açılmadıysa," +opening = "Swagger UI yeni bir sekmede açılıyor..." tags = "api,dokümantasyon,swagger,uç noktalar,geliştirme" title = "API Dokümantasyonu" @@ -9764,12 +10760,39 @@ selectFilesHint = "Bu aracı çalıştırmak için Etkin Dosyalar içinden dosya singleFileScope = "Yalnızca şu dosyaya uygulanıyor: {{fileName}}" viewerMode = "Birden çok dosya seçmek için dosya düzenleyiciye geçin." +[toolFormat] +ANY = "herhangi bir dosya" +CBR = "bir çizgi roman arşivi (CBR)" +CBZ = "bir çizgi roman arşivi (CBZ)" +CSV = "bir CSV dosyası" +EBOOK = "bir e-kitap" +EMAIL = "bir e-posta" +EXCEL = "bir elektronik tablo" +HTML = "bir web sayfası" +IMAGE = "görseller" +JAVASCRIPT = "bir JavaScript dosyası" +JSON = "bir JSON dosyası" +MARKDOWN = "bir Markdown dosyası" +NONE = "dosya yok" +PCL = "bir PCL yazdırma dosyası" +PDF = "bir PDF" +PDF_ENCRYPTED = "parola korumalı bir PDF" +POSTSCRIPT = "bir PostScript dosyası" +PPT = "bir sunum" +TEXT = "bir metin dosyası" +VIDEO = "bir video" +WORD = "bir Word belgesi" +XML = "bir XML dosyası" +XPS = "bir XPS belgesi" +ZIP = "bir ZIP arşivi" + [toolPanel] alpha = "Alfa" backToAllTools = "Tüm araçlara geri dön" collapse = "Paneli daralt" expand = "Paneli genişlet" goBack = "Geri dön" +pdfTools = "PDF Araçları" placeholder = "Başlamak için bir araç seçin" premiumFeature = "Premium özellik:" toolsHeader = "Araçlar" @@ -9806,11 +10829,6 @@ noToolsFound = "Hiç araç bulunamadı" quickAccess = "HIZLI ERİŞİM" searchPlaceholder = "Araçlarda ara..." -[toolPicker.categories] -advancedTools = "Gelişmiş Araçlar" -recommendedTools = "Önerilen Araçlar" -standardTools = "Standart Araçlar" - [toolPicker.subcategories] advancedFormatting = "Gelişmiş Biçimlendirme" automation = "Otomasyon" @@ -9824,6 +10842,9 @@ removal = "Kaldırma" signing = "İmzalama" verification = "Doğrulama" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Araç bulunamadı" noTools = "Kullanılabilir araç yok" @@ -9950,28 +10971,53 @@ title = "Kullanıcılar" you = "(siz)" [users.action] +cancelInvite = "Davetiyeyi iptal et" deleteTeam = "Takımı sil" disableMfa = "MFA'yı sıfırla" move = "Takıma taşı" reinstate = "Yeniden etkinleştir" remove = "Kuruluştan kaldır" +removeTeam = "Takımdan kaldır" rename = "Takımı yeniden adlandır" resetPw = "Parolayı sıfırla" suspend = "Askıya al" unlock = "Hesabın kilidini aç" +[users.activity] +daysAgo = "{{count}} gün önce" +hoursAgo = "{{count}} saat önce" +justNow = "Az önce" +minutesAgo = "{{count}} dk önce" +monthsAgo_one = "{{count}} ay önce" +monthsAgo_other = "{{count}} ay önce" +never = "Hiçbir zaman" +weeksAgo_one = "{{count}} hafta önce" +weeksAgo_other = "{{count}} hafta önce" +yearsAgo_one = "{{count}} yıl önce" +yearsAgo_other = "{{count}} yıl önce" + [users.cap] addProcessor = "+ Processor" approver = "İlkeyi onaylar" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "{{email}} adresine gönderilen davetiye iptal edilsin mi? Mevcut bağlantıyla katılamayacaklar." +cancelInviteTitle = "Davetiyeyi iptal et" deleteTeamBody = "{{name}} takımı silinsin mi? Takım önce boş olmalıdır - üyelerini başka bir takıma taşıyın ve hâlâ herhangi bir entegrasyon yapılandırmasına sahip olmamalıdır." deleteTeamTitle = "Takımı sil" disableMfaBody = "{{name}} adlı kullanıcının MFA kaydı kaldırılsın mı? Gerekliyse bir sonraki girişte tekrar kuracak." disableMfaTitle = "MFA'yı sıfırla" removeBody = "{{name}} adlı kullanıcı kuruluştan kalıcı olarak kaldırılsın mı? Bu işlem geri alınamaz." +removeTeamBody = "{{name}} takımdan kaldırılsın mı? Hesabını korur ancak bu takımın kaynaklarına erişimini kaybeder." removeTitle = "Üyeyi kaldır" [users.empty] @@ -9982,10 +11028,8 @@ title = "Henüz üye yok" addToTeam = "Takıma ekle" guestCount = "{{count}} misafir" guests = "Misafirler" -guestsDesc = "Dış iş ortakları, paylaştıklarınızla sınırlıdır. Yalnızca Editor." ledBy = "{{owner}} tarafından yönetiliyor" org = "Kuruluş" -orgDesc = "Kuruluş genelinde yetkiye ve ilke onayına sahip sahipler" owners = "{{count}} sahip" team = "{{name}} takımı" teamMeta = "{{count}} kişi" @@ -10022,6 +11066,18 @@ username = "Kullanıcı adı" usernameError = "Kullanıcı adı en az 3 karakter olmalıdır" usernamePlaceholder = "jsmith" +[users.invites] +by = "Davet eden: {{who}}" +cancel = "İptal" +expiresInDays_one = "{{count}} gün içinde sona erer" +expiresInDays_other = "{{count}} gün içinde sona erer" +expiresToday = "Bugün sona erer" +title = "Bekleyen davetiyeler" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Arka uca ulaşılırken bir sorun oluştu veya erişiminiz yok. Tekrar deneyin." title = "Üyeler yüklenemedi" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Eklerde ara" title = "Ekler" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Yer imi başlığı" +bookmarkTitleRequired = "Yer imi başlığı gereklidir" closeSidebar = "Yer imleri kenar çubuğunu kapat" collapseAll = "Tüm yer imlerini daralt" +empty = "No bookmarks in this document" expandAll = "Tüm yer imlerini genişlet" +searchPlaceholder = "Yer imlerini ara" +title = "Bookmarks" [viewer.comments] addComment = "Yorum ekle" @@ -10236,10 +11297,12 @@ locateAnnotation = "Belgede konumlandır" moreActions = "Diğer işlemler" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Sayfa {{page}}" placingHint = "Yerleştirmek için bir sayfaya tıklayın… (iptal)" removeCommentOnly = "Yalnızca yorumu kaldır" saveReply = "Yanıtı kaydet" +searchPlaceholder = "Search comments" title = "Yorumlar" typeComment = "Yorum" typeInsertText = "Metin Ekle" @@ -10262,6 +11325,7 @@ unsavedDesc = "Kaydedilmemiş değişiklikleriniz var" closeSidebar = "Katmanlar kenar çubuğunu kapat" hideAll = "Tüm katmanları gizle" showAll = "Tüm katmanları göster" +title = "Layers" [viewer.link] delete = "Bağlantıyı sil" @@ -10605,30 +11669,6 @@ title = "Filigran Metni" image = "Resim" text = "Metin" -[wetSignature.tooltip] -header = "İmza Oluşturma Yöntemleri" - -[wetSignature.tooltip.draw] -bullet1 = "Kalem rengi ve kalınlığını özelleştirin" -bullet2 = "Memnun kalana kadar temizleyip yeniden çizin" -bullet3 = "Dokunmatik cihazlarda çalışır (tabletler, telefonlar)" -description = "Fareyi veya dokunmatik ekranı kullanarak el yazısı imza oluşturun. Kişisel ve özgün imzalar için en iyisidir." -title = "İmza Çiz" - -[wetSignature.tooltip.type] -bullet1 = "Birden çok yazı tipinden seçin" -bullet2 = "Metin boyutunu ve rengini özelleştirin" -bullet3 = "Standartlaştırılmış imzalar için idealdir" -description = "Yazılan metinden bir imza oluşturun. Hızlı ve tutarlıdır, iş belgeleri için uygundur." -title = "İmza Yaz" - -[wetSignature.tooltip.upload] -bullet1 = "PNG, JPG ve diğer görüntü formatlarını destekler" -bullet2 = "En iyi sonuçlar için şeffaf arka plan önerilir" -bullet3 = "Görüntü, imza alanına sığacak şekilde yeniden boyutlandırılacaktır" -description = "Önceden oluşturulmuş bir imza görüntüsü yükleyin. Taranmış bir imzanız veya şirket logonuz varsa idealdir." -title = "İmza Görüntüsü Yükle" - [workbenchBar] activeFiles = "Etkin dosyalar" annotations = "Ek açıklamalar" @@ -10642,6 +11682,7 @@ exitRedaction = "Karartma modundan çık" exportAll = "PDF'yi dışa aktar" exportSelected = "Seçili sayfaları dışa aktar" formFill = "Form doldur" +hideToolbar = "Hide toolbar" multiTool = "Çoklu araç" panMode = "Kaydırma modu" print = "PDF'yi yazdır" @@ -10652,6 +11693,7 @@ redact = "Karart" rotateLeft = "Sola döndür" rotateRight = "Sağa döndür" ruler = "Cetvel / Ölçüm" +rulerSettings = "Ölçek Ayarları" save = "Kaydet" saveAll = "Tümünü kaydet" saveAs = "Farklı kaydet" @@ -10661,6 +11703,7 @@ selectAll = "Tümünü seç" selectByNumber = "Sayfa numaralarına göre seç" selectLanguage = "Dil seç" share = "Paylaş" +showToolbar = "Show toolbar" toggleAnnotations = "Ek açıklamaların görünürlüğünü değiştir" toggleAttachments = "Ekleri göster/gizle" toggleBookmarks = "Yer imlerini göster/gizle" diff --git a/frontend/editor/public/locales/uk-UA/translation.toml b/frontend/editor/public/locales/uk-UA/translation.toml index c4e274255b..aa777bf20a 100644 --- a/frontend/editor/public/locales/uk-UA/translation.toml +++ b/frontend/editor/public/locales/uk-UA/translation.toml @@ -1,7 +1,6 @@ alphabet = "Алфавіт" apply = "Застосувати" applyAndContinue = "Зберегти й вийти" -areYouSure = "Ви впевнені, що хочете вийти?" back = "Назад" black = "Чорний" blue = "Синій" @@ -46,10 +45,8 @@ filesSelected = "файлів обрано" font = "Шрифт" fontSizeTooltip = "Розмір тексту номера сторінки у пунктах. Більші числа дають більший текст." fontTypeTooltip = "Гарнітура для номерів сторінок. Оберіть відповідно до стилю документа." -genericSubmit = "Надіслати" green = "Зелений" help = "Допомога" -imgPrompt = "Оберіть зображення(я)" incorrectPasswordMessage = "Поточний пароль невірний." info = "Інформація" invalidUndoData = "Не можна скасувати: недійсні дані операції" @@ -62,7 +59,6 @@ loginAgreementTitle = "Угода під час входу" logOut = "Вийти" marginTooltip = "Відстань між номером сторінки та краєм сторінки." moreOptions = "Додаткові параметри" -multiPdfPrompt = "Оберіть PDFи (2+)" never = "Ніколи" no = "Ні" noFileSelected = "Файл не вибрано. Будь ласка, завантажте один." @@ -80,7 +76,6 @@ pageOrderPrompt = "Порядок сторінок (введіть список pages = "Сторінки" pageSelectionPrompt = "Користувацький вибір сторінки (введіть список номерів сторінок через кому 1,5,6 або функції типу 2n+1) :" password = "Пароль" -pdfPrompt = "Оберіть PDF(и)" pin = "Закріпити файл (залишити активним після виконання інструмента)" pro = "Pro" processingComplete = "Ваш файл готовий." @@ -110,6 +105,7 @@ undoSuccess = "Операцію успішно скасовано" unknown = "Невідомо" unpin = "Відкріпити файл (замінити після виконання інструмента)" unsavedChanges = "У вас є незбережені зміни у вашому PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Незбережені зміни" unsupported = "Не підтримується" username = "Ім'я користувача" @@ -356,6 +352,7 @@ text = "Ці дозволи контролюють, що користувачі title = "Змінити дозволи" [addStamp] +preview = "Попередній перегляд штампа" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Ручні завантаження: натисніть поси noLanguages = "У налаштованому каталозі не знайдено мов tessdata." permissionNotice = "Шлях до tessdata недоступний для запису. Завантаження буде відкрито в браузері; будь ласка, збережіть файли .traineddata вручну до папки tessdata." +[admin.settings.ai.documents] +description = "Налаштуйте модель ембедингів і параметри пошуку, що використовуються для відповідей на запитання щодо документів. Застосовується до AI engine після збереження." +title = "Документи та RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Залиште порожнім, щоб використовувати власні облікові дані середовища engine. Застосовується до самостійно розгорнутих одно-engine розгортань." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Збережено - залиште порожнім, щоб зберегти поточний ключ" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Базовий URL OpenAI-сумісної / Ollama кінцевої точки ембедингів, напр. http://ollama:11434/v1. Має вказувати на довірену внутрішню кінцеву точку (SSRF-чутливо)." +label = "Базовий URL ембедингів" + +[admin.settings.ai.documents.embeddingModel] +description = "Назва моделі ембедингів. Довільний текст; пропозиції є лише підказками." +label = "Модель ембедингів" + +[admin.settings.ai.documents.embeddingProvider] +description = "Постачальник, що використовується для перетворення тексту документа на векторні ембединги." +label = "Постачальник ембедингів" + +[admin.settings.ai.documents.maxSearches] +description = "Максимальна кількість пошукових запитів для отримання даних, які агент може виконати за один запит." +label = "Макс. пошуків" + +[admin.settings.ai.documents.reindexNote] +body = "Зміна моделі ембедингів набуває чинності негайно, але документи, проіндексовані попередньою моделлю, потрібно переіндексувати, щоб пошук повертав правильні результати." +title = "Потрібне переіндексування" + +[admin.settings.ai.documents.saved] +reindexNote = "Якщо ви змінили модель ембедингів, переіндексуйте наявні документи, щоб пошук використовував нову модель." + +[admin.settings.ai.documents.topK] +description = "Кількість найрелевантніших фрагментів, отриманих за один пошук." +label = "Top K" + +[admin.settings.ai.general] +description = "Підключіть Stirling до Python AI engine і виберіть, які можливості AI будуть доступні. Зміни застосовуються після перезапуску." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Увімкніть або вимкніть окремі функції AI. Вимкнені функції приховані в застосунку." +title = "Можливості" + +[admin.settings.ai.general.enabled] +description = "Головний перемикач. Якщо вимкнено, інструменти AI, агенти або виклики engine недоступні." +label = "Увімкнути AI" + +[admin.settings.ai.general.features.chat] +description = "Розмовний помічник для роботи з PDF." +label = "Чат-помічник" + +[admin.settings.ai.general.features.classify] +description = "Автоматично категоризувати документи за типом або вмістом." +label = "Класифікація документів" + +[admin.settings.ai.general.features.createPdf] +description = "Створити новий PDF (наприклад, з HTML) через AI-агента." +label = "Створити PDF із запиту" + +[admin.settings.ai.general.features.documentQuestions] +description = "Ставте запитання й отримуйте відповіді на основі завантаженого документа." +label = "Запитання щодо документа" + +[admin.settings.ai.general.features.mathAuditor] +description = "Перевіряти документи на математичні та числові помилки." +label = "Математичний аудитор" + +[admin.settings.ai.general.features.pdfComment] +description = "Додавати до PDF коментарі та анотації огляду, створені AI." +label = "Агент коментарів PDF" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Тайм-аут для важчих операцій агентів, таких як створення документів." +label = "Тайм-аут тривалих операцій (секунди)" + +[admin.settings.ai.general.note] +body = "AI engine працює як окремий сервіс. Його спільний секрет" +body2 = "задається через змінну середовища контейнера. Provider API keys можна ввести на цих сторінках або надати як змінні середовища engine; ключі, введені тут, надсилаються до engine після збереження." +title = "Про AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Тайм-аут для потокових (токен за токеном) відповідей чату." +label = "Тайм-аут потоку (секунди)" + +[admin.settings.ai.general.test] +button = "Перевірити з’єднання" +failBody = "AI engine не відповів. Перевірте URL, чи запущено контейнер engine і чи увімкнено AI (після увімкнення потрібен перезапуск)." +failTitle = "AI engine недоступний" +okBody = "AI engine відповів на перевірку справності." +okTitle = "AI engine доступний" + +[admin.settings.ai.general.timeoutSeconds] +description = "Тайм-аут для стандартних AI-запитів до engine." +label = "Тайм-аут запиту (секунди)" + +[admin.settings.ai.general.url] +description = "Внутрішній URL Python AI engine, напр. http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Обмеження для обсягу роботи, яку можуть виконувати AI-запити, і кількості одночасних запусків. Застосовується до AI engine після збереження." +title = "Ліміти та продуктивність" + +[admin.settings.ai.limits.maxCharacters] +description = "Обмеження: відхиляти AI-запити, витягнутий текст яких перевищує цю довжину." +label = "Макс. символів на запит" + +[admin.settings.ai.limits.maxPages] +description = "Обмеження: відхиляти AI-запити з більшою кількістю сторінок PDF." +label = "Макс. сторінок на запит" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Максимальна кількість одночасних активних викликів моделі в усьому engine." +label = "Макс. паралельність моделі" + +[admin.settings.ai.models] +description = "Виберіть LLM provider і розумні/швидкі моделі, які використовує AI engine. Застосовується до AI engine після збереження." +title = "Моделі та постачальники" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Залиште порожнім, щоб використовувати власні облікові дані середовища engine. Застосовується до самостійно розгорнутих одно-engine розгортань." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Збережено - залиште порожнім, щоб зберегти поточний ключ" + +[admin.settings.ai.models.baseUrl] +description = "Базовий URL OpenAI-сумісної / Ollama кінцевої точки, напр. http://ollama:11434/v1." +label = "Базовий URL постачальника" +warning = "Базовий URL має вказувати на довірену внутрішню кінцеву точку. Engine виконуватиме до неї серверні запити, тому недовірене значення є SSRF-чутливим." + +[admin.settings.ai.models.fastMaxTokens] +description = "Максимальна кількість вихідних токенів для швидкої моделі." +label = "Макс. токенів швидкої моделі" + +[admin.settings.ai.models.fastModel] +description = "Дешевша, швидша модель для легких завдань. Довільний текст; пропозиції є лише підказками." +label = "Швидка модель" + +[admin.settings.ai.models.provider] +description = "З яким LLM provider взаємодіє engine." +label = "Постачальник" + +[admin.settings.ai.models.smartMaxTokens] +description = "Максимальна кількість вихідних токенів для розумної моделі." +label = "Макс. токенів розумної моделі" + +[admin.settings.ai.models.smartModel] +description = "Модель із високими можливостями для складних міркувань. Довільний текст; пропозиції є лише підказками." +label = "Розумна модель" + +[admin.settings.ai.saved] +body = "Зміни автоматично надсилаються до AI engine." +bodyNoPush = "Налаштування збережено. Вони застосуються, коли AI engine наступного разу підхопить свою конфігурацію." +title = "Налаштування AI збережено" + [admin.settings.badge] clickToUpgrade = "Клацніть, щоб переглянути деталі плану" @@ -938,6 +1094,25 @@ label = "Генерувати знову під час запуску" description = "Кількість днів дії сертифіката" label = "Строк дії сертифіката (дні)" +[admin.settings.folderAccess] +description = "Каталоги, з яких джерелам папок і виходам папок дозволено читати та до яких дозволено записувати. Це межа безпеки: автоматизації ніколи не можна спрямувати на серверний шлях поза цим списком." +securityNote = "Залиште порожнім, щоб повністю вимкнути джерела папок і виходи. Власний каталог конфігурації Stirling завжди заборонений." +title = "Доступ до папок" + +[admin.settings.folderAccess.implied] +description = "Ці каталоги, керовані Stirling, завжди дозволені й не можуть бути змінені тут." +serverStorage = "Сховище файлів сервера" +title = "Завжди дозволено" +watchedFolder = "Відстежувана папка конвеєра" + +[admin.settings.folderAccess.roots] +add = "Додати" +empty = "Дозволених папок немає. Джерела папок і виходи наразі вимкнені." +hint = "Введіть абсолютні шляхи, наприклад /data/inbox." +label = "Дозволені корені папок" +placeholder = "/data/inbox" +remove = "Видалити корінь папки" + [admin.settings.general] description = "Налаштуйте загальносистемні параметри застосунку, зокрема брендинг і поведінку за замовчуванням." system = "Система" @@ -1704,6 +1879,9 @@ width = "Ширина" [app] description = "Безкоштовна альтернатива Adobe Acrobat (10M+ завантажень)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Перетворити в PDF/A-3b" convertToPdfA3bDescription = "Створює архівний PDF із вбудованими вкладеннями" @@ -1842,6 +2020,15 @@ export = "Експорт" accessDenied = "Доступ заборонено" insufficientPermissions = "У вас немає прав для виконання цієї дії." +[auth.callback] +completing = "Завершення автентифікації" +invalidToken = "Вхід через OAuth не вдався - недійсний токен." +missingToken = "Вхід через OAuth не вдався - токен не отримано." +oauthFailed = "Вхід через OAuth не вдався. Спробуйте ще раз." +pleaseWait = "Зачекайте, поки ми завершимо ваш вхід." +signedOut = "Ви вийшли з системи. Увійдіть знову." +windowMayClose = "Ви можете закрити це вікно після завершення." + [auth.displayName] guest = "Гість" user = "Користувач" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Захищає документи PDF, видаляючи description = "Цей інструмент автоматично перейменує файли PDF на основі їхнього вмісту. Він аналізує документ, щоб знайти найвідповіднішу назву з тексту." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Автоповорот" +tags = "автоповорот,виправити орієнтацію,догори дриґом,боком,вирівняти,орієнтувати,визначити орієнтацію,виправити поворот,автоорієнтація,скановано,OSD" + +[autoRotate.confidenceThreshold] +title = "Поріг упевненості" + +[autoRotate.detectionMode] +auto = "Авто" +osd = "Лише OCR" +text = "Лише текст" +title = "Метод визначення" + +[autoRotate.error] +failed = "Під час автоповороту PDF сталася помилка." + +[autoRotate.inferUndetected] +title = "Повернути сторінки з низькою впевненістю відповідно" + +[autoRotate.report] +applied = "{{degrees}}° за год. стр." +confidenceValue = "Упевненість {{value}}" +noChange = "Без змін" +pageLabel = "Сторінка {{number}}" +summary = "Повернуто {{rotated}} з {{total}} сторінок, {{unchanged}} залишено без змін" +title = "Звіт визначення" + +[autoRotate.report.method] +inferred = "Зіставлено" +none = "Пропущено" +osd = "OCR" +text = "Текст" + +[autoRotate.report.note] +belowThreshold = "Нижче порога" +blankPage = "Сторінка порожня" +inferredFromDocument = "Так само, як інші сторінки" +noDominantDirection = "Змішані напрямки тексту" +osdFailed = "Читабельний текст не знайдено" +osdNoVerdict = "Не вдалося визначити верх" +tesseractUnavailable = "OCR не встановлено" +tooFewGlyphs = "Замало тексту" + +[autoRotate.results] +title = "Результати автоповороту" + +[autoRotate.settings] +title = "Налаштування" + +[autoRotate.tooltip.confidenceThreshold] +text = "Наскільки певною має бути перевірка, перш ніж сторінку буде повернуто. Зменште, щоб повертати більше сторінок, збільште, щоб залишати більше сторінок без змін." +title = "Поріг упевненості" + +[autoRotate.tooltip.description] +text = "Визначає, якою стороною догори має бути кожна сторінка, і повертає її правильно. Сторінки, які вже розташовані правильно, залишаються без змін." + +[autoRotate.tooltip.detectionMode] +text = "Авто спочатку читає текст на сторінці, а потім, якщо цього недостатньо, розглядає сторінку як зображення. Лише текст пропускає перевірку зображення, що найшвидше для документів, які ніколи не сканувалися. Лише OCR обробляє кожну сторінку як зображення." +title = "Метод визначення" + +[autoRotate.tooltip.header] +title = "Автоповорот" + +[autoRotate.tooltip.inferUndetected] +text = "Порожні або майже порожні сторінки не можна перевірити окремо. Коли всі сторінки, які вдалося прочитати, потребують однакового повороту, ці сторінки повертаються відповідно до них." +title = "Повернути сторінки з низькою впевненістю відповідно" + [autoSizeSplitPDF] tags = "pdf,розділити,документ,організація" @@ -2189,33 +2443,6 @@ keywords = "Ключові слова: odd, even" numbers = "Числа/діапазони: 5, 10-20" progressions = "Прогресії: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Типи сертифікатів" - -[certificateChoice.tooltip.organization] -bullet1 = "Керується адміністраторами системи" -bullet2 = "Спільний для авторизованих користувачів" -bullet3 = "Представляє компанію, а не окрему особу" -bullet4 = "Найкраще для: офіційних документів, командних підписів" -description = "Спільний сертифікат, наданий вашою організацією. Використовується для загальноорганізаційного підписання." -title = "Сертифікат організації" - -[certificateChoice.tooltip.personal] -bullet1 = "Автоматично створюється під час першого використання" -bullet2 = "Прив’язаний до вашого облікового запису" -bullet3 = "Не може бути спільним з іншими користувачами" -bullet4 = "Найкраще для: особистих документів, індивідуальної відповідальності" -description = "Автоматично створений сертифікат, унікальний для вашого облікового запису. Підходить для індивідуальних підписів." -title = "Персональний сертифікат" - -[certificateChoice.tooltip.upload] -bullet1 = "Потрібен файл P12/PFX і пароль" -bullet2 = "Може бути виданий зовнішніми центрами сертифікації" -bullet3 = "Вищий рівень довіри для юридичних документів" -bullet4 = "Найкраще для: юридично зобов’язуючих контрактів, зовнішньої валідації" -description = "Використовуйте власний файл сертифіката PKCS#12. Забезпечує повний контроль над властивостями сертифіката." -title = "Завантажити власний P12" - [certSign] awaitingSignatures = "Очікування підписів" chooseCertificate = "Вибрати файл сертифіката" @@ -2542,34 +2769,6 @@ server = "Сервер" stepTitle = "Джерело сертифіката" upload = "Завантажити" -[certSign.tooltip.header] -title = "Про керування підписами" - -[certSign.tooltip.overview] -bullet1 = "Перевіряйте наявні підписи та їхню дійсність" -bullet2 = "Переглядайте детальну інформацію про підписантів і сертифікати" -bullet3 = "Додавайте нові цифрові підписи для захисту ваших документів" -bullet4 = "Підтримується кілька файлів із зручною навігацією" -text = "Цей інструмент дозволяє перевірити, чи підписані ваші PDF цифровим підписом і додати нові цифрові підписи. Цифрові підписи підтверджують, хто створив або затвердив документ, і показують, чи його змінювали після підписання." -title = "Що може цей інструмент?" - -[certSign.tooltip.signing] -bullet1 = "Підтримує формати PEM, PKCS12, JKS і серверні сертифікати" -bullet2 = "Можна показувати або приховувати підпис у PDF" -bullet3 = "Додавайте причину, місце та ім’я підписанта" -bullet4 = "Вибирайте сторінку для розміщення видимих підписів" -bullet5 = "Використовуйте серверний сертифікат для простого варіанту 'Підписати зі Stirling-PDF'" -text = "Щоб підписати PDF, вам потрібен цифровий сертифікат (наприклад, PEM, PKCS12 або JKS). Можна зробити підпис видимим у документі або невидимим — лише для безпеки." -title = "Додавання підписів" - -[certSign.tooltip.validation] -bullet1 = "Показує, чи підписи дійсні або недійсні" -bullet2 = "Відображає інформацію про підписанта та дату підпису" -bullet3 = "Перевіряє, чи документ було змінено після підписання" -bullet4 = "Може використовувати користувацькі сертифікати для перевірки" -text = "Під час перевірки інструмент повідомляє, чи дійсні підписи, хто підписав документ, коли його підписано та чи змінювався документ після підписання." -title = "Перевірка підписів" - [changeCreds] changeUsername = "Оновіть ім’я користувача. Після оновлення вас буде виведено із системи." credsUpdated = "Обліковий запис оновлено" @@ -2737,12 +2936,16 @@ text = "Щоб зробити ці дозволи незмінними, скор [chat.actions] copy = "Копіювати повідомлення" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Параметри агента Stirling" +assistant = "AI Assistant" clearChat = "Очистити чат" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Ця операція використає ваші хмарні кр tooltip = "Вибрати колір з екрана" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Виберіть колір" [common] +actions = "Actions" back = "Назад" cancel = "Скасувати" close = "Закрити" +codeSample = "Code sample" collapse = "Згорнути" confirm = "Підтвердити" continue = "Продовжити" copied = "Скопійовано!" copy = "Копіювати" done = "Готово" +enterValue = "Enter value" error = "Помилка" expand = "Розгорнути" loading = "Завантаження..." next = "Далі" +open = "Open" preview = "Попередній перегляд" previous = "Назад" refresh = "Оновити" +remaining = "Залишилося" retry = "Повторити" save = "Зберегти" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Очистити вибране" @@ -2988,6 +3199,7 @@ title = "Метод стиснення" [compress.settings] desiredSize = "Бажаний розмір файлу" desiredSizePlaceholder = "Введіть розмір" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Стиснення — простий спосіб зменшити розмір файлу. Виберіть «Розмір файлу», щоб вказати цільовий розмір і ми підлаштуємо якість за вас. Або виберіть «Якість», щоб вручну задати силу стиснення." @@ -3190,6 +3402,24 @@ pdfOptions = "Параметри PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Спочатку виберіть вихідний формат" settings = "Налаштування" single = "Один" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Розширений моніторинг" -allInOneWorkspace = "Єдиний робочий простір для PDF (переглядач, інструменти та агент)" -apiSandbox = "Пісочниця API" -dedicatedSupportSlas = "Виділена підтримка та SLA" -fasterThroughput = "Пропускна здатність у 10 разів вища" -fullyPrivateFiles = "Повністю приватні файли" -largeFileProcessing = "Обробка великих файлів" -orgWideAccess = "Контроль доступу по всій організації" -premiumAiModels = "Преміум AI-моделі" -prioritySupport = "Пріоритетна підтримка" -privateDocCloud = "Приватна хмара документів" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Безпечний доступ до API" -standardThroughput = "Стандартна пропускна здатність" -unlimitedApiAccess = "Необмежений доступ до API" -unlimitedSeats = "Необмежена кількість місць" - [crop] autoCrop = "Автообрізання білих полів" noFileSelected = "Виберіть PDF, щоб почати обрізання" @@ -3346,6 +3558,7 @@ message = "Не вдалося встановити обробник PDF за з title = "Помилка" [defaultApp.prompt] +dontRemind = "Більше не нагадувати" message = "Зробіть Stirling PDF типовою програмою для відкриття PDF-файлів." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Пошук..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Редагувати" @@ -3644,11 +3861,13 @@ shareSelected = "Поділитися вибраними" sharing = "Спільний доступ" showAll = "Показати все" showHistory = "Показати історію" +sortBy = "Sort files" sortByDate = "Сортувати за датою" sortByName = "Сортувати за назвою" sortBySize = "Сортувати за розміром" storage = "Сховище" storageState = "Сховище" +storageUsed = "Storage used" synced = "Синхронізовано" title = "Завантажити файли PDF" toolChain = "Застосовані інструменти" @@ -3694,13 +3913,17 @@ addFiles = "Додати файли" addingFiles = "Додавання файлів…" collapse = "Згорнути бічну панель" customizeGroups = "Налаштувати групи" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Відкрийте файли, щоб почати" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Розгорнути бічну панель" -files = "Файли" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive не налаштовано" leaveMyFiles = "Вийти з «Мої файли»" +library = "Бібліотека PDF" myFiles = "Мої файли" noFiles = "Файлів ще немає" openFileManager = "Відкрити файловий менеджер" @@ -3708,37 +3931,36 @@ openFromComputer = "Відкрити з комп’ютера" openSettings = "Відкрити налаштування" other = "Інше" recent = "Нещодавні" -search = "Пошук" -searchPlaceholder = "Пошук файлів..." viewAll = "Переглянути всі файли ({{count}})" [fileSidebar.fileItem] closeViewer = "Закрити переглядач" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Відкрити в переглядачі" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Додати" -addLabel = "Додати мітку…" -categoryIconAria = "Виберіть іконку для {{name}}" -createCategory = "Створити категорію" -delete = "Видалити категорію" done = "Готово" -hide = "Приховати категорію" -newCategory = "Назва нової категорії…" -removeLabel = "Видалити {{name}}" -rename = "Перейменувати" reset = "Скинути до стандартних налаштувань" -search = "Пошук міток…" -show = "Показати категорію" subtitle = "Групуйте файли в батьківські категорії. Додавайте наявні або нові мітки до категорії, перейменовуйте її або створюйте власну. Файли, які не належать до жодної з ваших категорій, відображаються в розділі «Інше»." title = "Категорії бічної панелі" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Додати до робочої області" addToWorkspaceCount = "Додати {{count}} до робочої області" @@ -3749,7 +3971,6 @@ backToMyFiles = "Назад до «Мої файли»" breadcrumbs = "Шлях до папки" cancel = "Скасувати" classification = "Класифікація" -clearSearch = "Очистити пошук" clearSelection = "Очистити вибір" closeDetails = "Закрити відомості" create = "Створити" @@ -3781,6 +4002,7 @@ downloadAll = "Завантажити все" downloadVersion = "Завантажити цю версію" dropOverlay = "Перетягніть файли для завантаження" dropOverlaySub = "Файли спочатку потрапляють у Локальні. Використовуйте «Перемістити до» або «Зберегти в хмарі», щоб упорядкувати їх у папці." +duplicate = "Duplicate" file = "Файл" fileInfo = "File info" fileMenu = "Дії з файлом" @@ -3808,13 +4030,12 @@ refresh = "Оновити з сервера" remove = "Видалити" removeVersion = "Вилучити цю версію" rename = "Перейменувати" +renamed = "Перейменовано" renameFolder = "Перейменувати папку" resizeFolderTree = "Змінити розмір дерева папок (клавіші зі стрілками, Shift — для більших кроків; подвійне клацання — авто-підгін)" save = "Зберегти" saveToServer = "Зберегти на сервері" saveToServerDisabledHint = "На цьому сервері збереження не ввімкнено. Попросіть адміністратора увімкнути його." -search = "Пошук" -searchPlaceholder = "Шукати в цій папці та підпапках" selectAll = "Вибрати все" selectAllHint = "Натисніть, щоб вибрати все. Підказка: утримуйте Ctrl (або Cmd), щоб додавати файли по одному, Shift — щоб вибрати діапазон." selectedCount = "Вибрано: {{count}}" @@ -3874,6 +4095,10 @@ title = "Ще немає хмарних файлів" hint = "Файли, збережені без завантаження, залишаються тут. Перетягніть файл, щоб додати його." title = "Немає локальних файлів" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Файли, які ви відкриєте або відредагуєте, з’являться тут." title = "Поки нічого не змінено" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Не вдалося видалити папку." deleteFolderFailedDetail = "Не вдалося видалити папку: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Не вдалося оновити вигляд папки." folderAppearanceFailedDetail = "Не вдалося оновити вигляд папки: {{message}}" moveFilesFailed = "Не вдалося перемістити файли." @@ -3910,9 +4137,7 @@ count = "Файли" folder = "Папка" labels = "Мітки" modified = "Змінено" -name = "Назва" size = "Розмір" -toolHistoryAtVersion = "Сукупний ланцюжок інструментів" totalSize = "Загальний розмір" type = "Тип" versionHistory = "Історія версій" @@ -3945,6 +4170,11 @@ localHint = "Зберігається лише в цьому браузері" shared = "Спільний доступ" sharedHint = "Надано вам за посиланням" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Спочатку найстаріші" modifiedDesc = "Спочатку останні" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "У сховищі немає файлів. Спершу open = "Відкрити" openFile = "Відкрити файл" openFiles = "Відкрити файли" +selectFile = "Select {{name}}" selectFromStorage = "Вибрати зі сховища" upload = "Завантажити" uploadFile = "Завантажити файл" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Усе збережено" +analyzingFields = "Analysing form fields..." +extractCsvError = "Не вдалося витягти CSV" +extractXlsxError = "Не вдалося витягти XLSX" +filled = "filled" flattenAfterFilling = "Сплющити після заповнення" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "обов." +requiredFieldsError = "Заповніть усі обов’язкові поля" rescanFields = "Повторно сканувати поля" rescanFormFields = "Повторно сканувати поля форми" save = "Зберегти" +saveShortcut = "Ctrl+S для збереження" +unsavedChanges = "Незбережені зміни" [formFill.sidebar] close = "Закрити бічну панель" @@ -4309,6 +4552,11 @@ desc = "Автоматично перейменовує PDF-файл на осн tags = "автовизначення,на основі заголовка,організувати,перейменувати" title = "Автоперейменити PDF" +[home.autoRotate] +desc = "Визначайте орієнтацію кожної сторінки й автоматично повертайте її вертикально." +tags = "автоповорот,виправити орієнтацію,догори дриґом,боком,вирівняти,орієнтувати,визначити орієнтацію,виправити поворот,автоорієнтація,скановано,OSD" +title = "Автоповорот" + [home.autoSizeSplitPDF] tags = "авто,розділити,розмір" title = "Автоподіл за розміром/стор." @@ -4608,15 +4856,12 @@ hint = "Завантажте PNG, JPG, SVG або інший файл зобра label = "Завантажити зображення" placeholder = "Виберіть файл зображення" -[imageToPdf] -tags = "конвертація,зображення,jpg,картинка,фото" - [imageToPDF] header = "Зображення в PDF" title = "Зображення в PDF" -[infoBanner] -dismiss = "Закрити" +[imageToPdf] +tags = "конвертація,зображення,jpg,картинка,фото" [invite] acceptError = "Не вдалося створити обліковий запис" @@ -4647,11 +4892,9 @@ welcomeTitle = "Вас запросили!" [landing] addFiles = "Додати файли" -heroSubtitle = "Перетягніть або додайте наявний PDF, щоб почати." mobileUpload = "Завантажити з мобільного" openFromComputer = "Відкрити з комп’ютера" uploadFromComputer = "Завантажити з комп’ютера" -workbenchEmptyStateHero = "Перетягніть PDF будь-куди" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Введіть ім’я користувача" failedToSignIn = "Не вдалося увійти через {{provider}}: {{message}}" forgotPassword = "Забули пароль?" loggingIn = "Вхід..." -logIn = "Увійти" login = "Вхід" magicLinkSent = "Магічне посилання надіслано на {{email}}! Перевірте пошту та перейдіть за посиланням, щоб увійти." mfaCode = "Код автентифікації" @@ -4715,26 +4957,10 @@ signInWith = "Увійти за допомогою" title = "Вхід" unexpectedError = "Неочікувана помилка: {{message}}" updatePassword = "Оновити пароль" -useEmailInstead = "Увійти за email" useMagicLink = "Використати магічне посилання" username = "Ім’я користувача" youAreLoggedIn = "Ви увійшли!" -[login.slides.edit] -alt = "Редагуйте PDF" -subtitle = "Понад десяток інструментів для приховування, підписання, читання та обробки PDF — ви точно знайдете потрібне." -title = "Редагуйте PDF, щоб відображати й захищати потрібну інформацію" - -[login.slides.overview] -alt = "Огляд Stirling PDF" -subtitle = "Хмарний пакет із пріоритетом приватності для PDF: конвертуйте, підписуйте, приховуйте та керуйте документами, плюс 50+ інших потужних інструментів." -title = "Усе для PDF в одному місці." - -[login.slides.secure] -alt = "Захищайте PDF" -subtitle = "Легко додавайте паролі, приховуйте вміст і керуйте сертифікатами." -title = "Захищайте конфіденційну інформацію у ваших PDF" - [margin] large = "Великий" medium = "Середній" @@ -4818,6 +5044,34 @@ uploadSuccess = "Завантаження успішне!" uploadSuccessMessage = "Ваші зображення передано." validating = "Перевірка сеансу..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Скануйте для завантаження фото. Зображення автоматично конвертуються в PDF." descriptionNoConvert = "Скануйте для завантаження фото зі свого мобільного пристрою." @@ -4836,8 +5090,13 @@ title = "Завантажити з мобільного" tags = "мультиінструмент,багатоопераційний,інтерфейс,перетягування,клієнтська частина,інтерактивний" title = "Мультиінструмент PDF" -[navbar] -search = "Пошук" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Автентифікація не відбулася. Можете закрити це вікно та спробувати ще раз." @@ -4966,6 +5225,7 @@ activeFiles = "Вигляд Активні файли пока allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Закрити" cropSettings = "Тепер, коли ми вибрали файл, який хочемо обрізати, можемо налаштувати інструмент Обрізка, щоб вибрати область, до якої слід обрізати PDF." +dialogLabel = "Onboarding" fileCheckbox = "Клацання по файлу позначає його для обробки. Можна вибрати кілька файлів для пакетних операцій." fileReplacement = "Змінений файл автоматично замінить оригінал у Робочій області, що дозволить легко пропустити його через інші інструменти." filesButton = "Кнопка Файли на панелі швидкого доступу дозволяє завантажувати PDF для роботи інструментів." @@ -4976,6 +5236,7 @@ pinButton = "Ви можете скористатися кнопкою results = "Після завершення роботи інструмента крок Перегляд покаже попередній перегляд результатів у цій панелі та дозволить скасувати операцію або завантажити файл. " runButton = "Після налаштування інструмента ця кнопка дозволяє запустити його для всіх вибраних PDF." selectCropTool = "Давайте виберемо інструмент Обрізка, щоб показати, як користуватися одним з інструментів." +stepOf = "Крок {{current}} з {{total}}" toolInterface = "Це інтерфейс інструменту Обрізка. Як бачите, тут поки що небагато, адже ми ще не додали жодного PDF для роботи." workbench = "Це Робоча область — головна зона, де ви переглядаєте й редагуєте свої PDF." wrapUp = "Готово! Ви дізналися про основні області застосунку та як ними користуватися. Натискайте кнопку Допомога, щоб будь-коли переглянути цей тур знову." @@ -4988,6 +5249,26 @@ showMeAround = "Проведіть екскурсію" skipForNow = "Пропустити зараз" skipTheTour = "Пропустити тур" +[onboarding.checklist] +dismiss = "Закрити" +title = "Налаштуйте Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Запускайте Stirling нативно на своєму комп’ютері" +title = "Завантажити Stirling для Desktop" + +[onboarding.checklist.inviteTeam] +description = "Співпрацюйте зі своєю командою" +title = "Запросити членів команди" + +[onboarding.checklist.shareAnalytics] +description = "Допоможіть покращити Stirling" +title = "Ділитися анонімними даними використання" + +[onboarding.checklist.takeTour] +description = "Подивіться, як працює Stirling, у короткому огляді" +title = "Пройти огляд" + [onboarding.desktopInstall] body = "Stirling найкраще працює як настільний додаток. Ви можете працювати офлайн, швидше отримувати доступ до документів і редагувати їх локально на своєму комп’ютері." selectOs = "Виберіть операційну систему" @@ -4996,7 +5277,21 @@ titleWithOs = "Завантажити для {{osLabel}}" [onboarding.mfa] authenticationCode = "Код автентифікації" +description = "Захистіть свій обліковий запис, підключивши застосунок автентифікатора. Відскануйте QR-код або введіть ключ налаштування, а потім підтвердьте 6-значний код для завершення." +enable = "Увімкнути MFA" +enableError = "Не вдалося увімкнути двофакторну автентифікацію. Перевірте код і спробуйте ще раз." +enterCode = "Введіть 6-значний код із вашого застосунку." +enterCodeError = "Введіть код автентифікації, щоб продовжити." +logout = "Вийти" +openAuthenticator = "Відкрийте Google Authenticator, Authy або 1Password." qrCodeLoading = "Створення вашого QR-коду…" +regenerateQrCode = "Згенерувати QR-код повторно" +scanQrCode = "Відскануйте QR-код або введіть ключ налаштування нижче." +setupError = "Не вдалося почати налаштування двофакторної автентифікації. Спробуйте ще раз." +setupKey = "Ключ налаштування (ручне введення)" +stepByStep = "Покроково" +success = "MFA увімкнено. Тепер ви можете продовжити." +title = "Налаштування багатофакторної автентифікації" [onboarding.planOverview] adminBodyLoginDisabled = "Щойно ввімкнете режим входу, ви зможете керувати користувачами, налаштовувати параметри та відстежувати стан сервера. Перші {{freeTierLimit}} людей на вашому сервері користуються Stirling безкоштовно." @@ -5005,6 +5300,11 @@ adminTitle = "Огляд для адміністратора" userBody = "Запрошуйте колег, призначайте ролі та тримайте документи впорядкованими в одному безпечному робочому просторі. Увімкніть режим входу, коли будете готові вирости за межі сольного використання." userTitle = "Огляд плану" +[onboarding.processorIntro] +body = "Stirling тепер запускає Політики — автоматизовані правила, які класифікують, захищають і обробляють кожен документ під час надходження. Налаштовуйте їх і відстежуйте запуски в Processor." +cta = "Переглянути Processor" +title = "Ознайомтеся зі Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Передплачений рік" + +[payg.prepaid.banner] +exhausted = "Вашу передплачену ємність вичерпано; тарифікацію за лічильником відновлено. Поповніть, щоб зберегти знижку." +expiring_one = "Ваш передплачений рік завершується через {{count}} день. Невикористана ємність закінчується — поповніть, щоб почати новий рік." +expiring_other = "Ваш передплачений рік завершується через {{count}} днів. Невикористана ємність закінчується — поповніть, щоб почати новий рік." +low = "У вас залишилося {{pct}}% передплаченої ємності. Поповніть будь-коли, щоб обробка не переривалася." + +[payg.prepaid.card] +subtitle = "Використовується перед тарифікацією за лічильником і поза вашим лімітом витрат." +title = "Передплачена ємність" + +[payg.prepaid.meter] +capSuffix = "із {{total}} передплачених кредитів" +expires = "Закінчується {{date}}" + +[payg.prepaid.state] +exhausted = "Вичерпано" +healthy = "Достатньо залишилося" +low = "Закінчується" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF в PDF/A" tags = "архів,довгостроковий,стандартний,конверсія,зберігання,консервація" title = "PDF в PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "друк,стандарт,конвертація,виробництво,додрукарська підготовка,архів" title = "PDF у PDF/X" @@ -5822,6 +6151,7 @@ small = "500 кредитів" xsmall = "100 кредитів" [plan.availablePlans] +currency = "Billing currency" subtitle = "Виберіть план, який відповідає вашим потребам" title = "Доступні плани" @@ -5937,49 +6267,12 @@ keyDescription = "Вставте ліцензійний ключ із листа success = "Ліцензію активовано!" successMessage = "Вашу ліцензію успішно активовано. Тепер можете закрити це вікно." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "застосовано" -enforcing = "Застосування..." -failed = "Застосування не вдалося" outputsUnavailable = "Результати політик більше недоступні для завантаження." partialOutputsUnavailable = "Деякі результати політик більше недоступні для завантаження." -retrying = "Зайнято, повторна спроба..." runNotFound = "Запуск застосування більше не знайдено." -step = "крок {{current}}/{{total}}" timedOut = "Час очікування застосування вичерпано до завершення запуску." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Закрити" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Застосовуємо {{names}}" applyingProgress = "Застосовуємо {{names}} ({{done}} з {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{failures}} з {{total}} файлів не вдалося обр failureTitle = "Експорт без повного застосування" printPolicyAppliedBody = "Цей PDF оновлено відповідно до політики. Перегляньте зміни, потім надрукуйте ще раз." printPolicyAppliedTitle = "Політику застосовано перед друком" -queued = "+{{count}} у черзі" successTitle = "{{names}} застосовано" summaryMore = "{{first}}, {{second}} і ще {{more}}" summaryTwo = "{{first}} і {{second}}" -[policies.enforcement.triggerVerb] -convert = "Застосування перед перетворенням" -default = "Застосування" -export = "Застосування перед експортом" -input = "Застосування під час імпорту" -print = "Застосування перед друком" - -[policies.field] -accessLog = "Журнал доступу" -archiveAfter = "Архівувати після" -auditTrail = "Аудиторський слід" -belowThreshold = "Нижче порога" -destination = "Призначення" -frameworks = "Фреймворки" -immutableHold = "Незмінне утримання" -keepFor = "Зберігати протягом" -minConfidence = "Мін. впевненість" -notify = "Сповіщати під час маршрутизації" -onViolation = "У разі невідповідності" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Додати" -addPlaceholder = "Додати мітку…" -customNote = "Налаштовано для вашої команди." -defaultNote = "Використовується вбудоване стандартне значення, спільне для вашої команди." -duplicate = "\"{{name}}\" уже існує." -edit = "Редагувати мітки" -empty = "Міток ще немає." -export = "Експорт JSON" -iconAria = "Виберіть іконку для {{name}}" -import = "Імпорт JSON" -importError = "Не вдалося імпортувати цей файл." -managedNote = "Командними мітками керує керівник вашої команди." -modalSubtitle = "Спільно для всієї вашої команди. Класифікатор вибирає мітки, що відповідають кожному документу." -modalTitle = "Мітки класифікації" -pickIcon = "Використати іконку {{label}}" -removeAria = "Видалити {{name}}" -resetToDefault = "Скинути до стандартного" -saveForTeam = "Зберегти для команди" -saving = "Збереження…" -sectionLabel = "Мітки класифікації" -startFromScratch = "Почати з нуля" -teamCount = "командні мітки" -tooLong = "Мітки можуть містити щонайбільше {{max}} символів." -ungrouped = "Без групи" -view = "Переглянути мітки" +categoryCount = "категорії" +hideCategory = "Приховати категорію" +labelCount = "мітки" +sharedNote = "Ці мітки вбудовані та спільні для всієї вашої команди." +showCategory = "Показати категорію" [policies.pii] account = "Номери рахунків (позначені)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Маршрутні номери США (ABA)" ssn = "Номери соціального страхування" -[policies.settings] -noneExport = "Наразі під час експорту не виконується жодна політика." -noneUpload = "Наразі під час завантаження не виконується жодна політика." -onExport = "Під час експорту" -onUpload = "Під час завантаження" -reorderHandle = "Перетягніть, щоб змінити порядок" -runOrderDesc = "Коли на одному тригері виконується кілька політик, вони виконуються в цьому порядку — кожна на вихідних даних попередньої політики. Перетягніть, щоб змінити порядок." -title = "Налаштування політик" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Параметри політики" -policySettings = "Налаштування політик" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Повторити невдалі політики ({{count}})" -rowProgress = "Оброблено файлів: {{completed}} з {{total}}" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Що таке політика?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Автоматично знаходить і замальовує конфіденційні дані — наприклад номери соціального страхування та карток — щоб їх не можна було прочитати в документі." -sanitize = "Видаляє прихований JavaScript з файлу, щоб нічого не запускалося автоматично під час відкриття." -watermark = "Додає видиму позначку (напр., \"Confidential\") на кожній сторінці." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Закрити" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} застосовується..." badgeRan = "Політика {{name}} виконана для цього файлу" +badgeRunning = "{{name}} виконується..." blockingAction = "{{action}} заблоковано під час застосування політики, зачекайте..." dismiss = "Закрити накладення" enforcingTitle = "Застосування політики..." @@ -6151,6 +6316,7 @@ viewAnyway = "Переглянути файл (політика ще засто [policyOption] autoRedactPhi = "Автоматично приховати PHI" blockExport = "Заблокувати експорт" +ccpa = "CCPA" contracts = "Договори" documents = "Документи" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "План Editor" subscribed = "План Processor" unlinked = "Не прив’язано" -[portal.agentBuilder] -bootstrapFromDocument = "Ініціалізувати з документа" -sectionsAriaLabel = "Розділи конструктора агентів" -selectorAriaLabel = "Агенти" -subtitle = "Проєктуйте, тестуйте й запускайте AI-агентів, які класифікують, витягують дані з ваших документів і маршрутизують їх. Визначайте сценарії, обмежуйте доступ до інструментів, запускайте еталонний набір і публікуйте версію." -title = "Конструктор агентів" - -[portal.agentBuilder.bootstrap] -cancel = "Скасувати" -dropzoneText = "Виберіть зразок документа (PDF або зображення)" -lead = "Додайте зразок документа, і ми запропонуємо сценарії та схему витягання, які ви зможете уточнити. Нічого не публікується, доки ви це не переглянете." -submit = "Ініціалізувати агента" -subtitle = "Створіть нового агента на основі одного репрезентативного файлу" -title = "Ініціалізація з документа" - -[portal.agentBuilder.empty] -description = "Ініціалізуйте агента зі зразка документа, щоб створити його сценарії та схему витягання, а потім уточніть і опублікуйте." -title = "Агентів ще немає" - -[portal.agentBuilder.evals] -casesPassing = "Успішні випадки" -columnCase = "Випадок оцінювання" -columnLatency = "Затримка" -columnResult = "Результат" -fail = "невдача" -goldenSetPassRate = "Частка успішних для еталонного набору" -latencyMs = "{{ms}} мс" -notRun = "не запускалося" -pass = "успіх" -passRate = "Частка успішних" -runEvals = "Запустити оцінювання" - -[portal.agentBuilder.evals.empty] -description = "Оцінювання перетворюють ваші сценарії на повторюваний еталонний набір. Оновіть план, щоб відстежувати частку успішних з часом і блокувати публікації на її основі." -title = "Еталонного набору ще немає" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "за еталонними наборами" -activeAgents = "Активні агенти" -avgPassRate = "Середня частка успішних оцінювань" -fleetWide = "по всьому парку" -latestPublished = "Остання опублікована" -scenarios = "Сценарії" -testCases = "тестові випадки" -totalDescription_one = "{{count}} загалом" -totalDescription_other = "{{count}} загалом" - -[portal.agentBuilder.scenarios] -add = "Додати" -addScenario = "Додати сценарій" -enable = "Увімкнути" -expectationLabel = "Очікувана поведінка" -expectationPlaceholder = "Що має зробити агент" -inEval = "в оцінюванні" -mute = "Вимкнути звук" -muted = "вимкнено" -nameLabel = "Назва" -namePlaceholder = "напр. ескалація відповідності" - -[portal.agentBuilder.tabs] -evals = "Оцінювання" -scenarios = "Сценарії" -tools = "Інструменти" -versions = "Версії" - -[portal.agentBuilder.tools] -broadAccess = "Широкий доступ" -deniedHint = "Вибрані інструменти заблоковано. Усе інше залишається доступним для виклику." -deniedTools = "Заборонені інструменти" -governanceGate = "Керування інструментами доступне на плані Enterprise." -restricted = "Обмежено" -restrictedAccess = "Обмежений доступ до інструментів" -restrictedDescription = "Дозволити всі інструменти, крім тих, які ви забороните нижче." - -[portal.agentBuilder.versions] -current = "поточна" -historyGate = "Повна історія версій і відкат доступні на плані Enterprise." -publish = "Опублікувати" -rollBack = "Відкотити" - [portal.assistant] close = "Закрити помічника" error = "Не вдалося зв’язатися з помічником." @@ -6365,9 +6451,21 @@ typing = "Друкує" loading = "Завантаження" redirectingToEditor = "Перенаправлення до редактора..." +[portal.billing.activation] +maybeLater = "Можливо, пізніше" +title = "Увімкнути Processor" + +[portal.billing.activation.payg] +sub = "Установіть місячний ліміт. Платіть за те, що запускаєте." +title = "Платіть за використання" + +[portal.billing.activation.prepay] +badge = "2 місяці безкоштовно" +sub = "Один рахунок, картка або банківський переказ." +title = "Передплатити рік" + [portal.billing.checkout] noClientSecret = "Edge-функція не повернула client_secret." -subtitle = "Додайте картку, щоб продовжити після безкоштовного гранту плану Editor. Stripe подбає про решту." title = "Увімкнути план Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Ваш платіж успішний, але активація трив close = "Закрити" title = "Майже готово" +[portal.billing.checkout.cap] +amountAria = "Місячний ліміт витрат" +back = "Назад" +continue = "Перейти до оплати" +error = "Не вдалося встановити ліміт витрат" +finePrint = "Рахунки виставляються щомісяця. Скасувати можна будь-коли." +noLimit = "Без ліміту" +note = "З вас ніколи не стягнуть більше вашого ліміту. Обробка просто призупиниться." +perMonth = "/ міс." +title = "Установіть ліміт витрат" + +[portal.billing.checkout.card] +fields = "Номер картки · MM / YY · CVC · ZIP" +label = "Дані картки" +note = "Дані картки збирає Stripe. Stirling ніколи не зберігає PAN або CVC." + [portal.billing.checkout.error] title = "Не вдалося розпочати checkout" @@ -6383,10 +6497,9 @@ body = "Ваш платіж пройшов. Ми вмикаємо обробку hint = "Будь ласка, залиште це вікно відкритим." title = "Активація вашого плану Processor..." -[portal.billing.checkout.notConfigured] -bodyAfter = "у portal env, щоб увімкнути checkout у застосунку." -bodyBefore = "Налаштуйте" -title = "Stripe не налаштовано" +[portal.billing.checkout.pay] +limit = "Місячний ліміт витрат {{amount}}/міс. Обробка призупиниться на ліміті." +limitNoCap = "Без ліміту витрат. Обробка не призупинятиметься. Рахунки виставляються щомісяця." [portal.billing.enterpriseUpsell] cta = "Сформувати вашу пропозицію Enterprise" @@ -6409,10 +6522,12 @@ currentPlan = "Поточний план" freeForever = "Безкоштовно назавжди" noTeamResolved = "Для вашого гаманця ще не визначено команду — оновіть і спробуйте ще раз." ownerOnly = "Лише власник команди може ввімкнути план Processor." +payInvoice = "Оплатити рахунок для завершення" planName = "Editor" ssoIncluded = "SSO включено" switchOnProcessor = "Увімкнути Processor →" unlimitedUsers = "Необмежена кількість користувачів" +viewQuote = "Переглянути пропозицію" [portal.billing.invoices] columnAmount = "Сума" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Прямі API-запити" segmentApiLabel = "API" segmentAutomationDesc = "Автоматизації та конвеєри" segmentAutomationLabel = "Автоматизація" +sizeMultiplier = "{{formatted}} PDF використали множник розміру" +summary = "{{unique}} унікальних · {{units}} одиниць лічильника · {{avg}} середнє на PDF" +summaryNoRate = "{{unique}} унікальних · {{units}} одиниць лічильника" unit = "PDF за лічильником" +unitsPending_one = "{{units}} одиниця лічильника очікує синхронізації з пов’язаних екземплярів" +unitsPending_other = "{{units}} одиниць лічильника очікують синхронізації з пов’язаних екземплярів" + +[portal.billing.prepaid] +capSuffix = "із {{total}} передплачених кредитів" +expires = "Закінчується {{date}}" +eyebrow = "Передплачена ємність" +note = "Використовується перед тарифікацією за лічильником і поза вашим лімітом витрат." +topUp = "Поповнити" + +[portal.billing.prepaid.buy] +back = "Назад" +calcTitle = "Розрахуйте свій річний платіж" +cancel = "Скасувати" +cancelPurchase = "Скасувати покупку" +close = "Закрити" +continueToPayment = "Перейти до оплати" +finalise = "Завершити" +invoiceErrorTitle = "Не вдалося згенерувати рахунок" +loadingTitle = "Завантаження вашої пропозиції" +payTitle = "Оплатіть свій рік" +step = "Крок {{current}} з {{total}}" + +[portal.billing.prepaid.calc] +change = "Змінити" +deployRow = "Розгортання" +done = "Готово" +enterpriseHint = "Це корпоративний масштаб. За такого обсягу корпоративні тарифи вигідніші за будь-яку знижку самообслуговування. Фіксація тарифу, умови та пропозиція за лічені хвилини." +governanceRow = "Управління" +governanceValue = "{{label}} · {{count}} політик" +handlesLabel = "Ваш Processor" +handlesValue = "обробляє {{volume}} кредитів / міс." +pipelinesLabel = "Конвеєри" +policiesMeta = "{{count}} політик" +poolCaption = "Один пул із {{credits}} кредитів на рік. Важкі місяці позичають у легких." +priceLabel = "Ваш рік · 12 місяців за ціною 10" +rateUnknown = "Ми покажемо точну ціну під час оформлення." +savings = "Ви заощаджуєте {{amount}} · 2 місяці безкоштовно." +sizingRow = "Розмір" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Усього користувачів" + +[portal.billing.prepaid.consent] +label = "Я розумію, що коли моя передплачена ємність буде вичерпана або закінчиться через 12 місяців, обробка автоматично продовжиться за стандартним тарифом pay-as-you-go за лічильником (до мого ліміту витрат), якщо я не скасую, і що я можу скасувати будь-коли з порталу виставлення рахунків." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Керується Stirling. Працює за лічені хвилини." +cloudValue = "Stirling Cloud · Керовано" +selfhost = "Self-hosted" +selfhostDesc = "Працює на приватній інфраструктурі. Такий самий тариф." +selfhostValue = "Self-hosted · Приватна інфраструктура" + +[portal.billing.prepaid.offer] +cta = "Переглянути пропозицію" +subtitle = "Передплатіть рік обробки PDF і отримайте два місяці безкоштовно; використовується перед тарифікацією за лічильником, поза вашим лімітом витрат." +title = "Отримайте 12 місяців за ціною 10" + +[portal.billing.prepaid.pay] +accountName = "Ім’я власника рахунку" +accountNamePlaceholder = "напр. Jane Smith" +companyName = "Назва компанії (необов’язково)" +companyNamePlaceholder = "напр. Acme Inc." +downloadInvoice = "Завантажити рахунок" +payOnline = "Оплатити онлайн" +poNumber = "Номер PO (необов’язково)" +poNumberPlaceholder = "напр. PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Розширені" +advancedDesc = "Конвеєри виконують більшість вашої обробки." +none = "Немає" +noneDesc = "Конвеєри ще не запущені. Увімкніть їх будь-коли." +standard = "Стандартні" +standardDesc = "Кілька конвеєрів повторно обробляють PDF, що надходять." + +[portal.billing.prepaid.posture] +essentials = "Основи" +essentialsDesc = "Класифікація, стандартно, плюс спільний доступ" +governed = "Керований" +governedDesc = "Додає безпеку та маршрутизацію" +regulated = "Регульований" +regulatedDesc = "Кожна категорія, включно з відповідністю вимогам, зберіганням, отриманням" + +[portal.billing.prepaid.review] +download = "Завантажити пропозицію (PDF)" +downloading = "Підготовка…" +share = "поділитися для затвердження" + +[portal.billing.prepaid.size] +compact = "Компактний" +compactDesc = "Файли до 25 MB, без плати за дані" +heavy = "Важкий" +heavyDesc = "Скановані або насичені зображеннями, зазвичай 50 MB+" +standard = "Стандартний" +standardDesc = "Переважно малі, деякі скани понад 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Вичерпано" +healthy = "Достатньо залишилося" +low = "Закінчується" [portal.billing.spendLimit] adjustLimit = "Змінити ліміт" @@ -6494,6 +6713,8 @@ label = "Прогнозується перевищення." [portal.billing.spendThisMonth] eyebrow = "Витрати цього місяця" +freeRemaining_one = "Залишився {{formatted}} безкоштовний PDF" +freeRemaining_other = "Залишилося {{formatted}} безкоштовних PDF" processed_one = "Оброблено {{formattedCount}} PDF." processed_other = "Оброблено {{formattedCount}} PDF." processedWithRate_one = "Оброблено {{formattedCount}} PDF, по {{rate}} за кожен." @@ -6510,6 +6731,7 @@ reachedTitle = "Досягнуто місячного ліміту витрат" title = "Не вдалося відкрити портал Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "з {{allowance}} безкоштовних PDF використано" capSuffix_other = "з {{allowance}} безкоштовних PDF використано" eyebrow = "Пробний період Processor" @@ -6534,6 +6756,376 @@ signature = "підпис" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Нове з’єднання" +createTitleFor = "Підключити {{name}}" +delete = "Видалити" +edit = "Редагувати" +editTitle = "Редагувати з’єднання" +unsupportedType = "Цей тип з’єднання не можна редагувати тут." + +[portal.connections.categories.advanced] +description = "Для сервісу, який ми ще не підтримуємо." +label = "Розширені" + +[portal.connections.categories.audit] +description = "Записуйте, що сталося з документом, там, де ваші аудитори вже переглядають." +label = "Журнал аудиту та відповідності" + +[portal.connections.categories.notify] +description = "Повідомте іншу систему, що щось сталося." +label = "Сповіщення та робочий процес" + +[portal.connections.categories.security] +description = "Скануйте або позначайте документ перед тим, як він рухатиметься далі." +label = "Безпека та класифікація" + +[portal.connections.categories.signing] +description = "Надішліть документ на підпис або нотаріальне засвідчення." +label = "Електронний підпис і нотаріальне засвідчення" + +[portal.connections.categories.storage] +description = "Куди потрапляють оброблені документи - bucket, диск, ticket або сторінка." +label = "Файли та вкладення" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling зберігає це зашифрованим і більше ніколи не показує." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Кроки, що використовують це з’єднання, можуть викликати лише шляхи під цим URL, ніколи інший host." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Пароль" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Ім’я користувача" + +[portal.connections.commonFields.webhookUrl] +helperText = "Вставте URL, який надав сервіс. Викликати можна лише шляхи під ним." +label = "Webhook URL" + +[portal.connections.fields] +name = "Назва з’єднання" +namePlaceholder = "напр. Production" + +[portal.connections.picker] +cancel = "Скасувати" +createNew = "Нове з’єднання..." +placeholder = "Виберіть з’єднання" +save = "Зберегти з’єднання" + +[portal.connections.picker2] +back = "Змінити інтеграцію" +heading = "Що ви хочете підключити?" +noResultsBody = "Ми ще не підтримуємо це - розкажіть нам про це, і ми розглянемо додавання." +noResultsTitle = "Нічого не збігається з «{{query}}»" +searchPlaceholder = "Пошук інтеграцій - спробуйте «sign», «ocr» або назву продукту" + +[portal.connections.types.api] +description = "Викликайте будь-який HTTP API з політики. Ви вибираєте URL, автентифікацію і payload." +label = "Custom API" + +[portal.connections.types.api.fields.authType] +label = "Автентифікація" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Ім’я користувача та пароль" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Токен у власному заголовку" + +[portal.connections.types.api.fields.authType.options.none] +label = "Немає" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Увійти для токена" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Кроки, що використовують це з’єднання, можуть викликати лише шляхи під цим URL, ніколи інший host." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Назва заголовка" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Шлях входу" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Пароль" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Якщо API відповідає посиланням для завантаження, отримувати можна лише з цих host. Розділяйте комами. Залиште порожнім, щоб дозволити лише власний host базового URL." +label = "Дозволені host результатів" + +[portal.connections.types.api.fields.token] +label = "Токен" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Надсилати токен як цей заголовок" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Заголовок відповіді, у якому виклик входу повертає токен, напр. X-Auth-Token." +label = "Заголовок відповіді з токеном" + +[portal.connections.types.api.fields.username] +label = "Ім’я користувача" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Скануйте документи на malware на сервері, яким ви керуєте - нічого не залишає вашу мережу." +label = "ClamAV (self-hosted)" + +[portal.connections.types.cloudmersive] +description = "Сканування документів на віруси та вміст у політиці." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Блокуйте документи, що містять макроси, виконувані файли, скрипти або вбудовані об’єкти - не лише відоме malware." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Прикріплюйте оброблені документи до сторінки Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Надсилайте документи до ConsignO Cloud (Notarius) для електронного підпису та нотаріального засвідчення." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Ваше середовище ConsignO Cloud, включно з /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Пароль" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Потрібно лише якщо ваша організація використовує tenant." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Ім’я користувача" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Публікуйте повідомлення в Discord, коли політика обробляє документ." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Індексуйте подію аудиту в Elasticsearch щоразу, коли політика обробляє документ." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Опитуйте FTP або FTPS сервер щодо нових документів." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Режим з’єднання" + +[portal.connections.types.ftp.fields.passive.options] +active = "Активний" +passive = "Пасивний (рекомендовано)" + +[portal.connections.types.ftp.fields.security] +helperText = "Звичайний FTP надсилає облікові дані та файли незашифрованими; виберіть FTPS, якщо сервер це підтримує." +label = "Шифрування" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Немає (звичайний FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Публікуйте повідомлення в просторі Google Chat, коли політика запускається." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Прикріплюйте оброблені документи до issue Jira." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Надсилайте документ або сповіщення Email, коли політика запускається." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Розміщуйте оброблені документи в Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Виявляйте персональні дані в тексті документа за допомогою сервера Presidio, яким ви керуєте." +label = "Presidio (self-hosted)" + +[portal.connections.types.purview] +description = "Читайте та застосовуйте мітки чутливості Microsoft Purview до PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Необов’язково. Потрібно лише для вибору міток за назвою; маркування працює й без цього." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Штампується на кожну мітку як її SiteId, щоб інструменти, сумісні з Purview, знали, яка організація її застосувала." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket, з якого джерела можуть читати і до якого виходи конвеєра можуть записувати." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Залиште порожнім для AWS. Установіть це для MinIO або іншого S3-сумісного сховища." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Надсилайте документ або сповіщення Email, коли політика запускається." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Опитуйте SFTP (SSH) сервер щодо нових документів." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Необов’язково. SHA-256 відбиток host key сервера (з `ssh-keyscan host | ssh-keygen -lf -`). Якщо задано, приймається лише сервер, що надає цей ключ; якщо порожньо, ключ, побачений під час першого підключення, закріплюється, а подальші зміни відхиляються." +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Парольна фраза ключа" + +[portal.connections.types.sftp.fields.password] +helperText = "Залиште порожнім, якщо замість цього автентифікуєтеся приватним ключем." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Необов’язково. Вставте приватний ключ OpenSSH для автентифікації ключем замість пароля." +label = "Приватний ключ" + +[portal.connections.types.slack] +description = "Публікуйте повідомлення в Slack, коли політика обробляє документ." +label = "Slack" + +[portal.connections.types.smb] +description = "Відстежуйте мережевий ресурс SMB / CIFS (Windows або Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Мережевий ресурс" + +[portal.connections.types.smb.fields.domain] +helperText = "Необов’язково. Windows domain або workgroup, якщо ваш сервер їх використовує." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Надсилайте подію аудиту до Splunk щоразу, коли політика обробляє документ." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Надсилайте подію аудиту до Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Публікуйте повідомлення в каналі Teams, коли політика запускається." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Надсилайте кожен документ або лише деталі на URL, який ви виберете." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Запускайте Zap або Make scenario з політики." +label = "Zapier & Make" + +[portal.docs] +browse = "Переглянути docs" +viewSource = "Переглянути source на GitHub" + [portal.docs.authentication] codeCaption = "кожен запит" eyebrow = "ПОЧАТОК РОБОТИ" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "Вбудовані іменовані можливості, які ваш агент викликає як один інструмент. Кожна навичка — це детермінований ланцюг операцій із прикріпленими оцінюваннями." title = "Навички агента" +[portal.docs.toc] +title = "На цій сторінці" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API REFERENCE" @@ -6697,6 +7292,8 @@ type = "Тип" user = "Користувач" [portal.documents.queue.empty] +connectSource = "Підключити джерело" +createPipeline = "Створити конвеєр" description = "Коли джерела подаватимуть документи у ваші конвеєри, вони з’являться тут для перевірки." title = "У черзі немає документів" @@ -6712,11 +7309,11 @@ editorAction = "Редактор" empty = "Жоден документ не відповідає цьому фільтру." rowActions = "Дії рядка" sensitiveLabel = "Конфіденційно" -sensitiveTitle = "Конфіденційно — потрібен доступ" [portal.documents.table.columns] action = "Конвеєр / Дія" document = "Документ" +labels = "Labels" product = "Продукт" status = "Статус" time = "Час" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Остання активність" region = "Регіон" status = "Статус" +target = "Target" version = "Версія" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "У цьому поданні сталася неочікуван retry = "Спробувати ще раз" title = "На цій сторінці щось пішло не так" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Назад" +body = "Завантажте desktop app або self-host сервер." +done = "Готово" +guide = "Відкрити посібник {{name}}" +openInBrowser = "Відкрити в браузері замість цього" +sectionDesktop = "Desktop app" +sectionSelfHosted = "Self-hosted" +title = "Встановити Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Запустіть офіційний image на своєму сервері." +note = "Відкривається на :8080. Стандартний вхід: admin / stirling." +tagline = "Запустіть сервер у container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Установіть офіційний Helm chart." +note = "Додайте PVC для /configs, щоб зберігати налаштування." +tagline = "Розгорніть у кластері за допомогою Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Нативні збірки для вашого дистрибутива." +guideBtn = "Переглянути варіанти встановлення Linux" +note = "Збірки AppImage, .deb і .rpm доступні для поширених дистрибутивів." +tagline = "Нативний настільний застосунок для вашого дистрибутива" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Або через Homebrew" +detailBody = "Запустіть інсталятор або скористайтеся Homebrew." +downloadBtn = "Завантажити для macOS" +tagline = "Нативний застосунок для Apple silicon та Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Запустіть .jar на фізичному сервері." +downloadBtn = "Завантажити .jar" +note = "Потрібна Java 21+. Відкривається на :8080; типовий логін admin / stirling." +runLabel = "Потім запустіть" +tagline = "Запустіть .jar на фізичному сервері" +title = "Ручне налаштування сервера" + +[portal.home.download.windows] +altLabel = "Або через winget" +detailBody = "Запустіть інсталятор або скористайтеся менеджером пакетів." +downloadBtn = "Завантажити інсталятор" +tagline = "Нативний застосунок для Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} активних" -invite = "Запросити колег" +activeOfDeployed = "{{active}} з {{total}} активні цього місяця" name = "Stirling PDF Editor" open = "Відкрити в браузері" updated = "оновлено {{time}}" +[portal.home.editor.deploy] +finish = "Завершити розгортання" +options = "Переглянути варіанти розгортання" +start = "Розгорнути Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Самостійне розміщення · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Добрий день" evening = "Добрий вечір" morning = "Доброго ранку" -[portal.home.onboarding] -dismiss = "Закрити налаштування" -notStarted = "Не розпочато" -progress = "{{done}} з {{total}} виконано" -title = "Завершіть налаштування" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC для всієї організації, погоджене ціноутворення за обсягом та ізольоване розгортання." -cta = "Почати пробний період" -ctaQuote = "Отримати пропозицію" -lead = "Для 250+ співробітників." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Розгорніть настільний застосунок, безкоштовно назавжди." -title = "Завантажте PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} активних · {{recommended}} рекомендовано" -chip = "{{active}} активних" -title = "Увімкніть політики" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} підключено · кожен PDF керується там, де він потрапляє" -chip = "{{connected}} підключено" -title = "Підключіть джерела" - -[portal.home.quickActions] -subtitle = "Головні завдання на сьогодні" -title = "Швидкі дії" - -[portal.home.quickActions.buildPipeline] -blurb = "3-кроковий конструктор над бібліотекою типізованих операцій" -title = "Створити конвеєр" - -[portal.home.quickActions.connectSource] -blurb = "S3, агенти, webhooks, відстежувані папки" -title = "Підключити джерело" - -[portal.home.quickActions.issueApiKey] -blurb = "Ключ з областю дії, обмеженнями швидкості та списком дозволених IP" -title = "Видати ключ API" - [portal.infrastructure] manageEditorDeployment = "Керувати розгортанням Редактора" sectionsAriaLabel = "Розділи інфраструктури" @@ -6878,13 +7541,9 @@ heading = "Ключі API" subheading = "Облікові дані з областю дії, обмеженнями швидкості для кожного ключа, дозволами та списками дозволених IP." [portal.infrastructure.apiKeys.card] -allowedIps = "Дозволені IP" -anyIp = "Будь-який IP (без списку дозволених)" created = "Створено" lastUsed = "Останнє використання" -permissions = "Дозволи" -rateLimit = "Обмеження швидкості" -rateLimitValue = "{{value}} зап./хв" +revoke = "Відкликати ключ" usageMonth = "Використання цього місяця" usageToday = "Використання сьогодні" @@ -6892,6 +7551,15 @@ usageToday = "Використання сьогодні" description = "Створіть ключ з областю дії, щоб почати викликати Stirling API." title = "Ключів API ще немає" +[portal.infrastructure.apiKeys.error] +load = "Не вдалося завантажити ваші API ключі. Спробуйте ще раз." + +[portal.infrastructure.apiKeys.revoke] +body = "Відкликати \"{{name}}\"? Будь-яка інтеграція, яка ще використовує цей ключ, негайно почне отримувати помилки 401. Цю дію не можна скасувати." +cancel = "Скасувати" +confirm = "Відкликати ключ" +title = "Відкликати API ключ" + [portal.infrastructure.audit] filterAriaLabel = "Фільтрувати події аудиту за категорією" heading = "Журнали аудиту" @@ -6935,6 +7603,7 @@ all = "Усі" auth = "Автентифікація" config = "Конфігурація" elevation = "Підвищення доступу" +policy = "Політика" processing = "Обробка" security = "Безпека" @@ -6944,19 +7613,30 @@ title = "У вас немає доступу" [portal.infrastructure.audit.metrics] config = "Конфігурація" -elevation = "Підвищення доступу" +policy = "Політика" processing = "Обробка" totalEvents = "Усього подій · 24 год" +[portal.infrastructure.auditCatLabel] +auth = "Автентифікація" +config = "Конфігурація" +elevation = "Підвищення прав" +policy = "Політика" +processing = "Обробка" +security = "Безпека" + +[portal.infrastructure.auditStatusLabel] +danger = "Помилка" +info = "Інформація" +success = "Успіх" +warning = "Попередження" + [portal.infrastructure.createKey] cancel = "Скасувати" createKey = "Створити ключ" done = "Готово" -ipAllowlistHelper = "Діапазони CIDR, розділені комами. Залиште порожнім, щоб дозволити будь-який IP." -ipAllowlistLabel = "Список дозволених IP" keyNameLabel = "Назва ключа" keyNamePlaceholder = "напр. Production · ingest" -permissionsLabel = "Дозволи" secretKeyCaption = "Секретний ключ" secretWarning = "Збережіть це в менеджері секретів. Stirling зберігає лише хеш — відновити його пізніше неможливо." subtitle = "Обмежте ключ мінімально необхідною областю. Ви можете ротувати або відкликати його будь-коли." @@ -6964,207 +7644,9 @@ subtitleCreated = "Скопіюйте цей секрет зараз — йог title = "Створити ключ API" titleCreated = "Ключ створено" -[portal.infrastructure.deployments] -msValue = "{{value}} мс" -throughputValue = "{{value}}/хв" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Розгорнув" -environment = "Середовище" -product = "Продукт" -status = "Статус" -version = "Версія" -when = "Коли" - -[portal.infrastructure.deployments.recent] -heading = "Останні розгортання" -subheading = "Останні випуски в продуктах і середовищах." - -[portal.infrastructure.deployments.regionColumns] -instances = "Екземпляри" -latency = "Затримка" -load = "Навантаження" -p99 = "P99" -region = "Регіон" -status = "Статус" -throughput = "Пропускна здатність" -uptime = "Час безвідмовної роботи" -version = "Версія" - -[portal.infrastructure.deployments.regions] -heading = "Регіони" -subheading = "Поточний стан кожного розгорнутого регіону Stirling — затримка, навантаження та версія випуску." - -[portal.infrastructure.deployments.regions.empty] -description = "Розгорнуті регіони з’являться тут після підготовки вашого робочого простору." -title = "Регіони не розгорнуто" - -[portal.infrastructure.models] -heading = "Моделі" -msValue = "{{value}} мс" -subheading = "Каталог моделей і маршрутизація, що забезпечують обробку документів у вашому робочому просторі." - -[portal.infrastructure.models.byom] -description = "Зареєструйте локальну або самостійно розміщену модель і закріпіть її за регіоном для обробки з урахуванням резидентності даних." -title = "Власна модель" - -[portal.infrastructure.models.catalogue] -heading = "Каталог" -sub = "Керовані моделі, доступні вашому робочому простору, з актуальною затримкою та вартістю." -subEnterprise = "Керовані, власні та локальні моделі — з доступним закріпленням за регіонами." - -[portal.infrastructure.models.catalogue.empty] -description = "Моделі в каталозі вашого робочого простору з’являться тут." -title = "Немає доступних моделей" - -[portal.infrastructure.models.columns] -cost = "Вартість" -latency = "Затримка" -load = "Навантаження" -model = "Модель" -status = "Статус" -type = "Тип" -version = "Версія" - -[portal.infrastructure.models.metrics] -activeModels = "Активні моделі" -avgLatency = "Середня затримка" -included = "Включено" -monthlySpend = "Щомісячні витрати на моделі" - -[portal.infrastructure.models.routing] -empty = "Правила маршрутизації не налаштовано." -heading = "Правила маршрутизації" -sub = "Яка модель обробляє кожну операцію. Типове правило застосовується, коли не збігається жодне вужче правило." -subLocked = "Спрямовуйте операції до конкретних моделей — доступно на платних планах." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Оновіться до Pro, щоб керувати тим, яка модель обробляє кожну операцію та тип документа." -title = "Маршрутизація моделей — платна функція" - -[portal.infrastructure.models.routingColumns] -default = "Типово" -docType = "Тип документа" -modelForAria = "Модель для {{operation}}" -operation = "Операція" -routedTo = "Спрямовано до" - -[portal.infrastructure.security.access.byok] -description = "Надайте ключ із власного KMS. Stirling шифрує ним, але все ще може читати." -label = "Власний ключ (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Ключі ніколи не залишають ваш KMS. Stirling зберігає лише шифротекст." -label = "Зберігайте власний ключ (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling керує ключами шифрування. Найпростіше — нуль операцій із ключами з вашого боку." -label = "Ключі, що зберігаються Stirling" - -[portal.infrastructure.security.accessPolicy] -heading = "Політика доступу до документів" -subheading = "Керує тим, хто може розшифровувати оброблені документи в стані спокою." - -[portal.infrastructure.security.attestations] -heading = "Підтвердження відповідності" -noReport = "Звіт недоступний" -subheading = "Стан аудиту за кожним фреймворком, зі звітами щодо підтверджених контролів." -viewReport = "Переглянути звіт →" - -[portal.infrastructure.security.compliance] -heading = "Відповідність вимогам" -subheading = "Підтвердження та сертифікації, що охоплюють платформу Stirling." - -[portal.infrastructure.security.empty] -description = "Конфігурація безпеки вашого робочого простору з’явиться тут." -title = "Стан безпеки недоступний" - -[portal.infrastructure.security.hyokBanner] -description = "З HYOK ключі шифрування ніколи не залишають ваш KMS. Stirling зберігає й обробляє лише шифротекст, який ви можете відкликати будь-коли." -title = "Stirling не може розшифрувати ваші документи" - -[portal.infrastructure.security.ipAllowlist] -empty = "Діапазони IP не налаштовано — дозволено всі IP." -heading = "Список дозволених IP" -sub = "Доступ до API обмежено цими діапазонами CIDR." -subLocked = "Обмежте доступ до API відомими діапазонами IP — доступно на платних планах." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Оновіться до Pro, щоб обмежити доступ до API конкретними мережами." -title = "Списки дозволених IP — платна функція" - -[portal.infrastructure.security.ipColumns] -added = "Додано" -addedBy = "Додав" -cidr = "CIDR" -label = "Мітка" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Керування ключами шифрування" -keyId = "Ідентифікатор ключа" -lastRotated = "Остання ротація" -rotateKey = "Ротувати ключ" -rotationPolicy = "Політика ротації" -subheading = "Зберігання ключів, що шифрують документи в стані спокою — хто може розшифровувати та як ключі ротуються." - -[portal.infrastructure.security.managedBanner] -description = "Зберігання власних ключів BYOK і HYOK доступне на Enterprise. Оновіться, щоб надавати ключі з власного KMS." -title = "На вашому плані ключами керує Stirling" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Азійсько-Тихоокеанський регіон" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · межа даних GDPR" -label = "Європейський Союз" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Сполучені Штати" - -[portal.infrastructure.security.residencyHeader] -heading = "Резидентність даних" -subheading = "Де документи зберігаються та обробляються." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "{{value}} використано" - -[portal.infrastructure.storage.empty] -description = "Підключене сховище та використання з’являться тут." -title = "Сховище не налаштовано" - -[portal.infrastructure.storage.lifecycle] -active = "Активно" -activeRange = "0–{{value}} дн." -archived = "Архівовано" -coldStorage = "холодне сховище" -deleted = "Видалено" -never = "ніколи" -purged = "очищено" - -[portal.infrastructure.storage.providers] -connect = "Підключити" -connected = "Підключено" -heading = "Підключені провайдери" -subheading = "Куди записуються оброблені артефакти." - -[portal.infrastructure.storage.retention] -heading = "Зберігання" -subheading = "Як довго артефакти зберігаються перед видаленням за життєвим циклом." -windowLabel = "Типове вікно зберігання" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} день" -days_other = "{{count}} днів" -never = "Ніколи не видаляти" - -[portal.infrastructure.storage.totalUsage] -heading = "Загальне використання" -progressLabel = "Використано сховища" -subheading = "Сховище, використане всіма підключеними провайдерами." +[portal.infrastructure.keyLabel] +active = "Активний" +revoked = "Відкликаний" [portal.infrastructure.tabs] apiKeys = "Ключі API" @@ -7174,6 +7656,49 @@ models = "Моделі" security = "Безпека" storage = "Сховище" +[portal.integrations] +availableHeading = "Доступні" +comingSoonHeading = "Незабаром" +connect = "Підключити" +connectionCount_one = "{{count}} підключення" +connectionCount_other = "{{count}} підключень" +customApi = "Власний API" +searchPlaceholder = "Пошук інтеграцій" +subtitle = "Усе, з чим Stirling може взаємодіяти. Облікові дані створюються тут один раз, а потім використовуються джерелами, політиками та конвеєрами." +title = "Інтеграції" +typedName = "Назва {{tool}}" + +[portal.integrations.filters] +all = "Усі" +audit = "Аудит" +connected = "Підключено" +notify = "Сповіщення" +security = "Безпека" +signing = "Підписування" +storage = "Сховище" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Підключено" + +[portal.integrations.table] +integration = "Інтеграція" +worksWith = "Працює з" + +[portal.integrations.worksWith] +pipelines = "Конвеєри" +policies = "Політики" +sources = "Джерела" + +[portal.legal] +draft = "{{label}} · чернетка" +loadError = "Не вдалося завантажити цей документ. Спробуйте ще раз." +loading = "Завантаження…" +title = "Юридичний документ" + [portal.nav] agent-builder = "Конструктор агентів" components = "Компоненти" @@ -7182,33 +7707,17 @@ documents = "Документи" editor = "Редактор" home = "Головна" infrastructure = "Інфраструктура" +integrations = "Інтеграції" pipelines = "Конвеєри" policies = "Політики" procurement = "Закупівлі" -settings = "Налаштування" sources = "Джерела" usage = "Використання та оплата" users = "Користувачі" -[portal.notifications] -markAllRead = "Позначити всі як прочитані" -title = "Сповіщення" -viewAll = "Переглянути всі" - -[portal.notifications.ariaLabel] -none = "Сповіщення, немає непрочитаних" -unread_one = "Сповіщення, {{count}} непрочитане" -unread_other = "Сповіщення, {{count}} непрочитаних" - -[portal.notifications.count] -allRead = "усі прочитані" -loading = "завантаження" -new_one = "{{count}} нове" -new_other = "{{count}} нових" - -[portal.notifications.empty] -description = "Немає нових сповіщень." -title = "У вас усе переглянуто" +[portal.nav.section] +platform = "PDF платформа" +processor = "PDF Processor" [portal.pipelines] subtitle = "Кожен автоматизований конвеєр документів на backend: упорядкований ланцюг операцій над набором джерел, що запускається тригером. Натисніть рядок, щоб переглянути його кроки та джерела." @@ -7218,54 +7727,87 @@ title = "Конвеєри" newPipeline = "Новий конвеєр" [portal.pipelines.builder] -addStep = "Додати інструмент" +activate = "Activate" back = "Назад до конвеєрів" +cannotFollow = "Не можна прийняти {{produced}}" +chooseAccount = "Виберіть обліковий запис" +chooseDestination = "Виберіть призначення" +chooseOperation = "Виберіть, що робить цей крок" +chooseSource = "Виберіть джерело" discard = "Відхилити зміни" -enabled = "Увімкнено" +inputs = "Вхідні дані" +inputSource = "Вхідне джерело" +inputTrigger = "Тригер" keepEditing = "Продовжити редагування" -needsUpload = "Потрібен завантажений файл" +moreActions = "More actions" +needsConfiguring = "Потрібне налаштування" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Жоден інструмент не відповідає вашому пошуку." -pipelineSettings = "Налаштування конвеєра" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Пошук інструментів" -selectToolBody = "Додайте інструмент, щоб створити конвеєр." -selectToolTitle = "Інструментів ще немає" -toolSettings = "Налаштування інструмента" +sendToSystem = "Надіслати до іншої системи" +stepsIncompatible = "Ці кроки не можуть працювати з тим, що створює попередній крок: {{tools}}." +stepsNeedSetup = "Ці кроки все ще потрібно налаштувати перед збереженням: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Нерозпізнана операція, залишено як є." unsavedBody = "У вас є незбережені зміни. Зберегти їх перед виходом чи відхилити?" unsavedTitle = "Незбережені зміни" -uploadUnsupported = "Завантажені файли поки не підтримуються в конвеєрах, тому ці кроки не можна зберегти: {{tools}}." usesDefaults = "Запускається з типовими налаштуваннями" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Об'єднує кожен файл із попереднього кроку" +fan-out = "Запускається один раз для кожного файлу з попереднього кроку" +format-mismatch = "Потрібно {{accepts}}, але попередній крок створює {{produced}}" +output-uncertain = "Може не запуститися: вихідні дані попереднього кроку залежать від його налаштування" +source-mismatch = "Потрібно {{accepts}}, але вхід цього конвеєра — {{produced}}" +undeclared-operation = "Не вдається перевірити, що приймає цей крок" [portal.pipelines.composer] addTool = "Додати інструмент" -cancel = "Скасувати" -chainEmpty = "Додайте інструмент, щоб почати створювати конвеєр." create = "Створити конвеєр" -directory = "Вихідна папка" -directoryHelp = "Абсолютний шлях на сервері. Має бути в межах налаштованих дозволених папок." +createPaused = "Create paused" editingUnsupported = "Відображення цих параметрів інструмента для редагування поки не підтримується." -moveDown = "Перемістити вниз" -moveUp = "Перемістити вгору" +editSource = "Edit source" name = "Назва" namePlaceholder = "напр. Redaction sweep" -noSources = "Джерела ще не підключено. Конвеєр усе одно може запускатися для файлів, переданих йому напряму." noToolSettings = "Цей інструмент не має налаштовуваних параметрів." -operations_one = "Операція ({{count}})" -operations_other = "Операції ({{count}})" output = "Вивід" -removeStep = "Вилучити операцію" save = "Зберегти зміни" scheduleEvery = "Запускати кожні" -sources = "Джерела" -sourcesLoading = "Завантаження джерел..." trigger = "Тригер" triggerManual = "Лише вручну" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "днів" hours = "годин" minutes = "хвилин" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Видалити \"{{name}}\"? Цю дію не можна скасувати." cancel = "Скасувати" @@ -7273,28 +7815,62 @@ confirm = "Видалити" title = "Видалити конвеєр?" [portal.pipelines.detail] +clearHistory = "Очистити історію" delete = "Видалити конвеєр" run = "Запустити зараз" [portal.pipelines.empty] action = "Створити конвеєр" +connectSource = "Підключити джерело" description = "Створіть свій перший конвеєр: виберіть джерела, для яких він запускається, з’єднайте операції та виберіть, куди надходитиме вивід." title = "Конвеєрів ще немає" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Активні" paused = "Призупинені" total = "Конвеєри" -[portal.pipelines.output] -folder = "Записати в папку" -inline = "Повернути файли" - [portal.pipelines.run] +allProcessed_one = "Нічого запускати: {{count}} документ джерела вже оброблено." +allProcessed_other = "Нічого запускати: усі {{count}} документів у джерелах уже оброблено." completed_one = "Запуск завершено." completed_other = "Усі {{count}} запусків завершено." empty = "Немає чого запускати: джерела не мали документів для обробки." failed = "Запуск не вдався: {{error}}" +inFlight = "Немає нічого нового для запуску: документи все ще обробляються з попереднього запуску." +parked_one = "Нічого запускати: {{count}} документ раніше не вдалося обробити, і його відкладено. Усуньте причину, а потім очистьте історію, щоб повторити спробу." +parked_other = "Нічого запускати: {{count}} документів раніше не вдалося обробити, і їх відкладено. Усуньте причину, а потім очистьте історію, щоб повторити спробу." running = "Запуск розпочато; ще виконується." timeout = "Запуск триває довше, ніж очікувалося; він може ще завершитися у фоновому режимі." @@ -7307,6 +7883,7 @@ name = "Конвеєр" sources = "Джерела" status = "Статус" steps = "Кроки" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Відстеження папки" @@ -7314,12 +7891,18 @@ manual = "Вручну" schedule = "За розкладом" [portal.policies] +defaultName = "Політика {{category}}" subtitle = "Постійні автоматизації, що застосовують конвеєр інструментів до кожного документа. Кожна політика спрацьовує під час завантаження або експорту, запускає свій ланцюг інструментів і зберігає примусово оброблену версію поруч з оригіналом." title = "Політики" [portal.policies.card] comingSoon = "Оновити до Enterprise" notSetUp = "Не налаштовано" +requiresAiEngine = "Потрібен AI рушій" + +[portal.policies.categories.classification] +desc = "Визначайте тип кожного документа під час завантаження та позначайте його метадані для зберігання й пошуку." +label = "Класифікація" [portal.policies.categories.compliance] desc = "Забезпечуйте вимоги HIPAA, GDPR, SOC 2 або FedRAMP для кожного документа." @@ -7344,6 +7927,13 @@ label = "Безпека" [portal.policies.config] scopeAll = "Усі документи" +[portal.policies.config.classification] +summary = "Класифікує кожен завантажений документ за мітками вашої команди та позначає його метадані." + +[portal.policies.config.classification.rules] +0 = "Класифікувати" +1 = "Позначити метадані" + [portal.policies.config.compliance] summary = "Перевіряє документи на відповідність нормативним фреймворкам перед тим, як вони залишать систему." @@ -7371,6 +7961,29 @@ minConfidence = "Мін. впевненість" 2 = "Назвати" 3 = "Нормалізувати" +[portal.policies.config.purview] +label = "Застосувати мітку чутливості Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Тенант Purview" +labelId = "ID мітки" +labelIdHelp = "GUID мітки з порталу Purview. Застосування мітки записує її метадані в PDF; це не може шифрувати, для цього потрібен клієнт Microsoft." +labelName = "Назва мітки (необов'язково)" +labelNameHelp = "Записується разом із ID, щоб люди й інструменти могли розпізнати мітку." + +[portal.policies.config.purview.fields.method] +help = "Purview відрізняє мітку, застосовану політикою, від мітки, яку вибрала людина." +label = "Як записується мітка" +privileged = "Як вибрана людиною" +standard = "Як застосована автоматично" + +[portal.policies.config.purviewRead] +label = "Прочитати мітку Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Тенант Purview" +connectionHelp = "Зчитує мітку, яку документ уже має, щоб наступні кроки могли діяти на її основі." + [portal.policies.config.retention] summary = "Застосовує правила щодо того, як довго зберігаються документи, коли архівуються та коли видаляються." @@ -7438,6 +8051,7 @@ title = "Активності ще немає" [portal.policies.endpoints] addWatermark = "Водяний знак" autoRedact = "Редагувати PII" +classifyAndLabel = "Класифікувати" compressPdf = "Стиснути" flatten = "Звести" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Ваші політики збережено, і вони з’я retry = "Повторити" title = "Backend недоступний" +[portal.policies.operations] +change = "Змінити, що робить цей крок" +noResults = "Жоден крок не відповідає цьому. Спробуйте назву продукту або \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Пошук кроків — спробуйте \"scan\", \"attach\" або назву продукту" + +[portal.policies.operations.bodyMode] +binary = "Сирі байти файлу" +json = "Усередині JSON payload" +multipart = "Як завантаження файлу (multipart)" + +[portal.policies.operations.clamavScan] +description = "Ваш власний сервер ClamAV перевіряє документ; нічого не залишає вашу мережу." +label = "Сканувати на віруси (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Відхиляє документи з макросами, виконуваними файлами, скриптами або вбудованими об'єктами." +label = "Блокувати активний вміст" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive перевіряє документ, і запуск зупиняється, якщо він не чистий." +label = "Сканувати на віруси" + +[portal.policies.operations.confluenceAttach] +description = "Зберігає оброблений документ на сторінці." +label = "Прикріпити до сторінки Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Надсилає документ до ConsignO Cloud для підписання." +label = "Надіслати на підпис (ConsignO)" +note = "Надсилає документ. Отримання підписаної копії ще не підтримується, оскільки крок не може передати результат наступному кроку." + +[portal.policies.operations.customApiCall] +description = "Створіть виклик самостійно: шлях, метод, тіло та обробка відповіді." +label = "Викликати власний API" + +[portal.policies.operations.discordNotify] +description = "Повідомляє канал, що політика обробила документ." +label = "Опублікувати повідомлення в Discord" + +[portal.policies.operations.elasticIndex] +description = "Записує дії політики як документ, доступний для пошуку." +label = "Індексувати подію аудиту в Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart підходить для upload API; JSON підходить для API, яким потрібен файл усередині payload; binary надсилає сирі байти." +label = "Як надіслати документ" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "JSON тіло, надіслане як є. {{document.base64}} містить файл; {{document.*}} і {{run.*}} заповнюються для кожного документа." +label = "JSON тіло" + +[portal.policies.operations.fields.connection] +label = "Обліковий запис" + +[portal.policies.operations.fields.domain] +label = "Домен Mailgun" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Поле форми, у якому API очікує документ." +label = "Назва поля файлу" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Від" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "JSON об'єкт пар назва/значення заголовків. Облікові дані підключення додаються автоматично." +label = "Заголовки" + +[portal.policies.operations.fields.index] +label = "Індекс" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Ключ задачі" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Надсилається як тіло повідомлення. {{document.*}} і {{run.*}} заповнюються для кожного документа." +label = "Повідомлення" + +[portal.policies.operations.fields.method] +label = "Метод" + +[portal.policies.operations.fields.pageId] +label = "ID сторінки" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Додається до базового URL підключення. Він ніколи не може звернутися до іншого хоста." +label = "Шлях" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Куди на диску записати файл. {{document.filename}} заповнюється для кожного документа." +label = "Шлях призначення" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Звіт залишає документ без змін. Заміна замінює його на те, що повернеться у відповіді." +label = "Що робити з відповіддю" + +[portal.policies.operations.fields.signerEmail] +label = "Email підписанта" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Тема" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio аналізує цей текст. Використайте {{document.title}} або вставте текст, який потрібно перевірити." +label = "Текст для перевірки" + +[portal.policies.operations.fields.to] +label = "Кому" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Ім'я користувача Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Повідомляє простір, що політика обробила документ." +label = "Опублікувати повідомлення в Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Зберігає оброблений документ у задачі." +label = "Прикріпити до задачі Jira" + +[portal.policies.operations.mailgunEmail] +description = "Надсилає оброблений документ як вкладення." +label = "Надіслати документ email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Записує оброблений документ у папку." +label = "Завантажити в Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Ваш власний сервер Presidio повідомляє про знайдені персональні дані." +label = "Виявити персональні дані" +note = "Presidio читає текст, а не файли. Надайте текст, який потрібно перевірити — сам документ не завантажується." + +[portal.policies.operations.responseMode] +replace = "Замінити документ відповіддю" +report = "Зберегти документ, записати відповідь" + +[portal.policies.operations.sendgridEmail] +description = "Надсилає оброблений документ як вкладення." +label = "Надіслати документ email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Повідомляє канал, що політика обробила документ." +label = "Опублікувати повідомлення в Slack" + +[portal.policies.operations.splunkEvent] +description = "Записує дії політики там, де ваші аудитори вже переглядають дані." +label = "Надіслати подію аудиту до Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Записує дії політики в HTTP колектор." +label = "Надіслати подію аудиту до Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Повідомляє канал, що політика обробила документ." +label = "Опублікувати повідомлення в Teams" + +[portal.policies.operations.webhookPost] +description = "Надсилає сам документ на URL, який ви виберете." +label = "Опублікувати документ у webhook" + +[portal.policies.operations.zapierNotify] +description = "Передає деталі запуску вашій автоматизації." +label = "Запустити сценарій Zap або Make" + [portal.policies.stats] activeFor = "Активна" dataProcessed = "Оброблено даних" @@ -7473,6 +8282,13 @@ label = "Оброблено документів" description = "Налаштовано, але не запускається" label = "Призупинено" +[portal.policies.table] +appliesTo = "Застосовується до" +docs = "Застосовано до документів" +enforces = "Забезпечує" +policy = "Політика" +status = "Статус" + [portal.policies.wizard.actions] back = "Назад" cancel = "Скасувати" @@ -7480,6 +8296,10 @@ continue = "Продовжити" enablePolicy = "Увімкнути політику" saveChanges = "Зберегти зміни" +[portal.policies.wizard.capability.classify] +desc = "Визначає тип документа за мітками вашої команди та позначає його, щоб він зберігався й шукався за категорією." +label = "Класифікувати документ" + [portal.policies.wizard.capability.compress] desc = "Стискає документ до меншого розміру файлу." label = "Зменшити розмір файлу" @@ -7500,10 +8320,18 @@ label = "Редагувати конфіденційну інформацію" desc = "Видаляє прихований JavaScript, щоб нічого не запускалося автоматично під час відкриття документа." label = "Видалити активний вміст" +[portal.policies.wizard.capability.timestampPdf] +desc = "Доводить, що документ існував саме в такій формі в певний момент часу, використовуючи незалежний центр часових позначок. Надсилається лише хеш — документ ніколи не залишає ваш сервер." +label = "Додати довірену часову позначку" + [portal.policies.wizard.capability.watermark] desc = "Наносить видиму позначку (наприклад, «Конфіденційно») на кожну сторінку." label = "Застосувати водяний знак" +[portal.policies.wizard.classification] +description = "Кожен завантажений документ класифікується за вбудованими мітками та позначається відповідними типами. Набір міток спільний для всієї вашої команди." +labelsHeading = "Мітки класифікації" + [portal.policies.wizard.errors] noTools = "Спочатку увімкніть принаймні один інструмент у робочому процесі." saveFailed = "Не вдалося зберегти політику. Спробуйте ще раз." @@ -7548,73 +8376,118 @@ setUp = "Налаштувати політику {{category}}" [portal.policies.wizard.workflow] description = "Виберіть, що ця політика робить із кожним документом, який обробляє." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} активні" -noRule = "Ще не застосовано жодного правила" -subtitle = "Постійні автоматизації, через які проходить кожен документ, незалежно від того, який конвеєр його обробляє." -title = "Що запускається для ваших PDF" - [portal.policySummary.action] -comingSoon = "Перейти на Enterprise" -configure = "Налаштувати" setUp = "Налаштувати" -[portal.policySummary.column] -activeRule = "Активне правило" -policy = "Політика" -status = "Статус" +[portal.processorFlow] +footnote = "Підрахунки наведено за останні 24 години. Швидкість потоку є ілюстративною." +liveBadge = "Live" +stats = "{{connected}} підключено · {{processed}} PDF оброблено" +title = "PDF Processor" -[portal.policySummary.empty] -description = "Після налаштування політик категорії з’являться тут." -title = "Політик ще немає" +[portal.processorFlow.lens] +ariaLabel = "Режим перегляду" +flow = "Потік" +sankey = "Sankey" -[portal.policySummary.state] -active = "Активна" -off = "Вимкнено" +[portal.processorFlow.outcomes] +count = "{{n}} · 24 год" +failed = "Не вдалося" +heading = "Результати" +success = "Доставлено" + +[portal.processorFlow.policies] +activeCount = "{{n}} активних" +count = "{{n}} · 24 год" +heading = "Політики" +setUp = "Налаштувати" soon = "Незабаром" -[portal.processingStatus] -managePlan = "Керувати планом" -volumeSuffix = "PDF оброблено · останні 30 днів" +[portal.processorFlow.sankey] +waist = "Політики · {{n}} активних" + +[portal.processorFlow.sankey.empty] +description = "Підключіть джерело та ввімкніть політику, щоб спостерігати, як документи проходять через processor." +title = "Потоку ще немає" + +[portal.processorFlow.sources] +comingSoonTag = "Підключити" +editor = "Stirling PDF Editor" +heading = "Джерела" +perDay = "{{n}} / 24 год" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Приймання email" [portal.procurement] -reset = "Скинути закупівлю (демо)" subtitle = "Оцініть, укладіть контракт і підключіть свою команду. Кожен документ в одному місці." title = "Закупівля" -[portal.procurement.action] -download = "Завантажити" -pay = "Сплатити зараз" -request = "Запитати" -sign = "Переглянути й підписати" -upload = "Завантажити" - [portal.procurement.agreement] agreeCta = "Погодитися й підписатися" +confidential = "Конфіденційно" confirm = "Я прочитав(-ла) і погоджуюся з Stirling Enterprise Agreement." -eyebrow = "Угода" -intro = "Одна об’єднана угода охоплює вашу угоду: Master Service Agreement, Order Form, EULA та Data Processing Agreement. Перегляньте її, а потім погодьтеся прийняти пропозицію в зобов’язану підписку." -title = "Перегляньте вашу корпоративну угоду" +docName = "Stirling Enterprise Agreement" +docSub = "MSA, Order Form, EULA і DPA, об'єднані в один підпис." +download = "Завантажити" +downloadDraftError = "Не вдалося згенерувати PDF угоди — рендерер документа недоступний. Спробуйте невдовзі ще раз або зверніться до служби підтримки." +downloadError = "Не вдалося завантажити підписану угоду. Спробуйте ще раз." +legalName = "Назва юридичної особи" +legalNamePlaceholder = "Юридична особа, яка підписуватиме" +loadError = "Не вдалося завантажити угоду. Спробуйте ще раз." +loading = "Завантаження угоди..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Запросити зміни" +scrollHint = "Прокрутіть, щоб переглянути" +signatory = "Ім'я підписанта" +signatoryPlaceholder = "Повне ім'я" +signatoryTitle = "Посада підписанта" +signatoryTitlePlaceholder = "наприклад, General Counsel" +signError = "Не вдалося записати ваш підпис. Спробуйте ще раз." [portal.procurement.builder] addons = "Додатки" +addressLine1 = "Адреса, рядок 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Адреса, рядок 2" +addressLine2Placeholder = "Suite, floor, building" back = "Назад" businessName = "Назва компанії" businessNamePlaceholder = "Ваша компанія" +city = "Місто" +cityPlaceholder = "San Francisco" +completeRequired = "Заповніть обов'язкові поля (позначені *) дійсним email перед створенням комерційної пропозиції." +contactEmail = "Контактний email" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Ім'я контактної особи" +contactNamePlaceholder = "Jane Doe" continue = "Продовжити" -country = "Країна" -countryEuro = "Єврозона (EUR €)" -countryUK = "Велика Британія (GBP £)" -countryUS = "Сполучені Штати (USD $)" +done = "Готово" eula = "Я прочитав(-ла) і погоджуюся зі Stirling Enterprise EULA. Вона регулює угоду, створену з цієї пропозиції." generate = "Створити пропозицію" -included = "Включено" indemnification = "Відшкодування за IP" indemnificationSub = "Ми захищаємо від прийнятних претензій щодо IP відповідно до EULA" -offlineLicense = "Офлайн / ізольована ліцензія" -offlineLicenseSub = "Файл ліцензії для завантаження для ізольованого self-hosted екземпляра" +paperEyebrow = "Enterprise пропозиція" +paperFor = "Підготовлено для" +pdfSize = "Розмір PDF" +poNumber = "Номер PO" +poNumberPlaceholder = "Необов'язково" +postalCode = "Поштовий індекс" +postalCodePlaceholder = "94105" +posture = "Управління" +posture_count = "~{{count}} політик" +posture_essentials = "Основи" +posture_essentialsSub = "Класифікація + спільний доступ — типові налаштування" +posture_governed = "Керований" +posture_governedSub = "Додає безпеку та маршрутизацію" +posture_regulated = "Регульований" +posture_regulatedSub = "Кожна категорія — відповідність, зберігання, приймання" qbr = "Щоквартальні бізнес-огляди" qbrSub = "Ваш SE щокварталу переглядає використання та roadmap" +region = "Штат / регіон" +regionPlaceholder = "California" running = "{{annual}} / рік · {{years}}-річний {{tcv}}" s1Sub = "Ваша команда та PDF, які ви плануєте обробляти щороку." s1Title = "Скільки ви оброблятимете?" @@ -7623,6 +8496,12 @@ s2Title = "Зобов’язання та сервіс" s3Sub = "Для пропозиції та угоди, яку вона створює." s3Title = "Ваші дані" serviceLevel = "Рівень сервісу" +size_compact = "Компактний" +size_compactSub = "Переважно текст, менше 1 MB" +size_heavy = "Важкий" +size_heavySub = "Сканований або з великою кількістю зображень, 5 MB+" +size_standard = "Стандартний" +size_standardSub = "Змішаний текст і зображення, від 1 до 5 MB" slDedicated = "Виділений" slDedicatedSub = "4 робочі години · виділений менеджер облікового запису · +30%" slPriority = "Пріоритетний" @@ -7630,6 +8509,8 @@ slPrioritySub = "Того ж робочого дня · призначений C slStandard = "Стандартний" slStandardSub = "Наступного робочого дня · спільний CSM · включено" stepOf = "Крок {{n}} з {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Необов'язково" term = "Термін" termDiscount = "Застосовано знижку {{pct}}% за багаторічне зобов’язання" title = "Створіть свою пропозицію" @@ -7637,6 +8518,7 @@ training = "Підключення та навчання" trainingSub = "Живі сесії, щоб ваша команда швидко почала роботу" users = "Усього користувачів" usersPlaceholder = "наприклад, 250" +viewEula = "Прочитати" volEstimated = "Оцінено на основі {{count}} користувачів (~2,000 PDF кожен, включно з автоматизацією). Змініть, якщо знаєте точніше." volManual = "Використовується ваше значення. Переоцініть на основі розміру команди в будь-який час." volNoUsers = "Не впевнені? Введіть розмір вашої команди, і ми оцінимо його." @@ -7645,55 +8527,55 @@ volumePlaceholder = "наприклад, 1,000,000" years_one = "{{count}} рік" years_other = "{{count}} років" -[portal.procurement.docs] -count_one = "{{count}} документ" -count_other = "{{count}} документів" -done = "Готово" -here = "Ви тут" -hide = "Приховати" -optional = "Необов’язково" -paidAddon = "Платний додаток" -show = "Показати" -subtitle = "Усе, що вам потрібно на кожному кроці шляху, відображається в міру просування угоди." -supportingSubtitle = "SOC 2, перевірки безпеки, податкові форми та інше — готові, коли ваша команда безпеки або закупівель попросить. Деякі мають одноразову плату." -supportingTitle = "Підтримка вашої оцінки" +[portal.procurement.documents] +agreement = "Enterprise Agreement" +agreementSub = "MSA, Order Form і DPA в одному підписі." +download = "Завантажити" +eula = "EULA та комерційні умови" +eulaSub = "Програмне забезпечення та комерційні умови самообслуговування." +invoice = "Рахунок" +invoiceSub = "Ваш перший рахунок за підписку." +laterInvoice = "Доступно після прийняття" +laterQuote = "Доступно після випуску вашої пропозиції" +quote = "Комерційна пропозиція" +quoteSub = "Ваша деталізована Enterprise пропозиція." +sla = "Додаток SLA" +slaSub = "Рівні підтримки та цілі часу відповіді." +subprocessors = "Субпроцесори" +subprocessorsSub = "Треті сторони, які обробляють дані від вашого імені." +subtitle = "Ваші документи угоди, доступні будь-коли." title = "Документи" -upcoming = "Майбутні" +view = "Переглянути" [portal.procurement.error] title = "Щось пішло не так" [portal.procurement.hero] -company = "Ваша корпоративна угода" +barAria = "Етап {{current}} з {{total}}" ctaAgreement = "Переглянути й підписати угоду" -ctaLive = "Ви вже працюєте" -ctaPayment = "Додати платіж" +ctaExploring = "Налаштувати пробний період" ctaQuote = "Переглянути вашу пропозицію" +ctaReviewQuote = "Переглянути пропозицію" ctaTrial = "Створити пропозицію" +documents = "Документи" eyebrow = "Корпоративна закупівля" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Запросити колег" -keyDocs = "Ключові документи" -nextStep = "Наступний крок: {{action}}" -notStarted = "Не розпочато" +licenseKey = "Ліцензійний ключ" +liveSub = "Вашу організацію підготовлено, і ваша ліцензія активна." +liveTitle = "Ви працюєте на Stirling Enterprise" +next = "Далі: {{stage}}" open = "Відкрити закупівлю" scheduleCall = "Запланувати дзвінок" -setup1Sub = "Запросіть своїх колег" -setup1Title = "Розгорнути PDF Editor" -setup2Sub = "Увімкніть обробку в редакторах та інших джерелах" -setup2Title = "Підключити PDF Processor" -setup3Sub = "Увімкніть Security, Compliance, Routing або Retention, коли вони вам потрібні" -setup3Title = "Додати рекомендовані політики" +sentenceAgreement = "перегляньте та підпишіть угоду" +sentenceLive = "підготовка та розгортання" +sentencePayment = "замовлення на купівлю та оплата" +sentenceQuote = "перегляньте та прийміть вашу пропозицію" +sentenceTrial = "налаштуйте пробний робочий простір" [portal.procurement.journey] daysLeft_one = "Залишився {{count}} день" daysLeft_other = "Залишилося {{count}} днів" -engineerLabel = "Ваш solutions engineer" -eyebrow = "Ваше впровадження" -live = "Ви вже працюєте в Stirling Enterprise" -nextStep = "Наступний крок: {{action}}" -subtitle = "Ваш solutions engineer супроводжує кожен крок. Одна наступна дія за раз; повний список нижче." -title = "Від trial до запуску — один керований шлях" -trialTitle = "Enterprise trial" [portal.procurement.journeySteps.agreement] blurb = "Один підпис охоплює MSA, order form, EULA та DPA." @@ -7727,6 +8609,9 @@ downloadError = "Поки що не вдалося створити офлайн downloadOffline = "Завантажити офлайн-ліцензію (.lic)" hint = "Вставте цей ключ у self-hosted екземпляр, щоб активувати його, або збережіть для своїх записів. Зберігайте його безпечно." label = "Ваш ліцензійний ключ" +subtitle = "Активуйте self-hosted інстанс цим ключем або збережіть його для своїх записів." +title = "Ваш ліцензійний ключ" +trialFileHint = "Це ваш пробний файл ліцензії. Коли угоду буде укладено, поверніться та завантажте його знову — на відміну від онлайн-ліцензійного ключа, файл .lic не оновлюється самостійно." [portal.procurement.link] cta = "Прив’язати обліковий запис" @@ -7739,65 +8624,62 @@ description = "Ваша підписка активна, і ліцензію в eyebrow = "Live" title = "Ви вже працюєте в Stirling Enterprise" -[portal.procurement.locked] -description = "Trial-ключі, пропозиції за зобов’язаним обсягом, угода з одним підписом, оплата та ваш реєстр документів — усе буде тут після початку корпоративної оцінки." -eyebrow = "Лише Enterprise" -talkToSales = "Зв’язатися з відділом продажів" -title = "Шлях закупівлі відкривається з Enterprise" - [portal.procurement.milestone] -accept = "Прийняти й продовжити" -description = "Завантажте PDF, щоб поділитися ним із командою, поверніться, щоб прийняти, коли будете готові, або внесіть зміни." -download = "Завантажити PDF" downloadError = "Поки що не вдалося завантажити PDF пропозиції — спробуйте ще раз за мить." -edit = "Редагувати пропозицію" -eyebrow = "Пропозиція {{number}}" -perYear = " / рік" -preparedFor = "Підготовлено для {{company}}" -tcv = "{{value}} загальна вартість контракту" -title = "Ваша пропозиція готова" - -[portal.procurement.modal] -cancel = "Скасувати" -chooseFile = "Вибрати файл" -close = "Закрити" -downloadBody = "Ваше завантаження незабаром почнеться." -downloadCta = "Завантажити" -downloadTitle = "Завантажити" -noFile = "Файл не вибрано" -payBody = "Сплатіть ваш зобов’язаний контракт карткою або банківським переказом через Stripe. Ваш робочий простір буде підготовлено, щойно платіж пройде." -payCta = "Продовжити до Stripe" -payTitle = "Підтвердити платіж" -requestBodyFree = "Ми створюємо це на вимогу. Підтвердьте, і ваш solutions engineer незабаром надішле це вам." -requestBodyPaid = "Це платний додаток. Підтвердьте, і ваш solutions engineer оцінить обсяг і надішле документи." -requestCta = "Запитати" -requestTitle = "Запитати цей документ" -signBody = "Відкриває Stirling Enterprise Agreement для електронного підпису: один підпис охоплює MSA, order form, EULA та DPA. Ми автоматично ставимо зустрічний підпис, і ви переходите до оплати." -signCta = "Відкрити для підпису" -signTitle = "Перегляньте та підпишіть вашу угоду" -uploadBody = "Надішліть нам ваш PO, і ми виставимо рахунок за вашими умовами. Перетягніть PDF або виберіть файл нижче." -uploadCta = "Завантажити purchase order" -uploadTitle = "Завантажте ваш purchase order" [portal.procurement.payment] description = "Вашу пропозицію прийнято, і ваша ліцензія вже активна — ваша команда може почати одразу. Сплатіть перший рахунок, коли будете готові; ви можете сплатити або завантажити його тут, без email." +downloadAgreement = "Завантажити підписану угоду" downloadInvoice = "Завантажити рахунок" -simulate = "Імітувати отримання платежу (демо)" +eyebrow = "Оплата" title = "Підписку створено" viewInvoice = "Переглянути й сплатити рахунок" +[portal.procurement.review] +acceptCta = "Прийняти пропозицію" +annual = "Річна плата (рік 1)" +downloadCta = "Завантажити пропозицію" +poNumber = "Замовлення на купівлю: {{po}}" +renewal = "Поновлюється за {{amount}}/рік після строку (+{{pct}}% CPI)" +tcv = "{{years}}-річний строк, повна передоплата: {{tcv}}" +validUntil = "Дійсно до {{date}}" + [portal.procurement.schedule] fallback = "Не вдалося завантажити планувальник." fallbackLink = "Відкрити планування в новій вкладці" +loading = "Завантаження планувальника..." subtitle = "Ваш solutions engineer проведе вашу команду через впровадження. Виберіть зручний час." title = "Запланувати дзвінок" -[portal.procurement.status] -action = "Потрібна дія" -available = "Доступно" -complete = "Завершено" -pending = "Очікує" -request = "За запитом" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Ізольовано, без вихідних з'єднань" +back = "Назад" +businessName = "Назва компанії" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Керований, найшвидший старт" +continue = "Продовжити" +deployment = "Де розгорнути?" +eula = "Я погоджуюся з Stirling EULA та комерційними умовами." +fullName = "Повне ім'я" +fullNamePlaceholder = "Ваше ім'я" +invites = "Запросити колег · необов'язково" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Запланувати дзвінок" +seats = "Користувачі для тестування" +seatsPlaceholder = "наприклад, 15" +selfhost = "Self-hosted" +selfhostSub = "Docker або Kubernetes" +start = "Почати пробний період" +stepOf = "Крок {{n}} з {{total}}" +subtitle = "Безкоштовно 14 днів, картка не потрібна." +subtitleDetails = "Кілька деталей для вашої пропозиції та угоди." +talkFirst = "Хочете спочатку поговорити?" +title = "Налаштуйте пробний період" +viewEula = "Прочитати" +workEmail = "Робочий email" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Продовження додає 7 днів і повідомляє вашого solutions engineer." @@ -7808,109 +8690,113 @@ maxed = "Ліміт вичерпано" subtitle = "Ваш безкоштовний trial триває до {{date}}. Картка не потрібна." title = "Enterprise trial" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Ціни за зобов’язаним обсягом, загальноорганізаційні SSO + SCIM + RBAC, 90-денний незмінний аудит і виділений SE." -homeCta = "Почати Trial →" -homeHeadline = "Обробляйте мільйони PDF." - -[portal.recentActivity] -title = "Нещодавня активність" -viewAll = "Переглянути все" - -[portal.recentActivity.empty] -description = "Запуски конвеєрів, розгортання та події агентів з’являться тут." -title = "Тут поки нічого немає" - -[portal.search] -ariaLabel = "Пошук" -placeholder = "Пошук у Stirling — endpoints, конвеєри, документи…" - -[portal.search.empty] -noActionsDescription = "Швидкі дії з’являться тут, коли стануть доступними." -noActionsTitle = "Немає швидких дій" -noMatches = "Немає збігів для «{{query}}»" -noMatchesDescription = "Спробуйте інше ключове слово або перегляньте каталог." - [portal.settings.groups] admin = "Адміністратор" [portal.settings.sections] account-link = "Прив’язка облікового запису" -[portal.shell.header] -accountFallback = "Обліковий запис" -accountMenu = "Меню облікового запису" -darkMode = "Темний режим" -lightMode = "Світлий режим" -search = "Пошук" -searchPlaceholder = "Пошук…" -signOut = "Вийти" -switchToDark = "Перемкнути на темну тему" -switchToLight = "Перемкнути на світлу тему" - [portal.shell.sidebar] appEditor = "Редактор" appProcessor = "Обробник" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} документів" linkAccount = "Прив’язати обліковий запис Stirling" -planEditor = "План Editor" -planEnterprise = "План Enterprise" -planProcessor = "План Processor" primaryNav = "Основна навігація" switchApp = "Перемкнути застосунок" +[portal.shell.topbar] +closeNav = "Закрити навігацію" +openNav = "Відкрити навігацію" +search = "Пошук" + [portal.sources] subtitle = "Багаторазові вхідні підключення, які передають документи в Stirling. Налаштуйте підключення один раз, а потім посилайтеся на нього з будь-якої кількості політик. Натисніть рядок, щоб переглянути його конфігурацію та політики, які його використовують." title = "Джерела" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Підключити джерело" +[portal.sources.builder] +back = "Назад до джерел" +backToTypes = "Усі типи джерел" +cancel = "Скасувати" +chooseHint = "Виберіть, звідки надходять документи. З'єднувачі, позначені сірим, уже в роботі." +comingSoon = "Незабаром" +comingSoonHeading = "Незабаром" +create = "Створити джерело" +createTitle = "Підключити джерело" +delete = "Видалити" +editTitle = "Редагувати джерело" +enabled = "Увімкнено" +save = "Зберегти зміни" + +[portal.sources.builder.folderAccess] +description = "Автоматизації папок можуть використовувати лише папки, дозволені адміністратором. Додайте її в налаштуваннях доступу до папок, а потім спробуйте ще раз." +openSettings = "Налаштування доступу до папок" +title = "Ця папка не дозволена" + [portal.sources.delete] body = "Видалити «{{name}}»? Цю дію не можна скасувати. Політики, які посилаються на нього, потрібно буде оновити." cancel = "Скасувати" confirm = "Видалити" title = "Видалити джерело?" -[portal.sources.detail] -closeAriaLabel = "Закрити деталі" -delete = "Видалити джерело" -docs24h = "Останні 24 год" -docs30d = "Останні 30 днів" -docsTotal = "Усього побачено" -docsTrend = "Документи за останні 30 днів" -documents = "Документи" -edit = "Редагувати" -notReferenced = "Не використовується жодною політикою, тому його безпечно видалити." -pause = "Призупинити" -resume = "Відновити" -subtitle = "{{type}} · {{status}}" -usedBy = "Використовується" - -[portal.sources.empty] -description = "Підключіть папку (а незабаром — і cloud storage), щоб ваші політики мали звідки отримувати документи." -title = "Джерела ще не підключено" - [portal.sources.kpi] inUse = "Використовується" total = "Підключення" unused = "Не використовується" +[portal.sources.networkFields.connection] +helperText = "Збережене підключення з адресою сервера та обліковими даними. Повторно використовується кожним джерелом, яке на нього посилається." +label = "Підключення" + +[portal.sources.networkFields.directory] +helperText = "Папка на сервері для опитування, відносно домашньої папки входу або кореня спільного ресурсу. Залиште порожнім для кореня." +label = "Папка" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume видаляє кожен файл із сервера після того, як кожна політика його обробить." +label = "Режим читання" + +[portal.sources.networkFields.mode.options] +consume = "Consume: обробляти кожен файл один раз" +snapshot = "Snapshot: перечитувати папку під час кожного запуску" + +[portal.sources.networkFields.recursive] +label = "Глибина папок" + +[portal.sources.networkFields.recursive.options] +all = "Включати підпапки" +top = "Лише верхній рівень" + [portal.sources.status] active = "Активне" disabled = "Вимкнене" unused = "Не використовується" [portal.sources.table] +documents = "Документи" source = "Джерело" status = "Статус" +type = "Type" usedBy = "Політики" +[portal.sources.types.box] +description = "Отримуйте документи з папок Box, якими ваша команда вже ділиться." +label = "Box" + +[portal.sources.types.dropbox] +description = "Отримуйте документи зі спільної папки Dropbox." +label = "Dropbox" + [portal.sources.types.editor] +description = "Документи, які ваша команда обробила в editor, у межах політик і AI запусків." label = "Редактор" +noPolicies = "Поки що немає політик, запущених з editor." + +[portal.sources.types.email] +description = "Перетворюйте вкладення, надіслані до спеціальної поштової скриньки, на документи." +label = "Email inbox" [portal.sources.types.folder] description = "Відстежувати каталог на сервері для нових документів." @@ -7943,25 +8829,80 @@ label = "Глибина папки" all = "Включити підпапки" top = "Лише верхній рівень" +[portal.sources.types.ftp] +description = "Опитуйте папку FTP або FTPS сервера на наявність нових документів." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Отримуйте документи зі спільних папок Google Drive." +label = "Google Drive" + +[portal.sources.types.network] +description = "Відстежуйте SMB мережевий ресурс на наявність нових документів." +label = "Мережевий диск" + +[portal.sources.types.onedrive] +description = "Відстежуйте папки OneDrive на наявність нових документів." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Отримуйте документи з Amazon S3 або S3-сумісного bucket." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Збережене підключення, що містить bucket та облікові дані. Повторно використовується кожним джерелом і виходом конвеєра, які на нього посилаються." +label = "Підключення" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume видаляє кожен об'єкт із bucket після того, як кожна політика його обробить." +label = "Режим читання" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: обробляти кожен об'єкт один раз" +snapshot = "Snapshot: перечитувати bucket під час кожного запуску" + +[portal.sources.types.s3.fields.prefix] +helperText = "Обробляються лише об'єкти, ключі яких починаються з цього префікса." +label = "Префікс ключа" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Опитуйте папку приймання SFTP на наявність нових документів." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Отримуйте документи з бібліотек документів SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Джерело" +[portal.sources.types.webhook] +createNote = "Delivery URL і секрет підписання генеруються під час створення цього webhook. Відправники POST документи на цей URL, а політики, що посилаються на нього, запускаються після надходження." +description = "Отримуйте документи через підписаний HTTP POST з будь-якої зовнішньої системи." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Секрет підписання показується один раз, коли webhook створено. Повторно створіть джерело, щоб змінити його." + +[portal.sources.types.webhook.reveal] +copy = "Копіювати" +done = "Готово" +secret = "Секрет підписання" +secretHelp = "Підписуйте сире тіло кожної доставки цим ключем (HMAC-SHA256) і надсилайте його як заголовок X-Stirling-Signature." +secretWarning = "Скопіюйте секрет підписання зараз. З міркувань безпеки він показується лише один раз і не може бути отриманий пізніше." +title = "Webhook створено" +url = "Delivery URL" +usage = "POST кожен документ як сире тіло запиту на Delivery URL з binary content type (application/pdf або application/octet-stream). Політики, що посилаються на нього, автоматично запускаються після надходження." + [portal.sources.wizard] -back = "Назад" -cancel = "Скасувати" -continue = "Продовжити" -editTitle = "Редагувати джерело" name = "Назва" namePlaceholder = "наприклад, Прийом заяв" -save = "Зберегти зміни" -subtitle = "Крок {{current}} з {{total}} · {{label}}" -title = "Підключити джерело" type = "Тип" -[portal.sources.wizard.steps] -chooseType = "Вибрати тип" -configure = "Налаштувати" -review = "Переглянути й підключити" +[portal.stepModal] +close = "Закрити" [portal.usage] managePayment = "Керувати платежем" @@ -8021,16 +8962,9 @@ summary = "Володіє командою — керує ресурсами ї 2 = "Доступ до порталу через стандартну політику" 3 = "Усе, що може Учасник" -[portal.welcome] -ariaLabel = "Вітаємо в Stirling PDF" -badge = "Open-source" -installEditor = "Установити Редактор" -inviteTeammates = "Запросити колег" -openInBrowser = "Відкрити в браузері" -perks = "Безкоштовно назавжди · Можна self-host" -subtitle = "Найбезпечніший PDF Editor у світі безкоштовний для команд будь-якого розміру. Включає понад 60 операцій із PDF та SSO." -title = "Вітаємо в" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Без обмежень" [printFile] title = "Роздрукувати файл" @@ -8613,6 +9547,14 @@ text = "Поверніть сторінки PDF за або проти годи [rotate.tooltip.header] title = "Огляд налаштувань повороту" +[ruler] +clearAll = "Очистити все" +hideAllLabels = "Приховати всі мітки" +hideSmallLabels = "Приховати малі мітки" +physicalValues = "Фізичні" +scaled = "Масштабовані" +showAllLabels = "Показати всі мітки" + [sanitize] desc = "Видаліть потенційно шкідливі елементи з файлів PDF." sanitizationResults = "Результати санітизації" @@ -8654,31 +9596,45 @@ label = "Видалити метадані XMP" [sanitize.steps] settings = "Налаштування" -[sanitizePdf] -tags = "чистка,безпека,безпечні,віддалення загроз" - [sanitizePDF] title = "Дезінфекція PDF" +[sanitizePdf] +tags = "чистка,безпека,безпечні,віддалення загроз" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Відрегулювати масштаб сторінки" +[scaleSettings] +activeScale = "Активний масштаб" +apply = "Застосувати масштаб" +applyCalibration = "Застосувати калібрування" +calculatedScale = "Обчислений масштаб" +calibrate = "Калібрувати" +calibrating = "Калібрування" +calibrationDistanceRequired = "Введіть реальну відстань, більшу за нуль" +calibrationInvalid = "Не вдалося обчислити масштаб за цим вимірюванням" +calibrationPaperDistance = "Виміряна відстань на сторінці: {{distance}}" +calibrationTitle = "Калібрувати масштаб" +calibrationTooltip = "Виміряйте відому відстань, щоб автоматично обчислити масштаб" +customScale = "Користувацький масштаб" +noneSet = "Користувацький масштаб не встановлено" +presets = "Попередньо задані масштаби" +ratio = "Співвідношення масштабу" +ratioHelp = "Співвідношення: 1 одиниця сторінки = X реальних одиниць" +ratioPlaceholder = "наприклад, 100" +ratioPositive = "Співвідношення має бути більшим за нуль" +ratioRequired = "Потрібно вказати співвідношення" +realDistance = "Реальна відстань" +realDistancePlaceholder = "наприклад, 5" +reset = "Скинути до стандартних" +unit = "Одиниця" +unitInvalid = "Недійсна одиниця: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Пороговий кут:" -10 = "Встановлює розмір додаваної та видаляної рамки, щоб запобігти появі білих рамок на виході (за замовчуванням: 1)." -2 = "Встановлює мінімальний абсолютний кут, необхідний для повороту зображення (за замовчуванням: 10)." -3 = "Толерантність:" -4 = "Визначає діапазон зміни кольору навколо передбачуваного кольору фону (за замовчуванням: 30)." -5 = "Мінімальна площа:" -6 = "Встановлює мінімальний поріг площі для фотографії (за замовчуванням: 10000)." -7 = "Мінімальна площа контуру:" -8 = "Встановлює мінімальний поріг площі контуру для фотографії" -9 = "Розмір рамки:" - [scannerImageSplit] submit = "Витягнути скановані зображення" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Що це робить" whatThisDoesDesc = "Автоматично знаходить і витягує кожне фото зі сканованої сторінки або комбінованого зображення—без ручного кадрування." whenToUse = "Коли використовувати" +[ScannerImageSplit.selectText] +1 = "Пороговий кут:" +10 = "Встановлює розмір додаваної та видаляної рамки, щоб запобігти появі білих рамок на виході (за замовчуванням: 1)." +2 = "Встановлює мінімальний абсолютний кут, необхідний для повороту зображення (за замовчуванням: 10)." +3 = "Толерантність:" +4 = "Визначає діапазон зміни кольору навколо передбачуваного кольору фону (за замовчуванням: 30)." +5 = "Мінімальна площа:" +6 = "Встановлює мінімальний поріг площі для фотографії (за замовчуванням: 10000)." +7 = "Мінімальна площа контуру:" +8 = "Встановлює мінімальний поріг площі контуру для фотографії" +9 = "Розмір рамки:" + [search] noResults = "Нічого не знайдено" placeholder = "Введіть пошуковий запит..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Ваш сервер Stirling-PDF офлайн і \"{{ [session] expired = "Ваш сеанс закінчився. Будь ласка, оновіть сторінку та повторіть спробу." -[sessionManagement.tooltip] -header = "Керування сеансами підписання" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Нових учасників додають у кінець порядку підписання" -bullet2 = "Неможливо додати учасників після завершення сеансу" -bullet3 = "Кожен учасник отримує сповіщення, коли настає його черга" -description = "Ви можете додати більше учасників до активного сеансу в будь-який час до завершення." -title = "Додавання учасників" - -[sessionManagement.tooltip.finalization] -bullet1 = "Повне завершення: Усі учасники підписали" -bullet2 = "Часткове завершення: Деякі учасники ще не підписали" -bullet3 = "Непідписані учасники будуть виключені з остаточного документа" -bullet4 = "Після завершення ви можете завантажити підписаний PDF до активних файлів" -description = "Завершення об’єднує всі підписи в один підписаний PDF. Цю дію неможливо скасувати." -title = "Завершення сеансу" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Неможливо видалити учасників, які вже підписали" -bullet2 = "Видалені учасники більше не отримуватимуть сповіщень" -bullet3 = "Порядок підписання коригується автоматично" -description = "Учасників можна видалити із сеансів до того, як вони підпишуть." -title = "Видалення учасників" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Кожен підпис застосовується до PDF послідовно" -bullet2 = "Наступні учасники можуть бачити попередні підписи" -bullet3 = "Важливо для процесів затвердження та юридичних ланцюгів збереження доказів" -description = "Порядок, який ви вказуєте під час створення сеансу, визначає, хто підписує першим." -title = "Порядок підписів" - [settings] close = "Закрити" +[settings.ai] +documents = "Документи й RAG" +general = "Загальні" +limits = "Обмеження та продуктивність" +models = "Моделі та провайдери" +title = "AI" + [settings.configuration] advanced = "Додатково" database = "База даних" endpoints = "Кінцеві точки" features = "Функції" +folderAccess = "Доступ до папок" mcp = "MCP Server" storageSharing = "Зберігання файлів і спільний доступ" systemSettings = "Системні налаштування" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "PDF-редактор за замовчуванням" defaultPdfEditorActive = "Stirling PDF — ваш PDF-редактор за замовчуванням" defaultPdfEditorChecking = "Перевірка..." defaultPdfEditorInactive = "Інша програма встановлена як типова" +defaultPdfEditorRemind = "Нагадувати встановити за замовчуванням" +defaultPdfEditorRemindDescription = "Показувати банер, коли Stirling PDF не є вашим PDF-застосунком за замовчуванням." defaultPdfEditorSet = "Вже типовий" defaultStartupView = "Типовий вигляд під час запуску" defaultStartupViewDescription = "Виберіть, яка вкладка буде активною в лівій колонці під час запуску застосунку" @@ -8817,9 +9763,18 @@ languageDescription = "Виберіть мову інтерфейсу" logout = "Вийти" setAsDefault = "Зробити типовим" theme = "Тема" +themeAccent = "Акцентний колір" +themeAccentCustom = "Користувацький колір" +themeAccentCustomHint = "Користувацький (рекомендовано стандартні)" +themeAccentDark = "Акцентний колір темного режиму" +themeAccentDefault = "Стандартний" +themeAccentDefaultHint = "Стандартна тема (рекомендовано)" +themeAccentDescription = "Кнопки, посилання та виділення використовують його, і він ледь помітно забарвлює застосунок. Світлий і темний режими мають власні. Системний режим використовує активний варіант." +themeAccentLight = "Акцентний колір світлого режиму" themeDark = "Темна" themeDescription = "Перемикання між світлим і темним режимами" themeLight = "Світла" +themeReset = "Відновити стандартну тему" themeSystem = "System" title = "Загальні" user = "Користувач" @@ -8833,6 +9788,12 @@ intro = "Увімкніть автентифікацію користувачі learnMore = "Докладніше в документації" title = "Для системних адміністраторів" +[settings.general.loginLanding] +description = "Виберіть, який застосунок відкриватиметься після входу в Stirling Cloud." +editor = "Редактор" +processor = "Обробник" +title = "Після входу" + [settings.general.mode] fullscreen = "Повноекранний" sidebar = "Бічна панель" @@ -9013,9 +9974,6 @@ title = "Політики та конфіденційність" [settings.preferences] title = "Параметри" -[settings.search] -placeholder = "Пошук сторінок налаштувань..." - [settings.security] description = "Оновіть свій пароль, щоб убезпечити обліковий запис." title = "Безпека" @@ -9041,6 +9999,11 @@ security = "Безпека" telegram = "Telegram" title = "Безпека та автентифікація" +[settings.signOut] +confirm = "Ви впевнені, що хочете вийти?" +submit = "Вийти" +title = "Вихід" + [settings.team] title = "Команда" @@ -9156,6 +10119,7 @@ last = "Остання сторінка" maintainRatio = "Переключити збереження пропорцій" next = "Наступна сторінка" noSavedSigs = "Збережені підписи не знайдено" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Особисті підписи" previous = "Попередня сторінка" redo = "Повторити" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Не вдалося прибрати фон" hint = "Завантажте зображення підпису у форматі PNG або JPG" label = "Завантажити зображення підпису" placeholder = "Виберіть файл зображення" +previewAlt = "Current image signature" processing = "Обробка зображення..." removeBackground = "Прибрати білий фон (зробити прозорим)" @@ -9207,6 +10172,17 @@ saved = "Виберіть збережений підпис вище, потім text = "Після введення імені вище клацніть будь-де по PDF, щоб розмістити підпис." title = "Як додати підпис" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Перемістити підпис" pause = "Призупинити розміщення" @@ -9653,10 +10629,30 @@ title = "Завантажити на сервер" updateButton = "Оновити на сервері" uploadButton = "Завантажити на сервер" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "натисніть тут" +fallback = "Якщо він не відкрився автоматично," +opening = "Відкриття Swagger UI у новій вкладці..." tags = "api,documentation,swagger,endpoints,development" title = "Документація API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Виберіть файли в Активних файлах, singleFileScope = "Застосовується лише до: {{fileName}}" viewerMode = "Перейдіть до редактора файлів, щоб вибрати кілька файлів." +[toolFormat] +ANY = "будь-який файл" +CBR = "архів коміксів (CBR)" +CBZ = "архів коміксів (CBZ)" +CSV = "CSV-файл" +EBOOK = "електронна книга" +EMAIL = "електронний лист" +EXCEL = "електронна таблиця" +HTML = "вебсторінка" +IMAGE = "зображення" +JAVASCRIPT = "файл JavaScript" +JSON = "JSON-файл" +MARKDOWN = "файл Markdown" +NONE = "немає файлу" +PCL = "файл друку PCL" +PDF = "PDF" +PDF_ENCRYPTED = "захищений паролем PDF" +POSTSCRIPT = "файл PostScript" +PPT = "презентація" +TEXT = "текстовий файл" +VIDEO = "відео" +WORD = "документ Word" +XML = "XML-файл" +XPS = "документ XPS" +ZIP = "ZIP-архів" + [toolPanel] alpha = "Альфа" backToAllTools = "Назад до всіх інструментів" collapse = "Згорнути панель" expand = "Розгорнути панель" goBack = "Повернутися" +pdfTools = "Інструменти PDF" placeholder = "Виберіть інструмент, щоб почати" premiumFeature = "Преміум-функція:" toolsHeader = "Інструменти" @@ -9806,11 +10829,6 @@ noToolsFound = "Інструментів не знайдено" quickAccess = "ШВИДКИЙ ДОСТУП" searchPlaceholder = "Пошук інструментів..." -[toolPicker.categories] -advancedTools = "Розширені інструменти" -recommendedTools = "Рекомендовані інструменти" -standardTools = "Стандартні інструменти" - [toolPicker.subcategories] advancedFormatting = "Розширене форматування" automation = "Автоматизація" @@ -9824,6 +10842,9 @@ removal = "Видалення" signing = "Підписання" verification = "Перевірка" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Інструментів не знайдено" noTools = "Немає доступних інструментів" @@ -9950,28 +10971,53 @@ title = "Користувачі" you = "(ви)" [users.action] +cancelInvite = "Скасувати запрошення" deleteTeam = "Видалити команду" disableMfa = "Скинути MFA" move = "Перемістити до команди" reinstate = "Відновити" remove = "Видалити з організації" +removeTeam = "Вилучити з команди" rename = "Перейменувати команду" resetPw = "Скинути пароль" suspend = "Призупинити" unlock = "Розблокувати обліковий запис" +[users.activity] +daysAgo = "{{count}} дн. тому" +hoursAgo = "{{count}} год тому" +justNow = "Щойно" +minutesAgo = "{{count}} хв тому" +monthsAgo_one = "{{count}} місяць тому" +monthsAgo_other = "{{count}} місяців тому" +never = "Ніколи" +weeksAgo_one = "{{count}} тиждень тому" +weeksAgo_other = "{{count}} тижнів тому" +yearsAgo_one = "{{count}} рік тому" +yearsAgo_other = "{{count}} років тому" + [users.cap] addProcessor = "+ Processor" approver = "Схвалює політику" editor = "Редактор" processor = "Обробник" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Скасувати запрошення для {{email}}? Вони не зможуть приєднатися за поточним посиланням." +cancelInviteTitle = "Скасувати запрошення" deleteTeamBody = "Видалити команду {{name}}? Спершу команда має бути порожньою — перемістіть її учасників до іншої команди, і вона не повинна все ще володіти жодними конфігураціями інтеграцій." deleteTeamTitle = "Видалити команду" disableMfaBody = "Видалити реєстрацію MFA для {{name}}? Якщо потрібно, вони налаштують її знову під час наступного входу." disableMfaTitle = "Скинути MFA" removeBody = "Назавжди видалити {{name}} з організації? Цю дію не можна скасувати." +removeTeamBody = "Вилучити {{name}} з команди? Вони збережуть свій обліковий запис, але втратять доступ до ресурсів цієї команди." removeTitle = "Видалити учасника" [users.empty] @@ -9982,10 +11028,8 @@ title = "Учасників ще немає" addToTeam = "Додати до команди" guestCount = "{{count}} гість" guests = "Гості" -guestsDesc = "Зовнішні співробітники, обмежені тим, чим ви поділилися. Лише Редактор." ledBy = "під керівництвом {{owner}}" org = "Організація" -orgDesc = "Власники з повноваженнями на рівні всієї організації та схваленням політик" owners = "{{count}} власник" team = "Команда {{name}}" teamMeta = "{{count}} людей" @@ -10022,6 +11066,18 @@ username = "Ім’я користувача" usernameError = "Ім’я користувача має містити щонайменше 3 символи" usernamePlaceholder = "jsmith" +[users.invites] +by = "Запросив(ла) {{who}}" +cancel = "Скасувати" +expiresInDays_one = "Закінчується через {{count}} день" +expiresInDays_other = "Закінчується через {{count}} днів" +expiresToday = "Закінчується сьогодні" +title = "Запрошення, що очікують" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Сталася помилка під час звернення до бекенду, або у вас немає доступу. Спробуйте ще раз." title = "Не вдалося завантажити учасників" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Пошук вкладень" title = "Вкладення" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Назва закладки" +bookmarkTitleRequired = "Потрібно вказати назву закладки" closeSidebar = "Закрити бічну панель закладок" collapseAll = "Згорнути всі закладки" +empty = "No bookmarks in this document" expandAll = "Розгорнути всі закладки" +searchPlaceholder = "Пошук закладок" +title = "Bookmarks" [viewer.comments] addComment = "Додати коментар" @@ -10236,10 +11297,12 @@ locateAnnotation = "Знайти в документі" moreActions = "Додаткові дії" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Сторінка {{page}}" placingHint = "Клацніть сторінку, щоб розмістити… (скасувати)" removeCommentOnly = "Прибрати лише коментар" saveReply = "Зберегти відповідь" +searchPlaceholder = "Search comments" title = "Коментарі" typeComment = "Коментар" typeInsertText = "Вставити текст" @@ -10262,6 +11325,7 @@ unsavedDesc = "У вас є незбережені зміни" closeSidebar = "Закрити бічну панель шарів" hideAll = "Приховати всі шари" showAll = "Показати всі шари" +title = "Layers" [viewer.link] delete = "Видалити посилання" @@ -10605,30 +11669,6 @@ title = "Текст водяного знака" image = "Зображення" text = "Текст" -[wetSignature.tooltip] -header = "Способи створення підпису" - -[wetSignature.tooltip.draw] -bullet1 = "Налаштуйте колір і товщину пера" -bullet2 = "Очищуйте та перемальовуйте, доки не будете задоволені" -bullet3 = "Працює на сенсорних пристроях (планшети, телефони)" -description = "Створіть рукописний підпис за допомогою миші або сенсорного екрана. Найкраще підходить для особистих, автентичних підписів." -title = "Намалювати підпис" - -[wetSignature.tooltip.type] -bullet1 = "Виберіть один із кількох шрифтів" -bullet2 = "Налаштуйте розмір і колір тексту" -bullet3 = "Ідеально для стандартизованих підписів" -description = "Згенеруйте підпис із введеного тексту. Швидко та уніфіковано, підходить для ділових документів." -title = "Ввести підпис" - -[wetSignature.tooltip.upload] -bullet1 = "Підтримує PNG, JPG та інші формати зображень" -bullet2 = "Для найкращих результатів рекомендовано прозоре тло" -bullet3 = "Зображення буде змінено за розміром під область підпису" -description = "Завантажте заздалегідь створене зображення підпису. Ідеально, якщо у вас є відсканований підпис або логотип компанії." -title = "Завантажити зображення підпису" - [workbenchBar] activeFiles = "Активні файли" annotations = "Анотації" @@ -10642,6 +11682,7 @@ exitRedaction = "Вийти з режиму затемнення" exportAll = "Експортувати PDF" exportSelected = "Експортувати вибрані сторінки" formFill = "Заповнити форму" +hideToolbar = "Hide toolbar" multiTool = "Мультиінструмент" panMode = "Режим переміщення" print = "Надрукувати PDF" @@ -10652,6 +11693,7 @@ redact = "Затемнити" rotateLeft = "Повернути ліворуч" rotateRight = "Повернути праворуч" ruler = "Лінійка / Вимірювання" +rulerSettings = "Налаштування масштабу" save = "Зберегти" saveAll = "Зберегти всі" saveAs = "Зберегти як" @@ -10661,6 +11703,7 @@ selectAll = "Вибрати все" selectByNumber = "Вибрати за номерами сторінок" selectLanguage = "Вибрати мову" share = "Надати спільний доступ" +showToolbar = "Show toolbar" toggleAnnotations = "Перемкнути видимість анотацій" toggleAttachments = "Перемкнути вкладення" toggleBookmarks = "Перемкнути закладки" diff --git a/frontend/editor/public/locales/vi-VN/translation.toml b/frontend/editor/public/locales/vi-VN/translation.toml index 9f6861ecbe..4ff67c254b 100644 --- a/frontend/editor/public/locales/vi-VN/translation.toml +++ b/frontend/editor/public/locales/vi-VN/translation.toml @@ -1,7 +1,6 @@ alphabet = "Bảng chữ cái" apply = "Áp dụng" applyAndContinue = "Lưu và rời đi" -areYouSure = "Bạn có chắc muốn rời đi không?" back = "Quay lại" black = "Đen" blue = "Xanh dương" @@ -46,10 +45,8 @@ filesSelected = "tệp đã chọn" font = "Phông chữ" fontSizeTooltip = "Kích thước chữ số trang theo point. Số lớn hơn sẽ tạo chữ lớn hơn." fontTypeTooltip = "Họ phông chữ cho số trang. Chọn theo phong cách tài liệu của bạn." -genericSubmit = "Gửi" green = "Xanh lá" help = "Trợ giúp" -imgPrompt = "Chọn (các) hình ảnh" incorrectPasswordMessage = "Mật khẩu hiện tại không chính xác." info = "Thông tin" invalidUndoData = "Không thể hoàn tác: dữ liệu thao tác không hợp lệ" @@ -62,7 +59,6 @@ loginAgreementTitle = "Thỏa thuận đăng nhập" logOut = "Đăng xuất" marginTooltip = "Khoảng cách giữa số trang và mép trang." moreOptions = "Tùy chọn khác" -multiPdfPrompt = "Chọn các tệp PDF (2+)" never = "Không bao giờ" no = "Không" noFileSelected = "Chưa chọn tệp. Vui lòng tải lên một tệp." @@ -80,7 +76,6 @@ pageOrderPrompt = "Thứ tự trang tùy chỉnh (Nhập danh sách số trang pages = "Trang" pageSelectionPrompt = "Lựa chọn trang tùy chỉnh (Nhập danh sách số trang được phân tách bằng dấu phẩy 1,5,6 hoặc Các hàm như 2n+1) :" password = "Mật khẩu" -pdfPrompt = "Chọn (các) tệp PDF" pin = "Ghim tệp (giữ hoạt động sau khi chạy công cụ)" pro = "Pro" processingComplete = "Tệp của bạn đã sẵn sàng." @@ -110,6 +105,7 @@ undoSuccess = "Hoàn tác thành công" unknown = "Không xác định" unpin = "Bỏ ghim tệp (thay thế sau khi chạy công cụ)" unsavedChanges = "Bạn có thay đổi chưa lưu đối với PDF." +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "Thay đổi chưa lưu" unsupported = "Không được hỗ trợ" username = "Tên người dùng" @@ -356,6 +352,7 @@ text = "Các quyền này kiểm soát những gì người dùng có thể làm title = "Thay đổi quyền" [addStamp] +preview = "Xem trước Dấu đóng" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "Tải xuống thủ công: nhấp vào các liên kết và đặ noLanguages = "Không tìm thấy ngôn ngữ tessdata trong thư mục đã cấu hình." permissionNotice = "Đường dẫn tessdata không thể ghi. Tệp tải xuống sẽ được mở trong trình duyệt; vui lòng lưu các tệp .traineddata thủ công vào thư mục tessdata." +[admin.settings.ai.documents] +description = "Cấu hình mô hình embedding và cài đặt truy xuất dùng để trả lời câu hỏi trên tài liệu. Được áp dụng cho AI engine khi lưu." +title = "Tài liệu & RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "Để trống để dùng thông tin xác thực môi trường riêng của engine. Áp dụng cho các triển khai tự lưu trữ một engine." +generic = "Embedding API key" +openai = "OpenAI API key" +setPlaceholder = "Đã lưu - để trống để giữ key hiện tại" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "Base URL của endpoint embedding tương thích OpenAI / Ollama, ví dụ http://ollama:11434/v1. Phải trỏ tới endpoint nội bộ đáng tin cậy (nhạy cảm với SSRF)." +label = "Embedding base URL" + +[admin.settings.ai.documents.embeddingModel] +description = "Tên mô hình embedding. Văn bản tự do; gợi ý chỉ mang tính tham khảo." +label = "Mô hình embedding" + +[admin.settings.ai.documents.embeddingProvider] +description = "Nhà cung cấp dùng để chuyển văn bản tài liệu thành vector embedding." +label = "Nhà cung cấp embedding" + +[admin.settings.ai.documents.maxSearches] +description = "Số lần tìm kiếm truy xuất tối đa mà agent có thể chạy cho mỗi yêu cầu." +label = "Số tìm kiếm tối đa" + +[admin.settings.ai.documents.reindexNote] +body = "Việc thay đổi mô hình embedding có hiệu lực ngay lập tức, nhưng các tài liệu đã được lập chỉ mục bằng mô hình trước đó phải được lập chỉ mục lại để tìm kiếm trả về kết quả chính xác." +title = "Cần lập chỉ mục lại" + +[admin.settings.ai.documents.saved] +reindexNote = "Nếu bạn đã thay đổi mô hình embedding, hãy lập chỉ mục lại các tài liệu hiện có để tìm kiếm dùng mô hình mới." + +[admin.settings.ai.documents.topK] +description = "Số đoạn liên quan nhất được truy xuất cho mỗi lần tìm kiếm." +label = "Top K" + +[admin.settings.ai.general] +description = "Kết nối Stirling với Python AI engine và chọn các khả năng AI được hiển thị. Thay đổi áp dụng khi khởi động lại." +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "Bật hoặc tắt từng tính năng AI. Các tính năng bị tắt sẽ bị ẩn trong ứng dụng." +title = "Khả năng" + +[admin.settings.ai.general.enabled] +description = "Công tắc chính. Khi tắt, không có công cụ AI, agent hoặc lệnh gọi engine nào khả dụng." +label = "Bật AI" + +[admin.settings.ai.general.features.chat] +description = "Trợ lý hội thoại để làm việc với PDF." +label = "Trợ lý chat" + +[admin.settings.ai.general.features.classify] +description = "Tự động phân loại tài liệu theo loại hoặc nội dung." +label = "Phân loại tài liệu" + +[admin.settings.ai.general.features.createPdf] +description = "Tạo PDF mới (ví dụ từ HTML) thông qua AI agent." +label = "Tạo PDF từ prompt" + +[admin.settings.ai.general.features.documentQuestions] +description = "Đặt câu hỏi và nhận câu trả lời dựa trên tài liệu đã tải lên." +label = "Câu hỏi về tài liệu" + +[admin.settings.ai.general.features.mathAuditor] +description = "Rà soát tài liệu để tìm lỗi toán học và số liệu." +label = "Kiểm toán toán học" + +[admin.settings.ai.general.features.pdfComment] +description = "Thêm nhận xét đánh giá và chú thích do AI viết vào PDF." +label = "PDF comment agent" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "Thời gian chờ cho các thao tác agent nặng hơn như tạo tài liệu." +label = "Thời gian chờ tác vụ dài (giây)" + +[admin.settings.ai.general.note] +body = "AI engine chạy như một dịch vụ riêng biệt. Bí mật dùng chung của nó" +body2 = "được đặt qua biến môi trường container. Provider API key có thể được nhập trên các trang này hoặc cung cấp dưới dạng biến môi trường của engine; các key nhập tại đây sẽ được đẩy tới engine khi lưu." +title = "Giới thiệu về AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "Thời gian chờ cho phản hồi chat được stream (từng token)." +label = "Thời gian chờ stream (giây)" + +[admin.settings.ai.general.test] +button = "Kiểm tra kết nối" +failBody = "AI engine không phản hồi. Kiểm tra URL, đảm bảo container engine đang chạy và AI đã được bật (cần khởi động lại sau khi bật)." +failTitle = "Không thể truy cập AI engine" +okBody = "AI engine đã phản hồi kiểm tra trạng thái." +okTitle = "Có thể truy cập AI engine" + +[admin.settings.ai.general.timeoutSeconds] +description = "Thời gian chờ cho các yêu cầu AI tiêu chuẩn tới engine." +label = "Thời gian chờ yêu cầu (giây)" + +[admin.settings.ai.general.url] +description = "URL nội bộ của Python AI engine, ví dụ http://stirling-pdf-engine:5001." +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "Giới hạn bảo vệ cho khối lượng công việc mà yêu cầu AI có thể thực hiện và số lượng chạy đồng thời. Được áp dụng cho AI engine khi lưu." +title = "Giới hạn & Hiệu năng" + +[admin.settings.ai.limits.maxCharacters] +description = "Giới hạn bảo vệ: từ chối yêu cầu AI nếu văn bản được trích xuất vượt quá độ dài này." +label = "Số ký tự tối đa mỗi yêu cầu" + +[admin.settings.ai.limits.maxPages] +description = "Giới hạn bảo vệ: từ chối yêu cầu AI vượt quá số trang PDF này." +label = "Số trang tối đa mỗi yêu cầu" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "Số lệnh gọi mô hình đang xử lý đồng thời tối đa trên toàn bộ engine." +label = "Số đồng thời tối đa của mô hình" + +[admin.settings.ai.models] +description = "Chọn nhà cung cấp LLM và các mô hình thông minh/nhanh mà AI engine sử dụng. Được áp dụng cho AI engine khi lưu." +title = "Mô hình & Nhà cung cấp" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "Để trống để dùng thông tin xác thực môi trường riêng của engine. Áp dụng cho các triển khai tự lưu trữ một engine." +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "Đã lưu - để trống để giữ key hiện tại" + +[admin.settings.ai.models.baseUrl] +description = "Base URL của endpoint tương thích OpenAI / Ollama, ví dụ http://ollama:11434/v1." +label = "Provider base URL" +warning = "Base URL phải trỏ tới endpoint nội bộ đáng tin cậy. Engine sẽ thực hiện các yêu cầu phía máy chủ tới đó, vì vậy giá trị không đáng tin cậy sẽ nhạy cảm với SSRF." + +[admin.settings.ai.models.fastMaxTokens] +description = "Số token đầu ra tối đa cho mô hình nhanh." +label = "Số token tối đa của mô hình nhanh" + +[admin.settings.ai.models.fastModel] +description = "Mô hình rẻ hơn, nhanh hơn cho các tác vụ nhẹ. Văn bản tự do; gợi ý chỉ mang tính tham khảo." +label = "Mô hình nhanh" + +[admin.settings.ai.models.provider] +description = "Nhà cung cấp LLM mà engine giao tiếp." +label = "Nhà cung cấp" + +[admin.settings.ai.models.smartMaxTokens] +description = "Số token đầu ra tối đa cho mô hình thông minh." +label = "Số token tối đa của mô hình thông minh" + +[admin.settings.ai.models.smartModel] +description = "Mô hình năng lực cao cho suy luận phức tạp. Văn bản tự do; gợi ý chỉ mang tính tham khảo." +label = "Mô hình thông minh" + +[admin.settings.ai.saved] +body = "Thay đổi được tự động đẩy tới AI engine." +bodyNoPush = "Đã lưu cài đặt. Chúng sẽ được áp dụng vào lần tới khi AI engine nhận cấu hình." +title = "Đã lưu cài đặt AI" + [admin.settings.badge] clickToUpgrade = "Nhấp để xem chi tiết gói" @@ -938,6 +1094,25 @@ label = "Tạo lại khi khởi động" description = "Số ngày chứng chỉ còn hiệu lực" label = "Hiệu lực chứng chỉ (ngày)" +[admin.settings.folderAccess] +description = "Các thư mục mà nguồn thư mục và đầu ra thư mục được phép đọc và ghi. Đây là ranh giới bảo mật: tự động hóa không bao giờ có thể được trỏ tới đường dẫn máy chủ nằm ngoài danh sách này." +securityNote = "Để trống mục này để tắt hoàn toàn nguồn thư mục và đầu ra thư mục. Thư mục cấu hình riêng của Stirling luôn bị cấm truy cập." +title = "Quyền truy cập thư mục" + +[admin.settings.folderAccess.implied] +description = "Các thư mục do Stirling quản lý này luôn được phép và không thể thay đổi tại đây." +serverStorage = "Lưu trữ tệp máy chủ" +title = "Luôn được phép" +watchedFolder = "Thư mục được Pipeline theo dõi" + +[admin.settings.folderAccess.roots] +add = "Thêm" +empty = "Không có thư mục nào được phép. Nguồn thư mục và đầu ra hiện đang bị tắt." +hint = "Nhập đường dẫn tuyệt đối, ví dụ /data/inbox." +label = "Gốc thư mục được phép" +placeholder = "/data/inbox" +remove = "Xóa gốc thư mục" + [admin.settings.general] description = "Cấu hình cài đặt hệ thống cho ứng dụng, bao gồm thương hiệu và hành vi mặc định." system = "Hệ thống" @@ -1704,6 +1879,9 @@ width = "Độ rộng" [app] description = "Giải pháp thay thế Adobe Acrobat miễn phí (10M+ lượt tải xuống)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "Chuyển sang PDF/A-3b" convertToPdfA3bDescription = "Tạo PDF lưu trữ với tệp đính kèm được nhúng" @@ -1842,6 +2020,15 @@ export = "Xuất" accessDenied = "Từ chối truy cập" insufficientPermissions = "Bạn không có quyền thực hiện thao tác này." +[auth.callback] +completing = "Đang hoàn tất xác thực" +invalidToken = "Đăng nhập OAuth thất bại - token không hợp lệ." +missingToken = "Đăng nhập OAuth thất bại - không nhận được token." +oauthFailed = "Đăng nhập OAuth thất bại. Vui lòng thử lại." +pleaseWait = "Vui lòng chờ trong khi chúng tôi hoàn tất đăng nhập cho bạn." +signedOut = "Bạn đã đăng xuất. Vui lòng đăng nhập lại." +windowMayClose = "Bạn có thể đóng cửa sổ này khi hoàn tất." + [auth.displayName] guest = "Khách" user = "Người dùng" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "Bảo vệ tài liệu PDF bằng cách loại bỏ nội description = "Công cụ này sẽ tự động đổi tên tệp PDF dựa trên nội dung của chúng. Nó phân tích tài liệu để tìm tiêu đề phù hợp nhất từ văn bản." tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "Tự động xoay" +tags = "auto rotate,sửa hướng,bị lộn ngược,nằm ngang,làm thẳng,định hướng,phát hiện hướng,sửa xoay,tự động định hướng,đã quét,OSD" + +[autoRotate.confidenceThreshold] +title = "Ngưỡng tin cậy" + +[autoRotate.detectionMode] +auto = "Tự động" +osd = "Chỉ OCR" +text = "Chỉ văn bản" +title = "Phương pháp phát hiện" + +[autoRotate.error] +failed = "Đã xảy ra lỗi khi tự động xoay PDF." + +[autoRotate.inferUndetected] +title = "Xoay các trang có độ tin cậy thấp để khớp" + +[autoRotate.report] +applied = "{{degrees}}° theo chiều kim đồng hồ" +confidenceValue = "Độ tin cậy {{value}}" +noChange = "Không thay đổi" +pageLabel = "Trang {{number}}" +summary = "Đã xoay {{rotated}} trên {{total}} trang, giữ nguyên {{unchanged}} trang" +title = "Báo cáo phát hiện" + +[autoRotate.report.method] +inferred = "Đã khớp" +none = "Đã bỏ qua" +osd = "OCR" +text = "Văn bản" + +[autoRotate.report.note] +belowThreshold = "Dưới ngưỡng" +blankPage = "Trang trống" +inferredFromDocument = "Giống các trang khác" +noDominantDirection = "Hướng văn bản hỗn hợp" +osdFailed = "Không tìm thấy văn bản đọc được" +osdNoVerdict = "Không xác định được chiều đúng" +tesseractUnavailable = "OCR chưa được cài đặt" +tooFewGlyphs = "Quá ít văn bản" + +[autoRotate.results] +title = "Kết quả tự động xoay" + +[autoRotate.settings] +title = "Cài đặt" + +[autoRotate.tooltip.confidenceThreshold] +text = "Mức độ chắc chắn cần có trước khi xoay một trang. Giảm để xoay nhiều trang hơn, tăng để giữ nguyên nhiều trang hơn." +title = "Ngưỡng tin cậy" + +[autoRotate.tooltip.description] +text = "Xác định chiều đúng của từng trang và xoay về đúng hướng. Các trang đã đúng chiều sẽ được giữ nguyên." + +[autoRotate.tooltip.detectionMode] +text = "Tự động đọc văn bản trên trang trước, sau đó xem trang như hình ảnh nếu chưa đủ. Chỉ văn bản bỏ qua kiểm tra hình ảnh, nhanh nhất cho tài liệu chưa từng được quét. Chỉ OCR xử lý mọi trang như hình ảnh." +title = "Phương pháp phát hiện" + +[autoRotate.tooltip.header] +title = "Tự động xoay" + +[autoRotate.tooltip.inferUndetected] +text = "Các trang trống hoặc gần như trống không thể tự kiểm tra. Khi mọi trang đọc được đều cần xoay cùng một hướng, các trang này sẽ được xoay để khớp với chúng." +title = "Xoay các trang có độ tin cậy thấp để khớp" + [autoSizeSplitPDF] tags = "pdf,chia,tài liệu,tổ chức" @@ -2189,33 +2443,6 @@ keywords = "Từ khóa: odd, even" numbers = "Số/khoảng: 5, 10-20" progressions = "Cấp số: 3n, 4n+1" -[certificateChoice.tooltip] -header = "Các loại chứng chỉ" - -[certificateChoice.tooltip.organization] -bullet1 = "Được quản lý bởi quản trị viên hệ thống" -bullet2 = "Chia sẻ cho người dùng được ủy quyền" -bullet3 = "Đại diện cho danh tính công ty, không phải cá nhân" -bullet4 = "Phù hợp cho: Tài liệu chính thức, chữ ký nhóm" -description = "Chứng chỉ dùng chung do tổ chức của bạn cung cấp. Dùng cho quyền ký ở cấp độ toàn công ty." -title = "Chứng chỉ tổ chức" - -[certificateChoice.tooltip.personal] -bullet1 = "Tự động tạo khi lần đầu sử dụng" -bullet2 = "Gắn với tài khoản người dùng của bạn" -bullet3 = "Không thể chia sẻ với người dùng khác" -bullet4 = "Phù hợp cho: Tài liệu cá nhân, trách nhiệm cá nhân" -description = "Chứng chỉ được tự động tạo, duy nhất cho tài khoản của bạn. Phù hợp với chữ ký cá nhân." -title = "Chứng chỉ cá nhân" - -[certificateChoice.tooltip.upload] -bullet1 = "Yêu cầu tệp P12/PFX và mật khẩu" -bullet2 = "Có thể do Tổ chức cấp Chứng chỉ bên ngoài cấp" -bullet3 = "Mức độ tin cậy cao hơn cho tài liệu pháp lý" -bullet4 = "Phù hợp cho: Hợp đồng có giá trị pháp lý, xác thực bên ngoài" -description = "Sử dụng tệp chứng chỉ PKCS#12 của bạn. Cung cấp toàn quyền kiểm soát thuộc tính chứng chỉ." -title = "Tải lên P12 tùy chỉnh" - [certSign] awaitingSignatures = "Đang chờ chữ ký" chooseCertificate = "Chọn tệp chứng chỉ" @@ -2542,34 +2769,6 @@ server = "Máy chủ" stepTitle = "Nguồn chứng chỉ" upload = "Tải lên" -[certSign.tooltip.header] -title = "Giới thiệu về quản lý chữ ký" - -[certSign.tooltip.overview] -bullet1 = "Kiểm tra chữ ký hiện có và tính hợp lệ của chúng" -bullet2 = "Xem thông tin chi tiết về người ký và chứng chỉ" -bullet3 = "Thêm chữ ký số mới để bảo vệ tài liệu của bạn" -bullet4 = "Hỗ trợ nhiều tệp với điều hướng dễ dàng" -text = "Công cụ này cho phép bạn kiểm tra PDF có được ký số không và thêm chữ ký số mới. Chữ ký số chứng minh ai đã tạo hoặc phê duyệt tài liệu và cho thấy tài liệu có bị thay đổi sau khi ký không." -title = "Công cụ này làm gì?" - -[certSign.tooltip.signing] -bullet1 = "Hỗ trợ định dạng PEM, PKCS12, JKS và chứng chỉ máy chủ" -bullet2 = "Tùy chọn hiển thị hoặc ẩn chữ ký trên PDF" -bullet3 = "Thêm lý do, địa điểm và tên người ký" -bullet4 = "Chọn trang để đặt chữ ký hiển thị" -bullet5 = "Dùng chứng chỉ máy chủ cho tùy chọn 'Sign with Stirling-PDF' đơn giản" -text = "Để ký PDF, bạn cần chứng chỉ số (như PEM, PKCS12 hoặc JKS). Bạn có thể chọn hiển thị chữ ký trên tài liệu hoặc giữ ẩn chỉ vì mục đích bảo mật." -title = "Thêm chữ ký" - -[certSign.tooltip.validation] -bullet1 = "Hiển thị chữ ký hợp lệ hoặc không hợp lệ" -bullet2 = "Hiển thị thông tin người ký và ngày ký" -bullet3 = "Kiểm tra tài liệu có bị chỉnh sửa sau khi ký không" -bullet4 = "Có thể dùng chứng chỉ tùy chỉnh để xác minh" -text = "Khi bạn kiểm tra chữ ký, công cụ sẽ cho biết chúng có hợp lệ không, ai đã ký tài liệu, khi nào ký và liệu tài liệu có bị thay đổi sau khi ký không." -title = "Kiểm tra chữ ký" - [changeCreds] changeUsername = "Cập nhật tên người dùng. Bạn sẽ bị đăng xuất sau khi cập nhật." credsUpdated = "Đã cập nhật tài khoản" @@ -2737,12 +2936,16 @@ text = "Để các quyền này không bị thay đổi, hãy dùng công cụ T [chat.actions] copy = "Sao chép tin nhắn" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Tùy chọn tác nhân Stirling" +assistant = "AI Assistant" clearChat = "Xóa trò chuyện" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "Thao tác này sẽ sử dụng tín dụng đám mây của bạn" tooltip = "Lấy màu từ màn hình" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "Chọn màu" [common] +actions = "Actions" back = "Quay lại" cancel = "Hủy" close = "Đóng" +codeSample = "Code sample" collapse = "Thu gọn" confirm = "Xác nhận" continue = "Tiếp tục" copied = "Đã sao chép!" copy = "Sao chép" done = "Xong" +enterValue = "Enter value" error = "Lỗi" expand = "Mở rộng" loading = "Đang tải..." next = "Tiếp" +open = "Open" preview = "Xem trước" previous = "Trước" refresh = "Làm mới" +remaining = "Còn lại" retry = "Thử lại" save = "Lưu" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "Xóa lựa chọn" @@ -2988,6 +3199,7 @@ title = "Phương pháp nén" [compress.settings] desiredSize = "Kích thước tệp mong muốn" desiredSizePlaceholder = "Nhập kích thước" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "Nén là cách đơn giản để giảm kích thước tệp. Chọn Kích thước tệp để nhập kích thước mục tiêu và chúng tôi sẽ điều chỉnh chất lượng cho bạn. Chọn Chất lượng để tự đặt mức nén thủ công." @@ -3190,6 +3402,24 @@ pdfOptions = "Tùy chọn PDF" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "Chọn định dạng nguồn trước" settings = "Cài đặt" single = "Một tệp" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog Analytics" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "Giám sát nâng cao" -allInOneWorkspace = "Không gian làm việc PDF tất cả trong một (viewer, tools & agent)" -apiSandbox = "API sandbox" -dedicatedSupportSlas = "Hỗ trợ riêng & SLA" -fasterThroughput = "Tốc độ xử lý nhanh gấp 10 lần" -fullyPrivateFiles = "Tệp hoàn toàn riêng tư" -largeFileProcessing = "Xử lý tệp lớn" -orgWideAccess = "Kiểm soát truy cập toàn tổ chức" -premiumAiModels = "Mô hình AI cao cấp" -prioritySupport = "Hỗ trợ ưu tiên" -privateDocCloud = "Đám mây tài liệu riêng tư" -ragFineTuning = "RAG + fine-tuning" -secureApiAccess = "Truy cập API an toàn" -standardThroughput = "Tốc độ xử lý tiêu chuẩn" -unlimitedApiAccess = "Truy cập API không giới hạn" -unlimitedSeats = "Số ghế không giới hạn" - [crop] autoCrop = "Tự động cắt khoảng trắng" noFileSelected = "Chọn một tệp PDF để bắt đầu cắt xén" @@ -3346,6 +3558,7 @@ message = "Không thể đặt trình xử lý PDF mặc định" title = "Lỗi" [defaultApp.prompt] +dontRemind = "Đừng nhắc lại" message = "Đặt Stirling PDF làm ứng dụng mặc định để mở tệp PDF." [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "Tìm kiếm..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "Sửa" @@ -3644,11 +3861,13 @@ shareSelected = "Chia sẻ mục đã chọn" sharing = "Chia sẻ" showAll = "Hiện tất cả" showHistory = "Hiện lịch sử" +sortBy = "Sort files" sortByDate = "Sắp xếp theo ngày" sortByName = "Sắp xếp theo tên" sortBySize = "Sắp xếp theo kích thước" storage = "Lưu trữ" storageState = "Lưu trữ" +storageUsed = "Storage used" synced = "Đã đồng bộ" title = "Tải tệp PDF lên" toolChain = "Công cụ đã áp dụng" @@ -3694,13 +3913,17 @@ addFiles = "Thêm tệp" addingFiles = "Đang thêm tệp…" collapse = "Thu gọn thanh bên" customizeGroups = "Tùy chỉnh nhóm" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "Mở tệp để bắt đầu" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "Mở rộng thanh bên" -files = "Tệp" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive chưa được cấu hình" leaveMyFiles = "Rời Tệp của tôi" +library = "Thư viện PDF" myFiles = "Tệp của tôi" noFiles = "Chưa có tệp nào" openFileManager = "Mở trình quản lý tệp" @@ -3708,37 +3931,36 @@ openFromComputer = "Mở từ máy tính" openSettings = "Mở cài đặt" other = "Khác" recent = "Gần đây" -search = "Tìm kiếm" -searchPlaceholder = "Tìm kiếm tệp..." viewAll = "Xem tất cả {{count}} tệp" [fileSidebar.fileItem] closeViewer = "Đóng trình xem" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "Mở trong trình xem" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "Thêm" -addLabel = "Thêm nhãn…" -categoryIconAria = "Chọn biểu tượng cho {{name}}" -createCategory = "Tạo danh mục" -delete = "Xóa danh mục" done = "Xong" -hide = "Ẩn danh mục" -newCategory = "Tên danh mục mới…" -removeLabel = "Xóa {{name}}" -rename = "Đổi tên" reset = "Đặt lại về mặc định" -search = "Tìm kiếm nhãn…" -show = "Hiển thị danh mục" subtitle = "Nhóm các tệp của bạn vào danh mục cha. Thêm nhãn hiện có hoặc nhãn mới vào danh mục, đổi tên danh mục hoặc tự tạo danh mục riêng. Các tệp không thuộc danh mục nào của bạn sẽ xuất hiện trong “Khác”." title = "Danh mục thanh bên" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "Thêm vào không gian làm việc" addToWorkspaceCount = "Thêm {{count}} vào không gian làm việc" @@ -3749,7 +3971,6 @@ backToMyFiles = "Quay lại Tệp của tôi" breadcrumbs = "Đường dẫn thư mục" cancel = "Hủy" classification = "Phân loại" -clearSearch = "Xóa tìm kiếm" clearSelection = "Bỏ chọn" closeDetails = "Đóng chi tiết" create = "Tạo" @@ -3781,6 +4002,7 @@ downloadAll = "Tải xuống tất cả" downloadVersion = "Tải xuống phiên bản này" dropOverlay = "Thả tệp để tải lên" dropOverlaySub = "Các tệp bắt đầu ở Cục bộ. Dùng 'Di chuyển đến' hoặc 'Lưu lên đám mây' để sắp xếp vào thư mục." +duplicate = "Duplicate" file = "Tệp" fileInfo = "File info" fileMenu = "Hành động tệp" @@ -3808,13 +4030,12 @@ refresh = "Làm mới từ máy chủ" remove = "Xóa" removeVersion = "Xóa phiên bản này" rename = "Đổi tên" +renamed = "Đã đổi tên" renameFolder = "Đổi tên thư mục" resizeFolderTree = "Thay đổi kích thước cây thư mục (phím mũi tên, Shift cho bước lớn hơn; nhấp đúp để tự khớp)" save = "Lưu" saveToServer = "Lưu lên máy chủ" saveToServerDisabledHint = "Lưu lên máy chủ chưa được bật trên máy chủ này. Hãy yêu cầu quản trị viên bật nó." -search = "Tìm kiếm" -searchPlaceholder = "Tìm trong thư mục này và các thư mục con" selectAll = "Chọn tất cả" selectAllHint = "Nhấn để chọn tất cả. Mẹo: giữ Ctrl (hoặc Cmd) để thêm từng tệp, Shift để chọn một dải." selectedCount = "Đã chọn {{count}}" @@ -3874,6 +4095,10 @@ title = "Chưa có tệp đám mây" hint = "Các tệp lưu mà không tải lên sẽ ở đây. Thả một tệp để thêm." title = "Không có tệp chỉ cục bộ" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "Các tệp bạn mở hoặc chỉnh sửa sẽ xuất hiện ở đây." title = "Chưa có gì được sửa đổi" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "Không thể xóa thư mục." deleteFolderFailedDetail = "Không thể xóa thư mục: {{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "Không thể cập nhật giao diện thư mục." folderAppearanceFailedDetail = "Không thể cập nhật giao diện thư mục: {{message}}" moveFilesFailed = "Không thể di chuyển tệp." @@ -3910,9 +4137,7 @@ count = "Tệp" folder = "Thư mục" labels = "Nhãn" modified = "Sửa đổi" -name = "Tên" size = "Kích thước" -toolHistoryAtVersion = "Chuỗi công cụ tích lũy" totalSize = "Tổng kích thước" type = "Loại" versionHistory = "Hành trình phiên bản" @@ -3945,6 +4170,11 @@ localHint = "Chỉ lưu trong trình duyệt này" shared = "Được chia sẻ" sharedHint = "Được chia sẻ với bạn qua liên kết" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "Cũ nhất trước" modifiedDesc = "Gần đây trước" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "Không có tệp trong bộ nhớ. Hãy mở một số open = "Mở" openFile = "Mở tệp" openFiles = "Mở nhiều tệp" +selectFile = "Select {{name}}" selectFromStorage = "Chọn từ Lưu trữ" upload = "Tải lên" uploadFile = "Tải tệp lên" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "Đã lưu tất cả" +analyzingFields = "Analysing form fields..." +extractCsvError = "Không thể trích xuất CSV" +extractXlsxError = "Không thể trích xuất XLSX" +filled = "filled" flattenAfterFilling = "Làm phẳng sau khi điền" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "bắt buộc" +requiredFieldsError = "Vui lòng điền tất cả các trường bắt buộc" rescanFields = "Quét lại trường" rescanFormFields = "Quét lại trường biểu mẫu" save = "Lưu" +saveShortcut = "Ctrl+S để lưu" +unsavedChanges = "Thay đổi chưa lưu" [formFill.sidebar] close = "Đóng thanh bên" @@ -4309,6 +4552,11 @@ desc = "Tự động đổi tên tệp PDF dựa trên tiêu đề được phá tags = "tự động phát hiện,dựa trên tiêu đề,tổ chức,đổi nhãn" title = "Tự động đổi tên tệp PDF" +[home.autoRotate] +desc = "Phát hiện hướng của từng trang và tự động xoay cho thẳng." +tags = "auto rotate,sửa hướng,bị lộn ngược,nằm ngang,làm thẳng,định hướng,phát hiện hướng,sửa xoay,tự động định hướng,đã quét,OSD" +title = "Tự động xoay" + [home.autoSizeSplitPDF] tags = "tự động,chia,kích thước" title = "Tự động chia theo kích thước/số lượng" @@ -4608,15 +4856,12 @@ hint = "Tải lên PNG, JPG, SVG hoặc tệp hình ảnh khác để đặt lê label = "Tải ảnh lên" placeholder = "Chọn tệp ảnh" -[imageToPdf] -tags = "chuyển đổi,img,jpg,hình ảnh,ảnh" - [imageToPDF] header = "Hình ảnh sang PDF" title = "Hình ảnh sang PDF" -[infoBanner] -dismiss = "Bỏ qua" +[imageToPdf] +tags = "chuyển đổi,img,jpg,hình ảnh,ảnh" [invite] acceptError = "Không thể tạo tài khoản" @@ -4647,11 +4892,9 @@ welcomeTitle = "Bạn đã được mời!" [landing] addFiles = "Thêm tệp" -heroSubtitle = "Kéo thả hoặc thêm một PDF hiện có để bắt đầu." mobileUpload = "Tải lên từ Di động" openFromComputer = "Mở từ máy tính" uploadFromComputer = "Tải lên từ máy tính" -workbenchEmptyStateHero = "Thả PDF vào bất kỳ đâu" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "Nhập tên người dùng" failedToSignIn = "Không thể đăng nhập bằng {{provider}}: {{message}}" forgotPassword = "Quên mật khẩu?" loggingIn = "Đang đăng nhập..." -logIn = "Đăng nhập" login = "Đăng nhập" magicLinkSent = "Magic link đã được gửi tới {{email}}! Hãy kiểm tra email và nhấp vào liên kết để đăng nhập." mfaCode = "Mã xác thực" @@ -4715,26 +4957,10 @@ signInWith = "Đăng nhập với" title = "Đăng nhập" unexpectedError = "Lỗi không mong muốn: {{message}}" updatePassword = "Cập nhật mật khẩu" -useEmailInstead = "Đăng nhập bằng email" useMagicLink = "Thay vào đó dùng magic link" username = "Tên người dùng" youAreLoggedIn = "Bạn đã đăng nhập!" -[login.slides.edit] -alt = "Chỉnh sửa PDF" -subtitle = "Với hơn chục công cụ giúp bạn ẩn/xóa, ký, đọc và thao tác với PDF, bạn chắc chắn sẽ tìm được thứ mình cần." -title = "Chỉnh sửa PDF để hiển thị/bảo vệ thông tin bạn muốn" - -[login.slides.overview] -alt = "Tổng quan Stirling PDF" -subtitle = "Bộ công cụ đám mây ưu tiên quyền riêng tư cho PDF cho phép bạn chuyển đổi, ký, ẩn/xóa thông tin và quản lý tài liệu, cùng hơn 50 công cụ mạnh mẽ khác." -title = "Giải pháp tất cả trong một cho mọi nhu cầu PDF của bạn." - -[login.slides.secure] -alt = "Bảo mật PDF" -subtitle = "Thêm mật khẩu, che nội dung và quản lý chứng chỉ dễ dàng." -title = "Bảo vệ thông tin nhạy cảm trong PDF của bạn" - [margin] large = "Lớn" medium = "Trung bình" @@ -4818,6 +5044,34 @@ uploadSuccess = "Tải lên thành công!" uploadSuccessMessage = "Hình ảnh của bạn đã được chuyển." validating = "Đang xác thực phiên..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "Quét để tải ảnh. Hình ảnh tự động chuyển sang PDF." descriptionNoConvert = "Quét để tải ảnh từ thiết bị di động của bạn." @@ -4836,8 +5090,13 @@ title = "Tải lên từ Di động" tags = "Đa công cụ,Đa thao tác,Giao diện người dùng,nhấp kéo,phía trước,phía máy khách,tương tác,có thể tương tác,di chuyển" title = "Công cụ đa năng PDF" -[navbar] -search = "Tìm kiếm" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "Xác thực không thành công. Bạn có thể đóng cửa sổ này và thử lại." @@ -4966,6 +5225,7 @@ activeFiles = "Chế độ xem Tệp đang hoạt động hiể allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "Đóng" cropSettings = "Bây giờ chúng ta đã chọn tệp muốn cắt, có thể cấu hình công cụ Cắt xén để chọn vùng cần cắt PDF." +dialogLabel = "Onboarding" fileCheckbox = "Nhấp vào một tệp sẽ chọn nó để xử lý. Bạn có thể chọn nhiều tệp cho các thao tác hàng loạt." fileReplacement = "Tệp đã chỉnh sửa sẽ tự động thay thế tệp gốc trong Bàn làm việc, giúp bạn dễ dàng chạy thêm các công cụ khác." filesButton = "Nút Tệp trên thanh Truy cập nhanh cho phép bạn tải PDF lên để dùng các công cụ." @@ -4976,6 +5236,7 @@ pinButton = "Bạn có thể dùng nút Ghim nếu muốn tệp results = "Sau khi công cụ chạy xong, bước Đánh giá sẽ hiển thị bản xem trước kết quả trong bảng này, và cho phép bạn hoàn tác thao tác hoặc tải tệp xuống." runButton = "Khi công cụ đã được cấu hình, nút này cho phép chạy công cụ trên tất cả PDF đã chọn." selectCropTool = "Hãy chọn công cụ Cắt xén để minh họa cách sử dụng một công cụ." +stepOf = "Bước {{current}} trên {{total}}" toolInterface = "Đây là giao diện công cụ Cắt xén. Như bạn thấy, chưa có nhiều vì chúng ta chưa thêm tệp PDF nào để làm việc." workbench = "Đây là Bàn làm việc - khu vực chính nơi bạn xem và chỉnh sửa PDF." wrapUp = "Bạn đã sẵn sàng! Bạn đã biết về các khu vực chính của ứng dụng và cách sử dụng chúng. Nhấp nút Trợ giúp bất cứ lúc nào để xem lại chuyến tham quan này." @@ -4988,6 +5249,26 @@ showMeAround = "Hướng dẫn nhanh" skipForNow = "Bỏ qua tạm thời" skipTheTour = "Bỏ qua hướng dẫn" +[onboarding.checklist] +dismiss = "Bỏ qua" +title = "Thiết lập Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "Chạy Stirling nguyên bản trên máy của bạn" +title = "Tải xuống Stirling cho Desktop" + +[onboarding.checklist.inviteTeam] +description = "Cộng tác với đội ngũ của bạn" +title = "Mời thành viên đội ngũ" + +[onboarding.checklist.shareAnalytics] +description = "Giúp cải thiện Stirling" +title = "Chia sẻ dữ liệu sử dụng ẩn danh" + +[onboarding.checklist.takeTour] +description = "Xem cách Stirling hoạt động qua hướng dẫn nhanh" +title = "Tham quan nhanh" + [onboarding.desktopInstall] body = "Stirling hoạt động tốt nhất như ứng dụng desktop. Bạn có thể dùng offline, truy cập tài liệu nhanh hơn và chỉnh sửa trực tiếp trên máy." selectOs = "Chọn hệ điều hành" @@ -4996,7 +5277,21 @@ titleWithOs = "Tải cho {{osLabel}}" [onboarding.mfa] authenticationCode = "Mã xác thực" +description = "Bảo mật tài khoản của bạn bằng cách liên kết ứng dụng xác thực. Quét mã QR hoặc nhập khóa thiết lập, sau đó xác nhận mã 6 chữ số để hoàn tất." +enable = "Bật MFA" +enableError = "Không thể bật xác thực hai yếu tố. Kiểm tra mã và thử lại." +enterCode = "Nhập mã 6 chữ số từ ứng dụng của bạn." +enterCodeError = "Nhập mã xác thực để tiếp tục." +logout = "Đăng xuất" +openAuthenticator = "Mở Google Authenticator, Authy hoặc 1Password." qrCodeLoading = "Đang tạo mã QR của bạn…" +regenerateQrCode = "Tạo lại mã QR" +scanQrCode = "Quét mã QR hoặc nhập khóa thiết lập bên dưới." +setupError = "Không thể bắt đầu thiết lập hai yếu tố. Vui lòng thử lại." +setupKey = "Khóa thiết lập (nhập thủ công)" +stepByStep = "Từng bước" +success = "MFA đã được bật. Bây giờ bạn có thể tiếp tục." +title = "Thiết lập xác thực đa yếu tố" [onboarding.planOverview] adminBodyLoginDisabled = "Khi bật chế độ đăng nhập, bạn có thể quản lý người dùng, cấu hình cài đặt và theo dõi tình trạng server. {{freeTierLimit}} người đầu tiên trên server của bạn được dùng Stirling miễn phí." @@ -5005,6 +5300,11 @@ adminTitle = "Tổng quan Admin" userBody = "Mời đồng đội, gán vai trò và quản lý tài liệu gọn gàng trong một không gian làm việc an toàn. Bật chế độ đăng nhập khi bạn sẵn sàng vượt qua chế độ dùng cá nhân." userTitle = "Tổng quan gói" +[onboarding.processorIntro] +body = "Stirling hiện chạy Chính sách — các quy tắc tự động phân loại, bảo mật và xử lý mọi tài liệu khi tài liệu đến. Thiết lập và theo dõi các lần chạy trong Processor." +cta = "Xem Processor" +title = "Khám phá Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "Năm trả trước" + +[payg.prepaid.banner] +exhausted = "Dung lượng trả trước của bạn đã dùng hết; tính phí theo mức sử dụng đã tiếp tục. Nạp thêm để giữ ưu đãi." +expiring_one = "Năm trả trước của bạn kết thúc sau {{count}} ngày. Dung lượng chưa dùng sẽ hết hạn — nạp thêm để bắt đầu một năm mới." +expiring_other = "Năm trả trước của bạn kết thúc sau {{count}} ngày. Dung lượng chưa dùng sẽ hết hạn — nạp thêm để bắt đầu một năm mới." +low = "Bạn chỉ còn {{pct}}% dung lượng trả trước. Nạp thêm bất cứ lúc nào để tiếp tục xử lý không gián đoạn." + +[payg.prepaid.card] +subtitle = "Được dùng trước tính phí theo mức sử dụng và ngoài giới hạn chi tiêu của bạn." +title = "Dung lượng trả trước" + +[payg.prepaid.meter] +capSuffix = "trên {{total}} tín dụng trả trước" +expires = "Hết hạn {{date}}" + +[payg.prepaid.state] +exhausted = "Đã dùng hết" +healthy = "Còn nhiều" +low = "Sắp hết" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF sang PDF/A" tags = "lưu trữ,dài hạn,tiêu chuẩn,chuyển đổi,lưu trữ,bảo quản" title = "PDF sang PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "in ấn,tiêu chuẩn,chuyển đổi,sản xuất,tiền in,lưu trữ" title = "PDF sang PDF/X" @@ -5822,6 +6151,7 @@ small = "500 Tín dụng" xsmall = "100 Tín dụng" [plan.availablePlans] +currency = "Billing currency" subtitle = "Chọn gói phù hợp với nhu cầu của bạn" title = "Các gói khả dụng" @@ -5937,49 +6267,12 @@ keyDescription = "Dán khóa giấy phép từ email của bạn" success = "Đã kích hoạt giấy phép!" successMessage = "Giấy phép của bạn đã được kích hoạt thành công. Bạn có thể đóng cửa sổ này." -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "đã thực thi" -enforcing = "Đang thực thi..." -failed = "Thực thi thất bại" outputsUnavailable = "Đầu ra của chính sách không còn khả dụng để tải xuống." partialOutputsUnavailable = "Một số đầu ra của chính sách không còn khả dụng để tải xuống." -retrying = "Đang bận, thử lại..." runNotFound = "Không còn tìm thấy lần thực thi." -step = "bước {{current}}/{{total}}" timedOut = "Thực thi đã hết thời gian chờ trước khi hoàn tất." -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "Đóng" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "Đang áp dụng {{names}}" applyingProgress = "Đang áp dụng {{names}} ({{done}} trong {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "Không thể xử lý {{failures}} trong số {{total}} tệp và failureTitle = "Đã xuất mà không thực thi đầy đủ" printPolicyAppliedBody = "PDF này đã được cập nhật để đáp ứng một chính sách. Hãy xem lại các thay đổi, rồi in lại." printPolicyAppliedTitle = "Đã áp dụng chính sách trước khi in" -queued = "+{{count}} đang chờ" successTitle = "Đã áp dụng {{names}}" summaryMore = "{{first}}, {{second}} và còn {{more}} nữa" summaryTwo = "{{first}} và {{second}}" -[policies.enforcement.triggerVerb] -convert = "Thực thi trước khi chuyển đổi" -default = "Đang thực thi" -export = "Thực thi trước khi xuất" -input = "Thực thi khi nhập" -print = "Thực thi trước khi in" - -[policies.field] -accessLog = "Nhật ký truy cập" -archiveAfter = "Lưu trữ sau" -auditTrail = "Dấu vết kiểm toán" -belowThreshold = "Dưới ngưỡng" -destination = "Đích đến" -frameworks = "Frameworks" -immutableHold = "Giữ bất biến" -keepFor = "Giữ trong" -minConfidence = "Độ tin cậy tối thiểu" -notify = "Thông báo khi định tuyến" -onViolation = "Khi không tuân thủ" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "Thêm" -addPlaceholder = "Thêm nhãn…" -customNote = "Đã tùy chỉnh cho nhóm của bạn." -defaultNote = "Đang dùng mặc định tích hợp, được chia sẻ với nhóm của bạn." -duplicate = "\"{{name}}\" đã tồn tại." -edit = "Chỉnh sửa nhãn" -empty = "Chưa có nhãn nào." -export = "Xuất JSON" -iconAria = "Chọn biểu tượng cho {{name}}" -import = "Nhập JSON" -importError = "Không thể nhập tệp đó." -managedNote = "Nhãn nhóm do trưởng nhóm của bạn quản lý." -modalSubtitle = "Được chia sẻ với toàn bộ nhóm của bạn. Bộ phân loại chọn các nhãn phù hợp với từng tài liệu." -modalTitle = "Nhãn phân loại" -pickIcon = "Dùng biểu tượng {{label}}" -removeAria = "Xóa {{name}}" -resetToDefault = "Đặt lại về mặc định" -saveForTeam = "Lưu cho nhóm" -saving = "Đang lưu…" -sectionLabel = "Nhãn phân loại" -startFromScratch = "Bắt đầu từ đầu" -teamCount = "nhãn nhóm" -tooLong = "Nhãn có tối đa {{max}} ký tự." -ungrouped = "Chưa nhóm" -view = "Xem nhãn" +categoryCount = "danh mục" +hideCategory = "Ẩn danh mục" +labelCount = "nhãn" +sharedNote = "Các nhãn này được tích hợp sẵn và chia sẻ trên toàn bộ đội ngũ của bạn." +showCategory = "Hiển thị danh mục" [policies.pii] account = "Số tài khoản (được gắn nhãn)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "Số routing của Mỹ (ABA)" ssn = "Số An sinh Xã hội" -[policies.settings] -noneExport = "Hiện không có chính sách nào chạy khi xuất." -noneUpload = "Hiện không có chính sách nào chạy khi tải lên." -onExport = "Khi xuất" -onUpload = "Khi tải lên" -reorderHandle = "Kéo để sắp xếp lại" -runOrderDesc = "Khi nhiều chính sách chạy trên cùng một trình kích hoạt, chúng chạy theo thứ tự này — mỗi chính sách chạy trên đầu ra của chính sách trước đó. Kéo để sắp xếp lại." -title = "Cài đặt chính sách" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "Tùy chọn chính sách" -policySettings = "Cài đặt chính sách" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "Thử lại chính sách thất bại ({{count}})" -rowProgress = "Đã xử lý {{completed}} trong {{total}} tệp" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "Chính sách là gì?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "Tự động tìm và làm đen các thông tin nhạy cảm — như số An sinh Xã hội và số thẻ — để không thể đọc được trong tài liệu." -sanitize = "Xóa JavaScript ẩn khỏi tệp, để không có gì chạy tự động khi ai đó mở tệp." -watermark = "Đóng một dấu hiển thị (ví dụ: \"Confidential\") trên mọi trang." - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "Đóng" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "Đang thực thi {{name}}..." badgeRan = "Chính sách {{name}} đã chạy trên tệp này" +badgeRunning = "{{name}} đang chạy..." blockingAction = "{{action}} bị chặn trong khi thực thi chính sách, vui lòng chờ..." dismiss = "Đóng lớp phủ" enforcingTitle = "Đang thực thi chính sách..." @@ -6151,6 +6316,7 @@ viewAnyway = "Xem tệp (chính sách vẫn đang thực thi)" [policyOption] autoRedactPhi = "Tự động che PHI" blockExport = "Chặn xuất" +ccpa = "CCPA" contracts = "Hợp đồng" documents = "Tài liệu" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Gói Editor" subscribed = "Gói Processor" unlinked = "Chưa liên kết" -[portal.agentBuilder] -bootstrapFromDocument = "Khởi tạo từ tài liệu" -sectionsAriaLabel = "Các phần của trình tạo agent" -selectorAriaLabel = "Agent" -subtitle = "Thiết kế, kiểm thử và triển khai các agent AI để phân loại, trích xuất từ và định tuyến tài liệu của bạn. Xác định kịch bản, giới hạn quyền truy cập công cụ, chạy bộ chuẩn và phát hành phiên bản." -title = "Trình tạo Agent" - -[portal.agentBuilder.bootstrap] -cancel = "Hủy" -dropzoneText = "Chọn tài liệu mẫu (PDF hoặc hình ảnh)" -lead = "Thả một tài liệu mẫu và chúng tôi sẽ đề xuất kịch bản cùng schema trích xuất để bạn tinh chỉnh. Không có gì được phát hành cho đến khi bạn xem xét." -submit = "Khởi tạo agent" -subtitle = "Tạo agent mới từ một tệp đại diện" -title = "Khởi tạo từ tài liệu" - -[portal.agentBuilder.empty] -description = "Khởi tạo agent từ tài liệu mẫu để tạo kịch bản và schema trích xuất, sau đó tinh chỉnh và phát hành." -title = "Chưa có agent nào" - -[portal.agentBuilder.evals] -casesPassing = "Trường hợp đạt" -columnCase = "Trường hợp đánh giá" -columnLatency = "Độ trễ" -columnResult = "Kết quả" -fail = "không đạt" -goldenSetPassRate = "Tỷ lệ đạt của bộ chuẩn" -latencyMs = "{{ms}} ms" -notRun = "chưa chạy" -pass = "đạt" -passRate = "Tỷ lệ đạt" -runEvals = "Chạy đánh giá" - -[portal.agentBuilder.evals.empty] -description = "Đánh giá biến các kịch bản của bạn thành một bộ chuẩn có thể lặp lại. Nâng cấp để ghi lại tỷ lệ đạt theo thời gian và dùng nó làm điều kiện phát hành." -title = "Chưa có bộ chuẩn" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "trên các bộ chuẩn" -activeAgents = "Agent đang hoạt động" -avgPassRate = "Tỷ lệ đạt đánh giá trung bình" -fleetWide = "toàn hệ thống" -latestPublished = "Đã phát hành mới nhất" -scenarios = "Kịch bản" -testCases = "trường hợp kiểm thử" -totalDescription_one = "Tổng {{count}}" -totalDescription_other = "Tổng {{count}}" - -[portal.agentBuilder.scenarios] -add = "Thêm" -addScenario = "Thêm kịch bản" -enable = "Bật" -expectationLabel = "Hành vi mong đợi" -expectationPlaceholder = "Agent nên làm gì" -inEval = "trong đánh giá" -mute = "Tắt tiếng" -muted = "đã tắt tiếng" -nameLabel = "Tên" -namePlaceholder = "ví dụ: Nâng cấp tuân thủ" - -[portal.agentBuilder.tabs] -evals = "Đánh giá" -scenarios = "Kịch bản" -tools = "Công cụ" -versions = "Phiên bản" - -[portal.agentBuilder.tools] -broadAccess = "Quyền truy cập rộng" -deniedHint = "Các công cụ đã chọn bị chặn. Mọi công cụ khác vẫn có thể được gọi." -deniedTools = "Công cụ bị từ chối" -governanceGate = "Quản trị công cụ có sẵn trên gói Enterprise." -restricted = "Bị hạn chế" -restrictedAccess = "Quyền truy cập công cụ bị hạn chế" -restrictedDescription = "Cho phép mọi công cụ ngoại trừ các công cụ bạn từ chối bên dưới." - -[portal.agentBuilder.versions] -current = "hiện tại" -historyGate = "Lịch sử phiên bản đầy đủ và rollback có sẵn trên gói Enterprise." -publish = "Phát hành" -rollBack = "Rollback" - [portal.assistant] close = "Đóng trợ lý" error = "Không thể kết nối với trợ lý." @@ -6365,9 +6451,21 @@ typing = "Đang nhập" loading = "Đang tải" redirectingToEditor = "Đang chuyển hướng đến trình chỉnh sửa..." +[portal.billing.activation] +maybeLater = "Để sau" +title = "Bật Processor" + +[portal.billing.activation.payg] +sub = "Đặt giới hạn hàng tháng. Trả tiền cho những gì bạn chạy." +title = "Trả theo mức sử dụng" + +[portal.billing.activation.prepay] +badge = "Miễn phí 2 tháng" +sub = "Một hóa đơn, thẻ hoặc chuyển khoản ngân hàng." +title = "Trả trước một năm" + [portal.billing.checkout] noClientSecret = "Edge function không trả về client_secret." -subtitle = "Thêm thẻ để tiếp tục sau quyền dùng miễn phí của gói Editor. Stripe xử lý phần còn lại." title = "Bật gói Processor" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "Thanh toán của bạn đã thành công, nhưng việc kích hoạt đ close = "Đóng" title = "Sắp xong" +[portal.billing.checkout.cap] +amountAria = "Giới hạn chi tiêu hàng tháng" +back = "Quay lại" +continue = "Tiếp tục đến thanh toán" +error = "Không thể đặt giới hạn chi tiêu của bạn" +finePrint = "Hóa đơn được lập hàng tháng. Hủy bất cứ lúc nào." +noLimit = "Không giới hạn" +note = "Bạn không bao giờ bị tính phí vượt quá giới hạn. Xử lý chỉ tạm dừng." +perMonth = "/ tháng" +title = "Đặt giới hạn chi tiêu của bạn" + +[portal.billing.checkout.card] +fields = "Số thẻ · MM / YY · CVC · ZIP" +label = "Chi tiết thẻ" +note = "Chi tiết thẻ do Stripe thu thập. Stirling không bao giờ lưu PAN hoặc CVC." + [portal.billing.checkout.error] title = "Không thể bắt đầu thanh toán" @@ -6383,10 +6497,9 @@ body = "Thanh toán của bạn đã hoàn tất. Chúng tôi đang bật xử l hint = "Vui lòng giữ cửa sổ này mở." title = "Đang kích hoạt gói Processor của bạn..." -[portal.billing.checkout.notConfigured] -bodyAfter = "trong portal env để bật thanh toán trong ứng dụng." -bodyBefore = "Đặt" -title = "Chưa cấu hình Stripe" +[portal.billing.checkout.pay] +limit = "Giới hạn chi tiêu hàng tháng {{amount}}/tháng. Xử lý tạm dừng khi đạt giới hạn." +limitNoCap = "Không có giới hạn chi tiêu. Xử lý sẽ không tạm dừng. Hóa đơn được lập hàng tháng." [portal.billing.enterpriseUpsell] cta = "Tạo báo giá Enterprise của bạn" @@ -6409,10 +6522,12 @@ currentPlan = "Gói hiện tại" freeForever = "Miễn phí mãi mãi" noTeamResolved = "Chưa xác định được nhóm nào trên ví của bạn — hãy làm mới và thử lại." ownerOnly = "Chỉ chủ sở hữu nhóm mới có thể bật gói Processor." +payInvoice = "Thanh toán hóa đơn để hoàn tất" planName = "Editor" ssoIncluded = "Bao gồm SSO" switchOnProcessor = "Bật Processor →" unlimitedUsers = "Người dùng không giới hạn" +viewQuote = "Xem báo giá" [portal.billing.invoices] columnAmount = "Số tiền" @@ -6465,7 +6580,111 @@ segmentApiDesc = "Yêu cầu API trực tiếp" segmentApiLabel = "API" segmentAutomationDesc = "Tự động hóa & pipeline" segmentAutomationLabel = "Tự động hóa" +sizeMultiplier = "{{formatted}} PDF đã dùng hệ số kích thước" +summary = "{{unique}} duy nhất · {{units}} đơn vị đo · trung bình {{avg}} mỗi PDF" +summaryNoRate = "{{unique}} duy nhất · {{units}} đơn vị đo" unit = "PDF tính theo mức sử dụng" +unitsPending_one = "{{units}} đơn vị đo đang chờ đồng bộ từ các phiên bản được liên kết" +unitsPending_other = "{{units}} đơn vị đo đang chờ đồng bộ từ các phiên bản được liên kết" + +[portal.billing.prepaid] +capSuffix = "trên {{total}} tín dụng trả trước" +expires = "Hết hạn {{date}}" +eyebrow = "Dung lượng trả trước" +note = "Được dùng trước tính phí theo mức sử dụng và ngoài giới hạn chi tiêu của bạn." +topUp = "Nạp thêm" + +[portal.billing.prepaid.buy] +back = "Quay lại" +calcTitle = "Tính khoản thanh toán hằng năm của bạn" +cancel = "Hủy" +cancelPurchase = "Hủy giao dịch mua" +close = "Đóng" +continueToPayment = "Tiếp tục đến thanh toán" +finalise = "Hoàn tất" +invoiceErrorTitle = "Không thể tạo hóa đơn" +loadingTitle = "Đang tải báo giá của bạn" +payTitle = "Thanh toán cho năm của bạn" +step = "Bước {{current}} trên {{total}}" + +[portal.billing.prepaid.calc] +change = "Thay đổi" +deployRow = "Triển khai" +done = "Xong" +enterpriseHint = "Đây là quy mô doanh nghiệp. Ở mức này, giá doanh nghiệp tốt hơn mọi ưu đãi tự phục vụ. Khóa giá, điều khoản và báo giá trong vài phút." +governanceRow = "Quản trị" +governanceValue = "{{label}} · {{count}} chính sách" +handlesLabel = "Processor của bạn" +handlesValue = "xử lý {{volume}} tín dụng / tháng" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} chính sách" +poolCaption = "Một nhóm {{credits}} tín dụng cho cả năm. Tháng dùng nhiều mượn từ tháng dùng ít." +priceLabel = "Năm của bạn · 12 tháng với giá của 10 tháng" +rateUnknown = "Chúng tôi sẽ hiển thị giá chính xác khi thanh toán." +savings = "Bạn tiết kiệm {{amount}} · miễn phí 2 tháng." +sizingRow = "Quy mô" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "Tổng số người dùng" + +[portal.billing.prepaid.consent] +label = "Tôi hiểu rằng khi dung lượng trả trước của tôi được dùng hết hoặc hết hạn sau 12 tháng, quá trình xử lý sẽ tự động tiếp tục theo mức giá trả theo mức sử dụng tiêu chuẩn (tối đa đến giới hạn chi tiêu của tôi) trừ khi tôi hủy, và tôi có thể hủy bất cứ lúc nào từ cổng thanh toán." + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "Do Stirling quản lý. Hoạt động trong vài phút." +cloudValue = "Stirling Cloud · Được quản lý" +selfhost = "Tự lưu trữ" +selfhostDesc = "Chạy trên hạ tầng riêng. Cùng mức giá." +selfhostValue = "Tự lưu trữ · Hạ tầng riêng" + +[portal.billing.prepaid.offer] +cta = "Xem ưu đãi" +subtitle = "Trả trước một năm xử lý PDF và nhận miễn phí hai tháng, được dùng trước tính phí theo mức sử dụng, ngoài giới hạn chi tiêu của bạn." +title = "Nhận 12 tháng với giá của 10 tháng" + +[portal.billing.prepaid.pay] +accountName = "Tên chủ tài khoản" +accountNamePlaceholder = "ví dụ Jane Smith" +companyName = "Tên công ty (tùy chọn)" +companyNamePlaceholder = "ví dụ Acme Inc." +downloadInvoice = "Tải xuống hóa đơn" +payOnline = "Thanh toán trực tuyến" +poNumber = "Số PO (tùy chọn)" +poNumberPlaceholder = "ví dụ PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "Nâng cao" +advancedDesc = "Pipelines thúc đẩy phần lớn quá trình xử lý của bạn." +none = "Không có" +noneDesc = "Chưa chạy pipelines. Bật chúng bất cứ lúc nào." +standard = "Tiêu chuẩn" +standardDesc = "Một vài pipelines xử lý lại các PDF đến." + +[portal.billing.prepaid.posture] +essentials = "Cơ bản" +essentialsDesc = "Phân loại, mặc định, cộng với Chia sẻ" +governed = "Được quản trị" +governedDesc = "Thêm Bảo mật và Định tuyến" +regulated = "Được quản lý theo quy định" +regulatedDesc = "Mọi danh mục, bao gồm Tuân thủ, Lưu giữ, Tiếp nhận" + +[portal.billing.prepaid.review] +download = "Tải xuống báo giá (PDF)" +downloading = "Đang chuẩn bị…" +share = "chia sẻ để phê duyệt" + +[portal.billing.prepaid.size] +compact = "Gọn nhẹ" +compactDesc = "Tệp dưới 25 MB, không tính phí dữ liệu" +heavy = "Nặng" +heavyDesc = "Đã quét hoặc nhiều hình ảnh, thường xuyên 50 MB+" +standard = "Tiêu chuẩn" +standardDesc = "Chủ yếu nhỏ, một số bản quét vượt 25 MB" + +[portal.billing.prepaid.state] +exhausted = "Đã dùng hết" +healthy = "Còn nhiều" +low = "Sắp hết" [portal.billing.spendLimit] adjustLimit = "Điều chỉnh giới hạn" @@ -6494,6 +6713,8 @@ label = "Dự kiến sẽ vượt." [portal.billing.spendThisMonth] eyebrow = "Chi tiêu tháng này" +freeRemaining_one = "Còn {{formatted}} PDF miễn phí" +freeRemaining_other = "Còn {{formatted}} PDF miễn phí" processed_one = "Đã xử lý {{formattedCount}} PDF." processed_other = "Đã xử lý {{formattedCount}} PDF." processedWithRate_one = "Đã xử lý {{formattedCount}} PDF, với {{rate}} mỗi PDF." @@ -6510,6 +6731,7 @@ reachedTitle = "Đã đạt giới hạn chi tiêu hàng tháng" title = "Không thể mở portal Stripe" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "trong {{allowance}} PDF miễn phí đã dùng" capSuffix_other = "trong {{allowance}} PDF miễn phí đã dùng" eyebrow = "Dùng thử Processor" @@ -6534,6 +6756,376 @@ signature = "chữ ký" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "Kết nối mới" +createTitleFor = "Kết nối {{name}}" +delete = "Xóa" +edit = "Chỉnh sửa" +editTitle = "Chỉnh sửa kết nối" +unsupportedType = "Không thể chỉnh sửa loại kết nối này tại đây." + +[portal.connections.categories.advanced] +description = "Cho một dịch vụ mà chúng tôi chưa hỗ trợ." +label = "Nâng cao" + +[portal.connections.categories.audit] +description = "Ghi lại những gì đã xảy ra với tài liệu, nơi kiểm toán viên của bạn đang xem." +label = "Ghi nhật ký kiểm toán & tuân thủ" + +[portal.connections.categories.notify] +description = "Thông báo cho hệ thống khác rằng có điều gì đó đã xảy ra." +label = "Thông báo & quy trình làm việc" + +[portal.connections.categories.security] +description = "Quét hoặc gắn nhãn tài liệu trước khi tài liệu tiếp tục di chuyển." +label = "Bảo mật & phân loại" + +[portal.connections.categories.signing] +description = "Gửi tài liệu đi để ký hoặc công chứng." +label = "Chữ ký điện tử & công chứng" + +[portal.connections.categories.storage] +description = "Nơi tài liệu đã xử lý kết thúc - bucket, drive, ticket hoặc page." +label = "Tệp & đính kèm" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling lưu trữ mục này ở dạng mã hóa và không bao giờ hiển thị lại." +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "Các bước dùng kết nối này chỉ có thể gọi các đường dẫn dưới URL này, không bao giờ gọi host khác." +label = "Server URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "Mật khẩu" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "Tên người dùng" + +[portal.connections.commonFields.webhookUrl] +helperText = "Dán URL mà dịch vụ đã cung cấp cho bạn. Chỉ các đường dẫn dưới URL đó mới có thể được gọi." +label = "Webhook URL" + +[portal.connections.fields] +name = "Tên kết nối" +namePlaceholder = "ví dụ Production" + +[portal.connections.picker] +cancel = "Hủy" +createNew = "Kết nối mới..." +placeholder = "Chọn một kết nối" +save = "Lưu kết nối" + +[portal.connections.picker2] +back = "Đổi tích hợp" +heading = "Bạn muốn kết nối gì?" +noResultsBody = "Chúng tôi chưa hỗ trợ mục này - hãy cho chúng tôi biết và chúng tôi sẽ xem xét thêm vào." +noResultsTitle = "Không có kết quả khớp với \"{{query}}\"" +searchPlaceholder = "Tìm kiếm tích hợp - thử \"sign\", \"ocr\" hoặc tên sản phẩm" + +[portal.connections.types.api] +description = "Gọi bất kỳ HTTP API nào từ một chính sách. Bạn chọn URL, xác thực và payload." +label = "API tùy chỉnh" + +[portal.connections.types.api.fields.authType] +label = "Xác thực" + +[portal.connections.types.api.fields.authType.options.basic] +label = "Tên người dùng và mật khẩu" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "Token trong header tùy chỉnh" + +[portal.connections.types.api.fields.authType.options.none] +label = "Không có" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "Đăng nhập để lấy token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "Các bước dùng kết nối này chỉ có thể gọi các đường dẫn dưới URL này, không bao giờ gọi host khác." +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Tên header" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "Đường dẫn đăng nhập" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "Mật khẩu" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "Nếu API phản hồi bằng liên kết tải xuống, chỉ các host này có thể được truy xuất. Phân tách bằng dấu phẩy. Để trống để chỉ cho phép host riêng của base URL." +label = "Host kết quả được phép" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "Gửi token dưới dạng header này" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "Header phản hồi mà lệnh gọi đăng nhập trả về token, ví dụ X-Auth-Token." +label = "Header phản hồi token" + +[portal.connections.types.api.fields.username] +label = "Tên người dùng" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "Quét tài liệu để tìm malware trên máy chủ bạn vận hành - không có gì rời khỏi mạng của bạn." +label = "ClamAV (tự lưu trữ)" + +[portal.connections.types.cloudmersive] +description = "Quét virus và nội dung cho tài liệu trong một chính sách." +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "Chặn tài liệu chứa macro, tệp thực thi, script hoặc đối tượng nhúng - không chỉ malware đã biết." +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "Đính kèm tài liệu đã xử lý vào một trang Confluence." +label = "Confluence" + +[portal.connections.types.consigno] +description = "Gửi tài liệu tới ConsignO Cloud (Notarius) để ký điện tử và công chứng." +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "Môi trường ConsignO Cloud của bạn, bao gồm /api/v1." +label = "Server URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "Mật khẩu" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "Chỉ cần nếu tổ chức của bạn dùng tenants." +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "Tên người dùng" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "Đăng tin nhắn lên Discord khi một chính sách xử lý tài liệu." +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "Lập chỉ mục một sự kiện kiểm toán trong Elasticsearch mỗi khi một chính sách xử lý tài liệu." +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "Thăm dò máy chủ FTP hoặc FTPS để tìm tài liệu mới." +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "Chế độ kết nối" + +[portal.connections.types.ftp.fields.passive.options] +active = "Active" +passive = "Passive (khuyến nghị)" + +[portal.connections.types.ftp.fields.security] +helperText = "FTP thường gửi thông tin xác thực và tệp không được mã hóa; chọn FTPS nếu máy chủ hỗ trợ." +label = "Mã hóa" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS (explicit / AUTH TLS)" +implicit = "FTPS (implicit)" +none = "Không có (FTP thường)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "Đăng tin nhắn lên một Google Chat space khi một chính sách chạy." +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "Đính kèm tài liệu đã xử lý vào một Jira issue." +label = "Jira" + +[portal.connections.types.mailgun] +description = "Gửi email tài liệu hoặc thông báo khi một chính sách chạy." +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "Lưu tài liệu đã xử lý vào Nextcloud." +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "Phát hiện dữ liệu cá nhân trong văn bản tài liệu bằng máy chủ Presidio bạn vận hành." +label = "Presidio (tự lưu trữ)" + +[portal.connections.types.purview] +description = "Đọc và áp dụng nhãn độ nhạy Microsoft Purview trên PDF." +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "Tùy chọn. Chỉ cần để chọn nhãn theo tên; việc gắn nhãn vẫn hoạt động khi không có." +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "Được đóng dấu lên mọi nhãn dưới dạng SiteId của nhãn, để các công cụ nhận biết Purview biết tổ chức nào đã áp dụng nhãn đó." +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "Một S3 bucket mà nguồn có thể đọc và đầu ra pipeline có thể ghi vào." +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "Để trống cho AWS. Đặt mục này cho MinIO hoặc kho lưu trữ tương thích S3 khác." +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "Gửi email tài liệu hoặc thông báo khi một chính sách chạy." +label = "SendGrid" + +[portal.connections.types.sftp] +description = "Thăm dò máy chủ SFTP (SSH) để tìm tài liệu mới." +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "Tùy chọn. Dấu vân tay SHA-256 host key của máy chủ (từ `ssh-keyscan host | ssh-keygen -lf -`). Khi đặt, chỉ máy chủ xuất trình key này mới được chấp nhận; khi để trống, key được thấy ở lần kết nối đầu tiên sẽ được ghim và các thay đổi sau đó bị từ chối." +label = "Dấu vân tay host key" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "Để trống nếu bạn xác thực bằng private key thay thế." + +[portal.connections.types.sftp.fields.privateKey] +helperText = "Tùy chọn. Dán OpenSSH private key để xác thực bằng key thay vì mật khẩu." +label = "Private key" + +[portal.connections.types.slack] +description = "Đăng tin nhắn lên Slack khi một chính sách xử lý tài liệu." +label = "Slack" + +[portal.connections.types.smb] +description = "Theo dõi một SMB / CIFS network share (Windows hoặc Samba)." +hostPlaceholder = "fileserver.example.com" +label = "SMB / Network share" + +[portal.connections.types.smb.fields.domain] +helperText = "Tùy chọn. Windows domain hoặc workgroup, nếu máy chủ của bạn dùng." +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "Gửi một sự kiện kiểm toán tới Splunk mỗi khi một chính sách xử lý tài liệu." +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "Gửi một sự kiện kiểm toán tới Sumo Logic HTTP collector." +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "Đăng tin nhắn lên một kênh Teams khi một chính sách chạy." +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "Đăng từng tài liệu, hoặc chỉ chi tiết, tới URL bạn chọn." +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "Kích hoạt một Zap hoặc Make scenario từ một chính sách." +label = "Zapier & Make" + +[portal.docs] +browse = "Duyệt tài liệu" +viewSource = "Xem mã nguồn trên GitHub" + [portal.docs.authentication] codeCaption = "mọi yêu cầu" eyebrow = "BẮT ĐẦU" @@ -6624,6 +7216,9 @@ eyebrow = "KỸ NĂNG" lead = "Các năng lực đi kèm, có tên, mà agent của bạn gọi như một công cụ duy nhất. Mỗi kỹ năng là một chuỗi thao tác xác định với đánh giá đính kèm." title = "Kỹ năng agent" +[portal.docs.toc] +title = "Trên trang này" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "THAM CHIẾU API" @@ -6697,6 +7292,8 @@ type = "Loại" user = "Người dùng" [portal.documents.queue.empty] +connectSource = "Kết nối nguồn" +createPipeline = "Tạo pipeline" description = "Khi các nguồn đưa tài liệu vào pipeline của bạn, chúng sẽ xuất hiện ở đây để xem xét." title = "Không có tài liệu trong hàng đợi" @@ -6712,11 +7309,11 @@ editorAction = "Trình chỉnh sửa" empty = "Không có tài liệu nào khớp với bộ lọc này." rowActions = "Hành động hàng" sensitiveLabel = "Nhạy cảm" -sensitiveTitle = "Nhạy cảm — yêu cầu quyền truy cập" [portal.documents.table.columns] action = "Pipeline / Hành động" document = "Tài liệu" +labels = "Labels" product = "Sản phẩm" status = "Trạng thái" time = "Thời gian" @@ -6732,6 +7329,7 @@ host = "Host" lastSeen = "Lần thấy gần nhất" region = "Khu vực" status = "Trạng thái" +target = "Target" version = "Phiên bản" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "Chế độ xem này gặp lỗi không mong muốn. Hãy thử l retry = "Thử lại" title = "Đã xảy ra lỗi trên trang này" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "Quay lại" +body = "Tải xuống ứng dụng desktop hoặc tự lưu trữ máy chủ." +done = "Xong" +guide = "Mở hướng dẫn {{name}}" +openInBrowser = "Mở trong trình duyệt thay thế" +sectionDesktop = "Ứng dụng desktop" +sectionSelfHosted = "Tự lưu trữ" +title = "Cài đặt Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "Chạy image chính thức trên máy chủ của bạn." +note = "Mở tại :8080. Đăng nhập mặc định: admin / stirling." +tagline = "Chạy máy chủ trong container" +title = "Docker" +variantFat = "Fat" +variantLite = "Ultra-Lite" +variantStandard = "Standard" + +[portal.home.download.kubernetes] +detailBody = "Cài đặt Helm chart chính thức." +note = "Thêm PVC cho /configs để duy trì cài đặt." +tagline = "Triển khai lên cluster bằng Helm" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "Bản build gốc cho bản phân phối của bạn." +guideBtn = "Xem các tùy chọn cài đặt Linux" +note = "Các bản build AppImage, .deb và .rpm có sẵn cho các distro phổ biến." +tagline = "Ứng dụng desktop gốc cho distro của bạn" +title = "Linux" + +[portal.home.download.mac] +altLabel = "Hoặc qua Homebrew" +detailBody = "Chạy trình cài đặt, hoặc dùng Homebrew." +downloadBtn = "Tải xuống cho macOS" +tagline = "Ứng dụng gốc cho Apple silicon và Intel" +title = "macOS" + +[portal.home.download.manual] +detailBody = "Chạy .jar trên máy chủ bare metal." +downloadBtn = "Tải xuống .jar" +note = "Yêu cầu Java 21+. Mở tại :8080; đăng nhập mặc định admin / stirling." +runLabel = "Sau đó chạy lệnh" +tagline = "Chạy .jar trên máy chủ bare metal" +title = "Thiết lập máy chủ thủ công" + +[portal.home.download.windows] +altLabel = "Hoặc qua winget" +detailBody = "Chạy trình cài đặt, hoặc dùng trình quản lý gói." +downloadBtn = "Tải xuống trình cài đặt" +tagline = "Ứng dụng gốc cho Microsoft Windows" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} đang hoạt động" -invite = "Mời đồng đội" +activeOfDeployed = "{{active}} trong {{total}} hoạt động tháng này" name = "Stirling PDF Editor" open = "Mở trong trình duyệt" updated = "đã cập nhật {{time}}" +[portal.home.editor.deploy] +finish = "Hoàn tất triển khai" +options = "Xem tùy chọn triển khai" +start = "Triển khai Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "Self-hosted · Docker" @@ -6823,49 +7529,6 @@ afternoon = "Chào buổi chiều" evening = "Chào buổi tối" morning = "Chào buổi sáng" -[portal.home.onboarding] -dismiss = "Bỏ qua thiết lập" -notStarted = "Chưa bắt đầu" -progress = "Đã hoàn tất {{done}}/{{total}}" -title = "Hoàn tất thiết lập" - -[portal.home.onboarding.enterprise] -body = "SSO + SCIM + RBAC toàn tổ chức, giá theo khối lượng cam kết và triển khai air-gapped." -cta = "Bắt đầu dùng thử" -ctaQuote = "Nhận báo giá" -lead = "Dành cho 250+ nhân viên." -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "Triển khai ứng dụng desktop, miễn phí mãi mãi." -title = "Tải PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} đang hoạt động · {{recommended}} được đề xuất" -chip = "{{active}} đang hoạt động" -title = "Bật chính sách" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} đã kết nối · mọi PDF được quản trị tại nơi nó đến" -chip = "{{connected}} đã kết nối" -title = "Kết nối nguồn của bạn" - -[portal.home.quickActions] -subtitle = "Các tác vụ hàng đầu hôm nay" -title = "Hành động nhanh" - -[portal.home.quickActions.buildPipeline] -blurb = "Trình soạn thảo 3 bước trên thư viện thao tác có kiểu" -title = "Tạo pipeline" - -[portal.home.quickActions.connectSource] -blurb = "S3, agent, webhook, thư mục được theo dõi" -title = "Kết nối nguồn" - -[portal.home.quickActions.issueApiKey] -blurb = "Khóa có phạm vi với giới hạn tốc độ và danh sách cho phép IP" -title = "Cấp khóa API" - [portal.infrastructure] manageEditorDeployment = "Quản lý triển khai Editor" sectionsAriaLabel = "Các phần hạ tầng" @@ -6878,13 +7541,9 @@ heading = "Khóa API" subheading = "Thông tin xác thực có phạm vi với giới hạn tốc độ theo từng khóa, quyền và danh sách cho phép IP." [portal.infrastructure.apiKeys.card] -allowedIps = "IP được phép" -anyIp = "Bất kỳ IP nào (không có danh sách cho phép)" created = "Đã tạo" lastUsed = "Lần dùng gần nhất" -permissions = "Quyền" -rateLimit = "Giới hạn tốc độ" -rateLimitValue = "{{value}} req/phút" +revoke = "Thu hồi khóa" usageMonth = "Sử dụng tháng này" usageToday = "Sử dụng hôm nay" @@ -6892,6 +7551,15 @@ usageToday = "Sử dụng hôm nay" description = "Tạo khóa có phạm vi để bắt đầu gọi Stirling API." title = "Chưa có khóa API" +[portal.infrastructure.apiKeys.error] +load = "Không thể tải API key của bạn. Vui lòng thử lại." + +[portal.infrastructure.apiKeys.revoke] +body = "Thu hồi \"{{name}}\"? Mọi tích hợp vẫn đang dùng khóa này sẽ ngay lập tức bắt đầu nhận lỗi 401. Không thể hoàn tác thao tác này." +cancel = "Hủy" +confirm = "Thu hồi khóa" +title = "Thu hồi API key" + [portal.infrastructure.audit] filterAriaLabel = "Lọc sự kiện kiểm tra theo danh mục" heading = "Nhật ký kiểm tra" @@ -6935,6 +7603,7 @@ all = "Tất cả" auth = "Xác thực" config = "Cấu hình" elevation = "Nâng quyền" +policy = "Chính sách" processing = "Xử lý" security = "Bảo mật" @@ -6944,19 +7613,30 @@ title = "Bạn không có quyền truy cập" [portal.infrastructure.audit.metrics] config = "Cấu hình" -elevation = "Nâng quyền" +policy = "Chính sách" processing = "Xử lý" totalEvents = "Tổng sự kiện · 24 giờ" +[portal.infrastructure.auditCatLabel] +auth = "Xác thực" +config = "Cấu hình" +elevation = "Nâng quyền" +policy = "Chính sách" +processing = "Xử lý" +security = "Bảo mật" + +[portal.infrastructure.auditStatusLabel] +danger = "Lỗi" +info = "Thông tin" +success = "Thành công" +warning = "Cảnh báo" + [portal.infrastructure.createKey] cancel = "Hủy" createKey = "Tạo khóa" done = "Xong" -ipAllowlistHelper = "Các dải CIDR phân tách bằng dấu phẩy. Để trống để cho phép bất kỳ IP nào." -ipAllowlistLabel = "Danh sách cho phép IP" keyNameLabel = "Tên khóa" keyNamePlaceholder = "ví dụ: Production · ingest" -permissionsLabel = "Quyền" secretKeyCaption = "Khóa bí mật" secretWarning = "Lưu khóa này trong trình quản lý bí mật. Stirling chỉ lưu hash — không có cách nào khôi phục lại sau này." subtitle = "Giới hạn phạm vi khóa ở mức tối thiểu cần thiết. Bạn có thể xoay vòng hoặc thu hồi bất cứ lúc nào." @@ -6964,207 +7644,9 @@ subtitleCreated = "Sao chép bí mật này ngay — nó sẽ không được hi title = "Tạo khóa API" titleCreated = "Đã tạo khóa" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/phút" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "Được triển khai bởi" -environment = "Môi trường" -product = "Sản phẩm" -status = "Trạng thái" -version = "Phiên bản" -when = "Thời điểm" - -[portal.infrastructure.deployments.recent] -heading = "Triển khai gần đây" -subheading = "Các đợt phát hành mới nhất trên các sản phẩm và môi trường." - -[portal.infrastructure.deployments.regionColumns] -instances = "Phiên bản" -latency = "Độ trễ" -load = "Tải" -p99 = "P99" -region = "Khu vực" -status = "Trạng thái" -throughput = "Thông lượng" -uptime = "Thời gian hoạt động" -version = "Phiên bản" - -[portal.infrastructure.deployments.regions] -heading = "Khu vực" -subheading = "Tình trạng trực tiếp cho từng khu vực Stirling đã triển khai — độ trễ, tải và phiên bản phát hành." - -[portal.infrastructure.deployments.regions.empty] -description = "Các khu vực đã triển khai sẽ xuất hiện tại đây sau khi workspace của bạn được cấp phát." -title = "Chưa triển khai khu vực nào" - -[portal.infrastructure.models] -heading = "Mô hình" -msValue = "{{value}} ms" -subheading = "Danh mục mô hình và định tuyến hỗ trợ xử lý tài liệu trên toàn workspace của bạn." - -[portal.infrastructure.models.byom] -description = "Đăng ký một mô hình on-prem hoặc self-hosted và ghim nó vào một khu vực để xử lý theo ràng buộc lưu trú dữ liệu." -title = "Mang mô hình của riêng bạn" - -[portal.infrastructure.models.catalogue] -heading = "Danh mục" -sub = "Các mô hình được quản lý khả dụng cho workspace của bạn, với độ trễ và chi phí trực tiếp." -subEnterprise = "Các mô hình được quản lý, mang theo của riêng bạn và on-prem — có thể ghim theo từng khu vực." - -[portal.infrastructure.models.catalogue.empty] -description = "Các mô hình trong danh mục của workspace sẽ xuất hiện tại đây." -title = "Không có mô hình khả dụng" - -[portal.infrastructure.models.columns] -cost = "Chi phí" -latency = "Độ trễ" -load = "Tải" -model = "Mô hình" -status = "Trạng thái" -type = "Loại" -version = "Phiên bản" - -[portal.infrastructure.models.metrics] -activeModels = "Mô hình đang hoạt động" -avgLatency = "Độ trễ trung bình" -included = "Đã bao gồm" -monthlySpend = "Chi tiêu mô hình hàng tháng" - -[portal.infrastructure.models.routing] -empty = "Chưa cấu hình quy tắc định tuyến nào." -heading = "Quy tắc định tuyến" -sub = "Mô hình nào xử lý từng thao tác. Mặc định áp dụng khi không có quy tắc hẹp hơn khớp." -subLocked = "Định tuyến thao tác đến các mô hình cụ thể — khả dụng trên gói trả phí." - -[portal.infrastructure.models.routing.lockedBanner] -description = "Nâng cấp lên Pro để kiểm soát mô hình nào xử lý từng thao tác và loại tài liệu." -title = "Định tuyến mô hình là tính năng trả phí" - -[portal.infrastructure.models.routingColumns] -default = "Mặc định" -docType = "Loại tài liệu" -modelForAria = "Mô hình cho {{operation}}" -operation = "Thao tác" -routedTo = "Định tuyến đến" - -[portal.infrastructure.security.access.byok] -description = "Cung cấp khóa từ KMS của riêng bạn. Stirling mã hóa bằng khóa đó nhưng vẫn có thể đọc." -label = "Mang khóa của riêng bạn (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "Khóa không bao giờ rời khỏi KMS của bạn. Stirling chỉ giữ ciphertext." -label = "Giữ khóa của riêng bạn (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling quản lý khóa mã hóa. Đơn giản nhất — phía bạn không cần vận hành khóa." -label = "Khóa do Stirling giữ" - -[portal.infrastructure.security.accessPolicy] -heading = "Chính sách truy cập tài liệu" -subheading = "Kiểm soát ai có thể giải mã tài liệu đã xử lý khi lưu trữ." - -[portal.infrastructure.security.attestations] -heading = "Chứng nhận tuân thủ" -noReport = "Không có báo cáo" -subheading = "Tư thế kiểm tra theo từng framework, với báo cáo khả dụng cho các kiểm soát đã được chứng nhận." -viewReport = "Xem báo cáo →" - -[portal.infrastructure.security.compliance] -heading = "Tuân thủ" -subheading = "Chứng nhận và chứng chỉ bao phủ nền tảng Stirling." - -[portal.infrastructure.security.empty] -description = "Cấu hình bảo mật của workspace sẽ xuất hiện tại đây." -title = "Không có tư thế bảo mật" - -[portal.infrastructure.security.hyokBanner] -description = "Với HYOK, khóa mã hóa không bao giờ rời khỏi KMS của bạn. Stirling chỉ lưu trữ và xử lý ciphertext mà bạn có thể thu hồi bất cứ lúc nào." -title = "Stirling không thể giải mã tài liệu của bạn" - -[portal.infrastructure.security.ipAllowlist] -empty = "Chưa cấu hình dải IP nào — tất cả IP đều được phép." -heading = "Danh sách cho phép IP" -sub = "Quyền truy cập API bị giới hạn trong các dải CIDR này." -subLocked = "Giới hạn quyền truy cập API vào các dải IP đã biết — khả dụng trên gói trả phí." - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "Nâng cấp lên Pro để giới hạn quyền truy cập API vào các mạng cụ thể." -title = "Danh sách cho phép IP là tính năng trả phí" - -[portal.infrastructure.security.ipColumns] -added = "Đã thêm" -addedBy = "Được thêm bởi" -cidr = "CIDR" -label = "Nhãn" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "Quản lý khóa mã hóa" -keyId = "Mã định danh khóa" -lastRotated = "Lần xoay vòng gần nhất" -rotateKey = "Xoay vòng khóa" -rotationPolicy = "Chính sách xoay vòng" -subheading = "Quyền lưu giữ các khóa mã hóa tài liệu khi lưu trữ — ai có thể giải mã và cách khóa được xoay vòng." - -[portal.infrastructure.security.managedBanner] -description = "Quyền lưu giữ bring-your-own-key (BYOK) và hold-your-own-key (HYOK) khả dụng trên Enterprise. Nâng cấp để cung cấp khóa từ KMS của riêng bạn." -title = "Khóa được Stirling quản lý trên gói của bạn" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "Châu Á - Thái Bình Dương" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · ranh giới dữ liệu GDPR" -label = "Liên minh Châu Âu" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "Hoa Kỳ" - -[portal.infrastructure.security.residencyHeader] -heading = "Lưu trú dữ liệu" -subheading = "Nơi tài liệu được lưu trữ và xử lý." - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "Đã dùng {{value}}" - -[portal.infrastructure.storage.empty] -description = "Lưu trữ đã kết nối và mức sử dụng sẽ xuất hiện tại đây." -title = "Chưa cấu hình lưu trữ" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "Đang hoạt động" -activeRange = "0–{{value}} ngày" -archived = "Đã lưu trữ" -coldStorage = "lưu trữ lạnh" -deleted = "Đã xóa" -never = "không bao giờ" -purged = "đã xóa vĩnh viễn" - -[portal.infrastructure.storage.providers] -connect = "Kết nối" -connected = "Đã kết nối" -heading = "Nhà cung cấp đã kết nối" -subheading = "Nơi các artifact đã xử lý được ghi." - -[portal.infrastructure.storage.retention] -heading = "Lưu giữ" -subheading = "Thời gian giữ artifact trước khi xóa theo vòng đời." -windowLabel = "Khoảng thời gian lưu giữ mặc định" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} ngày" -days_other = "{{count}} ngày" -never = "Không bao giờ xóa" - -[portal.infrastructure.storage.totalUsage] -heading = "Tổng mức sử dụng" -progressLabel = "Dung lượng đã dùng" -subheading = "Dung lượng đã tiêu thụ trên tất cả nhà cung cấp đã kết nối." +revoked = "Đã thu hồi" [portal.infrastructure.tabs] apiKeys = "Khóa API" @@ -7174,6 +7656,49 @@ models = "Mô hình" security = "Bảo mật" storage = "Lưu trữ" +[portal.integrations] +availableHeading = "Có sẵn" +comingSoonHeading = "Sắp ra mắt" +connect = "Kết nối" +connectionCount_one = "{{count}} kết nối" +connectionCount_other = "{{count}} kết nối" +customApi = "API tùy chỉnh" +searchPlaceholder = "Tìm kiếm tích hợp" +subtitle = "Mọi thứ Stirling có thể giao tiếp. Thông tin xác thực được tạo một lần tại đây, rồi được tham chiếu bởi nguồn, chính sách và pipeline." +title = "Tích hợp" +typedName = "Tên {{tool}}" + +[portal.integrations.filters] +all = "Tất cả" +audit = "Kiểm toán" +connected = "Đã kết nối" +notify = "Thông báo" +security = "Bảo mật" +signing = "Ký" +storage = "Lưu trữ" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "Đã kết nối" + +[portal.integrations.table] +integration = "Tích hợp" +worksWith = "Hoạt động với" + +[portal.integrations.worksWith] +pipelines = "Pipeline" +policies = "Chính sách" +sources = "Nguồn" + +[portal.legal] +draft = "{{label}} · bản nháp" +loadError = "Không thể tải tài liệu này. Vui lòng thử lại." +loading = "Đang tải…" +title = "Tài liệu pháp lý" + [portal.nav] agent-builder = "Trình tạo Agent" components = "Thành phần" @@ -7182,33 +7707,17 @@ documents = "Tài liệu" editor = "Editor" home = "Trang chủ" infrastructure = "Hạ tầng" +integrations = "Tích hợp" pipelines = "Pipeline" policies = "Chính sách" procurement = "Mua sắm" -settings = "Cài đặt" sources = "Nguồn" usage = "Sử dụng & Thanh toán" users = "Người dùng" -[portal.notifications] -markAllRead = "Đánh dấu tất cả đã đọc" -title = "Thông báo" -viewAll = "Xem tất cả" - -[portal.notifications.ariaLabel] -none = "Thông báo, không có mục chưa đọc" -unread_one = "Thông báo, {{count}} chưa đọc" -unread_other = "Thông báo, {{count}} chưa đọc" - -[portal.notifications.count] -allRead = "tất cả đã đọc" -loading = "đang tải" -new_one = "{{count}} mới" -new_other = "{{count}} mới" - -[portal.notifications.empty] -description = "Không có thông báo mới." -title = "Bạn đã xem hết" +[portal.nav.section] +platform = "Nền tảng PDF" +processor = "Bộ xử lý PDF" [portal.pipelines] subtitle = "Mọi pipeline tài liệu tự động trên backend: một chuỗi thao tác có thứ tự trên một tập nguồn, chạy theo trigger. Nhấp vào một hàng để xem các bước và nguồn của nó." @@ -7218,54 +7727,87 @@ title = "Pipeline" newPipeline = "Pipeline mới" [portal.pipelines.builder] -addStep = "Thêm công cụ" +activate = "Activate" back = "Quay lại pipeline" +cannotFollow = "Không thể nhận {{produced}}" +chooseAccount = "Chọn tài khoản" +chooseDestination = "Chọn đích đến" +chooseOperation = "Chọn việc bước này thực hiện" +chooseSource = "Chọn nguồn" discard = "Hủy thay đổi" -enabled = "Đã bật" +inputs = "Đầu vào" +inputSource = "Nguồn đầu vào" +inputTrigger = "Kích hoạt" keepEditing = "Tiếp tục chỉnh sửa" -needsUpload = "Cần một file đã upload" +moreActions = "More actions" +needsConfiguring = "Cần thiết lập" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "Không có công cụ nào khớp với tìm kiếm của bạn." -pipelineSettings = "Cài đặt pipeline" +pause = "Pause" +rename = "Rename pipeline" searchTools = "Tìm kiếm công cụ" -selectToolBody = "Thêm công cụ để xây dựng pipeline của bạn." -selectToolTitle = "Chưa có công cụ" -toolSettings = "Cài đặt công cụ" +sendToSystem = "Gửi đến hệ thống khác" +stepsIncompatible = "Các bước này không thể chạy trên kết quả mà bước trước tạo ra: {{tools}}." +stepsNeedSetup = "Các bước này vẫn cần thiết lập trước khi lưu: {{tools}}." +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "Thao tác không được nhận dạng, giữ nguyên." unsavedBody = "Bạn có thay đổi chưa lưu. Lưu trước khi rời đi hay hủy chúng?" unsavedTitle = "Thay đổi chưa lưu" -uploadUnsupported = "File đã upload chưa được hỗ trợ trong pipeline, vì vậy không thể lưu các bước này: {{tools}}." usesDefaults = "Chạy với cài đặt mặc định" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "Kết hợp mọi file từ bước trước" +fan-out = "Chạy một lần cho mỗi file từ bước trước" +format-mismatch = "Cần {{accepts}}, nhưng bước trước tạo ra {{produced}}" +output-uncertain = "Có thể không chạy: đầu ra của bước trước phụ thuộc vào cách thiết lập" +source-mismatch = "Cần {{accepts}}, nhưng đầu vào của pipeline này là {{produced}}" +undeclared-operation = "Không thể kiểm tra bước này chấp nhận gì" [portal.pipelines.composer] addTool = "Thêm công cụ" -cancel = "Hủy" -chainEmpty = "Thêm công cụ để bắt đầu xây dựng pipeline của bạn." create = "Tạo pipeline" -directory = "Thư mục đầu ra" -directoryHelp = "Đường dẫn tuyệt đối trên server. Phải nằm trong các thư mục được phép đã cấu hình." +createPaused = "Create paused" editingUnsupported = "Chưa hỗ trợ hiển thị các tham số công cụ này để chỉnh sửa." -moveDown = "Di chuyển xuống" -moveUp = "Di chuyển lên" +editSource = "Edit source" name = "Tên" namePlaceholder = "ví dụ: Quét che giấu" -noSources = "Chưa có nguồn nào được kết nối. Pipeline vẫn có thể chạy trên các file được cung cấp trực tiếp cho nó." noToolSettings = "Công cụ này không có cài đặt có thể cấu hình." -operations_one = "Thao tác ({{count}})" -operations_other = "Thao tác ({{count}})" output = "Đầu ra" -removeStep = "Xóa thao tác" save = "Lưu thay đổi" scheduleEvery = "Chạy mỗi" -sources = "Nguồn" -sourcesLoading = "Đang tải nguồn..." trigger = "Trigger" triggerManual = "Chỉ thủ công" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "ngày" hours = "giờ" minutes = "phút" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "Xóa \"{{name}}\"? Không thể hoàn tác thao tác này." cancel = "Hủy" @@ -7273,28 +7815,62 @@ confirm = "Xóa" title = "Xóa pipeline?" [portal.pipelines.detail] +clearHistory = "Xóa lịch sử" delete = "Xóa pipeline" run = "Chạy ngay" [portal.pipelines.empty] action = "Tạo pipeline" +connectSource = "Kết nối một nguồn" description = "Tạo pipeline đầu tiên của bạn: chọn các nguồn mà nó chạy, nối chuỗi các thao tác và chọn nơi đầu ra sẽ đến." title = "Chưa có pipeline" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "Đang hoạt động" paused = "Đã tạm dừng" total = "Pipeline" -[portal.pipelines.output] -folder = "Ghi vào thư mục" -inline = "Trả về file" - [portal.pipelines.run] +allProcessed_one = "Không có gì để chạy: {{count}} tài liệu của nguồn đã được xử lý." +allProcessed_other = "Không có gì để chạy: tất cả {{count}} tài liệu trong các nguồn đã được xử lý." completed_one = "Lượt chạy đã hoàn tất." completed_other = "Tất cả {{count}} lượt chạy đã hoàn tất." empty = "Không có gì để chạy: các nguồn không có tài liệu để xử lý." failed = "Lượt chạy thất bại: {{error}}" +inFlight = "Không có gì mới để chạy: tài liệu vẫn đang được xử lý từ một lần chạy trước đó." +parked_one = "Không có gì để chạy: {{count}} tài liệu đã thất bại trước đó và đang được tạm dừng. Khắc phục nguyên nhân, rồi xóa lịch sử để thử lại." +parked_other = "Không có gì để chạy: {{count}} tài liệu đã thất bại trước đó và đang được tạm dừng. Khắc phục nguyên nhân, rồi xóa lịch sử để thử lại." running = "Lượt chạy đã bắt đầu; vẫn đang tiến hành." timeout = "Lượt chạy mất nhiều thời gian hơn dự kiến; nó có thể vẫn hoàn tất trong nền." @@ -7307,6 +7883,7 @@ name = "Pipeline" sources = "Nguồn" status = "Trạng thái" steps = "Bước" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "Theo dõi thư mục" @@ -7314,12 +7891,18 @@ manual = "Thủ công" schedule = "Theo lịch" [portal.policies] +defaultName = "Chính sách {{category}}" subtitle = "Các tự động hóa thường trực áp dụng một pipeline công cụ trên mọi tài liệu. Mỗi chính sách kích hoạt khi upload hoặc export, chạy chuỗi công cụ của nó và lưu phiên bản đã áp dụng cùng với bản gốc." title = "Chính sách" [portal.policies.card] comingSoon = "Nâng cấp lên Enterprise" notSetUp = "Chưa thiết lập" +requiresAiEngine = "Yêu cầu AI engine" + +[portal.policies.categories.classification] +desc = "Xác định loại của từng tài liệu khi tải lên và gắn thẻ metadata để lưu trữ và tìm kiếm." +label = "Phân loại" [portal.policies.categories.compliance] desc = "Áp dụng các yêu cầu HIPAA, GDPR, SOC 2 hoặc FedRAMP trên mọi tài liệu." @@ -7344,6 +7927,13 @@ label = "Bảo mật" [portal.policies.config] scopeAll = "Tất cả tài liệu" +[portal.policies.config.classification] +summary = "Phân loại mọi tài liệu đã tải lên theo nhãn của nhóm bạn và gắn thẻ metadata của tài liệu." + +[portal.policies.config.classification.rules] +0 = "Phân loại" +1 = "Gắn thẻ metadata" + [portal.policies.config.compliance] summary = "Xác thực tài liệu theo các framework quy định trước khi chúng rời khỏi hệ thống." @@ -7371,6 +7961,29 @@ minConfidence = "Độ tin cậy tối thiểu" 2 = "Đặt tên" 3 = "Chuẩn hóa" +[portal.policies.config.purview] +label = "Áp dụng nhãn độ nhạy Microsoft Purview" + +[portal.policies.config.purview.fields] +connection = "Purview tenant" +labelId = "Label ID" +labelIdHelp = "GUID của nhãn, từ cổng Purview. Việc áp dụng nhãn ghi metadata của nhãn vào PDF; nó không thể mã hóa, việc này cần Microsoft client." +labelName = "Tên nhãn (tùy chọn)" +labelNameHelp = "Được ghi cùng ID để người dùng và công cụ có thể nhận diện nhãn." + +[portal.policies.config.purview.fields.method] +help = "Purview phân biệt nhãn do chính sách áp dụng với nhãn do một người chọn." +label = "Cách ghi nhãn" +privileged = "Như được một người chọn" +standard = "Như được áp dụng tự động" + +[portal.policies.config.purviewRead] +label = "Đọc nhãn Microsoft Purview" + +[portal.policies.config.purviewRead.fields] +connection = "Purview tenant" +connectionHelp = "Đọc nhãn mà tài liệu đã có, để các bước sau có thể hành động dựa trên đó." + [portal.policies.config.retention] summary = "Áp dụng thời gian giữ tài liệu, khi nào lưu trữ và khi nào xóa." @@ -7438,6 +8051,7 @@ title = "Chưa có hoạt động" [portal.policies.endpoints] addWatermark = "Hình mờ" autoRedact = "Che PII" +classifyAndLabel = "Phân loại" compressPdf = "Nén" flatten = "Làm phẳng" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "Các chính sách của bạn đã được lưu và sẽ xuất retry = "Thử lại" title = "Backend không khả dụng" +[portal.policies.operations] +change = "Thay đổi việc bước này thực hiện" +noResults = "Không có bước nào khớp. Hãy thử tên sản phẩm, hoặc \"scan\", \"notify\", \"attach\"." +searchPlaceholder = "Tìm kiếm bước - thử \"scan\", \"attach\" hoặc tên sản phẩm" + +[portal.policies.operations.bodyMode] +binary = "Byte file thô" +json = "Bên trong payload JSON" +multipart = "Dưới dạng file upload (multipart)" + +[portal.policies.operations.clamavScan] +description = "Máy chủ ClamAV của bạn kiểm tra tài liệu; không có gì rời khỏi mạng của bạn." +label = "Quét virus (self-hosted)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "Từ chối tài liệu chứa macro, file thực thi, script hoặc đối tượng nhúng." +label = "Chặn nội dung hoạt động" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive kiểm tra tài liệu và lần chạy sẽ dừng nếu tài liệu không sạch." +label = "Quét virus" + +[portal.policies.operations.confluenceAttach] +description = "Lưu tài liệu đã xử lý vào một trang." +label = "Đính kèm vào trang Confluence" + +[portal.policies.operations.consignoSubmit] +description = "Gửi tài liệu đến ConsignO Cloud để ký." +label = "Gửi để ký (ConsignO)" +note = "Gửi tài liệu. Hiện chưa hỗ trợ truy xuất bản đã ký, vì một bước không thể chuyển kết quả cho bước sau." + +[portal.policies.operations.customApiCall] +description = "Tự tạo lệnh gọi: đường dẫn, phương thức, body và xử lý phản hồi." +label = "Gọi API tùy chỉnh" + +[portal.policies.operations.discordNotify] +description = "Thông báo cho một kênh rằng chính sách đã xử lý một tài liệu." +label = "Đăng tin nhắn lên Discord" + +[portal.policies.operations.elasticIndex] +description = "Ghi lại việc chính sách đã làm dưới dạng tài liệu có thể tìm kiếm." +label = "Lập chỉ mục sự kiện kiểm toán trong Elasticsearch" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart phù hợp với API upload; JSON phù hợp với API muốn file nằm trong payload; binary gửi byte thô." +label = "Cách gửi tài liệu" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "Body JSON được gửi nguyên trạng. {{document.base64}} chứa file; {{document.*}} và {{run.*}} được điền cho từng tài liệu." +label = "Body JSON" + +[portal.policies.operations.fields.connection] +label = "Tài khoản" + +[portal.policies.operations.fields.domain] +label = "Mailgun domain" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "Trường form mà API mong đợi chứa tài liệu." +label = "Tên trường file" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "Từ" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "Một đối tượng JSON gồm các cặp tên/giá trị header. Thông tin xác thực của kết nối được thêm sẵn cho bạn." +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "Được gửi làm nội dung tin nhắn. {{document.*}} và {{run.*}} được điền cho từng tài liệu." +label = "Tin nhắn" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "Được nối vào URL cơ sở của kết nối. Nó không bao giờ có thể truy cập host khác." +label = "Đường dẫn" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "Vị trí trong drive để ghi. {{document.filename}} được điền cho từng tài liệu." +label = "Đường dẫn đích" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Báo cáo giữ nguyên tài liệu. Thay thế sẽ đổi tài liệu bằng bất kỳ nội dung nào trả về." +label = "Làm gì với phản hồi" + +[portal.policies.operations.fields.signerEmail] +label = "Email của người ký" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "Chủ đề" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio phân tích văn bản này. Dùng {{document.title}} hoặc dán văn bản bạn muốn kiểm tra." +label = "Văn bản cần kiểm tra" + +[portal.policies.operations.fields.to] +label = "Đến" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Tên người dùng Nextcloud" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "Thông báo cho một space rằng chính sách đã xử lý một tài liệu." +label = "Đăng tin nhắn lên Google Chat" + +[portal.policies.operations.jiraAttach] +description = "Lưu tài liệu đã xử lý vào một issue." +label = "Đính kèm vào Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "Gửi tài liệu đã xử lý dưới dạng tệp đính kèm." +label = "Gửi tài liệu qua email (Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "Ghi tài liệu đã xử lý vào một thư mục." +label = "Tải lên Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "Máy chủ Presidio của bạn báo cáo dữ liệu cá nhân mà nó tìm thấy." +label = "Phát hiện dữ liệu cá nhân" +note = "Presidio đọc văn bản, không đọc file. Cung cấp văn bản bạn muốn kiểm tra — bản thân tài liệu không được tải lên." + +[portal.policies.operations.responseMode] +replace = "Thay thế tài liệu bằng phản hồi" +report = "Giữ tài liệu, ghi lại phản hồi" + +[portal.policies.operations.sendgridEmail] +description = "Gửi tài liệu đã xử lý dưới dạng tệp đính kèm." +label = "Gửi tài liệu qua email (SendGrid)" + +[portal.policies.operations.slackNotify] +description = "Thông báo cho một kênh rằng chính sách đã xử lý một tài liệu." +label = "Đăng tin nhắn lên Slack" + +[portal.policies.operations.splunkEvent] +description = "Ghi lại việc chính sách đã làm, nơi kiểm toán viên của bạn đã xem." +label = "Gửi sự kiện kiểm toán đến Splunk" + +[portal.policies.operations.sumologicEvent] +description = "Ghi lại việc chính sách đã làm vào một HTTP collector." +label = "Gửi sự kiện kiểm toán đến Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "Thông báo cho một kênh rằng chính sách đã xử lý một tài liệu." +label = "Đăng tin nhắn lên Teams" + +[portal.policies.operations.webhookPost] +description = "Gửi chính tài liệu đến URL bạn chọn." +label = "Post tài liệu đến webhook" + +[portal.policies.operations.zapierNotify] +description = "Chuyển chi tiết lần chạy cho automation của bạn." +label = "Kích hoạt Zap hoặc Make scenario" + [portal.policies.stats] activeFor = "Hoạt động" dataProcessed = "Dữ liệu đã xử lý" @@ -7473,6 +8282,13 @@ label = "Tài liệu đã áp dụng" description = "Đã cấu hình nhưng không kích hoạt" label = "Đã tạm dừng" +[portal.policies.table] +appliesTo = "Áp dụng cho" +docs = "Tài liệu đã thực thi" +enforces = "Thực thi" +policy = "Chính sách" +status = "Trạng thái" + [portal.policies.wizard.actions] back = "Quay lại" cancel = "Hủy" @@ -7480,6 +8296,10 @@ continue = "Tiếp tục" enablePolicy = "Bật chính sách" saveChanges = "Lưu thay đổi" +[portal.policies.wizard.capability.classify] +desc = "Xác định loại tài liệu từ nhãn của nhóm bạn và gắn thẻ cho tài liệu, để tài liệu được lưu và tìm kiếm theo danh mục." +label = "Phân loại tài liệu" + [portal.policies.wizard.capability.compress] desc = "Nén tài liệu thành kích thước file nhỏ hơn." label = "Giảm kích thước file" @@ -7500,10 +8320,18 @@ label = "Che thông tin nhạy cảm" desc = "Xóa JavaScript ẩn để không có gì có thể tự động chạy khi tài liệu được mở." label = "Loại bỏ nội dung hoạt động" +[portal.policies.wizard.capability.timestampPdf] +desc = "Chứng minh tài liệu đã tồn tại đúng ở dạng này tại một thời điểm, bằng cách dùng cơ quan timestamp độc lập. Chỉ hash được gửi - tài liệu không bao giờ rời khỏi máy chủ của bạn." +label = "Thêm timestamp tin cậy" + [portal.policies.wizard.capability.watermark] desc = "Đóng dấu một dấu hiển thị (ví dụ: “Bảo mật”) trên mọi trang." label = "Áp dụng hình mờ" +[portal.policies.wizard.classification] +description = "Mọi tài liệu đã tải lên được phân loại theo các nhãn tích hợp và được gắn thẻ với các loại phù hợp. Bộ nhãn được chia sẻ trong toàn bộ nhóm của bạn." +labelsHeading = "Nhãn phân loại" + [portal.policies.wizard.errors] noTools = "Trước tiên hãy bật ít nhất một công cụ trong workflow." saveFailed = "Không thể lưu chính sách. Vui lòng thử lại." @@ -7548,73 +8376,118 @@ setUp = "Thiết lập chính sách {{category}}" [portal.policies.wizard.workflow] description = "Chọn những gì chính sách này thực hiện với mỗi tài liệu mà nó xử lý." -[portal.policySummary] -activeSummary = "{{active}} / {{total}} đang hoạt động" -noRule = "Chưa áp dụng quy tắc nào" -subtitle = "Các tự động hóa cố định mà mọi tài liệu đều đi qua, bất kể pipeline nào xử lý nó." -title = "Những gì chạy trên PDF của bạn" - [portal.policySummary.action] -comingSoon = "Nâng cấp lên Enterprise" -configure = "Cấu hình" setUp = "Thiết lập" -[portal.policySummary.column] -activeRule = "Quy tắc đang hoạt động" -policy = "Chính sách" -status = "Trạng thái" +[portal.processorFlow] +footnote = "Số đếm trong 24 giờ qua. Tốc độ luồng chỉ mang tính minh họa." +liveBadge = "Trực tiếp" +stats = "{{connected}} đã kết nối · {{processed}} PDF đã xử lý" +title = "Bộ xử lý PDF" -[portal.policySummary.empty] -description = "Sau khi chính sách được cấu hình, các danh mục sẽ xuất hiện ở đây." -title = "Chưa có chính sách" +[portal.processorFlow.lens] +ariaLabel = "Chế độ xem" +flow = "Luồng" +sankey = "Sankey" -[portal.policySummary.state] -active = "Hoạt động" -off = "Tắt" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "Thất bại" +heading = "Kết quả" +success = "Đã gửi" + +[portal.processorFlow.policies] +activeCount = "{{n}} đang hoạt động" +count = "{{n}} · 24h" +heading = "Chính sách" +setUp = "Thiết lập" soon = "Sắp có" -[portal.processingStatus] -managePlan = "Quản lý gói" -volumeSuffix = "PDF đã xử lý · 30 ngày qua" +[portal.processorFlow.sankey] +waist = "Chính sách · {{n}} đang hoạt động" + +[portal.processorFlow.sankey.empty] +description = "Kết nối một nguồn và bật một chính sách để xem tài liệu chảy qua bộ xử lý." +title = "Chưa có luồng" + +[portal.processorFlow.sources] +comingSoonTag = "Kết nối" +editor = "Stirling PDF Editor" +heading = "Nguồn" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Nhận Email" [portal.procurement] -reset = "Đặt lại mua sắm (demo)" subtitle = "Giúp đội ngũ của bạn được đánh giá, ký hợp đồng và triển khai. Mọi tài liệu ở cùng một nơi." title = "Mua sắm" -[portal.procurement.action] -download = "Tải xuống" -pay = "Thanh toán ngay" -request = "Yêu cầu" -sign = "Xem lại & ký" -upload = "Tải lên" - [portal.procurement.agreement] agreeCta = "Đồng ý & đăng ký" +confidential = "Bảo mật" confirm = "Tôi đã đọc và đồng ý với Thỏa thuận Stirling Enterprise." -eyebrow = "Thỏa thuận" -intro = "Một thỏa thuận kết hợp bao gồm giao dịch của bạn: Master Service Agreement, Order Form, EULA và Data Processing Agreement. Hãy xem lại, sau đó đồng ý chấp nhận báo giá thành một đăng ký cam kết." -title = "Xem lại thỏa thuận enterprise của bạn" +docName = "Thỏa thuận Stirling Enterprise" +docSub = "MSA, Order Form, EULA và DPA, kết hợp trong một chữ ký." +download = "Tải xuống" +downloadDraftError = "Không thể tạo PDF thỏa thuận - trình kết xuất tài liệu không khả dụng. Vui lòng thử lại sau hoặc liên hệ bộ phận hỗ trợ." +downloadError = "Không thể tải xuống thỏa thuận đã ký. Vui lòng thử lại." +legalName = "Tên pháp nhân" +legalNamePlaceholder = "Pháp nhân sẽ ký" +loadError = "Không thể tải thỏa thuận. Vui lòng thử lại." +loading = "Đang tải thỏa thuận..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "Yêu cầu thay đổi" +scrollHint = "Cuộn để xem lại" +signatory = "Tên người ký" +signatoryPlaceholder = "Họ và tên" +signatoryTitle = "Chức danh người ký" +signatoryTitlePlaceholder = "ví dụ: General Counsel" +signError = "Không thể ghi nhận chữ ký của bạn. Vui lòng thử lại." [portal.procurement.builder] addons = "Tiện ích bổ sung" +addressLine1 = "Địa chỉ dòng 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "Địa chỉ dòng 2" +addressLine2Placeholder = "Suite, tầng, tòa nhà" back = "Quay lại" businessName = "Tên doanh nghiệp" businessNamePlaceholder = "Công ty của bạn" +city = "Thành phố" +cityPlaceholder = "San Francisco" +completeRequired = "Vui lòng hoàn tất các trường bắt buộc (đánh dấu *) với email hợp lệ trước khi tạo báo giá." +contactEmail = "Email liên hệ" +contactEmailPlaceholder = "jane@acme.com" +contactName = "Tên liên hệ" +contactNamePlaceholder = "Jane Doe" continue = "Tiếp tục" -country = "Quốc gia" -countryEuro = "Khu vực đồng Euro (EUR €)" -countryUK = "Vương quốc Anh (GBP £)" -countryUS = "Hoa Kỳ (USD $)" +done = "Xong" eula = "Tôi đã đọc và đồng ý với Stirling Enterprise EULA. EULA này điều chỉnh thỏa thuận được tạo từ báo giá này." generate = "Tạo báo giá" -included = "Đã bao gồm" indemnification = "Bồi thường IP" indemnificationSub = "Chúng tôi bảo vệ các khiếu nại IP đủ điều kiện, theo EULA" -offlineLicense = "Giấy phép offline / air-gapped" -offlineLicenseSub = "File giấy phép có thể tải xuống cho một phiên bản tự host air-gapped" +paperEyebrow = "Báo giá Enterprise" +paperFor = "Chuẩn bị cho" +pdfSize = "Kích thước PDF" +poNumber = "Số PO" +poNumberPlaceholder = "Tùy chọn" +postalCode = "Mã bưu chính" +postalCodePlaceholder = "94105" +posture = "Quản trị" +posture_count = "~{{count}} chính sách" +posture_essentials = "Thiết yếu" +posture_essentialsSub = "Phân loại + Chia sẻ — mặc định" +posture_governed = "Được quản trị" +posture_governedSub = "Thêm Bảo mật và Định tuyến" +posture_regulated = "Được quản lý theo quy định" +posture_regulatedSub = "Mọi danh mục — Tuân thủ, Lưu giữ, Nạp dữ liệu" qbr = "Đánh giá kinh doanh hàng quý" qbrSub = "SE của bạn xem xét việc sử dụng và lộ trình mỗi quý" +region = "Tiểu bang / vùng" +regionPlaceholder = "California" running = "{{annual}} / năm · {{years}} năm {{tcv}}" s1Sub = "Đội ngũ của bạn và số PDF bạn dự kiến xử lý mỗi năm." s1Title = "Bạn sẽ xử lý bao nhiêu?" @@ -7623,6 +8496,12 @@ s2Title = "Cam kết và dịch vụ" s3Sub = "Cho báo giá và thỏa thuận mà nó tạo ra." s3Title = "Thông tin của bạn" serviceLevel = "Cấp dịch vụ" +size_compact = "Gọn nhẹ" +size_compactSub = "Chủ yếu là văn bản, dưới 1 MB" +size_heavy = "Nặng" +size_heavySub = "Đã quét hoặc nhiều hình ảnh, 5 MB+" +size_standard = "Tiêu chuẩn" +size_standardSub = "Kết hợp văn bản và hình ảnh, 1 đến 5 MB" slDedicated = "Chuyên trách" slDedicatedSub = "4 giờ làm việc · quản lý tài khoản chuyên trách · +30%" slPriority = "Ưu tiên" @@ -7630,6 +8509,8 @@ slPrioritySub = "Cùng ngày làm việc · CSM được chỉ định · +15%" slStandard = "Tiêu chuẩn" slStandardSub = "Ngày làm việc tiếp theo · CSM dùng chung · đã bao gồm" stepOf = "Bước {{n}} trên {{total}}" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "Tùy chọn" term = "Thời hạn" termDiscount = "Đã áp dụng giảm giá {{pct}}% cho cam kết nhiều năm" title = "Tạo báo giá của bạn" @@ -7637,6 +8518,7 @@ training = "Triển khai & đào tạo" trainingSub = "Các phiên trực tiếp để giúp đội ngũ của bạn bắt đầu" users = "Tổng số người dùng" usersPlaceholder = "ví dụ: 250" +viewEula = "Đọc" volEstimated = "Ước tính từ {{count}} người dùng (~2.000 PDF mỗi người, bao gồm tự động hóa). Chỉnh sửa nếu bạn biết rõ hơn." volManual = "Đang dùng số liệu của bạn. Ước tính lại từ quy mô đội ngũ bất cứ lúc nào." volNoUsers = "Không chắc? Nhập quy mô đội ngũ của bạn và chúng tôi sẽ ước tính." @@ -7645,55 +8527,55 @@ volumePlaceholder = "ví dụ: 1.000.000" years_one = "{{count}} năm" years_other = "{{count}} năm" -[portal.procurement.docs] -count_one = "{{count}} tài liệu" -count_other = "{{count}} tài liệu" -done = "Xong" -here = "Bạn đang ở đây" -hide = "Ẩn" -optional = "Tùy chọn" -paidAddon = "Tiện ích bổ sung trả phí" -show = "Hiển thị" -subtitle = "Mọi thứ bạn cần ở từng bước của hành trình, được hiển thị khi giao dịch tiến triển." -supportingSubtitle = "SOC 2, đánh giá bảo mật, biểu mẫu thuế và nhiều hơn nữa, sẵn sàng khi đội ngũ bảo mật hoặc mua sắm của bạn yêu cầu. Một số có phí một lần." -supportingTitle = "Hỗ trợ việc đánh giá của bạn" +[portal.procurement.documents] +agreement = "Thỏa thuận Enterprise" +agreementSub = "MSA, Order Form và DPA trong một chữ ký." +download = "Tải xuống" +eula = "EULA & Điều khoản thương mại" +eulaSub = "Điều khoản phần mềm và thương mại tự phục vụ." +invoice = "Hóa đơn" +invoiceSub = "Hóa đơn đăng ký đầu tiên của bạn." +laterInvoice = "Có sẵn sau khi bạn chấp nhận" +laterQuote = "Có sẵn khi báo giá của bạn được phát hành" +quote = "Báo giá" +quoteSub = "Báo giá enterprise chi tiết của bạn." +sla = "Phụ lục SLA" +slaSub = "Các cấp hỗ trợ và mục tiêu phản hồi." +subprocessors = "Subprocessors" +subprocessorsSub = "Bên thứ ba xử lý dữ liệu thay mặt bạn." +subtitle = "Giấy tờ giao dịch của bạn, có sẵn bất cứ lúc nào." title = "Tài liệu" -upcoming = "Sắp tới" +view = "Xem" [portal.procurement.error] title = "Đã xảy ra lỗi" [portal.procurement.hero] -company = "Giao dịch enterprise của bạn" +barAria = "Giai đoạn {{current}} trong {{total}}" ctaAgreement = "Xem lại & ký thỏa thuận" -ctaLive = "Bạn đã hoạt động" -ctaPayment = "Thêm thanh toán" +ctaExploring = "Thiết lập bản dùng thử của bạn" ctaQuote = "Xem lại báo giá của bạn" +ctaReviewQuote = "Xem lại báo giá" ctaTrial = "Tạo báo giá của bạn" +documents = "Tài liệu" eyebrow = "Mua sắm Enterprise" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "Mời đồng đội" -keyDocs = "Tài liệu chính" -nextStep = "Bước tiếp theo: {{action}}" -notStarted = "Chưa bắt đầu" +licenseKey = "Khóa giấy phép" +liveSub = "Tổ chức của bạn đã được cấp phát và giấy phép của bạn đang hoạt động." +liveTitle = "Bạn đang chạy Stirling Enterprise" +next = "Tiếp theo: {{stage}}" open = "Mở mua sắm" scheduleCall = "Lên lịch cuộc gọi" -setup1Sub = "Mời đồng đội của bạn" -setup1Title = "Triển khai PDF Editor" -setup2Sub = "Bật xử lý trên các trình chỉnh sửa và nguồn khác" -setup2Title = "Kết nối PDF Processor" -setup3Sub = "Bật Security, Compliance, Routing hoặc Retention khi bạn cần" -setup3Title = "Thêm chính sách được đề xuất" +sentenceAgreement = "xem lại và ký thỏa thuận" +sentenceLive = "cấp phát và triển khai" +sentencePayment = "đơn đặt hàng và thanh toán" +sentenceQuote = "xem lại và chấp nhận báo giá của bạn" +sentenceTrial = "thiết lập không gian làm việc dùng thử của bạn" [portal.procurement.journey] daysLeft_one = "Còn {{count}} ngày" daysLeft_other = "Còn {{count}} ngày" -engineerLabel = "Kỹ sư giải pháp của bạn" -eyebrow = "Triển khai của bạn" -live = "Bạn đã hoạt động trên Stirling Enterprise" -nextStep = "Bước tiếp theo: {{action}}" -subtitle = "Kỹ sư giải pháp của bạn đồng hành ở mọi bước. Mỗi lần một hành động tiếp theo; danh sách kiểm tra đầy đủ ở bên dưới." -title = "Từ dùng thử đến hoạt động, một lộ trình có hướng dẫn" -trialTitle = "Dùng thử Enterprise" [portal.procurement.journeySteps.agreement] blurb = "Một chữ ký bao gồm MSA, order form, EULA và DPA." @@ -7727,6 +8609,9 @@ downloadError = "Chưa thể tạo file giấy phép offline — vui lòng thử downloadOffline = "Tải xuống giấy phép offline (.lic)" hint = "Dán khóa này vào một phiên bản tự host để kích hoạt, hoặc lưu lại cho hồ sơ của bạn. Hãy giữ an toàn." label = "Khóa giấy phép của bạn" +subtitle = "Kích hoạt một phiên bản self-hosted bằng khóa này, hoặc giữ lại để lưu hồ sơ." +title = "Khóa giấy phép của bạn" +trialFileHint = "Đây là file giấy phép dùng thử của bạn. Khi thỏa thuận của bạn đã hoàn tất, hãy quay lại và tải xuống lần nữa — khác với khóa giấy phép trực tuyến, file .lic sẽ không tự cập nhật." [portal.procurement.link] cta = "Liên kết tài khoản" @@ -7739,65 +8624,62 @@ description = "Đăng ký của bạn đang hoạt động và giấy phép củ eyebrow = "Hoạt động" title = "Bạn đã hoạt động trên Stirling Enterprise" -[portal.procurement.locked] -description = "Khóa dùng thử, báo giá theo khối lượng cam kết, thỏa thuận một chữ ký, thanh toán và sổ cái tài liệu của bạn đều ở đây sau khi bạn bắt đầu đánh giá enterprise." -eyebrow = "Chỉ dành cho Enterprise" -talkToSales = "Trao đổi với sales" -title = "Lộ trình mua sắm mở với Enterprise" - [portal.procurement.milestone] -accept = "Chấp nhận & tiếp tục" -description = "Tải xuống PDF để chia sẻ với đội ngũ của bạn, quay lại để chấp nhận khi bạn sẵn sàng, hoặc thực hiện thay đổi." -download = "Tải xuống PDF" downloadError = "Chưa thể tải xuống PDF báo giá — vui lòng thử lại sau giây lát." -edit = "Chỉnh sửa báo giá" -eyebrow = "Báo giá {{number}}" -perYear = " / năm" -preparedFor = "Chuẩn bị cho {{company}}" -tcv = "{{value}} tổng giá trị hợp đồng" -title = "Báo giá của bạn đã sẵn sàng" - -[portal.procurement.modal] -cancel = "Hủy" -chooseFile = "Chọn file" -close = "Đóng" -downloadBody = "Tải xuống của bạn sẽ bắt đầu ngay." -downloadCta = "Tải xuống" -downloadTitle = "Tải xuống" -noFile = "Chưa chọn file" -payBody = "Thanh toán hợp đồng cam kết của bạn bằng thẻ hoặc chuyển khoản ngân hàng qua Stripe. Workspace của bạn sẽ được cấp phát ngay khi thanh toán hoàn tất." -payCta = "Tiếp tục đến Stripe" -payTitle = "Xác nhận thanh toán" -requestBodyFree = "Chúng tôi tạo tài liệu này theo yêu cầu. Xác nhận và kỹ sư giải pháp của bạn sẽ gửi tài liệu ngay sau đó." -requestBodyPaid = "Đây là tiện ích bổ sung trả phí. Xác nhận và kỹ sư giải pháp của bạn sẽ xác định phạm vi và gửi giấy tờ." -requestCta = "Yêu cầu" -requestTitle = "Yêu cầu tài liệu này" -signBody = "Mở Thỏa thuận Stirling Enterprise để ký điện tử: một chữ ký bao gồm MSA, order form, EULA và DPA. Chúng tôi tự động ký đối ứng và bạn chuyển sang thanh toán." -signCta = "Mở để ký" -signTitle = "Xem lại và ký thỏa thuận của bạn" -uploadBody = "Gửi cho chúng tôi PO của bạn và chúng tôi sẽ lập hóa đơn theo điều khoản của bạn. Kéo PDF vào hoặc chọn một file bên dưới." -uploadCta = "Tải lên purchase order" -uploadTitle = "Tải lên purchase order của bạn" [portal.procurement.payment] description = "Báo giá của bạn đã được chấp nhận và giấy phép của bạn đã hoạt động — đội ngũ của bạn có thể bắt đầu ngay. Thanh toán hóa đơn đầu tiên khi bạn sẵn sàng; bạn có thể thanh toán hoặc tải xuống tại đây, không cần email." +downloadAgreement = "Tải xuống thỏa thuận đã ký" downloadInvoice = "Tải xuống hóa đơn" -simulate = "Mô phỏng đã nhận thanh toán (demo)" +eyebrow = "Thanh toán" title = "Đã tạo đăng ký" viewInvoice = "Xem & thanh toán hóa đơn" +[portal.procurement.review] +acceptCta = "Chấp nhận báo giá" +annual = "Phí hằng năm (năm 1)" +downloadCta = "Tải xuống báo giá" +poNumber = "Đơn đặt hàng: {{po}}" +renewal = "Gia hạn ở mức {{amount}}/năm sau thời hạn (+{{pct}}% CPI)" +tcv = "Thời hạn {{years}} năm, trả trước toàn bộ: {{tcv}}" +validUntil = "Có hiệu lực đến {{date}}" + [portal.procurement.schedule] fallback = "Không thể tải trình lên lịch." fallbackLink = "Mở lên lịch trong tab mới" +loading = "Đang tải trình đặt lịch..." subtitle = "Kỹ sư giải pháp của bạn sẽ hướng dẫn đội ngũ của bạn qua quá trình triển khai. Chọn thời gian phù hợp với bạn." title = "Lên lịch cuộc gọi" -[portal.procurement.status] -action = "Cần hành động" -available = "Có sẵn" -complete = "Hoàn tất" -pending = "Đang chờ" -request = "Theo yêu cầu" +[portal.procurement.setup] +airgap = "Air-gapped" +airgapSub = "Kín, không có kết nối outbound" +back = "Quay lại" +businessName = "Tên doanh nghiệp" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "Được quản lý, bắt đầu nhanh nhất" +continue = "Tiếp tục" +deployment = "Nên triển khai ở đâu?" +eula = "Tôi đồng ý với Stirling EULA & Điều khoản thương mại." +fullName = "Họ và tên" +fullNamePlaceholder = "Tên của bạn" +invites = "Mời đồng đội · tùy chọn" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "Lên lịch cuộc gọi" +seats = "Người dùng thử" +seatsPlaceholder = "ví dụ: 15" +selfhost = "Self-hosted" +selfhostSub = "Docker hoặc Kubernetes" +start = "Bắt đầu dùng thử" +stepOf = "Bước {{n}} trong {{total}}" +subtitle = "Miễn phí trong 14 ngày, không cần thẻ." +subtitleDetails = "Một vài thông tin cho báo giá và thỏa thuận của bạn." +talkFirst = "Muốn trao đổi trước?" +title = "Thiết lập bản dùng thử của bạn" +viewEula = "Đọc" +workEmail = "Email công việc" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "Gia hạn sẽ thêm 7 ngày và thông báo cho kỹ sư giải pháp của bạn." @@ -7808,109 +8690,113 @@ maxed = "Đã đạt giới hạn" subtitle = "Bản dùng thử miễn phí của bạn kéo dài đến {{date}}. Không cần thẻ." title = "Dùng thử Enterprise" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "Giá theo khối lượng cam kết, SSO + SCIM + RBAC toàn tổ chức, audit bất biến 90 ngày và SE chuyên trách." -homeCta = "Bắt đầu dùng thử →" -homeHeadline = "Xử lý hàng triệu PDF." - -[portal.recentActivity] -title = "Hoạt động gần đây" -viewAll = "Xem tất cả" - -[portal.recentActivity.empty] -description = "Các lần chạy pipeline, triển khai và sự kiện agent sẽ xuất hiện ở đây." -title = "Chưa có gì ở đây" - -[portal.search] -ariaLabel = "Tìm kiếm" -placeholder = "Tìm kiếm Stirling — endpoint, pipeline, tài liệu…" - -[portal.search.empty] -noActionsDescription = "Hành động nhanh sẽ xuất hiện ở đây khi có sẵn." -noActionsTitle = "Không có hành động nhanh" -noMatches = "Không có kết quả cho \"{{query}}\"" -noMatchesDescription = "Thử từ khóa khác hoặc duyệt catalogue." - [portal.settings.groups] admin = "Quản trị" [portal.settings.sections] account-link = "Liên kết tài khoản" -[portal.shell.header] -accountFallback = "Tài khoản" -accountMenu = "Menu tài khoản" -darkMode = "Chế độ tối" -lightMode = "Chế độ sáng" -search = "Tìm kiếm" -searchPlaceholder = "Tìm kiếm…" -signOut = "Đăng xuất" -switchToDark = "Chuyển sang giao diện tối" -switchToLight = "Chuyển sang giao diện sáng" - [portal.shell.sidebar] appEditor = "Trình chỉnh sửa" appProcessor = "Trình xử lý" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} tài liệu" linkAccount = "Liên kết tài khoản Stirling" -planEditor = "Gói Editor" -planEnterprise = "Gói Enterprise" -planProcessor = "Gói Processor" primaryNav = "Điều hướng chính" switchApp = "Chuyển ứng dụng" +[portal.shell.topbar] +closeNav = "Đóng điều hướng" +openNav = "Mở điều hướng" +search = "Tìm kiếm" + [portal.sources] subtitle = "Các kết nối đầu vào có thể tái sử dụng để đưa tài liệu vào Stirling. Cấu hình một kết nối một lần, sau đó tham chiếu nó từ bất kỳ số lượng chính sách nào. Nhấp vào một hàng để xem cấu hình và những chính sách sử dụng nó." title = "Nguồn" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Kết nối nguồn" +[portal.sources.builder] +back = "Quay lại nguồn" +backToTypes = "Tất cả loại nguồn" +cancel = "Hủy" +chooseHint = "Chọn nơi tài liệu đến từ. Các connector bị làm mờ đang được phát triển." +comingSoon = "Sắp ra mắt" +comingSoonHeading = "Sắp ra mắt" +create = "Tạo nguồn" +createTitle = "Kết nối một nguồn" +delete = "Xóa" +editTitle = "Chỉnh sửa nguồn" +enabled = "Đã bật" +save = "Lưu thay đổi" + +[portal.sources.builder.folderAccess] +description = "Tự động hóa thư mục chỉ có thể dùng các thư mục mà quản trị viên đã cho phép. Thêm thư mục trong cài đặt Quyền truy cập thư mục, rồi thử lại." +openSettings = "Cài đặt Quyền truy cập thư mục" +title = "Thư mục này không được phép" + [portal.sources.delete] body = "Xóa \"{{name}}\"? Không thể hoàn tác. Các chính sách tham chiếu đến nguồn này sẽ cần được cập nhật." cancel = "Hủy" confirm = "Xóa" title = "Xóa nguồn?" -[portal.sources.detail] -closeAriaLabel = "Đóng chi tiết" -delete = "Xóa nguồn" -docs24h = "24 giờ qua" -docs30d = "30 ngày qua" -docsTotal = "Tổng đã thấy" -docsTrend = "Tài liệu trong 30 ngày qua" -documents = "Tài liệu" -edit = "Chỉnh sửa" -notReferenced = "Không được chính sách nào tham chiếu, nên có thể xóa an toàn." -pause = "Tạm dừng" -resume = "Tiếp tục" -subtitle = "{{type}} · {{status}}" -usedBy = "Được sử dụng bởi" - -[portal.sources.empty] -description = "Kết nối một thư mục (và sắp tới là cloud storage) để chính sách của bạn có nơi lấy tài liệu." -title = "Chưa kết nối nguồn nào" - [portal.sources.kpi] inUse = "Đang sử dụng" total = "Kết nối" unused = "Chưa dùng" +[portal.sources.networkFields.connection] +helperText = "Kết nối đã lưu với địa chỉ máy chủ và thông tin xác thực. Được tái sử dụng bởi mọi nguồn tham chiếu đến nó." +label = "Kết nối" + +[portal.sources.networkFields.directory] +helperText = "Thư mục trên máy chủ để thăm dò, tương đối với thư mục home đăng nhập hoặc gốc share. Để trống cho thư mục gốc." +label = "Thư mục" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume xóa từng file khỏi máy chủ sau khi mọi chính sách đã xử lý file đó." +label = "Chế độ đọc" + +[portal.sources.networkFields.mode.options] +consume = "Consume: xử lý mỗi file một lần" +snapshot = "Snapshot: đọc lại thư mục mỗi lần chạy" + +[portal.sources.networkFields.recursive] +label = "Độ sâu thư mục" + +[portal.sources.networkFields.recursive.options] +all = "Bao gồm thư mục con" +top = "Chỉ cấp trên cùng" + [portal.sources.status] active = "Hoạt động" disabled = "Đã tắt" unused = "Chưa dùng" [portal.sources.table] +documents = "Tài liệu" source = "Nguồn" status = "Trạng thái" +type = "Type" usedBy = "Chính sách" +[portal.sources.types.box] +description = "Kéo tài liệu từ các thư mục Box mà nhóm bạn đã chia sẻ." +label = "Box" + +[portal.sources.types.dropbox] +description = "Kéo tài liệu từ một thư mục Dropbox được chia sẻ." +label = "Dropbox" + [portal.sources.types.editor] +description = "Tài liệu mà nhóm bạn đã xử lý trong editor, qua các lần chạy chính sách và AI." label = "Trình chỉnh sửa" +noPolicies = "Chưa có chính sách nào chạy từ editor." + +[portal.sources.types.email] +description = "Biến tệp đính kèm gửi đến một hộp thư chuyên dụng thành tài liệu." +label = "Hộp thư Email" [portal.sources.types.folder] description = "Theo dõi một thư mục trên server để phát hiện tài liệu mới." @@ -7943,25 +8829,80 @@ label = "Độ sâu thư mục" all = "Bao gồm thư mục con" top = "Chỉ cấp cao nhất" +[portal.sources.types.ftp] +description = "Thăm dò một thư mục máy chủ FTP hoặc FTPS để tìm tài liệu mới." +label = "FTP" + +[portal.sources.types.googledrive] +description = "Kéo tài liệu từ các thư mục Google Drive được chia sẻ." +label = "Google Drive" + +[portal.sources.types.network] +description = "Theo dõi một SMB network share để tìm tài liệu mới." +label = "Network drive" + +[portal.sources.types.onedrive] +description = "Theo dõi các thư mục OneDrive để tìm tài liệu mới." +label = "OneDrive" + +[portal.sources.types.s3] +description = "Kéo tài liệu từ Amazon S3 hoặc bucket tương thích S3." +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "Kết nối đã lưu chứa bucket và thông tin xác thực. Được tái sử dụng bởi mọi nguồn và đầu ra pipeline tham chiếu đến nó." +label = "Kết nối" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume xóa từng object khỏi bucket sau khi mọi chính sách đã xử lý nó." +label = "Chế độ đọc" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume: xử lý mỗi object một lần" +snapshot = "Snapshot: đọc lại bucket mỗi lần chạy" + +[portal.sources.types.s3.fields.prefix] +helperText = "Chỉ xử lý các object có key bắt đầu bằng prefix này." +label = "Key prefix" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "Thăm dò thư mục drop SFTP để tìm tài liệu mới." +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "Kéo tài liệu từ các thư viện tài liệu SharePoint." +label = "SharePoint" + [portal.sources.types.unknown] label = "Nguồn" +[portal.sources.types.webhook] +createNote = "Delivery URL và signing secret được tạo khi bạn tạo webhook này. Người gửi POST tài liệu đến URL đó và các chính sách tham chiếu sẽ chạy khi nhận được." +description = "Nhận tài liệu bằng HTTP POST đã ký từ bất kỳ hệ thống bên ngoài nào." +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "Delivery URL" +secretNote = "Signing secret được hiển thị một lần, khi webhook được tạo. Tạo lại nguồn để xoay secret." + +[portal.sources.types.webhook.reveal] +copy = "Sao chép" +done = "Xong" +secret = "Signing secret" +secretHelp = "Ký raw body của mỗi lần gửi bằng khóa này (HMAC-SHA256) và gửi dưới dạng header X-Stirling-Signature." +secretWarning = "Sao chép signing secret ngay bây giờ. Vì bảo mật của bạn, nó chỉ được hiển thị một lần và không thể truy xuất sau." +title = "Webhook đã được tạo" +url = "Delivery URL" +usage = "POST từng tài liệu dưới dạng raw request body đến delivery URL với binary content type (application/pdf hoặc application/octet-stream). Các chính sách tham chiếu sẽ tự động chạy khi nhận được." + [portal.sources.wizard] -back = "Quay lại" -cancel = "Hủy" -continue = "Tiếp tục" -editTitle = "Chỉnh sửa nguồn" name = "Tên" namePlaceholder = "ví dụ: Tiếp nhận yêu cầu bồi thường" -save = "Lưu thay đổi" -subtitle = "Bước {{current}} trên {{total}} · {{label}}" -title = "Kết nối một nguồn" type = "Loại" -[portal.sources.wizard.steps] -chooseType = "Chọn loại" -configure = "Cấu hình" -review = "Xem lại & kết nối" +[portal.stepModal] +close = "Đóng" [portal.usage] managePayment = "Quản lý thanh toán" @@ -8021,16 +8962,9 @@ summary = "Sở hữu một đội — quản lý tài nguyên và cấu hình d 2 = "Quyền truy cập portal qua chính sách mặc định" 3 = "Mọi thứ Thành viên có thể làm" -[portal.welcome] -ariaLabel = "Chào mừng đến với Stirling PDF" -badge = "Open-source" -installEditor = "Cài đặt Editor" -inviteTeammates = "Mời đồng đội" -openInBrowser = "Mở trong trình duyệt" -perks = "Miễn phí mãi mãi · Có thể tự host" -subtitle = "PDF Editor bảo mật nhất thế giới miễn phí cho các đội ngũ ở mọi quy mô. Bao gồm hơn 60 thao tác PDF và SSO." -title = "Chào mừng đến với" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Không giới hạn" [printFile] title = "In tệp" @@ -8613,6 +9547,14 @@ text = "Xoay các trang PDF theo chiều kim đồng hồ hoặc ngược chiề [rotate.tooltip.header] title = "Tổng quan cài đặt xoay" +[ruler] +clearAll = "Xóa tất cả" +hideAllLabels = "Ẩn tất cả nhãn" +hideSmallLabels = "Ẩn nhãn nhỏ" +physicalValues = "Vật lý" +scaled = "Đã chia tỷ lệ" +showAllLabels = "Hiển thị tất cả nhãn" + [sanitize] desc = "Loại bỏ các thành phần có thể gây hại khỏi tệp PDF." sanitizationResults = "Kết quả làm sạch" @@ -8654,31 +9596,45 @@ label = "Xóa siêu dữ liệu XMP" [sanitize.steps] settings = "Cài đặt" -[sanitizePdf] -tags = "làm sạch,bảo mật,an toàn,loại bỏ mối đe dọa" - [sanitizePDF] title = "Làm sạch PDF" +[sanitizePdf] +tags = "làm sạch,bảo mật,an toàn,loại bỏ mối đe dọa" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "Điều chỉnh tỷ lệ trang" +[scaleSettings] +activeScale = "Tỷ lệ đang hoạt động" +apply = "Áp dụng tỷ lệ" +applyCalibration = "Áp dụng hiệu chuẩn" +calculatedScale = "Tỷ lệ đã tính" +calibrate = "Hiệu chuẩn" +calibrating = "Đang hiệu chỉnh" +calibrationDistanceRequired = "Nhập khoảng cách thực tế lớn hơn 0" +calibrationInvalid = "Không thể tính tỷ lệ từ phép đo này" +calibrationPaperDistance = "Khoảng cách trang đã đo: {{distance}}" +calibrationTitle = "Hiệu chỉnh tỷ lệ" +calibrationTooltip = "Đo một khoảng cách đã biết để tự động tính tỷ lệ" +customScale = "Tỷ lệ tùy chỉnh" +noneSet = "Chưa đặt tỷ lệ tùy chỉnh" +presets = "Tỷ lệ đặt sẵn" +ratio = "Tỷ lệ" +ratioHelp = "Tỷ lệ: 1 đơn vị trang = X đơn vị thực tế" +ratioPlaceholder = "ví dụ: 100" +ratioPositive = "Tỷ lệ phải lớn hơn 0" +ratioRequired = "Bắt buộc nhập tỷ lệ" +realDistance = "Khoảng cách thực tế" +realDistancePlaceholder = "ví dụ: 5" +reset = "Đặt lại về mặc định" +unit = "Đơn vị" +unitInvalid = "Đơn vị không hợp lệ: {{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "Ngưỡng góc:" -10 = "Đặt kích thước của viền được thêm vào và loại bỏ để ngăn chặn viền trắng trong đầu ra (mặc định: 1)." -2 = "Đặt góc tuyệt đối tối thiểu cần thiết để xoay hình ảnh (mặc định: 10)." -3 = "Dung sai:" -4 = "Xác định phạm vi biến đổi màu sắc xung quanh màu nền ước tính (mặc định: 30)." -5 = "Diện tích tối thiểu:" -6 = "Đặt ngưỡng diện tích tối thiểu cho một ảnh (mặc định: 10000)." -7 = "Diện tích đường viền tối thiểu:" -8 = "Đặt ngưỡng diện tích đường viền tối thiểu cho một ảnh" -9 = "Kích thước viền:" - [scannerImageSplit] submit = "Trích xuất ảnh quét" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "Tính năng" whatThisDoesDesc = "Tự động tìm và trích xuất từng ảnh từ một trang quét hoặc ảnh tổng hợp—không cần cắt thủ công." whenToUse = "Khi nào dùng" +[ScannerImageSplit.selectText] +1 = "Ngưỡng góc:" +10 = "Đặt kích thước của viền được thêm vào và loại bỏ để ngăn chặn viền trắng trong đầu ra (mặc định: 1)." +2 = "Đặt góc tuyệt đối tối thiểu cần thiết để xoay hình ảnh (mặc định: 10)." +3 = "Dung sai:" +4 = "Xác định phạm vi biến đổi màu sắc xung quanh màu nền ước tính (mặc định: 30)." +5 = "Diện tích tối thiểu:" +6 = "Đặt ngưỡng diện tích tối thiểu cho một ảnh (mặc định: 10000)." +7 = "Diện tích đường viền tối thiểu:" +8 = "Đặt ngưỡng diện tích đường viền tối thiểu cho một ảnh" +9 = "Kích thước viền:" + [search] noResults = "Không tìm thấy kết quả" placeholder = "Nhập từ khóa tìm kiếm..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "Máy chủ Stirling-PDF của bạn đang ngoại tuy [session] expired = "Phiên của bạn đã hết hạn. Vui lòng làm mới trang và thử lại." -[sessionManagement.tooltip] -header = "Quản lý phiên ký" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "Người tham gia mới được thêm vào cuối thứ tự ký" -bullet2 = "Không thể thêm người tham gia sau khi phiên đã hoàn tất" -bullet3 = "Mỗi người tham gia sẽ nhận thông báo khi đến lượt" -description = "Bạn có thể thêm nhiều người tham gia vào phiên đang hoạt động bất kỳ lúc nào trước khi hoàn tất." -title = "Thêm người tham gia" - -[sessionManagement.tooltip.finalization] -bullet1 = "Hoàn tất đầy đủ: Tất cả người tham gia đã ký" -bullet2 = "Hoàn tất một phần: Một số người tham gia chưa ký" -bullet3 = "Những người chưa ký sẽ bị loại khỏi tài liệu cuối cùng" -bullet4 = "Sau khi hoàn tất, bạn có thể tải PDF đã ký vào các tệp đang hoạt động" -description = "Việc hoàn tất kết hợp tất cả chữ ký thành một PDF đã ký duy nhất. Hành động này không thể hoàn tác." -title = "Hoàn tất phiên" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "Không thể xóa người tham gia đã ký" -bullet2 = "Người tham gia bị xóa sẽ không còn nhận thông báo" -bullet3 = "Thứ tự ký sẽ tự động điều chỉnh" -description = "Có thể xóa người tham gia khỏi phiên trước khi họ ký." -title = "Xóa người tham gia" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "Mỗi chữ ký được áp dụng tuần tự lên PDF" -bullet2 = "Người ký sau có thể thấy các chữ ký trước" -bullet3 = "Quan trọng cho quy trình phê duyệt và chuỗi lưu ký pháp lý" -description = "Thứ tự bạn chỉ định khi tạo phiên quyết định ai ký trước." -title = "Thứ tự chữ ký" - [settings] close = "Đóng" +[settings.ai] +documents = "Tài liệu & RAG" +general = "Chung" +limits = "Giới hạn & hiệu suất" +models = "Mô hình & nhà cung cấp" +title = "AI" + [settings.configuration] advanced = "Nâng cao" database = "Cơ sở dữ liệu" endpoints = "Endpoints" features = "Tính năng" +folderAccess = "Quyền truy cập thư mục" mcp = "MCP Server" storageSharing = "Lưu trữ & Chia sẻ Tệp" systemSettings = "Cài đặt hệ thống" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "Trình chỉnh sửa PDF mặc định" defaultPdfEditorActive = "Stirling PDF là trình chỉnh sửa PDF mặc định của bạn" defaultPdfEditorChecking = "Đang kiểm tra..." defaultPdfEditorInactive = "Ứng dụng khác đang được đặt mặc định" +defaultPdfEditorRemind = "Nhắc tôi đặt làm mặc định" +defaultPdfEditorRemindDescription = "Hiển thị biểu ngữ khi Stirling PDF không phải là ứng dụng PDF mặc định của bạn." defaultPdfEditorSet = "Đã là mặc định" defaultStartupView = "Chế độ xem mặc định khi khởi chạy" defaultStartupViewDescription = "Chọn tab sẽ hoạt động trong cột bên trái khi ứng dụng khởi động" @@ -8817,9 +9763,18 @@ languageDescription = "Chọn ngôn ngữ hiển thị" logout = "Đăng xuất" setAsDefault = "Đặt làm mặc định" theme = "Chủ đề" +themeAccent = "Màu nhấn" +themeAccentCustom = "Màu tùy chỉnh" +themeAccentCustomHint = "Tùy chỉnh (khuyến nghị dùng mặc định)" +themeAccentDark = "Màu nhấn chế độ tối" +themeAccentDefault = "Mặc định" +themeAccentDefaultHint = "Giao diện mặc định (khuyến nghị)" +themeAccentDescription = "Các nút, liên kết và phần tô sáng sẽ theo màu này, đồng thời tạo sắc thái nhẹ cho ứng dụng. Chế độ sáng và tối có màu riêng. Hệ thống sử dụng màu đang hoạt động." +themeAccentLight = "Màu nhấn chế độ sáng" themeDark = "Tối" themeDescription = "Chuyển giữa chế độ sáng và tối" themeLight = "Sáng" +themeReset = "Khôi phục giao diện về mặc định" themeSystem = "System" title = "Chung" user = "Người dùng" @@ -8833,6 +9788,12 @@ intro = "Bật xác thực người dùng, quản lý nhóm và các tính năng learnMore = "Tìm hiểu thêm trong tài liệu" title = "Dành cho Quản trị viên Hệ thống" +[settings.general.loginLanding] +description = "Chọn ứng dụng sẽ mở khi bạn đăng nhập vào Stirling Cloud." +editor = "Trình chỉnh sửa" +processor = "Bộ xử lý" +title = "Sau khi đăng nhập" + [settings.general.mode] fullscreen = "Toàn màn hình" sidebar = "Thanh bên" @@ -9013,9 +9974,6 @@ title = "Chính sách & Quyền riêng tư" [settings.preferences] title = "Tùy chọn" -[settings.search] -placeholder = "Tìm kiếm trang cài đặt..." - [settings.security] description = "Cập nhật mật khẩu để giữ an toàn cho tài khoản của bạn." title = "Bảo mật" @@ -9041,6 +9999,11 @@ security = "Bảo mật" telegram = "Telegram" title = "Bảo mật & Xác thực" +[settings.signOut] +confirm = "Bạn có chắc chắn muốn đăng xuất không?" +submit = "Đăng xuất" +title = "Đăng xuất" + [settings.team] title = "Nhóm" @@ -9156,6 +10119,7 @@ last = "Trang cuối" maintainRatio = "Bật/tắt giữ tỉ lệ khung hình" next = "Trang tiếp" noSavedSigs = "Không tìm thấy chữ ký đã lưu" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "Chữ ký cá nhân" previous = "Trang trước" redo = "Làm lại" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "Xóa nền thất bại" hint = "Tải lên ảnh chữ ký định dạng PNG hoặc JPG" label = "Tải lên ảnh chữ ký" placeholder = "Chọn tệp hình ảnh" +previewAlt = "Current image signature" processing = "Đang xử lý hình ảnh..." removeBackground = "Xóa nền trắng (tạo trong suốt)" @@ -9207,6 +10172,17 @@ saved = "Chọn một chữ ký đã lưu ở trên, rồi nhấp vào bất k text = "Sau khi nhập tên ở trên, bấm vào bất kỳ đâu trên PDF để đặt chữ ký của bạn." title = "Cách thêm chữ ký" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "Di chuyển chữ ký" pause = "Tạm dừng đặt" @@ -9653,10 +10629,30 @@ title = "Tải lên máy chủ" updateButton = "Cập nhật trên máy chủ" uploadButton = "Tải lên máy chủ" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "nhấp vào đây" +fallback = "Nếu không tự động mở," +opening = "Đang mở Swagger UI trong tab mới..." tags = "api,tài liệu,swagger,điểm cuối,phát triển" title = "Tài liệu API" @@ -9764,12 +10760,39 @@ selectFilesHint = "Chọn tệp trong Tệp đang hoạt động để chạy c singleFileScope = "Chỉ áp dụng cho: {{fileName}}" viewerMode = "Chuyển sang trình chỉnh sửa tệp để chọn nhiều tệp." +[toolFormat] +ANY = "bất kỳ file nào" +CBR = "kho truyện tranh (CBR)" +CBZ = "kho truyện tranh (CBZ)" +CSV = "file CSV" +EBOOK = "sách điện tử" +EMAIL = "email" +EXCEL = "bảng tính" +HTML = "trang web" +IMAGE = "hình ảnh" +JAVASCRIPT = "file JavaScript" +JSON = "file JSON" +MARKDOWN = "file Markdown" +NONE = "không có file" +PCL = "file in PCL" +PDF = "PDF" +PDF_ENCRYPTED = "PDF được bảo vệ bằng mật khẩu" +POSTSCRIPT = "file PostScript" +PPT = "bản trình bày" +TEXT = "file văn bản" +VIDEO = "video" +WORD = "tài liệu Word" +XML = "file XML" +XPS = "tài liệu XPS" +ZIP = "kho lưu trữ ZIP" + [toolPanel] alpha = "Alpha" backToAllTools = "Quay lại tất cả công cụ" collapse = "Thu gọn bảng" expand = "Mở rộng bảng" goBack = "Quay lại" +pdfTools = "Công cụ PDF" placeholder = "Chọn một công cụ để bắt đầu" premiumFeature = "Tính năng Premium:" toolsHeader = "Công cụ" @@ -9806,11 +10829,6 @@ noToolsFound = "Không tìm thấy công cụ" quickAccess = "TRUY CẬP NHANH" searchPlaceholder = "Tìm kiếm công cụ..." -[toolPicker.categories] -advancedTools = "Công cụ nâng cao" -recommendedTools = "Công cụ đề xuất" -standardTools = "Công cụ tiêu chuẩn" - [toolPicker.subcategories] advancedFormatting = "Định dạng nâng cao" automation = "Tự động hóa" @@ -9824,6 +10842,9 @@ removal = "Loại bỏ" signing = "Ký" verification = "Xác minh" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "Không tìm thấy công cụ nào" noTools = "Không có công cụ nào" @@ -9950,28 +10971,53 @@ title = "Người dùng" you = "(bạn)" [users.action] +cancelInvite = "Hủy lời mời" deleteTeam = "Xóa đội" disableMfa = "Đặt lại MFA" move = "Chuyển sang đội" reinstate = "Khôi phục" remove = "Xóa khỏi tổ chức" +removeTeam = "Xóa khỏi nhóm" rename = "Đổi tên đội" resetPw = "Đặt lại mật khẩu" suspend = "Đình chỉ" unlock = "Mở khóa tài khoản" +[users.activity] +daysAgo = "{{count}} ngày trước" +hoursAgo = "{{count}} giờ trước" +justNow = "Vừa xong" +minutesAgo = "{{count}} phút trước" +monthsAgo_one = "{{count}} tháng trước" +monthsAgo_other = "{{count}} tháng trước" +never = "Chưa bao giờ" +weeksAgo_one = "{{count}} tuần trước" +weeksAgo_other = "{{count}} tuần trước" +yearsAgo_one = "{{count}} năm trước" +yearsAgo_other = "{{count}} năm trước" + [users.cap] addProcessor = "+ Processor" approver = "Phê duyệt chính sách" editor = "Editor" processor = "Processor" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "Hủy lời mời đến {{email}}? Họ sẽ không thể tham gia bằng liên kết hiện tại." +cancelInviteTitle = "Hủy lời mời" deleteTeamBody = "Xóa đội {{name}}? Trước tiên đội phải trống - hãy chuyển thành viên sang đội khác, và đội không được còn sở hữu bất kỳ cấu hình tích hợp nào." deleteTeamTitle = "Xóa đội" disableMfaBody = "Xóa đăng ký MFA của {{name}}? Họ sẽ thiết lập lại vào lần đăng nhập tiếp theo nếu được yêu cầu." disableMfaTitle = "Đặt lại MFA" removeBody = "Xóa vĩnh viễn {{name}} khỏi tổ chức? Không thể hoàn tác thao tác này." +removeTeamBody = "Xóa {{name}} khỏi nhóm? Họ vẫn giữ tài khoản nhưng mất quyền truy cập vào tài nguyên của nhóm này." removeTitle = "Xóa thành viên" [users.empty] @@ -9982,10 +11028,8 @@ title = "Chưa có thành viên" addToTeam = "Thêm vào đội" guestCount = "{{count}} khách" guests = "Khách" -guestsDesc = "Cộng tác viên bên ngoài, giới hạn trong những gì bạn chia sẻ. Chỉ Editor." ledBy = "do {{owner}} dẫn dắt" org = "Tổ chức" -orgDesc = "Chủ sở hữu có quyền hạn toàn tổ chức và phê duyệt chính sách" owners = "{{count}} chủ sở hữu" team = "Đội {{name}}" teamMeta = "{{count}} người" @@ -10022,6 +11066,18 @@ username = "Tên người dùng" usernameError = "Tên người dùng phải có ít nhất 3 ký tự" usernamePlaceholder = "jsmith" +[users.invites] +by = "Được mời bởi {{who}}" +cancel = "Hủy" +expiresInDays_one = "Hết hạn sau {{count}} ngày" +expiresInDays_other = "Hết hạn sau {{count}} ngày" +expiresToday = "Hết hạn hôm nay" +title = "Lời mời đang chờ" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "Đã xảy ra lỗi khi kết nối tới backend, hoặc bạn không có quyền truy cập. Hãy thử lại." title = "Không thể tải thành viên" @@ -10211,10 +11267,15 @@ searchPlaceholder = "Tìm kiếm tệp đính kèm" title = "Tệp đính kèm" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "Tiêu đề dấu trang" +bookmarkTitleRequired = "Bắt buộc nhập tiêu đề dấu trang" closeSidebar = "Đóng thanh bên dấu trang" collapseAll = "Thu gọn tất cả dấu trang" +empty = "No bookmarks in this document" expandAll = "Mở rộng tất cả dấu trang" +searchPlaceholder = "Tìm kiếm dấu trang" +title = "Bookmarks" [viewer.comments] addComment = "Thêm bình luận" @@ -10236,10 +11297,12 @@ locateAnnotation = "Định vị trong tài liệu" moreActions = "Thao tác khác" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "Trang {{page}}" placingHint = "Nhấp vào một trang để đặt… (hủy)" removeCommentOnly = "Chỉ gỡ bình luận" saveReply = "Lưu trả lời" +searchPlaceholder = "Search comments" title = "Bình luận" typeComment = "Bình luận" typeInsertText = "Chèn văn bản" @@ -10262,6 +11325,7 @@ unsavedDesc = "Bạn có các thay đổi chưa được lưu" closeSidebar = "Đóng thanh bên lớp" hideAll = "Ẩn tất cả lớp" showAll = "Hiển thị tất cả lớp" +title = "Layers" [viewer.link] delete = "Xóa liên kết" @@ -10605,30 +11669,6 @@ title = "Văn bản hình mờ" image = "Hình ảnh" text = "Văn bản" -[wetSignature.tooltip] -header = "Phương thức tạo chữ ký" - -[wetSignature.tooltip.draw] -bullet1 = "Tùy chỉnh màu và độ dày bút" -bullet2 = "Xóa và vẽ lại cho đến khi hài lòng" -bullet3 = "Hoạt động trên thiết bị cảm ứng (máy tính bảng, điện thoại)" -description = "Tạo chữ ký viết tay bằng chuột hoặc màn hình cảm ứng của bạn. Phù hợp nhất cho chữ ký cá nhân, chân thực." -title = "Vẽ chữ ký" - -[wetSignature.tooltip.type] -bullet1 = "Chọn từ nhiều phông chữ" -bullet2 = "Tùy chỉnh kích thước và màu chữ" -bullet3 = "Lý tưởng cho chữ ký tiêu chuẩn hóa" -description = "Tạo chữ ký từ văn bản nhập. Nhanh và nhất quán, phù hợp cho tài liệu doanh nghiệp." -title = "Nhập chữ ký" - -[wetSignature.tooltip.upload] -bullet1 = "Hỗ trợ PNG, JPG và các định dạng ảnh khác" -bullet2 = "Khuyến nghị nền trong suốt để có kết quả tốt nhất" -bullet3 = "Hình ảnh sẽ được đổi kích thước để vừa vùng chữ ký" -description = "Tải lên hình ảnh chữ ký đã tạo sẵn. Lý tưởng nếu bạn có chữ ký quét hoặc logo công ty." -title = "Tải lên hình ảnh chữ ký" - [workbenchBar] activeFiles = "Tệp đang mở" annotations = "Chú thích" @@ -10642,6 +11682,7 @@ exitRedaction = "Thoát chế độ bôi đen" exportAll = "Xuất PDF" exportSelected = "Xuất các trang đã chọn" formFill = "Điền biểu mẫu" +hideToolbar = "Hide toolbar" multiTool = "Công cụ đa năng" panMode = "Chế độ di chuyển" print = "In PDF" @@ -10652,6 +11693,7 @@ redact = "Bôi đen" rotateLeft = "Xoay trái" rotateRight = "Xoay phải" ruler = "Thước / Đo" +rulerSettings = "Cài đặt tỷ lệ" save = "Lưu" saveAll = "Lưu tất cả" saveAs = "Lưu dưới dạng" @@ -10661,6 +11703,7 @@ selectAll = "Chọn tất cả" selectByNumber = "Chọn theo số trang" selectLanguage = "Chọn ngôn ngữ" share = "Chia sẻ" +showToolbar = "Show toolbar" toggleAnnotations = "Bật/Tắt hiển thị chú thích" toggleAttachments = "Bật/Tắt tệp đính kèm" toggleBookmarks = "Bật/Tắt dấu trang" diff --git a/frontend/editor/public/locales/zh-BO/translation.toml b/frontend/editor/public/locales/zh-BO/translation.toml index 603011afd5..e16a524f88 100644 --- a/frontend/editor/public/locales/zh-BO/translation.toml +++ b/frontend/editor/public/locales/zh-BO/translation.toml @@ -1,7 +1,6 @@ alphabet = "གསལ་བྱེད།" apply = "ཉེར་སྤྱོད།" applyAndContinue = "保存并离开" -areYouSure = "确定要离开吗?" back = "返回" black = "ནག་པོ" blue = "སྔོན་པོ" @@ -46,10 +45,8 @@ filesSelected = "ཡིག་ཆབདམས་ཟིན།" font = "ཡིག་གཟུགས་ཌྷ" fontSizeTooltip = "页码文本的磅值大小。数值越大文本越大。" fontTypeTooltip = "页码的字体族。可根据文档风格选择。" -genericSubmit = "ཕུལ་བཅོས།" green = "ལྗང་ཁུ།" help = "རོགས་རམ།" -imgPrompt = "པར་རིས་འདེམས་རོགས།" incorrectPasswordMessage = "ད་ལྟའི་གསང་ཚིག་ནོར་འདུག" info = "ཆ་འཕྲིན།" invalidUndoData = "无法撤销:操作数据无效" @@ -62,7 +59,6 @@ loginAgreementTitle = "登录协议" logOut = "退出登录" marginTooltip = "页码与页面边缘之间的距离。" moreOptions = "更多选项" -multiPdfPrompt = "PDF གཉིས་ཡན་འདེམས་རོགས།" never = "从不" no = "མིན།" noFileSelected = "未选择文件。请上传一个。" @@ -80,7 +76,6 @@ pageOrderPrompt = "ཤོག་ངོས་གོ་རིམ་རང་སྒ pages = "ཤོག་ངོས་ཁག" pageSelectionPrompt = "ཤོག་ངོས་འདེམས་སྒྲུག(ཤོག་གྲངས་ཀྱི་ཐོ་གཞུང་ 1,5,6 འམ་རྩིས་རྒྱག་བྱེད་ཐབས་ 2n+1 ལྟ་བུ་འཇུག་རོགས།)" password = "གསང་ཚིག།" -pdfPrompt = "PDF འདེམས་རོགས།" pin = "固定文件(工具运行后保持活动)" pro = "ཆེད་ལས།" processingComplete = "您的文件已准备就绪。" @@ -110,6 +105,7 @@ undoSuccess = "已成功撤销操作" unknown = "མི་ཤེས་པ།" unpin = "取消固定文件(工具运行后替换)" unsavedChanges = "您的 PDF 有未保存的更改。" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "未保存的更改" unsupported = "不支持" username = "སྤྱོད་མཁན་མིང་།" @@ -356,6 +352,7 @@ text = "这些权限控制用户对 PDF 的操作。与所有者密码结合使 title = "更改权限" [addStamp] +preview = "预览印章" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "手动下载:点击链接并将文件放入 tessdata 文件夹 noLanguages = "在已配置的目录中未找到 tessdata 语言。" permissionNotice = "tessdata 路径不可写。下载将会在浏览器中打开;请将 .traineddata 文件手动保存到 tessdata 文件夹中。" +[admin.settings.ai.documents] +description = "配置用于回答文档问题的嵌入模型和检索设置。保存后应用到 AI 引擎。" +title = "文档与 RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "留空以使用引擎自身的环境凭据。适用于自托管的单引擎部署。" +generic = "嵌入 API key" +openai = "OpenAI API key" +setPlaceholder = "已保存 - 留空以保留当前 key" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI 兼容 / Ollama 嵌入端点的基础 URL,例如 http://ollama:11434/v1。必须指向受信任的内部端点(SSRF 敏感)。" +label = "嵌入基础 URL" + +[admin.settings.ai.documents.embeddingModel] +description = "嵌入模型名称。自由文本;建议仅作提示。" +label = "嵌入模型" + +[admin.settings.ai.documents.embeddingProvider] +description = "用于将文档文本转换为向量嵌入的提供商。" +label = "嵌入提供商" + +[admin.settings.ai.documents.maxSearches] +description = "代理每个请求可运行的最大检索搜索次数。" +label = "最大搜索次数" + +[admin.settings.ai.documents.reindexNote] +body = "更改嵌入模型会立即生效,但使用先前模型建立索引的文档必须重新建立索引,搜索才能返回正确结果。" +title = "需要重新建立索引" + +[admin.settings.ai.documents.saved] +reindexNote = "如果你更改了嵌入模型,请重新索引现有文档,以便搜索使用新模型。" + +[admin.settings.ai.documents.topK] +description = "每次搜索检索的最相关片段数量。" +label = "Top K" + +[admin.settings.ai.general] +description = "将 Stirling 连接到 Python AI 引擎,并选择公开哪些 AI 功能。更改将在重启后应用。" +title = "AI 引擎" + +[admin.settings.ai.general.capabilities] +description = "开启或关闭单个 AI 功能。禁用的功能会在应用中隐藏。" +title = "功能" + +[admin.settings.ai.general.enabled] +description = "总开关。关闭时,AI 工具、代理或引擎调用均不可用。" +label = "启用 AI" + +[admin.settings.ai.general.features.chat] +description = "用于处理 PDFs 的对话助手。" +label = "聊天助手" + +[admin.settings.ai.general.features.classify] +description = "按类型或内容自动分类文档。" +label = "文档分类" + +[admin.settings.ai.general.features.createPdf] +description = "通过 AI 代理生成新的 PDF(例如从 HTML)。" +label = "从提示创建 PDF" + +[admin.settings.ai.general.features.documentQuestions] +description = "提出问题并获得基于上传文档的回答。" +label = "文档问答" + +[admin.settings.ai.general.features.mathAuditor] +description = "检查文档中的数学和数值错误。" +label = "数学审计器" + +[admin.settings.ai.general.features.pdfComment] +description = "向 PDF 添加由 AI 编写的审阅评论和注释。" +label = "PDF 评论代理" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "较重的代理操作(如文档生成)的超时时间。" +label = "长时间运行超时(秒)" + +[admin.settings.ai.general.note] +body = "AI 引擎作为单独服务运行。其共享密钥" +body2 = "通过容器环境变量设置。提供商 API key 可在这些页面输入,或作为引擎环境变量提供;此处输入的 key 会在保存时推送到引擎。" +title = "关于 AI 引擎" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "流式(逐 token)聊天响应的超时时间。" +label = "流超时(秒)" + +[admin.settings.ai.general.test] +button = "测试连接" +failBody = "AI 引擎未响应。请检查 URL、引擎容器是否正在运行,以及 AI 是否已启用(启用后需要重启)。" +failTitle = "AI 引擎无法访问" +okBody = "AI 引擎响应了健康检查。" +okTitle = "AI 引擎可访问" + +[admin.settings.ai.general.timeoutSeconds] +description = "发送到引擎的标准 AI 请求的超时时间。" +label = "请求超时(秒)" + +[admin.settings.ai.general.url] +description = "Python AI 引擎的内部 URL,例如 http://stirling-pdf-engine:5001。" +label = "AI 引擎 URL" + +[admin.settings.ai.limits] +description = "限制 AI 请求可执行的工作量以及并发运行数量。保存后应用到 AI 引擎。" +title = "限制与性能" + +[admin.settings.ai.limits.maxCharacters] +description = "保护限制:拒绝提取文本超过此长度的 AI 请求。" +label = "每个请求的最大字符数" + +[admin.settings.ai.limits.maxPages] +description = "保护限制:拒绝超过此 PDF 页数的 AI 请求。" +label = "每个请求的最大页数" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "整个引擎中同时进行的模型调用最大数量。" +label = "模型最大并发数" + +[admin.settings.ai.models] +description = "选择 AI 引擎使用的 LLM 提供商以及智能/快速模型。保存后应用到 AI 引擎。" +title = "模型与提供商" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "留空以使用引擎自身的环境凭据。适用于自托管的单引擎部署。" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "已保存 - 留空以保留当前 key" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI 兼容 / Ollama 端点的基础 URL,例如 http://ollama:11434/v1。" +label = "提供商基础 URL" +warning = "基础 URL 必须指向受信任的内部端点。引擎会向其发起服务器端请求,因此不受信任的值具有 SSRF 风险。" + +[admin.settings.ai.models.fastMaxTokens] +description = "快速模型的最大输出 tokens。" +label = "快速模型最大 tokens" + +[admin.settings.ai.models.fastModel] +description = "用于轻量任务的更便宜、更快的模型。自由文本;建议仅作提示。" +label = "快速模型" + +[admin.settings.ai.models.provider] +description = "引擎连接的 LLM 提供商。" +label = "提供商" + +[admin.settings.ai.models.smartMaxTokens] +description = "智能模型的最大输出 tokens。" +label = "智能模型最大 tokens" + +[admin.settings.ai.models.smartModel] +description = "用于复杂推理的高能力模型。自由文本;建议仅作提示。" +label = "智能模型" + +[admin.settings.ai.saved] +body = "更改会自动推送到 AI 引擎。" +bodyNoPush = "设置已保存。下次 AI 引擎获取其配置时将应用这些设置。" +title = "AI 设置已保存" + [admin.settings.badge] clickToUpgrade = "点击查看方案详情" @@ -938,6 +1094,25 @@ label = "启动时重新生成" description = "证书有效的天数" label = "证书有效期(天)" +[admin.settings.folderAccess] +description = "允许文件夹源和文件夹输出读取和写入的目录。这是安全边界:自动化永远不能指向此列表之外的服务器路径。" +securityNote = "留空可完全禁用文件夹源和输出。Stirling 自身的配置目录始终禁止访问。" +title = "文件夹访问" + +[admin.settings.folderAccess.implied] +description = "这些由 Stirling 管理的目录始终被允许,且无法在此处更改。" +serverStorage = "服务器文件存储" +title = "始终允许" +watchedFolder = "流水线监视文件夹" + +[admin.settings.folderAccess.roots] +add = "添加" +empty = "未允许任何文件夹。文件夹源和输出当前已禁用。" +hint = "输入绝对路径,例如 /data/inbox。" +label = "允许的文件夹根目录" +placeholder = "/data/inbox" +remove = "移除文件夹根目录" + [admin.settings.general] description = "配置全局应用设置,包括品牌和默认行为。" system = "系统" @@ -1704,6 +1879,9 @@ width = "宽度" [app] description = "免费的 Adobe Acrobat 替代方案(下载量 1000 万+)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "转换为 PDF/A-3b" convertToPdfA3bDescription = "创建带嵌入附件的归档 PDF" @@ -1842,6 +2020,15 @@ export = "导出" accessDenied = "拒绝访问" insufficientPermissions = "你没有执行此操作的权限。" +[auth.callback] +completing = "正在完成身份验证" +invalidToken = "OAuth 登录失败 - token 无效。" +missingToken = "OAuth 登录失败 - 未收到 token。" +oauthFailed = "OAuth 登录失败。请重试。" +pleaseWait = "请稍候,我们正在完成登录。" +signedOut = "你已退出登录。请重新登录。" +windowMayClose = "完成后你可以关闭此窗口。" + [auth.displayName] guest = "访客" user = "用户" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "通过移除可能的恶意内容(如 JavaScript 和嵌 description = "此工具将根据内容自动重命名 PDF 文件。它会分析文档以从文本中找到最合适的标题。" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "自动旋转" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "置信度阈值" + +[autoRotate.detectionMode] +auto = "自动" +osd = "仅 OCR" +text = "仅文本" +title = "检测方法" + +[autoRotate.error] +failed = "自动旋转 PDF 时发生错误。" + +[autoRotate.inferUndetected] +title = "旋转低置信度页面以匹配" + +[autoRotate.report] +applied = "{{degrees}}° 顺时针" +confidenceValue = "置信度 {{value}}" +noChange = "无更改" +pageLabel = "第 {{number}} 页" +summary = "已旋转 {{rotated}} / {{total}} 页,{{unchanged}} 页保持原样" +title = "检测报告" + +[autoRotate.report.method] +inferred = "已匹配" +none = "已跳过" +osd = "OCR" +text = "文本" + +[autoRotate.report.note] +belowThreshold = "低于阈值" +blankPage = "页面为空白" +inferredFromDocument = "与其他页面相同" +noDominantDirection = "文本方向混合" +osdFailed = "未找到可读文本" +osdNoVerdict = "无法判断朝向" +tesseractUnavailable = "OCR 未安装" +tooFewGlyphs = "文本过少" + +[autoRotate.results] +title = "自动旋转结果" + +[autoRotate.settings] +title = "设置" + +[autoRotate.tooltip.confidenceThreshold] +text = "在旋转页面前检测结果需要达到的确定程度。降低它可旋转更多页面,提高它可让更多页面保持不变。" +title = "置信度阈值" + +[autoRotate.tooltip.description] +text = "判断每页应朝向哪一边,并将其旋转到正确方向。已正向的页面会保持不变。" + +[autoRotate.tooltip.detectionMode] +text = "自动会先读取页面文本,如果不够,再将页面作为图像检查。仅文本会跳过图像检查,对于从未扫描过的文档最快。仅 OCR 会将每页都视为图像。" +title = "检测方法" + +[autoRotate.tooltip.header] +title = "自动旋转" + +[autoRotate.tooltip.inferUndetected] +text = "空白或近乎空白的页面无法单独检查。当所有可读取页面都需要相同旋转时,这些页面也会旋转以匹配它们。" +title = "旋转低置信度页面以匹配" + [autoSizeSplitPDF] tags = "pdf,ཁ་གྱེས།,ཡིག་ཆ།,གོ་སྒྲིག" @@ -2189,33 +2443,6 @@ keywords = "关键字:odd, even" numbers = "数字/范围:5, 10-20" progressions = "等差式:3n, 4n+1" -[certificateChoice.tooltip] -header = "证书类型" - -[certificateChoice.tooltip.organization] -bullet1 = "由系统管理员管理" -bullet2 = "在授权用户间共享" -bullet3 = "代表公司身份,而非个人" -bullet4 = "适用于:正式文档、团队签名" -description = "您的组织提供的共享证书。用于公司范围的签署授权。" -title = "组织证书" - -[certificateChoice.tooltip.personal] -bullet1 = "首次使用时自动生成" -bullet2 = "与您的用户账号绑定" -bullet3 = "不能与其他用户共享" -bullet4 = "适用于:个人文档、个人责任追溯" -description = "为您的用户账号自动生成且唯一的证书。适用于个人签名。" -title = "个人证书" - -[certificateChoice.tooltip.upload] -bullet1 = "需要 P12/PFX 文件和密码" -bullet2 = "可由外部 CA 机构签发" -bullet3 = "对法律文档具有更高信任级别" -bullet4 = "适用于:具有法律效力的合同、外部验证" -description = "使用您自己的 PKCS#12 证书文件。可完全控制证书属性。" -title = "上传自定义 P12" - [certSign] awaitingSignatures = "等待签署" chooseCertificate = "选择证书文件" @@ -2542,34 +2769,6 @@ server = "服务器" stepTitle = "证书来源" upload = "上传" -[certSign.tooltip.header] -title = "关于签名管理" - -[certSign.tooltip.overview] -bullet1 = "检查现有签名及其有效性" -bullet2 = "查看签名者和证书的详细信息" -bullet3 = "添加新的数字签名以保护文档" -bullet4 = "支持多个文件并提供便捷导航" -text = "此工具可检查您的 PDF 是否已数字签名,并添加新的数字签名。数字签名可证明谁创建或批准了文档,并显示自签名后是否被更改。" -title = "此工具能做什么?" - -[certSign.tooltip.signing] -bullet1 = "支持 PEM、PKCS12、JKS 和服务器证书格式" -bullet2 = "可选择在 PDF 上显示或隐藏签名" -bullet3 = "可添加原因、地点和签名者姓名" -bullet4 = "选择放置可见签名的页面" -bullet5 = "使用服务器证书以使用简单的“Sign with Stirling-PDF”选项" -text = "要签署 PDF,您需要数字证书(如 PEM、PKCS12 或 JKS)。您可以选择让签名在文档中可见或仅用于安全而保持不可见。" -title = "添加签名" - -[certSign.tooltip.validation] -bullet1 = "显示签名是否有效或无效" -bullet2 = "显示签名者信息和签署日期" -bullet3 = "检查文档是否在签名后被修改" -bullet4 = "可使用自定义证书进行验证" -text = "在检查签名时,工具会告诉您它们是否有效、谁签署了文档、签署时间,以及文档在签名后是否已被更改。" -title = "检查签名" - [changeCreds] changeUsername = "更新您的用户名。更新后您将被登出。" credsUpdated = "账户已更新" @@ -2737,12 +2936,16 @@ text = "要使这些权限不可更改,请使用添加密码工具设置所有 [chat.actions] copy = "复制消息" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling 代理选项" +assistant = "AI Assistant" clearChat = "清空聊天" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "此操作将使用您的云额度" tooltip = "从屏幕取色" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "选择颜色" [common] +actions = "Actions" back = "返回" cancel = "取消" close = "关闭" +codeSample = "Code sample" collapse = "折叠" confirm = "确认" continue = "继续" copied = "已复制!" copy = "复制" done = "完成" +enterValue = "Enter value" error = "错误" expand = "展开" loading = "正在加载..." next = "下一步" +open = "Open" preview = "预览" previous = "上一步" refresh = "刷新" +remaining = "剩余" retry = "重试" save = "保存" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "清除所选" @@ -2988,6 +3199,7 @@ title = "压缩方式" [compress.settings] desiredSize = "期望的文件大小" desiredSizePlaceholder = "输入大小" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "压缩是减少文件大小的简便方法。选择“文件大小”可输入目标大小,我们将为您调整质量;选择“质量”可手动设置压缩强度。" @@ -3190,6 +3402,24 @@ pdfOptions = "PDF 选项" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "请先选择源格式" settings = "设置" single = "单个" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog 分析" scarf = "Scarf 像素" -[credits.modal] -advancedMonitoring = "高级监控" -allInOneWorkspace = "一体化 PDF 工作空间(查看器、工具与代理)" -apiSandbox = "API 沙盒" -dedicatedSupportSlas = "专属支持与 SLA" -fasterThroughput = "10 倍吞吐速度" -fullyPrivateFiles = "完全私有的文件" -largeFileProcessing = "大文件处理" -orgWideAccess = "组织范围的访问控制" -premiumAiModels = "高级 AI 模型" -prioritySupport = "优先支持" -privateDocCloud = "私有文档云" -ragFineTuning = "RAG + 微调" -secureApiAccess = "安全的 API 访问" -standardThroughput = "标准吞吐" -unlimitedApiAccess = "无限 API 访问" -unlimitedSeats = "无限席位" - [crop] autoCrop = "自动裁剪空白" noFileSelected = "选择一个 PDF 文件开始裁剪" @@ -3346,6 +3558,7 @@ message = "设置默认 PDF 处理程序失败" title = "错误" [defaultApp.prompt] +dontRemind = "不再提醒我" message = "将 Stirling PDF 设为打开 PDF 的默认应用。" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "搜索..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "编辑" @@ -3644,11 +3861,13 @@ shareSelected = "共享所选" sharing = "共享" showAll = "显示全部" showHistory = "显示历史" +sortBy = "Sort files" sortByDate = "按日期排序" sortByName = "按名称排序" sortBySize = "按大小排序" storage = "存储" storageState = "存储" +storageUsed = "Storage used" synced = "已同步" title = "上传 PDF 文件" toolChain = "已应用的工具" @@ -3694,13 +3913,17 @@ addFiles = "添加文件" addingFiles = "正在添加文件…" collapse = "折叠侧边栏" customizeGroups = "自定义分组" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "打开文件以开始" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "展开侧边栏" -files = "文件" googleDrive = "Google Drive" googleDriveDisabled = "未配置 Google Drive" leaveMyFiles = "离开我的文件" +library = "PDF 库" myFiles = "我的文件" noFiles = "尚无文件" openFileManager = "打开文件管理器" @@ -3708,37 +3931,36 @@ openFromComputer = "从电脑打开" openSettings = "打开设置" other = "其他" recent = "最近" -search = "搜索" -searchPlaceholder = "搜索文件..." viewAll = "查看全部 {{count}} 个文件" [fileSidebar.fileItem] closeViewer = "关闭查看器" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "在查看器中打开" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "添加" -addLabel = "添加标签…" -categoryIconAria = "为 {{name}} 选择图标" -createCategory = "创建类别" -delete = "删除类别" done = "完成" -hide = "隐藏类别" -newCategory = "新类别名称…" -removeLabel = "移除 {{name}}" -rename = "重命名" reset = "重置为默认值" -search = "搜索标签…" -show = "显示类别" subtitle = "将文件归入父类别。向类别添加现有或新标签、重命名类别,或创建自己的类别。不属于任何类别的文件会显示在“其他”下。" title = "侧边栏类别" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "添加到工作区" addToWorkspaceCount = "添加 {{count}} 个到工作区" @@ -3749,7 +3971,6 @@ backToMyFiles = "返回到我的文件" breadcrumbs = "文件夹路径" cancel = "取消" classification = "分类" -clearSearch = "清除搜索" clearSelection = "清除选择" closeDetails = "关闭详情" create = "创建" @@ -3781,6 +4002,7 @@ downloadAll = "全部下载" downloadVersion = "下载此版本" dropOverlay = "拖放文件以上传" dropOverlaySub = "文件会先位于“本地”。使用“移动到”或“保存到云端”将它们整理到文件夹中。" +duplicate = "Duplicate" file = "文件" fileInfo = "File info" fileMenu = "文件操作" @@ -3808,13 +4030,12 @@ refresh = "从服务器刷新" remove = "删除" removeVersion = "移除此版本" rename = "重命名" +renamed = "已重命名" renameFolder = "重命名文件夹" resizeFolderTree = "调整文件夹树大小(方向键,按住 Shift 为更大步长;双击自动适配)" save = "保存" saveToServer = "保存到服务器" saveToServerDisabledHint = "此服务器未启用保存到服务器。请联系管理员启用。" -search = "搜索" -searchPlaceholder = "搜索此文件夹及其子文件夹" selectAll = "全选" selectAllHint = "点击全选。提示:按住 Ctrl(或 Cmd)可逐个添加文件,按住 Shift 可选择一个范围。" selectedCount = "已选 {{count}} 个" @@ -3874,6 +4095,10 @@ title = "还没有云端文件" hint = "未上传而保存的文件会留在这里。拖放文件以添加。" title = "没有仅本地文件" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "你打开或编辑过的文件会显示在这里。" title = "尚无修改" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "无法删除文件夹。" deleteFolderFailedDetail = "无法删除文件夹:{{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "无法更新文件夹外观。" folderAppearanceFailedDetail = "无法更新文件夹外观:{{message}}" moveFilesFailed = "无法移动文件。" @@ -3910,9 +4137,7 @@ count = "文件" folder = "文件夹" labels = "标签" modified = "修改时间" -name = "名称" size = "大小" -toolHistoryAtVersion = "累积工具链" totalSize = "总大小" type = "类型" versionHistory = "版本历程" @@ -3945,6 +4170,11 @@ localHint = "仅存储在此浏览器中" shared = "已共享" sharedHint = "通过链接与你共享" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "最早在前" modifiedDesc = "最新在前" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "存储中暂无可用文件。请先打开一些文件 open = "打开" openFile = "打开文件" openFiles = "打开多个文件" +selectFile = "Select {{name}}" selectFromStorage = "从存储中选择" upload = "上传" uploadFile = "上传文件" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "全部已保存" +analyzingFields = "Analysing form fields..." +extractCsvError = "提取 CSV 失败" +extractXlsxError = "提取 XLSX 失败" +filled = "filled" flattenAfterFilling = "填写后扁平化" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "必填" +requiredFieldsError = "请填写所有必填字段" rescanFields = "重新扫描字段" rescanFormFields = "重新扫描表单字段" save = "保存" +saveShortcut = "Ctrl+S 保存" +unsavedChanges = "未保存的更改" [formFill.sidebar] close = "关闭侧边栏" @@ -4309,6 +4552,11 @@ desc = "根据检测到的页眉自动重命名 PDF 文件" tags = "自动检测,基于页眉,组织,重命名" title = "自动重命名 PDF 文件" +[home.autoRotate] +desc = "检测每页方向并自动旋转为正向。" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "自动旋转" + [home.autoSizeSplitPDF] tags = "自动,拆分,大小" title = "按大小/页数自动拆分" @@ -4608,15 +4856,12 @@ hint = "上传 PNG、JPG、SVG 或其他图像文件以放置到 PDF 上。SVG label = "上传图片" placeholder = "选择图片文件" -[imageToPdf] -tags = "བསྒྱུར་བ།,པར་རིས།,jpg,པར།,འདྲ་པར།" - [imageToPDF] header = "པར་རིས་ནས་ PDF ལ།" title = "པར་རིས་ནས་ PDF ལ།" -[infoBanner] -dismiss = "关闭" +[imageToPdf] +tags = "བསྒྱུར་བ།,པར་རིས།,jpg,པར།,འདྲ་པར།" [invite] acceptError = "创建账户失败" @@ -4647,11 +4892,9 @@ welcomeTitle = "您已收到邀请!" [landing] addFiles = "添加文件" -heroSubtitle = "拖入或添加现有 PDF 以开始。" mobileUpload = "从手机上传" openFromComputer = "从电脑打开" uploadFromComputer = "从电脑上传" -workbenchEmptyStateHero = "将 PDF 拖放到任意位置" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "输入用户名" failedToSignIn = "使用 {{provider}} 登录失败:{{message}}" forgotPassword = "忘记密码?" loggingIn = "正在登录..." -logIn = "登录" login = "登录" magicLinkSent = "魔法链接已发送至 {{email}}!请检查邮箱并点击链接登录。" mfaCode = "身份验证代码" @@ -4715,26 +4957,10 @@ signInWith = "使用以下方式登录" title = "�ང་འཛུལ།" unexpectedError = "意外错误:{{message}}" updatePassword = "更新密码" -useEmailInstead = "使用邮箱登录" useMagicLink = "改用魔法链接" username = "用户名" youAreLoggedIn = "您已登录!" -[login.slides.edit] -alt = "编辑 PDF" -subtitle = "十余种工具助您涂黑、签名、阅读与处理 PDF,满足您的各种需求。" -title = "编辑 PDF 以展示或保护您想要的信息" - -[login.slides.overview] -alt = "Stirling PDF 概览" -subtitle = "以隐私为先的 PDF 云套件,支持转换、签名、涂黑和管理文档,并提供 50+ 款强大工具。" -title = "满足您所有 PDF 需求的一站式工具。" - -[login.slides.secure] -alt = "保护 PDF" -subtitle = "轻松添加密码、涂黑内容并管理证书。" -title = "保护 PDF 中的敏感信息" - [margin] large = "大" medium = "中" @@ -4818,6 +5044,34 @@ uploadSuccess = "上传成功!" uploadSuccessMessage = "您的图像已传输。" validating = "正在验证会话..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "扫码上传照片。图像将自动转换为 PDF。" descriptionNoConvert = "从移动设备扫码上传照片。" @@ -4836,8 +5090,13 @@ title = "从手机上传" tags = "ལག་ཆ་མང་པོ།,བཀོལ་སྤྱོད་མང་པོ།,UI,མཐེབ་གནོན་འཐེན་པ།,མདུན་ངོས།,མཁོ་མཁན་ཕྱོགས།,སྤྱོད་སྒོ།,འགུལ་སྐྱོད།,སུབ་པ།,གནས་སྤོ།,བགོ་བ།" title = "PDF ལག་ཆ་མང་པོ།" -[navbar] -search = "འཚོལ་བཤེར།" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "认证未成功。您可以关闭此窗口并重试。" @@ -4966,6 +5225,7 @@ activeFiles = "活动文件 视图显示您加载到工具中 allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "关闭" cropSettings = "现在我们已选择要裁剪的文件,可以配置裁剪工具以选择我们希望裁剪到的区域。" +dialogLabel = "Onboarding" fileCheckbox = "点击其中一个文件即可将其选中进行处理。您可以选择多个文件执行批量操作。" fileReplacement = "修改后的文件会自动替换工作台中的原文件,方便您继续通过更多工具处理。" filesButton = "文件 按钮位于快速访问栏,可用于上传需要使用工具处理的 PDF。" @@ -4976,6 +5236,7 @@ pinButton = "如果您希望在运行工具后文件保持活动状态,可以 results = "工具运行完成后,审阅 步骤将在此面板显示结果预览,并允许您撤销操作或下载文件。 " runButton = "配置好工具后,此按钮允许您对所有选中的 PDF 运行该工具。" selectCropTool = "让我们选择 裁剪 工具来演示如何使用其中一个工具。" +stepOf = "第 {{current}} 步,共 {{total}} 步" toolInterface = "这是 裁剪 工具界面。如您所见,因为我们还没有添加任何要处理的 PDF 文件,所以这里内容不多。" workbench = "这是 工作台 - 查看和编辑您的 PDF 的主区域。" wrapUp = "一切就绪!您已了解应用的主要区域及其用法。随时点击 帮助 按钮再次查看此导览。" @@ -4988,6 +5249,26 @@ showMeAround = "带我看看" skipForNow = "暂时跳过" skipTheTour = "跳过引导" +[onboarding.checklist] +dismiss = "关闭" +title = "设置 Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "在你的机器上原生运行 Stirling" +title = "下载 Stirling 桌面版" + +[onboarding.checklist.inviteTeam] +description = "与你的团队协作" +title = "邀请团队成员" + +[onboarding.checklist.shareAnalytics] +description = "帮助改进 Stirling" +title = "分享匿名使用数据" + +[onboarding.checklist.takeTour] +description = "通过快速演示了解 Stirling 的工作方式" +title = "开始导览" + [onboarding.desktopInstall] body = "Stirling 作为桌面应用效果最佳。可离线使用、更快访问文档,并在本机进行编辑。" selectOs = "选择操作系统" @@ -4996,7 +5277,21 @@ titleWithOs = "适用于 {{osLabel}} 的下载" [onboarding.mfa] authenticationCode = "验证码" +description = "通过关联身份验证器应用来保护你的账户。扫描 QR code 或输入设置 key,然后确认 6 位代码以完成。" +enable = "启用 MFA" +enableError = "无法启用双因素身份验证。请检查代码并重试。" +enterCode = "输入来自应用的 6 位代码。" +enterCodeError = "请输入身份验证代码以继续。" +logout = "退出登录" +openAuthenticator = "打开 Google Authenticator、Authy 或 1Password。" qrCodeLoading = "正在生成你的 QR 码…" +regenerateQrCode = "重新生成 QR code" +scanQrCode = "扫描 QR code 或输入下方的设置 key。" +setupError = "无法开始双因素设置。请重试。" +setupKey = "设置 key(手动输入)" +stepByStep = "逐步说明" +success = "MFA 已启用。你现在可以继续。" +title = "多因素身份验证设置" [onboarding.planOverview] adminBodyLoginDisabled = "启用登录模式后,您可以管理用户、配置设置并监控服务器健康状况。服务器上的前 {{freeTierLimit}} 位用户可免费使用 Stirling。" @@ -5005,6 +5300,11 @@ adminTitle = "管理员概览" userBody = "邀请队友、分配角色,并在一个安全的工作区中整理文档。准备扩大规模时,可随时启用登录模式。" userTitle = "方案概览" +[onboarding.processorIntro] +body = "Stirling 现在运行 策略 — 自动化规则会在每个文档到达时对其分类、保护并处理。在 处理器 中设置并监控运行。" +cta = "查看处理器" +title = "查看 Stirling 处理器" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "预付年度" + +[payg.prepaid.banner] +exhausted = "你的预付容量已用完;按量计费已恢复。充值以保留折扣。" +expiring_one = "你的预付年度将在 {{count}} 天后结束。未使用容量将过期 — 充值以开始新的年度。" +expiring_other = "你的预付年度将在 {{count}} 天后结束。未使用容量将过期 — 充值以开始新的年度。" +low = "你的预付容量已降至 {{pct}}%。可随时充值以保持处理不中断。" + +[payg.prepaid.card] +subtitle = "在按量计费之前使用,且不计入你的支出上限。" +title = "预付容量" + +[payg.prepaid.meter] +capSuffix = "共 {{total}} 预付 credits" +expires = "{{date}} 过期" + +[payg.prepaid.state] +exhausted = "已用完" +healthy = "剩余充足" +low = "即将用完" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF ནས་ PDF/A ལ།" tags = "ཡིག་མཛོད།,དུས་ཡུན་རིང་པོ།,ཚད་ལྡན།,བསྒྱུར་བ།,ཉར་ཚགས།,སྲུང་སྐྱོབ།" title = "PDF ནས་ PDF/A ལ།" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "打印,标准,转换,生产,印前,归档" title = "PDF 转 PDF/X" @@ -5822,6 +6151,7 @@ small = "500 点" xsmall = "100 点" [plan.availablePlans] +currency = "Billing currency" subtitle = "选择最符合您需求的套餐" title = "可用套餐" @@ -5937,49 +6267,12 @@ keyDescription = "粘贴您邮件中的许可证密钥" success = "许可证已激活!" successMessage = "您的许可证已成功激活。您现在可以关闭此窗口。" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "已强制执行" -enforcing = "正在强制执行…" -failed = "强制执行失败" outputsUnavailable = "策略输出已无法下载。" partialOutputsUnavailable = "部分策略输出已无法下载。" -retrying = "忙碌,正在重试…" runNotFound = "该次强制执行运行已无法找到。" -step = "步骤 {{current}}/{{total}}" timedOut = "在运行完成前强制执行已超时。" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "关闭" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "正在应用 {{names}}" applyingProgress = "正在应用 {{names}}({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} 个文件中有 {{failures}} 个无法处理,已原 failureTitle = "未完全强制执行即导出" printPolicyAppliedBody = "此 PDF 已为满足策略而更新。请检查更改后再打印。" printPolicyAppliedTitle = "打印前已应用策略" -queued = "+{{count}} 已排队" successTitle = "{{names}} 已应用" summaryMore = "{{first}}、{{second}} 以及另外 {{more}} 个" summaryTwo = "{{first}} 和 {{second}}" -[policies.enforcement.triggerVerb] -convert = "转换前强制执行" -default = "正在强制执行" -export = "导出前强制执行" -input = "导入时强制执行" -print = "打印前强制执行" - -[policies.field] -accessLog = "访问日志" -archiveAfter = "在此之后归档" -auditTrail = "审计跟踪" -belowThreshold = "低于阈值" -destination = "目标位置" -frameworks = "框架" -immutableHold = "不可变保留" -keepFor = "保留时长" -minConfidence = "最低置信度" -notify = "路由时通知" -onViolation = "不合规时" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "添加" -addPlaceholder = "添加标签…" -customNote = "已为您的团队自定义。" -defaultNote = "正在使用内置默认值,与您的团队共享。" -duplicate = "\"{{name}}\" 已存在。" -edit = "编辑标签" -empty = "还没有标签。" -export = "导出 JSON" -iconAria = "为 {{name}} 选择图标" -import = "导入 JSON" -importError = "无法导入该文件。" -managedNote = "团队标签由您的团队负责人管理。" -modalSubtitle = "与整个团队共享。分类器会为每个文档选择合适的标签。" -modalTitle = "分类标签" -pickIcon = "使用 {{label}} 图标" -removeAria = "移除 {{name}}" -resetToDefault = "重置为默认值" -saveForTeam = "为团队保存" -saving = "正在保存…" -sectionLabel = "分类标签" -startFromScratch = "从头开始" -teamCount = "团队标签" -tooLong = "标签最多可包含 {{max}} 个字符。" -ungrouped = "未分组" -view = "查看标签" +categoryCount = "类别" +hideCategory = "隐藏类别" +labelCount = "标签" +sharedNote = "这些标签是内置的,并在整个团队中共享。" +showCategory = "显示类别" [policies.pii] account = "已标注的账户号码" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "美国银行路由号码(ABA)" ssn = "社会安全号码" -[policies.settings] -noneExport = "当前没有在导出时运行的策略。" -noneUpload = "当前没有在上传时运行的策略。" -onExport = "导出时" -onUpload = "上传时" -reorderHandle = "拖动以重新排序" -runOrderDesc = "当多个策略在同一触发器上运行时,它们会按此顺序运行——每个策略都基于上一个策略的输出。拖动以重新排序。" -title = "策略设置" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "策略选项" -policySettings = "策略设置" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "重试失败的策略 ({{count}})" -rowProgress = "已处理 {{completed}} / {{total}} 个文件" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "什么是策略?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "自动查找并涂黑敏感信息(如社会安全号码和卡号),使其在文档中不可阅读。" -sanitize = "从文件中移除隐藏的 JavaScript,防止有人打开时自动运行任何内容。" -watermark = "在每一页加盖可见标记(例如“机密”)。" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "关闭" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} 正在执行..." badgeRan = "{{name}} 策略已在此文件上运行" +badgeRunning = "{{name}} 正在运行..." blockingAction = "执行策略时已阻止 {{action}},请稍候..." dismiss = "关闭覆盖层" enforcingTitle = "正在执行策略..." @@ -6151,6 +6316,7 @@ viewAnyway = "查看文件(策略仍在执行)" [policyOption] autoRedactPhi = "自动遮盖 PHI" blockExport = "阻止导出" +ccpa = "CCPA" contracts = "合同" documents = "文档" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor 方案" subscribed = "Processor 方案" unlinked = "未链接" -[portal.agentBuilder] -bootstrapFromDocument = "从文档引导创建" -sectionsAriaLabel = "智能体构建器部分" -selectorAriaLabel = "智能体" -subtitle = "设计、测试并发布用于分类、提取和路由文档的 AI 智能体。定义场景,限制工具访问,运行黄金集,并发布版本。" -title = "智能体构建器" - -[portal.agentBuilder.bootstrap] -cancel = "取消" -dropzoneText = "选择示例文档(PDF 或图像)" -lead = "放入一个示例文档,我们将提出场景和可供您完善的提取架构。在您审核之前,不会发布任何内容。" -submit = "引导创建智能体" -subtitle = "从一个代表性文件初始化新智能体" -title = "从文档引导创建" - -[portal.agentBuilder.empty] -description = "从示例文档引导创建智能体,以初始化其场景和提取架构,然后进行完善和发布。" -title = "还没有智能体" - -[portal.agentBuilder.evals] -casesPassing = "通过的用例" -columnCase = "评估用例" -columnLatency = "延迟" -columnResult = "结果" -fail = "失败" -goldenSetPassRate = "黄金集通过率" -latencyMs = "{{ms}} ms" -notRun = "未运行" -pass = "通过" -passRate = "通过率" -runEvals = "运行评估" - -[portal.agentBuilder.evals.empty] -description = "评估会将您的场景转换为可重复的黄金集。升级后可随时间捕获通过率,并将其作为发布门禁。" -title = "还没有黄金集" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "跨黄金集" -activeAgents = "活跃智能体" -avgPassRate = "平均评估通过率" -fleetWide = "全局范围" -latestPublished = "最新发布" -scenarios = "场景" -testCases = "测试用例" -totalDescription_one = "共 {{count}} 个" -totalDescription_other = "共 {{count}} 个" - -[portal.agentBuilder.scenarios] -add = "添加" -addScenario = "添加场景" -enable = "启用" -expectationLabel = "预期行为" -expectationPlaceholder = "智能体应执行的操作" -inEval = "在评估中" -mute = "静音" -muted = "已静音" -nameLabel = "名称" -namePlaceholder = "例如 合规升级" - -[portal.agentBuilder.tabs] -evals = "评估" -scenarios = "场景" -tools = "工具" -versions = "版本" - -[portal.agentBuilder.tools] -broadAccess = "广泛访问" -deniedHint = "所选工具已被阻止。其他所有工具仍可调用。" -deniedTools = "被拒绝的工具" -governanceGate = "工具治理在 Enterprise 方案中可用。" -restricted = "受限" -restrictedAccess = "受限工具访问" -restrictedDescription = "允许所有工具,除了您在下方拒绝的工具。" - -[portal.agentBuilder.versions] -current = "当前" -historyGate = "完整版本历史记录和回滚在 Enterprise 方案中可用。" -publish = "发布" -rollBack = "回滚" - [portal.assistant] close = "关闭助手" error = "无法连接到助手。" @@ -6365,9 +6451,21 @@ typing = "正在输入" loading = "正在加载" redirectingToEditor = "正在重定向到编辑器..." +[portal.billing.activation] +maybeLater = "稍后再说" +title = "启用处理器" + +[portal.billing.activation.payg] +sub = "设置月度限额。按实际运行量付费。" +title = "按量付费" + +[portal.billing.activation.prepay] +badge = "免费 2 个月" +sub = "一张发票,可用银行卡或银行转账。" +title = "预付一年" + [portal.billing.checkout] noClientSecret = "Edge function 未返回 client_secret。" -subtitle = "添加银行卡以在免费 Editor 方案额度用完后继续使用。其余由 Stripe 处理。" title = "启用 Processor 方案" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "您的付款已成功,但激活时间比平时稍长。它会自动启 close = "关闭" title = "即将完成" +[portal.billing.checkout.cap] +amountAria = "月度支出限额" +back = "返回" +continue = "继续付款" +error = "无法设置你的支出限额" +finePrint = "发票按月开具。可随时取消。" +noLimit = "无限制" +note = "你的账单永远不会超过限额。处理会暂停。" +perMonth = "/月" +title = "设置你的支出限额" + +[portal.billing.checkout.card] +fields = "卡号 · MM / YY · CVC · ZIP" +label = "银行卡详情" +note = "银行卡详情由 Stripe 收集。Stirling 从不存储 PAN 或 CVC。" + [portal.billing.checkout.error] title = "无法开始结账" @@ -6383,10 +6497,9 @@ body = "您的付款已完成。我们正在您的已链接实例上启用按量 hint = "请保持此窗口打开。" title = "正在激活您的 Processor 方案..." -[portal.billing.checkout.notConfigured] -bodyAfter = "在门户 env 中以启用应用内结账。" -bodyBefore = "设置" -title = "未配置 Stripe" +[portal.billing.checkout.pay] +limit = "月度支出限额 {{amount}}/月。达到限额时处理会暂停。" +limitNoCap = "无支出限额。处理不会暂停。发票按月开具。" [portal.billing.enterpriseUpsell] cta = "创建您的 Enterprise 报价" @@ -6409,10 +6522,12 @@ currentPlan = "当前方案" freeForever = "永久免费" noTeamResolved = "您的钱包尚未解析到团队——请刷新后重试。" ownerOnly = "只有团队所有者可以启用 Processor 方案。" +payInvoice = "支付发票以完成" planName = "Editor" ssoIncluded = "包含 SSO" switchOnProcessor = "启用 Processor →" unlimitedUsers = "无限用户" +viewQuote = "查看报价" [portal.billing.invoices] columnAmount = "金额" @@ -6465,7 +6580,111 @@ segmentApiDesc = "直接 API 请求" segmentApiLabel = "API" segmentAutomationDesc = "自动化和流水线" segmentAutomationLabel = "自动化" +sizeMultiplier = "{{formatted}} PDFs 使用了大小倍数" +summary = "{{unique}} 唯一 · {{units}} 计量单位 · 每个 PDF 平均 {{avg}}" +summaryNoRate = "{{unique}} 唯一 · {{units}} 计量单位" unit = "按量 PDFs" +unitsPending_one = "{{units}} 个计量单位正在等待从关联实例同步" +unitsPending_other = "{{units}} 个计量单位正在等待从关联实例同步" + +[portal.billing.prepaid] +capSuffix = "共 {{total}} 预付 credits" +expires = "{{date}} 过期" +eyebrow = "预付容量" +note = "在按量计费之前使用,且不计入你的支出限额。" +topUp = "充值" + +[portal.billing.prepaid.buy] +back = "返回" +calcTitle = "计算你的年度付款" +cancel = "取消" +cancelPurchase = "取消购买" +close = "关闭" +continueToPayment = "继续付款" +finalise = "完成" +invoiceErrorTitle = "无法生成发票" +loadingTitle = "正在加载你的报价" +payTitle = "支付你的年度费用" +step = "第 {{current}} 步,共 {{total}} 步" + +[portal.billing.prepaid.calc] +change = "更改" +deployRow = "部署" +done = "完成" +enterpriseHint = "这是企业级规模。在此用量下,企业费率优于任何自助折扣。费率锁定、条款和报价可在数分钟内获得。" +governanceRow = "治理" +governanceValue = "{{label}} · {{count}} 个策略" +handlesLabel = "你的处理器" +handlesValue = "每月处理 {{volume}} credits" +pipelinesLabel = "流水线" +policiesMeta = "{{count}} 个策略" +poolCaption = "全年共一个 {{credits}} credits 池。繁忙月份可借用空闲月份的额度。" +priceLabel = "你的年度 · 以 10 个月价格获得 12 个月" +rateUnknown = "我们会在结账时显示准确价格。" +savings = "你节省 {{amount}} · 免费 2 个月。" +sizingRow = "规模" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "总用户数" + +[portal.billing.prepaid.consent] +label = "我了解,当我的预付容量用完或 12 个月后过期时,除非我取消,否则处理会自动按标准按量付费费率继续(不超过我的支出限额),并且我可以随时从计费门户取消。" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "由 Stirling 托管。几分钟内上线。" +cloudValue = "Stirling Cloud · 托管" +selfhost = "自托管" +selfhostDesc = "在私有基础设施上运行。费率相同。" +selfhostValue = "自托管 · 私有基础设施" + +[portal.billing.prepaid.offer] +cta = "查看优惠" +subtitle = "预付一年 PDF 处理费用并获得两个月免费,在按量计费之前使用,且不计入你的支出限额。" +title = "以 10 个月价格获得 12 个月" + +[portal.billing.prepaid.pay] +accountName = "账户持有人姓名" +accountNamePlaceholder = "例如 Jane Smith" +companyName = "公司名称(可选)" +companyNamePlaceholder = "例如 Acme Inc." +downloadInvoice = "下载发票" +payOnline = "在线支付" +poNumber = "PO 编号(可选)" +poNumberPlaceholder = "例如 PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "高级" +advancedDesc = "流水线驱动你的大部分处理。" +none = "无" +noneDesc = "尚未运行流水线。可随时开启。" +standard = "标准" +standardDesc = "少量流水线会重新处理到达的 PDFs。" + +[portal.billing.prepaid.posture] +essentials = "基础" +essentialsDesc = "分类,默认项,加上共享" +governed = "受治理" +governedDesc = "增加安全和路由" +regulated = "受监管" +regulatedDesc = "每个类别,包括合规、保留、摄取" + +[portal.billing.prepaid.review] +download = "下载报价(PDF)" +downloading = "正在准备…" +share = "分享以供审批" + +[portal.billing.prepaid.size] +compact = "紧凑" +compactDesc = "文件小于 25 MB,无数据费用" +heavy = "重型" +heavyDesc = "扫描件或图像较多,通常 50 MB+" +standard = "标准" +standardDesc = "大多较小,部分扫描件超过 25 MB" + +[portal.billing.prepaid.state] +exhausted = "已用完" +healthy = "剩余充足" +low = "即将用完" [portal.billing.spendLimit] adjustLimit = "调整限额" @@ -6494,6 +6713,8 @@ label = "预计会超出。" [portal.billing.spendThisMonth] eyebrow = "本月支出" +freeRemaining_one = "剩余 {{formatted}} 个免费 PDF" +freeRemaining_other = "剩余 {{formatted}} 个免费 PDFs" processed_one = "已处理 {{formattedCount}} 个 PDF。" processed_other = "已处理 {{formattedCount}} 个 PDFs。" processedWithRate_one = "已处理 {{formattedCount}} 个 PDF,每个 {{rate}}。" @@ -6510,6 +6731,7 @@ reachedTitle = "已达到月度支出限额" title = "无法打开 Stripe 门户" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "已使用 {{allowance}} 个免费 PDFs 中的额度" capSuffix_other = "已使用 {{allowance}} 个免费 PDFs 中的额度" eyebrow = "Processor 试用" @@ -6534,6 +6756,376 @@ signature = "签名" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "新建连接" +createTitleFor = "连接 {{name}}" +delete = "删除" +edit = "编辑" +editTitle = "编辑连接" +unsupportedType = "此连接类型无法在此处编辑。" + +[portal.connections.categories.advanced] +description = "用于我们尚未支持的服务。" +label = "高级" + +[portal.connections.categories.audit] +description = "记录文档发生了什么,并记录到审计人员已查看的位置。" +label = "审计与合规日志" + +[portal.connections.categories.notify] +description = "通知另一个系统发生了某件事。" +label = "通知与工作流" + +[portal.connections.categories.security] +description = "在文档继续流转前扫描或标记它。" +label = "安全与分类" + +[portal.connections.categories.signing] +description = "发送文档以进行签名或公证。" +label = "电子签名与公证" + +[portal.connections.categories.storage] +description = "处理后的文档最终到达的位置 - 存储桶、驱动器、工单或页面。" +label = "文件与附加" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling 会加密存储此项,并且不会再次显示。" +label = "访问 token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "使用此连接的步骤只能调用此 URL 下的路径,不能调用其他主机。" +label = "服务器 URL" + +[portal.connections.commonFields.email] +label = "电子邮件" + +[portal.connections.commonFields.host] +label = "主机" + +[portal.connections.commonFields.password] +label = "密码" + +[portal.connections.commonFields.port] +label = "端口" + +[portal.connections.commonFields.username] +label = "用户名" + +[portal.connections.commonFields.webhookUrl] +helperText = "粘贴服务提供给你的 URL。只能调用其下的路径。" +label = "Webhook URL" + +[portal.connections.fields] +name = "连接名称" +namePlaceholder = "例如 Production" + +[portal.connections.picker] +cancel = "取消" +createNew = "新建连接..." +placeholder = "选择连接" +save = "保存连接" + +[portal.connections.picker2] +back = "更改集成" +heading = "你想连接什么?" +noResultsBody = "我们尚未支持它 - 告诉我们相关信息,我们会考虑添加。" +noResultsTitle = "没有匹配 \"{{query}}\" 的结果" +searchPlaceholder = "搜索集成 - 试试 \"sign\"、\"ocr\" 或产品名称" + +[portal.connections.types.api] +description = "从策略调用任何 HTTP API。你可以选择 URL、身份验证和 payload。" +label = "自定义 API" + +[portal.connections.types.api.fields.authType] +label = "身份验证" + +[portal.connections.types.api.fields.authType.options.basic] +label = "用户名和密码" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "自定义 header 中的 token" + +[portal.connections.types.api.fields.authType.options.none] +label = "无" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "登录以获取 token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "使用此连接的步骤只能调用此 URL 下的路径,不能调用其他主机。" +label = "基础 URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header 名称" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "登录路径" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "密码" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "如果 API 回复下载链接,只能获取这些主机。用逗号分隔。留空则只允许基础 URL 自身的主机。" +label = "允许的结果主机" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "将 token 作为此 header 发送" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "登录调用在其中返回 token 的响应 header,例如 X-Auth-Token。" +label = "Token 响应 header" + +[portal.connections.types.api.fields.username] +label = "用户名" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "在你运行的服务器上扫描文档中的恶意软件 - 不会离开你的网络。" +label = "ClamAV(自托管)" + +[portal.connections.types.cloudmersive] +description = "用于策略中文档的病毒和内容扫描。" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "阻止包含宏、可执行文件、脚本或嵌入对象的文档 - 不只是已知恶意软件。" +label = "Cloudmersive 高级扫描" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "将处理后的文档附加到 Confluence 页面。" +label = "Confluence" + +[portal.connections.types.consigno] +description = "将文档发送到 ConsignO Cloud (Notarius) 以进行电子签名和公证。" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "你的 ConsignO Cloud 环境,包括 /api/v1。" +label = "服务器 URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "密码" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "仅当你的组织使用 tenants 时需要。" +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "用户名" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "当策略处理文档时向 Discord 发布消息。" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "每当策略处理文档时,在 Elasticsearch 中索引一条审计事件。" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "轮询 FTP 或 FTPS 服务器以获取新文档。" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "连接模式" + +[portal.connections.types.ftp.fields.passive.options] +active = "主动" +passive = "被动(推荐)" + +[portal.connections.types.ftp.fields.security] +helperText = "普通 FTP 会以未加密方式发送凭据和文件;如果服务器支持,请选择 FTPS。" +label = "加密" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS(显式 / AUTH TLS)" +implicit = "FTPS(隐式)" +none = "无(普通 FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "当策略运行时向 Google Chat space 发布消息。" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "将处理后的文档附加到 Jira issue。" +label = "Jira" + +[portal.connections.types.mailgun] +description = "当策略运行时发送文档或通知电子邮件。" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "将处理后的文档归档到 Nextcloud。" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "使用你运行的 Presidio 服务器检测文档文本中的个人数据。" +label = "Presidio(自托管)" + +[portal.connections.types.purview] +description = "读取并应用 Microsoft Purview PDFs 敏感度标签。" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "可选。仅在按名称选择标签时需要;没有它也可以标记。" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "作为 SiteId 印到每个标签上,以便支持 Purview 的工具知道由哪个组织应用。" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "S3 bucket,源可从中读取,流水线输出可写入其中。" +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS 留空。为 MinIO 或其他 S3 兼容存储设置此项。" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "当策略运行时发送文档或通知电子邮件。" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "轮询 SFTP (SSH) 服务器以获取新文档。" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "可选。服务器的 SHA-256 主机 key fingerprint(来自 `ssh-keyscan host | ssh-keygen -lf -`)。设置后,仅接受提供此 key 的服务器;留空时,首次连接时看到的 key 会被固定,之后的更改将被拒绝。" +label = "主机 key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "如果你改用 private key 进行身份验证,请留空。" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "可选。粘贴 OpenSSH private key,以使用 key 而不是密码进行身份验证。" +label = "Private key" + +[portal.connections.types.slack] +description = "当策略处理文档时向 Slack 发布消息。" +label = "Slack" + +[portal.connections.types.smb] +description = "监视 SMB / CIFS 网络共享(Windows 或 Samba)。" +hostPlaceholder = "fileserver.example.com" +label = "SMB / 网络共享" + +[portal.connections.types.smb.fields.domain] +helperText = "可选。如果你的服务器使用 Windows domain 或 workgroup,请填写。" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "每当策略处理文档时向 Splunk 发送审计事件。" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "向 Sumo Logic HTTP collector 发送审计事件。" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "当策略运行时向 Teams channel 发布消息。" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "将每个文档或仅其详情发布到你选择的 URL。" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "从策略触发 Zap 或 Make scenario。" +label = "Zapier & Make" + +[portal.docs] +browse = "浏览文档" +viewSource = "在 GitHub 上查看源代码" + [portal.docs.authentication] codeCaption = "每个请求" eyebrow = "入门" @@ -6624,6 +7216,9 @@ eyebrow = "技能" lead = "捆绑的命名能力,您的智能体可将其作为单个工具调用。每项技能都是附带评估的确定性操作链。" title = "智能体技能" +[portal.docs.toc] +title = "本页内容" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API 参考" @@ -6697,6 +7292,8 @@ type = "类型" user = "用户" [portal.documents.queue.empty] +connectSource = "连接源" +createPipeline = "创建流水线" description = "当来源将文档送入您的管道时,它们会出现在这里以供审核。" title = "队列中没有文档" @@ -6712,11 +7309,11 @@ editorAction = "编辑器" empty = "没有符合此筛选条件的文档。" rowActions = "行操作" sensitiveLabel = "敏感" -sensitiveTitle = "敏感 — 需要访问权限" [portal.documents.table.columns] action = "管道 / 操作" document = "文档" +labels = "Labels" product = "产品" status = "状态" time = "时间" @@ -6732,6 +7329,7 @@ host = "主机" lastSeen = "上次在线" region = "区域" status = "状态" +target = "Target" version = "版本" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "此视图遇到意外错误。请重试,或从侧边栏选择 retry = "重试" title = "此页面出现问题" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "返回" +body = "下载桌面应用或自托管服务器。" +done = "完成" +guide = "打开 {{name}} 指南" +openInBrowser = "改为在浏览器中打开" +sectionDesktop = "桌面应用" +sectionSelfHosted = "自托管" +title = "安装 Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "在你的服务器上运行官方镜像。" +note = "在 :8080 打开。默认登录:admin / stirling。" +tagline = "在容器中运行服务器" +title = "Docker" +variantFat = "完整版" +variantLite = "超轻量版" +variantStandard = "标准版" + +[portal.home.download.kubernetes] +detailBody = "安装官方 Helm chart。" +note = "为 /configs 添加 PVC 以持久保存设置。" +tagline = "使用 Helm 部署到集群" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "适用于您的发行版的原生构建。" +guideBtn = "查看 Linux 安装选项" +note = "常见发行版可使用 AppImage、.deb 和 .rpm 构建。" +tagline = "适用于您发行版的原生桌面应用" +title = "Linux" + +[portal.home.download.mac] +altLabel = "或通过 Homebrew" +detailBody = "运行安装程序,或使用 Homebrew。" +downloadBtn = "下载 macOS 版本" +tagline = "适用于 Apple silicon 和 Intel 的原生应用" +title = "macOS" + +[portal.home.download.manual] +detailBody = "在裸机服务器上运行 .jar。" +downloadBtn = "下载 .jar" +note = "需要 Java 21+。在 :8080 打开;默认登录 admin / stirling。" +runLabel = "然后运行它" +tagline = "在裸机服务器上运行 .jar" +title = "手动服务器设置" + +[portal.home.download.windows] +altLabel = "或通过 winget" +detailBody = "运行安装程序,或使用包管理器。" +downloadBtn = "下载安装程序" +tagline = "适用于 Microsoft Windows 的原生应用" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} 位活跃" -invite = "邀请队友" +activeOfDeployed = "本月 {{total}} 个中有 {{active}} 个处于活跃状态" name = "Stirling PDF Editor" open = "在浏览器中打开" updated = "已更新 {{time}}" +[portal.home.editor.deploy] +finish = "完成部署" +options = "查看部署选项" +start = "部署 Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "自托管 · Docker" @@ -6823,49 +7529,6 @@ afternoon = "下午好" evening = "晚上好" morning = "早上好" -[portal.home.onboarding] -dismiss = "关闭设置" -notStarted = "未开始" -progress = "已完成 {{done}} / {{total}}" -title = "完成设置" - -[portal.home.onboarding.enterprise] -body = "全组织 SSO + SCIM + RBAC、承诺用量定价和隔离网络部署。" -cta = "开始试用" -ctaQuote = "获取报价" -lead = "适用于 250+ 名员工。" -tag = "企业版" - -[portal.home.onboarding.steps.editor] -blurb = "部署桌面应用,永久免费。" -title = "下载 PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} 个活跃 · {{recommended}} 个推荐" -chip = "{{active}} 个活跃" -title = "启用策略" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} 个已连接 · 每个 PDF 在落地处受管控" -chip = "{{connected}} 个已连接" -title = "连接您的来源" - -[portal.home.quickActions] -subtitle = "今日重点任务" -title = "快捷操作" - -[portal.home.quickActions.buildPipeline] -blurb = "基于类型化操作库的 3 步编排器" -title = "构建管道" - -[portal.home.quickActions.connectSource] -blurb = "S3、代理、webhooks、监视文件夹" -title = "连接来源" - -[portal.home.quickActions.issueApiKey] -blurb = "带速率限制和 IP 允许列表的范围化密钥" -title = "签发 API 密钥" - [portal.infrastructure] manageEditorDeployment = "管理 Editor 部署" sectionsAriaLabel = "基础设施部分" @@ -6878,13 +7541,9 @@ heading = "API 密钥" subheading = "带每密钥速率限制、权限和 IP 允许列表的范围化凭据。" [portal.infrastructure.apiKeys.card] -allowedIps = "允许的 IP" -anyIp = "任意 IP(无允许列表)" created = "已创建" lastUsed = "上次使用" -permissions = "权限" -rateLimit = "速率限制" -rateLimitValue = "{{value}} 请求/分钟" +revoke = "撤销密钥" usageMonth = "本月用量" usageToday = "今日用量" @@ -6892,6 +7551,15 @@ usageToday = "今日用量" description = "创建范围化密钥以开始调用 Stirling API。" title = "尚无 API 密钥" +[portal.infrastructure.apiKeys.error] +load = "无法加载您的 API 密钥。请重试。" + +[portal.infrastructure.apiKeys.revoke] +body = "撤销“{{name}}”?任何仍在使用此密钥的集成都将立即开始收到 401 错误。此操作无法撤销。" +cancel = "取消" +confirm = "撤销密钥" +title = "撤销 API 密钥" + [portal.infrastructure.audit] filterAriaLabel = "按类别筛选审计事件" heading = "审计日志" @@ -6935,6 +7603,7 @@ all = "全部" auth = "身份验证" config = "配置" elevation = "权限提升" +policy = "策略" processing = "处理" security = "安全" @@ -6944,19 +7613,30 @@ title = "您没有访问权限" [portal.infrastructure.audit.metrics] config = "配置" -elevation = "权限提升" +policy = "策略" processing = "处理" totalEvents = "总事件 · 24h" +[portal.infrastructure.auditCatLabel] +auth = "身份验证" +config = "配置" +elevation = "权限提升" +policy = "策略" +processing = "处理" +security = "安全" + +[portal.infrastructure.auditStatusLabel] +danger = "错误" +info = "信息" +success = "成功" +warning = "警告" + [portal.infrastructure.createKey] cancel = "取消" createKey = "创建密钥" done = "完成" -ipAllowlistHelper = "以逗号分隔的 CIDR 范围。留空则允许任意 IP。" -ipAllowlistLabel = "IP 允许列表" keyNameLabel = "密钥名称" keyNamePlaceholder = "例如:生产 · 摄取" -permissionsLabel = "权限" secretKeyCaption = "秘密密钥" secretWarning = "请将其存储在密钥管理器中。Stirling 只会存储哈希 — 之后无法恢复。" subtitle = "将密钥范围限制为所需的最小范围。您可以随时轮换或吊销。" @@ -6964,207 +7644,9 @@ subtitleCreated = "立即复制此密钥 — 它不会再次显示。" title = "创建 API 密钥" titleCreated = "密钥已创建" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/分钟" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "部署者" -environment = "环境" -product = "产品" -status = "状态" -version = "版本" -when = "时间" - -[portal.infrastructure.deployments.recent] -heading = "最近部署" -subheading = "跨产品和环境的最新发布。" - -[portal.infrastructure.deployments.regionColumns] -instances = "实例" -latency = "延迟" -load = "负载" -p99 = "P99" -region = "区域" -status = "状态" -throughput = "吞吐量" -uptime = "正常运行时间" -version = "版本" - -[portal.infrastructure.deployments.regions] -heading = "区域" -subheading = "每个已部署 Stirling 区域的实时健康状况 — 延迟、负载和发布版本。" - -[portal.infrastructure.deployments.regions.empty] -description = "工作区完成配置后,已部署区域会显示在这里。" -title = "没有已部署区域" - -[portal.infrastructure.models] -heading = "模型" -msValue = "{{value}} ms" -subheading = "为您工作区中的文档处理提供支持的模型目录和路由。" - -[portal.infrastructure.models.byom] -description = "注册本地或自托管模型,并将其固定到某个区域以进行受数据驻留约束的处理。" -title = "自带模型" - -[portal.infrastructure.models.catalogue] -heading = "目录" -sub = "您的工作区可用的托管模型,包含实时延迟和成本。" -subEnterprise = "托管、自带和本地模型 — 支持按区域固定。" - -[portal.infrastructure.models.catalogue.empty] -description = "您工作区目录中的模型会显示在这里。" -title = "没有可用模型" - -[portal.infrastructure.models.columns] -cost = "成本" -latency = "延迟" -load = "负载" -model = "模型" -status = "状态" -type = "类型" -version = "版本" - -[portal.infrastructure.models.metrics] -activeModels = "活跃模型" -avgLatency = "平均延迟" -included = "已包含" -monthlySpend = "每月模型支出" - -[portal.infrastructure.models.routing] -empty = "未配置路由规则。" -heading = "路由规则" -sub = "由哪个模型处理每个操作。没有匹配到更具体规则时,将应用默认规则。" -subLocked = "将操作路由到特定模型 — 付费计划可用。" - -[portal.infrastructure.models.routing.lockedBanner] -description = "升级到 Pro 以控制哪个模型处理每个操作和文档类型。" -title = "模型路由是付费功能" - -[portal.infrastructure.models.routingColumns] -default = "默认" -docType = "文档类型" -modelForAria = "{{operation}} 的模型" -operation = "操作" -routedTo = "路由至" - -[portal.infrastructure.security.access.byok] -description = "从您自己的 KMS 提供密钥。Stirling 使用它加密,但仍可读取。" -label = "自带密钥 (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "密钥永不离开您的 KMS。Stirling 仅持有密文。" -label = "自持密钥 (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling 管理加密密钥。最简单 — 您无需进行密钥运维。" -label = "Stirling 持有的密钥" - -[portal.infrastructure.security.accessPolicy] -heading = "文档访问策略" -subheading = "控制谁可以解密静态存储的已处理文档。" - -[portal.infrastructure.security.attestations] -heading = "合规证明" -noReport = "无可用报告" -subheading = "按框架展示的审计态势,并可查看已证明控制项的报告。" -viewReport = "查看报告 →" - -[portal.infrastructure.security.compliance] -heading = "合规" -subheading = "覆盖 Stirling 平台的证明和认证。" - -[portal.infrastructure.security.empty] -description = "您工作区的安全配置会显示在这里。" -title = "安全态势不可用" - -[portal.infrastructure.security.hyokBanner] -description = "使用 HYOK 时,加密密钥永不离开您的 KMS。Stirling 仅存储和处理密文,您可随时吊销。" -title = "Stirling 无法解密您的文档" - -[portal.infrastructure.security.ipAllowlist] -empty = "未配置 IP 范围 — 允许所有 IP。" -heading = "IP 允许列表" -sub = "API 访问限制为这些 CIDR 范围。" -subLocked = "将 API 访问限制为已知 IP 范围 — 付费计划可用。" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "升级到 Pro 以将 API 访问限制到特定网络。" -title = "IP 允许列表是付费功能" - -[portal.infrastructure.security.ipColumns] -added = "已添加" -addedBy = "添加者" -cidr = "CIDR" -label = "标签" - -[portal.infrastructure.security.keyManagement] -algorithm = "算法" -heading = "加密密钥管理" -keyId = "密钥标识符" -lastRotated = "上次轮换" -rotateKey = "轮换密钥" -rotationPolicy = "轮换策略" -subheading = "用于加密静态文档的密钥托管 — 谁可以解密,以及密钥如何轮换。" - -[portal.infrastructure.security.managedBanner] -description = "自带密钥 (BYOK) 和自持密钥 (HYOK) 托管在企业版中可用。升级后可从您自己的 KMS 提供密钥。" -title = "您当前计划中的密钥由 Stirling 管理" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "亚太地区" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR 数据边界" -label = "欧盟" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "美国" - -[portal.infrastructure.security.residencyHeader] -heading = "数据驻留" -subheading = "文档存储和处理的位置。" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "已使用 {{value}}" - -[portal.infrastructure.storage.empty] -description = "已连接的存储和用量会显示在这里。" -title = "未配置存储" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "活跃" -activeRange = "0–{{value}}d" -archived = "已归档" -coldStorage = "冷存储" -deleted = "已删除" -never = "从不" -purged = "已清除" - -[portal.infrastructure.storage.providers] -connect = "连接" -connected = "已连接" -heading = "已连接的提供商" -subheading = "已处理工件写入的位置。" - -[portal.infrastructure.storage.retention] -heading = "保留" -subheading = "工件在生命周期删除前保留多长时间。" -windowLabel = "默认保留窗口" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} 天" -days_other = "{{count}} 天" -never = "永不删除" - -[portal.infrastructure.storage.totalUsage] -heading = "总用量" -progressLabel = "已用存储" -subheading = "所有已连接提供商消耗的存储。" +revoked = "已撤销" [portal.infrastructure.tabs] apiKeys = "API 密钥" @@ -7174,6 +7656,49 @@ models = "模型" security = "安全" storage = "存储" +[portal.integrations] +availableHeading = "可用" +comingSoonHeading = "即将推出" +connect = "连接" +connectionCount_one = "{{count}} 个连接" +connectionCount_other = "{{count}} 个连接" +customApi = "自定义 API" +searchPlaceholder = "搜索集成" +subtitle = "Stirling 可以与之通信的一切。凭据在此处创建一次,然后由来源、策略和流水线引用。" +title = "集成" +typedName = "{{tool}} 名称" + +[portal.integrations.filters] +all = "全部" +audit = "审计" +connected = "已连接" +notify = "通知" +security = "安全" +signing = "签名" +storage = "存储" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "已连接" + +[portal.integrations.table] +integration = "集成" +worksWith = "适用于" + +[portal.integrations.worksWith] +pipelines = "流水线" +policies = "策略" +sources = "来源" + +[portal.legal] +draft = "{{label}} · 草稿" +loadError = "无法加载此文档。请重试。" +loading = "正在加载…" +title = "法律文件" + [portal.nav] agent-builder = "Agent Builder" components = "组件" @@ -7182,33 +7707,17 @@ documents = "文档" editor = "编辑器" home = "首页" infrastructure = "基础设施" +integrations = "集成" pipelines = "管道" policies = "策略" procurement = "采购" -settings = "设置" sources = "来源" usage = "用量与账单" users = "用户" -[portal.notifications] -markAllRead = "全部标为已读" -title = "通知" -viewAll = "查看全部" - -[portal.notifications.ariaLabel] -none = "通知,无未读" -unread_one = "通知,{{count}} 条未读" -unread_other = "通知,{{count}} 条未读" - -[portal.notifications.count] -allRead = "全部已读" -loading = "加载中" -new_one = "{{count}} 条新通知" -new_other = "{{count}} 条新通知" - -[portal.notifications.empty] -description = "没有新通知。" -title = "您已全部处理完毕" +[portal.nav.section] +platform = "PDF 平台" +processor = "PDF 处理器" [portal.pipelines] subtitle = "后端上的每个自动化文档管道:一组来源上的有序操作链,由触发器运行。点击一行查看其步骤和来源。" @@ -7218,54 +7727,87 @@ title = "管道" newPipeline = "新建管道" [portal.pipelines.builder] -addStep = "添加工具" +activate = "Activate" back = "返回管道" +cannotFollow = "无法接收 {{produced}}" +chooseAccount = "选择一个账户" +chooseDestination = "选择一个目标" +chooseOperation = "选择此步骤执行的操作" +chooseSource = "选择一个来源" discard = "放弃更改" -enabled = "已启用" +inputs = "输入" +inputSource = "输入来源" +inputTrigger = "触发器" keepEditing = "继续编辑" -needsUpload = "需要上传的文件" +moreActions = "More actions" +needsConfiguring = "需要设置" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "没有工具匹配您的搜索。" -pipelineSettings = "管道设置" +pause = "Pause" +rename = "Rename pipeline" searchTools = "搜索工具" -selectToolBody = "添加工具以构建您的管道。" -selectToolTitle = "尚无工具" -toolSettings = "工具设置" +sendToSystem = "发送到另一个系统" +stepsIncompatible = "这些步骤无法在其上一步生成的内容上运行:{{tools}}。" +stepsNeedSetup = "保存前这些步骤仍需要设置:{{tools}}。" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "无法识别的操作,已按原样保留。" unsavedBody = "您有未保存的更改。离开前保存,还是放弃?" unsavedTitle = "未保存的更改" -uploadUnsupported = "管道尚不支持上传的文件,因此这些步骤无法保存:{{tools}}。" usesDefaults = "使用默认设置运行" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "合并上一步中的每个文件" +fan-out = "对上一步中的每个文件运行一次" +format-mismatch = "需要 {{accepts}},但上一步生成 {{produced}}" +output-uncertain = "可能不会运行:上一步的输出取决于其设置方式" +source-mismatch = "需要 {{accepts}},但此流水线的输入是 {{produced}}" +undeclared-operation = "无法检查此步骤接受的内容" [portal.pipelines.composer] addTool = "添加工具" -cancel = "取消" -chainEmpty = "添加工具以开始构建您的管道。" create = "创建管道" -directory = "输出文件夹" -directoryHelp = "服务器上的绝对路径。必须位于已配置的允许文件夹内。" +createPaused = "Create paused" editingUnsupported = "尚不支持显示这些工具参数用于编辑。" -moveDown = "下移" -moveUp = "上移" +editSource = "Edit source" name = "名称" namePlaceholder = "例如:修订扫描" -noSources = "尚未连接来源。管道仍可在直接提供给它的文件上运行。" noToolSettings = "此工具没有可配置的设置。" -operations_one = "操作({{count}})" -operations_other = "操作({{count}})" output = "输出" -removeStep = "移除操作" save = "保存更改" scheduleEvery = "每隔" -sources = "来源" -sourcesLoading = "正在加载来源..." trigger = "触发器" triggerManual = "仅手动" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "天" hours = "小时" minutes = "分钟" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "删除 \"{{name}}\"?此操作无法撤销。" cancel = "取消" @@ -7273,28 +7815,62 @@ confirm = "删除" title = "删除管道?" [portal.pipelines.detail] +clearHistory = "清除历史记录" delete = "删除管道" run = "立即运行" [portal.pipelines.empty] action = "创建管道" +connectSource = "连接来源" description = "创建您的第一个管道:选择运行的来源,串联操作,并选择输出位置。" title = "尚无管道" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "活跃" paused = "已暂停" total = "管道" -[portal.pipelines.output] -folder = "写入文件夹" -inline = "返回文件" - [portal.pipelines.run] +allProcessed_one = "无需运行:来源中的 {{count}} 个文档已处理。" +allProcessed_other = "无需运行:来源中的所有 {{count}} 个文档均已处理。" completed_one = "运行已完成。" completed_other = "全部 {{count}} 次运行已完成。" empty = "没有可运行内容:来源中没有要处理的文档。" failed = "运行失败:{{error}}" +inFlight = "没有新的可运行内容:文档仍在从早先的运行中处理中。" +parked_one = "无需运行:{{count}} 个文档此前失败并已暂存。修复原因后,清除历史记录以重试。" +parked_other = "无需运行:{{count}} 个文档此前失败并已暂存。修复原因后,清除历史记录以重试。" running = "运行已开始;仍在进行中。" timeout = "运行耗时超过预期;它可能仍会在后台完成。" @@ -7307,6 +7883,7 @@ name = "管道" sources = "来源" status = "状态" steps = "步骤" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "文件夹监视" @@ -7314,12 +7891,18 @@ manual = "手动" schedule = "已计划" [portal.policies] +defaultName = "{{category}} 策略" subtitle = "常驻自动化会在每个文档上强制执行工具管道。每项策略在上传或导出时触发,运行其工具链,并将强制处理后的版本与原始文件一同保存。" title = "策略" [portal.policies.card] comingSoon = "升级到企业版" notSetUp = "未设置" +requiresAiEngine = "需要 AI 引擎" + +[portal.policies.categories.classification] +desc = "在上传时识别每个文档的类型,并标记其元数据以便归档和搜索。" +label = "分类" [portal.policies.categories.compliance] desc = "在每个文档上强制执行 HIPAA、GDPR、SOC 2 或 FedRAMP 要求。" @@ -7344,6 +7927,13 @@ label = "安全" [portal.policies.config] scopeAll = "所有文档" +[portal.policies.config.classification] +summary = "根据您团队的标签对每个上传文档进行分类,并标记其元数据。" + +[portal.policies.config.classification.rules] +0 = "分类" +1 = "标记元数据" + [portal.policies.config.compliance] summary = "在文档离开系统前,根据监管框架验证文档。" @@ -7371,6 +7961,29 @@ minConfidence = "最低置信度" 2 = "命名" 3 = "规范化" +[portal.policies.config.purview] +label = "应用 Microsoft Purview 敏感度标签" + +[portal.policies.config.purview.fields] +connection = "Purview 租户" +labelId = "标签 ID" +labelIdHelp = "来自 Purview 门户的标签 GUID。应用标签会将其元数据写入 PDF;它无法加密,加密需要 Microsoft 客户端。" +labelName = "标签名称(可选)" +labelNameHelp = "与 ID 一起记录,以便人员和工具识别该标签。" + +[portal.policies.config.purview.fields.method] +help = "Purview 会区分由策略应用的标签和由人员选择的标签。" +label = "标签的记录方式" +privileged = "作为人员选择的标签" +standard = "作为自动应用的标签" + +[portal.policies.config.purviewRead] +label = "读取 Microsoft Purview 标签" + +[portal.policies.config.purviewRead.fields] +connection = "Purview 租户" +connectionHelp = "读取文档已携带的标签,以便后续步骤可以对其执行操作。" + [portal.policies.config.retention] summary = "强制执行文档保留多久、何时归档以及何时删除。" @@ -7438,6 +8051,7 @@ title = "尚无活动" [portal.policies.endpoints] addWatermark = "水印" autoRedact = "遮盖 PII" +classifyAndLabel = "分类" compressPdf = "压缩" flatten = "扁平化" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "您的政策已保存,连接恢复后将显示。" retry = "重试" title = "后端不可用" +[portal.policies.operations] +change = "更改此步骤执行的操作" +noResults = "没有匹配的步骤。尝试产品名称,或“scan”、“notify”、“attach”。" +searchPlaceholder = "搜索步骤 - 试试“scan”、“attach”或产品名称" + +[portal.policies.operations.bodyMode] +binary = "原始文件字节" +json = "在 JSON payload 内" +multipart = "作为文件上传(multipart)" + +[portal.policies.operations.clamavScan] +description = "您自己的 ClamAV 服务器会检查文档;没有任何内容离开您的网络。" +label = "扫描病毒(自托管)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "拒绝包含宏、可执行文件、脚本或嵌入对象的文档。" +label = "阻止活动内容" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive 会检查文档;如果文档不干净,运行将停止。" +label = "扫描病毒" + +[portal.policies.operations.confluenceAttach] +description = "将处理后的文档归档到页面。" +label = "附加到 Confluence 页面" + +[portal.policies.operations.consignoSubmit] +description = "将文档提交到 ConsignO Cloud 进行签名。" +label = "发送签名(ConsignO)" +note = "提交文档。尚不支持取回已签名副本,因为步骤无法将结果传递给后续步骤。" + +[portal.policies.operations.customApiCall] +description = "自行编写调用:路径、方法、正文和响应处理。" +label = "调用自定义 API" + +[portal.policies.operations.discordNotify] +description = "通知频道该策略已处理一个文档。" +label = "向 Discord 发布消息" + +[portal.policies.operations.elasticIndex] +description = "将策略执行的操作记录为可搜索的文档。" +label = "在 Elasticsearch 中索引审计事件" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart 适合上传 API;JSON 适合希望文件位于 payload 内的 API;binary 发送原始字节。" +label = "如何发送文档" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "按原样发送的 JSON 正文。{{document.base64}} 携带文件;{{document.*}} 和 {{run.*}} 会按文档填充。" +label = "JSON 正文" + +[portal.policies.operations.fields.connection] +label = "账户" + +[portal.policies.operations.fields.domain] +label = "Mailgun 域名" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API 期望用于接收文档的表单字段。" +label = "文件字段名称" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "发件人" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "由 header 名称/值对组成的 JSON 对象。连接的凭据会为您添加。" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "索引" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "问题键" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "作为消息正文发送。{{document.*}} 和 {{run.*}} 会按文档填充。" +label = "消息" + +[portal.policies.operations.fields.method] +label = "方法" + +[portal.policies.operations.fields.pageId] +label = "页面 ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "附加到连接的基础 URL。它永远无法访问其他主机。" +label = "路径" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "写入驱动器中的位置。{{document.filename}} 会按文档填充。" +label = "目标路径" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "报告会保持文档不变。替换会将其换成返回的内容。" +label = "如何处理回复" + +[portal.policies.operations.fields.signerEmail] +label = "签名者的电子邮件" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "主题" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio 会分析此文本。使用 {{document.title}} 或粘贴您想检查的文本。" +label = "要检查的文本" + +[portal.policies.operations.fields.to] +label = "收件人" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud 用户名" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "通知空间该策略已处理一个文档。" +label = "向 Google Chat 发布消息" + +[portal.policies.operations.jiraAttach] +description = "将处理后的文档归档到问题。" +label = "附加到 Jira 问题" + +[portal.policies.operations.mailgunEmail] +description = "将处理后的文档作为附件发送。" +label = "通过电子邮件发送文档(Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "将处理后的文档写入文件夹。" +label = "上传到 Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "您自己的 Presidio 服务器会报告其发现的个人数据。" +label = "检测个人数据" +note = "Presidio 读取文本,而不是文件。请提供您想检查的文本 — 文档本身不会上传。" + +[portal.policies.operations.responseMode] +replace = "用回复替换文档" +report = "保留文档,记录回复" + +[portal.policies.operations.sendgridEmail] +description = "将处理后的文档作为附件发送。" +label = "通过电子邮件发送文档(SendGrid)" + +[portal.policies.operations.slackNotify] +description = "通知频道该策略已处理一个文档。" +label = "向 Slack 发布消息" + +[portal.policies.operations.splunkEvent] +description = "在审计人员已查看的位置记录策略执行的操作。" +label = "向 Splunk 发送审计事件" + +[portal.policies.operations.sumologicEvent] +description = "将策略执行的操作记录到 HTTP 收集器。" +label = "向 Sumo Logic 发送审计事件" + +[portal.policies.operations.teamsNotify] +description = "通知频道该策略已处理一个文档。" +label = "向 Teams 发布消息" + +[portal.policies.operations.webhookPost] +description = "将文档本身发送到您选择的 URL。" +label = "将文档发布到 webhook" + +[portal.policies.operations.zapierNotify] +description = "将运行详情交给您的自动化。" +label = "触发 Zap 或 Make 场景" + [portal.policies.stats] activeFor = "活动" dataProcessed = "已处理数据" @@ -7473,6 +8282,13 @@ label = "已执行文档" description = "已配置但未触发" label = "已暂停" +[portal.policies.table] +appliesTo = "应用于" +docs = "已执行文档" +enforces = "执行" +policy = "策略" +status = "状态" + [portal.policies.wizard.actions] back = "返回" cancel = "取消" @@ -7480,6 +8296,10 @@ continue = "继续" enablePolicy = "启用政策" saveChanges = "保存更改" +[portal.policies.wizard.capability.classify] +desc = "根据您团队的标签识别文档类型并为其打标签,使其可按类别归档和搜索。" +label = "分类文档" + [portal.policies.wizard.capability.compress] desc = "将文档压缩为更小的文件大小。" label = "减小文件大小" @@ -7500,10 +8320,18 @@ label = "遮盖敏感信息" desc = "移除隐藏的 JavaScript,确保文档打开时不会自动运行任何内容。" label = "移除活动内容" +[portal.policies.wizard.capability.timestampPdf] +desc = "使用独立时间戳机构证明该文档在某一时间点以此确切形式存在。只会发送 hash - 文档绝不会离开您的服务器。" +label = "添加可信时间戳" + [portal.policies.wizard.capability.watermark] desc = "在每一页上加盖可见标记(例如“机密”)。" label = "应用水印" +[portal.policies.wizard.classification] +description = "每个上传文档都会根据内置标签进行分类,并标记符合的类型。标签集在整个团队中共享。" +labelsHeading = "分类标签" + [portal.policies.wizard.errors] noTools = "请先在工作流中启用至少一个工具。" saveFailed = "无法保存政策。请重试。" @@ -7548,73 +8376,118 @@ setUp = "设置 {{category}} 政策" [portal.policies.wizard.workflow] description = "选择此政策对其处理的每个文档执行的操作。" -[portal.policySummary] -activeSummary = "{{active}} / {{total}} 活动" -noRule = "尚未执行规则" -subtitle = "每个文档都会经过的常驻自动化,无论由哪个管道处理。" -title = "您的 PDF 上运行的内容" - [portal.policySummary.action] -comingSoon = "升级到企业版" -configure = "配置" setUp = "设置" -[portal.policySummary.column] -activeRule = "活动规则" -policy = "政策" -status = "状态" +[portal.processorFlow] +footnote = "计数基于过去 24 小时。流程速度仅作示意。" +liveBadge = "实时" +stats = "{{connected}} 已连接 · {{processed}} 个 PDF 已处理" +title = "PDF 处理器" -[portal.policySummary.empty] -description = "配置政策后,类别将显示在这里。" -title = "尚无政策" +[portal.processorFlow.lens] +ariaLabel = "查看模式" +flow = "流程" +sankey = "桑基图" -[portal.policySummary.state] -active = "活动" -off = "关闭" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "失败" +heading = "结果" +success = "已交付" + +[portal.processorFlow.policies] +activeCount = "{{n}} 个活跃" +count = "{{n}} · 24h" +heading = "策略" +setUp = "设置" soon = "即将推出" -[portal.processingStatus] -managePlan = "管理方案" -volumeSuffix = "已处理 PDF · 最近 30 天" +[portal.processorFlow.sankey] +waist = "策略 · {{n}} 个活跃" + +[portal.processorFlow.sankey.empty] +description = "连接一个来源并启用一个策略,即可查看文档流经处理器。" +title = "暂无流程" + +[portal.processorFlow.sources] +comingSoonTag = "连接" +editor = "Stirling PDF Editor" +heading = "来源" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "电子邮件接收" [portal.procurement] -reset = "重置采购(演示)" subtitle = "让您的团队完成评估、签约和入门。所有文档集中一处。" title = "采购" -[portal.procurement.action] -download = "下载" -pay = "立即付款" -request = "请求" -sign = "审核并签署" -upload = "上传" - [portal.procurement.agreement] agreeCta = "同意并订阅" +confidential = "机密" confirm = "我已阅读并同意 Stirling Enterprise Agreement。" -eyebrow = "协议" -intro = "一份合并协议涵盖您的交易:Master Service Agreement、Order Form、EULA 和 Data Processing Agreement。请审核后同意,将报价接受为已承诺的订阅。" -title = "审核您的企业协议" +docName = "Stirling Enterprise Agreement" +docSub = "MSA、订单表、EULA 和 DPA,合并为一次签名。" +download = "下载" +downloadDraftError = "无法生成协议 PDF - 文档渲染器不可用。请稍后重试或联系支持。" +downloadError = "无法下载已签署协议。请重试。" +legalName = "法律实体名称" +legalNamePlaceholder = "将签署的法律实体" +loadError = "无法加载协议。请重试。" +loading = "正在加载协议..." +ref = "参考 {{ref}} · {{version}}" +requestChanges = "请求更改" +scrollHint = "滚动以审阅" +signatory = "签名人姓名" +signatoryPlaceholder = "全名" +signatoryTitle = "签名人职务" +signatoryTitlePlaceholder = "例如:总法律顾问" +signError = "无法记录您的签名。请重试。" [portal.procurement.builder] addons = "附加项" +addressLine1 = "地址行 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "地址行 2" +addressLine2Placeholder = "套房、楼层、建筑" back = "返回" businessName = "企业名称" businessNamePlaceholder = "您的公司" +city = "城市" +cityPlaceholder = "San Francisco" +completeRequired = "请在生成报价前填写必填字段(标记为 *),并提供有效电子邮件。" +contactEmail = "联系人电子邮件" +contactEmailPlaceholder = "jane@acme.com" +contactName = "联系人姓名" +contactNamePlaceholder = "Jane Doe" continue = "继续" -country = "国家/地区" -countryEuro = "欧元区 (EUR €)" -countryUK = "英国 (GBP £)" -countryUS = "美国 (USD $)" +done = "完成" eula = "我已阅读并同意 Stirling Enterprise EULA。它管理由此报价生成的协议。" generate = "生成报价" -included = "已包含" indemnification = "IP 赔偿" indemnificationSub = "我们根据 EULA 为符合条件的 IP 索赔提供抗辩" -offlineLicense = "离线 / 隔离网络许可证" -offlineLicenseSub = "用于隔离网络自托管实例的可下载许可证文件" +paperEyebrow = "企业报价" +paperFor = "准备给" +pdfSize = "PDF 大小" +poNumber = "PO 编号" +poNumberPlaceholder = "可选" +postalCode = "邮政编码" +postalCodePlaceholder = "94105" +posture = "治理" +posture_count = "~{{count}} 个策略" +posture_essentials = "基础" +posture_essentialsSub = "分类 + 共享 — 默认项" +posture_governed = "受治理" +posture_governedSub = "添加安全和路由" +posture_regulated = "受监管" +posture_regulatedSub = "每个类别 — 合规、保留、摄取" qbr = "季度业务评审" qbrSub = "您的 SE 每季度评审使用情况和路线图" +region = "州 / 地区" +regionPlaceholder = "California" running = "{{annual}} / 年 · {{years}} 年 {{tcv}}" s1Sub = "您的团队,以及预计每年运行的 PDF。" s1Title = "您将处理多少内容?" @@ -7623,6 +8496,12 @@ s2Title = "承诺和服务" s3Sub = "用于报价及其生成的协议。" s3Title = "您的详细信息" serviceLevel = "服务级别" +size_compact = "紧凑" +size_compactSub = "主要为文本,小于 1 MB" +size_heavy = "大型" +size_heavySub = "扫描件或大量图片,5 MB+" +size_standard = "标准" +size_standardSub = "文本和图片混合,1 到 5 MB" slDedicated = "专属" slDedicatedSub = "4 个工作小时 · 专属客户经理 · +30%" slPriority = "优先" @@ -7630,6 +8509,8 @@ slPrioritySub = "同一工作日 · 指定 CSM · +15%" slStandard = "标准" slStandardSub = "下一工作日 · 共享 CSM · 已包含" stepOf = "第 {{n}} 步,共 {{total}} 步" +taxId = "VAT / 税务 ID" +taxIdPlaceholder = "可选" term = "期限" termDiscount = "已应用 {{pct}}% 多年承诺折扣" title = "构建您的报价" @@ -7637,6 +8518,7 @@ training = "入门与培训" trainingSub = "实时课程,帮助您的团队开始使用" users = "用户总数" usersPlaceholder = "例如 250" +viewEula = "阅读" volEstimated = "根据 {{count}} 位用户估算(每人约 2,000 个 PDF,包括自动化)。如果您更清楚,可进行编辑。" volManual = "正在使用您的数值。可随时根据团队规模重新估算。" volNoUsers = "不确定?输入您的团队规模,我们会进行估算。" @@ -7645,55 +8527,55 @@ volumePlaceholder = "例如 1,000,000" years_one = "{{count}} 年" years_other = "{{count}} 年" -[portal.procurement.docs] -count_one = "{{count}} 个文档" -count_other = "{{count}} 个文档" -done = "完成" -here = "您在这里" -hide = "隐藏" -optional = "可选" -paidAddon = "付费附加项" -show = "显示" -subtitle = "旅程每一步所需的一切都会随着交易推进而显示出来。" -supportingSubtitle = "SOC 2、安全评审、税务表单等,在您的安全或采购团队需要时即可提供。部分项目会收取一次性费用。" -supportingTitle = "支持您的评估" +[portal.procurement.documents] +agreement = "企业协议" +agreementSub = "MSA、订单表和 DPA,一次签名完成。" +download = "下载" +eula = "EULA 和商业条款" +eulaSub = "软件和自助商业条款。" +invoice = "发票" +invoiceSub = "您的第一张订阅发票。" +laterInvoice = "接受后可用" +laterQuote = "报价发出后可用" +quote = "报价" +quoteSub = "您的逐项企业报价。" +sla = "SLA 附件" +slaSub = "支持等级和响应目标。" +subprocessors = "子处理方" +subprocessorsSub = "代表您处理数据的第三方。" +subtitle = "您的交易文件,随时可用。" title = "文档" -upcoming = "即将进行" +view = "查看" [portal.procurement.error] title = "出现问题" [portal.procurement.hero] -company = "您的企业交易" +barAria = "阶段 {{current}} / {{total}}" ctaAgreement = "审核并签署协议" -ctaLive = "您已上线" -ctaPayment = "添加付款" +ctaExploring = "设置您的试用" ctaQuote = "审核您的报价" +ctaReviewQuote = "审阅报价" ctaTrial = "构建您的报价" +documents = "文档" eyebrow = "企业采购" +eyebrowCompany = "{{company}} 企业版" inviteTeammates = "邀请队友" -keyDocs = "关键文档" -nextStep = "下一步:{{action}}" -notStarted = "未开始" +licenseKey = "许可证密钥" +liveSub = "您的组织已完成配置,许可证处于活跃状态。" +liveTitle = "您已在 Stirling Enterprise 上线" +next = "下一步:{{stage}}" open = "打开采购" scheduleCall = "安排通话" -setup1Sub = "邀请您的队友" -setup1Title = "部署 PDF 编辑器" -setup2Sub = "在编辑器和其他来源中启用处理" -setup2Title = "连接 PDF 处理器" -setup3Sub = "在需要时启用安全、合规、路由或保留" -setup3Title = "添加推荐政策" +sentenceAgreement = "审阅并签署协议" +sentenceLive = "配置和推出" +sentencePayment = "采购订单和付款" +sentenceQuote = "审阅并接受您的报价" +sentenceTrial = "设置您的试用工作区" [portal.procurement.journey] daysLeft_one = "剩余 {{count}} 天" daysLeft_other = "剩余 {{count}} 天" -engineerLabel = "您的解决方案工程师" -eyebrow = "您的推出流程" -live = "您已在 Stirling Enterprise 上线" -nextStep = "下一步:{{action}}" -subtitle = "您的解决方案工程师会参与每一步。一次只需完成一个下一步操作;完整清单如下。" -title = "从试用到上线,一条引导路径" -trialTitle = "企业试用" [portal.procurement.journeySteps.agreement] blurb = "一次签署涵盖 MSA、订单表、EULA 和 DPA。" @@ -7727,6 +8609,9 @@ downloadError = "暂时无法生成离线许可证文件——请稍后重试。 downloadOffline = "下载离线许可证 (.lic)" hint = "将此密钥粘贴到自托管实例中以激活它,或将其保留作记录。请妥善保管。" label = "您的许可证密钥" +subtitle = "使用此密钥激活自托管实例,或保留以作记录。" +title = "您的许可证密钥" +trialFileHint = "这是您的试用许可证文件。协议就位后,请回来再次下载 — 与在线许可证密钥不同,.lic 文件不会自行更新。" [portal.procurement.link] cta = "关联账户" @@ -7739,65 +8624,62 @@ description = "您的订阅已激活,许可证已签发。您的团队已预 eyebrow = "上线" title = "您已在 Stirling Enterprise 上线" -[portal.procurement.locked] -description = "开始企业评估后,试用密钥、承诺用量报价、一次签署协议、付款以及您的文档台账都将在这里。" -eyebrow = "仅限企业版" -talkToSales = "联系销售" -title = "采购流程随企业版开放" - [portal.procurement.milestone] -accept = "接受并继续" -description = "下载 PDF 与团队分享,准备好后回来接受,或进行更改。" -download = "下载 PDF" downloadError = "暂时无法下载报价 PDF——请稍后重试。" -edit = "编辑报价" -eyebrow = "报价 {{number}}" -perYear = " / 年" -preparedFor = "为 {{company}} 准备" -tcv = "{{value}} 合同总价值" -title = "您的报价已准备好" - -[portal.procurement.modal] -cancel = "取消" -chooseFile = "选择文件" -close = "关闭" -downloadBody = "您的下载将很快开始。" -downloadCta = "下载" -downloadTitle = "下载" -noFile = "未选择文件" -payBody = "通过 Stripe 使用银行卡或银行转账支付您的承诺合同。付款结清后,您的工作区会立即预配。" -payCta = "继续前往 Stripe" -payTitle = "确认付款" -requestBodyFree = "我们会按需生成此文档。确认后,您的解决方案工程师将很快发送给您。" -requestBodyPaid = "这是付费附加项。确认后,您的解决方案工程师将评估范围并发送文件。" -requestCta = "请求" -requestTitle = "请求此文档" -signBody = "打开 Stirling Enterprise Agreement 进行电子签名:一次签署涵盖 MSA、订单表、EULA 和 DPA。我们会自动会签,您将进入付款步骤。" -signCta = "打开以签署" -signTitle = "审核并签署您的协议" -uploadBody = "将您的 PO 发送给我们,我们会按您的条款开具发票。拖入 PDF 或在下方选择文件。" -uploadCta = "上传采购订单" -uploadTitle = "上传您的采购订单" [portal.procurement.payment] description = "您的报价已接受,许可证已激活——您的团队可以立即开始使用。准备好后支付第一张发票;您可以在这里付款或下载,无需电子邮件。" +downloadAgreement = "下载已签署协议" downloadInvoice = "下载发票" -simulate = "模拟已收到付款(演示)" +eyebrow = "付款" title = "订阅已创建" viewInvoice = "查看并支付发票" +[portal.procurement.review] +acceptCta = "接受报价" +annual = "年费(第 1 年)" +downloadCta = "下载报价" +poNumber = "采购订单:{{po}}" +renewal = "期限结束后以 {{amount}}/年续订(+{{pct}}% CPI)" +tcv = "{{years}} 年期限,预先全额支付:{{tcv}}" +validUntil = "有效期至 {{date}}" + [portal.procurement.schedule] fallback = "无法加载调度程序。" fallbackLink = "在新标签页中打开调度" +loading = "正在加载调度器..." subtitle = "您的解决方案工程师将带领团队完成推出流程。请选择适合您的时间。" title = "安排通话" -[portal.procurement.status] -action = "需要操作" -available = "可用" -complete = "完成" -pending = "待处理" -request = "按请求提供" +[portal.procurement.setup] +airgap = "隔离网络" +airgapSub = "封闭,无外发连接" +back = "返回" +businessName = "企业名称" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "托管,最快开始" +continue = "继续" +deployment = "应部署在哪里?" +eula = "我同意 Stirling EULA 和商业条款。" +fullName = "全名" +fullNamePlaceholder = "您的姓名" +invites = "邀请队友 · 可选" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "安排通话" +seats = "测试人数" +seatsPlaceholder = "例如:15" +selfhost = "自托管" +selfhostSub = "Docker 或 Kubernetes" +start = "开始试用" +stepOf = "第 {{n}} 步,共 {{total}} 步" +subtitle = "免费 14 天,无需银行卡。" +subtitleDetails = "为您的报价和协议提供一些详细信息。" +talkFirst = "想先聊聊?" +title = "设置您的试用" +viewEula = "阅读" +workEmail = "工作电子邮件" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "延长会增加 7 天并通知您的解决方案工程师。" @@ -7808,109 +8690,113 @@ maxed = "已达上限" subtitle = "您的免费试用持续到 {{date}}。无需银行卡。" title = "企业试用" -[portal.procurement.upsell] -homeBadge = "企业版" -homeBody = "承诺用量定价、全组织 SSO + SCIM + RBAC、90 天不可变审计,以及专属 SE。" -homeCta = "开始试用 →" -homeHeadline = "处理数百万个 PDF。" - -[portal.recentActivity] -title = "最近活动" -viewAll = "查看全部" - -[portal.recentActivity.empty] -description = "管道运行、部署和代理事件将显示在这里。" -title = "这里还没有内容" - -[portal.search] -ariaLabel = "搜索" -placeholder = "搜索 Stirling — 端点、管道、文档…" - -[portal.search.empty] -noActionsDescription = "快速操作可用后将显示在这里。" -noActionsTitle = "无快速操作" -noMatches = "没有匹配 \"{{query}}\" 的结果" -noMatchesDescription = "尝试其他关键词或浏览目录。" - [portal.settings.groups] admin = "管理员" [portal.settings.sections] account-link = "账户关联" -[portal.shell.header] -accountFallback = "账户" -accountMenu = "账户菜单" -darkMode = "深色模式" -lightMode = "浅色模式" -search = "搜索" -searchPlaceholder = "搜索…" -signOut = "退出登录" -switchToDark = "切换到深色主题" -switchToLight = "切换到浅色主题" - [portal.shell.sidebar] appEditor = "编辑器" appProcessor = "处理器" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} 个文档" linkAccount = "关联 Stirling 账户" -planEditor = "编辑器方案" -planEnterprise = "企业版方案" -planProcessor = "处理器方案" primaryNav = "主导航" switchApp = "切换应用" +[portal.shell.topbar] +closeNav = "关闭导航" +openNav = "打开导航" +search = "搜索" + [portal.sources] subtitle = "可重复使用的输入连接,将文档送入 Stirling。配置一次连接,然后可在任意数量的政策中引用它。点击一行可查看其配置以及使用它的政策。" title = "来源" [portal.sources.actions] -agentBuilder = "代理构建器" connectSource = "连接来源" +[portal.sources.builder] +back = "返回来源" +backToTypes = "所有来源类型" +cancel = "取消" +chooseHint = "选择文档来自哪里。灰色连接器即将推出。" +comingSoon = "即将推出" +comingSoonHeading = "即将推出" +create = "创建来源" +createTitle = "连接来源" +delete = "删除" +editTitle = "编辑来源" +enabled = "已启用" +save = "保存更改" + +[portal.sources.builder.folderAccess] +description = "文件夹自动化只能使用管理员已允许的文件夹。请在文件夹访问设置中添加它,然后重试。" +openSettings = "文件夹访问设置" +title = "不允许使用此文件夹" + [portal.sources.delete] body = "删除 \"{{name}}\"?此操作无法撤销。引用它的政策需要更新。" cancel = "取消" confirm = "删除" title = "删除来源?" -[portal.sources.detail] -closeAriaLabel = "关闭详情" -delete = "删除来源" -docs24h = "最近 24 小时" -docs30d = "最近 30 天" -docsTotal = "总计已见" -docsTrend = "最近 30 天的文档" -documents = "文档" -edit = "编辑" -notReferenced = "未被任何政策引用,因此可以安全删除。" -pause = "暂停" -resume = "恢复" -subtitle = "{{type}} · {{status}}" -usedBy = "使用者" - -[portal.sources.empty] -description = "连接一个文件夹(很快还可连接云存储),让您的政策有位置可拉取文档。" -title = "尚未连接来源" - [portal.sources.kpi] inUse = "使用中" total = "连接" unused = "未使用" +[portal.sources.networkFields.connection] +helperText = "包含服务器地址和凭据的已保存连接。由引用它的每个来源重复使用。" +label = "连接" + +[portal.sources.networkFields.directory] +helperText = "服务器上要轮询的文件夹,相对于登录主目录或共享根目录。留空表示根目录。" +label = "文件夹" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume 会在每个策略处理完文件后,从服务器移除该文件。" +label = "读取模式" + +[portal.sources.networkFields.mode.options] +consume = "Consume:每个文件只处理一次" +snapshot = "Snapshot:每次运行重新读取文件夹" + +[portal.sources.networkFields.recursive] +label = "文件夹深度" + +[portal.sources.networkFields.recursive.options] +all = "包含子文件夹" +top = "仅顶层" + [portal.sources.status] active = "活动" disabled = "已禁用" unused = "未使用" [portal.sources.table] +documents = "文档" source = "来源" status = "状态" +type = "Type" usedBy = "政策" +[portal.sources.types.box] +description = "从您团队已共享的 Box 文件夹中拉取文档。" +label = "Box" + +[portal.sources.types.dropbox] +description = "从共享 Dropbox 文件夹中拉取文档。" +label = "Dropbox" + [portal.sources.types.editor] +description = "您的团队在编辑器中处理过的文档,涵盖策略和 AI 运行。" label = "编辑器" +noPolicies = "编辑器尚未运行任何策略。" + +[portal.sources.types.email] +description = "将发送到专用邮箱的附件转换为文档。" +label = "电子邮件收件箱" [portal.sources.types.folder] description = "监视服务器上的目录以发现新文档。" @@ -7943,25 +8829,80 @@ label = "文件夹深度" all = "包含子文件夹" top = "仅顶层" +[portal.sources.types.ftp] +description = "轮询 FTP 或 FTPS 服务器文件夹以获取新文档。" +label = "FTP" + +[portal.sources.types.googledrive] +description = "从共享 Google Drive 文件夹中拉取文档。" +label = "Google Drive" + +[portal.sources.types.network] +description = "监视 SMB 网络共享以获取新文档。" +label = "网络驱动器" + +[portal.sources.types.onedrive] +description = "监视 OneDrive 文件夹以获取新文档。" +label = "OneDrive" + +[portal.sources.types.s3] +description = "从 Amazon S3 或 S3-compatible 存储桶中拉取文档。" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "保存的连接,包含存储桶和凭据。由引用它的每个来源和流水线输出重复使用。" +label = "连接" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume 会在每个策略处理完对象后,从存储桶移除该对象。" +label = "读取模式" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume:每个对象只处理一次" +snapshot = "Snapshot:每次运行重新读取存储桶" + +[portal.sources.types.s3.fields.prefix] +helperText = "仅处理键以此前缀开头的对象。" +label = "键前缀" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "轮询 SFTP 投递文件夹以获取新文档。" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "从 SharePoint 文档库中拉取文档。" +label = "SharePoint" + [portal.sources.types.unknown] label = "来源" +[portal.sources.types.webhook] +createNote = "创建此 webhook 时会生成投递 URL 和签名密钥。发送方将文档 POST 到该 URL,引用的策略会在到达时运行。" +description = "通过来自任何外部系统的已签名 HTTP POST 接收文档。" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "投递 URL" +secretNote = "签名密钥仅在创建 webhook 时显示一次。重新创建来源以轮换它。" + +[portal.sources.types.webhook.reveal] +copy = "复制" +done = "完成" +secret = "签名密钥" +secretHelp = "使用此密钥(HMAC-SHA256)对每次投递的原始正文进行签名,并将其作为 X-Stirling-Signature header 发送。" +secretWarning = "现在复制签名密钥。为了您的安全,它只显示一次,之后无法找回。" +title = "Webhook 已创建" +url = "投递 URL" +usage = "将每个文档作为原始请求正文 POST 到投递 URL,并使用 binary 内容类型(application/pdf 或 application/octet-stream)。引用的策略会在到达时自动运行。" + [portal.sources.wizard] -back = "返回" -cancel = "取消" -continue = "继续" -editTitle = "编辑来源" name = "名称" namePlaceholder = "例如 理赔接收" -save = "保存更改" -subtitle = "第 {{current}} 步,共 {{total}} 步 · {{label}}" -title = "连接来源" type = "类型" -[portal.sources.wizard.steps] -chooseType = "选择类型" -configure = "配置" -review = "审核并连接" +[portal.stepModal] +close = "关闭" [portal.usage] managePayment = "管理付款" @@ -8021,16 +8962,9 @@ summary = "拥有团队——管理其成员的资源和共享配置。" 2 = "通过默认政策访问门户" 3 = "成员能做的一切" -[portal.welcome] -ariaLabel = "欢迎使用 Stirling PDF" -badge = "开源" -installEditor = "安装编辑器" -inviteTeammates = "邀请队友" -openInBrowser = "在浏览器中打开" -perks = "永久免费 · 可自托管" -subtitle = "世界上最安全的 PDF 编辑器对各种规模的团队免费。包含 60+ 项 PDF 操作和 SSO。" -title = "欢迎使用" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · 无限" [printFile] title = "ཡིག་ཆ་པར་འདེབས།" @@ -8613,6 +9547,14 @@ text = "以 90 度为增量将 PDF 页面顺时针或逆时针旋转。PDF 中 [rotate.tooltip.header] title = "旋转设置概览" +[ruler] +clearAll = "全部清除" +hideAllLabels = "隐藏所有标签" +hideSmallLabels = "隐藏小标签" +physicalValues = "物理" +scaled = "已缩放" +showAllLabels = "显示所有标签" + [sanitize] desc = "从 PDF 文件中移除潜在有害元素。" sanitizationResults = "净化结果" @@ -8654,31 +9596,45 @@ label = "移除 XMP 元数据" [sanitize.steps] settings = "设置" -[sanitizePdf] -tags = "གཙང་སེལ།,བདེ་འཇགས།,ཉེན་མེད།,ཉེན་ཁ་སེལ་བ།" - [sanitizePDF] title = "PDF གཙང་སེལ།" +[sanitizePdf] +tags = "གཙང་སེལ།,བདེ་འཇགས།,ཉེན་མེད།,ཉེན་ཁ་སེལ་བ།" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "ཤོག་ངོས་ཆེ་ཆུང་སྙོམ་སྒྲིག" +[scaleSettings] +activeScale = "当前比例" +apply = "应用比例" +applyCalibration = "应用校准" +calculatedScale = "计算比例" +calibrate = "校准" +calibrating = "正在校准" +calibrationDistanceRequired = "请输入大于零的实际距离" +calibrationInvalid = "无法根据此测量值计算比例" +calibrationPaperDistance = "测得页面距离:{{distance}}" +calibrationTitle = "校准比例" +calibrationTooltip = "测量已知距离以自动计算比例" +customScale = "自定义比例" +noneSet = "未设置自定义比例" +presets = "预设比例" +ratio = "比例比率" +ratioHelp = "比率:1 个页面单位 = X 个实际单位" +ratioPlaceholder = "例如,100" +ratioPositive = "比率必须大于零" +ratioRequired = "必须填写比率" +realDistance = "实际距离" +realDistancePlaceholder = "例如,5" +reset = "重置为默认值" +unit = "单位" +unitInvalid = "无效单位:{{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "�ུར་ཚད་མཚམས།" -10 = "ཕྱིར་འདོན་པའི་ནང་དཀར་མཐའ་འགོག་པའི་ཆེད་དུ་སྣོན་པ་དང་སུབ་པ་བྱེད་པ�� (སྔོན་སྒྲིག 1)" -2 = "པར་རིས་འཁོར་སྐྱོད་བྱེད་དགོས་པའི་ཉུང་མཐའི་ཟུར་ཚད་སྒྲིག་འགོད་བྱེད་པ། (སྔོན་སྒྲིག 10)" -3 = "བཟོད་སྲན་ཚད།" -4 = "ཚོད་དཔག་བྱས་པའི་རྒྱབ་ལྗོངས་ཚོས་གཞིའི་མཐའ་འཁོར་གྱི་ཚོས་མདོག་འགྱུར་བའི་ཁྱབ་ཚད་ཐག་གཅོད་བྱེད་པ། (སྔོན་སྒྲིག 30)" -5 = "ཉུང་མཐའི་རྒྱ་ཁྱོན།" -6 = "པར་རིས་ཤིག་གི་ཉུང་མཐའི་རྒྱ་ཁྱོན་མཚམས་སྒྲིག་འགོད་བྱེད་པ། (སྔོན་སྒྲིག 10000)" -7 = "ཉུང་མཐའི་མཐའ་འཁོར་རྒྱ་ཁྱོན།" -8 = "པར་རིས་ཤིག་གི་ཉུང་མཐའི་མཐའ་འཁོར་རྒྱ་ཁྱོན་མཚམས་སྒྲིག་འགོད་བྱེད་པ།" -9 = "མཐའ་མཚམས་ཆེ་ཆུང་།" - [scannerImageSplit] submit = "提取图像扫描" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "功能简介" whatThisDoesDesc = "自动在扫描页或合成图中查找并提取每张照片——无需手动裁剪。" whenToUse = "适用场景" +[ScannerImageSplit.selectText] +1 = "�ུར་ཚད་མཚམས།" +10 = "ཕྱིར་འདོན་པའི་ནང་དཀར་མཐའ་འགོག་པའི་ཆེད་དུ་སྣོན་པ་དང་སུབ་པ་བྱེད་པ�� (སྔོན་སྒྲིག 1)" +2 = "པར་རིས་འཁོར་སྐྱོད་བྱེད་དགོས་པའི་ཉུང་མཐའི་ཟུར་ཚད་སྒྲིག་འགོད་བྱེད་པ། (སྔོན་སྒྲིག 10)" +3 = "བཟོད་སྲན་ཚད།" +4 = "ཚོད་དཔག་བྱས་པའི་རྒྱབ་ལྗོངས་ཚོས་གཞིའི་མཐའ་འཁོར་གྱི་ཚོས་མདོག་འགྱུར་བའི་ཁྱབ་ཚད་ཐག་གཅོད་བྱེད་པ། (སྔོན་སྒྲིག 30)" +5 = "ཉུང་མཐའི་རྒྱ་ཁྱོན།" +6 = "པར་རིས་ཤིག་གི་ཉུང་མཐའི་རྒྱ་ཁྱོན་མཚམས་སྒྲིག་འགོད་བྱེད་པ། (སྔོན་སྒྲིག 10000)" +7 = "ཉུང་མཐའི་མཐའ་འཁོར་རྒྱ་ཁྱོན།" +8 = "པར་རིས་ཤིག་གི་ཉུང་མཐའི་མཐའ་འཁོར་རྒྱ་ཁྱོན་མཚམས་སྒྲིག་འགོད་བྱེད་པ།" +9 = "མཐའ་མཚམས་ཆེ་ཆུང་།" + [search] noResults = "未找到结果" placeholder = "输入搜索词..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "您的 Stirling-PDF 服务器已离线,且本地后 [session] expired = "ཁྱེད་ཀྱི་གླེང་མོལ་དུས་ཡོལ་ཟིན། ཤོག་ངོས་གསར་སྒྱུར་བྱས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།" -[sessionManagement.tooltip] -header = "签署会话管理" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "新增参与者将被添加到签署顺序末尾" -bullet2 = "会话定稿后无法再添加参与者" -bullet3 = "到其轮次时,每位参与者都会收到通知" -description = "在定稿之前,您可随时向活动会话添加更多参与者。" -title = "添加参与者" - -[sessionManagement.tooltip.finalization] -bullet1 = "完全定稿:所有参与者均已签署" -bullet2 = "部分定稿:部分参与者尚未签署" -bullet3 = "未签署的参与者将被排除在最终文档之外" -bullet4 = "定稿后,您可以将已签署的 PDF 加载到活动文件中" -description = "定稿会将所有签名合并为一个已签署的 PDF。此操作无法撤销。" -title = "会话定稿" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "无法移除已签署的参与者" -bullet2 = "被移除的参与者将不再接收通知" -bullet3 = "签署顺序将自动调整" -description = "参与者可在签署前从会话中移除。" -title = "移除参与者" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "每个签名都会按顺序应用到 PDF" -bullet2 = "后续签署者可以看到先前的签名" -bullet3 = "对审批流程和法律保全链至关重要" -description = "您在创建会话时指定的顺序决定谁先签署。" -title = "签署顺序" - [settings] close = "关闭" +[settings.ai] +documents = "文档与 RAG" +general = "常规" +limits = "限制与性能" +models = "模型与提供商" +title = "AI" + [settings.configuration] advanced = "高级" database = "数据库" endpoints = "端点" features = "功能" +folderAccess = "文件夹访问权限" mcp = "MCP Server" storageSharing = "文件存储与共享" systemSettings = "系统设置" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "默认 PDF 编辑器" defaultPdfEditorActive = "Stirling PDF 是您的默认 PDF 编辑器" defaultPdfEditorChecking = "正在检查..." defaultPdfEditorInactive = "已由其他应用设为默认" +defaultPdfEditorRemind = "提醒我设为默认" +defaultPdfEditorRemindDescription = "当 Stirling PDF 不是您的默认 PDF 应用程序时显示横幅。" defaultPdfEditorSet = "已是默认" defaultStartupView = "启动时的默认视图" defaultStartupViewDescription = "选择应用启动时左侧栏中默认激活的选项卡" @@ -8817,9 +9763,18 @@ languageDescription = "选择显示语言" logout = "退出登录" setAsDefault = "设为默认" theme = "主题" +themeAccent = "强调色" +themeAccentCustom = "自定义颜色" +themeAccentCustomHint = "自定义(建议使用默认值)" +themeAccentDark = "深色模式强调色" +themeAccentDefault = "默认" +themeAccentDefaultHint = "默认主题(推荐)" +themeAccentDescription = "按钮、链接和高亮会遵循该颜色,并为应用轻微着色。浅色和深色模式各自有单独设置。系统会使用当前启用的设置。" +themeAccentLight = "浅色模式强调色" themeDark = "深色" themeDescription = "在浅色与深色模式之间切换" themeLight = "浅色" +themeReset = "将主题恢复为默认" themeSystem = "System" title = "常规" user = "用户" @@ -8833,6 +9788,12 @@ intro = "为您的组织启用用户认证、团队管理和工作区功能。" learnMore = "在文档中了解更多" title = "面向系统管理员" +[settings.general.loginLanding] +description = "选择登录 Stirling Cloud 后打开哪个应用。" +editor = "编辑器" +processor = "处理器" +title = "登录后" + [settings.general.mode] fullscreen = "全屏" sidebar = "侧边栏" @@ -9013,9 +9974,6 @@ title = "政策与隐私" [settings.preferences] title = "首选项" -[settings.search] -placeholder = "搜索设置页面..." - [settings.security] description = "更新您的密码以确保账户安全。" title = "安全" @@ -9041,6 +9999,11 @@ security = "安全" telegram = "Telegram" title = "安全与认证" +[settings.signOut] +confirm = "您确定要退出登录吗?" +submit = "退出登录" +title = "退出登录" + [settings.team] title = "团队" @@ -9156,6 +10119,7 @@ last = "ཤོག་ངོས་མཐའ་མ།" maintainRatio = "བསྡུར་ཚད་རྒྱུན་འཁྱོངས་སྒོ་རྒྱག་པ།" next = "ཤོག་ངོས་རྗེས་མ།" noSavedSigs = "ཉར་ཚགས་བྱས་པའི་མིང་རྟགས་མ་རྙེད།" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "སྒེར་གྱི་མིང་རྟགས།" previous = "ཤོག་ངོས་སྔོན་མ།" redo = "重做" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "背景移除失败" hint = "上传您的签名 PNG 或 JPG 图片" label = "上传签名图片" placeholder = "选择图像文件" +previewAlt = "Current image signature" processing = "正在处理图像..." removeBackground = "移除白色背景(变为透明)" @@ -9207,6 +10172,17 @@ saved = "在上方选择一个已保存签名,然后在 PDF 上任意点击进 text = "在上方输入您的姓名后,在 PDF 任意位置点击以放置签名。" title = "如何添加签名" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "移动签名" pause = "暂停放置" @@ -9653,10 +10629,30 @@ title = "上传到服务器" updateButton = "在服务器上更新" uploadButton = "上传到服务器" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "点击此处" +fallback = "如果没有自动打开," +opening = "正在新标签页中打开 Swagger UI..." tags = "api,文档,swagger,端点,开发" title = "API 文档" @@ -9764,12 +10760,39 @@ selectFilesHint = "在“活动文件”中选择文件以运行此工具" singleFileScope = "仅应用于:{{fileName}}" viewerMode = "切换到文件编辑器以选择多个文件。" +[toolFormat] +ANY = "任何文件" +CBR = "漫画书归档文件 (CBR)" +CBZ = "漫画书归档文件 (CBZ)" +CSV = "CSV 文件" +EBOOK = "电子书" +EMAIL = "电子邮件" +EXCEL = "电子表格" +HTML = "网页" +IMAGE = "图像" +JAVASCRIPT = "JavaScript 文件" +JSON = "JSON 文件" +MARKDOWN = "Markdown 文件" +NONE = "无文件" +PCL = "PCL 打印文件" +PDF = "PDF" +PDF_ENCRYPTED = "受密码保护的 PDF" +POSTSCRIPT = "PostScript 文件" +PPT = "演示文稿" +TEXT = "文本文件" +VIDEO = "视频" +WORD = "Word 文档" +XML = "XML 文件" +XPS = "XPS 文档" +ZIP = "ZIP 归档文件" + [toolPanel] alpha = "Alpha" backToAllTools = "返回所有工具" collapse = "折叠面板" expand = "展开面板" goBack = "返回" +pdfTools = "PDF 工具" placeholder = "选择一个工具以开始" premiumFeature = "高级功能:" toolsHeader = "工具" @@ -9806,11 +10829,6 @@ noToolsFound = "未找到工具" quickAccess = "快速访问" searchPlaceholder = "搜索工具..." -[toolPicker.categories] -advancedTools = "高级工具" -recommendedTools = "推荐工具" -standardTools = "标准工具" - [toolPicker.subcategories] advancedFormatting = "高级格式化" automation = "自动化" @@ -9824,6 +10842,9 @@ removal = "移除" signing = "签名" verification = "验证" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "未找到工具" noTools = "没有可用的工具" @@ -9950,28 +10971,53 @@ title = "用户" you = "(您)" [users.action] +cancelInvite = "取消邀请" deleteTeam = "删除团队" disableMfa = "重置 MFA" move = "移动到团队" reinstate = "恢复" remove = "从组织中移除" +removeTeam = "从团队中移除" rename = "重命名团队" resetPw = "重置密码" suspend = "暂停" unlock = "解锁账户" +[users.activity] +daysAgo = "{{count}} 天前" +hoursAgo = "{{count}} 小时前" +justNow = "刚刚" +minutesAgo = "{{count}} 分钟前" +monthsAgo_one = "{{count}} 个月前" +monthsAgo_other = "{{count}} 个月前" +never = "从未" +weeksAgo_one = "{{count}} 周前" +weeksAgo_other = "{{count}} 周前" +yearsAgo_one = "{{count}} 年前" +yearsAgo_other = "{{count}} 年前" + [users.cap] addProcessor = "+ 处理器" approver = "批准政策" editor = "编辑器" processor = "处理器" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "要取消对 {{email}} 的邀请吗?他们将无法使用当前链接加入。" +cancelInviteTitle = "取消邀请" deleteTeamBody = "删除 {{name}} 团队?团队必须先为空 - 将其成员移动到其他团队,并且它不能仍拥有任何集成配置。" deleteTeamTitle = "删除团队" disableMfaBody = "移除 {{name}} 的 MFA 注册?如果需要,他们将在下次登录时重新设置。" disableMfaTitle = "重置 MFA" removeBody = "永久从组织中移除 {{name}}?此操作无法撤销。" +removeTeamBody = "要将 {{name}} 从团队中移除吗?他们将保留账户,但会失去对此团队资源的访问权限。" removeTitle = "移除成员" [users.empty] @@ -9982,10 +11028,8 @@ title = "尚无成员" addToTeam = "添加到团队" guestCount = "{{count}} 位访客" guests = "访客" -guestsDesc = "外部协作者,范围限于您共享的内容。仅编辑器。" ledBy = "由 {{owner}} 领导" org = "组织" -orgDesc = "拥有组织范围权限和政策批准权的所有者" owners = "{{count}} 位所有者" team = "{{name}} 团队" teamMeta = "{{count}} 人" @@ -10022,6 +11066,18 @@ username = "用户名" usernameError = "用户名必须至少包含 3 个字符" usernamePlaceholder = "jsmith" +[users.invites] +by = "由 {{who}} 邀请" +cancel = "取消" +expiresInDays_one = "{{count}} 天后过期" +expiresInDays_other = "{{count}} 天后过期" +expiresToday = "今天过期" +title = "待处理邀请" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "连接后端时出错,或者你没有访问权限。请重试。" title = "无法加载成员" @@ -10211,10 +11267,15 @@ searchPlaceholder = "搜索附件" title = "附件" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "书签标题" +bookmarkTitleRequired = "必须填写书签标题" closeSidebar = "关闭书签侧边栏" collapseAll = "折叠所有书签" +empty = "No bookmarks in this document" expandAll = "展开所有书签" +searchPlaceholder = "搜索书签" +title = "Bookmarks" [viewer.comments] addComment = "添加评论" @@ -10236,10 +11297,12 @@ locateAnnotation = "在文档中定位" moreActions = "更多操作" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "第 {{page}} 页" placingHint = "单击页面进行放置…(取消)" removeCommentOnly = "仅移除评论" saveReply = "保存回复" +searchPlaceholder = "Search comments" title = "评论" typeComment = "评论" typeInsertText = "插入文本" @@ -10262,6 +11325,7 @@ unsavedDesc = "您有未保存的更改" closeSidebar = "关闭图层侧边栏" hideAll = "隐藏所有图层" showAll = "显示所有图层" +title = "Layers" [viewer.link] delete = "删除链接" @@ -10605,30 +11669,6 @@ title = "水印文本" image = "图像" text = "文本" -[wetSignature.tooltip] -header = "签名创建方式" - -[wetSignature.tooltip.draw] -bullet1 = "自定义笔的颜色和粗细" -bullet2 = "清除并重新绘制,直到满意为止" -bullet3 = "适用于触控设备(平板、手机)" -description = "使用鼠标或触摸屏创建手写签名。最适合个人、真实的签名。" -title = "绘制签名" - -[wetSignature.tooltip.type] -bullet1 = "可从多种字体中选择" -bullet2 = "自定义文本大小和颜色" -bullet3 = "非常适合标准化签名" -description = "从输入的文本生成签名。快速且一致,适用于商务文档。" -title = "输入签名" - -[wetSignature.tooltip.upload] -bullet1 = "支持 PNG、JPG 及其他图像格式" -bullet2 = "为获得最佳效果,建议使用透明背景" -bullet3 = "图像会调整大小以适配签名区域" -description = "上传预先创建的签名图像。若您有扫描的签名或公司徽标,效果理想。" -title = "上传签名图像" - [workbenchBar] activeFiles = "活动文件" annotations = "注释" @@ -10642,6 +11682,7 @@ exitRedaction = "退出涂黑模式" exportAll = "导出 PDF" exportSelected = "导出所选页面" formFill = "填写表单" +hideToolbar = "Hide toolbar" multiTool = "多功能工具" panMode = "平移模式" print = "打印 PDF" @@ -10652,6 +11693,7 @@ redact = "涂黑" rotateLeft = "向左旋转" rotateRight = "向右旋转" ruler = "标尺 / 测量" +rulerSettings = "比例设置" save = "保存" saveAll = "全部保存" saveAs = "另存为" @@ -10661,6 +11703,7 @@ selectAll = "全选" selectByNumber = "按页码选择" selectLanguage = "选择语言" share = "共享" +showToolbar = "Show toolbar" toggleAnnotations = "切换注释可见性" toggleAttachments = "切换附件" toggleBookmarks = "切换书签" diff --git a/frontend/editor/public/locales/zh-CN/translation.toml b/frontend/editor/public/locales/zh-CN/translation.toml index a391627a4b..c63f0e2b79 100644 --- a/frontend/editor/public/locales/zh-CN/translation.toml +++ b/frontend/editor/public/locales/zh-CN/translation.toml @@ -1,7 +1,6 @@ alphabet = "字母表" apply = "应用" applyAndContinue = "应用并继续" -areYouSure = "确定要离开吗?" back = "返回" black = "黑色" blue = "蓝色" @@ -46,10 +45,8 @@ filesSelected = "选中的文件" font = "字体" fontSizeTooltip = "页码文本的点数大小。数值越大文字越大。" fontTypeTooltip = "页码的字体族。根据文档风格选择。" -genericSubmit = "提交" green = "绿色" help = "帮助" -imgPrompt = "选择图像" incorrectPasswordMessage = "当前密码不正确。" info = "信息" invalidUndoData = "无法撤销:操作数据无效" @@ -62,7 +59,6 @@ loginAgreementTitle = "登录协议" logOut = "退出登录" marginTooltip = "页码与页面边缘的距离。" moreOptions = "更多选项" -multiPdfPrompt = "选择多个 PDF(2个或更多)" never = "从不" no = "否" noFileSelected = "未选择文件,请上传一个文件。" @@ -80,7 +76,6 @@ pageOrderPrompt = "页面顺序(输入逗号分隔的页码列表或函数) pages = "页码" pageSelectionPrompt = "自定义页面选择(输入以逗号分隔的页码列表或函数:1,5,6、2n+1):" password = "密码" -pdfPrompt = "选择 PDF" pin = "固定文件(在工具运行后保持激活)" pro = "专业版" processingComplete = "您的文件已就绪。" @@ -110,6 +105,7 @@ undoSuccess = "已成功撤销操作" unknown = "未知" unpin = "取消固定文件(在工具运行后替换)" unsavedChanges = "您的 PDF 有未保存的更改。您想做什么?" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "未保存的更改" unsupported = "不受支持" username = "用户名" @@ -356,6 +352,7 @@ text = "这些权限控制用户对 PDF 的操作。与所有者密码配合时 title = "更改权限" [addStamp] +preview = "预览印章" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "手动下载:点击链接并将文件放入 tessdata 文件夹 noLanguages = "在配置的目录中未找到 tessdata 语言。" permissionNotice = "tessdata 路径不可写。下载将会在浏览器中打开;请手动将 .traineddata 文件保存到 tessdata 文件夹中。" +[admin.settings.ai.documents] +description = "配置用于回答文档问题的嵌入模型和检索设置。保存后应用到 AI 引擎。" +title = "文档与 RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "留空以使用引擎自身的环境凭据。适用于自托管单引擎部署。" +generic = "嵌入 API key" +openai = "OpenAI API key" +setPlaceholder = "已保存 - 留空以保留当前 key" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI 兼容 / Ollama 嵌入端点的基础 URL,例如 http://ollama:11434/v1。必须指向受信任的内部端点(SSRF 敏感)。" +label = "嵌入基础 URL" + +[admin.settings.ai.documents.embeddingModel] +description = "嵌入模型名称。可自由输入;建议仅作提示。" +label = "嵌入模型" + +[admin.settings.ai.documents.embeddingProvider] +description = "用于将文档文本转换为向量嵌入的提供商。" +label = "嵌入提供商" + +[admin.settings.ai.documents.maxSearches] +description = "代理每个请求最多可执行的检索搜索次数。" +label = "最大搜索次数" + +[admin.settings.ai.documents.reindexNote] +body = "更改嵌入模型会立即生效,但使用先前模型索引的文档必须重新索引,搜索才能返回正确结果。" +title = "需要重新索引" + +[admin.settings.ai.documents.saved] +reindexNote = "如果更改了嵌入模型,请重新索引现有文档,以便搜索使用新模型。" + +[admin.settings.ai.documents.topK] +description = "每次搜索检索的最相关片段数量。" +label = "Top K" + +[admin.settings.ai.general] +description = "将 Stirling 连接到 Python AI 引擎,并选择公开哪些 AI 能力。更改将在重启后生效。" +title = "AI 引擎" + +[admin.settings.ai.general.capabilities] +description = "开启或关闭单个 AI 功能。已禁用的功能会在应用中隐藏。" +title = "能力" + +[admin.settings.ai.general.enabled] +description = "总开关。关闭时,所有 AI 工具、代理或引擎调用都不可用。" +label = "启用 AI" + +[admin.settings.ai.general.features.chat] +description = "用于处理 PDF 的对话助手。" +label = "聊天助手" + +[admin.settings.ai.general.features.classify] +description = "按类型或内容自动分类文档。" +label = "文档分类" + +[admin.settings.ai.general.features.createPdf] +description = "通过 AI 代理生成新的 PDF(例如从 HTML 生成)。" +label = "从提示词创建 PDF" + +[admin.settings.ai.general.features.documentQuestions] +description = "提问并获取基于上传文档的答案。" +label = "文档问答" + +[admin.settings.ai.general.features.mathAuditor] +description = "检查文档中的数学和数值错误。" +label = "数学审核器" + +[admin.settings.ai.general.features.pdfComment] +description = "向 PDF 添加由 AI 编写的审阅评论和注释。" +label = "PDF 评论代理" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "文档生成等较重代理操作的超时时间。" +label = "长时间运行超时(秒)" + +[admin.settings.ai.general.note] +body = "AI 引擎作为独立服务运行。其共享密钥" +body2 = "通过容器环境变量设置。提供商 API key 可以在这些页面输入,也可以作为引擎环境变量提供;此处输入的 key 会在保存时推送到引擎。" +title = "关于 AI 引擎" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "流式(逐 token)聊天响应的超时时间。" +label = "流超时(秒)" + +[admin.settings.ai.general.test] +button = "测试连接" +failBody = "AI 引擎未响应。请检查 URL、引擎容器是否正在运行,以及 AI 是否已启用(启用后需要重启)。" +failTitle = "无法访问 AI 引擎" +okBody = "AI 引擎已响应健康检查。" +okTitle = "AI 引擎可访问" + +[admin.settings.ai.general.timeoutSeconds] +description = "发送到引擎的标准 AI 请求超时时间。" +label = "请求超时(秒)" + +[admin.settings.ai.general.url] +description = "Python AI 引擎的内部 URL,例如 http://stirling-pdf-engine:5001。" +label = "AI 引擎 URL" + +[admin.settings.ai.limits] +description = "限制 AI 请求可执行的工作量以及并发运行数量的保护措施。保存后应用到 AI 引擎。" +title = "限制与性能" + +[admin.settings.ai.limits.maxCharacters] +description = "保护措施:拒绝提取文本超过此长度的 AI 请求。" +label = "每个请求最大字符数" + +[admin.settings.ai.limits.maxPages] +description = "保护措施:拒绝超过此 PDF 页数的 AI 请求。" +label = "每个请求最大页数" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "整个引擎中同时进行的模型调用最大数量。" +label = "模型最大并发数" + +[admin.settings.ai.models] +description = "选择 AI 引擎使用的 LLM 提供商以及智能/快速模型。保存后应用到 AI 引擎。" +title = "模型与提供商" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "留空以使用引擎自身的环境凭据。适用于自托管单引擎部署。" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "已保存 - 留空以保留当前 key" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI 兼容 / Ollama 端点的基础 URL,例如 http://ollama:11434/v1。" +label = "提供商基础 URL" +warning = "基础 URL 必须指向受信任的内部端点。引擎将向其发起服务器端请求,因此不受信任的值存在 SSRF 风险。" + +[admin.settings.ai.models.fastMaxTokens] +description = "快速模型的最大输出 token 数。" +label = "快速模型最大 token 数" + +[admin.settings.ai.models.fastModel] +description = "用于轻量任务的更便宜、更快速模型。可自由输入;建议仅作提示。" +label = "快速模型" + +[admin.settings.ai.models.provider] +description = "引擎连接的 LLM 提供商。" +label = "提供商" + +[admin.settings.ai.models.smartMaxTokens] +description = "智能模型的最大输出 token 数。" +label = "智能模型最大 token 数" + +[admin.settings.ai.models.smartModel] +description = "用于复杂推理的高能力模型。可自由输入;建议仅作提示。" +label = "智能模型" + +[admin.settings.ai.saved] +body = "更改会自动推送到 AI 引擎。" +bodyNoPush = "设置已保存。下次 AI 引擎获取其配置时将应用这些设置。" +title = "AI 设置已保存" + [admin.settings.badge] clickToUpgrade = "点击查看计划详情" @@ -938,6 +1094,25 @@ label = "启动时重新生成" description = "证书的有效天数" label = "证书有效期(天)" +[admin.settings.folderAccess] +description = "允许文件夹源和文件夹输出读取与写入的目录。这是一个安全边界:自动化绝不能指向此列表之外的服务器路径。" +securityNote = "留空可完全禁用文件夹源和输出。Stirling 自身的配置目录始终禁止访问。" +title = "文件夹访问" + +[admin.settings.folderAccess.implied] +description = "这些由 Stirling 管理的目录始终允许访问,且不能在此处更改。" +serverStorage = "服务器文件存储" +title = "始终允许" +watchedFolder = "流水线监视文件夹" + +[admin.settings.folderAccess.roots] +add = "添加" +empty = "未允许任何文件夹。文件夹源和输出当前已禁用。" +hint = "输入绝对路径,例如 /data/inbox。" +label = "允许的文件夹根目录" +placeholder = "/data/inbox" +remove = "移除文件夹根目录" + [admin.settings.general] description = "配置全局应用设置,包括品牌和默认行为。" system = "系统" @@ -1704,6 +1879,9 @@ width = "宽度" [app] description = "免费的 Adobe Acrobat 替代品(下载量 1000 万+)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "转换为 PDF/A-3b" convertToPdfA3bDescription = "创建带有嵌入附件的归档 PDF" @@ -1842,6 +2020,15 @@ export = "导出" accessDenied = "拒绝访问" insufficientPermissions = "您没有执行此操作的权限。" +[auth.callback] +completing = "正在完成身份验证" +invalidToken = "OAuth 登录失败 - token 无效。" +missingToken = "OAuth 登录失败 - 未收到 token。" +oauthFailed = "OAuth 登录失败。请重试。" +pleaseWait = "请稍候,我们正在完成登录。" +signedOut = "您已退出登录。请重新登录。" +windowMayClose = "完成后即可关闭此窗口。" + [auth.displayName] guest = "访客" user = "用户" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "通过移除可能的恶意内容(如 JavaScript 和嵌 description = "此工具将基于内容自动重命名 PDF 文件。它会分析文档以从文本中找到最合适的标题。" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "自动旋转" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "置信度阈值" + +[autoRotate.detectionMode] +auto = "自动" +osd = "仅 OCR" +text = "仅文本" +title = "检测方法" + +[autoRotate.error] +failed = "自动旋转 PDF 时发生错误。" + +[autoRotate.inferUndetected] +title = "旋转低置信度页面以匹配" + +[autoRotate.report] +applied = "{{degrees}}° 顺时针" +confidenceValue = "置信度 {{value}}" +noChange = "无更改" +pageLabel = "第 {{number}} 页" +summary = "{{total}} 页中已旋转 {{rotated}} 页,{{unchanged}} 页保持原样" +title = "检测报告" + +[autoRotate.report.method] +inferred = "已匹配" +none = "已跳过" +osd = "OCR" +text = "文本" + +[autoRotate.report.note] +belowThreshold = "低于阈值" +blankPage = "页面为空白" +inferredFromDocument = "与其他页面相同" +noDominantDirection = "文本方向混合" +osdFailed = "未找到可读文本" +osdNoVerdict = "无法判断朝向" +tesseractUnavailable = "未安装 OCR" +tooFewGlyphs = "文本过少" + +[autoRotate.results] +title = "自动旋转结果" + +[autoRotate.settings] +title = "设置" + +[autoRotate.tooltip.confidenceThreshold] +text = "在旋转页面前,检测结果需要达到的确定程度。降低它可旋转更多页面,提高它可让更多页面保持不变。" +title = "置信度阈值" + +[autoRotate.tooltip.description] +text = "判断每个页面应朝向哪边,并将其旋转到正确方向。已经正向的页面会保持不变。" + +[autoRotate.tooltip.detectionMode] +text = "自动会先读取页面上的文本,如果不足,再将页面作为图像检查。仅文本会跳过图像检查,适合从未扫描过的文档且速度最快。仅 OCR 会将每个页面都视为图像。" +title = "检测方法" + +[autoRotate.tooltip.header] +title = "自动旋转" + +[autoRotate.tooltip.inferUndetected] +text = "空白或几乎空白的页面无法单独检查。当所有可读取页面都需要相同旋转时,这些页面也会旋转以匹配它们。" +title = "旋转低置信度页面以匹配" + [autoSizeSplitPDF] tags = "pdf、拆分、文件、组织" @@ -2189,33 +2443,6 @@ keywords = "关键字:odd、even" numbers = "数字/范围:5,10-20" progressions = "等差:3n,4n+1" -[certificateChoice.tooltip] -header = "证书类型" - -[certificateChoice.tooltip.organization] -bullet1 = "由系统管理员管理" -bullet2 = "在授权用户间共享" -bullet3 = "代表公司身份,而非个人" -bullet4 = "最佳用途:正式文档、团队签署" -description = "由您的组织提供的共享证书。用于全公司范围的签署授权。" -title = "组织证书" - -[certificateChoice.tooltip.personal] -bullet1 = "首次使用时自动生成" -bullet2 = "与您的用户帐户绑定" -bullet3 = "不可与其他用户共享" -bullet4 = "最佳用途:个人文档、个人责任" -description = "为您的用户帐户自动生成的唯一证书。适用于个人签名。" -title = "个人证书" - -[certificateChoice.tooltip.upload] -bullet1 = "需要 P12/PFX 文件和密码" -bullet2 = "可由外部证书颁发机构签发" -bullet3 = "对法律文档具有更高信任级别" -bullet4 = "最佳用途:具有法律效力的合同、外部验证" -description = "使用您自己的 PKCS#12 证书文件。可完全控制证书属性。" -title = "上传自定义 P12" - [certSign] awaitingSignatures = "等待签署" chooseCertificate = "选择证书文件" @@ -2542,34 +2769,6 @@ server = "服务器" stepTitle = "证书来源" upload = "上传" -[certSign.tooltip.header] -title = "签名管理说明" - -[certSign.tooltip.overview] -bullet1 = "检查现有签名及其有效性" -bullet2 = "查看签署人和证书的详细信息" -bullet3 = "添加新的数字签名以保护文档" -bullet4 = "支持多个文件并便于导航" -text = "此工具可检查您的 PDF 是否已数字签名,并可添加新的数字签名。数字签名可证明文档的创建者或批准者,并显示签署后是否被更改。" -title = "该工具能做什么?" - -[certSign.tooltip.signing] -bullet1 = "支持 PEM、PKCS12、JKS 以及服务器证书格式" -bullet2 = "可选择在 PDF 上显示或隐藏签名" -bullet3 = "可添加原因、地点和签署人姓名" -bullet4 = "可选择放置可见签名的页面" -bullet5 = "可使用服务器证书实现简便的“使用 Stirling-PDF 签名”选项" -text = "要对 PDF 进行签名,您需要数字证书(如 PEM、PKCS12 或 JKS)。您可以选择让签名在文档上可见,或仅用于安全而不可见。" -title = "添加签名" - -[certSign.tooltip.validation] -bullet1 = "显示签名是否有效" -bullet2 = "显示签署人信息与签署日期" -bullet3 = "检查文档在签署后是否被修改" -bullet4 = "可使用自定义证书进行验证" -text = "检查签名时,工具会告诉您签名是否有效、谁签了名、何时签的,以及自签署后文档是否被更改。" -title = "检查签名" - [changeCreds] changeUsername = "更新您的用户名。更新后您将被登出。" credsUpdated = "账户已更新" @@ -2737,12 +2936,16 @@ text = "要使这些权限不可更改,请使用“添加密码”工具设置 [chat.actions] copy = "复制消息" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling 代理选项" +assistant = "AI Assistant" clearChat = "清空聊天" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "此操作将使用您的云额度" tooltip = "从屏幕拾取颜色" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "选择颜色" [common] +actions = "Actions" back = "返回" cancel = "取消" close = "关闭" +codeSample = "Code sample" collapse = "折叠" confirm = "确认" continue = "继续" copied = "已复制!" copy = "复制" done = "完成" +enterValue = "Enter value" error = "错误" expand = "展开" loading = "正在加载..." next = "下一页" +open = "Open" preview = "预览" previous = "上一页" refresh = "刷新" +remaining = "剩余" retry = "重试" save = "保存" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "清除已选择" @@ -2988,6 +3199,7 @@ title = "压缩方式" [compress.settings] desiredSize = "期望的文件大小" desiredSizePlaceholder = "输入大小" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "压缩是减小文件大小的简便方法。选择“文件大小”可输入目标大小,我们将为您自动调整质量;选择“质量”可手动设置压缩强度。" @@ -3190,6 +3402,24 @@ pdfOptions = "PDF 选项" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "请先选择源格式" settings = "设置" single = "单个" @@ -3264,24 +3494,6 @@ description = "These cookies are essential for the website to function properly. posthog = "PostHog 分析" scarf = "Scarf Pixel" -[credits.modal] -advancedMonitoring = "高级监控" -allInOneWorkspace = "一体化 PDF 工作空间(查看器、工具与代理)" -apiSandbox = "API 沙盒" -dedicatedSupportSlas = "专属支持与 SLA" -fasterThroughput = "吞吐量提升 10 倍" -fullyPrivateFiles = "完全私密的文件" -largeFileProcessing = "大文件处理" -orgWideAccess = "组织范围的访问控制" -premiumAiModels = "高级 AI 模型" -prioritySupport = "优先支持" -privateDocCloud = "私有文档云" -ragFineTuning = "RAG + 微调" -secureApiAccess = "安全 API 访问" -standardThroughput = "标准吞吐量" -unlimitedApiAccess = "无限 API 访问" -unlimitedSeats = "无限席位" - [crop] autoCrop = "自动裁剪空白" noFileSelected = "选择一个 PDF 文件开始裁剪" @@ -3346,6 +3558,7 @@ message = "设置默认 PDF 处理程序失败" title = "错误" [defaultApp.prompt] +dontRemind = "不再提醒我" message = "将 Stirling PDF 设为打开 PDF 文件的默认应用。" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "搜索…" +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "编辑" @@ -3644,11 +3861,13 @@ shareSelected = "共享所选" sharing = "共享" showAll = "显示全部" showHistory = "显示历史" +sortBy = "Sort files" sortByDate = "按日期排序" sortByName = "按名称排序" sortBySize = "按大小排序" storage = "存储" storageState = "存储" +storageUsed = "Storage used" synced = "已同步" title = "上传 PDF 文件" toolChain = "已应用工具" @@ -3694,13 +3913,17 @@ addFiles = "添加文件" addingFiles = "正在添加文件…" collapse = "折叠侧边栏" customizeGroups = "自定义分组" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "打开文件以开始" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "展开侧边栏" -files = "文件" googleDrive = "Google Drive" googleDriveDisabled = "未配置 Google Drive" leaveMyFiles = "离开我的文件" +library = "PDF 库" myFiles = "我的文件" noFiles = "暂无文件" openFileManager = "打开文件管理器" @@ -3708,37 +3931,36 @@ openFromComputer = "从电脑打开" openSettings = "打开设置" other = "其他" recent = "最近" -search = "搜索" -searchPlaceholder = "搜索文件..." viewAll = "查看全部 {{count}} 个文件" [fileSidebar.fileItem] closeViewer = "关闭查看器" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "在查看器中打开" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "添加" -addLabel = "添加标签…" -categoryIconAria = "为 {{name}} 选择图标" -createCategory = "创建类别" -delete = "删除类别" done = "完成" -hide = "隐藏类别" -newCategory = "新类别名称…" -removeLabel = "移除 {{name}}" -rename = "重命名" reset = "重置为默认值" -search = "搜索标签…" -show = "显示类别" subtitle = "将文件分组到父类别中。向类别添加现有或新标签、重命名类别,或创建自己的类别。不属于任何类别的文件会显示在“其他”下。" title = "侧边栏类别" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "添加到工作区" addToWorkspaceCount = "将 {{count}} 个添加到工作区" @@ -3749,7 +3971,6 @@ backToMyFiles = "返回“我的文件”" breadcrumbs = "文件夹路径" cancel = "取消" classification = "分类" -clearSearch = "清除搜索" clearSelection = "清除选择" closeDetails = "关闭详情" create = "创建" @@ -3781,6 +4002,7 @@ downloadAll = "全部下载" downloadVersion = "下载此版本" dropOverlay = "拖放文件以上传" dropOverlaySub = "文件起始于“本地”。使用 '移动到' 或 '保存到云端' 将其整理到文件夹中。" +duplicate = "Duplicate" file = "文件" fileInfo = "File info" fileMenu = "文件操作" @@ -3808,13 +4030,12 @@ refresh = "从服务器刷新" remove = "删除" removeVersion = "移除此版本" rename = "重命名" +renamed = "已重命名" renameFolder = "重命名文件夹" resizeFolderTree = "调整文件夹树大小(方向键,按住 Shift 步长更大;双击自动适配)" save = "保存" saveToServer = "保存到服务器" saveToServerDisabledHint = "此服务器未启用保存到服务器功能。请联系您的管理员启用。" -search = "搜索" -searchPlaceholder = "搜索此文件夹及其子文件夹" selectAll = "全选" selectAllHint = "点击可全选。提示:按住 Ctrl(或 Cmd)可逐个添加文件,按住 Shift 可选择范围。" selectedCount = "已选择 {{count}} 个" @@ -3874,6 +4095,10 @@ title = "尚无云端文件" hint = "未上传而保存的文件会保留在此处。拖放文件以添加。" title = "没有仅本地文件" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "您打开或编辑的文件会出现在这里。" title = "尚未有修改" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "无法删除文件夹。" deleteFolderFailedDetail = "无法删除文件夹:{{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "无法更新文件夹外观。" folderAppearanceFailedDetail = "无法更新文件夹外观:{{message}}" moveFilesFailed = "无法移动文件。" @@ -3910,9 +4137,7 @@ count = "文件数" folder = "文件夹" labels = "标签" modified = "修改时间" -name = "名称" size = "大小" -toolHistoryAtVersion = "累计工具链" totalSize = "总大小" type = "类型" versionHistory = "版本历程" @@ -3945,6 +4170,11 @@ localHint = "仅存储在此浏览器中" shared = "共享" sharedHint = "通过链接与您共享" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "最早优先" modifiedDesc = "最新优先" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "存储中无可用文件。请先打开一些文件。" open = "打开" openFile = "打开文件" openFiles = "打开文件" +selectFile = "Select {{name}}" selectFromStorage = "从存储中选择" upload = "上传" uploadFile = "上传文件" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "全部已保存" +analyzingFields = "Analysing form fields..." +extractCsvError = "提取 CSV 失败" +extractXlsxError = "提取 XLSX 失败" +filled = "filled" flattenAfterFilling = "填写后扁平化" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "必填" +requiredFieldsError = "请填写所有必填字段" rescanFields = "重新扫描字段" rescanFormFields = "重新扫描表单字段" save = "保存" +saveShortcut = "按 Ctrl+S 保存" +unsavedChanges = "未保存的更改" [formFill.sidebar] close = "关闭侧边栏" @@ -4309,6 +4552,11 @@ desc = "基于检测到的页眉自动重命名 PDF 文件" tags = "自动检测,基于标题,组织,重新标记" title = "自动重命名 PDF 文件" +[home.autoRotate] +desc = "检测每页的方向并自动旋转为正向。" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "自动旋转" + [home.autoSizeSplitPDF] tags = "自动,拆分,大小" title = "自动根据大小/数目拆分 PDF" @@ -4608,15 +4856,12 @@ hint = "上传 PNG、JPG、SVG 或其他图像文件以放置到 PDF 上。为 label = "上传图片" placeholder = "选择图片文件" -[imageToPdf] -tags = "转换、图像、JPG、图片、照片" - [imageToPDF] header = "将图片转换为 PDF" title = "图片转 PDF" -[infoBanner] -dismiss = "关闭" +[imageToPdf] +tags = "转换、图像、JPG、图片、照片" [invite] acceptError = "创建账户失败" @@ -4647,11 +4892,9 @@ welcomeTitle = "您已被邀请!" [landing] addFiles = "添加文件" -heroSubtitle = "拖放或添加现有 PDF 以开始使用。" mobileUpload = "从手机上传" openFromComputer = "从电脑打开" uploadFromComputer = "从电脑上传" -workbenchEmptyStateHero = "将 PDF 拖放到任意位置" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "输入用户名" failedToSignIn = "使用 {{provider}} 登录失败:{{message}}" forgotPassword = "忘记密码?" loggingIn = "正在登录…" -logIn = "登录" login = "登录" magicLinkSent = "魔法链接已发送至 {{email}}!请检查邮箱并点击链接登录。" mfaCode = "认证代码" @@ -4715,26 +4957,10 @@ signInWith = "使用以下方式登录" title = "登录" unexpectedError = "意外错误:{{message}}" updatePassword = "更新密码" -useEmailInstead = "使用邮箱登录" useMagicLink = "改用魔法链接" username = "用户名" youAreLoggedIn = "您已登录!" -[login.slides.edit] -alt = "编辑 PDF" -subtitle = "拥有十余种工具,助您涂黑、签名、阅读并处理 PDF,定能满足您的需求。" -title = "编辑 PDF,以展示/保护您所需的信息" - -[login.slides.overview] -alt = "Stirling PDF 概览" -subtitle = "面向隐私优先的 PDF 云套件,支持转换、签名、涂黑与管理文档,并提供 50+ 个其他强大工具。" -title = "满足您全部 PDF 需求的一站式平台。" - -[login.slides.secure] -alt = "保护 PDF" -subtitle = "轻松添加密码、涂黑内容并管理证书。" -title = "保护 PDF 中的敏感信息" - [margin] large = "大" medium = "中" @@ -4818,6 +5044,34 @@ uploadSuccess = "上传成功!" uploadSuccessMessage = "您的图像已传输。" validating = "正在验证会话..." +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "扫码上传照片。图像会自动转换为 PDF。" descriptionNoConvert = "使用手机扫码上传照片。" @@ -4836,8 +5090,13 @@ title = "从手机上传" tags = "多工具,多操作,用户界面,点击拖动,前端,客户端" title = "PDF 多功能工具" -[navbar] -search = "搜索" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "认证未成功。您可以关闭此窗口后重试。" @@ -4966,6 +5225,7 @@ activeFiles = "活动文件 视图显示您已加载到工具 allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "关闭" cropSettings = "现在我们已选择要裁剪的文件,可以配置裁剪工具,选择要将 PDF 裁剪到的区域。" +dialogLabel = "Onboarding" fileCheckbox = "单击某个文件即可选择其进行处理。您可以选择多个文件以进行批量操作。" fileReplacement = "修改后的文件会自动替换工作台中的原始文件,方便您继续使用更多工具处理。" filesButton = "快速访问栏上的 文件 按钮可用于上传 PDF,以便使用工具。" @@ -4976,6 +5236,7 @@ pinButton = "如果您希望在运行工具后文件仍保持为活动状态, results = "工具运行完成后,审阅 步骤会在此面板显示结果预览,并允许您撤销操作或下载文件。 " runButton = "配置完成后,使用此按钮在所有选中的 PDF 上运行该工具。" selectCropTool = "让我们选择 裁剪 工具,演示如何使用其中一个工具。" +stepOf = "第 {{current}} 步,共 {{total}} 步" toolInterface = "这是 裁剪 工具界面。正如您所见,目前内容不多,因为我们还没有添加可处理的 PDF 文件。" workbench = "这是 工作台 - 用于查看和编辑 PDF 的主要区域。" wrapUp = "一切就绪!您已了解应用的主要区域及其用法。随时点击 帮助 按钮重新查看本导览。" @@ -4988,6 +5249,26 @@ showMeAround = "带我看看" skipForNow = "暂时跳过" skipTheTour = "跳过导览" +[onboarding.checklist] +dismiss = "关闭" +title = "设置 Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "在您的计算机上原生运行 Stirling" +title = "下载 Stirling 桌面版" + +[onboarding.checklist.inviteTeam] +description = "与您的团队协作" +title = "邀请团队成员" + +[onboarding.checklist.shareAnalytics] +description = "帮助改进 Stirling" +title = "共享匿名使用数据" + +[onboarding.checklist.takeTour] +description = "通过快速导览了解 Stirling 的工作方式" +title = "开始导览" + [onboarding.desktopInstall] body = "Stirling 作为桌面应用效果最佳。可离线使用、更快访问文档,并在电脑本地进行编辑。" selectOs = "选择操作系统" @@ -4996,7 +5277,21 @@ titleWithOs = "为 {{osLabel}} 下载" [onboarding.mfa] authenticationCode = "验证码" +description = "通过关联身份验证器应用来保护您的账户。扫描 QR 码或输入设置 key,然后确认 6 位代码以完成。" +enable = "启用 MFA" +enableError = "无法启用双因素身份验证。请检查代码并重试。" +enterCode = "输入应用中的 6 位代码。" +enterCodeError = "输入身份验证代码以继续。" +logout = "退出登录" +openAuthenticator = "打开 Google Authenticator、Authy 或 1Password。" qrCodeLoading = "正在生成您的 QR 码…" +regenerateQrCode = "重新生成 QR 码" +scanQrCode = "扫描 QR 码或输入下面的设置 key。" +setupError = "无法开始双因素设置。请重试。" +setupKey = "设置 key(手动输入)" +stepByStep = "分步说明" +success = "MFA 已启用。您现在可以继续。" +title = "多因素身份验证设置" [onboarding.planOverview] adminBodyLoginDisabled = "启用登录模式后,您可以管理用户、配置设置并监控服务器健康。服务器上的前 {{freeTierLimit}} 位用户可免费使用 Stirling。" @@ -5005,6 +5300,11 @@ adminTitle = "管理员概览" userBody = "邀请队友、分配角色,并在一个安全的工作区中整理文档。准备好从单人使用扩展时,可随时启用登录模式。" userTitle = "方案概览" +[onboarding.processorIntro] +body = "Stirling 现在运行 策略 — 自动规则会在每个文档到达时对其进行分类、保护和处理。在 Processor 中设置并监控运行。" +cta = "查看 Processor" +title = "查看 Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "预付年" + +[payg.prepaid.banner] +exhausted = "您的预付容量已用完;已恢复按量计费。充值以保持折扣。" +expiring_one = "您的预付年将在 {{count}} 天后结束。未使用容量将过期 — 充值以开始新的年度。" +expiring_other = "您的预付年将在 {{count}} 天后结束。未使用容量将过期 — 充值以开始新的年度。" +low = "您的预付容量剩余 {{pct}}%。随时充值以保持处理不中断。" + +[payg.prepaid.card] +subtitle = "在按量计费之前使用,且不计入您的支出上限。" +title = "预付容量" + +[payg.prepaid.meter] +capSuffix = "共 {{total}} 个预付积分" +expires = "{{date}} 到期" + +[payg.prepaid.state] +exhausted = "已用完" +healthy = "余量充足" +low = "即将用完" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "将 PDF 转换为 PDF/A" tags = "归档、长期、标准、转换、存储、保存" title = "PDF 转 PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "打印,标准,转换,生产,印前,归档" title = "PDF 转为 PDF/X" @@ -5822,6 +6151,7 @@ small = "500 积分" xsmall = "100 积分" [plan.availablePlans] +currency = "Billing currency" subtitle = "选择符合您需求的套餐" title = "可用套餐" @@ -5937,49 +6267,12 @@ keyDescription = "粘贴您邮件中的许可证密钥" success = "许可证已激活!" successMessage = "您的许可证已成功激活。现在可以关闭此窗口。" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "已强制执行" -enforcing = "正在强制执行..." -failed = "强制执行失败" outputsUnavailable = "策略输出已无法下载。" partialOutputsUnavailable = "部分策略输出已无法下载。" -retrying = "忙碌中,正在重试..." runNotFound = "无法再找到此强制执行运行。" -step = "步骤 {{current}}/{{total}}" timedOut = "在运行完成之前强制执行已超时。" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "关闭" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "正在应用 {{names}}" applyingProgress = "正在应用 {{names}} ({{done}}/{{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} 个文件中有 {{failures}} 个无法处理,已按 failureTitle = "未完全强制执行的导出" printPolicyAppliedBody = "此 PDF 已更新以符合某项策略。请检查更改,然后重新打印。" printPolicyAppliedTitle = "打印前已应用策略" -queued = "+{{count}} 已排队" successTitle = "已应用 {{names}}" summaryMore = "{{first}}、{{second}} 以及另外 {{more}} 个" summaryTwo = "{{first}} 和 {{second}}" -[policies.enforcement.triggerVerb] -convert = "在转换前强制执行" -default = "正在强制执行" -export = "在导出前强制执行" -input = "在导入时强制执行" -print = "在打印前强制执行" - -[policies.field] -accessLog = "访问日志" -archiveAfter = "归档时间" -auditTrail = "审计跟踪" -belowThreshold = "低于阈值" -destination = "目标位置" -frameworks = "框架" -immutableHold = "不可变保留" -keepFor = "保留时长" -minConfidence = "最低置信度" -notify = "路由时通知" -onViolation = "不合规时" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "添加" -addPlaceholder = "添加标签…" -customNote = "已为您的团队自定义。" -defaultNote = "正在使用内置默认设置,并与您的团队共享。" -duplicate = "\"{{name}}\" 已存在。" -edit = "编辑标签" -empty = "还没有标签。" -export = "导出 JSON" -iconAria = "为 {{name}} 选择图标" -import = "导入 JSON" -importError = "无法导入该文件。" -managedNote = "团队标签由您的团队负责人管理。" -modalSubtitle = "与整个团队共享。分类器会选择适合每个文档的标签。" -modalTitle = "分类标签" -pickIcon = "使用 {{label}} 图标" -removeAria = "移除 {{name}}" -resetToDefault = "重置为默认值" -saveForTeam = "为团队保存" -saving = "正在保存…" -sectionLabel = "分类标签" -startFromScratch = "从头开始" -teamCount = "团队标签" -tooLong = "标签最多 {{max}} 个字符。" -ungrouped = "未分组" -view = "查看标签" +categoryCount = "类别" +hideCategory = "隐藏类别" +labelCount = "标签" +sharedNote = "这些标签为内置标签,并在整个团队中共享。" +showCategory = "显示类别" [policies.pii] account = "账户号码(带标签)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "美国银行路由号码(ABA)" ssn = "社会安全号码" -[policies.settings] -noneExport = "当前没有在导出时运行的策略。" -noneUpload = "当前没有在上传时运行的策略。" -onExport = "导出时" -onUpload = "上传时" -reorderHandle = "拖动以重新排序" -runOrderDesc = "当多个策略在同一触发器上运行时,它们会按此顺序运行——每个策略都基于前一个策略的输出。拖动以重新排序。" -title = "策略设置" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "策略选项" -policySettings = "策略设置" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "重试失败的策略({{count}})" -rowProgress = "已处理 {{completed}} / {{total}} 个文件" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "什么是策略?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "自动查找并涂黑敏感信息(如社会安全号码和卡号),以防在文档中被读取。" -sanitize = "从文件中移除隐藏的 JavaScript,这样在有人打开时不会自动运行任何内容。" -watermark = "在每一页上加盖可见标记(例如 \"Confidential\")。" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "关闭" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} 正在强制执行..." badgeRan = "{{name}} 策略已在此文件上运行" +badgeRunning = "{{name}} 正在运行..." blockingAction = "强制执行策略时已阻止 {{action}},请稍候..." dismiss = "关闭叠加层" enforcingTitle = "正在强制执行策略..." @@ -6151,6 +6316,7 @@ viewAnyway = "查看文件(策略仍在强制执行)" [policyOption] autoRedactPhi = "自动遮盖 PHI" blockExport = "阻止导出" +ccpa = "CCPA" contracts = "合同" documents = "文档" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor 计划" subscribed = "Processor 计划" unlinked = "未关联" -[portal.agentBuilder] -bootstrapFromDocument = "从文档引导创建" -sectionsAriaLabel = "智能体构建器部分" -selectorAriaLabel = "智能体" -subtitle = "设计、测试并发布用于对文档进行分类、提取和路由的 AI 智能体。定义场景、限制工具访问、运行黄金集,并发布版本。" -title = "智能体构建器" - -[portal.agentBuilder.bootstrap] -cancel = "取消" -dropzoneText = "选择示例文档(PDF 或图像)" -lead = "拖放一个示例文档,我们将提出场景和可供您优化的提取架构。在您审核之前,不会发布任何内容。" -submit = "引导创建智能体" -subtitle = "从一个代表性文件开始创建新智能体" -title = "从文档引导创建" - -[portal.agentBuilder.empty] -description = "从示例文档引导创建智能体,以生成其场景和提取架构,然后优化并发布。" -title = "还没有智能体" - -[portal.agentBuilder.evals] -casesPassing = "通过的用例" -columnCase = "评估用例" -columnLatency = "延迟" -columnResult = "结果" -fail = "失败" -goldenSetPassRate = "黄金集通过率" -latencyMs = "{{ms}} ms" -notRun = "未运行" -pass = "通过" -passRate = "通过率" -runEvals = "运行评估" - -[portal.agentBuilder.evals.empty] -description = "评估会将您的场景转化为可重复的黄金集。升级后可随时间捕获通过率,并将其作为发布门禁。" -title = "还没有黄金集" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "跨黄金集" -activeAgents = "活跃智能体" -avgPassRate = "平均评估通过率" -fleetWide = "全局范围" -latestPublished = "最新发布" -scenarios = "场景" -testCases = "测试用例" -totalDescription_one = "共 {{count}} 个" -totalDescription_other = "共 {{count}} 个" - -[portal.agentBuilder.scenarios] -add = "添加" -addScenario = "添加场景" -enable = "启用" -expectationLabel = "预期行为" -expectationPlaceholder = "智能体应执行的操作" -inEval = "在评估中" -mute = "静音" -muted = "已静音" -nameLabel = "名称" -namePlaceholder = "例如:合规升级" - -[portal.agentBuilder.tabs] -evals = "评估" -scenarios = "场景" -tools = "工具" -versions = "版本" - -[portal.agentBuilder.tools] -broadAccess = "广泛访问" -deniedHint = "所选工具已被阻止。其他所有工具仍可调用。" -deniedTools = "拒绝的工具" -governanceGate = "工具治理在 Enterprise 计划中可用。" -restricted = "受限" -restrictedAccess = "受限工具访问" -restrictedDescription = "允许除下方拒绝工具之外的所有工具。" - -[portal.agentBuilder.versions] -current = "当前" -historyGate = "完整版本历史记录和回滚在 Enterprise 计划中可用。" -publish = "发布" -rollBack = "回滚" - [portal.assistant] close = "关闭助手" error = "无法连接助手。" @@ -6365,9 +6451,21 @@ typing = "正在输入" loading = "正在加载" redirectingToEditor = "正在重定向到编辑器..." +[portal.billing.activation] +maybeLater = "稍后再说" +title = "开启 Processor" + +[portal.billing.activation.payg] +sub = "设置月度限额。按实际运行量付费。" +title = "按量付费" + +[portal.billing.activation.prepay] +badge = "免费 2 个月" +sub = "一张发票,可用银行卡或银行转账。" +title = "预付一年" + [portal.billing.checkout] noClientSecret = "Edge 函数未返回 client_secret。" -subtitle = "添加银行卡,即可在免费 Editor 计划额度用完后继续使用。其余由 Stripe 处理。" title = "启用 Processor 计划" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "您的付款已成功,但激活所需时间比平常稍长。它会自 close = "关闭" title = "即将完成" +[portal.billing.checkout.cap] +amountAria = "月度支出限额" +back = "返回" +continue = "继续付款" +error = "无法设置您的支出限额" +finePrint = "发票按月生成。可随时取消。" +noLimit = "无限额" +note = "您永远不会被收取超过限额的费用。处理只会暂停。" +perMonth = "/ 月" +title = "设置您的支出限额" + +[portal.billing.checkout.card] +fields = "卡号 · MM / YY · CVC · ZIP" +label = "银行卡详情" +note = "银行卡详情由 Stripe 收集。Stirling 从不存储 PAN 或 CVC。" + [portal.billing.checkout.error] title = "无法开始结账" @@ -6383,10 +6497,9 @@ body = "您的付款已完成。我们正在为您的已关联实例启用按量 hint = "请保持此窗口打开。" title = "正在激活您的 Processor 计划..." -[portal.billing.checkout.notConfigured] -bodyAfter = ",以启用应用内结账。" -bodyBefore = "在门户 env 中设置" -title = "Stripe 未配置" +[portal.billing.checkout.pay] +limit = "月度支出限额 {{amount}}/月。达到限额时处理会暂停。" +limitNoCap = "无支出限额。处理不会暂停。发票按月生成。" [portal.billing.enterpriseUpsell] cta = "生成您的 Enterprise 报价" @@ -6409,10 +6522,12 @@ currentPlan = "当前计划" freeForever = "永久免费" noTeamResolved = "您的钱包尚未解析到团队——请刷新并重试。" ownerOnly = "只有团队所有者可以启用 Processor 计划。" +payInvoice = "支付发票以完成" planName = "Editor" ssoIncluded = "包含 SSO" switchOnProcessor = "启用 Processor →" unlimitedUsers = "无限用户" +viewQuote = "查看报价" [portal.billing.invoices] columnAmount = "金额" @@ -6465,7 +6580,111 @@ segmentApiDesc = "直接 API 请求" segmentApiLabel = "API" segmentAutomationDesc = "自动化和流水线" segmentAutomationLabel = "自动化" +sizeMultiplier = "{{formatted}} 个 PDF 使用了大小倍率" +summary = "{{unique}} 个唯一 · {{units}} 个计量单位 · 平均每个 PDF {{avg}}" +summaryNoRate = "{{unique}} 个唯一 · {{units}} 个计量单位" unit = "按量计费 PDFs" +unitsPending_one = "{{units}} 个计量单位等待从已关联实例同步" +unitsPending_other = "{{units}} 个计量单位等待从已关联实例同步" + +[portal.billing.prepaid] +capSuffix = "共 {{total}} 个预付积分" +expires = "{{date}} 到期" +eyebrow = "预付容量" +note = "在按量计费之前使用,且不计入您的支出限额。" +topUp = "充值" + +[portal.billing.prepaid.buy] +back = "返回" +calcTitle = "计算您的年度付款" +cancel = "取消" +cancelPurchase = "取消购买" +close = "关闭" +continueToPayment = "继续付款" +finalise = "完成" +invoiceErrorTitle = "无法生成发票" +loadingTitle = "正在加载您的报价" +payTitle = "支付您的年度费用" +step = "第 {{current}} 步,共 {{total}} 步" + +[portal.billing.prepaid.calc] +change = "更改" +deployRow = "部署" +done = "完成" +enterpriseHint = "这是企业级规模。在此用量下,企业费率优于任何自助折扣。锁定费率、条款,并在几分钟内获得报价。" +governanceRow = "治理" +governanceValue = "{{label}} · {{count}} 个策略" +handlesLabel = "您的 Processor" +handlesValue = "处理 {{volume}} 积分 / 月" +pipelinesLabel = "流水线" +policiesMeta = "{{count}} 个策略" +poolCaption = "全年一个包含 {{credits}} 积分的池。高峰月份可借用低峰月份的额度。" +priceLabel = "您的年度 · 以 10 个月价格享 12 个月" +rateUnknown = "我们将在结账时显示准确价格。" +savings = "您节省 {{amount}} · 免费 2 个月。" +sizingRow = "规模" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "用户总数" + +[portal.billing.prepaid.consent] +label = "我理解,当我的预付容量用完或 12 个月后过期时,除非我取消,否则处理会自动按标准按量付费费率继续(最高到我的支出限额),并且我可以随时从计费门户取消。" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "由 Stirling 管理。几分钟内上线。" +cloudValue = "Stirling Cloud · 托管" +selfhost = "自托管" +selfhostDesc = "运行在私有基础设施上。费率相同。" +selfhostValue = "自托管 · 私有基础设施" + +[portal.billing.prepaid.offer] +cta = "查看优惠" +subtitle = "预付一年 PDF 处理费用,免费获得两个月,在按量计费之前使用,且不计入您的支出限额。" +title = "以 10 个月价格享 12 个月" + +[portal.billing.prepaid.pay] +accountName = "账户持有人姓名" +accountNamePlaceholder = "例如 Jane Smith" +companyName = "公司名称(可选)" +companyNamePlaceholder = "例如 Acme Inc." +downloadInvoice = "下载发票" +payOnline = "在线支付" +poNumber = "PO 编号(可选)" +poNumberPlaceholder = "例如 PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "高级" +advancedDesc = "流水线驱动您的大部分处理。" +none = "无" +noneDesc = "尚未运行流水线。可随时开启。" +standard = "标准" +standardDesc = "少量流水线会重新处理到达的 PDF。" + +[portal.billing.prepaid.posture] +essentials = "基础" +essentialsDesc = "分类、默认项以及共享" +governed = "受治理" +governedDesc = "增加安全和路由" +regulated = "受监管" +regulatedDesc = "所有类别,包括合规、保留、摄取" + +[portal.billing.prepaid.review] +download = "下载报价(PDF)" +downloading = "正在准备…" +share = "共享以供审批" + +[portal.billing.prepaid.size] +compact = "紧凑" +compactDesc = "文件小于 25 MB,无数据费用" +heavy = "重型" +heavyDesc = "扫描或图像较多,通常 50 MB+" +standard = "标准" +standardDesc = "主要为小文件,部分扫描件超过 25 MB" + +[portal.billing.prepaid.state] +exhausted = "已用完" +healthy = "余量充足" +low = "即将用完" [portal.billing.spendLimit] adjustLimit = "调整限额" @@ -6494,6 +6713,8 @@ label = "预计会超出。" [portal.billing.spendThisMonth] eyebrow = "本月支出" +freeRemaining_one = "剩余 {{formatted}} 个免费 PDF" +freeRemaining_other = "剩余 {{formatted}} 个免费 PDF" processed_one = "已处理 {{formattedCount}} 个 PDF。" processed_other = "已处理 {{formattedCount}} 个 PDFs。" processedWithRate_one = "已处理 {{formattedCount}} 个 PDF,每个 {{rate}}。" @@ -6510,6 +6731,7 @@ reachedTitle = "已达到每月支出限额" title = "无法打开 Stripe 门户" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "已使用 {{allowance}} 个免费 PDFs 中的额度" capSuffix_other = "已使用 {{allowance}} 个免费 PDFs 中的额度" eyebrow = "Processor 试用" @@ -6534,6 +6756,376 @@ signature = "签名" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "新建连接" +createTitleFor = "连接 {{name}}" +delete = "删除" +edit = "编辑" +editTitle = "编辑连接" +unsupportedType = "无法在此处编辑此连接类型。" + +[portal.connections.categories.advanced] +description = "用于我们尚未支持的服务。" +label = "高级" + +[portal.connections.categories.audit] +description = "记录文档发生了什么,并写入审计员已有查看位置。" +label = "审计与合规日志" + +[portal.connections.categories.notify] +description = "通知另一个系统发生了某件事。" +label = "通知与工作流" + +[portal.connections.categories.security] +description = "在文档继续流转前扫描或标记它。" +label = "安全与分类" + +[portal.connections.categories.signing] +description = "发送文档以进行签名或公证。" +label = "电子签名与公证" + +[portal.connections.categories.storage] +description = "处理后的文档最终到达的位置 - 存储桶、网盘、工单或页面。" +label = "文件与附件" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling 会加密存储此信息,且不会再次显示。" +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "使用此连接的步骤只能调用此 URL 下的路径,不能调用其他主机。" +label = "服务器 URL" + +[portal.connections.commonFields.email] +label = "电子邮件" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "密码" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "用户名" + +[portal.connections.commonFields.webhookUrl] +helperText = "粘贴服务提供给您的 URL。只能调用其下的路径。" +label = "Webhook URL" + +[portal.connections.fields] +name = "连接名称" +namePlaceholder = "例如 生产环境" + +[portal.connections.picker] +cancel = "取消" +createNew = "新建连接..." +placeholder = "选择连接" +save = "保存连接" + +[portal.connections.picker2] +back = "更改集成" +heading = "您想连接什么?" +noResultsBody = "我们尚未支持它 - 告诉我们相关信息,我们会考虑添加。" +noResultsTitle = "没有与 \"{{query}}\" 匹配的项" +searchPlaceholder = "搜索集成 - 试试 \"sign\"、\"ocr\" 或产品名称" + +[portal.connections.types.api] +description = "从策略调用任意 HTTP API。您可以选择 URL、身份验证和 payload。" +label = "自定义 API" + +[portal.connections.types.api.fields.authType] +label = "身份验证" + +[portal.connections.types.api.fields.authType.options.basic] +label = "用户名和密码" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "自定义 header 中的 token" + +[portal.connections.types.api.fields.authType.options.none] +label = "无" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "登录以获取 token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "使用此连接的步骤只能调用此 URL 下的路径,不能调用其他主机。" +label = "基础 URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header 名称" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "登录路径" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "密码" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "如果 API 返回下载链接,只能获取这些主机。用逗号分隔。留空则仅允许基础 URL 自身的主机。" +label = "允许的结果主机" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "将 token 作为此 header 发送" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "登录调用返回 token 所在的响应 header,例如 X-Auth-Token。" +label = "Token 响应 header" + +[portal.connections.types.api.fields.username] +label = "用户名" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "在您运行的服务器上扫描文档中的恶意软件 - 任何内容都不会离开您的网络。" +label = "ClamAV(自托管)" + +[portal.connections.types.cloudmersive] +description = "用于策略中文档的病毒和内容扫描。" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "阻止包含宏、可执行文件、脚本或嵌入对象的文档 - 不仅仅是已知恶意软件。" +label = "Cloudmersive 高级扫描" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "将处理后的文档附加到 Confluence 页面。" +label = "Confluence" + +[portal.connections.types.consigno] +description = "将文档发送到 ConsignO Cloud (Notarius) 进行电子签名和公证。" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "您的 ConsignO Cloud 环境,包括 /api/v1。" +label = "服务器 URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "密码" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "仅当您的组织使用租户时才需要。" +label = "租户" + +[portal.connections.types.consigno.fields.username] +label = "用户名" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "当策略处理文档时向 Discord 发布消息。" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "每当策略处理文档时,在 Elasticsearch 中索引审计事件。" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "轮询 FTP 或 FTPS 服务器以获取新文档。" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "连接模式" + +[portal.connections.types.ftp.fields.passive.options] +active = "主动" +passive = "被动(推荐)" + +[portal.connections.types.ftp.fields.security] +helperText = "普通 FTP 会以未加密方式发送凭据和文件;如果服务器支持,请选择 FTPS。" +label = "加密" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS(显式 / AUTH TLS)" +implicit = "FTPS(隐式)" +none = "无(普通 FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "当策略运行时向 Google Chat 空间发布消息。" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "将处理后的文档附加到 Jira issue。" +label = "Jira" + +[portal.connections.types.mailgun] +description = "当策略运行时发送文档或通知电子邮件。" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "将处理后的文档归档到 Nextcloud。" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "应用密码" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "使用您运行的 Presidio 服务器检测文档文本中的个人数据。" +label = "Presidio(自托管)" + +[portal.connections.types.purview] +description = "读取并应用 Microsoft Purview 对 PDF 的敏感度标签。" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "可选。仅按名称选择标签时需要;没有它也可进行标记。" +label = "应用程序(客户端)ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "客户端 secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "作为 SiteId 加盖到每个标签上,以便支持 Purview 的工具知道哪个组织应用了它。" +label = "目录(租户)ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "源可以读取、流水线输出可以写入的 S3 存储桶。" +label = "S3 存储" + +[portal.connections.types.s3.fields.accessKeyId] +label = "访问 key ID" + +[portal.connections.types.s3.fields.bucket] +label = "存储桶" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS 留空。若使用 MinIO 或其他 S3 兼容存储,请设置此项。" +label = "端点" + +[portal.connections.types.s3.fields.region] +label = "区域" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "当策略运行时发送文档或通知电子邮件。" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "轮询 SFTP (SSH) 服务器以获取新文档。" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "可选。服务器的 SHA-256 主机 key 指纹(来自 `ssh-keyscan host | ssh-keygen -lf -`)。设置后,仅接受提供此 key 的服务器;留空时,首次连接时看到的 key 会被固定,之后更改会被拒绝。" +label = "主机 key 指纹" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key 密码短语" + +[portal.connections.types.sftp.fields.password] +helperText = "如果您改用私钥进行身份验证,请留空。" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "可选。粘贴 OpenSSH 私钥,以使用 key 而不是密码进行身份验证。" +label = "私钥" + +[portal.connections.types.slack] +description = "当策略处理文档时向 Slack 发布消息。" +label = "Slack" + +[portal.connections.types.smb] +description = "监视 SMB / CIFS 网络共享(Windows 或 Samba)。" +hostPlaceholder = "fileserver.example.com" +label = "SMB / 网络共享" + +[portal.connections.types.smb.fields.domain] +helperText = "可选。如果您的服务器使用 Windows 域或工作组,请填写。" +label = "域" + +[portal.connections.types.smb.fields.share] +label = "共享" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "每当策略处理文档时向 Splunk 发送审计事件。" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "向 Sumo Logic HTTP 收集器发送审计事件。" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "当策略运行时向 Teams 频道发布消息。" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "将每个文档或仅其详细信息发布到您选择的 URL。" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "从策略触发 Zap 或 Make 场景。" +label = "Zapier & Make" + +[portal.docs] +browse = "浏览文档" +viewSource = "在 GitHub 上查看源代码" + [portal.docs.authentication] codeCaption = "每个请求" eyebrow = "入门" @@ -6624,6 +7216,9 @@ eyebrow = "技能" lead = "打包的命名能力,您的智能体可将其作为单个工具调用。每项技能都是附带评估的确定性操作链。" title = "智能体技能" +[portal.docs.toc] +title = "本页内容" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API 参考" @@ -6697,6 +7292,8 @@ type = "类型" user = "用户" [portal.documents.queue.empty] +connectSource = "连接源" +createPipeline = "创建流水线" description = "当来源将文档送入您的流水线时,它们会显示在此处以供审核。" title = "队列中没有文档" @@ -6712,11 +7309,11 @@ editorAction = "编辑器" empty = "没有文档符合此筛选条件。" rowActions = "行操作" sensitiveLabel = "敏感" -sensitiveTitle = "敏感 — 需要访问权限" [portal.documents.table.columns] action = "流水线 / 操作" document = "文档" +labels = "Labels" product = "产品" status = "状态" time = "时间" @@ -6732,6 +7329,7 @@ host = "主机" lastSeen = "最后在线" region = "区域" status = "状态" +target = "Target" version = "版本" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "此视图遇到意外错误。请重试,或从侧边栏选择 retry = "重试" title = "此页面出现问题" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "返回" +body = "下载桌面应用或自托管服务器。" +done = "完成" +guide = "打开 {{name}} 指南" +openInBrowser = "改为在浏览器中打开" +sectionDesktop = "桌面应用" +sectionSelfHosted = "自托管" +title = "安装 Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "在您的服务器上运行官方镜像。" +note = "在 :8080 打开。默认登录:admin / stirling。" +tagline = "在容器中运行服务器" +title = "Docker" +variantFat = "完整" +variantLite = "超轻量" +variantStandard = "标准" + +[portal.home.download.kubernetes] +detailBody = "安装官方 Helm chart。" +note = "为 /configs 添加 PVC 以持久保存设置。" +tagline = "使用 Helm 部署到集群" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "适用于你的发行版的原生构建。" +guideBtn = "查看 Linux 安装选项" +note = "AppImage、.deb 和 .rpm 构建可用于常见发行版。" +tagline = "适用于你的发行版的原生桌面应用" +title = "Linux" + +[portal.home.download.mac] +altLabel = "或通过 Homebrew" +detailBody = "运行安装程序,或使用 Homebrew。" +downloadBtn = "下载 macOS 版" +tagline = "适用于 Apple silicon 和 Intel 的原生应用" +title = "macOS" + +[portal.home.download.manual] +detailBody = "在裸机服务器上运行 .jar。" +downloadBtn = "下载 .jar" +note = "需要 Java 21+。在 :8080 打开;默认登录 admin / stirling。" +runLabel = "然后运行它" +tagline = "在裸机服务器上运行 .jar" +title = "手动服务器设置" + +[portal.home.download.windows] +altLabel = "或通过 winget" +detailBody = "运行安装程序,或使用包管理器。" +downloadBtn = "下载安装程序" +tagline = "适用于 Microsoft Windows 的原生应用" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} 活跃" -invite = "邀请队友" +activeOfDeployed = "本月 {{total}} 个中有 {{active}} 个处于活跃状态" name = "Stirling PDF Editor" open = "在浏览器中打开" updated = "已更新 {{time}}" +[portal.home.editor.deploy] +finish = "完成部署" +options = "查看部署选项" +start = "部署编辑器" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "自托管 · Docker" @@ -6823,49 +7529,6 @@ afternoon = "下午好" evening = "晚上好" morning = "早上好" -[portal.home.onboarding] -dismiss = "关闭设置" -notStarted = "未开始" -progress = "已完成 {{done}} / {{total}}" -title = "完成设置" - -[portal.home.onboarding.enterprise] -body = "组织范围的 SSO + SCIM + RBAC、承诺用量定价,以及隔离网络部署。" -cta = "开始试用" -ctaQuote = "获取报价" -lead = "适用于 250+ 名员工。" -tag = "企业版" - -[portal.home.onboarding.steps.editor] -blurb = "部署桌面应用,永久免费。" -title = "下载 PDF Editor" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} 个活跃 · {{recommended}} 个推荐" -chip = "{{active}} 个活跃" -title = "启用策略" - -[portal.home.onboarding.steps.sources] -blurb = "已连接 {{connected}} 个 · 每个 PDF 到达后均受管控" -chip = "已连接 {{connected}} 个" -title = "连接您的来源" - -[portal.home.quickActions] -subtitle = "今日热门任务" -title = "快捷操作" - -[portal.home.quickActions.buildPipeline] -blurb = "基于类型化操作库的 3 步编排器" -title = "构建流水线" - -[portal.home.quickActions.connectSource] -blurb = "S3、代理、webhook、监视文件夹" -title = "连接来源" - -[portal.home.quickActions.issueApiKey] -blurb = "带速率限制和 IP allowlist 的作用域密钥" -title = "签发 API 密钥" - [portal.infrastructure] manageEditorDeployment = "管理 Editor 部署" sectionsAriaLabel = "基础设施部分" @@ -6878,13 +7541,9 @@ heading = "API 密钥" subheading = "带有单密钥速率限制、权限和 IP allowlist 的作用域凭据。" [portal.infrastructure.apiKeys.card] -allowedIps = "允许的 IP" -anyIp = "任意 IP(无 allowlist)" created = "已创建" lastUsed = "最后使用" -permissions = "权限" -rateLimit = "速率限制" -rateLimitValue = "{{value}} 次请求/分钟" +revoke = "撤销密钥" usageMonth = "本月用量" usageToday = "今日用量" @@ -6892,6 +7551,15 @@ usageToday = "今日用量" description = "创建作用域密钥以开始调用 Stirling API。" title = "还没有 API 密钥" +[portal.infrastructure.apiKeys.error] +load = "无法加载你的 API 密钥。请重试。" + +[portal.infrastructure.apiKeys.revoke] +body = "撤销“{{name}}”?任何仍在使用此密钥的集成都将立即开始收到 401 错误。此操作无法撤消。" +cancel = "取消" +confirm = "撤销密钥" +title = "撤销 API 密钥" + [portal.infrastructure.audit] filterAriaLabel = "按类别筛选审计事件" heading = "审计日志" @@ -6935,6 +7603,7 @@ all = "全部" auth = "身份验证" config = "配置" elevation = "权限提升" +policy = "策略" processing = "处理" security = "安全" @@ -6944,19 +7613,30 @@ title = "您没有访问权限" [portal.infrastructure.audit.metrics] config = "配置" -elevation = "权限提升" +policy = "策略" processing = "处理" totalEvents = "事件总数 · 24h" +[portal.infrastructure.auditCatLabel] +auth = "认证" +config = "配置" +elevation = "提权" +policy = "策略" +processing = "处理" +security = "安全" + +[portal.infrastructure.auditStatusLabel] +danger = "错误" +info = "信息" +success = "成功" +warning = "警告" + [portal.infrastructure.createKey] cancel = "取消" createKey = "创建密钥" done = "完成" -ipAllowlistHelper = "逗号分隔的 CIDR 范围。留空则允许任意 IP。" -ipAllowlistLabel = "IP allowlist" keyNameLabel = "密钥名称" keyNamePlaceholder = "例如:生产 · 摄取" -permissionsLabel = "权限" secretKeyCaption = "密钥" secretWarning = "请将其存储在密钥管理器中。Stirling 只会存储哈希 — 之后无法恢复。" subtitle = "将密钥的作用域限定为所需的最小范围。您可以随时轮换或撤销。" @@ -6964,207 +7644,9 @@ subtitleCreated = "立即复制此密钥 — 它不会再次显示。" title = "创建 API 密钥" titleCreated = "密钥已创建" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/分钟" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "部署者" -environment = "环境" -product = "产品" -status = "状态" -version = "版本" -when = "时间" - -[portal.infrastructure.deployments.recent] -heading = "最近部署" -subheading = "各产品和环境中的最新发布。" - -[portal.infrastructure.deployments.regionColumns] -instances = "实例" -latency = "延迟" -load = "负载" -p99 = "P99" -region = "区域" -status = "状态" -throughput = "吞吐量" -uptime = "正常运行时间" -version = "版本" - -[portal.infrastructure.deployments.regions] -heading = "区域" -subheading = "每个已部署 Stirling 区域的实时健康状况 — 延迟、负载和发布版本。" - -[portal.infrastructure.deployments.regions.empty] -description = "工作区完成预配后,已部署区域会显示在此处。" -title = "未部署区域" - -[portal.infrastructure.models] -heading = "模型" -msValue = "{{value}} ms" -subheading = "为工作区中的文档处理提供支持的模型目录和路由。" - -[portal.infrastructure.models.byom] -description = "注册本地或自托管模型,并将其固定到某个区域,以进行受数据驻留约束的处理。" -title = "自带模型" - -[portal.infrastructure.models.catalogue] -heading = "目录" -sub = "可供工作区使用的托管模型,包含实时延迟和成本。" -subEnterprise = "托管、自带和本地模型 — 支持按区域固定。" - -[portal.infrastructure.models.catalogue.empty] -description = "工作区目录中的模型会显示在此处。" -title = "没有可用模型" - -[portal.infrastructure.models.columns] -cost = "成本" -latency = "延迟" -load = "负载" -model = "模型" -status = "状态" -type = "类型" -version = "版本" - -[portal.infrastructure.models.metrics] -activeModels = "活跃模型" -avgLatency = "平均延迟" -included = "已包含" -monthlySpend = "每月模型支出" - -[portal.infrastructure.models.routing] -empty = "未配置路由规则。" -heading = "路由规则" -sub = "由哪个模型处理每个操作。当没有更精确的规则匹配时,将应用默认规则。" -subLocked = "将操作路由到特定模型 — 付费套餐可用。" - -[portal.infrastructure.models.routing.lockedBanner] -description = "升级到 Pro,以控制哪个模型处理每种操作和文档类型。" -title = "模型路由是付费功能" - -[portal.infrastructure.models.routingColumns] -default = "默认" -docType = "文档类型" -modelForAria = "{{operation}} 的模型" -operation = "操作" -routedTo = "路由到" - -[portal.infrastructure.security.access.byok] -description = "从您自己的 KMS 提供密钥。Stirling 使用它进行加密,但仍可读取。" -label = "自带密钥 (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "密钥永不离开您的 KMS。Stirling 只持有密文。" -label = "自持密钥 (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling 管理加密密钥。最简单 — 您这边无需密钥运维。" -label = "Stirling 持有的密钥" - -[portal.infrastructure.security.accessPolicy] -heading = "文档访问策略" -subheading = "控制谁可以解密静态存储的已处理文档。" - -[portal.infrastructure.security.attestations] -heading = "合规证明" -noReport = "无可用报告" -subheading = "按框架展示的审计态势,并可查看已证明控制项的报告。" -viewReport = "查看报告 →" - -[portal.infrastructure.security.compliance] -heading = "合规" -subheading = "覆盖 Stirling 平台的证明和认证。" - -[portal.infrastructure.security.empty] -description = "工作区的安全配置会显示在此处。" -title = "安全态势不可用" - -[portal.infrastructure.security.hyokBanner] -description = "使用 HYOK 时,加密密钥永不离开您的 KMS。Stirling 仅存储和处理密文,您可以随时撤销。" -title = "Stirling 无法解密您的文档" - -[portal.infrastructure.security.ipAllowlist] -empty = "未配置 IP 范围 — 允许所有 IP。" -heading = "IP allowlist" -sub = "API 访问仅限这些 CIDR 范围。" -subLocked = "将 API 访问限制为已知 IP 范围 — 付费套餐可用。" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "升级到 Pro,以将 API 访问限制到特定网络。" -title = "IP allowlist 是付费功能" - -[portal.infrastructure.security.ipColumns] -added = "已添加" -addedBy = "添加者" -cidr = "CIDR" -label = "标签" - -[portal.infrastructure.security.keyManagement] -algorithm = "Algorithm" -heading = "加密密钥管理" -keyId = "密钥标识符" -lastRotated = "最后轮换" -rotateKey = "轮换密钥" -rotationPolicy = "轮换策略" -subheading = "用于加密静态文档的密钥托管 — 谁可以解密,以及密钥如何轮换。" - -[portal.infrastructure.security.managedBanner] -description = "自带密钥 (BYOK) 和自持密钥 (HYOK) 托管在企业版中可用。升级后可从您自己的 KMS 提供密钥。" -title = "在您的套餐中,密钥由 Stirling 管理" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "亚太地区" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR 数据边界" -label = "欧盟" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "美国" - -[portal.infrastructure.security.residencyHeader] -heading = "数据驻留" -subheading = "文档存储和处理的位置。" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "已使用 {{value}}" - -[portal.infrastructure.storage.empty] -description = "已连接的存储和用量会显示在此处。" -title = "未配置存储" - -[portal.infrastructure.storage.lifecycle] +[portal.infrastructure.keyLabel] active = "活跃" -activeRange = "0–{{value}}d" -archived = "已归档" -coldStorage = "冷存储" -deleted = "已删除" -never = "永不" -purged = "已清除" - -[portal.infrastructure.storage.providers] -connect = "连接" -connected = "已连接" -heading = "已连接的提供商" -subheading = "已处理工件的写入位置。" - -[portal.infrastructure.storage.retention] -heading = "保留" -subheading = "工件在生命周期删除前保留的时长。" -windowLabel = "默认保留窗口" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} 天" -days_other = "{{count}} 天" -never = "永不删除" - -[portal.infrastructure.storage.totalUsage] -heading = "总用量" -progressLabel = "已使用存储" -subheading = "所有已连接提供商消耗的存储。" +revoked = "已撤销" [portal.infrastructure.tabs] apiKeys = "API 密钥" @@ -7174,6 +7656,49 @@ models = "模型" security = "安全" storage = "存储" +[portal.integrations] +availableHeading = "可用" +comingSoonHeading = "即将推出" +connect = "连接" +connectionCount_one = "{{count}} 个连接" +connectionCount_other = "{{count}} 个连接" +customApi = "自定义 API" +searchPlaceholder = "搜索集成" +subtitle = "Stirling 可以通信的一切。凭据在此处创建一次,然后由源、策略和管道引用。" +title = "集成" +typedName = "{{tool}} 名称" + +[portal.integrations.filters] +all = "全部" +audit = "审计" +connected = "已连接" +notify = "通知" +security = "安全" +signing = "签名" +storage = "存储" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "已连接" + +[portal.integrations.table] +integration = "集成" +worksWith = "适用于" + +[portal.integrations.worksWith] +pipelines = "管道" +policies = "策略" +sources = "源" + +[portal.legal] +draft = "{{label}} · 草稿" +loadError = "无法加载此文档。请重试。" +loading = "正在加载…" +title = "法律文档" + [portal.nav] agent-builder = "Agent Builder" components = "组件" @@ -7182,33 +7707,17 @@ documents = "文档" editor = "编辑器" home = "主页" infrastructure = "基础设施" +integrations = "集成" pipelines = "流水线" policies = "策略" procurement = "采购" -settings = "设置" sources = "来源" usage = "用量与计费" users = "用户" -[portal.notifications] -markAllRead = "全部标为已读" -title = "通知" -viewAll = "查看全部" - -[portal.notifications.ariaLabel] -none = "通知,无未读" -unread_one = "通知,{{count}} 条未读" -unread_other = "通知,{{count}} 条未读" - -[portal.notifications.count] -allRead = "全部已读" -loading = "正在加载" -new_one = "{{count}} 条新通知" -new_other = "{{count}} 条新通知" - -[portal.notifications.empty] -description = "没有新通知。" -title = "您已全部处理完毕" +[portal.nav.section] +platform = "PDF 平台" +processor = "PDF 处理器" [portal.pipelines] subtitle = "后端上的每个自动化文档流水线:针对一组来源的有序操作链,由触发器运行。单击一行以查看其步骤和来源。" @@ -7218,54 +7727,87 @@ title = "流水线" newPipeline = "新建流水线" [portal.pipelines.builder] -addStep = "添加工具" +activate = "Activate" back = "返回流水线" +cannotFollow = "无法接收 {{produced}}" +chooseAccount = "选择账户" +chooseDestination = "选择目标位置" +chooseOperation = "选择此步骤执行的操作" +chooseSource = "选择源" discard = "放弃更改" -enabled = "已启用" +inputs = "输入" +inputSource = "输入源" +inputTrigger = "触发器" keepEditing = "继续编辑" -needsUpload = "需要上传文件" +moreActions = "More actions" +needsConfiguring = "需要设置" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "没有工具符合您的搜索。" -pipelineSettings = "流水线设置" +pause = "Pause" +rename = "Rename pipeline" searchTools = "搜索工具" -selectToolBody = "添加工具以构建您的流水线。" -selectToolTitle = "还没有工具" -toolSettings = "工具设置" +sendToSystem = "发送到另一个系统" +stepsIncompatible = "这些步骤无法处理其上一步产生的内容:{{tools}}。" +stepsNeedSetup = "保存前仍需设置这些步骤:{{tools}}。" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "无法识别的操作,已按原样保留。" unsavedBody = "您有未保存的更改。离开前保存,还是放弃更改?" unsavedTitle = "未保存的更改" -uploadUnsupported = "流水线尚不支持上传的文件,因此这些步骤无法保存:{{tools}}。" usesDefaults = "使用默认设置运行" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "合并上一步中的每个文件" +fan-out = "对上一步中的每个文件运行一次" +format-mismatch = "需要 {{accepts}},但上一步产生 {{produced}}" +output-uncertain = "可能不会运行:上一步的输出取决于其设置方式" +source-mismatch = "需要 {{accepts}},但此管道的输入是 {{produced}}" +undeclared-operation = "无法检查此步骤接受的内容" [portal.pipelines.composer] addTool = "添加工具" -cancel = "取消" -chainEmpty = "添加工具以开始构建您的流水线。" create = "创建流水线" -directory = "输出文件夹" -directoryHelp = "服务器上的绝对路径。必须位于已配置的允许文件夹内。" +createPaused = "Create paused" editingUnsupported = "尚不支持显示这些工具参数以进行编辑。" -moveDown = "下移" -moveUp = "上移" +editSource = "Edit source" name = "名称" namePlaceholder = "例如:修订扫描" -noSources = "尚未连接来源。流水线仍可直接对提供给它的文件运行。" noToolSettings = "此工具没有可配置的设置。" -operations_one = "操作 ({{count}})" -operations_other = "操作 ({{count}})" output = "输出" -removeStep = "移除操作" save = "保存更改" scheduleEvery = "运行间隔" -sources = "来源" -sourcesLoading = "正在加载来源..." trigger = "触发器" triggerManual = "仅手动" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "天" hours = "小时" minutes = "分钟" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "删除 \"{{name}}\"?此操作无法撤销。" cancel = "取消" @@ -7273,28 +7815,62 @@ confirm = "删除" title = "删除流水线?" [portal.pipelines.detail] +clearHistory = "清除历史记录" delete = "删除流水线" run = "立即运行" [portal.pipelines.empty] action = "创建流水线" +connectSource = "连接源" description = "创建您的第一个流水线:选择要运行的来源,串联操作,并选择输出位置。" title = "还没有流水线" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "活跃" paused = "已暂停" total = "流水线" -[portal.pipelines.output] -folder = "写入文件夹" -inline = "返回文件" - [portal.pipelines.run] +allProcessed_one = "无需运行:源的 {{count}} 个文档已处理。" +allProcessed_other = "无需运行:源中的全部 {{count}} 个文档已处理。" completed_one = "运行已完成。" completed_other = "所有 {{count}} 次运行均已完成。" empty = "没有可运行内容:来源中没有要处理的文档。" failed = "运行失败:{{error}}" +inFlight = "没有新的内容可运行:文档仍在从先前的运行中处理。" +parked_one = "无需运行:{{count}} 个文档之前处理失败并已搁置。修复原因后,清除历史记录以重试。" +parked_other = "无需运行:{{count}} 个文档之前处理失败并已搁置。修复原因后,清除历史记录以重试。" running = "运行已开始;仍在进行中。" timeout = "运行耗时超过预期;它可能仍会在后台完成。" @@ -7307,6 +7883,7 @@ name = "流水线" sources = "来源" status = "状态" steps = "步骤" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "文件夹监视" @@ -7314,12 +7891,18 @@ manual = "手动" schedule = "已计划" [portal.policies] +defaultName = "{{category}} 策略" subtitle = "在每个文档上强制执行工具流水线的常驻自动化。每个策略在上传或导出时触发,运行其工具链,并将强制执行后的版本与原始文件一起保存。" title = "策略" [portal.policies.card] comingSoon = "升级到企业版" notSetUp = "未设置" +requiresAiEngine = "需要 AI 引擎" + +[portal.policies.categories.classification] +desc = "在上传时识别每个文档的类型,并为其元数据添加标签,便于归档和搜索。" +label = "分类" [portal.policies.categories.compliance] desc = "对每个文档强制执行 HIPAA、GDPR、SOC 2 或 FedRAMP 要求。" @@ -7344,6 +7927,13 @@ label = "安全" [portal.policies.config] scopeAll = "所有文档" +[portal.policies.config.classification] +summary = "根据你团队的标签对每个上传的文档进行分类,并为其元数据添加标签。" + +[portal.policies.config.classification.rules] +0 = "分类" +1 = "标记元数据" + [portal.policies.config.compliance] summary = "在文档离开系统之前,根据监管框架验证文档。" @@ -7371,6 +7961,29 @@ minConfidence = "最低置信度" 2 = "命名" 3 = "规范化" +[portal.policies.config.purview] +label = "应用 Microsoft Purview 敏感度标签" + +[portal.policies.config.purview.fields] +connection = "Purview 租户" +labelId = "标签 ID" +labelIdHelp = "来自 Purview 门户的标签 GUID。应用标签会将其元数据写入 PDF;它无法加密,加密需要 Microsoft 客户端。" +labelName = "标签名称(可选)" +labelNameHelp = "与 ID 一起记录,以便用户和工具识别该标签。" + +[portal.policies.config.purview.fields.method] +help = "Purview 会区分策略应用的标签和人员选择的标签。" +label = "标签的记录方式" +privileged = "作为人员选择的标签" +standard = "作为自动应用的标签" + +[portal.policies.config.purviewRead] +label = "读取 Microsoft Purview 标签" + +[portal.policies.config.purviewRead.fields] +connection = "Purview 租户" +connectionHelp = "读取文档已携带的标签,以便后续步骤据此执行操作。" + [portal.policies.config.retention] summary = "强制执行文档保留时长、何时归档以及何时删除。" @@ -7438,6 +8051,7 @@ title = "暂无活动" [portal.policies.endpoints] addWatermark = "水印" autoRedact = "遮盖 PII" +classifyAndLabel = "分类" compressPdf = "压缩" flatten = "扁平化" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "你的策略已保存,连接恢复后将显示。" retry = "重试" title = "后端不可用" +[portal.policies.operations] +change = "更改此步骤执行的操作" +noResults = "没有匹配的步骤。尝试产品名称,或“scan”、“notify”、“attach”。" +searchPlaceholder = "搜索步骤 - 试试“scan”、“attach”或产品名称" + +[portal.policies.operations.bodyMode] +binary = "原始文件字节" +json = "在 JSON 负载中" +multipart = "作为文件上传(multipart)" + +[portal.policies.operations.clamavScan] +description = "你自己的 ClamAV 服务器检查文档;任何内容都不会离开你的网络。" +label = "扫描病毒(自托管)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "拒绝包含宏、可执行文件、脚本或嵌入对象的文档。" +label = "阻止活动内容" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive 检查文档,如果不干净,运行将停止。" +label = "扫描病毒" + +[portal.policies.operations.confluenceAttach] +description = "将处理后的文档归档到页面上。" +label = "附加到 Confluence 页面" + +[portal.policies.operations.consignoSubmit] +description = "将文档提交到 ConsignO Cloud 进行签名。" +label = "发送以供签名(ConsignO)" +note = "提交文档。尚不支持检索已签名副本,因为步骤无法将结果传递给后续步骤。" + +[portal.policies.operations.customApiCall] +description = "自行编写调用:路径、方法、正文和响应处理。" +label = "调用自定义 API" + +[portal.policies.operations.discordNotify] +description = "通知频道该策略已处理文档。" +label = "向 Discord 发布消息" + +[portal.policies.operations.elasticIndex] +description = "将策略执行的操作记录为可搜索文档。" +label = "在 Elasticsearch 中索引审计事件" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart 适合上传 API;JSON 适合需要将文件放入负载中的 API;binary 发送原始字节。" +label = "如何发送文档" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "按原样发送的 JSON 正文。{{document.base64}} 携带文件;{{document.*}} 和 {{run.*}} 会按文档填充。" +label = "JSON 正文" + +[portal.policies.operations.fields.connection] +label = "账户" + +[portal.policies.operations.fields.domain] +label = "Mailgun 域" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API 期望接收文档的表单字段。" +label = "文件字段名称" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "发件人" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "标头名称/值对的 JSON 对象。连接的凭据会为你添加。" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "索引" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "作为消息正文发送。{{document.*}} 和 {{run.*}} 会按文档填充。" +label = "消息" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "页面 ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "追加到连接的基础 URL。它绝不会访问另一个主机。" +label = "路径" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "写入驱动器中的位置。{{document.filename}} 会按文档填充。" +label = "目标路径" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "Report 会保持文档不变。Replace 会将其替换为返回的内容。" +label = "如何处理回复" + +[portal.policies.operations.fields.signerEmail] +label = "签署人邮箱" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "主题" +placeholder = "已处理:{{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio 会分析此文本。使用 {{document.title}} 或粘贴你想检查的文本。" +label = "要检查的文本" + +[portal.policies.operations.fields.to] +label = "收件人" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud 用户名" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "通知空间该策略已处理文档。" +label = "向 Google Chat 发布消息" + +[portal.policies.operations.jiraAttach] +description = "将处理后的文档归档到 issue 上。" +label = "附加到 Jira issue" + +[portal.policies.operations.mailgunEmail] +description = "将处理后的文档作为附件发送。" +label = "通过 Email 发送文档(Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "将处理后的文档写入文件夹。" +label = "上传到 Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "你自己的 Presidio 服务器报告其发现的个人数据。" +label = "检测个人数据" +note = "Presidio 读取文本,而不是文件。提供你想检查的文本 — 文档本身不会上传。" + +[portal.policies.operations.responseMode] +replace = "用回复替换文档" +report = "保留文档,记录回复" + +[portal.policies.operations.sendgridEmail] +description = "将处理后的文档作为附件发送。" +label = "通过 Email 发送文档(SendGrid)" + +[portal.policies.operations.slackNotify] +description = "通知频道该策略已处理文档。" +label = "向 Slack 发布消息" + +[portal.policies.operations.splunkEvent] +description = "在审计人员已查看的位置记录策略执行的操作。" +label = "向 Splunk 发送审计事件" + +[portal.policies.operations.sumologicEvent] +description = "将策略执行的操作记录到 HTTP 收集器。" +label = "向 Sumo Logic 发送审计事件" + +[portal.policies.operations.teamsNotify] +description = "通知频道该策略已处理文档。" +label = "向 Teams 发布消息" + +[portal.policies.operations.webhookPost] +description = "将文档本身发送到你选择的 URL。" +label = "将文档 POST 到 webhook" + +[portal.policies.operations.zapierNotify] +description = "将运行详情交给你的自动化。" +label = "触发 Zap 或 Make 场景" + [portal.policies.stats] activeFor = "活跃" dataProcessed = "已处理数据" @@ -7473,6 +8282,13 @@ label = "已应用策略的文档" description = "已配置但未触发" label = "已暂停" +[portal.policies.table] +appliesTo = "应用于" +docs = "已执行文档数" +enforces = "执行" +policy = "策略" +status = "状态" + [portal.policies.wizard.actions] back = "返回" cancel = "取消" @@ -7480,6 +8296,10 @@ continue = "继续" enablePolicy = "启用策略" saveChanges = "保存更改" +[portal.policies.wizard.capability.classify] +desc = "根据你团队的标签识别文档类型并为其添加标签,以便按类别归档和搜索。" +label = "对文档分类" + [portal.policies.wizard.capability.compress] desc = "将文档压缩为更小的文件大小。" label = "减小文件大小" @@ -7500,10 +8320,18 @@ label = "遮盖敏感信息" desc = "移除隐藏的 JavaScript,使文档打开时不会自动运行任何内容。" label = "移除活动内容" +[portal.policies.wizard.capability.timestampPdf] +desc = "使用独立的时间戳机构证明文档在某一时间点以此确切形式存在。只发送哈希值 - 文档绝不会离开你的服务器。" +label = "添加可信时间戳" + [portal.policies.wizard.capability.watermark] desc = "在每一页上盖上可见标记(例如“机密”)。" label = "应用水印" +[portal.policies.wizard.classification] +description = "每个上传的文档都会根据内置标签进行分类,并标记适合的类型。标签集在整个团队中共享。" +labelsHeading = "分类标签" + [portal.policies.wizard.errors] noTools = "请先在工作流中启用至少一个工具。" saveFailed = "无法保存策略。请重试。" @@ -7548,73 +8376,118 @@ setUp = "设置 {{category}} 策略" [portal.policies.wizard.workflow] description = "选择此策略对其处理的每个文档执行的操作。" -[portal.policySummary] -activeSummary = "{{active}} / {{total}} 个活跃" -noRule = "尚未执行任何规则" -subtitle = "每个文档都会经过的常驻自动化流程,无论由哪个流水线处理。" -title = "你的 PDFs 上运行的内容" - [portal.policySummary.action] -comingSoon = "升级到 Enterprise" -configure = "配置" setUp = "设置" -[portal.policySummary.column] -activeRule = "活跃规则" -policy = "策略" -status = "状态" +[portal.processorFlow] +footnote = "计数基于过去 24 小时。流动速度仅作示意。" +liveBadge = "实时" +stats = "{{connected}} 个已连接 · 已处理 {{processed}} 个 PDF" +title = "PDF 处理器" -[portal.policySummary.empty] -description = "配置策略后,类别将显示在这里。" -title = "暂无策略" +[portal.processorFlow.lens] +ariaLabel = "查看模式" +flow = "流" +sankey = "Sankey" -[portal.policySummary.state] -active = "活跃" -off = "关闭" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "失败" +heading = "结果" +success = "已交付" + +[portal.processorFlow.policies] +activeCount = "{{n}} 个活跃" +count = "{{n}} · 24h" +heading = "策略" +setUp = "设置" soon = "即将推出" -[portal.processingStatus] -managePlan = "管理方案" -volumeSuffix = "已处理 PDFs · 过去 30 天" +[portal.processorFlow.sankey] +waist = "策略 · {{n}} 个活跃" + +[portal.processorFlow.sankey.empty] +description = "连接源并启用策略,以查看文档流经处理器。" +title = "暂无流" + +[portal.processorFlow.sources] +comingSoonTag = "连接" +editor = "Stirling PDF 编辑器" +heading = "源" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email 接收" [portal.procurement] -reset = "重置采购(演示)" subtitle = "让你的团队完成评估、签约和上线。所有文档集中在一处。" title = "采购" -[portal.procurement.action] -download = "下载" -pay = "立即支付" -request = "请求" -sign = "审阅并签署" -upload = "上传" - [portal.procurement.agreement] agreeCta = "同意并订阅" +confidential = "机密" confirm = "我已阅读并同意 Stirling Enterprise Agreement。" -eyebrow = "协议" -intro = "一份合并协议涵盖你的交易:主服务协议、订单表、EULA 和数据处理协议。请审阅后同意,将报价接受为承诺订阅。" -title = "审阅你的企业协议" +docName = "Stirling 企业协议" +docSub = "MSA、订单表、EULA 和 DPA,合并为一次签名。" +download = "下载" +downloadDraftError = "无法生成协议 PDF - 文档渲染器不可用。请稍后重试或联系支持。" +downloadError = "无法下载已签署的协议。请重试。" +legalName = "法人实体名称" +legalNamePlaceholder = "将签署的法人实体" +loadError = "无法加载协议。请重试。" +loading = "正在加载协议..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "请求更改" +scrollHint = "滚动以审阅" +signatory = "签署人姓名" +signatoryPlaceholder = "全名" +signatoryTitle = "签署人职务" +signatoryTitlePlaceholder = "例如:总法律顾问" +signError = "无法记录你的签名。请重试。" [portal.procurement.builder] addons = "附加项" +addressLine1 = "地址行 1" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "地址行 2" +addressLine2Placeholder = "套房、楼层、建筑" back = "返回" businessName = "企业名称" businessNamePlaceholder = "你的公司" +city = "城市" +cityPlaceholder = "San Francisco" +completeRequired = "请填写必填字段(标有 *),并提供有效邮箱,然后生成报价。" +contactEmail = "联系人邮箱" +contactEmailPlaceholder = "jane@acme.com" +contactName = "联系人姓名" +contactNamePlaceholder = "Jane Doe" continue = "继续" -country = "国家/地区" -countryEuro = "欧元区 (EUR €)" -countryUK = "英国 (GBP £)" -countryUS = "美国 (USD $)" +done = "完成" eula = "我已阅读并同意 Stirling Enterprise EULA。它管理由此报价生成的协议。" generate = "生成报价" -included = "已包含" indemnification = "IP 赔偿" indemnificationSub = "我们会根据 EULA 为符合条件的 IP 索赔提供抗辩" -offlineLicense = "离线 / 隔离网络许可证" -offlineLicenseSub = "用于隔离网络自托管实例的可下载许可证文件" +paperEyebrow = "企业报价" +paperFor = "准备给" +pdfSize = "PDF 大小" +poNumber = "PO 编号" +poNumberPlaceholder = "可选" +postalCode = "邮政编码" +postalCodePlaceholder = "94105" +posture = "治理" +posture_count = "约 {{count}} 条策略" +posture_essentials = "基础" +posture_essentialsSub = "分类 + 共享 — 默认项" +posture_governed = "受治理" +posture_governedSub = "添加安全和路由" +posture_regulated = "受监管" +posture_regulatedSub = "每个类别 — 合规、保留、摄取" qbr = "季度业务回顾" qbrSub = "你的 SE 每季度审查使用情况和路线图" +region = "州 / 地区" +regionPlaceholder = "California" running = "{{annual}} / 年 · {{years}} 年 {{tcv}}" s1Sub = "你的团队,以及预计每年处理的 PDFs 数量。" s1Title = "你将处理多少?" @@ -7623,6 +8496,12 @@ s2Title = "承诺和服务" s3Sub = "用于报价及其生成的协议。" s3Title = "你的详细信息" serviceLevel = "服务级别" +size_compact = "紧凑" +size_compactSub = "主要为文本,小于 1 MB" +size_heavy = "较大" +size_heavySub = "扫描件或图像较多,5 MB+" +size_standard = "标准" +size_standardSub = "文本和图像混合,1 到 5 MB" slDedicated = "专属" slDedicatedSub = "4 个工作小时 · 专属客户经理 · +30%" slPriority = "优先" @@ -7630,6 +8509,8 @@ slPrioritySub = "同一工作日 · 指定 CSM · +15%" slStandard = "标准" slStandardSub = "下一个工作日 · 共享 CSM · 已包含" stepOf = "第 {{n}} 步,共 {{total}} 步" +taxId = "VAT / 税号" +taxIdPlaceholder = "可选" term = "期限" termDiscount = "已应用 {{pct}}% 多年承诺折扣" title = "构建你的报价" @@ -7637,6 +8518,7 @@ training = "入门与培训" trainingSub = "实时课程帮助你的团队开始使用" users = "用户总数" usersPlaceholder = "例如 250" +viewEula = "阅读" volEstimated = "根据 {{count}} 位用户估算(每人约 2,000 个 PDFs,包括自动化)。如果你更清楚,可自行编辑。" volManual = "正在使用你的数值。你可以随时根据团队规模重新估算。" volNoUsers = "不确定?输入你的团队规模,我们将为你估算。" @@ -7645,55 +8527,55 @@ volumePlaceholder = "例如 1,000,000" years_one = "{{count}} 年" years_other = "{{count}} 年" -[portal.procurement.docs] -count_one = "{{count}} 个文档" -count_other = "{{count}} 个文档" -done = "完成" -here = "你在这里" -hide = "隐藏" -optional = "可选" -paidAddon = "付费附加项" -show = "显示" -subtitle = "在旅程的每一步,你需要的所有内容都会随交易推进而显示。" -supportingSubtitle = "SOC 2、安全审查、税务表格等,在你的安全或采购团队需要时即可提供。有些需要一次性费用。" -supportingTitle = "支持你的评估" +[portal.procurement.documents] +agreement = "企业协议" +agreementSub = "MSA、订单表和 DPA,一次签名。" +download = "下载" +eula = "EULA 和商业条款" +eulaSub = "软件和自助商业条款。" +invoice = "发票" +invoiceSub = "你的第一张订阅发票。" +laterInvoice = "接受后可用" +laterQuote = "报价发布后可用" +quote = "报价" +quoteSub = "你的逐项企业报价。" +sla = "SLA 附件" +slaSub = "支持层级和响应目标。" +subprocessors = "子处理方" +subprocessorsSub = "代表你处理数据的第三方。" +subtitle = "你的交易文件,随时可用。" title = "文档" -upcoming = "即将到来" +view = "查看" [portal.procurement.error] title = "出现问题" [portal.procurement.hero] -company = "你的企业交易" +barAria = "第 {{current}} 阶段,共 {{total}} 阶段" ctaAgreement = "审阅并签署协议" -ctaLive = "你已上线" -ctaPayment = "添加付款" +ctaExploring = "设置你的试用" ctaQuote = "审阅你的报价" +ctaReviewQuote = "审阅报价" ctaTrial = "构建你的报价" +documents = "文档" eyebrow = "企业采购" +eyebrowCompany = "{{company}} 企业版" inviteTeammates = "邀请队友" -keyDocs = "关键文档" -nextStep = "下一步:{{action}}" -notStarted = "未开始" +licenseKey = "许可证密钥" +liveSub = "你的组织已预配,许可证已激活。" +liveTitle = "你已在 Stirling Enterprise 上线" +next = "下一步:{{stage}}" open = "打开采购" scheduleCall = "安排通话" -setup1Sub = "邀请你的队友" -setup1Title = "部署 PDF Editor" -setup2Sub = "在编辑器和其他源中开启处理" -setup2Title = "连接 PDF Processor" -setup3Sub = "在需要时开启安全、合规、路由或保留策略" -setup3Title = "添加推荐策略" +sentenceAgreement = "审阅并签署协议" +sentenceLive = "预配和推出" +sentencePayment = "采购订单和付款" +sentenceQuote = "审阅并接受你的报价" +sentenceTrial = "设置你的试用工作区" [portal.procurement.journey] daysLeft_one = "剩余 {{count}} 天" daysLeft_other = "剩余 {{count}} 天" -engineerLabel = "你的解决方案工程师" -eyebrow = "你的推出流程" -live = "你已在 Stirling Enterprise 上线" -nextStep = "下一步:{{action}}" -subtitle = "你的解决方案工程师会参与每一步。一次只处理一个下一步操作;完整清单如下。" -title = "从试用到上线,一条引导路径" -trialTitle = "Enterprise 试用" [portal.procurement.journeySteps.agreement] blurb = "一次签名涵盖 MSA、订单表、EULA 和 DPA。" @@ -7727,6 +8609,9 @@ downloadError = "暂时无法生成离线许可证文件——请稍后重试。 downloadOffline = "下载离线许可证 (.lic)" hint = "将此密钥粘贴到自托管实例中以激活它,或留作记录。请妥善保管。" label = "你的许可证密钥" +subtitle = "使用此密钥激活自托管实例,或将其保存作为记录。" +title = "你的许可证密钥" +trialFileHint = "这是你的试用许可证文件。协议就位后,请回来再次下载它 — 与在线许可证密钥不同,.lic 文件不会自行更新。" [portal.procurement.link] cta = "关联账户" @@ -7739,65 +8624,62 @@ description = "你的订阅已激活,许可证已签发。你的团队已配 eyebrow = "已上线" title = "你已在 Stirling Enterprise 上线" -[portal.procurement.locked] -description = "开始企业评估后,试用密钥、承诺用量报价、一签式协议、付款和你的文档账本都会在这里。" -eyebrow = "仅限 Enterprise" -talkToSales = "联系销售" -title = "采购流程随 Enterprise 开启" - [portal.procurement.milestone] -accept = "接受并继续" -description = "下载 PDF 与你的团队共享,准备好后回来接受,或进行更改。" -download = "下载 PDF" downloadError = "暂时无法下载报价 PDF——请稍后重试。" -edit = "编辑报价" -eyebrow = "报价 {{number}}" -perYear = " / 年" -preparedFor = "为 {{company}} 准备" -tcv = "{{value}} 合同总价值" -title = "你的报价已准备好" - -[portal.procurement.modal] -cancel = "取消" -chooseFile = "选择文件" -close = "关闭" -downloadBody = "你的下载即将开始。" -downloadCta = "下载" -downloadTitle = "下载" -noFile = "未选择文件" -payBody = "通过 Stripe 使用银行卡或银行转账支付你的承诺合同。付款到账后,你的工作区会立即配置。" -payCta = "继续到 Stripe" -payTitle = "确认付款" -requestBodyFree = "我们会按需生成此文档。确认后,你的解决方案工程师会很快发送给你。" -requestBodyPaid = "这是付费附加项。确认后,你的解决方案工程师会评估范围并发送文件。" -requestCta = "请求" -requestTitle = "请求此文档" -signBody = "打开 Stirling Enterprise Agreement 进行电子签名:一次签名涵盖 MSA、订单表、EULA 和 DPA。我们会自动会签,然后你将进入付款步骤。" -signCta = "打开签署" -signTitle = "审阅并签署你的协议" -uploadBody = "向我们发送你的 PO,我们将按其条款开具发票。拖入 PDF 或在下方选择文件。" -uploadCta = "上传采购订单" -uploadTitle = "上传你的采购订单" [portal.procurement.payment] description = "你的报价已接受,许可证也已激活——你的团队可以立即开始使用。准备好后支付第一张发票;你可以在这里支付或下载,无需 email。" +downloadAgreement = "下载已签署协议" downloadInvoice = "下载发票" -simulate = "模拟已收到付款(演示)" +eyebrow = "付款" title = "订阅已创建" viewInvoice = "查看并支付发票" +[portal.procurement.review] +acceptCta = "接受报价" +annual = "年度费用(第 1 年)" +downloadCta = "下载报价" +poNumber = "采购订单:{{po}}" +renewal = "期限结束后按 {{amount}}/年续订(+{{pct}}% CPI)" +tcv = "{{years}} 年期限,预先全额支付:{{tcv}}" +validUntil = "有效期至 {{date}}" + [portal.procurement.schedule] fallback = "无法加载调度器。" fallbackLink = "在新标签页中打开调度" +loading = "正在加载调度器..." subtitle = "你的解决方案工程师将带领你的团队完成推出流程。选择一个适合你的时间。" title = "安排通话" -[portal.procurement.status] -action = "需要操作" -available = "可用" -complete = "完成" -pending = "待处理" -request = "按请求提供" +[portal.procurement.setup] +airgap = "隔离网络" +airgapSub = "封闭,无出站连接" +back = "返回" +businessName = "企业名称" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "托管,最快开始" +continue = "继续" +deployment = "应部署在哪里?" +eula = "我同意 Stirling EULA 和商业条款。" +fullName = "全名" +fullNamePlaceholder = "你的姓名" +invites = "邀请队友 · 可选" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "预约通话" +seats = "测试人数" +seatsPlaceholder = "例如:15" +selfhost = "自托管" +selfhostSub = "Docker 或 Kubernetes" +start = "开始试用" +stepOf = "第 {{n}} 步,共 {{total}} 步" +subtitle = "免费 14 天,无需信用卡。" +subtitleDetails = "为你的报价和协议提供一些详细信息。" +talkFirst = "想先聊聊?" +title = "设置你的试用" +viewEula = "阅读" +workEmail = "工作邮箱" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "延长会增加 7 天,并通知你的解决方案工程师。" @@ -7808,109 +8690,113 @@ maxed = "已达上限" subtitle = "你的免费试用持续到 {{date}}。无需银行卡。" title = "Enterprise 试用" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "承诺用量定价、全组织 SSO + SCIM + RBAC、90 天不可变审计,以及专属 SE。" -homeCta = "开始试用 →" -homeHeadline = "处理数百万个 PDFs。" - -[portal.recentActivity] -title = "近期活动" -viewAll = "查看全部" - -[portal.recentActivity.empty] -description = "流水线运行、部署和代理事件将显示在这里。" -title = "这里还没有内容" - -[portal.search] -ariaLabel = "搜索" -placeholder = "搜索 Stirling — 端点、流水线、文档…" - -[portal.search.empty] -noActionsDescription = "可用后,快捷操作将显示在这里。" -noActionsTitle = "无快捷操作" -noMatches = "没有与“{{query}}”匹配的结果" -noMatchesDescription = "尝试其他关键词或浏览目录。" - [portal.settings.groups] admin = "管理员" [portal.settings.sections] account-link = "账户关联" -[portal.shell.header] -accountFallback = "账户" -accountMenu = "账户菜单" -darkMode = "深色模式" -lightMode = "浅色模式" -search = "搜索" -searchPlaceholder = "搜索…" -signOut = "退出登录" -switchToDark = "切换到深色主题" -switchToLight = "切换到浅色主题" - [portal.shell.sidebar] appEditor = "编辑器" appProcessor = "处理器" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} 个文档" linkAccount = "关联 Stirling 账户" -planEditor = "编辑器方案" -planEnterprise = "Enterprise 方案" -planProcessor = "处理器方案" primaryNav = "主导航" switchApp = "切换应用" +[portal.shell.topbar] +closeNav = "关闭导航" +openNav = "打开导航" +search = "搜索" + [portal.sources] subtitle = "可复用的输入连接,用于将文档送入 Stirling。配置一次连接,然后可在任意数量的策略中引用它。点击一行查看其配置以及使用它的策略。" title = "源" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "连接源" +[portal.sources.builder] +back = "返回源" +backToTypes = "所有源类型" +cancel = "取消" +chooseHint = "选择文档来源。灰显的连接器即将推出。" +comingSoon = "即将推出" +comingSoonHeading = "即将推出" +create = "创建源" +createTitle = "连接源" +delete = "删除" +editTitle = "编辑源" +enabled = "已启用" +save = "保存更改" + +[portal.sources.builder.folderAccess] +description = "文件夹自动化只能使用管理员已允许的文件夹。在文件夹访问设置中添加它,然后重试。" +openSettings = "文件夹访问设置" +title = "不允许使用此文件夹" + [portal.sources.delete] body = "删除“{{name}}”?此操作无法撤销。引用它的策略需要更新。" cancel = "取消" confirm = "删除" title = "删除源?" -[portal.sources.detail] -closeAriaLabel = "关闭详情" -delete = "删除源" -docs24h = "过去 24 小时" -docs30d = "过去 30 天" -docsTotal = "累计发现" -docsTrend = "过去 30 天的文档" -documents = "文档" -edit = "编辑" -notReferenced = "未被任何策略引用,因此可以安全删除。" -pause = "暂停" -resume = "恢复" -subtitle = "{{type}} · {{status}}" -usedBy = "使用者" - -[portal.sources.empty] -description = "连接一个文件夹(很快还会支持云存储),让你的策略有地方拉取文档。" -title = "尚未连接源" - [portal.sources.kpi] inUse = "使用中" total = "连接" unused = "未使用" +[portal.sources.networkFields.connection] +helperText = "包含服务器地址和凭据的已存储连接。由引用它的每个源重复使用。" +label = "连接" + +[portal.sources.networkFields.directory] +helperText = "要轮询的服务器文件夹,相对于登录主目录或共享根目录。留空表示根目录。" +label = "文件夹" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "Consume 会在每个策略处理完文件后,从服务器移除该文件。" +label = "读取模式" + +[portal.sources.networkFields.mode.options] +consume = "Consume:每个文件只处理一次" +snapshot = "Snapshot:每次运行重新读取文件夹" + +[portal.sources.networkFields.recursive] +label = "文件夹深度" + +[portal.sources.networkFields.recursive.options] +all = "包含子文件夹" +top = "仅顶层" + [portal.sources.status] active = "活跃" disabled = "已禁用" unused = "未使用" [portal.sources.table] +documents = "文档" source = "源" status = "状态" +type = "Type" usedBy = "策略" +[portal.sources.types.box] +description = "从团队已共享的 Box 文件夹中拉取文档。" +label = "Box" + +[portal.sources.types.dropbox] +description = "从共享的 Dropbox 文件夹中拉取文档。" +label = "Dropbox" + [portal.sources.types.editor] +description = "你的团队在编辑器中处理过的文档,跨策略和 AI 运行。" label = "编辑器" +noPolicies = "编辑器尚未运行任何策略。" + +[portal.sources.types.email] +description = "将发送到专用邮箱的附件转换为文档。" +label = "Email 收件箱" [portal.sources.types.folder] description = "监视服务器上的目录以发现新文档。" @@ -7943,25 +8829,80 @@ label = "文件夹深度" all = "包含子文件夹" top = "仅顶层" +[portal.sources.types.ftp] +description = "轮询 FTP 或 FTPS 服务器文件夹以获取新文档。" +label = "FTP" + +[portal.sources.types.googledrive] +description = "从共享的 Google Drive 文件夹中拉取文档。" +label = "Google Drive" + +[portal.sources.types.network] +description = "监视 SMB 网络共享以获取新文档。" +label = "网络驱动器" + +[portal.sources.types.onedrive] +description = "监视 OneDrive 文件夹以获取新文档。" +label = "OneDrive" + +[portal.sources.types.s3] +description = "从 Amazon S3 或 S3 兼容存储桶中拉取文档。" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "包含存储桶和凭据的已存储连接。由引用它的每个源和管道输出重复使用。" +label = "连接" + +[portal.sources.types.s3.fields.mode] +helperText = "Consume 会在每个策略处理完对象后,从存储桶移除该对象。" +label = "读取模式" + +[portal.sources.types.s3.fields.mode.options] +consume = "Consume:每个对象只处理一次" +snapshot = "Snapshot:每次运行重新读取存储桶" + +[portal.sources.types.s3.fields.prefix] +helperText = "仅处理键以此前缀开头的对象。" +label = "键前缀" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "轮询 SFTP 投递文件夹以获取新文档。" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "从 SharePoint 文档库中拉取文档。" +label = "SharePoint" + [portal.sources.types.unknown] label = "源" +[portal.sources.types.webhook] +createNote = "创建此 webhook 时会生成投递 URL 和签名密钥。发送方将文档 POST 到该 URL,引用的策略会在到达时运行。" +description = "通过来自任何外部系统的签名 HTTP POST 接收文档。" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "投递 URL" +secretNote = "签名密钥仅在创建 webhook 时显示一次。重新创建源以轮换它。" + +[portal.sources.types.webhook.reveal] +copy = "复制" +done = "完成" +secret = "签名密钥" +secretHelp = "使用此密钥(HMAC-SHA256)签署每次投递的原始正文,并将其作为 X-Stirling-Signature 标头发送。" +secretWarning = "立即复制签名密钥。为保障你的安全,它只显示一次,以后无法检索。" +title = "Webhook 已创建" +url = "投递 URL" +usage = "将每个文档作为原始请求正文 POST 到投递 URL,并使用二进制内容类型(application/pdf 或 application/octet-stream)。引用的策略会在到达时自动运行。" + [portal.sources.wizard] -back = "返回" -cancel = "取消" -continue = "继续" -editTitle = "编辑源" name = "名称" namePlaceholder = "例如 理赔接收" -save = "保存更改" -subtitle = "第 {{current}} 步,共 {{total}} 步 · {{label}}" -title = "连接源" type = "类型" -[portal.sources.wizard.steps] -chooseType = "选择类型" -configure = "配置" -review = "审阅并连接" +[portal.stepModal] +close = "关闭" [portal.usage] managePayment = "管理付款" @@ -8021,16 +8962,9 @@ summary = "拥有一个团队——管理其成员资源和共享配置。" 2 = "通过默认策略访问门户" 3 = "Member 可以做的所有事" -[portal.welcome] -ariaLabel = "欢迎使用 Stirling PDF" -badge = "开源" -installEditor = "安装编辑器" -inviteTeammates = "邀请队友" -openInBrowser = "在浏览器中打开" -perks = "永久免费 · 可自托管" -subtitle = "全球最安全的 PDF Editor 对各种规模的团队免费。包括 60+ 个 PDF 操作和 SSO。" -title = "欢迎使用" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · 无限" [printFile] title = "打印文件" @@ -8613,6 +9547,14 @@ text = "以 90° 为增量顺时针或逆时针旋转 PDF 页面。PDF 中所有 [rotate.tooltip.header] title = "旋转设置概览" +[ruler] +clearAll = "全部清除" +hideAllLabels = "隐藏所有标签" +hideSmallLabels = "隐藏小标签" +physicalValues = "物理" +scaled = "已缩放" +showAllLabels = "显示所有标签" + [sanitize] desc = "移除 PDF 文件中的潜在有害元素。" sanitizationResults = "安全清理结果" @@ -8654,31 +9596,45 @@ label = "删除 XMP 元数据" [sanitize.steps] settings = "设置" -[sanitizePdf] -tags = "清理、安全、安全、删除威胁" - [sanitizePDF] title = "清理 PDF" +[sanitizePdf] +tags = "清理、安全、安全、删除威胁" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "调整页面缩放比例" +[scaleSettings] +activeScale = "当前比例" +apply = "应用比例" +applyCalibration = "应用校准" +calculatedScale = "计算比例" +calibrate = "校准" +calibrating = "正在校准" +calibrationDistanceRequired = "请输入大于零的实际距离" +calibrationInvalid = "无法根据此测量值计算比例" +calibrationPaperDistance = "测得页面距离:{{distance}}" +calibrationTitle = "校准比例" +calibrationTooltip = "测量已知距离以自动计算比例" +customScale = "自定义比例" +noneSet = "未设置自定义比例" +presets = "预设比例" +ratio = "比例" +ratioHelp = "比例:1 页面单位 = X 实际单位" +ratioPlaceholder = "例如,100" +ratioPositive = "比例必须大于零" +ratioRequired = "比例为必填项" +realDistance = "实际距离" +realDistancePlaceholder = "例如,5" +reset = "恢复默认设置" +unit = "单位" +unitInvalid = "无效单位:{{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "角度阈值:" -10 = "设置添加和删除的边框大小,以防止输出中出现白边(默认值:1)。" -2 = "设置图像被旋转所需的最小绝对角度(默认:10)。" -3 = "公差:" -4 = "确定估计背景颜色周围的颜色变化范围(默认值:30)。" -5 = "最小面积:" -6 = "设置照片的最小面积阈值(默认:10000)。" -7 = "最小轮廓面积:" -8 = "设置照片的最小轮廓面积阈值。" -9 = "边框尺寸:" - [scannerImageSplit] submit = "提取图像扫描" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "功能说明" whatThisDoesDesc = "自动查找并从扫描页面或合成图像中提取每张照片 - 无需手动裁剪。" whenToUse = "何时使用" +[ScannerImageSplit.selectText] +1 = "角度阈值:" +10 = "设置添加和删除的边框大小,以防止输出中出现白边(默认值:1)。" +2 = "设置图像被旋转所需的最小绝对角度(默认:10)。" +3 = "公差:" +4 = "确定估计背景颜色周围的颜色变化范围(默认值:30)。" +5 = "最小面积:" +6 = "设置照片的最小面积阈值(默认:10000)。" +7 = "最小轮廓面积:" +8 = "设置照片的最小轮廓面积阈值。" +9 = "边框尺寸:" + [search] noResults = "未找到结果" placeholder = "输入搜索词…" @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "您的 Stirling-PDF 服务器处于离线状态,且 [session] expired = "您的会话已过期。请刷新页面并重试。" -[sessionManagement.tooltip] -header = "管理签署会话" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "新参与者将添加到签署顺序的末尾" -bullet2 = "会话最终确定后无法再添加参与者" -bullet3 = "轮到他们时,每位参与者都会收到通知" -description = "在最终确定前,您可以随时向活动会话添加更多参与者。" -title = "添加参与者" - -[sessionManagement.tooltip.finalization] -bullet1 = "完全最终确定:所有参与者均已签署" -bullet2 = "部分最终确定:部分参与者尚未签署" -bullet3 = "未签署的参与者将被排除在最终文档之外" -bullet4 = "最终确定后,您可以将已签署的 PDF 加载到活动文件中" -description = "最终确定会将所有签名合并为一个已签署的 PDF。此操作无法撤销。" -title = "会话最终确定" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "无法移除已签署的参与者" -bullet2 = "被移除的参与者将不再收到通知" -bullet3 = "签署顺序将自动调整" -description = "参与者可在签署前从会话中移除。" -title = "移除参与者" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "每个签名都会按顺序应用到 PDF" -bullet2 = "后续签署者可以看到较早的签名" -bullet3 = "对审批流程和法律保全至关重要" -description = "您在创建会话时指定的顺序决定谁先签署。" -title = "签署顺序" - [settings] close = "关闭" +[settings.ai] +documents = "文档与 RAG" +general = "常规" +limits = "限制与性能" +models = "模型与提供商" +title = "AI" + [settings.configuration] advanced = "高级" database = "数据库" endpoints = "端点" features = "功能" +folderAccess = "文件夹访问" mcp = "MCP Server" storageSharing = "文件存储与共享" systemSettings = "系统设置" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "默认 PDF 编辑器" defaultPdfEditorActive = "Stirling PDF 是您的默认 PDF 编辑器" defaultPdfEditorChecking = "正在检查..." defaultPdfEditorInactive = "其他应用被设为默认" +defaultPdfEditorRemind = "提醒我设置为默认" +defaultPdfEditorRemindDescription = "当 Stirling PDF 不是你的默认 PDF 应用程序时显示横幅。" defaultPdfEditorSet = "已为默认" defaultStartupView = "启动时的默认视图" defaultStartupViewDescription = "选择应用启动时左侧栏中激活的选项卡" @@ -8817,9 +9763,18 @@ languageDescription = "选择显示语言" logout = "退出登录" setAsDefault = "设为默认" theme = "主题" +themeAccent = "强调色" +themeAccentCustom = "自定义颜色" +themeAccentCustomHint = "自定义(建议使用默认值)" +themeAccentDark = "深色模式强调色" +themeAccentDefault = "默认" +themeAccentDefaultHint = "默认主题(推荐)" +themeAccentDescription = "按钮、链接和高亮会使用它,并为应用提供微妙的色调。浅色和深色模式各自拥有独立设置。系统模式会使用当前启用的设置。" +themeAccentLight = "浅色模式强调色" themeDark = "深色" themeDescription = "在浅色和深色模式间切换" themeLight = "浅色" +themeReset = "将主题恢复为默认" themeSystem = "System" title = "通用" user = "用户" @@ -8833,6 +9788,12 @@ intro = "为您的组织启用用户认证、团队管理和工作区功能。" learnMore = "在文档中了解更多" title = "面向系统管理员" +[settings.general.loginLanding] +description = "选择登录 Stirling Cloud 后打开的应用。" +editor = "编辑器" +processor = "处理器" +title = "登录后" + [settings.general.mode] fullscreen = "全屏" sidebar = "侧边栏" @@ -9013,9 +9974,6 @@ title = "策略与隐私" [settings.preferences] title = "首选项" -[settings.search] -placeholder = "搜索设置页面..." - [settings.security] description = "更新密码以保障您的账户安全。" title = "安全" @@ -9041,6 +9999,11 @@ security = "安全" telegram = "Telegram" title = "安全与认证" +[settings.signOut] +confirm = "确定要退出登录吗?" +submit = "退出登录" +title = "退出登录" + [settings.team] title = "团队" @@ -9156,6 +10119,7 @@ last = "末页" maintainRatio = "切换保持长宽比" next = "下一页" noSavedSigs = "未找到已保存的签名" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "个人签名" previous = "上一页" redo = "重做" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "背景移除失败" hint = "上传 PNG 或 JPG 格式的签名图像" label = "上传签名图像" placeholder = "选择图像文件" +previewAlt = "Current image signature" processing = "正在处理图像..." removeBackground = "移除白色背景(变为透明)" @@ -9207,6 +10172,17 @@ saved = "在上方选择一个已保存的签名,然后在 PDF 任意位置点 text = "在上方输入您的姓名后,在 PDF 上任意位置点击以放置签名。" title = "如何添加签名" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "移动签名" pause = "暂停放置" @@ -9653,10 +10629,30 @@ title = "上传到服务器" updateButton = "在服务器上更新" uploadButton = "上传到服务器" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "点击这里" +fallback = "如果未自动打开," +opening = "正在新标签页中打开 Swagger UI..." tags = "api,文档,swagger,端点,开发" title = "API 文档" @@ -9764,12 +10760,39 @@ selectFilesHint = "在“活动文件”中选择文件以运行此工具" singleFileScope = "仅应用于:{{fileName}}" viewerMode = "切换到文件编辑器以选择多个文件。" +[toolFormat] +ANY = "任意文件" +CBR = "漫画书归档文件 (CBR)" +CBZ = "漫画书归档文件 (CBZ)" +CSV = "CSV 文件" +EBOOK = "电子书" +EMAIL = "电子邮件" +EXCEL = "电子表格" +HTML = "网页" +IMAGE = "图片" +JAVASCRIPT = "JavaScript 文件" +JSON = "JSON 文件" +MARKDOWN = "Markdown 文件" +NONE = "无文件" +PCL = "PCL 打印文件" +PDF = "PDF" +PDF_ENCRYPTED = "受密码保护的 PDF" +POSTSCRIPT = "PostScript 文件" +PPT = "演示文稿" +TEXT = "文本文件" +VIDEO = "视频" +WORD = "Word 文档" +XML = "XML 文件" +XPS = "XPS 文档" +ZIP = "ZIP 归档文件" + [toolPanel] alpha = "Alpha" backToAllTools = "返回所有工具" collapse = "折叠面板" expand = "展开面板" goBack = "返回" +pdfTools = "PDF 工具" placeholder = "选择一个工具以开始" premiumFeature = "高级功能:" toolsHeader = "工具" @@ -9806,11 +10829,6 @@ noToolsFound = "未找到工具" quickAccess = "快速访问" searchPlaceholder = "搜索工具…" -[toolPicker.categories] -advancedTools = "高级工具" -recommendedTools = "推荐工具" -standardTools = "标准工具" - [toolPicker.subcategories] advancedFormatting = "高级排版" automation = "自动化" @@ -9824,6 +10842,9 @@ removal = "移除" signing = "签署" verification = "验证" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "未找到工具" noTools = "没有可用的工具" @@ -9950,28 +10971,53 @@ title = "用户" you = "(你)" [users.action] +cancelInvite = "取消邀请" deleteTeam = "删除团队" disableMfa = "重置 MFA" move = "移动到团队" reinstate = "恢复" remove = "从组织中移除" +removeTeam = "从团队移除" rename = "重命名团队" resetPw = "重置密码" suspend = "暂停" unlock = "解锁账户" +[users.activity] +daysAgo = "{{count}} 天前" +hoursAgo = "{{count}} 小时前" +justNow = "刚刚" +minutesAgo = "{{count}} 分钟前" +monthsAgo_one = "{{count}} 个月前" +monthsAgo_other = "{{count}} 个月前" +never = "从未" +weeksAgo_one = "{{count}} 周前" +weeksAgo_other = "{{count}} 周前" +yearsAgo_one = "{{count}} 年前" +yearsAgo_other = "{{count}} 年前" + [users.cap] addProcessor = "+ 处理器" approver = "批准策略" editor = "编辑器" processor = "处理器" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "取消对 {{email}} 的邀请?他们将无法使用当前链接加入。" +cancelInviteTitle = "取消邀请" deleteTeamBody = "删除 {{name}} 团队?该团队必须先为空——请将其成员移动到其他团队,并且它不能仍拥有任何集成配置。" deleteTeamTitle = "删除团队" disableMfaBody = "移除 {{name}} 的 MFA 注册?如果需要,他们将在下次登录时重新设置。" disableMfaTitle = "重置 MFA" removeBody = "永久从组织中移除 {{name}}?此操作无法撤销。" +removeTeamBody = "将 {{name}} 从团队中移除?他们会保留账户,但会失去对此团队资源的访问权限。" removeTitle = "移除成员" [users.empty] @@ -9982,10 +11028,8 @@ title = "暂无成员" addToTeam = "添加到团队" guestCount = "{{count}} 位访客" guests = "访客" -guestsDesc = "外部协作者,范围限于你共享的内容。仅编辑器。" ledBy = "由 {{owner}} 领导" org = "组织" -orgDesc = "拥有组织范围权限和策略批准权限的所有者" owners = "{{count}} 位所有者" team = "{{name}} 团队" teamMeta = "{{count}} 人" @@ -10022,6 +11066,18 @@ username = "用户名" usernameError = "用户名必须至少包含 3 个字符" usernamePlaceholder = "jsmith" +[users.invites] +by = "由 {{who}} 邀请" +cancel = "取消" +expiresInDays_one = "{{count}} 天后过期" +expiresInDays_other = "{{count}} 天后过期" +expiresToday = "今天过期" +title = "待处理邀请" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "连接后端时出现问题,或者你没有访问权限。请重试。" title = "无法加载成员" @@ -10211,10 +11267,15 @@ searchPlaceholder = "搜索附件" title = "附件" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "书签标题" +bookmarkTitleRequired = "书签标题为必填项" closeSidebar = "关闭书签侧边栏" collapseAll = "折叠所有书签" +empty = "No bookmarks in this document" expandAll = "展开所有书签" +searchPlaceholder = "搜索书签" +title = "Bookmarks" [viewer.comments] addComment = "添加评论" @@ -10236,10 +11297,12 @@ locateAnnotation = "在文档中定位" moreActions = "更多操作" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "第 {{page}} 页" placingHint = "单击页面以放置… (取消)" removeCommentOnly = "仅移除评论" saveReply = "保存回复" +searchPlaceholder = "Search comments" title = "评论" typeComment = "评论" typeInsertText = "插入文本" @@ -10262,6 +11325,7 @@ unsavedDesc = "您有未保存的更改" closeSidebar = "关闭图层侧边栏" hideAll = "隐藏所有图层" showAll = "显示所有图层" +title = "Layers" [viewer.link] delete = "删除链接" @@ -10605,30 +11669,6 @@ title = "水印文本" image = "图像" text = "文本" -[wetSignature.tooltip] -header = "签名创建方式" - -[wetSignature.tooltip.draw] -bullet1 = "自定义笔颜色和粗细" -bullet2 = "清除并重新绘制直至满意" -bullet3 = "适用于触控设备(平板、手机)" -description = "使用鼠标或触摸屏创建手写签名。最适合个人、真实的签名。" -title = "手写签名" - -[wetSignature.tooltip.type] -bullet1 = "从多种字体中选择" -bullet2 = "自定义文字大小和颜色" -bullet3 = "适用于标准化签名" -description = "从输入文本生成签名。快速一致,适用于商务文档。" -title = "文字签名" - -[wetSignature.tooltip.upload] -bullet1 = "支持 PNG、JPG 等图像格式" -bullet2 = "建议使用透明背景以获得最佳效果" -bullet3 = "图像将调整大小以适配签名区域" -description = "上传预先制作的签名图像。如果您有扫描的签名或公司徽标,理想之选。" -title = "上传签名图像" - [workbenchBar] activeFiles = "活动文件" annotations = "注释" @@ -10642,6 +11682,7 @@ exitRedaction = "退出涂黑模式" exportAll = "导出 PDF" exportSelected = "导出所选页面" formFill = "填写表单" +hideToolbar = "Hide toolbar" multiTool = "多功能工具" panMode = "平移模式" print = "打印 PDF" @@ -10652,6 +11693,7 @@ redact = "涂黑" rotateLeft = "向左旋转" rotateRight = "向右旋转" ruler = "标尺/测量" +rulerSettings = "比例设置" save = "保存" saveAll = "全部保存" saveAs = "另存为" @@ -10661,6 +11703,7 @@ selectAll = "全选" selectByNumber = "按页码选择" selectLanguage = "选择语言" share = "共享" +showToolbar = "Show toolbar" toggleAnnotations = "切换注释显示" toggleAttachments = "切换附件" toggleBookmarks = "切换书签" diff --git a/frontend/editor/public/locales/zh-TW/translation.toml b/frontend/editor/public/locales/zh-TW/translation.toml index c6684c234e..827d088e37 100644 --- a/frontend/editor/public/locales/zh-TW/translation.toml +++ b/frontend/editor/public/locales/zh-TW/translation.toml @@ -1,7 +1,6 @@ alphabet = "字母表" apply = "套用" applyAndContinue = "儲存並離開" -areYouSure = "確定要離開嗎?" back = "返回" black = "黑色" blue = "藍色" @@ -46,10 +45,8 @@ filesSelected = "已選取 {{count}} 個檔案" font = "字型" fontSizeTooltip = "頁碼文字大小(點)。數值越大文字越大。" fontTypeTooltip = "頁碼的字型家族。可依文件風格選擇。" -genericSubmit = "送出" green = "綠色" help = "說明" -imgPrompt = "選擇圖片" incorrectPasswordMessage = "目前密碼不正確。" info = "資訊" invalidUndoData = "無法復原:無效的作業資料" @@ -62,7 +59,6 @@ loginAgreementTitle = "登入協議" logOut = "登出" marginTooltip = "頁碼與頁面邊緣的距離。" moreOptions = "更多選項" -multiPdfPrompt = "選擇多個 PDF 檔案" never = "永不" no = "否" noFileSelected = "未選擇檔案,請上傳一個。" @@ -80,7 +76,6 @@ pageOrderPrompt = "自訂頁面順序(輸入以逗號分隔的頁碼或函式 pages = "頁面" pageSelectionPrompt = "自訂頁面選擇(輸入以逗號分隔的頁碼 1、5、6 或 2n+1 等函式的清單):" password = "密碼" -pdfPrompt = "選擇 PDF 檔案" pin = "釘選檔案(工具執行後保持作用中)" pro = "專業版" processingComplete = "您的檔案已就緒。" @@ -110,6 +105,7 @@ undoSuccess = "已成功復原作業" unknown = "未知" unpin = "取消釘選檔案(工具執行後以新檔取代)" unsavedChanges = "您對 PDF 的變更尚未儲存。" +unsavedChangesBody = "You have unsaved changes to your PDF. Are you sure you want to leave?" unsavedChangesTitle = "未儲存的變更" unsupported = "不支援" username = "使用者名稱" @@ -356,6 +352,7 @@ text = "這些權限控制使用者對 PDF 可進行的操作。與擁有者密 title = "變更權限" [addStamp] +preview = "預覽印章" tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" [AddStampRequest] @@ -691,6 +688,165 @@ manualLinks = "手動下載:按一下連結,並將檔案放入 tessdata 資 noLanguages = "在已設定的目錄中找不到任何 tessdata 語言。" permissionNotice = "tessdata 路徑不可寫入。下載將在瀏覽器中開啟;請手動將 .traineddata 檔案儲存到 tessdata 資料夾中。" +[admin.settings.ai.documents] +description = "設定用於回答文件相關問題的嵌入模型與檢索設定。儲存後會套用至 AI engine。" +title = "文件與 RAG" + +[admin.settings.ai.documents.embeddingApiKey] +description = "留空以使用 engine 自身的環境認證。適用於自架單一 engine 部署。" +generic = "嵌入 API key" +openai = "OpenAI API key" +setPlaceholder = "已儲存 - 留空以保留目前的 key" +voyageai = "VoyageAI API key" + +[admin.settings.ai.documents.embeddingBaseUrl] +description = "OpenAI 相容 / Ollama embeddings 端點的基礎 URL,例如 http://ollama:11434/v1。必須指向受信任的內部端點(SSRF 敏感)。" +label = "嵌入基礎 URL" + +[admin.settings.ai.documents.embeddingModel] +description = "嵌入模型名稱。自由文字;建議僅供提示。" +label = "嵌入模型" + +[admin.settings.ai.documents.embeddingProvider] +description = "用於將文件文字轉換為向量嵌入的提供者。" +label = "嵌入提供者" + +[admin.settings.ai.documents.maxSearches] +description = "代理程式每個請求可執行的最大檢索搜尋次數。" +label = "最大搜尋次數" + +[admin.settings.ai.documents.reindexNote] +body = "變更嵌入模型會立即生效,但使用先前模型建立索引的文件必須重新建立索引,搜尋才能回傳正確結果。" +title = "需要重新建立索引" + +[admin.settings.ai.documents.saved] +reindexNote = "如果您變更了嵌入模型,請重新為現有文件建立索引,讓搜尋使用新模型。" + +[admin.settings.ai.documents.topK] +description = "每次搜尋擷取的最相關區塊數量。" +label = "Top K" + +[admin.settings.ai.general] +description = "將 Stirling 連接至 Python AI engine,並選擇要公開的 AI 功能。變更會在重新啟動後套用。" +title = "AI Engine" + +[admin.settings.ai.general.capabilities] +description = "開啟或關閉個別 AI 功能。停用的功能會在應用程式中隱藏。" +title = "功能" + +[admin.settings.ai.general.enabled] +description = "主開關。關閉時,將無法使用任何 AI 工具、代理程式或 engine 呼叫。" +label = "啟用 AI" + +[admin.settings.ai.general.features.chat] +description = "用於處理 PDFs 的對話式助理。" +label = "聊天助理" + +[admin.settings.ai.general.features.classify] +description = "依類型或內容自動分類文件。" +label = "文件分類" + +[admin.settings.ai.general.features.createPdf] +description = "透過 AI 代理程式產生新的 PDF(例如從 HTML)。" +label = "從提示建立 PDF" + +[admin.settings.ai.general.features.documentQuestions] +description = "提出問題並取得以已上傳文件為依據的答案。" +label = "文件問答" + +[admin.settings.ai.general.features.mathAuditor] +description = "檢查文件中的數學與數值錯誤。" +label = "數學稽核員" + +[admin.settings.ai.general.features.pdfComment] +description = "將 AI 撰寫的審閱註解與標註新增至 PDF。" +label = "PDF 註解代理程式" + +[admin.settings.ai.general.longRunningTimeoutSeconds] +description = "文件產生等較繁重代理程式操作的逾時時間。" +label = "長時間執行逾時(秒)" + +[admin.settings.ai.general.note] +body = "AI engine 會作為獨立服務執行。其共用密鑰" +body2 = "會透過容器環境變數設定。提供者 API keys 可在這些頁面輸入,或作為 engine 環境變數提供;在此輸入的 keys 會在儲存時推送至 engine。" +title = "關於 AI engine" + +[admin.settings.ai.general.streamTimeoutSeconds] +description = "串流(逐 token)聊天回應的逾時時間。" +label = "串流逾時(秒)" + +[admin.settings.ai.general.test] +button = "測試連線" +failBody = "AI engine 未回應。請檢查 URL、engine 容器是否正在執行,以及 AI 是否已啟用(啟用後需要重新啟動)。" +failTitle = "無法連線至 AI engine" +okBody = "AI engine 已回應健康檢查。" +okTitle = "可連線至 AI engine" + +[admin.settings.ai.general.timeoutSeconds] +description = "對 engine 的標準 AI 請求逾時時間。" +label = "請求逾時(秒)" + +[admin.settings.ai.general.url] +description = "Python AI engine 的內部 URL,例如 http://stirling-pdf-engine:5001。" +label = "AI engine URL" + +[admin.settings.ai.limits] +description = "限制 AI 請求可執行的工作量,以及可同時執行的數量。儲存後會套用至 AI engine。" +title = "限制與效能" + +[admin.settings.ai.limits.maxCharacters] +description = "防護限制:拒絕擷取文字超過此長度的 AI 請求。" +label = "每個請求的最大字元數" + +[admin.settings.ai.limits.maxPages] +description = "防護限制:拒絕超過此 PDF 頁數的 AI 請求。" +label = "每個請求的最大頁數" + +[admin.settings.ai.limits.modelMaxConcurrency] +description = "整個 engine 中同時進行的模型呼叫最大數量。" +label = "模型最大並行數" + +[admin.settings.ai.models] +description = "選擇 LLM 提供者,以及 AI engine 使用的智慧/快速模型。儲存後會套用至 AI engine。" +title = "模型與提供者" + +[admin.settings.ai.models.apiKey] +anthropic = "Anthropic API key" +description = "留空以使用 engine 自身的環境認證。適用於自架單一 engine 部署。" +generic = "API key" +openai = "OpenAI API key" +setPlaceholder = "已儲存 - 留空以保留目前的 key" + +[admin.settings.ai.models.baseUrl] +description = "OpenAI 相容 / Ollama 端點的基礎 URL,例如 http://ollama:11434/v1。" +label = "提供者基礎 URL" +warning = "基礎 URL 必須指向受信任的內部端點。engine 會向其發出伺服器端請求,因此不受信任的值具有 SSRF 風險。" + +[admin.settings.ai.models.fastMaxTokens] +description = "快速模型的最大輸出 tokens。" +label = "快速模型最大 tokens" + +[admin.settings.ai.models.fastModel] +description = "較便宜、較快速的模型,用於輕量工作。自由文字;建議僅供提示。" +label = "快速模型" + +[admin.settings.ai.models.provider] +description = "engine 要與哪個 LLM 提供者通訊。" +label = "提供者" + +[admin.settings.ai.models.smartMaxTokens] +description = "智慧模型的最大輸出 tokens。" +label = "智慧模型最大 tokens" + +[admin.settings.ai.models.smartModel] +description = "具備高能力、用於複雜推理的模型。自由文字;建議僅供提示。" +label = "智慧模型" + +[admin.settings.ai.saved] +body = "變更會自動推送至 AI engine。" +bodyNoPush = "設定已儲存。下次 AI engine 取得其設定時會套用。" +title = "AI 設定已儲存" + [admin.settings.badge] clickToUpgrade = "點選查看方案詳細資訊" @@ -938,6 +1094,25 @@ label = "啟動時重新產生" description = "憑證的有效天數" label = "憑證有效期(天)" +[admin.settings.folderAccess] +description = "允許資料夾來源與資料夾輸出讀取和寫入的目錄。這是安全邊界:自動化絕不能指向此清單之外的伺服器路徑。" +securityNote = "留空可完全停用資料夾來源與輸出。Stirling 自身的設定目錄一律禁止存取。" +title = "資料夾存取" + +[admin.settings.folderAccess.implied] +description = "這些由 Stirling 管理的目錄一律允許,且無法在此變更。" +serverStorage = "伺服器檔案儲存空間" +title = "一律允許" +watchedFolder = "Pipeline 監看資料夾" + +[admin.settings.folderAccess.roots] +add = "新增" +empty = "未允許任何資料夾。目前已停用資料夾來源與輸出。" +hint = "輸入絕對路徑,例如 /data/inbox。" +label = "允許的資料夾根目錄" +placeholder = "/data/inbox" +remove = "移除資料夾根目錄" + [admin.settings.general] description = "設定全系統的應用程式選項,包括品牌與預設行為。" system = "系統" @@ -1704,6 +1879,9 @@ width = "寬度" [app] description = "免費的 Adobe Acrobat 替代方案(下載量 10M+)" +[appBanner] +dismiss = "Dismiss" + [attachments] convertToPdfA3b = "轉換為 PDF/A-3b" convertToPdfA3bDescription = "建立含內嵌附件的封存用 PDF" @@ -1842,6 +2020,15 @@ export = "匯出" accessDenied = "拒絕存取" insufficientPermissions = "您沒有執行此操作的權限。" +[auth.callback] +completing = "正在完成驗證" +invalidToken = "OAuth 登入失敗 - token 無效。" +missingToken = "OAuth 登入失敗 - 未收到 token。" +oauthFailed = "OAuth 登入失敗。請再試一次。" +pleaseWait = "請稍候,我們正在完成登入。" +signedOut = "您已登出。請再次登入。" +windowMayClose = "完成後您可以關閉此視窗。" + [auth.displayName] guest = "訪客" user = "使用者" @@ -1987,6 +2174,73 @@ secureWorkflowDesc = "移除 JavaScript 與內嵌檔案等可能的惡意內容 description = "此工具會根據內容自動重新命名 PDF 檔。它會分析文件,從文字中找出最合適的標題。" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" +[autoRotate] +submit = "自動旋轉" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" + +[autoRotate.confidenceThreshold] +title = "信心閾值" + +[autoRotate.detectionMode] +auto = "自動" +osd = "僅 OCR" +text = "僅文字" +title = "偵測方法" + +[autoRotate.error] +failed = "自動旋轉 PDF 時發生錯誤。" + +[autoRotate.inferUndetected] +title = "旋轉低信心頁面以符合其他頁面" + +[autoRotate.report] +applied = "{{degrees}}° 順時針" +confidenceValue = "信心 {{value}}" +noChange = "無變更" +pageLabel = "第 {{number}} 頁" +summary = "已旋轉 {{rotated}} / {{total}} 頁,{{unchanged}} 頁保持不變" +title = "偵測報告" + +[autoRotate.report.method] +inferred = "已符合" +none = "已略過" +osd = "OCR" +text = "文字" + +[autoRotate.report.note] +belowThreshold = "低於閾值" +blankPage = "頁面空白" +inferredFromDocument = "與其他頁面相同" +noDominantDirection = "文字方向混合" +osdFailed = "找不到可讀文字" +osdNoVerdict = "無法判斷正確方向" +tesseractUnavailable = "未安裝 OCR" +tooFewGlyphs = "文字太少" + +[autoRotate.results] +title = "自動旋轉結果" + +[autoRotate.settings] +title = "設定" + +[autoRotate.tooltip.confidenceThreshold] +text = "頁面被旋轉前,檢查需要達到的確定程度。降低可旋轉更多頁面,提高則會讓更多頁面保持不變。" +title = "信心閾值" + +[autoRotate.tooltip.description] +text = "判斷每一頁應朝上的方向,並將其旋轉至正確方向。已經正向的頁面會保持不變。" + +[autoRotate.tooltip.detectionMode] +text = "自動會先讀取頁面上的文字,若不足再將頁面視為影像檢查。僅文字會略過影像檢查,對於從未掃描的文件最快。僅 OCR 會將每一頁視為影像。" +title = "偵測方法" + +[autoRotate.tooltip.header] +title = "自動旋轉" + +[autoRotate.tooltip.inferUndetected] +text = "空白或幾乎空白的頁面無法單獨檢查。當所有可讀頁面都需要相同旋轉時,這些頁面也會旋轉以符合它們。" +title = "旋轉低信心頁面以符合其他頁面" + [autoSizeSplitPDF] tags = "pdf,分割,文件,組織" @@ -2189,33 +2443,6 @@ keywords = "關鍵字:odd, even" numbers = "數字/範圍:5, 10-20" progressions = "等差序列:3n, 4n+1" -[certificateChoice.tooltip] -header = "憑證類型" - -[certificateChoice.tooltip.organization] -bullet1 = "由系統管理員管理" -bullet2 = "授權的使用者共同使用" -bullet3 = "代表公司身分,而非個人" -bullet4 = "適用於:正式文件、團隊簽署" -description = "由組織提供的共用憑證,用於全公司的簽署授權。" -title = "組織憑證" - -[certificateChoice.tooltip.personal] -bullet1 = "首次使用時自動產生" -bullet2 = "綁定個人帳號" -bullet3 = "無法與其他使用者共用" -bullet4 = "適用於:個人文件、個人負責制" -description = "帳號專屬的自動產生憑證,適用於個人簽署。" -title = "個人憑證" - -[certificateChoice.tooltip.upload] -bullet1 = "需要 P12/PFX 檔案及密碼" -bullet2 = "可由外部憑證授權機構核發" -bullet3 = "法律文件具有更高的信任等級" -bullet4 = "適用於:具法律效力的合約、外部驗證" -description = "使用自己的 PKCS#12 憑證檔案,可完全掌控憑證屬性。" -title = "上傳自訂 P12" - [certSign] awaitingSignatures = "等待簽署中" chooseCertificate = "選擇憑證檔" @@ -2542,34 +2769,6 @@ server = "伺服器" stepTitle = "憑證來源" upload = "上傳" -[certSign.tooltip.header] -title = "關於管理簽章" - -[certSign.tooltip.overview] -bullet1 = "檢查現有簽章及其有效性" -bullet2 = "檢視簽署者與憑證的詳細資訊" -bullet3 = "新增數位簽章以保護您的文件" -bullet4 = "支援多檔,且易於導覽" -text = "此工具可檢查您的 PDF 是否已有數位簽章,並新增數位簽章。數位簽章可證明文件的建立者或核准者,並顯示簽署後是否被變更。" -title = "此工具可以做什麼?" - -[certSign.tooltip.signing] -bullet1 = "支援 PEM、PKCS12、JKS 與伺服器憑證格式" -bullet2 = "可選擇在 PDF 上顯示或隱藏簽章" -bullet3 = "可新增簽署原因、位置與簽署者姓名" -bullet4 = "可選擇放置可見簽章的頁面" -bullet5 = "使用伺服器憑證,享有簡單的「以 Stirling PDF 簽署」選項" -text = "要簽署 PDF,您需要數位憑證(如 PEM、PKCS12 或 JKS)。您可以選擇在文件上顯示簽章或僅加入隱形簽章以提高安全性。" -title = "新增簽章" - -[certSign.tooltip.validation] -bullet1 = "顯示簽章為有效或無效" -bullet2 = "顯示簽署者資訊與簽署日期" -bullet3 = "檢查文件是否在簽署後被修改" -bullet4 = "可使用自訂憑證進行驗證" -text = "當您檢查簽章時,工具會告知是否有效、誰簽署、何時簽署,以及文件在簽署後是否被變更。" -title = "檢查簽章" - [changeCreds] changeUsername = "更新您的使用者名稱。更新後您將被登出。" credsUpdated = "帳號已更新" @@ -2737,12 +2936,16 @@ text = "若要使這些權限無法被變更,請使用「新增密碼」工具 [chat.actions] copy = "複製訊息" +[chat.emptyState] +text = "Ask a question about your documents or get help with PDF tools." + [chat.fab] close = "Close chat" open = "Open Stirling AI assistant" [chat.header] agentMenu = "Stirling 代理選項" +assistant = "AI Assistant" clearChat = "清除聊天" [chat.input] @@ -2805,27 +3008,35 @@ tooltip = "此操作會使用您的雲端點數" tooltip = "從畫面取色" [colorPicker] +hue = "Hue" +saturation = "Saturation and brightness" title = "選擇顏色" [common] +actions = "Actions" back = "返回" cancel = "取消" close = "關閉" +codeSample = "Code sample" collapse = "收合" confirm = "確認" continue = "繼續" copied = "已複製!" copy = "複製" done = "完成" +enterValue = "Enter value" error = "錯誤" expand = "展開" loading = "正在載入..." next = "下一步" +open = "Open" preview = "預覽" previous = "上一步" refresh = "重新整理" +remaining = "剩餘" retry = "重試" save = "儲存" +stepOf = "Step {{current}} of {{total}}" [compare] clearSelected = "清除所選" @@ -2988,6 +3199,7 @@ title = "壓縮方法" [compress.settings] desiredSize = "期望檔案大小" desiredSizePlaceholder = "輸入大小" +desiredSizeUnit = "Size unit" [compress.tooltip.description] text = "壓縮是減少檔案大小的簡單方法。選擇『檔案大小』可輸入目標大小,系統會為您調整品質。選擇『品質』可手動設定壓縮強度。" @@ -3190,6 +3402,24 @@ pdfOptions = "PDF 選項" pdfToCbr = "PDF → CBR" pdfToCbz = "PDF → CBZ" pdfToEpub = "PDF → EPUB" +pdfUaAltTextNotice = "Images need a written description before a document can be certified. Descriptions are never generated automatically, because an invented one passes the checker while telling a screen-reader user nothing. Any image left without one is reported, and the file comes back tagged but not certified." +pdfUaAltTextScanFailed = "The images could not be listed. Convert anyway and the response reports what is missing." +pdfUaAltTextSingleFileOnly = "Descriptions belong to one document: an image is identified by its position, which is a different image in every file. Convert these {{fileCount}} files to tag them, then convert one at a time to describe its images." +pdfUaEmbedFonts = "Embed missing fonts" +pdfUaEmbedFontsHelp = "PDF/UA requires every font to be embedded. Turning this off is faster but usually prevents conformance." +pdfUaFigureLabel = "Page {{page}} {{kind}}" +pdfUaFigurePlaceholder = "What this image tells the reader" +pdfUaFindImages = "Find images needing a description" +pdfUaLanguage = "Document language" +pdfUaLanguageHelp = "A BCP-47 tag such as en-GB. Used only when the document does not already declare its own language." +pdfUaNoImagesNeedingText = "No image is missing a description." +pdfUaOptions = "PDF/UA Options" +pdfUaOverrideLanguage = "Replace the document's own language" +pdfUaOverrideLanguageHelp = "Only tick this if the language above is right and the document's own is wrong. Relabelling a document into a language it is not written in makes a screen reader unintelligible." +pdfUaProfile = "Conformance level" +pdfUaSignatureWarning = "This PDF is digitally signed. Tagging rewrites the page content the signature covers, so the signature will stop verifying. Convert first, then re-sign." +pdfUaTitle = "Document title" +pdfUaTitleHelp = "Shown by a reader instead of the filename. Left blank, the first heading is used." selectSourceFormatFirst = "請先選擇來源格式" settings = "設定" single = "單一" @@ -3264,24 +3494,6 @@ description = "這些 Cookies 對網站正常運作至關重要。它們讓核 posthog = "PostHog 分析" scarf = "Scarf 像素" -[credits.modal] -advancedMonitoring = "進階監控" -allInOneWorkspace = "一體化 PDF 工作空間(檢視器、工具與代理)" -apiSandbox = "API 沙箱" -dedicatedSupportSlas = "專屬支援與 SLA" -fasterThroughput = "10 倍更快的處理速度" -fullyPrivateFiles = "完全私密的檔案" -largeFileProcessing = "大型檔案處理" -orgWideAccess = "組織層級的存取控制" -premiumAiModels = "進階 AI 模型" -prioritySupport = "優先支援" -privateDocCloud = "私人檔案雲" -ragFineTuning = "RAG + 微調" -secureApiAccess = "安全的 API 存取" -standardThroughput = "標準吞吐量" -unlimitedApiAccess = "無限 API 存取" -unlimitedSeats = "不限人數" - [crop] autoCrop = "自動裁切空白" noFileSelected = "選取一個 PDF 檔以開始裁切" @@ -3346,6 +3558,7 @@ message = "設定預設 PDF 處理程式失敗" title = "錯誤" [defaultApp.prompt] +dontRemind = "不要再提醒我" message = "將 Stirling PDF 設為開啟 PDF 檔的預設應用程式。" [defaultApp.settingsOpened] @@ -3385,7 +3598,11 @@ tags = "automation,folder,scanning,watch folder,hot folder,automatic processing, tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP" [dropdownList] +noItems = "No items available" +noResults = "No results found" searchPlaceholder = "搜尋..." +selectLanguages = "Select languages" +selectOption = "Select an option" [editableSecretField] edit = "編輯" @@ -3644,11 +3861,13 @@ shareSelected = "共用已選取項" sharing = "共用" showAll = "顯示全部" showHistory = "顯示歷史" +sortBy = "Sort files" sortByDate = "依日期排序" sortByName = "依名稱排序" sortBySize = "依大小排序" storage = "儲存空間" storageState = "儲存狀態" +storageUsed = "Storage used" synced = "已同步" title = "上傳 PDF 檔案" toolChain = "已套用的工具" @@ -3694,13 +3913,17 @@ addFiles = "新增檔案" addingFiles = "正在新增檔案…" collapse = "收合側邊欄" customizeGroups = "自訂群組" +dataLostBody = "This browser lost this file's contents. Upload it again to keep working with it." +dataLostTitle = "File data is unavailable" +downloadFailed = "Download failed" dropHint = "開啟檔案以開始使用" dropToAdd = "Drop files to add" +duplicateFailed = "Could not duplicate file" expand = "展開側邊欄" -files = "檔案" googleDrive = "Google Drive" googleDriveDisabled = "尚未設定 Google Drive" leaveMyFiles = "離開我的檔案" +library = "PDF Library" myFiles = "我的檔案" noFiles = "尚無檔案" openFileManager = "開啟檔案管理器" @@ -3708,37 +3931,36 @@ openFromComputer = "從電腦開啟" openSettings = "開啟設定" other = "其他" recent = "最近" -search = "搜尋" -searchPlaceholder = "搜尋檔案…" viewAll = "檢視全部 {{count}} 個檔案" [fileSidebar.fileItem] closeViewer = "關閉檢視器" +dataLost = "Data lost" +dataLostTooltip = "This browser lost this file's contents. Upload it again to keep working with it." delete = "Delete" +duplicate = "Duplicate" moreActions = "More actions" openInViewer = "在檢視器中開啟" +rename = "Rename" savedToServer = "Saved to server" updateOnServer = "Update on server" uploadToServer = "Upload to server" versionHistory = "Version history" [fileSidebar.groupsModal] -add = "新增" -addLabel = "新增標籤…" -categoryIconAria = "為 {{name}} 選擇圖示" -createCategory = "建立類別" -delete = "刪除類別" done = "完成" -hide = "隱藏類別" -newCategory = "新類別名稱…" -removeLabel = "移除 {{name}}" -rename = "重新命名" reset = "重設為預設值" -search = "搜尋標籤…" -show = "顯示類別" subtitle = "將您的檔案分組到父類別中。將現有或新的標籤新增至類別、重新命名,或建立您自己的類別。不屬於任何類別的檔案會顯示在「其他」下。" title = "側邊欄類別" +[fileSidebar.rename] +cancel = "Cancel" +error = "Could not rename the file." +illegalCharacters = "A file name can't contain \\ / : * ? \" < > |" +label = "File name" +save = "Rename" +title = "Rename file" + [filesPage] addToWorkspace = "加入至工作區" addToWorkspaceCount = "將 {{count}} 個加入工作區" @@ -3749,7 +3971,6 @@ backToMyFiles = "返回我的檔案" breadcrumbs = "資料夾路徑" cancel = "取消" classification = "分類" -clearSearch = "清除搜尋" clearSelection = "清除選取" closeDetails = "關閉詳細資料" create = "建立" @@ -3781,6 +4002,7 @@ downloadAll = "全部下載" downloadVersion = "下載此版本" dropOverlay = "拖放檔案以上傳" dropOverlaySub = "檔案會先新增至本機。使用『移至』或『儲存到雲端』將它們整理到資料夾。" +duplicate = "Duplicate" file = "檔案" fileInfo = "File info" fileMenu = "檔案動作" @@ -3808,13 +4030,12 @@ refresh = "從伺服器重新整理" remove = "刪除" removeVersion = "移除此版本" rename = "重新命名" +renamed = "已重新命名" renameFolder = "重新命名資料夾" resizeFolderTree = "調整資料夾樹狀結構大小(方向鍵,按住 Shift 以加大步進;按兩下自動適配)" save = "儲存" saveToServer = "儲存到伺服器" saveToServerDisabledHint = "此伺服器未啟用儲存到伺服器。請聯絡管理員啟用。" -search = "搜尋" -searchPlaceholder = "搜尋此資料夾與子資料夾" selectAll = "全選" selectAllHint = "按一下以全選。提示:按住 Ctrl(或 Cmd)可逐一新增檔案,按住 Shift 可選取範圍。" selectedCount = "已選取 {{count}} 個" @@ -3874,6 +4095,10 @@ title = "尚無雲端檔案" hint = "未上傳而儲存的檔案會留在此處。拖放檔案以新增。" title = "沒有僅存在本機的檔案" +[filesPage.empty.noResults] +hint = "No files in this folder match your filter. Try a different term or clear the filter." +title = "No matching files" + [filesPage.empty.recent] hint = "您開啟或編輯的檔案會出現在這裡。" title = "尚未有變更" @@ -3893,6 +4118,8 @@ cloudDeleteFailed_one = "Couldn't delete 1 file from the cloud." cloudDeleteFailed_other = "Couldn't delete {{count}} files from the cloud." deleteFolderFailed = "無法刪除資料夾。" deleteFolderFailedDetail = "無法刪除資料夾:{{message}}" +downloadFailed = "Could not download the file." +duplicateFailed = "Could not duplicate the file." folderAppearanceFailed = "無法更新資料夾外觀。" folderAppearanceFailedDetail = "無法更新資料夾外觀:{{message}}" moveFilesFailed = "無法移動檔案。" @@ -3910,9 +4137,7 @@ count = "檔案數" folder = "資料夾" labels = "標籤" modified = "修改時間" -name = "名稱" size = "大小" -toolHistoryAtVersion = "累積工具鏈" totalSize = "總大小" type = "類型" versionHistory = "版本歷程" @@ -3945,6 +4170,11 @@ localHint = "僅儲存在此瀏覽器" shared = "已分享" sharedHint = "透過連結與您分享" +[filesPage.search] +clear = "Clear filter" +label = "Filter files by name" +placeholder = "Filter files…" + [filesPage.sort] modifiedAsc = "由舊到新" modifiedDesc = "最近優先" @@ -4000,6 +4230,7 @@ noFilesInStorageOpen = "儲存空間中沒有可用檔案。請先開啟一些 open = "開啟" openFile = "開啟檔案" openFiles = "開啟多個檔案" +selectFile = "Select {{name}}" selectFromStorage = "從儲存空間選取" upload = "上傳" uploadFile = "上傳檔案" @@ -4072,10 +4303,22 @@ discord = "Discord" issues = "GitHub" [formFill] +allSaved = "全部已儲存" +analyzingFields = "Analysing form fields..." +extractCsvError = "擷取 CSV 失敗" +extractXlsxError = "擷取 XLSX 失敗" +filled = "filled" flattenAfterFilling = "填寫後扁平化" +noFields = "No fillable form fields found in this PDF." +placeholderEnter = "Enter" +placeholderSelect = "Select" +requiredAbbreviation = "必填" +requiredFieldsError = "請填寫所有必填欄位" rescanFields = "重新掃描欄位" rescanFormFields = "重新掃描表單欄位" save = "儲存" +saveShortcut = "Ctrl+S 儲存" +unsavedChanges = "未儲存的變更" [formFill.sidebar] close = "關閉側邊欄" @@ -4309,6 +4552,11 @@ desc = "依偵測到的標頭自動重新命名 PDF 檔案" tags = "自動偵測,依標頭,整理,重新命名,auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "自動重新命名 PDF 檔案" +[home.autoRotate] +desc = "偵測每一頁的方向並自動旋轉為正向。" +tags = "auto rotate,fix orientation,upside down,sideways,straighten,orient,detect orientation,correct rotation,auto orient,scanned,OSD" +title = "自動旋轉" + [home.autoSizeSplitPDF] tags = "自動,分割,大小,auto,split,size" title = "根據大小/數量自動分割" @@ -4608,15 +4856,12 @@ hint = "上傳 PNG、JPG、SVG 或其他影像檔以放置在 PDF 上。為了 label = "上傳影像" placeholder = "選取影像檔" -[imageToPdf] -tags = "轉換,img,jpg,圖片,照片" - [imageToPDF] header = "圖片轉 PDF" title = "圖片轉 PDF" -[infoBanner] -dismiss = "關閉" +[imageToPdf] +tags = "轉換,img,jpg,圖片,照片" [invite] acceptError = "建立帳號失敗" @@ -4647,11 +4892,9 @@ welcomeTitle = "您已獲邀!" [landing] addFiles = "新增檔案" -heroSubtitle = "拖放或新增現有的 PDF 即可開始。" mobileUpload = "從行動裝置上傳" openFromComputer = "從電腦開啟" uploadFromComputer = "從電腦上傳" -workbenchEmptyStateHero = "將 PDF 拖放到任何位置" [language] direction = "ltr" @@ -4691,7 +4934,6 @@ enterUsername = "輸入使用者名稱" failedToSignIn = "使用 {{provider}} 登入失敗:{{message}}" forgotPassword = "忘記密碼?" loggingIn = "正在登入..." -logIn = "登入" login = "登入" magicLinkSent = "魔術連結已寄至 {{email}}!請查看您的電子郵件並點選連結登入。" mfaCode = "驗證碼" @@ -4715,26 +4957,10 @@ signInWith = "使用以下方式登入" title = "登入" unexpectedError = "未預期的錯誤:{{message}}" updatePassword = "更新密碼" -useEmailInstead = "使用電子郵件登入" useMagicLink = "改用魔術連結" username = "使用者名稱" youAreLoggedIn = "您已登入!" -[login.slides.edit] -alt = "編輯 PDF" -subtitle = "提供十多種工具,協助您遮蔽、簽署、閱讀與處理 PDF,滿足您的各種需求。" -title = "編輯 PDF 以顯示/保護您想要的資訊" - -[login.slides.overview] -alt = "Stirling PDF 概覽" -subtitle = "以隱私為先的 PDF 雲端套件,讓您轉換、簽署、遮蔽與管理文件,並提供 50+ 種強大工具。" -title = "滿足您所有 PDF 需求的一站式平台。" - -[login.slides.secure] -alt = "保護 PDF" -subtitle = "輕鬆新增密碼、遮蔽內容並管理憑證。" -title = "保護 PDF 中的敏感資訊" - [margin] large = "大" medium = "中" @@ -4818,6 +5044,34 @@ uploadSuccess = "上傳成功!" uploadSuccessMessage = "您的影像已傳送完成。" validating = "正在驗證工作階段…" +[mobileSign] +clear = "Clear" +invalidSession = "Session expired" +invalidSessionMessage = "This QR code is no longer valid. Open the Sign tool on your computer and scan the new code." +penSizeLabel = "Pen size" +send = "Send to computer" +sendAnother = "Send another signature" +sendError = "Could not send the signature. Check the connection and try again." +sentMessage = "Signature sent to your computer. You can send another or close this page." +tabsLabel = "Signature source" +undo = "Undo" +validating = "Checking session…" + +[mobileSign.photo] +fromGallery = "From gallery" +hint = "Photograph a signature on white paper, or choose an existing image." +invalidType = "Please choose an image file." +takePhoto = "Take a photo" + +[mobileSign.tab] +draw = "Draw" +photo = "Photo" +type = "Type" + +[mobileSign.type] +placeholder = "Your name" +previewPlaceholder = "Signature preview" + [mobileUpload] description = "掃描即可上傳相片。影像會自動轉換為 PDF。" descriptionNoConvert = "掃描即可從行動裝置上傳相片。" @@ -4836,8 +5090,13 @@ title = "從行動裝置上傳" tags = "複合工具,多功能,UI,點選拖曳,前端,客戶端,互動,互動式,移動" title = "PDF 複合工具" -[navbar] -search = "搜尋" +[navFooter] +openEditor = "Open PDF Editor" +openProcessor = "Open PDF Processor" + +[navFooter.credits] +count = "{{remaining}} of {{total}}" +label = "Free credits" [oauth.error] message = "驗證未成功。您可以關閉此視窗後再試一次。" @@ -4966,6 +5225,7 @@ activeFiles = "使用中檔案 檢視會顯示您載入工具 allTools = "This is the All Tools panel, where you can browse and select from all available PDF tools." close = "關閉" cropSettings = "選好要裁切的檔案後,我們可以設定裁切工具,選擇要裁切的區域。" +dialogLabel = "Onboarding" fileCheckbox = "點選其中一個檔案即可將其選取以進行處理。您可以選取多個檔案以批次操作。" fileReplacement = "修改後的檔案會自動在工作區取代原檔,讓您能輕鬆再用其他工具處理。" filesButton = "快速存取列上的 檔案 按鈕可讓您上傳 PDF 以使用各種工具。" @@ -4976,6 +5236,7 @@ pinButton = "如果您希望檔案在執行工具後保持使用中狀態,可 results = "工具執行完畢後,檢閱 步驟會在此面板顯示結果預覽,並允許您復原操作或下載檔案。 " runButton = "設定完成後,使用此按鈕即可在所有已選取的 PDF 上執行該工具。" selectCropTool = "我們先選擇 裁切 工具,示範如何使用其中一個工具。" +stepOf = "步驟 {{current}} / {{total}}" toolInterface = "這是 裁切 工具介面。如您所見,因為尚未加入任何 PDF 檔可供操作,因此內容不多。" workbench = "這裡是 工作區——檢視與編輯 PDF 的主要區域。" wrapUp = "大功告成!您已了解此應用程式的主要區域與使用方式。隨時點選 說明 按鈕可再度觀看本導覽。" @@ -4988,6 +5249,26 @@ showMeAround = "帶我導覽" skipForNow = "暫時略過" skipTheTour = "略過導覽" +[onboarding.checklist] +dismiss = "關閉" +title = "設定 Stirling PDF" + +[onboarding.checklist.downloadDesktop] +description = "在您的電腦上原生執行 Stirling" +title = "下載 Stirling Desktop" + +[onboarding.checklist.inviteTeam] +description = "與您的團隊協作" +title = "邀請團隊成員" + +[onboarding.checklist.shareAnalytics] +description = "協助改善 Stirling" +title = "分享匿名使用資料" + +[onboarding.checklist.takeTour] +description = "透過快速導覽了解 Stirling 的運作方式" +title = "開始導覽" + [onboarding.desktopInstall] body = "Stirling 最適合以桌面應用程式使用。您可以離線操作、更快存取文件,並在電腦本機進行編輯。" selectOs = "選擇作業系統" @@ -4996,7 +5277,21 @@ titleWithOs = "下載適用於 {{osLabel}} 的版本" [onboarding.mfa] authenticationCode = "驗證碼" +description = "透過連結驗證器應用程式保護您的帳號。掃描 QR code 或輸入設定 key,然後確認 6 位數代碼以完成。" +enable = "啟用 MFA" +enableError = "無法啟用雙因素驗證。請檢查代碼後再試一次。" +enterCode = "輸入您應用程式中的 6 位數代碼。" +enterCodeError = "請輸入驗證代碼以繼續。" +logout = "登出" +openAuthenticator = "開啟 Google Authenticator、Authy 或 1Password。" qrCodeLoading = "正在產生您的 QR 碼…" +regenerateQrCode = "重新產生 QR code" +scanQrCode = "掃描 QR code 或輸入下方設定 key。" +setupError = "無法開始雙因素設定。請再試一次。" +setupKey = "設定 key(手動輸入)" +stepByStep = "逐步說明" +success = "MFA 已啟用。您現在可以繼續。" +title = "多因素驗證設定" [onboarding.planOverview] adminBodyLoginDisabled = "啟用登入模式後,您可以管理使用者、設定參數並監控伺服器健康狀態。伺服器上的前 {{freeTierLimit}} 人可免費使用 Stirling。" @@ -5005,6 +5300,11 @@ adminTitle = "管理員總覽" userBody = "邀請隊友、指派角色,並在單一安全的工作區中整理您的文件。當您準備好超越個人使用時,可隨時啟用登入模式。" userTitle = "方案總覽" +[onboarding.processorIntro] +body = "Stirling 現在會執行 Policies — 自動化規則會在每份文件抵達時進行分類、保護與處理。請在 Processor 中設定並監控執行狀態。" +cta = "查看 Processor" +title = "查看 Stirling Processor" + [onboarding.saas.freeEditor] freeLine = "The editor is now completely free." premium = "We've added loads of new features, including Policies and Agent Chat." @@ -5362,6 +5662,7 @@ freeBody = "View, edit, merge, split, sign, watermark, compress, convert and man freeTitle = "Unlimited PDF editing" [payg.free.hero] +barAria = "Free PDFs remaining" capSuffix = "/ {{limit}} free PDFs" metaCategories = "Automation · AI · API requests" @@ -5401,6 +5702,28 @@ docs = "PDFs" subtitle = "Billable PDFs each teammate has processed this period." title = "Team member usage" +[payg.prepaid] +chip = "預付年費" + +[payg.prepaid.banner] +exhausted = "您的預付容量已用完;計量計費已恢復。加值以保留折扣。" +expiring_one = "您的預付年度將在 {{count}} 天後結束。未使用容量將到期 — 加值以開始新的一年。" +expiring_other = "您的預付年度將在 {{count}} 天後結束。未使用容量將到期 — 加值以開始新的一年。" +low = "您的預付容量只剩 {{pct}}%。隨時加值以保持處理不中斷。" + +[payg.prepaid.card] +subtitle = "在計量計費之前使用,且不受您的支出上限限制。" +title = "預付容量" + +[payg.prepaid.meter] +capSuffix = "共 {{total}} 預付 credits" +expires = "{{date}} 到期" + +[payg.prepaid.state] +exhausted = "已用完" +healthy = "剩餘充足" +low = "即將用完" + [payg.role] leader = "Team owner" member = "Member" @@ -5419,6 +5742,7 @@ automation = "automations" default = "this feature" [payg.spendCapMeter] +barAria = "Spend against cap" capSuffix = "/ {{amount}} cap" metaCategories = "Automation · AI · API spend" resets = "Resets each billing period" @@ -5747,6 +6071,11 @@ header = "PDF 轉 PDF/A" tags = "存檔,長期,標準,轉換,儲存,儲存" title = "PDF 轉 PDF/A" +[pdfToPDFUA] +header = "PDF To PDF/UA" +tags = "accessibility,accessible,tagged,screen reader,wcag,eaa,section 508,conversion" +title = "PDF To PDF/UA" + [pdfToPDFX] tags = "列印,標準,轉換,製作,印前,封存" title = "PDF 轉為 PDF/X" @@ -5822,6 +6151,7 @@ small = "500 點" xsmall = "100 點" [plan.availablePlans] +currency = "Billing currency" subtitle = "選擇符合您需求的方案" title = "可用方案" @@ -5937,49 +6267,12 @@ keyDescription = "貼上電子郵件中的授權金鑰" success = "授權已啟用!" successMessage = "您的授權已成功啟用。您現在可以關閉此視窗。" -[policies] -deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected." -deleteConfirmTitle = "Delete {{label}} policy?" - [policies.activity] -enforced = "已強制執行" -enforcing = "正在強制執行..." -failed = "強制執行失敗" outputsUnavailable = "政策輸出結果已無法下載。" partialOutputsUnavailable = "部分政策輸出結果已無法下載。" -retrying = "忙碌中,正在重試..." runNotFound = "找不到此強制執行作業。" -step = "步驟 {{current}}/{{total}}" timedOut = "強制執行在作業完成前逾時。" -[policies.catalog] -compliance = "Compliance" -ingestion = "Ingestion" -retention = "Retention" -routing = "Routing" -security = "Security" - -[policies.detail] -close = "關閉" -editSettings = "Edit Settings" -enforces = "Enforces" -managedByOrg = "Managed by your organisation. Contact a team leader to change this policy." -noActivityDescription = "Documents will appear here once this policy runs." -noActivityTitle = "No activity yet" -onEveryUpload = "On every upload" -originalsNote = "Originals stay untouched • Enforced version saved alongside" -pause = "Pause" -recentActivity = "Recent Activity" -resume = "Resume" -retry = "Retry" -showLess = "Show less" -showMore = "Show more" -statActive = "Active" -statDataProcessed = "Data processed" -statDocsEnforced = "Docs enforced" -statusActive = "Active" -statusPaused = "Paused" - [policies.enforcement] applying = "正在套用 {{names}}" applyingProgress = "正在套用 {{names}}({{done}} / {{total}})" @@ -5989,61 +6282,16 @@ failureBody = "{{total}} 個檔案中有 {{failures}} 個無法處理,已照 failureTitle = "未完全強制執行即匯出" printPolicyAppliedBody = "已更新此 PDF 以符合政策。請檢視變更後再重新列印。" printPolicyAppliedTitle = "列印前已套用政策" -queued = "+{{count}} 已佇列" successTitle = "{{names}} 已套用" summaryMore = "{{first}}、{{second}},以及另外 {{more}} 個" summaryTwo = "{{first}} 與 {{second}}" -[policies.enforcement.triggerVerb] -convert = "轉換前強制執行" -default = "正在強制執行" -export = "匯出前強制執行" -input = "匯入時強制執行" -print = "列印前強制執行" - -[policies.field] -accessLog = "存取記錄" -archiveAfter = "之後封存" -auditTrail = "稽核軌跡" -belowThreshold = "低於門檻" -destination = "目的地" -frameworks = "架構" -immutableHold = "不可變更保留" -keepFor = "保留期間" -minConfidence = "最低信心度" -notify = "路由時通知" -onViolation = "不符合規範時" -webhookUrl = "Webhook URL" - -[policies.fields] -selectedCount = "{{count}} selected" - [policies.labels] -add = "新增" -addPlaceholder = "新增標籤…" -customNote = "已為您的團隊自訂。" -defaultNote = "使用內建預設值,與您的團隊共用。" -duplicate = "\"{{name}}\" 已存在。" -edit = "編輯標籤" -empty = "尚無標籤。" -export = "匯出 JSON" -iconAria = "為 {{name}} 選擇圖示" -import = "匯入 JSON" -importError = "無法匯入該檔案。" -managedNote = "團隊標籤由您的團隊負責人管理。" -modalSubtitle = "與整個團隊共用。分類器會挑選適合每份文件的標籤。" -modalTitle = "分類標籤" -pickIcon = "使用 {{label}} 圖示" -removeAria = "移除 {{name}}" -resetToDefault = "重設為預設值" -saveForTeam = "為團隊儲存" -saving = "正在儲存…" -sectionLabel = "分類標籤" -startFromScratch = "從頭開始" -teamCount = "團隊標籤" -tooLong = "標籤最多可為 {{max}} 個字元。" -ungrouped = "未分組" -view = "檢視標籤" +categoryCount = "類別" +hideCategory = "隱藏類別" +labelCount = "標籤" +sharedNote = "這些標籤為內建,並在整個團隊中共用。" +showCategory = "顯示類別" [policies.pii] account = "帳戶號碼(具標籤)" @@ -6056,93 +6304,10 @@ placeholder = "Select PII types" routing = "美國銀行路由號碼(ABA)" ssn = "社會安全號碼" -[policies.settings] -noneExport = "目前匯出時沒有執行任何政策。" -noneUpload = "目前上傳時沒有執行任何政策。" -onExport = "匯出時" -onUpload = "上傳時" -reorderHandle = "拖曳以重新排序" -runOrderDesc = "當多個政策在同一觸發條件下執行時,它們會依此順序執行 — 每個政策都會使用前一個政策的輸出。拖曳以重新排序。" -title = "政策設定" - -[policies.sidebar] -activeCount = "{{count}} active" -infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps." -loading = "Loading…" -optionsAriaLabel = "政策選項" -policySettings = "政策設定" -railAriaLabel = "{{label}} policy — {{status}}" -railSuffixActive = " (Active)" -railSuffixPaused = " (Paused)" -retryFailed = "重試失敗的政策({{count}})" -rowProgress = "已處理 {{completed}} / {{total}} 個檔案" -setUp = "Set up" -title = "Policies" -upgradeToEnterprise = "Upgrade to enterprise" -whatIsPolicy = "什麼是政策?" - -[policies.status] -active = "Active" -paused = "Paused" -setup = "Set up" - -[policies.toolConfig] -enableAriaLabel = "Enable {{tool}}" -infoAriaLabel = "What does {{tool}} do?" - -[policies.toolConfig.info] -redact = "自動找出並塗黑敏感資訊(如社會安全號碼與卡號),使其在文件中無法讀取。" -sanitize = "移除檔案中的隱藏 JavaScript,以避免在開啟時自動執行任何內容。" -watermark = "在每一頁加蓋可見標記(例如 \"Confidential\")。" - -[policies.wizard] -allDocTypesDescription = "Enable the Classification policy to filter by document type." -allDocTypesTitle = "All document types" -back = "Back" -builderDesc = "Build the sequence of tools this policy runs on each document." -clear = "Clear" -close = "關閉" -continue = "Continue" -docTypesLabel = "Document types" -edit = "Edit" -editTitle = "Edit {{label}} Policy" -enablePolicy = "Enable Policy" -filenameAutoNumber = "Auto-number" -filenamePositionAria = "Filename position" -filenamePrefix = "Prefix" -filenameSuffix = "Suffix" -filenameTextAria = "Filename text" -filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact a team leader to change this policy." -lockedTitle = "Managed by your organisation" -maxRetriesLabel = "Max retries" -noToolsError = "Add at least one configured tool to the workflow first." -outputAsLabel = "Output as" -outputFilenameSubhead = "Output filename" -outputModeAria = "Output mode" -outputNewFile = "New file" -outputNewVersion = "New version" -outputRetriesLabel = "Output & retries" -outputSubhead = "Output" -retryDelayAria = "Retry delay minutes" -retryDelayLabel = "Retry delay (min)" -runOnExport = "Export" -runOnLabel = "Run on" -runOnSubhead = "Run on" -runOnUpload = "Upload" -saveChanges = "Save Changes" -saveError = "Couldn't save the policy. Please try again." -setupClassification = "Set up Classification" -setupTitle = "Set up {{label}} Policy" -sourcesDesc = "Choose where this policy runs and which document types it applies to." -sourcesLabel = "Sources" -stepOf = "Step {{step}} of {{total}}" -toolChainDesc = "Configure the tools this policy runs on each document." -typesSelected = "{{count}} types selected" - [policy] badgeEnforcing = "{{name}} 正在強制執行..." badgeRan = "{{name}} 政策已在此檔案上執行" +badgeRunning = "{{name}} 執行中..." blockingAction = "強制執行政策時已封鎖 {{action}},請稍候..." dismiss = "關閉覆蓋層" enforcingTitle = "正在強制執行政策..." @@ -6151,6 +6316,7 @@ viewAnyway = "檢視檔案(政策仍在強制執行)" [policyOption] autoRedactPhi = "自動遮蔽 PHI" blockExport = "封鎖匯出" +ccpa = "CCPA" contracts = "合約" documents = "文件" fedramp = "FedRAMP" @@ -6269,86 +6435,6 @@ free = "Editor 方案" subscribed = "Processor 方案" unlinked = "未連結" -[portal.agentBuilder] -bootstrapFromDocument = "從文件啟動" -sectionsAriaLabel = "代理程式建構器區段" -selectorAriaLabel = "代理程式" -subtitle = "設計、測試並發布可分類、擷取及路由文件的 AI 代理程式。定義情境、限制工具存取、執行 golden set,並發布版本。" -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "取消" -dropzoneText = "選擇範例文件(PDF 或影像)" -lead = "拖放範例文件,我們會提出情境和擷取 schema 供您調整。在您審查之前不會發布任何內容。" -submit = "啟動代理程式" -subtitle = "以一個代表性檔案建立新代理程式的初始資料" -title = "從文件啟動" - -[portal.agentBuilder.empty] -description = "從範例文件啟動代理程式,以建立其情境和擷取 schema 的初始資料,然後調整並發布。" -title = "尚無代理程式" - -[portal.agentBuilder.evals] -casesPassing = "通過案例" -columnCase = "Eval 案例" -columnLatency = "延遲" -columnResult = "結果" -fail = "失敗" -goldenSetPassRate = "Golden-set 通過率" -latencyMs = "{{ms}} ms" -notRun = "未執行" -pass = "通過" -passRate = "通過率" -runEvals = "執行 evals" - -[portal.agentBuilder.evals.empty] -description = "Evals 會將您的情境轉換為可重複的 golden set。升級即可追蹤一段時間內的通過率,並以此作為發布門檻。" -title = "尚無 golden set" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "跨 golden sets" -activeAgents = "啟用中的代理程式" -avgPassRate = "平均 eval 通過率" -fleetWide = "全系統" -latestPublished = "最新發布" -scenarios = "情境" -testCases = "測試案例" -totalDescription_one = "共 {{count}} 個" -totalDescription_other = "共 {{count}} 個" - -[portal.agentBuilder.scenarios] -add = "新增" -addScenario = "新增情境" -enable = "啟用" -expectationLabel = "預期行為" -expectationPlaceholder = "代理程式應該做什麼" -inEval = "在 eval 中" -mute = "靜音" -muted = "已靜音" -nameLabel = "名稱" -namePlaceholder = "例如:合規升級" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "情境" -tools = "工具" -versions = "版本" - -[portal.agentBuilder.tools] -broadAccess = "廣泛存取" -deniedHint = "選取的工具已被封鎖。其他所有工具仍可呼叫。" -deniedTools = "拒絕的工具" -governanceGate = "工具治理可在 Enterprise 方案使用。" -restricted = "受限制" -restrictedAccess = "受限制的工具存取" -restrictedDescription = "允許所有工具,除了您在下方拒絕的工具。" - -[portal.agentBuilder.versions] -current = "目前" -historyGate = "完整版本歷史記錄與復原可在 Enterprise 方案使用。" -publish = "發布" -rollBack = "復原" - [portal.assistant] close = "關閉助理" error = "無法連線到助理。" @@ -6365,9 +6451,21 @@ typing = "正在輸入" loading = "載入中" redirectingToEditor = "正在重新導向至編輯器..." +[portal.billing.activation] +maybeLater = "稍後再說" +title = "啟用 Processor" + +[portal.billing.activation.payg] +sub = "設定每月上限。依使用量付費。" +title = "隨用隨付" + +[portal.billing.activation.prepay] +badge = "免費 2 個月" +sub = "一張發票,以卡片或銀行轉帳付款。" +title = "預付一年" + [portal.billing.checkout] noClientSecret = "Edge function 未傳回 client_secret。" -subtitle = "新增卡片以在免費 Editor 方案額度用完後繼續使用。其餘由 Stripe 處理。" title = "啟用 Processor 方案" [portal.billing.checkout.activationSlow] @@ -6375,6 +6473,22 @@ body = "您的付款已成功,但啟用花費的時間比平常稍長。它會 close = "關閉" title = "快完成了" +[portal.billing.checkout.cap] +amountAria = "每月支出上限" +back = "返回" +continue = "繼續付款" +error = "無法設定您的支出上限" +finePrint = "發票每月開立。可隨時取消。" +noLimit = "無上限" +note = "您的帳單絕不會超過上限。處理只會暫停。" +perMonth = "/ 月" +title = "設定您的支出上限" + +[portal.billing.checkout.card] +fields = "卡號 · MM / YY · CVC · ZIP" +label = "卡片詳細資料" +note = "卡片詳細資料由 Stripe 收集。Stirling 絕不儲存 PAN 或 CVC。" + [portal.billing.checkout.error] title = "無法開始結帳" @@ -6383,10 +6497,9 @@ body = "您的付款已完成。我們正在您的已連結執行個體上啟用 hint = "請保持此視窗開啟。" title = "正在啟用您的 Processor 方案..." -[portal.billing.checkout.notConfigured] -bodyAfter = "於入口網站 env 中,以啟用應用程式內結帳。" -bodyBefore = "設定" -title = "尚未設定 Stripe" +[portal.billing.checkout.pay] +limit = "每月支出上限 {{amount}}/月。達到上限時處理會暫停。" +limitNoCap = "無支出上限。處理不會暫停。發票每月開立。" [portal.billing.enterpriseUpsell] cta = "建立您的 Enterprise 報價" @@ -6409,10 +6522,12 @@ currentPlan = "目前方案" freeForever = "永久免費" noTeamResolved = "您的錢包尚未解析到團隊 — 請重新整理後再試。" ownerOnly = "只有團隊擁有者可以啟用 Processor 方案。" +payInvoice = "支付發票以完成" planName = "Editor" ssoIncluded = "包含 SSO" switchOnProcessor = "啟用 Processor →" unlimitedUsers = "不限使用者數" +viewQuote = "檢視報價" [portal.billing.invoices] columnAmount = "金額" @@ -6465,7 +6580,111 @@ segmentApiDesc = "直接 API 請求" segmentApiLabel = "API" segmentAutomationDesc = "自動化與管線" segmentAutomationLabel = "自動化" +sizeMultiplier = "{{formatted}} PDFs 使用了大小乘數" +summary = "{{unique}} 個不重複 · {{units}} 個計量單位 · 每個 PDF 平均 {{avg}}" +summaryNoRate = "{{unique}} 個不重複 · {{units}} 個計量單位" unit = "計量 PDFs" +unitsPending_one = "{{units}} 個計量單位等待從連結的執行個體同步" +unitsPending_other = "{{units}} 個計量單位等待從連結的執行個體同步" + +[portal.billing.prepaid] +capSuffix = "共 {{total}} 預付 credits" +expires = "{{date}} 到期" +eyebrow = "預付容量" +note = "在計量計費之前使用,且不受您的支出上限限制。" +topUp = "加值" + +[portal.billing.prepaid.buy] +back = "返回" +calcTitle = "計算您的年度付款" +cancel = "取消" +cancelPurchase = "取消購買" +close = "關閉" +continueToPayment = "繼續付款" +finalise = "完成" +invoiceErrorTitle = "無法產生發票" +loadingTitle = "正在載入您的報價" +payTitle = "支付您的年度費用" +step = "步驟 {{current}} / {{total}}" + +[portal.billing.prepaid.calc] +change = "變更" +deployRow = "部署" +done = "完成" +enterpriseHint = "這是企業級規模。在此用量下,企業費率優於任何自助折扣。費率鎖定、條款與報價可在數分鐘內完成。" +governanceRow = "治理" +governanceValue = "{{label}} · {{count}} 個 policies" +handlesLabel = "您的 Processor" +handlesValue = "可處理 {{volume}} credits / 月" +pipelinesLabel = "Pipelines" +policiesMeta = "{{count}} 個 policies" +poolCaption = "年度共用 {{credits}} credits。用量高的月份可借用用量低月份的額度。" +priceLabel = "您的年度 · 12 個月只付 10 個月價格" +rateUnknown = "我們會在結帳時顯示確切價格。" +savings = "您節省 {{amount}} · 免費 2 個月。" +sizingRow = "規模" +sizingValue = "{{label}} · {{desc}}" +usersLabel = "使用者總數" + +[portal.billing.prepaid.consent] +label = "我了解當我的預付容量用完或 12 個月後到期時,除非我取消,處理會自動以標準計量隨用隨付費率繼續(最高至我的支出上限),且我可以隨時從帳單入口網站取消。" + +[portal.billing.prepaid.deploy] +cloud = "Stirling Cloud" +cloudDesc = "由 Stirling 管理。數分鐘內上線。" +cloudValue = "Stirling Cloud · 代管" +selfhost = "自架" +selfhostDesc = "在私人基礎架構上執行。相同費率。" +selfhostValue = "自架 · 私人基礎架構" + +[portal.billing.prepaid.offer] +cta = "檢視優惠" +subtitle = "預付一年 PDF 處理費用並獲得兩個月免費,在計量計費之前使用,且不受您的支出上限限制。" +title = "以 10 個月價格取得 12 個月" + +[portal.billing.prepaid.pay] +accountName = "帳戶持有人姓名" +accountNamePlaceholder = "例如 Jane Smith" +companyName = "公司名稱(選填)" +companyNamePlaceholder = "例如 Acme Inc." +downloadInvoice = "下載發票" +payOnline = "線上付款" +poNumber = "PO 編號(選填)" +poNumberPlaceholder = "例如 PO-2026-0142" + +[portal.billing.prepaid.pipelines] +advanced = "進階" +advancedDesc = "Pipelines 會驅動您大部分的處理。" +none = "無" +noneDesc = "尚未執行 pipelines。您可以隨時啟用。" +standard = "標準" +standardDesc = "少數 pipelines 會重新處理抵達的 PDFs。" + +[portal.billing.prepaid.posture] +essentials = "基本" +essentialsDesc = "分類、預設項目,加上分享" +governed = "受治理" +governedDesc = "新增安全性與路由" +regulated = "受規範" +regulatedDesc = "每個類別,包含合規、保留、擷取" + +[portal.billing.prepaid.review] +download = "下載報價(PDF)" +downloading = "準備中…" +share = "分享以供核准" + +[portal.billing.prepaid.size] +compact = "精簡" +compactDesc = "檔案小於 25 MB,無資料費用" +heavy = "大量" +heavyDesc = "掃描或影像較多,通常 50 MB+" +standard = "標準" +standardDesc = "多為小檔案,部分掃描超過 25 MB" + +[portal.billing.prepaid.state] +exhausted = "已用完" +healthy = "剩餘充足" +low = "即將用完" [portal.billing.spendLimit] adjustLimit = "調整限制" @@ -6494,6 +6713,8 @@ label = "預計會超出。" [portal.billing.spendThisMonth] eyebrow = "本月支出" +freeRemaining_one = "剩餘 {{formatted}} 個免費 PDF" +freeRemaining_other = "剩餘 {{formatted}} 個免費 PDFs" processed_one = "已處理 {{formattedCount}} 個 PDF。" processed_other = "已處理 {{formattedCount}} 個 PDFs。" processedWithRate_one = "已處理 {{formattedCount}} 個 PDF,每個 {{rate}}。" @@ -6510,6 +6731,7 @@ reachedTitle = "已達每月支出限制" title = "無法開啟 Stripe 入口網站" [portal.billing.walletMeter] +barAria = "Free PDFs remaining" capSuffix_one = "已使用 {{allowance}} 個免費 PDFs" capSuffix_other = "已使用 {{allowance}} 個免費 PDFs" eyebrow = "Processor 試用" @@ -6534,6 +6756,376 @@ signature = "簽章" beta = "Beta" ga = "GA" +[portal.connections] +createTitle = "新增連線" +createTitleFor = "連接 {{name}}" +delete = "刪除" +edit = "編輯" +editTitle = "編輯連線" +unsupportedType = "此連線類型無法在此編輯。" + +[portal.connections.categories.advanced] +description = "適用於我們尚未支援的服務。" +label = "進階" + +[portal.connections.categories.audit] +description = "在您的稽核人員已查看的位置記錄文件發生的事件。" +label = "稽核與合規記錄" + +[portal.connections.categories.notify] +description = "告知另一個系統發生了某件事。" +label = "通知與工作流程" + +[portal.connections.categories.security] +description = "在文件繼續移動前掃描或標記文件。" +label = "安全性與分類" + +[portal.connections.categories.signing] +description = "將文件送出以進行簽署或公證。" +label = "電子簽署與公證" + +[portal.connections.categories.storage] +description = "處理後文件的目的地 - bucket、drive、ticket 或 page。" +label = "檔案與附件" + +[portal.connections.commonFields.accessToken] +helperText = "Stirling 會加密儲存此項目,且不會再次顯示。" +label = "Access token" + +[portal.connections.commonFields.apiKey] +label = "API key" + +[portal.connections.commonFields.apiToken] +label = "API token" + +[portal.connections.commonFields.baseUrl] +helperText = "使用此連線的步驟只能呼叫此 URL 下的路徑,絕不能呼叫其他主機。" +label = "伺服器 URL" + +[portal.connections.commonFields.email] +label = "Email" + +[portal.connections.commonFields.host] +label = "Host" + +[portal.connections.commonFields.password] +label = "密碼" + +[portal.connections.commonFields.port] +label = "Port" + +[portal.connections.commonFields.username] +label = "使用者名稱" + +[portal.connections.commonFields.webhookUrl] +helperText = "貼上服務提供給您的 URL。只能呼叫其下的路徑。" +label = "Webhook URL" + +[portal.connections.fields] +name = "連線名稱" +namePlaceholder = "例如 Production" + +[portal.connections.picker] +cancel = "取消" +createNew = "新增連線..." +placeholder = "選擇連線" +save = "儲存連線" + +[portal.connections.picker2] +back = "變更整合" +heading = "您想連接什麼?" +noResultsBody = "我們尚未支援它 - 告訴我們,我們會考慮新增。" +noResultsTitle = "沒有符合「{{query}}」的項目" +searchPlaceholder = "搜尋整合 - 試試「sign」、「ocr」或產品名稱" + +[portal.connections.types.api] +description = "從 policy 呼叫任何 HTTP API。您可以選擇 URL、驗證與 payload。" +label = "自訂 API" + +[portal.connections.types.api.fields.authType] +label = "驗證" + +[portal.connections.types.api.fields.authType.options.basic] +label = "使用者名稱與密碼" + +[portal.connections.types.api.fields.authType.options.bearer] +label = "Bearer token" + +[portal.connections.types.api.fields.authType.options.header] +label = "自訂 header 中的 token" + +[portal.connections.types.api.fields.authType.options.none] +label = "無" + +[portal.connections.types.api.fields.authType.options.tokenLogin] +label = "登入以取得 token" + +[portal.connections.types.api.fields.baseUrl] +helperText = "使用此連線的步驟只能呼叫此 URL 下的路徑,絕不能呼叫其他主機。" +label = "Base URL" +placeholder = "https://api.example.com/v1" + +[portal.connections.types.api.fields.headerName] +label = "Header 名稱" +placeholder = "X-API-Key" + +[portal.connections.types.api.fields.loginPath] +label = "登入路徑" +placeholder = "/auth/login" + +[portal.connections.types.api.fields.password] +label = "密碼" + +[portal.connections.types.api.fields.resultUrlHosts] +helperText = "如果 API 回覆下載連結,只能擷取這些 hosts。以逗號分隔。留空則只允許基礎 URL 自身的 host。" +label = "允許的結果 hosts" + +[portal.connections.types.api.fields.token] +label = "Token" + +[portal.connections.types.api.fields.tokenHeaderName] +label = "以此 header 傳送 token" + +[portal.connections.types.api.fields.tokenResponseHeader] +helperText = "登入呼叫回傳 token 的 response header,例如 X-Auth-Token。" +label = "Token response header" + +[portal.connections.types.api.fields.username] +label = "使用者名稱" + +[portal.connections.types.clamav] +baseUrlPlaceholder = "http://clamav-rest:9000" +description = "在您執行的伺服器上掃描文件是否有惡意軟體 - 任何內容都不會離開您的網路。" +label = "ClamAV(自架)" + +[portal.connections.types.cloudmersive] +description = "在 policy 中對文件進行病毒與內容掃描。" +label = "Cloudmersive" + +[portal.connections.types.cloudmersiveadvanced] +description = "封鎖包含巨集、可執行檔、指令碼或嵌入物件的文件 - 不只是已知惡意軟體。" +label = "Cloudmersive Advanced Scan" + +[portal.connections.types.confluence] +baseUrlPlaceholder = "https://your-site.atlassian.net/wiki/api/v2" +description = "將處理後的文件附加到 Confluence 頁面。" +label = "Confluence" + +[portal.connections.types.consigno] +description = "將文件傳送至 ConsignO Cloud (Notarius) 進行電子簽署與公證。" +label = "ConsignO Cloud" + +[portal.connections.types.consigno.fields.baseUrl] +helperText = "您的 ConsignO Cloud 環境,包含 /api/v1。" +label = "伺服器 URL" +placeholder = "https://your-org.consignocloud.com/api/v1" + +[portal.connections.types.consigno.fields.clientId] +label = "API key" + +[portal.connections.types.consigno.fields.clientSecret] +label = "API secret" + +[portal.connections.types.consigno.fields.password] +label = "密碼" + +[portal.connections.types.consigno.fields.tenantId] +helperText = "僅在您的組織使用 tenants 時需要。" +label = "Tenant" + +[portal.connections.types.consigno.fields.username] +label = "使用者名稱" + +[portal.connections.types.discord] +baseUrlPlaceholder = "https://discord.com/api/webhooks/..." +description = "當 policy 處理文件時,將訊息發布到 Discord。" +label = "Discord" + +[portal.connections.types.elastic] +baseUrlPlaceholder = "https://your-cluster:9200" +description = "每次 policy 處理文件時,在 Elasticsearch 中建立稽核事件索引。" +label = "Elasticsearch" + +[portal.connections.types.ftp] +description = "輪詢 FTP 或 FTPS 伺服器以取得新文件。" +hostPlaceholder = "ftp.example.com" +label = "FTP / FTPS" + +[portal.connections.types.ftp.fields.passive] +label = "連線模式" + +[portal.connections.types.ftp.fields.passive.options] +active = "主動" +passive = "被動(建議)" + +[portal.connections.types.ftp.fields.security] +helperText = "純 FTP 會以未加密方式傳送認證與檔案;如果伺服器支援,請選擇 FTPS。" +label = "加密" + +[portal.connections.types.ftp.fields.security.options] +explicit = "FTPS(explicit / AUTH TLS)" +implicit = "FTPS(implicit)" +none = "無(純 FTP)" + +[portal.connections.types.googlechat] +baseUrlPlaceholder = "https://chat.googleapis.com/v1/spaces/..." +description = "當 policy 執行時,將訊息發布到 Google Chat space。" +label = "Google Chat" + +[portal.connections.types.jira] +baseUrlPlaceholder = "https://your-site.atlassian.net" +description = "將處理後的文件附加到 Jira issue。" +label = "Jira" + +[portal.connections.types.mailgun] +description = "當 policy 執行時,透過 Email 傳送文件或通知。" +label = "Mailgun" + +[portal.connections.types.n8n] +baseUrlPlaceholder = "https://your-n8n/webhook/9f2c-..." +description = "Hand documents to an n8n workflow, and take back whatever it produces." +label = "n8n" + +[portal.connections.types.n8n.fields.authType] +helperText = "Match the Authentication set on the n8n Webhook node." +label = "Webhook authentication" + +[portal.connections.types.n8n.fields.authType.options.basic] +label = "Basic auth" + +[portal.connections.types.n8n.fields.authType.options.header] +label = "Header auth" + +[portal.connections.types.n8n.fields.authType.options.none] +label = "None" + +[portal.connections.types.n8n.fields.headerName] +helperText = "The header name from the n8n credential, not its value." +label = "Header name" +placeholder = "X-N8N-Key" + +[portal.connections.types.n8n.fields.token] +label = "Header value" + +[portal.connections.types.nextcloud] +baseUrlPlaceholder = "https://your-server/remote.php/dav" +description = "將處理後的文件存入 Nextcloud。" +label = "Nextcloud" + +[portal.connections.types.nextcloud.fields.appPassword] +label = "App password" + +[portal.connections.types.presidio] +baseUrlPlaceholder = "http://presidio-analyzer:3000" +description = "使用您執行的 Presidio 伺服器偵測文件文字中的個人資料。" +label = "Presidio(自架)" + +[portal.connections.types.purview] +description = "讀取並套用 Microsoft Purview 敏感度標籤到 PDFs。" +label = "Microsoft Purview" + +[portal.connections.types.purview.fields.clientId] +helperText = "選填。只有在需要依名稱選取標籤時才需要;沒有它也能標記。" +label = "Application (client) ID" + +[portal.connections.types.purview.fields.clientSecret] +label = "Client secret" + +[portal.connections.types.purview.fields.tenantId] +helperText = "會加蓋到每個標籤作為其 SiteId,讓支援 Purview 的工具知道是哪個組織套用的。" +label = "Directory (tenant) ID" +placeholder = "cb46c030-1825-4e81-a295-151c039dbf02" + +[portal.connections.types.s3] +description = "來源可讀取且 pipeline 輸出可寫入的 S3 bucket。" +label = "S3 storage" + +[portal.connections.types.s3.fields.accessKeyId] +label = "Access key ID" + +[portal.connections.types.s3.fields.bucket] +label = "Bucket" + +[portal.connections.types.s3.fields.endpoint] +helperText = "AWS 請留空。若使用 MinIO 或其他 S3 相容儲存,請設定此項。" +label = "Endpoint" + +[portal.connections.types.s3.fields.region] +label = "Region" + +[portal.connections.types.s3.fields.secretAccessKey] +label = "Secret access key" + +[portal.connections.types.sendgrid] +description = "當 policy 執行時,透過 Email 傳送文件或通知。" +label = "SendGrid" + +[portal.connections.types.sftp] +description = "輪詢 SFTP (SSH) 伺服器以取得新文件。" +hostPlaceholder = "sftp.example.com" +label = "SFTP" + +[portal.connections.types.sftp.fields.hostKeyFingerprint] +helperText = "選填。伺服器的 SHA-256 host key fingerprint(來自 `ssh-keyscan host | ssh-keygen -lf -`)。設定後,只接受提供此 key 的伺服器;留空時,首次連線時看到的 key 會被釘選,之後若變更會被拒絕。" +label = "Host key fingerprint" +placeholder = "SHA256:..." + +[portal.connections.types.sftp.fields.passphrase] +label = "Key passphrase" + +[portal.connections.types.sftp.fields.password] +helperText = "如果您改用 private key 驗證,請留空。" + +[portal.connections.types.sftp.fields.privateKey] +helperText = "選填。貼上 OpenSSH private key,以使用 key 而非密碼進行驗證。" +label = "Private key" + +[portal.connections.types.slack] +description = "當 policy 處理文件時,將訊息發布到 Slack。" +label = "Slack" + +[portal.connections.types.smb] +description = "監看 SMB / CIFS 網路共用(Windows 或 Samba)。" +hostPlaceholder = "fileserver.example.com" +label = "SMB / 網路共用" + +[portal.connections.types.smb.fields.domain] +helperText = "選填。如果您的伺服器使用 Windows domain 或工作群組,請填寫。" +label = "Domain" + +[portal.connections.types.smb.fields.share] +label = "Share" +placeholder = "documents" + +[portal.connections.types.splunk] +baseUrlPlaceholder = "https://your-splunk:8088" +description = "每次 policy 處理文件時,將稽核事件傳送至 Splunk。" +label = "Splunk" + +[portal.connections.types.sumologic] +baseUrlPlaceholder = "https://endpoint.collection.sumologic.com/receiver/v1/http/..." +description = "將稽核事件傳送至 Sumo Logic HTTP collector。" +label = "Sumo Logic" + +[portal.connections.types.teams] +baseUrlPlaceholder = "https://your-org.webhook.office.com/webhookb2/..." +description = "當 policy 執行時,將訊息發布到 Teams 頻道。" +label = "Microsoft Teams" + +[portal.connections.types.webhook] +baseUrlPlaceholder = "https://example.com/hooks/documents" +description = "將每份文件或僅其詳細資料發布到您選擇的 URL。" +label = "Webhook" + +[portal.connections.types.zapier] +baseUrlPlaceholder = "https://hooks.zapier.com/hooks/catch/..." +description = "從 policy 觸發 Zap 或 Make scenario。" +label = "Zapier & Make" + +[portal.docs] +browse = "瀏覽文件" +viewSource = "在 GitHub 上檢視來源" + [portal.docs.authentication] codeCaption = "每個請求" eyebrow = "開始使用" @@ -6624,6 +7216,9 @@ eyebrow = "SKILLS" lead = "代理程式以單一工具呼叫的內建命名能力。每項 skill 都是附有 evals 的確定性操作鏈。" title = "代理程式 skills" +[portal.docs.toc] +title = "本頁內容" + [portal.docs.webhooks] codeCaption = "document.processed" eyebrow = "API 參考" @@ -6697,6 +7292,8 @@ type = "類型" user = "使用者" [portal.documents.queue.empty] +connectSource = "連接來源" +createPipeline = "建立 pipeline" description = "當來源將文件送入您的管線時,它們會出現在這裡供審查。" title = "佇列中沒有文件" @@ -6712,11 +7309,11 @@ editorAction = "編輯器" empty = "沒有符合此篩選條件的文件。" rowActions = "列動作" sensitiveLabel = "敏感" -sensitiveTitle = "敏感 — 需要存取權" [portal.documents.table.columns] action = "管線 / 動作" document = "文件" +labels = "Labels" product = "產品" status = "狀態" time = "時間" @@ -6732,6 +7329,7 @@ host = "主機" lastSeen = "上次出現" region = "區域" status = "狀態" +target = "Target" version = "版本" [portal.editorAdmin.health.empty] @@ -6806,13 +7404,121 @@ description = "此檢視發生未預期的錯誤。請再試一次,或從側 retry = "再試一次" title = "此頁面發生問題" +[portal.failures] +fromSource = "From source {{source}}" +occurrences = "{{count}} occurrences" +reportedBy = "Hit by {{actor}}" +runReference = "Run {{runId}}" +subtitle = "Failures recorded from your policy runs and your team's editors, with the actions you can take." +title = "Failures" + +[portal.failures.action] +acknowledge = "Acknowledge" +confirm = "Are you sure?" +dismiss = "Dismiss" +dismissSkipFile = "Skip this file" + +[portal.failures.debug] +copyJson = "Copy JSON" +dismissAll = "Dismiss all ({{total}})" +dismissing = "Dismissing..." +hideJson = "Hide raw JSON ({{total}})" +refresh = "Refresh failures" +showJson = "Show raw JSON ({{total}})" + +[portal.failures.disabled] +closed = "This failure is already closed." +unavailable = "Not available for this failure." + +[portal.failures.empty] +description = "Policy runs that fail will appear here with the actions you can take." +title = "No failures recorded" + +[portal.failures.kind.inputPasswordProtected] +description = "The pipeline could not open the document because it is password-protected. Unlock it and run it again, or skip this file." +title = "Password-protected document" + +[portal.failures.kind.unknown] +description = "This run failed for a reason Stirling does not yet recognise. The raw message is shown below." +title = "Unrecognised failure" + +[portal.failures.origin] +pipeline = "Pipeline" +policy = "Policy" +tool = "Tool run" + +[portal.failures.stage] +blocked = "Blocked" +input = "Input" +internal = "Internal" +never_ran = "Never ran" +output = "Output" + +[portal.home.download] +back = "返回" +body = "下載桌面應用程式或自架伺服器。" +done = "完成" +guide = "開啟 {{name}} 指南" +openInBrowser = "改在瀏覽器中開啟" +sectionDesktop = "桌面應用程式" +sectionSelfHosted = "自架" +title = "安裝 Stirling PDF Editor" + +[portal.home.download.docker] +detailBody = "在您的伺服器上執行官方 image。" +note = "於 :8080 開啟。預設登入:admin / stirling。" +tagline = "在 container 中執行伺服器" +title = "Docker" +variantFat = "完整" +variantLite = "超輕量" +variantStandard = "標準" + +[portal.home.download.kubernetes] +detailBody = "安裝官方 Helm chart。" +note = "為 /configs 新增 PVC 以保留設定。" +tagline = "使用 Helm 部署到叢集" +title = "Kubernetes" + +[portal.home.download.linux] +detailBody = "適用於您發行版的原生建置。" +guideBtn = "檢視 Linux 安裝選項" +note = "常見發行版可使用 AppImage、.deb 和 .rpm 建置。" +tagline = "適用於您發行版的原生桌面應用程式" +title = "Linux" + +[portal.home.download.mac] +altLabel = "或透過 Homebrew" +detailBody = "執行安裝程式,或使用 Homebrew。" +downloadBtn = "下載 macOS 版" +tagline = "適用於 Apple silicon 和 Intel 的原生應用程式" +title = "macOS" + +[portal.home.download.manual] +detailBody = "在裸機伺服器上執行 .jar。" +downloadBtn = "下載 .jar" +note = "需要 Java 21+。於 :8080 開啟;預設登入 admin / stirling。" +runLabel = "然後執行" +tagline = "在裸機伺服器上執行 .jar" +title = "手動伺服器設定" + +[portal.home.download.windows] +altLabel = "或透過 winget" +detailBody = "執行安裝程式,或使用套件管理器。" +downloadBtn = "下載安裝程式" +tagline = "適用於 Microsoft Windows 的原生應用程式" +title = "Windows" + [portal.home.editor] -activeUsers = "{{n}} 位活躍" -invite = "邀請隊友" +activeOfDeployed = "本月 {{total}} 個中有 {{active}} 個啟用" name = "Stirling PDF 編輯器" open = "在瀏覽器中開啟" updated = "已於 {{time}} 更新" +[portal.home.editor.deploy] +finish = "完成部署" +options = "檢視部署選項" +start = "部署 Editor" + [portal.home.editor.target] cloud = "Managed Cloud" docker = "自託管 · Docker" @@ -6823,49 +7529,6 @@ afternoon = "午安" evening = "晚安" morning = "早安" -[portal.home.onboarding] -dismiss = "關閉設定" -notStarted = "尚未開始" -progress = "已完成 {{done}} / {{total}}" -title = "完成設定" - -[portal.home.onboarding.enterprise] -body = "全組織 SSO + SCIM + RBAC、承諾用量價格,以及隔離網路部署。" -cta = "開始試用" -ctaQuote = "取得報價" -lead = "適用於 250+ 名員工。" -tag = "Enterprise" - -[portal.home.onboarding.steps.editor] -blurb = "部署桌面應用程式,永久免費。" -title = "下載 PDF 編輯器" - -[portal.home.onboarding.steps.policies] -blurb = "{{active}} 個啟用 · {{recommended}} 個建議" -chip = "{{active}} 個啟用" -title = "啟用政策" - -[portal.home.onboarding.steps.sources] -blurb = "{{connected}} 個已連接 · 每個 PDF 在抵達處皆受治理" -chip = "{{connected}} 個已連接" -title = "連接您的來源" - -[portal.home.quickActions] -subtitle = "今日重點任務" -title = "快速動作" - -[portal.home.quickActions.buildPipeline] -blurb = "以型別化操作庫為基礎的 3 步驟組合器" -title = "建立管線" - -[portal.home.quickActions.connectSource] -blurb = "S3、代理程式、webhook、監看資料夾" -title = "連接來源" - -[portal.home.quickActions.issueApiKey] -blurb = "具速率限制和 IP 允許清單的範圍化金鑰" -title = "發行 API 金鑰" - [portal.infrastructure] manageEditorDeployment = "管理編輯器部署" sectionsAriaLabel = "基礎設施區段" @@ -6878,13 +7541,9 @@ heading = "API 金鑰" subheading = "具每金鑰速率限制、權限與 IP 允許清單的範圍化憑證。" [portal.infrastructure.apiKeys.card] -allowedIps = "允許的 IP" -anyIp = "任何 IP(無允許清單)" created = "已建立" lastUsed = "上次使用" -permissions = "權限" -rateLimit = "速率限制" -rateLimitValue = "{{value}} req/min" +revoke = "撤銷金鑰" usageMonth = "本月用量" usageToday = "今日用量" @@ -6892,6 +7551,15 @@ usageToday = "今日用量" description = "建立範圍化金鑰以開始呼叫 Stirling API。" title = "尚無 API 金鑰" +[portal.infrastructure.apiKeys.error] +load = "無法載入您的 API 金鑰。請再試一次。" + +[portal.infrastructure.apiKeys.revoke] +body = "要撤銷「{{name}}」嗎?任何仍使用此金鑰的整合都會立即開始收到 401 錯誤。此操作無法復原。" +cancel = "取消" +confirm = "撤銷金鑰" +title = "撤銷 API 金鑰" + [portal.infrastructure.audit] filterAriaLabel = "依類別篩選稽核事件" heading = "稽核記錄" @@ -6935,6 +7603,7 @@ all = "全部" auth = "驗證" config = "設定" elevation = "權限提升" +policy = "政策" processing = "處理" security = "安全性" @@ -6944,19 +7613,30 @@ title = "您沒有存取權" [portal.infrastructure.audit.metrics] config = "設定" -elevation = "權限提升" +policy = "政策" processing = "處理" totalEvents = "事件總數 · 24h" +[portal.infrastructure.auditCatLabel] +auth = "身分驗證" +config = "設定" +elevation = "權限提升" +policy = "政策" +processing = "處理" +security = "安全性" + +[portal.infrastructure.auditStatusLabel] +danger = "錯誤" +info = "資訊" +success = "成功" +warning = "警告" + [portal.infrastructure.createKey] cancel = "取消" createKey = "建立金鑰" done = "完成" -ipAllowlistHelper = "以逗號分隔的 CIDR 範圍。留空可允許任何 IP。" -ipAllowlistLabel = "IP 允許清單" keyNameLabel = "金鑰名稱" keyNamePlaceholder = "例如:Production · ingest" -permissionsLabel = "權限" secretKeyCaption = "秘密金鑰" secretWarning = "請將此儲存在 secrets manager 中。Stirling 只會儲存 hash — 之後無法復原。" subtitle = "將金鑰範圍限制在最低需求。您可以隨時輪換或撤銷。" @@ -6964,207 +7644,9 @@ subtitleCreated = "立即複製此秘密 — 之後不會再顯示。" title = "建立 API 金鑰" titleCreated = "金鑰已建立" -[portal.infrastructure.deployments] -msValue = "{{value}} ms" -throughputValue = "{{value}}/min" - -[portal.infrastructure.deployments.deployColumns] -deployedBy = "部署者" -environment = "環境" -product = "產品" -status = "狀態" -version = "版本" -when = "時間" - -[portal.infrastructure.deployments.recent] -heading = "最近部署" -subheading = "各產品與環境的最新推出。" - -[portal.infrastructure.deployments.regionColumns] -instances = "執行個體" -latency = "延遲" -load = "負載" -p99 = "P99" -region = "區域" -status = "狀態" -throughput = "輸送量" -uptime = "正常運作時間" -version = "版本" - -[portal.infrastructure.deployments.regions] -heading = "區域" -subheading = "每個已部署 Stirling 區域的即時健康狀態 — 延遲、負載與推出版本。" - -[portal.infrastructure.deployments.regions.empty] -description = "工作區佈建後,已部署區域會顯示在這裡。" -title = "尚未部署區域" - -[portal.infrastructure.models] -heading = "模型" -msValue = "{{value}} ms" -subheading = "支援您工作區文件處理的模型目錄與路由。" - -[portal.infrastructure.models.byom] -description = "註冊 on-prem 或自託管模型,並將其固定到某個區域,以進行受資料駐留限制的處理。" -title = "帶入您自己的模型" - -[portal.infrastructure.models.catalogue] -heading = "目錄" -sub = "工作區可用的代管模型,包含即時延遲與成本。" -subEnterprise = "代管、自備與 on-prem 模型 — 可使用按區域固定。" - -[portal.infrastructure.models.catalogue.empty] -description = "工作區目錄中的模型會顯示在這裡。" -title = "沒有可用模型" - -[portal.infrastructure.models.columns] -cost = "成本" -latency = "延遲" -load = "負載" -model = "模型" -status = "狀態" -type = "類型" -version = "版本" - -[portal.infrastructure.models.metrics] -activeModels = "活躍模型" -avgLatency = "平均延遲" -included = "已包含" -monthlySpend = "每月模型支出" - -[portal.infrastructure.models.routing] -empty = "未設定路由規則。" -heading = "路由規則" -sub = "哪個模型處理每個操作。沒有符合更精確規則時會套用預設值。" -subLocked = "將操作路由到特定模型 — 付費方案可用。" - -[portal.infrastructure.models.routing.lockedBanner] -description = "升級到 Pro 以控制每個操作和文件類型由哪個模型處理。" -title = "模型路由是付費功能" - -[portal.infrastructure.models.routingColumns] -default = "預設" -docType = "文件類型" -modelForAria = "{{operation}} 的模型" -operation = "操作" -routedTo = "路由至" - -[portal.infrastructure.security.access.byok] -description = "提供來自您自己 KMS 的金鑰。Stirling 會使用它加密,但仍可讀取。" -label = "帶入您自己的金鑰 (BYOK)" - -[portal.infrastructure.security.access.hyok] -description = "金鑰永不離開您的 KMS。Stirling 只持有密文。" -label = "保留您自己的金鑰 (HYOK)" - -[portal.infrastructure.security.access.stirling] -description = "Stirling 管理加密金鑰。最簡單 — 您這邊無需金鑰操作。" -label = "Stirling 持有的金鑰" - -[portal.infrastructure.security.accessPolicy] -heading = "文件存取政策" -subheading = "控制誰可以解密靜態儲存的已處理文件。" - -[portal.infrastructure.security.attestations] -heading = "合規性證明" -noReport = "沒有可用報告" -subheading = "逐框架的稽核狀態,並提供已證明控制項的報告。" -viewReport = "檢視報告 →" - -[portal.infrastructure.security.compliance] -heading = "合規性" -subheading = "涵蓋 Stirling 平台的證明與認證。" - -[portal.infrastructure.security.empty] -description = "工作區的安全性設定會顯示在這裡。" -title = "安全狀態無法使用" - -[portal.infrastructure.security.hyokBanner] -description = "使用 HYOK 時,加密金鑰永不離開您的 KMS。Stirling 只儲存和處理您可隨時撤銷的密文。" -title = "Stirling 無法解密您的文件" - -[portal.infrastructure.security.ipAllowlist] -empty = "未設定 IP 範圍 — 允許所有 IP。" -heading = "IP 允許清單" -sub = "API 存取僅限於這些 CIDR 範圍。" -subLocked = "將 API 存取限制為已知 IP 範圍 — 付費方案可用。" - -[portal.infrastructure.security.ipAllowlist.lockedBanner] -description = "升級到 Pro 以將 API 存取限制為特定網路。" -title = "IP 允許清單是付費功能" - -[portal.infrastructure.security.ipColumns] -added = "已新增" -addedBy = "新增者" -cidr = "CIDR" -label = "標籤" - -[portal.infrastructure.security.keyManagement] -algorithm = "演算法" -heading = "加密金鑰管理" -keyId = "金鑰識別碼" -lastRotated = "上次輪換" -rotateKey = "輪換金鑰" -rotationPolicy = "輪換政策" -subheading = "加密靜態儲存文件的金鑰保管 — 誰可以解密,以及金鑰如何輪換。" - -[portal.infrastructure.security.managedBanner] -description = "帶入您自己的金鑰 (BYOK) 和保留您自己的金鑰 (HYOK) 保管可於 Enterprise 使用。升級以提供來自您自己 KMS 的金鑰。" -title = "在您的方案中,金鑰由 Stirling 管理" - -[portal.infrastructure.security.residency.apac] -description = "ap-southeast-1" -label = "亞太地區" - -[portal.infrastructure.security.residency.eu] -description = "eu-west-1 · GDPR 資料邊界" -label = "歐盟" - -[portal.infrastructure.security.residency.us] -description = "us-east-1 · us-west-2" -label = "美國" - -[portal.infrastructure.security.residencyHeader] -heading = "資料駐留" -subheading = "文件儲存與處理的位置。" - -[portal.infrastructure.storage] -gbValue = "{{value}} GB" -percentUsed = "已使用 {{value}}" - -[portal.infrastructure.storage.empty] -description = "已連接的儲存空間和用量會顯示在這裡。" -title = "未設定儲存空間" - -[portal.infrastructure.storage.lifecycle] -active = "啟用中" -activeRange = "0–{{value}}d" -archived = "已封存" -coldStorage = "冷儲存" -deleted = "已刪除" -never = "永不" -purged = "已清除" - -[portal.infrastructure.storage.providers] -connect = "連接" -connected = "已連接" -heading = "已連接提供者" -subheading = "已處理成品寫入的位置。" - -[portal.infrastructure.storage.retention] -heading = "保留" -subheading = "成品在生命週期刪除前保留的時間。" -windowLabel = "預設保留期間" - -[portal.infrastructure.storage.retentionOption] -days_one = "{{count}} 天" -days_other = "{{count}} 天" -never = "永不刪除" - -[portal.infrastructure.storage.totalUsage] -heading = "總用量" -progressLabel = "已使用儲存空間" -subheading = "所有已連接提供者消耗的儲存空間。" +[portal.infrastructure.keyLabel] +active = "啟用" +revoked = "已撤銷" [portal.infrastructure.tabs] apiKeys = "API 金鑰" @@ -7174,6 +7656,49 @@ models = "模型" security = "安全性" storage = "儲存空間" +[portal.integrations] +availableHeading = "可用" +comingSoonHeading = "即將推出" +connect = "連線" +connectionCount_one = "{{count}} 個連線" +connectionCount_other = "{{count}} 個連線" +customApi = "自訂 API" +searchPlaceholder = "搜尋整合" +subtitle = "Stirling 可連接的一切。憑證只需在此建立一次,之後由來源、政策和管線參照。" +title = "整合" +typedName = "{{tool}} 名稱" + +[portal.integrations.filters] +all = "全部" +audit = "稽核" +connected = "已連線" +notify = "通知" +security = "安全性" +signing = "簽署" +storage = "儲存" + +[portal.integrations.noResults] +description = "No integrations match your filters. Try a different category or search." +title = "No matches" + +[portal.integrations.status] +connected = "已連線" + +[portal.integrations.table] +integration = "整合" +worksWith = "搭配使用" + +[portal.integrations.worksWith] +pipelines = "管線" +policies = "政策" +sources = "來源" + +[portal.legal] +draft = "{{label}} · 草稿" +loadError = "無法載入此文件。請再試一次。" +loading = "載入中…" +title = "法律文件" + [portal.nav] agent-builder = "代理程式建構器" components = "元件" @@ -7182,33 +7707,17 @@ documents = "文件" editor = "編輯器" home = "首頁" infrastructure = "基礎設施" +integrations = "整合" pipelines = "管線" policies = "政策" procurement = "採購" -settings = "設定" sources = "來源" usage = "用量與帳單" users = "使用者" -[portal.notifications] -markAllRead = "全部標為已讀" -title = "通知" -viewAll = "檢視全部" - -[portal.notifications.ariaLabel] -none = "通知,沒有未讀" -unread_one = "通知,{{count}} 則未讀" -unread_other = "通知,{{count}} 則未讀" - -[portal.notifications.count] -allRead = "全部已讀" -loading = "載入中" -new_one = "{{count}} 則新通知" -new_other = "{{count}} 則新通知" - -[portal.notifications.empty] -description = "沒有新通知。" -title = "您已全部處理完畢" +[portal.nav.section] +platform = "PDF 平台" +processor = "PDF 處理器" [portal.pipelines] subtitle = "後端上的每個自動化文件管線:在一組來源上依序執行的操作鏈,並由觸發器執行。點擊列以查看其步驟與來源。" @@ -7218,54 +7727,87 @@ title = "管線" newPipeline = "新增管線" [portal.pipelines.builder] -addStep = "新增工具" +activate = "Activate" back = "返回管線" +cannotFollow = "無法接收 {{produced}}" +chooseAccount = "選擇帳戶" +chooseDestination = "選擇目的地" +chooseOperation = "選擇此步驟的作用" +chooseSource = "選擇來源" discard = "捨棄變更" -enabled = "已啟用" +inputs = "輸入" +inputSource = "輸入來源" +inputTrigger = "觸發器" keepEditing = "繼續編輯" -needsUpload = "需要上傳的檔案" +moreActions = "More actions" +needsConfiguring = "需要設定" +needsDestination = "No destination chosen" +needsSource = "No source chosen" noToolMatches = "沒有工具符合您的搜尋。" -pipelineSettings = "管線設定" +pause = "Pause" +rename = "Rename pipeline" searchTools = "搜尋工具" -selectToolBody = "新增工具以建立您的管線。" -selectToolTitle = "尚無工具" -toolSettings = "工具設定" +sendToSystem = "傳送到另一個系統" +stepsIncompatible = "這些步驟無法在其前一步產生的內容上執行:{{tools}}。" +stepsNeedSetup = "儲存前仍需設定這些步驟:{{tools}}。" +supportingFiles = "Supporting files" +testRun = "Test with a file" unknownStep = "無法辨識的操作,已原樣保留。" unsavedBody = "您有未儲存的變更。離開前要儲存,還是捨棄?" unsavedTitle = "未儲存的變更" -uploadUnsupported = "管線尚不支援上傳的檔案,因此無法儲存這些步驟:{{tools}}。" usesDefaults = "使用預設設定執行" +viewDefinition = "View definition" + +[portal.pipelines.builder.blocker] +destination = "Choose a destination" +heading = "To create this pipeline:" +incompatible = "Fix steps that can't run in order: {{tools}}" +name = "Give the pipeline a name" +saveHeading = "To save your changes:" +schedule = "Set how often it runs" +setup = "Finish setting up: {{tools}}" +source = "Choose an input source" + +[portal.pipelines.builder.diagnostic] +fan-in = "合併前一步的每個檔案" +fan-out = "對前一步的每個檔案執行一次" +format-mismatch = "需要 {{accepts}},但前一步產生 {{produced}}" +output-uncertain = "可能不會執行:前一步的輸出取決於其設定方式" +source-mismatch = "需要 {{accepts}},但此管線的輸入是 {{produced}}" +undeclared-operation = "無法檢查此步驟接受的內容" [portal.pipelines.composer] addTool = "新增工具" -cancel = "取消" -chainEmpty = "新增工具以開始建立您的管線。" create = "建立管線" -directory = "輸出資料夾" -directoryHelp = "伺服器上的絕對路徑。必須位於已設定的允許資料夾內。" +createPaused = "Create paused" editingUnsupported = "尚不支援顯示這些工具參數進行編輯。" -moveDown = "下移" -moveUp = "上移" +editSource = "Edit source" name = "名稱" namePlaceholder = "例如:Redaction sweep" -noSources = "尚未連接來源。管線仍可直接在提供給它的檔案上執行。" noToolSettings = "此工具沒有可設定的選項。" -operations_one = "操作({{count}})" -operations_other = "操作({{count}})" output = "輸出" -removeStep = "移除操作" save = "儲存變更" scheduleEvery = "執行間隔" -sources = "來源" -sourcesLoading = "正在載入來源..." trigger = "觸發器" triggerManual = "僅手動" +[portal.pipelines.composer.runsEvery] +days_one = "Runs every day" +days_other = "Runs every {{count}} days" +hours_one = "Runs every hour" +hours_other = "Runs every {{count}} hours" +minutes_one = "Runs every minute" +minutes_other = "Runs every {{count}} minutes" + [portal.pipelines.composer.unit] days = "天" hours = "小時" minutes = "分鐘" +[portal.pipelines.definition] +subtitle = "The pipeline as it would be saved." +title = "Definition" + [portal.pipelines.delete] body = "要刪除 \"{{name}}\" 嗎?此操作無法復原。" cancel = "取消" @@ -7273,28 +7815,62 @@ confirm = "刪除" title = "刪除管線?" [portal.pipelines.detail] +clearHistory = "清除歷程記錄" delete = "刪除管線" run = "立即執行" [portal.pipelines.empty] action = "建立管線" +connectSource = "連線來源" description = "建立您的第一個管線:選擇執行來源、串接操作,並選擇輸出位置。" title = "尚無管線" +[portal.pipelines.graph] +addFirstTool = "Add a tool" +dragHint = "Drop on a line to move it" +insertHere = "Add a tool here" +removeNode = "Remove {{name}}" +showError = "Show why {{name}} failed" + +[portal.pipelines.graph.add] +input = "Add a source" +output = "Add a destination" + +[portal.pipelines.graph.run] +done = "Done" +failed = "Failed" +running = "Running" + +[portal.pipelines.inspector] +multipleBody = "Drag any of them onto a line to move them together, or press Delete to remove them." +multipleSelected_one = "{{count}} step selected" +multipleSelected_other = "{{count}} steps selected" +noSelectionBody = "Pick a node in the graph to change what it does." +noSelectionTitle = "Nothing selected" + +[portal.pipelines.inspector.status] +completed_one = "Finished the only step" +completed_other = "Finished all {{count}} steps" +failed_one = "Failed on the only step" +failed_other = "Failed after {{done}} of {{count}} steps" +running_one = "Running the only step" +running_other = "Running step {{done}} of {{count}}" + [portal.pipelines.kpi] active = "啟用中" paused = "已暫停" total = "管線" -[portal.pipelines.output] -folder = "寫入資料夾" -inline = "傳回檔案" - [portal.pipelines.run] +allProcessed_one = "沒有可執行項目:來源的 {{count}} 份文件已經處理過。" +allProcessed_other = "沒有可執行項目:來源中的全部 {{count}} 份文件已經處理過。" completed_one = "執行已完成。" completed_other = "全部 {{count}} 次執行已完成。" empty = "沒有可執行的內容:來源沒有要處理的文件。" failed = "執行失敗:{{error}}" +inFlight = "沒有新項目可執行:文件仍在從先前的執行中處理。" +parked_one = "沒有可執行項目:{{count}} 份文件先前失敗且已暫停。修正原因後,清除歷程記錄以重試。" +parked_other = "沒有可執行項目:{{count}} 份文件先前失敗且已暫停。修正原因後,清除歷程記錄以重試。" running = "執行已開始;仍在進行中。" timeout = "執行時間比預期更長;它可能仍會在背景完成。" @@ -7307,6 +7883,7 @@ name = "管線" sources = "來源" status = "狀態" steps = "步驟" +trigger = "Trigger" [portal.pipelines.trigger] folder-watch = "資料夾監看" @@ -7314,12 +7891,18 @@ manual = "手動" schedule = "已排程" [portal.policies] +defaultName = "{{category}} 政策" subtitle = "常駐自動化,可對每個文件強制執行工具管線。每項政策會在上傳或匯出時觸發,執行其工具鏈,並將強制版本與原始檔一併儲存。" title = "政策" [portal.policies.card] comingSoon = "升級到 Enterprise" notSetUp = "尚未設定" +requiresAiEngine = "需要 AI 引擎" + +[portal.policies.categories.classification] +desc = "上傳時識別每份文件的類型,並標記其中繼資料以便歸檔與搜尋。" +label = "分類" [portal.policies.categories.compliance] desc = "對每個文件強制執行 HIPAA、GDPR、SOC 2 或 FedRAMP 要求。" @@ -7344,6 +7927,13 @@ label = "安全性" [portal.policies.config] scopeAll = "所有文件" +[portal.policies.config.classification] +summary = "依據您團隊的標籤分類每份上傳的文件,並標記其中繼資料。" + +[portal.policies.config.classification.rules] +0 = "分類" +1 = "標記中繼資料" + [portal.policies.config.compliance] summary = "在文件離開系統前,依監管框架驗證文件。" @@ -7371,6 +7961,29 @@ minConfidence = "最低信心度" 2 = "命名" 3 = "正規化" +[portal.policies.config.purview] +label = "套用 Microsoft Purview 敏感度標籤" + +[portal.policies.config.purview.fields] +connection = "Purview 租用戶" +labelId = "標籤 ID" +labelIdHelp = "來自 Purview 入口網站的標籤 GUID。套用標籤會將其中繼資料寫入 PDF;它無法加密,加密需要 Microsoft 用戶端。" +labelName = "標籤名稱(選填)" +labelNameHelp = "與 ID 一起記錄,讓人員和工具能辨識標籤。" + +[portal.policies.config.purview.fields.method] +help = "Purview 會區分由政策套用的標籤與人員選擇的標籤。" +label = "標籤的記錄方式" +privileged = "作為人員選擇" +standard = "作為自動套用" + +[portal.policies.config.purviewRead] +label = "讀取 Microsoft Purview 標籤" + +[portal.policies.config.purviewRead.fields] +connection = "Purview 租用戶" +connectionHelp = "讀取文件已帶有的標籤,讓後續步驟可依此處理。" + [portal.policies.config.retention] summary = "強制文件保留時間、何時封存以及何時刪除。" @@ -7438,6 +8051,7 @@ title = "尚無活動" [portal.policies.endpoints] addWatermark = "浮水印" autoRedact = "遮蔽 PII" +classifyAndLabel = "分類" compressPdf = "壓縮" flatten = "扁平化" ocrPdf = "OCR" @@ -7448,6 +8062,201 @@ description = "您的政策已儲存,連線恢復後會顯示。" retry = "重試" title = "後端無法使用" +[portal.policies.operations] +change = "變更此步驟的作用" +noResults = "沒有符合的步驟。請嘗試產品名稱,或「scan」、「notify」、「attach」。" +searchPlaceholder = "搜尋步驟 - 試試「scan」、「attach」或產品名稱" + +[portal.policies.operations.bodyMode] +binary = "原始檔案位元組" +json = "在 JSON payload 內" +multipart = "作為檔案上傳(multipart)" + +[portal.policies.operations.clamavScan] +description = "您自己的 ClamAV 伺服器會檢查文件;沒有任何內容會離開您的網路。" +label = "掃描病毒(自行託管)" + +[portal.policies.operations.cloudmersiveAdvancedScan] +description = "拒絕含有巨集、可執行檔、指令碼或嵌入物件的文件。" +label = "封鎖主動式內容" + +[portal.policies.operations.cloudmersiveScan] +description = "Cloudmersive 會檢查文件;若不乾淨,執行會停止。" +label = "掃描病毒" + +[portal.policies.operations.confluenceAttach] +description = "將處理後的文件歸檔到頁面。" +label = "附加到 Confluence 頁面" + +[portal.policies.operations.consignoSubmit] +description = "將文件提交到 ConsignO Cloud 進行簽署。" +label = "送出簽署(ConsignO)" +note = "提交文件。尚不支援擷取已簽署的副本,因為步驟無法將結果傳遞給後續步驟。" + +[portal.policies.operations.customApiCall] +description = "自行編寫呼叫:路徑、方法、主體和回應處理。" +label = "呼叫自訂 API" + +[portal.policies.operations.discordNotify] +description = "通知頻道政策已處理文件。" +label = "發佈訊息到 Discord" + +[portal.policies.operations.elasticIndex] +description = "將政策執行內容記錄為可搜尋的文件。" +label = "在 Elasticsearch 中索引稽核事件" + +[portal.policies.operations.fields.bodyMode] +helperText = "Multipart 適合上傳 API;JSON 適合需要檔案位於 payload 內的 API;binary 會傳送原始位元組。" +label = "如何傳送文件" + +[portal.policies.operations.fields.bodyTemplate] +helperText = "依原樣傳送的 JSON 主體。{{document.base64}} 會攜帶檔案;{{document.*}} 和 {{run.*}} 會依每份文件填入。" +label = "JSON 主體" + +[portal.policies.operations.fields.connection] +label = "帳戶" + +[portal.policies.operations.fields.domain] +label = "Mailgun 網域" +placeholder = "mg.acme.com" + +[portal.policies.operations.fields.fileFieldName] +helperText = "API 預期文件所在的表單欄位。" +label = "檔案欄位名稱" +placeholder = "file" + +[portal.policies.operations.fields.from] +label = "寄件者" +placeholder = "processor@acme.com" + +[portal.policies.operations.fields.headers] +helperText = "標頭名稱/值配對的 JSON 物件。系統會為您新增連線的憑證。" +label = "Headers" + +[portal.policies.operations.fields.index] +label = "Index" +placeholder = "stirling-audit" + +[portal.policies.operations.fields.issueKey] +label = "Issue key" +placeholder = "OPS-42" + +[portal.policies.operations.fields.message] +helperText = "作為訊息主體傳送。{{document.*}} 和 {{run.*}} 會依每份文件填入。" +label = "訊息" + +[portal.policies.operations.fields.method] +label = "Method" + +[portal.policies.operations.fields.pageId] +label = "Page ID" +placeholder = "66186" + +[portal.policies.operations.fields.path] +helperText = "附加到連線的基礎 URL。它永遠無法到達其他主機。" +label = "路徑" +placeholder = "/v1/scan" + +[portal.policies.operations.fields.remotePath] +helperText = "寫入到磁碟機中的位置。{{document.filename}} 會依每份文件填入。" +label = "目的地路徑" +placeholder = "Processed/{{document.filename}}" + +[portal.policies.operations.fields.responseMode] +helperText = "報告會讓文件保持不變。取代會將其換成回傳的任何內容。" +label = "如何處理回覆" + +[portal.policies.operations.fields.signerEmail] +label = "簽署者電子郵件" +placeholder = "signer@acme.com" + +[portal.policies.operations.fields.subject] +label = "主旨" +placeholder = "Processed: {{document.filename}}" + +[portal.policies.operations.fields.text] +helperText = "Presidio 會分析此文字。使用 {{document.title}} 或貼上您要檢查的文字。" +label = "要檢查的文字" + +[portal.policies.operations.fields.to] +label = "收件者" +placeholder = "records@acme.com" + +[portal.policies.operations.fields.username] +label = "Nextcloud 使用者名稱" +placeholder = "svc-stirling" + +[portal.policies.operations.googlechatNotify] +description = "通知聊天室政策已處理文件。" +label = "發佈訊息到 Google Chat" + +[portal.policies.operations.jiraAttach] +description = "將處理後的文件歸檔到議題。" +label = "附加到 Jira 議題" + +[portal.policies.operations.mailgunEmail] +description = "將處理後的文件作為附件傳送。" +label = "以電子郵件傳送文件(Mailgun)" + +[portal.policies.operations.n8nGate] +description = "Your workflow decides whether the run carries on." +label = "Ask an n8n workflow to approve" +note = "The workflow must reply with {\"approved\": true}. Anything else — including a timeout — stops the run and parks the document." + +[portal.policies.operations.n8nNotify] +description = "Sends the run's details, not the document." +label = "Tell an n8n workflow" + +[portal.policies.operations.n8nSend] +description = "Sends the document and the run's details to your workflow." +label = "Send the document to n8n" + +[portal.policies.operations.n8nTransform] +description = "Your workflow processes the document and the file it returns replaces it." +label = "Process the document in n8n" +note = "Set the Webhook node to respond \"Using Respond to Webhook Node\", and return a file. On the default \"Immediately\" setting n8n replies with an acknowledgement, and that would replace the document." + +[portal.policies.operations.nextcloudUpload] +description = "將處理後的文件寫入資料夾。" +label = "上傳到 Nextcloud" + +[portal.policies.operations.presidioAnalyze] +description = "您自己的 Presidio 伺服器會回報其找到的個人資料。" +label = "偵測個人資料" +note = "Presidio 讀取文字,而非檔案。請提供您要檢查的文字 — 文件本身不會上傳。" + +[portal.policies.operations.responseMode] +replace = "以回覆取代文件" +report = "保留文件,記錄回覆" + +[portal.policies.operations.sendgridEmail] +description = "將處理後的文件作為附件傳送。" +label = "以電子郵件傳送文件(SendGrid)" + +[portal.policies.operations.slackNotify] +description = "通知頻道政策已處理文件。" +label = "發佈訊息到 Slack" + +[portal.policies.operations.splunkEvent] +description = "在稽核人員已查看的位置記錄政策執行內容。" +label = "傳送稽核事件到 Splunk" + +[portal.policies.operations.sumologicEvent] +description = "將政策執行內容記錄到 HTTP collector。" +label = "傳送稽核事件到 Sumo Logic" + +[portal.policies.operations.teamsNotify] +description = "通知頻道政策已處理文件。" +label = "發佈訊息到 Teams" + +[portal.policies.operations.webhookPost] +description = "將文件本身傳送到您選擇的 URL。" +label = "將文件發佈到 webhook" + +[portal.policies.operations.zapierNotify] +description = "將執行詳細資料交給您的自動化。" +label = "觸發 Zap 或 Make scenario" + [portal.policies.stats] activeFor = "作用中" dataProcessed = "已處理資料" @@ -7473,6 +8282,13 @@ label = "已套用文件" description = "已設定但未觸發" label = "已暫停" +[portal.policies.table] +appliesTo = "套用至" +docs = "已強制執行的文件" +enforces = "強制執行" +policy = "政策" +status = "狀態" + [portal.policies.wizard.actions] back = "返回" cancel = "取消" @@ -7480,6 +8296,10 @@ continue = "繼續" enablePolicy = "啟用政策" saveChanges = "儲存變更" +[portal.policies.wizard.capability.classify] +desc = "從您團隊的標籤識別文件類型並加以標記,讓文件可依類別歸檔與搜尋。" +label = "分類文件" + [portal.policies.wizard.capability.compress] desc = "將文件壓縮為較小的檔案大小。" label = "減少檔案大小" @@ -7500,10 +8320,18 @@ label = "遮蔽敏感資訊" desc = "移除隱藏的 JavaScript,讓文件開啟時不會自動執行任何內容。" label = "移除主動內容" +[portal.policies.wizard.capability.timestampPdf] +desc = "使用獨立的時間戳記授權單位,證明文件在某個時間點以此確切形式存在。只會傳送雜湊值 - 文件永遠不會離開您的伺服器。" +label = "新增可信任時間戳記" + [portal.policies.wizard.capability.watermark] desc = "在每一頁加上可見標記(例如「機密」)。" label = "套用浮水印" +[portal.policies.wizard.classification] +description = "每份上傳的文件都會依內建標籤分類,並以符合的類型標記。標籤集會在整個團隊中共用。" +labelsHeading = "分類標籤" + [portal.policies.wizard.errors] noTools = "請先在工作流程中啟用至少一項工具。" saveFailed = "無法儲存政策。請再試一次。" @@ -7548,73 +8376,118 @@ setUp = "設定 {{category}} 政策" [portal.policies.wizard.workflow] description = "選擇此政策對每份處理的文件執行的動作。" -[portal.policySummary] -activeSummary = "{{active}} / {{total}} 作用中" -noRule = "尚未套用規則" -subtitle = "每份文件都會通過的常設自動化,無論由哪個 pipeline 處理。" -title = "您的 PDFs 上會執行什麼" - [portal.policySummary.action] -comingSoon = "升級至 Enterprise" -configure = "設定" setUp = "設定" -[portal.policySummary.column] -activeRule = "作用中規則" -policy = "政策" -status = "狀態" +[portal.processorFlow] +footnote = "計數為過去 24 小時內的數據。流程速度僅供示意。" +liveBadge = "即時" +stats = "{{connected}} 已連線 · 已處理 {{processed}} 個 PDF" +title = "PDF 處理器" -[portal.policySummary.empty] -description = "設定政策後,類別會顯示在這裡。" -title = "尚無政策" +[portal.processorFlow.lens] +ariaLabel = "檢視模式" +flow = "流程" +sankey = "桑基圖" -[portal.policySummary.state] -active = "作用中" -off = "關閉" +[portal.processorFlow.outcomes] +count = "{{n}} · 24h" +failed = "失敗" +heading = "結果" +success = "已交付" + +[portal.processorFlow.policies] +activeCount = "{{n}} 個啟用" +count = "{{n}} · 24h" +heading = "政策" +setUp = "設定" soon = "即將推出" -[portal.processingStatus] -managePlan = "管理方案" -volumeSuffix = "已處理 PDFs · 過去 30 天" +[portal.processorFlow.sankey] +waist = "政策 · {{n}} 個啟用" + +[portal.processorFlow.sankey.empty] +description = "連線來源並開啟政策,即可觀看文件流經處理器。" +title = "尚無流程" + +[portal.processorFlow.sources] +comingSoonTag = "連線" +editor = "Stirling PDF Editor" +heading = "來源" +perDay = "{{n}} / 24h" + +[portal.processorFlow.sources.comingSoon] +apiMcp = "API · MCP" +cloud = "Drive · Box · S3" +email = "Email intake" [portal.procurement] -reset = "重設採購(demo)" subtitle = "讓您的團隊完成評估、簽約並導入。所有文件集中在一處。" title = "採購" -[portal.procurement.action] -download = "下載" -pay = "立即付款" -request = "要求" -sign = "檢閱並簽署" -upload = "上傳" - [portal.procurement.agreement] agreeCta = "同意並訂閱" +confidential = "機密" confirm = "我已閱讀並同意 Stirling Enterprise Agreement。" -eyebrow = "協議" -intro = "一份合併協議涵蓋您的交易:Master Service Agreement、Order Form、EULA 和 Data Processing Agreement。檢閱後同意,即可接受報價並轉為承諾訂閱。" -title = "檢閱您的企業協議" +docName = "Stirling Enterprise Agreement" +docSub = "MSA、Order Form、EULA 和 DPA,合併為一份簽署文件。" +download = "下載" +downloadDraftError = "無法產生協議 PDF - 文件轉譯器無法使用。請稍後再試或聯絡支援。" +downloadError = "無法下載已簽署的協議。請再試一次。" +legalName = "法人實體名稱" +legalNamePlaceholder = "將簽署的法人實體" +loadError = "無法載入協議。請再試一次。" +loading = "正在載入協議..." +ref = "Ref {{ref}} · {{version}}" +requestChanges = "要求變更" +scrollHint = "捲動以審閱" +signatory = "簽署人姓名" +signatoryPlaceholder = "全名" +signatoryTitle = "簽署人職稱" +signatoryTitlePlaceholder = "例如 總法律顧問" +signError = "無法記錄您的簽署。請再試一次。" [portal.procurement.builder] addons = "附加項目" +addressLine1 = "地址第 1 行" +addressLine1Placeholder = "500 Howard St" +addressLine2 = "地址第 2 行" +addressLine2Placeholder = "套房、樓層、建築物" back = "返回" businessName = "企業名稱" businessNamePlaceholder = "您的公司" +city = "城市" +cityPlaceholder = "San Francisco" +completeRequired = "請先以有效的電子郵件完成必填欄位(標示 *),再產生報價。" +contactEmail = "聯絡電子郵件" +contactEmailPlaceholder = "jane@acme.com" +contactName = "聯絡人姓名" +contactNamePlaceholder = "Jane Doe" continue = "繼續" -country = "國家/地區" -countryEuro = "歐元區 (EUR €)" -countryUK = "英國 (GBP £)" -countryUS = "美國 (USD $)" +done = "完成" eula = "我已閱讀並同意 Stirling Enterprise EULA。它規範由此報價產生的協議。" generate = "產生報價" -included = "已包含" indemnification = "IP 賠償保障" indemnificationSub = "我們會依 EULA 為符合資格的 IP 索賠進行抗辯" -offlineLicense = "離線 / air-gapped 授權" -offlineLicenseSub = "供 air-gapped 自架執行個體使用的可下載授權檔" +paperEyebrow = "企業報價" +paperFor = "準備對象" +pdfSize = "PDF 大小" +poNumber = "PO 號碼" +poNumberPlaceholder = "選填" +postalCode = "郵遞區號" +postalCodePlaceholder = "94105" +posture = "治理" +posture_count = "~{{count}} 個政策" +posture_essentials = "Essentials" +posture_essentialsSub = "分類 + 分享 — 預設項目" +posture_governed = "Governed" +posture_governedSub = "新增安全性和路由" +posture_regulated = "Regulated" +posture_regulatedSub = "所有類別 — 合規、保留、擷取" qbr = "季度業務檢討" qbrSub = "您的 SE 每季檢視使用情況與 roadmap" +region = "州 / 地區" +regionPlaceholder = "California" running = "{{annual}} / 年 · {{years}} 年 {{tcv}}" s1Sub = "您的團隊,以及您預期每年處理的 PDFs。" s1Title = "您會處理多少量?" @@ -7623,6 +8496,12 @@ s2Title = "承諾與服務" s3Sub = "用於報價及其產生的協議。" s3Title = "您的詳細資料" serviceLevel = "服務等級" +size_compact = "精簡" +size_compactSub = "主要為文字,小於 1 MB" +size_heavy = "大型" +size_heavySub = "掃描或大量圖片,5 MB+" +size_standard = "標準" +size_standardSub = "文字與圖片混合,1 到 5 MB" slDedicated = "專屬" slDedicatedSub = "4 個工作小時 · 專屬客戶經理 · +30%" slPriority = "優先" @@ -7630,6 +8509,8 @@ slPrioritySub = "同一工作日 · 指定 CSM · +15%" slStandard = "標準" slStandardSub = "下一個工作日 · 共用 CSM · 已包含" stepOf = "第 {{n}} 步,共 {{total}} 步" +taxId = "VAT / Tax ID" +taxIdPlaceholder = "選填" term = "期限" termDiscount = "已套用 {{pct}}% 多年承諾折扣" title = "建立您的報價" @@ -7637,6 +8518,7 @@ training = "導入與訓練" trainingSub = "即時課程協助您的團隊開始使用" users = "使用者總數" usersPlaceholder = "例如 250" +viewEula = "閱讀" volEstimated = "根據 {{count}} 位使用者估算(每人約 2,000 份 PDFs,包含自動化)。如果您更清楚,可自行編輯。" volManual = "正在使用您提供的數字。您隨時可以依團隊規模重新估算。" volNoUsers = "不確定?輸入您的團隊規模,我們會為您估算。" @@ -7645,55 +8527,55 @@ volumePlaceholder = "例如 1,000,000" years_one = "{{count}} 年" years_other = "{{count}} 年" -[portal.procurement.docs] -count_one = "{{count}} 份文件" -count_other = "{{count}} 份文件" -done = "完成" -here = "您在這裡" -hide = "隱藏" -optional = "選用" -paidAddon = "付費附加項目" -show = "顯示" -subtitle = "在流程的每個步驟中,您需要的一切都會隨交易進展顯示出來。" -supportingSubtitle = "SOC 2、安全性審查、稅務表單等,在您的安全或採購團隊需要時即可提供。部分項目會收取一次性費用。" -supportingTitle = "支援您的評估" +[portal.procurement.documents] +agreement = "企業協議" +agreementSub = "MSA、Order Form 和 DPA 合併為一份簽署文件。" +download = "下載" +eula = "EULA 與商業條款" +eulaSub = "軟體與自助式商業條款。" +invoice = "發票" +invoiceSub = "您的第一張訂閱發票。" +laterInvoice = "接受後可用" +laterQuote = "報價發出後可用" +quote = "報價" +quoteSub = "您的逐項企業報價。" +sla = "SLA 附件" +slaSub = "支援層級和回應目標。" +subprocessors = "子處理者" +subprocessorsSub = "代表您處理資料的第三方。" +subtitle = "您的交易文件,可隨時存取。" title = "文件" -upcoming = "即將到來" +view = "檢視" [portal.procurement.error] title = "發生錯誤" [portal.procurement.hero] -company = "您的企業採購案" +barAria = "第 {{current}} 階段,共 {{total}} 階段" ctaAgreement = "檢閱並簽署協議" -ctaLive = "您已上線" -ctaPayment = "新增付款" +ctaExploring = "設定您的試用" ctaQuote = "檢閱您的報價" +ctaReviewQuote = "審閱報價" ctaTrial = "建立您的報價" +documents = "文件" eyebrow = "企業採購" +eyebrowCompany = "{{company}} Enterprise" inviteTeammates = "邀請隊友" -keyDocs = "重要文件" -nextStep = "下一步:{{action}}" -notStarted = "尚未開始" +licenseKey = "授權金鑰" +liveSub = "您的組織已佈建,授權已啟用。" +liveTitle = "您已在 Stirling Enterprise 上線" +next = "下一步:{{stage}}" open = "開啟採購" scheduleCall = "安排通話" -setup1Sub = "邀請您的隊友" -setup1Title = "部署 PDF Editor" -setup2Sub = "在編輯器和其他來源中啟用處理" -setup2Title = "連接 PDF Processor" -setup3Sub = "需要時啟用安全性、合規性、路由或保留" -setup3Title = "新增建議政策" +sentenceAgreement = "審閱並簽署協議" +sentenceLive = "佈建與推出" +sentencePayment = "採購單與付款" +sentenceQuote = "審閱並接受您的報價" +sentenceTrial = "設定您的試用工作區" [portal.procurement.journey] daysLeft_one = "剩餘 {{count}} 天" daysLeft_other = "剩餘 {{count}} 天" -engineerLabel = "您的解決方案工程師" -eyebrow = "您的推出流程" -live = "您已在 Stirling Enterprise 上線" -nextStep = "下一步:{{action}}" -subtitle = "您的解決方案工程師會陪同每個步驟。一次只需完成下一個動作;完整清單如下。" -title = "從試用到上線,一條受引導的路徑" -trialTitle = "Enterprise 試用" [portal.procurement.journeySteps.agreement] blurb = "一個簽名涵蓋 MSA、order form、EULA 和 DPA。" @@ -7727,6 +8609,9 @@ downloadError = "目前尚無法產生離線授權檔 — 請稍後再試。" downloadOffline = "下載離線授權 (.lic)" hint = "將此金鑰貼到自架執行個體以啟用,或保留作為記錄。請妥善保管。" label = "您的授權金鑰" +subtitle = "使用此金鑰啟用自行託管的執行個體,或保留作為記錄。" +title = "您的授權金鑰" +trialFileHint = "這是您的試用授權檔案。協議就緒後,請回來再次下載 — 與線上授權金鑰不同,.lic 檔案不會自行更新。" [portal.procurement.link] cta = "連結帳戶" @@ -7739,65 +8624,62 @@ description = "您的訂閱已啟用,授權已核發。您的團隊已完成 eyebrow = "上線" title = "您已在 Stirling Enterprise 上線" -[portal.procurement.locked] -description = "開始企業評估後,試用金鑰、承諾數量報價、一簽式協議、付款和文件帳本都會在這裡。" -eyebrow = "僅限 Enterprise" -talkToSales = "聯絡業務" -title = "採購流程會隨 Enterprise 開啟" - [portal.procurement.milestone] -accept = "接受並繼續" -description = "下載 PDF 與您的團隊分享,準備好時回來接受,或進行變更。" -download = "下載 PDF" downloadError = "目前尚無法下載報價 PDF — 請稍後再試。" -edit = "編輯報價" -eyebrow = "報價 {{number}}" -perYear = " / 年" -preparedFor = "為 {{company}} 準備" -tcv = "{{value}} 總合約價值" -title = "您的報價已準備就緒" - -[portal.procurement.modal] -cancel = "取消" -chooseFile = "選擇檔案" -close = "關閉" -downloadBody = "您的下載即將開始。" -downloadCta = "下載" -downloadTitle = "下載" -noFile = "未選擇檔案" -payBody = "透過 Stripe 使用卡片或銀行轉帳支付您的承諾合約。付款入帳後,您的工作區會立即佈建。" -payCta = "繼續前往 Stripe" -payTitle = "確認付款" -requestBodyFree = "我們會依需求產生此文件。確認後,您的解決方案工程師會很快寄送給您。" -requestBodyPaid = "這是付費附加項目。確認後,您的解決方案工程師會評估範圍並寄送文件。" -requestCta = "要求" -requestTitle = "要求此文件" -signBody = "開啟 Stirling Enterprise Agreement 進行電子簽署:一個簽名涵蓋 MSA、order form、EULA 和 DPA。我們會自動會簽,您將進入付款步驟。" -signCta = "開啟以簽署" -signTitle = "檢閱並簽署您的協議" -uploadBody = "將您的 PO 傳送給我們,我們會依您的條款開立發票。將 PDF 拖曳至此,或在下方選擇檔案。" -uploadCta = "上傳採購單" -uploadTitle = "上傳您的採購單" [portal.procurement.payment] description = "您的報價已被接受,授權也已啟用 — 您的團隊可以立即開始。準備好時支付第一張發票;您可以在這裡付款或下載,不需要 email。" +downloadAgreement = "下載已簽署協議" downloadInvoice = "下載發票" -simulate = "模擬已收到付款(demo)" +eyebrow = "付款" title = "訂閱已建立" viewInvoice = "檢視並支付發票" +[portal.procurement.review] +acceptCta = "接受報價" +annual = "年度費用(第 1 年)" +downloadCta = "下載報價" +poNumber = "採購單:{{po}}" +renewal = "期限結束後以 {{amount}}/年續約(+{{pct}}% CPI)" +tcv = "{{years}} 年期,預先全額支付:{{tcv}}" +validUntil = "有效至 {{date}}" + [portal.procurement.schedule] fallback = "無法載入排程工具。" fallbackLink = "在新分頁開啟排程" +loading = "正在載入排程器..." subtitle = "您的解決方案工程師會帶領您的團隊完成推出流程。請選擇適合您的時間。" title = "安排通話" -[portal.procurement.status] -action = "需要動作" -available = "可用" -complete = "完成" -pending = "待處理" -request = "依要求提供" +[portal.procurement.setup] +airgap = "隔離網路" +airgapSub = "封閉,無對外連線" +back = "返回" +businessName = "企業名稱" +businessNamePlaceholder = "Acme Corp" +cloud = "Stirling Cloud" +cloudSub = "受管理,最快開始" +continue = "繼續" +deployment = "要部署到哪裡?" +eula = "我同意 Stirling EULA 與商業條款。" +fullName = "全名" +fullNamePlaceholder = "您的姓名" +invites = "邀請團隊成員 · 選填" +invitesPlaceholder = "sam@acme.com, lee@acme.com" +scheduleCall = "安排通話" +seats = "測試人員" +seatsPlaceholder = "例如 15" +selfhost = "自行託管" +selfhostSub = "Docker 或 Kubernetes" +start = "開始試用" +stepOf = "第 {{n}} 步,共 {{total}} 步" +subtitle = "免費 14 天,無需信用卡。" +subtitleDetails = "為您的報價和協議提供一些詳細資料。" +talkFirst = "想先聊聊?" +title = "設定您的試用" +viewEula = "閱讀" +workEmail = "工作電子郵件" +workEmailPlaceholder = "you@company.com" [portal.procurement.trial] body = "延長會增加 7 天,並通知您的解決方案工程師。" @@ -7808,109 +8690,113 @@ maxed = "已達上限" subtitle = "您的免費試用持續到 {{date}}。不需要卡片。" title = "Enterprise 試用" -[portal.procurement.upsell] -homeBadge = "Enterprise" -homeBody = "承諾數量定價、全組織 SSO + SCIM + RBAC、90 天不可變稽核,以及專屬 SE。" -homeCta = "開始試用 →" -homeHeadline = "處理數百萬份 PDFs。" - -[portal.recentActivity] -title = "最近活動" -viewAll = "檢視全部" - -[portal.recentActivity.empty] -description = "Pipeline 執行、部署和代理程式事件會顯示在這裡。" -title = "這裡還沒有內容" - -[portal.search] -ariaLabel = "搜尋" -placeholder = "搜尋 Stirling — endpoints、pipelines、文件…" - -[portal.search.empty] -noActionsDescription = "快速動作可用後會顯示在這裡。" -noActionsTitle = "沒有快速動作" -noMatches = "找不到「{{query}}」的相符項目" -noMatchesDescription = "請嘗試不同關鍵字或瀏覽目錄。" - [portal.settings.groups] admin = "管理員" [portal.settings.sections] account-link = "帳戶連結" -[portal.shell.header] -accountFallback = "帳戶" -accountMenu = "帳戶選單" -darkMode = "深色模式" -lightMode = "淺色模式" -search = "搜尋" -searchPlaceholder = "搜尋…" -signOut = "登出" -switchToDark = "切換至深色主題" -switchToLight = "切換至淺色主題" - [portal.shell.sidebar] appEditor = "編輯器" appProcessor = "處理器" -brandSuffix = "Stirling Processor" -docsCount = "{{docs}} 份文件" linkAccount = "連結 Stirling 帳戶" -planEditor = "Editor 方案" -planEnterprise = "Enterprise 方案" -planProcessor = "Processor 方案" primaryNav = "主要導覽" switchApp = "切換應用程式" +[portal.shell.topbar] +closeNav = "關閉導覽" +openNav = "開啟導覽" +search = "搜尋" + [portal.sources] subtitle = "可重複使用的輸入連線,將文件送入 Stirling。設定一次連線後,即可從任意數量的政策參照它。點擊一列可查看其設定,以及使用它的政策。" title = "來源" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "連接來源" +[portal.sources.builder] +back = "返回來源" +backToTypes = "所有來源類型" +cancel = "取消" +chooseHint = "選擇文件來源。灰色顯示的連接器即將推出。" +comingSoon = "即將推出" +comingSoonHeading = "即將推出" +create = "建立來源" +createTitle = "連線來源" +delete = "刪除" +editTitle = "編輯來源" +enabled = "已啟用" +save = "儲存變更" + +[portal.sources.builder.folderAccess] +description = "資料夾自動化只能使用管理員允許的資料夾。請在資料夾存取設定中新增,然後再試一次。" +openSettings = "資料夾存取設定" +title = "不允許此資料夾" + [portal.sources.delete] body = "刪除「{{name}}」?此操作無法復原。參照它的政策需要更新。" cancel = "取消" confirm = "刪除" title = "刪除來源?" -[portal.sources.detail] -closeAriaLabel = "關閉詳細資料" -delete = "刪除來源" -docs24h = "過去 24 小時" -docs30d = "過去 30 天" -docsTotal = "總共看見" -docsTrend = "過去 30 天的文件" -documents = "文件" -edit = "編輯" -notReferenced = "未被任何政策參照,因此可以安全刪除。" -pause = "暫停" -resume = "繼續" -subtitle = "{{type}} · {{status}}" -usedBy = "使用者" - -[portal.sources.empty] -description = "連接資料夾(雲端儲存即將推出),讓您的政策有地方擷取文件。" -title = "尚未連接任何來源" - [portal.sources.kpi] inUse = "使用中" total = "連線" unused = "未使用" +[portal.sources.networkFields.connection] +helperText = "儲存的連線,包含伺服器位址和憑證。會由參照它的每個來源重複使用。" +label = "連線" + +[portal.sources.networkFields.directory] +helperText = "要輪詢的伺服器資料夾,相對於登入主目錄或分享根目錄。留空表示根目錄。" +label = "資料夾" +placeholder = "incoming/" + +[portal.sources.networkFields.mode] +helperText = "消耗會在每個政策處理完檔案後,將該檔案從伺服器移除。" +label = "讀取模式" + +[portal.sources.networkFields.mode.options] +consume = "消耗:每個檔案只處理一次" +snapshot = "快照:每次執行都重新讀取資料夾" + +[portal.sources.networkFields.recursive] +label = "資料夾深度" + +[portal.sources.networkFields.recursive.options] +all = "包含子資料夾" +top = "僅最上層" + [portal.sources.status] active = "作用中" disabled = "已停用" unused = "未使用" [portal.sources.table] +documents = "文件" source = "來源" status = "狀態" +type = "Type" usedBy = "政策" +[portal.sources.types.box] +description = "從您團隊已分享的 Box 資料夾拉取文件。" +label = "Box" + +[portal.sources.types.dropbox] +description = "從共用 Dropbox 資料夾拉取文件。" +label = "Dropbox" + [portal.sources.types.editor] +description = "您的團隊在編輯器中處理過的文件,涵蓋政策和 AI 執行。" label = "編輯器" +noPolicies = "尚無從編輯器執行的政策。" + +[portal.sources.types.email] +description = "將傳送到專用信箱的附件轉換為文件。" +label = "電子郵件收件匣" [portal.sources.types.folder] description = "監看伺服器上的目錄以取得新文件。" @@ -7943,25 +8829,80 @@ label = "資料夾深度" all = "包含子資料夾" top = "僅最上層" +[portal.sources.types.ftp] +description = "輪詢 FTP 或 FTPS 伺服器資料夾以尋找新文件。" +label = "FTP" + +[portal.sources.types.googledrive] +description = "從共用 Google Drive 資料夾拉取文件。" +label = "Google Drive" + +[portal.sources.types.network] +description = "監看 SMB 網路共用中的新文件。" +label = "網路磁碟機" + +[portal.sources.types.onedrive] +description = "監看 OneDrive 資料夾中的新文件。" +label = "OneDrive" + +[portal.sources.types.s3] +description = "從 Amazon S3 或 S3 相容儲存貯體拉取文件。" +label = "Amazon S3" + +[portal.sources.types.s3.fields.connection] +helperText = "儲存的連線,包含儲存貯體和憑證。會由參照它的每個來源和管線輸出重複使用。" +label = "連線" + +[portal.sources.types.s3.fields.mode] +helperText = "消耗會在每個政策處理完物件後,將該物件從儲存貯體移除。" +label = "讀取模式" + +[portal.sources.types.s3.fields.mode.options] +consume = "消耗:每個物件只處理一次" +snapshot = "快照:每次執行都重新讀取儲存貯體" + +[portal.sources.types.s3.fields.prefix] +helperText = "只會處理鍵以此前置詞開頭的物件。" +label = "鍵前置詞" +placeholder = "incoming/" + +[portal.sources.types.sftp] +description = "輪詢 SFTP 投遞資料夾以尋找新文件。" +label = "SFTP" + +[portal.sources.types.sharepoint] +description = "從 SharePoint 文件庫拉取文件。" +label = "SharePoint" + [portal.sources.types.unknown] label = "來源" +[portal.sources.types.webhook] +createNote = "建立此 webhook 時會產生交付 URL 和簽署密鑰。傳送者將文件 POST 到該 URL,參照的政策會在到達時執行。" +description = "透過來自任何外部系統的已簽署 HTTP POST 接收文件。" +label = "Webhook" + +[portal.sources.types.webhook.detail] +deliveryUrl = "交付 URL" +secretNote = "簽署密鑰只會在建立 webhook 時顯示一次。重新建立來源即可輪替。" + +[portal.sources.types.webhook.reveal] +copy = "複製" +done = "完成" +secret = "簽署密鑰" +secretHelp = "使用此金鑰簽署每次交付的原始主體(HMAC-SHA256),並以 X-Stirling-Signature header 傳送。" +secretWarning = "請立即複製簽署密鑰。為了您的安全,它只會顯示一次,之後無法擷取。" +title = "Webhook 已建立" +url = "交付 URL" +usage = "將每份文件以原始請求主體 POST 到交付 URL,並使用 binary content type(application/pdf 或 application/octet-stream)。參照的政策會在到達時自動執行。" + [portal.sources.wizard] -back = "返回" -cancel = "取消" -continue = "繼續" -editTitle = "編輯來源" name = "名稱" namePlaceholder = "例如 理賠收件" -save = "儲存變更" -subtitle = "第 {{current}} 步,共 {{total}} 步 · {{label}}" -title = "連接來源" type = "類型" -[portal.sources.wizard.steps] -chooseType = "選擇類型" -configure = "設定" -review = "檢閱並連接" +[portal.stepModal] +close = "關閉" [portal.usage] managePayment = "管理付款" @@ -8021,16 +8962,9 @@ summary = "擁有團隊 — 管理其成員資源和共用設定。" 2 = "透過預設政策取得 portal 存取權" 3 = "成員可執行的一切" -[portal.welcome] -ariaLabel = "歡迎使用 Stirling PDF" -badge = "Open-source" -installEditor = "安裝編輯器" -inviteTeammates = "邀請隊友" -openInBrowser = "在瀏覽器中開啟" -perks = "永久免費 · 可自架" -subtitle = "全球最安全的 PDF Editor 對各種規模的團隊免費。包含 60+ PDF 操作和 SSO。" -title = "歡迎使用" -titleAccent = "Stirling PDF" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · 無限制" [printFile] title = "列印檔案" @@ -8613,6 +9547,14 @@ text = "以 90 度為單位,將 PDF 頁面順時針或逆時針旋轉。PDF [rotate.tooltip.header] title = "旋轉設定概覽" +[ruler] +clearAll = "全部清除" +hideAllLabels = "隱藏所有標籤" +hideSmallLabels = "隱藏小標籤" +physicalValues = "實體" +scaled = "已縮放" +showAllLabels = "顯示所有標籤" + [sanitize] desc = "從 PDF 檔案移除可能有害的元素。" sanitizationResults = "淨化結果" @@ -8654,31 +9596,45 @@ label = "移除 XMP 中繼資料" [sanitize.steps] settings = "設定" -[sanitizePdf] -tags = "清理,安全,無害,移除威脅" - [sanitizePDF] title = "清理 PDF" +[sanitizePdf] +tags = "清理,安全,無害,移除威脅" + [scalePages] tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "調整頁面大小/比例" +[scaleSettings] +activeScale = "作用中比例" +apply = "套用比例" +applyCalibration = "套用校正" +calculatedScale = "計算比例" +calibrate = "校正" +calibrating = "正在校準" +calibrationDistanceRequired = "輸入大於零的實際距離" +calibrationInvalid = "無法從此測量值計算比例尺" +calibrationPaperDistance = "測得頁面距離:{{distance}}" +calibrationTitle = "校準比例尺" +calibrationTooltip = "測量已知距離以自動計算比例尺" +customScale = "自訂比例尺" +noneSet = "未設定自訂比例尺" +presets = "預設比例尺" +ratio = "比例尺比率" +ratioHelp = "比率:1 頁面單位 = X 實際單位" +ratioPlaceholder = "例如:100" +ratioPositive = "比率必須大於零" +ratioRequired = "必須輸入比率" +realDistance = "實際距離" +realDistancePlaceholder = "例如:5" +reset = "重設為預設值" +unit = "單位" +unitInvalid = "無效的單位:{{scaleUnit}}" + [scannerEffect] tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" -[ScannerImageSplit.selectText] -1 = "角度閾值:" -10 = "設定新增和移除的邊框大小,以防止輸出中的白色邊框(預設:1)。" -2 = "設定影像旋轉所需的最小絕對角度(預設:10)。" -3 = "容忍度:" -4 = "確定圍繞估計的背景顏色的顏色變化範圍(預設:30)。" -5 = "最小區域:" -6 = "設定照片的最小區域閾值(預設:10000)。" -7 = "最小輪廓區域:" -8 = "設定照片的最小輪廓區域閾值" -9 = "邊框大小:" - [scannerImageSplit] submit = "擷取相片掃描" tags = "separate,auto-detect,scans,multi-photo,organize" @@ -8710,6 +9666,18 @@ whatThisDoes = "功能說明" whatThisDoesDesc = "自動從掃描頁或合成影像中找出並擷取各張相片—無須手動裁切。" whenToUse = "適用時機" +[ScannerImageSplit.selectText] +1 = "角度閾值:" +10 = "設定新增和移除的邊框大小,以防止輸出中的白色邊框(預設:1)。" +2 = "設定影像旋轉所需的最小絕對角度(預設:10)。" +3 = "容忍度:" +4 = "確定圍繞估計的背景顏色的顏色變化範圍(預設:30)。" +5 = "最小區域:" +6 = "設定照片的最小區域閾值(預設:10000)。" +7 = "最小輪廓區域:" +8 = "設定照片的最小輪廓區域閾值" +9 = "邊框大小:" + [search] noResults = "找不到結果" placeholder = "輸入搜尋詞..." @@ -8727,46 +9695,22 @@ toolNotAvailableLocally = "您的 Stirling-PDF 伺服器已離線,且本機後 [session] expired = "您的工作階段已過期。請重新整理頁面並再試一次。" -[sessionManagement.tooltip] -header = "管理簽署工作階段" - -[sessionManagement.tooltip.addParticipants] -bullet1 = "新參與者會被加到簽署順序的末端" -bullet2 = "工作階段定稿後即無法新增參與者" -bullet3 = "每位參與者輪到時會收到通知" -description = "在定稿前,可隨時為進行中的工作階段新增更多參與者。" -title = "新增參與者" - -[sessionManagement.tooltip.finalization] -bullet1 = "完整定稿:所有參與者皆已簽署" -bullet2 = "部分定稿:部分參與者尚未簽署" -bullet3 = "未簽署的參與者將被排除在最終文件之外" -bullet4 = "定稿後即可將已簽署的 PDF 載入使用中檔案" -description = "定稿會將所有簽章合併為單一已簽署的 PDF。此操作無法復原。" -title = "工作階段定稿" - -[sessionManagement.tooltip.participantRemoval] -bullet1 = "無法移除已簽署的參與者" -bullet2 = "被移除的參與者將不再收到通知" -bullet3 = "簽署順序會自動調整" -description = "可在參與者簽署前將其移出工作階段。" -title = "移除參與者" - -[sessionManagement.tooltip.signatureOrder] -bullet1 = "每個簽章依序套用至 PDF" -bullet2 = "後續簽署者可看到先前的簽章" -bullet3 = "對於核准流程及法律保管鏈至關重要" -description = "建立工作階段時指定的順序決定誰先簽署。" -title = "簽署順序" - [settings] close = "關閉" +[settings.ai] +documents = "文件與 RAG" +general = "一般" +limits = "限制與效能" +models = "模型與提供者" +title = "AI" + [settings.configuration] advanced = "進階" database = "資料庫" endpoints = "端點" features = "功能" +folderAccess = "資料夾存取權" mcp = "MCP Server" storageSharing = "檔案儲存與共用" systemSettings = "系統設定" @@ -8800,6 +9744,8 @@ defaultPdfEditor = "預設 PDF 編輯器" defaultPdfEditorActive = "Stirling PDF 是您的預設 PDF 編輯器" defaultPdfEditorChecking = "正在檢查..." defaultPdfEditorInactive = "已由其他應用程式設為預設" +defaultPdfEditorRemind = "提醒我設為預設" +defaultPdfEditorRemindDescription = "當 Stirling PDF 不是您的預設 PDF 應用程式時顯示橫幅。" defaultPdfEditorSet = "已是預設" defaultStartupView = "啟動時的預設檢視" defaultStartupViewDescription = "選擇應用程式啟動時左側欄位中預設啟用的分頁" @@ -8817,9 +9763,18 @@ languageDescription = "選擇顯示語言" logout = "登出" setAsDefault = "設為預設" theme = "主題" +themeAccent = "重點色" +themeAccentCustom = "自訂顏色" +themeAccentCustomHint = "自訂(建議使用預設值)" +themeAccentDark = "深色模式重點色" +themeAccentDefault = "預設" +themeAccentDefaultHint = "預設主題(建議)" +themeAccentDescription = "按鈕、連結和醒目提示會遵循它,並為應用程式增添細微色調。淺色和深色各自有自己的設定。系統會使用目前啟用的設定。" +themeAccentLight = "淺色模式重點色" themeDark = "深色" themeDescription = "在淺色與深色模式間切換" themeLight = "淺色" +themeReset = "將主題還原為預設值" themeSystem = "System" title = "一般" user = "使用者" @@ -8833,6 +9788,12 @@ intro = "為您的組織啟用使用者驗證、團隊管理與工作區功能 learnMore = "在說明文件中了解更多" title = "系統管理員專用" +[settings.general.loginLanding] +description = "選擇登入 Stirling Cloud 時要開啟的應用程式。" +editor = "編輯器" +processor = "處理器" +title = "登入後" + [settings.general.mode] fullscreen = "全螢幕" sidebar = "側邊欄" @@ -9013,9 +9974,6 @@ title = "政策與隱私" [settings.preferences] title = "偏好設定" -[settings.search] -placeholder = "搜尋設定頁面..." - [settings.security] description = "更新您的密碼以保護帳號安全。" title = "安全性" @@ -9041,6 +9999,11 @@ security = "安全性" telegram = "Telegram" title = "安全性與身分驗證" +[settings.signOut] +confirm = "您確定要登出嗎?" +submit = "登出" +title = "登出" + [settings.team] title = "團隊" @@ -9156,6 +10119,7 @@ last = "最後一頁" maintainRatio = "切換維持長寬比" next = "下一頁" noSavedSigs = "尚未儲存任何簽章" +penSizePlaceholder = "Type or select pen size (1-200)" personalSigs = "個人簽章" previous = "上一頁" redo = "重做" @@ -9194,6 +10158,7 @@ backgroundRemovalFailedTitle = "背景移除失敗" hint = "上傳您的簽名 PNG 或 JPG 影像" label = "上傳簽名影像" placeholder = "選擇影像檔" +previewAlt = "Current image signature" processing = "正在處理影像…" removeBackground = "移除白色背景(設為透明)" @@ -9207,6 +10172,17 @@ saved = "在上方選擇已儲存的簽名,然後在 PDF 任意位置點選以 text = "在上方輸入您的姓名後,於 PDF 任意位置點選以放置簽名。" title = "如何加入簽名" +[sign.mobile] +createFromPhone = "Mobile upload" +description = "Scan this QR code with your phone or tablet, draw your signature, and it will appear here automatically." +error = "Connection Error" +expiryWarning = "QR Code Expiring Soon" +expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically." +instructions = "Open the camera app on your phone and scan this code. Keep this window open while you draw." +pollingError = "Error checking for the signature" +sessionCreateError = "Failed to create session" +title = "Draw on your phone" + [sign.mode] move = "移動簽名" pause = "暫停放置" @@ -9653,10 +10629,30 @@ title = "上傳至伺服器" updateButton = "更新至伺服器" uploadButton = "上傳至伺服器" +[superSearch] +all = "All" +ariaLabel = "Super search" +filtersAriaLabel = "Search filters" +hint = "Type to search" +placeholder = "Search Stirling" +showLess = "Show less" +showMore = "Show {{count}} more" + +[superSearch.group] +docs = "Docs" +files = "Files" +pages = "Pages" +processor = "Processor" +settings = "Settings" +tools = "Tools" + [survey] title = "Stirling-PDF Survey" [swagger] +clickHere = "點擊這裡" +fallback = "如果未自動開啟," +opening = "正在新分頁中開啟 Swagger UI..." tags = "api,文件,swagger,端點,開發" title = "API 文件" @@ -9764,12 +10760,39 @@ selectFilesHint = "請在「使用中的檔案」中選取檔案以執行此工 singleFileScope = "僅套用至:{{fileName}}" viewerMode = "切換到檔案編輯器以選取多個檔案。" +[toolFormat] +ANY = "任意檔案" +CBR = "漫畫書封存檔 (CBR)" +CBZ = "漫畫書封存檔 (CBZ)" +CSV = "CSV 檔案" +EBOOK = "電子書" +EMAIL = "電子郵件" +EXCEL = "試算表" +HTML = "網頁" +IMAGE = "圖片" +JAVASCRIPT = "JavaScript 檔案" +JSON = "JSON 檔案" +MARKDOWN = "Markdown 檔案" +NONE = "無檔案" +PCL = "PCL 列印檔案" +PDF = "PDF" +PDF_ENCRYPTED = "受密碼保護的 PDF" +POSTSCRIPT = "PostScript 檔案" +PPT = "簡報" +TEXT = "文字檔案" +VIDEO = "影片" +WORD = "Word 文件" +XML = "XML 檔案" +XPS = "XPS 文件" +ZIP = "ZIP 封存檔" + [toolPanel] alpha = "Alpha" backToAllTools = "返回所有工具" collapse = "收合面板" expand = "展開面板" goBack = "返回" +pdfTools = "PDF 工具" placeholder = "選擇一個工具以開始" premiumFeature = "進階功能:" toolsHeader = "工具" @@ -9806,11 +10829,6 @@ noToolsFound = "找不到工具" quickAccess = "快速存取" searchPlaceholder = "搜尋工具..." -[toolPicker.categories] -advancedTools = "進階工具" -recommendedTools = "推薦工具" -standardTools = "標準工具" - [toolPicker.subcategories] advancedFormatting = "進階格式化" automation = "自動化" @@ -9824,6 +10842,9 @@ removal = "移除" signing = "簽章" verification = "驗證" +[toolRenderer] +notFound = "Tool not found: {{tool}}" + [tools] noSearchResults = "找不到工具" noTools = "沒有可用的工具" @@ -9950,28 +10971,53 @@ title = "使用者" you = "(您)" [users.action] +cancelInvite = "取消邀請" deleteTeam = "刪除團隊" disableMfa = "重設 MFA" move = "移至團隊" reinstate = "恢復" remove = "從組織移除" +removeTeam = "從團隊移除" rename = "重新命名團隊" resetPw = "重設密碼" suspend = "停權" unlock = "解鎖帳戶" +[users.activity] +daysAgo = "{{count}} 天前" +hoursAgo = "{{count}} 小時前" +justNow = "剛剛" +minutesAgo = "{{count}} 分鐘前" +monthsAgo_one = "{{count}} 個月前" +monthsAgo_other = "{{count}} 個月前" +never = "從未" +weeksAgo_one = "{{count}} 週前" +weeksAgo_other = "{{count}} 週前" +yearsAgo_one = "{{count}} 年前" +yearsAgo_other = "{{count}} 年前" + [users.cap] addProcessor = "+ Processor" approver = "核准政策" editor = "編輯器" processor = "處理器" +[users.columns] +capabilities = "Capabilities" +email = "Email" +person = "Person" +role = "Role" +status = "Status" + [users.confirm] +cancelInviteBody = "要取消對 {{email}} 的邀請嗎?他們將無法使用目前的連結加入。" +cancelInviteTitle = "取消邀請" deleteTeamBody = "刪除 {{name}} 團隊?團隊必須先為空 — 請將其成員移至另一個團隊,且不能仍擁有任何整合設定。" deleteTeamTitle = "刪除團隊" disableMfaBody = "移除 {{name}} 的 MFA 註冊?若有要求,他們下次登入時會再次設定。" disableMfaTitle = "重設 MFA" removeBody = "永久從組織移除 {{name}}?此操作無法復原。" +removeTeamBody = "要將 {{name}} 從團隊移除嗎?他們會保留帳戶,但會失去此團隊資源的存取權。" removeTitle = "移除成員" [users.empty] @@ -9982,10 +11028,8 @@ title = "尚無成員" addToTeam = "新增至團隊" guestCount = "{{count}} 位訪客" guests = "訪客" -guestsDesc = "外部協作者,範圍限於您分享的內容。僅限編輯器。" ledBy = "由 {{owner}} 領導" org = "組織" -orgDesc = "擁有組織範圍權限與政策核准權的擁有者" owners = "{{count}} 位擁有者" team = "{{name}} 團隊" teamMeta = "{{count}} 人" @@ -10022,6 +11066,18 @@ username = "使用者名稱" usernameError = "使用者名稱至少需 3 個字元" usernamePlaceholder = "jsmith" +[users.invites] +by = "由 {{who}} 邀請" +cancel = "取消" +expiresInDays_one = "{{count}} 天後到期" +expiresInDays_other = "{{count}} 天後到期" +expiresToday = "今天到期" +title = "待處理邀請" + +[users.invites.columns] +expires = "Expires" +invitee = "Invitee" + [users.loadError] description = "連線至後端時發生錯誤,或您沒有存取權限。請再試一次。" title = "無法載入成員" @@ -10211,10 +11267,15 @@ searchPlaceholder = "搜尋附件" title = "附件" [viewer.bookmarks] +addBookmark = "Add bookmark" bookmarkTitle = "書籤標題" +bookmarkTitleRequired = "必須輸入書籤標題" closeSidebar = "關閉書籤側邊欄" collapseAll = "收合所有書籤" +empty = "No bookmarks in this document" expandAll = "展開所有書籤" +searchPlaceholder = "搜尋書籤" +title = "Bookmarks" [viewer.comments] addComment = "新增評論" @@ -10236,10 +11297,12 @@ locateAnnotation = "在文件中定位" moreActions = "更多動作" nComments_one = "{{count}} comment" nComments_other = "{{count}} comments" +noMatch = "No comments match your search" pageLabel = "第 {{page}} 頁" placingHint = "按一下頁面以放置…(取消)" removeCommentOnly = "僅移除評論" saveReply = "儲存回覆" +searchPlaceholder = "Search comments" title = "評論" typeComment = "評論" typeInsertText = "插入文字" @@ -10262,6 +11325,7 @@ unsavedDesc = "您有未儲存的變更" closeSidebar = "關閉圖層側邊欄" hideAll = "隱藏所有圖層" showAll = "顯示所有圖層" +title = "Layers" [viewer.link] delete = "刪除連結" @@ -10605,30 +11669,6 @@ title = "浮水印文字" image = "影像" text = "文字" -[wetSignature.tooltip] -header = "簽名建立方式" - -[wetSignature.tooltip.draw] -bullet1 = "可自訂筆跡顏色與粗細" -bullet2 = "可清除並重繪直到滿意為止" -bullet3 = "支援觸控裝置(平板電腦、手機)" -description = "使用滑鼠或觸控螢幕繪製手寫簽名。最適合個人化的真跡簽名。" -title = "手繪簽名" - -[wetSignature.tooltip.type] -bullet1 = "可從多種字型中選擇" -bullet2 = "可自訂文字大小與顏色" -bullet3 = "非常適合標準化簽名" -description = "透過輸入文字產生簽名。快速且一致,適用於商務文件。" -title = "文字簽名" - -[wetSignature.tooltip.upload] -bullet1 = "支援 PNG、JPG 及其他圖片格式" -bullet2 = "建議使用透明背景以獲得最佳效果" -bullet3 = "圖片將調整大小以符合簽名區域" -description = "上傳預先製作的簽名圖片。適合已有掃描簽名或公司 Logo 的情況。" -title = "上傳簽名圖片" - [workbenchBar] activeFiles = "使用中的檔案" annotations = "註解" @@ -10642,6 +11682,7 @@ exitRedaction = "離開遮蔽模式" exportAll = "匯出 PDF" exportSelected = "匯出所選頁面" formFill = "填寫表單" +hideToolbar = "Hide toolbar" multiTool = "多功能工具" panMode = "平移模式" print = "列印 PDF" @@ -10652,6 +11693,7 @@ redact = "遮蔽" rotateLeft = "向左旋轉" rotateRight = "向右旋轉" ruler = "尺規 / 測量" +rulerSettings = "比例尺設定" save = "儲存" saveAll = "全部儲存" saveAs = "另存新檔" @@ -10661,6 +11703,7 @@ selectAll = "全選" selectByNumber = "依頁碼選取" selectLanguage = "選擇語言" share = "分享" +showToolbar = "Show toolbar" toggleAnnotations = "切換註解顯示" toggleAttachments = "切換附件" toggleBookmarks = "切換書籤" diff --git a/frontend/editor/public/mstile-144x144.png b/frontend/editor/public/mstile-144x144.png new file mode 100644 index 0000000000..3c47163f60 Binary files /dev/null and b/frontend/editor/public/mstile-144x144.png differ diff --git a/frontend/editor/public/mstile-150x150.png b/frontend/editor/public/mstile-150x150.png new file mode 100644 index 0000000000..1833c9340f Binary files /dev/null and b/frontend/editor/public/mstile-150x150.png differ diff --git a/frontend/editor/public/mstile-310x150.png b/frontend/editor/public/mstile-310x150.png new file mode 100644 index 0000000000..e5ad21a345 Binary files /dev/null and b/frontend/editor/public/mstile-310x150.png differ diff --git a/frontend/editor/public/mstile-310x310.png b/frontend/editor/public/mstile-310x310.png new file mode 100644 index 0000000000..044a032aaf Binary files /dev/null and b/frontend/editor/public/mstile-310x310.png differ diff --git a/frontend/editor/public/mstile-70x70.png b/frontend/editor/public/mstile-70x70.png new file mode 100644 index 0000000000..23f118d9f1 Binary files /dev/null and b/frontend/editor/public/mstile-70x70.png differ diff --git a/frontend/editor/public/og-metadata.json b/frontend/editor/public/og-metadata.json index 3053b4fd9e..f9733bd83f 100644 --- a/frontend/editor/public/og-metadata.json +++ b/frontend/editor/public/og-metadata.json @@ -16,7 +16,7 @@ "description": "Adds signature to PDF by drawing, text or image" }, "sharedSign": { - "image": "/og_images/home.png", + "image": "/og_images/shared-sign.png", "title": "Shared Signing - Stirling PDF", "description": "Request signatures from others and track signing sessions" }, @@ -100,6 +100,11 @@ "title": "Rotate - Stirling PDF", "description": "Easily rotate your PDFs." }, + "autoRotate": { + "image": "/og_images/home.png", + "title": "Auto Rotate - Stirling PDF", + "description": "Detect each page's orientation and rotate it upright automatically." + }, "annotate": { "image": "/og_images/annotate.png", "title": "Annotate - Stirling PDF", @@ -300,16 +305,16 @@ "title": "Air-gapped Setup - Stirling PDF", "description": "Link to air-gapped setup guide" }, + "/editor": { + "image": "/og_images/home.png", + "title": "Editor - Stirling PDF", + "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" + }, "/login": { "image": "/og_images/home.png", "title": "Sign In - Stirling PDF", "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" }, - "/signup": { - "image": "/og_images/home.png", - "title": "Sign Up - Stirling PDF", - "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" - }, "/mobile-scanner": { "image": "/og_images/home.png", "title": "Mobile Scanner - Stirling PDF", @@ -551,6 +556,7 @@ "/ocr": "ocr", "/add-image": "addImage", "/rotate": "rotate", + "/auto-rotate": "autoRotate", "/annotate": "annotate", "/scanner-image-split": "scannerImageSplit", "/edit-table-of-contents": "editTableOfContents", @@ -644,8 +650,8 @@ "/overlay-pdf": "overlayPdfs", "/split-pdf-by-sections": "split", "/split-pdf-by-chapters": "split", + "/editor": "/editor", "/login": "/login", - "/signup": "/signup", "/mobile-scanner": "/mobile-scanner", "/files": "/files", "/settings": "/settings", diff --git a/frontend/editor/public/og-metadata.saas.json b/frontend/editor/public/og-metadata.saas.json index 39f6bf414e..be2e7a3e92 100644 --- a/frontend/editor/public/og-metadata.saas.json +++ b/frontend/editor/public/og-metadata.saas.json @@ -17,7 +17,7 @@ "description": "Adds signature to PDF by drawing, text or image" }, "sharedSign": { - "image": "/og_images/home.png", + "image": "/og_images/shared-sign.png", "title": "Shared Signing - Stirling PDF", "description": "Request signatures from others and track signing sessions" }, @@ -101,6 +101,11 @@ "title": "Rotate - Stirling PDF", "description": "Easily rotate your PDFs." }, + "autoRotate": { + "image": "/og_images/home.png", + "title": "Auto Rotate - Stirling PDF", + "description": "Detect each page's orientation and rotate it upright automatically." + }, "annotate": { "image": "/og_images/annotate.png", "title": "Annotate - Stirling PDF", @@ -301,16 +306,17 @@ "title": "Air-gapped Setup - Stirling PDF", "description": "Link to air-gapped setup guide" }, + "/editor": { + "image": "/og_images/saas/app-editor.png", + "title": "Stirling - The world's most secure PDF editor", + "ogTitle": "The world's most secure PDF editor", + "description": "Edit, sign, redact, and convert PDFs in your browser. Free forever, open source, and self-hostable." + }, "/login": { "image": "/og_images/home.png", "title": "Sign In - Stirling PDF", "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" }, - "/signup": { - "image": "/og_images/home.png", - "title": "Sign Up - Stirling PDF", - "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" - }, "/mobile-scanner": { "image": "/og_images/home.png", "title": "Mobile Scanner - Stirling PDF", @@ -531,17 +537,16 @@ "title": "Account Link Settings - Stirling PDF", "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" }, + "/signup": { + "image": "/og_images/home.png", + "title": "Sign Up - Stirling PDF", + "description": "The Free Adobe Acrobat alternative (10M+ Downloads)" + }, "/processor": { "image": "/og_images/saas/app-processor.png", "title": "Stirling Processor - Govern every PDF your organization touches", "ogTitle": "Govern every PDF your organization touches", "description": "Redaction, retention, and encryption policies enforced everywhere PDFs enter your org. Distribute the free Editor anywhere. 1¢ per PDF." - }, - "/editor": { - "image": "/og_images/saas/app-editor.png", - "title": "Stirling - The world's most secure PDF editor", - "ogTitle": "The world's most secure PDF editor", - "description": "Edit, sign, redact, and convert PDFs in your browser. Free forever, open source, and self-hostable." } }, "byPath": { @@ -564,6 +569,7 @@ "/ocr": "ocr", "/add-image": "addImage", "/rotate": "rotate", + "/auto-rotate": "autoRotate", "/annotate": "annotate", "/scanner-image-split": "scannerImageSplit", "/edit-table-of-contents": "editTableOfContents", @@ -657,8 +663,8 @@ "/overlay-pdf": "overlayPdfs", "/split-pdf-by-sections": "split", "/split-pdf-by-chapters": "split", + "/editor": "/editor", "/login": "/login", - "/signup": "/signup", "/mobile-scanner": "/mobile-scanner", "/files": "/files", "/settings": "/settings", @@ -703,7 +709,7 @@ "/settings/frontendThirdPartyLicenses": "/settings/frontendThirdPartyLicenses", "/settings/payg": "/settings/payg", "/settings/account-link": "/settings/account-link", - "/processor": "/processor", - "/editor": "/editor" + "/signup": "/signup", + "/processor": "/processor" } } diff --git a/frontend/editor/public/og_images/saas/app-editor.png b/frontend/editor/public/og_images/saas/app-editor.png index e89c1d060a..2c7fb2b653 100644 Binary files a/frontend/editor/public/og_images/saas/app-editor.png and b/frontend/editor/public/og_images/saas/app-editor.png differ diff --git a/frontend/editor/public/og_images/saas/app-processor.png b/frontend/editor/public/og_images/saas/app-processor.png index fd1ebfc620..2cd3cecbcd 100644 Binary files a/frontend/editor/public/og_images/saas/app-processor.png and b/frontend/editor/public/og_images/saas/app-processor.png differ diff --git a/frontend/editor/public/og_images/saas/app.png b/frontend/editor/public/og_images/saas/app.png index 9c6b300e6e..81e69b8f4c 100644 Binary files a/frontend/editor/public/og_images/saas/app.png and b/frontend/editor/public/og_images/saas/app.png differ diff --git a/frontend/editor/public/og_images/shared-sign.png b/frontend/editor/public/og_images/shared-sign.png new file mode 100644 index 0000000000..85c1558494 Binary files /dev/null and b/frontend/editor/public/og_images/shared-sign.png differ diff --git a/frontend/editor/public/safari-pinned-tab.svg b/frontend/editor/public/safari-pinned-tab.svg new file mode 100644 index 0000000000..9a7f46f2f6 --- /dev/null +++ b/frontend/editor/public/safari-pinned-tab.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/editor/scripts/generate-og-metadata.mjs b/frontend/editor/scripts/generate-og-metadata.mjs index dfd39ec609..942bd9767e 100644 --- a/frontend/editor/scripts/generate-og-metadata.mjs +++ b/frontend/editor/scripts/generate-og-metadata.mjs @@ -180,12 +180,16 @@ const humanizeLabel = (s) => .replace(/\b\w/g, (c) => c.toUpperCase()); const pageTitles = { + // The editor's own URL ("/" only routes, by role). + "/editor": "Editor", "/login": "Sign In", - "/signup": "Sign Up", "/mobile-scanner": "Mobile Scanner", "/files": "Files", "/settings": "Settings", }; + +// Routes only the SaaS build serves - self-hosted has no signup page. +const SAAS_ONLY_PAGE_TITLES = { "/signup": "Sign Up" }; for (const key of navKeys) pageTitles[`/settings/${key}`] = `${humanizeLabel(key)} Settings`; @@ -215,6 +219,14 @@ const saasManifest = { byTool: { ...byTool }, byPath: { ...byPath }, }; +for (const [routePath, label] of Object.entries(SAAS_ONLY_PAGE_TITLES)) { + saasManifest.byTool[routePath] = { + image: `/og_images/${DEFAULT_IMAGE_BASENAME}.png`, + title: `${label} - ${SITE_NAME}`, + description: SITE_DESC, + }; + saasManifest.byPath[routePath] = routePath; +} for (const [routePath, entry] of Object.entries(SAAS_ROUTE_OVERRIDES)) { saasManifest.byTool[routePath] = entry; saasManifest.byPath[routePath] = routePath; diff --git a/frontend/editor/scripts/generate-tool-api-types.mts b/frontend/editor/scripts/generate-tool-api-types.mts index f12572620a..38eb7fe7b6 100644 --- a/frontend/editor/scripts/generate-tool-api-types.mts +++ b/frontend/editor/scripts/generate-tool-api-types.mts @@ -1,7 +1,9 @@ /** - * Generates the committed frontend tool API types (toolApiTypes.ts) from the - * Java backend's OpenAPI spec, so the frontend's request shapes stay in step - * with the backend. + * Generates the committed frontend files derived from the Java OpenAPI spec: + * toolApiTypes.ts (each tool's request shape) and toolIO.ts (what it accepts and + * produces, from `@ToolIO` via the `x-stirling-io` extension). + * + * One pass over one spec, so the two cannot drift apart. */ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; @@ -10,20 +12,27 @@ import { parseArgs } from "node:util"; import { compile, type JSONSchema } from "json-schema-to-typescript"; import * as prettier from "prettier"; -// The API namespaces whose endpoints are real, callable tools. `/api/v1/filter/` -// (pipeline-only) and `/api/v1/ai/tools/` (not in the spec) are intentionally -// excluded. Extend this list when other namespaces become tools. +// The API namespaces whose endpoints a pipeline can reference. `/api/v1/ai/tools/` +// is absent from the spec, so it cannot appear here. Extend this list when other +// namespaces become tools. +// +// `/api/v1/filter/` and `/api/v1/integration/` are included even though neither is a +// user-facing tool: a stored pipeline can contain one, and ToolEndpoint keys the I/O +// table, so leaving them out would stop a chain being checked past such a step. const ALLOWED_PATH_PREFIXES = [ "/api/v1/general/", "/api/v1/misc/", "/api/v1/security/", "/api/v1/convert/", + "/api/v1/filter/", + "/api/v1/integration/", ]; -// File plumbing, not user parameters: `fileInput` is the uploaded document and -// `fileId` a server-side handle. Stripped from every generated request model. -// Named file fields (stampImage, attachments, ...) are real parameters and kept. -const BASE_FILE_FIELDS = new Set(["fileInput", "fileId"]); +// File plumbing, not user parameters: `fileInput` and `file` are the uploaded primary document +// (endpoints use one name or the other - `file` is never a second, supporting upload) and `fileId` +// a server-side handle. Stripped from every generated request model. Named supporting-file fields +// (stampImage, attachments, ...) are real parameters and kept. +const BASE_FILE_FIELDS = new Set(["fileInput", "file", "fileId"]); // The shared "upload a file or provide a file ID" wrapper schema and its two // branches. An endpoint whose body is exactly this has no parameters, so it must @@ -38,12 +47,21 @@ const FILE_WRAPPER_COMPONENTS = new Set([ const COMPONENT_REF_PREFIX = "#/components/schemas/"; -const FILE_HEADER = [ - "// AUTO-GENERATED FILE. DO NOT EDIT.", - "// Generated by editor/scripts/generate-tool-api-types.mts from the Java OpenAPI spec", - "// (SwaggerDoc.json). Regenerate with: task frontend:tool-models", +const IO_EXTENSION = "x-stirling-io"; +const IO_VOCABULARY_EXTENSION = "x-stirling-io-vocabulary"; + +function fileHeader(...extra: string[]): string { + return [ + "// AUTO-GENERATED FILE. DO NOT EDIT.", + "// Generated by editor/scripts/generate-tool-api-types.mts from the Java OpenAPI spec", + "// (SwaggerDoc.json). Regenerate with: task frontend:tool-models", + ...extra, + ].join("\n"); +} + +const FILE_HEADER = fileHeader( "// Tools that take only a file input have no parameters; their model is Record.", -].join("\n"); +); type Json = Record; @@ -56,6 +74,19 @@ function isObject(value: unknown): value is Json { return typeof value === "object" && value !== null && !Array.isArray(value); } +/** A single file upload: `type: string, format: binary` (a Java MultipartFile param). */ +function isBinaryField(schema: unknown): schema is Json { + return ( + isObject(schema) && schema.type === "string" && schema.format === "binary" + ); +} + +/** A multi file upload: an array of binary items (some specs also flag the array itself binary). */ +function isBinaryArrayField(schema: unknown): schema is Json { + if (!isObject(schema) || schema.type !== "array") return false; + return schema.format === "binary" || isBinaryField(schema.items); +} + /** * Recursively sort object keys so the output is byte-stable regardless of the * key ordering springdoc happens to emit. @@ -170,23 +201,166 @@ function computeRequired(schema: Json, properties: Json): string[] { }); } +/** + * The `x-stirling-io` declaration for every endpoint that carries one, path-sorted. + * + * Restricted to `endpoints`, the paths that become {@link ToolEndpoint}: the table is typed by + * that union, so a declaration outside it would not type-check. `dropped` is reported rather + * than discarded silently, since a tool losing its declaration this way is invisible otherwise. + */ +function collectToolIO( + paths: Json, + endpoints: Set, +): { table: Record; dropped: string[] } { + const table: Record = {}; + const dropped: string[] = []; + for (const path of Object.keys(paths).sort()) { + const pathItem = paths[path]; + if (!isObject(pathItem)) continue; + for (const operation of Object.values(pathItem)) { + if (isObject(operation) && operation[IO_EXTENSION]) { + if (endpoints.has(path)) table[path] = operation[IO_EXTENSION]; + else dropped.push(path); + } + } + } + return { table, dropped }; +} + +async function renderToolIO( + spec: Json, + table: Record, + outputPath: string, +): Promise { + if (Object.keys(table).length === 0) { + throw new Error( + `No ${IO_EXTENSION} declarations in the spec. The backend publishes these from @ToolIO; regenerate with 'task backend:swagger'.`, + ); + } + // Published separately: deriving the unions from the declarations present would shrink them + // whenever an endpoint is disabled in a build. + const vocabulary = spec[IO_VOCABULARY_EXTENSION]; + if (!isObject(vocabulary)) { + throw new Error( + `No ${IO_VOCABULARY_EXTENSION} in the spec; regenerate it from a current backend.`, + ); + } + const union = (values: string[]) => + values.map((v) => JSON.stringify(v)).join(" | "); + const formats = vocabulary.formats as string[]; + + const body = `${fileHeader()} + +/** + * What each tool endpoint accepts and produces, so a chain can be checked while it is being + * edited rather than by running it. Declared in Java with \`@ToolIO\`; see + * \`utils/toolIOCompat\` for the compatibility rules that read this table. + */ + +import { type ToolEndpoint } from "@app/types/toolApiTypes"; + +/** The kind of file a tool consumes or produces. \`ANY\` accepts or produces anything; \`NONE\` means no file at all. */ +export type ToolFormat = ${union(formats)}; + +/** Every format, for iteration. */ +export const TOOL_FORMATS = ${JSON.stringify(formats)} as const satisfies readonly ToolFormat[]; + +/** How many files go in and come out. A multi-output tool returns its results zipped, and the caller unpacks them. */ +export type ToolArity = ${union(vocabulary.arities as string[])}; + +/** One condition on a request parameter, guarding a {@link ToolIOCase}. */ +export interface ToolIOWhen { + param: string; + matches: string[]; + /** The value the endpoint uses when this parameter is absent; omitted when it has none. */ + default?: string; +} + +/** An output that applies when every condition in \`when\` holds. */ +export interface ToolIOCase { + when: ToolIOWhen[]; + produces: ToolFormat; + arity: ToolArity; +} + +/** What one endpoint accepts and produces. */ +export interface ToolIOSpec { + accepts: ToolFormat[]; + produces: ToolFormat; + arity: ToolArity; + cases?: ToolIOCase[]; +} + +/** + * What each endpoint accepts and produces, keyed by {@link ToolEndpoint} so the paths live in + * exactly one place. Partial: endpoints that manage a session, a device or a stored resource + * rather than transform a document declare nothing. + */ +export type ToolIOTable = Partial>; + +export const TOOL_IO: ToolIOTable = ${JSON.stringify(table)}; + +/** The declaration for an endpoint, or undefined when it declares none. */ +export function toolIOFor( + operation: string, + table: ToolIOTable = TOOL_IO, +): ToolIOSpec | undefined { + // The table's own keys are the valid set, so a hasOwn guard is the narrowing. + return Object.hasOwn(table, operation) + ? table[operation as ToolEndpoint] + : undefined; +} +`; + + const prettierConfig = await prettier.resolveConfig(outputPath); + return prettier.format(body, { ...prettierConfig, parser: "typescript" }); +} + +/** In check mode, fail when the committed file is out of date. */ +function writeOrCheck( + outputPath: string, + formatted: string, + check: boolean, + task: string, +): void { + if (check) { + let current = ""; + try { + current = readFileSync(outputPath, "utf-8"); + } catch { + // Missing file counts as out of date. + } + if (current !== formatted) { + throw new Error( + `${outputPath} is out of date. Run '${task}' and commit the result.`, + ); + } + return; + } + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, formatted, "utf-8"); +} + async function main(): Promise { const { values } = parseArgs({ options: { spec: { type: "string" }, output: { type: "string" }, + "io-output": { type: "string" }, check: { type: "boolean", default: false }, }, }); - if (!values.spec || !values.output) { + if (!values.spec || !values.output || !values["io-output"]) { throw new Error( - "Usage: generate-tool-api-types.mts --spec --output [--check]", + "Usage: generate-tool-api-types.mts --spec --output --io-output [--check]", ); } const specPath = resolve(values.spec); const outputPath = resolve(values.output); + const ioOutputPath = resolve(values["io-output"]); const spec = JSON.parse(readFileSync(specPath, "utf-8")) as Json; + const paths = isObject(spec.paths) ? spec.paths : {}; const components = isObject(spec.components) && isObject(spec.components.schemas) @@ -198,6 +372,9 @@ async function main(): Promise { const usedClassNames = new Set(); const pendingComponents = new Set(); const skipped: string[] = []; + // Named file fields (as File uploads) per model, so a caller can tell a file param from a scalar + // string param - which `format: binary` -> `string` would otherwise erase. + const fileFieldsByClass: Record = {}; for (const path of Object.keys(paths).sort()) { if ( @@ -248,7 +425,32 @@ async function main(): Promise { const query = queryParameters(pathItem); // Body wins over query on a name collision. const properties: Json = { ...query.props, ...bodyProps }; + // `file` is stripped as a primary-document alias (see BASE_FILE_FIELDS). That only holds while + // no endpoint uses `file` as a *supporting* upload beside a primary `fileInput`; if one ever + // does, blanket-stripping would silently drop it. Fail generation so the assumption is fixed + // here rather than shipping a lost file. + if ("file" in properties && "fileInput" in properties) { + throw new Error( + `${path} has both 'fileInput' and 'file' uploads. 'file' is stripped as a primary-document` + + " alias, which would drop it as a supporting file. Rename the supporting param or revise" + + " BASE_FILE_FIELDS handling in this generator.", + ); + } for (const field of BASE_FILE_FIELDS) delete properties[field]; + // Type each named file upload as File/File[] (not the `string` a binary format yields) via + // json-schema-to-typescript's `tsType` override, and record it. Base file fields are already + // stripped, so what remains is the real supporting-file params. + const fileFields: string[] = []; + for (const [name, prop] of Object.entries(properties)) { + if (isBinaryField(prop)) { + prop.tsType = "File"; + fileFields.push(name); + } else if (isBinaryArrayField(prop)) { + prop.tsType = "File[]"; + fileFields.push(name); + } + } + fileFieldsByClass[className] = fileFields; modelSchema.properties = properties; const required = new Set(computeRequired(modelSchema, properties)); for (const name of query.required) { @@ -267,6 +469,25 @@ async function main(): Promise { tools.push({ path, className }); } + const { table: ioDeclarations, dropped } = collectToolIO( + paths, + new Set(tools.map((tool) => tool.path)), + ); + if (dropped.length > 0) { + console.warn( + `Dropped ${dropped.length} @ToolIO declaration(s) on paths that are not tool endpoints. Add the namespace to ALLOWED_PATH_PREFIXES if a pipeline can contain these steps:\n ${dropped.join("\n ")}`, + ); + } + writeOrCheck( + ioOutputPath, + await renderToolIO(spec, ioDeclarations, ioOutputPath), + values.check ?? false, + "task frontend:tool-models", + ); + console.log( + `${values.check ? "Up to date" : "Generated"}: ${Object.keys(ioDeclarations).length} tool I/O declarations.`, + ); + // Transitively inline every referenced component into `definitions`, rewriting its refs too. const queue = [...pendingComponents]; while (queue.length > 0) { @@ -285,6 +506,7 @@ async function main(): Promise { await compileAndWrite( tools, definitions, + fileFieldsByClass, outputPath, values.check ?? false, skipped, @@ -294,6 +516,7 @@ async function main(): Promise { async function compileAndWrite( tools: DiscoveredTool[], definitions: Record, + fileFieldsByClass: Record, outputPath: string, check: boolean, skipped: string[], @@ -346,6 +569,15 @@ async function compileAndWrite( const endpointList = tools .map((t) => ` ${JSON.stringify(t.path)},`) .join("\n"); + // Endpoints that take supporting files, mapped to those file params' names. Only endpoints with at + // least one are listed, so membership answers "does this tool take extra files". + const fileFieldEntries = tools + .filter((t) => (fileFieldsByClass[t.className] ?? []).length > 0) + .map( + (t) => + ` ${JSON.stringify(t.path)}: ${JSON.stringify(fileFieldsByClass[t.className])},`, + ) + .join("\n"); const footer = [ "/** Endpoint path for a generated tool operation (the operation identity across languages). */", @@ -357,6 +589,9 @@ async function compileAndWrite( "/** Every generated tool endpoint, for iteration. */", `export const TOOL_ENDPOINTS = [\n${endpointList}\n] as const satisfies readonly ToolEndpoint[];`, "", + "/** The supporting-file parameters each endpoint accepts beyond its primary fileInput, by name. */", + `export const TOOL_FILE_FIELDS = {\n${fileFieldEntries}\n} as const satisfies Partial<\n Record\n>;`, + "", "/** Union of every generated tool request model. */", `export type ToolApiRequest = ToolApiParams[ToolEndpoint];`, ].join("\n"); @@ -368,26 +603,11 @@ async function compileAndWrite( parser: "typescript", }); - if (check) { - let current = ""; - try { - current = readFileSync(outputPath, "utf-8"); - } catch { - // Missing file counts as out of date. - } - if (current !== formatted) { - throw new Error( - `${outputPath} is out of date. Run 'task frontend:tool-models' and commit the result.`, - ); - } - console.log(`Up to date: ${tools.length} tool endpoints.`); - return; - } - - mkdirSync(dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, formatted, "utf-8"); - console.log(`Generated ${tools.length} tool endpoints -> ${outputPath}`); - if (skipped.length > 0) { + writeOrCheck(outputPath, formatted, check, "task frontend:tool-models"); + console.log( + `${check ? "Up to date" : "Generated"}: ${tools.length} tool endpoints.`, + ); + if (!check && skipped.length > 0) { console.log( `Skipped ${skipped.length} POST endpoint(s) with no request body: ${skipped.join(", ")}`, ); diff --git a/frontend/editor/scripts/lint/theme-lint.mjs b/frontend/editor/scripts/lint/theme-lint.mjs index 71a9cf07e5..97b32ef208 100644 --- a/frontend/editor/scripts/lint/theme-lint.mjs +++ b/frontend/editor/scripts/lint/theme-lint.mjs @@ -17,7 +17,7 @@ // // Structural black / white / transparent (shadows, scrims) are always allowed. -import { readFileSync, readdirSync } from "node:fs"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; import { execSync } from "node:child_process"; import { relative, resolve, join } from "node:path"; @@ -314,6 +314,10 @@ function check() { const violations = []; const primitiveValues = new Map(); const lineOf = (text, index) => text.slice(0, index).split("\n").length; + // path.relative emits backslashes on Windows; normalize so the PRIMITIVES + // comparison below matches and printed paths stay POSIX-style. + const posixRel = (name) => + relative(process.cwd(), join(THEME, name)).replaceAll("\\", "/"); // Fail if a theme .css exists that isn't registered above (readdir is only // compared here — never used to build a path passed to readFileSync). @@ -321,7 +325,7 @@ function check() { for (const name of readdirSync(THEME)) { if (name.endsWith(".css") && !known.has(name)) { violations.push({ - file: relative(process.cwd(), join(THEME, name)), + file: posixRel(name), line: 1, msg: `unregistered theme CSS — add "${name}" to THEME_FILES in theme-lint.mjs`, }); @@ -562,12 +566,23 @@ function reportToneContrast() { // App-wide guard that source CSS routes every colour through the palette. File // list from `git ls-files` (a VCS query, never a directory walk feeding a read). // primitives.css (the literal home) and generated output.css are exempt. -function checkAppCss() { - const EXEMPT = /(?:^|\/)(?:primitives\.css|output\.css)$/; - const listed = execSync("git ls-files -- editor/src", { encoding: "utf8" }) +/** + * Tracked source files that are actually present. `git ls-files` is the index + * view, so it still lists files deleted in the working tree - reading one of + * those throws ENOENT and takes the whole lint down with it. + */ +function trackedFiles() { + return execSync("git ls-files -- editor/src", { encoding: "utf8" }) .split("\n") .map((l) => l.trim()) - .filter((l) => l && l.endsWith(".css") && !EXEMPT.test(l)); + .filter((l) => l && existsSync(l)); +} + +function checkAppCss() { + const EXEMPT = /(?:^|\/)(?:primitives\.css|output\.css)$/; + const listed = trackedFiles().filter( + (l) => l.endsWith(".css") && !EXEMPT.test(l), + ); const violations = []; const lineOf = (text, index) => text.slice(0, index).split("\n").length; @@ -627,8 +642,9 @@ const CODE_EXEMPT_PATH = [ /mantineTheme|\/theme\.ts$|toolsTaxonomy|LayoutPreview|PageNumberPreview|CloudStorageIcons|BrandMarks/, /\/onboarding\//, /addStamp|addWatermark|\/tooltips\//, - /UpgradeBanner|AdminPlanSection/, - /\.test\.[jt]sx?$|\.stories\.[jt]sx?$|\/types\//, + // Stories are checked like app code; colour-as-data lines opt out with + // `theme-allow-color`. + /\.test\.[jt]sx?$|\/types\//, ]; const CODE_HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/g; @@ -669,13 +685,9 @@ function codeRgbIsColour(inner) { return k !== "0,0,0" && k !== "255,255,255"; } function checkCodeColors() { - const files = execSync("git ls-files -- editor/src", { encoding: "utf8" }) - .split("\n") - .map((l) => l.trim()) - .filter( - (l) => - /\.(ts|tsx)$/.test(l) && !CODE_EXEMPT_PATH.some((re) => re.test(l)), - ); + const files = trackedFiles().filter( + (l) => /\.(ts|tsx)$/.test(l) && !CODE_EXEMPT_PATH.some((re) => re.test(l)), + ); const violations = []; for (const rel of files) { const raw = readFileSync(rel, "utf8"); @@ -709,10 +721,7 @@ function checkCodeColors() { // reference has a definition somewhere (any source .css/.ts/.tsx) or a fallback. // Runtime-injected families (--user-*, --mantine-*, --accent-*) are out of scope. function checkTokenResolution() { - const files = execSync("git ls-files -- editor/src", { encoding: "utf8" }) - .split("\n") - .map((l) => l.trim()) - .filter((l) => /\.(css|ts|tsx)$/.test(l)); + const files = trackedFiles().filter((l) => /\.(css|ts|tsx)$/.test(l)); const DEF_RE = /(--[a-z0-9-]+)\s*:/gi; // Capture the token and the char that follows it (`,` ⇒ has a fallback). const REF_RE = /var\(\s*(--[a-z0-9-]+)\s*(,|\))/gi; @@ -740,19 +749,15 @@ const PRIMITIVE_LAYER = [ /^editor\/src\/core\/theme\//, /^editor\/src\/core\/styles\/theme\.css$/, /^editor\/src\/core\/tokens\/tokens\.css$/, - /^editor\/src\/saas\/styles\/saas-theme\.css$/, /^editor\/src\/proprietary\/auth\/ui\/auth-theme\.css$/, /^editor\/src\/core\/ui\/accents\.css$/, ]; function checkNoPrimitives() { - const files = execSync("git ls-files -- editor/src", { encoding: "utf8" }) - .split("\n") - .map((l) => l.trim()) - .filter( - (l) => - /\.(css|scss|ts|tsx)$/.test(l) && - !PRIMITIVE_LAYER.some((re) => re.test(l)), - ); + const files = trackedFiles().filter( + (l) => + /\.(css|scss|ts|tsx)$/.test(l) && + !PRIMITIVE_LAYER.some((re) => re.test(l)), + ); const REF = /var\(\s*(--p-[a-z0-9-]+)/g; const violations = []; const lineOf = (text, index) => text.slice(0, index).split("\n").length; diff --git a/frontend/editor/scripts/sync-portal-docs.mts b/frontend/editor/scripts/sync-portal-docs.mts index 08ba00137b..96f616eb46 100644 --- a/frontend/editor/scripts/sync-portal-docs.mts +++ b/frontend/editor/scripts/sync-portal-docs.mts @@ -13,7 +13,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; // tsx/node16 can't resolve the @portal alias here, so import by relative .ts path. -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import { buildManifest, type CategoryMap, diff --git a/frontend/editor/scripts/tsconfig.json b/frontend/editor/scripts/tsconfig.json index d80666471b..9377b2c7f3 100644 --- a/frontend/editor/scripts/tsconfig.json +++ b/frontend/editor/scripts/tsconfig.json @@ -8,5 +8,5 @@ // sync-portal-docs.mts imports the shared transform by its .ts path (run via tsx). "allowImportingTsExtensions": true }, - "include": ["./**/*.ts", "./**/*.mts"] + "include": ["./**/*.ts", "./**/*.mts", "../../oxlint.config.ts"] } diff --git a/frontend/editor/src-tauri/Cargo.lock b/frontend/editor/src-tauri/Cargo.lock index 6b04a564f8..fe8352f5a7 100644 --- a/frontend/editor/src-tauri/Cargo.lock +++ b/frontend/editor/src-tauri/Cargo.lock @@ -274,6 +274,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -400,6 +406,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -577,6 +592,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -859,9 +885,9 @@ checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -869,11 +895,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -883,9 +908,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -1602,11 +1627,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1616,10 +1639,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -2403,9 +2429,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" dependencies = [ "value-bag", ] @@ -3442,14 +3468,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.1", "lru-slab", - "rand 0.9.2", + "rand 0.10.2", + "rand_pcg 0.10.2", "ring", "rustc-hash", "rustls", @@ -3507,7 +3534,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", - "rand_pcg", + "rand_pcg 0.2.1", ] [[package]] @@ -3531,6 +3558,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3588,6 +3626,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_hc" version = "0.2.0" @@ -3606,6 +3650,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4146,9 +4199,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4168,22 +4221,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -4199,9 +4252,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -4253,11 +4306,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -4272,9 +4326,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", @@ -4497,7 +4551,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "stirling-pdf" version = "0.1.0" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "core-foundation 0.10.1", "core-services", "keyring", @@ -4506,7 +4560,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "objc2-pdf-kit", - "rand 0.9.2", + "rand 0.10.2", "reqwest 0.12.28", "serde", "serde_json", @@ -4529,7 +4583,7 @@ dependencies = [ "tokio", "url", "urlencoding", - "windows", + "windows 0.62.2", ] [[package]] @@ -4626,6 +4680,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4712,7 +4777,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -4800,7 +4865,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -5028,7 +5093,7 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.18", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -5155,7 +5220,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -5180,7 +5245,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -5243,7 +5308,7 @@ checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-version", ] @@ -6093,7 +6158,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -6117,7 +6182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -6173,11 +6238,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -6189,6 +6266,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -6223,7 +6309,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -6270,6 +6367,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -6430,6 +6537,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -6737,7 +6853,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/frontend/editor/src-tauri/Cargo.toml b/frontend/editor/src-tauri/Cargo.toml index ddfd8911de..f617cd0738 100644 --- a/frontend/editor/src-tauri/Cargo.toml +++ b/frontend/editor/src-tauri/Cargo.toml @@ -44,8 +44,8 @@ tiny_http = "0.12" url = "2.5" urlencoding = "2.1" sha2 = "0.11" -base64 = "0.22" -rand = "0.9" +base64 = "0.23" +rand = "0.10" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10" @@ -56,7 +56,7 @@ objc2-foundation = { version = "0.3.2", features = ["NSObject", "NSString", "NSU objc2-pdf-kit = { version = "0.3.2", features = ["PDFDocument", "objc2-app-kit"] } [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.61", features = [ +windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_System_Com", "Win32_UI_Shell", diff --git a/frontend/editor/src-tauri/icons/128x128.png b/frontend/editor/src-tauri/icons/128x128.png deleted file mode 100644 index d233685dd2..0000000000 Binary files a/frontend/editor/src-tauri/icons/128x128.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/128x128@2x.png b/frontend/editor/src-tauri/icons/128x128@2x.png index 9473eda3f1..e4f69e37ff 100644 Binary files a/frontend/editor/src-tauri/icons/128x128@2x.png and b/frontend/editor/src-tauri/icons/128x128@2x.png differ diff --git a/frontend/editor/src-tauri/icons/16x16.png b/frontend/editor/src-tauri/icons/16x16.png deleted file mode 100644 index d88e6615d0..0000000000 Binary files a/frontend/editor/src-tauri/icons/16x16.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/192x192.png b/frontend/editor/src-tauri/icons/192x192.png deleted file mode 100644 index 4219bb8403..0000000000 Binary files a/frontend/editor/src-tauri/icons/192x192.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/32x32.png b/frontend/editor/src-tauri/icons/32x32.png deleted file mode 100644 index 9e8dd8a5db..0000000000 Binary files a/frontend/editor/src-tauri/icons/32x32.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/64x64.png b/frontend/editor/src-tauri/icons/64x64.png deleted file mode 100644 index 280a9c5ac3..0000000000 Binary files a/frontend/editor/src-tauri/icons/64x64.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/Square107x107Logo.png b/frontend/editor/src-tauri/icons/Square107x107Logo.png index 840e43164a..f2ef1260e5 100644 Binary files a/frontend/editor/src-tauri/icons/Square107x107Logo.png and b/frontend/editor/src-tauri/icons/Square107x107Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square142x142Logo.png b/frontend/editor/src-tauri/icons/Square142x142Logo.png index 3a74337ff0..27e766be49 100644 Binary files a/frontend/editor/src-tauri/icons/Square142x142Logo.png and b/frontend/editor/src-tauri/icons/Square142x142Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square150x150Logo.png b/frontend/editor/src-tauri/icons/Square150x150Logo.png index 7efa08a3ae..b72682f1b7 100644 Binary files a/frontend/editor/src-tauri/icons/Square150x150Logo.png and b/frontend/editor/src-tauri/icons/Square150x150Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square284x284Logo.png b/frontend/editor/src-tauri/icons/Square284x284Logo.png index 3138c510c9..5174444203 100644 Binary files a/frontend/editor/src-tauri/icons/Square284x284Logo.png and b/frontend/editor/src-tauri/icons/Square284x284Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square30x30Logo.png b/frontend/editor/src-tauri/icons/Square30x30Logo.png index dc2fe4cfc7..72f49af68e 100644 Binary files a/frontend/editor/src-tauri/icons/Square30x30Logo.png and b/frontend/editor/src-tauri/icons/Square30x30Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square310x310Logo.png b/frontend/editor/src-tauri/icons/Square310x310Logo.png index a38038f8cc..58f31c4a9c 100644 Binary files a/frontend/editor/src-tauri/icons/Square310x310Logo.png and b/frontend/editor/src-tauri/icons/Square310x310Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square44x44Logo.png b/frontend/editor/src-tauri/icons/Square44x44Logo.png index 097839b004..a7e08945ed 100644 Binary files a/frontend/editor/src-tauri/icons/Square44x44Logo.png and b/frontend/editor/src-tauri/icons/Square44x44Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square71x71Logo.png b/frontend/editor/src-tauri/icons/Square71x71Logo.png index fb066fb32a..1d64d7ad39 100644 Binary files a/frontend/editor/src-tauri/icons/Square71x71Logo.png and b/frontend/editor/src-tauri/icons/Square71x71Logo.png differ diff --git a/frontend/editor/src-tauri/icons/Square89x89Logo.png b/frontend/editor/src-tauri/icons/Square89x89Logo.png index 00d893d7cf..7017222db0 100644 Binary files a/frontend/editor/src-tauri/icons/Square89x89Logo.png and b/frontend/editor/src-tauri/icons/Square89x89Logo.png differ diff --git a/frontend/editor/src-tauri/icons/StoreLogo.png b/frontend/editor/src-tauri/icons/StoreLogo.png index c56df3f8ad..f9a89b4c08 100644 Binary files a/frontend/editor/src-tauri/icons/StoreLogo.png and b/frontend/editor/src-tauri/icons/StoreLogo.png differ diff --git a/frontend/editor/src-tauri/icons/android-chrome-192x192.png b/frontend/editor/src-tauri/icons/android-chrome-192x192.png index 4219bb8403..55c165df4b 100644 Binary files a/frontend/editor/src-tauri/icons/android-chrome-192x192.png and b/frontend/editor/src-tauri/icons/android-chrome-192x192.png differ diff --git a/frontend/editor/src-tauri/icons/android-chrome-512x512.png b/frontend/editor/src-tauri/icons/android-chrome-512x512.png index 19bc603ec5..1b043a72ec 100644 Binary files a/frontend/editor/src-tauri/icons/android-chrome-512x512.png and b/frontend/editor/src-tauri/icons/android-chrome-512x512.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png index 6a361221e1..bb04f26bb7 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png and b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png index dd48655a9b..64d33096ba 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png and b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png index 6a361221e1..be50b5487a 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png and b/frontend/editor/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png index a82e68b38f..e0d4b496ad 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png and b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png index d563b2d258..be11e05b10 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png and b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index a82e68b38f..fba443fccd 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png and b/frontend/editor/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png index 6c28ce5994..fb4a2583fe 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png and b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png index 9b7807975c..251b991a03 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png and b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png index 6c28ce5994..06aa109793 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png and b/frontend/editor/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png index 95a8327d0c..3459a810d4 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png index 6b0bf8f45e..e6c0d49b64 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png index 95a8327d0c..3459a810d4 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png index e6603a9268..02f73102aa 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png index 503572a657..e51cd37946 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png index e6603a9268..02f73102aa 100644 Binary files a/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png and b/frontend/editor/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/frontend/editor/src-tauri/icons/icon.icns b/frontend/editor/src-tauri/icons/icon.icns deleted file mode 100644 index 983df8577c..0000000000 Binary files a/frontend/editor/src-tauri/icons/icon.icns and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/icon.ico b/frontend/editor/src-tauri/icons/icon.ico deleted file mode 100644 index b058a5591f..0000000000 Binary files a/frontend/editor/src-tauri/icons/icon.ico and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/icon.png b/frontend/editor/src-tauri/icons/icon.png deleted file mode 100644 index 5819d1b89d..0000000000 Binary files a/frontend/editor/src-tauri/icons/icon.png and /dev/null differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@1x.png index b440dda9d9..14945dd067 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index 44ec1c6bc9..a42c0f75f0 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png index 44ec1c6bc9..5045b9a17f 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png index e388901c86..2417dadd8a 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@1x.png index df4c10e2f4..e12aa0d5bd 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index 8a78c7b879..97cabce4ec 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png index 8a78c7b879..7942561423 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png index da7b0097be..58c87d5ea7 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png index 44ec1c6bc9..882b9b5d2f 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index 70f8711ff4..ac1f2800cd 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png index 70f8711ff4..cb2137c6b1 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png index 1648f9cc90..ffdac97f01 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-512@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-512@2x.png index e75780f7ff..b7eb3b3fad 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-512@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png index 1648f9cc90..d1bbbcdd09 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@3x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@3x.png index 53ff5b05dc..80aa0bbbc4 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png index 9d97d05bb3..5e18a2da14 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png index 9df1bcea80..618fb3b29d 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index 6403edcfee..e796d944b5 100644 Binary files a/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/frontend/editor/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-128.png b/frontend/editor/src-tauri/icons/linux/app-128.png new file mode 100644 index 0000000000..9a87d8abb3 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-128.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-16.png b/frontend/editor/src-tauri/icons/linux/app-16.png new file mode 100644 index 0000000000..35fc507cfc Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-16.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-192.png b/frontend/editor/src-tauri/icons/linux/app-192.png new file mode 100644 index 0000000000..36fd70e987 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-192.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-32.png b/frontend/editor/src-tauri/icons/linux/app-32.png new file mode 100644 index 0000000000..c975d69987 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-32.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-512.png b/frontend/editor/src-tauri/icons/linux/app-512.png new file mode 100644 index 0000000000..b20a797a05 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-512.png differ diff --git a/frontend/editor/src-tauri/icons/linux/app-64.png b/frontend/editor/src-tauri/icons/linux/app-64.png new file mode 100644 index 0000000000..e6138f71e3 Binary files /dev/null and b/frontend/editor/src-tauri/icons/linux/app-64.png differ diff --git a/frontend/editor/src-tauri/icons/macos/app-512.png b/frontend/editor/src-tauri/icons/macos/app-512.png new file mode 100644 index 0000000000..c5638a5270 Binary files /dev/null and b/frontend/editor/src-tauri/icons/macos/app-512.png differ diff --git a/frontend/editor/src-tauri/icons/macos/app.icns b/frontend/editor/src-tauri/icons/macos/app.icns new file mode 100644 index 0000000000..7f6dc2c6e8 Binary files /dev/null and b/frontend/editor/src-tauri/icons/macos/app.icns differ diff --git a/frontend/editor/src-tauri/icons/mstile-144x144.png b/frontend/editor/src-tauri/icons/mstile-144x144.png index ff28cf1ac6..3c47163f60 100644 Binary files a/frontend/editor/src-tauri/icons/mstile-144x144.png and b/frontend/editor/src-tauri/icons/mstile-144x144.png differ diff --git a/frontend/editor/src-tauri/icons/mstile-150x150.png b/frontend/editor/src-tauri/icons/mstile-150x150.png index c900c83ae3..4fe5606ad8 100644 Binary files a/frontend/editor/src-tauri/icons/mstile-150x150.png and b/frontend/editor/src-tauri/icons/mstile-150x150.png differ diff --git a/frontend/editor/src-tauri/icons/mstile-310x150.png b/frontend/editor/src-tauri/icons/mstile-310x150.png index 43a095f36d..9ef3b02fe7 100644 Binary files a/frontend/editor/src-tauri/icons/mstile-310x150.png and b/frontend/editor/src-tauri/icons/mstile-310x150.png differ diff --git a/frontend/editor/src-tauri/icons/mstile-310x310.png b/frontend/editor/src-tauri/icons/mstile-310x310.png index fd52bd61d2..044a032aaf 100644 Binary files a/frontend/editor/src-tauri/icons/mstile-310x310.png and b/frontend/editor/src-tauri/icons/mstile-310x310.png differ diff --git a/frontend/editor/src-tauri/icons/mstile-70x70.png b/frontend/editor/src-tauri/icons/mstile-70x70.png index 7692923f74..23f118d9f1 100644 Binary files a/frontend/editor/src-tauri/icons/mstile-70x70.png and b/frontend/editor/src-tauri/icons/mstile-70x70.png differ diff --git a/frontend/editor/src-tauri/icons/windows/app.ico b/frontend/editor/src-tauri/icons/windows/app.ico new file mode 100644 index 0000000000..2351219afa Binary files /dev/null and b/frontend/editor/src-tauri/icons/windows/app.ico differ diff --git a/frontend/editor/src-tauri/provisioner/Cargo.lock b/frontend/editor/src-tauri/provisioner/Cargo.lock index 0bc318ab62..f397ec3e03 100644 --- a/frontend/editor/src-tauri/provisioner/Cargo.lock +++ b/frontend/editor/src-tauri/provisioner/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -44,18 +44,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", @@ -64,9 +64,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", diff --git a/frontend/editor/src-tauri/src/commands/auth.rs b/frontend/editor/src-tauri/src/commands/auth.rs index b906bbbf7d..6734ba6025 100644 --- a/frontend/editor/src-tauri/src/commands/auth.rs +++ b/frontend/editor/src-tauri/src/commands/auth.rs @@ -6,7 +6,7 @@ use tauri_plugin_store::StoreExt; use tiny_http::{Response, Server}; use sha2::{Sha256, Digest}; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; -use rand::Rng; +use rand::RngExt; use rand::distr::Alphanumeric; const STORE_FILE: &str = "connection.json"; diff --git a/frontend/editor/src-tauri/src/commands/default_app.rs b/frontend/editor/src-tauri/src/commands/default_app.rs index 41c59fdd09..1ca4e0c004 100644 --- a/frontend/editor/src-tauri/src/commands/default_app.rs +++ b/frontend/editor/src-tauri/src/commands/default_app.rs @@ -171,7 +171,11 @@ fn set_default_macos() -> Result { use core_foundation::string::{CFString, CFStringRef}; use std::os::raw::c_int; - // Define the LSSetDefaultRoleHandlerForContentType function + // LSSetDefaultRoleHandlerForContentType is deprecated and has no public + // replacement for document UTIs (unlike default browser/mail). It is still + // the only one-click option. Prefer Finder Get Info → Change All when + // switching *away* from Stirling — Open With → Always Open With can trip a + // Gatekeeper false positive (LSRiskCategoryHasRedirectedBinding). #[link(name = "CoreServices", kind = "framework")] extern "C" { fn LSSetDefaultRoleHandlerForContentType( @@ -184,7 +188,6 @@ fn set_default_macos() -> Result { const K_LS_ROLES_ALL: c_int = 0xFFFFFFFF_u32 as c_int; unsafe { - // Set our app as the default handler for PDF files let pdf_uti = CFString::new("com.adobe.pdf"); let our_bundle_id = CFString::new("stirling.pdf.dev"); @@ -195,7 +198,7 @@ fn set_default_macos() -> Result { ); if status == 0 { - add_log("Successfully triggered default app dialog".to_string()); + add_log("Successfully set as default PDF handler on macOS".to_string()); Ok("set_successfully".to_string()) } else { let error_msg = format!("LaunchServices returned status: {}", status); @@ -209,6 +212,48 @@ fn set_default_macos() -> Result { // Linux Implementation // ============================================================================ +/// Installed desktop entry names vary by packaging (template vs binary name). +#[cfg(target_os = "linux")] +const LINUX_DESKTOP_CANDIDATES: &[&str] = + &["Stirling-PDF.desktop", "stirling-pdf.desktop"]; + +#[cfg(target_os = "linux")] +fn is_stirling_desktop_handler(handler: &str) -> bool { + let name = handler.trim().to_lowercase(); + name.contains("stirling") && name.ends_with(".desktop") +} + +#[cfg(target_os = "linux")] +fn linux_desktop_file_exists(name: &str) -> bool { + use std::path::PathBuf; + + let mut dirs = Vec::new(); + if let Ok(home) = std::env::var("HOME") { + dirs.push(PathBuf::from(home).join(".local/share/applications")); + } + if let Ok(xdg_data_dirs) = std::env::var("XDG_DATA_DIRS") { + for dir in xdg_data_dirs.split(':').filter(|d| !d.is_empty()) { + dirs.push(PathBuf::from(dir).join("applications")); + } + } else { + dirs.push(PathBuf::from("/usr/local/share/applications")); + dirs.push(PathBuf::from("/usr/share/applications")); + } + + dirs.iter().any(|dir| dir.join(name).is_file()) +} + +#[cfg(target_os = "linux")] +fn resolve_linux_desktop_file() -> String { + for name in LINUX_DESKTOP_CANDIDATES { + if linux_desktop_file_exists(name) { + return (*name).to_string(); + } + } + // Matches tauri.conf.json desktopTemplate + "stirling-pdf.desktop".to_string() +} + #[cfg(target_os = "linux")] fn check_default_linux() -> Result { use std::process::Command; @@ -222,18 +267,23 @@ fn check_default_linux() -> Result { let handler = String::from_utf8_lossy(&output.stdout); add_log(format!("Linux PDF handler: {}", handler.trim())); - // Check if it's our .desktop file - let is_default = handler.trim() == "stirling-pdf.desktop"; - Ok(is_default) + // Accept stirling-pdf.desktop and Stirling-PDF.desktop (and similar) + Ok(is_stirling_desktop_handler(&handler)) } #[cfg(target_os = "linux")] fn set_default_linux() -> Result { use std::process::Command; + let desktop_file = resolve_linux_desktop_file(); + add_log(format!( + "Setting Linux default PDF handler to {}", + desktop_file + )); + // Use xdg-mime to set the default application for PDF files let result = Command::new("xdg-mime") - .args(["default", "stirling-pdf.desktop", "application/pdf"]) + .args(["default", &desktop_file, "application/pdf"]) .output() .map_err(|e| format!("Failed to set default app: {}", e))?; diff --git a/frontend/editor/src-tauri/src/lib.rs b/frontend/editor/src-tauri/src/lib.rs index 8855604624..16810649fd 100644 --- a/frontend/editor/src-tauri/src/lib.rs +++ b/frontend/editor/src-tauri/src/lib.rs @@ -71,9 +71,43 @@ fn parse_launch_files(args: &[String]) -> Vec { .collect() } +// URLs the webview is allowed to show: the bundled app and dev server only. +// Anything else (file://, https://...) must never replace the app UI. +fn is_app_url(url: &tauri::Url) -> bool { + match url.scheme() { + "tauri" | "about" | "blob" | "data" => true, + "http" | "https" => matches!( + url.host_str(), + Some("tauri.localhost") | Some("localhost") | Some("127.0.0.1") + ), + _ => false, + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // WebKitGTK's DMA-BUF renderer crashes the web process on NVIDIA and some + // Wayland stacks (blank window, app dying on tool switch). Opt out unless overridden. + #[cfg(target_os = "linux")] + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + tauri::Builder::default() + .plugin( + // Dropping a file outside a dropzone makes WebKit navigate the webview to + // that file, killing the app UI and its close handler (window becomes + // unclosable). Block every off-app navigation at the Rust layer. + tauri::plugin::Builder::::new("navigation-guard") + .on_navigation(|_webview, url| { + let allowed = is_app_url(url); + if !allowed { + add_log(format!("🚫 Blocked webview navigation to: {}", url)); + } + allowed + }) + .build() + ) .plugin( tauri_plugin_log::Builder::new() .level(log::LevelFilter::Info) @@ -209,10 +243,20 @@ pub fn run() { // Use Tauri's built-in cleanup app_handle.cleanup_before_exit(); } - RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => { + RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, label, .. } => { add_log("🔄 Window close requested (will cleanup on actual exit)...".to_string()); // Don't cleanup here - let JavaScript handler prevent close if needed // Backend cleanup happens in ExitRequested when window actually closes + // + // Failsafe: if the webview somehow left the app (JS close handler gone, + // window would stay open forever), destroy the window directly. + if let Some(window) = app_handle.get_webview_window(&label) { + let off_app = window.url().map(|u| !is_app_url(&u)).unwrap_or(false); + if off_app { + add_log(format!("🚨 Webview '{}' is off-app, destroying window directly", label)); + let _ = window.destroy(); + } + } } RunEvent::WindowEvent { event: WindowEvent::DragDrop(drag_drop_event), label, .. } => { use tauri::DragDropEvent; @@ -267,3 +311,39 @@ pub fn run() { } }); } + +#[cfg(test)] +mod tests { + use super::is_app_url; + + fn allows(raw: &str) -> bool { + is_app_url(&tauri::Url::parse(raw).expect("valid url")) + } + + #[test] + fn allows_bundled_app_and_dev_server() { + assert!(allows("tauri://localhost/index.html")); + assert!(allows("http://tauri.localhost/")); + assert!(allows("http://localhost:5173/")); + assert!(allows("http://127.0.0.1:8080/api")); + assert!(allows("about:blank")); + assert!(allows("blob:http://localhost:5173/abc")); + assert!(allows("data:text/html,hi")); + } + + #[test] + fn blocks_dropped_files() { + // The #6872 lockup: webview navigating to a dropped PDF. + assert!(!allows("file:///C:/Users/me/report.pdf")); + assert!(!allows("file:///home/me/report.pdf")); + } + + #[test] + fn blocks_remote_origins() { + assert!(!allows("https://example.com/")); + assert!(!allows("http://evil.test/")); + // Look-alike hosts must not slip past the allowlist. + assert!(!allows("https://localhost.evil.test/")); + assert!(!allows("https://nottauri.localhost.evil.test/")); + } +} diff --git a/frontend/editor/src-tauri/tauri.conf.json b/frontend/editor/src-tauri/tauri.conf.json index dee2cc7023..cdd29e708f 100644 --- a/frontend/editor/src-tauri/tauri.conf.json +++ b/frontend/editor/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling PDF", "mainBinaryName": "Stirling-PDF", - "version": "2.14.2", + "version": "2.14.3", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", @@ -29,14 +29,9 @@ "publisher": "Stirling PDF Inc.", "targets": ["deb", "rpm", "appimage", "dmg", "app", "msi"], "icon": [ - "icons/icon.png", - "icons/icon.icns", - "icons/icon.ico", - "icons/16x16.png", - "icons/32x32.png", - "icons/64x64.png", - "icons/128x128.png", - "icons/192x192.png" + "icons/windows/app.ico", + "icons/macos/app.icns", + "icons/macos/app-512.png" ], "resources": ["libs/*.jar", "runtime/jre/**/*"], "fileAssociations": [ diff --git a/frontend/editor/src-tauri/tauri.linux.conf.json b/frontend/editor/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000000..54f87a037b --- /dev/null +++ b/frontend/editor/src-tauri/tauri.linux.conf.json @@ -0,0 +1,12 @@ +{ + "bundle": { + "icon": [ + "icons/linux/app-512.png", + "icons/linux/app-192.png", + "icons/linux/app-128.png", + "icons/linux/app-64.png", + "icons/linux/app-32.png", + "icons/linux/app-16.png" + ] + } +} diff --git a/frontend/editor/src-tauri/windows/wix/banner.bmp b/frontend/editor/src-tauri/windows/wix/banner.bmp index 8c120d1fe2..d6ee476bcc 100644 Binary files a/frontend/editor/src-tauri/windows/wix/banner.bmp and b/frontend/editor/src-tauri/windows/wix/banner.bmp differ diff --git a/frontend/editor/src-tauri/windows/wix/dialog.bmp b/frontend/editor/src-tauri/windows/wix/dialog.bmp index 680241e9e7..49725f82db 100644 Binary files a/frontend/editor/src-tauri/windows/wix/dialog.bmp and b/frontend/editor/src-tauri/windows/wix/dialog.bmp differ diff --git a/frontend/editor/src/assets/3rdPartyLicenses.json b/frontend/editor/src/assets/3rdPartyLicenses.json index 14d2e1fa5b..83d9af793b 100644 --- a/frontend/editor/src/assets/3rdPartyLicenses.json +++ b/frontend/editor/src/assets/3rdPartyLicenses.json @@ -10,7 +10,7 @@ { "moduleName": "@cantoo/pdf-lib", "moduleUrl": "https://github.com/cantoo-scribe/pdf-lib", - "moduleVersion": "2.6.5", + "moduleVersion": "2.8.2", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -23,186 +23,179 @@ }, { "moduleName": "@embedpdf/core", - "moduleUrl": "https://www.npmjs.com/package/@embedpdf/core", - "moduleVersion": "2.14.1", + "moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-2.15.0.tgz", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/engines", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/models", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-annotation", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-attachment", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-bookmark", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-document-manager", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-export", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-history", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-interaction-manager", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-pan", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-print", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-redaction", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-render", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-rotate", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-scroll", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-search", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-selection", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-spread", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-thumbnail", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-tiling", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-viewport", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@embedpdf/plugin-zoom", "moduleUrl": "https://github.com/embedpdf/embed-pdf-viewer", - "moduleVersion": "2.14.1", + "moduleVersion": "2.15.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@emotion/react", - "moduleUrl": "https://github.com/emotion-js/emotion/tree/main/packages/react", + "moduleUrl": "https://github.com/emotion-js/emotion.git#main", "moduleVersion": "11.14.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@emotion/styled", - "moduleUrl": "https://github.com/emotion-js/emotion/tree/main/packages/styled", + "moduleUrl": "https://github.com/emotion-js/emotion.git#main", "moduleVersion": "11.14.1", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, - { - "moduleName": "@iconify/react", - "moduleUrl": "https://github.com/iconify/iconify", - "moduleVersion": "6.0.2", - "moduleLicense": "MIT", - "moduleLicenseUrl": "https://opensource.org/licenses/MIT" - }, { "moduleName": "@mantine/core", "moduleUrl": "https://github.com/mantinedev/mantine", @@ -234,14 +227,14 @@ { "moduleName": "@mui/icons-material", "moduleUrl": "https://github.com/mui/material-ui", - "moduleVersion": "9.0.0", + "moduleVersion": "9.2.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@mui/material", "moduleUrl": "https://github.com/mui/material-ui", - "moduleVersion": "9.0.0", + "moduleVersion": "9.2.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -262,14 +255,14 @@ { "moduleName": "@stripe/react-stripe-js", "moduleUrl": "https://github.com/stripe/react-stripe-js", - "moduleVersion": "4.0.2", + "moduleVersion": "6.8.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "@stripe/stripe-js", "moduleUrl": "https://github.com/stripe/stripe-js", - "moduleVersion": "7.9.0", + "moduleVersion": "9.10.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -287,6 +280,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@tanstack/react-query", + "moduleUrl": "https://github.com/TanStack/query", + "moduleVersion": "5.101.4", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "@tanstack/react-table", + "moduleUrl": "https://github.com/TanStack/table", + "moduleVersion": "9.1.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@tanstack/react-virtual", "moduleUrl": "https://github.com/TanStack/virtual", @@ -345,7 +352,7 @@ }, { "moduleName": "autoprefixer", - "moduleUrl": "postcss/autoprefixer", + "moduleUrl": "https://github.com/postcss/autoprefixer", "moduleVersion": "10.4.27", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" @@ -353,7 +360,7 @@ { "moduleName": "axios", "moduleUrl": "https://github.com/axios/axios", - "moduleVersion": "1.15.0", + "moduleVersion": "1.18.1", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -364,17 +371,10 @@ "moduleLicense": "ISC", "moduleLicenseUrl": "https://opensource.org/licenses/ISC" }, - { - "moduleName": "globals", - "moduleUrl": "sindresorhus/globals", - "moduleVersion": "17.5.0", - "moduleLicense": "MIT", - "moduleLicenseUrl": "https://opensource.org/licenses/MIT" - }, { "moduleName": "i18next", "moduleUrl": "https://github.com/i18next/i18next", - "moduleVersion": "25.10.10", + "moduleVersion": "26.3.6", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -394,7 +394,7 @@ }, { "moduleName": "license-report", - "moduleUrl": "https://github.com/kessler/license-report", + "moduleUrl": "https://github.com/bepo65/license-report", "moduleVersion": "6.8.2", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" @@ -423,9 +423,9 @@ { "moduleName": "posthog-js", "moduleUrl": "https://github.com/PostHog/posthog-js", - "moduleVersion": "1.363.3", - "moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", - "moduleLicenseUrl": "https://github.com/PostHog/posthog-js/blob/main/LICENSE" + "moduleVersion": "1.405.2", + "moduleLicense": "(Apache-2.0 AND MIT)", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "qrcode.react", @@ -436,15 +436,15 @@ }, { "moduleName": "react", - "moduleUrl": "https://github.com/facebook/react", - "moduleVersion": "19.2.4", + "moduleUrl": "https://github.com/react/react", + "moduleVersion": "19.2.8", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "react-dom", - "moduleUrl": "https://github.com/facebook/react", - "moduleVersion": "19.2.4", + "moduleUrl": "https://github.com/react/react", + "moduleVersion": "19.2.8", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -458,13 +458,13 @@ { "moduleName": "react-i18next", "moduleUrl": "https://github.com/i18next/react-i18next", - "moduleVersion": "16.6.6", + "moduleVersion": "17.0.11", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, { "moduleName": "react-markdown", - "moduleUrl": "remarkjs/react-markdown", + "moduleUrl": "https://github.com/remarkjs/react-markdown", "moduleVersion": "9.1.0", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" @@ -479,7 +479,7 @@ { "moduleName": "react-router-dom", "moduleUrl": "https://github.com/remix-run/react-router", - "moduleVersion": "7.13.2", + "moduleVersion": "7.18.1", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, @@ -492,7 +492,7 @@ }, { "moduleName": "remark-gfm", - "moduleUrl": "remarkjs/remark-gfm", + "moduleUrl": "https://github.com/remarkjs/remark-gfm", "moduleVersion": "4.0.1", "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" @@ -518,10 +518,17 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "use-sync-external-store", + "moduleUrl": "https://github.com/facebook/react", + "moduleVersion": "1.6.0", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "web-vitals", "moduleUrl": "https://github.com/GoogleChrome/web-vitals", - "moduleVersion": "5.1.0", + "moduleVersion": "5.3.0", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" } diff --git a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx index 3b373e9c3b..638c752f9e 100644 --- a/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx +++ b/frontend/editor/src/cloud/components/shared/TeamInvitationBanner.tsx @@ -3,7 +3,7 @@ import { Group, Text } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { useTranslation } from "react-i18next"; import LocalIcon from "@app/components/shared/LocalIcon"; -import { InfoBanner } from "@app/components/shared/InfoBanner"; +import { AppBanner } from "@app/components/shared/AppBanner"; import { useSaaSTeam } from "@app/contexts/SaaSTeamContext"; /** @@ -105,7 +105,7 @@ export function TeamInvitationBanner() { ); return ( - ); } diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css index 581f3af671..9cbda9b6ca 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.css @@ -95,7 +95,7 @@ margin-bottom: 8px; } .payg-planhead__lbl--free { - color: var(--c-success); + color: var(--color-green-dark); } .payg-planhead__lbl--meter { color: var(--payg-accent); @@ -242,7 +242,7 @@ background: color-mix(in srgb, var(--c-success) 14%, transparent); } [data-mantine-color-scheme="dark"] .payg-hero__credit { - color: var(--c-success); + color: var(--color-green-dark); background: color-mix(in srgb, var(--c-success) 18%, transparent); } @@ -459,11 +459,11 @@ } .payg-gate[data-enabled="true"] .payg-gate__chip { background: color-mix(in srgb, var(--c-success) 16%, transparent); - color: var(--c-success); + color: var(--color-green-dark); } .payg-gate[data-enabled="false"] .payg-gate__chip { background: color-mix(in srgb, var(--c-danger) 16%, transparent); - color: var(--c-danger); + color: var(--color-red-dark); } .payg-gate__label { font-size: 0.8125rem; @@ -486,11 +486,11 @@ background: var(--c-surface-sunken); } .payg-gate__tag[data-variant="pause"] { - color: var(--c-danger); + color: var(--color-red-dark); background: color-mix(in srgb, var(--c-danger) 12%, transparent); } [data-mantine-color-scheme="dark"] .payg-gate__tag[data-variant="pause"] { - color: var(--c-danger); + color: var(--color-red-dark); background: color-mix(in srgb, var(--c-danger) 18%, transparent); } diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx index 7774b6bc72..da6db6e46c 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx @@ -32,9 +32,9 @@ import { prepaidSnapshotFromWallet, } from "@app/components/shared/config/configSections/usageMeters"; // Relative (not @app/*) so the co-located CSS + sibling component resolve directly. -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import "./Payg.css"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import SpendCapControl from "./SpendCapControl"; import { useTranslation } from "react-i18next"; import type { Wallet } from "@app/hooks/useWallet"; diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css index 9e89fa38eb..af391f0aeb 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.css @@ -298,7 +298,7 @@ font-size: 1rem !important; } .paygf-explainer__icon--free { - color: var(--c-success); + color: var(--color-green-dark); } .paygf-explainer__icon--paid { color: var(--payg-accent); diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx index ae3738bd5f..0df1f56d64 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx @@ -33,13 +33,13 @@ import LockIcon from "@mui/icons-material/LockOutlined"; import { useTranslation } from "react-i18next"; import { useRenderCount } from "@app/hooks/useRenderCount"; import { useWallet } from "@app/hooks/useWallet"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import "./Payg.css"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import "./PaygFree.css"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import UpgradeModal from "./UpgradeModal"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import { DocHelp } from "./Payg"; import { FreeMeterPanel, diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css index d0ed711434..b6b72f504a 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css +++ b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.css @@ -10,7 +10,7 @@ .scc { --scc-accent: var(--c-primary); - --scc-accent-text: var(--c-primary); + --scc-accent-text: var(--c-accent-text); --scc-accent-soft: color-mix(in srgb, var(--c-primary) 12%, transparent); --scc-accent-border: color-mix(in srgb, var(--c-primary) 25%, transparent); --scc-chip-bg: var(--c-surface-sunken); @@ -24,7 +24,7 @@ [data-mantine-color-scheme="dark"] .scc { /* Chip surface/border track the neutral --c-* tokens (base rule); only the brand-azure accent is tuned brighter for dark. */ - --scc-accent-text: var(--c-primary); + --scc-accent-text: var(--c-accent-text); --scc-accent-soft: color-mix(in srgb, var(--c-primary) 16%, transparent); } diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx index d6b8b336f4..6cd00b1b3e 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx @@ -12,7 +12,7 @@ import { DEFAULT_CAP_PRESETS, SpendCapControl as SharedSpendCapControl, } from "@app/billing"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import "./SpendCapControl.css"; export { DEFAULT_CAP_PRESETS }; diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx index bdbd84c6bc..f5ee5054fc 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx @@ -25,9 +25,9 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBackRounded"; import ShieldIcon from "@mui/icons-material/ShieldOutlined"; import CheckCircleIcon from "@mui/icons-material/CheckCircleRounded"; import { useTranslation } from "react-i18next"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import "./UpgradeModal.css"; -// eslint-disable-next-line no-restricted-imports +// oxlint-disable-next-line no-restricted-imports import SpendCapControl from "./SpendCapControl"; /** diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx index cb6eb511c9..c4407cae7f 100644 --- a/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx +++ b/frontend/editor/src/cloud/components/shared/config/configSections/usageMeters.tsx @@ -12,6 +12,7 @@ import { formatPeriodDate, MeterBar, meterState, + remainingMeter, } from "@app/billing"; import "@app/components/shared/config/configSections/Payg.css"; import "@app/components/shared/config/configSections/PaygFree.css"; @@ -48,7 +49,8 @@ export function useFreeSnapshot(): FreeSnapshot { export function FreeMeterPanel({ snap }: { snap: FreeSnapshot }) { const { t } = useTranslation(); - const { state, pct } = meterState(snap.billableUsed, snap.billableLimit); + const remaining = Math.max(0, snap.billableLimit - snap.billableUsed); + const { state, pct } = remainingMeter(remaining, snap.billableLimit); const stateLabel = state === "DEGRADED" ? t("payg.free.state.limitReached", "Limit reached") @@ -60,8 +62,9 @@ export function FreeMeterPanel({ snap }: { snap: FreeSnapshot }) { { + if (live !== undefined) writeCachedCredits(live); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallet]); + + return (live !== undefined ? live : seed) ?? null; +} diff --git a/frontend/editor/src/cloud/hooks/useOpenPlan.ts b/frontend/editor/src/cloud/hooks/useOpenPlan.ts new file mode 100644 index 0000000000..4d532319f6 --- /dev/null +++ b/frontend/editor/src/cloud/hooks/useOpenPlan.ts @@ -0,0 +1,13 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +/** + * Cloud editor builds open the settings modal on its Plan section, which is + * where the free grant is explained and the Processor plan is switched on. + * Routed rather than called directly because the modal is URL-driven here + * (`/settings/*`), the same path the admin tour uses to open it. + */ +export function useOpenPlan(): (() => void) | null { + const navigate = useNavigate(); + return useCallback(() => navigate("/settings/plan"), [navigate]); +} diff --git a/frontend/editor/src/cloud/hooks/useWallet.ts b/frontend/editor/src/cloud/hooks/useWallet.ts index ccef26a8bc..ed3cb2ce6b 100644 --- a/frontend/editor/src/cloud/hooks/useWallet.ts +++ b/frontend/editor/src/cloud/hooks/useWallet.ts @@ -32,6 +32,14 @@ * promise see the UI flip exactly once the new state is visible — no * intermediate flash of the old value. * + *

Freshness

+ * + * The figures drain as metered work runs, so a mounted consumer re-reads the + * wallet every {@link WALLET_POLL_MS} and again whenever the tab regains + * visibility. Those refreshes are silent — they leave {@code loading} and + * {@code error} alone and only commit fresher data — so consumers that gate on + * those flags don't flicker on a background tick. + * *

Dev preview fallback

* * When the hook is rendered outside the saas app (e.g. on {@code @@ -128,6 +136,7 @@ function reuseIfEqual(prev: Wallet | null, next: Wallet): Wallet { prev.freeAllowance !== next.freeAllowance || prev.freeRemaining !== next.freeRemaining || prev.pricePerDocMinor !== next.pricePerDocMinor || + prev.bundleRatePerCreditMinor !== next.bundleRatePerCreditMinor || prev.currency !== next.currency || prev.estimatedBillMinor !== next.estimatedBillMinor || prev.capUsd !== next.capUsd || @@ -177,6 +186,13 @@ function reuseIfEqual(prev: Wallet | null, next: Wallet): Wallet { return prev; } +/** + * How often a mounted consumer re-reads the wallet. Matches the app query + * client's staleTime, so the sidebar meter and anything cached elsewhere age + * out on the same clock. + */ +const WALLET_POLL_MS = 30_000; + export function useWallet(): UseWalletResult { // Resolved once: the dev-preview side-channel when rendered outside the real // app (saas /dev/payg-preview route), else null (every real build + desktop). @@ -200,13 +216,29 @@ export function useWallet(): UseWalletResult { // "the request fired." Cleared when no load is pending. const inFlight = useRef | null>(null); + // Set for refreshes the user didn't ask for (the poll below). Silence governs + // whether a load may RAISE `loading` / `error`, never whether it may clear + // them: consumers gate on both — the limit modals do + // `if (loading || !wallet) return null`, and Plan swaps in an error alert — + // so a background tick must not blink an open modal out or replace a working + // page over a transient failure. Clearing is always the latest request's job, + // silent or not; a silent load that skipped the clear would strand `loading` + // true after superseding a visible one, which suppresses those modals for the + // rest of the session. + const silentRefresh = useRef(false); + useEffect(() => { const reqId = ++latestReqId.current; let cancelled = false; + const silent = silentRefresh.current; + silentRefresh.current = false; + const promise = (async () => { - setLoading(true); - setError(null); + if (!silent) { + setLoading(true); + setError(null); + } if (devPreview) { const synth = devPreview.buildWallet(devPreview.role()); @@ -220,11 +252,22 @@ export function useWallet(): UseWalletResult { const res = await apiClient.get("/api/v1/payg/wallet"); if (cancelled || reqId !== latestReqId.current) return; setWallet((prev) => reuseIfEqual(prev, res.data)); + // Fresh data retires any earlier failure, including one a silent poll + // is recovering from — otherwise Plan keeps its alert over good data. + setError(null); } catch (e: unknown) { if (cancelled || reqId !== latestReqId.current) return; - console.warn("[useWallet] fetch failed", e); - setError(e instanceof Error ? e.message : "Failed to load wallet"); + if (!silent) { + console.warn("[useWallet] fetch failed", e); + setError(e instanceof Error ? e.message : "Failed to load wallet"); + } + // A failed background refresh is a non-event: the last good snapshot + // stands and the next tick self-heals, so it neither surfaces nor + // logs — otherwise an offline tab warns every WALLET_POLL_MS. } finally { + // Deliberately not gated on `silent`: whichever load is latest owns + // settling the flag, or a silent refresh that supersedes a visible one + // leaves it stuck true. if (!cancelled && reqId === latestReqId.current) { setLoading(false); } @@ -241,6 +284,46 @@ export function useWallet(): UseWalletResult { }; }, [devPreview, refetchTick]); + // The wallet drains as automation, AI and API work runs, so a figure fetched + // on mount goes stale while the user watches it. Refresh on a timer, and + // immediately on returning to the tab — coming back to a stale number is the + // case people actually notice. Hidden tabs don't poll, and the dev-preview + // wallet is synthesised locally so there is nothing to re-read. + useEffect(() => { + if (devPreview) return; + + let timer: ReturnType | undefined; + const refresh = () => { + silentRefresh.current = true; + setRefetchTick((t) => t + 1); + }; + const stop = () => { + if (timer !== undefined) { + clearInterval(timer); + timer = undefined; + } + }; + const start = () => { + stop(); + timer = setInterval(refresh, WALLET_POLL_MS); + }; + const onVisibilityChange = () => { + if (document.visibilityState === "visible") { + refresh(); + start(); + } else { + stop(); + } + }; + + if (document.visibilityState === "visible") start(); + document.addEventListener("visibilitychange", onVisibilityChange); + return () => { + stop(); + document.removeEventListener("visibilitychange", onVisibilityChange); + }; + }, [devPreview]); + const refetch = useCallback(async () => { setRefetchTick((t) => t + 1); // Snapshot the next-tick promise so the caller awaits this refetch diff --git a/frontend/editor/src/cloud/hooks/walletDevPreview.ts b/frontend/editor/src/cloud/hooks/walletDevPreview.ts index 7a1477cb4f..25bd9d261a 100644 --- a/frontend/editor/src/cloud/hooks/walletDevPreview.ts +++ b/frontend/editor/src/cloud/hooks/walletDevPreview.ts @@ -4,7 +4,7 @@ * The cloud/ layer is the SHARED hosted experience consumed by BOTH the saas * (web) and desktop (Tauri) leaves, so it must stay platform-portable: it can't * read {@code import.meta.env}, {@code window.location} or web storage directly - * (the cloud ESLint guardrail enforces this). The PAYG dev-preview route + * (the cloud oxlint guardrail enforces this). The PAYG dev-preview route * ({@code /dev/payg-preview}) is a saas-only local-design affordance that * synthesises a wallet from {@code localStorage} when the real backend isn't * mounted — all three of those banned reads. {@link useWallet} reaches that diff --git a/frontend/editor/src/core/App.tsx b/frontend/editor/src/core/App.tsx index 566e861520..81db0564b8 100644 --- a/frontend/editor/src/core/App.tsx +++ b/frontend/editor/src/core/App.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { Suspense, lazy } from "react"; import { Routes, Route } from "react-router-dom"; import { AppProviders } from "@app/components/AppProviders"; import { AppLayout } from "@app/components/AppLayout"; @@ -6,9 +6,11 @@ import { LoadingFallback } from "@app/components/shared/LoadingFallback"; import { ThemeProvider } from "@app/components/shared/ThemeProvider"; import { PreferencesProvider } from "@app/contexts/PreferencesContext"; import HomePage from "@app/pages/HomePage"; -import MobileScannerPage from "@app/pages/MobileScannerPage"; import Onboarding from "@app/components/onboarding/Onboarding"; +const MobileScannerPage = lazy(() => import("@app/pages/MobileScannerPage")); +const MobileSignPage = lazy(() => import("@app/pages/MobileSignPage")); + // Import global styles import "@app/styles/tailwind.css"; import "@app/styles/cookieconsent.css"; @@ -41,6 +43,16 @@ export default function App() { } /> + {/* Mobile signature drawing - reached from the Sign tool QR code */} + + + + } + /> + {/* All other routes need AppProviders for backend integration */} { + const simulated = getSimulatedAppConfig(); + if (simulated) return simulated; + + try { + const response = await apiClient.get( + "/api/v1/config/app-config", + { suppressErrorToast: true, skipAuthRedirect: true }, + ); + return response.data; + } catch (error) { + // 401 is an answer, not a failure: the app runs unauthenticated. + if ((error as { response?: { status?: number } })?.response?.status === 401) + return DEFAULT_APP_CONFIG; + throw error; + } +} + +export type EndpointAvailabilityMap = Record< + string, + EndpointAvailabilityDetails +>; + +/** + * Fires on app load before auth settles, so a 401 must not trigger the global + * login redirect. Callers treat a failure as "assume enabled". + */ +export async function fetchEndpointsAvailability(): Promise { + const response = await apiClient.get( + "/api/v1/config/endpoints-availability", + { suppressErrorToast: true, skipAuthRedirect: true }, + ); + return Object.fromEntries( + Object.entries(response.data).map(([name, detail]) => [ + name, + { enabled: detail?.enabled ?? true, reason: detail?.reason ?? null }, + ]), + ); +} + +export async function fetchEndpointEnabled(endpoint: string): Promise { + const response = await apiClient.get( + `/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, + ); + return response.data; +} + +export interface FooterInfo { + analyticsEnabled?: boolean; + termsAndConditions?: string; + privacyPolicy?: string; + accessibilityStatement?: string; + cookiePolicy?: string; + impressum?: string; +} + +/** Public — no authentication required. */ +export async function fetchFooterInfo(): Promise { + try { + const response = await apiClient.get( + "/api/v1/ui-data/footer-info", + { suppressErrorToast: true }, + ); + return response.data; + } catch (error) { + // Toasts are suppressed here, so the failure would otherwise be silent. + console.error("[api/config] footer-info failed:", error); + throw error; + } +} + +export async function fetchGroupEnabled(group: string): Promise { + const response = await apiClient.get( + `/api/v1/config/group-enabled?group=${encodeURIComponent(group)}`, + ); + return response.data; +} diff --git a/frontend/editor/src/core/api/users.ts b/frontend/editor/src/core/api/users.ts new file mode 100644 index 0000000000..851757d8db --- /dev/null +++ b/frontend/editor/src/core/api/users.ts @@ -0,0 +1,8 @@ +import apiClient from "@app/services/apiClient"; +import { UserSummary } from "@app/types/signingSession"; + +export async function fetchUsers(): Promise { + const response = await apiClient.get("/api/v1/user/users"); + // A proxy can answer 200 with an HTML login page; callers assume an array. + return Array.isArray(response.data) ? response.data : []; +} diff --git a/frontend/editor/src/core/assets/Brand.stories.tsx b/frontend/editor/src/core/assets/Brand.stories.tsx index ac144c88b1..b70d30cca1 100644 --- a/frontend/editor/src/core/assets/Brand.stories.tsx +++ b/frontend/editor/src/core/assets/Brand.stories.tsx @@ -13,6 +13,10 @@ import classicBlack from "@app/assets/brand/classic-logo/StirlingPDFLogoBlackTex import classicWhite from "@app/assets/brand/classic-logo/StirlingPDFLogoWhiteText.svg"; import classicGrey from "@app/assets/brand/classic-logo/StirlingPDFLogoGreyText.svg"; +// Fixed swatch so the light-on-dark mark variant previews on a dark +// surface in either theme. +const DARK_SWATCH = "#1a1a1a"; // theme-allow-color fixed preview swatch + type Asset = { label: string; src: string; onDark?: boolean }; type VariantSet = { variant: string; mark: Asset[]; wordmark: Asset[] }; @@ -55,15 +59,13 @@ function Swatch({ label, src, onDark, h }: Asset & { h: number }) { padding: 16, minWidth: 140, borderRadius: 8, - border: "1px solid rgba(128,128,128,0.25)", - background: onDark ? "#1a1a1a" : "#ffffff", + border: "1px solid var(--c-border)", + background: onDark ? DARK_SWATCH : "#ffffff", }} > {label} -
+
{label}
diff --git a/frontend/editor/src/core/assets/brand/branding-logo/logo-mark.svg b/frontend/editor/src/core/assets/brand/branding-logo/logo-mark.svg new file mode 100644 index 0000000000..de7d337f91 --- /dev/null +++ b/frontend/editor/src/core/assets/brand/branding-logo/logo-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/editor/src/core/assets/brand/branding-logo/wordmark-dark.svg b/frontend/editor/src/core/assets/brand/branding-logo/wordmark-dark.svg new file mode 100644 index 0000000000..5c29f32920 --- /dev/null +++ b/frontend/editor/src/core/assets/brand/branding-logo/wordmark-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/editor/src/core/assets/brand/branding-logo/wordmark-light.svg b/frontend/editor/src/core/assets/brand/branding-logo/wordmark-light.svg new file mode 100644 index 0000000000..a9eeaa9e74 --- /dev/null +++ b/frontend/editor/src/core/assets/brand/branding-logo/wordmark-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png index 3cee859e7f..ce40d97faf 100644 Binary files a/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png and b/frontend/editor/src/core/assets/brand/classic-logo/Firstpage.png differ diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg b/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg index a19eaabc9c..4556168c5f 100644 --- a/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg +++ b/frontend/editor/src/core/assets/brand/classic-logo/logo-tooltip.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo192.png b/frontend/editor/src/core/assets/brand/classic-logo/logo192.png index 08101ad33c..6c8d372c56 100644 Binary files a/frontend/editor/src/core/assets/brand/classic-logo/logo192.png and b/frontend/editor/src/core/assets/brand/classic-logo/logo192.png differ diff --git a/frontend/editor/src/core/assets/brand/classic-logo/logo512.png b/frontend/editor/src/core/assets/brand/classic-logo/logo512.png index 1f7fe384fd..b71e8b11aa 100644 Binary files a/frontend/editor/src/core/assets/brand/classic-logo/logo512.png and b/frontend/editor/src/core/assets/brand/classic-logo/logo512.png differ diff --git a/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png b/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png index f12133f4f7..dab3d43aa3 100644 Binary files a/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png and b/frontend/editor/src/core/assets/brand/modern-logo/Firstpage.png differ diff --git a/frontend/editor/src/core/assets/brand/modern-logo/logo192.png b/frontend/editor/src/core/assets/brand/modern-logo/logo192.png index 2994ca293a..2019b93d99 100644 Binary files a/frontend/editor/src/core/assets/brand/modern-logo/logo192.png and b/frontend/editor/src/core/assets/brand/modern-logo/logo192.png differ diff --git a/frontend/editor/src/core/assets/brand/modern-logo/logo512.png b/frontend/editor/src/core/assets/brand/modern-logo/logo512.png index b481550734..90934019e8 100644 Binary files a/frontend/editor/src/core/assets/brand/modern-logo/logo512.png and b/frontend/editor/src/core/assets/brand/modern-logo/logo512.png differ diff --git a/frontend/editor/src/core/assets/login/authentik.svg b/frontend/editor/src/core/assets/login/authentik.svg index 26dc0189ef..4ed18d49b4 100644 --- a/frontend/editor/src/core/assets/login/authentik.svg +++ b/frontend/editor/src/core/assets/login/authentik.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/github.svg b/frontend/editor/src/core/assets/login/github.svg index 1174b67928..41c82d53fe 100644 --- a/frontend/editor/src/core/assets/login/github.svg +++ b/frontend/editor/src/core/assets/login/github.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/microsoft.svg b/frontend/editor/src/core/assets/login/microsoft.svg index fc1130cbb2..691300857f 100644 --- a/frontend/editor/src/core/assets/login/microsoft.svg +++ b/frontend/editor/src/core/assets/login/microsoft.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/assets/login/oidc.svg b/frontend/editor/src/core/assets/login/oidc.svg index 440b54487c..6c697d709a 100644 --- a/frontend/editor/src/core/assets/login/oidc.svg +++ b/frontend/editor/src/core/assets/login/oidc.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/frontend/editor/src/core/auth/UseSession.tsx b/frontend/editor/src/core/auth/UseSession.tsx index ee3314606c..207c4da786 100644 --- a/frontend/editor/src/core/auth/UseSession.tsx +++ b/frontend/editor/src/core/auth/UseSession.tsx @@ -26,6 +26,14 @@ export interface AuthContextType { error: Error | null; signOut: () => Promise; refreshSession: () => Promise; + /** + * Session-level permission flags, provided by layers whose auth knows them + * (proprietary's Spring session carries both). Absent in core OSS, which + * has no auth context — consumers must treat undefined as "not granted" + * and fall back to app-config gates. + */ + isAdmin?: boolean; + portalAccess?: boolean; } /** diff --git a/frontend/editor/src/core/components/AppLayout.stories.tsx b/frontend/editor/src/core/components/AppLayout.stories.tsx new file mode 100644 index 0000000000..69aceef9d7 --- /dev/null +++ b/frontend/editor/src/core/components/AppLayout.stories.tsx @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AppLayout } from "@app/components/AppLayout"; +import { BannerProvider, useBanner } from "@app/contexts/BannerContext"; +import { NavigationProvider } from "@app/contexts/NavigationContext"; +import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider"; +import { AppBanner } from "@app/components/shared/AppBanner"; + +const meta = { + title: "Components/AppLayout", + component: AppLayout, + parameters: { layout: "fullscreen" }, + // AppLayout reads the active banner from BannerContext and renders + // NavigationWarningModal + LoginAgreementModal, which need the navigation + // guard (backed by the tool registry) mounted above them. + decorators: [ + (Story) => ( + + + + + + + + ), + ], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +const sampleContent = ( +
+

Workbench

+

Tool panels and file previews render in this area.

+
+); + +export const Default: Story = { + args: { + children: sampleContent, + }, +}; + +// Calls BannerContext's setBanner on mount so the story can exercise +// AppLayout's height-adjustment behaviour (the child area shrinks to make +// room for the banner) without adding a banner prop to AppLayout itself. +function BannerSetter() { + const { setBanner } = useBanner(); + useEffect(() => { + setBanner( + , + ); + return () => setBanner(null); + }, [setBanner]); + return null; +} + +export const WithBanner: Story = { + args: { + children: sampleContent, + }, + decorators: [ + (Story) => ( + <> + + + + ), + ], +}; diff --git a/frontend/editor/src/core/components/AppLayout.tsx b/frontend/editor/src/core/components/AppLayout.tsx index 38ab1f1907..48c4cb9149 100644 --- a/frontend/editor/src/core/components/AppLayout.tsx +++ b/frontend/editor/src/core/components/AppLayout.tsx @@ -22,7 +22,7 @@ export function AppLayout({ children }: AppLayoutProps) { } `}
{banner}
{children}
diff --git a/frontend/editor/src/core/components/AppProviders.tsx b/frontend/editor/src/core/components/AppProviders.tsx index f050ae7f9b..01b2e42a82 100644 --- a/frontend/editor/src/core/components/AppProviders.tsx +++ b/frontend/editor/src/core/components/AppProviders.tsx @@ -1,4 +1,6 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode, useEffect, useState } from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { createAppQueryClient } from "@app/query/queryClient"; import { ThemeProvider } from "@app/components/shared/ThemeProvider"; import { FileContextProvider } from "@app/contexts/FileContext"; import { NavigationProvider } from "@app/contexts/NavigationContext"; @@ -119,70 +121,73 @@ export function AppProviders({ appConfigRetryOptions, appConfigProviderProps, }: AppProvidersProps) { + const [queryClient] = useState(createAppQueryClient); return ( - - - - - - - - - - {/* Auto-popup on startup when a newer Stirling-PDF release is available. - No-ops inside Tauri — the desktop popup handles that flow. */} - - + + + + + - - - - - - - - - - - - - - - - - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - - + + + + + {/* Auto-popup on startup when a newer Stirling-PDF release is available. + No-ops inside Tauri — the desktop popup handles that flow. */} + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/editor/src/core/components/StorageStatsCard.stories.tsx b/frontend/editor/src/core/components/StorageStatsCard.stories.tsx new file mode 100644 index 0000000000..d8c61ec3b3 --- /dev/null +++ b/frontend/editor/src/core/components/StorageStatsCard.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import StorageStatsCard from "@app/components/StorageStatsCard"; +import { StorageStats } from "@app/services/fileStorage"; + +const storageStats: StorageStats = { + used: 128 * 1024 * 1024, + available: 512 * 1024 * 1024, + fileCount: 12, + quota: 512 * 1024 * 1024, +}; + +const meta = { + title: "Components/StorageStatsCard", + component: StorageStatsCard, + parameters: { layout: "padded" }, + args: { + storageStats, + filesCount: 12, + onClearAll: () => {}, + onReloadFiles: () => {}, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const NearingQuota: Story = { + args: { + storageStats: { + ...storageStats, + used: 460 * 1024 * 1024, + fileCount: 48, + }, + filesCount: 48, + }, +}; + +export const NoQuota: Story = { + args: { + storageStats: { + used: 32 * 1024 * 1024, + available: 0, + fileCount: 3, + }, + filesCount: 3, + }, +}; diff --git a/frontend/editor/src/core/components/StorageStatsCard.tsx b/frontend/editor/src/core/components/StorageStatsCard.tsx index 186b56c4ca..d075e053b2 100644 --- a/frontend/editor/src/core/components/StorageStatsCard.tsx +++ b/frontend/editor/src/core/components/StorageStatsCard.tsx @@ -39,6 +39,7 @@ const StorageStatsCard: React.FC = ({ {storageStats.quota && ( 80 diff --git a/frontend/editor/src/core/components/annotation/providers/PDFAnnotationProvider.tsx b/frontend/editor/src/core/components/annotation/providers/PDFAnnotationProvider.tsx index bbc6484f2e..a009e3d1a1 100644 --- a/frontend/editor/src/core/components/annotation/providers/PDFAnnotationProvider.tsx +++ b/frontend/editor/src/core/components/annotation/providers/PDFAnnotationProvider.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, ReactNode } from "react"; +import { SignParameters } from "@app/hooks/tools/sign/useSignParameters"; interface PDFAnnotationContextValue { // Drawing mode management @@ -22,8 +23,8 @@ interface PDFAnnotationContextValue { isPlacementMode: boolean; // Signature configuration - signatureConfig: any | null; - setSignatureConfig: (config: any | null) => void; + signatureConfig: SignParameters | null; + setSignatureConfig: (config: SignParameters | null) => void; } const PDFAnnotationContext = createContext< @@ -43,8 +44,8 @@ interface PDFAnnotationProviderProps { storeImageData: (id: string, data: string) => void; getImageData: (id: string) => string | undefined; isPlacementMode: boolean; - signatureConfig: any | null; - setSignatureConfig: (config: any | null) => void; + signatureConfig: SignParameters | null; + setSignatureConfig: (config: SignParameters | null) => void; } export const PDFAnnotationProvider: React.FC = ({ diff --git a/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.stories.tsx b/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.stories.tsx new file mode 100644 index 0000000000..9b896a62d4 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.stories.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { BaseAnnotationTool } from "@app/components/annotation/shared/BaseAnnotationTool"; +import { PDFAnnotationProvider } from "@app/components/annotation/providers/PDFAnnotationProvider"; +import { SignatureProvider } from "@app/contexts/SignatureContext"; + +// BaseAnnotationTool reads usePDFAnnotation()/useSignature() for undo/redo and +// placement wiring — stub both providers so the story can mount standalone. +const StoryProviders = ({ children }: { children: ReactNode }) => ( + + {}} + deactivateDrawMode={() => {}} + activateSignaturePlacementMode={() => {}} + activateDeleteMode={() => {}} + updateDrawSettings={() => {}} + undo={() => {}} + redo={() => {}} + storeImageData={() => {}} + getImageData={() => undefined} + isPlacementMode={false} + signatureConfig={null} + setSignatureConfig={() => {}} + > + {children} + + +); + +// BaseAnnotationTool clones its child with tool-specific props (selectedColor, +// signatureData, onSignatureDataChange, onColorSwatchClick, disabled) — a real +// tool component absorbs these; a bare DOM element would just log prop warnings. +const ToolContent = (_props: Record) => ( +
Tool content
+); + +const meta = { + title: "Annotation/BaseAnnotationTool", + component: BaseAnnotationTool, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + config: { + enableImageUpload: true, + showPlaceButton: true, + placeButtonText: "Place Image", + }, + children: , + }, +}; + +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.tsx b/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.tsx index 4c0862a012..e611ba4b83 100644 --- a/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.tsx +++ b/frontend/editor/src/core/components/annotation/shared/BaseAnnotationTool.tsx @@ -14,9 +14,17 @@ export interface AnnotationToolConfig { placeButtonText?: string; } +interface InjectedAnnotationToolProps { + selectedColor: string; + signatureData: string | null; + onSignatureDataChange: (data: string | null) => void; + onColorSwatchClick: () => void; + disabled: boolean; +} + interface BaseAnnotationToolProps { config: AnnotationToolConfig; - children: React.ReactNode; + children: React.ReactElement>; onSignatureDataChange?: (data: string | null) => void; disabled?: boolean; } @@ -90,7 +98,7 @@ export const BaseAnnotationTool: React.FC = ({ /> {/* Tool Content */} - {React.cloneElement(children as React.ReactElement, { + {React.cloneElement(children, { selectedColor, signatureData, onSignatureDataChange: handleSignatureDataChange, diff --git a/frontend/editor/src/core/components/annotation/shared/ColorControl.stories.tsx b/frontend/editor/src/core/components/annotation/shared/ColorControl.stories.tsx new file mode 100644 index 0000000000..fff7ffc224 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/ColorControl.stories.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ColorControl } from "@app/components/annotation/shared/ColorControl"; + +const meta: Meta = { + title: "Annotation/ColorControl", + component: ColorControl, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +function ColorControlDemo({ + initialColor = "#ff0000", + disabled, +}: { + initialColor?: string; + disabled?: boolean; +}) { + const [color, setColor] = useState(initialColor); + return ( + + ); +} + +export const Default: Story = { render: () => }; + +export const Disabled: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/annotation/shared/ColorPicker.stories.tsx b/frontend/editor/src/core/components/annotation/shared/ColorPicker.stories.tsx new file mode 100644 index 0000000000..1ffb5d76d2 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/ColorPicker.stories.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ColorPicker } from "@app/components/annotation/shared/ColorPicker"; + +const meta = { + title: "Annotation/Shared/ColorPicker", + component: ColorPicker, + parameters: { layout: "fullscreen" }, + args: { + isOpen: true, + onClose: () => {}, + selectedColor: "#cc0000", + onColorChange: () => {}, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +function ColorPickerDemo( + props: Partial>, +) { + const [color, setColor] = useState(props.selectedColor ?? "#cc0000"); + const [opacity, setOpacity] = useState(props.opacity ?? 100); + return ( + {}} + {...props} + selectedColor={color} + onColorChange={setColor} + opacity={opacity} + onOpacityChange={setOpacity} + /> + ); +} + +/** The base modal: swatches + hex picker, no opacity slider. */ +export const Default: Story = { + render: () => , +}; + +/** With the opacity slider shown, for tools that need translucent fills (e.g. highlight, watermark). */ +export const WithOpacity: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx index ec854426e6..bfc53d5252 100644 --- a/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/editor/src/core/components/annotation/shared/ColorPicker.tsx @@ -51,6 +51,13 @@ export const ColorPicker: React.FC = ({ format="hex" value={selectedColor} onChange={onColorChange} + // The saturation area and hue bar are role="slider" divs; these are + // their only accessible names. + saturationLabel={t( + "colorPicker.saturation", + "Saturation and brightness", + )} + hueLabel={t("colorPicker.hue", "Hue")} swatches={[ "#000000", "#0066cc", @@ -73,6 +80,7 @@ export const ColorPicker: React.FC = ({ max={100} value={opacity} onChange={onOpacityChange} + thumbLabel={resolvedOpacityLabel} marks={[ { value: 25, label: "25%" }, { value: 50, label: "50%" }, diff --git a/frontend/editor/src/core/components/annotation/shared/DrawingCanvas.stories.tsx b/frontend/editor/src/core/components/annotation/shared/DrawingCanvas.stories.tsx new file mode 100644 index 0000000000..ebdc7282ce --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/DrawingCanvas.stories.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas"; + +const meta = { + title: "Annotation/DrawingCanvas", + component: DrawingCanvas, + parameters: { layout: "padded" }, + args: { + selectedColor: "#000000", + penSize: 3, + penSizeInput: "3", + onColorSwatchClick: () => {}, + onPenSizeChange: () => {}, + onPenSizeInputChange: () => {}, + onSignatureDataChange: () => {}, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +function DrawingCanvasDemo( + props: Partial>, +) { + const [penSize, setPenSize] = useState(3); + const [penSizeInput, setPenSizeInput] = useState("3"); + + return ( + {}} + onPenSizeChange={(size) => { + setPenSize(size); + setPenSizeInput(String(size)); + }} + onPenSizeInputChange={setPenSizeInput} + onSignatureDataChange={() => {}} + {...props} + /> + ); +} + +/** Empty canvas preview with the drawing modal closed. */ +export const Default: Story = { render: () => }; + +/** Preview disabled — clicking the canvas should not open the modal. */ +export const Disabled: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/annotation/shared/DrawingControls.stories.tsx b/frontend/editor/src/core/components/annotation/shared/DrawingControls.stories.tsx new file mode 100644 index 0000000000..b847f9046e --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/DrawingControls.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { DrawingControls } from "@app/components/annotation/shared/DrawingControls"; + +const meta: Meta = { + title: "Annotation/DrawingControls", + component: DrawingControls, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onUndo: () => {}, + onRedo: () => {}, + onPlaceSignature: () => {}, + hasSignatureData: true, + canUndo: true, + canRedo: true, + }, +}; + +export const NoHistory: Story = { + args: { + onUndo: () => {}, + onRedo: () => {}, + onPlaceSignature: () => {}, + hasSignatureData: false, + canUndo: false, + canRedo: false, + }, +}; + +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/annotation/shared/ImageUploader.stories.tsx b/frontend/editor/src/core/components/annotation/shared/ImageUploader.stories.tsx new file mode 100644 index 0000000000..f8500795ba --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/ImageUploader.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ImageUploader } from "@app/components/annotation/shared/ImageUploader"; + +const meta = { + title: "Annotation/Shared/ImageUploader", + component: ImageUploader, + parameters: { layout: "padded" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onImageChange: () => {}, + }, +}; + +export const WithLabelAndHint: Story = { + args: { + onImageChange: () => {}, + label: "Signature image", + hint: "PNG, JPG, or SVG - transparent backgrounds work best", + }, +}; + +export const WithBackgroundRemoval: Story = { + args: { + onImageChange: () => {}, + allowBackgroundRemoval: true, + onProcessedImageData: () => {}, + }, +}; + +export const Disabled: Story = { + args: { + onImageChange: () => {}, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/annotation/shared/OpacityControl.stories.tsx b/frontend/editor/src/core/components/annotation/shared/OpacityControl.stories.tsx new file mode 100644 index 0000000000..79bb3fddd8 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/OpacityControl.stories.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { OpacityControl } from "@app/components/annotation/shared/OpacityControl"; + +const meta: Meta = { + title: "Annotation/OpacityControl", + component: OpacityControl, +}; +export default meta; +type Story = StoryObj; + +function OpacityControlDemo({ disabled }: { disabled?: boolean }) { + const [value, setValue] = useState(80); + return ( + + ); +} + +export const Default: Story = { render: () => }; + +export const Disabled: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/annotation/shared/PropertiesPopover.stories.tsx b/frontend/editor/src/core/components/annotation/shared/PropertiesPopover.stories.tsx new file mode 100644 index 0000000000..b8b37dd980 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/PropertiesPopover.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PropertiesPopover } from "@app/components/annotation/shared/PropertiesPopover"; + +const meta = { + title: "Annotation/Shared/PropertiesPopover", + component: PropertiesPopover, + parameters: { layout: "centered" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Text: Story = { + args: { + annotationType: "text", + annotation: undefined, + onUpdate: () => {}, + }, +}; + +export const Shape: Story = { + args: { + annotationType: "shape", + annotation: undefined, + onUpdate: () => {}, + }, +}; + +export const Disabled: Story = { + args: { + annotationType: "text", + annotation: undefined, + onUpdate: () => {}, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/annotation/shared/TextInputWithFont.stories.tsx b/frontend/editor/src/core/components/annotation/shared/TextInputWithFont.stories.tsx new file mode 100644 index 0000000000..4390f1b62d --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/TextInputWithFont.stories.tsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TextInputWithFont } from "@app/components/annotation/shared/TextInputWithFont"; + +const meta = { + title: "Annotation/Shared/TextInputWithFont", + component: TextInputWithFont, + parameters: { layout: "padded" }, + args: { + text: "Confidential", + onTextChange: () => {}, + fontSize: 24, + onFontSizeChange: () => {}, + fontFamily: "Helvetica", + onFontFamilyChange: () => {}, + label: "Text", + placeholder: "Enter text", + fontLabel: "Font", + fontSizeLabel: "Size", + fontSizePlaceholder: "24", + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +function TextInputWithFontDemo( + props: Partial>, +) { + const [text, setText] = useState(props.text ?? "Confidential"); + const [fontSize, setFontSize] = useState(props.fontSize ?? 24); + const [fontFamily, setFontFamily] = useState(props.fontFamily ?? "Helvetica"); + const [textColor, setTextColor] = useState(props.textColor ?? "#000000"); + const [textAlign, setTextAlign] = useState<"left" | "center" | "right">( + props.textAlign ?? "left", + ); + + return ( + + ); +} + +/** Full control set: text, font, size, colour and alignment. */ +export const Default: Story = { + render: () => , +}; + +/** Without the colour picker or alignment control, for tools that don't need them. */ +export const WithoutColorOrAlign: Story = { + render: () => ( + + ), +}; + +/** All fields disabled. */ +export const Disabled: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/annotation/shared/WidthControl.stories.tsx b/frontend/editor/src/core/components/annotation/shared/WidthControl.stories.tsx new file mode 100644 index 0000000000..46cc4980ad --- /dev/null +++ b/frontend/editor/src/core/components/annotation/shared/WidthControl.stories.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { WidthControl } from "@app/components/annotation/shared/WidthControl"; + +const meta: Meta = { + title: "Annotation/WidthControl", + component: WidthControl, +}; +export default meta; +type Story = StoryObj; + +function WidthControlDemo({ + min = 1, + max = 12, + disabled, +}: { + min?: number; + max?: number; + disabled?: boolean; +}) { + const [value, setValue] = useState(Math.round((min + max) / 2)); + return ( + + ); +} + +export const Default: Story = { render: () => }; + +export const Disabled: Story = { render: () => }; diff --git a/frontend/editor/src/core/components/annotation/tools/DrawingTool.stories.tsx b/frontend/editor/src/core/components/annotation/tools/DrawingTool.stories.tsx new file mode 100644 index 0000000000..002d07663f --- /dev/null +++ b/frontend/editor/src/core/components/annotation/tools/DrawingTool.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { DrawingTool } from "@app/components/annotation/tools/DrawingTool"; +import { SignatureProvider } from "@app/contexts/SignatureContext"; +import { PDFAnnotationProvider } from "@app/components/annotation/providers/PDFAnnotationProvider"; + +// DrawingTool renders BaseAnnotationTool, which reads both SignatureContext and +// PDFAnnotationContext — neither is mounted by the shared preview, so stub both +// here with no-op handlers. +const meta = { + title: "Annotation/DrawingTool", + component: DrawingTool, + decorators: [ + (Story) => ( + + {}} + deactivateDrawMode={() => {}} + activateSignaturePlacementMode={() => {}} + activateDeleteMode={() => {}} + updateDrawSettings={() => {}} + undo={() => {}} + redo={() => {}} + storeImageData={() => {}} + getImageData={() => undefined} + isPlacementMode={false} + signatureConfig={null} + setSignatureConfig={() => {}} + > + + + + ), + ], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + onDrawingChange: () => {}, + }, +}; + +export const Disabled: Story = { + args: { + onDrawingChange: () => {}, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/annotation/tools/ImageTool.stories.tsx b/frontend/editor/src/core/components/annotation/tools/ImageTool.stories.tsx new file mode 100644 index 0000000000..ee0ae3ca24 --- /dev/null +++ b/frontend/editor/src/core/components/annotation/tools/ImageTool.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ImageTool } from "@app/components/annotation/tools/ImageTool"; +import { SignatureProvider } from "@app/contexts/SignatureContext"; +import { PDFAnnotationProvider } from "@app/components/annotation/providers/PDFAnnotationProvider"; + +// ImageTool renders BaseAnnotationTool, which reads both SignatureContext and +// PDFAnnotationContext — neither is mounted by the shared preview, so stub both +// here with no-op handlers. +const meta = { + title: "Annotation/ImageTool", + component: ImageTool, + decorators: [ + (Story) => ( + + {}} + deactivateDrawMode={() => {}} + activateSignaturePlacementMode={() => {}} + activateDeleteMode={() => {}} + updateDrawSettings={() => {}} + undo={() => {}} + redo={() => {}} + storeImageData={() => {}} + getImageData={() => undefined} + isPlacementMode={false} + signatureConfig={null} + setSignatureConfig={() => {}} + > + + + + ), + ], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + onImageChange: () => {}, + }, +}; + +export const Disabled: Story = { + args: { + onImageChange: () => {}, + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx index 5491138e6b..5bce53410e 100644 --- a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx @@ -161,7 +161,7 @@ const AddFileCard = ({ icon={icons.uploadIconName} width="1.25rem" height="1.25rem" - style={{ color: "var(--c-primary)", flexShrink: 0 }} + style={{ color: "var(--c-accent-text)", flexShrink: 0 }} /> {isUploadHover && ( {terminology.dropFilesHere} diff --git a/frontend/editor/src/core/components/fileEditor/FileEditor.module.css b/frontend/editor/src/core/components/fileEditor/FileEditor.module.css index aa89d250e1..410ade863b 100644 --- a/frontend/editor/src/core/components/fileEditor/FileEditor.module.css +++ b/frontend/editor/src/core/components/fileEditor/FileEditor.module.css @@ -320,23 +320,24 @@ color: var(--c-text-muted); /* subtitle text */ } -/* Light mode selected header stroke override */ -:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] { - outline-color: var(--c-primary); -} - /* ========================= Add File Card Styles ========================= */ .addFileCard { - width: 260px; - max-width: 260px; - height: calc(310px - 0.5rem); - margin: 0.5rem auto 0; - background: var(--c-bg); - border: 1.5px solid var(--c-border); - border-radius: 12px; + /* Fill the same slot a portrait page thumbnail does. Height: the 310px + .thumbWrap minus the always-reserved 26px toolchain bar (22px + 4px) that + sits above the page. Width: the file card's 260px minus its 10px side + padding. Offset 36px down (that padding-top + the bar) so the two line up. + A fixed size rather than an aspect-ratio, because each thumbnail derives + --thumb-aspect from its own PDF's page dimensions. */ + height: calc(310px - 26px); + width: calc(260px - 20px); + max-width: 100%; + margin: 36px auto auto; + background: var(--c-surface); + border: 1px solid var(--c-border-subtle); + border-radius: 0.625rem; box-shadow: var(--shadow-md); cursor: pointer; transition: diff --git a/frontend/editor/src/core/components/fileEditor/FileEditorFileName.stories.tsx b/frontend/editor/src/core/components/fileEditor/FileEditorFileName.stories.tsx new file mode 100644 index 0000000000..55645e9a0b --- /dev/null +++ b/frontend/editor/src/core/components/fileEditor/FileEditorFileName.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FileEditorFileName from "@app/components/fileEditor/FileEditorFileName"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const meta = { + title: "FileEditor/FileEditorFileName", + component: FileEditorFileName, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + file: buildFileStub(), + }, +}; + +export const LongFileName: Story = { + args: { + file: buildFileStub({ + name: "annual-financial-report-quarter-four-2026-final-version.pdf", + }), + maxLength: 30, + }, +}; diff --git a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.module.css b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.module.css index a148cc9fa8..1201402bf7 100644 --- a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.module.css +++ b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.module.css @@ -279,3 +279,16 @@ opacity: 0.5; pointer-events: auto; } + +/* Non-blocking policy run (e.g. classification tagging): small top-right pill + * with the policy's icon + a loader. Colour is set inline to the policy accent. */ +.backgroundPolicyPill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 7px; + border-radius: 8px; + background: color-mix(in srgb, currentColor 14%, var(--c-surface)); + box-shadow: var(--shadow-md); + pointer-events: auto; +} diff --git a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx index e183e45d6e..22c969e704 100644 --- a/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/editor/src/core/components/fileEditor/FileEditorThumbnail.tsx @@ -22,6 +22,7 @@ import { dropTargetForElements, } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { StirlingFileStub } from "@app/types/fileContext"; +import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon"; import { PolicyBadges, type FileItemPolicyRef, @@ -31,7 +32,10 @@ import { zipFileService } from "@app/services/zipFileService"; import styles from "@app/components/fileEditor/FileEditorThumbnail.module.css"; import { useFileContext } from "@app/contexts/FileContext"; -import { useFileState } from "@app/contexts/file/fileHooks"; +import { + useFileSelector, + useFileSelectors, +} from "@app/contexts/file/fileHooks"; import { FileId } from "@app/types/file"; import ToolChain from "@app/components/shared/ToolChain"; import HoverActionMenu, { @@ -45,6 +49,7 @@ import { VersionHistoryModal } from "@app/components/filesPage/VersionHistoryMod import { useAppConfig } from "@app/contexts/AppConfigContext"; import { useFileThumbnail } from "@app/hooks/useFileThumbnail"; import DocumentThumbnail from "@app/components/shared/filePreview/DocumentThumbnail"; +import { LARGE_PDF_PARSE_LIMIT } from "@app/utils/thumbnailUtils"; import { truncateCenter } from "@app/utils/textUtils"; import { FileEditorStatusDot } from "@app/components/fileEditor/FileEditorStatusDot"; @@ -89,7 +94,7 @@ const FileEditorThumbnail = ({ actions: fileActions, openEncryptedUnlockPrompt, } = useFileContext(); - const { state, selectors } = useFileState(); + const selectors = useFileSelectors(); const isMobile = useIsMobile(); const actualFile = useMemo( @@ -100,7 +105,7 @@ const FileEditorThumbnail = ({ const isZipFile = zipFileService.isZipFileStub(file); - const hasError = state.ui.errorFileIds.includes(file.id); + const hasError = useFileSelector((s) => s.ui.errorFileIds.includes(file.id)); const pageCount = file.processedFile?.totalPages || 0; const { isEncrypted, @@ -295,9 +300,16 @@ const FileEditorThumbnail = ({ const [showVersionHistory, setShowVersionHistory] = useState(false); const policyEnforcing = policies.some((p) => p.enforcing); - // Accent of the policy currently enforcing, so the overlay's icon/spinner match - // that policy's badge instead of a fixed blue. - const enforcingAccent = policies.find((p) => p.enforcing)?.accentColor; + // The overlay swallows clicks, so a run that never settles would leave the card + // unusable with no way out. Dismissible, like the viewer's; resets per run. + const [enforcingDismissed, setEnforcingDismissed] = useState(false); + if (!policyEnforcing && enforcingDismissed) setEnforcingDismissed(false); + // The policy currently enforcing, so the overlay's icon/spinner match that + // policy's badge instead of a fixed blue. + const enforcingPolicy = policies.find((p) => p.enforcing); + // A non-blocking run (e.g. classification tagging) — indicated by a small + // top-right chip instead of the blocking overlay. + const backgroundPolicy = policies.find((p) => p.background && !p.enforcing); const hoverActions = useMemo(() => { const uploadLabel = isUploaded @@ -540,9 +552,11 @@ const FileEditorThumbnail = ({ {/* Policy enforcement overlay — shown while any policy is in-flight */} setEnforcingDismissed(true)} + accentVar={enforcingPolicy?.accentColor} + categoryId={enforcingPolicy?.id} /> {/* Thumbnail image or loading state */} @@ -553,6 +567,9 @@ const FileEditorThumbnail = ({ isLoading={ !isEncrypted && !displayThumbnail && + // No thumbnail is ever produced at/above the parse limit, so + // without this the spinner has no terminal state. + file.size < LARGE_PDF_PARSE_LIMIT && (isThumbGenerating || file.type?.startsWith("application/pdf") || file.type?.startsWith("image/")) @@ -564,6 +581,27 @@ const FileEditorThumbnail = ({ }} /> + {backgroundPolicy && ( + + + + {policyCategoryIcon(backgroundPolicy.id, { + fontSize: 14, + })} + + + + + )} + {/* Badges — top-left: version, pin, ownership, encrypted */}
diff --git a/frontend/editor/src/core/components/fileManager/CompactFileDetails.stories.tsx b/frontend/editor/src/core/components/fileManager/CompactFileDetails.stories.tsx new file mode 100644 index 0000000000..760b4b7781 --- /dev/null +++ b/frontend/editor/src/core/components/fileManager/CompactFileDetails.stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import CompactFileDetails from "@app/components/fileManager/CompactFileDetails"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024 * 512, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const meta = { + title: "FileManager/CompactFileDetails", + component: CompactFileDetails, + args: { + onPrevious: () => {}, + onNext: () => {}, + onOpenFiles: () => {}, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + currentFile: buildFileStub(), + thumbnail: null, + selectedFiles: [buildFileStub()], + currentFileIndex: 0, + numberOfFiles: 1, + isAnimating: false, + }, +}; + +export const MultipleFiles: Story = { + args: { + currentFile: buildFileStub({ name: "invoice-final.pdf", versionNumber: 2 }), + thumbnail: null, + selectedFiles: [ + buildFileStub({ id: "file-1" as FileId, name: "invoice-final.pdf" }), + buildFileStub({ id: "file-2" as FileId, name: "receipt.pdf" }), + buildFileStub({ id: "file-3" as FileId, name: "statement.pdf" }), + ], + currentFileIndex: 1, + numberOfFiles: 3, + isAnimating: false, + }, +}; + +export const NoFileLoaded: Story = { + args: { + currentFile: null, + thumbnail: null, + selectedFiles: [], + currentFileIndex: 0, + numberOfFiles: 0, + isAnimating: false, + }, +}; diff --git a/frontend/editor/src/core/components/fileManager/CompactFileDetails.tsx b/frontend/editor/src/core/components/fileManager/CompactFileDetails.tsx index 11657cc288..988ecf789c 100644 --- a/frontend/editor/src/core/components/fileManager/CompactFileDetails.tsx +++ b/frontend/editor/src/core/components/fileManager/CompactFileDetails.tsx @@ -107,7 +107,7 @@ const CompactFileDetails: React.FC = ({ {currentFile && ` • v${currentFile.versionNumber || 1}`} {hasMultipleFiles && ( - + {currentFileIndex + 1} of {selectedFiles.length} )} diff --git a/frontend/editor/src/core/components/fileManager/DragOverlay.stories.tsx b/frontend/editor/src/core/components/fileManager/DragOverlay.stories.tsx new file mode 100644 index 0000000000..a047cd9e94 --- /dev/null +++ b/frontend/editor/src/core/components/fileManager/DragOverlay.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import DragOverlay from "@app/components/fileManager/DragOverlay"; + +const meta = { + title: "FileManager/DragOverlay", + component: DragOverlay, + parameters: { layout: "fullscreen" }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isVisible: true, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Hidden: Story = { + args: { + isVisible: false, + }, +}; diff --git a/frontend/editor/src/core/components/fileManager/DragOverlay.tsx b/frontend/editor/src/core/components/fileManager/DragOverlay.tsx index 023bb59d14..04af539346 100644 --- a/frontend/editor/src/core/components/fileManager/DragOverlay.tsx +++ b/frontend/editor/src/core/components/fileManager/DragOverlay.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Stack, Text, useMantineTheme, alpha } from "@mantine/core"; +import { Stack, Text } from "@mantine/core"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import { useTranslation } from "react-i18next"; @@ -9,7 +9,6 @@ interface DragOverlayProps { const DragOverlay: React.FC = ({ isVisible }) => { const { t } = useTranslation(); - const theme = useMantineTheme(); if (!isVisible) return null; @@ -21,8 +20,9 @@ const DragOverlay: React.FC = ({ isVisible }) => { left: 0, right: 0, bottom: 0, - backgroundColor: alpha(theme.colors.blue[6], 0.1), - border: `0.125rem dashed ${theme.colors.blue[6]}`, + // The prompt below is the drop affordance on its own. Tinting the whole + // region and ringing it in dashed accent reads as a second, competing + // surface, so the overlay stays transparent. borderRadius: "1.875rem", display: "flex", alignItems: "center", @@ -32,10 +32,12 @@ const DragOverlay: React.FC = ({ isVisible }) => { }} > + {/* Muted ink rather than the accent shade: it has to read on whatever + the overlay happens to sit on, in either scheme. */} - + {t("fileManager.dropFilesHere", "Drop files here to upload")} diff --git a/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx b/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx index 59b42ef954..890906abda 100644 --- a/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx +++ b/frontend/editor/src/core/components/fileManager/EmptyFilesState.tsx @@ -103,7 +103,7 @@ const EmptyFilesState: React.FC = () => { icon={icons.uploadIconName} width="1.25rem" height="1.25rem" - style={{ color: "var(--c-primary)" }} + style={{ color: "var(--c-accent-text)" }} /> {isUploadHover && ( diff --git a/frontend/editor/src/core/components/fileManager/FileDetails.stories.tsx b/frontend/editor/src/core/components/fileManager/FileDetails.stories.tsx new file mode 100644 index 0000000000..38ba423deb --- /dev/null +++ b/frontend/editor/src/core/components/fileManager/FileDetails.stories.tsx @@ -0,0 +1,73 @@ +import type { ReactElement } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FileDetails from "@app/components/fileManager/FileDetails"; +import { FileContextProvider } from "@app/contexts/FileContext"; +import { FileManagerProvider } from "@app/contexts/FileManagerContext"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const mockFile: StirlingFileStub = { + id: "story-file-1" as FileId, + name: "quarterly-report.pdf", + type: "application/pdf", + size: 2_400_000, + lastModified: Date.now(), + isLeaf: true, + originalFileId: "story-file-1", + versionNumber: 1, + // Set so useIndexedDBThumbnail short-circuits on the stored thumbnail + // instead of trying to read file bytes out of IndexedDB. + thumbnailUrl: + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='160'%3E%3Crect width='120' height='160' fill='%23e9ecef'/%3E%3C/svg%3E", +}; + +/** + * FileDetails reads from FileManagerContext, which itself needs FileContext + * (for useFileActions/useFileManagement) and IndexedDBContext (pulled in by + * FileContextProvider) further up the tree — neither is part of the shared + * preview decorators, so both are stood up here with static mock data. + */ +function withFileManager(activeFileIds: FileId[]) { + return (Story: () => ReactElement) => ( + + {}} + onNewFilesSelect={() => {}} + onClose={() => {}} + isFileSupported={() => true} + isOpen + onFileRemove={() => {}} + modalHeight="600px" + refreshRecentFiles={async () => {}} + isLoading={false} + activeFileIds={activeFileIds} + > + + + + ); +} + +const meta = { + title: "FileManager/FileDetails", + component: FileDetails, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [withFileManager([mockFile.id])], +}; + +export const Empty: Story = { + decorators: [withFileManager([])], +}; + +export const Compact: Story = { + args: { + compact: true, + }, + decorators: [withFileManager([mockFile.id])], +}; diff --git a/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx b/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx index f43df3d86c..4f94155d4e 100644 --- a/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx +++ b/frontend/editor/src/core/components/fileManager/FileInfoCard.tsx @@ -86,19 +86,29 @@ const FileInfoCard: React.FC = ({ }} > - + {t("fileManager.details", "File Details")} - + {/* The viewport is focusable and named so keyboard users can scroll the + detail list once it overflows. */} + diff --git a/frontend/editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx b/frontend/editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx new file mode 100644 index 0000000000..db4a285fcc --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/DeleteFilesDialog.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { DeleteFilesDialog } from "@app/components/filesPage/DeleteFilesDialog"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1024, + lastModified: 0, + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + ...overrides, +}); + +const localFile = buildFileStub(); +const cloudOnlyFile = buildFileStub({ + id: "server-1" as FileId, + name: "shared-invoice.pdf", + originalFileId: "server-1", + remoteStorageId: 42, + remoteOwnedByCurrentUser: true, +}); + +const meta = { + title: "FilesPage/DeleteFilesDialog", + component: DeleteFilesDialog, + args: { + opened: true, + onClose: () => {}, + onConfirm: async () => {}, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + files: [localFile], + }, +}; + +export const CloudOnly: Story = { + args: { + files: [cloudOnlyFile], + }, +}; + +export const LocalAndCloudChoice: Story = { + args: { + files: [localFile, cloudOnlyFile], + }, +}; diff --git a/frontend/editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx b/frontend/editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx new file mode 100644 index 0000000000..891b72c58a --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/DeleteFolderDialog.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { DeleteFolderDialog } from "@app/components/filesPage/DeleteFolderDialog"; +import { createFolderId } from "@app/types/folder"; + +const mockFolder = { + id: createFolderId(), + name: "Invoices", + parentFolderId: null, + createdAt: Date.now(), + updatedAt: Date.now(), +}; + +const meta = { + title: "FilesPage/DeleteFolderDialog", + component: DeleteFolderDialog, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + opened: true, + folder: mockFolder, + fileCount: 0, + onClose: () => {}, + onConfirm: () => {}, + }, +}; + +export const WithFiles: Story = { + args: { + opened: true, + folder: mockFolder, + fileCount: 12, + onClose: () => {}, + onConfirm: () => {}, + }, +}; diff --git a/frontend/editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx b/frontend/editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx new file mode 100644 index 0000000000..883e2fa8ba --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FileDetailsPanel.stories.tsx @@ -0,0 +1,88 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FileDetailsPanel } from "@app/components/filesPage/FileDetailsPanel"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; +import type { FolderRecord } from "@app/types/folder"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 245_760, + lastModified: Date.now(), + isLeaf: true, + originalFileId: "file-1" as FileId, + versionNumber: 1, + ...overrides, +}); + +const folder: FolderRecord = { + id: "folder-1" as FolderRecord["id"], + name: "Contracts", + parentFolderId: null, + createdAt: Date.now(), + updatedAt: Date.now(), +}; + +const meta = { + title: "FilesPage/FileDetailsPanel", + component: FileDetailsPanel, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +const singleFile = buildFileStub(); +const fileMap = new Map([ + [singleFile.id, singleFile], +]); + +export const Default: Story = { + args: { + selectedFileIds: [singleFile.id], + fileMap, + currentFolder: null, + onClose: () => {}, + onAddToWorkspace: () => {}, + onMove: () => {}, + onRemove: () => {}, + }, +}; + +export const InFolder: Story = { + args: { + ...Default.args, + currentFolder: folder, + }, +}; + +export const MultiSelect: Story = { + args: (() => { + const fileA = buildFileStub({ id: "file-a" as FileId, name: "a.pdf" }); + const fileB = buildFileStub({ + id: "file-b" as FileId, + name: "b.pdf", + size: 102_400, + }); + return { + selectedFileIds: [fileA.id, fileB.id], + fileMap: new Map([ + [fileA.id, fileA], + [fileB.id, fileB], + ]), + currentFolder: null, + onClose: () => {}, + onAddToWorkspace: () => {}, + onMove: () => {}, + onRemove: () => {}, + }; + })(), +}; + +export const LocalOnlyWithSaveToServer: Story = { + args: { + ...Default.args, + onSaveToServer: () => {}, + }, +}; diff --git a/frontend/editor/src/core/components/filesPage/FileGrid.stories.tsx b/frontend/editor/src/core/components/filesPage/FileGrid.stories.tsx new file mode 100644 index 0000000000..d1cb9f32b6 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FileGrid.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { ReactElement } from "react"; +import { + FileGrid, + type FilesPageEntry, +} from "@app/components/filesPage/FileGrid"; +import { FileContextProvider } from "@app/contexts/FileContext"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const buildFileStub = ( + overrides: Partial = {}, +): StirlingFileStub => ({ + id: "file-1" as FileId, + name: "report.pdf", + type: "application/pdf", + size: 1_240_000, + lastModified: Date.now(), + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, + // Set so useLazyThumbnail short-circuits on the stored thumbnail instead of + // trying to read file bytes out of IndexedDB. + thumbnailUrl: + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='160'%3E%3Crect width='120' height='160' fill='%23e9ecef'/%3E%3C/svg%3E", + ...overrides, +}); + +const localFile = buildFileStub(); +const cloudFile = buildFileStub({ + id: "file-2" as FileId, + name: "shared-invoice.pdf", + originalFileId: "file-2", + remoteStorageId: 42, + remoteOwnedByCurrentUser: true, +}); +const spreadsheetFile = buildFileStub({ + id: "file-3" as FileId, + name: "budget.xlsx", + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + originalFileId: "file-3", + thumbnailUrl: undefined, +}); + +const fileEntries: FilesPageEntry[] = [ + { kind: "file", file: localFile }, + { kind: "file", file: cloudFile }, + { kind: "file", file: spreadsheetFile }, +]; + +/** + * FileGrid renders file cards/rows via useLazyThumbnail, which reads + * IndexedDBContext + FileContext further up the tree - neither is part of + * the shared preview decorators, so FileContextProvider is stood up here. + * Folder entries are intentionally left out of these mocks: FolderCard / + * FolderRow call useFolders(), which needs a FolderProvider wired to + * IndexedDB/auth/app-config context this story doesn't stand up. + */ +function withFileContext(Story: () => ReactElement) { + return ( + + + + ); +} + +const meta = { + title: "FilesPage/FileGrid", + component: FileGrid, + decorators: [withFileContext], + args: { + entries: fileEntries, + selectedFileIds: new Set(), + viewMode: "grid", + onSelectFile: () => {}, + onOpenFolder: () => {}, + onOpenFile: () => {}, + onMoveFiles: () => {}, + onMoveFolder: () => {}, + onRenameFolder: () => {}, + onDeleteFolder: () => {}, + onChangeFolderAppearance: () => {}, + onRemoveFiles: () => {}, + onPromptMoveFiles: () => {}, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const ListMode: Story = { + args: { + viewMode: "list", + }, +}; + +export const Loading: Story = { + args: { + entries: [], + loading: true, + }, +}; + +export const Empty: Story = { + args: { + entries: [], + loading: false, + currentTab: "all", + onEmptyUpload: () => {}, + onEmptyCreateFolder: () => {}, + }, +}; diff --git a/frontend/editor/src/core/components/filesPage/FileGrid.tsx b/frontend/editor/src/core/components/filesPage/FileGrid.tsx index ce9be12ad2..40e17dc502 100644 --- a/frontend/editor/src/core/components/filesPage/FileGrid.tsx +++ b/frontend/editor/src/core/components/filesPage/FileGrid.tsx @@ -13,9 +13,11 @@ import DeleteIcon from "@mui/icons-material/Delete"; import HistoryIcon from "@mui/icons-material/History"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"; +import ContentCopyOutlinedIcon from "@mui/icons-material/ContentCopyOutlined"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; +import SearchIcon from "@mui/icons-material/Search"; import { FileId } from "@app/types/file"; import { FolderId, FolderRecord, ROOT_FOLDER_ID } from "@app/types/folder"; @@ -35,6 +37,8 @@ import { FolderThumbnail } from "@app/components/filesPage/FolderThumbnail"; import { findFolderIcon } from "@app/components/filesPage/folderIcons"; import { FolderAppearancePicker } from "@app/components/filesPage/FolderAppearancePicker"; import { useLazyThumbnail } from "@app/hooks/useLazyThumbnail"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; import type { FilesPageSortMode } from "@app/contexts/FilesPageContext"; import { OpenInNewWindowMenuItem } from "@app/components/filesPage/OpenInNewWindowMenuItem"; @@ -82,6 +86,12 @@ interface FileGridProps { onSaveToServer?: (file: StirlingFileStub) => void; /** Open the version-history modal for a file (only when it has >1 version). */ onVersionHistory?: (file: StirlingFileStub) => void; + /** Download a copy (desktop: save a copy). */ + onDownloadFile?: (file: StirlingFileStub) => void; + /** Open the rename dialog for a file. */ + onRenameFile?: (file: StirlingFileStub) => void; + /** Save a second copy of the file into the library. */ + onDuplicateFile?: (file: StirlingFileStub) => void; /** When set, the Save to server item renders disabled with this tooltip. */ saveToServerDisabledReason?: string | null; /** When supplied the list-view column headers become sortable. */ @@ -89,6 +99,8 @@ interface FileGridProps { onChangeSortMode?: (mode: FilesPageSortMode) => void; /** Drives the empty-state copy. */ currentTab?: "all" | "local" | "cloud" | "recent" | "shared" | "sharedByMe"; + /** A filter is applied; an empty result then means "no matches", not "no files". */ + searchActive?: boolean; /** Cloud reachability; switches the cloud empty-state copy. */ serverReachable?: boolean; /** Empty-state CTA handlers; if absent the matching button hides. */ @@ -104,6 +116,7 @@ export function FileGrid(props: FileGridProps & { loading?: boolean }) { entries, loading, currentTab, + searchActive, serverReachable, onEmptyUpload, onEmptyCreateFolder, @@ -115,15 +128,28 @@ export function FileGrid(props: FileGridProps & { loading?: boolean }) { } if (entries.length === 0) { - return ( + const emptyState = ( ); + // When a filter empties the list view, keep the column headers in place and + // show the no-results message beneath them, rather than replacing the whole + // table. Grid view (cards, no headers) just shows the empty state. + if (viewMode === "list" && searchActive) { + return ( + <> + + {emptyState} + + ); + } + return emptyState; } if (viewMode === "list") { @@ -187,6 +213,8 @@ function SkeletonGrid({ viewMode }: { viewMode: FilesPageViewMode }) { interface EmptyStateProps { /** Drives copy + iconography. */ tab?: "all" | "local" | "cloud" | "recent" | "shared" | "sharedByMe"; + /** When true the empty list is the result of a filter, not a bare folder. */ + searchActive?: boolean; /** Switches the cloud empty-state copy. */ serverReachable?: boolean; /** CTA handlers; absent => button hidden. */ @@ -198,12 +226,35 @@ interface EmptyStateProps { function EmptyState({ tab = "all", + searchActive = false, serverReachable = true, onUpload, onCreateFolder, newFolderDisabledReason, }: EmptyStateProps) { const { t } = useTranslation(); + + // A filter with no matches isn't an empty folder - say so, and skip the + // upload / new-folder CTAs since clearing the filter is the way out. + if (searchActive) { + return ( +
+ + + +
+ {t("filesPage.empty.noResults.title", "No matching files")} +
+
+ {t( + "filesPage.empty.noResults.hint", + "No files in this folder match your filter. Try a different term or clear the filter.", + )} +
+
+ ); + } + const { titleKey, titleFallback, hintKey, hintFallback } = (() => { switch (tab) { case "local": @@ -324,24 +375,21 @@ function EmptyState({ ); } -function GridView({ - entries, - selectedFileIds, - activeWorkspaceFileIds, - onSelectFile, - onOpenFolder, - onOpenFile, - onMoveFiles, - onMoveFolder, - onRenameFolder, - onDeleteFolder, - onChangeFolderAppearance, - onRemoveFiles, - onPromptMoveFiles, - onSaveToServer, - onVersionHistory, - saveToServerDisabledReason, -}: FileGridProps) { +function GridView(props: FileGridProps) { + const { + entries, + selectedFileIds, + activeWorkspaceFileIds, + onSelectFile, + onOpenFolder, + onOpenFile, + onMoveFiles, + onMoveFolder, + onRenameFolder, + onDeleteFolder, + onChangeFolderAppearance, + } = props; + const menuHandlersFor = useFileMenuHandlers(props); return (
{entries.map((entry) => { @@ -382,22 +430,7 @@ function GridView({ onSelectFile(entry.file!.id, e.shiftKey, e.metaKey || e.ctrlKey) } onDoubleClick={() => onOpenFile(entry.file!)} - onRemove={() => onRemoveFiles([entry.file!.id])} - onMove={() => { - const target = selectedFileIds.has(entry.file!.id) - ? Array.from(selectedFileIds) - : [entry.file!.id]; - onPromptMoveFiles(target); - }} - onSaveToServer={ - onSaveToServer ? () => onSaveToServer(entry.file!) : undefined - } - onVersionHistory={ - onVersionHistory - ? () => onVersionHistory(entry.file!) - : undefined - } - saveToServerDisabledReason={saveToServerDisabledReason} + {...menuHandlersFor(entry.file)} /> ); } @@ -584,7 +617,222 @@ function PolicyBadges({ fileId }: { fileId: string }) { return ; } -interface FileCardProps { +/** Per-file actions. Shared verbatim by the grid card and the list row, and + * kept in step with the file sidebar's kebab so both surfaces offer the same. */ +interface FileActionsMenuProps { + file: StirlingFileStub; + triggerRef: React.RefObject; + onOpen: () => void; + onMove: () => void; + onRemove: () => void; + onDownload?: () => void; + onRename?: () => void; + onDuplicate?: () => void; + onSaveToServer?: () => void; + onVersionHistory?: () => void; + saveToServerDisabledReason?: string | null; +} + +function FileActionsMenu({ + file, + triggerRef, + onOpen, + onMove, + onRemove, + onDownload, + onRename, + onDuplicate, + onSaveToServer, + onVersionHistory, + saveToServerDisabledReason, +}: FileActionsMenuProps) { + const { t } = useTranslation(); + const terminology = useFileActionTerminology(); + const DownloadIcon = useFileActionIcons().download; + const showSaveToServer = + Boolean(onSaveToServer) && file.remoteStorageId == null; + const showVersionHistory = + Boolean(onVersionHistory) && (file.versionNumber ?? 1) > 1; + return ( + + + e.stopPropagation()} + aria-label={t("filesPage.fileMenu", "File actions")} + data-testid="file-card-actions" + > + + + + + } + onClick={(e) => { + e.stopPropagation(); + onOpen(); + }} + > + {t("filesPage.addToWorkspace", "Add to workspace")} + + + } + onClick={(e) => { + e.stopPropagation(); + onMove(); + }} + data-testid="file-menu-move-to" + > + {t("filesPage.moveTo", "Move to…")} + + + {(onDownload || onRename || onDuplicate) && } + {onDownload && ( + } + onClick={(e) => { + e.stopPropagation(); + onDownload(); + }} + data-testid="file-menu-download" + > + {terminology.download} + + )} + {onRename && ( + } + onClick={(e) => { + e.stopPropagation(); + onRename(); + }} + data-testid="file-menu-rename" + > + {t("filesPage.rename", "Rename")} + + )} + {onDuplicate && ( + } + onClick={(e) => { + e.stopPropagation(); + onDuplicate(); + }} + data-testid="file-menu-duplicate" + > + {t("filesPage.duplicate", "Duplicate")} + + )} + + {(showSaveToServer || showVersionHistory) && } + {/* Per-file Save to server; shown for local-only files. When + storage is off it stays visible but disabled with a tooltip. */} + {showSaveToServer && onSaveToServer && ( + + } + disabled={Boolean(saveToServerDisabledReason)} + onClick={(e) => { + e.stopPropagation(); + onSaveToServer(); + }} + style={ + saveToServerDisabledReason + ? { pointerEvents: "auto" } + : undefined + } + > + {t("filesPage.saveToServer", "Save to server")} + + + )} + {showVersionHistory && onVersionHistory && ( + } + onClick={(e) => { + e.stopPropagation(); + onVersionHistory(); + }} + > + {t("filesPage.versionHistory", "Version history")} + + )} + + + } + onClick={(e) => { + e.stopPropagation(); + onRemove(); + }} + > + {t("filesPage.remove", "Delete")} + + + + ); +} + +/** Binds one file's kebab handlers, so grid and list wire them identically. */ +function useFileMenuHandlers( + props: FileGridProps, +): (file: StirlingFileStub) => FileMenuHandlers { + const { + selectedFileIds, + onRemoveFiles, + onPromptMoveFiles, + onSaveToServer, + onVersionHistory, + onDownloadFile, + onRenameFile, + onDuplicateFile, + saveToServerDisabledReason, + } = props; + return (file: StirlingFileStub) => ({ + onRemove: () => onRemoveFiles([file.id]), + // A move acts on the whole selection when this file is part of it. + onMove: () => + onPromptMoveFiles( + selectedFileIds.has(file.id) ? Array.from(selectedFileIds) : [file.id], + ), + onDownload: onDownloadFile ? () => onDownloadFile(file) : undefined, + onRename: onRenameFile ? () => onRenameFile(file) : undefined, + onDuplicate: onDuplicateFile ? () => onDuplicateFile(file) : undefined, + onSaveToServer: onSaveToServer ? () => onSaveToServer(file) : undefined, + onVersionHistory: onVersionHistory + ? () => onVersionHistory(file) + : undefined, + saveToServerDisabledReason, + }); +} + +/** Per-file kebab handlers, shared by the card and row wrappers. */ +interface FileMenuHandlers { + onRemove: () => void; + onMove: () => void; + onDownload?: () => void; + onRename?: () => void; + onDuplicate?: () => void; + /** Kebab Save to server; only fires when file is local-only. */ + onSaveToServer?: () => void; + /** Open the version-history modal; shown only when file has >1 version. */ + onVersionHistory?: () => void; + /** When set, the kebab Save to server is disabled with this tooltip. */ + saveToServerDisabledReason?: string | null; +} + +interface FileCardProps extends FileMenuHandlers { file: StirlingFileStub; isSelected: boolean; isInWorkspace: boolean; @@ -595,14 +843,6 @@ interface FileCardProps { multiSelectActive: boolean; onClick: (e: React.MouseEvent) => void; onDoubleClick: () => void; - onRemove: () => void; - onMove: () => void; - /** Kebab Save to server; only fires when file is local-only. */ - onSaveToServer?: () => void; - /** Open the version-history modal; shown only when file has >1 version. */ - onVersionHistory?: () => void; - /** When set, the kebab Save to server is disabled with this tooltip. */ - saveToServerDisabledReason?: string | null; } function FileCard({ @@ -614,11 +854,7 @@ function FileCard({ multiSelectActive, onClick, onDoubleClick, - onRemove, - onMove, - onSaveToServer, - onVersionHistory, - saveToServerDisabledReason, + ...menuHandlers }: FileCardProps) { const { t } = useTranslation(); const cardRef = useRef(null); @@ -742,126 +978,48 @@ function FileCard({ )}
{fileSize} - · + {fileDate}
- - - e.stopPropagation()} - aria-label={t("filesPage.fileMenu", "File actions")} - data-testid="file-card-actions" - > - - - - - } - onClick={(e) => { - e.stopPropagation(); - onDoubleClick(); - }} - > - {t("filesPage.addToWorkspace", "Add to workspace")} - - - } - onClick={(e) => { - e.stopPropagation(); - onMove(); - }} - data-testid="file-menu-move-to" - > - {t("filesPage.moveTo", "Move to…")} - - {/* Per-file Save to server; shown for local-only files. When - storage is off it stays visible but disabled with a tooltip. */} - {onSaveToServer && file.remoteStorageId == null && ( - - } - disabled={Boolean(saveToServerDisabledReason)} - onClick={(e) => { - e.stopPropagation(); - onSaveToServer(); - }} - style={ - saveToServerDisabledReason - ? { pointerEvents: "auto" } - : undefined - } - > - {t("filesPage.saveToServer", "Save to server")} - - - )} - {onVersionHistory && (file.versionNumber ?? 1) > 1 && ( - } - onClick={(e) => { - e.stopPropagation(); - onVersionHistory(); - }} - > - {t("filesPage.versionHistory", "Version history")} - - )} - - } - onClick={(e) => { - e.stopPropagation(); - onRemove(); - }} - > - {t("filesPage.remove", "Delete")} - - - +
); } -function ListView({ - entries, - selectedFileIds, - activeWorkspaceFileIds, - onSelectFile, - onSetSelection, - onOpenFolder, - onOpenFile, - onMoveFiles, - onMoveFolder, - onRenameFolder, - onDeleteFolder, - onSaveToServer, - onVersionHistory, - saveToServerDisabledReason, - onChangeFolderAppearance, - onRemoveFiles, - onPromptMoveFiles, - sortMode, - onChangeSortMode, -}: FileGridProps & { - sortMode?: FilesPageSortMode; - onChangeSortMode?: (next: FilesPageSortMode) => void; -}) { +function ListView( + props: FileGridProps & { + sortMode?: FilesPageSortMode; + onChangeSortMode?: (next: FilesPageSortMode) => void; + }, +) { + const { + entries, + selectedFileIds, + activeWorkspaceFileIds, + onSelectFile, + onSetSelection, + onOpenFolder, + onOpenFile, + onMoveFiles, + onMoveFolder, + onRenameFolder, + onDeleteFolder, + onChangeFolderAppearance, + sortMode, + onChangeSortMode, + } = props; + const menuHandlersFor = useFileMenuHandlers(props); const { t } = useTranslation(); // Tri-state header checkbox state - computed from current entries. @@ -906,35 +1064,47 @@ function ListView({ return (
+ {/* Each direct child is a columnheader: a role="row" may only own cells, so + the sort controls and the select-all box have to sit inside one. */}
{onSetSelection && visibleFileIds.length > 0 ? ( - { - onSetSelection(allSelected ? new Set() : new Set(visibleFileIds)); - }} - aria-label={ - allSelected - ? t("filesPage.deselectAll", "Clear selection") - : t("filesPage.selectAll", "Select all") - } - /> + + { + onSetSelection( + allSelected ? new Set() : new Set(visibleFileIds), + ); + }} + aria-label={ + allSelected + ? t("filesPage.deselectAll", "Clear selection") + : t("filesPage.selectAll", "Select all") + } + /> + ) : (
@@ -975,22 +1145,7 @@ function ListView({ onSelectFile(entry.file!.id, e.shiftKey, e.metaKey || e.ctrlKey) } onOpen={() => onOpenFile(entry.file!)} - onRemove={() => onRemoveFiles([entry.file!.id])} - onMove={() => { - const target = selectedFileIds.has(entry.file!.id) - ? Array.from(selectedFileIds) - : [entry.file!.id]; - onPromptMoveFiles(target); - }} - onSaveToServer={ - onSaveToServer ? () => onSaveToServer(entry.file!) : undefined - } - onVersionHistory={ - onVersionHistory - ? () => onVersionHistory(entry.file!) - : undefined - } - saveToServerDisabledReason={saveToServerDisabledReason} + {...menuHandlersFor(entry.file)} /> ); } @@ -1091,7 +1246,12 @@ function FolderRow({ className={`files-page-list-row${isDropTarget ? " is-drop-target" : ""}`} >
); } -interface FileRowProps { +interface FileRowProps extends FileMenuHandlers { file: StirlingFileStub; isSelected: boolean; isInWorkspace: boolean; @@ -1188,14 +1352,6 @@ interface FileRowProps { multiSelectActive: boolean; onClick: (e: React.MouseEvent) => void; onOpen: () => void; - onRemove: () => void; - onMove: () => void; - /** Kebab Save to server; only fires when file is local-only. */ - onSaveToServer?: () => void; - /** Open the version-history modal; shown only when file has >1 version. */ - onVersionHistory?: () => void; - /** When set, the kebab Save to server is disabled with this tooltip. */ - saveToServerDisabledReason?: string | null; } function FileRow({ @@ -1207,11 +1363,7 @@ function FileRow({ multiSelectActive, onClick, onOpen, - onRemove, - onMove, - onSaveToServer, - onVersionHistory, - saveToServerDisabledReason, + ...menuHandlers }: FileRowProps) { const { t } = useTranslation(); const kebabRef = useRef(null); @@ -1254,35 +1406,40 @@ function FileRow({ isInWorkspace ? " is-in-workspace" : "" }`} > - {/* Checkbox only shows in multi-select mode (see FileCard). When the - checkbox is hidden the first grid column collapses, but the row's - CSS grid keeps the columns aligned via the named template, so no - empty cell shows. */} + {/* Each direct child is a gridcell: a role="row" may only own cells, so the + checkbox and the actions menu have to sit inside one. + + The checkbox only shows in multi-select mode (see FileCard). When it is + hidden the first grid column collapses, but the row's CSS grid keeps the + columns aligned via the named template, so no empty cell shows. */} {multiSelectActive ? ( - { - // Toggle this file in/out of the selection without modifier keys. - e.stopPropagation(); - onClick({ - ...e, - shiftKey: false, - ctrlKey: true, - metaKey: true, - } as unknown as React.MouseEvent); - }} - onChange={() => { - /* handled by onClick */ - }} - aria-label={t("filesPage.selectFile", "Select file {{name}}", { - name: file.name, - })} - /> + + { + // Toggle this file in/out of the selection without modifier keys. + e.stopPropagation(); + onClick({ + ...e, + shiftKey: false, + ctrlKey: true, + metaKey: true, + } as unknown as React.MouseEvent); + }} + onChange={() => { + /* handled by onClick */ + }} + aria-label={t("filesPage.selectFile", "Select file {{name}}", { + name: file.name, + })} + /> + ) : ( // Empty cell preserves grid column alignment.
); } diff --git a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx index cc6a74d09e..36ffb9c4dc 100644 --- a/frontend/editor/src/core/components/filesPage/FileManagerView.tsx +++ b/frontend/editor/src/core/components/filesPage/FileManagerView.tsx @@ -7,11 +7,19 @@ import React, { } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; -import { Drawer, Group, MultiSelect, Select, Tooltip } from "@mantine/core"; +import { + Drawer, + Group, + MultiSelect, + Select, + TextInput, + Tooltip, +} from "@mantine/core"; import { Button } from "@app/ui/Button"; import { ActionIcon } from "@app/ui/ActionIcon"; import { SegmentedControl } from "@app/ui/SegmentedControl"; import { useMediaQuery } from "@mantine/hooks"; +import CloseIcon from "@mui/icons-material/Close"; import SearchIcon from "@mui/icons-material/Search"; import UploadFileIcon from "@mui/icons-material/UploadFile"; import QrCode2Icon from "@mui/icons-material/QrCode2"; @@ -25,6 +33,10 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { FilesToolbarBulkMenu } from "@app/components/filesPage/FilesToolbarBulkMenu"; +import { FilesToolbarCount } from "@app/components/filesPage/FilesToolbarCount"; +import { FilesToolbarFilterMenu } from "@app/components/filesPage/FilesToolbarFilterMenu"; +import { FilesToolbarSortMenu } from "@app/components/filesPage/FilesToolbarSortMenu"; import { stripBasePath } from "@app/constants/app"; import { useAuth } from "@app/auth/UseSession"; @@ -51,6 +63,8 @@ import { StirlingFileStub } from "@app/types/fileContext"; import { FolderId, ROOT_FOLDER_ID } from "@app/types/folder"; import { FileGrid, FilesPageEntry } from "@app/components/filesPage/FileGrid"; +import SuperSearch from "@app/components/shared/superSearch/SuperSearch"; +import { useEditorSearchScopes } from "@app/hooks/useSuperSearch"; import { FileDetailsPanel } from "@app/components/filesPage/FileDetailsPanel"; import BulkUploadToServerModal from "@app/components/shared/BulkUploadToServerModal"; import MobileUploadModal from "@app/components/shared/MobileUploadModal"; @@ -61,18 +75,24 @@ import { FolderNameDialog } from "@app/components/filesPage/FolderNameDialog"; import { DeleteFolderDialog } from "@app/components/filesPage/DeleteFolderDialog"; import { DeleteFilesDialog } from "@app/components/filesPage/DeleteFilesDialog"; import { VersionHistoryModal } from "@app/components/filesPage/VersionHistoryModal"; +import { RenameFileDialog } from "@app/components/shared/RenameFileDialog"; +import { duplicateStoredFile } from "@app/utils/duplicateFile"; +import { downloadFileFromStorage } from "@app/utils/downloadUtils"; +import { fileStorage } from "@app/services/fileStorage"; import { materializeServerStubs } from "@app/services/fileSyncService"; import { FILES_PAGE_DRAG_TYPE, parseFilesPageDragPayload, } from "@app/components/filesPage/dragDrop"; import { clearFilesPageReturnRoute } from "@app/components/filesPage/filesPageReturnRoute"; +import { EDITOR_BASENAME } from "@app/routes/editorBasename"; import "@app/components/filesPage/FilesPage.css"; export default function FileManagerView() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); + const searchScopes = useEditorSearchScopes(); // Hide Shared tab when storageSharingEnabled is false. const { sharingEnabled } = useSharingEnabled(); @@ -578,7 +598,7 @@ export default function FileManagerView() { } else if (materialized.length > 1) { navActions.setWorkbench("fileEditor"); } - navigate("/"); + navigate(EDITOR_BASENAME); }; requestNavigation(() => { @@ -681,16 +701,15 @@ export default function FileManagerView() { const handleClose = useCallback(() => { // Drop the return-route hint so the workbench doesn't show a stale back. clearFilesPageReturnRoute(); - navigate("/"); + navigate(EDITOR_BASENAME); }, [navigate]); // ─── keyboard shortcuts ───────────────────────────────────────────────── - const searchInputRef = useRef(null); - // External focus trigger (used by the FileSidebar rail Search button). - useEffect(() => { - const onFocus = () => searchInputRef.current?.focus(); - window.addEventListener("files-page:focus-search", onFocus); - return () => window.removeEventListener("files-page:focus-search", onFocus); + // Focus the super-search input (stable id), used by the "/" shortcut. + const focusSearch = useCallback(() => { + ( + document.getElementById("super-search-input") as HTMLInputElement | null + )?.focus(); }, []); useEffect(() => { const onKey = (e: KeyboardEvent) => { @@ -734,13 +753,19 @@ export default function FileManagerView() { // "/" focuses the search field. if (e.key === "/" && !inInput) { e.preventDefault(); - searchInputRef.current?.focus(); + focusSearch(); return; } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [visibleFiles, selectedFileIds, removeFiles, setSelectedFileIds]); + }, [ + visibleFiles, + selectedFileIds, + removeFiles, + setSelectedFileIds, + focusSearch, + ]); useEffect(() => { const onKey = (e: KeyboardEvent) => { @@ -777,6 +802,92 @@ export default function FileManagerView() { [removeFiles], ); + // ─── per-file kebab: download / rename / duplicate ─────────────────────── + // Same actions the file sidebar's kebab offers, so both surfaces match. + + /** Cloud-only rows hold no bytes; pull them local before acting on them. */ + const localCopyOf = useCallback( + async (file: StirlingFileStub): Promise => { + const [materialized] = await materializeServerStubs([file], { + addFiles: fileActions.addFilesWithOptions, + updateStub: fileActions.updateStirlingFileStub, + }); + return materialized ?? null; + }, + [fileActions], + ); + + const handleDownloadFile = useCallback( + async (file: StirlingFileStub) => { + try { + const local = await localCopyOf(file); + if (!local) return; + await downloadFileFromStorage(local); + } catch (err) { + console.error("[FilesPage] Download failed", err); + folders.setError( + t("filesPage.error.downloadFailed", "Could not download the file."), + ); + } + }, + [localCopyOf, folders, t], + ); + + const handleDuplicateFile = useCallback( + async (file: StirlingFileStub) => { + try { + const local = await localCopyOf(file); + if (!local) return; + const copyId = await duplicateStoredFile( + local, + allFiles.map((f) => f.name), + addFiles, + ); + if (!copyId) { + throw new Error(`File "${local.name}" not found in storage`); + } + await refresh(); + } catch (err) { + console.error("[FilesPage] Duplicate failed", err); + folders.setError( + t("filesPage.error.duplicateFailed", "Could not duplicate the file."), + ); + } + }, + [localCopyOf, allFiles, addFiles, refresh, folders, t], + ); + + const [renameTarget, setRenameTarget] = useState( + null, + ); + + // The stub name is what the UI and exports read, so a rename is a metadata + // write; the workbench copy (if any) is updated in the same breath. + const handleConfirmRename = useCallback( + async (name: string) => { + const file = renameTarget; + if (!file) return; + const local = await localCopyOf(file); + if (!local) return; + // quickKey is name|size|lastModified; a stale one would make a re-upload + // of the original look like a duplicate of the renamed file. + const quickKey = `${name}|${local.size}|${local.lastModified}`; + const saved = await fileStorage.updateFileMetadata(local.id, { + name, + quickKey, + }); + if (!saved) { + throw new Error( + t("fileSidebar.rename.error", "Could not rename the file."), + ); + } + fileActions.updateStirlingFileStub(local.id, { name, quickKey }); + setRenameTarget(null); + await refresh(); + }, + [renameTarget, localCopyOf, fileActions, refresh, t], + ); + // ─── derived UI bits ──────────────────────────────────────────────────── const currentFolderRecord = currentFolderId ? (foldersById.get(currentFolderId) ?? null) @@ -786,6 +897,9 @@ export default function FileManagerView() { () => Array.from(selectedFileIds), [selectedFileIds], ); + // A phone with files selected shows a contextual selection bar instead of the + // full toolbar - five bulk buttons plus filters cannot fit the width. + const mobileSelection = isMobile && selectedFiles.length > 0; // Local-only subset of selection; drives Save-to-server visibility. const localOnlySelectedStubs = useMemo( @@ -889,11 +1003,9 @@ export default function FileManagerView() { }; return ( <> - +
+ +
- - {loading - ? t("filesPage.loading", "Loading…") - : t("filesPage.summary", "{{count}} items", { - count: totalCount, - })} - {selectedFiles.length > 0 && ( - - {" "} - ·{" "} - {t("filesPage.selectedCount", "{{count}} selected", { - count: selectedFiles.length, - })} - - )} - + {(() => { // Select all / Clear toggle over visible files. if (visibleFiles.length === 0) return null; @@ -1160,268 +1262,382 @@ export default function FileManagerView() { ); })()}
- {selectedFiles.length > 0 && - (() => { - // Bulk-action labels; CSS collapses to icon-only below 900px. - const addLabel = + {mobileSelection ? ( + handleAddToWorkspace(selectedFiles)} + onSaveToServer={ + localOnlySelectedStubs.length > 0 + ? () => setSaveToServerTarget(localOnlySelectedStubs) + : undefined + } + saveToServerDisabledReason={ + saveToServerDisabledReason ?? undefined + } + onShowDetails={ selectedFiles.length === 1 - ? t("filesPage.addToWorkspace", "Add to workspace") - : t( - "filesPage.addToWorkspaceCount", - "Add {{count}} to workspace", - { count: selectedFiles.length }, - ); - const moveLabel = t("filesPage.moveTo", "Move to…"); - const removeLabel = t("filesPage.remove", "Remove"); - return ( - // wrap="nowrap" keeps the row single-line. - - - - - {/* Save to server; shown whenever local-only files are + ? () => setMobileDetailsOpen(true) + : undefined + } + onMove={() => promptMoveFiles(selectedFiles)} + onRemove={() => handleRemoveFiles(selectedFiles)} + /> + ) : ( + <> + {selectedFiles.length > 0 && + (() => { + // Bulk-action labels; CSS collapses to icon-only below 900px. + const addLabel = + selectedFiles.length === 1 + ? t("filesPage.addToWorkspace", "Add to workspace") + : t( + "filesPage.addToWorkspaceCount", + "Add {{count}} to workspace", + { count: selectedFiles.length }, + ); + const moveLabel = t("filesPage.moveTo", "Move to…"); + const removeLabel = t("filesPage.remove", "Remove"); + return ( + // wrap="nowrap" keeps the row single-line. + + + + + {/* Save to server; shown whenever local-only files are selected. When storage is off it stays visible but disabled, tooltip pointing at the admin. */} - {localOnlySelectedStubs.length > 0 && ( - - + + )} + {/* Show details button on compact viewports. */} + {selectedFiles.length === 1 && + isCompactDetailsViewport && ( + + + )} - > - {t("filesPage.saveToServer", "Save to server")} - - - )} - {/* Show details button on compact viewports. */} - {selectedFiles.length === 1 && - isCompactDetailsViewport && ( - + + + + + + clearSelection()} + aria-label={t( + "filesPage.clearSelection", + "Clear selection", + )} + > + × + + + + ); + })()} + {selectedFiles.length > 0 && ( +
@@ -1433,6 +1649,7 @@ export default function FileManagerView() { entries={entries} loading={loading} currentTab={currentTab} + searchActive={search.trim().length > 0} serverReachable={folders.serverReachable} selectedFileIds={selectedFileIds} activeWorkspaceFileIds={activeWorkspaceFileIdSet} @@ -1466,6 +1683,9 @@ export default function FileManagerView() { onPromptMoveFiles={promptMoveFiles} onSaveToServer={(file) => setSaveToServerTarget([file])} onVersionHistory={(file) => setVersionHistoryFile(file)} + onDownloadFile={handleDownloadFile} + onRenameFile={setRenameTarget} + onDuplicateFile={handleDuplicateFile} saveToServerDisabledReason={saveToServerDisabledReason} // Center-of-grid CTAs when the empty state shows - same // handlers the corner header buttons use so behaviour @@ -1625,6 +1845,14 @@ export default function FileManagerView() { onConfirm={confirmRemoveFiles} /> + {/* Rename (opened from the card kebab). */} + setRenameTarget(null)} + onSubmit={handleConfirmRename} + /> + {/* Version journey in a modal (opened from the card kebab). */} void } ->(function SearchField({ value, onChange }, ref) { - const { t } = useTranslation(); - return ( -
- - onChange(e.currentTarget.value)} - placeholder={t( - "filesPage.searchPlaceholder", - "Search this folder & subfolders", - )} - aria-label={t("filesPage.search", "Search")} - /> - {value && ( - onChange("")} - aria-label={t("filesPage.clearSearch", "Clear search")} - > - × - - )} -
- ); -}); - function Breadcrumbs() { const { t } = useTranslation(); const folders = useFolders(); diff --git a/frontend/editor/src/core/components/filesPage/FileOriginBadge.stories.tsx b/frontend/editor/src/core/components/filesPage/FileOriginBadge.stories.tsx new file mode 100644 index 0000000000..2f3ede8857 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FileOriginBadge.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FileOriginBadge } from "@app/components/filesPage/FileOriginBadge"; + +const meta: Meta = { + title: "FilesPage/FileOriginBadge", + component: FileOriginBadge, + parameters: { layout: "padded" }, +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + origin: "local", + }, +}; + +export const Cloud: Story = { + args: { + origin: "cloud", + }, +}; + +export const SharedWithMe: Story = { + args: { + origin: "shared-with-me", + }, +}; + +export const Compact: Story = { + args: { + origin: "cloud", + compact: true, + }, +}; diff --git a/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx b/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx index a4e9b6e3f9..3d94610e0b 100644 --- a/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx +++ b/frontend/editor/src/core/components/filesPage/FileOriginBadge.tsx @@ -32,12 +32,12 @@ const styles = { }, cloud: { background: "color-mix(in srgb, var(--c-primary) 16%, transparent)", - color: "var(--c-primary)", + color: "var(--c-accent-text)", }, shared: { background: "color-mix(in srgb, var(--mantine-color-orange-6) 16%, transparent)", - color: "var(--mantine-color-orange-6)", + color: "var(--color-amber-dark)", }, }; diff --git a/frontend/editor/src/core/components/filesPage/FilesPage.css b/frontend/editor/src/core/components/filesPage/FilesPage.css index dfdb35dcb7..6d4070edb6 100644 --- a/frontend/editor/src/core/components/filesPage/FilesPage.css +++ b/frontend/editor/src/core/components/filesPage/FilesPage.css @@ -34,6 +34,33 @@ gap: 0.5rem; } +.files-page-header-search { + display: flex; + align-items: center; + justify-content: center; + min-width: 0; +} + +.files-page-header-search .super-search { + flex: 0 1 24rem; + width: min(100%, 24rem); + max-width: 24rem; +} + +.files-page-header-search .super-search input { + background-color: transparent; + padding-top: 4px; + padding-bottom: 4px; + font-size: 12.5px; +} + +[data-mantine-color-scheme="dark"] + .files-page-header-search + .super-search + input { + background-color: transparent; +} + .files-page-breadcrumbs { display: flex; align-items: center; @@ -79,28 +106,6 @@ flex-shrink: 0; } -.files-page-search { - display: flex; - align-items: center; - gap: 0.35rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: 999px; - padding: 0.2rem 0.75rem; - /* Fills its grid cell; the cell's minmax(...) clamps to a sensible range. */ - width: 100%; - min-width: 0; -} - -.files-page-search input { - background: transparent; - border: none; - outline: none; - flex: 1; - color: var(--c-text); - font-size: 0.9rem; -} - .files-page-body { display: flex; flex: 1 1 auto; @@ -156,6 +161,11 @@ flex-shrink: 0; flex-wrap: wrap; min-height: 3rem; + /* Safety net - the content should always fit thanks to the shrink rules on + `.files-page-toolbar-actions` below, but if a future addition ever breaks + that, this stops the action group bleeding into sibling areas (notably the + details panel on the right). */ + overflow-x: hidden; } .files-page-toolbar-info { @@ -205,18 +215,22 @@ .files-page-toolbar-actions .mantine-Button-label { display: none; } +/* Exception: the bulk-actions trigger IS its label. Collapsed to an icon it + would read as a bare chevron with nothing to say what it opens. */ +.files-page-toolbar-actions + .files-page-toolbar-bulk-trigger + .mantine-Button-label { + display: inline; +} +.files-page-toolbar-actions .files-page-toolbar-bulk-trigger { + padding-left: 0.75rem; + padding-right: 0.5rem; +} /* Pin the view toggle: never let it clip off the right. flex-shrink:0 keeps its width fixed; the rest of the row shrinks around it. */ .files-page-toolbar-actions .mantine-SegmentedControl-root { flex-shrink: 0; } -/* Clip overflow on the toolbar itself as a safety net - the content - should always fit thanks to the rules above, but if a future addition - ever breaks that, this prevents the action group from bleeding into - sibling areas (notably the details panel on the right). */ -.files-page-toolbar { - overflow-x: hidden; -} .files-page-toolbar-divider { width: 1px; @@ -341,6 +355,9 @@ } .files-page-list-row.is-header [data-sortable="true"] { + /* Block so the hit area and hover tint fill the columnheader cell that wraps + it, rather than hugging the label text. */ + display: block; cursor: pointer; padding: 0.2rem 0.4rem; margin: -0.2rem -0.4rem; @@ -470,6 +487,23 @@ gap: 0.4rem; } +/* Narrow cards: let the values wrap as whole units onto their own lines rather + than breaking mid-value ("239.26 / KB") around a stranded separator. */ +@media (max-width: 64rem) { + .files-page-card-meta { + flex-wrap: wrap; + align-items: baseline; + column-gap: 0.4rem; + row-gap: 0.05rem; + } + .files-page-card-meta > span { + white-space: nowrap; + } + .files-page-card-meta-sep { + display: none; + } +} + /* Parent-folder breadcrumb shown on cards/rows during recursive search so the user can tell which folder each hit lives in without navigating. */ .files-page-card-path { @@ -743,7 +777,7 @@ height: 5rem; border-radius: 50%; background: color-mix(in srgb, var(--c-primary) 12%, transparent); - color: var(--c-primary); + color: var(--c-accent-text); margin-bottom: 0.25rem; } @@ -958,110 +992,160 @@ like a commit graph; clicking a row's summary toggles its expanded detail (cumulative tool chain, full meta). Long chains (> 6) collapse the middle behind a "Show N earlier versions" button. */ +/* Version Timeline styling */ .files-page-details-version-timeline { display: flex; flex-direction: column; - gap: 0.4rem; - padding: 0.55rem 0.7rem 0.7rem; - background: var(--c-surface); - border: 1px solid var(--c-border-subtle); - border-radius: 0.5rem; + gap: 0.6rem; + padding: 0.5rem 0; + background: transparent; } + .files-page-details-version-timeline-label { display: flex; align-items: center; - gap: 0.35rem; - font-size: 0.72rem; + gap: 0.4rem; + font-size: 0.75rem; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--c-text-subtle); + margin-bottom: 0.85rem; } + .files-page-details-version-timeline-count { margin-left: auto; font-weight: 600; - color: var(--c-text-muted, var(--c-text-subtle)); + color: var(--c-accent-text); text-transform: none; letter-spacing: 0; } + .files-page-details-version-timeline-list { - list-style: none; - margin: 0; - padding: 0; + list-style: none !important; + margin: 0 !important; + padding: 1.1rem 0 0 0 !important; display: flex; flex-direction: column; + gap: 1.25rem; } + .files-page-details-version-timeline-row, .files-page-details-version-timeline-ellipsis { display: flex; - gap: 0.6rem; - padding: 0.15rem 0; + gap: 0.85rem; + padding: 0; position: relative; + list-style: none !important; } + +.files-page-details-version-timeline-row::before, +.files-page-details-version-timeline-ellipsis::before { + content: none !important; +} + .files-page-details-version-timeline-rail { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; - width: 0.8rem; - padding-top: 0.45rem; + width: 1rem; + padding-top: 0.85rem; } + .files-page-details-version-timeline-rail-dot { - width: 0.55rem; - height: 0.55rem; + width: 0.65rem; + height: 0.65rem; border-radius: 50%; background: var(--c-bg-raised); border: 2px solid var(--c-border-strong, var(--c-border-subtle)); - z-index: 1; + z-index: 2; flex-shrink: 0; + transition: all 0.2s ease; } + .files-page-details-version-timeline-rail-dot.is-active { background: var(--c-primary); border-color: var(--c-primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 25%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 30%, transparent); } + .files-page-details-version-timeline-rail-dot.is-ellipsis { - width: 0.35rem; - height: 0.35rem; + width: 0.4rem; + height: 0.4rem; background: var(--c-text-subtle); border-color: transparent; } + .files-page-details-version-timeline-rail-line { width: 2px; flex: 1; background: var(--c-border-subtle); - min-height: 0.6rem; - margin-top: 2px; + min-height: 1.25rem; + margin-top: 6px; } + .files-page-details-version-timeline-body { flex: 1; min-width: 0; display: flex; flex-direction: column; - gap: 0.2rem; - padding: 0.25rem 0.3rem 0.4rem; - border-radius: 0.35rem; - transition: background-color 0.12s ease; + gap: 0.5rem; + padding: 1rem 1.25rem; + border-radius: 0.65rem; + background: var(--c-surface); + border: 1px solid var(--c-border-subtle); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + transition: + border-color 0.15s ease, + background-color 0.15s ease, + box-shadow 0.15s ease; } + +.files-page-details-version-timeline-body:hover { + border-color: var(--c-border-strong, var(--c-border-subtle)); +} + .files-page-details-version-timeline-row.is-active .files-page-details-version-timeline-body { - background: color-mix(in srgb, var(--c-primary) 10%, transparent); + border-color: color-mix(in srgb, var(--c-primary) 40%, transparent); + background: color-mix(in srgb, var(--c-primary) 5%, var(--c-surface)); + box-shadow: 0 2px 6px color-mix(in srgb, var(--c-primary) 12%, transparent); } -.files-page-details-version-timeline-summary { + +.files-page-details-version-timeline-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.files-page-details-version-timeline-tool-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--c-text); +} + +.files-page-details-version-timeline-card-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding-top: 0.1rem; +} + +.files-page-details-version-timeline-expand-btn { appearance: none; background: none; border: 0; padding: 0; - margin: 0; + cursor: pointer; display: flex; align-items: center; - gap: 0.4rem; - cursor: pointer; - text-align: left; - color: inherit; - font: inherit; } -.files-page-details-version-timeline-summary:hover - .files-page-details-version-timeline-chevron { + +.files-page-details-version-timeline-expand-btn:hover span { color: var(--c-text); } .files-page-details-version-timeline-delta { @@ -1081,7 +1165,7 @@ font-style: italic; } .files-page-details-version-timeline-delta-plus { - color: var(--c-primary); + color: var(--c-accent-text); font-weight: 700; } .files-page-details-version-timeline-spacer { @@ -1091,45 +1175,43 @@ color: var(--c-text-subtle); transition: transform 0.15s ease; } + .files-page-details-version-timeline-chevron.is-expanded { transform: rotate(180deg); - color: var(--c-text); -} -.files-page-details-version-timeline-meta-line { - display: flex; - align-items: center; - gap: 0.35rem; - font-size: 0.7rem; - color: var(--c-text-subtle); + color: var(--c-accent-text); } + .files-page-details-version-timeline-expanded { display: flex; flex-direction: column; gap: 0.4rem; - margin-top: 0.45rem; + margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px dashed var(--c-border-subtle); } + .files-page-details-version-timeline-toolchain { display: flex; flex-direction: column; gap: 0.2rem; } + .files-page-details-version-timeline-toolchain-label { font-size: 0.65rem; color: var(--c-text-subtle); text-transform: uppercase; letter-spacing: 0.05em; } + .files-page-details-version-timeline-ellipsis-btn, .files-page-details-version-timeline-collapse-btn { appearance: none; background: none; border: 1px dashed var(--c-border-subtle); - border-radius: 0.3rem; - padding: 0.25rem 0.5rem; + border-radius: 0.4rem; + padding: 0.35rem 0.7rem; margin: 0.1rem 0; - font-size: 0.72rem; + font-size: 0.75rem; color: var(--c-text-subtle); cursor: pointer; text-align: left; @@ -1137,11 +1219,13 @@ border-color 0.12s ease, color 0.12s ease; } + .files-page-details-version-timeline-ellipsis-btn:hover, .files-page-details-version-timeline-collapse-btn:hover { color: var(--c-text); - border-color: var(--c-border-strong, var(--c-text-subtle)); + border-color: var(--c-primary); } + .files-page-details-version-timeline-collapse-btn { align-self: flex-start; margin-top: 0.2rem; @@ -1159,7 +1243,7 @@ justify-content: center; gap: 1rem; pointer-events: none; - color: var(--c-primary); + color: var(--c-accent-text); font-weight: 600; font-size: 1.1rem; z-index: 10; @@ -1239,30 +1323,38 @@ sits next to the Upload button without breaking the action row. */ display: none; } -@media (max-width: 900px) { +@media (max-width: 1024px) { .files-page-toolbar { /* nowrap so "7 items" + "Select all" sit on the same row as the filter dropdowns and view-toggle instead of stacking on three - separate lines. Per-child min-width:0 lets them shrink as needed. - Used to only kick in at ≤640px which left a broken zone where - both side panels were hidden but the toolbar was still wrapping - to multiple rows. */ + separate lines. Runs to the app's mobile breakpoint: capping it at + 900px left 901-1024px wrapping to two rows, which is the band the + mobile layout actually renders in. + + Scrolls rather than clips. With a selection active the bulk-action + strip cannot fit any phone width, and `overflow-x: hidden` put those + buttons permanently out of reach behind the edge. */ flex-wrap: nowrap; gap: 0.35rem; padding: 0.35rem 0.5rem; min-height: auto; - overflow-x: hidden; + overflow-x: auto; + scrollbar-width: none; + } + .files-page-toolbar::-webkit-scrollbar { + display: none; } .files-page-toolbar-info { - /* Was `flex-basis: 100%` which forced a row break. Let it share - the row, shrink hard if needed, and ellipsize so the count line - collapses gracefully (was overlapping the bulk-action buttons - at ~400px because no truncation rule existed). */ - flex: 0 1 auto; + /* The toolbar's only status text. Pinned, because against nowrap + siblings it lost every shrink round and rendered as "3 i". */ + flex-shrink: 0; min-width: 0; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + } + /* Filter and sort collapse to icon triggers here (see FilesToolbar*Menu); + they are the whole control, so they never shrink. */ + .files-page-toolbar-icon-btn { + flex-shrink: 0; } .files-page-toolbar-actions { flex-wrap: nowrap; @@ -1294,61 +1386,48 @@ navigation, so the in-header Home/Apps/Close trio is duplicated and the first to go. Same for "Upload" - the user can use the centre drop overlay. */ -@media (max-width: 640px) { - /* Drop the 3-column grid on phones; flex-wrap lets the search slip onto - * its own row when chrome is too cramped to share. */ +/* ── Mobile + tablet chrome (≤1024px = useIsMobile) ────────────────── + The desktop header is a 3-column grid whose middle track can grow to + 40rem. Below ~1024px that track eats the row: the breadcrumb column + collapsed to ~36px (wrapping "All files" to two lines) and the action + column overflowed, pushing Upload off the right edge. One flex row + instead - breadcrumb and actions keep their intrinsic width and the + search takes whatever is left. Ends at the app's mobile breakpoint so + it matches the layout HomePage is already rendering. */ +@media (max-width: 1024px) { .files-page-header { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; gap: 0.4rem; - padding: 0 0.4rem; + padding: 0.25rem 0.4rem; overflow-x: hidden; } - .files-page-header [data-mobile-hide="true"] { - display: none !important; + .files-page-header-search { + flex: 1 1 auto; + min-width: 0; + justify-content: flex-start; } - .files-page-header [data-desktop-hide="true"] { - display: inline-flex !important; - } - /* Mobile-hide for sub-toolbar create buttons. */ - .files-page-toolbar [data-mobile-hide="true"] { - display: none !important; + /* Undo the fixed 24rem basis so the pill tracks the row's spare width. */ + .files-page-header-search .super-search { + flex: 1 1 auto; + width: 100%; + max-width: none; } .files-page-header-actions { + flex: 0 0 auto; margin-left: auto; - gap: 0.3rem; + gap: 0.25rem; flex-wrap: nowrap; } - .files-page-search { - flex: 1 1 100%; - } .files-page-breadcrumbs { + flex: 0 1 auto; font-size: 0.85rem; flex-wrap: nowrap; overflow-x: auto; min-width: 0; } - .files-page-search { - /* Shrink hard so the search bar doesn't eat the whole action row. - Users still see the icon + a few chars of the placeholder. - `overflow: hidden` clips the input's natural intrinsic width so - placeholder text never leaks outside the rounded pill. */ - min-width: 0; - flex: 0 1 5.5rem; - max-width: 6.5rem; - overflow: hidden; - } - .files-page-search input { - /* `min-width: 0` lets flex actually shrink the input below its - default ~20-char intrinsic size - without this, the placeholder - extends beyond the parent's clip box and bleeds onto neighbours. */ - min-width: 0; - text-overflow: ellipsis; - } - /* Upload becomes an icon-only square button on mobile so the action - row stops getting clipped. Scoped to `.files-page-header-actions` - so the Back button at the header level keeps its visible "Back" - label (Back has no other on-screen indicator that it's about leaving). */ + /* Icon-only actions: the labels are what pushed Upload past the edge. */ .files-page-header-actions .mantine-Button-root { padding-left: 0.55rem; padding-right: 0.55rem; @@ -1359,6 +1438,9 @@ .files-page-header-actions .mantine-Button-label { display: none; } +} + +@media (max-width: 640px) { /* Grid: single column on very narrow phones; two columns from ~440px */ .files-page-grid { grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx new file mode 100644 index 0000000000..3e344ac7b0 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarBulkMenu.tsx @@ -0,0 +1,104 @@ +import { Menu } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DriveFileMoveIcon from "@mui/icons-material/DriveFileMove"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; + +import { Button } from "@app/ui/Button"; + +interface FilesToolbarBulkMenuProps { + selectedCount: number; + onAddToWorkspace: () => void; + /** Local-only files in the selection; omit when there are none to upload. */ + onSaveToServer?: () => void; + /** Set when storage is off - the item stays listed but disabled. */ + saveToServerDisabledReason?: string; + onShowDetails?: () => void; + onMove: () => void; + onRemove: () => void; +} + +/** + * Bulk actions behind one trigger. The full strip is five buttons wide, which + * no phone can hold alongside the count and the clear control, so rather than + * letting the row scroll them off the edge they collapse into a menu where + * every action keeps its label. + */ +export function FilesToolbarBulkMenu({ + selectedCount, + onAddToWorkspace, + onSaveToServer, + saveToServerDisabledReason, + onShowDetails, + onMove, + onRemove, +}: FilesToolbarBulkMenuProps) { + const { t } = useTranslation(); + + const addLabel = + selectedCount === 1 + ? t("filesPage.addToWorkspace", "Add to workspace") + : t("filesPage.addToWorkspaceCount", "Add {{count}} to workspace", { + count: selectedCount, + }); + + return ( + + + + + + } + onClick={onAddToWorkspace} + > + {addLabel} + + {onSaveToServer && ( + } + disabled={Boolean(saveToServerDisabledReason)} + onClick={onSaveToServer} + > + {t("filesPage.saveToServer", "Save to server")} + + )} + {onShowDetails && ( + } + onClick={onShowDetails} + > + {t("filesPage.showDetails", "Show details")} + + )} + } + onClick={onMove} + > + {t("filesPage.moveTo", "Move to…")} + + + } + onClick={onRemove} + > + {t("filesPage.remove", "Remove")} + + + + ); +} + +export default FilesToolbarBulkMenu; diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx new file mode 100644 index 0000000000..d808638d63 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarCount.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; + +interface FilesToolbarCountProps { + loading: boolean; + totalCount: number; + selectedCount: number; + /** + * Selection-bar mode: report only the selection. A phone spends the room on + * the actions rather than on "3 items · 3 selected". + */ + selectionOnly: boolean; +} + +/** Status text at the head of the files toolbar. */ +export function FilesToolbarCount({ + loading, + totalCount, + selectedCount, + selectionOnly, +}: FilesToolbarCountProps) { + const { t } = useTranslation(); + + const selected = t("filesPage.selectedCount", "{{count}} selected", { + count: selectedCount, + }); + + if (selectionOnly) { + return {selected}; + } + + return ( + + {loading + ? t("filesPage.loading", "Loading…") + : t("filesPage.summary", "{{count}} items", { count: totalCount })} + {selectedCount > 0 && · {selected}} + + ); +} + +export default FilesToolbarCount; diff --git a/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx b/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx new file mode 100644 index 0000000000..81d84daf67 --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FilesToolbarFilterMenu.tsx @@ -0,0 +1,149 @@ +import { MultiSelect, Popover, Select, Stack, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CloseIcon from "@mui/icons-material/Close"; +import SearchIcon from "@mui/icons-material/Search"; +import TuneIcon from "@mui/icons-material/Tune"; + +import { ActionIcon } from "@app/ui/ActionIcon"; +import { Button } from "@app/ui/Button"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import type { FilesPageOriginFilter } from "@app/contexts/FilesPageContext"; + +interface FilesToolbarFilterMenuProps { + originFilter: FilesPageOriginFilter; + onOriginChange: (value: FilesPageOriginFilter) => void; + availableTypes: string[]; + typeFilter: string[]; + onTypeChange: (value: string[]) => void; + search: string; + onSearchChange: (value: string) => void; +} + +/** + * Source, type and name filters collapsed behind one icon. Side by side these + * three need ~480px, so on narrow viewports they were each truncated to + * unreadable stubs ("All sour"). In the popover they get their full width back, + * and a dot on the trigger keeps an active filter discoverable while hidden. + */ +export function FilesToolbarFilterMenu({ + originFilter, + onOriginChange, + availableTypes, + typeFilter, + onTypeChange, + search, + onSearchChange, +}: FilesToolbarFilterMenuProps) { + const { t } = useTranslation(); + + const activeCount = + (originFilter !== "all" ? 1 : 0) + + (typeFilter.length > 0 ? 1 : 0) + + (search.trim() !== "" ? 1 : 0); + const label = t("filesPage.filters.label", "Filters"); + + const clearAll = () => { + onOriginChange("all"); + onTypeChange([]); + onSearchChange(""); + }; + + return ( + + +
+ 0 + ? t( + "filesPage.filters.activeCount", + "{{count}} filters active", + { + count: activeCount, + }, + ) + : label + } + position="bottom" + > + 0 ? "primary" : "tertiary"} + size="sm" + aria-label={label} + className="files-page-toolbar-icon-btn" + > + + + +
+
+ + + onSearchChange(e.currentTarget.value)} + placeholder={t("filesPage.search.placeholder", "Filter files…")} + leftSection={} + rightSection={ + search ? ( + onSearchChange("")} + aria-label={t("filesPage.search.clear", "Clear filter")} + > + + + ) : null + } + aria-label={t("filesPage.search.label", "Filter files by name")} + /> + {}, + onSelectFiles: () => {}, + storedFiles: mockStoredFiles, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +/** Storage populated with a few files available to pick from. */ +export const Default: Story = {}; + +/** No files exist in storage yet — shows the empty-state message. */ +export const Empty: Story = { + args: { + storedFiles: [], + }, +}; diff --git a/frontend/editor/src/core/components/shared/FilePickerModal.tsx b/frontend/editor/src/core/components/shared/FilePickerModal.tsx index 5e077d3437..d9fc6e9502 100644 --- a/frontend/editor/src/core/components/shared/FilePickerModal.tsx +++ b/frontend/editor/src/core/components/shared/FilePickerModal.tsx @@ -189,6 +189,13 @@ const FilePickerModal = ({ checked={isSelected} onChange={() => toggleFileSelection(fileId)} onClick={(e) => e.stopPropagation()} + aria-label={t( + "fileUpload.selectFile", + "Select {{name}}", + { + name: file.name, + }, + )} /> {/* Thumbnail */} @@ -239,7 +246,7 @@ const FilePickerModal = ({ {/* Selection summary */} {selectedFileIds.length > 0 && ( - + {selectedFileIds.length}{" "} {t("fileManager.filesSelected", "files selected")} diff --git a/frontend/editor/src/core/components/shared/FilePreview.stories.tsx b/frontend/editor/src/core/components/shared/FilePreview.stories.tsx new file mode 100644 index 0000000000..30e2fec884 --- /dev/null +++ b/frontend/editor/src/core/components/shared/FilePreview.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FilePreview from "@app/components/shared/FilePreview"; +import { StirlingFileStub } from "@app/types/fileContext"; +import type { FileId } from "@app/types/file"; + +const mockFile: StirlingFileStub = { + id: "file-1" as FileId, + name: "annual-report.pdf", + type: "application/pdf", + size: 245_000, + lastModified: Date.now(), + isLeaf: true, + originalFileId: "file-1", + versionNumber: 1, +}; + +const meta = { + title: "Shared/FilePreview", + component: FilePreview, + parameters: { layout: "padded" }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + file: mockFile, + thumbnail: null, + }, +}; + +export const Empty: Story = { + args: { + file: null, + }, +}; + +export const WithNavigation: Story = { + args: { + file: mockFile, + thumbnail: null, + showStacking: true, + showHoverOverlay: true, + showNavigation: true, + totalFiles: 3, + onFileClick: () => {}, + onPrevious: () => {}, + onNext: () => {}, + }, +}; diff --git a/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css b/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css index cb388c28aa..82d45a9e2c 100644 --- a/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css +++ b/frontend/editor/src/core/components/shared/FileSelectorPicker.module.css @@ -134,7 +134,7 @@ } .slimTabUpload:hover:not(:disabled) { - color: var(--mantine-color-blue-filled); + color: var(--c-accent-text); background: color-mix(in srgb, var(--mantine-color-blue-1) 35%, transparent); } diff --git a/frontend/editor/src/core/components/shared/FileSelectorPicker.stories.tsx b/frontend/editor/src/core/components/shared/FileSelectorPicker.stories.tsx new file mode 100644 index 0000000000..3d01996b99 --- /dev/null +++ b/frontend/editor/src/core/components/shared/FileSelectorPicker.stories.tsx @@ -0,0 +1,48 @@ +import type { ReactElement } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FileSelectorPicker } from "@app/components/shared/FileSelectorPicker"; +import { FileContextProvider } from "@app/contexts/FileContext"; + +/** + * Reads from FileContext (workbench files) and IndexedDBContext (persisted + * saved files) further up the tree — neither is part of the shared preview + * decorators, so FileContextProvider (which also wraps IndexedDBProvider) is + * stood up here. The popover starts closed, so no IndexedDB read happens + * until a story interacts with it. + */ +function withFileContext(Story: () => ReactElement) { + return ( + +
+ +
+
+ ); +} + +const meta = { + title: "Shared/FileSelectorPicker", + component: FileSelectorPicker, + parameters: { layout: "padded" }, + args: { + onSelect: () => {}, + }, + decorators: [withFileContext], +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const CustomPlaceholder: Story = { + args: { + placeholder: "Choose a comparison file", + }, +}; + +export const Disabled: Story = { + args: { + disabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/shared/FileSelectorPicker.tsx b/frontend/editor/src/core/components/shared/FileSelectorPicker.tsx index 83a249c38f..17502ee9b6 100644 --- a/frontend/editor/src/core/components/shared/FileSelectorPicker.tsx +++ b/frontend/editor/src/core/components/shared/FileSelectorPicker.tsx @@ -404,6 +404,7 @@ export function FileSelectorPicker({ }} aria-expanded={isOpen} aria-haspopup="listbox" + aria-disabled={disabled || undefined} > : it brings its own boxes and padding, + so this class only positions it in the column. */ + +/* Collapsed rail: the file tree isn't rendered, so hide its (empty) box and + let the boxes stack at the top — controls, then the settings footer right + after — instead of the files box stretching to fill. */ +.file-sidebar[data-collapsed="true"] .file-sidebar-controls { + padding: 0.25rem; +} +.file-sidebar[data-collapsed="true"] .file-sidebar-files-box { + display: none; +} +.file-sidebar[data-collapsed="true"] .file-sidebar-inner { + flex: 0 0 auto; +} +/* Centre each row's icon in the narrow rail (no side padding/margin to shove + it off the edge). */ +.file-sidebar[data-collapsed="true"] .file-sidebar-action-row, +.file-sidebar[data-collapsed="true"] .file-sidebar-cloud-row { + justify-content: center; + padding-inline: 0; + margin: 0; +} /* ---- Native file drag-and-drop ---- */ .file-sidebar[data-file-drag-over] { @@ -48,119 +125,14 @@ } .file-sidebar-drop-overlay-icon { - color: var(--mantine-color-blue-6, var(--c-primary)) !important; + color: var(--c-accent-text) !important; font-size: 28px !important; } .file-sidebar-drop-overlay-text { font-size: 13px; font-weight: 600; - color: var(--mantine-color-blue-6, var(--c-primary)); -} - -/* ---- Header ---- */ -.file-sidebar-header { - display: flex; - align-items: center; - height: 48px; - padding: 0 14px; - gap: 10px; - cursor: pointer; - border-radius: 4px; - margin: 4px 4px 0 4px; - flex-shrink: 0; - transition: background-color 0.15s ease; -} - -/* Icons stay left-aligned during animation; overflow:hidden on inner clips text naturally */ - -.file-sidebar-header:hover { - background-color: var(--c-hover); -} - -.file-sidebar-menu-icon { - color: var(--c-text-subtle) !important; - font-size: 18px !important; - flex-shrink: 0; -} - -/* Inherits font-size so swap-in icons render at 18px like the original. */ -.file-sidebar-menu-icon > svg { - font-size: inherit; - width: 1em; - height: 1em; -} - -/* Flip directional toggle icons in RTL (skipped for the symmetric burger). */ -[dir="rtl"] .file-sidebar-menu-icon[data-toggle-flip-rtl="true"] > svg { - transform: scaleX(-1); -} - -.file-sidebar-brand-text { - height: 22px; - width: auto; - flex-shrink: 0; -} - -/* App switcher (portal builds only) sits at the far end of the header row. - The content-fade animation makes this span a stacking context, which would - trap the menu's z-index below later sidebar rows — elevate the span so the - open menu paints above them. */ -.file-sidebar-app-switch { - margin-inline-start: auto; - display: flex; - align-items: center; - position: relative; - z-index: var(--z-dropdown); -} - -/* ---- Search row ---- */ -.file-sidebar-search-row { - display: flex; - align-items: center; - min-height: 32px; - padding: 0 14px; - gap: 0; - cursor: pointer; - border-radius: 4px; - margin: 0 4px; - flex-shrink: 0; - transition: background-color 0.15s ease; -} - -.file-sidebar-search-row:not(.active):hover { - background-color: var(--c-hover); -} - -.file-sidebar-search-icon { - color: var(--c-text-subtle) !important; - font-size: 18px !important; - flex-shrink: 0; -} - -.file-sidebar-search-close { - cursor: pointer; -} - -.file-sidebar-search-input { - flex: 1; - background: transparent; - border: none; - outline: none; - font-size: 14px; - color: var(--c-text); - margin-left: 12px; - min-width: 0; -} - -.file-sidebar-search-input::placeholder { - color: var(--c-text-subtle); -} - -.file-sidebar-search-label { - margin-left: 12px; - font-size: 14px; - color: var(--c-text-muted); + color: var(--c-accent-text); } /* ---- Scrollable content ---- */ @@ -170,7 +142,8 @@ min-height: 0; display: flex; flex-direction: column; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; } .file-sidebar-scroll::-webkit-scrollbar { @@ -189,10 +162,10 @@ display: flex; align-items: center; height: 32px; - padding: 0 14px; + padding: 0 8px; cursor: pointer; border-radius: 4px; - margin: 0 4px; + margin: 0; gap: 0; transition: background-color 0.15s ease; flex-shrink: 0; @@ -234,7 +207,7 @@ .file-sidebar-action-label { margin-left: 12px; font-size: 14px; - color: var(--c-text-muted); + color: var(--c-text); white-space: nowrap; } @@ -243,10 +216,10 @@ display: flex; align-items: center; height: 32px; - padding: 0 14px; + padding: 0 8px; cursor: pointer; border-radius: 4px; - margin: 0 4px; + margin: 0; gap: 0; transition: background-color 0.15s ease; flex-shrink: 0; @@ -447,28 +420,21 @@ } .file-sidebar-section-header { + position: relative; display: flex; align-items: center; justify-content: space-between; gap: 8px; - padding: 12px 14px 6px 14px; - margin: 4px 0 0 0; -} - -.file-sidebar-section-header { - position: relative; - border-top: 1px solid var(--c-border-subtle); - padding-top: 10px; - margin-top: 4px; + padding: 0 6px 2px 6px; + margin: 0; flex-shrink: 0; } .file-sidebar-section-label { - font-size: 13px; + font-size: 0.875rem; font-weight: 600; - letter-spacing: 0.02em; - color: var(--c-text-subtle); - text-transform: uppercase; + letter-spacing: -0.01em; + color: var(--c-text); } /* Slim "Adding files… X/Y" progress row shown during a bulk drop's pre-scan, @@ -569,87 +535,3 @@ pointer-events: none; animation: none; } - -/* ---- Bottom bar (user + settings) ---- */ -.file-sidebar-bottom-bar { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 10px; - border-top: 1px solid var(--c-border-subtle); - flex-shrink: 0; - min-height: 48px; -} - -/* Bottom bar settings icon tracks the right edge during collapse animation */ - -.file-sidebar-bottom-avatar { - width: 28px; - height: 28px; - border-radius: 50%; - background-color: var(--mantine-color-blue-6, var(--c-primary)); - color: var(--c-text-on-primary); - font-size: 12px; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - user-select: none; - overflow: hidden; -} - -/* No colored disc behind an actual photo; keep it for the initials fallback. */ -.file-sidebar-bottom-avatar--picture { - background-color: transparent; -} - -.file-sidebar-bottom-avatar-img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; -} - -.file-sidebar-bottom-name { - flex: 1; - font-size: 13px; - font-weight: 500; - color: var(--c-text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} - -.file-sidebar-bottom-bar[role="button"]:hover { - background-color: var(--c-hover); -} - -.file-sidebar-bottom-bar[role="button"]:focus-visible { - outline: 2px solid var(--c-primary); - outline-offset: -2px; -} - -.file-sidebar-bottom-settings { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 6px; - color: var(--c-text-subtle); - padding: 0; - flex-shrink: 0; - margin-left: auto; -} - -.file-sidebar[data-collapsed="true"] .file-sidebar-bottom-settings { - width: 32px; - height: 32px; -} - -.file-sidebar[data-collapsed="true"] .file-sidebar-bottom-bar { - justify-content: center; - padding: 8px 0; -} diff --git a/frontend/editor/src/core/components/shared/FileSidebar.tsx b/frontend/editor/src/core/components/shared/FileSidebar.tsx index 501c5385d1..3979a40041 100644 --- a/frontend/editor/src/core/components/shared/FileSidebar.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebar.tsx @@ -8,6 +8,7 @@ import React, { } from "react"; import { Loader, Tooltip } from "@mantine/core"; import { ActionIcon } from "@app/ui/ActionIcon"; +import { NavSurface } from "@app/ui/NavSurface"; import { Button } from "@app/ui/Button"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -21,26 +22,24 @@ import { } from "@app/contexts/NavigationContext"; import { useViewer } from "@app/contexts/ViewerContext"; import { useFileHandler } from "@app/hooks/useFileHandler"; -import { useAuth } from "@app/auth/UseSession"; -import { useProfilePictureUrl } from "@app/hooks/useProfilePictureUrl"; +import { useAccountIdentity } from "@app/hooks/useAccountIdentity"; +import { useFreeCreditsSummary } from "@app/hooks/useFreeCreditsSummary"; +import { useOtherAppSwitch } from "@app/hooks/useOtherAppSwitch"; +import { useOpenPlan } from "@app/hooks/useOpenPlan"; +import { NavFooter } from "@app/components/shared/navFooter/NavFooter"; import { useIndexedDB, useIndexedDBRevision, } from "@app/contexts/IndexedDBContext"; -import { accountService } from "@app/services/accountService"; import { GoogleDriveIcon } from "@app/components/shared/CloudStorageIcons"; -import { Wordmark } from "@app/components/shared/Wordmark"; import { AppSwitcher } from "@app/components/shared/AppSwitcher"; +import { SidebarToggleIcon } from "@app/components/shared/SidebarToggleIcon"; import type { StirlingFileStub } from "@app/types/fileContext"; -import MenuIcon from "@mui/icons-material/Menu"; -import SearchIcon from "@mui/icons-material/Search"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import FolderSpecialIcon from "@mui/icons-material/FolderSpecial"; import UploadFileIcon from "@mui/icons-material/UploadFile"; -import CloseIcon from "@mui/icons-material/Close"; import AddIcon from "@mui/icons-material/Add"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import SettingsIcon from "@mui/icons-material/Settings"; +import OpenInFullIcon from "@mui/icons-material/OpenInFull"; import type { FileId } from "@app/types/file"; import { FileItem } from "@app/components/shared/FileSidebarFileItem"; import { useLabelName } from "@app/data/labelDisplay"; @@ -56,12 +55,17 @@ import BulkUploadToServerModal from "@app/components/shared/BulkUploadToServerMo import { getFileOrigin } from "@app/components/filesPage/fileOrigin"; import { VersionHistoryModal } from "@app/components/filesPage/VersionHistoryModal"; import { DeleteFilesDialog } from "@app/components/filesPage/DeleteFilesDialog"; +import { RenameFileDialog } from "@app/components/shared/RenameFileDialog"; +import { duplicateStoredFile } from "@app/utils/duplicateFile"; import { SidebarChecklistSlot } from "@app/components/shared/SidebarChecklistSlot"; import { deleteServerFile, type DeleteScope, } from "@app/services/serverStorageDelete"; -import { fileStorage } from "@app/services/fileStorage"; +import { fileStorage, onRecordUnreadable } from "@app/services/fileStorage"; +import { downloadFileWithPolicy } from "@app/services/exportWithPolicy"; +import { useOpenInNewWindow } from "@app/extensions/openInNewWindow"; +import { alert } from "@app/components/toast"; import { useBulkAddProgress } from "@app/services/bulkAddProgress"; import { useFolderMembership } from "@app/hooks/useFolderMembership"; import { useAllWatchedFolders } from "@app/hooks/useAllWatchedFolders"; @@ -81,9 +85,10 @@ const EXPANDED_WIDTH = "16.25rem"; // ~260px const WATCHED_FOLDER_VIEW_ID = "watchedFolder"; const WATCHED_FOLDER_WORKBENCH_ID = "custom:watchedFolder"; -// Stable empty props for rows without folders, so the memoized FileItem -// isn't re-rendered by a fresh `?? []` identity on every list render. +// Stable empty props for rows without folders/policies, so the memoized +// FileItem isn't re-rendered by a fresh `?? []` identity on every list render. const NO_FOLDERS: never[] = []; +const NO_POLICIES: never[] = []; /** Only surface the "Adding files…" progress row for drops big enough that the * pre-dispatch scan is user-visible; small adds finish before it would paint. */ @@ -101,8 +106,6 @@ export interface FileSidebarProps { onUploadFiles?: (files: File[]) => void | Promise; /** Override the Google Drive handler. */ onPickGoogleDriveFiles?: (files: File[]) => void | Promise; - /** Override the Search row click (e.g. focus the /files search input). */ - onSearchClick?: () => void; /** Extra action row inserted under Open-from-computer (e.g. New folder). */ extraAction?: { icon: React.ReactNode; @@ -152,12 +155,11 @@ const FileSidebar = forwardRef( collapsed = false, onToggleCollapse, onOpenSettings, - toggleAriaLabel, - toggleIcon, onUploadFiles, onPickGoogleDriveFiles, - onSearchClick, extraAction, + toggleAriaLabel, + toggleIcon, }, ref, ) { @@ -167,9 +169,6 @@ const FileSidebar = forwardRef( // Classification off (non-SaaS / AI-off) → never show the per-row label chip, // even if a stub carries labels from an imported PDF; keeps the row plain. const classificationEnabled = useClassificationEnabled(); - const [searchActive, setSearchActive] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const searchInputRef = useRef(null); const nativeFileInputRef = useRef(null); // State (not ref) so setting it triggers a re-render - avoids racing addFiles state updates. const [pendingViewFileId, setPendingViewFileId] = useState( @@ -247,46 +246,27 @@ const FileSidebar = forwardRef( const { addFiles } = useFileHandler(); const indexedDB = useIndexedDB(); - // Each auth layer derives its own displayName from its native user shape. - // Fall back to the proprietary REST endpoint only when the auth - // context yields nothing - then to "User" as a generic last resort. - const { displayName: authDisplayName, isAnonymous } = useAuth(); - const [accountUsername, setAccountUsername] = useState(null); - const displayName = - authDisplayName ?? accountUsername ?? t("auth.displayName.user", "User"); - - const profilePictureUrl = useProfilePictureUrl(); - const [pictureFailed, setPictureFailed] = useState(false); - useEffect(() => setPictureFailed(false), [profilePictureUrl]); - const showProfilePicture = !!profilePictureUrl && !pictureFailed; - - useEffect(() => { - if (!config?.enableLogin) { - setAccountUsername(null); - return; - } - if (authDisplayName) { - // The auth context has a name; don't bother hitting the REST - // endpoint, but clear any stale cached value from a prior call. - setAccountUsername(null); - return; - } - accountService - .getAccountData() - .then((data) => { - // Always reflect the latest result - including clearing it on - // sign-out, when the endpoint returns no username (or 401s into - // the catch branch below). Without this, signing out would leave - // the old username on screen. - setAccountUsername(data?.username ?? null); - }) - .catch(() => { - setAccountUsername(null); - }); - }, [config?.enableLogin, authDisplayName]); + const { displayName, profilePictureUrl, isAnonymous } = + useAccountIdentity(); + const credits = useFreeCreditsSummary(); + const otherApp = useOtherAppSwitch(); + const openPlan = useOpenPlan(); // Leaf files = user-visible files (excludes intermediate tool outputs) const [allFileStubs, setAllFileStubs] = useState([]); + // Files whose stored bytes this session PROVED unreadable. Rows render a + // "data lost" state instead of pretending the file can open; storage keeps + // the record so a reload re-tests it. + const [lostFileIds, setLostFileIds] = useState>( + () => new Set(), + ); + useEffect( + () => + onRecordUnreadable((fileId) => + setLostFileIds((prev) => new Set(prev).add(fileId as string)), + ), + [], + ); const [stubsLoaded, setStubsLoaded] = useState(false); // Kebab "Save to cloud" target; drives BulkUploadToServerModal. const [saveToServerTarget, setSaveToServerTarget] = useState< @@ -300,37 +280,54 @@ const FileSidebar = forwardRef( const [deleteTarget, setDeleteTarget] = useState( null, ); + // Kebab "Rename" target; drives RenameFileDialog. + const [renameTarget, setRenameTarget] = useState( + null, + ); // Storage gate: only offer Save-to-cloud when the server allows it and // the user is signed in (guests have no cloud library). const storageEnabled = config?.storageEnabled === true && !isAnonymous; const refreshStubs = useCallback(async () => { - // Leaf files from IDB - same source as the file selection modal. - const stubs = await indexedDB.loadLeafMetadata(); - const idbIds = new Set(stubs.map((s) => s.id as string)); + // `stubsLoaded` gates the spinner, so the `finally` below must set it on + // every path - callers never await this, so a rejection goes nowhere. + let stubs: StirlingFileStub[] = []; + try { + // Leaf files from IDB - same source as the file selection modal. + stubs = await indexedDB.loadLeafMetadata(); + } catch (error) { + // Carry on with the in-memory workbench files: an unreadable library + // should cost the user their history, not the file they're working on. + console.error("Failed to read the file library from storage:", error); + } - // Also include workbench files not yet flushed to IDB. - const pendingStubs = state.files.ids - .map((id) => state.files.byId[id]) - .filter( - (stub): stub is NonNullable => - !!stub && stub.isLeaf !== false && !idbIds.has(stub.id as string), + try { + const idbIds = new Set(stubs.map((s) => s.id as string)); + + // Also include workbench files not yet flushed to IDB. + const pendingStubs = state.files.ids + .map((id) => state.files.byId[id]) + .filter( + (stub): stub is NonNullable => + !!stub && stub.isLeaf !== false && !idbIds.has(stub.id as string), + ); + + const allStubs = [...stubs, ...pendingStubs]; + // A version swap briefly lists both the old leaf (IDB) and its replacement (workbench); two stubs for one lineage collide on the row key and corrupt React reconciliation, so drop any stub another names as its parent. + const superseded = new Set( + allStubs.map((s) => s.parentFileId as string | undefined), ); - - const allStubs = [...stubs, ...pendingStubs]; - // A version swap briefly lists both the old leaf (IDB) and its replacement (workbench); two stubs for one lineage collide on the row key and corrupt React reconciliation, so drop any stub another names as its parent. - const superseded = new Set( - allStubs.map((s) => s.parentFileId as string | undefined), - ); - const currentStubs = allStubs.filter( - (s) => !superseded.has(s.id as string), - ); - setAllFileStubs( - currentStubs.sort( - (a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0), - ), - ); - setStubsLoaded(true); + const currentStubs = allStubs.filter( + (s) => !superseded.has(s.id as string), + ); + setAllFileStubs( + currentStubs.sort( + (a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0), + ), + ); + } finally { + setStubsLoaded(true); + } }, [indexedDB, state.files.ids, state.files.byId]); // Refresh on mount, workbench changes, or external IndexedDB writes — @@ -369,7 +366,9 @@ const FileSidebar = forwardRef( setDeleteTarget(stub); return; } - await fileActions.removeFiles([fileId], true); + // Its superseded versions go too - see orphanedAncestorIds. + const orphans = await fileStorage.orphanedAncestorIds([fileId]); + await fileActions.removeFiles([fileId, ...orphans], true); await refreshStubs(); }, [allFileStubs, fileActions, refreshStubs], @@ -387,7 +386,8 @@ const FileSidebar = forwardRef( await deleteServerFile(stub.remoteStorageId); } if (scope === "device" || scope === "everywhere") { - await fileActions.removeFiles([stub.id], true); + const orphans = await fileStorage.orphanedAncestorIds([stub.id]); + await fileActions.removeFiles([stub.id, ...orphans], true); } else if (scope === "cloud") { // Local copy kept - drop the dead remote pointer so the cloud badge // clears (the sidebar doesn't reconcile with the server itself). @@ -425,6 +425,121 @@ const FileSidebar = forwardRef( [allFileStubs], ); + const warnDataUnavailable = useCallback(() => { + alert({ + alertType: "warning", + title: t("fileSidebar.dataLostTitle", "File data is unavailable"), + body: t( + "fileSidebar.dataLostBody", + "This browser lost this file's contents. Upload it again to keep working with it.", + ), + expandable: false, + durationMs: 6000, + }); + }, [t]); + + // Kebab: download a copy (desktop saves via the native dialog). Routed + // through the policy wrapper so export policies enforce here too. + const handleDownload = useCallback( + async (fileId: FileId) => { + const stub = allFileStubs.find((s) => s.id === fileId); + const file = await fileStorage.getStirlingFile(fileId); + if (!file) { + warnDataUnavailable(); + return; + } + try { + await downloadFileWithPolicy({ + data: file, + filename: stub?.name ?? file.name, + fileId: fileId as string, + }); + } catch (error) { + console.error("[FileSidebar] Download failed:", error); + alert({ + alertType: "error", + title: t("fileSidebar.downloadFailed", "Download failed"), + body: error instanceof Error ? error.message : String(error), + expandable: false, + }); + } + }, + [allFileStubs, warnDataUnavailable, t], + ); + + // Kebab: copy the file into the library under a free "(copy)" name. + const handleDuplicate = useCallback( + async (fileId: FileId) => { + const stub = allFileStubs.find((s) => s.id === fileId); + if (!stub) return; + try { + const copyId = await duplicateStoredFile( + stub, + allFileStubs.map((s) => s.name), + addFiles, + ); + if (!copyId) { + warnDataUnavailable(); + return; + } + await refreshStubs(); + } catch (error) { + console.error("[FileSidebar] Duplicate failed:", error); + alert({ + alertType: "error", + title: t("fileSidebar.duplicateFailed", "Could not duplicate file"), + body: error instanceof Error ? error.message : String(error), + expandable: false, + }); + } + }, + [allFileStubs, addFiles, refreshStubs, warnDataUnavailable, t], + ); + + // Kebab: open the rename dialog for this one file. + const handleRename = useCallback( + (fileId: FileId) => { + const stub = allFileStubs.find((s) => s.id === fileId); + if (stub) setRenameTarget(stub); + }, + [allFileStubs], + ); + + // The stub name is what the UI and exports read, so a rename is a metadata + // write - storage first, then the workbench copy if the file is open. + const handleConfirmRename = useCallback( + async (name: string) => { + const stub = renameTarget; + if (!stub) return; + // quickKey is name|size|lastModified; a stale one would make a re-upload + // of the original look like a duplicate of the renamed file. + const quickKey = `${name}|${stub.size}|${stub.lastModified}`; + const saved = await fileStorage.updateFileMetadata(stub.id, { + name, + quickKey, + }); + if (!saved) { + throw new Error( + t("fileSidebar.rename.error", "Could not rename the file."), + ); + } + fileActions.updateStirlingFileStub(stub.id, { name, quickKey }); + setRenameTarget(null); + await refreshStubs(); + }, + [renameTarget, fileActions, refreshStubs, t], + ); + + // Desktop-only; a no-op stub on web, where this stays hidden. + const { canOpenInNewWindow, openInNewWindow } = useOpenInNewWindow(); + const handleOpenInNewWindow = useCallback( + (fileId: FileId) => { + const stub = allFileStubs.find((s) => s.id === fileId); + if (stub) openInNewWindow(stub); + }, + [allFileStubs, openInNewWindow], + ); + // Once a pending file lands in state, open it in the viewer. useEffect(() => { if (!pendingViewFileId) return; @@ -438,17 +553,8 @@ const FileSidebar = forwardRef( } }, [pendingViewFileId, state.files.ids, setActiveFileId, navActions]); - // Memoized so an unrelated re-render (e.g. a policy-run store tick) keeps a - // stable array identity — avoids re-running the grouping memo + backfill effect. - const filteredFileStubs = useMemo(() => { - const q = searchQuery.trim().toLowerCase(); - return q - ? allFileStubs.filter((stub) => stub.name.toLowerCase().includes(q)) - : allFileStubs; - }, [allFileStubs, searchQuery]); - // SaaS groups by classification label; core returns null → one flat, recency-sorted list. - const fileGroups = useFileSidebarGroups(filteredFileStubs); + const fileGroups = useFileSidebarGroups(allFileStubs); // Workbench membership as a Set for O(1) per-row lookups (see renderFileRow). const workbenchIds = useMemo( () => new Set(state.files.ids.map((id) => id as string)), @@ -458,12 +564,12 @@ const FileSidebar = forwardRef( // must key by their unique leaf id rather than the shared lineage (see renderFileRow). const lineageCounts = useMemo(() => { const counts = new Map(); - for (const s of filteredFileStubs) { + for (const s of allFileStubs) { const k = (s.originalFileId ?? s.id) as string; counts.set(k, (counts.get(k) ?? 0) + 1); } return counts; - }, [filteredFileStubs]); + }, [allFileStubs]); // Per-group expand/collapse, falling back to each group's default until toggled. const [groupOpen, setGroupOpen] = useState>({}); const setGroupOpenState = useCallback( @@ -472,29 +578,6 @@ const FileSidebar = forwardRef( [], ); - // Handle search activation - const handleSearchClick = useCallback(() => { - if (onSearchClick) { - onSearchClick(); - return; - } - if (collapsed && onToggleCollapse) { - onToggleCollapse(); - } - setSearchActive(true); - }, [collapsed, onToggleCollapse, onSearchClick]); - - const handleSearchClose = useCallback(() => { - setSearchActive(false); - setSearchQuery(""); - }, []); - - useEffect(() => { - if (searchActive && searchInputRef.current) { - searchInputRef.current.focus(); - } - }, [searchActive]); - // Handle Google Drive const handleGoogleDriveClick = useCallback(async () => { if (!isGoogleDriveEnabled) return; @@ -523,6 +606,22 @@ const FileSidebar = forwardRef( const stub = allFileStubs.find((s) => s.id === fileId); if (!stub) return; + // Its bytes are gone; opening it can only fail. Say so instead of a + // click that goes nowhere. + if (stub.dataUnavailable || lostFileIds.has(fileId as string)) { + alert({ + alertType: "warning", + title: t("fileSidebar.dataLostTitle", "File data is unavailable"), + body: t( + "fileSidebar.dataLostBody", + "This browser lost this file's contents. Upload it again to keep working with it.", + ), + expandable: false, + durationMs: 6000, + }); + return; + } + // In the Watched Folders view a click sends the file into the open folder // (mirrors how a click toggles a file into the active workbench elsewhere). // On the folder list (no folder open) it's a no-op so browsing isn't disrupted. @@ -577,6 +676,8 @@ const FileSidebar = forwardRef( }, [ allFileStubs, + lostFileIds, + t, state.files.ids, state.ui.selectedFileIds, fileActions, @@ -764,6 +865,8 @@ const FileSidebar = forwardRef( ? state.files.byId[workbenchFileId]?.thumbnailUrl : undefined) || stub.thumbnailUrl; const fileOrigin = getFileOrigin(stub); + const dataUnavailable = + stub.dataUnavailable === true || lostFileIds.has(stub.id as string); // Key by lineage (originalFileId) so a version swap updates the row in place instead of // remounting. But a 1-input→many-output op (split) yields sibling leaves that share one // originalFileId; those would collide on the key, so fall back to the unique leaf id when a @@ -786,12 +889,19 @@ const FileSidebar = forwardRef( thumbnailUrl={thumbnailUrl} onClick={handleFileClick} onEyeClick={handleEyeClick} + dataUnavailable={dataUnavailable} draggable={isWatchedFoldersActive} onDragStart={handleWatchedFolderDragStart} folders={memberFolders} onFolderClick={openWatchedFolder} - policies={policyFileBadges.get(stub.id as string) ?? []} + policies={policyFileBadges.get(stub.id as string) ?? NO_POLICIES} onDelete={isWatchedFoldersActive ? undefined : handleSidebarDelete} + onDownload={handleDownload} + onRename={isWatchedFoldersActive ? undefined : handleRename} + onDuplicate={isWatchedFoldersActive ? undefined : handleDuplicate} + onOpenInNewWindow={ + canOpenInNewWindow(stub) ? handleOpenInNewWindow : undefined + } onSaveToCloud={isWatchedFoldersActive ? undefined : handleSaveToCloud} canSaveToCloud={storageEnabled && fileOrigin !== "shared-with-me"} isUploadedToCloud={fileOrigin === "cloud"} @@ -833,110 +943,29 @@ const FileSidebar = forwardRef( )}
- {/* Header: hamburger + branding */} - -
onToggleCollapse?.()} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onToggleCollapse?.(); +
+ + {onToggleCollapse && ( + onToggleCollapse()} + aria-label={ + toggleAriaLabel ?? + (collapsed + ? t("fileSidebar.expand", "Expand sidebar") + : t("fileSidebar.collapse", "Collapse sidebar")) } - }} - aria-label={ - toggleAriaLabel ?? - (collapsed - ? t("fileSidebar.expand", "Expand sidebar") - : t("fileSidebar.collapse", "Collapse sidebar")) - } - > - {/* Wrapper carries sizing; data-toggle-flip-rtl flips icon in RTL. */} - - {toggleIcon ?? } - - {!collapsed && ( - - )} - {!collapsed && ( - // The header row itself toggles collapse; stop the switcher's - // clicks and key presses from reaching it. - e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - - - )} -
- + {toggleIcon ?? } + + )} +
- {/* Search row */} - -
e.key === "Enter" && handleSearchClick() - : undefined - } - > - {searchActive && !collapsed ? ( - { - e.stopPropagation(); - handleSearchClose(); - }} - /> - ) : ( - - )} - {!collapsed && - (searchActive ? ( - setSearchQuery(e.target.value)} - placeholder={t( - "fileSidebar.searchPlaceholder", - "Search files...", - )} - onClick={(e) => e.stopPropagation()} - /> - ) : ( - - {t("fileSidebar.search", "Search")} - - ))} -
-
- - {/* Scrollable content */} -
+ {/* Box 1 — top controls (open / my files / cloud). No title. File + search lives in the global super search (top bar), not here. */} + {/* Hidden native file input - kept outside the !collapsed gate so the "Open from computer" row below (always rendered) can fire it in either sidebar state without a silent no-op. */} @@ -1157,132 +1186,140 @@ const FileSidebar = forwardRef( )}
)} + - {/* Files section - always visible when expanded */} - {!collapsed && ( -
-
- - {t("fileSidebar.files", "Files")} - - - navigate("/files")} - title={t( - "fileSidebar.openFileManager", - "Browse all files & folders", - )} - aria-label={t( - "fileSidebar.openFileManager", - "Browse all files & folders", - )} - data-testid="open-files-page" - > - - - nativeFileInputRef.current?.click()} - title={t("fileSidebar.addFiles", "Add files")} - aria-label={t("fileSidebar.addFiles", "Add files")} - > - - -
- - - - {!stubsLoaded ? ( -
- + {/* Box 2 — the file tree (this box scrolls). */} + +
+ {/* Files section - always visible when expanded */} + {!collapsed && ( +
+
+ + {t("fileSidebar.library", "PDF Library")} + + + navigate("/files")} + title={t( + "fileSidebar.openFileManager", + "Browse all files & folders", + )} + aria-label={t( + "fileSidebar.openFileManager", + "Browse all files & folders", + )} + data-testid="open-files-page" + > + + + nativeFileInputRef.current?.click()} + title={t("fileSidebar.addFiles", "Add files")} + aria-label={t("fileSidebar.addFiles", "Add files")} + > + +
- ) : filteredFileStubs.length > 0 ? ( -
- {fileGroups ? ( - <> - {fileGroups.map((group) => { - const isOpen = - groupOpen[group.id] ?? group.defaultExpanded; - return ( -
- -
- {isOpen && group.stubs.map(renderFileRow)} + +
+ {isOpen && group.stubs.map(renderFileRow)} +
-
- ); - })} - - - ) : ( - filteredFileStubs.map(renderFileRow) - )} -
- ) : ( - !searchActive && ( + ); + })} + + + ) : ( + allFileStubs.map(renderFileRow) + )} +
+ ) : (

{t("fileSidebar.noFiles", "No files yet")} @@ -1291,11 +1328,11 @@ const FileSidebar = forwardRef( {t("fileSidebar.dropHint", "Open files to get started")}

- ) - )} -
- )} -
+ )} +
+ )} +
+ {/* Kebab "Save to cloud" upload modal (one file at a time). */} @@ -1314,6 +1351,14 @@ const FileSidebar = forwardRef( onChanged={refreshStubs} /> + {/* Kebab "Rename" dialog. */} + setRenameTarget(null)} + onSubmit={handleConfirmRename} + /> + {/* Cloud-aware delete choice (only opened for cloud-uploaded files). */} ( {/* Getting-started checklist, floating above the footer (SaaS only). */} - {/* Bottom bar: user name + settings */} - -
e.key === "Enter" && onOpenSettings() - : undefined - } - data-testid={onOpenSettings ? "config-button" : undefined} - data-tour={onOpenSettings ? "config-button" : undefined} - aria-label={ - onOpenSettings - ? t("fileSidebar.openSettings", "Open settings") - : displayName - } - style={onOpenSettings ? { cursor: "pointer" } : undefined} - > -
- {showProfilePicture ? ( - setPictureFailed(true)} - /> - ) : ( - displayName.charAt(0).toUpperCase() - )} -
- {!collapsed && ( - - {displayName} - - )} - {onOpenSettings && !collapsed && ( -
- -
- )} -
-
+ {/* Box 3 — the shared footer: credits, app switch, account row. */} + ); }, diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css index 4d742f7be6..bfabe83544 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.css +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.css @@ -56,12 +56,6 @@ pointer-events: auto; } -/* Always reserve space for action buttons so file name truncation dots (...) - never bleed under action buttons or overlap their outline/focus box. */ -.file-sidebar-file-info { - padding-right: 3.75rem; -} - .file-sidebar-file-item:hover:not(.selected) { background-color: color-mix(in srgb, var(--c-primary) 6%, transparent); } @@ -173,6 +167,9 @@ .file-sidebar-file-info { flex: 1; min-width: 0; + /* Always reserve space for action buttons so file name truncation dots (...) + never bleed under action buttons or overlap their outline/focus box. */ + padding-right: 3.75rem; } .file-sidebar-file-name { @@ -186,7 +183,7 @@ } .file-sidebar-file-item.selected .file-sidebar-file-name { - color: var(--c-accent-fg); + color: var(--c-accent-text); } .file-sidebar-file-meta-row { @@ -211,7 +208,7 @@ align-items: center; justify-content: center; flex-shrink: 0; - color: var(--c-primary); + color: var(--c-accent-text); } /* ---- Folder membership tags ---- */ @@ -305,7 +302,7 @@ } .file-sidebar-file-item.viewed .file-sidebar-file-name { - color: var(--c-success); + color: var(--color-green-dark); } .file-sidebar-file-item.viewed .file-sidebar-file-check { @@ -324,7 +321,7 @@ /* Always show eye for the currently viewed file */ .file-sidebar-file-item.viewed .file-sidebar-eye-btn { opacity: 1; - color: var(--c-success); + color: var(--color-green-dark); } .file-sidebar-eye-btn:hover { @@ -376,6 +373,28 @@ color: var(--c-text); } +/* Menu header: the full file name the row had to truncate, plus its size. */ +.file-sidebar-kebab-header { + display: flex; + flex-direction: column; + gap: 2px; + padding-bottom: 6px; +} + +.file-sidebar-kebab-header-name { + color: var(--c-text); + font-size: 12px; + font-weight: 600; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.file-sidebar-kebab-header-meta { + color: var(--c-text-subtle); + font-size: 11px; + text-transform: none; +} + /* ---- Date group headers ---- */ .file-sidebar-date-group-header { font-size: 11px; @@ -450,3 +469,13 @@ transform: translateY(-50%) scale(1); } } + +/* The stored bytes are gone - the row says so instead of pretending to open. */ +.file-sidebar-datalost-badge { + display: inline-flex; + align-items: center; + gap: 0.15rem; + color: var(--c-danger); + font-size: 0.7rem; + white-space: nowrap; +} diff --git a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx index 8647e18a68..a6d0e19514 100644 --- a/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx +++ b/frontend/editor/src/core/components/shared/FileSidebarFileItem.tsx @@ -9,8 +9,12 @@ import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import CloudUploadOutlinedIcon from "@mui/icons-material/CloudUploadOutlined"; import CloudDoneIcon from "@mui/icons-material/CloudDone"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlineOutlined"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; import HistoryIcon from "@mui/icons-material/History"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"; +import ContentCopyOutlinedIcon from "@mui/icons-material/ContentCopyOutlined"; import type { FileId } from "@app/types/file"; import { FileDocIcon } from "@app/components/shared/FileDocIcon"; import { @@ -19,7 +23,9 @@ import { } from "@app/components/shared/PolicyBadges"; import { getFileDocVariant } from "@app/components/shared/filePreview/getFileTypeIcon"; import { useLazyThumbnail } from "@app/hooks/useLazyThumbnail"; -import { IMAGE_EXTENSIONS } from "@app/utils/fileUtils"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { formatFileSize, IMAGE_EXTENSIONS } from "@app/utils/fileUtils"; import "@app/components/shared/FileSidebarFileItem.css"; export function getFileExtension(name: string): string { @@ -34,14 +40,6 @@ export type DateGroup = | "thisMonth" | "older"; -export const DATE_GROUP_ORDER: DateGroup[] = [ - "today", - "yesterday", - "thisWeek", - "thisMonth", - "older", -]; - export function getDateGroup(lastModified: number | undefined): DateGroup { if (!lastModified) return "older"; const now = new Date(); @@ -161,6 +159,14 @@ export interface FileItemProps { primaryLabel?: string; /** Delete (local only) from the kebab menu. Omit to hide the menu's delete. */ onDelete?: (fileId: FileId) => void; + /** Download a copy (desktop: save a copy) from the kebab menu. */ + onDownload?: (fileId: FileId) => void; + /** Rename the file from the kebab menu. */ + onRename?: (fileId: FileId) => void; + /** Save a second copy of the file into the library. */ + onDuplicate?: (fileId: FileId) => void; + /** Desktop only: open the file in its own window. Omit where unsupported. */ + onOpenInNewWindow?: (fileId: FileId) => void; /** Save to cloud from the kebab menu. */ onSaveToCloud?: (fileId: FileId) => void; /** Whether the upload-to-server menu item is offered (storage on, signed in). */ @@ -171,11 +177,56 @@ export interface FileItemProps { onVersionHistory?: (fileId: FileId) => void; /** Whether this file has more than one version (drives the menu item). */ hasVersionHistory?: boolean; + /** The stored bytes are gone (WebKit lost the blob's backing store). The row + * says so instead of pretending the file can open. */ + dataUnavailable?: boolean; } const MAX_VISIBLE_FOLDER_TAGS = 2; -export function FileItem({ +/** One kebab row. `disabledReason`, when set, greys the row out and says why. */ +function FileMenuItem({ + disabledReason, + icon, + color, + onClick, + children, +}: { + disabledReason?: React.ReactNode; + icon: React.ReactNode; + color?: string; + onClick: (e: React.MouseEvent) => void; + children: React.ReactNode; +}) { + return ( + + {/* Disabled items swallow pointer events, so the tooltip needs a live wrapper. */} +
+ { + e.stopPropagation(); + onClick(e); + }} + > + {children} + +
+
+ ); +} + +// Memoized: sidebar rows bail out unless THEIR props change, so one file's +// update (e.g. a new version landing) re-renders one row, not the whole list. +export const FileItem = React.memo(function FileItem({ fileId, name, size, @@ -183,6 +234,7 @@ export function FileItem({ isSelected, isActive, isViewedInViewer, + dataUnavailable, thumbnailUrl, onClick, onEyeClick, @@ -193,6 +245,10 @@ export function FileItem({ policies = [], primaryLabel, onDelete, + onDownload, + onRename, + onDuplicate, + onOpenInNewWindow, onSaveToCloud, canSaveToCloud = false, isUploadedToCloud = false, @@ -200,9 +256,14 @@ export function FileItem({ hasVersionHistory = false, }: FileItemProps) { const { t } = useTranslation(); + const terminology = useFileActionTerminology(); + const DownloadIcon = useFileActionIcons().download; const ext = getFileExtension(name); const dateLabel = lastModified ? formatFileDate(lastModified) : ""; const typeLabel = ext ? ext.toUpperCase() : "File"; + const metaLine = [typeLabel, size ? formatFileSize(size) : null, dateLabel] + .filter(Boolean) + .join(" · "); const policyEnforcing = policies.some((p) => p.enforcing); const enforcingTooltip = (action: string): React.ReactNode => ( @@ -221,6 +282,25 @@ export function FileItem({
); + // Why an action can't run right now: a policy is rewriting the file, or its + // bytes are gone. `needsBytes` actions are the ones that read the file. + const blockedReason = ( + action: string, + needsBytes = true, + ): React.ReactNode | null => { + if (policyEnforcing) return enforcingTooltip(action); + if (needsBytes && dataUnavailable) + return t( + "fileSidebar.fileItem.dataLostTooltip", + "This browser lost this file's contents. Upload it again to keep working with it.", + ); + return null; + }; + + const viewerLabel = isViewedInViewer + ? t("fileSidebar.fileItem.closeViewer", "Close viewer") + : t("fileSidebar.fileItem.openInViewer", "Open in viewer"); + const visibleFolders = folders.slice(0, MAX_VISIBLE_FOLDER_TAGS); const overflowFolders = folders.slice(MAX_VISIBLE_FOLDER_TAGS); @@ -234,6 +314,7 @@ export function FileItem({ const itemRef = useRef(null); const [hoverRect, setHoverRect] = useState(null); + const [menuOpened, setMenuOpened] = useState(false); const handleMouseEnter = useCallback(() => { setHoverRect(itemRef.current?.getBoundingClientRect() ?? null); @@ -241,9 +322,10 @@ export function FileItem({ const handleMouseLeave = useCallback(() => setHoverRect(null), []); - // Reactive: tooltip appears as soon as both hover rect and thumbnail are ready + // Reactive: tooltip appears as soon as both hover rect and thumbnail are ready. + // The kebab suppresses it - two cards floating off one row read as a glitch. const thumbPos = - hoverRect && resolvedThumbnail + hoverRect && resolvedThumbnail && !menuOpened ? { top: hoverRect.top + hoverRect.height / 2, left: hoverRect.right + 10, @@ -300,6 +382,21 @@ export function FileItem({ )} + {dataUnavailable && ( + + + + {t("fileSidebar.fileItem.dataLost", "Data lost")} + + + )} {isUploadedToCloud && ( - {(onDelete || - (canSaveToCloud && onSaveToCloud) || - (hasVersionHistory && onVersionHistory)) && ( - - - e.stopPropagation()} - tabIndex={-1} - aria-label={t( - "fileSidebar.fileItem.moreActions", - "More actions", - )} - > - - - - e.stopPropagation()}> - {hasVersionHistory && onVersionHistory && ( - } - onClick={(e) => { - e.stopPropagation(); - onVersionHistory(fileId); - }} - > - {t( - "fileSidebar.fileItem.versionHistory", - "Version history", - )} - + + + e.stopPropagation()} + tabIndex={-1} + aria-label={t( + "fileSidebar.fileItem.moreActions", + "More actions", )} - {canSaveToCloud && - onSaveToCloud && - (() => { - const uploadLabel = isUploadedToCloud - ? t( - "fileSidebar.fileItem.updateOnServer", - "Update on server", - ) - : t( - "fileSidebar.fileItem.uploadToServer", - "Upload to server", - ); - return ( - -
- - } - onClick={(e) => { - e.stopPropagation(); - onSaveToCloud(fileId); - }} - > - {uploadLabel} - -
-
- ); - })()} - {onDelete && - (() => { - const deleteLabel = t( - "fileSidebar.fileItem.delete", - "Delete", - ); - return ( - -
- - } - onClick={(e) => { - e.stopPropagation(); - onDelete(fileId); - }} - > - {deleteLabel} - -
-
- ); - })()} - -
- )} + > + + + + e.stopPropagation()}> + {/* Rows truncate long names; the menu header is where the whole + name (and the size the row has no space for) is readable. */} + + {name} + + {metaLine} + + + + + ) : ( + + ) + } + onClick={(e) => onEyeClick(fileId, e)} + > + {viewerLabel} + + + {onOpenInNewWindow && ( + } + onClick={() => onOpenInNewWindow(fileId)} + > + {t("openInNewWindow", "Open in new window")} + + )} + + {(onDownload || onRename || onDuplicate) && } + + {onDownload && ( + } + onClick={() => onDownload(fileId)} + > + {terminology.download} + + )} + + {onRename && ( + } + onClick={() => onRename(fileId)} + > + {t("fileSidebar.fileItem.rename", "Rename")} + + )} + + {onDuplicate && ( + } + onClick={() => onDuplicate(fileId)} + > + {t("fileSidebar.fileItem.duplicate", "Duplicate")} + + )} + + {((canSaveToCloud && onSaveToCloud) || + (hasVersionHistory && onVersionHistory)) && } + + {canSaveToCloud && + onSaveToCloud && + (() => { + const uploadLabel = isUploadedToCloud + ? t( + "fileSidebar.fileItem.updateOnServer", + "Update on server", + ) + : t( + "fileSidebar.fileItem.uploadToServer", + "Upload to server", + ); + return ( + } + onClick={() => onSaveToCloud(fileId)} + > + {uploadLabel} + + ); + })()} + + {hasVersionHistory && onVersionHistory && ( + } + onClick={() => onVersionHistory(fileId)} + > + {t("fileSidebar.fileItem.versionHistory", "Version history")} + + )} + + {onDelete && ( + <> + + } + onClick={() => onDelete(fileId)} + > + {t("fileSidebar.fileItem.delete", "Delete")} + + + )} + +
@@ -517,4 +656,4 @@ export function FileItem({ )} ); -} +}); diff --git a/frontend/editor/src/core/components/shared/FileUploadButton.stories.tsx b/frontend/editor/src/core/components/shared/FileUploadButton.stories.tsx new file mode 100644 index 0000000000..d0411f1be9 --- /dev/null +++ b/frontend/editor/src/core/components/shared/FileUploadButton.stories.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FileUploadButton from "@app/components/shared/FileUploadButton"; + +const meta: Meta = { + title: "Shared/FileUploadButton", + component: FileUploadButton, + parameters: { layout: "padded" }, + decorators: [ + (S) => ( +
+ +
+ ), + ], +}; +export default meta; +type Story = StoryObj; + +function UploadDemo({ + initialFile, + ...rest +}: { + initialFile?: File; + disabled?: boolean; + accept?: string; + placeholder?: string; +}) { + const [file, setFile] = useState(initialFile); + return ( + setFile(next ?? undefined)} + {...rest} + /> + ); +} + +/** No file chosen yet — shows the default "Choose File" placeholder. */ +export const Default: Story = { render: () => }; + +/** A file has already been selected — the button shows its name. */ +export const WithFileSelected: Story = { + render: () => ( + + ), +}; + +/** Disabled state — should still be legible but non-interactive. */ +export const Disabled: Story = { + render: () => , +}; diff --git a/frontend/editor/src/core/components/shared/FirstLoginModal.stories.tsx b/frontend/editor/src/core/components/shared/FirstLoginModal.stories.tsx new file mode 100644 index 0000000000..30941579f4 --- /dev/null +++ b/frontend/editor/src/core/components/shared/FirstLoginModal.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FirstLoginModal from "@app/components/shared/FirstLoginModal"; + +const meta = { + title: "Shared/FirstLoginModal", + component: FirstLoginModal, + parameters: { layout: "fullscreen" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + opened: true, + username: "jane.doe", + onPasswordChanged: () => {}, + }, +}; diff --git a/frontend/editor/src/core/components/shared/FitText.stories.tsx b/frontend/editor/src/core/components/shared/FitText.stories.tsx new file mode 100644 index 0000000000..63b93d0ed7 --- /dev/null +++ b/frontend/editor/src/core/components/shared/FitText.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import FitText from "@app/components/shared/FitText"; + +const meta: Meta = { + title: "Shared/FitText", + component: FitText, + parameters: { layout: "padded" }, + decorators: [ + (S) => ( +
+ +
+ ), + ], +}; +export default meta; +type Story = StoryObj; + +/** Single-line text that shrinks its font size to fit the available width. */ +export const Default: Story = { + args: { + text: "Invoice_2026_Quarterly_Report.pdf", + }, +}; + +/** Multi-line clamp with soft-break hints inserted after '/', '-' and '_'. */ +export const MultiLine: Story = { + args: { + text: "path/to/some-very/long_document/name_that_needs_multiple_lines.pdf", + lines: 3, + }, +}; + +/** Explicit font size (rem) with a lower minimum shrink scale. */ +export const CustomFontSize: Story = { + args: { + text: "Custom Sized Label", + fontSize: 1.5, + minimumFontScale: 0.5, + }, +}; diff --git a/frontend/editor/src/core/components/shared/Footer.stories.tsx b/frontend/editor/src/core/components/shared/Footer.stories.tsx new file mode 100644 index 0000000000..af1d4a5805 --- /dev/null +++ b/frontend/editor/src/core/components/shared/Footer.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import Footer from "@app/components/shared/Footer"; + +const meta = { + title: "Shared/Footer", + component: Footer, + parameters: { layout: "padded" }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +/** Defaults: no overrides supplied, only the always-present links render. */ +export const Default: Story = { + args: {}, +}; + +/** All optional legal links populated, plus the cookie preferences button. */ +export const AllLinksAndCookieBanner: Story = { + args: { + privacyPolicy: "https://example.com/privacy", + termsAndConditions: "https://example.com/terms", + accessibilityStatement: "https://example.com/accessibility", + cookiePolicy: "https://example.com/cookies", + impressum: "https://example.com/impressum", + analyticsEnabled: true, + }, +}; diff --git a/frontend/editor/src/core/components/shared/HoverActionMenu.stories.tsx b/frontend/editor/src/core/components/shared/HoverActionMenu.stories.tsx new file mode 100644 index 0000000000..dc391d0e9a --- /dev/null +++ b/frontend/editor/src/core/components/shared/HoverActionMenu.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import HoverActionMenu, { + type HoverAction, +} from "@app/components/shared/HoverActionMenu"; +import { iconMap } from "@app/components/tools/automate/iconMap"; + +const { EditIcon, DeleteIcon, DownloadIcon } = iconMap; + +const actions: HoverAction[] = [ + { + id: "edit", + icon: , + label: "Edit", + onClick: () => {}, + }, + { + id: "download", + icon: , + label: "Download", + onClick: () => {}, + }, + { + id: "delete", + icon: , + label: "Delete", + onClick: () => {}, + color: "var(--text-error)", + }, +]; + +const meta: Meta = { + title: "Shared/HoverActionMenu", + component: HoverActionMenu, + parameters: { layout: "padded" }, + decorators: [ + (S) => ( +
+ +
+ ), + ], +}; +export default meta; +type Story = StoryObj; + +/** Visible menu with the standard edit/download/delete action set. */ +export const Default: Story = { + args: { + show: true, + actions, + }, +}; + +/** Hidden state (`show: false`) — menu stays mounted but faded/non-interactive. */ +export const Hidden: Story = { + args: { + show: false, + actions, + }, +}; + +/** One action disabled with a custom tooltip explaining why. */ +export const WithDisabledAction: Story = { + args: { + show: true, + actions: [ + actions[0], + actions[1], + { + ...actions[2], + disabled: true, + tooltip: "Deletion is restricted by policy", + }, + ], + }, +}; diff --git a/frontend/editor/src/core/components/shared/InfoBanner.tsx b/frontend/editor/src/core/components/shared/InfoBanner.tsx deleted file mode 100644 index 6572022299..0000000000 --- a/frontend/editor/src/core/components/shared/InfoBanner.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { ReactNode } from "react"; -import { Paper, Group, Text, Stack } from "@mantine/core"; -import { Button, type ButtonVariant, type ButtonAccent } from "@app/ui/Button"; -import { ActionIcon } from "@app/ui/ActionIcon"; -import { useTranslation } from "react-i18next"; -import LocalIcon from "@app/components/shared/LocalIcon"; - -type InfoBannerTone = "info" | "warning"; - -const toneStyles: Record< - InfoBannerTone, - { - background: string; - border: string; - text: string; - icon: string; - buttonColor: string; - } -> = { - info: { - background: "var(--mantine-color-blue-0)", - border: "var(--mantine-color-blue-2)", - text: "var(--mantine-color-blue-9)", - icon: "var(--mantine-color-blue-6)", - buttonColor: "blue", - }, - warning: { - background: "var(--mantine-color-orange-0)", - border: "var(--mantine-color-orange-3)", - text: "var(--mantine-color-orange-9)", - icon: "var(--mantine-color-orange-7)", - buttonColor: "orange", - }, -}; - -function toSharedButtonVariant( - variant: "light" | "filled" | "white" | "outline" | "subtle", -): ButtonVariant { - switch (variant) { - case "filled": - return "primary"; - case "outline": - return "secondary"; - case "subtle": - return "tertiary"; - case "light": - case "white": - default: - return "secondary"; - } -} - -function toSharedButtonAccent(color: string | undefined): ButtonAccent { - // Mantine colours may carry a shade suffix (e.g. "orange.7"); use the hue. - const hue = (color ?? "").split(".")[0]; - switch (hue) { - case "red": - return "danger"; - case "green": - return "success"; - case "yellow": - case "orange": - return "warning"; - case "blue": - default: - return "default"; - } -} - -interface InfoBannerProps { - /** - * Either a LocalIcon name (string) for the standard sized icon slot, or a - * pre-rendered ReactNode (e.g. a logo image) which is dropped in as-is. - */ - icon?: string | ReactNode; - title?: ReactNode; - message: ReactNode; - buttonText?: string; - buttonIcon?: string; - onButtonClick?: () => void; - onDismiss?: () => void; - dismissible?: boolean; - loading?: boolean; - show?: boolean; - tone?: InfoBannerTone; - background?: string; - borderColor?: string; - textColor?: string; - iconColor?: string; - buttonColor?: string; - buttonVariant?: "light" | "filled" | "white" | "outline" | "subtle"; - /** Override the button label colour (for dark/custom theme variants). */ - buttonTextColor?: string; - minHeight?: number | string; - closeIconColor?: string; - compact?: boolean; -} - -/** - * Generic info banner component for displaying dismissible messages at the top of the app - */ -export const InfoBanner: React.FC = ({ - icon, - title, - message, - buttonText, - buttonIcon = "check-circle-rounded", - onButtonClick, - onDismiss, - dismissible = true, - loading = false, - show = true, - tone = "info", - background, - borderColor, - textColor, - iconColor, - buttonColor, - buttonVariant = "light", - buttonTextColor, - minHeight = 56, - closeIconColor, - compact = false, -}) => { - const { t } = useTranslation(); - if (!show) { - return null; - } - - const toneStyle = toneStyles[tone] ?? toneStyles.info; - const handleDismiss = () => { - onDismiss?.(); - }; - - const iconSize = compact ? "1rem" : "1.2rem"; - const textSize = compact ? "xs" : "sm"; - - return ( - - - - {icon != null && - (typeof icon === "string" ? ( - - ) : ( -
- {icon} -
- ))} - - {title && ( - - {title} - - )} - - {message} - - -
- - {buttonText && onButtonClick && ( - - )} - {dismissible && ( - - - - )} - -
-
- ); -}; diff --git a/frontend/editor/src/core/components/shared/LandingActions.tsx b/frontend/editor/src/core/components/shared/LandingActions.tsx index ec237b0394..750848f3c7 100644 --- a/frontend/editor/src/core/components/shared/LandingActions.tsx +++ b/frontend/editor/src/core/components/shared/LandingActions.tsx @@ -32,7 +32,7 @@ export function LandingActions({ <> - - - - {hasApply && ( - - )} - {hasExport && ( - - )} - - - - {/* Mobile layout: centered stack of 4 buttons */} - - - + {hasApply && ( )} {hasExport && ( )} + + + + diff --git a/frontend/editor/src/core/components/shared/ObscuredOverlay.stories.tsx b/frontend/editor/src/core/components/shared/ObscuredOverlay.stories.tsx new file mode 100644 index 0000000000..4b8d936cc5 --- /dev/null +++ b/frontend/editor/src/core/components/shared/ObscuredOverlay.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import ObscuredOverlay from "@app/components/shared/ObscuredOverlay"; + +const meta: Meta = { + title: "Shared/ObscuredOverlay", + component: ObscuredOverlay, + parameters: { layout: "padded" }, + decorators: [ + (S) => ( +
+ +
+ ), + ], +}; +export default meta; +type Story = StoryObj; + +const Content = () => ( +
+ Underlying content that gets obscured. +
+); + +export const Unobscured: Story = { + args: { + obscured: false, + children: , + }, +}; + +export const Obscured: Story = { + args: { + obscured: true, + overlayMessage: "This feature requires an upgrade", + buttonText: "Upgrade", + onButtonClick: () => {}, + children: , + }, +}; + +export const RoundedCorners: Story = { + args: { + obscured: true, + overlayMessage: "Locked", + borderRadius: "0.5rem", + children: , + }, +}; diff --git a/frontend/editor/src/core/components/shared/ObscuredOverlay/ObscuredOverlay.module.css b/frontend/editor/src/core/components/shared/ObscuredOverlay/ObscuredOverlay.module.css index 4f390cf066..75c4c2fa41 100644 --- a/frontend/editor/src/core/components/shared/ObscuredOverlay/ObscuredOverlay.module.css +++ b/frontend/editor/src/core/components/shared/ObscuredOverlay/ObscuredOverlay.module.css @@ -12,7 +12,7 @@ padding: 16px; color: #ffffff; font-weight: 600; - background: rgba(0, 0, 0, 0.55); + background: rgba(0, 0, 0, 0.62); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); border: 1px solid rgba(255, 255, 255, 0.06); diff --git a/frontend/editor/src/core/components/shared/PageEditorFileDropdown.tsx b/frontend/editor/src/core/components/shared/PageEditorFileDropdown.tsx index b15374d101..e26bc19fda 100644 --- a/frontend/editor/src/core/components/shared/PageEditorFileDropdown.tsx +++ b/frontend/editor/src/core/components/shared/PageEditorFileDropdown.tsx @@ -175,7 +175,12 @@ export const PageEditorFileDropdown: React.FC = ({ return ( + {/* role="button" so the aria-haspopup/aria-expanded Menu.Target stamps on + this element are permitted. It stays a div because it renders inside + the workbench SegmentedControl's